@helping-ai-workflow/md2doc 3.5.0 → 3.6.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.
@@ -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 (see wave-ui.js's
2842
- * own comment on `directFirstLaneOf`).
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,