@cardenelabs/dragon 0.28.0 → 0.32.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.
Files changed (66) hide show
  1. package/README.md +391 -16
  2. package/dist/index.cjs +13479 -7403
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +2360 -208
  5. package/dist/index.d.ts +2360 -208
  6. package/dist/index.js +13462 -7405
  7. package/dist/index.js.map +1 -1
  8. package/package.json +12 -13
  9. package/src/compile/actor-ids.ts +297 -0
  10. package/src/compile/actor-option-notice.ts +379 -0
  11. package/src/compile/actors.ts +103 -0
  12. package/src/compile/c4.ts +74 -0
  13. package/src/compile/chain.ts +93 -0
  14. package/src/compile/chart-fields.ts +193 -0
  15. package/src/compile/deep-rewrite.ts +30 -0
  16. package/src/compile/derived-source.ts +18 -0
  17. package/src/compile/direction.ts +102 -0
  18. package/src/compile/edge-option-notice.ts +112 -0
  19. package/src/compile/er-relation.ts +200 -0
  20. package/src/compile/flow.ts +54 -0
  21. package/src/compile/flowchart.ts +344 -0
  22. package/src/compile/funnel.ts +87 -0
  23. package/src/compile/gantt.ts +337 -0
  24. package/src/compile/generic.ts +696 -0
  25. package/src/compile/hierarchy.ts +92 -0
  26. package/src/compile/journey.ts +93 -0
  27. package/src/compile/kinds.ts +148 -0
  28. package/src/compile/lanes.ts +138 -0
  29. package/src/compile/metro.ts +271 -0
  30. package/src/compile/mind.ts +393 -0
  31. package/src/compile/node-title.ts +24 -0
  32. package/src/compile/notice.ts +141 -0
  33. package/src/compile/order.ts +62 -0
  34. package/src/compile/parts-lookup.ts +17 -0
  35. package/src/compile/parts.ts +2709 -0
  36. package/src/compile/placement-apply.ts +689 -0
  37. package/src/compile/placement.ts +15 -0
  38. package/src/compile/quadrant.ts +265 -0
  39. package/src/compile/record.ts +75 -0
  40. package/src/compile/row-marks.ts +128 -0
  41. package/src/compile/rows.ts +40 -0
  42. package/src/compile/sequence.ts +305 -0
  43. package/src/compile/slug.ts +37 -0
  44. package/src/compile/stages.ts +247 -0
  45. package/src/compile/subtitle.ts +35 -0
  46. package/src/compile/swimlane-marks.ts +87 -0
  47. package/src/compile/swimlane.ts +82 -0
  48. package/src/compile/timeline.ts +213 -0
  49. package/src/compile/topology.ts +44 -0
  50. package/src/compile/tree.ts +69 -0
  51. package/src/compile/value-chart.ts +337 -0
  52. package/src/compile/values.ts +432 -0
  53. package/src/compile/word-state.ts +82 -0
  54. package/src/compile.ts +1806 -6971
  55. package/src/focus.ts +2 -2
  56. package/src/index.ts +42 -9
  57. package/src/json-line-map.ts +30 -0
  58. package/src/json-parser.ts +1760 -168
  59. package/src/keywords.ts +251 -17
  60. package/src/near-name.ts +42 -0
  61. package/src/notation-lint.ts +41 -64
  62. package/src/relative-pos.ts +71 -2
  63. package/src/schema.ts +1 -1
  64. package/src/schemas/diagram.json +680 -23
  65. package/src/types.ts +337 -36
  66. package/src/v05/parser.ts +2553 -354
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cardenelabs/dragon",
3
- "version": "0.28.0",
3
+ "version": "0.32.0",
4
4
  "description": "Dragon — Mermaid 感覚で animated SVG を生成する Text DSL。 cdl engine を内部利用。",
5
5
  "license": "MIT",
6
6
  "author": "cardene777",
@@ -46,18 +46,8 @@
46
46
  "node": ">=18"
47
47
  },
48
48
  "sideEffects": false,
49
- "scripts": {
50
- "build": "tsup",
51
- "dev": "tsup --watch",
52
- "typecheck": "tsc --noEmit",
53
- "typecheck:test": "tsc --noEmit -p tsconfig.test.json",
54
- "test": "cd ../.. && vitest run packages/dragon",
55
- "test:watch": "cd ../.. && vitest packages/dragon",
56
- "lint": "tsc --noEmit",
57
- "prepublishOnly": "pnpm run typecheck && pnpm run test && pnpm run build"
58
- },
59
49
  "dependencies": {
60
- "@cardenelabs/cdl": "^0.66.0"
50
+ "@cardenelabs/cdl": "^0.139.0"
61
51
  },
