@cardenelabs/dragon 0.34.0 → 0.35.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 (127) hide show
  1. package/README.md +39 -720
  2. package/dist/cli.cjs +4529 -0
  3. package/dist/cli.cjs.map +1 -0
  4. package/dist/cli.js +4523 -0
  5. package/dist/cli.js.map +1 -0
  6. package/dist/export.cjs +3912 -0
  7. package/dist/export.cjs.map +1 -0
  8. package/dist/export.d.cts +36 -0
  9. package/dist/export.d.ts +36 -0
  10. package/dist/export.js +3907 -0
  11. package/dist/export.js.map +1 -0
  12. package/dist/fonts.cjs +7 -0
  13. package/dist/fonts.cjs.map +1 -0
  14. package/dist/fonts.js +4 -0
  15. package/dist/fonts.js.map +1 -0
  16. package/dist/index.cjs +4105 -20356
  17. package/dist/index.cjs.map +1 -1
  18. package/dist/index.d.cts +227 -5522
  19. package/dist/index.d.ts +227 -5522
  20. package/dist/index.js +4091 -20283
  21. package/dist/index.js.map +1 -1
  22. package/dist/player.global.js +2 -0
  23. package/dist/schema-BBWfniwH.d.cts +112 -0
  24. package/dist/schema-BBWfniwH.d.ts +112 -0
  25. package/package.json +39 -24
  26. package/src/cli.ts +237 -0
  27. package/src/export/capture.ts +277 -0
  28. package/src/export/fonts.ts +68 -0
  29. package/src/export/html.ts +32 -0
  30. package/src/export/index.ts +146 -0
  31. package/src/field-messages.ts +104 -0
  32. package/src/figure-text.ts +42 -0
  33. package/src/index.ts +30 -283
  34. package/src/layout/architecture.ts +381 -0
  35. package/src/layout/measure.ts +21 -0
  36. package/src/layout/scene.ts +353 -0
  37. package/src/layout/sequence.ts +189 -0
  38. package/src/load-fonts.ts +8 -0
  39. package/src/look/fonts.ts +33 -0
  40. package/src/look/palettes.json +209 -0
  41. package/src/look/values.ts +155 -0
  42. package/src/markdown.ts +35 -0
  43. package/src/messages.ts +122 -0
  44. package/src/parser.ts +342 -310
  45. package/src/player-global.ts +4 -0
  46. package/src/player.ts +271 -0
  47. package/src/printer.ts +76 -0
  48. package/src/render/icons.ts +41 -0
  49. package/src/render/motion.ts +106 -0
  50. package/src/render/parts.ts +279 -0
  51. package/src/render/svg.ts +341 -0
  52. package/src/schema.ts +161 -16
  53. package/src/schemas/diagram.schema.json +150 -0
  54. package/src/schemas/validate-diagram.d.ts +12 -0
  55. package/src/schemas/validate-diagram.js +8 -0
  56. package/src/timeline.ts +166 -0
  57. package/src/tokenize.ts +20 -281
  58. package/src/version.ts +55 -0
  59. package/src/view.ts +16 -0
  60. package/src/words.ts +79 -0
  61. package/examples/quick-start.md +0 -142
  62. package/src/canvas-bounds.ts +0 -52
  63. package/src/color.ts +0 -197
  64. package/src/compile/actor-ids.ts +0 -297
  65. package/src/compile/actor-option-notice.ts +0 -379
  66. package/src/compile/actors.ts +0 -103
  67. package/src/compile/c4.ts +0 -74
  68. package/src/compile/chain.ts +0 -93
  69. package/src/compile/chart-fields.ts +0 -193
  70. package/src/compile/deep-rewrite.ts +0 -30
  71. package/src/compile/derived-source.ts +0 -18
  72. package/src/compile/direction.ts +0 -102
  73. package/src/compile/edge-option-notice.ts +0 -112
  74. package/src/compile/er-relation.ts +0 -200
  75. package/src/compile/flow.ts +0 -54
  76. package/src/compile/flowchart.ts +0 -344
  77. package/src/compile/funnel.ts +0 -87
  78. package/src/compile/gantt.ts +0 -337
  79. package/src/compile/generic.ts +0 -696
  80. package/src/compile/hierarchy.ts +0 -92
  81. package/src/compile/journey.ts +0 -93
  82. package/src/compile/kinds.ts +0 -139
  83. package/src/compile/lanes.ts +0 -138
  84. package/src/compile/metro.ts +0 -271
  85. package/src/compile/mind.ts +0 -393
  86. package/src/compile/node-title.ts +0 -24
  87. package/src/compile/notice.ts +0 -141
  88. package/src/compile/order.ts +0 -57
  89. package/src/compile/parts-lookup.ts +0 -17
  90. package/src/compile/parts.ts +0 -2709
  91. package/src/compile/placement-apply.ts +0 -689
  92. package/src/compile/placement.ts +0 -15
  93. package/src/compile/quadrant.ts +0 -265
  94. package/src/compile/record.ts +0 -75
  95. package/src/compile/row-marks.ts +0 -128
  96. package/src/compile/rows.ts +0 -40
  97. package/src/compile/sequence.ts +0 -305
  98. package/src/compile/slug.ts +0 -37
  99. package/src/compile/stages.ts +0 -247
  100. package/src/compile/subtitle.ts +0 -35
  101. package/src/compile/swimlane-marks.ts +0 -87
  102. package/src/compile/swimlane.ts +0 -82
  103. package/src/compile/timeline.ts +0 -213
  104. package/src/compile/topology.ts +0 -44
  105. package/src/compile/tree.ts +0 -69
  106. package/src/compile/value-chart.ts +0 -337
  107. package/src/compile/values.ts +0 -432
  108. package/src/compile/word-state.ts +0 -82
  109. package/src/compile.ts +0 -3168
  110. package/src/focus.ts +0 -46
  111. package/src/input-size.ts +0 -181
  112. package/src/json-line-map.ts +0 -30
  113. package/src/json-parser.ts +0 -4306
  114. package/src/keywords.ts +0 -417
  115. package/src/near-name.ts +0 -42
  116. package/src/notation-lint.ts +0 -342
  117. package/src/relative-pos.ts +0 -251
  118. package/src/schemas/diagram.json +0 -3088
  119. package/src/types.ts +0 -1009
  120. package/src/v05/index.ts +0 -9
  121. package/src/v05/input-table.generated.ts +0 -138
  122. package/src/v05/parser-types.ts +0 -32
  123. package/src/v05/parser.ts +0 -6668
  124. package/src/v05/readout-table.generated.ts +0 -1088
  125. package/src/v05/removed-syntax.ts +0 -115
  126. package/src/value-syntax.ts +0 -313
  127. package/src/write-position.ts +0 -270
