@helping-ai-workflow/md2doc 3.4.2 → 3.5.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.
@@ -311,10 +311,193 @@ function cellRect(layout, laneIndex, cycle) {
311
311
  };
312
312
  }
313
313
 
314
+ /** 端點把手的邊長(px)。夠大到手指按得到,小到不會蓋住相鄰格。 */
315
+ const EDGE_HANDLE = 12;
316
+
317
+ function centerOfCell(layout, laneIndex, cell) {
318
+ const r = cellRect(layout, laneIndex, cell);
319
+ if (r === null) return null;
320
+ return { x: r.x + r.width / 2, y: r.y + r.height / 2 };
321
+ }
322
+
323
+ /**
324
+ * 每條 edge 的兩個端點座標、SVG path 與標籤錨點。
325
+ *
326
+ * 字母的位置問 `codec.nodesOf(doc)`,不自己走樹——`layoutOf` 已經立下這個規矩
327
+ * (它問 `codec.lanePaths`),第二份走樹邏輯會跟它漂移。用的是本檔案頂端那顆
328
+ * 模組層級的 `codec`(`enumerateLanes`/`cyclesOf` 也是同一顆):這個檔案已經
329
+ * 對 `wave-codec.js` 有一份 `require`,purity guard 也只准這一份,injected 參數
330
+ * 不會讓檔案更「純」,只會讓同名綁定的讀者分不清是哪一顆、也讓少傳一個參數的
331
+ * 改動悄悄落回外層而不報錯。
332
+ *
333
+ * 引用到不存在字母的 edge 直接跳過:那是一份使用者手寫壞掉的文件,畫不出那條線
334
+ * 是對的,讓整張圖倒掉不是。
335
+ */
336
+ /**
337
+ * `edgeLayout`'s three `continue`s, WITH the reason each one fired —
338
+ * final review finding 3. `edgeLayout` itself stays a thin wrapper around
339
+ * this (`.drawn`), so every existing caller keeps its array-of-drawn-edges
340
+ * shape; a caller that also needs to explain a gap (`renderUnmodelled` in
341
+ * `wave-ui.js`) asks this one instead of re-deriving any of the three checks
342
+ * from scratch — there is exactly one walk of `doc.edge`, and exactly one
343
+ * place that decides whether an entry counts as drawn.
344
+ *
345
+ * `skipped[i].reason` is one of:
346
+ * - `'unparseable'` — `codec.parseEdge` returned `null` (the shape is not
347
+ * one of `EDGE_SHAPES`, or the entry does not have `<letter><shape><letter>`
348
+ * to begin with). `edge` is `null`; there is no `from`/`to` to report.
349
+ * - `'dangling'` — the shape parsed, but `from` or `to` names a letter
350
+ * `codec.nodesOf` never placed anywhere in the document (a hand-edited or
351
+ * half-finished `edge:` array). This is the one case the pinned engine
352
+ * ALSO refuses to draw — measured against `node_modules/wavedrom`, an
353
+ * edge naming an absent letter renders nothing, same as here.
354
+ * - `'out-of-range'` — the shape parsed and both letters resolve to a real
355
+ * `{lane, cell}`, but that cell has no box in this layout: `centerOfCell`
356
+ * returned `null`, which happens when a lane's `node` string outlives the
357
+ * lane itself (`insertCycles`/`deleteCycles` narrow `wave` but do not
358
+ * reindex `node`, so a deleted cycle can leave an anchor past the lane's
359
+ * new length). Measured against the pinned engine, THIS case is the
360
+ * opposite of `'dangling'`: the engine draws a line for it anyway, so a
361
+ * silent skip here is exactly the canvas/preview divergence this version
362
+ * exists to prevent.
363
+ *
364
+ * Anything reaching this file only through `doc.edge` being non-empty but
365
+ * `layout` being unusable, or `doc` itself not carrying an `edge` array,
366
+ * still returns `{drawn: [], skipped: []}` — there is nothing to classify,
367
+ * not a fourth reason.
368
+ */
369
+ function edgeLayoutDetail(doc, layout) {
370
+ const drawn = [];
371
+ const skipped = [];
372
+ if (doc === null || typeof doc !== 'object' || !Array.isArray(doc.edge)) {
373
+ return { drawn: drawn, skipped: skipped };
374
+ }
375
+ if (layout === null || typeof layout !== 'object') {
376
+ return { drawn: drawn, skipped: skipped };
377
+ }
378
+ const nodes = codec.nodesOf(doc);
379
+ for (let i = 0; i < doc.edge.length; i += 1) {
380
+ const edge = codec.parseEdge(doc.edge[i]);
381
+ if (edge === null) {
382
+ skipped.push({ index: i, edge: null, reason: 'unparseable' });
383
+ continue;
384
+ }
385
+ const a = nodes[edge.from];
386
+ const b = nodes[edge.to];
387
+ if (a === undefined || b === undefined) {
388
+ skipped.push({ index: i, edge: edge, reason: 'dangling' });
389
+ continue;
390
+ }
391
+ const from = centerOfCell(layout, a.at, a.cell);
392
+ const to = centerOfCell(layout, b.at, b.cell);
393
+ if (from === null || to === null) {
394
+ skipped.push({ index: i, edge: edge, reason: 'out-of-range' });
395
+ continue;
396
+ }
397
+ from.at = a.at; from.cell = a.cell;
398
+ to.at = b.at; to.cell = b.cell;
399
+ drawn.push({
400
+ index: i,
401
+ edge: edge,
402
+ from: from,
403
+ to: to,
404
+ d: pathFor(edge.shape, from, to),
405
+ labelPos: { x: (from.x + to.x) / 2, y: (from.y + to.y) / 2 },
406
+ });
407
+ }
408
+ return { drawn: drawn, skipped: skipped };
409
+ }
410
+
411
+ function edgeLayout(doc, layout) {
412
+ return edgeLayoutDetail(doc, layout).drawn;
413
+ }
414
+
415
+ /**
416
+ * 折線(`-|` 系)三種轉角次序,抄自 pin 住的
417
+ * node_modules/wavedrom/lib/arc-shape.js 的三組 case(座標系裡的 `m` 相對位移
418
+ * 換成這裡的絕對 `L`):
419
+ *
420
+ * - MID:先橫走半程、再直走全程、再橫走半程——轉角在 `(from.x+to.x)/2`。
421
+ * - VERT_FIRST:先直(`from.x, to.y`)再橫——`|-` 系,垂直先行。
422
+ * - 其餘(HORIZ_FIRST):先橫(`to.x, from.y`)再直——`-|` 系,水平先行。
423
+ *
424
+ * 三個陣列各自窮舉 EDGE_SHAPES 裡屬於這族的每個成員(含箭頭與雙箭頭變體),
425
+ * 用精確字串比對而非子字串——shape 一律來自 `codec.parseEdge` 收斂過的
426
+ * EDGE_SHAPES 集合,不會有夾雜字元,子字串比對(舊寫法 `has('|')`)分不出
427
+ * `-|` 與 `|-` 誰先誰後,是這裡曾經量到的缺陷。
428
+ */
429
+ const ELBOW_MID = ['-|-', '-|->', '<-|->'];
430
+ const ELBOW_VERT_FIRST = ['|-', '|->'];
431
+ const ELBOW_HORIZ_FIRST = ['-|', '-|>', '<-|>'];
432
+
433
+ /**
434
+ * 形狀 → SVG path。
435
+ *
436
+ * 三族:直線(`-` 系)、曲線(`~` 系)、折線(`-|` 系)。箭頭本身不在 path 裡,
437
+ * 由 UI 掛 marker——這裡只負責「線走哪裡」。
438
+ */
439
+ function pathFor(shape, from, to) {
440
+ if (ELBOW_MID.indexOf(shape) !== -1) {
441
+ const midX = (from.x + to.x) / 2;
442
+ return 'M' + from.x + ',' + from.y + ' L' + midX + ',' + from.y +
443
+ ' L' + midX + ',' + to.y + ' L' + to.x + ',' + to.y;
444
+ }
445
+ if (ELBOW_VERT_FIRST.indexOf(shape) !== -1) {
446
+ return 'M' + from.x + ',' + from.y + ' L' + from.x + ',' + to.y +
447
+ ' L' + to.x + ',' + to.y;
448
+ }
449
+ if (ELBOW_HORIZ_FIRST.indexOf(shape) !== -1) {
450
+ return 'M' + from.x + ',' + from.y + ' L' + to.x + ',' + from.y +
451
+ ' L' + to.x + ',' + to.y;
452
+ }
453
+ const has = function (s) { return shape.indexOf(s) !== -1; };
454
+ if (has('~')) {
455
+ const dx = (to.x - from.x) / 2;
456
+ return 'M' + from.x + ',' + from.y +
457
+ ' C' + (from.x + dx) + ',' + from.y + ' ' + (to.x - dx) + ',' + to.y +
458
+ ' ' + to.x + ',' + to.y;
459
+ }
460
+ return 'M' + from.x + ',' + from.y + ' L' + to.x + ',' + to.y;
461
+ }
462
+
463
+ /** 端點把手的矩形,以端點為中心。 */
464
+ function edgeHandleRect(point) {
465
+ return {
466
+ x: point.x - EDGE_HANDLE / 2,
467
+ y: point.y - EDGE_HANDLE / 2,
468
+ width: EDGE_HANDLE,
469
+ height: EDGE_HANDLE,
470
+ };
471
+ }
472
+
473
+ /**
474
+ * `(x, y)` 命中哪一個端點把手。
475
+ *
476
+ * 用的是 `edgeHandleRect` 產生的同一個矩形——命中與繪製共用一個運算式,
477
+ * 就像 `cellAt` 是 `cellRect` 的構造反函數。另寫一份圓形碰撞會慢慢跟畫面分家。
478
+ */
479
+ function edgeHandleAt(edges, x, y) {
480
+ if (!Array.isArray(edges)) return null;
481
+ for (let i = edges.length - 1; i >= 0; i -= 1) {
482
+ for (const end of ['to', 'from']) {
483
+ const r = edgeHandleRect(edges[i][end]);
484
+ if (x >= r.x && x < r.x + r.width && y >= r.y && y < r.y + r.height) {
485
+ return { index: edges[i].index, end: end };
486
+ }
487
+ }
488
+ }
489
+ return null;
490
+ }
491
+
314
492
  module.exports = {
315
493
  layoutOf: layoutOf,
316
494
  cellAt: cellAt,
317
495
  cellRect: cellRect,
496
+ edgeLayout: edgeLayout,
497
+ edgeLayoutDetail: edgeLayoutDetail,
498
+ edgeHandleRect: edgeHandleRect,
499
+ edgeHandleAt: edgeHandleAt,
500
+ EDGE_HANDLE: EDGE_HANDLE,
318
501
  DEFAULTS: DEFAULTS,
319
502
  MAX_LANES: MAX_LANES,
320
503
  };
