- footnote-sync: remove the now-dead `refReids` (CollisionPlan field, local, return, the 6a consumer loop) — references are never re-id'd under reuse, so it was dead structure on the hot reconciliation path. Rewrite the stale comments (plugin header, step 0, refOccurrences field) that still described the old "duplicates re-id'd so both survive" model to the reuse model. - Shared footnote lexer: new packages/mcp/src/lib/footnote-lex.ts (lexFootnoteLines + forEachFootnoteReference). extractFootnotes (collaboration) and analyzeFootnotes now consume the SAME fence-aware lexer, so "the analyzer sees exactly what the importer keeps/strips" is structural, not comment-kept. Removed the duplicated DEF_RE/fence machine from both consumers. - Tests: new mock test for the footnoteWarnings plumbing on createPage (problems -> field present; clean -> omitted); new paste-reuse case for TWO colliding pasted definitions (reservation -> distinct ids). Updated the derive-id golden test header (no MCP copy / parity test anymore). - CHANGELOG: [Unreleased] entries for footnote reuse (Changed, supersedes 0.93.0) and footnoteWarnings (Added). editor-ext 129, MCP 301, server roundtrip 2; client+server tsc clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
56 lines
2.3 KiB
JavaScript
56 lines
2.3 KiB
JavaScript
/**
|
|
* Shared, fence-aware line lexer for footnote markdown (MCP-internal).
|
|
*
|
|
* Both the importer (`extractFootnotes` in collaboration.ts, which strips
|
|
* definition lines and rebuilds a footnotes section) and the diagnostics
|
|
* (`analyzeFootnotes` in footnote-analyze.ts) must agree EXACTLY on which lines
|
|
* are definitions and which lines are inert (inside a code fence). Sharing one
|
|
* lexer makes "the analyzer sees what the importer leaves" a structural property
|
|
* instead of two hand-kept copies that can drift (#166 review).
|
|
*
|
|
* NOTE: this is deliberately NOT shared with editor-ext's
|
|
* `extractFootnoteDefinitions` — that lives in a different package and the
|
|
* decoupling between the editor and the MCP mirror is intentional.
|
|
*/
|
|
/** A footnote DEFINITION line: `[^id]: text` (id + text captured). */
|
|
export const FOOTNOTE_DEF_RE = /^\[\^([^\]\s]+)\]:[ \t]*(.*)$/;
|
|
/** Every footnote REFERENCE `[^id]` in a line (global; id captured). */
|
|
export const FOOTNOTE_REF_RE_G = /\[\^([^\]\s]+)\]/g;
|
|
/** Opening/closing code fence marker (``` or ~~~). */
|
|
const FENCE_RE = /^(\s*)(`{3,}|~{3,})/;
|
|
/** Classify every line of `markdown`, tracking fenced-code state. Pure. */
|
|
export function lexFootnoteLines(markdown) {
|
|
const out = [];
|
|
let fence = null;
|
|
for (const line of markdown.split("\n")) {
|
|
const fenceMatch = FENCE_RE.exec(line);
|
|
if (fenceMatch) {
|
|
const marker = fenceMatch[2][0];
|
|
if (fence === null)
|
|
fence = marker; // opening fence
|
|
else if (marker === fence)
|
|
fence = null; // matching closing fence
|
|
out.push({ line, inFence: true, definition: null });
|
|
continue;
|
|
}
|
|
if (fence !== null) {
|
|
out.push({ line, inFence: true, definition: null });
|
|
continue;
|
|
}
|
|
const m = FOOTNOTE_DEF_RE.exec(line);
|
|
out.push({
|
|
line,
|
|
inFence: false,
|
|
definition: m ? { id: m[1], text: m[2] } : null,
|
|
});
|
|
}
|
|
return out;
|
|
}
|
|
/** Scan a line for every `[^id]` reference, invoking `onRef(id)` for each. */
|
|
export function forEachFootnoteReference(line, onRef) {
|
|
FOOTNOTE_REF_RE_G.lastIndex = 0;
|
|
let m;
|
|
while ((m = FOOTNOTE_REF_RE_G.exec(line)) !== null)
|
|
onRef(m[1]);
|
|
}
|