@helping-ai-workflow/md2doc 3.12.0 → 3.13.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.
@@ -88,8 +88,11 @@ const PASTE_MD_SRC = fs.readFileSync(path.join(__dirname, 'paste-md.js'), 'utf8'
88
88
  //
89
89
  // wave-ui.js is UMD like its neighbours and needs no shim; in the page it
90
90
  // defines `window.md2docWaveUi` and takes the other five as parameters.
91
+ const WAVE_OVERFLOW_SRC = fs.readFileSync(path.join(__dirname, '..', 'wave-overflow.js'), 'utf8');
91
92
  const WAVE_CODEC_SRC = fs.readFileSync(path.join(__dirname, 'wave-codec.js'), 'utf8');
92
93
  const WAVE_GEOMETRY_SRC = fs.readFileSync(path.join(__dirname, 'wave-geometry.js'), 'utf8');
94
+ // wave-redesign P1 Task 4: the engine-drawn canvas (createCanvas), same shim.
95
+ const WAVE_CANVAS_SRC = fs.readFileSync(path.join(__dirname, 'wave-canvas.js'), 'utf8');
93
96
  const WAVE_STORE_SRC = fs.readFileSync(path.join(__dirname, 'wave-store.js'), 'utf8');
94
97
  const WAVE_DRAW_SRC = fs.readFileSync(path.join(__dirname, 'wave-draw.js'), 'utf8');
95
98
  const WAVE_PANELS_SRC = fs.readFileSync(path.join(__dirname, 'wave-panels.js'), 'utf8');
@@ -742,7 +745,9 @@ async function createEditorServer({
742
745
  `<script>${TURNDOWN_SRC}</script>\n` +
743
746
  `<script>${PASTE_MD_SRC}</script>\n` +
744
747
  waveModuleTag('wave-codec.js', WAVE_CODEC_SRC) +
748
+ waveModuleTag('wave-overflow.js', WAVE_OVERFLOW_SRC) +
745
749
  waveModuleTag('wave-geometry.js', WAVE_GEOMETRY_SRC) +
750
+ waveModuleTag('wave-canvas.js', WAVE_CANVAS_SRC) +
746
751
  waveModuleTag('wave-store.js', WAVE_STORE_SRC) +
747
752
  waveModuleTag('wave-draw.js', WAVE_DRAW_SRC) +
748
753
  waveModuleTag('wave-panels.js', WAVE_PANELS_SRC) +
@@ -0,0 +1,301 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * The wave editor's canvas, drawn by the WaveDrom engine itself
5
+ * (wave-redesign spec §3).
6
+ *
7
+ * The editor used to paint its own picture of the waveform, and four of the
8
+ * seven defects in spec §5 were that picture disagreeing with the reader's.
9
+ * Here the engine draws the canvas — the same call the old preview panel
10
+ * made — so what the editor shows IS the output. This module owns that call
11
+ * and the coordinate bridge on top of it; the interaction layer (selection,
12
+ * cursor, dots, handles) is drawn by the layers above, in client coordinates
13
+ * this hands out.
14
+ *
15
+ * Coordinates, three spaces:
16
+ * - engine units: the svg's viewBox (a cycle is 40, a lane 30). `layout`
17
+ * and `measured` speak these.
18
+ * - the svg is enlarged by `scale` (width/height multiplied, viewBox kept),
19
+ * so one engine unit is `scale` CSS px.
20
+ * - client: what pointer events carry. Converted by subtracting the svg's
21
+ * client rect and dividing by `scale`.
22
+ *
23
+ * UMD: in the edit page lib/editor/server.js wraps this as the
24
+ * `window.__md2docWave['wave-canvas.js']` module; every dependency (the
25
+ * document, the engine, the theme API, geometry, codec) is handed in.
26
+ */
27
+
28
+ /** How long a label can be beyond its segment: lib/wave-overflow.js `PAD`. */
29
+ const LABEL_PAD = 6;
30
+ /** The engine's cycle width in its own units (2 bricks of `lane.xs` 20). */
31
+ const CYCLE_UNITS = 40;
32
+
33
+ /**
34
+ * A plain, unfrozen copy of a document: the store's documents are frozen and
35
+ * the engine writes into the source it is given. Same bound as wave-panels.js
36
+ * `plainCopy`, which this mirrors.
37
+ */
38
+ function plainCopy(value, depth) {
39
+ if (depth > 64 || value === null || typeof value !== 'object') return value;
40
+ if (Array.isArray(value)) {
41
+ const out = [];
42
+ for (let i = 0; i < value.length; i++) out.push(plainCopy(value[i], depth + 1));
43
+ return out;
44
+ }
45
+ const out = {};
46
+ for (const key of Object.keys(value)) out[key] = plainCopy(value[key], depth + 1);
47
+ return out;
48
+ }
49
+
50
+ function createCanvas(opts) {
51
+ const d = opts.d;
52
+ const engine = opts.engine;
53
+ const theme = opts.theme || null;
54
+ const geometry = opts.geometry;
55
+ const codec = opts.codec;
56
+ const host = opts.host;
57
+ // Not 0: the document's own diagrams render at 0, and the ids the engine
58
+ // derives from the index would collide (wave-ui.js PREVIEW_INDEX explains
59
+ // the measurement). The prefix avoids `WaveDrom_Display_`, which the
60
+ // reader's own scripts (lightbox, recolour observer) treat as theirs.
61
+ const index = opts.index === undefined ? 9000 : opts.index;
62
+ const prefix = opts.prefix === undefined ? 'md2doc-wave-preview-' : opts.prefix;
63
+ const scale = opts.scale === undefined ? 1.5 : opts.scale;
64
+
65
+ const target = d.createElement('div');
66
+ target.id = prefix + index;
67
+ host.appendChild(target);
68
+
69
+ let svg = null;
70
+ let layout = null;
71
+ let measured = null;
72
+ let lastDoc = null;
73
+ let destroyed = false;
74
+
75
+ /**
76
+ * The canvas background the label backings take in dark (spec §5-5): the
77
+ * first ancestor of the host, the host included, whose computed background
78
+ * is not transparent.
79
+ */
80
+ function backing() {
81
+ for (let el = host; el && el.nodeType === 1; el = el.parentElement) {
82
+ const bg = getComputedStyle(el).backgroundColor;
83
+ if (bg && bg !== 'transparent' && !/^rgba\(.*,\s*0\)$/.test(bg)) return bg;
84
+ }
85
+ return undefined;
86
+ }
87
+
88
+ function recolour(dark) {
89
+ if (theme === null || svg === null) return;
90
+ theme.recolourSvg(svg, dark, { backing: backing() });
91
+ }
92
+
93
+ /**
94
+ * Whether a WaveDrom skin is on the page OUTSIDE this canvas. The engine
95
+ * empties the target before drawing, so a skin this canvas carried itself
96
+ * would be gone by the time the new svg looked for it. `socket` is asked
97
+ * for inside a WaveDrom svg's defs, not anywhere: a document's inline HTML
98
+ * may carry its own `id="socket"` (the same test the WaveDrom preview
99
+ * panel the redesign retired used to make).
100
+ */
101
+ function skinElsewhere() {
102
+ const found = d.querySelectorAll('svg.WaveDrom defs #socket');
103
+ for (let i = 0; i < found.length; i++) if (!target.contains(found[i])) return true;
104
+ return false;
105
+ }
106
+
107
+ function render(doc) {
108
+ if (engine === null || engine === undefined || typeof engine.RenderWaveForm !== 'function') {
109
+ return { ok: false, error: '這一頁沒有載入 WaveDrom' };
110
+ }
111
+ if (!target.isConnected) return { ok: false, error: '畫布還沒放進頁面' };
112
+ try {
113
+ engine.RenderWaveForm(index, plainCopy(doc, 0), prefix, skinElsewhere());
114
+ } catch (e) {
115
+ svg = null; layout = null; measured = null;
116
+ return { ok: false, error: 'WaveDrom 畫不出這份文件:' + String(e && e.message) };
117
+ }
118
+ svg = target.querySelector('svg');
119
+ if (svg === null) {
120
+ layout = null; measured = null;
121
+ return { ok: false, error: 'WaveDrom 沒有畫出圖' };
122
+ }
123
+ const w = Number(svg.getAttribute('width'));
124
+ const h = Number(svg.getAttribute('height'));
125
+ svg.setAttribute('width', String(w * scale));
126
+ svg.setAttribute('height', String(h * scale));
127
+ if (theme !== null && theme.isDark()) recolour(true);
128
+ lastDoc = doc;
129
+ measured = geometry.measureEngineSvg(svg, doc);
130
+ layout = geometry.layoutOf(doc, {
131
+ laneHeight: measured.laneHeight,
132
+ cycleWidth: CYCLE_UNITS,
133
+ nameColWidth: measured.xg + 0.5,
134
+ originY: measured.laneTops.length > 0 ? measured.laneTops[0] : measured.originY,
135
+ });
136
+ return { ok: true };
137
+ }
138
+
139
+ function origin() {
140
+ const r = svg.getBoundingClientRect();
141
+ return { left: r.left, top: r.top };
142
+ }
143
+
144
+ function toEngine(clientX, clientY) {
145
+ const o = origin();
146
+ return { x: (clientX - o.left) / scale, y: (clientY - o.top) / scale };
147
+ }
148
+
149
+ function cellAt(clientX, clientY) {
150
+ if (layout === null) return null;
151
+ const p = toEngine(clientX, clientY);
152
+ return geometry.cellAt(layout, p.x, p.y);
153
+ }
154
+
155
+ function boundaryAt(clientX, clientY) {
156
+ if (layout === null) return null;
157
+ const p = toEngine(clientX, clientY);
158
+ return geometry.boundaryAt(layout, p.x, p.y);
159
+ }
160
+
161
+ function cellRectClient(laneIndex, cycle) {
162
+ if (layout === null) return null;
163
+ const r = geometry.cellRect(layout, laneIndex, cycle);
164
+ if (r === null) return null;
165
+ const o = origin();
166
+ return { left: o.left + r.x * scale, top: o.top + r.y * scale,
167
+ width: r.width * scale, height: r.height * scale };
168
+ }
169
+
170
+ function anchorClient(laneIndex, cell) {
171
+ if (layout === null) return null;
172
+ const a = geometry.anchorOfCell(layout, laneIndex, cell);
173
+ if (a === null) return null;
174
+ const o = origin();
175
+ return { x: o.left + a.x * scale, y: o.top + a.y * scale };
176
+ }
177
+
178
+ function labelBoxes() {
179
+ if (measured === null) return [];
180
+ const o = origin();
181
+ return measured.labels.map(function (l) {
182
+ return {
183
+ laneIndex: l.laneIndex, slot: l.slot, text: l.text,
184
+ left: o.left + (l.x - l.width / 2) * scale, top: o.top + l.y * scale,
185
+ width: l.width * scale, height: l.height * scale,
186
+ };
187
+ });
188
+ }
189
+
190
+ /**
191
+ * Labels wider than their segment, by the width the engine really drew —
192
+ * the same rule as lib/wave-overflow.js `overflowOf` (label + LABEL_PAD
193
+ * against the segment's `cells * 40 * period`, times hscale), which can
194
+ * only estimate the width.
195
+ */
196
+ function overflow() {
197
+ if (measured === null || layout === null) return [];
198
+ const out = [];
199
+ for (const l of measured.labels) {
200
+ if (l.slot === null || l.cells === null) continue;
201
+ const row = layout.lanes[l.laneIndex];
202
+ const lane = row && row.lane && typeof row.lane === 'object' ? row.lane : {};
203
+ const p = Number(lane.period);
204
+ const unit = l.cells * CYCLE_UNITS * (p > 0 ? p : 1);
205
+ const need = l.width + LABEL_PAD;
206
+ if (need <= unit * layout.hscale) continue;
207
+ out.push({ laneIndex: l.laneIndex, slot: l.slot, label: l.text, needScale: Math.ceil(need / unit) });
208
+ }
209
+ return out;
210
+ }
211
+
212
+ /**
213
+ * The engine's own drawing of every `edge` entry it drew (Task 9): `[{index,
214
+ * path, label}]`, `index` the entry's position in `doc.edge`, `path` the
215
+ * engine's `gmark_<from>_<to>` path, `label` the `g` holding its label box
216
+ * or null. The engine writes them into `wavearcs_<index>` in entry order,
217
+ * each path followed by its label when the entry has one, and skips an
218
+ * entry when either letter names no anchor (render-arcs.js `archer`), so
219
+ * the walk below repeats exactly that test: the same split of the entry
220
+ * (`trim().split(/\s+/)`, the label taken from the UNtrimmed string) and
221
+ * the same anchor set (every non-`.` character of every lane's `node`).
222
+ * The lowercase letter labels the engine adds after the last edge are
223
+ * never reached. If a drawn element is not the one expected (an id that
224
+ * does not name the entry's letters), the walk stops there rather than
225
+ * pair an entry with another entry's drawing.
226
+ */
227
+ function arcs() {
228
+ if (svg === null || lastDoc === null || !Array.isArray(lastDoc.edge)) return [];
229
+ const group = svg.querySelector('g[id^="wavearcs_"]');
230
+ if (group === null) return [];
231
+ const events = new Set();
232
+ for (const p of codec.lanePaths(lastDoc)) {
233
+ const lane = geometry.valueAt(lastDoc, p);
234
+ const node = lane !== null && typeof lane === 'object' && typeof lane.node === 'string' ? lane.node : '';
235
+ for (const ch of node) if (ch !== '.') events.add(ch);
236
+ }
237
+ const kids = [];
238
+ for (let n = group.firstChild; n !== null; n = n.nextSibling) if (n.nodeType === 1) kids.push(n);
239
+ const out = [];
240
+ let k = 0;
241
+ for (let i = 0; i < lastDoc.edge.length; i++) {
242
+ const entry = lastDoc.edge[i];
243
+ if (typeof entry !== 'string') return out;
244
+ const head = entry.trim().split(/\s+/)[0];
245
+ const from = head.substr(0, 1);
246
+ const to = head.substr(-1, 1);
247
+ if (!events.has(from) || !events.has(to)) continue;
248
+ const path = kids[k];
249
+ if (path === undefined || path.tagName.toLowerCase() !== 'path' ||
250
+ path.getAttribute('id') !== 'gmark_' + from + '_' + to) return out;
251
+ k++;
252
+ let label = null;
253
+ if (entry.substring(head.length).substring(1) !== '') {
254
+ label = kids[k];
255
+ if (label === undefined || label.tagName.toLowerCase() !== 'g') return out;
256
+ k++;
257
+ }
258
+ out.push({ index: i, path: path, label: label });
259
+ }
260
+ return out;
261
+ }
262
+
263
+ // `onTheme` returns the unsubscribe (lib/theme/runtime.js); `destroy()`
264
+ // calls it, so a closed canvas is not kept alive by the theme's listener
265
+ // list — that listener reaches the host, and through it the whole editor
266
+ // overlay and its store, for the life of the page (final review M3).
267
+ let unsubscribe = null;
268
+ if (theme !== null && typeof theme.onTheme === 'function') {
269
+ const off = theme.onTheme(function (dark) { if (!destroyed) recolour(dark); });
270
+ if (typeof off === 'function') unsubscribe = off;
271
+ }
272
+
273
+ /** Stops following the theme and takes the canvas out of the host. */
274
+ function destroy() {
275
+ destroyed = true;
276
+ if (unsubscribe !== null) { unsubscribe(); unsubscribe = null; }
277
+ if (target.parentNode) target.parentNode.removeChild(target);
278
+ svg = null; layout = null; measured = null; lastDoc = null;
279
+ }
280
+
281
+ return {
282
+ render: render,
283
+ get svg() { return svg; },
284
+ get layout() { return layout; },
285
+ get measured() { return measured; },
286
+ get doc() { return lastDoc; },
287
+ scale: scale,
288
+ cellAt: cellAt,
289
+ boundaryAt: boundaryAt,
290
+ cellRectClient: cellRectClient,
291
+ anchorClient: anchorClient,
292
+ labelBoxes: labelBoxes,
293
+ overflow: overflow,
294
+ arcs: arcs,
295
+ destroy: destroy,
296
+ };
297
+ }
298
+
299
+ const api = { createCanvas: createCanvas, plainCopy: plainCopy };
300
+ if (typeof module === 'object' && module.exports) module.exports = api;
301
+ else if (typeof window !== 'undefined') window.md2docWaveCanvas = api;