@helping-ai-workflow/md2doc 3.17.2 → 3.17.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -374,13 +374,73 @@ function pushListItemBlocks(listToken, cursor, indent, blocks, nextId, inherited
374
374
  }
375
375
  }
376
376
 
377
+ // v3.17.3 N2 (R36): LINK REFERENCE DEFINITIONS LEAVE NO TOKEN. marked's lexer
378
+ // consumes '[r]: http://u' (its trailing blank lines included: the rule ends in
379
+ // `\n+`) into `tokens.links` and pushes nothing, so the cursor never advanced
380
+ // over those lines and every later block named a line above its own. Typing X
381
+ // at the end of 'P.' in '# D\n\nA.\n\n[r]: http://u\n\nP.\n' wrote
382
+ // '# D\n\nA.\n\nP.X\n\nP.\n': the definition was replaced and the old 'P.'
383
+ // left behind.
384
+ //
385
+ // So at every top-level boundary that sits at a line start, marked's own
386
+ // definition rule is asked whether a definition begins there, and each one it
387
+ // matches is stepped over (several in a row, a title on its own line). That is
388
+ // the lexer's decision, not a lookalike: at a boundary the lexer reaches `def`
389
+ // only after space, indented code, fences, heading, hr, blockquote, list and
390
+ // html have all declined, and none of those can start on a line whose first
391
+ // character after at most three spaces is '['. Where the rule matches, the
392
+ // lexer took a definition. A boundary inside a line (after a paragraph whose
393
+ // raw ends without its newline) is never tested: the source there starts with
394
+ // '\n', which no definition matches, and testing from the line start would
395
+ // re-read the paragraph's own last line. A definition inside a list item or a
396
+ // blockquote needs nothing here: those raws are faithful slices that already
397
+ // contain its lines.
398
+ const DEF_RULES = marked.Lexer.rules.block;
399
+ function definitionRule() {
400
+ const o = marked.defaults;
401
+ return (o.pedantic ? DEF_RULES.pedantic : o.gfm ? DEF_RULES.gfm : DEF_RULES.normal).def;
402
+ }
403
+ // The dropped definitions that start at line `line`: how many lines they take
404
+ // (their trailing blank lines included) and which of those lines hold text.
405
+ function droppedDefinitions(text, lineStarts, line, rule) {
406
+ let skipped = 0;
407
+ const textLines = [];
408
+ while (line + skipped <= lineStarts.length) {
409
+ const m = rule.exec(text.slice(lineStarts[line + skipped - 1]));
410
+ if (!m) break;
411
+ const parts = m[0].split('\n');
412
+ if (parts.length === 1) break; // a definition at EOF: no block follows it
413
+ parts.slice(0, -1).forEach((l, k) => { if (l.trim() !== '') textLines.push(line + skipped + k); });
414
+ skipped += parts.length - 1;
415
+ }
416
+ return { skipped, textLines };
417
+ }
418
+
377
419
  function buildBlockMap(mdText) {
378
420
  const tokens = marked.lexer(mdText);
379
421
  const blocks = [];
380
422
  let cursor = 1; // current line number of the token's first character
381
423
  const nextId = { v: 0 };
424
+ // The lexer reads '\r\n' as '\n'; so does the definition rule. Same line count.
425
+ const text = mdText.replace(/\r\n/g, '\n');
426
+ const lineStarts = [0];
427
+ for (let i = text.indexOf('\n'); i >= 0; i = text.indexOf('\n', i + 1)) lineStarts.push(i + 1);
428
+ const defRule = definitionRule();
429
+ let atLineStart = true;
430
+ // v3.17.3 N2b (R54): the text lines of the definitions dropped since the
431
+ // last block, handed to the next block as `defLinesAbove` (distances above
432
+ // its startLine, nearest last). No block owns them, and the client's range
433
+ // helpers keep them when a gesture rewrites a range that spans them.
434
+ let pendingDefLines = [];
382
435
  for (const t of tokens) {
436
+ if (atLineStart) {
437
+ const d = droppedDefinitions(text, lineStarts, cursor, defRule);
438
+ cursor += d.skipped;
439
+ pendingDefLines = pendingDefLines.concat(d.textLines);
440
+ }
441
+ atLineStart = t.raw.charAt(t.raw.length - 1) === '\n';
383
442
  const info = topLevelLineInfo(t);
443
+ const firstNew = blocks.length;
384
444
  if (t.type === 'list') {
385
445
  pushListItemBlocks(t, cursor, 0, blocks, nextId);
386
446
  } else if (t.type !== 'space') {
@@ -396,6 +456,11 @@ function buildBlockMap(mdText) {
396
456
  // text-based count instead — see its own comment for why.
397
457
  // If raw has no trailing newline (EOF), the next token doesn't exist,
398
458
  // so the off-by-one is unobservable.
459
+ if (pendingDefLines.length && blocks.length > firstNew) {
460
+ const b = blocks[firstNew];
461
+ b.defLinesAbove = pendingDefLines.map((l) => b.startLine - l);
462
+ pendingDefLines = [];
463
+ }
399
464
  cursor += info.rawNewlines;
400
465
  }
401
466
  return { blocks, lineCount: mdText.split('\n').length };