tests/headings.test.ts
Rama main · réplica de lectura del repositorio original.
// ONE HEADING CONTRACT — the editor's `[[Note#` offers, the outline, the
// anchor table and the reading view's ids are the same list.
//
// shared/headings.ts is the rule; this file holds four consumers to it on one
// note built to hit every seam that used to split them: frontmatter holding a
// YAML `# comment`, an indented heading, a closed-ATX heading, an alignment
// marker, a furigana reading, inline markdown, a heading inside a fence, and a
// repeated title.
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { describe, it } from "node:test";
import { findAnchor, markdownAnchors, noteAnchors } from "../shared/anchors.ts";
import { findHeadingLine, HEADING_RE, headingTitle, isHeadingLine, scanHeadings } from "../shared/headings.ts";
import { extractHeadings as outline } from "../client/reading/toc.ts";
const NOTE = [
"---",
"# a YAML comment, not a heading",
"title: Contract",
"tags: [a]",
"---",
"# Contract",
"",
" ## Indented three",
"",
"## Closed ##",
"",
"## Centred {.center}",
"",
"## {漢字|かんじ} reading",
"",
"## **Bold** and `code`",
"",
"```md",
"## Fenced, not a heading",
"```",
"",
" ## Four spaces is code",
"",
"## Repeat",
"",
"## Repeat",
"",
"#notatag heading",
"",
].join("\n");
/** What the editor's `[[Note#` completion offers: the anchor table's heading
* ids (client/editor/autocomplete.ts). */
const editorOffers = (md: string): string[] =>
markdownAnchors(md).filter((a) => a.kind === "heading").map((a) => a.id);
const TITLES = ["Contract", "Indented three", "Closed", "Centred", "漢字 reading", "Bold and code", "Repeat", "Repeat"];
const IDS = ["contract", "indented-three", "closed", "centred", "漢字-reading", "bold-and-code", "repeat", "repeat-1"];
describe("the heading contract (shared/headings.ts)", () => {
it("the shared scan finds exactly the headings, with the reading view's ids", () => {
const all = scanHeadings(NOTE);
assert.deepEqual(all.map((h) => h.title), TITLES);
assert.deepEqual(all.map((h) => h.id), IDS);
});
it("the editor offers exactly the ids the outline and the reading view use", () => {
assert.deepEqual(editorOffers(NOTE), IDS);
assert.deepEqual(outline(NOTE).map((h) => h.text), TITLES);
assert.deepEqual(outline(NOTE).map((h) => h.slug), IDS);
});
it("the anchor table files the same titles under the same ids, on the same lines", () => {
const anchors = markdownAnchors(NOTE).filter((a) => a.kind === "heading");
assert.deepEqual(anchors.map((a) => a.title), TITLES);
assert.deepEqual(anchors.map((a) => a.id), IDS);
assert.deepEqual(anchors.map((a) => a.line), scanHeadings(NOTE).map((h) => h.line));
assert.deepEqual(anchors.map((a) => a.line), outline(NOTE).map((h) => h.line));
});
it("every offer lands: the editor and the anchor table resolve it to one line", () => {
const anchors = noteAnchors("Contract.md", NOTE);
for (const offer of new Set(editorOffers(NOTE))) {
const inEditor = findHeadingLine(NOTE, offer);
const inReader = findAnchor(anchors, offer)?.line ?? null;
assert.notEqual(inEditor, null, `editor cannot land on ${offer}`);
assert.equal(inEditor, inReader, `#${offer}`);
}
});
it("a YAML `# comment` is never a heading, in any view", () => {
const comment = "a YAML comment, not a heading";
assert.ok(!editorOffers(NOTE).some((t) => t.includes("yaml")));
assert.equal(findHeadingLine(NOTE, comment), null);
assert.equal(findAnchor(noteAnchors("Contract.md", NOTE), comment), null);
});
it("one line shape: indent up to three, a space after the hashes, seven is not a heading", () => {
assert.ok(isHeadingLine("# a"));
assert.ok(isHeadingLine(" ###### a"));
assert.ok(isHeadingLine("##\ta"));
assert.ok(!isHeadingLine(" # code"));
assert.ok(!isHeadingLine("#tag"));
assert.ok(!isHeadingLine("####### seven"));
assert.equal(headingTitle("Title {.right}"), "Title");
assert.equal(headingTitle("Closed ##"), "Closed");
});
it("the reading renderer and the outline import the shared rule rather than keeping a copy", () => {
const render = readFileSync(new URL("../client/reading/render.ts", import.meta.url), "utf8");
const toc = readFileSync(new URL("../client/reading/toc.ts", import.meta.url), "utf8");
assert.match(render, /import \{ HEADING_RE \} from "..\/..\/shared\/headings.ts"/);
assert.doesNotMatch(render, /const HEADING_RE =/);
assert.doesNotMatch(toc, /const HEADING_RE =/);
assert.equal(HEADING_RE.source, "^ {0,3}(#{1,6})[ \\t]+(.*)$");
});
});