@cardenelabs/dragon 0.30.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 (55) hide show
  1. package/README.md +314 -12
  2. package/dist/index.cjs +6966 -1438
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +2150 -118
  5. package/dist/index.d.ts +2150 -118
  6. package/dist/index.js +6950 -1440
  7. package/dist/index.js.map +1 -1
  8. package/package.json +12 -13
  9. package/src/compile/actor-ids.ts +6 -15
  10. package/src/compile/actor-option-notice.ts +379 -0
  11. package/src/compile/actors.ts +32 -3
  12. package/src/compile/c4.ts +0 -3
  13. package/src/compile/chain.ts +43 -4
  14. package/src/compile/chart-fields.ts +16 -1
  15. package/src/compile/direction.ts +50 -11
  16. package/src/compile/edge-option-notice.ts +112 -0
  17. package/src/compile/flowchart.ts +344 -0
  18. package/src/compile/funnel.ts +14 -2
  19. package/src/compile/gantt.ts +141 -36
  20. package/src/compile/generic.ts +473 -61
  21. package/src/compile/journey.ts +14 -5
  22. package/src/compile/kinds.ts +5 -13
  23. package/src/compile/lanes.ts +95 -13
  24. package/src/compile/metro.ts +271 -0
  25. package/src/compile/mind.ts +55 -5
  26. package/src/compile/notice.ts +51 -4
  27. package/src/compile/order.ts +62 -0
  28. package/src/compile/parts.ts +107 -33
  29. package/src/compile/placement-apply.ts +27 -11
  30. package/src/compile/quadrant.ts +171 -16
  31. package/src/compile/record.ts +75 -0
  32. package/src/compile/row-marks.ts +128 -0
  33. package/src/compile/sequence.ts +105 -26
  34. package/src/compile/slug.ts +9 -1
  35. package/src/compile/stages.ts +247 -0
  36. package/src/compile/subtitle.ts +15 -2
  37. package/src/compile/swimlane-marks.ts +87 -0
  38. package/src/compile/swimlane.ts +13 -14
  39. package/src/compile/timeline.ts +213 -0
  40. package/src/compile/topology.ts +3 -9
  41. package/src/compile/tree.ts +13 -4
  42. package/src/compile/value-chart.ts +179 -25
  43. package/src/compile.ts +1550 -370
  44. package/src/focus.ts +2 -2
  45. package/src/index.ts +34 -2
  46. package/src/json-parser.ts +1407 -48
  47. package/src/keywords.ts +251 -17
  48. package/src/notation-lint.ts +41 -64
  49. package/src/schemas/diagram.json +643 -20
  50. package/src/types.ts +336 -35
  51. package/src/v05/parser.ts +2451 -271
  52. package/src/compile/class.ts +0 -100
  53. package/src/compile/er.ts +0 -44
  54. package/src/compile/solidity.ts +0 -39
  55. package/src/compile/state.ts +0 -71
@@ -3,19 +3,19 @@ import type { CdlDiagram } from "@cardenelabs/cdl";
3
3
  import type { DslDocument } from "../types";
4
4
 
5
5
  import { actorRefTable } from "./actors";
6
- import { 書いた多重度を読む } from "./er-relation";
6
+ import { 効かない矢印の欄を並べる, 向きだけを使う図が伝えない矢印の欄 } from "./edge-option-notice";
7
7
  import type { CompileNotice } from "./notice";
8
8
  import { slugify } from "./slug";
9
9
  import { 箱の題 } from "./node-title";
10
10
  import { truncateForMessage, 図の小見出し } from "./subtitle";
11
+ import { 図表の大きさを読む } from "./chart-fields";
11
12
  /**
12
- * 工程表 (`type: gantt`) の組み立て (#2030 で `compile.ts` から移した)。
13
+ * ガントチャート (`type: gantt`) の組み立て (#2030 で `compile.ts` から移した)。
13
14
  */
14
15
 
