Files
plainleaf/client/markdown_parser/footnote.test.ts
T
Zef Hemel 9581433cd3 Fixes #212: adds footnote support
Based on https://www.markdownlang.com/extended/footnotes.html

* Both "official" [^1] and [^1]: def
* And inline ^[My foot note]

Features:
* Properly parsed in markdown parser
* Syntax highlighting
* Live Preview to ... when on-hover preview of content, on-click moves
  cursor to definition
* Reference completion
* Invalid references are marked
2026-03-13 08:28:43 +01:00

222 lines
7.3 KiB
TypeScript

import { expect, test } from "vitest";
import {
collectNodesOfType,
findNodeOfType,
renderToText,
} from "@silverbulletmd/silverbullet/lib/tree";
import { parseMarkdown } from "./parser.ts";
// Footnote ref tests
const footnoteRefSample = `Here is a footnote[^1] and another[^my-note].`;
test("Test footnote ref parser", () => {
const tree = parseMarkdown(footnoteRefSample);
const refs = collectNodesOfType(tree, "FootnoteRef");
expect(refs.length).toEqual(2);
const label1 = findNodeOfType(refs[0], "FootnoteRefLabel");
expect(label1!.children![0].text).toEqual("1");
const label2 = findNodeOfType(refs[1], "FootnoteRefLabel");
expect(label2!.children![0].text).toEqual("my-note");
// Round-trip
expect(renderToText(tree)).toEqual(footnoteRefSample);
});
test("Unclosed footnote ref does not parse", () => {
const tree = parseMarkdown("This is [^ not a footnote");
const refs = collectNodesOfType(tree, "FootnoteRef");
expect(refs.length).toEqual(0);
});
test("Footnote ref inside list item", () => {
const tree = parseMarkdown("* Item with ref[^x]");
const refs = collectNodesOfType(tree, "FootnoteRef");
expect(refs.length).toEqual(1);
expect(
findNodeOfType(refs[0], "FootnoteRefLabel")!.children![0].text,
).toEqual("x");
});
// Footnote definition tests
const footnoteDefSample = `[^1]: This is a footnote definition.`;
test("Test footnote definition parser", () => {
const tree = parseMarkdown(footnoteDefSample);
const defs = collectNodesOfType(tree, "FootnoteDefinition");
expect(defs.length).toEqual(1);
const label = findNodeOfType(defs[0], "FootnoteDefLabel");
expect(label!.children![0].text).toEqual("1");
const body = findNodeOfType(defs[0], "FootnoteDefBody");
expect(body!.children![0].text).toContain("This is a footnote definition.");
});
const multipleDefsSample = `[^1]: First footnote.
[^2]: Second footnote.`;
test("Multiple footnote definitions", () => {
const tree = parseMarkdown(multipleDefsSample);
const defs = collectNodesOfType(tree, "FootnoteDefinition");
expect(defs.length).toEqual(2);
const label1 = findNodeOfType(defs[0], "FootnoteDefLabel");
expect(label1!.children![0].text).toEqual("1");
const label2 = findNodeOfType(defs[1], "FootnoteDefLabel");
expect(label2!.children![0].text).toEqual("2");
});
const footnoteFullSample = `Text with a ref[^1] here.
[^1]: The definition.`;
test("Document with refs and definitions round-trips", () => {
const tree = parseMarkdown(footnoteFullSample);
expect(renderToText(tree)).toEqual(footnoteFullSample);
const refs = collectNodesOfType(tree, "FootnoteRef");
expect(refs.length).toEqual(1);
const defs = collectNodesOfType(tree, "FootnoteDefinition");
expect(defs.length).toEqual(1);
});
// Footnote definition with markup
test("Footnote definition with bold and italic", () => {
const tree = parseMarkdown("[^1]: This is **bold** and _italic_ text.");
const defs = collectNodesOfType(tree, "FootnoteDefinition");
expect(defs.length).toEqual(1);
const body = findNodeOfType(defs[0], "FootnoteDefBody");
expect(findNodeOfType(body!, "StrongEmphasis")).not.toBeUndefined();
expect(findNodeOfType(body!, "Emphasis")).not.toBeUndefined();
expect(renderToText(tree)).toEqual(
"[^1]: This is **bold** and _italic_ text.",
);
});
// Multi-line footnote definitions
test("Multi-line footnote definition with 4-space indent", () => {
const src = `[^1]: First line of the footnote.
Second line continues here.
Third line as well.`;
const tree = parseMarkdown(src);
const defs = collectNodesOfType(tree, "FootnoteDefinition");
expect(defs.length).toEqual(1);
const body = findNodeOfType(defs[0], "FootnoteDefBody");
const bodyText = renderToText(body!);
expect(bodyText).toContain("First line");
expect(bodyText).toContain("Second line");
expect(bodyText).toContain("Third line");
expect(renderToText(tree)).toEqual(src);
});
test("Multi-line footnote definition with tab indent", () => {
const src = "[^1]: First line.\n\tContinuation with tab.";
const tree = parseMarkdown(src);
const defs = collectNodesOfType(tree, "FootnoteDefinition");
expect(defs.length).toEqual(1);
const bodyText = renderToText(findNodeOfType(defs[0], "FootnoteDefBody")!);
expect(bodyText).toContain("Continuation with tab");
});
test("Multi-line footnote with blank line between paragraphs", () => {
const src = `[^1]: First paragraph.
Second paragraph after blank line.`;
const tree = parseMarkdown(src);
const defs = collectNodesOfType(tree, "FootnoteDefinition");
expect(defs.length).toEqual(1);
const bodyText = renderToText(findNodeOfType(defs[0], "FootnoteDefBody")!);
expect(bodyText).toContain("First paragraph");
expect(bodyText).toContain("Second paragraph");
});
test("Multi-line footnote definition with markup", () => {
const src = `[^note]: This has **bold** on the first line.
And _italic_ on the continuation.`;
const tree = parseMarkdown(src);
const defs = collectNodesOfType(tree, "FootnoteDefinition");
expect(defs.length).toEqual(1);
const body = findNodeOfType(defs[0], "FootnoteDefBody");
expect(findNodeOfType(body!, "StrongEmphasis")).not.toBeUndefined();
expect(findNodeOfType(body!, "Emphasis")).not.toBeUndefined();
});
test("Multi-line footnote stops at non-indented line", () => {
const src = `[^1]: Footnote body.
Continuation.
Regular paragraph.`;
const tree = parseMarkdown(src);
const defs = collectNodesOfType(tree, "FootnoteDefinition");
expect(defs.length).toEqual(1);
const bodyText = renderToText(findNodeOfType(defs[0], "FootnoteDefBody")!);
expect(bodyText).toContain("Continuation");
expect(bodyText).not.toContain("Regular paragraph");
});
test("2-space indent does not continue footnote", () => {
const src = `[^1]: First line.
Not a continuation.`;
const tree = parseMarkdown(src);
const defs = collectNodesOfType(tree, "FootnoteDefinition");
expect(defs.length).toEqual(1);
const bodyText = renderToText(findNodeOfType(defs[0], "FootnoteDefBody")!);
expect(bodyText).not.toContain("Not a continuation");
});
// Inline footnote tests
const inlineFootnoteSample = `This has an inline footnote^[some content here] in it.`;
test("Test inline footnote parser", () => {
const tree = parseMarkdown(inlineFootnoteSample);
const fns = collectNodesOfType(tree, "InlineFootnote");
expect(fns.length).toEqual(1);
const content = findNodeOfType(fns[0], "InlineFootnoteContent");
expect(content!.children![0].text).toEqual("some content here");
// Round-trip
expect(renderToText(tree)).toEqual(inlineFootnoteSample);
});
test("Multiple inline footnotes", () => {
const tree = parseMarkdown("A^[first] and B^[second].");
const fns = collectNodesOfType(tree, "InlineFootnote");
expect(fns.length).toEqual(2);
expect(
findNodeOfType(fns[0], "InlineFootnoteContent")!.children![0].text,
).toEqual("first");
expect(
findNodeOfType(fns[1], "InlineFootnoteContent")!.children![0].text,
).toEqual("second");
});
test("Unclosed inline footnote does not parse", () => {
const tree = parseMarkdown("This is ^[not closed");
const fns = collectNodesOfType(tree, "InlineFootnote");
expect(fns.length).toEqual(0);
});
test("Empty inline footnote does not parse", () => {
const tree = parseMarkdown("This is ^[] empty");
const fns = collectNodesOfType(tree, "InlineFootnote");
expect(fns.length).toEqual(0);
});