chore(mcp): drop build/ + node_modules leftovers after rebase
These files (build/lib/footnote-analyze.js, build/lib/footnote-lex.js from the merged footnote work, and the y-prosemirror node_modules symlink) survived the rebase because this branch's earlier "stop committing build/ and node_modules" commit predated them. They are gitignored (packages/mcp/build/) and generated / symlinked, so untrack them to keep the branch consistent with that decision. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,101 +0,0 @@
|
|||||||
/**
|
|
||||||
* Footnote diagnostics for imported Markdown (issue #166).
|
|
||||||
*
|
|
||||||
* A PURE, fence-aware text scan (independent of the Markdown->ProseMirror
|
|
||||||
* conversion path, so it reports the same problems for `create_page`,
|
|
||||||
* `update_page` and `import_page_markdown`). It never changes the document — the
|
|
||||||
* importer still creates the page; this only surfaces footnote problems to the
|
|
||||||
* caller so an agent can fix its own markup instead of shipping broken footnotes.
|
|
||||||
*
|
|
||||||
* Detected problems:
|
|
||||||
* - danglingReferences: a `[^id]` reference with no `[^id]:` definition.
|
|
||||||
* - emptyDefinitions: a `[^id]:` whose (kept) text is empty/whitespace.
|
|
||||||
* - duplicateDefinitions: an id defined by two or more `[^id]:` lines (only the
|
|
||||||
* first is kept on import — first-wins; see extractFootnotes).
|
|
||||||
* - referencesInTables: a `[^id]` marker found in a GFM table row (heuristic:
|
|
||||||
* the line, trimmed, starts with `|`) — footnotes in table cells often do not
|
|
||||||
* render as expected.
|
|
||||||
*/
|
|
||||||
import { lexFootnoteLines, forEachFootnoteReference, } from "./footnote-lex.js";
|
|
||||||
/**
|
|
||||||
* Analyze the footnotes in a Markdown string. Pure; safe to call on any body.
|
|
||||||
*/
|
|
||||||
export function analyzeFootnotes(markdown) {
|
|
||||||
// Distinct reference ids in first-appearance order, plus the set of ids seen
|
|
||||||
// inside a table row.
|
|
||||||
const refIds = [];
|
|
||||||
const refIdSet = new Set();
|
|
||||||
const referencesInTables = new Set();
|
|
||||||
const addRef = (id, inTable) => {
|
|
||||||
if (!refIdSet.has(id)) {
|
|
||||||
refIdSet.add(id);
|
|
||||||
refIds.push(id);
|
|
||||||
}
|
|
||||||
if (inTable)
|
|
||||||
referencesInTables.add(id);
|
|
||||||
};
|
|
||||||
// Definition texts per id, in first-appearance order of the id.
|
|
||||||
const defTextsById = new Map();
|
|
||||||
// Same lexer the importer uses, so the analysis matches exactly what import
|
|
||||||
// keeps/strips (#166): fenced lines are inert, definition lines are pulled.
|
|
||||||
for (const tok of lexFootnoteLines(markdown)) {
|
|
||||||
if (tok.inFence)
|
|
||||||
continue;
|
|
||||||
if (tok.definition) {
|
|
||||||
const { id, text } = tok.definition;
|
|
||||||
const arr = defTextsById.get(id);
|
|
||||||
if (arr)
|
|
||||||
arr.push(text);
|
|
||||||
else
|
|
||||||
defTextsById.set(id, [text]);
|
|
||||||
// A definition's TEXT can itself reference another footnote (`[^a]: see
|
|
||||||
// [^b]`); count those so such a `[^b]` is not falsely reported dangling.
|
|
||||||
forEachFootnoteReference(text, (rid) => addRef(rid, false));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const inTable = tok.line.trimStart().startsWith("|");
|
|
||||||
forEachFootnoteReference(tok.line, (id) => addRef(id, inTable));
|
|
||||||
}
|
|
||||||
const danglingReferences = refIds.filter((id) => !defTextsById.has(id));
|
|
||||||
const duplicateDefinitions = [];
|
|
||||||
const emptyDefinitions = [];
|
|
||||||
for (const [id, texts] of defTextsById) {
|
|
||||||
if (texts.length >= 2)
|
|
||||||
duplicateDefinitions.push(id);
|
|
||||||
// First-wins: the kept definition is the first one; flag it if it is blank.
|
|
||||||
if ((texts[0] ?? "").trim().length === 0)
|
|
||||||
emptyDefinitions.push(id);
|
|
||||||
}
|
|
||||||
const tableRefs = [...referencesInTables];
|
|
||||||
const warnings = [];
|
|
||||||
const list = (ids) => ids.map((id) => `[^${id}]`).join(", ");
|
|
||||||
if (danglingReferences.length > 0) {
|
|
||||||
warnings.push(`Footnote reference(s) with no matching definition: ${list(danglingReferences)} (each will render as an empty footnote in the editor).`);
|
|
||||||
}
|
|
||||||
if (emptyDefinitions.length > 0) {
|
|
||||||
warnings.push(`Footnote definition(s) with empty text: ${list(emptyDefinitions)}.`);
|
|
||||||
}
|
|
||||||
if (duplicateDefinitions.length > 0) {
|
|
||||||
warnings.push(`Footnote id(s) defined more than once (only the first definition was kept): ${list(duplicateDefinitions)}.`);
|
|
||||||
}
|
|
||||||
if (tableRefs.length > 0) {
|
|
||||||
warnings.push(`Footnote marker(s) inside a table row (footnotes in table cells may not render as expected): ${list(tableRefs)}.`);
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
danglingReferences,
|
|
||||||
emptyDefinitions,
|
|
||||||
duplicateDefinitions,
|
|
||||||
referencesInTables: tableRefs,
|
|
||||||
warnings,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* The optional `footnoteWarnings` field for a page-write tool result: present
|
|
||||||
* (with the warning lines) only when `markdown` has footnote problems, omitted
|
|
||||||
* otherwise. One helper so all three call sites (create/update/import) attach the
|
|
||||||
* field identically. Spread into the result: `{ ...result, ...footnoteWarningsField(text) }`.
|
|
||||||
*/
|
|
||||||
export function footnoteWarningsField(markdown) {
|
|
||||||
const { warnings } = analyzeFootnotes(markdown);
|
|
||||||
return warnings.length > 0 ? { footnoteWarnings: warnings } : {};
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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]);
|
|
||||||
}
|
|
||||||
1
packages/mcp/node_modules/y-prosemirror
generated
vendored
1
packages/mcp/node_modules/y-prosemirror
generated
vendored
@@ -1 +0,0 @@
|
|||||||
../../../node_modules/.pnpm/y-prosemirror@1.3.7_prosemirror-model@1.25.1_prosemirror-state@1.4.3_prosemirror-view@1_0ad6648b7e1f6d6f3287a40e0e62139b/node_modules/y-prosemirror
|
|
||||||
Reference in New Issue
Block a user