package/src/compile.ts DELETED
@@ -1,3168 +0,0 @@
1
- /**
2
- * AST (DslDocument) → 既存 preset API 経由 → LaidDiagram
3
- *
4
- * v0.3 ... アニメーション ブロック full compile 対応 (sequence preset のみ、 他 preset は v0.4 で順次)
5
- * - state / tween / set / highlight / badge / body を実 phase に注入
6
- * - アニメーション ありなら builder 直接経路 ... preset の標準 phase を置換
7
- * - アニメーション なしは v0.2 と同じく preset 経由
8
- *
9
- * v0.2 ... 6 preset 全対応 (sequence / flow / swimlane / er / state / topology)
10
- */
11
-
12
- import type { DslDocument, DslLane, DslStep, DslEventBinding, PresetType } from "./types";
13
- // 図種ごとの組み立てを分けた先 (#2030)。 共有の小道具から順に出している。
14
- // どれも他の file を取り込まない葉なので、図種ごとの file と両方から呼んでも輪にならない
15
- import { 向きを選べる図種, 縦列より向きが勝つ図種, 既定の向き } from "./compile/direction";
16
- import { 並び順が効くか, 並び順を選べる図種 } from "./compile/order";
17
- import { actorRefTable, canonicalizeFlowActors } from "./compile/actors";
18
- import { compileC4 } from "./compile/c4";
19
- import { 鎖でつなぐ形か, 鎖に並べる登場人物, 鎖にしない書き方の案内 } from "./compile/chain";
20
- import { compileRecord } from "./compile/record";
21
- import { 行頭の印にする, 行頭の印を読むか, 鍵の行を上にまとめる } from "./compile/row-marks";
22
- import { compileFlow } from "./compile/flow";
23
- import { compileFunnel } from "./compile/funnel";
24
- import { 共通の組み立てへ回す } from "./compile/generic";
25
- import { compileGantt } from "./compile/gantt";
26
-
27
- import { compileJourney } from "./compile/journey";
28
- import { compileMind } from "./compile/mind";
29
- import { compileQuadrant } from "./compile/quadrant";
30
- import { compileSequence } from "./compile/sequence";
31
- import { compileFlowchart } from "./compile/flowchart";
32
- import { compileSwimlane } from "./compile/swimlane";
33
- import { compileTopology } from "./compile/topology";
34
- import { compileTree } from "./compile/tree";
35
- import { compileValueChart } from "./compile/value-chart";
36
- import { deepRewriteStrings } from "./compile/deep-rewrite";
37
- import {
38
- 効かない箱の欄を並べる,
39
- 板が伝えない箱の欄,
40
- 木の図が伝えない箱の欄,
41
- 値として読む図種,
42
- 値として読む図が伝えない箱の欄,
43
- 値として読む図が主役を読む,
44
- 骨組みの図種,
45
- 骨組みの図が伝えない箱の欄,
46
- } from "./compile/actor-option-notice";
47
- import {
48
- 効かない矢印の欄を並べる,
49
- 向きだけを使う図が伝えない矢印の欄,
50
- } from "./compile/edge-option-notice";
51
- import { recordDerivedSourceLine } from "./compile/derived-source";
52
- import {
53
- mergePartsFromActors,
54
- reportPartNodeNotHonored,
55
- 測るために配置する,
56
- } from "./compile/parts";
57
- // 見本の取り込みが外へ出していた 13 個を、この file から出していた形のまま渡す (#2034)。
58
- // `index.ts` と検査が `./compile` から取り込んでいるので、窓口をここに残す
59
- export {
60
- MAX_PART_SCALE,
61
- normalizePartScale,
62
- partBoxInFrame,
63
- partDrawsInDiagram,
64
- partIsMeasurable,
65
- partRenderSize,
66
- partScaleFactor,
67
- partTargetScale,
68
- partTargetSize,
69
- partVisualSize,
70
- partsBaseBottom,
71
- partsGridCenters,
72
- 部品に上書きを当てる,
73
- 測るために受け取った知らせの数,
74
- } from "./compile/parts";
75
- import { SINGLE_BOX_KINDS, 図種の作り } from "./compile/kinds";
76
- import { collectRenamedTargets, disambiguateActorIds, restoreActorNames } from "./compile/actor-ids";
77
- import { 書いた多重度を読む, 端の形が決まる語 } from "./compile/er-relation";
78
- import { attachDerivedValues, foldValueTriggers, 鎖のどの行から来たか } from "./compile/values";
79
- import {
80
- applyCanvasPivotPositions,
81
- applyLayoutOffsets,
82
- applyNodeTones,
83
- resolveRelativeDoc,
84
- } from "./compile/placement-apply";
85
- // 箱を測る物差しを、この file から出していた形のまま外へ渡す (#2036)。
86
- // `index.ts` が画面側の測りに使う
87
- export { measureActorBoxes } from "./compile/placement-apply";
88
- import { 縦列を選べる図種, 書いた縦列に置く, 縦列の判定に数える箱 } from "./compile/lanes";
89
- import type { CompileNotice } from "./compile/notice";
90
- // 分けた先の型を、この file から出していた形のまま外へ渡す (#2030)。
91
- // `index.ts` と 47 個の検査が `./compile` から取り込んでいるので、窓口をここに残す
92
- export type { CompileNotice } from "./compile/notice";
93
- import { 段を読み取る } from "./compile/rows";
94
- import { truncateForMessage } from "./compile/subtitle";
95
- import { slugify } from "./compile/slug";
96
- import { 省く泳法図の印を外す, 泳法図で印をどう描く } from "./compile/swimlane-marks";
97
- import { 語の状態を図の語へ直す } from "./compile/word-state";
98
- import type { CdlDiagram, CdlEdge, CdlLegendItem, CdlNode } from "@cardenelabs/cdl";
99
- import { parseFormula, extractIdentifiers, inputDefaultValue } from "@cardenelabs/cdl";
100
- import { parseFocusEntry } from "./focus";
101
- import { DRAW_TARGETS, 描く語がその図を指すか } from "./v05/parser";
102
- import {
103
- DIAGRAM_SHAPES,
104
- resolveShape,
105
- shapesForDiagramType,
106
- 棒を描く節か,
107
- 意匠の棒の角,
108
- type DslShape,
109
- } from "./keywords";
110
- import { pointsOutside, stripExternalPaint } from "./color";
111
- import { countDocElements, describeOversize } from "./input-size";
112
- export interface CompileToCdlOpts {
113
- /**
114
- * CAR-1657 = parts identifier lookup catalog、 caller (CdlEditor / test) が inject。
115
- * DslActor.partId が set された actor を検出したら partsCatalog[partId] から CdlDiagram を
116
- * lookup + mergePartIntoDiagram で target に統合。 未渡し時は parts kind actor を skip + warn。
117
- */
118
- partsCatalog?: Record<string, CdlDiagram>;
119
- /**
120
- * 組み立ての途中で分かった「書いたのに効かなかったこと」 の受け取り口。
121
- *
122
- * 図は出せるので誤りにはしないが、 黙って捨てると書いた人が理由を追えない。 editor は
123
- * これを受けて画面に出す。 判定は組み立て側だけが持ち、 画面側は表示に徹する。
124
- */
125
- onNotice?: (notice: CompileNotice) => void;
126
- /**
127
- * edge が DSL のどの行から来たかの受け取り口 (#998)。
128
- *
129
- * preset によっては書いた step と生成される edge が一致しない (`type: flow` は actor を鎖状に
130
- * 繋ぐため `a -> c` と書いても `a -> b` になる)。 edge を起点に本文の行を直す機能は、 この
131
- * 対応が無いと別の行を書き換える。
132
- *
133
- * **対応が取れない edge については呼ばれない**。 「対応が無い」 と「行 0」 を区別するため。
134
- */
135
- onEdgeSource?: (edgeId: string, line: number) => void;
136
- }
137
-
138
- export function compileToCdl(doc: DslDocument, opts?: CompileToCdlOpts): CdlDiagram {
139
- // 大きすぎる図は組み立てない (#1005)。 組み立てにかかる時間は要素数の 2 乗で伸び、
140
- // 待機の後に同じ流れの中で走るため、 貼ってしまうと画面が戻らない (実測 = 10,000 要素で 7.6 秒)。
141
- // 両方の記法がここを通るので、 入口ごとに置かずここで 1 度だけ見る
142
- const oversize = describeOversize({ elements: countDocElements(doc), bytes: 0 });
143
- if (oversize) throw new Error(oversize);
144
-
145
- // 矢印の指す先を `actors` に書いた名前へ揃える (#1209)。 **図種ごとの組み立てより前**。
146
- //
147
- // 動きを書いた図は slug に落として引き、 書いていない図は名前の完全一致で引く。 揃えないと
148
- // 同じ本文が図種ごとに別の相手を指す = 知らせは出ないのに label が消える / 題が slug に
149
- // 化ける / 依存が切れる (Round 1 で実測)。
150
- doc = canonicalizeFlowActors(doc);
151
-
152
- // 知らせは **落とす前の矢印** を見る (#1219)。 落とした後を渡すと、 図が壊れないように
153
- // 外した矢印が書いた人に届かなくなる
154
- const 書いたまま = doc;
155
- // 解決できない矢印を組み立てから外す (#1219)。 残すと、 存在しない箱や枠を指す図ができて
156
- // 描画の直前で落ちる (実測 = 8 図種)
157
- doc = dropUnresolvedFlow(doc);
158
-
159
- // 名前から作る id が重なる分を解く (#1220)。 **矢印を落とした後**に見る = 落とした矢印の
160
- // 端にしか出てこない名前で id を分けても、 その箱は作られない
161
- const 分けた = disambiguateActorIds(doc, opts?.onNotice);
162
- doc = 分けた.doc;
163
- doc = 省く泳法図の印を外す(doc);
164
-
165
- // 図種の組み立てが知らせた行を控える (#2107 / #2111)。 同じ行に多重度の知らせを重ねない。
166
- // 知らせる図種も知らせの種類も手で並べず、実際に出た知らせで決める
167
- const 受け取り口 = opts?.onNotice;
168
- const 図種が知らせた行 = new Set<number>();
169
- const 図種の知らせ = 受け取り口
170
- ? (n: CompileNotice): void => {
171
- 図種が知らせた行.add(n.line);
172
- 受け取り口(n);
173
- }
174
- : undefined;
175
-
176
- let diagram: CdlDiagram;
177
- switch (doc.type) {
178
- case "sequence":
179
- diagram = compileSequence(doc);
180
- break;
181
- case "flow":
182
- diagram = compileFlow(doc, opts?.partsCatalog);
183
- break;
184
- case "flowchart":
185
- diagram = compileFlowchart(doc);
186
- break;
187
- case "swimlane":
188
- diagram = compileSwimlane(doc);
189
- break;
190
- case "record":
191
- diagram = compileRecord(doc);
192
- break;
193
- case "topology":
194
- diagram = compileTopology(doc);
195
- break;
196
- case "gantt":
197
- diagram = compileGantt(doc, 図種の知らせ);
198
- break;
199
- case "chart":
200
- // 形は `shape:` が決める (#2657)。 書かなければ棒 = 数を並べる図で最も素直な形で、
201
- // 形を書かずに数だけ書いた記法が図にならない状態を作らない
202
- diagram = compileValueChart(
203
- doc,
204
- doc.shape === undefined ? "bar" : (resolveShape(doc.shape) ?? "bar"),
205
- 図種の知らせ,
206
- );
207
- break;
208
- case "funnel":
209
- diagram = compileFunnel(doc, 図種の知らせ);
210
- break;
211
- case "tree":
212
- diagram = compileTree(doc, 図種の知らせ);
213
- break;
214
- case "journey":
215
- diagram = compileJourney(doc, 図種の知らせ);
216
- break;
217
- case "quadrant":
218
- diagram = compileQuadrant(doc, 図種の知らせ);
219
- break;
220
- case "c4":
221
- diagram = compileC4(doc);
222
- break;
223
- case "mind":
224
- diagram = compileMind(doc, 図種の知らせ);
225
- break;
226
- default:
227
- // switch case で全 type を網羅済のため default は unreachable、 template expression で
228
- // never 型を直接埋込めないので String() で明示 (defensive runtime error message 用)。
229
- throw new Error(`unknown type: ${String(doc.type)}`);
230
- }
231
- // 作り替えた名前を持つ箱と枠を、 **組み立て直後に** 控える (#1220)。 出口で題の文字だけを
232
- // 見て戻すと、 後から足された見本の中の箱がたまたま同じ題を持っていた時に書き換えてしまう
233
- const 作り替えた対象 = collectRenamedTargets(diagram, 分けた.元の名前);
234
-
235
- // edge と本文の行の対応は表に集めてから 1 edge = 1 回で知らせる (#998)。 経路ごとに
236
- // その場で呼ぶと、 同じ edge に別の行を 2 度知らせることになる。
237
- const edgeSourceLines = opts?.onEdgeSource ? new Map<string, number>() : undefined;
238
- // 矢印ごとに、どの行から来たか。 部品へ引いた矢印を部品の要素へ繋ぎ直す時に、その行に書いた
239
- // 名指し (`toPartNode`) を読む (#1979)。 矢印の id は部品の取り込みで変わらない
240
- const 矢印の行 = new Map<string, DslStep>();
241
- applyEdgeInlineOptions(diagram, doc, edgeSourceLines, 矢印の行, opts?.partsCatalog);
242
- const 作った組の枠 = applyGroupContainers(diagram, doc);
243
- // 光らせる相手が実在するかを確かめる。 id への解決は図種ごとに違うが、 名前が居るか
244
- // 居ないかは記述だけで決まるので 1 か所で見る。
245
- // 記法にはあるのに図がその相手を持たない形 (#2398) は、組み上がった図と突き合わせる
246
- reportMissingFocusTargets(書いたまま, opts?.onNotice, diagram);
247
- // 矢印が指す名前が actors に居るかを確かめる。 図種ごとの解決より前に、 記述だけで決まる
248
- reportMissingFlowActors(書いたまま, opts?.onNotice);
249
- // 両端が同じ矢印を伝える (#1227)。 落とす前の `flow` を見る
250
- // 静止した `type: flow` で書いた矢印の端が使われないことを伝える (#1269)。
251
- // 自分へ戻る形と居ない名前を指す形は既に落ちた後の `doc` を見る = 上の 2 件と重ねない
252
- reportFlowEndpointNotHonored(doc, 分けた.元の名前, opts?.onNotice, opts?.partsCatalog);
253
- reportLaneNotHonored(書いたまま, opts?.onNotice);
254
- /*
255
- * 箱の知らせが出た行を控える (#2368)。 片方だけの位置の知らせ (#2362) を同じ行に重ねない。
256
- *
257
- * 知らせる図種も知らせの種類も手で並べず、実際に出た知らせで決める (`図種の知らせ` と同じ形)。
258
- */
259
- const 箱に知らせた行 = new Set<number>();
260
- const 箱の知らせ = 受け取り口
261
- ? (n: CompileNotice): void => {
262
- 箱に知らせた行.add(n.line);
263
- 受け取り口(n);
264
- }
265
- : undefined;
266
- reportActorKindNotHonored(書いたまま, 箱の知らせ);
267
- reportTreeActorOptionNotHonored(書いたまま, 箱の知らせ);
268
- reportValueChartActorOptionNotHonored(書いたまま, 箱の知らせ);
269
- reportSkeletonActorOptionNotHonored(書いたまま, 箱の知らせ);
270
- reportHalfWrittenPosition(書いたまま, 箱に知らせた行, opts?.onNotice);
271
- // 出した行を `図種が知らせた行` に控える = 多重度の知らせ (#2107) を同じ行に重ねない
272
- reportSkeletonEdgeOptionNotHonored(書いたまま, 図種が知らせた行, 図種の知らせ);
273
- reportMessageOptionNotHonored(書いたまま, opts?.onNotice);
274
- reportBandProblems(書いたまま, diagram, opts?.onNotice);
275
- reportCardinalityNotHonored(書いたまま, 図種が知らせた行, opts?.onNotice);
276
- reportFlowOffsetNotHonored(書いたまま, opts?.onNotice);
277
- reportDocEyebrowNotHonored(書いたまま, opts?.onNotice);
278
- reportDirectionNotHonored(書いたまま, opts?.onNotice);
279
- reportOrderNotHonored(書いたまま, opts?.onNotice);
280
- reportShapeNotHonored(書いたまま, opts?.onNotice);
281
- reportFormNotHonored(書いたまま, opts?.onNotice);
282
- reportTicksNotHonored(書いたまま, opts?.onNotice);
283
- reportLaneSubtitleNotHonored(書いたまま, opts?.onNotice);
284
- reportStageFromLane(書いたまま, opts?.onNotice);
285
- reportTrackFromName(書いたまま, opts?.onNotice);
286
- reportThemeAlsoPalette(書いたまま, opts?.onNotice);
287
- reportDrawNotHonored(書いたまま, opts?.onNotice);
288
- reportChartFieldsNotHonored(書いたまま, opts?.onNotice);
289
- reportAxesNotHonored(書いたまま, opts?.onNotice);
290
- reportPartNodeNotHonored(書いたまま, opts?.onNotice);
291
- // `位置: Web の右` を実際の配置から絶対座標に直す。 以降は座標を直接書いた時と同じ経路
292
- const placed = resolveRelativeDoc(diagram, doc, opts?.onNotice, opts?.partsCatalog);
293
- // canvas pivot 新 spec = 全 preset 共通の post-process で actor.posX/Y を CDL lane / node に伝播
294
- applyCanvasPivotPositions(diagram, placed);
295
- // CAR-1657 = parts kind actor を merge (opts.partsCatalog 経由)、 applyV05Extensions 後段で実行
296
- const 追加した縦列: DslLane[] = [];
297
- const extended = applyV05Extensions(diagram, placed, 追加した縦列);
298
- /*
299
- * 書いた色を箱に載せる (#2333)。 **題を写した後に呼ぶ**。
300
- *
301
- * 照合は箱に出る題で行う (理由は `applyNodeTones` の説明)。 その題が箱に入る時点が
302
- * 図種で 2 つに割れており、15 図種は組み立て器が `箱の題` で入れる一方、
303
- * 順序図 / 帯図 / c4 は登場人物の名前で箱を作り、`applyV05Extensions` が
304
- * 題へ書き換える。 書き換えより前に照合すると、後者の 4 図種で題と名前が食い違う。
305
- *
306
- * 実測 = ここより前で呼んでいた間、題を書いた箱の色が 5 図種 (フロー / 構成図 /
307
- * 状態遷移 / クラス図 / ER 図) で消え、名前で照合していたため帯図と c4 だけが通っていた。
308
- * 呼ぶ位置を変えずに照合だけを題へ寄せると、今度は帯図と c4 が落ちる (実測)。
309
- */
310
- applyNodeTones(extended, placed);
311
- // 値の知らせは、本文なら値を書いた行、見本なら見本を置いた行を指す。 `derived` 自体には
312
- // source position が無いため、見本を重ねる間だけ別表で宣言元を持ち回る (#1180)。
313
- const inheritedDerivedSourceLines = opts?.onNotice ? new Map<string, number[]>() : undefined;
314
- for (const value of extended.derived ?? []) {
315
- recordDerivedSourceLine(inheritedDerivedSourceLines, value.id, 0);
316
- }
317
- const merged = mergePartsFromActors(
318
- extended,
319
- placed,
320
- opts?.partsCatalog,
321
- opts?.onNotice,
322
- inheritedDerivedSourceLines,
323
- 矢印の行,
324
- );
325
- // 箱が 1 つも入らなかった縦列を伝える (#1241)。
326
- //
327
- // **見本 (parts) を重ねた後に見る**。 見本の箱は `lane` で行き先を選べるため、
328
- // `lanes:` で作った縦列に後から入る (実測 = 重ねる前に見ると、箱が入っている縦列にまで
329
- // 知らせが出た)。
330
- //
331
- // 字の集合では防げない = 全角 (`lane-A` に対し生成は `lane-a`) でも打ち間違い
332
- // (`lane-idl`) でも結果は同じで、新しい縦列が増えるだけで書いた幅は元の縦列に届かない。
333
- // 受け付けを字で絞るのではなく、合わなかったこと自体を伝える
334
- reportEmptyDeclaredLanes(merged, 追加した縦列, opts?.onNotice);
335
-
336
- // 表が揃ってから 1 edge = 1 回で知らせる。 merge 後に残っている edge だけを対象にする =
337
- // 途中で消えた edge の行を知らせても呼出側が使えない。
338
- if (edgeSourceLines && opts?.onEdgeSource) {
339
- const alive = new Set(merged.edges.map((e) => e.id));
340
- for (const [id, line] of edgeSourceLines) {
341
- if (alive.has(id)) opts.onEdgeSource(id, line);
342
- }
343
- }
344
- // 動かない図に段を 1 つ入れる (#1086)。
345
- //
346
- // 描画側は「段が 1 件以上」 を要求するが、 段を作るかどうかは種類ごとにばらけている。
347
- // 実測 = `animation:` を書かない同じ記法を旧 12 種に与えると、6 種 (sequence / flow / er /
348
- // state / topology) は描かれ、6 種 (swimlane / gantt / class / pie / c4 / mind)
349
- // は「phase が 0 件です」 で弾かれた。 書く人から見ると区別する手がかりが無い。
350
- //
351
- // **出口で 1 度だけ見る**。 種類ごとに塞ぐと 12 経路のどれかを見落とす。 図は必ずここを
352
- // 通るので、 ここで段が無ければ入れる。
353
- injectStaticPhase(merged);
354
- // 書いた状態を図に載せる (#1162)。 段を書かない図でも値が届くようにする。
355
- // **値を載せるより先に呼ぶ**。 状態が空のまま式を解くと、参照が全て「無い名前」 になる。
356
- materializeStates(merged, doc);
357
- /*
358
- * 矢印をいつ出すか (#1470)。 描き手がそのまま読む欄なので、書いた語を素通しする。
359
- *
360
- * **出口で 1 度だけ載せる**。 図の種類ごとに組み立てが分かれており、経路ごとに書くと
361
- * どれかを見落とす (`injectStaticPhase` / `materializeStates` と同じ理由)。
362
- */
363
- if (doc.reveal !== undefined) merged.edgeReveal = doc.reveal;
364
- if (doc.relations !== undefined) merged.relationFocus = doc.relations;
365
- // 語の欄が状態を読むとき、その状態には記法の語が入っている。 図の語へ直す (#1201)
366
- 語の状態を図の語へ直す(merged);
367
- // きっかけ形の値を段の時計を読む式へ畳む (#1161 段 2)。 **値を載せるより先に呼ぶ** =
368
- // 畳んだ式を `attachDerivedValues` が他の値と同じ経路で載せるため、 順序 / 重なり / 知らせの
369
- // 扱いが式形と揃う。 時計の状態と段の補間もここで足す
370
- const 畳んだきっかけ = foldValueTriggers(merged, doc, opts?.onNotice);
371
- attachDerivedValues(merged, doc, opts?.onNotice, inheritedDerivedSourceLines, 畳んだきっかけ);
372
- // 値を見せる部品を図に載せる (#1374)。 parts が持つ部品は merge 済みなので残したまま足す。
373
- // **外部参照を落とす前に載せる**。 後から足すと readout の color / colors だけが出口の検査を
374
- // 迂回し、 `url(https://...)` がそのまま SVG の paint 属性へ届く。
375
- if (doc.readouts && doc.readouts.length > 0) {
376
- // 出口の paint 検査は diagram を直接書き換える。 doc の object を共有すると、検査が
377
- // compileToCdl の入力まで書き換えて入力不変性を壊すため、nested field も含めて写す。
378
- //
379
- // **書いた行は図へ載せない** (#2405)。 行は読む元の名前を突き合わせた知らせのために持つ。
380
- // 残すと、行を足しただけで組み上がる図が変わる。
381
- type 図の読み取り値 = NonNullable<CdlDiagram["readouts"]>[number];
382
- const ownReadouts = doc.readouts.map(({ pos: _書いた行, ...readout }) =>
383
- deepRewriteStrings(readout, (value) => value) as 図の読み取り値,
384
- );
385
- merged.readouts = [...(merged.readouts ?? []), ...ownReadouts];
386
- }
387
- /*
388
- * 読む人が動かすつまみを図に載せる (#1389)。
389
- *
390
- * 部品と同じく写して載せる = 出口の paint 検査が diagram を直接書き換えるため、doc の
391
- * object を共有すると `compileToCdl` の入力まで書き換わって入力不変性が壊れる。
392
- */
393
- if (doc.inputs && doc.inputs.length > 0) {
394
- const ownInputs = doc.inputs.map(
395
- (input) => deepRewriteStrings(input, (value) => value) as typeof input,
396
- );
397
- merged.inputs = [...(merged.inputs ?? []), ...ownInputs];
398
- }
399
- /*
400
- * つまみの値から決まる値を図に載せる (#1391)。
401
- *
402
- * 部品やつまみと同じく写して載せる。 式の文字列は色を塗る位置に届かないが、
403
- * 入力を書き換えない形を 3 経路で揃える方が読み手に説明しやすい。
404
- */
405
- if (doc.formulas && doc.formulas.length > 0) {
406
- const ownFormulas = doc.formulas.map((formula) => ({
407
- id: formula.id,
408
- expression: formula.expression,
409
- // 名札は操作部が式の名前として描く (#1916、cdl#853)。 書かなければ欄ごと付けない
410
- ...(formula.label !== undefined ? { label: formula.label } : {}),
411
- line: formula.pos?.line ?? 0,
412
- }));
413
- /*
414
- * 式が読む名前が、どこにも書かれていないことを知らせる (#1391)。
415
- *
416
- * engine が読める名前は、つまみと **先に宣言した式** だけ。綴り違い、状態、
417
- * `values:`、後から宣言する式を渡すと、描画時に未定義参照として例外になる。
418
- *
419
- * **解けない式は図へ載せない**。 engine は未定義参照や input/formula の同名衝突を
420
- * runtime error にするため、残すと図全体が描けない。式以外の図は出し、知らせを返す。
421
- */
422
- const つまみ = new Map((merged.inputs ?? []).map((input) => [input.id, input]));
423
- const 先に書かれた式 = new Set((merged.formulas ?? []).map((formula) => formula.id));
424
- const 載せる式 = [...(merged.formulas ?? [])];
425
- for (const formula of ownFormulas) {
426
- if (つまみ.has(formula.id) || 先に書かれた式.has(formula.id)) {
427
- opts?.onNotice?.({
428
- kind: "formula-unresolved",
429
- actor: formula.id,
430
- line: formula.line,
431
- message: `式 "${formula.id}" の名前が、先に書かれたつまみまたは式と重なっています`,
432
- hint: "`inputs:` と `formulas:` では重ならない名前を使う",
433
- });
434
- continue;
435
- }
436
- let 名前たち: Set<string>;
437
- try {
438
- 名前たち = extractIdentifiers(parseFormula(formula.expression));
439
- } catch {
440
- // 読めない式は記法の読み取りが既に知らせている。 ここで二重に出さない
441
- continue;
442
- }
443
- let 解けない = false;
444
- for (const 名 of 名前たち) {
445
- if (先に書かれた式.has(名)) continue;
446
- const input = つまみ.get(名);
447
- if (input) {
448
- const 初期値 = inputDefaultValue(input);
449
- if (typeof 初期値 === "number" || typeof 初期値 === "boolean") continue;
450
- 解けない = true;
451
- opts?.onNotice?.({
452
- kind: "formula-unresolved",
453
- actor: formula.id,
454
- line: formula.line,
455
- message: `式 "${formula.id}" が、数でも真偽でもないつまみ "${名}" を読んでいます`,
456
- hint: "式が読めるつまみは slider / number / stepper / timeline / toggle",
457
- });
458
- continue;
459
- }
460
- 解けない = true;
461
- opts?.onNotice?.({
462
- kind: "formula-unresolved",
463
- actor: formula.id,
464
- line: formula.line,
465
- message: `式 "${formula.id}" が、つまみまたは先に書かれた式ではない名前 "${名}" を読んでいます`,
466
- hint: "`inputs:` に書くか、参照される式をこの式より前に書く",
467
- });
468
- }
469
- if (解けない) continue;
470
- 載せる式.push({
471
- id: formula.id,
472
- expression: formula.expression,
473
- ...(formula.label !== undefined ? { label: formula.label } : {}),
474
- });
475
- 先に書かれた式.add(formula.id);
476
- }
477
- if (載せる式.length > 0) merged.formulas = 載せる式;
478
- else delete merged.formulas;
479
- }
480
- // **3 つの節を全て載せ終えてから呼ぶ** = 読む元は `states` / `inputs` / `formulas` に散っており、
481
- // 途中で呼ぶと、後から載る節の名前を「無い」 と知らせる
482
- // **つまみを載せ終えてから呼ぶ** = 記法は `states:` を `inputs:` より前に書けるため、
483
- // 途中で呼ぶと後から載るつまみを見落とす
484
- reportStateInputMismatch(doc, merged, opts?.onNotice);
485
- reportReadoutSourceMissing(doc, merged, opts?.onNotice);
486
- /*
487
- * 押下などの出来事で動く仕掛けを図に載せる (#1393)。
488
- *
489
- * **相手の名前を識別子へ直す**。 記法は識別子を書けないため名前で指す。 指す先が
490
- * 見つからない形は載せずに知らせる = 描画側は知らない識別子を黙って無視するため、
491
- * 残すと「書いたのに押しても何も起きない」 が手掛かりなしで起きる。
492
- *
493
- * 識別子は書いた順に `evt-1` から振る (組み立て API と同じ)。
494
- */
495
- if (doc.events && doc.events.length > 0) {
496
- const 載せる: NonNullable<CdlDiagram["eventBindings"]> = [...(merged.eventBindings ?? [])];
497
- for (const e of doc.events) {
498
- const 相手 = 出来事の相手を解く(merged, doc, e);
499
- if (相手 === undefined) {
500
- /*
501
- * **原因を 2 つに分ける** (#2336)。 書いた名前が本文に在るかで、直し方が正反対になる。
502
- *
503
- * 無い = 綴り違いなので「名前で書く」 の案内が効く。
504
- * 在る = その図種が登場人物を箱 / 矢印にしないので、同じ案内に従っても永久に直らない。
505
- * 1 つの知らせにまとめていた間、後者は書き直しの繰り返しに入る形だった
506
- * (実測 = 24 図種のうち箱を指せるのは 7 図種)。
507
- */
508
- const 呼び名 = 出来事の相手の呼び名[e.target.kind];
509
- opts?.onNotice?.(
510
- 出来事の相手が本文に在る(doc, e)
511
- ? {
512
- kind: "event-target-not-honored",
513
- actor: e.handlerId,
514
- line: e.pos.line,
515
- message:
516
- `type: ${doc.type} は書いた名前を${呼び名}にしないため、` +
517
- ` 出来事 "${e.handlerId}" を付けられません`,
518
- hint: `名前の書き方の問題ではありません。 図そのもの (\`diagram: true\`) を指すか、${呼び名}を作る図種に変えてください`,
519
- }
520
- : {
521
- kind: "event-target-missing",
522
- actor: e.handlerId,
523
- line: e.pos.line,
524
- message: `出来事 "${e.handlerId}" が指す相手が見つかりません`,
525
- hint: "box は箱の名前、 lane は縦列の名前、 arrow は `A -> B` で書く",
526
- },
527
- );
528
- continue;
529
- }
530
- 載せる.push({
531
- id: `evt-${載せる.length + 1}`,
532
- event: e.event,
533
- target: 相手,
534
- handlerId: e.handlerId,
535
- });
536
- }
537
- if (載せる.length > 0) merged.eventBindings = 載せる;
538
- }
539
- // 巻き上げに応じて進む値を図に載せる (#1393)。 相手を持たないのでそのまま写す
540
- if (doc.scrolls && doc.scrolls.length > 0) {
541
- merged.scrollTriggers = [
542
- ...(merged.scrollTriggers ?? []),
543
- ...doc.scrolls.map((x) => ({ ...x })),
544
- ];
545
- }
546
- // 図の外を指す値を、 色を塗る位置から落とす (#1004)。
547
- //
548
- // 入口ごとに塞ぐ形は採らない。 状態の上書き / phase が入れる値 / 画面が直接書く背景色 /
549
- // 埋め込んだ JSON / states / values と入口が複数あり、 1 つ見落とすと穴が残る。
550
- // **図への追加を全て終えた後**、 出口で 1 度だけ見る。 この後に状態を足すと検査を迂回する。
551
- const 外した部品の配色 = stripExternalReadoutPalettes(merged);
552
- for (const dropped of [...stripExternalPaint(merged), ...外した部品の配色]) {
553
- opts?.onNotice?.({
554
- kind: "external-paint-dropped",
555
- actor: dropped.path,
556
- line: 0,
557
- message: `図の外を指す値 (${truncateForMessage(dropped.value)}) は色として使えないため外しました`,
558
- hint: "色は `#ff0000` のような色番号か、 `red` のような色名で書く",
559
- });
560
- }
561
- // 作り替えた名前を表示だけ戻す (#1220)。 **図への追加を全て終えた後**に戻す = 途中で戻すと、
562
- // 後続の処理が名前で引く時に作り替え前と後が混ざる
563
- restoreActorNames(merged, 分けた.元の名前, 作り替えた対象);
564
- // 字幅・配色・行の縞は **図への追加を全て終えた後**に当てる (#1553 / #2918)。
565
- // 図種ごとの組み立ては 20 か所以上あり、そのどれに足しても残りが取り残される
566
- 配色と縞を当てる(merged, doc);
567
- if (doc.legend && doc.legend.length > 0) {
568
- // cdl 0.129.0 の凡例は印付き項目と注記の union。両方の欄を落とさず、組み立て API と
569
- // 同じ順 (mark/text/tone/lineStyle または text/align/lead) で写す。
570
- merged.legend = doc.legend.map((item) =>
571
- (item.mark === undefined
572
- ? {
573
- text: item.text,
574
- ...(item.align === undefined ? {} : { align: item.align }),
575
- ...(item.lead === undefined ? {} : { lead: item.lead }),
576
- }
577
- : {
578
- mark: item.mark,
579
- text: item.text,
580
- ...(item.tone === undefined ? {} : { tone: item.tone }),
581
- ...(item.lineStyle === undefined ? {} : { lineStyle: item.lineStyle }),
582
- }) as CdlLegendItem,
583
- );
584
- }
585
- if (doc.legendFontSize !== undefined) merged.legendFontSize = doc.legendFontSize;
586
- if (doc.legendOffset !== undefined) merged.legendOffset = doc.legendOffset;
587
- if (doc.edgeEndGap !== undefined) merged.edgeEndGap = doc.edgeEndGap;
588
- if (doc.stageLayout !== undefined) merged.stageLayout = doc.stageLayout;
589
- 静止した図の焦点を外す(merged, doc);
590
- // 位置のずらしは **配置に効く欄を全て載せた後** に当てる (#1971)。 縦列の幅や視点の間隔を
591
- // 足す前に測ると、後から足された分だけ狙いがずれる
592
- applyLayoutOffsets(merged, doc, opts?.onNotice);
593
- // 組の枠は **縦列の位置が全て決まった後** に置く (#1972)。 位置のずらしより前に置くと、
594
- // ずらした縦列から枠が離れる
595
- applyGroupFrames(merged, doc, 作った組の枠, opts?.onNotice);
596
- cdlの未指定欄を外す(merged);
597
- return merged;
598
- }
599
-
600
- /** cdl が object に足す未指定欄を、記法で値を書かなかった時の省略形へ戻す。 */
601
- function cdlの未指定欄を外す(diagram: CdlDiagram): void {
602
- for (const node of diagram.nodes) {
603
- if (node.terminalDecoration === undefined) delete node.terminalDecoration;
604
- if (node.cornerForm === undefined) delete node.cornerForm;
605
- if (node.figureKindLabel === undefined) delete node.figureKindLabel;
606
- if (node.figureInset === undefined) delete node.figureInset;
607
- if (node.chartBarSize === undefined) delete node.chartBarSize;
608
- if (node.chartBarShape === undefined) delete node.chartBarShape;
609
- if (node.barCornerRadii === undefined) delete node.barCornerRadii;
610
- if (node.chartLineSize === undefined) delete node.chartLineSize;
611
- if (node.chartPieSize === undefined) delete node.chartPieSize;
612
- if (node.chartWaffleSize === undefined) delete node.chartWaffleSize;
613
- if (node.chartSlopeLabelTemplate === undefined) delete node.chartSlopeLabelTemplate;
614
- if (node.chartSlopeSize === undefined) delete node.chartSlopeSize;
615
- if (node.chartStackedLayout === undefined) delete node.chartStackedLayout;
616
- if (node.chartGaugeSize === undefined) delete node.chartGaugeSize;
617
- if (node.ganttSize === undefined) delete node.ganttSize;
618
- if (node.journeySize === undefined) delete node.journeySize;
619
- if (node.quadrantLabelPosition === undefined) delete node.quadrantLabelPosition;
620
- if (node.quadrantSize === undefined) delete node.quadrantSize;
621
- }
622
- for (const edge of diagram.edges) {
623
- if (edge.posX1 === undefined) delete edge.posX1;
624
- if (edge.posY1 === undefined) delete edge.posY1;
625
- if (edge.posX2 === undefined) delete edge.posX2;
626
- if (edge.posY2 === undefined) delete edge.posY2;
627
- if (edge.waypoints === undefined) delete edge.waypoints;
628
- if (edge.labelCollision === undefined) delete edge.labelCollision;
629
- if (edge.labelSize === undefined) delete edge.labelSize;
630
- }
631
- if (diagram.stageLayout === undefined) delete diagram.stageLayout;
632
- }
633
-
634
- /**
635
- * 段を書かない図では、どの箱も「いま」 にしない (#1557)。
636
- *
637
- * ## 何が起きていたか
638
- *
639
- * `animation:` を書かない図でも段は 1 つ作られる。 作るのは 2 経路あり、cdl の組み立て器
640
- * (`er()` / `flow()` / `topology()` 等) が図種ごとに 1 段を作る経路と、どちらも作らない図種に
641
- * dragon が 1 段を足す経路 (`injectStaticPhase`)。 どちらも **全ての箱と矢印を段に載せる**。
642
- *
643
- * 描画側は段に載った箱を「いま」 として描く (枠を主役色にして太くする)。 その結果、静止した図は
644
- * 全ての箱が「いま」 になり、止まっている箱の枠が 1 度も出ない (実測 = ER 図 3 箱 / フロー 2 箱)。
645
- *
646
- * 全部を強調するのと何も強調しないのは、どちらも「区別が無い」 状態。 後者の方が落ち着いて読める。
647
- *
648
- * ## 矢印は段に載せたまま置く
649
- *
650
- * 描画側の既定 (`edgeReveal: "phase"`) は「段が名指しする矢印は、その段が来るまで描かない」。
651
- * 矢印まで外すと、静止した図から線が 1 本も出なくなる。
652
- *
653
- * ## 判定は書いた内容で決める
654
- *
655
- * 「段に全ての箱が載っている図」 を目印にしない = 書き手が 1 段だけ書いて全部を焦点にした図と
656
- * 見分けが付かない。 見るのは `animation:` を書いたかどうかで、これは書き手が段のために書く値。
657
- */
658
- function 静止した図の焦点を外す(diagram: CdlDiagram, doc: DslDocument): void {
659
- if ((doc.animate?.phases.length ?? 0) > 0) return;
660
- const 箱 = new Set(diagram.nodes.map((n) => n.id));
661
- for (const phase of diagram.phases) {
662
- phase.activate = phase.activate.filter((id) => !箱.has(id));
663
- }
664
- }
665
-
666
- /**
667
- * 行を書いた箱か (#2782)。 縞を敷く相手を決める。
668
- *
669
- * 縞の目的は行を横に追うことなので、行が無い箱に敷いても追う対象が無い。
670
- */
671
- function 行を書いた箱か(node: CdlNode): boolean {
672
- return (node.rows?.length ?? 0) > 0;
673
- }
674
-
675
- /**
676
- * 図の字幅・配色と、行を書いた箱の縞を当てる (#1553 / #2782 / #2918)。
677
- *
678
- * ## 配色
679
- *
680
- * cdl は色を持たない。 名前だけを `data-cdl-palette` として markup に出し、消費側
681
- * (`cdl-theme.css`) が名前を見て 7 つの口 (台 / 行の面 / 縞 / 枠 / 字 / 型名 / 線) に色を当てる。
682
- * 字の測り方は配色の有無にかかわらず、この出口で比例・等幅の広い方へ揃える。端末だけ
683
- * 等幅へ替えると配色で骨組みが変わり、比例だけでは端末の字が箱から出るためである。
684
- * この出口は後段の配置計算より前なので、箱・札・縦列の隙間も同じ幅で測られる (#2818)。
685
- *
686
- * **`record` は書かなければ配色を持たない** (#2782)。 畳む前は `type: er` が書かなくても
687
- * `kinari` (生成りに茶) になったが、畳んだ先には移り変わりを書いた図も入る。
688
- * 表の図と移り変わりの図は行の有無でも行の中身でも分かれない = 同じ `kind: storage` に
689
- * 同じ形の行 (`督促を送る: 7 日ごと`) を書く。 分けられない材料で推し量ると、
690
- * どちらかの図が書いていない色みを名乗る。
691
- *
692
- * これは #2782 で落とした 3 つの既定 (始まりと終わりの札 / 矢印の先端 / 箱の種類) と
693
- * 同じ形で、**書かないものを図が名乗らない** 側に倒している。
694
- * 表の図には見本の側で `palette: kinari` を書く。
695
- *
696
- * ## 行の縞
697
- *
698
- * 縞の色は配色からしか来ない。 配色が無い図に敷くと箱の面と同じ色に落ちて 1 本も出ないので、
699
- * **配色が決まった図の、行を書いた表の箱** にだけ敷く。
700
- *
701
- * 畳む前は組み立て器 `er()` が縞を敷く経路と、箱を直に組む経路の 2 つがあり、同じ図が
702
- * 動きの有無で縞を持ったり持たなかったりしていた。 経路が 1 本になった後も出口で揃える。
703
- */
704
- function 配色と縞を当てる(diagram: CdlDiagram, doc: DslDocument): void {
705
- diagram.textMetrics = "widest";
706
- const 配色 = doc.theme;
707
- if (配色 === undefined) return;
708
- diagram.palette = 配色;
709
- for (const node of diagram.nodes) {
710
- if (棒を描く節か(node)) {
711
- const 棒の角 = 意匠の棒の角(配色, node);
712
- if (棒の角 !== undefined) node.barCornerRadii = 棒の角;
713
- }
714
- if (node.kind === "storage" && 行を書いた箱か(node) && node.rowStripe === undefined) {
715
- node.rowStripe = true;
716
- }
717
- }
718
- }
719
-
720
- /**
721
- * readout の配色配列から外部参照を落とす (#1374)。
722
- *
723
- * 共通の `stripExternalPaint` は `color` / `fill` のような key を見るが、 `colors` の中へ
724
- * 入ると配列要素には key が無い。 `heat-cell` が公開した配色だけはここで要素ごとに閉じる。
725
- */
726
- function stripExternalReadoutPalettes(diagram: CdlDiagram): Array<{ path: string; value: string }> {
727
- const stripped: Array<{ path: string; value: string }> = [];
728
- for (let i = 0; i < (diagram.readouts?.length ?? 0); i += 1) {
729
- const readout = diagram.readouts?.[i] as { colors?: readonly string[] } | undefined;
730
- if (!readout?.colors) continue;
731
- const colors = [...readout.colors];
732
- for (let j = 0; j < colors.length; j += 1) {
733
- const color = colors[j];
734
- if (color === undefined || !pointsOutside(color)) continue;
735
- colors[j] = "none";
736
- stripped.push({ path: `readouts[${i}].colors[${j}]`, value: color });
737
- }
738
- readout.colors = colors;
739
- }
740
- return stripped;
741
- }
742
-
743
- /**
744
- * 書いた状態 (`states:`) を図に載せる (#1162)。
745
- *
746
- * 状態を図に登録するのは `animation:` を書いた経路だけだった (`compileGenericWithAnimate` と
747
- * `injectPhasesFallback` のどちらも段がある時しか走らない)。 このため `states:` と `values:`
748
- * だけを書いた図では図の状態が 0 件になり、描画側が組み立てる値が空になる。 書いた値は
749
- * 1 つも届かず、箱には `{waiting}` の生の形が出ていた (実測)。
750
- *
751
- * **出口で 1 度だけ載せる**。 段を作る経路は図の種類ごとにばらけており、経路ごとに書くと
752
- * どれかを見落とす (`injectStaticPhase` と同じ理由)。
753
- *
754
- * 既に載っている名前は触らない。 段の経路が登録した初期値と、見本から引き継いだ状態
755
- * (`alias__id` の形) の両方を保つ。
756
- */
757
- function materializeStates(diagram: CdlDiagram, doc: DslDocument): void {
758
- const 載っている = new Set(diagram.states.map((s) => s.id));
759
- for (const st of doc.animate?.states ?? []) {
760
- if (載っている.has(st.name)) continue;
761
- diagram.states.push({ id: st.name, initial: st.initial });
762
- 載っている.add(st.name);
763
- }
764
- }
765
-
766
- /**
767
- * 動かない図に段を 1 つ入れる (#1086)。
768
- *
769
- * 描画側は段が 1 件以上あることを要求する。 一方で段を作るかどうかは種類ごとにばらけており、
770
- * `animation:` を書かない図は 12 種のうち 6 種だけが描かれ、 残り 6 種は弾かれていた。
771
- *
772
- * ## 空の段にはしない
773
- *
774
- * 描画側の既定 (`edgeReveal: "phase"`) は「段が名指しする矢印は、その段が来るまで描かない」。
775
- * 空の段を入れると、静止した図から線が 1 本も出ない。
776
- *
777
- * **箱は段に載せた後で外す** (#1557)。 ここで載せるのは矢印を描かせるためで、箱まで
778
- * 「いま」 のままにすると静止した図の全ての箱が主役色の枠になる。 外すのは出口の
779
- * `静止した図の焦点を外す` が 1 か所で行う = 段を作る経路は cdl の組み立て器にもあり、
780
- * ここだけ直しても図種によって残る。
781
- *
782
- * ## 既に段がある図には触らない
783
- *
784
- * `animation:` を書いた図と、 描画側が段を作る 6 種はここに入らない。 段の数と中身は書いた
785
- * とおりに保たれる。
786
- */
787
- function injectStaticPhase(diagram: CdlDiagram): void {
788
- if (diagram.phases.length > 0) return;
789
- // 光らせる相手が 1 つも無い図 (要素ゼロ) でも段は入れる。 描画側が要求するのは段の存在で
790
- // あって中身ではなく、 ここで諦めると「空の図は描けない」 という別の欠落になる
791
- const activate = [...diagram.nodes.map((n) => n.id), ...diagram.edges.map((e) => e.id)];
792
- diagram.phases.push({
793
- id: "static",
794
- duration: 1000,
795
- title: diagram.topic ?? "全体",
796
- body: "",
797
- activate,
798
- tweens: [],
799
- sets: [],
800
- });
801
- }
802
-
803
- /**
804
- * 解決できない矢印を落とした `flow` を返す (#1219)。
805
- *
806
- * 落とすのは **組み立てに渡す分だけ**。 知らせ (`reportMissingFlowActors`) は元の `flow` を
807
- * 見るので、 落とした矢印も書いた人に届く。
808
- */
809
- function dropUnresolvedFlow(doc: DslDocument): DslDocument {
810
- if (図種の作り[doc.type] === "図全体を 1 箱") return doc;
811
- const 表 = actorRefTable(doc);
812
- const flow = doc.flow.filter((s) => 表.has(s.from) && 表.has(s.to));
813
- return flow.length === doc.flow.length ? doc : { ...doc, flow };
814
- }
815
-
816
- /**
817
- * 箱ごとに分かれる図種で最上位の小見出しを書いた時に伝える (#1247)。
818
- *
819
- * 小見出しは **箱 1 つに対して 1 つ**。 図全体を 1 箱にする図種 (`pie` / `bar` 等) では
820
- * 相手が決まるが、 箱ごとに分かれる図種では「どの箱の小見出しか」 が決まらない。
821
- *
822
- * 黙って捨てると「書いたのに出ない」 が手掛かりなしで起きる。 箱ごとに書く形
823
- * (`- A: { eyebrow: "..." }`) を案内する = そちらは従来どおり効く。
824
- */
825
- function reportDocEyebrowNotHonored(doc: DslDocument, onNotice?: (n: CompileNotice) => void): void {
826
- if (!onNotice) return;
827
- if (doc.eyebrow === undefined) return;
828
- if (図種の作り[doc.type] === "図全体を 1 箱") return;
829
- onNotice({
830
- kind: "eyebrow-not-honored",
831
- actor: doc.title,
832
- // 図の `pos` は常に 1 行目を指す。 書いた行に辿り着けるよう `eyebrowPos` を優先する
833
- line: doc.eyebrowPos?.line ?? doc.pos?.line ?? 0,
834
- message: `最上位に書いた eyebrow は効きません (type: ${doc.type} は箱ごとに分かれるため相手が決まりません)`,
835
- hint: '箱ごとに書いてください (`- A: { eyebrow: "..." }`)',
836
- });
837
- }
838
-
839
- /**
840
- * 起点から描く動きを持たない図種で段に `draw:` を書いた時に伝える (#1312 / #1318)。
841
- *
842
- * 対応する 8 図種以外では書いても何も起きないため、黙って捨てると
843
- * 「書いたのに現れない」 が手掛かりなしで起きる。
844
- *
845
- * 段ごとに知らせる = 5 段のうち 1 段だけに書いた形で、どの段が効いていないかを読めるようにする。
846
- */
847
- function reportDrawNotHonored(doc: DslDocument, onNotice?: (n: CompileNotice) => void): void {
848
- if (!onNotice) return;
849
- for (const phase of doc.animate?.phases ?? []) {
850
- if (phase.draw === undefined) continue;
851
- // 段の `pos` は `- step:` の行を指す。 書いた行に辿り着けるよう `drawPos` を優先する
852
- const line = phase.drawPos?.line ?? phase.pos?.line ?? 0;
853
- if (!DRAWABLE_DOC_TYPES.has(doc.type)) {
854
- onNotice({
855
- kind: "draw-not-honored",
856
- actor: phase.name,
857
- line,
858
- message: `段 "${phase.name}" に書いた draw は効きません (type: ${doc.type} は起点から描く動きを持ちません)`,
859
- hint: `draw が効くのは type: ${[...DRAWABLE_DOC_TYPES].join(" / ")} です`,
860
- });
861
- continue;
862
- }
863
- // 語が別の図を指している形 (#1314)。 図は描けるので誤りにはしない
864
- const 相手 = DRAW_TARGETS.get(phase.draw);
865
- const 数の図の形 = doc.shape === undefined ? undefined : (resolveShape(doc.shape) ?? undefined);
866
- if (相手 !== undefined && !描く語がその図を指すか(phase.draw, doc.type, 数の図の形)) {
867
- // 数を描く図は 1 つの型に 9 つの形が入るので、相手も この図も形で名乗る (#2657)
868
- const 名乗り = (t: PresetType, sh: DslShape | undefined): string =>
869
- t === "chart" && sh !== undefined ? `type: chart と shape: ${sh}` : `type: ${t}`;
870
- const この図 = 名乗り(doc.type, 数の図の形);
871
- onNotice({
872
- kind: "draw-target-mismatch",
873
- actor: phase.name,
874
- line,
875
- message: `段 "${phase.name}" の draw: ${phase.draw} は ${この図} では効きません (${phase.draw} は ${名乗り(相手.type, 相手.shape)} の図に書きます)`,
876
- hint: `この図では draw: ${doc.type === "chart" ? (doc.shape ?? "bar") : doc.type} と書いてください`,
877
- });
878
- }
879
- }
880
- }
881
-
882
- /**
883
- * 段の `draw:` が効く図種 (#1312 / #1314 / #1318)。
884
- *
885
- * **語と図種の対応表から導く** (`DRAW_TARGETS`)。 一覧を写すと、語を足した時に片方だけ
886
- * 古いまま残る。 描画側 (`cdl` の `DRAW_KINDS`) が対応する種別と一致する。
887
- */
888
- const DRAWABLE_DOC_TYPES: ReadonlySet<PresetType> = new Set<PresetType>(
889
- [...DRAW_TARGETS.values()].map((v) => v.type),
890
- );
891
-
892
- /**
893
- * 縦列を選べる図種で、一部の箱だけが縦列を書いた時に伝える (#1263)。
894
- *
895
- * 書かなかった箱の行き先を決める規則が要るが、既定の縦列に集めても自分の縦列を作っても
896
- * 書いた人の意図と一致する保証が無い。 **全部書くか 1 つも書かないか** を求める。
897
- *
898
- * 1 つも書いていない形は従来どおりの並びになるだけなので知らせない。
899
- *
900
- * 数える箱は置く側と同じ `縦列の判定に数える箱` から採る (#2372)。 別々に数えると、
901
- * 見本だけの図で「一部だけ書いた」 形が知らせずに落ちる。
902
- */
903
- function reportLaneMixed(doc: DslDocument, onNotice: (n: CompileNotice) => void): void {
904
- const 対象 = 縦列の判定に数える箱(doc);
905
- const 書いた = 対象.filter((a) => a.lane !== undefined);
906
- if (書いた.length === 0 || 書いた.length === 対象.length) return;
907
- const 書いていない = 対象.filter((a) => a.lane === undefined);
908
- onNotice({
909
- kind: "lane-not-honored",
910
- actor: 書いていない[0]?.name ?? "",
911
- line: 書いていない[0]?.pos?.line ?? 0,
912
- message: `type: ${doc.type} では縦列を書くなら全ての箱に書きます (書いていない箱: ${書いていない.map((a) => truncateForMessage(a.name)).join(" / ")})`,
913
- hint: "書かなかった箱をどの縦列に置くかを決められないため、全部書くか 1 つも書かないかにしてください",
914
- });
915
- }
916
-
917
- /**
918
- * 静止した `type: flow` で、書いた矢印の端が使われないことを伝える (#1269)。
919
- *
920
- * この図種は **登場人物を書いた順に鎖状に繋ぐ**。 矢印の説明文は「その箱を to に持つ行」
921
- * から拾い、書いた側の端 (from) は使わない (`compileFlow`)。
922
- *
923
- * そのため `A -> C` と書いても出来るのは `A -> B` で、書いた形と違う図になる。
924
- * 実測 = `A -> C` / `C -> B` と書くと `A -> B` に `"y"`、`B -> C` に `"x"` が載った。
925
- * 知らせは 1 件も出ていなかった。
926
- *
927
- * ## 鎖にすること自体は変えない
928
- *
929
- * 線形の流れを描く図種なので、鎖にするのは仕様。 書いた端どおりに繋ぎたい形は
930
- * 箱に `lane:` を書くか (#1266)、`direction:` を書けば別の経路へ回る (#1494)。 知らせの hint で
931
- * それを案内する。 `direction: 縦` を先に挙げるのは、箱ごとに書かずに済み、並びも鎖の時と変わらないため。
932
- *
933
- * ## 偶然一致する形では知らせない
934
- *
935
- * `A -> B` / `B -> C` のように書いた端がそのまま鎖になる形は、書いたとおりの図になる。
936
- * ここで知らせると、正しく書いた人にまで出る。
937
- *
938
- * ## 既に落ちた行は見ない
939
- *
940
- * 居ない名前を指す形 (`flow-actor-missing`) は別の知らせが担う。 落とした後の `doc` を
941
- * 見ることで、同じ 1 行に知らせが 2 件並ぶのを避ける。
942
- *
943
- * 自分へ戻る形は #1462 で落とさなくなった (描画側が輪として描ける)。
944
- */
945
- function reportFlowEndpointNotHonored(
946
- doc: DslDocument,
947
- 元の名前: Map<string, string>,
948
- onNotice?: (n: CompileNotice) => void,
949
- /** 鎖に並べない部品を決める一覧 (#1987)。 `compileFlow` に渡したものと同じ */
950
- partsCatalog?: Record<string, CdlDiagram>,
951
- ): void {
952
- if (!onNotice) return;
953
- if (!鎖でつなぐ形か(doc)) return;
954
- // 名前が重なった登場人物は組み立ての間だけ尾を付けて分けている (`disambiguateActorIds`)。
955
- // **判定は分けた後の名前で行い、伝える時は書いた名前に戻す** = 判定は `compileFlow` の
956
- // 繋ぎ方と揃える必要があり、伝える先は本文なので書いていない名前を指すと直せない
957
- // (実測 = `"A B 546d26" の端は使われません` と出て、本文にその名前は無い)
958
- const 書いた名前 = (name: string): string => 元の名前.get(name) ?? name;
959
- // 鎖が作る組を集める。 登場人物が 1 人以下なら矢印が 1 本も出来ないので、
960
- // 書いた矢印は全て使われない扱いになる
961
- const 鎖の組 = new Set<string>();
962
- const 並び = 鎖に並べる登場人物(doc, partsCatalog);
963
- 並び.forEach((a, i) => {
964
- const 次 = 並び[i + 1];
965
- if (次 === undefined) return;
966
- 鎖の組.add(`${a.name}\u0000${次.name}`);
967
- });
968
- for (const s of doc.flow) {
969
- if (鎖の組.has(`${s.from}\u0000${s.to}`)) continue;
970
- onNotice({
971
- kind: "flow-endpoint-not-honored",
972
- actor: 書いた名前(s.from),
973
- line: s.pos.line,
974
- message: `"${truncateForMessage(書いた名前(s.from))} -> ${truncateForMessage(書いた名前(s.to))}" の端は使われません (type: flow は登場人物を書いた順に繋ぎます)`,
975
- hint: `書いた端どおりに繋ぐには次のどれかを書いてください: ${鎖にしない書き方の案内()} (どれも書いた端がそのまま矢印になります)`,
976
- });
977
- }
978
- }
979
-
980
- /**
981
- * 板の言づてで、効かないと伝えない矢印の欄 (#2356)。
982
- *
983
- * **除外側を書く**。 効かない欄を並べる形にすると、矢印に欄を足した日にその欄だけが
984
- * 一覧から漏れ、書いた人には「書いたのに何も起きない」 としか見えない。
985
- *
986
- * | 区分 | 欄 |
987
- * |---|---|
988
- * | 板が描く | 出どころ / 行き先 / 語 / 言づての種類 と、行番号と書いた場所 |
989
- * | 別の知らせが受け持つ | 部品の端 (`part-node-ignored` が 1 件ずつ伝える) |
990
- */
991
- const 板が伝えない矢印の欄: ReadonlySet<string> = new Set([
992
- "no",
993
- "from",
994
- "to",
995
- "label",
996
- "msgKind",
997
- "pos",
998
- "fromPartNode",
999
- "toPartNode",
1000
- ]);
1001
-
1002
- /**
1003
- * 順序図の言づてに書いた飾りが使われないことを伝える (#1466)。
1004
- *
1005
- * 板は言づてを **語と向きと種類** で描き、それ以外の飾りを載せる場所が無い = 矢印だった頃は
1006
- * 色味 (`tone`) / 添え字 (`sub`) / 寄せ (`side`) が矢印に付いていたが、板では行になった。
1007
- * 黙って落とすと、書いた側は効いていると思い込む。
1008
- *
1009
- * **伝える欄を並べない** (#2356)。 以前は色味 / 添え字 / 寄せ / 名前のずらし / 多重度 を手で並べて
1010
- * いたため、後から足した項目がどこにも入らなかった (実測 = 矢印に書ける 21 項目のうち 13 件が、
1011
- * 図も変わらず知らせも出ないまま落ちていた)。 板が描く欄を **除いた残り全部** を伝える。
1012
- *
1013
- * `actors` に無い名前を指す言づては見ない (#2111)。 その言づては組み立ての前に落ちて板に載らず、
1014
- * 同じ行を居ない名前の知らせ (`reportMissingFlowActors`) が指している。
1015
- */
1016
- function reportMessageOptionNotHonored(doc: DslDocument, onNotice?: (n: CompileNotice) => void): void {
1017
- if (!onNotice) return;
1018
- if (doc.type !== "sequence") return;
1019
- const 名前の表 = actorRefTable(doc);
1020
- for (const s of doc.flow) {
1021
- if (!名前の表.has(s.from) || !名前の表.has(s.to)) continue;
1022
- const 効かない = 効かない矢印の欄を並べる(s, 板が伝えない矢印の欄);
1023
- if (効かない.length === 0) continue;
1024
- onNotice({
1025
- kind: "message-option-not-honored",
1026
- actor: s.from,
1027
- line: s.pos?.line ?? 0,
1028
- message: `"${truncateForMessage(s.label)}" に書いた ${効かない.join(" / ")} は効きません (type: ${doc.type} の板は語と向きと種類だけを描きます)`,
1029
- hint: "板には矢印が無いため飾りを載せる先がありません。 言づての種類 (kind: call / return / fire) で描き分けてください",
1030
- });
1031
- }
1032
- }
1033
-
1034
- /**
1035
- * 名前を指す欄かどうかを、 綴りで決める (#2405)。
1036
- *
1037
- * 部品の表 (`readout-table.generated.ts`) の 107 種が持つ欄のうち、 別の所に書いた値を名前で
1038
- * 指すのは `source` / `〜Source` / `sourceA` の 3 つの形。 表そのものは「文字列」 としか
1039
- * 言わないため、 どの文字列が名前かは綴りからしか決められない。
1040
- *
1041
- * **規則から外れた綴りで名前の欄が足されると、 その欄だけが黙る**。 規則に当たらない欄の
1042
- * 一覧を検査で固定し、 増えた日に人が 1 件ずつ見分ける形にする
1043
- * (`test/readout-source-2405.test.ts`)。
1044
- */
1045
- function 名前を指す欄(欄名: string): boolean {
1046
- return 欄名 === "source" || 欄名.endsWith("Source") || /^source[A-Z]$/.test(欄名);
1047
- }
1048
-
1049
- /**
1050
- * 値を見せる部品 (`readouts:`) が読む元の名前を突き合わせて知らせる (#2405)。
1051
- *
1052
- * ## 直す前に起きていたこと
1053
- *
1054
- * 読み取り値は `source: done` の形で、 別の所に書いた値を名前で指す。 **その名前が在るかを
1055
- * 誰も見ていなかった** = 状態を指す図も、 どこにも無い名前を指す図も、 大文字小文字を
1056
- * 間違えた図も、 すべて知らせ 0 件で同じ形のまま描く側へ渡っていた (実測 6 通り)。
1057
- * 描く側は知らない名前を読むと何も出さないので、 書いた人には「部品を置いたのに数字が
1058
- * 出ない」 だけが残る。
1059
- *
1060
- * ## 読む元は 3 つ
1061
- *
1062
- * `states` / `inputs` / `formulas`。 式の名前の突き合わせ (`formula-unresolved`) は
1063
- * つまみと先に書いた式の 2 つしか見ないが、 読み取り値は状態も読めるので 3 つを合わせる。
1064
- *
1065
- * ## 3 つの節を全て載せ終えてから呼ぶ
1066
- *
1067
- * 記法は `readouts:` を 3 つの節より前に書ける。 途中で呼ぶと、 後から載る節の名前を
1068
- * 「無い」 と知らせる。
1069
- *
1070
- * ## 見る欄を `source` だけにしない
1071
- *
1072
- * `stacked-bar` は `source` を持たず `sourceA` / `sourceB` だけを持つ。 `source` だけを
1073
- * 見る形にすると、 その種類が丸ごと黙ったまま残る (表の 107 種が持つ名前の欄は 24 種類)。
1074
- *
1075
- * ## 1 つの部品で 2 つ外れたら 2 件とも出す
1076
- *
1077
- * 片方で打ち切ると、 直した次の回にもう片方が初めて出る。
1078
- */
1079
- function reportReadoutSourceMissing(
1080
- doc: DslDocument,
1081
- merged: CdlDiagram,
1082
- onNotice?: (n: CompileNotice) => void,
1083
- ): void {
1084
- if (!onNotice) return;
1085
- const 読み取り値 = doc.readouts ?? [];
1086
- if (読み取り値.length === 0) return;
1087
- const 書かれた名前 = new Set<string>([
1088
- ...(merged.states ?? []).map((x) => x.id),
1089
- ...(merged.inputs ?? []).map((x) => x.id),
1090
- ...(merged.formulas ?? []).map((x) => x.id),
1091
- ]);
1092
- const hint = 書ける値の案内(書かれた名前);
1093
- for (const r of 読み取り値) {
1094
- for (const [欄, 値] of Object.entries(r)) {
1095
- if (!名前を指す欄(欄) || typeof 値 !== "string") continue;
1096
- if (書かれた名前.has(値)) continue;
1097
- onNotice({
1098
- kind: "readout-source-missing",
1099
- actor: 値,
1100
- line: r.pos?.line ?? 0,
1101
- message: `部品 "${truncateForMessage(r.id)}" の ${欄} が、 状態でもつまみでも式でもない名前 "${truncateForMessage(値)}" を読んでいます`,
1102
- hint,
1103
- });
1104
- }
1105
- }
1106
- }
1107
-
1108
- /**
1109
- * 状態 (`states:`) とつまみ (`inputs:`) に同じ名前を書いて、 値がずれたことを知らせる (#2413)。
1110
- *
1111
- * ## 両方に書くこと自体は意図された書き方
1112
- *
1113
- * 状態が静止した図の値を決め、 つまみが読み手に値を変えさせる。 カタログの見本 105 箇所が
1114
- * この形で書かれているので、 重なりそのものは誤りにしない。
1115
- *
1116
- * ## ずれると状態の初期値は図のどこにも出ない
1117
- *
1118
- * 描く側は `mergeStateValues(base, overrides)` でつまみの既定値を状態の上に塗り、
1119
- * 静止した最初の一枚にもつまみの既定値が入る。 見本 77 組は 1 組もずれておらず、
1120
- * 書き手が全部手で揃えている決まりなのに、 守れているかを見る仕組みが無かった。
1121
- *
1122
- * ## 効く既定値を自前で導かない
1123
- *
1124
- * 種類によって値の作り方が違い、 欄の名前からは導けない (`timeline` は `defaultSpeedIdx` を
1125
- * 持つが効く既定値は 0)。 描く側が書き出している `inputDefaultValue` をそのまま呼ぶ。
1126
- *
1127
- * ## 突き合わせは文字列で行う
1128
- *
1129
- * 描く側が `merged[key] = String(v)` で文字列にしてから塗るため、 `42` と `"42"` は
1130
- * 同じ値として扱う。
1131
- *
1132
- * ## 式と巻き上げは対象にしない
1133
- *
1134
- * 静止した最初の一枚 (`serverSnapshot`) に入るのはつまみの既定値だけで、 式や巻き上げが
1135
- * 値を作るまでは状態の初期値が見える。 置き値として役に立つので、 ずれていても誤りにならない。
1136
- */
1137
- function reportStateInputMismatch(
1138
- doc: DslDocument,
1139
- merged: CdlDiagram,
1140
- onNotice?: (n: CompileNotice) => void,
1141
- ): void {
1142
- if (!onNotice) return;
1143
- const つまみ = merged.inputs ?? [];
1144
- if (つまみ.length === 0) return;
1145
- const 状態 = new Map((doc.animate?.states ?? []).map((s) => [s.name, s]));
1146
- if (状態.size === 0) return;
1147
- for (const input of つまみ) {
1148
- const s = 状態.get(input.id);
1149
- if (s === undefined) continue;
1150
- const 既定 = inputDefaultValue(input);
1151
- if (既定 === undefined) continue;
1152
- if (String(s.initial) === String(既定)) continue;
1153
- onNotice({
1154
- kind: "state-shadowed-by-input",
1155
- actor: input.id,
1156
- line: s.pos.line,
1157
- message: `"${truncateForMessage(input.id)}" を states と inputs の両方に書いていますが、 値が違います (states の ${truncateForMessage(String(s.initial))} は出ません。 つまみの ${truncateForMessage(String(既定))} が使われます)`,
1158
- hint: `states の初期値をつまみの既定値 ${truncateForMessage(String(既定))} に揃えるか、 どちらか一方だけに書く`,
1159
- });
1160
- }
1161
- }
1162
-
1163
- /**
1164
- * 「states / inputs / formulas に書いた名前で指す」 の直し方を 1 本作る (#2405)。
1165
- *
1166
- * 並べる件数と 1 件ずつの長さを切るのは `書ける面の案内` と同じ理由 = 名前を上限まで書いた
1167
- * 図で知らせが数百 MB になる。
1168
- */
1169
- function 書ける値の案内(書かれた名前: ReadonlySet<string>): string {
1170
- const 見せる数 = 8;
1171
- const 全部 = [...書かれた名前];
1172
- if (全部.length === 0) return "states / inputs / formulas のどれかに値を書く";
1173
- const 一覧 = 全部.slice(0, 見せる数).map(truncateForMessage).join(" / ");
1174
- const 残り = 全部.length - 見せる数;
1175
- return `states / inputs / formulas に書いた名前で指す (${一覧}${残り > 0 ? ` ほか ${残り} 件` : ""})`;
1176
- }
1177
-
1178
- /**
1179
- * 順序図の帯 (`bands:`) に書いた面の名前と言づての番号を突き合わせて知らせる (#2404)。
1180
- *
1181
- * ## なぜ誤りにせず知らせにするか
1182
- *
1183
- * 帯が 1 本ずれても図は出る。 「別の所に書いた名前を指す」 欄は 4 つとも知らせで扱っており
1184
- * (箱の `lane` / 組の `lanes` / 出来事の `box` / 矢印の端)、 帯だけを誤りにする理由が無い。
1185
- *
1186
- * ## 帯を描かない図種は、 それを先に知らせて打ち切る
1187
- *
1188
- * 帯は板の縦線に重ねる四角なので、 板を作らない図種には載せる先が無い
1189
- * (実測 = 24 図種のうち 22 図種が `bands:` を読んだうえで黙って捨てていた)。
1190
- * その図で名前や番号を直しても帯は出ないため、 直しても直らない案内を返さない。
1191
- *
1192
- * **図種を手で並べない**。 組み上がった図が板を持つかで決める = 図種を足した日にずれない
1193
- * (`rules/quality.md` の導出可能記述)。
1194
- *
1195
- * ## なぜ読む側ではなくここで確かめるか
1196
- *
1197
- * 記法は `bands:` を `actors:` より前に書ける = 読む途中では面の一覧が揃っていない。
1198
- * 板に載る言づての数も `flow:` を読み終えるまで決まらない。 組み立ての入口は両方を持っている。
1199
- *
1200
- * ## 突き合わせ先は「書いた `actors`」
1201
- *
1202
- * 板の図種は箱が 1 つしか無く、 面は箱の中の行になる = 組み上がった図と突き合わせる相手が居ない
1203
- * (段の注目先 #2398 と同じ)。 名前の受け方は矢印の端と同じ表 (`actorRefTable`) を使うので、
1204
- * id の形 (`api-gateway`) で書いた指定も通る。
1205
- *
1206
- * ## 番号が指すのは言づての行で、 0 から数える
1207
- *
1208
- * 記法の `- DB: 1..2` は段 (`animation` の `step`) ではなく **板に載る言づての行**を指す
1209
- * (実測 = 描く側は `rowY(from)` と `rowY(to)` で四角の上下を決め、 `rowY(i)` は i 通目の
1210
- * 言づての高さを返す)。 見本もその読み方で書かれている (面 4 つ ・ 言づて 7 通の図で
1211
- * `ブラウザ: 0..5` / `DB: 6..6`)。
1212
- *
1213
- * 段と言づては数も起点も違う = 段で数えると、 正しく書いた見本を範囲外として知らせてしまう。
1214
- *
1215
- * ## 板に載らない言づてを数に入れない
1216
- *
1217
- * `actors` に無い名前を指す言づては組み立ての前に落ちて板に載らない (実測 = 3 通のうち 1 通が
1218
- * 落ちて 2 通になる)。 書いた行数で数えると、 落ちた分だけ上限が広がって範囲外を見逃す。
1219
- *
1220
- * ## 言づてが 1 通も無い図でも黙らない
1221
- *
1222
- * 比べる相手が無いからと飛ばすと、 帯を書いたのに何も描かれない図が黙る経路になる。
1223
- * 範囲を示せないので、 理由を「言づてが 1 通も無い」 に変えて同じ知らせを出す。
1224
- *
1225
- * ## 負の番号はここで見ない
1226
- *
1227
- * 記法は数字だけを受ける形 (`\d+`) で読み、 JSON は「0 以上の整数」 を要求する = 2 つの入口が
1228
- * どちらも先に断っている。 ここに枝を置いても 1 度も通らない。 入口の側が緩んだら気付けるよう、
1229
- * 断ることそのものを検査で固定する。
1230
- *
1231
- * ## 名前と番号で打ち切らない
1232
- *
1233
- * 両方外れた帯には 2 件とも出す。 片方で止めると、 名前を直した次の回に番号の誤りが初めて出る。
1234
- */
1235
- function reportBandProblems(
1236
- doc: DslDocument,
1237
- diagram: CdlDiagram,
1238
- onNotice?: (n: CompileNotice) => void,
1239
- ): void {
1240
- if (!onNotice) return;
1241
- const 帯 = doc.bands ?? [];
1242
- if (帯.length === 0) return;
1243
- const 板がある = diagram.nodes.some((n) => n.sequenceData !== undefined);
1244
- if (!板がある) {
1245
- for (const b of 帯) {
1246
- onNotice({
1247
- kind: "band-not-honored",
1248
- actor: b.actor,
1249
- line: b.pos?.line ?? 0,
1250
- message: `type: ${doc.type} は帯を描きません (書いた帯は図に出ません)`,
1251
- hint: "帯は板の縦線に重ねる四角なので、 type: sequence で使う",
1252
- });
1253
- }
1254
- return;
1255
- }
1256
- const 名前の表 = actorRefTable(doc);
1257
- const hint = 書ける面の案内(doc);
1258
- const 言づての数 = doc.flow.filter((s) => 名前の表.has(s.from) && 名前の表.has(s.to)).length;
1259
- for (const b of 帯) {
1260
- const line = b.pos?.line ?? 0;
1261
- if (!名前の表.has(b.actor)) {
1262
- onNotice({
1263
- kind: "band-actor-missing",
1264
- actor: b.actor,
1265
- line,
1266
- message: `帯が "${truncateForMessage(b.actor)}" を指していますが、 actors に書かれていません (先頭の面に帯が出ます)`,
1267
- hint,
1268
- });
1269
- }
1270
- const 外れ =
1271
- 言づての数 === 0
1272
- ? "板に載る言づてが 1 通もありません"
1273
- : b.to > 言づての数 - 1
1274
- ? `板に載る言づては ${言づての数} 通で、 最後は ${言づての数 - 1} 番です`
1275
- : b.from > b.to
1276
- ? "始まりが終わりより後ろです"
1277
- : undefined;
1278
- if (外れ === undefined) continue;
1279
- onNotice({
1280
- kind: "band-row-out-of-range",
1281
- actor: b.actor,
1282
- line,
1283
- message: `帯 "${truncateForMessage(b.actor)}: ${b.from}..${b.to}" の区間が言づての並びに収まりません (${外れ})`,
1284
- hint:
1285
- 言づての数 === 0
1286
- ? "帯は言づての行に重ねる四角なので、 flow に言づてを書く"
1287
- : `0..${言づての数 - 1} の範囲で、 始まりを終わり以下にする (番号は flow に書いた言づての行、 段ではありません)`,
1288
- });
1289
- }
1290
- }
1291
-
1292
- /**
1293
- * 矢印に書いた多重度 (`cardinality`) から端の形を描けない時に伝える (#2107)。
1294
- *
1295
- * 多重度で端の形が決まるのは `type: record` の 6 語だけ (描画側の `ER_CARDINALITY_HEAD`)。
1296
- * `er` でそれ以外の語を書くと名前に `(語)` と添えるだけで、`er` 以外では何も描かない。
1297
- * 黙って通すと、書いた側は端の形が付くと思い込む (`N:M` の代わりに `N:N` と書く形で起きる)。
1298
- *
1299
- * `er` で端を両方とも書いていれば、描かない端が無いので伝えない = 6 語に無い個数 (`2..5`) を
1300
- * 名前に添えて、端は自分で描く書き方を止めない。
1301
- *
1302
- * 同じ行に 2 件並べない。 避ける行は 3 つあり、どれも知らせる図種や知らせの種類を並べずに決める。
1303
- *
1304
- * | 避ける行 | 決め方 | 既に知らせているもの |
1305
- * |---|---|---|
1306
- * | 順序図の板の言づて | 図種 | `reportMessageOptionNotHonored` が他の飾りと合わせて伝える |
1307
- * | `actors` に無い名前を指す矢印 | 名前の解決に使う表 (`actorRefTable`) | 居ない名前の知らせ (`reportMissingFlowActors`) |
1308
- * | 図種の組み立てが知らせた行 | 実際に出た知らせ | 捨てた矢印 / 自分を親にする矢印 / ガントチャートの矢印の飾り (多重度を含む) など |
1309
- *
1310
- * 値の図 (`pie` 等) は捨てた本数を最初の矢印の行でまとめて伝えるため、2 本目以降の矢印に書いた
1311
- * 多重度はここで伝わる。
1312
- *
1313
- * 行は、図種の知らせと矢印を結ぶ唯一の手掛かり。 行を持たない矢印は全て 0 行になり、図種の組み立てが
1314
- * 0 行を指して 1 件でも知らせると、その文書の多重度の知らせは出ない。 矢印を見分けられない以上、
1315
- * 同じ矢印に 2 件出す側ではなく重ねない側に倒す。
1316
- *
1317
- * 行を持たないのは、書いた場所の表を渡さずに JSON を読んだ文書 (#2117)。 表を渡す入口 (編集画面の
1318
- * YAML 欄) は矢印ごとに別の行を持つため、ここでも矢印ごとに判定できる。
1319
- */
1320
- function reportCardinalityNotHonored(
1321
- doc: DslDocument,
1322
- 図種が知らせた行: ReadonlySet<number>,
1323
- onNotice?: (n: CompileNotice) => void,
1324
- ): void {
1325
- if (!onNotice) return;
1326
- if (doc.type === "sequence") return;
1327
- const 名前の表 = actorRefTable(doc);
1328
- for (const s of doc.flow) {
1329
- const 書いた = 書いた多重度を読む(s.cardinality);
1330
- if (書いた === undefined) continue;
1331
- if (!名前の表.has(s.from) || !名前の表.has(s.to)) continue;
1332
- const line = s.pos?.line ?? 0;
1333
- if (図種が知らせた行.has(line)) continue;
1334
- const 矢印 = `${truncateForMessage(s.from)} -> ${truncateForMessage(s.to)}`;
1335
- const 字 = truncateForMessage(書いた.字);
1336
- if (doc.type !== "record") {
1337
- onNotice({
1338
- kind: "cardinality-not-honored",
1339
- actor: s.from,
1340
- line,
1341
- message: `${矢印} に書いた多重度 "${字}" は効きません (type: ${doc.type} は多重度を描きません)`,
1342
- hint: "多重度から両端の形を描くのは type: record です",
1343
- });
1344
- continue;
1345
- }
1346
- if (書いた.語 !== null) continue;
1347
- const 描かない端 = [
1348
- s.tailHead === undefined ? "出どころ側" : "",
1349
- s.head === undefined ? "行き先側" : "",
1350
- ].filter((x) => x !== "");
1351
- if (描かない端.length === 0) continue;
1352
- onNotice({
1353
- kind: "cardinality-not-honored",
1354
- actor: s.from,
1355
- line,
1356
- message: `${矢印} の多重度 "${字}" は端の形が決まる語ではないため、${描かない端.length === 2 ? "両端" : 描かない端[0]}の形を描きません (名前に (${字}) と添えるだけです)`,
1357
- hint: `端の形が決まる語は ${端の形が決まる語.join(" / ")} です。 それ以外の個数を示す時は、端の形を tailHead / head に書いてください`,
1358
- });
1359
- }
1360
- }
1361
-
1362
- /**
1363
- * 名前の無い矢印に書いた位置のずらしを知らせる (#1971)。
1364
- *
1365
- * 矢印の位置のずらしは名前をずらす欄に足す。 名前が無い矢印では動くものが無く、書いても図は
1366
- * 変わらない。 順序図の板は `reportMessageOptionNotHonored` が知らせるので、ここでは見ない。
1367
- */
1368
- function reportFlowOffsetNotHonored(doc: DslDocument, onNotice?: (n: CompileNotice) => void): void {
1369
- if (!onNotice) return;
1370
- if (doc.type === "sequence") return;
1371
- for (const s of doc.flow) {
1372
- if (s.layoutPos === undefined) continue;
1373
- if (s.label !== undefined && s.label.trim() !== "") continue;
1374
- onNotice({
1375
- kind: "position-offset-ignored",
1376
- actor: s.from,
1377
- line: s.pos?.line ?? 0,
1378
- message: `${s.from} -> ${s.to} の位置のずらしは、名前の無い矢印では動かすものがありません`,
1379
- hint: "矢印の位置のずらしは名前をずらします。 名前を書くか、ずらしを外す",
1380
- });
1381
- }
1382
- }
1383
-
1384
- /**
1385
- * 順序図で面に書いた飾りが使われないことを伝える (#1466)。
1386
- *
1387
- * 順序図は 1 つの板が図を丸ごと描く形になり、面は上端の見出しに **名前と呼び名だけ** で並ぶ。
1388
- * 面ごとの箱が無いので、種類 / 大きさ / 位置 / 行 / 色 / 小見出し / 値 / 図形を載せる先も無い。
1389
- * 黙って落とすと、書いた側は効いていると思い込む。
1390
- *
1391
- * **伝える欄を並べない** (#2358)。 以前は 8 種を手で並べていたため、後から足した項目が
1392
- * どこにも入らなかった (実測 = 箱に書ける 31 項目のうち 11 件が、図も変わらず知らせも出ない)。
1393
- * 板が描く欄を **除いた残り全部** を伝える。
1394
- *
1395
- * 見本 (`parts`) を重ねた面は対象外 = 見本は別経路で図に取り込まれ、板の見出しには並ばない。
1396
- */
1397
- function reportActorKindNotHonored(doc: DslDocument, onNotice?: (n: CompileNotice) => void): void {
1398
- if (!onNotice) return;
1399
- if (doc.type !== "sequence") return;
1400
- /*
1401
- * **種類で並べ替える図では鳴らさない** (#2655)。
1402
- *
1403
- * 種類は板の絵には出ないが、並び順を書いた図では **面の並びを決めている**ので、
1404
- * 書いた意味は figure に出ている。 落ちたと伝えると、正しく効いている指定に毎回鳴る。
1405
- *
1406
- * 畳む前はこの判定を図種で行っていた。 型を畳んだので、同じことを並び順が効いているかで
1407
- * 見る。
1408
- */
1409
- if (並び順が効くか(doc)) return;
1410
- for (const a of doc.actors) {
1411
- if (a.partId !== undefined) continue;
1412
- /*
1413
- * 記法を通すと既定の `actor` が必ず入るため、値ではなく書いたかどうかの印で見る。
1414
- * 記法を通さず直接組み立てた場合はこの印が無いので、種類を置いたこと自体を「書いた」 とみなす
1415
- */
1416
- const 判定で外す = doc.type !== "sequence" || a.kindWritten === false ? ["kind"] : [];
1417
- const 効かない = 効かない箱の欄を並べる(a, 板が伝えない箱の欄, 判定で外す);
1418
- if (効かない.length === 0) continue;
1419
- onNotice({
1420
- kind: "actor-kind-not-honored",
1421
- actor: a.name,
1422
- line: a.pos?.line ?? 0,
1423
- message: `"${truncateForMessage(a.name)}" に書いた ${効かない.join(" / ")} は効きません (type: ${doc.type} の板は名前と呼び名だけを描きます)`,
1424
- hint: "板には面ごとの箱が無いため飾りを載せる先がありません。 呼び名 (subtitle) に書くか、箱を持つ図種を使ってください",
1425
- });
1426
- }
1427
- }
1428
-
1429
- /**
1430
- * 矢印を骨格として描く図種 (#2364)。
1431
- *
1432
- * 木の図と思考の地図は矢印を親子のつながりとして描き、線の形も向きも階層から決める。
1433
- * 飾りを載せる先が無い。
1434
- *
1435
- * ガントチャート (`gantt`) も同じ性質を持つが、知らせを図種ごとの組み立て (`compile/gantt.ts`) の
1436
- * 中で出しているので、ここには入れない。
1437
- */
1438
- const 矢印を骨格にする図種: ReadonlySet<string> = new Set(["tree", "mind"]);
1439
-
1440
- /**
1441
- * 矢印を骨格として描く図で、矢印に書いた飾りが使われないことを伝える (#2364)。
1442
- *
1443
- * 実測 = 矢印に書ける 21 項目のうち 18 件が、図も変わらず知らせも出なかった (2 図種とも)。
1444
- * 文字を足すと 19 件になる。
1445
- *
1446
- * 呼び名は板と同じ表を使う (`効かない矢印の欄の呼び名`) = 同じ欄を 2 つの文言で呼ぶと、
1447
- * 図種を変えた読み手が同じ飾りだと気付けない。
1448
- */
1449
- function reportSkeletonEdgeOptionNotHonored(
1450
- doc: DslDocument,
1451
- 図種が知らせた行: ReadonlySet<number>,
1452
- onNotice?: (n: CompileNotice) => void,
1453
- ): void {
1454
- if (!onNotice) return;
1455
- if (!矢印を骨格にする図種.has(doc.type)) return;
1456
- const 名前の表 = actorRefTable(doc);
1457
- for (const s of doc.flow) {
1458
- if (!名前の表.has(s.from) || !名前の表.has(s.to)) continue;
1459
- /*
1460
- * 図種の組み立てが知らせた行には重ねない (#2107 と同じ決まり)。
1461
- *
1462
- * 捨てた矢印 (自分を親にする形など) は組み立てが既に伝えており、そこへ飾りの知らせを
1463
- * 足すと同じ行に 2 件並ぶ。 実測 = `A -> A` に多重度を書いた木の図で 2 件出た。
1464
- */
1465
- if (図種が知らせた行.has(s.pos?.line ?? 0)) continue;
1466
- const 効かない = 効かない矢印の欄を並べる(s, 向きだけを使う図が伝えない矢印の欄);
1467
- if (効かない.length === 0) continue;
1468
- onNotice({
1469
- kind: "edge-option-not-honored",
1470
- actor: s.from,
1471
- line: s.pos?.line ?? 0,
1472
- message: `type: ${doc.type} で "${truncateForMessage(s.from)} -> ${truncateForMessage(s.to)}" に書いた ${効かない.join(" / ")} は描けません (矢印は親子のつながりだけを描きます)`,
1473
- hint: "つながりの線は階層から決まるため飾りを載せる先がありません。 箱の名前で伝えるか、矢印を自分で引く図種を使ってください",
1474
- });
1475
- }
1476
- }
1477
-
1478
- /**
1479
- * 木の図で、箱に書いた指定が使われないことを伝える (#2360)。
1480
- *
1481
- * 木の図は箱を階層の節として描き、位置も大きさも親子関係から決める。 箱ごとの飾りを
1482
- * 載せる先が無いのは設計どおりだが、**書いても知らせが出なかった** (実測 = 箱に書ける
1483
- * 31 項目のうち 21 件が、図も変わらず知らせも出ない)。
1484
- *
1485
- * 呼び名は板と同じ表を使う (`箱の欄の呼び名`) = 同じ欄を 2 つの文言で呼ぶと、
1486
- * 図種を変えた読み手が同じ指定だと気付けない。
1487
- */
1488
- function reportTreeActorOptionNotHonored(
1489
- doc: DslDocument,
1490
- onNotice?: (n: CompileNotice) => void,
1491
- ): void {
1492
- if (!onNotice) return;
1493
- if (doc.type !== "tree") return;
1494
- for (const a of doc.actors) {
1495
- if (a.partId !== undefined) continue;
1496
- // 種類は書かなくても既定の `actor` が入るため、値ではなく書いたかどうかの印で見る (#1058)
1497
- const 効かない = 効かない箱の欄を並べる(
1498
- a,
1499
- 木の図が伝えない箱の欄,
1500
- a.kindWritten === false ? ["kind"] : [],
1501
- );
1502
- if (効かない.length === 0) continue;
1503
- onNotice({
1504
- kind: "actor-option-not-honored",
1505
- actor: a.name,
1506
- line: a.pos?.line ?? 0,
1507
- message: `"${truncateForMessage(a.name)}" に書いた ${効かない.join(" / ")} は効きません (type: tree は名前と値だけを描き、位置も大きさも親子関係から決めます)`,
1508
- hint: "木の図の箱は階層の節なので飾りを載せる先がありません。 呼び名 (subtitle) に書くか、箱を並べる図種を使ってください",
1509
- });
1510
- }
1511
- }
1512
-
1513
- /**
1514
- * 知らせの文に出す「その図種が読むもの」 (#2370)。
1515
- *
1516
- * 表に無い図種は「名前と値」 とする = 足し忘れても文が消えず、読み手に伝わる。
1517
- */
1518
- const 図種が読むものの呼び名: ReadonlyMap<string, string> = new Map([
1519
- ["chart", "名前と値と色"],
1520
- ["gantt", "名前と時期と色と担当"],
1521
- ["journey", "名前と気持ちと接点"],
1522
- ]);
1523
-
1524
- /**
1525
- * 箱を名前と値の組として読む図で、箱に書いた指定が使われないことを伝える (#2368 / #2370)。
1526
- *
1527
- * 対象の図種と、図種ごとに読む欄は `値として読む図種` が持つ (実装が SSOT)。
1528
- *
1529
- * 実測 = 箱に書ける 30 項目のうち、値の図で 17 件 / じょうごで 19 件 /
1530
- * ガントチャートと体験の地図と四象限で 22-23 件が、図も変わらず知らせも出なかった。
1531
- *
1532
- * 位置 (`posX` + `posY`) は片方だけだと別の知らせ (#2362) が出るため、1 項目ずつ足す走査では
1533
- * 「知らせる」 に数えられ、組で書いた時の穴が隠れていた。
1534
- *
1535
- * 呼び名は板と木の図と同じ表を使う (`効かない箱の欄の呼び名`) = 同じ欄を 2 つの文言で呼ぶと、
1536
- * 図種を変えた読み手が同じ指定だと気付けない。
1537
- */
1538
- function reportValueChartActorOptionNotHonored(
1539
- doc: DslDocument,
1540
- onNotice?: (n: CompileNotice) => void,
1541
- ): void {
1542
- if (!onNotice) return;
1543
- if (!値として読む図種.has(doc.type)) return;
1544
- const 伝えない欄 = 値として読む図が伝えない箱の欄(doc.type);
1545
- // 何を読むかを知らせの文に出す。 図種ごとに違うので表から引く (実装が SSOT)
1546
- const 読む欄 = 図種が読むものの呼び名.get(doc.type) ?? "名前と値";
1547
- for (const a of doc.actors) {
1548
- if (a.partId !== undefined) continue;
1549
- const 判定で外す: string[] = [];
1550
- // 種類は書かなくても既定の `actor` が入るため、値ではなく書いたかどうかの印で見る (#1058)
1551
- if (a.kindWritten === false) 判定で外す.push("kind");
1552
- /*
1553
- * 呼び名は **値を書いていない箱でだけ** 値として読まれる (#2390)。
1554
- * 読む側と同じ式 (`a.value ?? a.subtitle`) に合わせる = 条件を写すと、
1555
- * 読む側を直した日に伝える側が古くなる。
1556
- */
1557
- if (a.value === undefined) 判定で外す.push("subtitle");
1558
- const 効かない = 効かない箱の欄を並べる(a, 伝えない欄, 判定で外す);
1559
- if (効かない.length === 0) continue;
1560
- onNotice({
1561
- kind: "actor-option-not-honored",
1562
- actor: a.name,
1563
- line: a.pos?.line ?? 0,
1564
- message: `"${truncateForMessage(a.name)}" に書いた ${効かない.join(" / ")} は効きません (type: ${doc.type} は${読む欄}だけを読み、1 枚の図にまとめて描きます)`,
1565
- hint: 値として読む図の案内(効かない),
1566
- });
1567
- }
1568
- }
1569
-
1570
- /**
1571
- * 値として読む図の案内を、伝えた欄から組み立てる (#2390)。
1572
- *
1573
- * **固定の 1 文を添えない** (#2382 の決まり)。 飾りの欄 (位置 / 大きさ / 図形 ほか) は
1574
- * 「箱を並べる図種を使ってください」 が行き先になるが、呼び名の行き先は同じ図種の中にある =
1575
- * 値を消せば値として読まれ、題 (`title`) を書けば箱に出る。
1576
- * 1 文で済ませると、呼び名だけを書いた読み手が図種を変えろと読まされる。
1577
- */
1578
- function 値として読む図の案内(効かない: readonly string[]): string | undefined {
1579
- const 案内: string[] = [];
1580
- if (効かない.includes("呼び名")) {
1581
- 案内.push(
1582
- "呼び名を値として読ませるなら同じ箱の値 (value) を消します。 箱に題を出すなら title を書きます",
1583
- );
1584
- }
1585
- if (効かない.some((欄) => 欄 !== "呼び名")) {
1586
- 案内.push(
1587
- "値の図は箱ごとの絵を持たないため飾りを載せる先がありません。 箱を並べる図種を使ってください",
1588
- );
1589
- }
1590
- return 案内.length === 0 ? undefined : 案内.join("、");
1591
- }
1592
-
1593
- /**
1594
- * 箱を並べて線で繋ぐ図で、箱に書いた指定が使われないことを伝える (#2376)。
1595
- *
1596
- * 対象の図種と、図種ごとに読まない欄は `骨組みの図種` が持つ (実装が SSOT)。
1597
- *
1598
- * 実測 = フロー 447 枚 ・ 泳路の図 35 枚 ・ 配置の図 8 枚の見本すべてで、始まりの印
1599
- * (`initial`) ・ 終わりの印 (`final`) ・ 印 (`marks`) ・ 前の値 (`previous`) の 4 件が、
1600
- * 図も変わらず知らせも出なかった。構成の図はこれに 1 件足した形になる。
1601
- *
1602
- * 始まりと終わりの印を読むのは状態遷移図だけ (`compile/generic.ts` の札)。
1603
- * 同じ記法をフローに書いても札は出ないが、書き手には状態遷移図と同じに見える。
1604
- *
1605
- * 印 (`marks`) は **行と組にして読み替えられる図種でだけ** 外す (#2377)。
1606
- * 読み替えの表は `行頭の印にする` が持つので、図種の一覧をここに写さず関数に聞く。
1607
- * 行を書いていない印は、どの図種でも読み替える先が無いので伝える。
1608
- *
1609
- * 案内は **伝えた欄から組み立てる** (#2382)。 3 つの行き先を並べた 1 文を固定で添えていたが、
1610
- * 状態遷移図を族に入れた時に 2 つが誤りになった = 読み手は既に状態遷移図に居るのに
1611
- * 「始まりと終わりの印は type: record が描きます」 と読まされ、印については
1612
- * 「type: record が描きます」 と読まされる (状態遷移図も描く。 足りないのは行のほう)。
1613
- *
1614
- * 段 (`stack`) は **図 1 枚ごとに読むかが決まる** (#2386)。 書いた番号の段に置くのは
1615
- * 全ての箱が縦列を書いた図だけで、1 つでも書いていない箱があると図全体が別の並べ方に落ち、
1616
- * 段はどの箱でも読まれない。 図種では表せない条件なので、置く側と同じ関数
1617
- * (`書いた縦列に置く`) に聞く。
1618
- *
1619
- */
1620
- function reportSkeletonActorOptionNotHonored(
1621
- doc: DslDocument,
1622
- onNotice?: (n: CompileNotice) => void,
1623
- ): void {
1624
- if (!onNotice) return;
1625
- if (!骨組みの図種.has(doc.type)) return;
1626
- const 伝えない欄 = 図ごとの条件も見た伝えない箱の欄(doc);
1627
- for (const a of doc.actors) {
1628
- if (a.partId !== undefined) continue;
1629
- const 判定で外す: string[] = [];
1630
- // 種類は書かなくても既定の `actor` が入るため、値ではなく書いたかどうかの印で見る (#1058)
1631
- if (a.kindWritten === false || 泳法図で印をどう描く(doc, a) !== undefined) 判定で外す.push("kind");
1632
- // 行と組にして読み替えられる印は効く = 効いている指定には鳴らさない
1633
- if (
1634
- a.rows !== undefined &&
1635
- a.marks !== undefined &&
1636
- 行頭の印を読むか(doc.type)
1637
- ) {
1638
- 判定で外す.push("marks");
1639
- }
1640
- const 効かない = 効かない箱の欄を並べる(a, 伝えない欄, 判定で外す);
1641
- if (効かない.length === 0) continue;
1642
- onNotice({
1643
- kind: "actor-option-not-honored",
1644
- actor: a.name,
1645
- line: a.pos?.line ?? 0,
1646
- message: `"${truncateForMessage(a.name)}" に書いた ${効かない.join(" / ")} は効きません (type: ${doc.type} は箱を並べて線で繋ぐ図です)`,
1647
- hint:
1648
- doc.type === "swimlane" && doc.shape === "stages"
1649
- ? "札は段階ごとの列へ actors に書いた順に積み、種類は card で固定されます。 decision は札の右に「分かれ道」を添え、mark-start と mark-end は描きません"
1650
- : doc.type === "swimlane" && doc.shape === "timeline"
1651
- ? "番号付きの札は軸の左右へ交互に並び、種類は card で固定されます。 decision の脇の札は同じ段へ置き、decision と mark-end は軸の上に描き、mark-start は描きません"
1652
- : doc.type === "swimlane" && doc.shape === "metro"
1653
- ? "駅は actors に書いた順に並び、種類は station で固定されます。 decision / mark-start / mark-end は分かれ道と始まりと終わりの印として描きます"
1654
- : 骨組みの図の案内(doc.type, 効かない),
1655
- });
1656
- }
1657
- }
1658
-
1659
- /**
1660
- * その図で、効かないと伝えない箱の欄 (#2386 / #2388)。
1661
- *
1662
- * 図種ごとの違いは族の表 (`骨組みの図種`) が持つ。 **図 1 枚ごとに** 読むかが決まる欄は
1663
- * 表では表せないので、ここで読まない図から抜いて伝える側へ回す。
1664
- *
1665
- * 条件は使う側と同じ関数に聞く = 2 か所に写すと、使う側を直した日に伝える側が古くなる。
1666
- * 段 (`stack`) は縦列を選べる図種すべてに同じ条件が掛かるので `書いた縦列に置く` を直に呼ぶ。
1667
- *
1668
- * 図種ごとに違う条件を持つ欄は 1 つも無い (#2782)。 畳む前は表の図の種類 (`kind`) が
1669
- * 2 経路に分かれていたが、経路を 1 本にしたので図種の表だけで決まる。
1670
- *
1671
- * @param doc 組み立てる本文
1672
- * @returns 族の除外から、この図では読まない欄を抜いた集合
1673
- */
1674
- function 図ごとの条件も見た伝えない箱の欄(doc: DslDocument): ReadonlySet<string> {
1675
- const 族の除外 = 骨組みの図が伝えない箱の欄(doc.type);
1676
- const 段階ごとの箱か = doc.type === "swimlane" && doc.shape === "stages";
1677
- const 路線図か = doc.type === "swimlane" && doc.shape === "metro";
1678
- const 時間軸か = doc.type === "swimlane" && doc.shape === "timeline";
1679
- const 読まない欄: string[] = [];
1680
- const この形が読む欄: string[] = [];
1681
- if (doc.type === "flowchart") この形が読む欄.push("titleFontSize", "markGap");
1682
- if (路線図か) この形が読む欄.push("stationNamePosition");
1683
- if (時間軸か) この形が読む欄.push("subtitlePlacement");
1684
- // 段を読まない図では伝える側へ回す。 既に抜けている図種 (c4) は下の `delete` が空振りする
1685
- if (路線図か || 時間軸か || (!段階ごとの箱か && !書いた縦列に置く(doc.type, doc))) 読まない欄.push("stack");
1686
- const 残り = new Set(族の除外);
1687
- // `stage` はこの形でだけ箱の置き先を決める。 他の骨組みの図では知らせる側へ残す (#2797)。
1688
- let 変えた = 段階ごとの箱か && !残り.has("stage");
1689
- if (段階ごとの箱か) 残り.add("stage");
1690
- // 3 形とも種類を固定する (段の箱は札、路線図は駅、時間軸は札)。書いた種類は伝える側へ回す (#2831)
1691
- if ((段階ごとの箱か || 路線図か || 時間軸か) && 残り.delete("kind")) 変えた = true;
1692
- for (const 欄 of 読まない欄) if (残り.delete(欄)) 変えた = true;
1693
- for (const 欄 of この形が読む欄) {
1694
- if (!残り.has(欄)) 変えた = true;
1695
- 残り.add(欄);
1696
- }
1697
- return 変えた ? 残り : 族の除外;
1698
- }
1699
-
1700
- /**
1701
- * 行頭の印を読む図種 (#2382)。 `行頭の印を読むか` に聞いて導く。
1702
- *
1703
- * 図種の名前を案内の文に写すと、読み替える図種を足した日に案内だけが古くなる。
1704
- * 走査の範囲は骨組みの族に閉じる = この案内が出るのも族の中だけ。
1705
- */
1706
- const 行頭の印を読む図種: readonly string[] = [...骨組みの図種.keys()].filter((t) =>
1707
- 行頭の印を読むか(t),
1708
- );
1709
-
1710
- /**
1711
- * 骨組みの図の知らせに添える案内を、伝えた欄から組み立てる (#2382)。
1712
- *
1713
- * **伝えていない欄の行き先を書かない**。 3 つの行き先を並べた 1 文を固定で添えていた間、
1714
- * 状態遷移図では 2 つが誤りになっていた。
1715
- *
1716
- * **行き先を持たない欄では案内を付けない**。 当たり障りのない 1 文で埋めると、
1717
- * 読み手は次に何をすればよいか分からないまま「案内は読んだ」 状態になる。
1718
- *
1719
- * @param 図種 いま組んでいる図の種類
1720
- * @param 効かない 知らせに並べた呼び名 (`効かない箱の欄を並べる` の返り値)
1721
- * @returns 案内の文。 行き先を持つ欄が 1 つも無ければ `undefined`
1722
- */
1723
- function 骨組みの図の案内(図種: string, 効かない: readonly string[]): string | undefined {
1724
- const 案内: string[] = [];
1725
- /*
1726
- * 段は **縦列を選べる図種でだけ** 書き方の行き先を持つ (#2386)。 その図種で伝えたということは
1727
- * 縦列を書いていない箱が在ったということなので、足せば効く。
1728
- * 縦列を選べない図種 (構成の図) では、どう書いても効かないので行き先が無い。
1729
- */
1730
- if (効かない.includes("段") && 縦列を選べる図種.has(図種 as never)) {
1731
- 案内.push("段は全ての箱に縦列 (lane) を書いた図で効きます");
1732
- }
1733
- if (効かない.includes("始まりの印") || 効かない.includes("終わりの印")) {
1734
- 案内.push("始まりと終わりの印は type: record が描きます");
1735
- }
1736
- if (効かない.includes("前の値")) 案内.push("前の値は値を並べる図が描きます");
1737
- /*
1738
- * この族で「色」 に立つのは色番号だけ (#2384)。 色味 (`tone`) は共通の除外に入っているので
1739
- * ここへ来ない = 呼び名が同じでも、伝えているのは 16 進数で書いた形に限る。
1740
- */
1741
- if (効かない.includes("色")) {
1742
- 案内.push("色番号が効くのは部品を置いた箱だけです。 色の名前 (color: 成功) は箱の色になります");
1743
- }
1744
- if (効かない.includes("印")) {
1745
- 案内.push(
1746
- 行頭の印を読む図種.includes(図種)
1747
- ? "行頭の印は行 (rows) と組にして書くと記号になります"
1748
- : `行頭の印は ${行頭の印を読む図種.map((t) => `type: ${t}`).join(" と ")} が、行 (rows) と組にした時に描きます`,
1749
- );
1750
- }
1751
- return 案内.length === 0 ? undefined : 案内.join("、");
1752
- }
1753
-
1754
- /**
1755
- * 箱の座標を片方だけ書いた時に伝える (#2362)。
1756
- *
1757
- * 座標を図に写す処理 (`applyCanvasPivotPositions`) は **横と縦の両方が書かれている** 時だけ
1758
- * 動く。 片方だけの時は何もしないまま通るため、図が 1 bit も変わらず知らせも出なかった
1759
- * (実測 = 座標が効く 5 図種すべてで消えた)。
1760
- *
1761
- * **効かせる側には倒さない**。 片方だけ動かす書き方は既にずらし (`offsetX` / `offsetY`) が
1762
- * 受け持っており、座標に同じ役目を持たせると 2 つの書き方が同じことをする。
1763
- *
1764
- * 大きさ (`posW` / `posH`) は片方だけでも効くので見ない (実測)。
1765
- * 部品として置いた箱は別経路 (`mergePartsFromActors`) が位置を決めるので外す。
1766
- */
1767
- function reportHalfWrittenPosition(
1768
- doc: DslDocument,
1769
- 箱に知らせた行: ReadonlySet<number>,
1770
- onNotice?: (n: CompileNotice) => void,
1771
- ): void {
1772
- if (!onNotice) return;
1773
- for (const a of doc.actors) {
1774
- if (a.partId !== undefined) continue;
1775
- /*
1776
- * 箱の知らせが出た行には重ねない (#2368)。
1777
- *
1778
- * 位置そのものが効かない図種では「片方だけでは決まらない」 という案内が誤りになる
1779
- * (両方書いても効かない)。 その図種では箱の知らせが位置を含めて伝えている。
1780
- */
1781
- if (箱に知らせた行.has(a.pos?.line ?? 0)) continue;
1782
- const 書いた = a.posX !== undefined ? "posX" : a.posY !== undefined ? "posY" : undefined;
1783
- if (書いた === undefined) continue;
1784
- if (a.posX !== undefined && a.posY !== undefined) continue;
1785
- const 足りない = 書いた === "posX" ? "posY" : "posX";
1786
- onNotice({
1787
- kind: "position-axis-missing",
1788
- actor: a.name,
1789
- line: a.pos?.line ?? 0,
1790
- message: `"${truncateForMessage(a.name)}" に書いた ${書いた} だけでは位置が決まりません (${足りない} も書くと効きます)`,
1791
- hint: `横と縦を両方書くか、片方だけ動かすなら ずらし (offsetX / offsetY) を使ってください`,
1792
- });
1793
- }
1794
- }
1795
-
1796
- /**
1797
- * 箱に書いた縦列が効かないことを伝える (#1246)。
1798
- *
1799
- * 縦列は **図種が決める**。 `flow` / `topology` は 1 本にまとめ、 `swimlane` / `er` / `state`
1800
- * は箱ごとに 1 本作り、 `sequence` はそれがそのまま生命線になる。 図全体を 1 箱にする図種
1801
- * (`pie` / `bar` 等) では箱が 1 つしかない。
1802
- *
1803
- * **箱の `lane` を読むのは、 `縦列を選べる図種` で全ての箱が書いた時だけ** (#1263)。
1804
- * 一部の箱だけが書いた形は `reportLaneMixed` が伝えるので、 ここではそれ以外の図種に書いた
1805
- * 縦列を伝える。
1806
- *
1807
- * 黙って捨てると、 書いた縦列は消え、 `lanes:` で宣言した縦列だけが中身のないまま残る。
1808
- * 実測 (#1246) = `type: flow` で `lane: ui` / `lane: api` を書くと箱は両方 `flow` に入り、
1809
- * 宣言した `ui` / `api` は空のまま増えた。 知らせは 1 件も出なかった。
1810
- *
1811
- * ## なぜ組み立ての側で伝えるのか
1812
- *
1813
- * 記法の解析は図種を見ずに 1 行ずつ読む。 そこで弾くと **見本 (parts) の張替え先** まで
1814
- * 巻き添えになる = 見本では `lane` が実際に読まれる (`mergePartsFromActors` が読む)。
1815
- * 図種を知っているのは組み立ての側なので、 効くかどうかの判断もここに置く。
1816
- *
1817
- * ## 見本と `type: mind` では伝えない
1818
- *
1819
- * 見本は上のとおり実際に効く。 `type: mind` は描けない欄をまとめて 1 件で伝えており
1820
- * (`compileMind` の「名前と副題 / 値、 枝の色しか描けません」)、 そこに `枠の指定` が既に
1821
- * 入っている。 二重に伝えると同じ 1 行について知らせが 2 件並ぶ。
1822
- */
1823
- function reportLaneNotHonored(doc: DslDocument, onNotice?: (n: CompileNotice) => void): void {
1824
- if (!onNotice) return;
1825
- if (doc.type === "mind") return;
1826
- // 時間軸の lane は縦列ではなく札の担当名なので、箱ごとに省いてよい。
1827
- if (doc.type === "swimlane" && doc.shape === "timeline") return;
1828
- // 縦列を選べる図種では、全ての箱が書いていれば効く (#1263)。 効く形では知らせない
1829
- const 効く図種 = 縦列を選べる図種.has(doc.type);
1830
- if (効く図種 && 書いた縦列に置く(doc.type, doc)) return;
1831
- if (効く図種) {
1832
- reportLaneMixed(doc, onNotice);
1833
- return;
1834
- }
1835
- for (const a of doc.actors) {
1836
- if (a.partId !== undefined) continue;
1837
- if (a.lane === undefined) continue;
1838
- onNotice({
1839
- kind: "lane-not-honored",
1840
- actor: a.name,
1841
- line: a.pos?.line ?? 0,
1842
- message: `"${truncateForMessage(a.name)}" に書いた lane は効きません (縦列は type: ${doc.type} が決めます)`,
1843
- hint: "縦列は図種が決めるため箱からは選べません。 lane を消してください (見本では張替え先として使えます)",
1844
- });
1845
- }
1846
- }
1847
-
1848
- /**
1849
- * 「actors に書いた名前で指す」 の直し方を 1 本作る (#1209、 #2404 で帯と共有)。
1850
- *
1851
- * **1 度だけ作る**。 知らせごとに全ての面の名前を並べ直すと、 名前も矢印も上限 (各 1,000) まで
1852
- * 書いた図で数百 MB になる。 並べる数にも上限を置く。
1853
- *
1854
- * **1 件ずつの長さも切る**。 件数だけを絞っても、 名前 1 つが 2 万字なら知らせも 2 万字になる
1855
- * (実測)。 名前も指定も外から来る文字列なので、 表示に使う所はすべて `truncateForMessage` を通す。
1856
- *
1857
- * 矢印の端と帯で同じ文を使う = 同じ「actors に無い名前を指した」 という誤りに、 欄ごとに違う
1858
- * 直し方が返らないようにする。
1859
- */
1860
- function 書ける面の案内(doc: DslDocument): string {
1861
- const 見せる数 = 8;
1862
- if (doc.actors.length === 0) return "actors に登場人物を書く";
1863
- const 名前一覧 = doc.actors
1864
- .slice(0, 見せる数)
1865
- .map((a) => truncateForMessage(a.name))
1866
- .join(" / ");
1867
- const 残り = doc.actors.length - 見せる数;
1868
- return `actors に書いた名前で指す (${名前一覧}${残り > 0 ? ` ほか ${残り} 件` : ""})`;
1869
- }
1870
-
1871
- /**
1872
- * 矢印が `actors` に無い名前を指したことを知らせる (#1209)。
1873
- *
1874
- * 知らせずに通すと、 **どちらに転んでも書いた人の意図が消える**。 動きを書いていない図では
1875
- * 種類ごとの組み立てが actors を順に繋ぐため、 書いた矢印そのものが捨てられて label も
1876
- * 消える (実測 = "変換" が "→" になった)。 動きを書いた図では名前がそのまま下流へ渡り、
1877
- * 存在しない node を指す図ができて描画の直前で落ちる
1878
- * (実測 = `unknown-ref: edge "e0-v-c" の from "v" が node に存在しません`)。
1879
- *
1880
- * 落ちる場所も消える場所も本文から遠いので、 書いた行で知らせる。
1881
- */
1882
- function reportMissingFlowActors(
1883
- doc: DslDocument,
1884
- onNotice?: (notice: CompileNotice) => void,
1885
- ): void {
1886
- if (!onNotice) return;
1887
- const 表 = actorRefTable(doc);
1888
- // hint は **1 度だけ作る**。 知らせごとに全 actor 名を並べ直すと、 名前も矢印も上限
1889
- // (各 1,000) まで書いた図で数百 MB になる (Round 1 の指摘)。 並べる数にも上限を置く
1890
- //
1891
- // **1 件ずつの長さも切る**。 件数だけを絞っても、 名前 1 つが 2 万字なら知らせも 2 万字に
1892
- // なる (Round 4 の実測)。 名前も矢印の指定も外から来る文字列なので、 表示に使う所は
1893
- // すべて `truncateForMessage` を通す (光らせる相手の知らせと同じ扱い)。
1894
- const hint = 書ける面の案内(doc);
1895
- const 知らせた = new Set<string>();
1896
- for (const s of doc.flow) {
1897
- for (const ref of [s.from, s.to]) {
1898
- if (表.has(ref) || 知らせた.has(ref)) continue;
1899
- 知らせた.add(ref);
1900
- onNotice({
1901
- kind: "flow-actor-missing",
1902
- actor: ref,
1903
- line: s.pos.line,
1904
- message: `矢印が "${truncateForMessage(ref)}" を指していますが、 actors に書かれていません`,
1905
- hint,
1906
- });
1907
- }
1908
- }
1909
- }
1910
-
1911
- /**
1912
- * 光らせる相手 (`focus:`) が届かない分を知らせる。 理由で 2 通りに分ける。
1913
- *
1914
- * 名前が当たらなかった指定は静かに消える。 光らせたい相手を書いたのに光らない状態が、
1915
- * 手掛かりなしで起きる。
1916
- *
1917
- * | 理由 | 見るもの | 知らせ |
1918
- * |---|---|---|
1919
- * | 記法にその名前が無い | 記述だけ | `focus-target-missing` (綴り違い) |
1920
- * | 記法にはあるが、書いたとおりに光らない | 組み上がった図 | `focus-target-not-honored` (#2398) |
1921
- *
1922
- * **2 つを 1 つにまとめない**。 直し方が正反対で、前者は書き直せば直り、後者は同じ案内に
1923
- * 従っても永久に直らない。 出来事 (`events:`) の側は #2336 で同じ分け方を持っており、
1924
- * 注目先の側だけ持っていなかった。
1925
- *
1926
- * 綴り違いの側は **記述だけを見る**。 id の形は図種で違うが「その名前の箱が居るか」
1927
- * 「その矢印が流れにあるか」 は書かれた内容だけで決まる。 図種ごとの解決経路に検査を分けると、
1928
- * 経路が増えるたびに検査が取り残される。
1929
- *
1930
- * ## 何が光るかは図種で 3 通りに分かれる (実測)
1931
- *
1932
- * | 光り方 | 図種 | 知らせる条件 |
1933
- * |---|---|---|
1934
- * | 書いた箱 / 矢印が光る | `c4` `flow` `record` `swimlane` `topology` | 図がその相手を持たない時 |
1935
- * | 書いた名前に合う言づてまで板が進む | `sequence` | 知らせない (下記) |
1936
- * | 図全体の 1 箱が光る | 残る 15 図種 | 書かなかった箱がある時と、矢印を書いた時 |
1937
- *
1938
- * **板の 2 図種では知らせない**。 板は箱も矢印も持たないが、書いた名前は捨てられておらず
1939
- * 板が何通目まで描くかを決めている (`段の番号`、 実測 = 3 通の図で `focus: ["あ -> い"]` が
1940
- * 0 通目、 `["う -> あ"]` が 2 通目)。 図に相手が居ないことだけを見ると、効いている指定に
1941
- * 「選べません」 と知らせることになる。
1942
- *
1943
- * **図全体が光る図種でも、全ての箱を書いた段では知らせない**。 光る結果 (全ての箱) と
1944
- * 書いたこと (全ての箱) が一致するため、伝えることが無い。 知らせるのは **書かなかった箱も
1945
- * 光る時** = 4 本の棒のうち 1 本だけを書いても 4 本とも光る形で、ここだけが書いたとおりに
1946
- * ならない。
1947
- *
1948
- * @param diagram 組み上がった図。 渡さない時は綴り違いだけを見る
1949
- */
1950
- function reportMissingFocusTargets(
1951
- doc: DslDocument,
1952
- onNotice?: (notice: CompileNotice) => void,
1953
- diagram?: CdlDiagram,
1954
- ): void {
1955
- if (!onNotice || !doc.animate) return;
1956
- const names = new Set(doc.actors.map((a) => a.name));
1957
- // 解決側は名前が見つからない時に slug へ落とす。 受理集合もそれに合わせる。
1958
- // 合わせないと、 実際は光る指定 (`API Gateway` を `api-gateway` と書いた形) を
1959
- // 「見つかりません」 と誤報する (実測)
1960
- //
1961
- // 2 つ以上の名前が同じ slug になる時は受理しない。 解決側も曖昧として光らせないため、
1962
- // 受理すると「知らせは出ないのに何も光らない」 状態になる (実測)
1963
- const accepted = new Set(names);
1964
- const slugCount = new Map<string, number>();
1965
- for (const n of names) {
1966
- const sl = slugify(n);
1967
- slugCount.set(sl, (slugCount.get(sl) ?? 0) + 1);
1968
- }
1969
- for (const [sl, count] of slugCount) if (count === 1) accepted.add(sl);
1970
- //
1971
- // 縦列の id は受理しない。 3 つの解決経路はいずれも縦列を光らせないため、 受理すると
1972
- // 「知らせは出ないのに何も光らない」 状態を作る (実測 = `focus: [main]` で activate が空)
1973
- // 矢印は流れに書かれた組合せだけを認める。 名前に空白を含められる (`決済 基盤`) ため、
1974
- // 連結した 1 本の鍵にはしない (`"a b" -> "c"` と `"a" -> "b c"` が同じ鍵になる)
1975
- const steps = new Map<string, Set<string>>();
1976
- for (const st of doc.flow) {
1977
- const tos = steps.get(st.from) ?? new Set<string>();
1978
- tos.add(st.to);
1979
- steps.set(st.from, tos);
1980
- }
1981
-
1982
- // 矢印の両端は **流れと同じ表で名前へ揃えてから** 照合する (#1209 Round 2)。
1983
- //
1984
- // 流れは入口で名前へ揃えている (`canonicalizeFlowActors`) 一方、 光らせる指定は生のまま
1985
- // 来る。 揃えずに比べると、 slug で書いた矢印 (`api-gateway -> db`) が実際は光るのに
1986
- // 「見つかりません」 と誤報する (実測)
1987
- const 名前へ = actorRefTable(doc);
1988
- const 揃える = (ref: string): string => 名前へ.get(ref) ?? ref;
1989
-
1990
- // 部品の中の要素と線は `{部品の名前}__{要素}` で名指しする (#2151)。 ここでは部品の一覧を持たず、
1991
- // 要素があるかは部品を取り込んだ後にしか決まらない = 名前の前半が部品の名前なら知らせず、
1992
- // 取り込む側 (`部品の名前で光らせる`) に任せる。 両方で知らせると同じ名前に 2 件出る
1993
- const 部品の名前 = doc.actors.filter((a) => a.partId !== undefined).map((a) => a.name);
1994
- const 部品の中を指す = (name: string): boolean => 部品の名前.some((p) => name.startsWith(`${p}__`));
1995
-
1996
- // 部品そのものの名前は、取り込む側が図の相手を決める (`部品の名前で光らせる`)。
1997
- // ここで図を見ると、取り込む前の図にしか当たらず「選べません」 の誤報になる
1998
- const 部品そのもの = new Set(部品の名前);
1999
-
2000
- // 板の 2 図種は書いた名前を板の番号に使う (doc comment の表)。 図と突き合わせない
2001
- const 板になる = doc.type === "sequence";
2002
- const 見比べる図 = 板になる ? undefined : diagram;
2003
-
2004
- // 綴り違いの補足は **1 度だけ作る**。 知らせごとに全ての名前を並べ直すと、名前も注目先も
2005
- // 上限 (各 1,000) まで書いた図で知らせが数百 MB になる (矢印の端の知らせが #1209 で踏んだ形)
2006
- const 名前の案内 = `actors: に書かれている名前 = ${並べて切る([...names])}`;
2007
-
2008
- for (const phase of doc.animate.phases) {
2009
- /** 記法には書いてあるのに、書いたとおりに光らない指定 (#2398) */
2010
- const 選べない: string[] = [];
2011
- /** その段が名指しした箱。 図全体が光る図種で「書かなかった箱があるか」 を見る */
2012
- const 書いた箱 = new Set<string>();
2013
- /** 選べない指定に矢印が混じったか。 図全体が光る図種は矢印を 1 本も描かない */
2014
- let 矢印も書いた = false;
2015
- /** 書いた箱のうち 1 つでも図に居たか。 居るなら図全体が光る形ではない */
2016
- let 箱が図に居た = false;
2017
- for (const raw of phase.highlight ?? []) {
2018
- const entry = parseFocusEntry(raw, names);
2019
- const found =
2020
- entry.kind === "edge"
2021
- ? (steps.get(揃える(entry.from))?.has(揃える(entry.to)) ?? false)
2022
- : accepted.has(entry.name) || 部品の中を指す(entry.name);
2023
- if (!found) {
2024
- onNotice({
2025
- kind: "focus-target-missing",
2026
- actor: raw,
2027
- line: phase.pos.line,
2028
- message:
2029
- entry.kind === "edge"
2030
- ? `光らせる矢印が流れにありません: "${truncateForMessage(raw)}"`
2031
- : `光らせる相手が見つかりません: "${truncateForMessage(raw)}"`,
2032
- hint: entry.kind === "edge" ? "flow: に書いた矢印と同じ向きで書く" : 名前の案内,
2033
- // 縦列の id は受理しないので、 その旨は hint に出さない (光らせられないため)
2034
- });
2035
- continue;
2036
- }
2037
- if (見比べる図 === undefined) continue;
2038
- if (entry.kind === "node") 書いた箱.add(揃える(entry.name));
2039
- if (entry.kind === "node" && (部品そのもの.has(entry.name) || 部品の中を指す(entry.name)))
2040
- continue;
2041
- const 相手 =
2042
- entry.kind === "edge"
2043
- ? 図の矢印を探す(見比べる図, doc, entry.from, entry.to)
2044
- : 図の箱を探す(見比べる図, entry.name);
2045
- if (相手 === undefined) {
2046
- 選べない.push(raw);
2047
- if (entry.kind === "edge") 矢印も書いた = true;
2048
- } else if (entry.kind === "node") 箱が図に居た = true;
2049
- }
2050
- if (選べない.length === 0) continue;
2051
- /*
2052
- * 全ての箱を書いた段は、光る結果 (図全体) と書いたことが一致する (doc comment の表)。
2053
- *
2054
- * **1 つでも図に居た段はここへ入れない**。 箱を 1 つずつ選べる図種で 1 箱だけが図から
2055
- * 落ちた時、全ての名前を書いていると「一致している」 と読めてしまう。 図全体が光る図種は
2056
- * どの名前も図に当たらないので、この条件で落ちることはない
2057
- */
2058
- if (!矢印も書いた && !箱が図に居た && [...names].every((n) => 書いた箱.has(n))) continue;
2059
- /*
2060
- * **1 行に 1 件だけ出す** (#2398)。 同じ段に書いた名前はどれも同じ理由で選べないので、
2061
- * 1 つずつ知らせると同じ文が 1 行に並ぶ。 書いた名前は文に並べる。
2062
- */
2063
- const 書かなかった箱 = [...names].filter((n) => !書いた箱.has(n));
2064
- onNotice({
2065
- kind: "focus-target-not-honored",
2066
- actor: 選べない[0]!,
2067
- line: phase.pos.line,
2068
- message:
2069
- `type: ${doc.type} は箱を 1 つずつ選べず図全体が光るため、` +
2070
- ` ${並べて切る(選べない, (x) => `"${x}"`)} を注目先に書いても` +
2071
- (書かなかった箱.length > 0
2072
- ? ` 書かなかった箱 (${並べて切る(書かなかった箱)}) も光ります`
2073
- : " 図全体が光ります"),
2074
- hint: "名前の書き方の問題ではありません。 全ての箱を書くか、箱を 1 つずつ選べる図種 (flow / record など) に変えてください",
2075
- });
2076
- }
2077
- }
2078
-
2079
- /**
2080
- * 知らせの本文に名前を並べる。 件数も 1 件あたりの長さも切る (#2398)。
2081
- *
2082
- * **件数だけを絞っても足りない**。 名前は外から来る文字列なので、1 つが 2 万字なら
2083
- * 知らせも 2 万字になる (矢印の端の知らせが #1209 で踏んだ形)。 箱の上限は 1,000 件で、
2084
- * 全て並べると 1 件の知らせが数百 KB になる。
2085
- */
2086
- function 並べて切る(一覧: readonly string[], 包む = (s: string): string => s): string {
2087
- const 見せる数 = 8;
2088
- const 並び = 一覧.slice(0, 見せる数).map((x) => 包む(truncateForMessage(x)));
2089
- const 残り = 一覧.length - 見せる数;
2090
- return 残り > 0 ? `${並び.join(" / ")} ほか ${残り} 件` : 並び.join(" / ");
2091
- }
2092
-
2093
- /**
2094
- * 図の中から、書いた名前の箱を探す (#2336 / #2398)。
2095
- *
2096
- * 名前から作った識別子と、名前そのものと、描かれる題の 3 通りで探す。 記法は名前で書き、
2097
- * 図の識別子はそこから作られるが、見本を重ねた図など識別子が名前と揃わない形もある。
2098
- *
2099
- * **出来事の相手と注目先が同じ探し方を使う**。 片方だけ厳しくすると、正しく書いた名前が
2100
- * 一方では光り、もう一方では「見つからない」 と知らされる。
2101
- */
2102
- function 図の箱を探す(diagram: CdlDiagram, 名: string): CdlNode | undefined {
2103
- const slug = slugify(名);
2104
- return diagram.nodes.find((n) => n.id === slug || n.id === 名 || n.title === 名);
2105
- }
2106
-
2107
- /**
2108
- * 図の中から、書いた両端の矢印を探す (#2336 / #2398)。
2109
- *
2110
- * 板になる図種 (`sequence`) は識別子に行の番号が入るので、書いた組が
2111
- * 流れの何番目かを先に引いてから照合する。
2112
- */
2113
- function 図の矢印を探す(
2114
- diagram: CdlDiagram,
2115
- doc: DslDocument,
2116
- fromName: string,
2117
- toName: string,
2118
- used?: ReadonlySet<string>,
2119
- ): CdlEdge | undefined {
2120
- const from = slugify(fromName);
2121
- const to = slugify(toName);
2122
- // 同じ両端を持つ行は出現順に別の線へ写すため、書き写し済みの線を候補から外す。
2123
- const 未使用 = (edge: CdlEdge): boolean => !used?.has(edge.id);
2124
- if (doc.type === "sequence") {
2125
- const stepIdx = doc.flow.findIndex(
2126
- (step) => slugify(step.from) === from && slugify(step.to) === to,
2127
- );
2128
- if (stepIdx < 0) return undefined;
2129
- return diagram.edges.find(
2130
- (x) =>
2131
- 未使用(x) &&
2132
- (x.from === `s${stepIdx}-${from}` || x.from === from) &&
2133
- (x.to === `s${stepIdx}-${to}` || x.from === x.to),
2134
- );
2135
- }
2136
- // 時間軸の進行線は番号の箱を結ぶが、id は札の両端を残す。
2137
- // focus と出来事は書き手が札の名前で指定するため、その id の末尾で照合する。
2138
- if (doc.type === "swimlane" && doc.shape === "timeline") {
2139
- return diagram.edges.find((x) => 未使用(x) && x.id.endsWith(`-${from}-${to}`));
2140
- }
2141
- return diagram.edges.find((x) => 未使用(x) && x.from === from && x.to === to);
2142
- }
2143
-
2144
- /**
2145
- * v0.5+ flow inline option (guard / cardinality / labelOffsetX / labelOffsetY / overlay) を
2146
- * 既存 preset 経由で生成された CdlEdge に対し、 doc.flow の (from, to) 一致順マッチングで反映する。
2147
- *
2148
- * 設計:
2149
- * - preset 経路ごとに edge id 命名規則が異なる (sequence: e{idx}-..、 ER: rel-{idx}-..、 FSM: t{idx}-..、
2150
- * topology: c{idx}-..、 flow preset: e-{prev}-{node}、 swimlane: e{idx}-..) ため、 id 直接マッチは脆い。
2151
- * - 代わりに doc.flow の 1 step に対し、 同じ (slugified-from, slugified-to) を持つ未マッチ edge を
2152
- * 順に 1 つ消費する double-pointer 走査で対応付ける。 同 from-to の重複は出現順で順番に対応。
2153
- * - ER の多重度は名前にも端にも写さない。 名前 / 名前の下の行 / 両端は組み立ての時に
2154
- * `compile/er-relation.ts` が決めている (#2105)。
2155
- */
2156
- function applyEdgeInlineOptions(
2157
- diagram: CdlDiagram,
2158
- doc: DslDocument,
2159
- /** 対応が取れた edge を記録する表。 callback は呼ばない (1 edge = 1 回にするため)。 */
2160
- sourceLines?: Map<string, number>,
2161
- /** 対応が取れた edge の行そのもの。 部品の取り込みが、行に書いた要素の名指しを読む (#1979) */
2162
- edgeSteps?: Map<string, DslStep>,
2163
- /** 鎖に並べない部品を決める一覧 (#1987)。 `compileFlow` に渡したものと同じ */
2164
- partsCatalog?: Record<string, CdlDiagram>,
2165
- ): void {
2166
- // **静止した `type: flow` は書いた端で対応が取れない** (#1267)。 鎖の規則で先に埋める
2167
- if (鎖でつなぐ形か(doc)) {
2168
- const 並び = 鎖に並べる登場人物(doc, partsCatalog);
2169
- diagram.edges.forEach((e, idx) => {
2170
- const s = 鎖のどの行から来たか(doc, 並び, idx);
2171
- if (s === undefined) return;
2172
- sourceLines?.set(e.id, s.pos.line);
2173
- edgeSteps?.set(e.id, s);
2174
- 矢印へ書き写す(e, s, doc);
2175
- });
2176
- return;
2177
- }
2178
- const used = new Set<string>();
2179
- doc.flow.forEach((s) => {
2180
- // focus・出来事と同じ照合を使い、図種が線の端を置き換えても書いた行との対応を保つ。
2181
- const target = 図の矢印を探す(diagram, doc, s.from, s.to, used);
2182
- if (!target) return;
2183
- used.add(target.id);
2184
- sourceLines?.set(target.id, s.pos.line);
2185
- edgeSteps?.set(target.id, s);
2186
- 矢印へ書き写す(target, s, doc);
2187
- });
2188
- }
2189
-
2190
- /**
2191
- * 出来事が指す相手を、図の識別子へ直す (#1393)。 見つからなければ `undefined`。
2192
- *
2193
- * 箱と縦列は **名前から作った識別子** と **描かれる題** の両方で探す。 記法は名前で書き、
2194
- * 図の識別子はそこから作られるが、見本を重ねた図など識別子が名前と揃わない形もある。
2195
- * 矢印は両端の名前から識別子を作って突き合わせる。
2196
- */
2197
- function 出来事の相手を解く(
2198
- diagram: CdlDiagram,
2199
- doc: DslDocument,
2200
- e: DslEventBinding,
2201
- ): NonNullable<CdlDiagram["eventBindings"]>[number]["target"] | undefined {
2202
- if (e.target.kind === "diagram") return { kind: "diagram" };
2203
- if (e.target.kind === "edge") {
2204
- const 矢印 = 図の矢印を探す(diagram, doc, e.target.from, e.target.to);
2205
- return 矢印 ? { kind: "edge", id: 矢印.id } : undefined;
2206
- }
2207
- const 名 = e.target.name;
2208
- if (e.target.kind === "node") {
2209
- const 箱 = 図の箱を探す(diagram, 名);
2210
- return 箱 ? { kind: "node", id: 箱.id } : undefined;
2211
- }
2212
- const slug = slugify(名);
2213
- const 列 = (diagram.lanes ?? []).find((l) => l.id === slug || l.id === 名 || l.label === 名);
2214
- return 列 ? { kind: "lane", id: 列.id } : undefined;
2215
- }
2216
-
2217
- /**
2218
- * 出来事が指す名前を、書いた人が本文に書いているか (#2336)。
2219
- *
2220
- * 相手が解けなかった原因を 2 つに分けるために使う。 **本文に在るなら書き直しても直らない** =
2221
- * その図種が登場人物を箱にしない (値を並べる図種は図全体で 1 つの箱、 順序図は
2222
- * 1 枚の板)。 本文に無いなら綴り違いで、書き直せば直る。
2223
- *
2224
- * 分けないと、直る件と直らない件に同じ「名前で書く」 という案内が付く。 案内に従っても
2225
- * 直らない側では、書いた人は綴りを疑って何度も書き直すことになる (実測 = 24 図種のうち
2226
- * 箱を指せるのは 7 図種)。
2227
- *
2228
- * 照合は図の識別子と同じ作り方に揃える = 箱は名前と題の両方で探し (`出来事の相手を解く` と
2229
- * 同じ 2 経路)、矢印は両端の名前から作った識別子で突き合わせる。 ここだけ厳しくすると、
2230
- * 正しく書いた名前を綴り違いとして知らせてしまう。
2231
- */
2232
- function 出来事の相手が本文に在る(doc: DslDocument, e: DslEventBinding): boolean {
2233
- if (e.target.kind === "diagram") return true;
2234
- if (e.target.kind === "edge") {
2235
- const from = slugify(e.target.from);
2236
- const to = slugify(e.target.to);
2237
- return doc.flow.some((s) => slugify(s.from) === from && slugify(s.to) === to);
2238
- }
2239
- const 名 = e.target.name;
2240
- const slug = slugify(名);
2241
- if (e.target.kind === "node") {
2242
- return doc.actors.some(
2243
- (a) => a.name === 名 || slugify(a.name) === slug || (a.title ?? a.name) === 名,
2244
- );
2245
- }
2246
- return Object.entries(doc.lanes ?? {}).some(
2247
- ([id, 列]) => id === 名 || slugify(id) === slug || 列.label === 名,
2248
- );
2249
- }
2250
-
2251
- /** 出来事の相手の呼び名。 知らせの本文で「何を指したか」 を言うために使う */
2252
- const 出来事の相手の呼び名: Record<DslEventBinding["target"]["kind"], string> = {
2253
- node: "箱",
2254
- lane: "縦列",
2255
- edge: "矢印",
2256
- diagram: "図そのもの",
2257
- };
2258
-
2259
- /** 本文に書いた矢印の指定を、対応が取れた矢印へ書き写す。 対応の取り方は呼出側が決める。 */
2260
- function 矢印へ書き写す(target: CdlEdge, s: DslStep, doc: DslDocument): void {
2261
- // **書いた補足が勝つ** (#1275)。 ここで写さないと 2 つ落ちる。 静止した `type: flow` は
2262
- // 鎖を作る時に説明文しか渡さないため補足が消え、`record` は見本が多重度から作った補足が
2263
- // 残って書いた値が無視される (どちらも実測)
2264
- //
2265
- // `relation` を書いた record の `sub` は行き先側の多重度。それ以外の `sub` は
2266
- // 従来どおり名前の下の補足にする (#2783)。
2267
- if (s.sub !== undefined) {
2268
- if (doc.type === "record" && s.relation !== undefined) target.headLabel = s.sub;
2269
- else target.sub = s.sub;
2270
- }
2271
- if (s.guard !== undefined) {
2272
- target.guard = s.guard;
2273
- // FSM preset では sub が guard 同期、 author 明示 guard を sub に反映 (sub 既存なら上書きしない)
2274
- // 書いた条件は名前の下の行にも出す (`sub` が既に在れば上書きしない)
2275
- if (doc.type === "record" && target.sub === undefined) target.sub = s.guard;
2276
- }
2277
- // 色味と線の種類も、図種ごとの組み立てではなくここで写す (#2394)。
2278
- //
2279
- // **渡す側を図種ごとに並べる形にしない**。 この 2 欄は図種ごとの組み立てが個別に渡していて、
2280
- // 渡し忘れた図種でそのまま落ちていた (実測 = 線の種類は `record` / 鎖でつないだ `flow`、
2281
- // 色味は鎖でつないだ `flow` で黙って消える)。 図種を足した日に同じ落とし方が再発するため、
2282
- // 矢印を描く図種が必ず通るここへ移す。 図種ごとの組み立てに置いていた同じ受け渡しは外した =
2283
- // 2 か所に置くと、片方だけ直した日に食い違う。 板になる図種 (`sequence`) は
2284
- // 矢印を作らずここを通らないので、そちらは組み立てが持ったまま
2285
- if (s.tone !== undefined) target.tone = s.tone;
2286
- if (s.style !== undefined) target.style = s.style;
2287
- // 出どころの端に添える字 (#2394)。 engine は `tailLabel` として端に置く (cdl#825)。
2288
- //
2289
- if (s.tailSub !== undefined) target.tailLabel = s.tailSub;
2290
- // 多重度が名前と端にどう出るかは組み立ての時に決まっている (`compile/er-relation.ts`、#2105)。
2291
- // ここで名前へ `(1:N)` を足すと、組み立てが名前の下の行に出した語と 2 度並ぶ
2292
- if (s.cardinality !== undefined) target.cardinality = s.cardinality;
2293
- // 矢印がどの辺から出るか (#1385)。 書かなければ描画側が自動で選ぶ
2294
- if (s.side !== undefined) target.side = s.side;
2295
- if (s.fromSide !== undefined) target.fromSide = s.fromSide;
2296
- if (s.toSide !== undefined) target.toSide = s.toSide;
2297
- // 矢印の先の形 (#1462)。 書かない矢印には値を入れない = 既存の図が変わらない。
2298
- //
2299
- // **矢印を作る 9 図種すべてがここを通る**。 図種ごとの組み立てにも同じ形を置いたが、
2300
- // 外しても 9 図種とも渡っていた (変異試験で実測) = 余分だったので消した
2301
- if (s.head !== undefined) target.head = s.head;
2302
- // 端の残り 3 欄 (#1466)。 出どころ側の印と、両端の塗り。 ER は端ごとに違う個数を示す
2303
- if (s.tailHead !== undefined) target.tailHead = s.tailHead;
2304
- if (s.headFill !== undefined) target.headFill = s.headFill;
2305
- if (s.tailHeadFill !== undefined) target.tailHeadFill = s.tailHeadFill;
2306
- // 辺の役目と名前の下地 (cdl#618)。 主となる道を朱で引き、丸い下地を外せる。
2307
- // 書かない辺には値を入れない = 既存の図が変わらない
2308
- if (s.role !== undefined) target.role = s.role;
2309
- if (s.labelPlate !== undefined) target.labelPlate = s.labelPlate;
2310
- if (s.labelOffsetX !== undefined) target.labelOffsetX = s.labelOffsetX;
2311
- if (s.labelOffsetY !== undefined) target.labelOffsetY = s.labelOffsetY;
2312
- if (s.posX1 !== undefined) target.posX1 = s.posX1;
2313
- if (s.posY1 !== undefined) target.posY1 = s.posY1;
2314
- if (s.posX2 !== undefined) target.posX2 = s.posX2;
2315
- if (s.posY2 !== undefined) target.posY2 = s.posY2;
2316
- if (s.waypoints !== undefined) target.waypoints = s.waypoints;
2317
- if (s.labelCollision !== undefined) target.labelCollision = s.labelCollision;
2318
- if (s.labelSize !== undefined) target.labelSize = s.labelSize;
2319
- // 矢印の位置のずらし (#1971)。 矢印は自分の位置を持たず、通り道は両端の箱で決まるので、
2320
- // 動かせるのは名前だけ。 名前のずらしに足す = 両方書いた時は合わせた量だけ動く
2321
- if (s.layoutPos !== undefined) {
2322
- target.labelOffsetX = (s.labelOffsetX ?? 0) + s.layoutPos.x;
2323
- target.labelOffsetY = (s.labelOffsetY ?? 0) + s.layoutPos.y;
2324
- }
2325
- if (s.overlay !== undefined) target.overlay = s.overlay;
2326
- // 値に追随する 3 欄 (#1396)。 太さ / 色 / 破線の位置が値に合わせて動く。
2327
- // 通り道そのものは動かないので、配置計算と重なり解消には影響しない
2328
- if (s.widthBind !== undefined) target.widthBind = s.widthBind;
2329
- if (s.strokeBind !== undefined) target.strokeBind = s.strokeBind;
2330
- if (s.dashOffsetBind !== undefined) target.dashOffsetBind = s.dashOffsetBind;
2331
- }
2332
-
2333
- /**
2334
- * v0.5+ groups section を、 束ねる縦列に重ねる枠の縦列 (`group-{id}`、 contain: true) として図に足す。
2335
- *
2336
- * ここでは枠を作るだけで、 位置と大きさは縦列の位置が全て決まった後に `applyGroupFrames` が決める。
2337
- * 作った枠の id を返す = 同じ id の縦列を書き手が `lanes:` に書いていた時は作らず、 後で置き直さない。
2338
- */
2339
- function applyGroupContainers(diagram: CdlDiagram, doc: DslDocument): Set<string> {
2340
- const 作った = new Set<string>();
2341
- if (!doc.groups || Object.keys(doc.groups).length === 0) return 作った;
2342
- for (const [id, g] of Object.entries(doc.groups)) {
2343
- const containerId = `group-${id}`;
2344
- // 書き手が `lanes:` に同じ id を書いた縦列は、ここより後で図に入る。 枠として作ると書いた縦列と
2345
- // 重なって 1 本になり、 置き直しが書き手の位置と幅を上書きする
2346
- if (diagram.lanes.some((l) => l.id === containerId) || doc.lanes?.[containerId]) continue;
2347
- diagram.lanes.push({
2348
- id: containerId,
2349
- width: 800,
2350
- label: g.label ?? id,
2351
- contain: true,
2352
- // 束ねる lane 群に重ねて描く枠。 横に並べる lane ではないので、 engine の間隔調整
2353
- // (lane を詰めた分を幅で埋め合わせる処理) の対象から外す。
2354
- role: "overlay",
2355
- // 置き直すまでは座標を固定しておく。 固定しない枠は縦列の並びに加わり、 他の縦列の間隔を
2356
- // 広げる (実測 = 3 本の縦列の 2 本目が 666 から 710 へ動いた)。 並びから外した配置で測らないと、
2357
- // 枠を置いた後に縦列が詰め直されて枠から外れる
2358
- posX: 0,
2359
- posY: 0,
2360
- posW: 1,
2361
- posH: 1,
2362
- });
2363
- 作った.add(containerId);
2364
- }
2365
- return 作った;
2366
- }
2367
-
2368
- /**
2369
- * 組の枠と束ねた縦列の間の余白。
2370
- *
2371
- * | 向き | 値 | 理由 (実測) |
2372
- * |---|---|---|
2373
- * | 上 | 40 | 枠の名札を縦列の名札に重ねない高さ。 0 で 74 重なり、 40 で重ならない |
2374
- * | 下 | 20 | 箱の下端から枠を離す |
2375
- * | 横 (囲いを持つ縦列) | 20 | 枠を縦列の囲いの線に重ねない。 囲いを持つ縦列の間は矢印の名前が入る広さがある |
2376
- * | 横 (囲いの無い縦列) | 0 | 縦列の端は箱から既に離れている。 20 取ると、 縦列の間に置いた矢印の名前を枠の線が貫く (箱の種別 `person` で 8 食い込んだ) |
2377
- */
2378
- const 組の枠の余白 = { 上: 40, 下: 20, 囲いの横: 20, 囲いの無い横: 0 } as const;
2379
-
2380
- /**
2381
- * 組 (`groups:`) の枠を、 束ねた縦列とその中の箱を全て囲む位置と大きさに置く (#1972)。
2382
- *
2383
- * **縦列の位置が全て決まった後に 1 度だけ配置して測る**。 枠は作った時から座標を固定してあるので、
2384
- * 枠を置いても他の縦列は動かない (固定した縦列は並びに加わらない)。
2385
- *
2386
- * 束ねた縦列の扱い。
2387
- *
2388
- * | 状態 | 扱い |
2389
- * |---|---|
2390
- * | 図に無い縦列を書いた | その縦列を除いて囲み、 無いことを知らせる |
2391
- * | 図に在る縦列が 1 本も無い | 枠を作らず、 知らせる (図の端に何も囲まない枠を描かない) |
2392
- * | 間に束ねない縦列を挟む | 間の縦列ごと囲み、 挟んだ縦列を知らせる (縦列の並びは書いた順を変えない) |
2393
- */
2394
- function applyGroupFrames(
2395
- diagram: CdlDiagram,
2396
- doc: DslDocument,
2397
- 作った枠: ReadonlySet<string>,
2398
- onNotice?: (notice: CompileNotice) => void,
2399
- ): void {
2400
- if (作った枠.size === 0) return;
2401
- const 置く: { 枠: CdlDiagram["lanes"][number]; 名前: string; line: number; 縦列: string[] }[] = [];
2402
- for (const [id, g] of Object.entries(doc.groups ?? {})) {
2403
- const 枠 = diagram.lanes.find((l) => l.id === `group-${id}`);
2404
- if (!枠 || !作った枠.has(枠.id)) continue;
2405
- const 書いた = Array.isArray(g.lanes) ? g.lanes : [];
2406
- const 在る = 書いた.filter((x) => diagram.lanes.some((l) => l.id === x && !作った枠.has(l.id)));
2407
- const 無い = 書いた.filter((x) => !在る.includes(x));
2408
- if (在る.length === 0) {
2409
- diagram.lanes = diagram.lanes.filter((l) => l !== 枠);
2410
- onNotice?.({
2411
- kind: "group-lane-missing",
2412
- actor: id,
2413
- line: g.pos?.line ?? 0,
2414
- message:
2415
- 書いた.length === 0
2416
- ? `組 "${truncateForMessage(id)}" は束ねる縦列を書いていないので、枠を描きません`
2417
- : `組 "${truncateForMessage(id)}" が束ねる縦列 ${無い.map((x) => `"${truncateForMessage(x)}"`).join(", ")} が図に無いので、枠を描きません`,
2418
- hint: "`lanes: [縦列の名前, ...]` に `lanes:` で書いた縦列の名前を並べる",
2419
- });
2420
- continue;
2421
- }
2422
- if (無い.length > 0) {
2423
- onNotice?.({
2424
- kind: "group-lane-missing",
2425
- actor: id,
2426
- line: g.pos?.line ?? 0,
2427
- message: `組 "${truncateForMessage(id)}" が束ねる縦列 ${無い.map((x) => `"${truncateForMessage(x)}"`).join(", ")} が図に無いので、残りの縦列だけを囲みます`,
2428
- hint: "`lanes:` で書いた縦列の名前か確かめる",
2429
- });
2430
- }
2431
- 置く.push({ 枠, 名前: id, line: g.pos?.line ?? 0, 縦列: 在る });
2432
- }
2433
- if (置く.length === 0) return;
2434
-
2435
- // **ここで作った枠を測る図から外す** (#2300)。 枠はまだ置いていない = `posX: 0, posY: 0` の
2436
- // 仮置きなので、 渡すと組の名札が全部同じ `x = 16` に積み上がり、 記法の engine が
2437
- // 実物と合わない重なりを知らせる (組を 2 つ書くと `1994px²`、 組を N 個書くと最大 N(N-1)/2 件)。
2438
- // 出来上がった図では 1 度も重ならない (名札の箱は `16.0..87.2` と `564.0..682.4`)。
2439
- //
2440
- // 外しても測る値は変わらない。 枠は `posX` と `posY` を持つ縦列として並べ直しの対象から
2441
- // 外れており (`role: "overlay"` で間隔の調整からも外れる)、 結果からは元から捨てていた。
2442
- // 渡す時点で外すだけで、 縦列と箱の位置は 1 単位も動かない。
2443
- const 配置 = 測るために配置する({
2444
- ...diagram,
2445
- lanes: diagram.lanes.filter((l) => !作った枠.has(l.id)),
2446
- });
2447
- const 並びの縦列 = 配置.lanes;
2448
- for (const { 枠, 名前, line, 縦列 } of 置く) {
2449
- const 束 = 並びの縦列.filter((l) => 縦列.includes(l.id));
2450
- const 左 = Math.min(...束.map((l) => l.x ?? 0));
2451
- const 右 = Math.max(...束.map((l) => (l.x ?? 0) + l.width));
2452
- // 束ねた縦列の左端と右端の間に入る縦列は、 束ねなくても枠に入る
2453
- const 挟んだ = 並びの縦列.filter(
2454
- (l) => !縦列.includes(l.id) && (l.x ?? 0) < 右 && (l.x ?? 0) + l.width > 左,
2455
- );
2456
- if (挟んだ.length > 0) {
2457
- onNotice?.({
2458
- kind: "group-lanes-apart",
2459
- actor: 名前,
2460
- line,
2461
- message: `組 "${truncateForMessage(名前)}" が束ねる縦列の間に ${挟んだ.map((l) => `"${truncateForMessage(l.id)}"`).join(", ")} があるので、その縦列も枠に入ります`,
2462
- hint: "束ねる縦列を `lanes:` で隣り合う順に並べる",
2463
- });
2464
- }
2465
- const 囲む縦列 = new Set([...束, ...挟んだ].map((l) => l.id));
2466
- const 囲む = [...束, ...挟んだ];
2467
- const 箱 = 配置.nodes.filter((n) => 囲む縦列.has(n.lane));
2468
- const 上 = Math.min(...囲む.map((l) => l.y ?? 0));
2469
- const 下 = Math.max(
2470
- ...囲む.map((l) => (l.y ?? 0) + (l.height ?? 0)),
2471
- ...箱.map((n) => n.cy + n.h / 2),
2472
- );
2473
- const 囲いを持つ = diagram.lanes.some((l) => 囲む縦列.has(l.id) && l.contain === true);
2474
- const 横 = 囲いを持つ ? 組の枠の余白.囲いの横 : 組の枠の余白.囲いの無い横;
2475
- 枠.posX = 左 - 横;
2476
- 枠.posY = 上 - 組の枠の余白.上;
2477
- 枠.posW = 右 - 左 + 横 * 2;
2478
- 枠.posH = 下 - 上 + 組の枠の余白.上 + 組の枠の余白.下;
2479
- }
2480
- }
2481
-
2482
- /**
2483
- * ユーザージャーニーの欄を、 それを描けない図種で書いた時に伝える (#1251)。
2484
- *
2485
- * `touchpoint` と `opportunity` と `opportunityPosition` は `type: journey` の段だけが持つ。 他の図種では相手が無く、
2486
- * 黙って捨てると「書いたのに出ない」 が手掛かりなしで起きる。
2487
- *
2488
- * `type: mind` では伝えない = `compileMind` が描けない欄をまとめて 1 件で伝えており、
2489
- * そこに 2 つとも入っている (`放射で描けない欄`)。 二重に伝えない (#1246 と同じ扱い)。
2490
- */
2491
- function reportChartFieldsNotHonored(
2492
- doc: DslDocument,
2493
- onNotice?: (n: CompileNotice) => void,
2494
- ): void {
2495
- if (!onNotice) return;
2496
- if (doc.type === "mind") return;
2497
- for (const a of doc.actors) {
2498
- if (a.partId !== undefined) continue;
2499
- const 道筋 =
2500
- doc.type === "journey"
2501
- ? []
2502
- : [
2503
- ...(a.touchpoint !== undefined ? ["touchpoint"] : []),
2504
- ...(a.opportunity !== undefined ? ["opportunity"] : []),
2505
- ...(a.opportunityPosition !== undefined ? ["opportunityPosition"] : []),
2506
- ];
2507
- const 工程 =
2508
- doc.type === "gantt"
2509
- ? []
2510
- : [
2511
- ...(a.owner !== undefined ? ["owner"] : []),
2512
- ...(a.end !== undefined ? ["end"] : []),
2513
- ...(a.startLabel !== undefined ? ["startLabel"] : []),
2514
- ...(a.milestone !== undefined ? ["milestone"] : []),
2515
- ...(a.emphasis !== undefined && doc.type !== "record" && !値として読む図が主役を読む(doc.type, doc.shape)
2516
- ? ["emphasis"]
2517
- : []),
2518
- ];
2519
- if (道筋.length > 0) {
2520
- onNotice({
2521
- kind: "chart-value-unreadable",
2522
- actor: a.name,
2523
- line: a.pos?.line ?? 0,
2524
- message: `"${truncateForMessage(a.name)}" に書いた ${道筋.join(" / ")} は効きません (type: ${doc.type} にはユーザージャーニーの欄がありません)`,
2525
- hint: "ユーザージャーニーを描くなら type: journey を使ってください",
2526
- });
2527
- }
2528
- if (工程.length > 0) {
2529
- onNotice({
2530
- kind: "chart-value-unreadable",
2531
- actor: a.name,
2532
- line: a.pos?.line ?? 0,
2533
- message: `"${truncateForMessage(a.name)}" に書いた ${工程.join(" / ")} は効きません (type: ${doc.type} には工程の並びの欄がありません)`,
2534
- hint: "工程の並びを描くなら type: gantt を使ってください",
2535
- });
2536
- }
2537
- }
2538
- }
2539
-
2540
- /**
2541
- * 軸の名前を、それを持たない図種で書いた時に伝える (#1251)。
2542
- *
2543
- * 軸を持つのは `type: quadrant` だけ。 他の図種では相手が無く、黙って捨てると
2544
- * 「書いたのに出ない」 が手掛かりなしで起きる。
2545
- */
2546
- function reportAxesNotHonored(doc: DslDocument, onNotice?: (n: CompileNotice) => void): void {
2547
- if (!onNotice) return;
2548
- if (doc.axes === undefined) return;
2549
- if (doc.type === "quadrant") return;
2550
- onNotice({
2551
- kind: "chart-value-unreadable",
2552
- actor: doc.title,
2553
- line: doc.axesPos?.line ?? doc.pos?.line ?? 0,
2554
- message: `最上位に書いた axes は効きません (type: ${doc.type} には軸がありません)`,
2555
- hint: "2 つの軸で仕分ける図を描くなら type: quadrant を使ってください",
2556
- });
2557
- }
2558
-
2559
- /**
2560
- * `lanes:` で作った縦列と、見本 (parts) が作った同一idの縦列を 1 つに重ねる (#1241)。
2561
- *
2562
- * `lanes:` の中身を読むのは見本を重ねるより前で、その時点では見本の縦列がまだ無い。
2563
- * そのため同じ id を書くと **縦列が 2 つできて、書いた幅は箱の入っていない方に付く**
2564
- * (実測 = `g__l` が 777 と 400 の 2 本になり、箱は 400 の方に入った)。
2565
- *
2566
- * 後から来た見本の縦列に書いた値を移し、先に作った空の方を外す。 書いた人から見れば
2567
- * 「id を書けば効く」 が成り立つ。
2568
- */
2569
- function mergeDuplicateDeclaredLanes(diagram: CdlDiagram, 追加した縦列: readonly DslLane[]): void {
2570
- for (const 宣言 of 追加した縦列) {
2571
- const { id } = 宣言;
2572
- const 同一idの縦列 = diagram.lanes.filter((l) => l.id === id);
2573
- if (同一idの縦列.length < 2) continue;
2574
- // 宣言 lane は parts より先に追加されるため、後から作った parts lane を残す。
2575
- const 残す = 同一idの縦列.at(-1);
2576
- if (残す === undefined) continue;
2577
- const 元の中心X = (残す.x ?? 0) + 残す.width / 2;
2578
- // 新規 lane に入れた既定値ではなく、DSL に明示された値だけを parts lane へ重ねる。
2579
- if (宣言.x !== undefined) 残す.x = 宣言.x;
2580
- if (宣言.width !== undefined) 残す.width = 宣言.width;
2581
- if (宣言.label !== undefined) 残す.label = 宣言.label;
2582
- if (宣言.contain !== undefined) 残す.contain = 宣言.contain;
2583
- if (宣言.lifeline !== undefined) 残す.lifeline = 宣言.lifeline;
2584
- const 移動X = (残す.x ?? 0) + 残す.width / 2 - 元の中心X;
2585
- // parts node は絶対座標を持つため、lane だけ動かすと箱が元の場所に残る。
2586
- for (const node of diagram.nodes) {
2587
- if (node.lane === id && node.posX !== undefined) node.posX += 移動X;
2588
- }
2589
- for (let i = diagram.lanes.length - 1; i >= 0; i -= 1) {
2590
- if (diagram.lanes[i]?.id === id && diagram.lanes[i] !== 残す) diagram.lanes.splice(i, 1);
2591
- }
2592
- }
2593
- }
2594
-
2595
- /**
2596
- * `lanes:` で作った縦列に箱が 1 つも入らなかったら伝える (#1241)。
2597
- *
2598
- * 意図して空の縦列を置くことはあるが、その場合も「置いた」 と分かる形で知らせる方が、
2599
- * 書き間違いを黙って捨てるより良い。
2600
- */
2601
- function reportEmptyDeclaredLanes(
2602
- diagram: CdlDiagram,
2603
- 追加した縦列: readonly DslLane[],
2604
- onNotice?: (n: CompileNotice) => void,
2605
- ): void {
2606
- if (追加した縦列.length === 0) return;
2607
- mergeDuplicateDeclaredLanes(diagram, 追加した縦列);
2608
- if (!onNotice) return;
2609
- const 使われている = new Set(diagram.nodes.map((n) => n.lane));
2610
- const ある縦列 = diagram.lanes.map((l) => l.id).filter((x) => 使われている.has(x));
2611
- for (const { id, pos } of 追加した縦列) {
2612
- if (使われている.has(id)) continue;
2613
- onNotice({
2614
- kind: "lane-declared-empty",
2615
- actor: id,
2616
- line: pos?.line ?? 0,
2617
- message: `lanes に書いた ${truncateForMessage(id)} はどの箱も入らない縦列です (新しく作りました)`,
2618
- hint:
2619
- ある縦列.length > 0
2620
- ? `この図が持つ縦列 = ${ある縦列.join(" / ")}`
2621
- : "この図は箱の入った縦列を持ちません",
2622
- });
2623
- }
2624
- }
2625
-
2626
- /**
2627
- * 登場人物に書いた欄と、最上位の `lanes:` / `viewport:` を、組み立て済みの図へ後から重ねる。
2628
- *
2629
- * 図種ごとの組み立て器が基本の並びを作った後に、次の 3 つを重ねる。
2630
- *
2631
- * - 登場人物に書いた欄を、同じ id の箱へ写す
2632
- * - `lanes:` に書いた x / width / label / contain / lifeline を、同じ id の縦列へ写す。
2633
- * 無ければ縦列を作り、`追加した縦列out` に積む
2634
- * - `viewport:` の `laneWidth` を全ての縦列の幅にし、残りの欄を図の `viewport` にまとめる
2635
- *
2636
- * どの欄を写すかは本体が持つ。 `groups:` は `applyGroupContainers` が扱う。
2637
- */
2638
- function applyV05Extensions(
2639
- diagram: CdlDiagram,
2640
- doc: DslDocument,
2641
- /** `lanes:` が新しく作った縦列。 箱が入ったかは見本を重ねた後でないと分からない (#1241) */
2642
- 追加した縦列out?: DslLane[],
2643
- ): CdlDiagram {
2644
- // actor の主要 node を preset 種別で回収する。 sequence は header/footer を対で生成する
2645
- // preset で主要 node は header、 それ以外の preset は actor 名 slug がそのまま node id になる。
2646
- //
2647
- // seq-like の非 animate 経路は実 node id を CDL preset 側 slugify (`_` → `-` 置換 + 全角正規化) で
2648
- // 生成する。 dragon slugify (`_` / 全角 保持) で `{slug}-header` を決め打つと、 actor `A_B` の
2649
- // primaryNodeId `a_b-header` が実 node `a-b-header` と食い違い、 inline option (subtitle / eyebrow /
2650
- // value / rows) が drop する (#881、 #873 / #877 と同根の dragon⇔CDL slug 不一致)。
2651
- //
2652
- // **順序図はここを通らない** (#1466)。 `sequence` は 1 枚の板になり、面ごとの
2653
- // 箱も縦列も作らなくなった = 書いた欄を写す相手が無い。 面に書いた内容が効かないことは
2654
- // `reportActorKindNotHonored` が伝える。
2655
- // actor inline option → node merge
2656
- for (const a of doc.actors) {
2657
- const dragonSlug = slugify(a.name);
2658
- // 1 actor = 1 node (id = dragon slug) で node id と dragon slug が一致する。
2659
- const primaryNodes = diagram.nodes.filter((n) => n.id === dragonSlug);
2660
- for (const node of primaryNodes) {
2661
- // 識別に使う名前と、箱に出す題を分ける (#1381)。 preset が actor 名で
2662
- // node を作る経路 (sequence / swimlane / c4) もここで書き換える。
2663
- if (a.title !== undefined) node.title = a.title;
2664
- // `type: c4` では説明の先頭に段の目印 (`L1` / `L2` / `L3`) を書く。 目印は組み立てに
2665
- // 段を伝えるためのもので読む人に意味を持たず、 段の名前は枠のラベルが既に出している。
2666
- // ここで落とさないと、 組み立てが読み取った目印がそのまま箱の説明として出る (#1098)
2667
- const 説明 = doc.type === "c4" ? 段を読み取る(a.subtitle).説明 : a.subtitle;
2668
- if (説明 !== undefined) node.subtitle = 説明;
2669
- if (a.kindForm !== undefined) node.kindForm = a.kindForm;
2670
- if (a.icon !== undefined) node.icon = a.icon;
2671
- if (a.emptyRowsNote !== undefined) node.emptyRowsNote = a.emptyRowsNote;
2672
- if (a.emphasis !== undefined && doc.type === "record") node.emphasis = a.emphasis;
2673
- if (a.titleAlign !== undefined) node.titleAlign = a.titleAlign;
2674
- if (a.rowRules !== undefined) node.rowRules = a.rowRules;
2675
- if (a.rowStripe !== undefined) node.rowStripe = a.rowStripe;
2676
- if (a.rowLayout !== undefined) node.rowLayout = a.rowLayout;
2677
- if (a.rowDividerForm !== undefined) node.rowDividerForm = a.rowDividerForm;
2678
- if (a.rowWell !== undefined) node.rowWell = a.rowWell;
2679
- if (a.rowMarkForm !== undefined) node.rowMarkForm = a.rowMarkForm;
2680
- if (a.nestIn !== undefined) node.nestIn = slugify(a.nestIn);
2681
- if (a.figureCard !== undefined) node.figureCard = a.figureCard;
2682
- if (a.eyebrow !== undefined) node.eyebrow = a.eyebrow;
2683
- if (a.value !== undefined) node.value = a.value;
2684
- if (a.rows !== undefined) node.rows = a.rows;
2685
- // 行頭の印 (#1466)。 書いた語を図の種類ごとの意味で読み、群の分け方も種類が決める
2686
- if (a.marks !== undefined && a.rows !== undefined && 行頭の印を読むか(doc.type)) {
2687
- const 組 = 鍵の行を上にまとめる(a.rows, 行頭の印にする(a.marks, a.rows.length));
2688
- node.rows = 組.rows;
2689
- node.rowMarks = 組.rowMarks;
2690
- }
2691
- /*
2692
- * **行を持たない状態でも欄を置く** (#1466)。
2693
- *
2694
- * 描き手は `rowMarks` の有無で新しい意匠かどうかを決める (`storage.tsx`)。 行の無い箱で
2695
- * 欄ごと省くと、その箱だけ従来の意匠に落ちて呼び名が消える (組み立て API 側の
2696
- * `stateMachine` は同じ理由で空の欄を置いている)。
2697
- */
2698
- if (doc.type === "record" && node.kind === "storage" && node.rowMarks === undefined) {
2699
- node.rows = a.rows ?? [];
2700
- node.rowMarks = [];
2701
- }
2702
- // 箱の大きさを反映する (#1259)。 **animation の有無に関係なく** = 動く図専用の
2703
- // 組み立てだけで渡すと、同じ記法でも静止図では指定が消える。
2704
- //
2705
- // **幅が図に出るかは縦列との大小で決まる**。 縦列に収まれば箱だけが変わり、
2706
- // 縦列より広ければ縦列ごと押し広げる (実測 = 状態遷移図の 320 は縦列 370 に収まって
2707
- // 図が変わらないが、入れ子の状態遷移図の 280 に対し既定 640 は縦列 330 を押し広げた)。
2708
- // #1260 で 1 件だけ見て「見た目に出ない」 と判断し配線を外した = 同じ誤りを繰り返さない
2709
- if (a.posW !== undefined) node.w = a.posW;
2710
- if (a.posH !== undefined) node.h = a.posH;
2711
- // 箱の中に描く図形 (#1374)。 renderer が shape を描くのは dyn-* kind だけなので、
2712
- // shape 自身を SSOT にして対応する kind へ揃える。 card 等のまま shape だけ渡すと、指定を
2713
- // 保持しているのに画面には何も出ない。 paint 検査が入力を mutate しないよう object も写す。
2714
- if (a.shape !== undefined) {
2715
- const dynamicKind = `dyn-${a.shape.kind}` as typeof node.kind;
2716
- node.kind = dynamicKind;
2717
- node.shape = { ...a.shape };
2718
- }
2719
- // その箱を出すかどうかの条件 (#1381)
2720
- if (a.visibleIf !== undefined) node.visibleIf = a.visibleIf;
2721
- /*
2722
- * 値に追随する 5 欄 (#1392)。
2723
- *
2724
- * 見た目の大きさ / 濃さ / ずらしを、書いた箱にだけ効かせる。
2725
- */
2726
- if (a.wBind !== undefined) node.wBind = a.wBind;
2727
- if (a.hBind !== undefined) node.hBind = a.hBind;
2728
- if (a.opacity !== undefined) node.opacity = a.opacity;
2729
- if (a.renderOffsetX !== undefined) node.renderOffsetX = a.renderOffsetX;
2730
- if (a.renderOffsetY !== undefined) node.renderOffsetY = a.renderOffsetY;
2731
- }
2732
- }
2733
- // v0.5+ animation phase 後段注入 (CAR-1657 fix、 元 dragon PR #413 report user)。
2734
- // 単一箱の preset (chart / c4 / mind / gantt 等) が doc.animate を無視して build するケースを補償。
2735
- // 既に preset が phase を生成済 (sequence / flow / swimlane / er / state / topology 経由 = compileGenericWithAnimate) なら skip。
2736
- // doc に phase 指定があって diagram.phases が空なら、 preset 由来 lane/node/edge に対して generic phase を注入する。
2737
- if (doc.animate && doc.animate.phases.length > 0 && diagram.phases.length === 0) {
2738
- injectPhasesFallback(diagram, doc);
2739
- }
2740
- // top-level lanes section → lane merge
2741
- //
2742
- // **書いた id がどの縦列とも合わない形を後で伝える** (#1241)。 合わない id は新しい縦列を
2743
- // 作るだけで、書いた幅や見出しは元の縦列に届かない。 書き間違い (`lane-idl` / 全角の
2744
- // `lane-A`) がこの形になり、黙って捨てられていた (実測 = 幅 999 を持つ空の縦列が増え、
2745
- // 元の縦列は 360 のままだった)
2746
- const 追加した縦列: DslLane[] = 追加した縦列out ?? [];
2747
- if (doc.lanes && !(doc.type === "swimlane" && (doc.shape === "stages" || doc.shape === "metro" || doc.shape === "timeline"))) {
2748
- for (const [id, laneOpt] of Object.entries(doc.lanes)) {
2749
- const lane = diagram.lanes.find((l) => l.id === id);
2750
- if (lane) {
2751
- if (laneOpt.x !== undefined) lane.x = laneOpt.x;
2752
- if (laneOpt.width !== undefined) lane.width = laneOpt.width;
2753
- if (laneOpt.label !== undefined) lane.label = laneOpt.label;
2754
- if (laneOpt.subtitle !== undefined) lane.subtitle = laneOpt.subtitle;
2755
- if (laneOpt.contain !== undefined) lane.contain = laneOpt.contain;
2756
- if (laneOpt.lifeline !== undefined) lane.lifeline = laneOpt.lifeline;
2757
- } else {
2758
- // lane が preset で作られていなければ新規追加。
2759
- //
2760
- // **横位置を書いていなければ横位置を持たせない** (#2147)。 描画側は横位置を持たない縦列を
2761
- // 書いた順に前の縦列の右へ並べ、横位置を持つ縦列は横位置の順に並べる。 0 を入れると、
2762
- // 型が作った縦列 (横位置を持たない) の間に割り込む (実測 = 登場人物 受付 / 出荷 の後に書いた
2763
- // `置き場` が 受付 と 出荷 の間に描かれた)
2764
- diagram.lanes.push({
2765
- id,
2766
- ...(laneOpt.x !== undefined ? { x: laneOpt.x } : {}),
2767
- width: laneOpt.width ?? 320,
2768
- label: laneOpt.label,
2769
- subtitle: laneOpt.subtitle,
2770
- contain: laneOpt.contain,
2771
- lifeline: laneOpt.lifeline,
2772
- });
2773
- 追加した縦列.push(laneOpt);
2774
- }
2775
- }
2776
- }
2777
- // viewport.laneWidth → 全 lane width に override
2778
- if (doc.viewport?.laneWidth !== undefined) {
2779
- for (const lane of diagram.lanes) {
2780
- lane.width = doc.viewport.laneWidth;
2781
- }
2782
- }
2783
- // viewport.width / height / gap / laneGap / nodeGap / scale / labelMargin → CdlDiagram.viewport に集約
2784
- if (doc.viewport) {
2785
- diagram.viewport = {
2786
- ...(diagram.viewport ?? {}),
2787
- ...(doc.viewport.width !== undefined ? { width: doc.viewport.width } : {}),
2788
- ...(doc.viewport.height !== undefined ? { height: doc.viewport.height } : {}),
2789
- ...(doc.viewport.contentOffsetX !== undefined ? { contentOffsetX: doc.viewport.contentOffsetX } : {}),
2790
- ...(doc.viewport.gap !== undefined ? { gap: doc.viewport.gap } : {}),
2791
- ...(doc.viewport.laneGap !== undefined ? { laneGap: doc.viewport.laneGap } : {}),
2792
- ...(doc.viewport.nodeGap !== undefined ? { nodeGap: doc.viewport.nodeGap } : {}),
2793
- ...(doc.viewport.scale !== undefined ? { scale: doc.viewport.scale } : {}),
2794
- ...(doc.viewport.labelMargin !== undefined ? { labelMargin: doc.viewport.labelMargin } : {}),
2795
- };
2796
- }
2797
- return diagram;
2798
- }
2799
-
2800
- /**
2801
- * v0.5+ animation phase 後段 fallback 注入 (CAR-1657)。
2802
- *
2803
- * chart / c4 / mind / gantt 等の preset は独自 layout を持ち、compileGenericWithAnimate 経路に
2804
- * 乗らないため、 doc.animate.phases があっても diagram.phases が空になる。
2805
- * 本 helper が applyV05Extensions から呼ばれて post-hoc に phase を差込む、 lane/node/edge は
2806
- * 既存 preset 出力を保持したまま animation だけ追加する。
2807
- *
2808
- * highlight resolution = actor 名 = slugify → diagram.nodes.id 対応、 edge の "A -> B" は
2809
- * from/to の slug で 1:1 対応する edge を検索。 preset 由来 edge id は各種 (`e-{from}-{to}` /
2810
- * `e{idx}-{fromId}-{toId}` 等) 揺れがあるため、 (edge.from === slug(A) && edge.to === slug(B))
2811
- * で辞書 lookup せず走査で解決する。
2812
- */
2813
- function injectPhasesFallback(diagram: CdlDiagram, doc: DslDocument): void {
2814
- if (!doc.animate) return;
2815
-
2816
- // states 反映 (未登録なら追加、 既存は上書きしない)。 CdlState は id field (name ではない)。
2817
- const existingStateIds = new Set(diagram.states.map((s) => s.id));
2818
- for (const st of doc.animate.states) {
2819
- if (!existingStateIds.has(st.name)) {
2820
- diagram.states.push({ id: st.name, initial: st.initial });
2821
- }
2822
- }
2823
-
2824
- // highlight 解決関数 = actor 名 or "A -> B" / "A → B" を node.id / edge.id に変換。
2825
- // 矢印は半角 (`->`) と全角 (`→`) の両方を受ける (他の経路が全角で書かれても同じ結果になる)。
2826
- // 同じ from / to の線が複数ある時は 1 本目で止めず、全件を光らせる。
2827
- // 実在する名前。 矢印を含む名前 (`"A -> B"`) を矢印と読み違えないために渡す
2828
- const knownNames = new Set(doc.actors.map((a) => a.name));
2829
- // 図全体を 1 つの箱で描く種類は、 登場人物ごとの箱を持たない。
2830
- // 箱が 1 つの時だけ対象にする = 2 つ以上あるとどれを指したのか決められない
2831
- const singleBoxNodes = diagram.nodes.filter((n) => SINGLE_BOX_KINDS.has(String(n.kind)));
2832
- const singleBoxNode = singleBoxNodes.length === 1 ? singleBoxNodes[0] : undefined;
2833
- const resolveIds = (highlight: readonly string[]): string[] => {
2834
- const out: string[] = [];
2835
- for (const h of highlight) {
2836
- const entry = parseFocusEntry(h, knownNames);
2837
- if (entry.kind === "edge") {
2838
- const fromSlug = slugify(entry.from);
2839
- const toSlug = slugify(entry.to);
2840
- let 見つかった = false;
2841
- for (const e of diagram.edges) {
2842
- if (e.from === fromSlug && e.to === toSlug) {
2843
- out.push(e.id);
2844
- 見つかった = true;
2845
- }
2846
- }
2847
- // 線を持たない種類 (帯の依存等) では矢印が edge にならない。 両端が実在するなら
2848
- // その箱を光らせる = 矢印を指した段で何も光らないより意図に近い (#1077)
2849
- if (
2850
- !見つかった &&
2851
- singleBoxNode !== undefined &&
2852
- knownNames.has(entry.from) &&
2853
- knownNames.has(entry.to)
2854
- ) {
2855
- out.push(singleBoxNode.id);
2856
- }
2857
- continue;
2858
- }
2859
- const nodeSlug = slugify(entry.name);
2860
- const node = diagram.nodes.find((n) => n.id === nodeSlug || n.id === `${nodeSlug}-header`);
2861
- if (node) {
2862
- out.push(node.id);
2863
- continue;
2864
- }
2865
- // 図全体が 1 つの箱になる種類 (円グラフ等) では、 登場人物ごとの箱が無い。 名前で
2866
- // 指しても解決できず、 書いた `focus:` が丸ごと消える (#1076 で pie を 1 箱にした時に
2867
- // 発生)。 **書いた名前が実在するなら、 その箱を光らせる** = 何も光らないより意図に近い。
2868
- // 実在しない名前は従来どおり無視する (綴り誤りを黙って光らせない)
2869
- if (knownNames.has(entry.name) && singleBoxNode !== undefined) out.push(singleBoxNode.id);
2870
- }
2871
- // 同じ箱を複数回指した時に重複させない (円グラフで 4 人を指すと 4 回入る)
2872
- return [...new Set(out)];
2873
- };
2874
-
2875
- // phase 注入。 CdlPhase.tweens[].stateId / sets[].stateId で state 参照 (state ではない)。
2876
- for (const p of doc.animate.phases) {
2877
- const activateIds: string[] = [...resolveIds(p.highlight ?? [])];
2878
- // `draw:` を描画側の欄へ写す (#1312 / #1314 / #1318)。 相手は 1 箱で図全体を描く
2879
- // 対象の箱で、必ず 1 つに決まるため名前を書かせずに引ける。
2880
- //
2881
- // **`activate` と兼ねない**。 描画側は焦点と別集合で持つ (`cdl#512`) = 焦点が当たり
2882
- // 続ける図で毎段引き直しになるため。 書いた段だけが欄を持つ
2883
- const drawIds =
2884
- p.draw !== undefined &&
2885
- 描く語がその図を指すか(
2886
- p.draw,
2887
- doc.type,
2888
- doc.shape === undefined ? undefined : (resolveShape(doc.shape) ?? undefined),
2889
- ) &&
2890
- singleBoxNode !== undefined
2891
- ? [singleBoxNode.id]
2892
- : [];
2893
- diagram.phases.push({
2894
- id: slugify(p.name) || p.name,
2895
- duration: p.durationMs,
2896
- title: p.name,
2897
- body: p.body ?? "",
2898
- activate: activateIds,
2899
- ...(drawIds.length > 0 ? { draw: drawIds } : {}),
2900
- // 描く速さ (#1441)。 **描く相手が決まった段にだけ載せる** = 語が効かない図種
2901
- // (`drawIds` が空) で割合だけ渡すと、描画側が「draw が空なのに割合がある」 と知らせる
2902
- ...(drawIds.length > 0 && p.drawRatio !== undefined ? { drawRatio: p.drawRatio } : {}),
2903
- tweens: (p.tweens ?? []).map((t) => ({ stateId: t.state, from: t.from, to: t.to })),
2904
- sets: (p.sets ?? []).map((s) => ({ stateId: s.state, value: s.value })),
2905
- ...(p.badge ? { badge: p.badge } : {}),
2906
- });
2907
- }
2908
- }
2909
-
2910
- /**
2911
- * 向きの行を外しても、同じ組み立ての経路を通るか (#2421)。
2912
- *
2913
- * **向きを書くこと自体が経路を切り替える**。 フローは鎖をやめて書いた端のとおりに繋ぎ
2914
- * (`鎖にしない書き方`)、担当の図は静止図の組み立てから共通の組み立てへ回る
2915
- * (`共通の組み立てへ回す` の図種ごとの理由)。
2916
- *
2917
- * だから「既定と同じ値なら何もしない」 は成り立たない。 実測 = 動きを書かないフローに
2918
- * `direction: 縦` を足すと、viewBox が 490×539 から 504×631 に変わり、矢印の線種も変わる。
2919
- *
2920
- * 同じ経路を通る時だけ、書いた向きは本当に何もしない。 その経路の中で向きを読むのは
2921
- * `並べる向き` だけで、既定と同じ値ならそこも同じ値を返す。
2922
- *
2923
- * **判定は経路を決める関数から導く**。 条件を手で並べると、`鎖にしない書き方` に行が増えた日に
2924
- * ここだけ古くなる。
2925
- */
2926
- function 向きを外しても同じ経路か(doc: DslDocument): boolean {
2927
- const 向きなし: DslDocument = { ...doc, direction: undefined };
2928
- if (doc.type === "flow") return 鎖でつなぐ形か(向きなし) === 鎖でつなぐ形か(doc);
2929
- if (doc.type === "swimlane") {
2930
- return 共通の組み立てへ回す("swimlane", 向きなし, false) === 共通の組み立てへ回す("swimlane", doc, true);
2931
- }
2932
- /*
2933
- * 分かれ道の図は向きで経路が変わらない (#2524)。 組み立ては 1 本で、向きは中で
2934
- * 「縦列 1 本に積むか」 の 1 点だけを切り替える。 既定と同じ値を書いた図は同じ並びになる。
2935
- */
2936
- if (doc.type === "flowchart") return true;
2937
- return false;
2938
- }
2939
-
2940
- /**
2941
- * 書いた向きが図を変えない時に伝える (#1494 / #2421)。
2942
- *
2943
- * 変えない形は 3 つある。 向きを選べない図種に書いた形と、全ての箱が縦列を書いた形と、
2944
- * 既定と同じ向きを書いて経路も変わらない形。
2945
- * 前の 2 つは書いた向きが捨てられる形なので `direction-not-honored`、
2946
- * 3 つ目は書いても書かなくても同じ図になる形なので `direction-same-as-default`。
2947
- *
2948
- * **読み手が次に取る手が違うので 1 つにまとめない**。 前者は書き方を変えれば効くが、
2949
- * 後者は既に効いている値と同じで、行を外すか別の値を書くかの選択になる。
2950
- *
2951
- * 黙って捨てると「書いたのに変わらない」 が手掛かりなしで起きる。
2952
- */
2953
- function reportDirectionNotHonored(doc: DslDocument, onNotice?: (n: CompileNotice) => void): void {
2954
- if (!onNotice) return;
2955
- if (doc.direction === undefined) return;
2956
- const 行 = doc.directionPos?.line ?? doc.pos?.line ?? 0;
2957
- if (doc.type === "swimlane" && doc.shape === "timeline") {
2958
- onNotice({
2959
- kind: "direction-not-honored",
2960
- actor: doc.title,
2961
- line: 行,
2962
- message: "書いた direction は効きません (shape: timeline は段を常に上から下へ並べます)",
2963
- hint: "段の並びは actors に書いた順で決まります",
2964
- });
2965
- return;
2966
- }
2967
- if (doc.type === "swimlane" && doc.shape === "metro") {
2968
- onNotice({
2969
- kind: "direction-not-honored",
2970
- actor: doc.title,
2971
- line: 行,
2972
- message: "書いた direction は効きません (shape: metro は駅を常に左から右へ並べます)",
2973
- hint: "駅の並びは actors に書いた順で決まります",
2974
- });
2975
- return;
2976
- }
2977
- if (doc.type === "swimlane" && doc.shape === "stages") {
2978
- onNotice({
2979
- kind: "direction-not-honored",
2980
- actor: doc.title,
2981
- line: 行,
2982
- message: "書いた direction は効きません (shape: stages は段階を常に左から右へ並べます)",
2983
- hint: "段階の並びは actors に最初に現れた順で決まります",
2984
- });
2985
- return;
2986
- }
2987
- if (!向きを選べる図種.has(doc.type)) {
2988
- onNotice({
2989
- kind: "direction-not-honored",
2990
- actor: doc.title,
2991
- line: 行,
2992
- message: `書いた direction は効きません (type: ${doc.type} は並び方そのものが読み方を決めます)`,
2993
- hint: `direction を書けるのは ${[...向きを選べる図種].join(" / ")} です`,
2994
- });
2995
- return;
2996
- }
2997
- if (!縦列より向きが勝つ図種.has(doc.type) && 書いた縦列に置く(doc.type, doc)) {
2998
- onNotice({
2999
- kind: "direction-not-honored",
3000
- actor: doc.title,
3001
- line: 行,
3002
- message: "書いた direction は効きません (全ての箱が縦列を書いているので、そちらが優先されます)",
3003
- hint: "direction で並べたい時は箱の `lane` を外してください",
3004
- });
3005
- return;
3006
- }
3007
- const 既定 = 既定の向き(doc.type);
3008
- if (doc.direction === 既定 && 向きを外しても同じ経路か(doc)) {
3009
- onNotice({
3010
- kind: "direction-same-as-default",
3011
- actor: doc.title,
3012
- line: 行,
3013
- message: `書いた direction は図を変えません (${既定} は type: ${doc.type} の既定で、この図は向きの行を外しても同じ並びになります)`,
3014
- hint: `並びを変えるなら ${既定 === "縦" ? "横" : "縦"} を書いてください。 今の並びのままにするなら direction の行は外せます`,
3015
- });
3016
- }
3017
- }
3018
-
3019
- /**
3020
- * 書いた並び順が効かない図種に書かれた時に伝える (#2655)。
3021
- *
3022
- * 向きの知らせと同じ形にする。 黙って捨てると「書いたのに並びが変わらない」 が
3023
- * 手掛かりなしで起きる。
3024
- *
3025
- * **既定と同じ値の知らせは持たない** = 並び順は書かなければ書いた順で、`種類` はそれと
3026
- * 必ず違う結果になる (同じ並びになるのは箱が種類の順に書かれていた時だけで、それは
3027
- * 図ごとに変わる)。 向きのように「既定と同じ語」 が存在しない。
3028
- */
3029
- function reportOrderNotHonored(doc: DslDocument, onNotice?: (n: CompileNotice) => void): void {
3030
- if (!onNotice) return;
3031
- if (doc.order === undefined) return;
3032
- if (並び順を選べる図種.has(doc.type)) return;
3033
- onNotice({
3034
- kind: "order-not-honored",
3035
- actor: doc.title,
3036
- line: doc.orderPos?.line ?? doc.pos?.line ?? 0,
3037
- message: `書いた order は効きません (type: ${doc.type} は並び方そのものが読み方を決めます)`,
3038
- hint: `order を書けるのは ${[...並び順を選べる図種].join(" / ")} です`,
3039
- });
3040
- }
3041
-
3042
- /**
3043
- * 書いた形が効かない図種に書かれた時に伝える (#2657)。
3044
- *
3045
- * 並べ替えの知らせと同じ形にする。 黙って捨てると「書いたのに形が変わらない」 が
3046
- * 手掛かりなしで起きる。
3047
- *
3048
- * 書ける形は `DIAGRAM_SHAPES` が図種ごとに決める。 別の図種の語は解析後も残し、ここで
3049
- * 「読めたがこの図種では効かない」 と伝える。
3050
- */
3051
- function reportShapeNotHonored(doc: DslDocument, onNotice?: (n: CompileNotice) => void): void {
3052
- if (!onNotice) return;
3053
- if (doc.shape === undefined) return;
3054
- const 図種の語 = shapesForDiagramType(doc.type);
3055
- if (図種の語.includes(doc.shape)) return;
3056
- const hint =
3057
- 図種の語.length > 0
3058
- ? `type: ${doc.type} で書ける shape = ${図種の語.join(" / ")}`
3059
- : `shape を書けるのは ${Object.entries(DIAGRAM_SHAPES)
3060
- .map(([type, shapes]) => `type: ${type} (${shapes.join(" / ")})`)
3061
- .join(" / ")} です`;
3062
- onNotice({
3063
- kind: "shape-not-honored",
3064
- actor: doc.title,
3065
- line: doc.shapePos?.line ?? doc.pos?.line ?? 0,
3066
- message: `書いた shape: ${doc.shape} は効きません (type: ${doc.type} では使えません)`,
3067
- hint,
3068
- });
3069
- }
3070
-
3071
- /** `form` は円グラフだけ、`ticks` はガントチャートだけが読む。 */
3072
- function reportFormNotHonored(doc: DslDocument, onNotice?: (n: CompileNotice) => void): void {
3073
- if (doc.form === undefined || (doc.type === "chart" && doc.shape === "pie")) return;
3074
- onNotice?.({
3075
- kind: "chart-value-unreadable",
3076
- actor: doc.title,
3077
- line: doc.formPos?.line ?? 0,
3078
- message: `form (見せ方) は shape: pie にだけ効きます (type: ${doc.type} では無視しました)`,
3079
- });
3080
- }
3081
-
3082
- function reportTicksNotHonored(doc: DslDocument, onNotice?: (n: CompileNotice) => void): void {
3083
- if (doc.type === "gantt") return;
3084
- const fields = [
3085
- ...(doc.ticks !== undefined ? ["ticks (目盛り)"] : []),
3086
- ...(doc.ganttToday !== undefined ? ["ganttToday"] : []),
3087
- ...(doc.ganttTickLabels !== undefined ? ["ganttTickLabels"] : []),
3088
- ...(doc.ganttBarEnd !== undefined ? ["ganttBarEnd"] : []),
3089
- ...(doc.ganttBarThickness !== undefined ? ["ganttBarThickness"] : []),
3090
- ];
3091
- if (fields.length === 0) return;
3092
- onNotice?.({
3093
- kind: "chart-value-unreadable",
3094
- actor: doc.title,
3095
- line: doc.ticksPos?.line ?? 0,
3096
- message: `${fields.join(" / ")} は type: gantt にだけ効きます (type: ${doc.type} では無視しました)`,
3097
- });
3098
- }
3099
-
3100
- /** 特別な泳路配置が元の lane を組み直す時、補足を黙って捨てないために伝える。 */
3101
- function reportLaneSubtitleNotHonored(
3102
- doc: DslDocument,
3103
- onNotice?: (n: CompileNotice) => void,
3104
- ): void {
3105
- if (
3106
- !onNotice ||
3107
- doc.type !== "swimlane" ||
3108
- (doc.shape !== "stages" && doc.shape !== "timeline")
3109
- ) return;
3110
- for (const lane of Object.values(doc.lanes ?? {})) {
3111
- if (lane.subtitle === undefined) continue;
3112
- onNotice({
3113
- kind: "lane-option-not-honored",
3114
- actor: lane.id,
3115
- line: lane.pos.line,
3116
- message: `縦列 "${truncateForMessage(lane.id)}" に書いた subtitle は shape: ${doc.shape} では効きません`,
3117
- hint: "subtitle を消すか、通常の縦列または shape: metro の路線図で使ってください",
3118
- });
3119
- }
3120
- }
3121
-
3122
- /** `stage` を省いた箱で担当または箱名を段階に使ったことを、図全体で 1 件だけ伝える (#2797)。 */
3123
- function reportStageFromLane(doc: DslDocument, onNotice?: (n: CompileNotice) => void): void {
3124
- if (!onNotice || doc.type !== "swimlane" || doc.shape !== "stages") return;
3125
- const 補った = doc.actors.filter(
3126
- (actor) => actor.stage === undefined && 泳法図で印をどう描く(doc, actor) !== "省く",
3127
- );
3128
- const 最初 = 補った[0];
3129
- if (最初 === undefined) return;
3130
- onNotice({
3131
- kind: "stage-from-lane",
3132
- actor: doc.title,
3133
- line: 最初.pos?.line ?? doc.pos?.line ?? 0,
3134
- message: `段階を書いていない箱は担当または箱の名前を段階として使いました: ${補った
3135
- .map((actor) => truncateForMessage(actor.name))
3136
- .join(" / ")}`,
3137
- hint: "段階を分けて決めるなら、各箱に stage または 段階 を書いてください",
3138
- });
3139
- }
3140
-
3141
- /** `lane` を省いた箱で箱名を線路名に使ったことを、図全体で 1 件だけ伝える (#2799)。 */
3142
- function reportTrackFromName(doc: DslDocument, onNotice?: (n: CompileNotice) => void): void {
3143
- if (!onNotice || doc.type !== "swimlane" || doc.shape !== "metro") return;
3144
- const 補った = doc.actors.filter((actor) => actor.lane === undefined);
3145
- const 最初 = 補った[0];
3146
- if (最初 === undefined) return;
3147
- onNotice({
3148
- kind: "track-from-name",
3149
- actor: doc.title,
3150
- line: 最初.pos?.line ?? doc.pos?.line ?? 0,
3151
- message: `担当を書いていない箱は箱の名前を線路に使いました: ${補った
3152
- .map((actor) => truncateForMessage(actor.name))
3153
- .join(" / ")}`,
3154
- hint: "線路を担当でまとめるなら、各箱に lane を書いてください",
3155
- });
3156
- }
3157
-
3158
- /** `theme:` と別名の `palette:` を両方書いた時に、消す行を 1 件だけ伝える (#2790)。 */
3159
- function reportThemeAlsoPalette(doc: DslDocument, onNotice?: (n: CompileNotice) => void): void {
3160
- if (!onNotice || doc.themeAlsoPalettePos === undefined || doc.theme === undefined) return;
3161
- onNotice({
3162
- kind: "theme-palette-both",
3163
- actor: doc.title,
3164
- line: doc.themeAlsoPalettePos.paletteLine,
3165
- message: `theme: と palette: を両方書きました。 palette: は theme: の別名なので theme: の値 (${doc.theme}) を使います`,
3166
- hint: "palette: の行を消してください",
3167
- });
3168
- }