tests/exportLinks.test.ts
Rama main · réplica de lectura del repositorio original.
// The export's link rewrite (shared/exportLinks.ts): wikilinks and embeds
// become relative Markdown links in the archive's COPIES, on exactly the
// terms the docs state — unresolvable targets stay as written, code is left
// alone, every other byte survives.
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import { encodeLinkPath, relativePath, rewriteWikilinks, type LinkResolver } from "../shared/exportLinks.ts";
import { wikilinkRegex } from "../server/indexer.ts";
/** A small vault: the resolver an export would build from the index. */
const VAULT: Record<string, string> = {
other: "Other.md",
"deep note": "Folder/Sub/Deep note.md",
"cover.png": "Media/cover.png",
"paper.pdf": "Media/paper.pdf",
"فكرة": "ملاحظات/فكرة.md",
self: "Folder/Self.md",
};
const resolve: LinkResolver = (target) => VAULT[target.toLowerCase()] ?? null;
describe("relativePath", () => {
it("walks up and down between folders", () => {
assert.equal(relativePath("Ideas.md", "Other.md"), "Other.md");
assert.equal(relativePath("Ideas.md", "Media/cover.png"), "Media/cover.png");
assert.equal(relativePath("Folder/Self.md", "Other.md"), "../Other.md");
assert.equal(relativePath("Folder/Self.md", "Folder/Sub/Deep note.md"), "Sub/Deep note.md");
assert.equal(relativePath("Folder/Sub/Deep note.md", "Media/cover.png"), "../../Media/cover.png");
assert.equal(relativePath("Folder/Sub/Deep note.md", "Folder/Self.md"), "../Self.md");
});
});
describe("encodeLinkPath", () => {
it("encodes only what a link destination cannot carry bare", () => {
assert.equal(encodeLinkPath("Folder/Sub/Deep note.md"), "Folder/Sub/Deep%20note.md");
assert.equal(encodeLinkPath("a (b).md"), "a%20%28b%29.md");
assert.equal(encodeLinkPath("100%.md"), "100%25.md");
// Arabic stays readable: it is the whole point of the UTF-8 flag next door.
assert.equal(encodeLinkPath("ملاحظات/فكرة.md"), "ملاحظات/فكرة.md");
});
});
describe("rewriteWikilinks", () => {
it("turns a wikilink into a relative Markdown link", () => {
assert.equal(rewriteWikilinks("See [[Other]].", "Ideas.md", resolve), "See [Other](Other.md).");
assert.equal(
rewriteWikilinks("See [[Deep note|the deep one]].", "Ideas.md", resolve),
"See [the deep one](Folder/Sub/Deep%20note.md).",
);
assert.equal(
rewriteWikilinks("Up: [[Other]]", "Folder/Sub/Deep note.md", resolve),
"Up: [Other](../../Other.md)",
);
});
it("slugs a heading anchor the way the reading view ids it", () => {
assert.equal(
rewriteWikilinks("[[Other#My Heading!]]", "Ideas.md", resolve),
"[Other#My Heading!](Other.md#my-heading)",
);
assert.equal(rewriteWikilinks("[[Other#My Heading|x]]", "Ideas.md", resolve), "[x](Other.md#my-heading)");
// A block reference has no standard target; the link goes to the note.
assert.equal(rewriteWikilinks("[[Other#^abc123]]", "Ideas.md", resolve), "[Other#^abc123](Other.md)");
// Inside the same note only the anchor is needed.
assert.equal(rewriteWikilinks("[[Self#Part two]]", "Folder/Self.md", resolve), "[Self#Part two](#part-two)");
});
it("keeps an embed an embed for a picture, and makes it a link for a note", () => {
assert.equal(
rewriteWikilinks("![[cover.png]]", "Ideas.md", resolve),
"",
);
assert.equal(
rewriteWikilinks("![[cover.png|The cover]]", "Folder/Self.md", resolve),
"",
);
assert.equal(rewriteWikilinks("![[paper.pdf]]", "Ideas.md", resolve), "");
assert.equal(rewriteWikilinks("![[Other]]", "Ideas.md", resolve), "[Other](Other.md)");
assert.equal(rewriteWikilinks("![[Other#Part]]", "Ideas.md", resolve), "[Other#Part](Other.md#part)");
});
it("leaves an unresolvable target exactly as written", () => {
const md = "A [[Nowhere]] and ![[missing.png]] and [[Other]].";
assert.equal(rewriteWikilinks(md, "Ideas.md", resolve), "A [[Nowhere]] and ![[missing.png]] and [Other](Other.md).");
assert.equal(rewriteWikilinks(md, "Ideas.md", () => null), md);
});
it("does not touch fences or inline code", () => {
const md = [
"Prose [[Other]] here.",
"```md",
"A fence with [[Other]] in it.",
"```",
"Inline `[[Other]]` stays, [[Other]] goes.",
"~~~",
"[[Other]]",
"~~~",
"````",
"```",
"[[Other]] still inside the four-tick fence",
"````",
"[[Other]]",
].join("\n");
assert.equal(
rewriteWikilinks(md, "Ideas.md", resolve),
[
"Prose [Other](Other.md) here.",
"```md",
"A fence with [[Other]] in it.",
"```",
"Inline `[[Other]]` stays, [Other](Other.md) goes.",
"~~~",
"[[Other]]",
"~~~",
"````",
"```",
"[[Other]] still inside the four-tick fence",
"````",
"[Other](Other.md)",
].join("\n"),
);
});
it("keeps CRLF endings and every other byte", () => {
const md = "---\r\ntitle: x\r\n---\r\n\r\nSee [[Other]] \r\n\r\n trailing\r\n";
assert.equal(rewriteWikilinks(md, "Ideas.md", resolve), md.replace("[[Other]]", "[Other](Other.md)"));
});
it("keeps an Arabic path readable", () => {
assert.equal(
rewriteWikilinks("انظر [[فكرة]]", "Ideas.md", resolve),
"انظر [فكرة](ملاحظات/فكرة.md)",
);
});
it("matches the indexer's wikilink grammar", () => {
// The two regexes are typed twice on purpose (shared/ must not import
// server/); this pins them to the same answers on the shapes that matter.
const cases = ["[[A]]", "[[A|b]]", "[[A#h]]", "[[A#h|b]]", "[[ A ]]", "[[A]] [[B]]", "[[]]", "[[A|b|c]]"];
for (const text of cases) {
const ours = [...text.matchAll(/(!?)\[\[([^[\]|#]+)(#[^[\]|]*)?(\|[^[\]]*)?\]\]/g)].map((m) => m[2]);
const theirs = [...text.matchAll(wikilinkRegex())].map((m) => m[1]);
assert.deepEqual(ours, theirs, text);
}
});
});