Files
plainleaf/client/markdown_parser/footnote.ts
T
2026-03-13 09:24:52 +01:00

155 lines
4.7 KiB
TypeScript

import { tags as t } from "@lezer/highlight";
import type { Line, MarkdownConfig } from "@lezer/markdown";
import * as ct from "./customtags.ts";
const footnoteRefRegex = /^\[\^([^\]\s]+)\]/;
export const FootnoteRef: MarkdownConfig = {
defineNodes: [
{ name: "FootnoteRef" },
{ name: "FootnoteRefMark", style: t.processingInstruction },
{ name: "FootnoteRefLabel", style: ct.FootnoteRefTag },
],
parseInline: [
{
name: "FootnoteRef",
parse(cx, next, pos) {
if (next !== 91 /* '[' */) {
return -1;
}
const match = footnoteRefRegex.exec(cx.slice(pos, cx.end));
if (!match) {
return -1;
}
// If followed by ':', this is a definition, not a reference
const afterMatch = pos + match[0].length;
if (
afterMatch < cx.end &&
cx.slice(afterMatch, afterMatch + 1) === ":"
) {
return -1;
}
const endPos = pos + match[0].length;
return cx.addElement(
cx.elt("FootnoteRef", pos, endPos, [
cx.elt("FootnoteRefMark", pos, pos + 2), // [^
cx.elt("FootnoteRefLabel", pos + 2, endPos - 1), // label
cx.elt("FootnoteRefMark", endPos - 1, endPos), // ]
]),
);
},
after: "Emphasis",
},
],
};
const footnoteDefRegex = /^\[\^([^\]\s]+)\]:\s?/;
export const FootnoteDefinition: MarkdownConfig = {
defineNodes: [
{ name: "FootnoteDefinition", block: true },
{ name: "FootnoteDefMark", style: t.processingInstruction },
{ name: "FootnoteDefLabel", style: ct.FootnoteDefTag },
{ name: "FootnoteDefBody" },
],
parseBlock: [
{
name: "FootnoteDefinition",
parse: (cx, line: Line) => {
const match = footnoteDefRegex.exec(line.text);
if (!match) {
return false;
}
const label = match[1];
const markLen = match[0].length; // [^label]:
const startPos = cx.parsedPos;
let bodyText = line.text.slice(markLen);
let endPos = cx.parsedPos + line.text.length + 1;
// Consume continuation lines (indented by 4+ spaces or tab)
// Blank lines are allowed between continuation paragraphs
while (cx.nextLine()) {
if (/^(?: {4}|\t)/.test(line.text)) {
bodyText += `\n${line.text}`;
endPos = cx.parsedPos + line.text.length + 1;
} else if (line.text.trim() === "") {
// Blank line: peek ahead to see if next line is indented
bodyText += "\n";
endPos = cx.parsedPos + line.text.length + 1;
} else {
break;
}
}
// Trim trailing blank lines from the body
while (bodyText.endsWith("\n")) {
const trimmed = bodyText.slice(0, -1);
if (trimmed.endsWith("\n") || trimmed.length === 0) {
bodyText = trimmed;
endPos--;
} else {
break;
}
}
const labelStart = startPos + 2; // after [^
const labelEnd = labelStart + label.length;
const markEnd = startPos + markLen; // after [^label]:
// endPos - 1 to exclude trailing newline
const bodyEnd = endPos - 1;
const elts = [
cx.elt("FootnoteDefMark", startPos, startPos + 2), // [^
cx.elt("FootnoteDefLabel", labelStart, labelEnd),
cx.elt("FootnoteDefMark", labelEnd, markEnd), // ]:
];
if (bodyEnd > markEnd) {
elts.push(
cx.elt(
"FootnoteDefBody",
markEnd,
bodyEnd,
cx.parser.parseInline(bodyText, markEnd),
),
);
}
cx.addElement(cx.elt("FootnoteDefinition", startPos, bodyEnd, elts));
return true;
},
before: "LinkReference",
},
],
};
const inlineFootnoteRegex = /^\^\[([^\]]+)\]/;
export const InlineFootnote: MarkdownConfig = {
defineNodes: [
{ name: "InlineFootnote" },
{ name: "InlineFootnoteMark", style: t.processingInstruction },
{ name: "InlineFootnoteContent", style: ct.InlineFootnoteTag },
],
parseInline: [
{
name: "InlineFootnote",
parse(cx, next, pos) {
if (next !== 94 /* '^' */) {
return -1;
}
const match = inlineFootnoteRegex.exec(cx.slice(pos, cx.end));
if (!match) {
return -1;
}
const endPos = pos + match[0].length;
return cx.addElement(
cx.elt("InlineFootnote", pos, endPos, [
cx.elt("InlineFootnoteMark", pos, pos + 2), // ^[
cx.elt("InlineFootnoteContent", pos + 2, endPos - 1), // content
cx.elt("InlineFootnoteMark", endPos - 1, endPos), // ]
]),
);
},
after: "Emphasis",
},
],
};