@cardenelabs/dragon 0.28.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.
- package/README.md +78 -5
- package/dist/index.cjs +9246 -8698
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +186 -66
- package/dist/index.d.ts +186 -66
- package/dist/index.js +9247 -8700
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/compile/actor-ids.ts +306 -0
- package/src/compile/actors.ts +74 -0
- package/src/compile/c4.ts +77 -0
- package/src/compile/chain.ts +54 -0
- package/src/compile/chart-fields.ts +178 -0
- package/src/compile/class.ts +100 -0
- package/src/compile/deep-rewrite.ts +30 -0
- package/src/compile/derived-source.ts +18 -0
- package/src/compile/direction.ts +63 -0
- package/src/compile/er-relation.ts +200 -0
- package/src/compile/er.ts +44 -0
- package/src/compile/flow.ts +54 -0
- package/src/compile/funnel.ts +75 -0
- package/src/compile/gantt.ts +232 -0
- package/src/compile/generic.ts +284 -0
- package/src/compile/hierarchy.ts +92 -0
- package/src/compile/journey.ts +84 -0
- package/src/compile/kinds.ts +156 -0
- package/src/compile/lanes.ts +56 -0
- package/src/compile/mind.ts +343 -0
- package/src/compile/node-title.ts +24 -0
- package/src/compile/notice.ts +94 -0
- package/src/compile/parts-lookup.ts +17 -0
- package/src/compile/parts.ts +2635 -0
- package/src/compile/placement-apply.ts +673 -0
- package/src/compile/placement.ts +15 -0
- package/src/compile/quadrant.ts +110 -0
- package/src/compile/rows.ts +40 -0
- package/src/compile/sequence.ts +226 -0
- package/src/compile/slug.ts +29 -0
- package/src/compile/solidity.ts +39 -0
- package/src/compile/state.ts +71 -0
- package/src/compile/subtitle.ts +22 -0
- package/src/compile/swimlane.ts +83 -0
- package/src/compile/topology.ts +50 -0
- package/src/compile/tree.ts +60 -0
- package/src/compile/value-chart.ts +183 -0
- package/src/compile/values.ts +432 -0
- package/src/compile/word-state.ts +82 -0
- package/src/compile.ts +513 -6858
- package/src/index.ts +8 -7
- package/src/json-line-map.ts +30 -0
- package/src/json-parser.ts +356 -123
- package/src/near-name.ts +42 -0
- package/src/relative-pos.ts +71 -2
- package/src/schema.ts +1 -1
- package/src/schemas/diagram.json +38 -4
- package/src/types.ts +1 -1
- package/src/v05/parser.ts +91 -72
package/package.json
CHANGED
|
@@ -0,0 +1,306 @@
|
|
|
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
|
+
* | 残る 6 図種 | 箱 `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
|
+
solidity: ["actor-"],
|
|
93
|
+
swimlane: ["lane-"],
|
|
94
|
+
flow: [],
|
|
95
|
+
er: [],
|
|
96
|
+
state: [],
|
|
97
|
+
topology: [],
|
|
98
|
+
class: [],
|
|
99
|
+
c4: [],
|
|
100
|
+
// 図全体を 1 箱で描く群 (作り替えないのでここは使わない)
|
|
101
|
+
gantt: [],
|
|
102
|
+
pie: [],
|
|
103
|
+
bar: [],
|
|
104
|
+
line: [],
|
|
105
|
+
gauge: [],
|
|
106
|
+
radial: [],
|
|
107
|
+
stat: [],
|
|
108
|
+
waffle: [],
|
|
109
|
+
stacked: [],
|
|
110
|
+
slope: [],
|
|
111
|
+
funnel: [],
|
|
112
|
+
tree: [],
|
|
113
|
+
journey: [],
|
|
114
|
+
quadrant: [],
|
|
115
|
+
mind: [],
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* その名前が下流で id になりうる形。 どれか 1 つでも重なれば衝突する。
|
|
120
|
+
*
|
|
121
|
+
* 位置と逃げ先が分からない時 (作り替えた候補を確かめる時) は逃げ先を数えない。 作り替えた
|
|
122
|
+
* 名前は尾が付いて形が空にならないので、 そもそも逃げ道を通らない。
|
|
123
|
+
*/
|
|
124
|
+
function idになる形(name: string, 位置?: number, 逃げ先の頭: string[] = []): string[] {
|
|
125
|
+
const cdl = cdl側のslug(name);
|
|
126
|
+
const out = [slugify(name), cdl];
|
|
127
|
+
if (cdl === "" && 位置 !== undefined) {
|
|
128
|
+
for (const 頭 of 逃げ先の頭) out.push(`${頭}${位置}`);
|
|
129
|
+
}
|
|
130
|
+
return out;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function disambiguateActorIds(
|
|
134
|
+
doc: DslDocument,
|
|
135
|
+
onNotice?: (notice: CompileNotice) => void,
|
|
136
|
+
): { doc: DslDocument; 元の名前: Map<string, string> } {
|
|
137
|
+
const 元の名前 = new Map<string, string>();
|
|
138
|
+
if (図種の作り[doc.type] !== "登場人物ごとに箱") return { doc, 元の名前 };
|
|
139
|
+
|
|
140
|
+
// 1. まったく同じ名前を畳む
|
|
141
|
+
const 見た = new Set<string>();
|
|
142
|
+
const 残す: DslActor[] = [];
|
|
143
|
+
for (const a of doc.actors) {
|
|
144
|
+
if (見た.has(a.name)) {
|
|
145
|
+
onNotice?.({
|
|
146
|
+
kind: "chart-value-unreadable",
|
|
147
|
+
actor: a.name,
|
|
148
|
+
line: a.pos?.line ?? 0,
|
|
149
|
+
message: `"${truncateForMessage(a.name)}" を 2 度書いています (先に書いた方だけ描きます)`,
|
|
150
|
+
hint: "違う名前にするか、 1 つにまとめる",
|
|
151
|
+
});
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
見た.add(a.name);
|
|
155
|
+
残す.push(a);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// 2. どの名前が重なるかを見る。 **両方の規則で見る** = 片方だけだと cdl 側の経路で落ちる
|
|
159
|
+
const 形ごとの名前 = new Map<string, Set<string>>();
|
|
160
|
+
const 位置 = new Map<string, number>();
|
|
161
|
+
残す.forEach((a, i) => 位置.set(a.name, i));
|
|
162
|
+
// **動きを書いた図では cdl の逃げ道を通らない** (Round 4 の指摘)。 動きがあると組み立てが
|
|
163
|
+
// 別経路 (`compileGenericWithAnimate` / `compileSequenceWithAnimate`) に切り替わり、 id は
|
|
164
|
+
// dragon 側の規則で作られる (実測 = `😀` は 枠 `n` / `lane-n` になり、 位置を使わない)。
|
|
165
|
+
//
|
|
166
|
+
// 逃げ先を鍵に入れたままにすると、 その経路で **重なっていない図の id を変える**。
|
|
167
|
+
// dragon 側の規則で作る分は、 1 つ目の鍵 (`slugify`) が既に見ている。
|
|
168
|
+
const 動きを書いた = (doc.animate?.phases.length ?? 0) > 0;
|
|
169
|
+
const 逃げ先の頭 = 動きを書いた ? [] : 空の形の逃げ先[doc.type];
|
|
170
|
+
for (const a of 残す) {
|
|
171
|
+
for (const 形 of idになる形(a.name, 位置.get(a.name), 逃げ先の頭)) {
|
|
172
|
+
const 群 = 形ごとの名前.get(形) ?? new Set<string>();
|
|
173
|
+
群.add(a.name);
|
|
174
|
+
形ごとの名前.set(形, 群);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
const 重なる = (name: string): boolean =>
|
|
178
|
+
idになる形(name, 位置.get(name), 逃げ先の頭).some(
|
|
179
|
+
(形) => (形ごとの名前.get(形)?.size ?? 0) > 1,
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
// **見本 (`parts`) を重ねた登場人物は作り替えない**。 見本の中身は `別名__元の id` の形で
|
|
183
|
+
// 名前空間を持ち、 別名は名前から作るため、 作り替えると見本の id が総入れ替えになる。
|
|
184
|
+
//
|
|
185
|
+
// ただし **重なりの判定には数える** (Round 1 の指摘)。 数えないと、 素の登場人物と見本が
|
|
186
|
+
// 同じ id の仮置きを共有し、 見本を片付ける時に素の登場人物の箱まで消える。
|
|
187
|
+
const 作り替える = 残す.filter((a) => a.partId === undefined && 重なる(a.name));
|
|
188
|
+
|
|
189
|
+
// 3. 名前から決まる尾を付ける。 **できあがる id が一意になるまで見る**
|
|
190
|
+
//
|
|
191
|
+
// 尾は元の名前だけから決まる = 並べ替えても同じ id になる。 それでも重なる時 (尾そのものが
|
|
192
|
+
// 重なる形) は、 名前を並べ替えた順で番号を足す = ここも書き順に依らない
|
|
193
|
+
const 使う形 = new Set<string>();
|
|
194
|
+
for (const a of 残す) {
|
|
195
|
+
if (作り替える.some((b) => b.name === a.name)) continue;
|
|
196
|
+
for (const 形 of idになる形(a.name)) 使う形.add(形);
|
|
197
|
+
}
|
|
198
|
+
const 新しい名前 = new Map<string, string>();
|
|
199
|
+
// 名前で並べてから配る = 書いた順に依らない
|
|
200
|
+
for (const a of [...作り替える].sort((x, y) =>
|
|
201
|
+
x.name < y.name ? -1 : x.name > y.name ? 1 : 0,
|
|
202
|
+
)) {
|
|
203
|
+
const 尾 = 名前の尾(a.name);
|
|
204
|
+
// **id の長さの上限のぶん、 元の名前を先に切る** (Round 1 の指摘)。 切らないと尾が
|
|
205
|
+
// 64 字で落ちて、 同じ頭を持つ長い名前どうしが元のまま重なる
|
|
206
|
+
const 余地 = ID_MAX - (尾.length + 1);
|
|
207
|
+
const 基底 = a.name.slice(0, 余地);
|
|
208
|
+
let 候補 = `${基底} ${尾}`;
|
|
209
|
+
let n = 0;
|
|
210
|
+
while (idになる形(候補).some((形) => 使う形.has(形))) {
|
|
211
|
+
n += 1;
|
|
212
|
+
候補 = `${基底.slice(0, 余地 - String(n).length)} ${尾}${n}`;
|
|
213
|
+
}
|
|
214
|
+
for (const 形 of idになる形(候補)) 使う形.add(形);
|
|
215
|
+
新しい名前.set(a.name, 候補);
|
|
216
|
+
元の名前.set(候補, a.name);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (新しい名前.size === 0 && 残す.length === doc.actors.length) return { doc, 元の名前 };
|
|
220
|
+
|
|
221
|
+
const 直す = (名: string): string => 新しい名前.get(名) ?? 名;
|
|
222
|
+
const 次: DslDocument = {
|
|
223
|
+
...doc,
|
|
224
|
+
actors: 残す.map((a) => (新しい名前.has(a.name) ? { ...a, name: 直す(a.name) } : a)),
|
|
225
|
+
flow: doc.flow.map((s) => {
|
|
226
|
+
const from = 直す(s.from);
|
|
227
|
+
const to = 直す(s.to);
|
|
228
|
+
return from === s.from && to === s.to ? s : { ...s, from, to };
|
|
229
|
+
}),
|
|
230
|
+
events: doc.events?.map((event) => {
|
|
231
|
+
const target = event.target;
|
|
232
|
+
if (target.kind === "node") {
|
|
233
|
+
const name = 直す(target.name);
|
|
234
|
+
return name === target.name ? event : { ...event, target: { ...target, name } };
|
|
235
|
+
}
|
|
236
|
+
if (target.kind === "edge") {
|
|
237
|
+
const from = 直す(target.from);
|
|
238
|
+
const to = 直す(target.to);
|
|
239
|
+
return from === target.from && to === target.to
|
|
240
|
+
? event
|
|
241
|
+
: { ...event, target: { ...target, from, to } };
|
|
242
|
+
}
|
|
243
|
+
// sequence 系の縦列は登場人物から作るため、同じ名前の読み替えが必要。
|
|
244
|
+
if (target.kind === "lane" && (doc.type === "sequence" || doc.type === "solidity")) {
|
|
245
|
+
const name = 直す(target.name);
|
|
246
|
+
return name === target.name ? event : { ...event, target: { ...target, name } };
|
|
247
|
+
}
|
|
248
|
+
return event;
|
|
249
|
+
}),
|
|
250
|
+
};
|
|
251
|
+
// 光らせる指定と位置の基準も名前で書くので、 同じ表で直す
|
|
252
|
+
if (次.animate) {
|
|
253
|
+
次.animate = {
|
|
254
|
+
...次.animate,
|
|
255
|
+
phases: 次.animate.phases.map((ph) =>
|
|
256
|
+
ph.highlight ? { ...ph, highlight: ph.highlight.map((h) => 直す(h)) } : ph,
|
|
257
|
+
),
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
次.actors = 次.actors.map((a) =>
|
|
261
|
+
a.posRel ? { ...a, posRel: { ...a.posRel, anchor: 直す(a.posRel.anchor) } } : a,
|
|
262
|
+
);
|
|
263
|
+
return { doc: 次, 元の名前 };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* 作り替えた名前を持つ箱と枠を控える (#1220)。
|
|
268
|
+
*
|
|
269
|
+
* **組み立て直後に控える** (Round 1 の指摘)。 出口で題の文字だけを見て戻すと、 後から足された
|
|
270
|
+
* 見本の中の箱がたまたま同じ題を持っていた時に、 その表示まで書き換えてしまう。
|
|
271
|
+
*/
|
|
272
|
+
export function collectRenamedTargets(
|
|
273
|
+
diagram: CdlDiagram,
|
|
274
|
+
元の名前: Map<string, string>,
|
|
275
|
+
): { 箱: Set<string>; 枠: Set<string> } {
|
|
276
|
+
const 箱 = new Set<string>();
|
|
277
|
+
const 枠 = new Set<string>();
|
|
278
|
+
if (元の名前.size === 0) return { 箱, 枠 };
|
|
279
|
+
for (const n of diagram.nodes) if (元の名前.has(n.title)) 箱.add(n.id);
|
|
280
|
+
for (const l of diagram.lanes) if (l.label !== undefined && 元の名前.has(l.label)) 枠.add(l.id);
|
|
281
|
+
return { 箱, 枠 };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* 作り替えた名前を、 図の表示だけ元に戻す (#1220)。
|
|
286
|
+
*
|
|
287
|
+
* 戻すのは題と名札だけ。 id は作り替えたまま = 分けるために作り替えたので、 戻すと元の
|
|
288
|
+
* 重なりに帰る。
|
|
289
|
+
*/
|
|
290
|
+
export function restoreActorNames(
|
|
291
|
+
diagram: CdlDiagram,
|
|
292
|
+
元の名前: Map<string, string>,
|
|
293
|
+
対象: { 箱: Set<string>; 枠: Set<string> },
|
|
294
|
+
): void {
|
|
295
|
+
if (元の名前.size === 0) return;
|
|
296
|
+
for (const n of diagram.nodes) {
|
|
297
|
+
if (!対象.箱.has(n.id)) continue;
|
|
298
|
+
const 元 = 元の名前.get(n.title);
|
|
299
|
+
if (元 !== undefined) n.title = 元;
|
|
300
|
+
}
|
|
301
|
+
for (const l of diagram.lanes) {
|
|
302
|
+
if (!対象.枠.has(l.id) || l.label === undefined) continue;
|
|
303
|
+
const 元 = 元の名前.get(l.label);
|
|
304
|
+
if (元 !== undefined) l.label = 元;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { DslActor, DslDocument } from "../types";
|
|
2
|
+
import { slugify } from "./slug";
|
|
3
|
+
/**
|
|
4
|
+
* 登場人物の参照表と、並びの整え (#2030 で `compile.ts` から移した)。
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 矢印の指す先を `actors` に書いた **正規の名前** へ解決する表 (#1209)。
|
|
9
|
+
*
|
|
10
|
+
* 名前そのものに加えて **一意な slug も受ける**。 動きを書いた図の組み立ては `slugify` に
|
|
11
|
+
* 落として引くため、 `API Gateway` を `api-gateway` と書いた形が届く。 2 つ以上の名前が
|
|
12
|
+
* 同じ slug になる時は受けない = どちらを指したか決められない。
|
|
13
|
+
*
|
|
14
|
+
* **返すのは正規の名前で、 slug ではない**。 slug を返すと、 動きを書いていない図の組み立て
|
|
15
|
+
* (名前の完全一致で引く) と食い違う = 知らせは出ないのに label が消える / 題が slug に化ける /
|
|
16
|
+
* 依存が切れる、 という形になる (Round 1 で実測)。 中央で名前へ揃えれば全経路が同じ相手を指す。
|
|
17
|
+
*/
|
|
18
|
+
export function actorRefTable(doc: DslDocument): Map<string, string> {
|
|
19
|
+
const 表 = new Map<string, string>();
|
|
20
|
+
const slug数 = new Map<string, number>();
|
|
21
|
+
for (const a of doc.actors) {
|
|
22
|
+
const sl = slugify(a.name);
|
|
23
|
+
slug数.set(sl, (slug数.get(sl) ?? 0) + 1);
|
|
24
|
+
}
|
|
25
|
+
for (const a of doc.actors) {
|
|
26
|
+
表.set(a.name, a.name);
|
|
27
|
+
const sl = slugify(a.name);
|
|
28
|
+
if ((slug数.get(sl) ?? 0) === 1) 表.set(sl, a.name);
|
|
29
|
+
}
|
|
30
|
+
return 表;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 矢印の指す先を正規の名前へ揃えた `flow` を返す (#1209)。
|
|
35
|
+
*
|
|
36
|
+
* 解決できない矢印はそのまま残す = 図種ごとに扱いが違う (木は独自の知らせを出し、 値で描く図は
|
|
37
|
+
* 落とす)。 中央で消すとその扱いが効かなくなる。 残した分は `reportMissingFlowActors` が
|
|
38
|
+
* 知らせ、 動きを書いた図の組み立てが落とす。
|
|
39
|
+
*/
|
|
40
|
+
export function canonicalizeFlowActors(doc: DslDocument): DslDocument {
|
|
41
|
+
const 表 = actorRefTable(doc);
|
|
42
|
+
let 変えた = false;
|
|
43
|
+
const flow = doc.flow.map((s) => {
|
|
44
|
+
const from = 表.get(s.from) ?? s.from;
|
|
45
|
+
const to = 表.get(s.to) ?? s.to;
|
|
46
|
+
if (from === s.from && to === s.to) return s;
|
|
47
|
+
変えた = true;
|
|
48
|
+
return { ...s, from, to };
|
|
49
|
+
});
|
|
50
|
+
return 変えた ? { ...doc, flow } : doc;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 状態遷移図で、どの箱を始まり / 終わりとみなすか (#1275)。
|
|
55
|
+
*
|
|
56
|
+
* **書いた値が勝つ**。 `initial:` / `final:` は記法で書けるのに 1 度も読まれておらず、
|
|
57
|
+
* 位置だけで決まっていた (実測 = 中央の箱に `final: true` を書いても、最後に書いた箱が
|
|
58
|
+
* 「最終」 になった)。
|
|
59
|
+
*
|
|
60
|
+
* 1 つも書いていなければ従来どおり位置で決める = 書かない記法の図は変わらない。
|
|
61
|
+
* 片方だけ書いた形も、書いた側だけが切り替わる。
|
|
62
|
+
*/
|
|
63
|
+
export function 始まりと終わりの決め方(doc: DslDocument): {
|
|
64
|
+
始まり: (a: DslActor, idx: number) => boolean;
|
|
65
|
+
終わり: (a: DslActor, idx: number) => boolean;
|
|
66
|
+
} {
|
|
67
|
+
const 書いた始まり = doc.actors.some((a) => a.initial === true);
|
|
68
|
+
const 書いた終わり = doc.actors.some((a) => a.final === true);
|
|
69
|
+
return {
|
|
70
|
+
始まり: (a, idx) => (書いた始まり ? a.initial === true : idx === 0),
|
|
71
|
+
終わり: (a, idx) =>
|
|
72
|
+
書いた終わり ? a.final === true : idx === doc.actors.length - 1 && doc.actors.length > 1,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { diagram } from "@cardenelabs/cdl";
|
|
2
|
+
import type { CdlDiagram } from "@cardenelabs/cdl";
|
|
3
|
+
import type { DslDocument } from "../types";
|
|
4
|
+
import { 描ける種別 } from "./kinds";
|
|
5
|
+
import { 段を読み取る } from "./rows";
|
|
6
|
+
import { slugify } from "./slug";
|
|
7
|
+
/**
|
|
8
|
+
* C4 preset (C4 model 階層 system context 専用 layout)
|
|
9
|
+
*
|
|
10
|
+
* 設計 ... actor.subtitle の先頭に "L1" / "L2" / "L3" を置き、 階層 lane を生成。
|
|
11
|
+
* - L1 = 全体の見取り図 (C4 model の System Context)
|
|
12
|
+
* - L2 = 動かす単位 (同 Container = 単体で動かす application や保管庫)
|
|
13
|
+
* - L3 = 部品 (同 Component)
|
|
14
|
+
*
|
|
15
|
+
* 実装 ... **中身のある段だけ** lane を作り、 使う段を左から順に詰めて横並び (contain: true で
|
|
16
|
+
* 囲む) 配置する。 3 lane を常に作ると中身のない枠が画面に残り、 描かれ損ねたように見える (#1078)。
|
|
17
|
+
* 同 lane 内の actor は内部 stack で縦並びになる (横並びは layout 制約上不可、
|
|
18
|
+
* 段の区別が視覚的に最重要)。 subtitle marker 未指定なら L1 fallback。
|
|
19
|
+
*
|
|
20
|
+
* flow ... actor 間の関係を edge で表現。
|
|
21
|
+
*/
|
|
22
|
+
export function compileC4(doc: DslDocument): CdlDiagram {
|
|
23
|
+
const b = diagram(slugify(doc.title), { topic: doc.title, type: "infrastructure" });
|
|
24
|
+
const LANE_W = 400;
|
|
25
|
+
const LANE_GAP = 80;
|
|
26
|
+
// 段の名前は **枠に描く字** で、記法に打ち込む識別子ではない (#1886)。 打ち込む字は目印
|
|
27
|
+
// (`L1` / `L2` / `L3`) の側で、そちらは綴りを変えない
|
|
28
|
+
const 段の名前: Record<number, string> = { 1: "全体の見取り図", 2: "動かす単位", 3: "部品" };
|
|
29
|
+
|
|
30
|
+
// 段は **先頭一致** で読み、 読んだ目印は説明から落とす (`段を読み取る` の説明を参照)
|
|
31
|
+
const 割当 = doc.actors.map((a, idx) => {
|
|
32
|
+
const { 段, 説明 } = 段を読み取る(a.subtitle);
|
|
33
|
+
return { 段, 説明, id: slugify(a.name) || `n${idx}`, actor: a };
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// **中身のある段だけ枠を作る**。 3 段を必ず作ると、 書いていない段が空の点線枠として残り、
|
|
37
|
+
// 見た人には「何かが描かれ損ねた」 ようにしか見えない
|
|
38
|
+
const 使う段 = [1, 2, 3].filter((lv) => 割当.some((x) => x.段 === lv));
|
|
39
|
+
使う段.forEach((lv, i) => {
|
|
40
|
+
b.lane(`c4-l${lv}`, {
|
|
41
|
+
// 空の段を飛ばした分だけ左に詰める。 飛ばした位置に隙間を残すと、 やはり
|
|
42
|
+
// 「何かが抜けている」 ように見える
|
|
43
|
+
x: i * (LANE_W + LANE_GAP),
|
|
44
|
+
width: LANE_W,
|
|
45
|
+
label: 段の名前[lv]!,
|
|
46
|
+
contain: true,
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const stackPerLane: Record<string, number> = { "c4-l1": 0, "c4-l2": 0, "c4-l3": 0 };
|
|
51
|
+
for (const x of 割当) {
|
|
52
|
+
const lid = `c4-l${x.段}`;
|
|
53
|
+
const stack = stackPerLane[lid]!;
|
|
54
|
+
stackPerLane[lid] = stack + 1;
|
|
55
|
+
b.node(x.id, {
|
|
56
|
+
lane: lid,
|
|
57
|
+
stack,
|
|
58
|
+
kind: 描ける種別(x.actor.kind),
|
|
59
|
+
title: x.actor.name,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
for (const s of doc.flow) {
|
|
64
|
+
const fromId = slugify(s.from);
|
|
65
|
+
const toId = slugify(s.to);
|
|
66
|
+
b.edge(fromId, toId, {
|
|
67
|
+
label: s.label,
|
|
68
|
+
...(s.sub ? { sub: s.sub } : {}),
|
|
69
|
+
...(s.side ? { side: s.side } : {}),
|
|
70
|
+
|
|
71
|
+
...(s.tone ? { tone: s.tone } : {}),
|
|
72
|
+
...(s.style ? { style: s.style } : {}),
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return b.build();
|
|
77
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { CdlDiagram } from "@cardenelabs/cdl";
|
|
2
|
+
import type { DslActor, DslDocument } from "../types";
|
|
3
|
+
import { 書いた縦列に置く } from "./lanes";
|
|
4
|
+
import { lookupPartRaw } from "./parts-lookup";
|
|
5
|
+
/**
|
|
6
|
+
* 箱を鎖のように 1 本につなぐ形か (#2030 で `compile.ts` から移した)。
|
|
7
|
+
*
|
|
8
|
+
* 図種ごとの file と、`compile.ts` に残る部品の取り込みの両方が呼ぶ。
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 静止した `type: flow` かどうか。 この形だけ矢印を鎖状に作る (`compileFlow`)。
|
|
13
|
+
*
|
|
14
|
+
* 段を持つ形・縦列を書いた形・向きを書いた形は generic 経路へ回るため鎖にならない。
|
|
15
|
+
*
|
|
16
|
+
* **`compileFlow` の分岐もこの判定を使う**。 判定を 2 か所に書くと、組み立てだけが別経路へ回り、
|
|
17
|
+
* 行の対応と端の知らせが鎖のまま残る。 向きを書いた形 (#1494) は組み立てにだけ足され、`A -> C` に
|
|
18
|
+
* 書いた `head` が `C -> B` に載り、書いた端のとおりの矢印に「端は使われません」 が出ていた (#1986)。
|
|
19
|
+
*/
|
|
20
|
+
export function 鎖でつなぐ形か(doc: DslDocument): boolean {
|
|
21
|
+
if (doc.type !== "flow") return false;
|
|
22
|
+
if (doc.animate && doc.animate.phases.length > 0) return false;
|
|
23
|
+
if (doc.direction !== undefined) return false;
|
|
24
|
+
return !書いた縦列に置く("flow", doc);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 静止した流れ図の鎖に並べる登場人物 (#1987)。
|
|
29
|
+
*
|
|
30
|
+
* **どの行にも書かれていない部品は鎖に並べない**。 並べると、部品の前後の並び順の矢印は書き手が
|
|
31
|
+
* 部品へ引いたものではないため部品へ繋がずに外され (#1979)、部品を途中に置いた図から矢印が全て
|
|
32
|
+
* 消えていた (実測 = `受付 / 印 / 出荷` で矢印 0 本、知らせ 0 件)。 編集画面は部品を本文から抜いてから
|
|
33
|
+
* 鎖を作るので `受付 -> 出荷` を描き、カタログと絵が食い違っていた。 部品は図の下の格子に置くため、
|
|
34
|
+
* 鎖から外しても流れの列に空きは生まれない。
|
|
35
|
+
*
|
|
36
|
+
* - 部品の一覧に無い部品は仮の箱のまま描かれるので残す (一覧を渡さない組み立ても同じ)
|
|
37
|
+
* - 大きすぎて取り込まない部品は一覧にあるので外す (図に入らない場所へ鎖を通さない)
|
|
38
|
+
* - 行の端に書かれた部品は残し、#1979 の繋ぎ方に従う
|
|
39
|
+
*
|
|
40
|
+
* 鎖を作る `compileFlow`、行との対応 (`鎖のどの行から来たか`)、端の知らせ
|
|
41
|
+
* (`reportFlowEndpointNotHonored`) の 3 つがこの並びを使う。 1 つでも `doc.actors` を見ると、
|
|
42
|
+
* N 本目の矢印と行の対応がずれる。
|
|
43
|
+
*/
|
|
44
|
+
export function 鎖に並べる登場人物(
|
|
45
|
+
doc: DslDocument,
|
|
46
|
+
partsCatalog: Record<string, CdlDiagram> | undefined,
|
|
47
|
+
): DslActor[] {
|
|
48
|
+
if (!partsCatalog) return doc.actors;
|
|
49
|
+
const 行に書かれた = new Set(doc.flow.flatMap((s) => [s.from, s.to]));
|
|
50
|
+
// 部品でない箱は `partId` を持たず一覧を引けないので、一覧に無い部品と同じく残る
|
|
51
|
+
return doc.actors.filter(
|
|
52
|
+
(a) => 行に書かれた.has(a.name) || lookupPartRaw(partsCatalog, a.partId) === undefined,
|
|
53
|
+
);
|
|
54
|
+
}
|