@helping-ai-workflow/md2doc 3.17.5 → 3.17.6

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.
@@ -126,13 +126,19 @@
126
126
  // (verified round-trip-identical either way), so we always pad once a
127
127
  // fence is needed — simpler, and preserves the original single-backtick
128
128
  // test's exact padded form.
129
- function serializeCode(node) {
129
+ //
130
+ // `opts.codePipe` (v3.17.6, table cells only): what a '|' in the code text
131
+ // is written as. table-md.js passes a placeholder here and turns it into
132
+ // `\|` after its own whole-cell '|' pass, which must not see it. The
133
+ // fence is measured on the real text; the placeholder holds no backtick.
134
+ function serializeCode(node, opts) {
130
135
  const raw = node.textContent;
131
136
  const runs = raw.match(/`+/g);
132
137
  const longestRun = runs ? Math.max.apply(null, runs.map((r) => r.length)) : 0;
133
- if (longestRun === 0) return '`' + raw + '`';
138
+ const body = opts && opts.codePipe !== undefined ? raw.split('|').join(opts.codePipe) : raw;
139
+ if (longestRun === 0) return '`' + body + '`';
134
140
  const fence = new Array(longestRun + 2).join('`');
135
- return fence + ' ' + raw + ' ' + fence;
141
+ return fence + ' ' + body + ' ' + fence;
136
142
  }
137
143
 
138
144
  // IMPORTANT 3 (degrade-never-lose): the citation form only round-trips
@@ -148,7 +154,7 @@
148
154
  return inner.indexOf(']') === -1;
149
155
  }
150
156
 
151
- function serializeAnchor(node, unsupported) {
157
+ function serializeAnchor(node, unsupported, opts) {
152
158
  const href = node.getAttribute('href');
153
159
  // A raw HTML anchor (`<a id="ref-x"></a>`, NO href attribute) is not a
154
160
  // markdown link: emitting '[label]()' for it erased the anchor on the
@@ -167,7 +173,7 @@
167
173
  unsupported.push('A');
168
174
  return '';
169
175
  }
170
- const label = walkChildren(node.childNodes, unsupported).md;
176
+ const label = walkChildren(node.childNodes, unsupported, opts).md;
171
177
  return '[' + label + '](' + (href || '') + ')';
172
178
  }
173
179
 
@@ -385,7 +391,7 @@
385
391
  if (typeof node.getAttribute !== 'function') return false;
386
392
  return (' ' + (node.getAttribute('class') || '') + ' ').indexOf(' search-hit ') !== -1;
387
393
  }
