@cardenelabs/dragon 0.27.0 → 0.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +119 -12
- package/dist/index.cjs +9619 -8023
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +407 -175
- package/dist/index.d.ts +407 -175
- package/dist/index.js +9617 -8024
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/compile/actor-ids.ts +306 -0
- package/src/compile/actors.ts +74 -0
- package/src/compile/c4.ts +77 -0
- package/src/compile/chain.ts +54 -0
- package/src/compile/chart-fields.ts +178 -0
- package/src/compile/class.ts +100 -0
- package/src/compile/deep-rewrite.ts +30 -0
- package/src/compile/derived-source.ts +18 -0
- package/src/compile/direction.ts +63 -0
- package/src/compile/er-relation.ts +200 -0
- package/src/compile/er.ts +44 -0
- package/src/compile/flow.ts +54 -0
- package/src/compile/funnel.ts +75 -0
- package/src/compile/gantt.ts +232 -0
- package/src/compile/generic.ts +284 -0
- package/src/compile/hierarchy.ts +92 -0
- package/src/compile/journey.ts +84 -0
- package/src/compile/kinds.ts +156 -0
- package/src/compile/lanes.ts +56 -0
- package/src/compile/mind.ts +343 -0
- package/src/compile/node-title.ts +24 -0
- package/src/compile/notice.ts +94 -0
- package/src/compile/parts-lookup.ts +17 -0
- package/src/compile/parts.ts +2635 -0
- package/src/compile/placement-apply.ts +673 -0
- package/src/compile/placement.ts +15 -0
- package/src/compile/quadrant.ts +110 -0
- package/src/compile/rows.ts +40 -0
- package/src/compile/sequence.ts +226 -0
- package/src/compile/slug.ts +29 -0
- package/src/compile/solidity.ts +39 -0
- package/src/compile/state.ts +71 -0
- package/src/compile/subtitle.ts +22 -0
- package/src/compile/swimlane.ts +83 -0
- package/src/compile/topology.ts +50 -0
- package/src/compile/tree.ts +60 -0
- package/src/compile/value-chart.ts +183 -0
- package/src/compile/values.ts +432 -0
- package/src/compile/word-state.ts +82 -0
- package/src/compile.ts +596 -5672
- package/src/index.ts +13 -10
- package/src/json-line-map.ts +30 -0
- package/src/json-parser.ts +509 -176
- package/src/near-name.ts +42 -0
- package/src/notation-lint.ts +112 -74
- package/src/relative-pos.ts +71 -2
- package/src/schema.ts +1 -1
- package/src/schemas/diagram.json +130 -34
- package/src/types.ts +60 -41
- package/src/v05/input-table.generated.ts +6 -4
- package/src/v05/parser-types.ts +12 -1
- package/src/v05/parser.ts +503 -149
package/src/json-parser.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* で確実に生成できるよう、 flat な object array を優先した shape になっている。
|
|
7
7
|
*
|
|
8
8
|
* 使い方 (LLM):
|
|
9
|
-
* 1. `packages/dragon/schemas/diagram.json` の JSON Schema を LLM の tool schema に注入
|
|
9
|
+
* 1. `packages/dragon/src/schemas/diagram.json` の JSON Schema を LLM の tool schema に注入
|
|
10
10
|
* 2. LLM が JSON を返す
|
|
11
11
|
* 3. `jsonToDiagram(json)` で CdlDiagram に変換
|
|
12
12
|
* 4. validation error は throw、 retry loop で LLM に修正させる
|
|
@@ -34,6 +34,8 @@ import {
|
|
|
34
34
|
部品の組の表,
|
|
35
35
|
つまみの表,
|
|
36
36
|
EVENT_KINDS,
|
|
37
|
+
EVENT_TARGET_KEYS,
|
|
38
|
+
EVENT_REQUIRED_KEYS,
|
|
37
39
|
type 図形の定義,
|
|
38
40
|
} from "./v05/parser";
|
|
39
41
|
// 図の配色 (#1553)。 記法の読み手と同じ解決を通す = 別名 (`生成り` / `青磁`) の受け方が
|
|
@@ -49,6 +51,7 @@ import type {
|
|
|
49
51
|
EdgeHead,
|
|
50
52
|
EdgeHeadFill,
|
|
51
53
|
EdgeReveal,
|
|
54
|
+
RelationFocus,
|
|
52
55
|
ClassRelationType,
|
|
53
56
|
SequenceMessageKind,
|
|
54
57
|
} from "@cardenelabs/cdl";
|
|
@@ -70,7 +73,15 @@ import type {
|
|
|
70
73
|
DslDynShape,
|
|
71
74
|
} from "./types";
|
|
72
75
|
import { checkValueExpression, isValueName, valueNameIssue } from "./value-syntax";
|
|
76
|
+
import { 近い名前 } from "./near-name";
|
|
77
|
+
import { 書いた場所の鍵, type 書いた行の表 } from "./json-line-map";
|
|
73
78
|
import { compileToCdl } from "./compile";
|
|
79
|
+
import {
|
|
80
|
+
RELATIVE_DIRECTIONS,
|
|
81
|
+
findRelativeProblems,
|
|
82
|
+
type RelativeDirection,
|
|
83
|
+
type RelativePos,
|
|
84
|
+
} from "./relative-pos";
|
|
74
85
|
|
|
75
86
|
/**
|
|
76
87
|
* LLM 向け JSON DSL の入力 shape。 YAML DSL と 1:1 対応、 top-level は flat な object。
|
|
@@ -117,8 +128,11 @@ export interface DragonJson {
|
|
|
117
128
|
* つまみの値から決まる値 (optional、 #1391)。 記法の最上位 `formulas:` と同じ。
|
|
118
129
|
*
|
|
119
130
|
* 形は `{ 名前: "式" }`。 `values:` と経路が別で、式は名前を中括弧で囲わない。
|
|
131
|
+
*
|
|
132
|
+
* 名札を付ける時は `{ 名前: { expression: "式", label: "名札" } }` の組で書く (#1916)。
|
|
133
|
+
* 描画側は名札を操作部の式の名前に描く (cdl#853)。
|
|
120
134
|
*/
|
|
121
|
-
formulas?: Record<string, string>;
|
|
135
|
+
formulas?: Record<string, string | { expression: string; label?: string }>;
|
|
122
136
|
/**
|
|
123
137
|
* 押下などの出来事で動く仕掛け (optional、 #1393)。 記法の最上位 `events:` と同じ。
|
|
124
138
|
*
|
|
@@ -173,8 +187,8 @@ export interface DragonJson {
|
|
|
173
187
|
contain?: boolean;
|
|
174
188
|
lifeline?: boolean;
|
|
175
189
|
/**
|
|
176
|
-
*
|
|
177
|
-
*
|
|
190
|
+
* 縦列の位置のずらし (#1971)。 配置後の縦列の左上を (x, y) だけ動かす。 記法の `offsetX` / `offsetY`
|
|
191
|
+
* と同じで、読み方は `DslLane.layoutPos` が持つ。
|
|
178
192
|
*/
|
|
179
193
|
pos?: LayoutPos;
|
|
180
194
|
}
|
|
@@ -191,6 +205,8 @@ export interface DragonJson {
|
|
|
191
205
|
bands?: { actor: string; from: number; to: number }[];
|
|
192
206
|
/** 矢印をいつ出すか (#1470)。 記法の最上位 `reveal:` と同じ */
|
|
193
207
|
reveal?: EdgeReveal;
|
|
208
|
+
/** 箱に触れると関係する線だけを光らせるか (#1757)。 記法の最上位 `relations:` と同じ */
|
|
209
|
+
relations?: RelationFocus;
|
|
194
210
|
/** 図の並ぶ向き (#1494)。 記法の最上位 `direction:` と同じ。 JSON は英語の語で書く */
|
|
195
211
|
direction?: "vertical" | "horizontal";
|
|
196
212
|
/**
|
|
@@ -205,6 +221,24 @@ export interface DragonJson {
|
|
|
205
221
|
palette?: DslPalette;
|
|
206
222
|
}
|
|
207
223
|
|
|
224
|
+
/**
|
|
225
|
+
* 位置を他の要素からの相対で書く指定の、JSON 側の形 (#2039)。
|
|
226
|
+
*
|
|
227
|
+
* 記法の `位置: Web の右 200` を 3 つの欄に開いたもの。 内側の値は `RelativePos` と同じで、
|
|
228
|
+
* 向きの語も同じ 4 つ (`right` / `left` / `above` / `below`) を使う。
|
|
229
|
+
*
|
|
230
|
+
* **向きの語を並べ直さない**。 `RelativeDirection` をそのまま使うことで、本文側に向きを
|
|
231
|
+
* 足した時に JSON 側が黙って取り残される形を作らない。
|
|
232
|
+
*/
|
|
233
|
+
export interface JsonRelativePos {
|
|
234
|
+
/** 基準にする相手の名前。 `actors:` に書かれた名前をそのまま書く */
|
|
235
|
+
anchor: string;
|
|
236
|
+
/** 基準からどちら側に置くか */
|
|
237
|
+
dir: RelativeDirection;
|
|
238
|
+
/** 相手との間隔。 書かなければ `RELATIVE_GAP_DEFAULT` */
|
|
239
|
+
gap?: number;
|
|
240
|
+
}
|
|
241
|
+
|
|
208
242
|
export interface JsonActor {
|
|
209
243
|
name: string;
|
|
210
244
|
/**
|
|
@@ -233,8 +267,9 @@ export interface JsonActor {
|
|
|
233
267
|
renderOffsetX?: number | string;
|
|
234
268
|
renderOffsetY?: number | string;
|
|
235
269
|
/**
|
|
236
|
-
* CAR-1657 unified syntax = 既存 NodeKind (
|
|
237
|
-
*
|
|
270
|
+
* CAR-1657 unified syntax = 既存 NodeKind (一覧は `v05/parser.ts` の `NODE_KIND_VALID`) に
|
|
271
|
+
* 加えて parts identifier (arc-gauge 等) を accept する。
|
|
272
|
+
* 未知 kind 値は parts 候補として partId に格納、 compile 側 partsCatalog で解決。
|
|
238
273
|
* LLM structured output の typing 制約を緩めるため union に string 追加。
|
|
239
274
|
* `string & {}` = NodeKind の候補を IDE 補完で提示しつつ任意 string も許容する idiom。
|
|
240
275
|
* 素の `NodeKind | string` は no-redundant-type-constituents に抵触し補完も潰れる (#865)。
|
|
@@ -256,10 +291,21 @@ export interface JsonActor {
|
|
|
256
291
|
initial?: boolean;
|
|
257
292
|
final?: boolean;
|
|
258
293
|
/**
|
|
259
|
-
*
|
|
260
|
-
*
|
|
294
|
+
* 箱の位置のずらし (#1971)。 配置後の箱の中心を (x, y) だけ動かす。 記法の `offsetX` / `offsetY`
|
|
295
|
+
* と同じで、読み方は `DslActor.layoutPos` が持つ。
|
|
261
296
|
*/
|
|
262
297
|
pos?: LayoutPos;
|
|
298
|
+
/**
|
|
299
|
+
* 位置を他の要素からの相対で書く指定 (#2039)。 記法の `位置: Web の右 200` と同じ。
|
|
300
|
+
*
|
|
301
|
+
* 記法は 1 本の文字列に「誰の」「どちら側に」「どれだけ離して」 を詰めるが、JSON では
|
|
302
|
+
* 3 つの欄に分ける。 文字列を組み立てて渡す形にすると、書く側が記法の並び順と区切りを
|
|
303
|
+
* 覚える必要が出る = JSON を書くのは主に LLM で、読み取りの規則は欄の名前で示す方が短い。
|
|
304
|
+
*
|
|
305
|
+
* 組み立ての段階で基準の実座標から `posX` / `posY` に直すため、解決した後は座標を直接
|
|
306
|
+
* 書いた時と同じ経路を通る。 `pos` (配置後のずらし幅) とは別の欄。
|
|
307
|
+
*/
|
|
308
|
+
posRel?: JsonRelativePos;
|
|
263
309
|
/**
|
|
264
310
|
* CAR-1657 parts state override (kind = parts identifier 時のみ有効)。
|
|
265
311
|
* LLM JSON DSL では nested 明示 = `{ "state": { "v": 50 } }` が natural、 human 側の
|
|
@@ -312,12 +358,6 @@ export interface JsonActor {
|
|
|
312
358
|
posY?: number;
|
|
313
359
|
posW?: number;
|
|
314
360
|
posH?: number;
|
|
315
|
-
/**
|
|
316
|
-
* 箱の中の要素ごとに位置と大きさを固定する (#1294)。 記法の `nodes:` と同じ。
|
|
317
|
-
*
|
|
318
|
-
* key は箱が作る要素の名前 (`header` / `footer` / `spacer` / `s0` 等)。
|
|
319
|
-
*/
|
|
320
|
-
nodes?: Record<string, JsonActorNodeOverride>;
|
|
321
361
|
/**
|
|
322
362
|
* 見本を何倍で描くか (#1294)。 記法の `scale:` / `倍率:` と同じ。
|
|
323
363
|
*
|
|
@@ -325,6 +365,17 @@ export interface JsonActor {
|
|
|
325
365
|
* 読めない項目名として知らせる)。
|
|
326
366
|
*/
|
|
327
367
|
scale?: number;
|
|
368
|
+
/**
|
|
369
|
+
* 見本が持つ段を使うか (#2125)。 記法の `{ kind: wave-gauge, phase: false }` と同じ。
|
|
370
|
+
*
|
|
371
|
+
* 既定 (書かない) は見本の段を書いた段と番号ごとに並べて再生する。 `false` を書くと
|
|
372
|
+
* 見本の段の中身 (点灯と値の変化) を空にし、値は書いた段だけが動かす。
|
|
373
|
+
*
|
|
374
|
+
* 見本 (parts) にしか効かない。 中身は状態の上書き (`state`) に `phase` として入る =
|
|
375
|
+
* `"state": { "phase": false }` と同じ所へ届く。 記法が中括弧の中に 1 つの名前として
|
|
376
|
+
* 書ける以上、JSON でも同じ名前で書けるようにする (入口で書き方が分かれない)。
|
|
377
|
+
*/
|
|
378
|
+
phase?: boolean;
|
|
328
379
|
/** 行頭の印 (#1466)。 行と対で読む。 語の意味は図の種類が決める */
|
|
329
380
|
marks?: string[];
|
|
330
381
|
}
|
|
@@ -335,14 +386,6 @@ export interface JsonAxes {
|
|
|
335
386
|
y?: { bottom?: string; top?: string };
|
|
336
387
|
}
|
|
337
388
|
|
|
338
|
-
/** 箱の中の要素 1 つ分の位置と大きさ (#1294)。 記法の `nodes: { header: { ... } }` と同じ */
|
|
339
|
-
export interface JsonActorNodeOverride {
|
|
340
|
-
posX?: number;
|
|
341
|
-
posY?: number;
|
|
342
|
-
posW?: number;
|
|
343
|
-
posH?: number;
|
|
344
|
-
}
|
|
345
|
-
|
|
346
389
|
export interface JsonStep {
|
|
347
390
|
from: string;
|
|
348
391
|
to: string;
|
|
@@ -353,7 +396,7 @@ export interface JsonStep {
|
|
|
353
396
|
/**
|
|
354
397
|
* 矢印の先の形 (#1462)。 記法の `{ head: triangle }` と同じ。
|
|
355
398
|
*
|
|
356
|
-
* 三角 (
|
|
399
|
+
* 三角 (継承・実装) / 菱 (集約・コンポジション) / 開いた矢 (関連・依存) / 鳥の足 (多)。
|
|
357
400
|
* 書かなければ従来どおり塗った三角になる。
|
|
358
401
|
*/
|
|
359
402
|
head?: EdgeHead;
|
|
@@ -361,6 +404,8 @@ export interface JsonStep {
|
|
|
361
404
|
tailHead?: EdgeHead;
|
|
362
405
|
headFill?: EdgeHeadFill;
|
|
363
406
|
tailHeadFill?: EdgeHeadFill;
|
|
407
|
+
/** クラス図の出どころ側の多重度 (#1771)。 記法の `{ tailSub: "1" }` と同じ */
|
|
408
|
+
tailSub?: string;
|
|
364
409
|
/**
|
|
365
410
|
* 辺の役目 (cdl#618)。 記法の `{ role: main }` と同じ。
|
|
366
411
|
*
|
|
@@ -394,10 +439,15 @@ export interface JsonStep {
|
|
|
394
439
|
/** true で説明文を矢印の線の上に重ねる。 分岐図の条件ラベル用。 */
|
|
395
440
|
overlay?: boolean;
|
|
396
441
|
/**
|
|
397
|
-
*
|
|
398
|
-
* backward compat、 set 済は Phase 2 の applyPosOffset で edge label 位置を shift する。
|
|
442
|
+
* 矢印の位置のずらし (#1971)。 名前のずらし (`labelOffsetX` / `labelOffsetY`) に足して名前を動かす。
|
|
399
443
|
*/
|
|
400
444
|
pos?: LayoutPos;
|
|
445
|
+
/**
|
|
446
|
+
* 部品の端で矢印を繋ぐ部品の中の要素の id (#1979)。 記法の `{ fromPartNode: rC }` / `{ toPartNode: gC }` と同じ。
|
|
447
|
+
* 要素を 1 つだけ持つ部品は書かなくてもその要素に繋ぐ
|
|
448
|
+
*/
|
|
449
|
+
fromPartNode?: string;
|
|
450
|
+
toPartNode?: string;
|
|
401
451
|
}
|
|
402
452
|
|
|
403
453
|
export interface JsonPhase {
|
|
@@ -452,11 +502,13 @@ export interface JsonDslError {
|
|
|
452
502
|
* 受け付ける種類 (`kind`)。 **記法側と同じ集合を使う** (`v05/parser.ts` の `NODE_KIND_VALID`)。
|
|
453
503
|
*
|
|
454
504
|
* ここに無い値は見本 (parts) の名前とみなし、`partId` へ退避して箱を `actor` に倒す
|
|
455
|
-
* (CAR-1657 の unified syntax)。
|
|
505
|
+
* (CAR-1657 の unified syntax)。 つまりこの集合が小さいほど、**書いた種類が箱の見た目に効かなくなる**。
|
|
506
|
+
* 部品を持つ一覧を渡した組み立てでは一覧に無い名前を知らせる (`part-not-found`、#2113) が、
|
|
507
|
+
* 一覧を渡さない組み立てと部品を持たない一覧の組み立てでは `console.warn` にしか残らない。
|
|
456
508
|
*
|
|
457
509
|
* 以前はここに一覧を写していたため、記法に種類が増えても JSON 経路が古い一覧のまま
|
|
458
510
|
* 見本の名前として扱った (実測 = 記法が知る 108 種のうち 77 種が該当、`shape-*` は全滅)。
|
|
459
|
-
*
|
|
511
|
+
* 当時はカタログに無い名前が `console.warn` にしか残らず `onNotice` を呼ばなかったため、
|
|
460
512
|
* 画面には何も出ないまま `actor` の箱が描かれていた (#1293)。
|
|
461
513
|
*
|
|
462
514
|
* 型が 2 箇所に散らばると、必ずどちらかが古くなる。 `VALID_PRESETS` (図種) と同じ扱いにする。
|
|
@@ -511,6 +563,8 @@ export const ACCEPTED_KEYS = {
|
|
|
511
563
|
"bands",
|
|
512
564
|
// 矢印をいつ出すか (#1470)
|
|
513
565
|
"reveal",
|
|
566
|
+
// 箱に触れると関係する線だけを光らせるか (#1757)
|
|
567
|
+
"relations",
|
|
514
568
|
// 図の並ぶ向き (#1494)
|
|
515
569
|
"direction",
|
|
516
570
|
// 図の配色 (#1553)
|
|
@@ -539,7 +593,6 @@ export const ACCEPTED_KEYS = {
|
|
|
539
593
|
"posY",
|
|
540
594
|
"posW",
|
|
541
595
|
"posH",
|
|
542
|
-
"nodes",
|
|
543
596
|
"scale",
|
|
544
597
|
"state",
|
|
545
598
|
"pos",
|
|
@@ -557,6 +610,10 @@ export const ACCEPTED_KEYS = {
|
|
|
557
610
|
"renderOffsetY",
|
|
558
611
|
// 行頭の印 (#1466)。 行ごとに 1 つ、図の種類ごとの語で書く
|
|
559
612
|
"marks",
|
|
613
|
+
// 位置を他の要素からの相対で書く指定 (#2039)
|
|
614
|
+
"posRel",
|
|
615
|
+
// 見本が持つ段を使うか (#2125)
|
|
616
|
+
"phase",
|
|
560
617
|
],
|
|
561
618
|
step: [
|
|
562
619
|
"from",
|
|
@@ -571,6 +628,8 @@ export const ACCEPTED_KEYS = {
|
|
|
571
628
|
"tailHead",
|
|
572
629
|
"headFill",
|
|
573
630
|
"tailHeadFill",
|
|
631
|
+
// クラス図の出どころ側の多重度 (#1771)
|
|
632
|
+
"tailSub",
|
|
574
633
|
"relation",
|
|
575
634
|
// 辺の役目と名前の下地 (cdl#618)
|
|
576
635
|
"role",
|
|
@@ -588,16 +647,20 @@ export const ACCEPTED_KEYS = {
|
|
|
588
647
|
"labelOffsetY",
|
|
589
648
|
"overlay",
|
|
590
649
|
"pos",
|
|
650
|
+
// 部品の端で繋ぐ要素の id (#1979)
|
|
651
|
+
"fromPartNode",
|
|
652
|
+
"toPartNode",
|
|
591
653
|
],
|
|
592
654
|
phase: ["step", "duration", "focus", "body", "badge", "tween", "set", "draw", "drawRatio"],
|
|
593
655
|
viewport: ["width", "height", "scale", "laneWidth", "gap", "laneGap", "nodeGap", "labelMargin"],
|
|
594
656
|
lane: ["x", "width", "label", "contain", "lifeline", "pos"],
|
|
595
657
|
group: ["label", "lanes"],
|
|
596
|
-
actorNode: ["posX", "posY", "posW", "posH"],
|
|
597
658
|
axes: ["x", "y"],
|
|
598
659
|
axesX: ["left", "right"],
|
|
599
660
|
axesY: ["bottom", "top"],
|
|
600
661
|
layoutPos: ["x", "y"],
|
|
662
|
+
// 位置を他の要素からの相対で書く指定 (#2039)。 間隔は書かなくてよい
|
|
663
|
+
posRel: ["anchor", "dir", "gap"],
|
|
601
664
|
} as const satisfies Record<string, readonly string[]>;
|
|
602
665
|
|
|
603
666
|
/** 受ける項目を持つ階層の名前 */
|
|
@@ -639,6 +702,8 @@ export type 欄の型 =
|
|
|
639
702
|
| "線種"
|
|
640
703
|
| "辺"
|
|
641
704
|
| "端の形"
|
|
705
|
+
// 相対で置く時の向き (#2039)。 書けば必ず要る = 向きが無いとどちら側か決まらない
|
|
706
|
+
| "必須の向き"
|
|
642
707
|
| "色か色番号"
|
|
643
708
|
| "描くもの"
|
|
644
709
|
| "必須の図種"
|
|
@@ -674,6 +739,8 @@ export const 欄の型表 = {
|
|
|
674
739
|
bands: "並び",
|
|
675
740
|
// 矢印をいつ出すか (#1470)
|
|
676
741
|
reveal: "非空の文字列",
|
|
742
|
+
// 箱に触れると関係する線だけを光らせるか (#1757)
|
|
743
|
+
relations: "非空の文字列",
|
|
677
744
|
// 図の並ぶ向き (#1494)
|
|
678
745
|
direction: "非空の文字列",
|
|
679
746
|
// 図の配色 (#1553)
|
|
@@ -701,7 +768,6 @@ export const 欄の型表 = {
|
|
|
701
768
|
posY: "数",
|
|
702
769
|
posW: "数",
|
|
703
770
|
posH: "数",
|
|
704
|
-
nodes: "object",
|
|
705
771
|
scale: "数",
|
|
706
772
|
state: "object",
|
|
707
773
|
pos: "object",
|
|
@@ -719,6 +785,10 @@ export const 欄の型表 = {
|
|
|
719
785
|
renderOffsetY: "数か文字列",
|
|
720
786
|
// 行頭の印 (#1466)
|
|
721
787
|
marks: "文字列の並び",
|
|
788
|
+
// 相対で置く指定 (#2039)。 中身の形は `validateRelativePos` が見る
|
|
789
|
+
posRel: "object",
|
|
790
|
+
// 見本が持つ段を使うか (#2125)
|
|
791
|
+
phase: "真偽",
|
|
722
792
|
},
|
|
723
793
|
step: {
|
|
724
794
|
from: "必須の文字列",
|
|
@@ -732,6 +802,8 @@ export const 欄の型表 = {
|
|
|
732
802
|
tailHead: "端の形",
|
|
733
803
|
headFill: "非空の文字列",
|
|
734
804
|
tailHeadFill: "非空の文字列",
|
|
805
|
+
// クラス図の出どころ側の多重度 (#1771)
|
|
806
|
+
tailSub: "文字列",
|
|
735
807
|
relation: "非空の文字列",
|
|
736
808
|
// 辺の役目と名前の下地 (cdl#618)
|
|
737
809
|
role: "非空の文字列",
|
|
@@ -749,6 +821,9 @@ export const 欄の型表 = {
|
|
|
749
821
|
labelOffsetY: "数",
|
|
750
822
|
overlay: "真偽",
|
|
751
823
|
pos: "object",
|
|
824
|
+
// 部品の端で繋ぐ要素の id (#1979)。 空の id は要素を指せないので受けない
|
|
825
|
+
fromPartNode: "非空の文字列",
|
|
826
|
+
toPartNode: "非空の文字列",
|
|
752
827
|
},
|
|
753
828
|
phase: {
|
|
754
829
|
step: "必須の非空文字列",
|
|
@@ -780,12 +855,13 @@ export const 欄の型表 = {
|
|
|
780
855
|
pos: "object",
|
|
781
856
|
},
|
|
782
857
|
group: { label: "文字列", lanes: "文字列の並び" },
|
|
783
|
-
actorNode: { posX: "数", posY: "数", posW: "数", posH: "数" },
|
|
784
858
|
axes: { x: "object", y: "object" },
|
|
785
859
|
axesX: { left: "文字列", right: "文字列" },
|
|
786
860
|
axesY: { bottom: "文字列", top: "文字列" },
|
|
787
861
|
// 位置は書けば x と y の両方が要る。 片方だけでは寄せ幅が決まらない
|
|
788
862
|
layoutPos: { x: "必須の数", y: "必須の数" },
|
|
863
|
+
// 相対で置く指定 (#2039)。 相手と向きは必ず要り、間隔は書かなければ既定で置く
|
|
864
|
+
posRel: { anchor: "必須の非空文字列", dir: "必須の向き", gap: "数" },
|
|
789
865
|
} as const satisfies {
|
|
790
866
|
[層 in 階層]: { [欄 in (typeof ACCEPTED_KEYS)[層][number]]: 欄の型 };
|
|
791
867
|
};
|
|
@@ -830,6 +906,8 @@ function JSONの色名か(v: string): boolean {
|
|
|
830
906
|
export const 見本にしか効かない欄 = [
|
|
831
907
|
"state",
|
|
832
908
|
"scale",
|
|
909
|
+
// 見本が持つ段を使うか (#2125)。 普通の箱は自分の段を持たないので効かない
|
|
910
|
+
"phase",
|
|
833
911
|
] as const satisfies readonly (typeof ACCEPTED_KEYS.actor)[number][];
|
|
834
912
|
|
|
835
913
|
/**
|
|
@@ -988,6 +1066,17 @@ function 値を検査(
|
|
|
988
1066
|
});
|
|
989
1067
|
}
|
|
990
1068
|
return;
|
|
1069
|
+
case "必須の向き":
|
|
1070
|
+
// 受ける語は記法と同じ一覧を見る (`RELATIVE_DIRECTIONS`)。 写すと向きが増えた時に
|
|
1071
|
+
// 片方だけ古くなる
|
|
1072
|
+
if (typeof v !== "string" || !(RELATIVE_DIRECTIONS as readonly string[]).includes(v)) {
|
|
1073
|
+
errors.push({
|
|
1074
|
+
path,
|
|
1075
|
+
message: `${名前} must be one of: ${RELATIVE_DIRECTIONS.join(", ")}`,
|
|
1076
|
+
hint: typeof v === "string" ? `got "${v}"` : `got ${v === undefined ? "nothing" : typeof v}`,
|
|
1077
|
+
});
|
|
1078
|
+
}
|
|
1079
|
+
return;
|
|
991
1080
|
case "端の形":
|
|
992
1081
|
if (v === undefined) return;
|
|
993
1082
|
// 受ける語は記法と同じ一覧を見る (`EDGE_HEAD_VALUES`)。 写すと描画側が形を増やした時に
|
|
@@ -1077,48 +1166,6 @@ function 表で検査(
|
|
|
1077
1166
|
}
|
|
1078
1167
|
}
|
|
1079
1168
|
|
|
1080
|
-
/**
|
|
1081
|
-
* 綴り違いの候補を返す (#1295)。
|
|
1082
|
-
*
|
|
1083
|
-
* 「知らない項目です」 だけだと、`animations` と書いた人は正しい綴りを探しに行く必要がある。
|
|
1084
|
-
* 1 文字の違い (足りない / 多い / 入れ替わり / 別の字) までを候補とする。
|
|
1085
|
-
*
|
|
1086
|
-
* 遠い名前は勧めない。 無関係な項目名を勧めると、書いた人がそちらへ直して二度手間になる。
|
|
1087
|
-
*/
|
|
1088
|
-
function 近い項目名(key: string, 候補: readonly string[]): string | undefined {
|
|
1089
|
-
const 小文字 = key.toLowerCase();
|
|
1090
|
-
let 最短: { 名: string; 距離: number } | undefined;
|
|
1091
|
-
for (const c of 候補) {
|
|
1092
|
-
const d = 編集距離(小文字, c.toLowerCase(), 2);
|
|
1093
|
-
if (d <= 2 && (最短 === undefined || d < 最短.距離)) 最短 = { 名: c, 距離: d };
|
|
1094
|
-
}
|
|
1095
|
-
return 最短?.名;
|
|
1096
|
-
}
|
|
1097
|
-
|
|
1098
|
-
/**
|
|
1099
|
-
* 2 つの語の編集距離 (上限付き)。
|
|
1100
|
-
*
|
|
1101
|
-
* 上限を持つのは、長い語どうしで表を全部埋めないため。 上限を超えた時点で打ち切る。
|
|
1102
|
-
*/
|
|
1103
|
-
function 編集距離(a: string, b: string, 上限: number): number {
|
|
1104
|
-
if (Math.abs(a.length - b.length) > 上限) return 上限 + 1;
|
|
1105
|
-
let 前 = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
1106
|
-
for (let i = 1; i <= a.length; i += 1) {
|
|
1107
|
-
const 今: number[] = [i];
|
|
1108
|
-
let 行の最小 = i;
|
|
1109
|
-
for (let j = 1; j <= b.length; j += 1) {
|
|
1110
|
-
const 費用 = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
1111
|
-
const v = Math.min(今[j - 1]! + 1, 前[j]! + 1, 前[j - 1]! + 費用);
|
|
1112
|
-
今.push(v);
|
|
1113
|
-
if (v < 行の最小) 行の最小 = v;
|
|
1114
|
-
}
|
|
1115
|
-
// その行の最小が上限を超えたら、以降どう進んでも上限以下にはならない
|
|
1116
|
-
if (行の最小 > 上限) return 上限 + 1;
|
|
1117
|
-
前 = 今;
|
|
1118
|
-
}
|
|
1119
|
-
return 前[b.length]!;
|
|
1120
|
-
}
|
|
1121
|
-
|
|
1122
1169
|
/**
|
|
1123
1170
|
* 知らない項目を誤りとして積む (#1295)。
|
|
1124
1171
|
*
|
|
@@ -1128,9 +1175,9 @@ function 編集距離(a: string, b: string, 上限: number): number {
|
|
|
1128
1175
|
function checkUnknownKeys(v: unknown, 層: 階層, path: string, errors: JsonDslError[]): void {
|
|
1129
1176
|
if (!v || typeof v !== "object" || Array.isArray(v)) return;
|
|
1130
1177
|
const 受ける = ACCEPTED_KEYS[層] as readonly string[];
|
|
1131
|
-
for (const key of Object.keys(v
|
|
1178
|
+
for (const key of Object.keys(v)) {
|
|
1132
1179
|
if (受ける.includes(key)) continue;
|
|
1133
|
-
const 候補 =
|
|
1180
|
+
const 候補 = 近い名前(key, 受ける);
|
|
1134
1181
|
errors.push({
|
|
1135
1182
|
path: `${path}.${key}`,
|
|
1136
1183
|
message: `unknown key "${key}"`,
|
|
@@ -1139,13 +1186,9 @@ function checkUnknownKeys(v: unknown, 層: 階層, path: string, errors: JsonDsl
|
|
|
1139
1186
|
}
|
|
1140
1187
|
}
|
|
1141
1188
|
|
|
1142
|
-
/**
|
|
1143
|
-
* shape validation。 layer 1 = 必須 field + 型 check、 layer 2 は compile 側の validation に委譲。
|
|
1144
|
-
* fail-fast ではなく全 error 収集して返す (LLM に一括で修正させるため)。
|
|
1145
|
-
*/
|
|
1146
1189
|
/**
|
|
1147
1190
|
* CAR-1693 Phase 1: DSL 表面 `pos: {x, y}` の型 check helper。 finite number pair を必須にし、
|
|
1148
|
-
* `NaN` / `Infinity` / non-number は reject する (
|
|
1191
|
+
* `NaN` / `Infinity` / non-number は reject する (組み立ての出口 `applyLayoutOffsets` が配置後の位置に足すため)。
|
|
1149
1192
|
*/
|
|
1150
1193
|
function validateLayoutPos(v: unknown, path: string, errors: JsonDslError[]): void {
|
|
1151
1194
|
if (v === undefined) return;
|
|
@@ -1158,6 +1201,77 @@ function validateLayoutPos(v: unknown, path: string, errors: JsonDslError[]): vo
|
|
|
1158
1201
|
表で検査(p, "layoutPos", path, "pos", errors);
|
|
1159
1202
|
}
|
|
1160
1203
|
|
|
1204
|
+
/**
|
|
1205
|
+
* 相対で置く指定の形を見る (#2039)。
|
|
1206
|
+
*
|
|
1207
|
+
* 相手の名前が実在するかはここでは見ない。 組み立て側 (`resolveRelativeDoc`) が figure 全体を
|
|
1208
|
+
* 組んだ後に照合し、いない相手を指した時は知らせを出す。 入口ごとに別の場所で判定すると、
|
|
1209
|
+
* 本文で書いた時と JSON で書いた時で返るものが変わる。
|
|
1210
|
+
*/
|
|
1211
|
+
function validateRelativePos(v: unknown, path: string, errors: JsonDslError[]): void {
|
|
1212
|
+
if (v === undefined) return;
|
|
1213
|
+
if (!v || typeof v !== "object" || Array.isArray(v)) {
|
|
1214
|
+
errors.push({ path, message: "posRel must be an object with anchor and dir" });
|
|
1215
|
+
return;
|
|
1216
|
+
}
|
|
1217
|
+
const p = v as Record<string, unknown>;
|
|
1218
|
+
checkUnknownKeys(p, "posRel", path, errors);
|
|
1219
|
+
表で検査(p, "posRel", path, "posRel", errors);
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
/**
|
|
1223
|
+
* 図全体として相対の指定が解けるかを見る (#2039)。
|
|
1224
|
+
*
|
|
1225
|
+
* 判定は `relative-pos.ts` の `findRelativeProblems` が持つ。 記法側も同じ判定を呼ぶので、
|
|
1226
|
+
* 同じ書き方が入口によって通ったり通らなかったりしない。
|
|
1227
|
+
*
|
|
1228
|
+
* 誤りの文だけをここで作る = 記法側は行番号を、こちらは欄の場所を添えるため。
|
|
1229
|
+
*/
|
|
1230
|
+
function validateRelativeAnchors(actors: readonly unknown[], errors: JsonDslError[]): void {
|
|
1231
|
+
const items: Array<{ name: string; rel?: RelativePos }> = [];
|
|
1232
|
+
const 場所 = new Map<string, number>();
|
|
1233
|
+
actors.forEach((a, i) => {
|
|
1234
|
+
if (!a || typeof a !== "object" || Array.isArray(a)) return;
|
|
1235
|
+
const o = a as Record<string, unknown>;
|
|
1236
|
+
if (typeof o.name !== "string") return;
|
|
1237
|
+
const rel = o.posRel;
|
|
1238
|
+
// 形の誤りは `validateRelativePos` が既に返している。 ここは形の整った指定だけを見る
|
|
1239
|
+
const 整った =
|
|
1240
|
+
rel !== null &&
|
|
1241
|
+
typeof rel === "object" &&
|
|
1242
|
+
!Array.isArray(rel) &&
|
|
1243
|
+
typeof (rel as Record<string, unknown>).anchor === "string" &&
|
|
1244
|
+
typeof (rel as Record<string, unknown>).dir === "string";
|
|
1245
|
+
items.push({ name: o.name, rel: 整った ? (rel as RelativePos) : undefined });
|
|
1246
|
+
if (!場所.has(o.name)) 場所.set(o.name, i);
|
|
1247
|
+
});
|
|
1248
|
+
|
|
1249
|
+
const named = items.map((i) => i.name);
|
|
1250
|
+
for (const p of findRelativeProblems(items)) {
|
|
1251
|
+
const i = 場所.get(p.name) ?? 0;
|
|
1252
|
+
errors.push({
|
|
1253
|
+
path: `$.actors[${i}].posRel`,
|
|
1254
|
+
...(p.kind === "self"
|
|
1255
|
+
? {
|
|
1256
|
+
message: `posRel.anchor must not be the actor itself: "${p.name}"`,
|
|
1257
|
+
hint: "別の登場人物の名前を書く",
|
|
1258
|
+
}
|
|
1259
|
+
: p.kind === "missing-anchor"
|
|
1260
|
+
? {
|
|
1261
|
+
message: `posRel.anchor must name an actor: "${p.anchor ?? ""}"`,
|
|
1262
|
+
hint:
|
|
1263
|
+
named.length > 0
|
|
1264
|
+
? `actors に書かれている名前 = ${named.join(", ")}`
|
|
1265
|
+
: "actors に基準にする登場人物を書く",
|
|
1266
|
+
}
|
|
1267
|
+
: {
|
|
1268
|
+
message: `posRel.anchor must not form a cycle: "${p.name}"`,
|
|
1269
|
+
hint: "どれか 1 つは posX / posY か自動配置にする",
|
|
1270
|
+
}),
|
|
1271
|
+
});
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1161
1275
|
/**
|
|
1162
1276
|
* 見本 (parts) の名前として扱う値かどうか (#1294)。
|
|
1163
1277
|
*
|
|
@@ -1169,28 +1283,6 @@ function 見本の名前か(kind: unknown): boolean {
|
|
|
1169
1283
|
return kind !== "actor" && !VALID_KIND_SET.has(kind);
|
|
1170
1284
|
}
|
|
1171
1285
|
|
|
1172
|
-
/** 箱の中の要素ごとの位置と大きさを見る (#1294) */
|
|
1173
|
-
function validateActorNodes(v: unknown, path: string, errors: JsonDslError[]): void {
|
|
1174
|
-
if (v === undefined) return;
|
|
1175
|
-
if (!v || typeof v !== "object" || Array.isArray(v)) {
|
|
1176
|
-
errors.push({
|
|
1177
|
-
path,
|
|
1178
|
-
message: "actor.nodes must be a plain object of name -> { posX, posY, posW, posH }",
|
|
1179
|
-
});
|
|
1180
|
-
return;
|
|
1181
|
-
}
|
|
1182
|
-
for (const [name, o] of Object.entries(v as Record<string, unknown>)) {
|
|
1183
|
-
const nodePath = `${path}.${name}`;
|
|
1184
|
-
if (!o || typeof o !== "object" || Array.isArray(o)) {
|
|
1185
|
-
errors.push({ path: nodePath, message: "actor.nodes entry must be a plain object" });
|
|
1186
|
-
continue;
|
|
1187
|
-
}
|
|
1188
|
-
const n = o as Record<string, unknown>;
|
|
1189
|
-
checkUnknownKeys(n, "actorNode", nodePath, errors);
|
|
1190
|
-
表で検査(n, "actorNode", nodePath, `nodes.${name}`, errors);
|
|
1191
|
-
}
|
|
1192
|
-
}
|
|
1193
|
-
|
|
1194
1286
|
/**
|
|
1195
1287
|
* 図全体の大きさと間隔を見る (#1295)。
|
|
1196
1288
|
*
|
|
@@ -1209,14 +1301,69 @@ function validateViewport(v: unknown, errors: JsonDslError[]): void {
|
|
|
1209
1301
|
}
|
|
1210
1302
|
|
|
1211
1303
|
/**
|
|
1212
|
-
*
|
|
1304
|
+
* 選択肢の組が受ける項目。 どれも必ず書く (#2258)。
|
|
1213
1305
|
*
|
|
1214
|
-
*
|
|
1215
|
-
*
|
|
1306
|
+
* **判定と知らせが同じ集合を読む**。 別々に並べていた間、判定に項目を足しても
|
|
1307
|
+
* 知らせは古い一覧を教え続ける形だった。
|
|
1308
|
+
*/
|
|
1309
|
+
const 選択肢の項目 = ["value", "label"] as const;
|
|
1310
|
+
|
|
1311
|
+
/**
|
|
1312
|
+
* 選択肢の並び (`["green", { "value": "red", "label": "失敗" }]`) を要素ごとに検査する (#1920)。
|
|
1216
1313
|
*
|
|
1217
|
-
*
|
|
1218
|
-
*
|
|
1314
|
+
* 要素は文字列か、`value` と `label` の 2 項目を持つ組。 誤りは要素の場所 (`options[1].label`) で
|
|
1315
|
+
* 返す = 並び全体の型違いとして返すと、どの組のどの項目を直せばよいかが読み手に届かない。
|
|
1219
1316
|
*/
|
|
1317
|
+
function 選択肢の並びを検査する(値: unknown, path: string, errors: JsonDslError[]): void {
|
|
1318
|
+
if (!Array.isArray(値)) {
|
|
1319
|
+
errors.push({
|
|
1320
|
+
path,
|
|
1321
|
+
message: "options must be 選択肢の並び",
|
|
1322
|
+
hint: `got ${値 === null ? "null" : typeof 値}`,
|
|
1323
|
+
});
|
|
1324
|
+
return;
|
|
1325
|
+
}
|
|
1326
|
+
値.forEach((x, i) => {
|
|
1327
|
+
const here = `${path}[${i}]`;
|
|
1328
|
+
if (typeof x === "string") return;
|
|
1329
|
+
if (typeof x !== "object" || x === null || Array.isArray(x)) {
|
|
1330
|
+
errors.push({
|
|
1331
|
+
path: here,
|
|
1332
|
+
message: "option must be a string or { value, label }",
|
|
1333
|
+
hint: `got ${Array.isArray(x) ? "array" : x === null ? "null" : typeof x}`,
|
|
1334
|
+
});
|
|
1335
|
+
return;
|
|
1336
|
+
}
|
|
1337
|
+
const 組 = x as Record<string, unknown>;
|
|
1338
|
+
for (const k of Object.keys(組)) {
|
|
1339
|
+
if ((選択肢の項目 as readonly string[]).includes(k)) continue;
|
|
1340
|
+
errors.push({
|
|
1341
|
+
path: `${here}.${k}`,
|
|
1342
|
+
message: `unknown key "${k}"`,
|
|
1343
|
+
hint: `使える項目 = ${選択肢の項目.join(", ")}`,
|
|
1344
|
+
});
|
|
1345
|
+
}
|
|
1346
|
+
for (const k of 選択肢の項目) {
|
|
1347
|
+
const v = 組[k];
|
|
1348
|
+
if (v === undefined) {
|
|
1349
|
+
errors.push({
|
|
1350
|
+
path: `${here}.${k}`,
|
|
1351
|
+
message: `${k} is required`,
|
|
1352
|
+
hint: `必須の項目 = ${選択肢の項目.join(", ")}`,
|
|
1353
|
+
});
|
|
1354
|
+
} else if (typeof v !== "string") {
|
|
1355
|
+
errors.push({ path: `${here}.${k}`, message: `${k} must be 文字列`, hint: `got ${v === null ? "null" : typeof v}` });
|
|
1356
|
+
} else if (v === "") {
|
|
1357
|
+
errors.push({
|
|
1358
|
+
path: `${here}.${k}`,
|
|
1359
|
+
message: `${k} must not be empty`,
|
|
1360
|
+
hint: k === "label" ? "名前を書かないなら文字列だけを並べる" : "値を 1 文字以上書く",
|
|
1361
|
+
});
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
});
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1220
1367
|
/**
|
|
1221
1368
|
* 図形と部品の中身を、記法と **同じ表** で検査する (#1374)。
|
|
1222
1369
|
*
|
|
@@ -1258,6 +1405,10 @@ function 表で中身を検査する(
|
|
|
1258
1405
|
});
|
|
1259
1406
|
continue;
|
|
1260
1407
|
}
|
|
1408
|
+
if (形 === "選択肢の並び") {
|
|
1409
|
+
選択肢の並びを検査する(値, `${path}.${欄}`, errors);
|
|
1410
|
+
continue;
|
|
1411
|
+
}
|
|
1261
1412
|
const 型が合う =
|
|
1262
1413
|
形 === "数"
|
|
1263
1414
|
? typeof 値 === "number" && Number.isFinite(値)
|
|
@@ -1305,18 +1456,30 @@ function 表で中身を検査する(
|
|
|
1305
1456
|
}
|
|
1306
1457
|
}
|
|
1307
1458
|
|
|
1459
|
+
/** 帯に書く相手の項目。 値が文字列で、他の 2 つと型が違う */
|
|
1460
|
+
const 帯の相手の項目 = ["actor"] as const;
|
|
1461
|
+
/** 帯に書く段の項目。 値が 0 以上の整数 */
|
|
1462
|
+
const 帯の段の項目 = ["from", "to"] as const;
|
|
1308
1463
|
/**
|
|
1309
|
-
*
|
|
1464
|
+
* 帯が受ける項目。 判定と知らせの両方がここから作る (#2258)。
|
|
1310
1465
|
*
|
|
1311
|
-
*
|
|
1312
|
-
*
|
|
1313
|
-
* 素通りする。
|
|
1466
|
+
* 公開 schema の `$.bands` も同じ 3 語を閉じた集合として宣言しており、
|
|
1467
|
+
* `closed-level-coverage.test.ts` が両者の一致を見る。
|
|
1314
1468
|
*/
|
|
1469
|
+
export const 帯の項目: readonly string[] = [...帯の相手の項目, ...帯の段の項目];
|
|
1470
|
+
|
|
1315
1471
|
/**
|
|
1316
1472
|
* 順序図の帯の並びを検査する (#1466)。
|
|
1317
1473
|
*
|
|
1318
1474
|
* **外側の形もここで見る**。 `欄の型表` は「並び」 とだけ宣言し、中身の検査は専用の検査に
|
|
1319
1475
|
* 委ねる作りなので、ここで見ないと `bands: 1` が素通りする。
|
|
1476
|
+
*
|
|
1477
|
+
* ## 知らない項目も見る (#2262)
|
|
1478
|
+
*
|
|
1479
|
+
* 書いてある 3 つの型だけを見て、**知らない項目があるかを確かめていなかった**。
|
|
1480
|
+
* 公開 schema は `$.bands` を閉じた階層として宣言しているので、
|
|
1481
|
+
* schema を信じて書いた入力がこちらでは通り、schema 検証器では落ちる状態だった。
|
|
1482
|
+
* 公開 schema が閉じた階層のうち、素通りしていたのはここだけだった。
|
|
1320
1483
|
*/
|
|
1321
1484
|
function validateBands(v: unknown, errors: JsonDslError[]): void {
|
|
1322
1485
|
if (v === undefined) return;
|
|
@@ -1335,17 +1498,34 @@ function validateBands(v: unknown, errors: JsonDslError[]): void {
|
|
|
1335
1498
|
return;
|
|
1336
1499
|
}
|
|
1337
1500
|
const o = b as Record<string, unknown>;
|
|
1338
|
-
|
|
1339
|
-
|
|
1501
|
+
for (const k of Object.keys(o)) {
|
|
1502
|
+
if (帯の項目.includes(k)) continue;
|
|
1503
|
+
errors.push({
|
|
1504
|
+
path: `${path}.${k}`,
|
|
1505
|
+
message: `unknown key "${k}"`,
|
|
1506
|
+
hint: `使える項目 = ${帯の項目.join(", ")}`,
|
|
1507
|
+
});
|
|
1340
1508
|
}
|
|
1341
|
-
for (const k of
|
|
1342
|
-
if (typeof o[k] !== "
|
|
1509
|
+
for (const k of 帯の相手の項目) {
|
|
1510
|
+
if (typeof o[k] !== "string" || o[k] === "") {
|
|
1511
|
+
errors.push({ path: `${path}.${k}`, message: `band.${k} must be a non-empty string` });
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
for (const k of 帯の段の項目) {
|
|
1515
|
+
if (typeof o[k] !== "number" || !Number.isInteger(o[k]) || o[k] < 0) {
|
|
1343
1516
|
errors.push({ path: `${path}.${k}`, message: `band.${k} must be a non-negative integer` });
|
|
1344
1517
|
}
|
|
1345
1518
|
}
|
|
1346
1519
|
});
|
|
1347
1520
|
}
|
|
1348
1521
|
|
|
1522
|
+
/**
|
|
1523
|
+
* 値を見せる部品の並びを検査する (#1374)。
|
|
1524
|
+
*
|
|
1525
|
+
* **外側の形もここで見る**。 `欄の型表` は「並び」 とだけ宣言し、中身の検査は専用の検査に
|
|
1526
|
+
* 委ねる作りなので (`checkFieldType` の `case "並び"`)、ここで見ないと `readouts: 1` が
|
|
1527
|
+
* 素通りする。
|
|
1528
|
+
*/
|
|
1349
1529
|
function validateReadouts(v: unknown, errors: JsonDslError[]): void {
|
|
1350
1530
|
if (v === undefined) return;
|
|
1351
1531
|
if (!Array.isArray(v)) {
|
|
@@ -1478,12 +1658,15 @@ function validateFormulas(v: unknown, inputs: unknown, errors: JsonDslError[]):
|
|
|
1478
1658
|
}
|
|
1479
1659
|
}
|
|
1480
1660
|
const 先に書かれた式 = new Set<string>();
|
|
1481
|
-
for (const [名前,
|
|
1661
|
+
for (const [名前, 値] of Object.entries(v as Record<string, unknown>)) {
|
|
1482
1662
|
const path = `$.formulas.${名前}`;
|
|
1483
1663
|
if (!isValueName(名前)) {
|
|
1484
1664
|
errors.push({ path, ...valueNameIssue(名前) });
|
|
1485
1665
|
continue;
|
|
1486
1666
|
}
|
|
1667
|
+
const 検査結果 = 式の組を検査する(値, path, errors);
|
|
1668
|
+
if (!検査結果.読めた) continue;
|
|
1669
|
+
const 式 = 検査結果.値;
|
|
1487
1670
|
if (typeof 式 !== "string" || 式.trim() === "") {
|
|
1488
1671
|
errors.push({
|
|
1489
1672
|
path,
|
|
@@ -1525,6 +1708,60 @@ function validateFormulas(v: unknown, inputs: unknown, errors: JsonDslError[]):
|
|
|
1525
1708
|
}
|
|
1526
1709
|
}
|
|
1527
1710
|
|
|
1711
|
+
/**
|
|
1712
|
+
* 式の組を検査した結果 (#1916 / #2006)。
|
|
1713
|
+
*
|
|
1714
|
+
* **読めたかどうかを別の欄に持つ**。 値そのものに合図を混ぜると、`{ a: null }` の `null` が
|
|
1715
|
+
* 合図と区別が付かず、呼び手が「組の誤りは知らせ済み」 と読んで何も知らせずに通してしまう (実測)。
|
|
1716
|
+
*
|
|
1717
|
+
* 印 (`unique symbol`) を値に混ぜる形も採らない。 返す型が `unknown` を含むと印が飲み込まれ、
|
|
1718
|
+
* 呼び手は型で見分けられないまま等値比較に頼ることになる。
|
|
1719
|
+
*/
|
|
1720
|
+
type 式の検査結果 = { readonly 読めた: true; readonly 値: unknown } | { readonly 読めた: false };
|
|
1721
|
+
|
|
1722
|
+
/**
|
|
1723
|
+
* 式の組が受ける項目 (#2258)。 判定と知らせが同じ集合を読む。
|
|
1724
|
+
*/
|
|
1725
|
+
const 式の組の項目 = ["expression", "label"] as const;
|
|
1726
|
+
|
|
1727
|
+
/**
|
|
1728
|
+
* 式の値が組の形 (`{ expression, label }`) なら組を検査し、式の文字列を返す (#1916)。
|
|
1729
|
+
*
|
|
1730
|
+
* 文字列やそれ以外の形は **そのまま返す** = 式の文字列の検査は呼び手が文字列の形と同じ経路で行う。
|
|
1731
|
+
* 組に誤りがあれば知らせて `読めた: false` を返す。
|
|
1732
|
+
*/
|
|
1733
|
+
function 式の組を検査する(値: unknown, path: string, errors: JsonDslError[]): 式の検査結果 {
|
|
1734
|
+
if (!値 || typeof 値 !== "object" || Array.isArray(値)) return { 読めた: true, 値 };
|
|
1735
|
+
const 組 = 値 as Record<string, unknown>;
|
|
1736
|
+
let 読めた = true;
|
|
1737
|
+
for (const 鍵 of Object.keys(組)) {
|
|
1738
|
+
if ((式の組の項目 as readonly string[]).includes(鍵)) continue;
|
|
1739
|
+
読めた = false;
|
|
1740
|
+
errors.push({
|
|
1741
|
+
path: `${path}.${鍵}`,
|
|
1742
|
+
message: `unknown formula field: ${鍵}`,
|
|
1743
|
+
hint: `使える項目 = ${式の組の項目.join(", ")}`,
|
|
1744
|
+
});
|
|
1745
|
+
}
|
|
1746
|
+
if (typeof 組.expression !== "string" || 組.expression.trim() === "") {
|
|
1747
|
+
読めた = false;
|
|
1748
|
+
errors.push({
|
|
1749
|
+
path: `${path}.expression`,
|
|
1750
|
+
message: "expression must be a non-empty expression string",
|
|
1751
|
+
hint: `got ${組.expression === undefined ? "nothing" : JSON.stringify(組.expression)}`,
|
|
1752
|
+
});
|
|
1753
|
+
}
|
|
1754
|
+
if (組.label !== undefined && (typeof 組.label !== "string" || 組.label.trim() === "")) {
|
|
1755
|
+
読めた = false;
|
|
1756
|
+
errors.push({
|
|
1757
|
+
path: `${path}.label`,
|
|
1758
|
+
message: "label must be a non-empty string",
|
|
1759
|
+
hint: "名札を付けないなら label を書かない",
|
|
1760
|
+
});
|
|
1761
|
+
}
|
|
1762
|
+
return 読めた ? { 読めた: true, 値: 組.expression } : { 読めた: false };
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1528
1765
|
/**
|
|
1529
1766
|
* 押下などの出来事で動く仕掛けを検査する (#1393)。
|
|
1530
1767
|
*
|
|
@@ -1541,7 +1778,11 @@ function validateEvents(v: unknown, errors: JsonDslError[]): void {
|
|
|
1541
1778
|
});
|
|
1542
1779
|
return;
|
|
1543
1780
|
}
|
|
1544
|
-
|
|
1781
|
+
// 相手の指し方は記法の側が 1 か所で決める (#2256)。 同じ 4 語をここにも並べていた間、
|
|
1782
|
+
// 1:1 を名乗りながら受ける項目が両側で別々に決まっていた。
|
|
1783
|
+
// 必ず書く 2 つも同じ定数から取る (#2258) = 知らせが判定と別に並べる形をやめる
|
|
1784
|
+
const 相手の鍵: readonly string[] = EVENT_TARGET_KEYS;
|
|
1785
|
+
const 出来事の項目: readonly string[] = [...EVENT_REQUIRED_KEYS, ...EVENT_TARGET_KEYS];
|
|
1545
1786
|
v.forEach((e, i) => {
|
|
1546
1787
|
const path = `$.events[${i}]`;
|
|
1547
1788
|
if (!e || typeof e !== "object" || Array.isArray(e)) {
|
|
@@ -1550,11 +1791,11 @@ function validateEvents(v: unknown, errors: JsonDslError[]): void {
|
|
|
1550
1791
|
}
|
|
1551
1792
|
const o = e as Record<string, unknown>;
|
|
1552
1793
|
for (const k of Object.keys(o)) {
|
|
1553
|
-
if (
|
|
1794
|
+
if (出来事の項目.includes(k)) continue;
|
|
1554
1795
|
errors.push({
|
|
1555
1796
|
path: `${path}.${k}`,
|
|
1556
1797
|
message: `unknown key "${k}"`,
|
|
1557
|
-
hint: `使える項目 =
|
|
1798
|
+
hint: `使える項目 = ${出来事の項目.join(", ")}`,
|
|
1558
1799
|
});
|
|
1559
1800
|
}
|
|
1560
1801
|
if (typeof o.on !== "string" || !(EVENT_KINDS as readonly string[]).includes(o.on)) {
|
|
@@ -1592,7 +1833,7 @@ function validateEvents(v: unknown, errors: JsonDslError[]): void {
|
|
|
1592
1833
|
}
|
|
1593
1834
|
return;
|
|
1594
1835
|
}
|
|
1595
|
-
if (typeof o[鍵] !== "string" ||
|
|
1836
|
+
if (typeof o[鍵] !== "string" || o[鍵].trim() === "") {
|
|
1596
1837
|
errors.push({ path: `${path}.${鍵}`, message: `${鍵} must be a non-empty string` });
|
|
1597
1838
|
return;
|
|
1598
1839
|
}
|
|
@@ -1609,6 +1850,16 @@ function validateEvents(v: unknown, errors: JsonDslError[]): void {
|
|
|
1609
1850
|
});
|
|
1610
1851
|
}
|
|
1611
1852
|
|
|
1853
|
+
/**
|
|
1854
|
+
* 巻き上げに応じて進む値が受ける項目 (#2258)。
|
|
1855
|
+
*
|
|
1856
|
+
* **数を書く項目と文字を書く項目を分けて持つ** = 受け方が違うので判定が分かれるが、
|
|
1857
|
+
* 知らせは 2 つを繋いで 1 つの一覧として出す。 別々に並べていた間、判定が 3 語で
|
|
1858
|
+
* 知らせが 4 語という食い違いが同じ関数の中にあった。
|
|
1859
|
+
*/
|
|
1860
|
+
const 送りの数の項目 = ["start", "end", "scrub"] as const;
|
|
1861
|
+
const 送りの文の項目 = ["label"] as const;
|
|
1862
|
+
|
|
1612
1863
|
/**
|
|
1613
1864
|
* 巻き上げに応じて進む値を検査する (#1393)。
|
|
1614
1865
|
*
|
|
@@ -1658,17 +1909,17 @@ function validateScrolls(
|
|
|
1658
1909
|
continue;
|
|
1659
1910
|
}
|
|
1660
1911
|
for (const [k, x] of Object.entries(spec as Record<string, unknown>)) {
|
|
1661
|
-
if (
|
|
1912
|
+
if ((送りの文の項目 as readonly string[]).includes(k)) {
|
|
1662
1913
|
if (typeof x !== "string") {
|
|
1663
|
-
errors.push({ path: `${path}
|
|
1914
|
+
errors.push({ path: `${path}.${k}`, message: `${k} must be a string` });
|
|
1664
1915
|
}
|
|
1665
1916
|
continue;
|
|
1666
1917
|
}
|
|
1667
|
-
if (
|
|
1918
|
+
if (!(送りの数の項目 as readonly string[]).includes(k)) {
|
|
1668
1919
|
errors.push({
|
|
1669
1920
|
path: `${path}.${k}`,
|
|
1670
1921
|
message: `unknown key "${k}"`,
|
|
1671
|
-
hint:
|
|
1922
|
+
hint: `使える項目 = ${[...送りの数の項目, ...送りの文の項目].join(", ")}`,
|
|
1672
1923
|
});
|
|
1673
1924
|
continue;
|
|
1674
1925
|
}
|
|
@@ -1703,6 +1954,15 @@ function validateActorShape(v: unknown, path: string, errors: JsonDslError[]): v
|
|
|
1703
1954
|
表で中身を検査する(v as Record<string, unknown>, 図形の表, path, "shape", errors);
|
|
1704
1955
|
}
|
|
1705
1956
|
|
|
1957
|
+
/**
|
|
1958
|
+
* 鍵を利用者が決める入れ物 (縦列 / 群) を見る (#1304)。
|
|
1959
|
+
*
|
|
1960
|
+
* `lanes` / `groups` は id を鍵に持つため、鍵そのものは縛れない。 縛れるのは
|
|
1961
|
+
* 「入れ物が plain object か」 と「各 id の中身の項目名と値の型」 の 2 つ。
|
|
1962
|
+
*
|
|
1963
|
+
* #1304 まで外側の形が違う入力 (`lanes: 5`) は走査ごと飛ばされ、誤りが 1 件も返らなかった。
|
|
1964
|
+
* 形が違うものを黙って捨てると、書いた縦列が 1 つも効かない図が知らせなしで出る。
|
|
1965
|
+
*/
|
|
1706
1966
|
function validateIdMap(
|
|
1707
1967
|
v: unknown,
|
|
1708
1968
|
欄: "lanes" | "groups",
|
|
@@ -1927,7 +2187,7 @@ export function 素のデータに写す(
|
|
|
1927
2187
|
// **名前も数に入れる** (Round 5 の指摘)。 値を読む前に名前の一覧を作るため、 値だけを
|
|
1928
2188
|
// 数えると「名前が 20,000 個ある段を 63 回降りる」 形で 126 万個を並べられる = 上限を
|
|
1929
2189
|
// 見る前に資源を使い切れる
|
|
1930
|
-
const 名前の並び = Object.keys(v
|
|
2190
|
+
const 名前の並び = Object.keys(v);
|
|
1931
2191
|
項目数 += 名前の並び.length;
|
|
1932
2192
|
if (項目数 > 写しの最大の項目数) {
|
|
1933
2193
|
throw new 写せない(path, `項目が多すぎる (上限 ${写しの最大の項目数})`);
|
|
@@ -1984,6 +2244,10 @@ export function 素のデータに写す(
|
|
|
1984
2244
|
}
|
|
1985
2245
|
}
|
|
1986
2246
|
|
|
2247
|
+
/**
|
|
2248
|
+
* shape validation。 layer 1 = 必須 field + 型 check、 layer 2 は compile 側の validation に委譲。
|
|
2249
|
+
* fail-fast ではなく全 error 収集して返す (LLM に一括で修正させるため)。
|
|
2250
|
+
*/
|
|
1987
2251
|
function validateJson(
|
|
1988
2252
|
json: unknown,
|
|
1989
2253
|
): { ok: true; data: DragonJson } | { ok: false; errors: JsonDslError[] } {
|
|
@@ -2059,6 +2323,7 @@ function validateJson(
|
|
|
2059
2323
|
const 代わりに使う欄: Record<(typeof 見本にしか効かない欄)[number], string> = {
|
|
2060
2324
|
state: "箱の見た目を変えるなら tone / color を使う",
|
|
2061
2325
|
scale: "大きさを変えるなら posW / posH を使う",
|
|
2326
|
+
phase: "普通の箱は自分の段を持たない。 動かす段は animation に書く",
|
|
2062
2327
|
};
|
|
2063
2328
|
if (!見本か) {
|
|
2064
2329
|
for (const 欄 of 見本にしか効かない欄) {
|
|
@@ -2082,9 +2347,8 @@ function validateJson(
|
|
|
2082
2347
|
});
|
|
2083
2348
|
}
|
|
2084
2349
|
}
|
|
2085
|
-
|
|
2086
|
-
//
|
|
2087
|
-
// `{ v: {} }` 等 nested object や null が流入すると CdlState.initial に不正な型が入り compile 崩れる。
|
|
2350
|
+
// 値の上書き (`state`) は素の object に限り、中の値も数 / 文字 / 真偽のいずれかに限る。
|
|
2351
|
+
// `{ v: {} }` のような入れ子や null を通すと、値の初期値に扱えない型が入って組み立てが崩れる。
|
|
2088
2352
|
if (ao.state !== undefined) {
|
|
2089
2353
|
if (!ao.state || typeof ao.state !== "object" || Array.isArray(ao.state)) {
|
|
2090
2354
|
errors.push({
|
|
@@ -2105,7 +2369,11 @@ function validateJson(
|
|
|
2105
2369
|
}
|
|
2106
2370
|
// CAR-1693 Phase 1: actor DSL 表面 pos の validation
|
|
2107
2371
|
validateLayoutPos(ao.pos, `$.actors[${i}].pos`, errors);
|
|
2372
|
+
// 相対で置く指定 (#2039)
|
|
2373
|
+
validateRelativePos(ao.posRel, `$.actors[${i}].posRel`, errors);
|
|
2108
2374
|
});
|
|
2375
|
+
// 解けない相対の指定 (#2039)。 欄ごとの形を見た後に、図全体として解けるかを見る
|
|
2376
|
+
validateRelativeAnchors(j.actors, errors);
|
|
2109
2377
|
}
|
|
2110
2378
|
if (!Array.isArray(j.flow)) {
|
|
2111
2379
|
errors.push({ path: "$.flow", message: "flow must be an array" });
|
|
@@ -2210,7 +2478,7 @@ function validatePhaseMotion(po: Record<string, unknown>, i: number, errors: Jso
|
|
|
2210
2478
|
const t = typeof value;
|
|
2211
2479
|
if (t !== "number" && t !== "string") {
|
|
2212
2480
|
errors.push({ path, message: "set value must be a number or string", hint: `got ${t}` });
|
|
2213
|
-
} else if (t === "number" && !Number.isFinite(value
|
|
2481
|
+
} else if (t === "number" && !Number.isFinite(value)) {
|
|
2214
2482
|
errors.push({ path, message: "set value must be a finite number" });
|
|
2215
2483
|
}
|
|
2216
2484
|
}
|
|
@@ -2244,7 +2512,7 @@ function validateStates(v: unknown, errors: JsonDslError[]): void {
|
|
|
2244
2512
|
message: "state initial must be a number or string",
|
|
2245
2513
|
hint: `got ${t}`,
|
|
2246
2514
|
});
|
|
2247
|
-
} else if (t === "number" && !Number.isFinite(initial
|
|
2515
|
+
} else if (t === "number" && !Number.isFinite(initial)) {
|
|
2248
2516
|
// `NaN` / `Infinity` は JSON には書けないが、object を直接渡す経路では届く。
|
|
2249
2517
|
// 描画側は文字列に直して式に流すため、そのまま通すと計算が全て壊れる
|
|
2250
2518
|
errors.push({ path: `$.states.${name}`, message: "state initial must be a finite number" });
|
|
@@ -2285,12 +2553,6 @@ function validateValues(v: unknown, errors: JsonDslError[]): void {
|
|
|
2285
2553
|
}
|
|
2286
2554
|
}
|
|
2287
2555
|
|
|
2288
|
-
/**
|
|
2289
|
-
* JSON DSL → DslDocument (AST) 変換。 pos は JSON なので line 情報なし、 全て line 0。
|
|
2290
|
-
*
|
|
2291
|
-
* CAR-1693 Phase 1: DSL 表面 `pos: {x, y}` → 内部 AST `layoutPos:` の 2 層 mapping の実装 core。
|
|
2292
|
-
* test で mapping logic を実 execute するため export する (pos-field.test.ts の regression guard)。
|
|
2293
|
-
*/
|
|
2294
2556
|
/**
|
|
2295
2557
|
* 図表の箱の上の小見出しを、 記法側と同じ形に整える (#1247)。
|
|
2296
2558
|
*
|
|
@@ -2303,14 +2565,43 @@ function 整えた小見出し(v: string | undefined): string | undefined {
|
|
|
2303
2565
|
return t.length > 0 ? t : undefined;
|
|
2304
2566
|
}
|
|
2305
2567
|
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2568
|
+
/**
|
|
2569
|
+
* 見本の状態の上書きに、段を使うかどうか (`phase`) を混ぜる (#2125)。
|
|
2570
|
+
*
|
|
2571
|
+
* 組み立て側は段を外す指定を状態の上書きの中の `phase` として読む
|
|
2572
|
+
* (`compile/parts.ts` の `段を外す`)。 記法は中括弧に書いた名前をすべて上書きへ入れるため
|
|
2573
|
+
* `{ kind: wave-gauge, phase: false }` がそのまま届くが、JSON は欄ごとに型を宣言する形なので、
|
|
2574
|
+
* 欄として受けたものを同じ場所へ入れ直す。
|
|
2575
|
+
*
|
|
2576
|
+
* **書かなかった時は欄を作らない**。 `{ phase: undefined }` を混ぜると、上書きの有無で
|
|
2577
|
+
* 分岐する側から見て「書いた」 ことになる。
|
|
2578
|
+
*/
|
|
2579
|
+
function 段を含めた上書き(a: JsonActor): Record<string, number | string | boolean> | undefined {
|
|
2580
|
+
if (a.phase === undefined) return a.state;
|
|
2581
|
+
return { ...(a.state ?? {}), phase: a.phase };
|
|
2582
|
+
}
|
|
2583
|
+
|
|
2584
|
+
/**
|
|
2585
|
+
* JSON DSL → DslDocument (AST) 変換。
|
|
2586
|
+
*
|
|
2587
|
+
* 行番号は `行の表` を渡した時だけ入る (#2117)。 JSON そのものは書いた場所を持たないため、
|
|
2588
|
+
* 本文の行が分かる入口 (編集画面の YAML 欄) が場所ごとの行を集めて渡す。 渡さない呼出は
|
|
2589
|
+
* 全ての要素が 0 行になる = 「行が分からない」 ことを 0 で表す。
|
|
2590
|
+
*
|
|
2591
|
+
* CAR-1693 Phase 1: DSL 表面 `pos: {x, y}` → 内部 AST `layoutPos:` の 2 層 mapping の実装 core。
|
|
2592
|
+
* test で mapping logic を実 execute するため export する (pos-field.test.ts の regression guard)。
|
|
2593
|
+
*/
|
|
2594
|
+
export function jsonToDoc(json: DragonJson, 行の表?: 書いた行の表): DslDocument {
|
|
2595
|
+
// 書いた場所から行を引く。 引けない場所は 0 行 = 表を渡さない呼出と同じ状態になる
|
|
2596
|
+
const 位置 = (...道: readonly (string | number)[]): { line: number } => ({
|
|
2597
|
+
line: 行の表?.get(書いた場所の鍵(...道)) ?? 0,
|
|
2598
|
+
});
|
|
2599
|
+
const actors: DslActor[] = json.actors.map((a, i) => {
|
|
2309
2600
|
if (typeof a === "string") {
|
|
2310
|
-
return { name: a, kind: "actor" as NodeKind, kindWritten: false, pos:
|
|
2601
|
+
return { name: a, kind: "actor" as NodeKind, kindWritten: false, pos: 位置("actors", i) };
|
|
2311
2602
|
}
|
|
2312
2603
|
// CAR-1657 = kind が既存 NodeKind に無い値なら parts identifier 候補、 partId に格納
|
|
2313
|
-
const kindStr =
|
|
2604
|
+
const kindStr = a.kind ?? "actor";
|
|
2314
2605
|
const isPart = kindStr !== "actor" && !VALID_KIND_SET.has(kindStr);
|
|
2315
2606
|
// 色は記法と同じ振り分けを通す (#1294)。 `#` で始まれば色番号、それ以外は色の名前。
|
|
2316
2607
|
// 別々に書くと、同じ値が入口によって色番号にも色名にもなる
|
|
@@ -2355,29 +2646,24 @@ export function jsonToDoc(json: DragonJson): DslDocument {
|
|
|
2355
2646
|
opportunity: a.opportunity,
|
|
2356
2647
|
}),
|
|
2357
2648
|
colorHex: 色.hex,
|
|
2649
|
+
// 部品に書いた色の名前は効かない。 記法と同じく書いた名前として残し、組み立て側が知らせる (#1973)
|
|
2650
|
+
partColorName: isPart ? 色.tone : undefined,
|
|
2358
2651
|
// 絶対座標と大きさ (#1294)。 `pos` (ずらし幅) とは別経路
|
|
2359
2652
|
posX: a.posX,
|
|
2360
2653
|
posY: a.posY,
|
|
2361
2654
|
posW: a.posW,
|
|
2362
2655
|
posH: a.posH,
|
|
2363
|
-
// 箱の中の要素ごとの固定 (#1294)。 写しを作って外から書き換えられないようにする
|
|
2364
|
-
nodes: a.nodes
|
|
2365
|
-
? Object.fromEntries(
|
|
2366
|
-
Object.entries(a.nodes).map(([id, o]) => [
|
|
2367
|
-
id,
|
|
2368
|
-
{ posX: o.posX, posY: o.posY, posW: o.posW, posH: o.posH },
|
|
2369
|
-
]),
|
|
2370
|
-
)
|
|
2371
|
-
: undefined,
|
|
2372
2656
|
// 倍率は見本にしか効かない (検査が見本でない箱を弾く)。 書かれた名前は JSON では
|
|
2373
2657
|
// 常に `scale` で、記法の別名 (`倍率`) は JSON に持ち込まない
|
|
2374
2658
|
scale: isPart ? a.scale : undefined,
|
|
2375
2659
|
scaleKeys: isPart && a.scale !== undefined ? ["scale"] : undefined,
|
|
2376
2660
|
partId: isPart ? kindStr : undefined,
|
|
2377
|
-
stateOverride: isPart ? a
|
|
2661
|
+
stateOverride: isPart ? 段を含めた上書き(a) : undefined,
|
|
2378
2662
|
// CAR-1693 Phase 1: DSL 表面 pos → 内部 AST layoutPos の 2 層 mapping (naming collision 回避)
|
|
2379
2663
|
layoutPos: a.pos,
|
|
2380
|
-
|
|
2664
|
+
// 相対で置く指定 (#2039)。 欄の名前が内部と同じなのでそのまま渡す
|
|
2665
|
+
posRel: a.posRel,
|
|
2666
|
+
pos: 位置("actors", i),
|
|
2381
2667
|
};
|
|
2382
2668
|
});
|
|
2383
2669
|
// 図の配色 (#1553)。 解くのは 1 度だけにする
|
|
@@ -2389,13 +2675,15 @@ export function jsonToDoc(json: DragonJson): DslDocument {
|
|
|
2389
2675
|
to: s.to,
|
|
2390
2676
|
label: s.label,
|
|
2391
2677
|
sub: s.sub,
|
|
2392
|
-
side: s.side
|
|
2678
|
+
side: s.side,
|
|
2393
2679
|
// 矢印の先の形 (#1462)。 読めない語は組み立てが落とす
|
|
2394
2680
|
head: s.head,
|
|
2395
2681
|
// 端の印の残り 3 欄と、関係の語 / 言づての種類 (#1466)
|
|
2396
2682
|
tailHead: s.tailHead,
|
|
2397
2683
|
headFill: s.headFill,
|
|
2398
2684
|
tailHeadFill: s.tailHeadFill,
|
|
2685
|
+
// クラス図の出どころ側の多重度 (#1771)
|
|
2686
|
+
tailSub: s.tailSub,
|
|
2399
2687
|
relation: s.relation,
|
|
2400
2688
|
// 辺の役目と名前の下地 (cdl#618)
|
|
2401
2689
|
role: s.role,
|
|
@@ -2414,9 +2702,12 @@ export function jsonToDoc(json: DragonJson): DslDocument {
|
|
|
2414
2702
|
labelOffsetX: s.labelOffsetX,
|
|
2415
2703
|
labelOffsetY: s.labelOffsetY,
|
|
2416
2704
|
overlay: s.overlay,
|
|
2705
|
+
// 部品の端で繋ぐ要素の id (#1979)。 書かない矢印には欄を足さない = 記法と同じ形になる
|
|
2706
|
+
...(s.fromPartNode !== undefined ? { fromPartNode: s.fromPartNode } : {}),
|
|
2707
|
+
...(s.toPartNode !== undefined ? { toPartNode: s.toPartNode } : {}),
|
|
2417
2708
|
// CAR-1693 Phase 1: DSL 表面 pos → 内部 AST layoutPos
|
|
2418
2709
|
layoutPos: s.pos,
|
|
2419
|
-
pos:
|
|
2710
|
+
pos: 位置("flow", i),
|
|
2420
2711
|
}));
|
|
2421
2712
|
// 状態は段が無くても図に載る (#1162 で組み立ての出口が載せる)。 **段の有無で分けない** =
|
|
2422
2713
|
// 分けると `states` だけを書いた JSON で値が 1 つも届かない (記法側で起きていた形、 #1181)
|
|
@@ -2429,7 +2720,7 @@ export function jsonToDoc(json: DragonJson): DslDocument {
|
|
|
2429
2720
|
*
|
|
2430
2721
|
* 相手は名前のまま持ち、識別子への読み替えは組み立てが行う = 2 つの入口で同じ経路を通る。
|
|
2431
2722
|
*/
|
|
2432
|
-
const events: DslEventBinding[] | undefined = json.events?.map((e) => ({
|
|
2723
|
+
const events: DslEventBinding[] | undefined = json.events?.map((e, i) => ({
|
|
2433
2724
|
event: e.on as DslEventBinding["event"],
|
|
2434
2725
|
target:
|
|
2435
2726
|
e.diagram === true
|
|
@@ -2444,28 +2735,38 @@ export function jsonToDoc(json: DragonJson): DslDocument {
|
|
|
2444
2735
|
? ({ kind: "lane" as const, name: e.lane } as const)
|
|
2445
2736
|
: ({ kind: "node" as const, name: e.box ?? "" } as const),
|
|
2446
2737
|
handlerId: e.handler.trim(),
|
|
2447
|
-
pos:
|
|
2738
|
+
pos: 位置("events", i),
|
|
2448
2739
|
}));
|
|
2449
2740
|
const scrolls: DslScrollTrigger[] | undefined = json.scrolls
|
|
2450
2741
|
? Object.entries(json.scrolls).map(([id, spec]) => ({ id, ...spec }))
|
|
2451
2742
|
: undefined;
|
|
2452
|
-
// 式は `{ 名前: "式" }` から並びへ写す (#1391)。 記法側と同じ形にして組み立てを 1
|
|
2743
|
+
// 式は `{ 名前: "式" }` から並びへ写す (#1391)。 記法側と同じ形にして組み立てを 1 本にする。
|
|
2744
|
+
// 組の形 (`{ expression, label }`) は名札を持ったまま写す (#1916)
|
|
2453
2745
|
const formulas: DslFormula[] | undefined = json.formulas
|
|
2454
|
-
? Object.entries(json.formulas).map(([id,
|
|
2746
|
+
? Object.entries(json.formulas).map(([id, 値]) =>
|
|
2747
|
+
typeof 値 === "string"
|
|
2748
|
+
? { id, expression: 値, pos: 位置("formulas", id) }
|
|
2749
|
+
: {
|
|
2750
|
+
id,
|
|
2751
|
+
expression: 値.expression,
|
|
2752
|
+
...(値.label !== undefined ? { label: 値.label } : {}),
|
|
2753
|
+
pos: 位置("formulas", id),
|
|
2754
|
+
},
|
|
2755
|
+
)
|
|
2455
2756
|
: undefined;
|
|
2456
2757
|
const states: DslState[] = Object.entries(json.states ?? {}).map(([name, initial]) => ({
|
|
2457
2758
|
name,
|
|
2458
2759
|
initial,
|
|
2459
|
-
pos:
|
|
2760
|
+
pos: 位置("states", name),
|
|
2460
2761
|
}));
|
|
2461
|
-
const phases: DslPhase[] = (json.animation ?? []).map((p) => ({
|
|
2762
|
+
const phases: DslPhase[] = (json.animation ?? []).map((p, i) => ({
|
|
2462
2763
|
name: p.step,
|
|
2463
2764
|
durationMs: Math.round((p.duration ?? 1.4) * 1000),
|
|
2464
2765
|
highlight: p.focus,
|
|
2465
2766
|
body: p.body,
|
|
2466
2767
|
badge: p.badge,
|
|
2467
2768
|
// 書いた段だけが欄を持つ。 空文字を置くと「書いた」 と「書いていない」 が同じ形になる
|
|
2468
|
-
...(p.draw !== undefined ? { draw: p.draw, drawPos:
|
|
2769
|
+
...(p.draw !== undefined ? { draw: p.draw, drawPos: 位置("animation", i, "draw") } : {}),
|
|
2469
2770
|
// 割合は `draw` がある段にだけ写す。 単独で書いても描く相手が決まらず何も起きないので、
|
|
2470
2771
|
// 記法側 (同じ行に書かせる形) と同じ状態に揃える
|
|
2471
2772
|
...(p.draw !== undefined && p.drawRatio !== undefined ? { drawRatio: p.drawRatio } : {}),
|
|
@@ -2477,17 +2778,27 @@ export function jsonToDoc(json: DragonJson): DslDocument {
|
|
|
2477
2778
|
state,
|
|
2478
2779
|
from,
|
|
2479
2780
|
to,
|
|
2480
|
-
pos:
|
|
2781
|
+
pos: 位置("animation", i, "tween", state),
|
|
2481
2782
|
})),
|
|
2482
2783
|
}
|
|
2483
2784
|
: {}),
|
|
2484
2785
|
...(p.set && Object.keys(p.set).length > 0
|
|
2485
|
-
? {
|
|
2786
|
+
? {
|
|
2787
|
+
sets: Object.entries(p.set).map(([state, value]) => ({
|
|
2788
|
+
state,
|
|
2789
|
+
value,
|
|
2790
|
+
pos: 位置("animation", i, "set", state),
|
|
2791
|
+
})),
|
|
2792
|
+
}
|
|
2486
2793
|
: {}),
|
|
2487
|
-
pos:
|
|
2794
|
+
pos: 位置("animation", i),
|
|
2488
2795
|
}));
|
|
2489
2796
|
const animate: DslAnimate | undefined =
|
|
2490
|
-
states.length > 0 || phases.length > 0
|
|
2797
|
+
states.length > 0 || phases.length > 0
|
|
2798
|
+
? // 動きの塊そのものには書いた場所が無い。 段がある文書は `animation`、無い文書は
|
|
2799
|
+
// `states` を指す = どちらも「動きを書き始めた場所」 になる
|
|
2800
|
+
{ states, phases, pos: phases.length > 0 ? 位置("animation") : 位置("states") }
|
|
2801
|
+
: undefined;
|
|
2491
2802
|
return {
|
|
2492
2803
|
title: json.title,
|
|
2493
2804
|
type: json.type,
|
|
@@ -2495,12 +2806,12 @@ export function jsonToDoc(json: DragonJson): DslDocument {
|
|
|
2495
2806
|
// 空かどうかを判定するため、 揃えないと **空白だけの値で入口ごとに図が変わる**
|
|
2496
2807
|
// (記法は書かなかった扱い、 JSON は中身のない帯を描く。 Round 2 の指摘で実測)
|
|
2497
2808
|
...(整えた小見出し(json.eyebrow) !== undefined
|
|
2498
|
-
? { eyebrow: 整えた小見出し(json.eyebrow), eyebrowPos:
|
|
2809
|
+
? { eyebrow: 整えた小見出し(json.eyebrow), eyebrowPos: 位置("eyebrow") }
|
|
2499
2810
|
: {}),
|
|
2500
2811
|
// 2 軸で仕分ける図の軸の名前 (#1294)。 中身の無い形は「書かなかった」 と同じにする =
|
|
2501
2812
|
// 空の軸を渡すと、書いていない側の名前が空文字で描かれる (記法側と同じ扱い)
|
|
2502
2813
|
...(json.axes && (json.axes.x !== undefined || json.axes.y !== undefined)
|
|
2503
|
-
? { axes: json.axes, axesPos:
|
|
2814
|
+
? { axes: json.axes, axesPos: 位置("axes") }
|
|
2504
2815
|
: {}),
|
|
2505
2816
|
actors,
|
|
2506
2817
|
flow,
|
|
@@ -2508,16 +2819,20 @@ export function jsonToDoc(json: DragonJson): DslDocument {
|
|
|
2508
2819
|
// 他の値から決まる値 (#1181)。 書いた順に並べる = 解く順は参照から決まるので順序に
|
|
2509
2820
|
// 意味は無いが、知らせの並びが書いた順になる
|
|
2510
2821
|
values: json.values
|
|
2511
|
-
? Object.entries(json.values).map(([name, expression]) => ({
|
|
2822
|
+
? Object.entries(json.values).map(([name, expression]) => ({
|
|
2823
|
+
name,
|
|
2824
|
+
expression,
|
|
2825
|
+
pos: 位置("values", name),
|
|
2826
|
+
}))
|
|
2512
2827
|
: undefined,
|
|
2513
|
-
viewport: json.viewport ? { ...json.viewport, pos:
|
|
2828
|
+
viewport: json.viewport ? { ...json.viewport, pos: 位置("viewport") } : undefined,
|
|
2514
2829
|
lanes: json.lanes
|
|
2515
2830
|
? Object.fromEntries(
|
|
2516
2831
|
Object.entries(json.lanes).map(([id, l]) => {
|
|
2517
2832
|
// CAR-1693 Phase 1: DSL 表面 pos → 内部 AST layoutPos の 2 層 mapping。
|
|
2518
2833
|
// JSON input の { pos, x, width, ... } を分離し、 pos のみ layoutPos に rename する。
|
|
2519
2834
|
const { pos: layoutPos, ...laneRest } = l;
|
|
2520
|
-
return [id, { id, ...laneRest, layoutPos, pos:
|
|
2835
|
+
return [id, { id, ...laneRest, layoutPos, pos: 位置("lanes", id) }];
|
|
2521
2836
|
}),
|
|
2522
2837
|
)
|
|
2523
2838
|
: undefined,
|
|
@@ -2525,7 +2840,7 @@ export function jsonToDoc(json: DragonJson): DslDocument {
|
|
|
2525
2840
|
? Object.fromEntries(
|
|
2526
2841
|
Object.entries(json.groups).map(([id, g]) => [
|
|
2527
2842
|
id,
|
|
2528
|
-
{ id, label: g.label, lanes: g.lanes, pos:
|
|
2843
|
+
{ id, label: g.label, lanes: g.lanes, pos: 位置("groups", id) },
|
|
2529
2844
|
]),
|
|
2530
2845
|
)
|
|
2531
2846
|
: undefined,
|
|
@@ -2538,12 +2853,20 @@ export function jsonToDoc(json: DragonJson): DslDocument {
|
|
|
2538
2853
|
bands: json.bands,
|
|
2539
2854
|
// 矢印をいつ出すか (#1470)
|
|
2540
2855
|
reveal: json.reveal,
|
|
2541
|
-
//
|
|
2542
|
-
|
|
2856
|
+
// 箱に触れると関係する線だけを光らせるか (#1757)
|
|
2857
|
+
relations: json.relations,
|
|
2858
|
+
// 図の並ぶ向き (#1494)。 JSON は英語で書くので、記法と同じ語に直してから渡す。
|
|
2859
|
+
// 書いた行も記法と同じく持つ = 持たないと、向きの知らせが文書の行 (1 行目) を指す
|
|
2860
|
+
...(json.direction !== undefined
|
|
2861
|
+
? {
|
|
2862
|
+
direction: json.direction === "horizontal" ? ("横" as const) : ("縦" as const),
|
|
2863
|
+
directionPos: 位置("direction"),
|
|
2864
|
+
}
|
|
2865
|
+
: {}),
|
|
2543
2866
|
// 図の配色 (#1553)。 記法と同じ解決を通す = 別名 (`生成り` / `青磁`) の受け方がずれない。
|
|
2544
2867
|
// 読めない語は渡さない = 上流の型検査が語を絞っているので、ここに来るのは書き間違いだけ
|
|
2545
2868
|
...(配色 !== null ? { palette: 配色 } : {}),
|
|
2546
|
-
pos:
|
|
2869
|
+
pos: 位置(),
|
|
2547
2870
|
};
|
|
2548
2871
|
}
|
|
2549
2872
|
|
|
@@ -2573,7 +2896,17 @@ export function jsonToDiagram(
|
|
|
2573
2896
|
// **`onNotice` も通す**。 記法経路だけに通知を付けていたため、 同じ型を受ける JSON / YAML
|
|
2574
2897
|
// 経路では読めない値や捨てた矢印が利用者へ届かなかった (review 指摘)。 エディタの YAML タブは
|
|
2575
2898
|
// ここを通る
|
|
2576
|
-
opts?: {
|
|
2899
|
+
opts?: {
|
|
2900
|
+
partsCatalog?: Record<string, CdlDiagram>;
|
|
2901
|
+
onNotice?: CompileToCdlOpts["onNotice"];
|
|
2902
|
+
/**
|
|
2903
|
+
* 本文の場所から書いた行を引く表 (#2117)。 渡すと知らせが書いた行を指す。
|
|
2904
|
+
*
|
|
2905
|
+
* JSON そのものは書いた場所を持たないため、本文の行が分かる入口 (編集画面の YAML 欄) が
|
|
2906
|
+
* 集めて渡す。 渡さない呼出は今までどおり全ての知らせが 0 行になる。
|
|
2907
|
+
*/
|
|
2908
|
+
行の表?: 書いた行の表;
|
|
2909
|
+
},
|
|
2577
2910
|
): CdlDiagram {
|
|
2578
2911
|
const v = validateJson(json);
|
|
2579
2912
|
if (!v.ok) {
|
|
@@ -2582,7 +2915,7 @@ export function jsonToDiagram(
|
|
|
2582
2915
|
.join("\n");
|
|
2583
2916
|
throw new Error(`Dragon JSON DSL validation error:\n${msg}`);
|
|
2584
2917
|
}
|
|
2585
|
-
const doc = jsonToDoc(v.data);
|
|
2918
|
+
const doc = jsonToDoc(v.data, opts?.行の表);
|
|
2586
2919
|
return compileToCdl(doc, opts);
|
|
2587
2920
|
}
|
|
2588
2921
|
|