@helping-ai-workflow/md2doc 3.13.0 → 3.14.0

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.
@@ -47,6 +47,50 @@
47
47
  return out;
48
48
  }
49
49
 
50
+ // v3.13.1 T2 (F-1): text a person typed at the start of a paragraph or
51
+ // list-item line that a renderer would read as a block marker. Measured
52
+ // before this: typing `## `, `- `, `> ` or `1. ` at a paragraph start saved
53
+ // the line unescaped, and the next render silently turned the paragraph
54
+ // into a heading, list or quote.
55
+ //
56
+ // Input is escapeText() output, possibly several lines (a hard break's
57
+ // backslash-newline, or a lazy continuation). Each line, after up to three
58
+ // spaces of indent, gets the FIRST matching rule below; nothing else is
59
+ // touched. The list is the decision:
60
+ // ATX heading `#`..`######` then space/tab/end -> `\#…`
61
+ // bullet `-` or `+` then space/tab/end -> `\-…` / `\+…`
62
+ // blockquote `>` (no space needed) -> `\>…`
63
+ // ordered 1-9 digits, `.` or `)`, space/end -> `1\.` / `1\)`
64
+ // setext/rule a run of `=` or of `-`, alone -> `\===` / `\---`
65
+ // spaced rule 3+ `-` with spaces between, alone -> `\-- -`
66
+ // table row `|` -> `\|…`
67
+ // `*`, `_`, backtick, `~`, `<` and `[` never get here unescaped
68
+ // (escapeText()). A line whose first character is `\` matches nothing,
69
+ // which is what keeps already-escaped text byte-stable. `-5 dBm` and
70
+ // `1.5 V` match nothing either: the marker must be followed by a space, a
71
+ // tab or the end of the line. Indented code (4+ spaces) is out of scope.
72
+ const BLOCK_START_RULES = [
73
+ { re: /^#{1,6}(?=[ \t]|$)/, fix: (rest) => '\\' + rest },
74
+ { re: /^[-+](?=[ \t]|$)/, fix: (rest) => '\\' + rest },
75
+ { re: /^>/, fix: (rest) => '\\' + rest },
76
+ { re: /^(\d{1,9})([.)])(?=[ \t]|$)/, fix: (rest) => rest.replace(/^(\d{1,9})([.)])/, '$1\\$2') },
77
+ { re: /^(=+|-+)[ \t]*$/, fix: (rest) => '\\' + rest },
78
+ // A thematic break may have spaces between its dashes (`-- -`,
79
+ // `---- -`); the run rule above only sees an unbroken one.
80
+ { re: /^-(?:[ \t]*-){2,}[ \t]*$/, fix: (rest) => '\\' + rest },
81
+ { re: /^\|/, fix: (rest) => '\\' + rest },
82
+ ];
83
+ function escapeBlockStarts(md) {
84
+ return String(md).split('\n').map((line) => {
85
+ const ind = /^ {0,3}/.exec(line)[0];
86
+ const rest = line.slice(ind.length);
87
+ for (let i = 0; i < BLOCK_START_RULES.length; i++) {
88
+ if (BLOCK_START_RULES[i].re.test(rest)) return ind + BLOCK_START_RULES[i].fix(rest);
89
+ }
90
+ return line;
91
+ }).join('\n');
92
+ }
93
+
50
94
  // Real contenteditable output wraps plain runs in attribute-less <span>s
51
95
  // (Chrome/Firefox formatting artifacts). Those are transparent. A <span>
52
96
  // carrying any of these attributes is a real style/class/behavior carrier
@@ -223,6 +267,10 @@
223
267
  // STRONG, DEL/S, U, the citation label) only ever needs `.md`, because
224
268
  // its own outer delimiter is a fixed, known tag regardless of what's
225
269
  // nested inside.
270
+ function markIsSearchHit(node) {
271
+ if (typeof node.getAttribute !== 'function') return false;
272
+ return (' ' + (node.getAttribute('class') || '') + ' ').indexOf(' search-hit ') !== -1;
273
+ }
226
274
  function walkChildren(nodes, unsupported) {
227
275
  let out = '';
228
276
  let firstSegment = true;
@@ -248,6 +296,15 @@
248
296
  firstSegment = false;
249
297
  continue;
250
298
  }
