@helping-ai-workflow/md2doc 3.5.0 → 3.6.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/client.js +44 -5
- package/lib/editor/server.js +21 -2
- package/lib/editor/wave-codec.js +160 -3
- package/lib/editor/wave-draw.js +1470 -0
- package/lib/editor/wave-geometry.js +469 -19
- package/lib/editor/wave-panels.js +1102 -0
- package/lib/editor/wave-store.js +36 -1
- package/lib/editor/wave-ui.js +1807 -1802
- package/lib/md2doc.js +520 -50
- package/package.json +2 -2
package/lib/editor/client.js
CHANGED
|
@@ -17542,7 +17542,8 @@
|
|
|
17542
17542
|
// An older cached page that predates the injection must degrade to "the
|
|
17543
17543
|
// button does nothing visible", never to a throw out of a click handler.
|
|
17544
17544
|
if (!ui || !mods || !mods['wave-store.js'] || !mods['wave-codec.js'] ||
|
|
17545
|
-
!mods['wave-geometry.js']
|
|
17545
|
+
!mods['wave-geometry.js'] || !mods['wave-draw.js'] ||
|
|
17546
|
+
!mods['wave-panels.js']) return;
|
|
17546
17547
|
if (waveEditor !== null) return;
|
|
17547
17548
|
hideWaveEditBtn();
|
|
17548
17549
|
|
|
@@ -17603,8 +17604,27 @@
|
|
|
17603
17604
|
document: document,
|
|
17604
17605
|
host: document.body,
|
|
17605
17606
|
source: body.text,
|
|
17607
|
+
// v3.6.0 Task 15: which block in this document the editor is over,
|
|
17608
|
+
// for the 匯出 filename (`exportName()` in wave-ui.js). The editor
|
|
17609
|
+
// cannot derive it — it is handed one block's TEXT and nothing about
|
|
17610
|
+
// where that text came from — and two diagrams in one document
|
|
17611
|
+
// exporting under the same filename is a silent overwrite in the
|
|
17612
|
+
// user's downloads folder.
|
|
17613
|
+
blockIndex: id,
|
|
17614
|
+
// …and the document's own name for the other half of that filename.
|
|
17615
|
+
// `baseTitle`, NOT `document.title`: this file owns the page title
|
|
17616
|
+
// while a session is open and writes the dirty marker into it
|
|
17617
|
+
// (`(documentIsDirty() ? '● ' : '') + baseTitle`, above). Handing over
|
|
17618
|
+
// the decorated title would land a user who has actually edited with
|
|
17619
|
+
// `●-doc-wave-1.svg`, and a user who has not with the clean name —
|
|
17620
|
+
// i.e. it would look right in exactly the case nobody exports in.
|
|
17621
|
+
// `baseTitle` is the title as it was before this file touched it,
|
|
17622
|
+
// which is md2doc's `path.basename(src, '.md')`.
|
|
17623
|
+
fileLabel: baseTitle,
|
|
17606
17624
|
codec: mods['wave-codec.js'],
|
|
17607
17625
|
geometry: mods['wave-geometry.js'],
|
|
17626
|
+
draw: mods['wave-draw.js'],
|
|
17627
|
+
panels: mods['wave-panels.js'],
|
|
17608
17628
|
createStore: mods['wave-store.js'].createStore,
|
|
17609
17629
|
// The store's own answer to「哪一條 lane 是哪一條」across one operation,
|
|
17610
17630
|
// handed over rather than re-derived: the editor's cell cursor and its
|
|
@@ -17760,10 +17780,29 @@
|
|
|
17760
17780
|
|
|
17761
17781
|
/** One refusal, recorded and raised. The only producer of `waveUnwrittenEdit`. */
|
|
17762
17782
|
function waveRefuseGesture(seam, reason) {
|
|
17763
|
-
// A refusal is not papered over and not rolled back.
|
|
17764
|
-
//
|
|
17765
|
-
//
|
|
17766
|
-
//
|
|
17783
|
+
// A refusal is not papered over and not rolled back. This raises it as a
|
|
17784
|
+
// banner naming the lines it could not write, and waits for the user to
|
|
17785
|
+
// acknowledge it.
|
|
17786
|
+
//
|
|
17787
|
+
// v3.6.0 Task 16 fix round 2 (N1) withdraws the reason that used to stand
|
|
17788
|
+
// here — "wave-ui has already said so on the status line, which is where
|
|
17789
|
+
// the user's eyes are, but a status line is not a decision". wave-ui does
|
|
17790
|
+
// write the reason there (`afterStoreMoved`'s
|
|
17791
|
+
// `這個改動沒辦法只改幾個位元組寫回去:…`), but that line is ONE
|
|
17792
|
+
// `role="status"` string and the next thing the same gesture says
|
|
17793
|
+
// replaces it. MEASURED on the journey fixture: the fourth Alt+↑ of the
|
|
17794
|
+
// Task 16 scenario refuses — "the value at [\"signal\"] is or contains an
|
|
17795
|
+
// object, so replacing it would re-serialise part of the document's
|
|
17796
|
+
// structure…" — and `moveSelectedLane`'s landing line
|
|
17797
|
+
// (`搬動的 lane 落在群組「bus」裡`) is written straight after it, so the
|
|
17798
|
+
// sentence the user is left looking at reads like success.
|
|
17799
|
+
//
|
|
17800
|
+
// That is a FAMILY, not one caller: `addLaneFromToolbar` and 複製 have
|
|
17801
|
+
// written over the same refusal since before Task 16, and the rail's drag
|
|
17802
|
+
// drop announces nothing at all. Re-ordering those says is behaviour, and
|
|
17803
|
+
// it is parked as a next-version follow-up rather than done here. So the
|
|
17804
|
+
// banner below is not a nicety layered on a status line that already said
|
|
17805
|
+
// it — it is the only part of the refusal that cannot be clobbered.
|
|
17767
17806
|
seam.unwritten = true;
|
|
17768
17807
|
waveUnwrittenEdit = true;
|
|
17769
17808
|
setDirty();
|
package/lib/editor/server.js
CHANGED
|
@@ -56,7 +56,7 @@ const TURNDOWN_SRC = fs.readFileSync(
|
|
|
56
56
|
require.resolve('turndown/lib/turndown.browser.umd.js'), 'utf8');
|
|
57
57
|
const PASTE_MD_SRC = fs.readFileSync(path.join(__dirname, 'paste-md.js'), 'utf8');
|
|
58
58
|
|
|
59
|
-
// v3.4.0 batch3 Task 6: the wavedrom GUI's
|
|
59
|
+
// v3.4.0 batch3 Task 6: the wavedrom GUI's modules.
|
|
60
60
|
//
|
|
61
61
|
// wave-codec.js / wave-geometry.js / wave-store.js are plain CommonJS ON
|
|
62
62
|
// PURPOSE, and rewriting them into the UMD header every other module here uses
|
|
@@ -67,11 +67,28 @@ const PASTE_MD_SRC = fs.readFileSync(path.join(__dirname, 'paste-md.js'), 'utf8'
|
|
|
67
67
|
// the browser gets them through the small CommonJS shim below instead —
|
|
68
68
|
// the module text itself is inlined byte for byte.
|
|
69
69
|
//
|
|
70
|
+
// v3.6.0 Task 1: wave-draw.js joins them, plain CommonJS too — a pure
|
|
71
|
+
// extraction out of wave-ui.js, not test-guarded like the other three, but
|
|
72
|
+
// needing the identical shim for a different reason: wave-ui.js itself is
|
|
73
|
+
// injected as a bare <script> with no `require()` of its own (see its own
|
|
74
|
+
// header comment), so it cannot resolve `require('./wave-draw.js')` in the
|
|
75
|
+
// browser any more than the page around it could. `createWaveEditor()` takes
|
|
76
|
+
// it in as `opts.draw` instead, the same way it already takes `opts.codec`
|
|
77
|
+
// and `opts.geometry`.
|
|
78
|
+
//
|
|
79
|
+
// v3.6.0 Task 2: wave-panels.js joins them for the identical reason —
|
|
80
|
+
// another pure extraction out of wave-ui.js (the toolbar, the right-hand
|
|
81
|
+
// panels and the lane rail this time), needing the same shim because
|
|
82
|
+
// wave-ui.js still cannot `require()` anything in the browser. Taken in as
|
|
83
|
+
// `opts.panels`.
|
|
84
|
+
//
|
|
70
85
|
// wave-ui.js is UMD like its neighbours and needs no shim; in the page it
|
|
71
|
-
// defines `window.md2docWaveUi` and takes the other
|
|
86
|
+
// defines `window.md2docWaveUi` and takes the other five as parameters.
|
|
72
87
|
const WAVE_CODEC_SRC = fs.readFileSync(path.join(__dirname, 'wave-codec.js'), 'utf8');
|
|
73
88
|
const WAVE_GEOMETRY_SRC = fs.readFileSync(path.join(__dirname, 'wave-geometry.js'), 'utf8');
|
|
74
89
|
const WAVE_STORE_SRC = fs.readFileSync(path.join(__dirname, 'wave-store.js'), 'utf8');
|
|
90
|
+
const WAVE_DRAW_SRC = fs.readFileSync(path.join(__dirname, 'wave-draw.js'), 'utf8');
|
|
91
|
+
const WAVE_PANELS_SRC = fs.readFileSync(path.join(__dirname, 'wave-panels.js'), 'utf8');
|
|
75
92
|
const WAVE_UI_SRC = fs.readFileSync(path.join(__dirname, 'wave-ui.js'), 'utf8');
|
|
76
93
|
|
|
77
94
|
// One CommonJS module, wrapped so a plain <script> can carry it.
|
|
@@ -585,6 +602,8 @@ async function createEditorServer({
|
|
|
585
602
|
waveModuleTag('wave-codec.js', WAVE_CODEC_SRC) +
|
|
586
603
|
waveModuleTag('wave-geometry.js', WAVE_GEOMETRY_SRC) +
|
|
587
604
|
waveModuleTag('wave-store.js', WAVE_STORE_SRC) +
|
|
605
|
+
waveModuleTag('wave-draw.js', WAVE_DRAW_SRC) +
|
|
606
|
+
waveModuleTag('wave-panels.js', WAVE_PANELS_SRC) +
|
|
588
607
|
`<script>${WAVE_UI_SRC}</script>\n` +
|
|
589
608
|
`<script>${clientJs}</script>\n`;
|
|
590
609
|
// Splice at the LAST "</body>" — the document's real closing tag.
|
package/lib/editor/wave-codec.js
CHANGED
|
@@ -602,6 +602,10 @@ function resolveEdit(ctx, edit) {
|
|
|
602
602
|
const op = edit.op === undefined ? 'set' : edit.op;
|
|
603
603
|
if (op === 'set') return resolveSet(ctx, edit);
|
|
604
604
|
if (op === 'insert') return resolveInsert(ctx, edit);
|
|
605
|
+
// v3.6.0 Task 13 fix round 1: several new members on one object, in a
|
|
606
|
+
// single local edit — see `wave-store.js`'s `diffMembers` for why one
|
|
607
|
+
// `insert` per key is not safe once a session can add more than one.
|
|
608
|
+
if (op === 'insert-members') return resolveInsertMembers(ctx, edit);
|
|
605
609
|
if (op === 'remove') return resolveRemove(ctx, edit);
|
|
606
610
|
if (op === 'move') return resolveMove(ctx, edit);
|
|
607
611
|
if (op === 'replace') return resolveReplace(ctx, edit);
|
|
@@ -900,6 +904,81 @@ function resolveInsertMember(ctx, edit) {
|
|
|
900
904
|
return appendAfterMember(ctx, path, last, body);
|
|
901
905
|
}
|
|
902
906
|
|
|
907
|
+
/**
|
|
908
|
+
* v3.6.0 Task 13 fix round 1 (Critical 1): `resolveInsertMember` above,
|
|
909
|
+
* generalised to SEVERAL new keys on the same object in one edit —
|
|
910
|
+
* `edit = {op: 'insert-members', path: <parent path>, entries: [[key,
|
|
911
|
+
* value], …]}`, produced by `wave-store.js`'s `diffMembers` whenever a
|
|
912
|
+
* session added more than one new field to the same object. See that
|
|
913
|
+
* function's own comment for why one `insert` per key is not safe once
|
|
914
|
+
* there can be more than one: every one of them would anchor at the SAME
|
|
915
|
+
* "object's last existing member" and `patchSource`'s "two edits land at
|
|
916
|
+
* the same place" guard would refuse the batch.
|
|
917
|
+
*
|
|
918
|
+
* This resolves to exactly ONE edit at exactly ONE anchor — the object's
|
|
919
|
+
* last existing member, same as `resolveInsertMember` — with every new
|
|
920
|
+
* key's `key: value` joined into one body. Each entry gets its own
|
|
921
|
+
* duplicate-key and serialization checks, same as `resolveInsertMember`
|
|
922
|
+
* gives its one key, so a batch that only has ONE new key refuses (or
|
|
923
|
+
* succeeds) identically either way; `diffMembers` never actually calls this
|
|
924
|
+
* with fewer than two entries, but nothing here depends on that.
|
|
925
|
+
*/
|
|
926
|
+
function resolveInsertMembers(ctx, edit) {
|
|
927
|
+
const path = edit.path;
|
|
928
|
+
const parentSpan = ctx.parsed.spans.get(JSON.stringify(path));
|
|
929
|
+
const parent = lookup(ctx.parsed.doc, path);
|
|
930
|
+
if (!parentSpan || !parent.found || parent.value === null ||
|
|
931
|
+
typeof parent.value !== 'object' || Array.isArray(parent.value)) {
|
|
932
|
+
return refuse('insert-members needs an object at ' + JSON.stringify(path),
|
|
933
|
+
nearestRange(ctx, path));
|
|
934
|
+
}
|
|
935
|
+
if (touchesDuplicateKey(ctx, path)) {
|
|
936
|
+
return refuse(duplicateWhy(path), rangeOf(path, path, parentSpan));
|
|
937
|
+
}
|
|
938
|
+
if (!Array.isArray(edit.entries) || edit.entries.length === 0) {
|
|
939
|
+
return refuse('insert-members needs at least one [key, value] entry',
|
|
940
|
+
rangeOf(path, path, parentSpan));
|
|
941
|
+
}
|
|
942
|
+
const parts = [];
|
|
943
|
+
for (const entry of edit.entries) {
|
|
944
|
+
const key = entry[0];
|
|
945
|
+
const value = entry[1];
|
|
946
|
+
const memberPath = path.concat([key]);
|
|
947
|
+
if (Object.prototype.hasOwnProperty.call(parent.value, key)) {
|
|
948
|
+
return refuse(JSON.stringify(memberPath) + ' already has a member there, so this is a ' +
|
|
949
|
+
'set rather than an insert', rangeOf(memberPath, path, parentSpan));
|
|
950
|
+
}
|
|
951
|
+
const ser = serialize(value, dominantQuote(ctx), MAX_DEPTH - path.length - 1);
|
|
952
|
+
if (ser.ok !== true) return refuse(ser.why, rangeOf(memberPath, path, parentSpan));
|
|
953
|
+
parts.push(renderKey(key, '"') + ': ' + ser.text);
|
|
954
|
+
}
|
|
955
|
+
const keys = Object.keys(parent.value);
|
|
956
|
+
if (keys.length === 0) {
|
|
957
|
+
const at = parentSpan[0] + 1;
|
|
958
|
+
const close = lineIndentBefore(ctx.text, parentSpan[1] - 1);
|
|
959
|
+
if (close !== null) {
|
|
960
|
+
const eol = lineBreakBefore(ctx.text, parentSpan[1] - 1);
|
|
961
|
+
return { ok: true, path: path, start: at, end: at, text: eol + close + ' ' + parts.join(', ') };
|
|
962
|
+
}
|
|
963
|
+
return { ok: true, path: path, start: at, end: at, text: parts.join(', ') };
|
|
964
|
+
}
|
|
965
|
+
const lastPath = path.concat([keys[keys.length - 1]]);
|
|
966
|
+
const last = ctx.parsed.members.get(JSON.stringify(lastPath));
|
|
967
|
+
if (!last) {
|
|
968
|
+
return refuse('no member span for ' + JSON.stringify(lastPath),
|
|
969
|
+
rangeOf(path, path, parentSpan));
|
|
970
|
+
}
|
|
971
|
+
// The same comma-plus-gap `appendAfterMember` puts between what was
|
|
972
|
+
// already there and this edit's own body is used AGAIN here, between the
|
|
973
|
+
// new members joined into that one body — the indent/line-break come from
|
|
974
|
+
// the same anchor (`last[0]`) either way, so a multi-key insert reads on
|
|
975
|
+
// the page exactly like the same keys inserted one at a time would have,
|
|
976
|
+
// had that been safe to do.
|
|
977
|
+
const indent = lineIndentBefore(ctx.text, last[0]);
|
|
978
|
+
const gap = indent === null ? ' ' : lineBreakBefore(ctx.text, last[0]) + indent;
|
|
979
|
+
return appendAfterMember(ctx, path, last, parts.join(',' + gap));
|
|
980
|
+
}
|
|
981
|
+
|
|
903
982
|
/**
|
|
904
983
|
* Move one array element, bytes and all.
|
|
905
984
|
*
|
|
@@ -2838,8 +2917,10 @@ function groupLanes(doc, ats, label) {
|
|
|
2838
2917
|
* ungrouping always names one group unambiguously. It also means a group made
|
|
2839
2918
|
* of nothing but sub-groups — no lane of its own anywhere in it — cannot be
|
|
2840
2919
|
* named this way; there is no lane whose immediate parent IS that group. The
|
|
2841
|
-
* UI does not offer the control there for the same reason
|
|
2842
|
-
*
|
|
2920
|
+
* UI does not offer the control there for the same reason: v3.6.0 Task 14's
|
|
2921
|
+
* 解散群組 button is enabled only when the SELECTED lane is a direct member
|
|
2922
|
+
* of some group (`laneIsInGroup` in `wave-ui.js`), and a group made only of
|
|
2923
|
+
* sub-groups has no such lane to select.
|
|
2843
2924
|
*
|
|
2844
2925
|
* `at` outside the document, or a lane that is not inside any group (`signal`
|
|
2845
2926
|
* is not one), answers the document unchanged.
|
|
@@ -2919,6 +3000,82 @@ function setBannerField(doc, which, key, value) {
|
|
|
2919
3000
|
return next;
|
|
2920
3001
|
}
|
|
2921
3002
|
|
|
3003
|
+
// ── v3.6.0 Task 13: doc.config.<key> / a lane's own <key> ─────────────────
|
|
3004
|
+
|
|
3005
|
+
/**
|
|
3006
|
+
* Write, or clear, one field of `doc.config` — today only `hscale`, the
|
|
3007
|
+
* document-wide field the right-hand 文件 panel's ninth input exposes.
|
|
3008
|
+
*
|
|
3009
|
+
* Same clear-deletes-key discipline as `setBannerField` just above, and for
|
|
3010
|
+
* the identical reason: `value === undefined` deletes the key (and `config`
|
|
3011
|
+
* itself, if that empties it) instead of leaving `''` behind. `undefined`
|
|
3012
|
+
* rather than `''` is the delete sentinel here — unlike the six banner
|
|
3013
|
+
* fields, this field is meant to hold a NUMBER
|
|
3014
|
+
* (`wave-geometry.hscaleOf` gates on `typeof cfg.hscale === 'number'`), and
|
|
3015
|
+
* the caller (`wave-panels.js`'s `commitConfigField`) already turns a
|
|
3016
|
+
* well-formed numeral into a real `number` before this ever runs — an empty
|
|
3017
|
+
* string would be a legitimate thing to want to write if `''` meant
|
|
3018
|
+
* "clear", so the sentinel is moved to a value JSON can never actually hold.
|
|
3019
|
+
*
|
|
3020
|
+
* Returns the SAME `doc` (`===`) when nothing changes, like every other op
|
|
3021
|
+
* in this file.
|
|
3022
|
+
*/
|
|
3023
|
+
function setConfigField(doc, key, value) {
|
|
3024
|
+
if (doc === null || typeof doc !== 'object') return doc;
|
|
3025
|
+
if (typeof key !== 'string' || key === '') return doc;
|
|
3026
|
+
|
|
3027
|
+
const had = doc.config;
|
|
3028
|
+
const hadObj = (had !== null && typeof had === 'object') ? had : null;
|
|
3029
|
+
|
|
3030
|
+
if (value === undefined) {
|
|
3031
|
+
if (hadObj === null || hadObj[key] === undefined) return doc;
|
|
3032
|
+
const nextObj = copyObject(hadObj);
|
|
3033
|
+
delete nextObj[key];
|
|
3034
|
+
const next = copyObject(doc);
|
|
3035
|
+
if (Object.keys(nextObj).length === 0) delete next.config;
|
|
3036
|
+
else next.config = nextObj;
|
|
3037
|
+
return next;
|
|
3038
|
+
}
|
|
3039
|
+
|
|
3040
|
+
if (hadObj !== null && hadObj[key] === value) return doc;
|
|
3041
|
+
const nextObj = copyObject(hadObj || {});
|
|
3042
|
+
nextObj[key] = value;
|
|
3043
|
+
const next = copyObject(doc);
|
|
3044
|
+
next.config = nextObj;
|
|
3045
|
+
return next;
|
|
3046
|
+
}
|
|
3047
|
+
|
|
3048
|
+
/**
|
|
3049
|
+
* Write, or clear, one field of the lane at display position `at` — today
|
|
3050
|
+
* `period`/`phase`, the two fields the right-hand Lane panel exposes for
|
|
3051
|
+
* whichever lane the cycle selection currently names.
|
|
3052
|
+
*
|
|
3053
|
+
* Built on `mapLane` (the same bridge `renameLane` above uses), so this
|
|
3054
|
+
* inherits its refusal for good measure: an out-of-range `at` hands the same
|
|
3055
|
+
* `doc` back rather than throwing. `value === undefined` deletes the key,
|
|
3056
|
+
* same sentinel and same reason as `setConfigField` just above —
|
|
3057
|
+
* `wave-geometry.layoutOf` also gates `period`/`phase` on `typeof … ===
|
|
3058
|
+
* 'number'`, so the caller commits a real `number` for a well-formed
|
|
3059
|
+
* numeral, and `''` stays available as an ordinary (if odd) string value
|
|
3060
|
+
* rather than being overloaded to mean "clear".
|
|
3061
|
+
*/
|
|
3062
|
+
function setLaneField(doc, at, key, value) {
|
|
3063
|
+
if (key !== 'period' && key !== 'phase') return doc;
|
|
3064
|
+
return mapLane(doc, at, function (lane) {
|
|
3065
|
+
const laneObj = isLane(lane) ? lane : {};
|
|
3066
|
+
if (value === undefined) {
|
|
3067
|
+
if (laneObj[key] === undefined) return lane;
|
|
3068
|
+
const next = copyObject(laneObj);
|
|
3069
|
+
delete next[key];
|
|
3070
|
+
return next;
|
|
3071
|
+
}
|
|
3072
|
+
if (laneObj[key] === value) return lane;
|
|
3073
|
+
const next = copyObject(laneObj);
|
|
3074
|
+
next[key] = value;
|
|
3075
|
+
return next;
|
|
3076
|
+
});
|
|
3077
|
+
}
|
|
3078
|
+
|
|
2922
3079
|
// ── v3.5.0 Task 1: edge 字串 ↔ 物件 ────────────────────────────────────────
|
|
2923
3080
|
// 形狀清單與切字串的方式都是從 pin 住的 wavedrom 3.5.0 原始碼量到的
|
|
2924
3081
|
// (node_modules/wavedrom/lib/arc-shape.js 的 case 清單、
|
|
@@ -3240,7 +3397,7 @@ module.exports = {
|
|
|
3240
3397
|
dataSlotOf, setDataAt,
|
|
3241
3398
|
addLane, removeLane, moveLane, duplicateLane, renameLane, groupLanes, ungroupLanes,
|
|
3242
3399
|
lanePath, lanePaths, laneInsertPath, laneRemovePath, laneRemovePaths,
|
|
3243
|
-
setBannerField,
|
|
3400
|
+
setBannerField, setConfigField, setLaneField,
|
|
3244
3401
|
parseEdge, formatEdge, EDGE_SHAPES,
|
|
3245
3402
|
nodesOf, ensureNode, pruneNodes,
|
|
3246
3403
|
addEdge, updateEdge, removeEdge, moveEdgeEnd,
|