@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.
- package/lib/editor/inline-md.js +24 -16
- package/lib/editor/table-md.js +58 -25
- package/package.json +1 -1
package/lib/editor/inline-md.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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 + ' ' +
|
|
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 `<` -> `<` 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
|
-
|
|
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
|
|
package/lib/editor/table-md.js
CHANGED
|
@@ -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
|
-
//
|
|
74
|
-
//
|
|
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
|
-
//
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
//
|
|
130
|
-
//
|
|
131
|
-
//
|
|
132
|
-
//
|
|
133
|
-
//
|
|
134
|
-
//
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
//
|
|
138
|
-
//
|
|
139
|
-
|
|
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: '|' is never
|
|
128
|
+
// decoded inside a code span, so `` `a|b` `` would come back as `a|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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
|
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
|
-
//
|
|
163
|
-
//
|
|
193
|
+
// Source THIS serializer produced holds '\|' only inside code spans (plain
|
|
194
|
+
// text gets '|'), 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
|
-
|
|
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
|
-
|
|
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) {
|