// INLINE CITATIONS — the one syntax for citing in markdown, and the numbers a // reader sees. // // [label](cite:) // // anywhere a document's markdown is (a report's summary, a section's body, a // claim's findings). `` names a citation in the same document. A renderer // replaces the link with a numbered marker that previews the citation and // jumps to it; the citation's anchor is `#c-` (`citationAnchor`), stable // across edits because it is the id, not the number. // // NUMBERS ARE PER DOCUMENT, BY FIRST APPEARANCE: the first citation cited is // [1], the next new one [2], and a citation cited again keeps its number. The // document decides the order it is read in (lib/report/uses.ts walks a // report); `numberCitations` only counts. // // Code is not citing: a `cite:` link inside an inline code span or a fenced // block is text about the syntax, and is skipped. // // Pure, no imports: the export site's pages and the browser can use it. export const CITE_SCHEME = "cite:"; export type CiteRef = { id: string; label: string; // Where the link starts in the markdown (a UTF-16 offset). offset: number; }; // `[label](cite:id)`. The label may hold one level of balanced brackets — an // editorial insertion in a quote, `[“told [the mayor] so”](cite:id)`, which // markdown-to-jsx links too — but not a lone `]`; the id runs to the first `)` // or space (an empty id is still a ref, so validation can name it). const CITE_LINK_RE = /\[((?:[^[\]]|\[[^[\]]*\])*)\]\(\s*cite:([^)\s]*)\s*\)/g; // Fenced blocks: a line opening with ``` or ~~~ (up to three spaces in) to // the line closing it with at least as many of the same, or the end. const FENCE_OPEN_RE = /^ {0,3}(`{3,}|~{3,})/; // An inline code span: a run of backticks, to the next run of the same length. const CODE_SPAN_RE = /(? s.replace(/[^\n]/g, " "); // The markdown with every fenced block and code span blanked to spaces, so // offsets still point into the original. function blankCode(md: string): string { const lines = md.split("\n"); let fence: string | null = null; for (let i = 0; i < lines.length; i++) { if (fence) { const close = new RegExp(`^ {0,3}${fence[0] === "`" ? "`" : "~"}{${fence.length},}\\s*$`); if (close.test(lines[i])) fence = null; lines[i] = blank(lines[i]); continue; } const open = FENCE_OPEN_RE.exec(lines[i]); if (open) { fence = open[1]; lines[i] = blank(lines[i]); } } return lines.join("\n").replace(CODE_SPAN_RE, blank); } // Every `cite:` link in the markdown, in order. export function extractCiteRefs(md: string | null | undefined): CiteRef[] { if (!md) return []; const scan = blankCode(md); const out: CiteRef[] = []; for (const m of scan.matchAll(CITE_LINK_RE)) { out.push({ id: m[2], label: md.slice(m.index + 1, m.index + 1 + m[1].length), offset: m.index }); } return out; } // Numbers by first appearance: each id's number, from 1, in the order the ids // are given; a repeated id keeps its first number. export function numberCitations(ids: Iterable): Map { const out = new Map(); for (const id of ids) if (!out.has(id)) out.set(id, out.size + 1); return out; } // The href an inline citation is written with. export function citeHref(id: string): string { return `${CITE_SCHEME}${id}`; } // The anchor (fragment id, without `#`) of a citation's entry in a document. export function citationAnchor(id: string): string { return `c-${id}`; }