@@ -41,7 +41,6 @@
41
41
  * about it — rewrite the block, cancel, edit by hand — belongs to the
42
42
  * layer that can ask them, and re-serialising the block behind their back
43
43
  * is the one answer this file may never give.
44
- " PLACEHOLDER
45
44
  *
46
45
  * A throw inside an `apply` callback is NOT caught. The store's own state is
47
46
  * untouched when one happens (nothing is pushed until the new document is in
@@ -63,6 +62,17 @@ const C = require('./wave-codec.js');
63
62
  */
64
63
  const MAX_DEPTH = C.MAX_DEPTH;
65
64
 
65
+ /**
66
+ * The undo stack's cap, in entries.
67
+ *
68
+ * Every entry holds a whole frozen document, so an unbounded stack grows with
69
+ * the length of the editing session rather than with anything about the
70
+ * document. 200 is a ruling, not a measurement of any observed cost — there is
71
+ * no profiling behind the number, just a decision that a session's undo depth
72
+ * stops mattering well before 200 steps back.
73
+ */
74
+ const HISTORY_LIMIT = 200;
75
+
66
76
  /**
67
77
  * Open a store over one wavedrom block's source text.
68
78
  *
@@ -141,6 +151,23 @@ function createStore(sourceText) {
141
151
  origins: origins,
142
152
  });
143
153
  at++;
154
+ // Cap the stack, not just its growth: apply always leaves `at` on the
155
+ // entry it just pushed (the truncation above already dropped any redo
156
+ // tail), so history.length is always at + 1 here. Dropping the oldest
157
+ // entry when the cap is exceeded means `at` has to move down with it, or
158
+ // every canUndo/undoName/redo answer after the drop would be off by one —
159
+ // pointing one entry short of the one actually current.
160
+ // `while`, not `if`: apply today only ever pushes one entry per call, so
161
+ // the stack is never more than one over the cap and `if` is provably
162
+ // enough — but that "one entry per call" fact lives in this function, not
163
+ // in the invariant. A future batch-push (or a cap lowered at runtime)
164
+ // would leave history silently one-or-more over HISTORY_LIMIT forever,
165
+ // since nothing after this point ever re-checks. `while` costs nothing
166
+ // today and stops correctness depending on a fact this loop cannot see.
167
+ while (history.length > HISTORY_LIMIT) {
168
+ history.shift();
169
+ at--;
170
+ }
144
171
  return true;
145
172
  }
146
173