@helping-ai-workflow/md2doc 2.10.1 → 2.11.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.
- package/lib/editor/blockmap.js +126 -8
- package/lib/editor/client.js +2341 -651
- package/lib/editor/convert-md.js +200 -0
- package/lib/editor/indent-clamp.js +152 -0
- package/lib/editor/inline-md.js +25 -1
- package/lib/editor/list-md.js +500 -4
- package/lib/editor/server.js +56 -3
- package/lib/md2doc.js +357 -40
- package/package.json +2 -2
package/lib/md2doc.js
CHANGED
|
@@ -175,23 +175,98 @@ try {
|
|
|
175
175
|
process.exit(1);
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
//
|
|
178
|
+
// S1: render one list token into a FLAT sequence of per-li ed-block HTML.
|
|
179
|
+
// There is NO <ul>/<ol> container and no <li> — every list item, at every
|
|
180
|
+
// depth, is a top-level `<div class="ed-block" data-block-type="li">` sibling
|
|
181
|
+
// of every other block, and nesting is carried by `data-indent` alone.
|
|
182
|
+
//
|
|
183
|
+
// `biRef` is a shared box that advances through blocks[] in the same DFS order
|
|
184
|
+
// as blockmap.js pushListItemBlocks:
|
|
181
185
|
// 1. Push the item's own block.
|
|
182
186
|
// 2. Recurse into that item's nested child lists, left-to-right.
|
|
183
187
|
// 3. Move on to the next sibling item.
|
|
184
|
-
//
|
|
185
|
-
//
|
|
188
|
+
// The ONLY change from the pre-S1 nested renderer is WHERE the child HTML
|
|
189
|
+
// lands — as a sibling in the same output array, never inside the parent's
|
|
190
|
+
// element.
|
|
191
|
+
//
|
|
192
|
+
// RULING F-L is unchanged: use marked.Parser.parseInline ONLY when the item is
|
|
193
|
+
// tight (!item.loose) AND its own non-list tokens are exactly one 'text' token.
|
|
186
194
|
// In every other case emit marked.parser(ownTokens) so that loose items keep
|
|
187
195
|
// their <p> wrapper — list-md.js detects loose items by the <p> and flags them
|
|
188
|
-
//
|
|
189
|
-
//
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
196
|
+
// unsupported per-li (spec §8). Stripping the <p> would be silent data-shape
|
|
197
|
+
// corruption on the first edit.
|
|
198
|
+
//
|
|
199
|
+
// The marker's TEXT is deliberately NOT written into the HTML: a `.ed-li-marker`
|
|
200
|
+
// span is emitted empty and CSS draws the bullet / ordinal / checkbox, so
|
|
201
|
+
// renumbering a run never has to redraw the DOM. Two hooks exist purely for
|
|
202
|
+
// that CSS (they have no consumer inside this function, and none in this
|
|
203
|
+
// task — see the S1 plan's controller note R1):
|
|
204
|
+
// * `style="--ed-indent:<indent>"` — the indent custom property.
|
|
205
|
+
// * `data-run-start="1"` on the FIRST block of every run — where an ordered
|
|
206
|
+
// list's CSS counter must reset.
|
|
207
|
+
// `data-run-start` uses spec §3.8's run rule, and it is the SAME rule
|
|
208
|
+
// lib/editor/list-md.js's serializeBlocks() applies when it restarts an
|
|
209
|
+
// ordinal: a run starts when the previous block is not an li, OR is shallower,
|
|
210
|
+
// OR is at the same depth with a different data-list-type. Deeper items never
|
|
211
|
+
// break the run they are nested under, which is why `types` (the last list
|
|
212
|
+
// type seen AT EACH DEPTH) is consulted rather than the immediately previous
|
|
213
|
+
// block — a nested item sitting between two same-depth items must not make a
|
|
214
|
+
// list-type change invisible. If this and serializeBlocks() ever disagree, the
|
|
215
|
+
// symptom is wrong ordinals or a wrong commit range.
|
|
216
|
+
//
|
|
217
|
+
// THIRD hook, and a spec gap this task had to close: `data-list-start="1"` on
|
|
218
|
+
// the first block of EVERY marked list token, at every depth. Spec §3.8's three
|
|
219
|
+
// operational rules cannot separate two ADJACENT lists of the same type —
|
|
220
|
+
// `marked.lexer('- a\n* c\n')` returns TWO list tokens (a bullet-char change
|
|
221
|
+
// starts a new list, blank line or not) whose blocks are all
|
|
222
|
+
// data-indent="0" data-list-type="ul", i.e. indistinguishable to rules (a),
|
|
223
|
+
// (b) and (c). Before flattening, the two <ul> roots carried that distinction;
|
|
224
|
+
// afterwards nothing did, and a run scan merged them — re-markering and
|
|
225
|
+
// re-flowing a list the user never touched. The renderer is the only place
|
|
226
|
+
// that still knows where marked's token boundaries were, so it stamps them,
|
|
227
|
+
// and the client's run scan treats the attribute as rule (d): a run never
|
|
228
|
+
// crosses a data-list-start AT ITS OWN DEPTH.
|
|
229
|
+
//
|
|
230
|
+
// "Every token", not "every TOP-LEVEL token": the same delimiter-change shape
|
|
231
|
+
// nests. `- a / (2sp)1. x / (2sp)1) y / - d` is an outer ul plus TWO nested ol
|
|
232
|
+
// tokens, and stamping only the outer one renumbered `1) y` to `2. y`.
|
|
233
|
+
// listRunOf() spans the outermost run PLUS its subtrees, so a nested token
|
|
234
|
+
// boundary is always inside a span serializeBlocks() is handed — the earlier
|
|
235
|
+
// claim that a cross-list span could never reach it was wrong.
|
|
236
|
+
function liRunStartsHere(runState, b) {
|
|
237
|
+
const prev = runState.prevLi;
|
|
238
|
+
const isStart = !prev || b.indent > prev.indent || runState.types[b.indent] !== b.listType;
|
|
239
|
+
for (let k = runState.types.length - 1; k > b.indent; k--) runState.types[k] = undefined;
|
|
240
|
+
runState.types[b.indent] = b.listType;
|
|
241
|
+
runState.prevLi = b;
|
|
242
|
+
return isStart;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// A new list TOKEN closes every run open at its own depth or deeper, and
|
|
246
|
+
// leaves the shallower ones alone — the depths ABOVE a nested sublist are
|
|
247
|
+
// still inside the same enclosing list, so `- a / (2sp)1. x / - d` must not
|
|
248
|
+
// treat `- d` as opening a new top-level run.
|
|
249
|
+
function resetLiRunStateAtDepth(runState, indent) {
|
|
250
|
+
runState.types.length = Math.min(runState.types.length, indent);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Rule (c): any non-li block terminates every open run, at every depth.
|
|
254
|
+
function resetLiRunState(runState) {
|
|
255
|
+
runState.prevLi = null;
|
|
256
|
+
runState.types = [];
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function renderEditModeList(listToken, blocks, biRef, out) {
|
|
260
|
+
const parts = out || [];
|
|
261
|
+
// EVERY list token begins a new list, at whatever depth it sits: close the
|
|
262
|
+
// runs open at that depth and below (never the shallower ones — see
|
|
263
|
+
// resetLiRunStateAtDepth) so this token's first block is stamped, then stamp
|
|
264
|
+
// it. The token's depth is its first item's block indent; the render walk and
|
|
265
|
+
// blockmap.js consume blocks[] in lockstep, so blocks[biRef.v] is that block.
|
|
266
|
+
let listStart = true;
|
|
267
|
+
const firstBlock = blocks[biRef.v];
|
|
268
|
+
resetLiRunStateAtDepth(biRef, firstBlock ? firstBlock.indent : 0);
|
|
269
|
+
listToken.items.forEach((item) => {
|
|
195
270
|
const b = blocks[biRef.v++];
|
|
196
271
|
const ownTokens = item.tokens.filter((tk) => tk.type !== 'list');
|
|
197
272
|
let inner;
|
|
@@ -201,22 +276,35 @@ function renderEditModeList(listToken, blocks, biRef) {
|
|
|
201
276
|
inner = marked.Parser.parseInline(ownTokens[0].tokens);
|
|
202
277
|
} else {
|
|
203
278
|
// Loose item or non-standard own-token shape: use marked.parser, which
|
|
204
|
-
// emits block-level HTML (including <p> for loose items).
|
|
279
|
+
// emits block-level HTML (including <p> for loose items). The <p>
|
|
205
280
|
// presence is load-bearing for list-md.js's loose-item detection.
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
281
|
+
// Trimmed because the surface is `white-space: pre-line` (see the CSS):
|
|
282
|
+
// marked pretty-prints a trailing "\n" after the closing tag, which is
|
|
283
|
+
// insignificant to every serializer (list-md.js drops it via isBlankText)
|
|
284
|
+
// but would render as a phantom blank line inside the item.
|
|
285
|
+
inner = marked.parser(ownTokens).trim();
|
|
286
|
+
}
|
|
287
|
+
const check = b.task
|
|
288
|
+
? `<span class="ed-li-check" data-checked="${b.checked ? 1 : 0}" role="checkbox" aria-checked="${!!b.checked}"></span>`
|
|
210
289
|
: '';
|
|
290
|
+
const runStart = liRunStartsHere(biRef, b) ? ' data-run-start="1"' : '';
|
|
291
|
+
const listStartAttr = listStart ? ' data-list-start="1"' : '';
|
|
292
|
+
listStart = false;
|
|
293
|
+
parts.push(
|
|
294
|
+
`<div class="ed-block" data-block-id="${b.id}" data-block-type="li"` +
|
|
295
|
+
` data-list-type="${b.listType}" data-task="${b.task ? 1 : 0}" data-indent="${b.indent}"` +
|
|
296
|
+
runStart + listStartAttr + ` style="--ed-indent:${b.indent}">` +
|
|
297
|
+
`<span class="ed-li-marker" aria-hidden="true"></span>` + check +
|
|
298
|
+
`<div class="ed-li-text">${inner}</div>` +
|
|
299
|
+
`</div>`
|
|
300
|
+
);
|
|
211
301
|
// Recurse into nested child lists AFTER this item's block (matches
|
|
212
|
-
// pushListItemBlocks order: own block
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
` data-list-type="${b.listType}" data-indent="${b.indent}">` +
|
|
217
|
-
check + `<div class="ed-li-text">${inner}</div>` + children + '</li>';
|
|
302
|
+
// pushListItemBlocks order: own block -> nested lists -> next sibling).
|
|
303
|
+
// The child HTML is pushed into the SAME array, as a SIBLING.
|
|
304
|
+
item.tokens.filter((tk) => tk.type === 'list')
|
|
305
|
+
.forEach((ct) => renderEditModeList(ct, blocks, biRef, parts));
|
|
218
306
|
});
|
|
219
|
-
return
|
|
307
|
+
return parts.join('\n');
|
|
220
308
|
}
|
|
221
309
|
|
|
222
310
|
async function renderMarkdown(mdText, srcPath, opts = {}) {
|
|
@@ -616,6 +704,29 @@ ${itemsHtml}
|
|
|
616
704
|
return inlineImagesInHtmlChunk(baseHtml.apply(this, arguments));
|
|
617
705
|
};
|
|
618
706
|
|
|
707
|
+
// Spec §3.12 — a markdown HARD BREAK and a literal '<br>' the source spelled
|
|
708
|
+
// out by hand both reach the DOM as an identical <br> element, and
|
|
709
|
+
// lib/editor/inline-md.js sees only the DOM. So editing ANY block holding a
|
|
710
|
+
// hard break degraded it to a literal '<br>' and cost the file a line
|
|
711
|
+
// (measured, list AND paragraph: '- a··' / ' b ~t' -> '- a<br>b \~t').
|
|
712
|
+
//
|
|
713
|
+
// THIS LAYER is the only place the two are still distinguishable: marked
|
|
714
|
+
// lexes a hard break as a `br` token, which arrives here, and a literal
|
|
715
|
+
// '<br>' as an inline `html` token, which goes to renderer.html above
|
|
716
|
+
// (verified: marked.lexer('x \ny')[0].tokens -> ['text','br','text'] while
|
|
717
|
+
// marked.lexer('x<br>y')[0].tokens -> ['text','html','text']). That
|
|
718
|
+
// separation holds because `breaks: false` is pinned in the setOptions()
|
|
719
|
+
// call below — under breaks:true every single newline would arrive here too
|
|
720
|
+
// and the marker would stop meaning "hard break".
|
|
721
|
+
//
|
|
722
|
+
// Edit mode only: reader and PDF output must stay byte-identical, and the
|
|
723
|
+
// attribute has no reader-side meaning. The Shift+Enter literal-'<br>'
|
|
724
|
+
// round-trip contract is untouched by construction — such a <br> never
|
|
725
|
+
// passes through this function.
|
|
726
|
+
if (opts.editMode) {
|
|
727
|
+
renderer.br = function() { return '<br data-hard-break="1">'; };
|
|
728
|
+
}
|
|
729
|
+
|
|
619
730
|
renderer.code = function(token) {
|
|
620
731
|
// token is either a string (old API) or {text, lang} object (new API)
|
|
621
732
|
const lang = (typeof token === 'object') ? (token.lang || '') : (arguments[1] || '');
|
|
@@ -774,13 +885,17 @@ ${itemsHtml}
|
|
|
774
885
|
// using the same renderer/options set above.
|
|
775
886
|
const tokens = marked.lexer(mdPre);
|
|
776
887
|
const parts = [];
|
|
777
|
-
|
|
888
|
+
// `v` is the block cursor; `prevLi`/`types` are the §3.8 run state the
|
|
889
|
+
// flat list renderer above threads through its own recursion (see
|
|
890
|
+
// liRunStartsHere()).
|
|
891
|
+
const biRef = { v: 0, prevLi: null, types: [] };
|
|
778
892
|
for (const t of tokens) {
|
|
779
893
|
if (t.type === 'space') continue;
|
|
780
894
|
if (t.type === 'list') {
|
|
781
895
|
parts.push(renderEditModeList(t, blocks, biRef));
|
|
782
896
|
continue;
|
|
783
897
|
}
|
|
898
|
+
resetLiRunState(biRef); // §3.8 rule (c): a non-li block ends every run
|
|
784
899
|
const b = blocks[biRef.v++];
|
|
785
900
|
const inner = marked.parser([t]);
|
|
786
901
|
parts.push(
|
|
@@ -788,6 +903,14 @@ ${itemsHtml}
|
|
|
788
903
|
inner + '</div>'
|
|
789
904
|
);
|
|
790
905
|
}
|
|
906
|
+
// Development-time alignment guard: the render walk and blockmap.js's
|
|
907
|
+
// pushListItemBlocks must consume blocks[] in lockstep. A mismatch means
|
|
908
|
+
// the two DFS orders have drifted apart, which silently mis-attributes
|
|
909
|
+
// every subsequent block id — far better to fail loudly here.
|
|
910
|
+
if (biRef.v !== blocks.length) {
|
|
911
|
+
throw new Error('edit-mode render consumed ' + biRef.v +
|
|
912
|
+
' blocks but the block map has ' + blocks.length);
|
|
913
|
+
}
|
|
791
914
|
bodyHtml = parts.join('\n');
|
|
792
915
|
} else {
|
|
793
916
|
bodyHtml = marked.parse(mdPre);
|
|
@@ -939,6 +1062,157 @@ ${itemsHtml}
|
|
|
939
1062
|
});
|
|
940
1063
|
</script>`;
|
|
941
1064
|
|
|
1065
|
+
// Edit-mode-only layout: give the block gutter (⠿ / +) its own room.
|
|
1066
|
+
//
|
|
1067
|
+
// The stylesheet above is SHARED by reader HTML, PDF export and edit mode,
|
|
1068
|
+
// so a bare `.content { padding-left }` there would shift every rendered
|
|
1069
|
+
// document — not wanted. This chunk is emitted ONLY when opts.editMode is
|
|
1070
|
+
// set, and it is a ruleset of its OWN (never merged into the existing
|
|
1071
|
+
// `.content { min-width: 0; flex: 1 1 auto; }` block, which
|
|
1072
|
+
// test/md2doc.test.js matches verbatim).
|
|
1073
|
+
//
|
|
1074
|
+
// Why it exists: the gutter buttons hang OUTSIDE the content box, and the
|
|
1075
|
+
// table row grip straddles the table's left border (half of it outside
|
|
1076
|
+
// too — see the .ed-te-grip-row comment above). Without real padding the
|
|
1077
|
+
// two fight over the same few pixels, which is what forced the earlier
|
|
1078
|
+
// (now reverted) "inset the row grip into the table" hack that ended up
|
|
1079
|
+
// covering the first cell's text. 56px of content padding plus moving the
|
|
1080
|
+
// gutter buttons out (⠿ to left:-36px, + to left:-54px) separates them
|
|
1081
|
+
// properly: the gutter pair occupies [contentLeft-54, contentLeft-18] and
|
|
1082
|
+
// the 20px-wide row grip straddles contentLeft at
|
|
1083
|
+
// [contentLeft-10, contentLeft+10] — an 8px gap, and the grip's inner half
|
|
1084
|
+
// stays within the cell's own 14px padding, so it never touches cell text.
|
|
1085
|
+
// (The padding was 48px until §4.2's hit-test conflict 1 was fixed; see the
|
|
1086
|
+
// .content rule in editModeLayoutCss below for why it is 56px now.)
|
|
1087
|
+
// S1 Task 5: the ordered-list ordinal is drawn by a CSS counter, and the
|
|
1088
|
+
// flat DOM took away the one thing a real <ol> gave for free — a counter
|
|
1089
|
+
// SCOPE per nesting level. Every li is a sibling of every other li now, so a
|
|
1090
|
+
// single shared counter has no way to leave the outer run's value alone
|
|
1091
|
+
// while a nested run counts: `1. / (nested) 1. / 2. / 2.` would render its
|
|
1092
|
+
// last item as "3.", because the nested run's reset landed in the same
|
|
1093
|
+
// scope. One counter PER DEPTH fixes it — the nested run resets ed-ol-1 and
|
|
1094
|
+
// never touches ed-ol-0 — and `data-indent` is the right key because it is
|
|
1095
|
+
// the attribute client.js keeps in step with the model (setBlockIndent()).
|
|
1096
|
+
//
|
|
1097
|
+
// Reset happens on `data-run-start`, which the renderer stamps per spec
|
|
1098
|
+
// §3.8 and client.js re-derives after a structural key (refreshRunStarts()).
|
|
1099
|
+
// `counter-reset: n 0` then `counter-increment: n` on the same element is
|
|
1100
|
+
// resolved reset-then-increment, so a run's first item is 1.
|
|
1101
|
+
//
|
|
1102
|
+
// Depths beyond MAX get no counter of their own and no fallback rule — there
|
|
1103
|
+
// is no depth-agnostic ordered rule below this loop — so their ::before
|
|
1104
|
+
// computes to `none` and the marker renders BLANK (measured at indent 10 and
|
|
1105
|
+
// 11). A missing ordinal beats a wrong one, and real markdown does not nest
|
|
1106
|
+
// that far; raise ED_OL_MAX_DEPTH if it ever does.
|
|
1107
|
+
const ED_OL_MAX_DEPTH = 9;
|
|
1108
|
+
let edOlCounterCss = '';
|
|
1109
|
+
for (let d = 0; d <= ED_OL_MAX_DEPTH; d++) {
|
|
1110
|
+
const sel = '.ed-block[data-block-type="li"][data-indent="' + d + '"]';
|
|
1111
|
+
edOlCounterCss +=
|
|
1112
|
+
'\n ' + sel + ' { counter-increment: ed-ol-' + d + '; }' +
|
|
1113
|
+
'\n ' + sel + '[data-run-start="1"] { counter-reset: ed-ol-' + d + ' 0;' +
|
|
1114
|
+
' counter-increment: ed-ol-' + d + '; }' +
|
|
1115
|
+
'\n .ed-block[data-list-type="ol"][data-indent="' + d + '"] > .ed-li-marker::before' +
|
|
1116
|
+
' { content: counter(ed-ol-' + d + ') "."; }';
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
const editModeLayoutCss = `
|
|
1120
|
+
/* 56px, not 48px — spec §4.2's hit-test conflict 1 (gutter vs
|
|
1121
|
+
.sidebar-splitter), which was the one of its three that never got a
|
|
1122
|
+
guard. .sidebar-splitter's right edge IS .content's border-box left edge
|
|
1123
|
+
(they are adjacent flex items), so the gutter pair at
|
|
1124
|
+
[contentLeft-54, contentLeft-18] hung 6px back over the splitter's own
|
|
1125
|
+
drag zone at 48px of padding. Measured at 1400x900: splitter [324, 356],
|
|
1126
|
+
.ed-insert [350, 368], elementFromPoint(354, +'s own y) === button
|
|
1127
|
+
.ed-insert, and a real pointer drag started at x=352 or x=354 left the
|
|
1128
|
+
sidebar at 300px instead of resizing it to 420 — while x=336 and x=348
|
|
1129
|
+
both worked. Worse, .ed-insert is only 20px tall and li rows carried none
|
|
1130
|
+
at all before S2, so the dead zone was striped both vertically and within
|
|
1131
|
+
a row: the same x resized the sidebar or silently opened an insert menu
|
|
1132
|
+
depending on which pixel row the user grabbed.
|
|
1133
|
+
|
|
1134
|
+
8 more pixels of padding move the pair to [contentLeft-54,
|
|
1135
|
+
contentLeft-18] = [splitterRight+2, splitterRight+20], i.e. entirely
|
|
1136
|
+
inside the content column with 2px of clearance. The 20px table row grip
|
|
1137
|
+
still straddles contentLeft at [contentLeft-10, contentLeft+10], so its
|
|
1138
|
+
8px gap to ⠿ is unchanged. Guarded by the elementFromPoint(splitter.right
|
|
1139
|
+
- 2, y) assertion §4.2 asks for, in
|
|
1140
|
+
test/editor-client-runtime.test.js. */
|
|
1141
|
+
.content { padding-left: 56px; }
|
|
1142
|
+
|
|
1143
|
+
/* S1: every block is one full-width row, whatever its type or depth. The
|
|
1144
|
+
indent is a property of the item's own MARKER, never of its box, so all
|
|
1145
|
+
the hover outlines match and — the load-bearing part — the absolutely
|
|
1146
|
+
positioned .ed-handle / .ed-insert keep one origin and therefore one
|
|
1147
|
+
vertical axis. Putting the indent on the .ed-block itself (padding or
|
|
1148
|
+
margin) drags that origin along with it and breaks exactly the promise
|
|
1149
|
+
this layout exists to make.
|
|
1150
|
+
|
|
1151
|
+
flex, not grid: the child count varies (marker, optional .ed-li-check,
|
|
1152
|
+
text), and a fixed grid track list would push a plain item's text into
|
|
1153
|
+
the column a task item's checkbox occupies. */
|
|
1154
|
+
.ed-block[data-block-type="li"] {
|
|
1155
|
+
display: flex; align-items: baseline; column-gap: 6px;
|
|
1156
|
+
}
|
|
1157
|
+
/* width, NOT min-width: the marker box is FIXED, so "10." and up overflow
|
|
1158
|
+
into the gutter instead of widening the column and pushing their own row's
|
|
1159
|
+
text 2-3px right of items 1-9. That is what a real <ol> does, and it is
|
|
1160
|
+
what keeps every item's text on one left edge.
|
|
1161
|
+
|
|
1162
|
+
"display: flex; justify-content: flex-end" is what makes that overflow go
|
|
1163
|
+
LEFT, and it is not interchangeable with "text-align: right". Once the
|
|
1164
|
+
content is wider than the box, text-align has nothing left to distribute:
|
|
1165
|
+
the line box is already full, so the glyphs grow from the box's LEFT edge
|
|
1166
|
+
rightward, across the 6px column-gap and onto the item's own text.
|
|
1167
|
+
Measured with the old rule at 15px on a 120-item list: the marker box is
|
|
1168
|
+
[72, 90] and .ed-li-text starts at 96, while marker.scrollWidth reports 21
|
|
1169
|
+
at item 10 and 29 at item 100 — i.e. ink out to x=101, five pixels INTO
|
|
1170
|
+
the text, which a screenshot reads back as "100item 100". Font-dependent
|
|
1171
|
+
in degree only (under DejaVu Sans it collides at two digits, a 10-item
|
|
1172
|
+
list), never in direction.
|
|
1173
|
+
|
|
1174
|
+
A flex container overflows toward its START side when justified to the
|
|
1175
|
+
end — the same "overflow goes the other way" property that safe/unsafe
|
|
1176
|
+
alignment exists to talk about — so the ink now grows leftward into the
|
|
1177
|
+
gutter, which is where the space is. The guard is
|
|
1178
|
+
"marker.scrollWidth === marker.clientWidth" (scrollWidth counts END-side
|
|
1179
|
+
overflow only, so left overflow is invisible to it by construction) in
|
|
1180
|
+
test/editor-client-runtime.test.js; asserting .ed-li-text's left edge does
|
|
1181
|
+
NOT catch this, because the box is fixed and that edge never moves. */
|
|
1182
|
+
.ed-block[data-block-type="li"] > .ed-li-marker {
|
|
1183
|
+
flex: 0 0 auto;
|
|
1184
|
+
margin-left: calc(var(--ed-indent, 0) * 1.6em);
|
|
1185
|
+
width: 1.2em;
|
|
1186
|
+
display: flex; justify-content: flex-end;
|
|
1187
|
+
color: #6b7280; -webkit-user-select: none; user-select: none;
|
|
1188
|
+
}
|
|
1189
|
+
/* min-width: 0 so an unbreakable token inside an item cannot widen the row
|
|
1190
|
+
past its siblings — that would break the equal-width guarantee above. */
|
|
1191
|
+
.ed-block[data-block-type="li"] > .ed-li-text { flex: 1 1 auto; min-width: 0; }
|
|
1192
|
+
/* column-gap already provides the marker/checkbox separation. */
|
|
1193
|
+
.ed-block[data-block-type="li"] > .ed-li-check { flex: 0 0 auto; margin-right: 0; }
|
|
1194
|
+
.ed-block[data-list-type="ul"] > .ed-li-marker::before { content: "\\2022"; }${edOlCounterCss}
|
|
1195
|
+
/* A BULLETED task item's checkbox IS its marker, so the • is suppressed and
|
|
1196
|
+
the marker box collapses (the indent lives on its margin and survives).
|
|
1197
|
+
An ORDERED task item keeps its number — that is what GFM renders — and
|
|
1198
|
+
the counter rules above outrank this one on specificity for ol, so
|
|
1199
|
+
scoping this to ul is what keeps the two cases apart. */
|
|
1200
|
+
.ed-block[data-list-type="ul"][data-task="1"] > .ed-li-marker { width: 0; }
|
|
1201
|
+
.ed-block[data-list-type="ul"][data-task="1"] > .ed-li-marker::before { content: none; }
|
|
1202
|
+
|
|
1203
|
+
/* D6: + immediately left of ⠿, on the same row, no gap — replacing the
|
|
1204
|
+
stacked layout the shared stylesheet still declares (see .ed-insert
|
|
1205
|
+
there, and why it had to stack before the gutter existed). The 48px of
|
|
1206
|
+
content padding above is what buys the room: the pair occupies
|
|
1207
|
+
[contentLeft-54, contentLeft-18], which with 56px of padding sits wholly
|
|
1208
|
+
inside the content column (2px clear of .sidebar-splitter's right edge —
|
|
1209
|
+
see the padding rule's own comment for the measurement), so + never
|
|
1210
|
+
reaches a negative viewport x and never covers the splitter. The 20px
|
|
1211
|
+
table row grip still straddles contentLeft at
|
|
1212
|
+
[contentLeft-10, contentLeft+10] - an 8px gap, unchanged. */
|
|
1213
|
+
.ed-handle { left: -36px; top: 0; }
|
|
1214
|
+
.ed-insert { left: -54px; top: 0; }`;
|
|
1215
|
+
|
|
942
1216
|
const html = `<!DOCTYPE html>
|
|
943
1217
|
<html lang="en">
|
|
944
1218
|
<head>
|
|
@@ -1704,8 +1978,24 @@ ${itemsHtml}
|
|
|
1704
1978
|
output, same precedent as the lightbox selectors above. */
|
|
1705
1979
|
.ed-block { position: relative; cursor: pointer; }
|
|
1706
1980
|
.ed-block:hover { outline: 1px dashed #b0b0b0; }
|
|
1707
|
-
|
|
1708
|
-
|
|
1981
|
+
/* white-space: pre-line is LOAD-BEARING, not styling — do not relax it.
|
|
1982
|
+
A hard-wrapped ("lazy continuation") list item's own content contains a
|
|
1983
|
+
real newline, and its .ed-li-text is an editing host. Under the default
|
|
1984
|
+
white-space:normal Chromium NORMALISES that newline to a space on the
|
|
1985
|
+
FIRST keystroke (measured: "alpha\ncont" -> "alphaZ cont"), so the item's
|
|
1986
|
+
source wrapping is destroyed by any edit and cannot be round-tripped —
|
|
1987
|
+
which is what forced an earlier revision to refuse such items outright and
|
|
1988
|
+
make ~22% of real list items read-only. pre-line (not pre-wrap) is the
|
|
1989
|
+
narrowest fix: newlines become significant, while runs of spaces still
|
|
1990
|
+
collapse exactly as before, so nothing about a single-line item changes.
|
|
1991
|
+
(No backticks in this comment: it lives inside a JS template literal.) */
|
|
1992
|
+
.ed-li-text { display: block; min-height: 1em; white-space: pre-line; }
|
|
1993
|
+
/* S1: a list item is a flat <div class="ed-block" data-block-type="li">,
|
|
1994
|
+
never an <li> — the selector follows the DOM. The marker's own glyph /
|
|
1995
|
+
ordinal, the --ed-indent indent and the run-scoped ordered counter are
|
|
1996
|
+
Task 5's and live in editModeLayoutCss (edit-mode only), fed by the hooks
|
|
1997
|
+
renderEditModeList() emits here. */
|
|
1998
|
+
.ed-block[data-block-type="li"] { cursor: text; }
|
|
1709
1999
|
.ed-li-check { display: inline-block; width: 14px; height: 14px; margin-right: 6px;
|
|
1710
2000
|
border: 1px solid #8a8a8a; border-radius: 3px; vertical-align: middle; cursor: pointer; }
|
|
1711
2001
|
.ed-li-check[data-checked="1"] { background: #3b82f6; border-color: #3b82f6; }
|
|
@@ -1749,23 +2039,38 @@ ${itemsHtml}
|
|
|
1749
2039
|
.ed-block:hover .ed-handle,
|
|
1750
2040
|
.ed-handle:focus { opacity: 1; }
|
|
1751
2041
|
.ed-handle:hover { background: rgba(0, 0, 0, 0.08); }
|
|
1752
|
-
/* The ⠿ handle's
|
|
1753
|
-
translucent
|
|
1754
|
-
.ed-seltb below.
|
|
2042
|
+
/* The ⠿ handle's menu: 轉換成 › / 複製 / 刪除 / MD 原始碼 (spec §3.7).
|
|
2043
|
+
Dark translucent panel, bordered rows — same visual language as
|
|
2044
|
+
.ed-seltb below.
|
|
2045
|
+
|
|
2046
|
+
S2: flex-direction is COLUMN, and that is behaviour rather than styling.
|
|
2047
|
+
The panel now carries word labels instead of the old ±/✕ glyphs, and a
|
|
2048
|
+
row of four CJK labels is wider than the content column on a narrow
|
|
2049
|
+
window — the last item scrolls out of reach. A stacked panel is also what
|
|
2050
|
+
lets 轉換成 grow a submenu beside its OWN row rather than below the whole
|
|
2051
|
+
bar. test/editor-client-runtime.test.js asserts the computed
|
|
2052
|
+
flex-direction and that every button shares one left edge. */
|
|
1755
2053
|
.ed-handle-menu {
|
|
1756
2054
|
position: absolute; top: -4px; left: -4px; z-index: 6;
|
|
1757
|
-
display: flex; align-items:
|
|
1758
|
-
padding: 4px
|
|
2055
|
+
display: flex; flex-direction: column; align-items: stretch; gap: 2px;
|
|
2056
|
+
padding: 4px; border-radius: 8px;
|
|
1759
2057
|
background: rgba(16, 18, 21, 0.92); color: #e6edf3;
|
|
1760
2058
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25);
|
|
1761
2059
|
}
|
|
1762
2060
|
.ed-handle-menu-btn {
|
|
1763
|
-
min-width:
|
|
2061
|
+
min-width: 132px; height: 26px; padding: 0 10px;
|
|
1764
2062
|
border: 1px solid rgba(255, 255, 255, 0.25); border-radius: 6px;
|
|
1765
2063
|
background: rgba(255, 255, 255, 0.08); color: inherit;
|
|
1766
2064
|
font: inherit; font-size: 12px; line-height: 1;
|
|
1767
|
-
white-space: nowrap; cursor: pointer;
|
|
1768
|
-
}
|
|
2065
|
+
text-align: left; white-space: nowrap; cursor: pointer;
|
|
2066
|
+
}
|
|
2067
|
+
/* The 轉換成 submenu carries BOTH classes, so it inherits every rule above
|
|
2068
|
+
and only overrides the horizontal offset. It is a CHILD of the menu, and
|
|
2069
|
+
the menu is itself position:absolute, so the menu's own padding box is
|
|
2070
|
+
this element's containing block and left:100% lands it flush against
|
|
2071
|
+
the menu's right edge. Source order matters: equal specificity with
|
|
2072
|
+
.ed-handle-menu's own left:-4px, so this rule must stay AFTER it. */
|
|
2073
|
+
.ed-handle-submenu { left: 100%; margin-left: 4px; }
|
|
1769
2074
|
.ed-handle-menu-btn:hover { background: rgba(255, 255, 255, 0.18); }
|
|
1770
2075
|
.ed-handle-menu-btn[hidden] { display: none; }
|
|
1771
2076
|
/* §10-gap fix: block-level INSERT — the + button sits NEXT TO the ⠿
|
|
@@ -1778,7 +2083,15 @@ ${itemsHtml}
|
|
|
1778
2083
|
layout — measured directly). A second 18px-wide button placed FURTHER
|
|
1779
2084
|
left (e.g. left:-44px) would land at a NEGATIVE viewport x and be
|
|
1780
2085
|
unreachable/unclickable outside the visible page. The brief's own
|
|
1781
|
-
wording allows this ("left gutter, above or beside it").
|
|
2086
|
+
wording allows this ("left gutter, above or beside it").
|
|
2087
|
+
|
|
2088
|
+
S1 Task 5 (D6): edit mode now overrides BOTH buttons to sit side by side
|
|
2089
|
+
(+ at left:-54px, ⠿ at left:-36px, both top:0) — see editModeLayoutCss.
|
|
2090
|
+
That became possible only once .content gained 48px of edit-mode padding;
|
|
2091
|
+
the stacked geometry declared here is what any NON-edit render would use,
|
|
2092
|
+
and those never emit .ed-block at all, so it is inert there. Kept rather
|
|
2093
|
+
than deleted because it is the correct fallback if the gutter padding is
|
|
2094
|
+
ever conditioned differently. */
|
|
1782
2095
|
.ed-insert {
|
|
1783
2096
|
position: absolute; left: -22px; top: -22px; width: 18px; height: 20px;
|
|
1784
2097
|
display: flex; align-items: center; justify-content: center;
|
|
@@ -1866,10 +2179,13 @@ ${itemsHtml}
|
|
|
1866
2179
|
affordance) with two real, adequately-sized (>=18x24px) click/drag
|
|
1867
2180
|
targets. '.ed-te-grip-row' is a vertical 6-dot handle shown at the LEFT
|
|
1868
2181
|
EDGE of the hovered row -- every row, the HEADER included (spec 3.10:
|
|
1869
|
-
the header is draggable too),
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
2182
|
+
the header is draggable too), STRADDLING the table's left border (its
|
|
2183
|
+
centreline ON that border), which is the same rule '.ed-te-grip-col'
|
|
2184
|
+
uses on the table's TOP border -- one geometry for both axes, no
|
|
2185
|
+
per-row-type special case. The block's own gutter is kept clear of it by
|
|
2186
|
+
the edit-mode-only layout chunk emitted near the end of this stylesheet,
|
|
2187
|
+
not by insetting the grip; '.ed-te-grip-col' is a horizontal 6-dot
|
|
2188
|
+
handle shown just ABOVE the hovered column (every column). Dots are plain <span>s laid out via CSS grid with
|
|
1873
2189
|
place-content: center, so the dot cluster stays compact/centered
|
|
1874
2190
|
regardless of the button's own (larger, hit-target-sized) box — no
|
|
1875
2191
|
images, no background gradients. '.ed-te-grip-dragging' is EITHER
|
|
@@ -1946,6 +2262,7 @@ ${itemsHtml}
|
|
|
1946
2262
|
background: #fff; color: #b00020; border: none; border-radius: 4px;
|
|
1947
2263
|
padding: 4px 12px; cursor: pointer; font-weight: bold;
|
|
1948
2264
|
}
|
|
2265
|
+
${opts.editMode ? editModeLayoutCss : ''}
|
|
1949
2266
|
</style>
|
|
1950
2267
|
${usesMath ? buildKatexStyleTag() : ''}
|
|
1951
2268
|
</head>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@helping-ai-workflow/md2doc",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.11.0",
|
|
4
4
|
"description": "Markdown → HTML / PDF renderer with WaveDrom, Mermaid, and Graphviz support",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"markdown",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
},
|
|
37
37
|
"scripts": {
|
|
38
38
|
"preinstall": "node scripts/preinstall.js",
|
|
39
|
-
"test": "node test/md2doc.test.js && node test/blockmap.test.js && node test/images.test.js && node test/scroll-anchor.test.js && node test/lightbox.test.js && node test/lightbox-anno.test.js && node test/lightbox-anno-style.test.js && node test/reader-panels.test.js && node test/cli.test.js && node test/code-operator.test.js && node test/render-api.test.js && node test/lineops.test.js && node test/editmode-render.test.js && node test/editmode-wavedrom-reinit.test.js && node test/editor-server.test.js && node test/cli-edit.test.js && node test/open-viewer.test.js && node test/editor-client.test.js && node test/editor-client-runtime.test.js && node test/roundtrip.test.js && node test/byte-stability.test.js && node test/editor-server-throw.test.js && node test/editor-reader-rebind.test.js && node test/inline-md.test.js && node test/table-md.test.js && node test/gate-compat.test.js && node test/history.test.js && node test/list-md.test.js"
|
|
39
|
+
"test": "node test/md2doc.test.js && node test/blockmap.test.js && node test/images.test.js && node test/scroll-anchor.test.js && node test/lightbox.test.js && node test/lightbox-anno.test.js && node test/lightbox-anno-style.test.js && node test/reader-panels.test.js && node test/cli.test.js && node test/code-operator.test.js && node test/convert-md.test.js && node test/render-api.test.js && node test/lineops.test.js && node test/indent-clamp.test.js && node test/editmode-render.test.js && node test/editmode-wavedrom-reinit.test.js && node test/editor-server.test.js && node test/cli-edit.test.js && node test/open-viewer.test.js && node test/editor-client.test.js && node test/editor-client-runtime.test.js && node test/roundtrip.test.js && node test/byte-stability.test.js && node test/editor-server-throw.test.js && node test/editor-reader-rebind.test.js && node test/inline-md.test.js && node test/table-md.test.js && node test/gate-compat.test.js && node test/history.test.js && node test/list-md.test.js"
|
|
40
40
|
},
|
|
41
41
|
"repository": {
|
|
42
42
|
"type": "git",
|