@cardenelabs/dragon 0.27.0 → 0.30.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 (60) hide show
  1. package/README.md +119 -12
  2. package/dist/index.cjs +9619 -8023
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +407 -175
  5. package/dist/index.d.ts +407 -175
  6. package/dist/index.js +9617 -8024
  7. package/dist/index.js.map +1 -1
  8. package/package.json +2 -2
  9. package/src/compile/actor-ids.ts +306 -0
  10. package/src/compile/actors.ts +74 -0
  11. package/src/compile/c4.ts +77 -0
  12. package/src/compile/chain.ts +54 -0
  13. package/src/compile/chart-fields.ts +178 -0
  14. package/src/compile/class.ts +100 -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 +63 -0
  18. package/src/compile/er-relation.ts +200 -0
  19. package/src/compile/er.ts +44 -0
  20. package/src/compile/flow.ts +54 -0
  21. package/src/compile/funnel.ts +75 -0
  22. package/src/compile/gantt.ts +232 -0
  23. package/src/compile/generic.ts +284 -0
  24. package/src/compile/hierarchy.ts +92 -0
  25. package/src/compile/journey.ts +84 -0
  26. package/src/compile/kinds.ts +156 -0
  27. package/src/compile/lanes.ts +56 -0
  28. package/src/compile/mind.ts +343 -0
  29. package/src/compile/node-title.ts +24 -0
  30. package/src/compile/notice.ts +94 -0
  31. package/src/compile/parts-lookup.ts +17 -0
  32. package/src/compile/parts.ts +2635 -0
  33. package/src/compile/placement-apply.ts +673 -0
  34. package/src/compile/placement.ts +15 -0
  35. package/src/compile/quadrant.ts +110 -0
  36. package/src/compile/rows.ts +40 -0
  37. package/src/compile/sequence.ts +226 -0
  38. package/src/compile/slug.ts +29 -0
  39. package/src/compile/solidity.ts +39 -0
  40. package/src/compile/state.ts +71 -0
  41. package/src/compile/subtitle.ts +22 -0
  42. package/src/compile/swimlane.ts +83 -0
  43. package/src/compile/topology.ts +50 -0
  44. package/src/compile/tree.ts +60 -0
  45. package/src/compile/value-chart.ts +183 -0
  46. package/src/compile/values.ts +432 -0
  47. package/src/compile/word-state.ts +82 -0
  48. package/src/compile.ts +596 -5672
  49. package/src/index.ts +13 -10
  50. package/src/json-line-map.ts +30 -0
  51. package/src/json-parser.ts +509 -176
  52. package/src/near-name.ts +42 -0
  53. package/src/notation-lint.ts +112 -74
  54. package/src/relative-pos.ts +71 -2
  55. package/src/schema.ts +1 -1
  56. package/src/schemas/diagram.json +130 -34
  57. package/src/types.ts +60 -41
  58. package/src/v05/input-table.generated.ts +6 -4
  59. package/src/v05/parser-types.ts +12 -1
  60. package/src/v05/parser.ts +503 -149
