fix(studio): preserve Markdown references and escaped wikilinks

This commit is contained in:
jinli.yl 2026-09-30 20:08:32 +08:00
parent 7d49b700c0
commit da7ee9ba12
2 changed files with 62 additions and 38 deletions

View file

@ -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);

View file

@ -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\]\]/);
});