@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
@@ -0,0 +1,337 @@
1
+ import { diagram } from "@cardenelabs/cdl";
2
+ import type { CdlDiagram } from "@cardenelabs/cdl";
3
+ import type { DslDocument } from "../types";
4
+
5
+ import { actorRefTable } from "./actors";
6
+ import { 効かない矢印の欄を並べる, 向きだけを使う図が伝えない矢印の欄 } from "./edge-option-notice";
7
+ import type { CompileNotice } from "./notice";
8
+ import { slugify } from "./slug";
9
+ import { 箱の題 } from "./node-title";
10
+ import { truncateForMessage, 図の小見出し } from "./subtitle";
11
+ import { 図表の大きさを読む } from "./chart-fields";
12
+ /**
13
+ * ガントチャート (`type: gantt`) の組み立て (#2030 で `compile.ts` から移した)。
14
+ */
15
+
16
+ export function compileGantt(doc: DslDocument, onNotice?: (n: CompileNotice) => void): CdlDiagram {
17
+ const b = diagram(slugify(doc.title), { topic: doc.title, type: "gantt" });
18
+ const 既定W = 720;
19
+
20
+ /**
21
+ * 書いたのに効かない形を伝える。 **`console.warn` だけにしない** (#2111) = 編集画面は知らせだけを
22
+ * 画面に出すため、log だけだと書いている人に理由が見えない。 値の図 (`value-chart.ts`) と同じ形
23
+ */
24
+ const 伝える = (種類: CompileNotice["kind"], 名前: string, message: string, line = 0): void => {
25
+ onNotice?.({ kind: 種類, actor: 名前, line, message });
26
+ if (typeof console !== "undefined" && console.warn) console.warn(`[dragon] ${message}`);
27
+ };
28
+
29
+ // 目盛りは **書かれた順** に並べる。 以前は `Q1=200 / Q2=600 / ...` の決め打ちで、 Q1-Q4 以外は
30
+ // 全て同じ位置に落ちていた。 順に並べれば月名でも週番号でも同じ規則で置ける
31
+ const 明示した目盛り = doc.ticks !== undefined;
32
+ const 目盛り: string[] = doc.ticks === undefined ? [] : [...doc.ticks];
33
+ const 目盛りなし: string[] = [];
34
+ // 最初に時期を読めなかった項目の行。 画面が案内できるようにする (値の図と同じ)
35
+ let 目盛りなし行 = 0;
36
+ const タスク: {
37
+ name: string;
38
+ title: string;
39
+ label: string;
40
+ line: number;
41
+ tone?: DslDocument["actors"][number]["tone"];
42
+ owner?: string;
43
+ end?: string;
44
+ startLabel?: string;
45
+ milestone?: boolean;
46
+ emphasis?: "primary";
47
+ startFraction: number;
48
+ precise: boolean;
49
+ }[] = [];
50
+ for (const a of doc.actors) {
51
+ const label = (a.value ?? a.subtitle ?? "").trim();
52
+ if (label === "") {
53
+ if (目盛りなし.length === 0) 目盛りなし行 = a.pos?.line ?? 0;
54
+ 目盛りなし.push(a.name);
55
+ continue;
56
+ }
57
+ const 始まり = 時期を読む(label);
58
+ if (始まり === null) {
59
+ 伝える(
60
+ "chart-value-unreadable",
61
+ a.name,
62
+ `type: gantt で ${truncateForMessage(a.name)} の始まり (${truncateForMessage(label)}) が読めません (帯に載せません)。 月の中の位置は \`6月+0.55\` の形で 0..1 を書いてください`,
63
+ a.pos?.line ?? 0,
64
+ );
65
+ continue;
66
+ }
67
+ if (!明示した目盛り && !目盛り.includes(始まり.label)) 目盛り.push(始まり.label);
68
+ if (明示した目盛り && !目盛り.includes(始まり.label)) {
69
+ 伝える(
70
+ "chart-value-unreadable",
71
+ a.name,
72
+ `type: gantt で ${truncateForMessage(a.name)} の始まり (${truncateForMessage(始まり.label)}) は ticks にありません (帯に載せません)。 ticks = ${目盛り.map(truncateForMessage).join(" / ")}`,
73
+ a.pos?.line ?? 0,
74
+ );
75
+ continue;
76
+ }
77
+ // 色は帯にそのまま渡す。 箱が 1 つになっても、 書いた色が消えないようにする
78
+ タスク.push({
79
+ name: a.name,
80
+ title: 箱の題(a),
81
+ label,
82
+ startFraction: 始まり.fraction,
83
+ precise: 明示した目盛り || 始まり.written || (a.end?.includes("+") ?? false),
84
+ line: a.pos?.line ?? 0,
85
+ ...(a.tone !== undefined ? { tone: a.tone } : {}),
86
+ ...(a.owner !== undefined ? { owner: a.owner } : {}),
87
+ ...(a.end !== undefined ? { end: a.end } : {}),
88
+ ...(a.startLabel !== undefined ? { startLabel: a.startLabel } : {}),
89
+ ...(a.milestone !== undefined ? { milestone: a.milestone } : {}),
90
+ ...(a.emphasis !== undefined ? { emphasis: a.emphasis } : {}),
91
+ });
92
+ }
93
+ if (目盛りなし.length > 0) {
94
+ 伝える(
95
+ "chart-value-unreadable",
96
+ 目盛りなし[0]!,
97
+ `type: gantt で時期を読めない項目があります (帯に載せません): ${目盛りなし.join(", ")}。` +
98
+ ` \`- 設計: "Q1"\` の形で書いてください`,
99
+ 目盛りなし行,
100
+ );
101
+ }
102
+
103
+ // 矢印は依存として読む (`- 設計 -> 実装` = 実装は設計の後)。 帯どうしを結ぶ線は描画側が
104
+ // 依存として描くので、 書いた矢印を捨てずに使う
105
+ const タスク名 = new Set(タスク.map((t) => t.name));
106
+ // 矢印の端の名前を引く表。 全ての図種に共通の「居ない名前」 の知らせと同じ表を使う
107
+ const 名前の表 = actorRefTable(doc);
108
+ const 依存元 = new Map<string, string[]>();
109
+ for (const s of doc.flow) {
110
+ const line = s.pos?.line ?? 0;
111
+ const 矢印 = `${truncateForMessage(s.from)} -> ${truncateForMessage(s.to)}`;
112
+ if (!タスク名.has(s.from) || !タスク名.has(s.to)) {
113
+ // `actors` に居ない名前を指す矢印は、全ての図種に共通の知らせ (`flow-actor-missing`) が
114
+ // 書いた行で伝えている。 ここでも伝えると同じ行に 2 件並ぶ (#2111)
115
+ if (!名前の表.has(s.from) || !名前の表.has(s.to)) continue;
116
+ const 時期なし = [s.from, s.to].filter((n) => !タスク名.has(n)).map(truncateForMessage);
117
+ 伝える(
118
+ "chart-edge-dropped",
119
+ s.from,
120
+ `type: gantt で ${矢印} の依存を結べません (${時期なし.join(" / ")} に時期がありません)`,
121
+ line,
122
+ );
123
+ continue;
124
+ }
125
+ const 前工程 = 依存元.get(s.to) ?? [];
126
+ if (!前工程.includes(s.from)) {
127
+ 前工程.push(s.from);
128
+ 依存元.set(s.to, 前工程);
129
+ }
130
+ /*
131
+ * 帯の依存は「どちらが先か」 だけを持つ。 矢印に書いた飾りは描けないので伝える。
132
+ *
133
+ * **伝える欄を並べない** (#2366)。 以前は 文字 / 色 / 線種 / 多重度 の 4 種を手で並べて
134
+ * いたため、後から足した項目がどこにも入らなかった (実測 = 矢印に書ける 21 項目のうち
135
+ * 17 件が、図も変わらず知らせも出ないまま落ちていた)。 矢印が使う欄を除いた残り全部を伝える。
136
+ *
137
+ * 多重度もここに含める = 多重度の知らせ (#2107) と同じ行に 2 件並べない。
138
+ */
139
+ const 効かない = 効かない矢印の欄を並べる(s, 向きだけを使う図が伝えない矢印の欄);
140
+ if (効かない.length > 0) {
141
+ 伝える(
142
+ "edge-option-not-honored",
143
+ s.from,
144
+ `type: gantt で ${矢印} に書いた ${効かない.join(" / ")} は描けません (ガントチャートの矢印は前後の関係だけを使います)`,
145
+ line,
146
+ );
147
+ }
148
+ }
149
+
150
+ // 高さは件数から決める。 描画側は 1 行 28 以上 + 行間 20 で積み、 上下に 32 / 44 の余白を取る
151
+ // (`kinds/gantt.tsx`)。 360 の固定だと 8 件目から最後の帯が枠の外に出る (実測 = 8 件で 56 はみ出す)
152
+ const 既定H = Math.max(360, 48 * タスク.length + 96);
153
+ const { w: CHART_W, h: CHART_H } = 図表の大きさを読む(doc, { w: 既定W, h: 既定H });
154
+ b.lane("gantt", { width: CHART_W });
155
+
156
+ b.node(`${slugify(doc.title) || "gantt"}-chart`, {
157
+ lane: "gantt",
158
+ stack: 0,
159
+ kind: "gantt-timeline",
160
+ title: doc.title,
161
+ ...図の小見出し(doc),
162
+ w: CHART_W,
163
+ h: CHART_H,
164
+ ganttData: タスク.map((t) => {
165
+ const startName = 時期を読む(t.label)?.label ?? t.label;
166
+ const idx = 目盛り.indexOf(startName);
167
+ const startIdx = idx + t.startFraction;
168
+ const dependencies = (依存元.get(t.name) ?? []).map((from) => slugify(from) || from);
169
+ const [firstDependency] = dependencies;
170
+ const dependsOn = firstDependency === undefined
171
+ ? undefined
172
+ : dependencies.length === 1 ? firstDependency : dependencies;
173
+ // 帯の誤りは、その項目を書いた行で伝える。 知らせの種類は呼ばれる側が決める (#2392)
174
+ const 帯を伝える = (種類: CompileNotice["kind"], 名: string, message: string): void =>
175
+ 伝える(種類, 名, message, t.line);
176
+ const 終わり = 終わる位置(
177
+ t.end,
178
+ startIdx,
179
+ 目盛り,
180
+ t.name,
181
+ 帯を伝える,
182
+ doc,
183
+ t.precise,
184
+ );
185
+ const startLabel = t.startLabel ?? (t.precise ? 位置の目盛り(startIdx, 目盛り) : t.label);
186
+ return {
187
+ id: slugify(t.name) || t.name,
188
+ title: t.title,
189
+ startIdx,
190
+ endIdx: 終わり.idx,
191
+ startLabel,
192
+ endLabel: 終わり.label ?? startLabel,
193
+ ...(t.owner !== undefined ? { owner: t.owner } : {}),
194
+ ...(dependsOn !== undefined ? { dependsOn } : {}),
195
+ ...(t.tone !== undefined ? { tone: t.tone } : {}),
196
+ ...(t.emphasis !== undefined ? { emphasis: t.emphasis } : {}),
197
+ ...(t.milestone === true ? { milestone: true as const } : {}),
198
+ };
199
+ }),
200
+ ...(明示した目盛り ? { ganttAxisMax: 目盛り.length } : {}),
201
+ ...(doc.ganttToday !== undefined ? { ganttToday: doc.ganttToday } : {}),
202
+ ...(doc.ganttTickLabels !== undefined ? { ganttTickLabels: doc.ganttTickLabels } : {}),
203
+ ...(doc.ganttBarEnd !== undefined ? { ganttBarEnd: doc.ganttBarEnd } : {}),
204
+ ...(doc.ganttBarThickness !== undefined ? { ganttBarThickness: doc.ganttBarThickness } : {}),
205
+ ...(doc.ganttSize !== undefined ? { ganttSize: doc.ganttSize } : {}),
206
+ });
207
+
208
+ return b.build();
209
+ }
210
+
211
+ /**
212
+ * `{名前}` が指す状態が取りうる値を、記法に書かれた範囲で集める (#1251)。
213
+ *
214
+ * 初期値と、段が動かす先 (`tween:` の両端と `set:` の値) を見る。 数として読めない値は
215
+ * 落とす = 位置として使われないため、下限の判定には関係しない。
216
+ */
217
+ function 状態が取る値(参照: string, doc: DslDocument): number[] {
218
+ const 名 = 参照.slice(1, -1);
219
+ const out: number[] = [];
220
+ const 数にする = (v: unknown): void => {
221
+ if (typeof v === "number") {
222
+ if (Number.isFinite(v)) out.push(v);
223
+ return;
224
+ }
225
+ // **空文字と空白だけの値を数にしない**。 `Number("")` は 0 を返すため、そのままだと
226
+ // 位置 0 として扱われ、始まりが 1 以降の帯に誤った知らせが出る。 描画側はこの値を
227
+ // 解けず始まりへ倒すので、警告する相手ではない
228
+ const 文字 = String(v).trim();
229
+ if (文字 === "") return;
230
+ const n = Number(文字);
231
+ if (Number.isFinite(n)) out.push(n);
232
+ };
233
+ for (const st of doc.animate?.states ?? []) if (st.name === 名) 数にする(st.initial);
234
+ for (const p of doc.animate?.phases ?? []) {
235
+ for (const t of p.tweens ?? []) {
236
+ if (t.state !== 名) continue;
237
+ 数にする(t.from);
238
+ 数にする(t.to);
239
+ }
240
+ for (const v of p.sets ?? []) if (v.state === 名) 数にする(v.value);
241
+ }
242
+ return out;
243
+ }
244
+
245
+ /**
246
+ * 工程が終わる位置を決める (#1251)。
247
+ *
248
+ * 書かなければ始まりと同じ = 帯が 1 コマ (従来の挙動)。
249
+ *
250
+ * `{名前}` を書いたらそのまま渡す。 描画側が状態を解いて位置に直すため、段で帯が伸び縮みする。
251
+ * その場合 **時期の名前は始まりのものを使う** = 状態が指すのは位置であって時期の名前ではなく、
252
+ * 帯の端に出す字が段ごとに変わるわけではない。
253
+ *
254
+ * 時期の名前を書いたら、その名前の位置に終わる。 書いた名前が目盛りに無い形は始まりと同じに
255
+ * 倒し、**読めないことを伝える** (#2392) = 目盛りは書かれた順に作るため載っていない名前は
256
+ * 位置を持たないが、黙って倒すと「書いたのに 1 コマのまま」 になる。
257
+ * 始まりより前を書いた時 (下の分岐) と同じ扱いにする。
258
+ */
259
+ function 終わる位置(
260
+ end: string | undefined,
261
+ 始まり: number,
262
+ 目盛り: readonly string[],
263
+ 名前: string,
264
+ 伝える: (種類: CompileNotice["kind"], 名: string, message: string) => void,
265
+ doc: DslDocument,
266
+ precise = false,
267
+ ): { idx: number | string; label?: string } {
268
+ if (end === undefined) return { idx: 始まり };
269
+ if (/^\{\w+\}$/.test(end)) {
270
+ // **状態が取る値は記法に全部書いてある**。 初期値と、段が動かす先 (`tween:` の両端と
271
+ // `set:` の値) を集めれば、始まりより前に落ちる値をここで見つけられる。
272
+ //
273
+ // 覆えないのは `values:` の式から決まる値だけ = 他の状態から計算されるため、
274
+ // 段ごとの結果を組み立ての時点では出せない
275
+ const 低い = 状態が取る値(end, doc).filter((v) => v < 始まり);
276
+ if (低い.length > 0) {
277
+ 伝える(
278
+ "gantt-end-before-start",
279
+ 名前,
280
+ `type: gantt で ${truncateForMessage(名前)} の終わり (${truncateForMessage(end)}) が始まりより前になる値を取ります (${[...new Set(低い)].join(", ")})。 始まりは ${始まり} 番目です`,
281
+ );
282
+ }
283
+ return {
284
+ idx: end,
285
+ ...(precise ? { label: 状態の目盛り(end, doc, 目盛り) ?? 位置の目盛り(始まり, 目盛り) } : {}),
286
+ };
287
+ }
288
+ const 時点 = 時期を読む(end);
289
+ const i = 時点 === null ? -1 : 目盛り.indexOf(時点.label);
290
+ // 目盛りは箱に書いた時期から作るので、どの箱も書いていない時期は位置を持たない。
291
+ // 読めた時期を並べて伝える = 目盛りは図ごとに変わるため、固定の一覧を文に書けない
292
+ if (i < 0 || 時点 === null) {
293
+ 伝える(
294
+ "chart-value-unreadable",
295
+ 名前,
296
+ `type: gantt で ${truncateForMessage(名前)} の終わり (${truncateForMessage(end)}) は目盛りにありません (帯を伸ばしません)。` +
297
+ ` 目盛りは箱に書いた時期から作ります = ${目盛り.map((x) => truncateForMessage(x)).join(" / ")} のどれかを書いてください`,
298
+ );
299
+ return { idx: 始まり };
300
+ }
301
+ // 始まりより前に終わる帯は描けない。 そのまま渡すと横幅が負になり、帯が始まりの位置から
302
+ // 左へはみ出す。 始まりと同じに倒して伝える (黙って倒すと「書いたのに 1 コマのまま」 になる)
303
+ const endIdx = precise ? i + (時点.written ? 時点.fraction : 1) - 1 : i;
304
+ if (endIdx < 始まり) {
305
+ 伝える(
306
+ "gantt-end-before-start",
307
+ 名前,
308
+ `type: gantt で ${truncateForMessage(名前)} の終わり (${truncateForMessage(end)}) が始まりより前です (始まりと同じに倒しました)`,
309
+ );
310
+ return { idx: 始まり, ...(precise ? { label: 位置の目盛り(始まり, 目盛り) } : {}) };
311
+ }
312
+ return { idx: endIdx, label: precise ? 位置の目盛り(endIdx, 目盛り) : end };
313
+ }
314
+
315
+ /** `6月+0.55` を目盛り名と、その目盛り内の位置に分ける。 */
316
+ function 時期を読む(raw: string): { label: string; fraction: number; written: boolean } | null {
317
+ const m = raw.trim().match(/^(.+?)(?:\+([+-]?(?:\d+(?:\.\d+)?|\.\d+)))?$/);
318
+ if (m === null) return null;
319
+ const fraction = m[2] === undefined ? 0 : Number(m[2]);
320
+ if (!Number.isFinite(fraction) || fraction < 0 || fraction > 1) return null;
321
+ return { label: m[1]!.trim(), fraction, written: m[2] !== undefined };
322
+ }
323
+
324
+ function 位置の目盛り(idx: number, 目盛り: readonly string[]): string {
325
+ const rounded = Math.max(0, Math.min(目盛り.length - 1, Math.round(idx)));
326
+ return 目盛り[rounded] ?? "";
327
+ }
328
+
329
+ function 状態の目盛り(
330
+ ref: string,
331
+ doc: DslDocument,
332
+ 目盛り: readonly string[],
333
+ ): string | undefined {
334
+ const values = 状態が取る値(ref, doc);
335
+ if (values.length === 0) return undefined;
336
+ return 位置の目盛り(Math.max(...values), 目盛り);
337
+ }