@@ -0,0 +1,15 @@
1
+ /**
2
+ * 置けたかどうかの判定に使う幅 (#2034 で `compile.ts` から移した)。
3
+ *
4
+ * **葉に置く** = 配置を確かめる側 (`verifyPlacement` / `applyLayoutOffsets`) と
5
+ * 部品の取り込み側 (`縦列に置いた部品を揃える`) の両方が読む。 どちらかに置くと、
6
+ * もう片方から逆向きの取り込みが生まれて輪になる。
7
+ */
8
+
9
+ /**
10
+ * 書いた通りに置けたと見なす誤差。
11
+ *
12
+ * 補正が効いた図種では実測 0.0 で一致する。 効かない向き (順序図の縦) は数百ずれるので、
13
+ * その間で切る。 丸めと配置計算の揺れを吸収する幅として 1 を取る。
14
+ */
15
+ export const PLACEMENT_TOLERANCE = 1;
@@ -0,0 +1,110 @@
1
+ import { diagram } from "@cardenelabs/cdl";
2
+ import type { CdlDiagram } from "@cardenelabs/cdl";
3
+ import type { DslDocument } from "../types";
4
+ import { 図表の大きさ, 語の欄から参照できる名前 } from "./chart-fields";
5
+ import { 箱の題 } from "./node-title";
6
+ import type { CompileNotice } from "./notice";
7
+ import { slugify } from "./slug";
8
+ import { 図の小見出し } from "./subtitle";
9
+ import { 区画 } from "./word-state";
10
+ /**
11
+ * 四象限 (`type: quadrant`) の組み立て (#1154)。 描画側に 1 つの箱で渡す。
12
+ *
13
+ * 登場人物ごとに **どの区画か** を読む (`- 重複削除: "左上"`)。 使える語は `区画` が持つ。
14
+ */
15
+ export function compileQuadrant(
16
+ doc: DslDocument,
17
+ onNotice?: (n: CompileNotice) => void,
18
+ ): CdlDiagram {
19
+ const b = diagram(slugify(doc.title), { topic: doc.title, type: "quadrant" });
20
+ const { w: W, h: H } = 図表の大きさ.quadrant;
21
+ b.lane("chart", { width: W + 64 });
22
+ const items: NonNullable<CdlDiagram["nodes"][number]["quadrantData"]>["items"] = [];
23
+ const 読めない: string[] = [];
24
+ const 参照できる = 語の欄から参照できる名前(doc, 区画);
25
+ for (const a of doc.actors) {
26
+ const 語 = (a.value ?? a.subtitle ?? "").trim();
27
+ const 参照 = 語.match(/^\{(\w+)\}$/);
28
+ if (参照) {
29
+ if (!参照できる.has(参照[1]!)) {
30
+ 読めない.push(a.name);
31
+ continue;
32
+ }
33
+ items.push({ id: slugify(a.name), title: 箱の題(a), quadrant: 語 });
34
+ continue;
35
+ }
36
+ const q = 区画.get(語);
37
+ if (q === undefined) {
38
+ 読めない.push(a.name);
39
+ continue;
40
+ }
41
+ items.push({ id: slugify(a.name), title: 箱の題(a), quadrant: q });
42
+ }
43
+ if (読めない.length > 0) {
44
+ const m = `type: quadrant で区画を読めない項目があります (図に載せません): ${読めない.join(", ")}。 \`- 重複削除: "左上"\` の形で、 ${[...区画.keys()].join(" / ")} のどれかを書いてください`;
45
+ onNotice?.({ kind: "chart-value-unreadable", actor: 読めない[0]!, line: 0, message: m });
46
+ if (typeof console !== "undefined" && console.warn) console.warn(`[dragon] ${m}`);
47
+ }
48
+ // 矢印は描けない。 書かれていたら伝える (黙って捨てると「書いたのに効かない」 が残る)
49
+ if (doc.flow.length > 0) {
50
+ const m2 = `type: quadrant では矢印を描けません (${doc.flow.length} 本を無視しました)。 関係を描くなら type: flow を使ってください`;
51
+ onNotice?.({
52
+ kind: "chart-edge-dropped",
53
+ actor: doc.flow[0]?.from ?? "",
54
+ line: doc.flow[0]?.pos?.line ?? 0,
55
+ message: m2,
56
+ });
57
+ if (typeof console !== "undefined" && console.warn) console.warn(`[dragon] ${m2}`);
58
+ }
59
+ b.node(`${slugify(doc.title) || "quadrant"}-chart`, {
60
+ lane: "chart",
61
+ stack: 0,
62
+ kind: "quadrant-matrix",
63
+ title: doc.title,
64
+ ...図の小見出し(doc),
65
+ w: W,
66
+ h: H,
67
+ quadrantData: { ...軸と区画の名前(doc), items },
68
+ });
69
+ return b.build();
70
+ }
71
+
72
+ /** 軸を書かなかった時の名前。 何の軸か分からないため、位置をそのまま出す */
73
+ const 軸の既定 = {
74
+ xAxis: { left: "小さい", right: "大きい" },
75
+ yAxis: { bottom: "小さい", top: "大きい" },
76
+ quadrantLabels: { topLeft: "左上", topRight: "右上", bottomLeft: "左下", bottomRight: "右下" },
77
+ } as const;
78
+
79
+ /**
80
+ * 2 軸で仕分ける図の軸と区画の名前を決める (#1251)。
81
+ *
82
+ * `axes:` を書かなければ従来どおり位置の名前 (`左上` 等) を出す = 既に描いてある図が動かない。
83
+ *
84
+ * 書いたら区画の名前は **軸の名前から決める** (`{上} × {右}`)。 組立て API 側が同じ規則で
85
+ * 導いており (実測 = `xAxis: {left: "L", right: "R"}` / `yAxis: {bottom: "B", top: "T"}` で
86
+ * `topLeft: "T × L"`)、 別の規則にすると同じ内容を書いても図が食い違う。
87
+ *
88
+ * 片側だけ書いた形では、書かなかった側は既定のままにする。 空文字を渡すと名前の無い軸が描かれる。
89
+ */
90
+ function 軸と区画の名前(doc: DslDocument): {
91
+ xAxis: { left: string; right: string };
92
+ yAxis: { bottom: string; top: string };
93
+ quadrantLabels: { topLeft: string; topRight: string; bottomLeft: string; bottomRight: string };
94
+ } {
95
+ if (doc.axes === undefined) return 軸の既定;
96
+ const left = doc.axes.x?.left ?? 軸の既定.xAxis.left;
97
+ const right = doc.axes.x?.right ?? 軸の既定.xAxis.right;
98
+ const bottom = doc.axes.y?.bottom ?? 軸の既定.yAxis.bottom;
99
+ const top = doc.axes.y?.top ?? 軸の既定.yAxis.top;
100
+ return {
101
+ xAxis: { left, right },
102
+ yAxis: { bottom, top },
103
+ quadrantLabels: {
104
+ topLeft: `${top} × ${left}`,
105
+ topRight: `${top} × ${right}`,
106
+ bottomLeft: `${bottom} × ${left}`,
107
+ bottomRight: `${bottom} × ${right}`,
108
+ },
109
+ };
110
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * 箱に書いた行を読み取る (#2030 で `compile.ts` から移した)。
3
+ *
4
+ * `compileC4` と、`compile.ts` に残る v0.5 の拡張の両方が呼ぶ。
5
+ *
6
+ * **葉に置く** = どちらかの側に置くと、もう片方から逆向きの取り込みが生まれる。
7
+ */
8
+
9
+ /**
10
+ * 段の目印を読み取る。 目印と、 それを落とした残りの説明を返す (#1098)。
11
+ *
12
+ * 目印 (`L1` / `L2` / `L3`) は「どの段に置くか」 を組み立てに伝えるためのもので、 読む人には
13
+ * 意味を持たない。 段の名前は枠のラベル (`全体の見取り図` 等) が出すので二重でもある。
14
+ * 読み取ったら説明から落とす = 書いた人が説明として書いた部分だけが箱に出る。
15
+ *
16
+ * | 書いた文字 | 段 | 残る説明 |
17
+ * |---|---|---|
18
+ * | `"L1"` | 1 | (無し) |
19
+ * | `"L2: container"` | 2 | `container` |
20
+ * | `"L1 利用者"` | 1 | `利用者` |
21
+ * | `"利用者"` | 1 (既定) | `利用者` |
22
+ *
23
+ * 目印の直後の区切り (`:` と空白) も落とす。 残さないと `: container` のように区切りだけが
24
+ * 先頭に残る。 `L2X` のような別の語を目印と読み違えないよう、 数字の直後が英数字でないことを
25
+ * 条件にする。
26
+ */
27
+ export function 段を読み取る(subtitle: string | undefined): {
28
+ 段: number;
29
+ 説明: string | undefined;
30
+ } {
31
+ const 元 = (subtitle ?? "").trim();
32
+ const m = 元.match(/^L([123])(?![0-9A-Za-z])/i);
33
+ if (m === null) return { 段: 1, 説明: subtitle };
34
+ // 目印と、 その直後の区切り (`:` / 全角コロン / 空白) を落とす
35
+ const 残り = 元
36
+ .slice(m[0].length)
37
+ .replace(/^[::\s]+/, "")
38
+ .trim();
39
+ return { 段: Number(m[1]), 説明: 残り === "" ? undefined : 残り };
40
+ }
@@ -0,0 +1,226 @@
1
+ import { sequence, sequenceStepId } from "@cardenelabs/cdl";
2
+ import type { CdlDiagram } from "@cardenelabs/cdl";
3
+ import type { DslDocument } from "../types";
4
+
5
+ import { parseFocusEntry } from "../focus";
6
+ import { 箱の題 } from "./node-title";
7
+
8
+ import { slugify } from "./slug";
9
+
10
+ export function compileSequence(doc: DslDocument): CdlDiagram {
11
+ // v0.3 ... アニメーション 有無で経路を分岐。
12
+ // 有り = builder 直接経路で state / 複数 phase を注入。
13
+ /*
14
+ * **段があっても組み立て器へ渡す** (#1466)。
15
+ *
16
+ * 順序図は 1 つの箱が図を丸ごと描く形になり、言づては箱の中の行になった。 段ごとに
17
+ * 別経路で箱と縦線を組む形 (`compileSequenceWithAnimate`) では、その骨格が出ない。
18
+ */
19
+ const seqBuilder = sequence({
20
+ id: slugify(doc.title),
21
+ topic: doc.title,
22
+ /*
23
+ * 見出しに出すのは **書いた題** (#1466)。 名前は矢印の端として指すためのもので、
24
+ * `title:` を書いたらそちらを出す (`箱の題`)。 板でも他の図種と同じ規約にする。
25
+ */
26
+ actors: doc.actors.map((a) =>
27
+ a.subtitle ? { name: 箱の題(a), subtitle: a.subtitle } : 箱の題(a),
28
+ ),
29
+ ...(doc.bands && doc.bands.length > 0 ? { bands: doc.bands } : {}),
30
+ });
31
+ for (const s of doc.flow) {
32
+ seqBuilder.step({
33
+ from: s.from,
34
+ to: s.to,
35
+ label: s.label,
36
+ ...(s.sub ? { sub: s.sub } : {}),
37
+ ...(s.side ? { side: s.side } : {}),
38
+ ...(s.tone ? { tone: s.tone } : {}),
39
+ ...(s.style ? { style: s.style } : {}),
40
+ ...(s.msgKind ? { kind: s.msgKind } : {}),
41
+ });
42
+ }
43
+ const built = seqBuilder.build();
44
+ const 段 = doc.animate?.phases ?? [];
45
+ if (段.length === 0) return built;
46
+ /*
47
+ * 書いた段を「今どの言づてか」 に読み替える (#1466)。
48
+ *
49
+ * 記法は段ごとに光らせる矢印を並べる (`focus: [A -> B, ...]`) が、言づては矢印ではなく
50
+ * 箱の中の行になった = 光らせる先が無い。 代わりに **その段までに出た言づての番号** を
51
+ * 状態へ書き、箱がそこまでを描く。 番号の導き方は `段の番号` が持つ。
52
+ */
53
+ const 番号 = 段の番号(doc);
54
+ const 状態名 = sequenceStepId();
55
+ return {
56
+ ...built,
57
+ states: [...built.states, { id: 状態名, initial: "0" }],
58
+ phases: 段.map((p, i) => {
59
+ const 番 = 番号[i]!;
60
+ return {
61
+ id: `p${i}`,
62
+ duration: p.durationMs,
63
+ title: p.name,
64
+ body: p.body ?? "",
65
+ activate: [slugify(doc.title)],
66
+ ...(p.badge ? { badge: p.badge } : {}),
67
+ /*
68
+ * **書いた状態の動きも一緒に運ぶ** (#1466)。 板の段は「今どの言づてか」 を状態に
69
+ * 書くが、記法は同じ段に `遷移:` / `切替:` も書ける。 板の分だけを載せると、
70
+ * 書いた動きが黙って落ちる (実測で `tweens` が 0 件になっていた)。
71
+ */
72
+ sets: [
73
+ ...(p.sets ?? []).map((x) => ({ stateId: x.state, value: x.value })),
74
+ { stateId: 状態名, value: 番 },
75
+ ],
76
+ tweens: (p.tweens ?? []).map((t) => ({ stateId: t.state, from: t.from, to: t.to })),
77
+ };
78
+ }),
79
+ };
80
+ }
81
+
82
+ /** 名前が同じ箱を指すか。 書いた名前のほか、 id の形 (`api-gateway`) で書いた指定も受ける */
83
+ function 同じ箱(a: string, b: string): boolean {
84
+ return a === b || slugify(a) === slugify(b);
85
+ }
86
+
87
+ type 言づて = { from: string; to: string };
88
+
89
+ /**
90
+ * 段の `focus:` を、 言づてが合うかを返す関数にする。 何も書いていない段は `undefined`。
91
+ *
92
+ * | 書いたもの | 合う言づて |
93
+ * |---|---|
94
+ * | 矢印 `A -> B` | 元が A で先が B |
95
+ * | 箱の名前 2 つ以上 | 元と先の両方が書いた箱に含まれる |
96
+ * | 箱の名前 1 つ | 元か先がその箱 |
97
+ *
98
+ * 矢印は 1 本ずつ、 箱の名前はまとめて 1 件として扱い、 どれか 1 つに合えば合うとする。
99
+ *
100
+ * **矢印を 1 本でも書いた段は、 矢印だけで言づてを選ぶ**。 同じ段に並べた箱の名前は縦線を
101
+ * 光らせるための指定で、 言づてを選ぶには広すぎる。 `["利用者側 -> 処理側", 利用者側]` の
102
+ * 箱の名前まで数えると、 利用者側が関わる全ての言づてに合い、 矢印で指した言づてが決まらない
103
+ * (見本 `websocket` / `retryBackoff` で実測)。
104
+ */
105
+ function 合い方(
106
+ focus: readonly string[],
107
+ 箱の名前: ReadonlySet<string>,
108
+ ): ((f: 言づて) => boolean) | undefined {
109
+ const 合う: ((f: 言づて) => boolean)[] = [];
110
+ const 名前: string[] = [];
111
+ for (const raw of focus) {
112
+ const e = parseFocusEntry(raw, 箱の名前);
113
+ if (e.kind === "edge") 合う.push((f) => 同じ箱(f.from, e.from) && 同じ箱(f.to, e.to));
114
+ else 名前.push(e.name);
115
+ }
116
+ const 矢印がある = 合う.length > 0;
117
+ if (!矢印がある && 名前.length === 1) {
118
+ const n = 名前[0]!;
119
+ 合う.push((f) => 同じ箱(f.from, n) || 同じ箱(f.to, n));
120
+ } else if (!矢印がある && 名前.length >= 2) {
121
+ const 含む = (x: string) => 名前.some((n) => 同じ箱(x, n));
122
+ 合う.push((f) => 含む(f.from) && 含む(f.to));
123
+ }
124
+ return 合う.length === 0 ? undefined : (f) => 合う.some((c) => c(f));
125
+ }
126
+
127
+ /**
128
+ * 切り分けの良さ。 前から順に比べ、 大きい方を良いとする。
129
+ *
130
+ * [合う言づてを 1 通以上持つ段の数, 段に合う言づての数, 合った言づてを置いた段の広さの合計に負号を付けたもの]
131
+ */
132
+ type 得点 = readonly [number, number, number];
133
+
134
+ function 上回る(x: 得点, y: 得点): boolean {
135
+ for (let i = 0; i < x.length; i++) if (x[i] !== y[i]) return x[i]! > y[i]!;
136
+ return false;
137
+ }
138
+
139
+ /**
140
+ * 段ごとに、 板が今どの言づてを強調するかを導く (#2133)。
141
+ *
142
+ * 板は番号の言づてを強調し、 それより前を描き済みとして残し、 後ろを隠す。
143
+ *
144
+ * **言づてを先頭から段の数に切り分ける**。 `focus:` を書いた段ごとに続きの言づてを 1 区間ずつ
145
+ * 受け持たせ、 次の順で最も良い切り分けを選ぶ。
146
+ *
147
+ * 1. 合う言づてを 1 通以上持つ段が多い (どの段も板を進める)
148
+ * 2. 段に合う言づての数が多い (段が束ねる言づてを、 束ねた段に置く)
149
+ * 3. 両方の段に合う言づてを、 狭く指した段に置く。 段の広さは、 図の全ての言づてのうち
150
+ * その段に合う数
151
+ * 4. それでも並んだら、 前の段の区間を短くする
152
+ *
153
+ * 段の番号は、 その区間で `focus:` に合う最後の言づてにする。 箱の名前で書いた段は複数の
154
+ * 言づてを束ね、 見本の段の題は束の最後を指している (`[利用者, 認証窓口, DB]` の段の題は 2 通目の
155
+ * 「照合」)。
156
+ *
157
+ * 前の段から順に「合う最初の言づて」 を取る形では、 次の 3 つを同時に解けない。
158
+ *
159
+ * | 形 | 段の `focus:` | 起きること | 決める順 |
160
+ * |---|---|---|---|
161
+ * | 見本 `cacheReadThrough` (直す前) | `[利用者側, API, 一時置き場, DB]` → `[API, 一時置き場]` | 次の段が 2 通目 (探す) を先に取り、 前の段が 1 通目で止まる | 2 |
162
+ * | 見本 `rateLimit` | `[利用者側, 流量制限, API]` → `[利用者側, 流量制限]` | 5 通目は両方に合う。 前の段が取ると、 次の段の題「超過」 の前の要求が前の段で出る | 3 |
163
+ * | まとめの段 | `[C -> A]` → `[A -> B, B -> C, C -> A]` | 3 通目は両方に合う。 まとめの段が取ると `C -> A` の段が進まない | 3 |
164
+ *
165
+ * **合う言づてが無い段は前の段の番号を保つ**。 見出しだけの段や、 描いた矢印をまとめて並べる
166
+ * 段で板が 0 へ巻き戻らないようにする。
167
+ *
168
+ * 以前は「元 -> 先」 の組から番号を引く表だけを持っていた。 箱の名前は表に無いので全ての段が
169
+ * 0 になり (カタログの見本 30 件のうち 26 件)、 同じ組の言づてが複数あると表が最後の番号で
170
+ * 上書きされて、 最初の段で最後の言づてまで飛んでいた (3 件)。
171
+ */
172
+ export function 段の番号(doc: DslDocument): number[] {
173
+ const 段 = doc.animate?.phases ?? [];
174
+ const 箱の名前 = new Set(doc.actors.map((a) => a.name));
175
+ const 合う = 段.map((p) => 合い方(p.highlight ?? [], 箱の名前));
176
+ const 対象 = 合う.flatMap((c, k) => (c ? [k] : []));
177
+ const n = doc.flow.length;
178
+ const m = 対象.length;
179
+
180
+ // 累計[t][i] = 対象 t の段に、 先頭から i 通のうち何通が合うか。 累計[t][n] がその段の広さ
181
+ const 累計 = 対象.map((k) => {
182
+ const c = 合う[k]!;
183
+ const a = [0];
184
+ doc.flow.forEach((f, i) => a.push(a[i]! + (c(f) ? 1 : 0)));
185
+ return a;
186
+ });
187
+
188
+ // 最良[t][a] = 対象 t 以降の段で a 通目以降を切り分けた時の最良。 切れ目[t][a] = 対象 t の区間の終わり
189
+ const 最良: 得点[][] = Array.from({ length: m + 1 }, () => Array<得点>(n + 1).fill([0, 0, 0]));
190
+ const 切れ目: number[][] = Array.from({ length: m }, () => Array<number>(n + 1).fill(n));
191
+ for (let t = m - 1; t >= 0; t--) {
192
+ const 広さ = 累計[t]![n]!;
193
+ for (let a = 0; a <= n; a++) {
194
+ // 最後の段は残りを全て受け持つ。 短い区間から試し、 上回った時だけ置き換える (並んだら短い方)
195
+ for (let b = t === m - 1 ? n : a; b <= n; b++) {
196
+ const 数 = 累計[t]![b]! - 累計[t]![a]!;
197
+ const 次 = 最良[t + 1]![b]!;
198
+ const s: 得点 = [(数 > 0 ? 1 : 0) + 次[0], 数 + 次[1], -数 * 広さ + 次[2]];
199
+ if (b === (t === m - 1 ? n : a) || 上回る(s, 最良[t]![a]!)) {
200
+ 最良[t]![a] = s;
201
+ 切れ目[t]![a] = b;
202
+ }
203
+ }
204
+ }
205
+ }
206
+
207
+ const 番号 = new Map<number, number>();
208
+ let a = 0;
209
+ for (let t = 0; t < m; t++) {
210
+ const b = 切れ目[t]![a]!;
211
+ const c = 合う[対象[t]!]!;
212
+ for (let j = b - 1; j >= a; j--) {
213
+ if (c(doc.flow[j]!)) {
214
+ 番号.set(対象[t]!, j);
215
+ break;
216
+ }
217
+ }
218
+ a = b;
219
+ }
220
+
221
+ let 今 = 0;
222
+ return 段.map((_, k) => {
223
+ 今 = 番号.get(k) ?? 今;
224
+ return 今;
225
+ });
226
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * 名前から id を作る (#2030 で `compile.ts` から移した)。
3
+ *
4
+ * この repo で最も広く使われる小道具で、組み立ての全域から 37 箇所が呼ぶ。
5
+ *
6
+ * **葉に置く** = ここが他の file を 1 つも取り込まないことで、どこから呼んでも取り込みの輪が
7
+ * できない。 図種ごとの file と `compile.ts` の両方が呼ぶため、どちらかの側に置くと
8
+ * もう片方から逆向きの取り込みが生まれる。
9
+ */
10
+
11
+ /** id の長さの上限。 これを超える名前は切り詰める */
12
+ export const ID_MAX = 64;
13
+
14
+ /**
15
+ * 名前を id に直す。
16
+ *
17
+ * 英数字と日本語と `-` `_` だけを残し、それ以外は `-` にまとめる。 前後の `-` は落とす。
18
+ * 何も残らなければ `n` を返す = 空の id は図の参照先にできないため。
19
+ */
20
+ export function slugify(s: string): string {
21
+ return (
22
+ s
23
+ .toLowerCase()
24
+ .normalize("NFKC")
25
+ .replace(/[^a-z0-9ぁ-んァ-ヶ一-龯\-_]+/g, "-")
26
+ .replace(/^-+|-+$/g, "")
27
+ .slice(0, ID_MAX) || "n"
28
+ );
29
+ }
@@ -0,0 +1,39 @@
1
+ import type { CdlDiagram } from "@cardenelabs/cdl";
2
+ import type { DslDocument } from "../types";
3
+ import { compileSequence } from "./sequence";
4
+ /**
5
+ * Solidity 専用 preset。
6
+ *
7
+ * 設計:
8
+ * - actors を kind=contract / eoa / multisig 等で配置 (EOA は左、 contract は中央、 storage は右など layered layout)
9
+ * - flow は function call の sequence (msg.sender → contract.fn() → internal call → emit event)
10
+ * - storage 更新は state + rows binding で自動 (kind: storage の actor に rows: ["bal[A]: {balA}", ...])
11
+ * - event は kind: event の actor を右端に並べ、 emit edge で発火を表現
12
+ * - revert は tone: error の edge で表現
13
+ *
14
+ * sequence preset を base に使い、 Solidity 文脈に最適化した default を載せる:
15
+ * - default tone: accent (call) / success (emit) / error (revert)
16
+ * - default style: solid (call) / dotted-flow (state-change)
17
+ */
18
+ export function compileSolidity(doc: DslDocument): CdlDiagram {
19
+ // sequence preset と同等構造で組み立てる、 lane 順は eoa / contract / storage / event の優先順で sort
20
+ const kindOrder: Record<string, number> = {
21
+ eoa: 0,
22
+ actor: 0,
23
+ multisig: 0,
24
+ signer: 0,
25
+ wallet: 0,
26
+ contract: 1,
27
+ proxy: 1,
28
+ library: 1,
29
+ interface: 1,
30
+ storage: 2,
31
+ event: 3,
32
+ };
33
+ const sorted = [...doc.actors].sort(
34
+ (a, b) => (kindOrder[a.kind] ?? 5) - (kindOrder[b.kind] ?? 5),
35
+ );
36
+ // sorted を doc.actors に上書きしてから sequence preset 経由で compile
37
+ const sortedDoc: DslDocument = { ...doc, actors: sorted };
38
+ return compileSequence(sortedDoc);
39
+ }
@@ -0,0 +1,71 @@
1
+ import { stateMachine } from "@cardenelabs/cdl";
2
+ import type { NodeKind } from "@cardenelabs/cdl";
3
+ import { 箱の題 } from "./node-title";
4
+ import type { CdlDiagram } from "@cardenelabs/cdl";
5
+ import type { DslDocument } from "../types";
6
+
7
+ import { compileGenericWithAnimate } from "./generic";
8
+ import { 描ける種別 } from "./kinds";
9
+
10
+ import { slugify } from "./slug";
11
+ import { 始まりと終わりの決め方 } from "./actors";
12
+ export function compileState(doc: DslDocument): CdlDiagram {
13
+ // v0.4 ... animation あり時 builder 直接経路 (各 state を lane で配置、 transition を edge)
14
+ //
15
+ // **種類を書いた形は動きの有無に関わらず generic 経路へ** (#1450、 #1263 と同じ理由)。
16
+ // 動く図だけで効かせると、同じ記法でも静止図では指定が黙って消える
17
+ // (実測 = `kind: mark-start` を書いた箱が `card` になり知らせも出ない)
18
+ if ((doc.animate && doc.animate.phases.length > 0) || 既定と違う種類を書いた(doc)) {
19
+ return compileGenericWithAnimate(doc, { kind: "state", laneWidth: 360 });
20
+ }
21
+ // stateMachine preset ... actors を state に、 流れ を transition に
22
+ const fsm = stateMachine({
23
+ id: slugify(doc.title),
24
+ topic: doc.title,
25
+ });
26
+ const 決め方 = 始まりと終わりの決め方(doc);
27
+ for (let i = 0; i < doc.actors.length; i++) {
28
+ const a = doc.actors[i]!;
29
+ // 書いた `initial:` / `final:` が勝つ。 1 つも書いていなければ順序で決める
30
+ const initial = 決め方.始まり(a, i);
31
+ const final = 決め方.終わり(a, i);
32
+ fsm.state({
33
+ id: slugify(a.name) || `s${i}`,
34
+ title: 箱の題(a),
35
+ ...(initial ? { initial: true } : {}),
36
+ ...(final ? { final: true } : {}),
37
+ });
38
+ }
39
+ for (const s of doc.flow) {
40
+ fsm.transition({
41
+ from: slugify(s.from),
42
+ to: slugify(s.to),
43
+ trigger: s.label,
44
+ ...(s.sub ? { guard: s.sub } : {}),
45
+ ...(s.tone ? { tone: s.tone } : {}),
46
+ });
47
+ }
48
+ return fsm.build();
49
+ }
50
+
51
+ /** 状態の図で `stateMachine` preset が全ての箱に使う種類 */
52
+ export const 状態の図の既定の種類: NodeKind = "card";
53
+
54
+ /**
55
+ * 状態の図で、既定と違う種類を書いた登場人物がいるか (#1450)。
56
+ *
57
+ * `stateMachine` preset は全ての状態を `card` で描くため、書いた `kind:` が黙って消える。
58
+ * 始点終点の印 (`mark-start` / `mark-end`) を書けるようにするには、書いた形を効かせる必要がある。
59
+ *
60
+ * **「書いたか」 だけでは広すぎる**。 記法だけの種類 (`kind: state` 等) は `card` に読み替わり、
61
+ * preset の出す形と同じになる = 書いても何も変わらないので経路を切り替える理由が無い。
62
+ * 切り替えると既存の図の id と枠の作りが変わる (実測で golden 8 件が落ちた)。
63
+ *
64
+ * 読み替えた後の種類が既定と違う時だけ切り替える。 こうすると「書いたのに効かない」 は消え、
65
+ * 「書いたが結果が同じ」 は従来の経路に留まる。
66
+ */
67
+ export function 既定と違う種類を書いた(doc: DslDocument): boolean {
68
+ return doc.actors.some(
69
+ (a) => a.kindWritten === true && 描ける種別(a.kind) !== 状態の図の既定の種類,
70
+ );
71
+ }
@@ -0,0 +1,22 @@
1
+ import type { DslDocument } from "../types";
2
+ /**
3
+ * 図の小見出しと、知らせに出す字の切り詰め (#2030 で `compile.ts` から移した)。
4
+ *
5
+ * 7 図種が小見出しを使う。 切り詰めは知らせを出す全域が使う。
6
+ */
7
+
8
+ /**
9
+ * 図全体を 1 箱にする図種で、 その箱の上に出す小見出しを渡す (#1247)。
10
+ *
11
+ * 書かなければ何も渡さない = 従来どおり小見出しは付かない。 `undefined` を明示して渡すと、
12
+ * 組立て側が「空の小見出しを書いた」 と区別できなくなるので、 項目ごと落とす。
13
+ */
14
+ export function 図の小見出し(doc: DslDocument): { eyebrow?: string } {
15
+ return doc.eyebrow === undefined ? {} : { eyebrow: doc.eyebrow };
16
+ }
17
+
18
+ /** 知らせに載せる値を短く切る。 長い URL をそのまま出すと画面の帯が読めなくなる */
19
+ export function truncateForMessage(v: string): string {
20
+ const s = v.trim();
21
+ return s.length <= 40 ? s : `${s.slice(0, 37)}...`;
22
+ }
@@ -0,0 +1,83 @@
1
+ import { swimlane } from "@cardenelabs/cdl";
2
+ import type { CdlDiagram } from "@cardenelabs/cdl";
3
+ import type { DslDocument } from "../types";
4
+ import { compileGenericWithAnimate } from "./generic";
5
+ import { 描ける種別 } from "./kinds";
6
+ import { 書いた縦列に置く } from "./lanes";
7
+ import { 箱の題 } from "./node-title";
8
+ import { slugify } from "./slug";
9
+ export function compileSwimlane(doc: DslDocument): CdlDiagram {
10
+ // v0.4 ... animation あり時 builder 直接経路 (各 actor 別 lane で配置)
11
+ // **縦列を書いた形は動きの有無に関わらず generic 経路へ** (#1263)。 動く図だけで効かせると、
12
+ // 同じ記法でも静止図では指定が黙って消える (実測 = 縦列 3 本のはずが 1 本になり知らせも出ない)
13
+ // **向きを書いた形も generic 経路へ** (#1494)。 静止図の経路は並びを固定で持つので、
14
+ // ここを通さないと書いた向きが黙って消える (縦列を書いた形と同じ理由)
15
+ if (
16
+ (doc.animate && doc.animate.phases.length > 0) ||
17
+ 書いた縦列に置く("swimlane", doc) ||
18
+ doc.direction !== undefined
19
+ ) {
20
+ return compileGenericWithAnimate(doc, { kind: "swimlane", laneWidth: 400 });
21
+ }
22
+ // swimlane preset は lane 配置 + 自由 node/edge。
23
+ // v0.2 では actors を lane 化、 流れ の各 step から node を生成、 edge を引く。
24
+ const swim = swimlane({
25
+ id: slugify(doc.title),
26
+ topic: doc.title,
27
+ lanes: doc.actors.map((a) => a.name),
28
+ });
29
+
30
+ // 各 step で from / to の node を lane 内 stack 配置
31
+ const placedNodes = new Set<string>();
32
+ const laneStackCount = new Map<string, number>();
33
+ let edgeIdx = 0;
34
+
35
+ for (const s of doc.flow) {
36
+ for (const actorName of [s.from, s.to]) {
37
+ if (placedNodes.has(actorName)) continue;
38
+ const laneId = swim.laneId(actorName);
39
+ const actor = doc.actors.find((a) => a.name === actorName);
40
+ const stack = laneStackCount.get(laneId) ?? 0;
41
+ const nodeId = slugify(actorName) || `n${placedNodes.size}`;
42
+ swim.node(nodeId, {
43
+ lane: laneId,
44
+ stack,
45
+ kind: 描ける種別(actor?.kind),
46
+ title: actorName,
47
+ });
48
+ laneStackCount.set(laneId, stack + 1);
49
+ placedNodes.add(actorName);
50
+ }
51
+ const fromId = slugify(s.from);
52
+ const toId = slugify(s.to);
53
+ swim.edge(fromId, toId, {
54
+ id: `e${edgeIdx++}-${fromId}-${toId}`,
55
+ label: s.label,
56
+ ...(s.sub ? { sub: s.sub } : {}),
57
+ ...(s.side ? { side: s.side } : {}),
58
+
59
+ ...(s.tone ? { tone: s.tone } : {}),
60
+ ...(s.style ? { style: s.style } : {}),
61
+ });
62
+ }
63
+
64
+ // **箱が 1 つも置けなかった時は、 登場人物をそのまま置く** (#1219)。
65
+ //
66
+ // この図種は矢印の端から箱を作るため、 矢印が 1 本も無いと箱が 0 件になり `compile` が
67
+ // 落ちる (実測 = 矢印を書かない図は本 Issue の前から落ちていた)。 解決できない矢印を
68
+ // 外すと同じ形になるので、 受け皿を置く。
69
+ //
70
+ // **1 つでも置けた時は触らない**。 矢印に出てこない登場人物にも箱を置くと、 今まで枠だけ
71
+ // だった所に箱が増えて既存の図の見た目が変わる (それを変えるかは別の判断)。
72
+ if (placedNodes.size === 0) {
73
+ doc.actors.forEach((a, i) => {
74
+ swim.node(slugify(a.name) || `n${i}`, {
75
+ lane: swim.laneId(a.name),
76
+ stack: 0,
77
+ kind: 描ける種別(a.kind),
78
+ title: 箱の題(a),
79
+ });
80
+ });
81
+ }
82
+ return swim.build();
83
+ }