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
222 lines
7.3 KiB
TypeScript
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);
|
|
});
|