299
+ if (name === 'MARK' && markIsSearchHit(node)) {
300
+ // v3.14.0 T9: the reader's search paints its hits as this element and
301
+ // it is transient, so it serializes as what it wraps.
302
+ const inner = walkChildren(node.childNodes, unsupported);
303
+ out += inner.md;
304
+ if (inner.md.length) lastMarkTag = inner.lastMarkTag;
305
+ firstSegment = false;
306
+ continue;
307
+ }
251
308
  if (name === 'SPAN') {
252
309
  if (spanHasAttributes(node)) {
253
310
  unsupported.push(name);
@@ -312,6 +369,14 @@
312
369
  firstSegment = false;
313
370
  continue;
314
371
  }
372
+ if (name === 'BR' && typeof node.getAttribute === 'function' &&
373
+ node.getAttribute('data-ed-tail') === '1') {
374
+ // v3.14.0 T8: the editor's tail <br> (insertBrAtCaret() in client.js)
375
+ // only gives a line break at the end of a surface a line to stand on.
376
+ // It is not content: nothing written, and like an empty text node it
377
+ // leaves lastMarkTag / firstSegment as they were.
378
+ continue;
379
+ }
315
380
  if (name === 'BR') {
316
381
  // Spec §3.12. Two different things arrive here as the same element:
317
382
  // * a <br> the EDIT-MODE renderer marked (lib/md2doc.js's
@@ -358,5 +423,5 @@
358
423
  return serializeInline(rootEl).unsupported.length === 0;
359
424
  }
360
425
 
361
- return { serializeInline, canWysiwyg, escapeText };
426
+ return { serializeInline, canWysiwyg, escapeText, escapeBlockStarts };
362
427
  });
@@ -224,7 +224,9 @@
224
224
 
225
225
  // strip leading whitespace left behind by a dropped leading element
226
226
  // (e.g. a checkbox <input>) — see header note.
227
- const itemMd = rawItemMd.replace(/^[ \t]+/, '');
227
+ // v3.13.1 T2 (F-1): text that begins with a block marker is escaped,
228
+ // or the item's own text would re-lex as a nested heading/list/quote.
229
+ const itemMd = inlineMd.escapeBlockStarts(rawItemMd.replace(/^[ \t]+/, ''));
228
230
 
229
231
  // Two-part marker: bullet (ordered ordinal or plain '- ') and an
230
232
  // optional checkbox prefix. The two parts are independent so that
@@ -617,6 +619,11 @@
617
619
  // zero-width character it never asked for, and the item would stop
618
620
  // reading as empty to every `itemMd === ''` test in this function.
619
621
  if (SETEXT_ESCAPE_RE.test(itemMd)) itemMd = '';
622
+ // v3.13.1 T2 (F-1): an item whose text begins with `## `, `- `, `> `,
623
+ // `1. ` (…) is written escaped, or the next render reads it as a
624
+ // nested block. Only re-serialized items get here; a bystander
625
+ // replayed from its source lines never does.
626
+ itemMd = inlineMd.escapeBlockStarts(itemMd);
620
627
  }
621
628
 
622
629
  // Anything in the block that is neither chrome nor the text surface is
@@ -101,7 +101,7 @@ const MARK_IDS = Object.freeze(Object.keys(MARK_TAG_BY_ID));
101
101
  // 'inert' pressing the button would do nothing at all
102
102
 
103
103
  // ctx: {blockType, indent, headingDepth, inList, listOrdered, hasSelection,
104
- // marks, dirty}
104
+ // marks, dirty, transient}
105
105
  // -> { [id]: {active: bool, mixed: bool, disabled: bool, label?: string} }
106
106
  function deriveState(ctx) {
107
107
  const c = ctx || {};
@@ -151,6 +151,16 @@ function deriveState(ctx) {
151
151
  // primary case (spec review ruling: gating it on inList made it dead for
152
152
  // that case).
153
153
 
154
+ // v3.14.0 T4 (D15): an EMPTY transient paragraph has no text to convert, so
155
+ // the conversions that wrap existing text (quote, numbered list, check) and
156
+ // the list-depth buttons stay off until it has some. ctx.transient is
157
+ // 'empty' | 'text' | null; the client derives it from the DOM.
158
+ if (c.transient === 'empty') {
159
+ for (const id of ['quote', 'ordered-list', 'check', 'indent', 'outdent']) {
160
+ state[id].disabled = true;
161
+ }
162
+ }
163
+
154
164
  // No block at all (rerenderAll's zeroed state): everything disabled except
155
165
  // the handful of buttons that don't depend on having a focused block.
156
166
  if (!hasBlock) {