feat(voyage): implement block-boundary-fallback for code-fence/table/list anchors
Step 17 of v4.3 playground plan. Pure function relocateAnchorsToBlockBoundaries (text, anchors) detects atomic markdown blocks (fenced code, tables, deeply nested lists) and relocates anchor-comment insertion to the line BEFORE block opening rather than inside the block. Pure markdown-text -> markdown-text transform (no DOM, no markdown-it dependency). Companion test tests/integration/annotation-block-boundary.test.mjs extracts the function via balanced-brace scan and exercises it through Function() — 7 unit tests covering empty anchors, outside-block stays, fenced-code relocation, table relocation, deeply-nested list relocation, mixed inside/outside, and shape contract. Trace: SC6, research/04 Dim 3 (Notion block-level fallback), plan-critic major #6 (DOM-vs-no-DOM contradiction resolved via pure-function design).
This commit is contained in:
parent
3973be2a90
commit
75130fe979
3 changed files with 269 additions and 0 deletions
|
|
@ -60,6 +60,101 @@
|
|||
if (intent !== null && VOYAGE_ANCHOR_INTENTS.indexOf(intent) === -1) return null;
|
||||
return { id: attrs.id, target: attrs.target, line: lineNum, snippet: snippet, intent: intent };
|
||||
}
|
||||
|
||||
// Block-boundary fallback (Step 17). Pure markdown-text -> markdown-text transform.
|
||||
// For each anchor whose line falls inside an atomic block (fenced code-block,
|
||||
// table-row, or deeply-nested list), inject the anchor-comment at the line
|
||||
// BEFORE block-opening rather than inside. Anchors outside atomic blocks
|
||||
// inject at their original line. Mirrors addAnchors semantics from
|
||||
// lib/parsers/anchor-parser.mjs but with block-boundary awareness.
|
||||
function relocateAnchorsToBlockBoundaries(text, anchors) {
|
||||
if (typeof text !== 'string') return text;
|
||||
if (!Array.isArray(anchors) || anchors.length === 0) return text;
|
||||
|
||||
var lines = text.split(/\r?\n/);
|
||||
var FENCED_RE = /^\s*```/;
|
||||
var TABLE_ROW_RE = /^\s*\|.*\|\s*$/;
|
||||
var TABLE_SEP_RE = /^\s*\|[\s\-:|]+\|\s*$/;
|
||||
var LIST_RE = /^(\s*)(?:[-*+]|\d+[.)])\s+/;
|
||||
|
||||
var atomicRanges = [];
|
||||
var inFence = false;
|
||||
var fenceStart = -1;
|
||||
var inTable = false;
|
||||
var tableStart = -1;
|
||||
|
||||
for (var i = 0; i < lines.length; i++) {
|
||||
var ln = lines[i];
|
||||
if (FENCED_RE.test(ln)) {
|
||||
if (!inFence) { inFence = true; fenceStart = i + 1; }
|
||||
else { atomicRanges.push({ start: fenceStart, end: i + 1 }); inFence = false; fenceStart = -1; }
|
||||
continue;
|
||||
}
|
||||
if (inFence) continue;
|
||||
|
||||
if (!inTable) {
|
||||
var nextLine = i + 1 < lines.length ? lines[i + 1] : '';
|
||||
if (TABLE_ROW_RE.test(ln) && TABLE_SEP_RE.test(nextLine)) {
|
||||
inTable = true;
|
||||
tableStart = i + 1;
|
||||
}
|
||||
} else if (!TABLE_ROW_RE.test(ln) || ln.trim() === '') {
|
||||
atomicRanges.push({ start: tableStart, end: i });
|
||||
inTable = false;
|
||||
tableStart = -1;
|
||||
}
|
||||
}
|
||||
if (inTable) atomicRanges.push({ start: tableStart, end: lines.length });
|
||||
if (inFence && fenceStart > 0) atomicRanges.push({ start: fenceStart, end: lines.length });
|
||||
|
||||
// Deeply-nested list-items (indent >= 4 spaces = depth >= 2 in CommonMark)
|
||||
for (var j = 0; j < lines.length; j++) {
|
||||
var lm = lines[j].match(LIST_RE);
|
||||
if (lm && lm[1].length >= 4) {
|
||||
var nestStart = j + 1;
|
||||
var k = j;
|
||||
while (k + 1 < lines.length) {
|
||||
var nm = lines[k + 1].match(LIST_RE);
|
||||
if (nm && nm[1].length >= 2) k++;
|
||||
else break;
|
||||
}
|
||||
atomicRanges.push({ start: nestStart, end: k + 1 });
|
||||
j = k;
|
||||
}
|
||||
}
|
||||
|
||||
function insertionLine(line) {
|
||||
var n = Number(line);
|
||||
if (!Number.isInteger(n) || n < 1) return n;
|
||||
for (var r = 0; r < atomicRanges.length; r++) {
|
||||
var range = atomicRanges[r];
|
||||
if (n >= range.start && n <= range.end) {
|
||||
return Math.max(1, range.start - 1);
|
||||
}
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
var adjusted = anchors.map(function (a) {
|
||||
var newLine = insertionLine(a.line);
|
||||
return Object.assign({}, a, { line: newLine });
|
||||
});
|
||||
|
||||
var sorted = adjusted.slice().sort(function (a, b) {
|
||||
return (Number(b.line) || 0) - (Number(a.line) || 0);
|
||||
});
|
||||
for (var s = 0; s < sorted.length; s++) {
|
||||
var d = sorted[s];
|
||||
var dl = Number(d.line);
|
||||
if (!dl || dl < 1 || dl > lines.length + 1) continue;
|
||||
var attrParts = ['id="' + d.id + '"', 'target="' + (d.target || 'page') + '"', 'line="' + dl + '"'];
|
||||
if (d.snippet) attrParts.push('snippet="' + String(d.snippet).slice(0, 80).replace(/"/g, '"') + '"');
|
||||
if (d.intent) attrParts.push('intent="' + d.intent + '"');
|
||||
var anchorLine = '<!-- voyage:anchor ' + attrParts.join(' ') + ' -->';
|
||||
lines.splice(dl - 1, 0, anchorLine, '');
|
||||
}
|
||||
return lines.join('\n');
|
||||
}
|
||||
</script>
|
||||
|
||||
<link rel="stylesheet" href="vendor/playground-design-system/fonts.css">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue