diff --git a/reme_studio/app/files-workspace/wikilinks.ts b/reme_studio/app/files-workspace/wikilinks.ts index e0867075..d2ade3f8 100644 --- a/reme_studio/app/files-workspace/wikilinks.ts +++ b/reme_studio/app/files-workspace/wikilinks.ts @@ -114,40 +114,34 @@ export function remarkWikilinks(this: Processor) { const visit = (node: MarkdownNode) => { if ( !node.children || - ["link", "image", "code", "inlineCode"].includes(node.type) + [ + "link", + "linkReference", + "image", + "imageReference", + "code", + "inlineCode", + ].includes(node.type) ) { literalize(node); return; } - node.children = node.children.flatMap((child) => { - if ( - (child.type !== "text" && child.type !== "remeWikilink") || - !child.value - ) { + node.children = node.children.map((child) => { + if (child.type !== "remeWikilink" || !child.value) { visit(child); - return [child]; + return child; } - const parts: MarkdownNode[] = []; - let end = 0; - for (const match of child.value.matchAll( - /\[\[([^[\]|#\n]+?)(?:#[^[\]|\n]+)?(?:\|([^[\]\n]+))?\]\]/g, - )) { - const target = match[1].trim(); - if (!target) continue; - parts.push({ - type: "text", - value: child.value.slice(end, match.index), - }); - parts.push({ - type: "link", - url: `#reme-file=${encodeURIComponent(target)}`, - children: [{ type: "text", value: match[2]?.trim() || target }], - }); - end = match.index + match[0].length; + const match = WIKILINK_PATTERN.exec(child.value); + if (!match?.[1].trim()) { + literalize(child); + return child; } - if (!end) return [child]; - parts.push({ type: "text", value: child.value.slice(end) }); - return parts; + const target = match[1].trim(); + return { + type: "link", + url: `#reme-file=${encodeURIComponent(target)}`, + children: [{ type: "text", value: match[2]?.trim() || target }], + }; }); }; visit(tree); diff --git a/reme_studio/tests/wikilinks.test.mjs b/reme_studio/tests/wikilinks.test.mjs index bb3dc7ee..bb793b92 100644 --- a/reme_studio/tests/wikilinks.test.mjs +++ b/reme_studio/tests/wikilinks.test.mjs @@ -30,6 +30,16 @@ function parse(content, wikilinks = true) { return processor.runSync(processor.parse(content)); } +function render(content, wikilinks = true) { + return renderToStaticMarkup( + createElement( + ReactMarkdown, + { remarkPlugins: wikilinks ? [remarkGfm, remarkWikilinks] : [remarkGfm] }, + content, + ), + ); +} + function links(tree) { return ( tree.children?.flatMap((node) => @@ -72,17 +82,7 @@ test("code examples and existing Markdown links are not rewritten", () => { "`[[sample.md]]` and [See [[sample.md]]](https://example.com)", "![See [[sample.md]]](image.png)", ].join("\n"); - const render = (plugins) => - renderToStaticMarkup( - createElement( - ReactMarkdown, - { - remarkPlugins: plugins, - }, - content, - ), - ); - assert.equal(render([remarkGfm, remarkWikilinks]), render([remarkGfm])); + assert.equal(render(content), render(content, false)); }); test("malformed wikilinks leave normal Markdown parsing unchanged", () => { @@ -107,3 +107,33 @@ test("wikilinks work beside ordinary emphasis and consecutive links", () => { assert.equal(tree.children[0].children[0].type, "strong"); assert.equal(tree.children[0].children.at(-1).type, "emphasis"); }); + +test("reference links and images preserve literal wikilinks in their labels", () => { + for (const content of [ + "[See [[sample.md]]][ref]\n\n[ref]: https://example.com", + "[See [[sample.md]]][REF]\n\n[ref]: https://example.com", + "![See [[sample.md]]][ref]\n\n[ref]: image.png", + ]) { + assert.equal(render(content), render(content, false)); + assert.doesNotMatch(render(content), /#reme-file=/); + } +}); + +test("escaped brackets and character entities remain literal text", () => { + for (const content of [ + String.raw`\[\[example.md\]\]`, + "[[example.md]]", + "[[example.md]]", + String.raw`\[[example.md]]`, + ]) { + assert.equal(render(content), render(content, false)); + assert.equal(links(parse(content)).length, 0); + } + const mixed = String.raw`\[\[literal.md\]\] [[real.md]] [[entity.md]]`; + assert.deepEqual( + links(parse(mixed)).map((node) => node.url), + ["#reme-file=real.md"], + ); + assert.match(render(mixed), /\[\[literal\.md\]\]/); + assert.match(render(mixed), /\[\[entity\.md\]\]/); +});