@helping-ai-workflow/md2doc 3.6.0 → 3.7.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/cell-range.js +85 -0
- package/lib/editor/cli.js +2 -2
- package/lib/editor/client.js +847 -14
- package/lib/editor/server.js +209 -47
- package/lib/md2doc.js +13 -0
- package/package.json +2 -2
package/lib/editor/server.js
CHANGED
|
@@ -38,6 +38,10 @@ const PATCHMAP_SRC = fs.readFileSync(path.join(__dirname, 'patchmap.js'), 'utf8'
|
|
|
38
38
|
// property as indent-clamp.js and convert-md.js above; it only has to land
|
|
39
39
|
// before client.js reads window.md2docSelection.
|
|
40
40
|
const SELECTION_SRC = fs.readFileSync(path.join(__dirname, 'selection.js'), 'utf8');
|
|
41
|
+
// Table cell-range selection's pure model — same "no dependency on any other
|
|
42
|
+
// editor module" property as selection.js; it only has to land before
|
|
43
|
+
// client.js reads window.md2docCellRange.
|
|
44
|
+
const CELL_RANGE_SRC = fs.readFileSync(path.join(__dirname, 'cell-range.js'), 'utf8');
|
|
41
45
|
// v3.1.0 Task E: the four Phase-1 modules behind the visible toolbar, the
|
|
42
46
|
// paste pipeline, the image-drop asset endpoint and the whole-document
|
|
43
47
|
// source escape hatch. Same "pure module, no dependency on any other editor
|
|
@@ -208,21 +212,64 @@ function assertBlockRangesFit(blocks, lines) {
|
|
|
208
212
|
}
|
|
209
213
|
}
|
|
210
214
|
|
|
211
|
-
// F13: the never-opened grace deadline is deliberately its own constant,
|
|
212
|
-
//
|
|
213
|
-
//
|
|
214
|
-
//
|
|
215
|
-
// Before the URL is opened there is no
|
|
216
|
-
// is instead for a human to alt-tab to a
|
|
217
|
-
// printed URL across to a local machine and
|
|
218
|
-
// takes longer than
|
|
219
|
-
// wait would close sessions on people
|
|
220
|
-
// Five minutes is chosen as a margin
|
|
221
|
-
// still finite; test/cli-edit.test.js
|
|
222
|
-
// override — an unopened session still exits
|
|
223
|
-
// opened partway through the grace window keeps
|
|
215
|
+
// F13: the never-opened grace deadline is deliberately its own constant, not
|
|
216
|
+
// connectionGraceMs (see that constant's own comment) reused. connectionGraceMs
|
|
217
|
+
// governs the gap between one /api/alive connection dropping and a
|
|
218
|
+
// replacement showing up once a real tab is confirmed to have existed at
|
|
219
|
+
// all. Before the URL is opened there is no connection to have dropped in
|
|
220
|
+
// the first place — the wait here is instead for a human to alt-tab to a
|
|
221
|
+
// browser, or, over SSH, copy the printed URL across to a local machine and
|
|
222
|
+
// load it there, which routinely takes longer than connectionGraceMs allows.
|
|
223
|
+
// Reusing connectionGraceMs for that wait would close sessions on people
|
|
224
|
+
// still reading the terminal output. Five minutes is chosen as a margin
|
|
225
|
+
// generous for that human handoff while still finite; test/cli-edit.test.js
|
|
226
|
+
// drives it through a shortened override — an unopened session still exits
|
|
227
|
+
// on its own, and a session opened partway through the grace window keeps
|
|
228
|
+
// working afterward.
|
|
224
229
|
const DEFAULT_NEVER_OPENED_GRACE_MS = 5 * 60 * 1000;
|
|
225
230
|
|
|
231
|
+
// v3.6.0: replaces idleTimeoutMs as the "is anyone still there" clock.
|
|
232
|
+
// idleTimeoutMs measured the gap since the last /api/ping — a plain
|
|
233
|
+
// setInterval(..., 10000) in client.js — and pings from a backgrounded
|
|
234
|
+
// (merely unfocused, tab still open) browser tab are throttled by the
|
|
235
|
+
// browser itself. MEASURED on this branch (see LIVENESS-REPORT.md): once
|
|
236
|
+
// Chrome's real background-timer throttling is allowed to engage — puppeteer
|
|
237
|
+
// disables it by default for automation stability, and the first measurement
|
|
238
|
+
// pass on this branch silently measured THAT instead — a backgrounded tab's
|
|
239
|
+
// 10s ping settled at roughly once per minute. 60s > the old 30,000ms
|
|
240
|
+
// default, so a tab the user had only alt-tabbed away from looked exactly
|
|
241
|
+
// like a closed one and the session was killed out from under a document
|
|
242
|
+
// still open on screen. A connection is not a timer, so it is not subject to
|
|
243
|
+
// that clamp: GET /api/alive holds its response open for as long as the tab
|
|
244
|
+
// (or this same document's tab, after a reload) keeps it open, and dropping
|
|
245
|
+
// to zero open connections is what this grace measures from.
|
|
246
|
+
//
|
|
247
|
+
// The grace itself exists for one trap: an F5 reload closes the old
|
|
248
|
+
// connection and opens a new one, and the two are NOT the same event — there
|
|
249
|
+
// is a real (if small) gap between them, during which the connection count is
|
|
250
|
+
// genuinely zero. Without a grace, every reload would kill the session.
|
|
251
|
+
// MEASURED (LIVENESS-REPORT.md, five consecutive page.reload() calls against
|
|
252
|
+
// a real headless Chrome hitting the real /api/alive route): the old
|
|
253
|
+
// connection's 'close' to the replacement connection's arrival ran
|
|
254
|
+
// 55-73ms, averaging 64ms. This default (5s) is roughly 70x that measured
|
|
255
|
+
// margin — headroom for a slower disk, a loaded machine, or a proxy in the
|
|
256
|
+
// path — while still closing a genuinely abandoned session within a few
|
|
257
|
+
// seconds rather than lingering. test/cli-edit.test.js and
|
|
258
|
+
// test/editor-server.test.js drive it through a shortened override, the same
|
|
259
|
+
// pattern DEFAULT_NEVER_OPENED_GRACE_MS above already uses.
|
|
260
|
+
const DEFAULT_CONNECTION_GRACE_MS = 5000;
|
|
261
|
+
|
|
262
|
+
// server.js sends one of these on every open /api/alive stream so an idle
|
|
263
|
+
// stream is never silently dropped by an intermediary (a corporate proxy, a
|
|
264
|
+
// misbehaving localhost tool) that closes connections it hasn't seen traffic
|
|
265
|
+
// on in a while, and so a half-open socket (the far end vanished without a
|
|
266
|
+
// TCP FIN, e.g. a laptop that lost power) is discovered on the next write
|
|
267
|
+
// rather than held forever. A bare SSE comment line (no `data:` field) is
|
|
268
|
+
// invisible to EventSource's onmessage — nothing to parse, nothing to react
|
|
269
|
+
// to, exactly the "the connection's mere existence is the signal" contract
|
|
270
|
+
// client.js documents at its own call site.
|
|
271
|
+
const ALIVE_KEEPALIVE_MS = 15000;
|
|
272
|
+
|
|
226
273
|
// T21 item 3: closing this server has to mean "stop serving now", not "stop
|
|
227
274
|
// serving once every open socket happens to go away". `server.close()` on its
|
|
228
275
|
// own does the latter: it stops accepting, then waits for the connections it
|
|
@@ -249,20 +296,43 @@ function shutdownServer(server, reason) {
|
|
|
249
296
|
}
|
|
250
297
|
|
|
251
298
|
async function createEditorServer({
|
|
252
|
-
files,
|
|
299
|
+
files, neverOpenedGraceMs = DEFAULT_NEVER_OPENED_GRACE_MS,
|
|
300
|
+
connectionGraceMs = DEFAULT_CONNECTION_GRACE_MS,
|
|
253
301
|
clientJs = '', listenPort = 0,
|
|
254
302
|
}) {
|
|
255
303
|
const absFiles = files.map((f) => path.resolve(f));
|
|
256
|
-
let idleTimer = null;
|
|
257
304
|
let neverOpenedTimer = null;
|
|
258
305
|
let started = false;
|
|
306
|
+
// v3.6.0: the liveness mechanism. `aliveConnections` holds every currently
|
|
307
|
+
// open GET /api/alive response, GLOBALLY across every tab and every file
|
|
308
|
+
// this one server serves — `md2doc --edit a.md b.md` is one session, and
|
|
309
|
+
// closing the tab for a.md must not tear down b.md's still-open tab, so
|
|
310
|
+
// the decision that matters is "is the set empty", never "which file was
|
|
311
|
+
// this connection for". `started` now means "at least one such connection
|
|
312
|
+
// has ever been registered" — see registerAliveConnection() and
|
|
313
|
+
// DEFAULT_NEVER_OPENED_GRACE_MS's own comment on why that boundary moved
|
|
314
|
+
// off GET /edit/:id.
|
|
315
|
+
const aliveConnections = new Set();
|
|
316
|
+
let shutdownGraceTimer = null;
|
|
317
|
+
// Guards against the (already-benign, but worth making impossible on
|
|
318
|
+
// purpose) case of two shutdown triggers landing close together — e.g. a
|
|
319
|
+
// connection's 'close' event arriving from closeAllConnections() itself
|
|
320
|
+
// while the process is already on its way out through srv.close().
|
|
321
|
+
let closing = false;
|
|
259
322
|
|
|
260
323
|
// v3.4.0 batch2 Task 6 — Ruling B2-2: watch every `.drawio`/`.xml` file the
|
|
261
324
|
// CURRENTLY OPEN document references, and let /api/ping's already-firing
|
|
262
325
|
// 10s heartbeat (see its own comment below) double as the staleness check,
|
|
263
|
-
// rather than inventing a push transport
|
|
264
|
-
//
|
|
265
|
-
//
|
|
326
|
+
// rather than inventing a SEPARATE push transport for it. v3.6.0 DID add
|
|
327
|
+
// one push transport to this stack — GET /api/alive, an SSE stream, but
|
|
328
|
+
// only for session liveness (see registerAliveConnection() below and
|
|
329
|
+
// DEFAULT_CONNECTION_GRACE_MS's comment for why polling could not do that
|
|
330
|
+
// job). The reasoning that kept drawio staleness on polling is unchanged
|
|
331
|
+
// and still applies to drawio specifically: a filesystem watch would need
|
|
332
|
+
// one watcher per referenced file with no bound on how many a document can
|
|
333
|
+
// reference, and the existing 10s heartbeat already has a client on the
|
|
334
|
+
// other end of it for a different reason, so riding it costs nothing extra
|
|
335
|
+
// beyond the stat calls in checkDrawioStale() below.
|
|
266
336
|
//
|
|
267
337
|
// TWO maps, and the split is the whole point of fix round 1:
|
|
268
338
|
//
|
|
@@ -486,13 +556,84 @@ async function createEditorServer({
|
|
|
486
556
|
c.pending = null;
|
|
487
557
|
}
|
|
488
558
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
559
|
+
// Fires shutdownServer() at most once, regardless of which of this file's
|
|
560
|
+
// several deadlines got there first (see `closing`'s own comment above).
|
|
561
|
+
function shutdownOnce(server, reason) {
|
|
562
|
+
if (closing) return;
|
|
563
|
+
closing = true;
|
|
564
|
+
cancelShutdownGrace();
|
|
565
|
+
shutdownServer(server, reason);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function cancelShutdownGrace() {
|
|
569
|
+
if (shutdownGraceTimer) { clearTimeout(shutdownGraceTimer); shutdownGraceTimer = null; }
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// Armed the moment `aliveConnections` drops to zero, cancelled the moment
|
|
573
|
+
// a new one arrives (registerAliveConnection() below calls
|
|
574
|
+
// cancelShutdownGrace() unconditionally on every new connection, whether or
|
|
575
|
+
// not a grace timer happens to be running). This is the trap named in the
|
|
576
|
+
// design doc: an F5 reload closes the old /api/alive stream and opens a
|
|
577
|
+
// fresh one, and those are two SEPARATE events with a real gap between
|
|
578
|
+
// them — without this grace, every reload would read as "the tab closed"
|
|
579
|
+
// and kill the session out from under the user mid-refresh. See
|
|
580
|
+
// DEFAULT_CONNECTION_GRACE_MS's own comment for the measurement that sized
|
|
581
|
+
// the default.
|
|
582
|
+
function armShutdownGrace(server) {
|
|
583
|
+
if (shutdownGraceTimer || closing) return;
|
|
584
|
+
shutdownGraceTimer = setTimeout(() => {
|
|
585
|
+
shutdownGraceTimer = null;
|
|
586
|
+
// Re-check rather than trust the callback firing at all means "still
|
|
587
|
+
// zero": a connection could have registered and then, in a pathological
|
|
588
|
+
// fast sequence, this timer's callback could still be queued from
|
|
589
|
+
// before that registration cancelled it. Belt-and-braces; cheap.
|
|
590
|
+
if (aliveConnections.size > 0) return;
|
|
591
|
+
shutdownOnce(server, 'idle');
|
|
592
|
+
}, connectionGraceMs);
|
|
593
|
+
if (shutdownGraceTimer.unref) shutdownGraceTimer.unref();
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// Registers one open GET /api/alive response as "this tab is here". Every
|
|
597
|
+
// call cancels any pending shutdown grace FIRST — a fresh connection is by
|
|
598
|
+
// definition proof the session should stay up, whether it is genuinely the
|
|
599
|
+
// first tab, a second tab, or a reload's replacement for one that just
|
|
600
|
+
// dropped. `started` flips here rather than on GET /edit/:id — see
|
|
601
|
+
// DEFAULT_NEVER_OPENED_GRACE_MS's comment on why the "was this session ever
|
|
602
|
+
// really opened" boundary is a live connection now, not the HTML request
|
|
603
|
+
// that precedes it by however long the browser takes to run client.js.
|
|
604
|
+
function registerAliveConnection(server, res, fileId, clientId) {
|
|
605
|
+
cancelShutdownGrace();
|
|
606
|
+
started = true;
|
|
607
|
+
// Stashed on the response object itself rather than threaded through a
|
|
608
|
+
// second Map: nothing in the closing decision reads these back (see this
|
|
609
|
+
// function's own comment), so a real diagnostic (a future `/api/debug`,
|
|
610
|
+
// a log line) is the only consumer, and it already has `res` in hand.
|
|
611
|
+
res._md2docAliveFileId = fileId;
|
|
612
|
+
res._md2docAliveClientId = clientId;
|
|
613
|
+
res.writeHead(200, {
|
|
614
|
+
'content-type': 'text/event-stream; charset=utf-8',
|
|
615
|
+
'cache-control': 'no-store',
|
|
616
|
+
connection: 'keep-alive',
|
|
617
|
+
});
|
|
618
|
+
// Flushes the headers immediately rather than waiting for the first
|
|
619
|
+
// keepalive tick — the client's EventSource does not consider the
|
|
620
|
+
// connection open (readyState transitions to OPEN) until bytes arrive.
|
|
621
|
+
res.write(': connected\n\n');
|
|
622
|
+
aliveConnections.add(res);
|
|
623
|
+
const keepalive = setInterval(() => {
|
|
624
|
+
// A write to an already-gone socket throws; the 'close' listener below
|
|
625
|
+
// is what actually tears this connection's bookkeeping down; this
|
|
626
|
+
// catch only stops that throw from reaching an uncaught spot on a beat
|
|
627
|
+
// that lost a race against the socket's own teardown.
|
|
628
|
+
try { res.write(': keepalive\n\n'); } catch (e) { /* 'close' handles cleanup */ }
|
|
629
|
+
}, ALIVE_KEEPALIVE_MS);
|
|
630
|
+
if (keepalive.unref) keepalive.unref();
|
|
631
|
+
res.on('close', () => {
|
|
632
|
+
clearInterval(keepalive);
|
|
633
|
+
if (!aliveConnections.has(res)) return; // already handled (e.g. shutdown path)
|
|
634
|
+
aliveConnections.delete(res);
|
|
635
|
+
if (aliveConnections.size === 0) armShutdownGrace(server);
|
|
636
|
+
});
|
|
496
637
|
}
|
|
497
638
|
|
|
498
639
|
const server = http.createServer(async (req, res) => {
|
|
@@ -594,6 +735,7 @@ async function createEditorServer({
|
|
|
594
735
|
`<script>${CONVERT_MD_SRC}</script>\n` +
|
|
595
736
|
`<script>${PATCHMAP_SRC}</script>\n` +
|
|
596
737
|
`<script>${SELECTION_SRC}</script>\n` +
|
|
738
|
+
`<script>${CELL_RANGE_SRC}</script>\n` +
|
|
597
739
|
`<script>${TOOLBAR_MODEL_SRC}</script>\n` +
|
|
598
740
|
`<script>${DOCSOURCE_SRC}</script>\n` +
|
|
599
741
|
`<script>${ASSET_SRC}</script>\n` +
|
|
@@ -615,8 +757,10 @@ async function createEditorServer({
|
|
|
615
757
|
const out = bodyAt !== -1
|
|
616
758
|
? html.slice(0, bodyAt) + inject + html.slice(bodyAt)
|
|
617
759
|
: html + inject;
|
|
618
|
-
started
|
|
619
|
-
|
|
760
|
+
// `started` no longer flips here — see registerAliveConnection()'s
|
|
761
|
+
// comment. This request only proves the HTML was served, not that a
|
|
762
|
+
// live tab is behind it (this same GET is also what a plain HTTP
|
|
763
|
+
// client, e.g. curl or this file's own test helpers, sends).
|
|
620
764
|
// no-store: the page embeds a snapshot of the client runtime AND the
|
|
621
765
|
// file's lines/mtime — a cached copy is stale code + a guaranteed
|
|
622
766
|
// mtime conflict after any external edit.
|
|
@@ -635,7 +779,6 @@ async function createEditorServer({
|
|
|
635
779
|
// Refs only — NOT any client's baseline. See trackDrawioRefs()'s own
|
|
636
780
|
// comment: the client has not yet decided whether it can apply this.
|
|
637
781
|
trackDrawioRefs(fileId, drawioRefs);
|
|
638
|
-
bumpIdle(server);
|
|
639
782
|
return send(res, 200, { parts, blocks });
|
|
640
783
|
}
|
|
641
784
|
|
|
@@ -656,7 +799,6 @@ async function createEditorServer({
|
|
|
656
799
|
const tmp = file + '.md2doc-tmp';
|
|
657
800
|
fs.writeFileSync(tmp, content, 'utf8');
|
|
658
801
|
fs.renameSync(tmp, file);
|
|
659
|
-
bumpIdle(server);
|
|
660
802
|
return send(res, 200, { mtimeMs: fs.statSync(file).mtimeMs });
|
|
661
803
|
}
|
|
662
804
|
|
|
@@ -791,19 +933,37 @@ async function createEditorServer({
|
|
|
791
933
|
}
|
|
792
934
|
throw e;
|
|
793
935
|
}
|
|
794
|
-
bumpIdle(server);
|
|
795
936
|
return send(res, 200, { name, path: assetLib.relPath(name) });
|
|
796
937
|
}
|
|
797
938
|
|
|
939
|
+
// v3.6.0: the liveness stream. A GET (EventSource cannot send any other
|
|
940
|
+
// method), held open for as long as the tab exists. fileId/clientId are
|
|
941
|
+
// read here purely so a future diagnostic has them to hand — see
|
|
942
|
+
// registerAliveConnection()'s own comment for why nothing about
|
|
943
|
+
// whether the session stays alive depends on either one being
|
|
944
|
+
// well-formed; the response object's own identity is the only thing
|
|
945
|
+
// the closing decision is keyed on, deliberately, the same way
|
|
946
|
+
// drawioClients above is keyed on a validated pair while THIS map does
|
|
947
|
+
// not need one at all.
|
|
948
|
+
if (req.method === 'GET' && url.pathname === '/api/alive') {
|
|
949
|
+
const aliveFileId = Number(url.searchParams.get('fileId'));
|
|
950
|
+
const aliveClientId = url.searchParams.get('clientId');
|
|
951
|
+
registerAliveConnection(server, res, aliveFileId, aliveClientId);
|
|
952
|
+
return; // response intentionally stays open — nothing more to send
|
|
953
|
+
}
|
|
954
|
+
|
|
798
955
|
if (req.method === 'POST' && url.pathname === '/api/ping') {
|
|
799
|
-
bumpIdle(server);
|
|
800
956
|
// v3.4.0 batch2 Task 6: fileId / drawioClientId / drawioAck are NEW,
|
|
801
957
|
// all optional, on the body this heartbeat already sent every 10s
|
|
802
958
|
// (old clients / the existing test in test/editor-server.test.js send
|
|
803
959
|
// `{}` — that still resolves to "no tracked state", the same
|
|
804
960
|
// zero-cost 204 as before). A body that fails to parse degrades the
|
|
805
961
|
// same way: no staleness check, plain 204 — a malformed ping body
|
|
806
|
-
// must never be the thing that breaks the
|
|
962
|
+
// must never be the thing that breaks the drawio-staleness contract.
|
|
963
|
+
// v3.6.0: this route no longer has any bearing on whether the
|
|
964
|
+
// session stays alive — see GET /api/alive above for that. It is
|
|
965
|
+
// kept exactly as it was for the ONE job it still does: the drawio
|
|
966
|
+
// staleness heartbeat below.
|
|
807
967
|
//
|
|
808
968
|
// Fix round 1 (review F8): an explicit 8 KB readJson limit, not the
|
|
809
969
|
// 50 MB default — the same precedent /api/save set for its own body,
|
|
@@ -864,22 +1024,25 @@ async function createEditorServer({
|
|
|
864
1024
|
|
|
865
1025
|
// F13: counts from listen (this point), not from process start — listen
|
|
866
1026
|
// is the moment a client could first reach the server, and that is the
|
|
867
|
-
// deadline this timer is measuring against. Unref'd like
|
|
868
|
-
// for the same reason: the listening socket
|
|
869
|
-
//
|
|
870
|
-
//
|
|
871
|
-
//
|
|
1027
|
+
// deadline this timer is measuring against. Unref'd like shutdownGraceTimer,
|
|
1028
|
+
// and for the same reason: the listening socket (or, once one exists, an
|
|
1029
|
+
// open /api/alive connection) is what should keep the process alive, not
|
|
1030
|
+
// this timer. A timer that held a ref would keep the process up on its own
|
|
1031
|
+
// even if the socket already went away, which is backwards for a clock
|
|
1032
|
+
// whose only job is to give up and exit.
|
|
872
1033
|
neverOpenedTimer = setTimeout(() => {
|
|
873
1034
|
// The guard against closing a session that has already been opened —
|
|
874
|
-
// this timer is armed once, at listen time, and left to run; it is
|
|
875
|
-
//
|
|
876
|
-
//
|
|
877
|
-
// `started` fresh at fire time
|
|
878
|
-
//
|
|
879
|
-
//
|
|
880
|
-
//
|
|
1035
|
+
// this timer is armed once, at listen time, and left to run; it is not
|
|
1036
|
+
// cancelled by GET /api/alive registering a connection. Whether a live
|
|
1037
|
+
// connection was ever established by the time the deadline arrives is
|
|
1038
|
+
// decided here, by reading `started` fresh at fire time (registerAliveConnection()
|
|
1039
|
+
// is the only place that sets it — see its own comment). Proven
|
|
1040
|
+
// load-bearing by mutation: with this line removed, an actively-connected
|
|
1041
|
+
// session still gets closed the moment this deadline passes (see
|
|
1042
|
+
// test/cli-edit.test.js's "must survive the never-opened grace deadline"
|
|
1043
|
+
// case).
|
|
881
1044
|
if (started) return;
|
|
882
|
-
|
|
1045
|
+
shutdownOnce(server, 'never-opened');
|
|
883
1046
|
}, neverOpenedGraceMs);
|
|
884
1047
|
if (neverOpenedTimer.unref) neverOpenedTimer.unref();
|
|
885
1048
|
|
|
@@ -891,9 +1054,8 @@ async function createEditorServer({
|
|
|
891
1054
|
return i === -1 ? null : `http://127.0.0.1:${port}/edit/${i}`;
|
|
892
1055
|
},
|
|
893
1056
|
close() {
|
|
894
|
-
if (idleTimer) clearTimeout(idleTimer);
|
|
895
1057
|
if (neverOpenedTimer) clearTimeout(neverOpenedTimer);
|
|
896
|
-
|
|
1058
|
+
shutdownOnce(server, null);
|
|
897
1059
|
},
|
|
898
1060
|
};
|
|
899
1061
|
}
|
package/lib/md2doc.js
CHANGED
|
@@ -1423,6 +1423,14 @@ ${itemsHtml}
|
|
|
1423
1423
|
/* min-width: 0 so an unbreakable token inside an item cannot widen the row
|
|
1424
1424
|
past its siblings — that would break the equal-width guarantee above. */
|
|
1425
1425
|
.ed-block[data-block-type="li"] > .ed-li-text { flex: 1 1 auto; min-width: 0; }
|
|
1426
|
+
/* The MD 原始碼 raw editor replaces the item's children with one .ed-editing
|
|
1427
|
+
wrap, which is then this flex row's only child. Without a grow it shrank
|
|
1428
|
+
to the textarea's intrinsic size and .ed-raw's width: 100% resolved
|
|
1429
|
+
against that: 189px at every viewport width (measured), where every other
|
|
1430
|
+
block's raw editor spans the column. The marker, and with it the indent,
|
|
1431
|
+
is gone while the textarea is open, so it spans the full column at every
|
|
1432
|
+
depth; the source line's own leading spaces show the nesting. */
|
|
1433
|
+
.ed-block[data-block-type="li"] > .ed-editing { flex: 1 1 auto; min-width: 0; }
|
|
1426
1434
|
/* column-gap already provides the marker/checkbox separation. */
|
|
1427
1435
|
.ed-block[data-block-type="li"] > .ed-li-check { flex: 0 0 auto; margin-right: 0; }
|
|
1428
1436
|
.ed-block[data-list-type="ul"] > .ed-li-marker::before { content: "\\2022"; }${edOlCounterCss}
|
|
@@ -3267,6 +3275,11 @@ ${usesDrawio ? ` /* v3.4.0 §4: a document with no drawio reference must not pa
|
|
|
3267
3275
|
indicator' is the singleton line shown while dragging a row.
|
|
3268
3276
|
'.ed-te-row-dragging' dims the row actually being dragged. */
|
|
3269
3277
|
.ed-te-hl { background: rgba(59, 130, 246, 0.15) !important; }
|
|
3278
|
+
/* Table cell-range selection: the painted rectangle, and the table that
|
|
3279
|
+
holds one (its caret and native selection are hidden while it stands). */
|
|
3280
|
+
.ed-cell-range { background: rgba(59, 130, 246, 0.18) !important; }
|
|
3281
|
+
.ed-cell-range-active { caret-color: transparent; }
|
|
3282
|
+
.ed-cell-range-active ::selection { background: transparent; }
|
|
3270
3283
|
.ed-te-menu {
|
|
3271
3284
|
position: fixed; z-index: 12;
|
|
3272
3285
|
display: flex; align-items: center; gap: 4px;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@helping-ai-workflow/md2doc",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.7.0",
|
|
4
4
|
"description": "Markdown → HTML / PDF renderer with WaveDrom, Mermaid, Graphviz, and draw.io support",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"markdown",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
},
|
|
46
46
|
"scripts": {
|
|
47
47
|
"preinstall": "node scripts/preinstall.js",
|
|
48
|
-
"test": "node test/md2doc.test.js && node test/blockmap.test.js && node test/drawio.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/selection.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/table-anchor-recovery.test.js && node test/gate-compat.test.js && node test/history.test.js && node test/list-md.test.js && node test/table-width.test.js && node test/toolbar-model.test.js && node test/docsource.test.js && node test/asset.test.js && node test/paste-md.test.js && node test/patchmap.test.js && node test/wave-codec.test.js && node test/wave-draw.test.js && node test/wave-geometry.test.js && node test/wave-store.test.js && node test/wave-rail-clearance.test.js && node test/editor-journey.test.js && node test/detach-census.test.js"
|
|
48
|
+
"test": "node test/md2doc.test.js && node test/blockmap.test.js && node test/drawio.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/selection.test.js && node test/cell-range.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/table-anchor-recovery.test.js && node test/gate-compat.test.js && node test/history.test.js && node test/list-md.test.js && node test/table-width.test.js && node test/toolbar-model.test.js && node test/docsource.test.js && node test/asset.test.js && node test/paste-md.test.js && node test/patchmap.test.js && node test/wave-codec.test.js && node test/wave-draw.test.js && node test/wave-geometry.test.js && node test/wave-store.test.js && node test/wave-rail-clearance.test.js && node test/editor-journey.test.js && node test/detach-census.test.js && node test/editor-arrow-nav.test.js && node test/editor-cell-range.test.js"
|
|
49
49
|
},
|
|
50
50
|
"repository": {
|
|
51
51
|
"type": "git",
|