@cardenelabs/dragon 0.12.0 → 0.13.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/src/input-size.ts CHANGED
@@ -66,6 +66,9 @@ export function countDocElements(doc: DslDocument): number {
66
66
  phaseChildren +
67
67
  (doc.animate?.states.length ?? 0) +
68
68
  (doc.values?.length ?? 0) +
69
+ // 値を見せる部品も描画時に 1 widget ずつ展開される (#1374)。 数えないと、 actors が
70
+ // 少ないまま readouts だけを大量に並べた入力が組み立て前の上限をすり抜ける。
71
+ (doc.readouts?.length ?? 0) +
69
72
  (doc.groups ? Object.keys(doc.groups).length : 0) +
70
73
  (doc.lanes ? Object.keys(doc.lanes).length : 0)
71
74
  );
@@ -95,7 +98,12 @@ export function countDiagramElements(diagram: {
95
98
  }): number {
96
99
  const phases = Array.isArray(diagram.phases) ? diagram.phases : [];
97
100
  const phaseChildren = phases.reduce((acc: number, p) => {
98
- const ph = p as { activate?: unknown[]; highlight?: unknown[]; tweens?: unknown[]; sets?: unknown[] };
101
+ const ph = p as {
102
+ activate?: unknown[];
103
+ highlight?: unknown[];
104
+ tweens?: unknown[];
105
+ sets?: unknown[];
106
+ };
99
107
  return (
100
108
  acc +
101
109
  (Array.isArray(ph?.activate) ? ph.activate.length : 0) +
@@ -20,6 +20,7 @@
20
20
 
21
21
  import {
22
22
  DRAW_WORDS,
23
+ EDGE_SIDE_VALUES,
23
24
  NODE_KIND_VALID,
24
25
  PRESET_TYPES,
25
26
  STYLE_VALID,
@@ -27,6 +28,10 @@ import {
27
28
  resolveTone,
28
29
  splitColorValue,
29
30
  書ける色名,
31
+ 図形の表,
32
+ 部品の表,
33
+ 部品の組の表,
34
+ type 図形の定義,
30
35
  } from "./v05/parser";
31
36
  import type { CompileToCdlOpts } from "./compile";
32
37
  import type { CdlDiagram, NodeKind, Tone, EdgeStyle } from "@cardenelabs/cdl";
@@ -39,6 +44,8 @@ import type {
39
44
  DslState,
40
45
  PresetType,
41
46
  LayoutPos,
47
+ DslReadout,
48
+ DslDynShape,
42
49
  } from "./types";
43
50
  import { checkValueExpression, isValueName, valueNameIssue } from "./value-syntax";
44
51
  import { compileToCdl } from "./compile";
@@ -72,6 +79,12 @@ export interface DragonJson {
72
79
  actors: (string | JsonActor)[];
73
80
  /** flow step 配列 (必須): { from, to, label, ... } */
74
81
  flow: JsonStep[];
82
+ /**
83
+ * 値を見せる部品 (optional、 #1374)。 記法の最上位 `readouts:` と同じ。
84
+ *
85
+ * 箱ではないので縦列に載らない。 図全体に 1 つの並びとして持つ。
86
+ */
87
+ readouts?: DslReadout[];
75
88
  /**
76
89
  * 状態の初期値 (optional)。 記法の `states:` と同じ (#1181)。
77
90
  *
@@ -129,6 +142,20 @@ export interface DragonJson {
129
142
 
130
143
  export interface JsonActor {
131
144
  name: string;
145
+ /**
146
+ * 箱の中に描く図形 (optional、 #1374)。 記法の `shape:` と同じ。
147
+ *
148
+ * 水位や角度を状態で動かす。 形は描画側の型が縛る。
149
+ */
150
+ shape?: DslDynShape;
151
+ /**
152
+ * その箱を出すかどうかの条件 (optional、 #1381)。 記法の `visibleIf:` と同じ。
153
+ */
154
+ visibleIf?: string;
155
+ /**
156
+ * 箱に出す題 (optional、 #1381)。 記法の `title:` と同じ。
157
+ */
158
+ title?: string;
132
159
  /**
133
160
  * CAR-1657 unified syntax = 既存 NodeKind (28 個) に加えて parts identifier (arc-gauge 等) を
134
161
  * accept する。 未知 kind 値は parts 候補として partId に格納、 compile 側 partsCatalog で解決。
@@ -236,6 +263,8 @@ export interface JsonStep {
236
263
  to: string;
237
264
  label: string;
238
265
  sub?: string;
266
+ /** 矢印がどの辺から出るか (#1385)。 記法の `side:` と同じ */
267
+ side?: "top" | "right" | "bottom" | "left";
239
268
  /**
240
269
  * 矢印の色 (#1304)。 記法の `(成功)` と同じく別名 (`成功` / `neutral` 等) も受ける。
241
270
  *
@@ -347,6 +376,8 @@ export const ACCEPTED_KEYS = {
347
376
  "viewport",
348
377
  "lanes",
349
378
  "groups",
379
+ // 値を見せる部品 (#1374)
380
+ "readouts",
350
381
  ],
351
382
  actor: [
352
383
  "name",
@@ -373,12 +404,20 @@ export const ACCEPTED_KEYS = {
373
404
  "scale",
374
405
  "state",
375
406
  "pos",
407
+ // 箱の中に描く図形 (#1374)
408
+ "shape",
409
+ // その箱を出すかどうかの条件 (#1381)
410
+ "visibleIf",
411
+ // 箱に出す題 (#1381)
412
+ "title",
376
413
  ],
377
414
  step: [
378
415
  "from",
379
416
  "to",
380
417
  "label",
381
418
  "sub",
419
+ // 矢印がどの辺から出るか (#1385)
420
+ "side",
382
421
  "tone",
383
422
  "style",
384
423
  "guard",
@@ -434,6 +473,7 @@ export type 欄の型 =
434
473
  | "真偽"
435
474
  | "色"
436
475
  | "線種"
476
+ | "辺"
437
477
  | "色か色番号"
438
478
  | "描くもの"
439
479
  | "必須の図種"
@@ -456,6 +496,8 @@ export const 欄の型表 = {
456
496
  viewport: "object",
457
497
  lanes: "object",
458
498
  groups: "object",
499
+ // 値を見せる部品 (#1374)。 中身は下の検査が種類ごとに見る
500
+ readouts: "並び",
459
501
  },
460
502
  actor: {
461
503
  name: "必須の非空文字列",
@@ -482,12 +524,20 @@ export const 欄の型表 = {
482
524
  scale: "数",
483
525
  state: "object",
484
526
  pos: "object",
527
+ // 箱の中に描く図形 (#1374)。 中身は下の検査が種類ごとに見る
528
+ shape: "object",
529
+ // その箱を出すかどうかの条件 (#1381)
530
+ visibleIf: "文字列",
531
+ // 箱に出す題 (#1381)
532
+ title: "文字列",
485
533
  },
486
534
  step: {
487
535
  from: "必須の文字列",
488
536
  to: "必須の文字列",
489
537
  label: "必須の文字列",
490
538
  sub: "文字列",
539
+ // 矢印がどの辺から出るか (#1385)
540
+ side: "辺",
491
541
  tone: "色",
492
542
  style: "線種",
493
543
  guard: "文字列",
@@ -573,7 +623,10 @@ function JSONの色名か(v: string): boolean {
573
623
  * `見本に効かない欄` (#1308) と対になる。 2 つの表で「どちらの箱にしか効かないか」 を
574
624
  * 両方向から宣言する = 片方だけ増えると鏡の関係が崩れる。
575
625
  */
576
- export const 見本にしか効かない欄 = ["state", "scale"] as const satisfies readonly (typeof ACCEPTED_KEYS.actor)[number][];
626
+ export const 見本にしか効かない欄 = [
627
+ "state",
628
+ "scale",
629
+ ] as const satisfies readonly (typeof ACCEPTED_KEYS.actor)[number][];
577
630
 
578
631
  /**
579
632
  * 普通の箱にしか効かない欄 (#1308)。
@@ -598,6 +651,12 @@ export const 見本に効かない欄 = [
598
651
  "end",
599
652
  "touchpoint",
600
653
  "opportunity",
654
+ // 箱の中に描く図形 (#1374)。 見本は自分の形を持つため、外から図形を差し替えられない
655
+ "shape",
656
+ // その箱を出すかどうかの条件 (#1381)。 見本は自分の出方を持つ
657
+ "visibleIf",
658
+ // 箱に出す題 (#1381)。 見本は自分の題を持つ
659
+ "title",
601
660
  ] as const satisfies readonly (typeof ACCEPTED_KEYS.actor)[number][];
602
661
 
603
662
  /**
@@ -695,6 +754,16 @@ function 値を検査(
695
754
  });
696
755
  }
697
756
  return;
757
+ case "辺":
758
+ if (v === undefined) return;
759
+ if (typeof v !== "string" || !(EDGE_SIDE_VALUES as readonly string[]).includes(v)) {
760
+ errors.push({
761
+ path,
762
+ message: `${名前} must be one of: ${EDGE_SIDE_VALUES.join(", ")}`,
763
+ hint: typeof v === "string" ? `got "${v}"` : `got ${typeof v}`,
764
+ });
765
+ }
766
+ return;
698
767
  case "描くもの":
699
768
  if (v === undefined) return;
700
769
  // 受ける語は記法と同じ一覧を見る (`DRAW_WORDS`)。 写すと語が増えた時に片方だけ古くなる
@@ -912,6 +981,183 @@ function validateViewport(v: unknown, errors: JsonDslError[]): void {
912
981
  * #1304 まで外側の形が違う入力 (`lanes: 5`) は走査ごと飛ばされ、誤りが 1 件も返らなかった。
913
982
  * 形が違うものを黙って捨てると、書いた縦列が 1 つも効かない図が知らせなしで出る。
914
983
  */
984
+ /**
985
+ * 図形と部品の中身を、記法と **同じ表** で検査する (#1374)。
986
+ *
987
+ * 表を 2 つ持つと片方だけ直してずれる。 記法側 (`v05/parser.ts`) が持つ表をそのまま引く。
988
+ *
989
+ * 記法は値が全て文字列で届くため読み替えが要るが、JSON は型のまま届く。 ここでは
990
+ * 「知らない種類」 「知らない欄」 「足りない必須の欄」 「欄の型違い」 の 4 つを見る。
991
+ */
992
+ function 表で中身を検査する(
993
+ o: Record<string, unknown>,
994
+ 表: Record<string, 図形の定義>,
995
+ path: string,
996
+ 何: string,
997
+ errors: JsonDslError[],
998
+ ): void {
999
+ // JSON は公開 schema の enum と同じ正規名だけを受ける。 ここだけ小文字化すると、
1000
+ // schema が拒む種類を validator が通した上、種類別の追加検査も回避できてしまう。
1001
+ const kind = typeof o.kind === "string" ? o.kind : "";
1002
+ const 定義 = 表[kind];
1003
+ if (定義 === undefined) {
1004
+ errors.push({
1005
+ path: `${path}.kind`,
1006
+ // **文字列でない値をそのまま文にしない**。 object を混ぜると `[object Object]` になり、
1007
+ // 何を書いたのかが読み手に届かない
1008
+ message: `unknown ${何} kind ${typeof o.kind === "string" ? `"${o.kind}"` : JSON.stringify(o.kind ?? null)}`,
1009
+ hint: `使える種類 = ${Object.keys(表).join(", ")}`,
1010
+ });
1011
+ return;
1012
+ }
1013
+ for (const [欄, 値] of Object.entries(o)) {
1014
+ if (欄 === "kind" || 欄 === "id") continue;
1015
+ const 形 = 定義.欄[欄];
1016
+ if (形 === undefined) {
1017
+ errors.push({
1018
+ path: `${path}.${欄}`,
1019
+ message: `unknown key "${欄}"`,
1020
+ hint: `使える項目 = ${Object.keys(定義.欄).join(", ")}`,
1021
+ });
1022
+ continue;
1023
+ }
1024
+ const 型が合う =
1025
+ 形 === "数"
1026
+ ? typeof 値 === "number" && Number.isFinite(値)
1027
+ : 形 === "数か文字列"
1028
+ ? (typeof 値 === "number" && Number.isFinite(値)) || typeof 値 === "string"
1029
+ : 形 === "文字列の並び"
1030
+ ? Array.isArray(値) && 値.every((x) => typeof x === "string")
1031
+ : 形 === "真偽"
1032
+ ? typeof 値 === "boolean"
1033
+ : 形 === "組の並び"
1034
+ ? Array.isArray(値) &&
1035
+ 値.length > 0 &&
1036
+ 値.every(
1037
+ (x) =>
1038
+ typeof x === "object" &&
1039
+ x !== null &&
1040
+ !Array.isArray(x) &&
1041
+ Object.keys(x as object).length > 0 &&
1042
+ Object.values(x as object).every(
1043
+ (y) =>
1044
+ typeof y === "string" || (typeof y === "number" && Number.isFinite(y)),
1045
+ ),
1046
+ )
1047
+ : 形 === "向き"
1048
+ ? typeof 値 === "string" && ["up", "down", "left", "right"].includes(値)
1049
+ : typeof 値 === "string";
1050
+ if (!型が合う) {
1051
+ errors.push({
1052
+ path: `${path}.${欄}`,
1053
+ message: `${欄} must be ${形}`,
1054
+ hint: `got ${Array.isArray(値) ? "array" : 値 === null ? "null" : typeof 値}`,
1055
+ });
1056
+ }
1057
+ }
1058
+ for (const 欄 of 定義.必須) {
1059
+ if (o[欄] === undefined) {
1060
+ errors.push({
1061
+ path: `${path}.${欄}`,
1062
+ message: `${欄} is required for ${何} kind "${kind}"`,
1063
+ hint: `必須の項目 = ${定義.必須.join(", ")}`,
1064
+ });
1065
+ }
1066
+ }
1067
+ }
1068
+
1069
+ /**
1070
+ * 値を見せる部品の並びを検査する (#1374)。
1071
+ *
1072
+ * **外側の形もここで見る**。 `欄の型表` は「並び」 とだけ宣言し、中身の検査は専用の検査に
1073
+ * 委ねる作りなので (`checkFieldType` の `case "並び"`)、ここで見ないと `readouts: 1` が
1074
+ * 素通りする。
1075
+ */
1076
+ function validateReadouts(v: unknown, errors: JsonDslError[]): void {
1077
+ if (v === undefined) return;
1078
+ if (!Array.isArray(v)) {
1079
+ errors.push({
1080
+ path: "$.readouts",
1081
+ message: "readouts must be an array of readout objects",
1082
+ hint: `got ${v === null ? "null" : typeof v}`,
1083
+ });
1084
+ return;
1085
+ }
1086
+ v.forEach((r, i) => {
1087
+ const path = `$.readouts[${i}]`;
1088
+ if (!r || typeof r !== "object" || Array.isArray(r)) {
1089
+ errors.push({ path, message: "readout must be a plain object", hint: `got ${typeof r}` });
1090
+ return;
1091
+ }
1092
+ const o = r as Record<string, unknown>;
1093
+ if (typeof o.id !== "string" || o.id === "") {
1094
+ errors.push({ path: `${path}.id`, message: "id is required", hint: "空でない文字列で書く" });
1095
+ }
1096
+ 表で中身を検査する(o, 部品の表, path, "readout", errors);
1097
+
1098
+ /*
1099
+ * 組の並びを取る欄の中身を、記法と同じ表で見る (#1385)。
1100
+ *
1101
+ * **種類を手で並べない**。 元は `status-dot` と `status-timeline` を直書きしていたが、
1102
+ * 表は描画側の型定義から生成しており、種類が増えるたびに書き足す形になる。
1103
+ */
1104
+ const 組の定義 = 部品の組の表[String(o.kind)];
1105
+ for (const [欄名, 定義] of Object.entries(組の定義 ?? {})) {
1106
+ if (!Array.isArray(o[欄名])) continue;
1107
+ (o[欄名] as unknown[]).forEach((entry, j) => {
1108
+ const entryPath = `${path}.${欄名}[${j}]`;
1109
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) return;
1110
+ const item = entry as Record<string, unknown>;
1111
+ for (const key of Object.keys(item)) {
1112
+ if (key in 定義.欄) continue;
1113
+ errors.push({
1114
+ path: `${entryPath}.${key}`,
1115
+ message: `${key} is not allowed for readout kind "${String(o.kind)}"`,
1116
+ hint: `使える項目 = ${Object.keys(定義.欄).join(", ")}`,
1117
+ });
1118
+ }
1119
+ for (const key of 定義.必須) {
1120
+ if (item[key] !== undefined) continue;
1121
+ errors.push({
1122
+ path: `${entryPath}.${key}`,
1123
+ message: `${key} is required for readout kind "${String(o.kind)}"`,
1124
+ });
1125
+ }
1126
+ for (const [key, 形] of Object.entries(定義.欄)) {
1127
+ const 値 = item[key];
1128
+ if (値 === undefined) continue;
1129
+ const 合う =
1130
+ 形 === "数" ? typeof 値 === "number" && Number.isFinite(値) : typeof 値 === "string";
1131
+ if (合う) continue;
1132
+ errors.push({
1133
+ path: `${entryPath}.${key}`,
1134
+ message: `${key} must be ${形}`,
1135
+ hint: `got ${Array.isArray(値) ? "array" : 値 === null ? "null" : typeof 値}`,
1136
+ });
1137
+ }
1138
+ });
1139
+ }
1140
+ });
1141
+ }
1142
+
1143
+ /**
1144
+ * 箱の中に描く図形を検査する (#1374)。
1145
+ *
1146
+ * `validateReadouts` と同じ理由で外側の形もここで見る。
1147
+ */
1148
+ function validateActorShape(v: unknown, path: string, errors: JsonDslError[]): void {
1149
+ if (v === undefined) return;
1150
+ if (!v || typeof v !== "object" || Array.isArray(v)) {
1151
+ errors.push({
1152
+ path,
1153
+ message: "shape must be a plain object",
1154
+ hint: `got ${v === null ? "null" : Array.isArray(v) ? "array" : typeof v}`,
1155
+ });
1156
+ return;
1157
+ }
1158
+ 表で中身を検査する(v as Record<string, unknown>, 図形の表, path, "shape", errors);
1159
+ }
1160
+
915
1161
  function validateIdMap(
916
1162
  v: unknown,
917
1163
  欄: "lanes" | "groups",
@@ -1230,6 +1476,8 @@ function validateJson(
1230
1476
  // 知らない項目を先に見る (#1295)。 綴り違いは「書いた項目が効かない」 形で表に出るため、
1231
1477
  // 個々の型の誤りより先に伝える方が直しやすい
1232
1478
  checkUnknownKeys(j, "root", "$", errors);
1479
+ // 値を見せる部品の中身を、記法と同じ表で見る (#1374)
1480
+ validateReadouts(j.readouts, errors);
1233
1481
 
1234
1482
  // 値そのものの型は表が見る (#1304)。 図表の箱の上の小見出し (#1247) の空文字は
1235
1483
  // 「書かなかった」 と同じ扱いにするため通す (記法側の `eyebrow:` と揃える。 落とすのは `jsonToDoc`)
@@ -1249,6 +1497,8 @@ function validateJson(
1249
1497
  // 値そのものの型は表が見る (#1304)。 `kind` は見本 (parts) の名前も受けるため
1250
1498
  // 非空の文字列までしか縛らない (CAR-1657 の unified syntax)
1251
1499
  表で検査(ao, "actor", `$.actors[${i}]`, "actor", errors);
1500
+ // 箱の中に描く図形の中身を、記法と同じ表で見る (#1374)
1501
+ validateActorShape(ao.shape, `$.actors[${i}].shape`, errors);
1252
1502
  // 見本 (parts) にしか効かない項目は、見本でない箱に書かれたら誤りにする (#1294)。
1253
1503
  // 記法側は読めない項目名として行番号付きで知らせるため、黙って捨てると入口で扱いが変わる。
1254
1504
  const 見本か = 見本の名前か(ao.kind);
@@ -1526,6 +1776,10 @@ export function jsonToDoc(json: DragonJson): DslDocument {
1526
1776
  stack: a.stack,
1527
1777
  initial: a.initial,
1528
1778
  final: a.final,
1779
+ // 箱の中に描く図形 (#1374)。 見本では状態の上書きが効くため、通常の箱にだけ渡す
1780
+ shape: isPart ? undefined : a.shape,
1781
+ visibleIf: isPart ? undefined : a.visibleIf,
1782
+ title: isPart ? undefined : a.title,
1529
1783
  // 普通の箱にしか効かない欄は見本では落とす。 落とす欄の一覧は `見本に効かない欄` が
1530
1784
  // 唯一の出どころで、検査 (#1308) も同じ表を見る = 「検査は通すが組み立てが捨てる」
1531
1785
  // 状態が作れない
@@ -1570,6 +1824,7 @@ export function jsonToDoc(json: DragonJson): DslDocument {
1570
1824
  to: s.to,
1571
1825
  label: s.label,
1572
1826
  sub: s.sub,
1827
+ side: s.side as "top" | "right" | "bottom" | "left" | undefined,
1573
1828
  // 箱と同じ読み替えを通す (#1304)。 通さないと `tone: "成功"` が色名として解決されないまま
1574
1829
  // 図に届き、同じ値が箱では色になり矢印では色にならない
1575
1830
  tone: resolveTone(s.tone),
@@ -1585,6 +1840,8 @@ export function jsonToDoc(json: DragonJson): DslDocument {
1585
1840
  }));
1586
1841
  // 状態は段が無くても図に載る (#1162 で組み立ての出口が載せる)。 **段の有無で分けない** =
1587
1842
  // 分けると `states` だけを書いた JSON で値が 1 つも届かない (記法側で起きていた形、 #1181)
1843
+ // 値を見せる部品はそのまま渡す (#1374)。 形は描画側の型が縛る
1844
+ const readouts: DslReadout[] | undefined = json.readouts ? [...json.readouts] : undefined;
1588
1845
  const states: DslState[] = Object.entries(json.states ?? {}).map(([name, initial]) => ({
1589
1846
  name,
1590
1847
  initial,
@@ -1658,6 +1915,7 @@ export function jsonToDoc(json: DragonJson): DslDocument {
1658
1915
  ]),
1659
1916
  )
1660
1917
  : undefined,
1918
+ readouts,
1661
1919
  pos: p0,
1662
1920
  };
1663
1921
  }