15
16
  export function compileGantt(doc: DslDocument, onNotice?: (n: CompileNotice) => void): CdlDiagram {
16
17
  const b = diagram(slugify(doc.title), { topic: doc.title, type: "gantt" });
17
- const CHART_W = 720;
18
- b.lane("gantt", { width: CHART_W });
18
+ const 既定W = 720;
19
19
 
20
20
  /**
21
21
  * 書いたのに効かない形を伝える。 **`console.warn` だけにしない** (#2111) = 編集画面は知らせだけを
@@ -28,7 +28,8 @@ export function compileGantt(doc: DslDocument, onNotice?: (n: CompileNotice) =>
28
28
 
29
29
  // 目盛りは **書かれた順** に並べる。 以前は `Q1=200 / Q2=600 / ...` の決め打ちで、 Q1-Q4 以外は
30
30
  // 全て同じ位置に落ちていた。 順に並べれば月名でも週番号でも同じ規則で置ける
31
- const 目盛り: string[] = [];
31
+ const 明示した目盛り = doc.ticks !== undefined;
32
+ const 目盛り: string[] = doc.ticks === undefined ? [] : [...doc.ticks];
32
33
  const 目盛りなし: string[] = [];
33
34
  // 最初に時期を読めなかった項目の行。 画面が案内できるようにする (値の図と同じ)
34
35
  let 目盛りなし行 = 0;
@@ -40,6 +41,11 @@ export function compileGantt(doc: DslDocument, onNotice?: (n: CompileNotice) =>
40
41
  tone?: DslDocument["actors"][number]["tone"];
41
42
  owner?: string;
42
43
  end?: string;
44
+ startLabel?: string;
45
+ milestone?: boolean;
46
+ emphasis?: "primary";
47
+ startFraction: number;
48
+ precise: boolean;
43
49
  }[] = [];
44
50
  for (const a of doc.actors) {
45
51
  const label = (a.value ?? a.subtitle ?? "").trim();
@@ -48,16 +54,40 @@ export function compileGantt(doc: DslDocument, onNotice?: (n: CompileNotice) =>
48
54
  目盛りなし.push(a.name);
49
55
  continue;
50
56
  }
51
- if (!目盛り.includes(label)) 目盛り.push(label);
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
+ }
52
77
  // 色は帯にそのまま渡す。 箱が 1 つになっても、 書いた色が消えないようにする
53
78
  タスク.push({
54
79
  name: a.name,
55
80
  title: 箱の題(a),
56
81
  label,
82
+ startFraction: 始まり.fraction,
83
+ precise: 明示した目盛り || 始まり.written || (a.end?.includes("+") ?? false),
57
84
  line: a.pos?.line ?? 0,
58
85
  ...(a.tone !== undefined ? { tone: a.tone } : {}),
59
86
  ...(a.owner !== undefined ? { owner: a.owner } : {}),
60
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 } : {}),
61
91
  });
62
92
  }
63
93
  if (目盛りなし.length > 0) {
@@ -75,7 +105,7 @@ export function compileGantt(doc: DslDocument, onNotice?: (n: CompileNotice) =>
75
105
  const タスク名 = new Set(タスク.map((t) => t.name));
76
106
  // 矢印の端の名前を引く表。 全ての図種に共通の「居ない名前」 の知らせと同じ表を使う
77
107
  const 名前の表 = actorRefTable(doc);
78
- const 依存元 = new Map<string, string>();
108
+ const 依存元 = new Map<string, string[]>();
79
109
  for (const s of doc.flow) {
80
110
  const line = s.pos?.line ?? 0;
81
111
  const 矢印 = `${truncateForMessage(s.from)} -> ${truncateForMessage(s.to)}`;
@@ -92,20 +122,26 @@ export function compileGantt(doc: DslDocument, onNotice?: (n: CompileNotice) =>
92
122
  );
93
123
  continue;
94
124
  }
95
- 依存元.set(s.to, s.from);
96
- // 帯の依存は「どちらが先か」 だけを持つ。 矢印に書いた文字や色は描けないので伝える。
97
- // 多重度もここに含める = 多重度の知らせ (#2107) と同じ行に 2 件並べない
98
- const 効かない = [
99
- (s.label ?? "") !== "" || (s.sub ?? "") !== "" ? "文字" : "",
100
- s.tone !== undefined ? "色" : "",
101
- s.style !== undefined ? "線種" : "",
102
- 書いた多重度を読む(s.cardinality) !== undefined ? "多重度" : "",
103
- ].filter((x) => x !== "");
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, 向きだけを使う図が伝えない矢印の欄);
104
140
  if (効かない.length > 0) {
105
141
  伝える(
106
142
  "edge-option-not-honored",
107
143
  s.from,
108
- `type: gantt で ${矢印} に書いた ${効かない.join(" / ")} は描けません (工程表の矢印は前後の関係だけを使います)`,
144
+ `type: gantt で ${矢印} に書いた ${効かない.join(" / ")} は描けません (ガントチャートの矢印は前後の関係だけを使います)`,
109
145
  line,
110
146
  );
111
147
  }
@@ -113,7 +149,9 @@ export function compileGantt(doc: DslDocument, onNotice?: (n: CompileNotice) =>
113
149
 
114
150
  // 高さは件数から決める。 描画側は 1 行 28 以上 + 行間 20 で積み、 上下に 32 / 44 の余白を取る
115
151
  // (`kinds/gantt.tsx`)。 360 の固定だと 8 件目から最後の帯が枠の外に出る (実測 = 8 件で 56 はみ出す)
116
- const CHART_H = Math.max(360, 48 * タスク.length + 96);
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 });
117
155
 
118
156
  b.node(`${slugify(doc.title) || "gantt"}-chart`, {
119
157
  lane: "gantt",
@@ -124,24 +162,47 @@ export function compileGantt(doc: DslDocument, onNotice?: (n: CompileNotice) =>
124
162
  w: CHART_W,
125
163
  h: CHART_H,
126
164
  ganttData: タスク.map((t) => {
127
- const idx = 目盛り.indexOf(t.label);
128
- const from = 依存元.get(t.name);
129
- // 帯の向きの誤りは、その項目を書いた行で伝える
130
- const 逆向きを伝える = (名: string, message: string): void =>
131
- 伝える("gantt-end-before-start", 名, message, t.line);
132
- const 終わり = 終わる位置(t.end, idx, 目盛り, t.name, 逆向きを伝える, doc);
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);
133
186
  return {
134
187
  id: slugify(t.name) || t.name,
135
188
  title: t.title,
136
- startIdx: idx,
189
+ startIdx,
137
190
  endIdx: 終わり.idx,
138
- startLabel: t.label,
139
- endLabel: 終わり.label ?? t.label,
191
+ startLabel,
192
+ endLabel: 終わり.label ?? startLabel,
140
193
  ...(t.owner !== undefined ? { owner: t.owner } : {}),
141
- ...(from !== undefined ? { dependsOn: slugify(from) || from } : {}),
194
+ ...(dependsOn !== undefined ? { dependsOn } : {}),
142
195
  ...(t.tone !== undefined ? { tone: t.tone } : {}),
196
+ ...(t.emphasis !== undefined ? { emphasis: t.emphasis } : {}),
197
+ ...(t.milestone === true ? { milestone: true as const } : {}),
143
198
  };
144
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 } : {}),
145
206
  });
146
207
 
147
208
  return b.build();
@@ -191,15 +252,18 @@ function 状態が取る値(参照: string, doc: DslDocument): number[] {
191
252
  * 帯の端に出す字が段ごとに変わるわけではない。
192
253
  *
193
254
  * 時期の名前を書いたら、その名前の位置に終わる。 書いた名前が目盛りに無い形は始まりと同じに
194
- * 倒す = 目盛りは書かれた順に作るため、載っていない名前は位置を持たない。
255
+ * 倒し、**読めないことを伝える** (#2392) = 目盛りは書かれた順に作るため載っていない名前は
256
+ * 位置を持たないが、黙って倒すと「書いたのに 1 コマのまま」 になる。
257
+ * 始まりより前を書いた時 (下の分岐) と同じ扱いにする。
195
258
  */
196
259
  function 終わる位置(
197
260
  end: string | undefined,
198
261
  始まり: number,
199
262
  目盛り: readonly string[],
200
263
  名前: string,
201
- 伝える: (名: string, message: string) => void,
264
+ 伝える: (種類: CompileNotice["kind"], 名: string, message: string) => void,
202
265
  doc: DslDocument,
266
+ precise = false,
203
267
  ): { idx: number | string; label?: string } {
204
268
  if (end === undefined) return { idx: 始まり };
205
269
  if (/^\{\w+\}$/.test(end)) {
@@ -211,22 +275,63 @@ function 終わる位置(
211
275
  const 低い = 状態が取る値(end, doc).filter((v) => v < 始まり);
212
276
  if (低い.length > 0) {
213
277
  伝える(
278
+ "gantt-end-before-start",
214
279
  名前,
215
280
  `type: gantt で ${truncateForMessage(名前)} の終わり (${truncateForMessage(end)}) が始まりより前になる値を取ります (${[...new Set(低い)].join(", ")})。 始まりは ${始まり} 番目です`,
216
281
  );
217
282
  }
218
- return { idx: end };
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: 始まり };
219
300
  }
220
- const i = 目盛り.indexOf(end);
221
- if (i < 0) return { idx: 始まり };
222
301
  // 始まりより前に終わる帯は描けない。 そのまま渡すと横幅が負になり、帯が始まりの位置から
223
302
  // 左へはみ出す。 始まりと同じに倒して伝える (黙って倒すと「書いたのに 1 コマのまま」 になる)
224
- if (i < 始まり) {
303
+ const endIdx = precise ? i + (時点.written ? 時点.fraction : 1) - 1 : i;
304
+ if (endIdx < 始まり) {
225
305
  伝える(
306
+ "gantt-end-before-start",
226
307
  名前,
227
308
  `type: gantt で ${truncateForMessage(名前)} の終わり (${truncateForMessage(end)}) が始まりより前です (始まりと同じに倒しました)`,
228
309
  );
229
- return { idx: 始まり };
310
+ return { idx: 始まり, ...(precise ? { label: 位置の目盛り(始まり, 目盛り) } : {}) };
230
311
  }
231
- return { idx: i, label: end };
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), 目盛り);
232
337
  }