@helping-ai-workflow/md2doc 3.12.1 → 3.13.1

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.
@@ -47,6 +47,50 @@
47
47
  return out;
48
48
  }
49
49
 
50
+ // v3.13.1 T2 (F-1): text a person typed at the start of a paragraph or
51
+ // list-item line that a renderer would read as a block marker. Measured
52
+ // before this: typing `## `, `- `, `> ` or `1. ` at a paragraph start saved
53
+ // the line unescaped, and the next render silently turned the paragraph
54
+ // into a heading, list or quote.
55
+ //
56
+ // Input is escapeText() output, possibly several lines (a hard break's
57
+ // backslash-newline, or a lazy continuation). Each line, after up to three
58
+ // spaces of indent, gets the FIRST matching rule below; nothing else is
59
+ // touched. The list is the decision:
60
+ // ATX heading `#`..`######` then space/tab/end -> `\#…`
61
+ // bullet `-` or `+` then space/tab/end -> `\-…` / `\+…`
62
+ // blockquote `>` (no space needed) -> `\>…`
63
+ // ordered 1-9 digits, `.` or `)`, space/end -> `1\.` / `1\)`
64
+ // setext/rule a run of `=` or of `-`, alone -> `\===` / `\---`
65
+ // spaced rule 3+ `-` with spaces between, alone -> `\-- -`
66
+ // table row `|` -> `\|…`
67
+ // `*`, `_`, backtick, `~`, `<` and `[` never get here unescaped
68
+ // (escapeText()). A line whose first character is `\` matches nothing,
69
+ // which is what keeps already-escaped text byte-stable. `-5 dBm` and
70
+ // `1.5 V` match nothing either: the marker must be followed by a space, a
71
+ // tab or the end of the line. Indented code (4+ spaces) is out of scope.
72
+ const BLOCK_START_RULES = [
73
+ { re: /^#{1,6}(?=[ \t]|$)/, fix: (rest) => '\\' + rest },
74
+ { re: /^[-+](?=[ \t]|$)/, fix: (rest) => '\\' + rest },
75
+ { re: /^>/, fix: (rest) => '\\' + rest },
76
+ { re: /^(\d{1,9})([.)])(?=[ \t]|$)/, fix: (rest) => rest.replace(/^(\d{1,9})([.)])/, '$1\\$2') },
77
+ { re: /^(=+|-+)[ \t]*$/, fix: (rest) => '\\' + rest },
78
+ // A thematic break may have spaces between its dashes (`-- -`,
79
+ // `---- -`); the run rule above only sees an unbroken one.
80
+ { re: /^-(?:[ \t]*-){2,}[ \t]*$/, fix: (rest) => '\\' + rest },
81
+ { re: /^\|/, fix: (rest) => '\\' + rest },
82
+ ];
83
+ function escapeBlockStarts(md) {
84
+ return String(md).split('\n').map((line) => {
85
+ const ind = /^ {0,3}/.exec(line)[0];
86
+ const rest = line.slice(ind.length);
87
+ for (let i = 0; i < BLOCK_START_RULES.length; i++) {
88
+ if (BLOCK_START_RULES[i].re.test(rest)) return ind + BLOCK_START_RULES[i].fix(rest);
89
+ }
90
+ return line;
91
+ }).join('\n');
92
+ }
93
+
50
94
  // Real contenteditable output wraps plain runs in attribute-less <span>s
51
95
  // (Chrome/Firefox formatting artifacts). Those are transparent. A <span>
52
96
  // carrying any of these attributes is a real style/class/behavior carrier
@@ -358,5 +402,5 @@
358
402
  return serializeInline(rootEl).unsupported.length === 0;
359
403
  }
360
404
 
361
- return { serializeInline, canWysiwyg, escapeText };
405
+ return { serializeInline, canWysiwyg, escapeText, escapeBlockStarts };
362
406
  });
@@ -224,7 +224,9 @@
224
224
 
225
225
  // strip leading whitespace left behind by a dropped leading element
226
226
  // (e.g. a checkbox <input>) — see header note.
227
- const itemMd = rawItemMd.replace(/^[ \t]+/, '');
227
+ // v3.13.1 T2 (F-1): text that begins with a block marker is escaped,
228
+ // or the item's own text would re-lex as a nested heading/list/quote.
229
+ const itemMd = inlineMd.escapeBlockStarts(rawItemMd.replace(/^[ \t]+/, ''));
228
230
 
229
231
  // Two-part marker: bullet (ordered ordinal or plain '- ') and an
230
232
  // optional checkbox prefix. The two parts are independent so that
@@ -617,6 +619,11 @@
617
619
  // zero-width character it never asked for, and the item would stop
618
620
  // reading as empty to every `itemMd === ''` test in this function.
619
621
  if (SETEXT_ESCAPE_RE.test(itemMd)) itemMd = '';
622
+ // v3.13.1 T2 (F-1): an item whose text begins with `## `, `- `, `> `,
623
+ // `1. ` (…) is written escaped, or the next render reads it as a
624
+ // nested block. Only re-serialized items get here; a bystander
625
+ // replayed from its source lines never does.
626
+ itemMd = inlineMd.escapeBlockStarts(itemMd);
620
627
  }
621
628
 
622
629
  // Anything in the block that is neither chrome nor the text surface is
@@ -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;