From 7298436ea9ff60399d5e77bf0ed3831a2a909c33 Mon Sep 17 00:00:00 2001 From: robinqu Date: Mon, 10 Aug 2026 12:12:12 +0800 Subject: [PATCH] fix: fold a linked image's trailing magic comment into the image mark MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An image followed by a sizing magic comment folds that comment into the image mark so it round-trips as source while supplying the width — but only for a standalone image. Inside a link label (`[![alt](img)](target)`) the comment was emitted as plain visible text instead, leaking raw markup into the rendering. Thread takeMagicComment through walkResolvedLink's image call site, the same folding the standalone-image path already does, and extract the per-child dispatch into walkResolvedLinkChild to stay within the statement limit. --- .../inline-text-to-mark-chunks.test.ts | 12 ++ .../extensions/inline-text-to-mark-chunks.ts | 112 +++++++++++------- 2 files changed, 82 insertions(+), 42 deletions(-) diff --git a/packages/core/src/extensions/inline-text-to-mark-chunks.test.ts b/packages/core/src/extensions/inline-text-to-mark-chunks.test.ts index 1b042b53..baa4ba33 100644 --- a/packages/core/src/extensions/inline-text-to-mark-chunks.test.ts +++ b/packages/core/src/extensions/inline-text-to-mark-chunks.test.ts @@ -671,6 +671,18 @@ describe('image', () => { " `) }) + + it('folds a trailing width comment on an image inside a link label', () => { + expect(parse('[![a](u)](/target)')).toMatchInlineSnapshot(` + " + [0, 1] mdPack(key=link,data={"form":"inline","href":"/target","title":""},revealInFocus=true) + mdLinkText(href=/target) + mdMark + [1, 30] mdPack(key=link,data={"form":"inline","href":"/target","title":""},revealInFocus=true) + mdLinkText(href=/target) + mdPack(key=image) + mdImage(src=u,alt=a,width=320) + [30, 32] mdPack(key=link,data={"form":"inline","href":"/target","title":""},revealInFocus=true) + mdMark + [32, 39] mdPack(key=link,data={"form":"inline","href":"/target","title":""},revealInFocus=true) + mdLinkUri + [39, 40] mdPack(key=link,data={"form":"inline","href":"/target","title":""},revealInFocus=true) + mdMark + " + `) + }) }) describe('wiki embed', () => { diff --git a/packages/core/src/extensions/inline-text-to-mark-chunks.ts b/packages/core/src/extensions/inline-text-to-mark-chunks.ts index c742e159..248023ea 100644 --- a/packages/core/src/extensions/inline-text-to-mark-chunks.ts +++ b/packages/core/src/extensions/inline-text-to-mark-chunks.ts @@ -564,59 +564,87 @@ function walkResolvedLink( const base = [...parentMarks, pack] let pos = node.from - for (const child of node.children) { + for (let index = 0; index < node.children.length; index++) { + const child = node.children[index] + // A previous child may have consumed this one (e.g. an image folding its + // trailing magic comment), so anything fully behind `pos` is done. + if (child.to <= pos) continue if (child.from > pos) { const childMarks = inLabel(pos) ? [...base, linkTextMark] : base emit(out, pos, child.from, childMarks) } const baseForChild = inLabel(child.from) ? [...base, linkTextMark] : base - // A wikilink in the label needs its own source/view walk, not the generic - // per-child mark mapping. - if (child.type === LEZER_NODE_IDS.Wikilink) { - walkWikilink(child, baseForChild, text, marks, out) - pos = child.to - continue - } - if (child.type === LEZER_NODE_IDS.WikiEmbed) { - walkWikiEmbed(child, baseForChild, text, marks, out, options) - pos = child.to - continue - } - if (child.type === LEZER_NODE_IDS.Image) { - walkImage(child, baseForChild, text, marks, out, options, context) - pos = child.to - continue - } - if (isReference && child.type === LEZER_NODE_IDS.LinkLabel) { - emit(out, child.from, child.to, [...baseForChild, marks.mdMark.create()]) - pos = child.to - continue - } - // An autolink inside the label is plain label text: the outer link owns - // the href, and the muted `mdLinkUri` styling belongs to the destination. - if (child.type === LEZER_NODE_IDS.URL && inLabel(child.from)) { - emit(out, child.from, child.to, baseForChild) - pos = child.to - continue - } - const maybeMarkName = MARK_NAME_BY_TYPE_ID.get(child.type) - const childMarks = maybeMarkName - ? [...baseForChild, marks[maybeMarkName].create()] - : baseForChild - if (child.children.length === 0) { - emit(out, child.from, child.to, childMarks) - } else { - // A link label cannot contain another `[label](url)` link, but custom - // atom syntax inside the label still uses the host resolvers. - walk(child.children, childMarks, child.from, child.to, text, marks, out, options, context) - } - pos = child.to + pos = walkResolvedLinkChild( + child, + node.children[index + 1], + baseForChild, + isReference, + inLabel, + text, + marks, + out, + options, + context, + ) } if (pos < node.to) { emit(out, pos, node.to, base) } } +/** + * Walk one child of a resolved link's inline content; returns the new source + * position. + */ +function walkResolvedLinkChild( + child: InlineElement, + next: InlineElement | undefined, + baseForChild: readonly Mark[], + isReference: boolean, + inLabel: (pos: number) => boolean, + text: string, + marks: TypedMarkBuilders, + out: MarkChunk[], + options: InlineMarkOptions | undefined, + context: InlineMarkContext | undefined, +): number { + // A wikilink in the label needs its own source/view walk, not the generic + // per-child mark mapping. + if (child.type === LEZER_NODE_IDS.Wikilink) { + walkWikilink(child, baseForChild, text, marks, out) + return child.to + } + if (child.type === LEZER_NODE_IDS.WikiEmbed) { + walkWikiEmbed(child, baseForChild, text, marks, out, options) + return child.to + } + if (child.type === LEZER_NODE_IDS.Image) { + const trailing = takeMagicComment(child, next, text) + walkImage(child, baseForChild, text, marks, out, options, context, trailing) + return trailing ? trailing.to : child.to + } + if (isReference && child.type === LEZER_NODE_IDS.LinkLabel) { + emit(out, child.from, child.to, [...baseForChild, marks.mdMark.create()]) + return child.to + } + // An autolink inside the label is plain label text: the outer link owns the + // href, and the muted `mdLinkUri` styling belongs to the destination. + if (child.type === LEZER_NODE_IDS.URL && inLabel(child.from)) { + emit(out, child.from, child.to, baseForChild) + return child.to + } + const maybeMarkName = MARK_NAME_BY_TYPE_ID.get(child.type) + const childMarks = maybeMarkName ? [...baseForChild, marks[maybeMarkName].create()] : baseForChild + if (child.children.length === 0) { + emit(out, child.from, child.to, childMarks) + } else { + // A link label cannot contain another `[label](url)` link, but custom + // atom syntax inside the label still uses the host resolvers. + walk(child.children, childMarks, child.from, child.to, text, marks, out, options, context) + } + return child.to +} + interface AdjacentMagicComment { magic: MagicComment to: number