388
- function walkChildren(nodes, unsupported) {
394
+ function walkChildren(nodes, unsupported, opts) {
389
395
  let out = '';
390
396
  let firstSegment = true;
391
397
  let lastMarkTag = null;
@@ -403,7 +409,7 @@
403
409
  if (name === 'DIV') {
404
410
  let added = '';
405
411
  if (!firstSegment) added += '<br>';
406
- const inner = walkChildren(node.childNodes, unsupported);
412
+ const inner = walkChildren(node.childNodes, unsupported, opts);
407
413
  added += inner.md;
408
414
  out = appendPiece(out, added);
409
415
  if (added.length) lastMarkTag = inner.md.length ? inner.lastMarkTag : null;
@@ -413,7 +419,7 @@
413
419
  if (name === 'MARK' && markIsSearchHit(node)) {
414
420
  // v3.14.0 T9: the reader's search paints its hits as this element and
415
421
  // it is transient, so it serializes as what it wraps.
416
- const inner = walkChildren(node.childNodes, unsupported);
422
+ const inner = walkChildren(node.childNodes, unsupported, opts);
417
423
  out = appendPiece(out, inner.md);
418
424
  if (inner.md.length) lastMarkTag = inner.lastMarkTag;
419
425
  firstSegment = false;
@@ -423,7 +429,7 @@
423
429
  if (spanHasAttributes(node)) {
424
430
  unsupported.push(name);
425
431
  } else {
426
- const inner = walkChildren(node.childNodes, unsupported);
432
+ const inner = walkChildren(node.childNodes, unsupported, opts);
427
433
  out = appendPiece(out, inner.md);
428
434
  if (inner.md.length) lastMarkTag = inner.lastMarkTag;
429
435
  }
@@ -433,7 +439,7 @@
433
439
  if (name === 'STRONG' || name === 'B' || name === 'EM' || name === 'I') {
434
440
  const tag = (name === 'STRONG' || name === 'B') ? 'STRONG' : 'EM';
435
441
  const delim = tag === 'STRONG' ? '**' : '*';
436
- const e = splitEdges(walkChildren(node.childNodes, unsupported).md);
442
+ const e = splitEdges(walkChildren(node.childNodes, unsupported, opts).md);
437
443
  if (e.core) {
438
444
  // a hoisted leading space already separates two same-tag runs
439
445
  const guard = !e.lead && lastMarkTag === tag ? '<!-- -->' : '';
@@ -452,7 +458,7 @@
452
458
  // accepted on input (some contenteditable/paste paths produce it)
453
459
  // but the toolbar/serializer always speak DEL, matching what marked
454
460
  // itself emits.
455
- const e = splitEdges(walkChildren(node.childNodes, unsupported).md);
461
+ const e = splitEdges(walkChildren(node.childNodes, unsupported, opts).md);
456
462
  out = appendPiece(out, e.core ? e.lead + '~~' + e.core + '~~' + e.trail : e.lead + e.trail);
457
463
  if (e.core || e.lead || e.trail) lastMarkTag = null;
458
464
  firstSegment = false;
@@ -469,19 +475,19 @@
469
475
  // backtick, literal `<u>` typed as plain text is already escaped by
470
476
  // the `<` -> `&lt;` rule above, so it can never collide with a real
471
477
  // toolbar-made underline.
472
- out = appendPiece(out, '<u>' + walkChildren(node.childNodes, unsupported).md + '</u>');
478
+ out = appendPiece(out, '<u>' + walkChildren(node.childNodes, unsupported, opts).md + '</u>');
473
479
  lastMarkTag = null;
474
480
  firstSegment = false;
475
481
  continue;
476
482
  }
477
483
  if (name === 'CODE') {
478
- out = appendPiece(out, serializeCode(node));
484
+ out = appendPiece(out, serializeCode(node, opts));
479
485
  lastMarkTag = null;
480
486
  firstSegment = false;
481
487
  continue;
482
488
  }
483
489
  if (name === 'A') {
484
- out = appendPiece(out, serializeAnchor(node, unsupported));
490
+ out = appendPiece(out, serializeAnchor(node, unsupported, opts));
485
491
  lastMarkTag = null;
486
492
  firstSegment = false;
487
493
  continue;
@@ -530,9 +536,11 @@
530
536
  return { md: out, lastMarkTag };
531
537
  }
532
538
 
533
- function serializeInline(rootEl) {
539
+ // `opts` is optional; its one field, `codePipe`, is described at
540
+ // serializeCode(). Every caller but table-md.js passes nothing.
541
+ function serializeInline(rootEl, opts) {
534
542
  const unsupported = [];
535
- const md = walkChildren(rootEl.childNodes, unsupported).md;
543
+ const md = walkChildren(rootEl.childNodes, unsupported, opts).md;
536
544
  return { md, unsupported };
537
545
  }
538
546
 
@@ -70,8 +70,8 @@
70
70
  // it has rows. No trailing whitespace on any emitted line (every row/
71
71
  // separator line is built as '|'-delimited fields, so it always ends in
72
72
  // '|', never in whitespace, by construction). A '|' inside a <code> span
73
- // has no faithful gate-safe emission — see cellHasCodePipe() below —
74
- // and reports 'CODE' unsupported instead of corrupting it.
73
+ // is written as '\|' instead (v3.17.6) — see codePipesOf() below; the one
74
+ // code text that has no table spelling reports 'CODE' unsupported.
75
75
  //
76
76
  // Constrained (same as inline-md.js, and for the same reason: the node
77
77
  // test drives this with the hand-rolled element stub from
@@ -123,32 +123,63 @@
123
123
  return s.replace(/\r\n|\r|\n/g, '<br>');
124
124
  }
125
125
 
126
- // Finding 4: a '|' inside a <code> span has no faithful gate-safe
127
- // emission — escapePipes() above runs on the WHOLE cell's serialized md
128
- // and can't tell a code span's content from plain text, so naively
129
- // applying it would corrupt the code span (turning `` `a|b` `` into the
130
- // broken `` `a&#124;b` ``, which never decodes back inside a code span).
131
- // `\|` (some other renderers' escape) is the gate's documented trap, so
132
- // there's no safe escape at all here — degrade-never-lose: detect a CODE
133
- // element anywhere in the cell whose textContent contains '|' and report
134
- // it unsupported instead, same signal an IMG/SPAN/etc. already uses to
135
- // degrade the whole table to raw-edit (see canWysiwygForTable() in
136
- // client.js). Recurses through the cell's element children looking for
137
- // CODE — constrained to childNodes/nodeType/nodeName like the rest of
138
- // this file (no querySelector, per the node-test stub).
139
- function cellHasCodePipe(node) {
126
+ // A '|' inside a <code> span (v3.17.6; Finding 4 of 2026-08-25 refused
127
+ // every one). escapePipes() above cannot be used on it: '&#124;' is never
128
+ // decoded inside a code span, so `` `a|b` `` would come back as `a&#124;b`.
129
+ // GFM's own spelling is '\|': the table tokenizer drops that backslash when
130
+ // it splits the row into cells, before the cell is parsed inline, so the
131
+ // code span shows a bare '|'. It is also what paperwork asks for. Its
132
+ // quality gate (G-MD-TABLE-PIPE,
133
+ // `scan_table_rows_for_unescaped_pipe_in_backtick` in scripts/gate/shared.py,
134
+ // 10.4.0) hard-FAILs an unescaped pipe inside a backtick span of a table
135
+ // row and gives '\|' as the fix. The paperwork-workspace-rtk design-docs
136
+ // are written that way, and those tables used to fall back to raw-edit.
137
+ //
138
+ // Correction, recorded because the old comment here said otherwise: it
139
+ // called '\|' "the gate's documented trap". Nothing in paperwork 10.4.0
140
+ // backs that. The only trap note about an escaped pipe there is a docstring
141
+ // in scripts/gate/hwe1.py (`_hwe1_requirement_units`): that helper splits
142
+ // hwe1 requirement rows with a plain split("|"), so it would cut such a
143
+ // cell in two, and the docstring itself says no gate reads that cell text
144
+ // today. It is a hazard for whoever extends that helper. It is not a rule
145
+ // against the form, and design-doc gates do not use that helper.
146
+ //
147
+ // One code text still has no spelling. marked's splitCells() treats a pipe
148
+ // as escaped when an ODD run of backslashes precedes it, then deletes one
149
+ // backslash. Code text with an even run before its '|' (none, two, …) is
150
+ // written with one more backslash and reads back exactly. With an odd run,
151
+ // that extra backslash makes the run even and the pipe becomes a cell
152
+ // boundary. That case reports 'CODE' unsupported, and the whole table falls
153
+ // back to raw-edit as before (canWysiwygForTable() in client.js).
154
+ //
155
+ // Returns { pipes, unwritable } over every CODE element in the cell. It
156
+ // recurses through elements that are not CODE and does not descend into
157
+ // CODE, because serializeCode() writes a CODE element's textContent whole.
158
+ // It sticks to childNodes/nodeType/nodeName like the rest of this file (no
159
+ // querySelector, per the node-test stub).
160
+ const ODD_BACKSLASH_PIPE = /(?:^|[^\\])(?:\\\\)*\\\|/;
161
+ function codePipesOf(node, acc) {
162
+ const out = acc || { pipes: 0, unwritable: false };
140
163
  const kids = elementChildren(node);
141
164
  for (let i = 0; i < kids.length; i++) {
142
165
  const k = kids[i];
143
166
  if (k.nodeName === 'CODE') {
144
- if (k.textContent.indexOf('|') !== -1) return true;
145
- } else if (cellHasCodePipe(k)) {
146
- return true;
167
+ const t = k.textContent;
168
+ out.pipes += t.split('|').length - 1;
169
+ if (ODD_BACKSLASH_PIPE.test(t)) out.unwritable = true;
170
+ } else {
171
+ codePipesOf(k, out);
147
172
  }
148
173
  }
149
- return false;
174
+ return out;
150
175
  }
151
176
 
177
+ // The placeholder serializeCode() writes for a code '|' while the cell's
178
+ // markdown goes through escapePipes(). A private-use character, so real
179
+ // text almost never holds it. If one does, the count check in
180
+ // cellContentOf() catches it and the cell degrades rather than guessing.
181
+ const CODE_PIPE_MARK = '\uE000';
182
+
152
183
  // ── v3.0.1 欄寬保留 ────────────────────────────────────────────────────
153
184
  // A cell FIELD is the text between two '|' delimiters, PADDING INCLUDED:
154
185
  // in '| a | b |' the fields are ' a ' and ' b '. A column only counts
@@ -159,8 +190,8 @@
159
190
  // width of 3 is indistinguishable from this serializer's own minimal
160
191
  // form, which is exactly 3 on every row by construction).
161
192
  //
162
- // A raw '|' never appears inside a cell in source THIS serializer produced
163
- // (it emits '&#124;'), but a hand-written table may use the '\|' escape, so
193
+ // Source THIS serializer produced holds '\|' only inside code spans (plain
194
+ // text gets '&#124;'), and a hand-written table may use '\|' anywhere, so
164
195
  // the split must not break on an escaped delimiter.
165
196
  function splitFields(line) {
166
197
  const t = line.trim();
@@ -312,10 +343,12 @@
312
343
  // `unsupported` for the same cell), so width measurement and emission
313
344
  // below both read from these same strings.
314
345
  function cellContentOf(cell, unsupported) {
315
- if (cellHasCodePipe(cell)) unsupported.push('CODE');
316
- const res = inlineMd.serializeInline(cell);
346
+ const code = codePipesOf(cell);
347
+ const res = inlineMd.serializeInline(cell, { codePipe: CODE_PIPE_MARK });
317
348
  res.unsupported.forEach((u) => unsupported.push(u));
318
- return escapeNewlines(escapePipes(res.md));
349
+ const parts = escapeNewlines(escapePipes(res.md)).split(CODE_PIPE_MARK);
350
+ if (code.unwritable || parts.length - 1 !== code.pipes) unsupported.push('CODE');
351
+ return parts.join('\\|');
319
352
  }
320
353
 
321
354
  function serializeRow(contents, widths) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@helping-ai-workflow/md2doc",
3
- "version": "3.17.5",
3
+ "version": "3.17.6",
4
4
  "description": "Markdown → HTML / PDF renderer with WaveDrom, Mermaid, Graphviz, and draw.io support",
5
5
  "keywords": [
6
6
  "markdown",