62
52
  "peerDependencies": {
63
53
  "react": "^19.0.0",
@@ -76,5 +66,14 @@
76
66
  "react": "^19.1.0",
77
67
  "tsup": "^8.5.1",
78
68
  "typescript": "^5.8.0"
69
+ },
70
+ "scripts": {
71
+ "build": "tsup",
72
+ "dev": "tsup --watch",
73
+ "typecheck": "tsc --noEmit",
74
+ "typecheck:test": "tsc --noEmit -p tsconfig.test.json",
75
+ "test": "cd ../.. && vitest run packages/dragon",
76
+ "test:watch": "cd ../.. && vitest packages/dragon",
77
+ "lint": "tsc --noEmit"
79
78
  }
80
- }
79
+ }
@@ -0,0 +1,297 @@
1
+ import type { CdlDiagram } from "@cardenelabs/cdl";
2
+ import type { DslActor, DslDocument, PresetType } from "../types";
3
+ import { 図種の作り } from "./kinds";
4
+ import type { CompileNotice } from "./notice";
5
+ import { ID_MAX, slugify } from "./slug";
6
+ import { truncateForMessage } from "./subtitle";
7
+ /**
8
+ * 名前の重なりを解く (#2036 で `compile.ts` から移した)。
9
+ *
10
+ * 違う名前が同じ id に潰れると、箱が 1 つ消えたり、矢印が別の箱を指したりする。
11
+ * 潰れる前に名前を作り替え、図を組み立てた後で元へ戻す。
12
+ *
13
+ * 外へ出す口は 3 つ。 作り替える `disambiguateActorIds`、作り替えた相手を控える
14
+ * `collectRenamedTargets`、元へ戻す `restoreActorNames`。 いずれも入口が呼ぶ。
15
+ */
16
+
17
+ /**
18
+ * 名前から作る id の重なりを解く (#1220)。
19
+ *
20
+ * 箱と枠の id は登場人物の名前から作る (`slugify`)。 **違う名前が同じ id に潰れる** と、
21
+ * どちらも正しく書いているのに図が組み立たない (実測 = `foo-bar` と `Foo Bar` を書くと
22
+ * 9 図種が `duplicate-id` で落ちる)。 知らせも出ない = どちらの名前も `actors` に在るため。
23
+ *
24
+ * ## なぜ名前を作り替えるのか
25
+ *
26
+ * id を作る所は 90 箇所を超え、 さらに **cdl 側の組み立てが名前から id を作る経路** がある
27
+ * (`swimlane()` / `er()` は渡した名札から lane id を作る)。 dragon 側だけを直しても届かない。
28
+ *
29
+ * そこで **渡す名前を変え、 最後に表示だけ戻す**。 作り替えた名前は組み立ての間だけ使い、
30
+ * 出口で `nodes[].title` と `lanes[].label` を元に戻す (`restoreActorNames`)。
31
+ *
32
+ * ## 尾は名前から作る
33
+ *
34
+ * 書き順で決めると、 登場人物を並べ替えただけで id が入れ替わる。 元の名前だけから決まる
35
+ * 短い値を尾に付ければ、 並べ替えても同じ id になる。
36
+ *
37
+ * ## まったく同じ名前は畳む
38
+ *
39
+ * 名前が 1 文字も違わない登場人物は区別できない。 2 つの箱に同じ題が付くだけなので、
40
+ * 先に書いた方を残して知らせる。
41
+ */
42
+ function 名前の尾(name: string): string {
43
+ // FNV-1a。 短くて名前だけから決まればよく、 衝突しても下の検査が拾う
44
+ let h = 0x811c9dc5;
45
+ for (const c of name) {
46
+ h ^= c.codePointAt(0) ?? 0;
47
+ h = Math.imul(h, 0x01000193) >>> 0;
48
+ }
49
+ return h.toString(16).padStart(8, "0").slice(0, 6);
50
+ }
51
+
52
+ /**
53
+ * cdl 側が名札から id を作る時の規則 (`presets.ts` の `slugify`)。
54
+ *
55
+ * **dragon の規則と違う**。 dragon は `-` と `_` を残し `NFKC` で揃え 64 字で切るが、 cdl は
56
+ * どちらも `-` に潰し、 長さも切らない。 このため `a_b` と `a-b` は **dragon では別 id、 cdl では
57
+ * 同じ id** になる (実測 = 動きを書かない `sequence` / `solidity` が `duplicate-id` で落ちる)。
58
+ *
59
+ * ここに写している = cdl は `slugify` を公開していない。 **ずれると衝突を見落とす** ので、
60
+ * 下の検査が既知の組で対応を固定する。
61
+ */
62
+ function cdl側のslug(s: string): string {
63
+ return s
64
+ .toLowerCase()
65
+ .replace(/[^a-z0-9぀-ゟ゠-ヿ一-龯]+/g, "-")
66
+ .replace(/^-|-$/g, "");
67
+ }
68
+
69
+ /**
70
+ * cdl 側が「形が空になった名前」 に付ける id の頭 (#1220 Round 2 / 3)。
71
+ *
72
+ * cdl は形が空になった時に並びの位置へ逃げる。 逃げ先を鍵に入れないと、 逃げ先と同じ名前の
73
+ * 登場人物が居る図で重なる (実測 = `sequence` の `😀` と `actor-0`)。
74
+ *
75
+ * **図種ごとに違う** (Round 3 の指摘)。 両方を入れると、 その図種では使われない逃げ先まで
76
+ * 衝突とみなして **重なっていない図の id を変える**。 実測した対応は次のとおり。
77
+ *
78
+ * | 図種 | `😀` だけを書いた時に付く id |
79
+ * |---|---|
80
+ * | `sequence` / `solidity` | 枠 `actor-0` (cdl の `sequence()`) |
81
+ * | `swimlane` | 枠 `lane-0` (cdl の `swimlane()`) |
82
+ * | 残る 5 図種 | 箱 `n` (dragon 側の逃げ先。 cdl の逃げ道を通らない) |
83
+ *
84
+ * 空配列は「cdl の逃げ道を通らない」 を表す。 1 箱で描く図種はここに来ない (作り替え自体を
85
+ * しない) が、 図種を足した時に決め忘れないよう全種を並べる。
86
+ *
87
+ * **この表が効くのは動きを書いていない図だけ** (Round 4 の指摘)。 動きを書くと組み立てが別経路に
88
+ * 切り替わり、 id は dragon 側の規則で作られる = cdl の逃げ道を通らない。 使う側で条件を見る。
89
+ */
90
+ const 空の形の逃げ先: Record<PresetType, string[]> = {
91
+ sequence: ["actor-"],
92
+ swimlane: ["lane-"],
93
+ // 分かれ道の図も縦列を持ち、 描画側は縦列の名前から箱を作る (`swimlane` と同じ逃げ先)
94
+ flowchart: ["lane-"],
95
+ flow: [],
96
+ record: [],
97
+ topology: [],
98
+ c4: [],
99
+ // 図全体を 1 箱で描く群 (作り替えないのでここは使わない)
100
+ gantt: [],
101
+ chart: [],
102
+ funnel: [],
103
+ tree: [],
104
+ journey: [],
105
+ quadrant: [],
106
+ mind: [],
107
+ };
108
+
109
+ /**
110
+ * その名前が下流で id になりうる形。 どれか 1 つでも重なれば衝突する。
111
+ *
112
+ * 位置と逃げ先が分からない時 (作り替えた候補を確かめる時) は逃げ先を数えない。 作り替えた
113
+ * 名前は尾が付いて形が空にならないので、 そもそも逃げ道を通らない。
114
+ */
115
+ function idになる形(name: string, 位置?: number, 逃げ先の頭: string[] = []): string[] {
116
+ const cdl = cdl側のslug(name);
117
+ const out = [slugify(name), cdl];
118
+ if (cdl === "" && 位置 !== undefined) {
119
+ for (const 頭 of 逃げ先の頭) out.push(`${頭}${位置}`);
120
+ }
121
+ return out;
122
+ }
123
+
124
+ export function disambiguateActorIds(
125
+ doc: DslDocument,
126
+ onNotice?: (notice: CompileNotice) => void,
127
+ ): { doc: DslDocument; 元の名前: Map<string, string> } {
128
+ const 元の名前 = new Map<string, string>();
129
+ if (図種の作り[doc.type] !== "登場人物ごとに箱") return { doc, 元の名前 };
130
+
131
+ // 1. まったく同じ名前を畳む
132
+ const 見た = new Set<string>();
133
+ const 残す: DslActor[] = [];
134
+ for (const a of doc.actors) {
135
+ if (見た.has(a.name)) {
136
+ onNotice?.({
137
+ kind: "chart-value-unreadable",
138
+ actor: a.name,
139
+ line: a.pos?.line ?? 0,
140
+ message: `"${truncateForMessage(a.name)}" を 2 度書いています (先に書いた方だけ描きます)`,
141
+ hint: "違う名前にするか、 1 つにまとめる",
142
+ });
143
+ continue;
144
+ }
145
+ 見た.add(a.name);
146
+ 残す.push(a);
147
+ }
148
+
149
+ // 2. どの名前が重なるかを見る。 **両方の規則で見る** = 片方だけだと cdl 側の経路で落ちる
150
+ const 形ごとの名前 = new Map<string, Set<string>>();
151
+ const 位置 = new Map<string, number>();
152
+ 残す.forEach((a, i) => 位置.set(a.name, i));
153
+ // **動きを書いた図では cdl の逃げ道を通らない** (Round 4 の指摘)。 動きがあると組み立てが
154
+ // 別経路 (`compileGenericWithAnimate` / `compileSequenceWithAnimate`) に切り替わり、 id は
155
+ // dragon 側の規則で作られる (実測 = `😀` は 枠 `n` / `lane-n` になり、 位置を使わない)。
156
+ //
157
+ // 逃げ先を鍵に入れたままにすると、 その経路で **重なっていない図の id を変える**。
158
+ // dragon 側の規則で作る分は、 1 つ目の鍵 (`slugify`) が既に見ている。
159
+ const 動きを書いた = (doc.animate?.phases.length ?? 0) > 0;
160
+ const 逃げ先の頭 = 動きを書いた ? [] : 空の形の逃げ先[doc.type];
161
+ for (const a of 残す) {
162
+ for (const 形 of idになる形(a.name, 位置.get(a.name), 逃げ先の頭)) {
163
+ const 群 = 形ごとの名前.get(形) ?? new Set<string>();
164
+ 群.add(a.name);
165
+ 形ごとの名前.set(形, 群);
166
+ }
167
+ }
168
+ const 重なる = (name: string): boolean =>
169
+ idになる形(name, 位置.get(name), 逃げ先の頭).some(
170
+ (形) => (形ごとの名前.get(形)?.size ?? 0) > 1,
171
+ );
172
+
173
+ // **見本 (`parts`) を重ねた登場人物は作り替えない**。 見本の中身は `別名__元の id` の形で
174
+ // 名前空間を持ち、 別名は名前から作るため、 作り替えると見本の id が総入れ替えになる。
175
+ //
176
+ // ただし **重なりの判定には数える** (Round 1 の指摘)。 数えないと、 素の登場人物と見本が
177
+ // 同じ id の仮置きを共有し、 見本を片付ける時に素の登場人物の箱まで消える。
178
+ const 作り替える = 残す.filter((a) => a.partId === undefined && 重なる(a.name));
179
+
180
+ // 3. 名前から決まる尾を付ける。 **できあがる id が一意になるまで見る**
181
+ //
182
+ // 尾は元の名前だけから決まる = 並べ替えても同じ id になる。 それでも重なる時 (尾そのものが
183
+ // 重なる形) は、 名前を並べ替えた順で番号を足す = ここも書き順に依らない
184
+ const 使う形 = new Set<string>();
185
+ for (const a of 残す) {
186
+ if (作り替える.some((b) => b.name === a.name)) continue;
187
+ for (const 形 of idになる形(a.name)) 使う形.add(形);
188
+ }
189
+ const 新しい名前 = new Map<string, string>();
190
+ // 名前で並べてから配る = 書いた順に依らない
191
+ for (const a of [...作り替える].sort((x, y) =>
192
+ x.name < y.name ? -1 : x.name > y.name ? 1 : 0,
193
+ )) {
194
+ const 尾 = 名前の尾(a.name);
195
+ // **id の長さの上限のぶん、 元の名前を先に切る** (Round 1 の指摘)。 切らないと尾が
196
+ // 64 字で落ちて、 同じ頭を持つ長い名前どうしが元のまま重なる
197
+ const 余地 = ID_MAX - (尾.length + 1);
198
+ const 基底 = a.name.slice(0, 余地);
199
+ let 候補 = `${基底} ${尾}`;
200
+ let n = 0;
201
+ while (idになる形(候補).some((形) => 使う形.has(形))) {
202
+ n += 1;
203
+ 候補 = `${基底.slice(0, 余地 - String(n).length)} ${尾}${n}`;
204
+ }
205
+ for (const 形 of idになる形(候補)) 使う形.add(形);
206
+ 新しい名前.set(a.name, 候補);
207
+ 元の名前.set(候補, a.name);
208
+ }
209
+
210
+ if (新しい名前.size === 0 && 残す.length === doc.actors.length) return { doc, 元の名前 };
211
+
212
+ const 直す = (名: string): string => 新しい名前.get(名) ?? 名;
213
+ const 次: DslDocument = {
214
+ ...doc,
215
+ actors: 残す.map((a) => (新しい名前.has(a.name) ? { ...a, name: 直す(a.name) } : a)),
216
+ flow: doc.flow.map((s) => {
217
+ const from = 直す(s.from);
218
+ const to = 直す(s.to);
219
+ return from === s.from && to === s.to ? s : { ...s, from, to };
220
+ }),
221
+ events: doc.events?.map((event) => {
222
+ const target = event.target;
223
+ if (target.kind === "node") {
224
+ const name = 直す(target.name);
225
+ return name === target.name ? event : { ...event, target: { ...target, name } };
226
+ }
227
+ if (target.kind === "edge") {
228
+ const from = 直す(target.from);
229
+ const to = 直す(target.to);
230
+ return from === target.from && to === target.to
231
+ ? event
232
+ : { ...event, target: { ...target, from, to } };
233
+ }
234
+ // sequence 系の縦列は登場人物から作るため、同じ名前の読み替えが必要。
235
+ if (target.kind === "lane" && doc.type === "sequence") {
236
+ const name = 直す(target.name);
237
+ return name === target.name ? event : { ...event, target: { ...target, name } };
238
+ }
239
+ return event;
240
+ }),
241
+ };
242
+ // 光らせる指定と位置の基準も名前で書くので、 同じ表で直す
243
+ if (次.animate) {
244
+ 次.animate = {
245
+ ...次.animate,
246
+ phases: 次.animate.phases.map((ph) =>
247
+ ph.highlight ? { ...ph, highlight: ph.highlight.map((h) => 直す(h)) } : ph,
248
+ ),
249
+ };
250
+ }
251
+ 次.actors = 次.actors.map((a) =>
252
+ a.posRel ? { ...a, posRel: { ...a.posRel, anchor: 直す(a.posRel.anchor) } } : a,
253
+ );
254
+ return { doc: 次, 元の名前 };
255
+ }
256
+
257
+ /**
258
+ * 作り替えた名前を持つ箱と枠を控える (#1220)。
259
+ *
260
+ * **組み立て直後に控える** (Round 1 の指摘)。 出口で題の文字だけを見て戻すと、 後から足された
261
+ * 見本の中の箱がたまたま同じ題を持っていた時に、 その表示まで書き換えてしまう。
262
+ */
263
+ export function collectRenamedTargets(
264
+ diagram: CdlDiagram,
265
+ 元の名前: Map<string, string>,
266
+ ): { 箱: Set<string>; 枠: Set<string> } {
267
+ const 箱 = new Set<string>();
268
+ const 枠 = new Set<string>();
269
+ if (元の名前.size === 0) return { 箱, 枠 };
270
+ for (const n of diagram.nodes) if (元の名前.has(n.title)) 箱.add(n.id);
271
+ for (const l of diagram.lanes) if (l.label !== undefined && 元の名前.has(l.label)) 枠.add(l.id);
272
+ return { 箱, 枠 };
273
+ }
274
+
275
+ /**
276
+ * 作り替えた名前を、 図の表示だけ元に戻す (#1220)。
277
+ *
278
+ * 戻すのは題と名札だけ。 id は作り替えたまま = 分けるために作り替えたので、 戻すと元の
279
+ * 重なりに帰る。
280
+ */
281
+ export function restoreActorNames(
282
+ diagram: CdlDiagram,
283
+ 元の名前: Map<string, string>,
284
+ 対象: { 箱: Set<string>; 枠: Set<string> },
285
+ ): void {
286
+ if (元の名前.size === 0) return;
287
+ for (const n of diagram.nodes) {
288
+ if (!対象.箱.has(n.id)) continue;
289
+ const 元 = 元の名前.get(n.title);
290
+ if (元 !== undefined) n.title = 元;
291
+ }
292
+ for (const l of diagram.lanes) {
293
+ if (!対象.枠.has(l.id) || l.label === undefined) continue;
294
+ const 元 = 元の名前.get(l.label);
295
+ if (元 !== undefined) l.label = 元;
296
+ }
297
+ }