@cardenelabs/dragon 0.13.0 → 0.14.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 +46 -34
- package/dist/index.cjs +1511 -30
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +402 -3
- package/dist/index.d.ts +402 -3
- package/dist/index.js +1512 -31
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/color.ts +31 -6
- package/src/compile.ts +268 -9
- package/src/input-size.ts +12 -0
- package/src/json-parser.ts +456 -21
- package/src/schemas/diagram.json +356 -0
- package/src/types.ts +91 -0
- package/src/v05/input-table.generated.ts +136 -0
- package/src/v05/parser-types.ts +5 -1
- package/src/v05/parser.ts +695 -8
package/src/json-parser.ts
CHANGED
|
@@ -31,10 +31,13 @@ import {
|
|
|
31
31
|
図形の表,
|
|
32
32
|
部品の表,
|
|
33
33
|
部品の組の表,
|
|
34
|
+
つまみの表,
|
|
35
|
+
EVENT_KINDS,
|
|
34
36
|
type 図形の定義,
|
|
35
37
|
} from "./v05/parser";
|
|
36
38
|
import type { CompileToCdlOpts } from "./compile";
|
|
37
39
|
import type { CdlDiagram, NodeKind, Tone, EdgeStyle } from "@cardenelabs/cdl";
|
|
40
|
+
import { extractIdentifiers, parseFormula } from "@cardenelabs/cdl";
|
|
38
41
|
import type {
|
|
39
42
|
DslDocument,
|
|
40
43
|
DslActor,
|
|
@@ -45,6 +48,10 @@ import type {
|
|
|
45
48
|
PresetType,
|
|
46
49
|
LayoutPos,
|
|
47
50
|
DslReadout,
|
|
51
|
+
DslInput,
|
|
52
|
+
DslFormula,
|
|
53
|
+
DslEventBinding,
|
|
54
|
+
DslScrollTrigger,
|
|
48
55
|
DslDynShape,
|
|
49
56
|
} from "./types";
|
|
50
57
|
import { checkValueExpression, isValueName, valueNameIssue } from "./value-syntax";
|
|
@@ -85,6 +92,33 @@ export interface DragonJson {
|
|
|
85
92
|
* 箱ではないので縦列に載らない。 図全体に 1 つの並びとして持つ。
|
|
86
93
|
*/
|
|
87
94
|
readouts?: DslReadout[];
|
|
95
|
+
/**
|
|
96
|
+
* 読む人が動かすつまみ (optional、 #1389)。 記法の最上位 `inputs:` と同じ。
|
|
97
|
+
*
|
|
98
|
+
* 部品と同じく箱ではないので縦列に載らない。 図全体に 1 つの並びとして持つ。
|
|
99
|
+
*/
|
|
100
|
+
inputs?: DslInput[];
|
|
101
|
+
/**
|
|
102
|
+
* つまみの値から決まる値 (optional、 #1391)。 記法の最上位 `formulas:` と同じ。
|
|
103
|
+
*
|
|
104
|
+
* 形は `{ 名前: "式" }`。 `values:` と経路が別で、式は名前を中括弧で囲わない。
|
|
105
|
+
*/
|
|
106
|
+
formulas?: Record<string, string>;
|
|
107
|
+
/**
|
|
108
|
+
* 押下などの出来事で動く仕掛け (optional、 #1393)。 記法の最上位 `events:` と同じ。
|
|
109
|
+
*
|
|
110
|
+
* 相手は名前で指す (`box` / `lane` / `arrow` / `diagram` のどれか 1 つ)。
|
|
111
|
+
*/
|
|
112
|
+
events?: {
|
|
113
|
+
on: string;
|
|
114
|
+
handler: string;
|
|
115
|
+
box?: string;
|
|
116
|
+
lane?: string;
|
|
117
|
+
arrow?: string;
|
|
118
|
+
diagram?: boolean;
|
|
119
|
+
}[];
|
|
120
|
+
/** 巻き上げに応じて進む値 (optional、 #1393)。 記法の最上位 `scrolls:` と同じ */
|
|
121
|
+
scrolls?: Record<string, { start?: number; end?: number; scrub?: number; label?: string }>;
|
|
88
122
|
/**
|
|
89
123
|
* 状態の初期値 (optional)。 記法の `states:` と同じ (#1181)。
|
|
90
124
|
*
|
|
@@ -156,6 +190,17 @@ export interface JsonActor {
|
|
|
156
190
|
* 箱に出す題 (optional、 #1381)。 記法の `title:` と同じ。
|
|
157
191
|
*/
|
|
158
192
|
title?: string;
|
|
193
|
+
/**
|
|
194
|
+
* 値に追随する 5 欄 (optional、 #1392)。 記法の `wBind:` / `hBind:` / `opacity:` /
|
|
195
|
+
* `renderOffsetX:` / `renderOffsetY:` と同じ。
|
|
196
|
+
*
|
|
197
|
+
* 大きさは文字列だけ (数を書いても追随のしようが無い)、濃さとずらしは数も受ける。
|
|
198
|
+
*/
|
|
199
|
+
wBind?: string;
|
|
200
|
+
hBind?: string;
|
|
201
|
+
opacity?: number | string;
|
|
202
|
+
renderOffsetX?: number | string;
|
|
203
|
+
renderOffsetY?: number | string;
|
|
159
204
|
/**
|
|
160
205
|
* CAR-1657 unified syntax = 既存 NodeKind (28 個) に加えて parts identifier (arc-gauge 等) を
|
|
161
206
|
* accept する。 未知 kind 値は parts 候補として partId に格納、 compile 側 partsCatalog で解決。
|
|
@@ -274,6 +319,13 @@ export interface JsonStep {
|
|
|
274
319
|
style?: EdgeStyle;
|
|
275
320
|
guard?: string;
|
|
276
321
|
cardinality?: string;
|
|
322
|
+
/**
|
|
323
|
+
* 矢印を値に追随させる 3 欄 (optional、 #1396)。 記法の `widthBind:` / `strokeBind:` /
|
|
324
|
+
* `dashOffsetBind:` と同じ。 描画側は文字列だけを取る。
|
|
325
|
+
*/
|
|
326
|
+
widthBind?: string;
|
|
327
|
+
strokeBind?: string;
|
|
328
|
+
dashOffsetBind?: string;
|
|
277
329
|
labelOffsetX?: number;
|
|
278
330
|
labelOffsetY?: number;
|
|
279
331
|
/** true で説明文を矢印の線の上に重ねる。 分岐図の条件ラベル用。 */
|
|
@@ -378,6 +430,13 @@ export const ACCEPTED_KEYS = {
|
|
|
378
430
|
"groups",
|
|
379
431
|
// 値を見せる部品 (#1374)
|
|
380
432
|
"readouts",
|
|
433
|
+
// 読む人が動かすつまみ (#1389)
|
|
434
|
+
"inputs",
|
|
435
|
+
// つまみの値から決まる値 (#1391)
|
|
436
|
+
"formulas",
|
|
437
|
+
// 押下などの出来事で動く仕掛けと、巻き上げに応じて進む値 (#1393)
|
|
438
|
+
"events",
|
|
439
|
+
"scrolls",
|
|
381
440
|
],
|
|
382
441
|
actor: [
|
|
383
442
|
"name",
|
|
@@ -410,6 +469,12 @@ export const ACCEPTED_KEYS = {
|
|
|
410
469
|
"visibleIf",
|
|
411
470
|
// 箱に出す題 (#1381)
|
|
412
471
|
"title",
|
|
472
|
+
// 値に追随する 5 欄 (#1392)
|
|
473
|
+
"wBind",
|
|
474
|
+
"hBind",
|
|
475
|
+
"opacity",
|
|
476
|
+
"renderOffsetX",
|
|
477
|
+
"renderOffsetY",
|
|
413
478
|
],
|
|
414
479
|
step: [
|
|
415
480
|
"from",
|
|
@@ -422,6 +487,10 @@ export const ACCEPTED_KEYS = {
|
|
|
422
487
|
"style",
|
|
423
488
|
"guard",
|
|
424
489
|
"cardinality",
|
|
490
|
+
// 値に追随する 3 欄 (#1396)
|
|
491
|
+
"widthBind",
|
|
492
|
+
"strokeBind",
|
|
493
|
+
"dashOffsetBind",
|
|
425
494
|
"labelOffsetX",
|
|
426
495
|
"labelOffsetY",
|
|
427
496
|
"overlay",
|
|
@@ -470,6 +539,8 @@ export type 欄の型 =
|
|
|
470
539
|
| "文字列の並び"
|
|
471
540
|
| "数"
|
|
472
541
|
| "必須の数"
|
|
542
|
+
// 値に追随する欄 (#1392)。 数を直に書く形と、状態の名前を書く形の両方を受ける
|
|
543
|
+
| "数か文字列"
|
|
473
544
|
| "真偽"
|
|
474
545
|
| "色"
|
|
475
546
|
| "線種"
|
|
@@ -498,6 +569,13 @@ export const 欄の型表 = {
|
|
|
498
569
|
groups: "object",
|
|
499
570
|
// 値を見せる部品 (#1374)。 中身は下の検査が種類ごとに見る
|
|
500
571
|
readouts: "並び",
|
|
572
|
+
// 読む人が動かすつまみ (#1389)。 部品と同じく中身は下の検査が種類ごとに見る
|
|
573
|
+
inputs: "並び",
|
|
574
|
+
// つまみの値から決まる値 (#1391)。 中身は下の検査が式ごとに見る
|
|
575
|
+
formulas: "object",
|
|
576
|
+
// 押下と巻き上げ (#1393)。 中身は下の検査が 1 件ずつ見る
|
|
577
|
+
events: "並び",
|
|
578
|
+
scrolls: "object",
|
|
501
579
|
},
|
|
502
580
|
actor: {
|
|
503
581
|
name: "必須の非空文字列",
|
|
@@ -530,6 +608,12 @@ export const 欄の型表 = {
|
|
|
530
608
|
visibleIf: "文字列",
|
|
531
609
|
// 箱に出す題 (#1381)
|
|
532
610
|
title: "文字列",
|
|
611
|
+
// 値に追随する 5 欄 (#1392)。 大きさは文字列だけ、濃さとずらしは数も受ける
|
|
612
|
+
wBind: "非空の文字列",
|
|
613
|
+
hBind: "非空の文字列",
|
|
614
|
+
opacity: "数か文字列",
|
|
615
|
+
renderOffsetX: "数か文字列",
|
|
616
|
+
renderOffsetY: "数か文字列",
|
|
533
617
|
},
|
|
534
618
|
step: {
|
|
535
619
|
from: "必須の文字列",
|
|
@@ -542,6 +626,10 @@ export const 欄の型表 = {
|
|
|
542
626
|
style: "線種",
|
|
543
627
|
guard: "文字列",
|
|
544
628
|
cardinality: "文字列",
|
|
629
|
+
// 値に追随する 3 欄 (#1396)。 描画側は文字列だけを取る
|
|
630
|
+
widthBind: "非空の文字列",
|
|
631
|
+
strokeBind: "非空の文字列",
|
|
632
|
+
dashOffsetBind: "非空の文字列",
|
|
545
633
|
labelOffsetX: "数",
|
|
546
634
|
labelOffsetY: "数",
|
|
547
635
|
overlay: "真偽",
|
|
@@ -657,6 +745,12 @@ export const 見本に効かない欄 = [
|
|
|
657
745
|
"visibleIf",
|
|
658
746
|
// 箱に出す題 (#1381)。 見本は自分の題を持つ
|
|
659
747
|
"title",
|
|
748
|
+
// 値に追随する 5 欄 (#1392)。 見本は自分の大きさと出方を持つ
|
|
749
|
+
"wBind",
|
|
750
|
+
"hBind",
|
|
751
|
+
"opacity",
|
|
752
|
+
"renderOffsetX",
|
|
753
|
+
"renderOffsetY",
|
|
660
754
|
] as const satisfies readonly (typeof ACCEPTED_KEYS.actor)[number][];
|
|
661
755
|
|
|
662
756
|
/**
|
|
@@ -688,7 +782,7 @@ function 値を検査(
|
|
|
688
782
|
return;
|
|
689
783
|
case "非空の文字列":
|
|
690
784
|
if (v === undefined) return;
|
|
691
|
-
if (typeof v !== "string" || v.length === 0) 型違い("a non-empty string if present");
|
|
785
|
+
if (typeof v !== "string" || v.trim().length === 0) 型違い("a non-empty string if present");
|
|
692
786
|
return;
|
|
693
787
|
case "文字列":
|
|
694
788
|
if (v === undefined) return;
|
|
@@ -725,6 +819,20 @@ function 値を検査(
|
|
|
725
819
|
型違い("a finite number", typeof v === "number" ? `got ${String(v)}` : undefined);
|
|
726
820
|
}
|
|
727
821
|
return;
|
|
822
|
+
case "数か文字列":
|
|
823
|
+
if (v === undefined) return;
|
|
824
|
+
// 空文字は受けない。 描画側は 0 として読むため、書き忘れが「箱が消える」 形で出る
|
|
825
|
+
if (typeof v === "string") {
|
|
826
|
+
if (v.trim().length === 0) 型違い("a non-empty string if present");
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
if (typeof v !== "number" || !Number.isFinite(v)) {
|
|
830
|
+
型違い(
|
|
831
|
+
"a finite number or a non-empty string if present",
|
|
832
|
+
typeof v === "number" ? `got ${String(v)}` : undefined,
|
|
833
|
+
);
|
|
834
|
+
}
|
|
835
|
+
return;
|
|
728
836
|
case "真偽":
|
|
729
837
|
if (v === undefined) return;
|
|
730
838
|
if (typeof v !== "boolean") 型違い("true or false if present");
|
|
@@ -999,7 +1107,8 @@ function 表で中身を検査する(
|
|
|
999
1107
|
// JSON は公開 schema の enum と同じ正規名だけを受ける。 ここだけ小文字化すると、
|
|
1000
1108
|
// schema が拒む種類を validator が通した上、種類別の追加検査も回避できてしまう。
|
|
1001
1109
|
const kind = typeof o.kind === "string" ? o.kind : "";
|
|
1002
|
-
|
|
1110
|
+
// 通常の object を表に使うため、継承した名前を own kind として扱わない。
|
|
1111
|
+
const 定義 = Object.hasOwn(表, kind) ? 表[kind] : undefined;
|
|
1003
1112
|
if (定義 === undefined) {
|
|
1004
1113
|
errors.push({
|
|
1005
1114
|
path: `${path}.kind`,
|
|
@@ -1028,25 +1137,27 @@ function 表で中身を検査する(
|
|
|
1028
1137
|
? (typeof 値 === "number" && Number.isFinite(値)) || typeof 値 === "string"
|
|
1029
1138
|
: 形 === "文字列の並び"
|
|
1030
1139
|
? Array.isArray(値) && 値.every((x) => typeof x === "string")
|
|
1031
|
-
: 形 === "
|
|
1032
|
-
? typeof
|
|
1033
|
-
: 形 === "
|
|
1034
|
-
?
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
x
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
(
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
:
|
|
1140
|
+
: 形 === "数の並び"
|
|
1141
|
+
? Array.isArray(値) && 値.every((x) => typeof x === "number" && Number.isFinite(x))
|
|
1142
|
+
: 形 === "真偽"
|
|
1143
|
+
? typeof 値 === "boolean"
|
|
1144
|
+
: 形 === "組の並び"
|
|
1145
|
+
? Array.isArray(値) &&
|
|
1146
|
+
値.length > 0 &&
|
|
1147
|
+
値.every(
|
|
1148
|
+
(x) =>
|
|
1149
|
+
typeof x === "object" &&
|
|
1150
|
+
x !== null &&
|
|
1151
|
+
!Array.isArray(x) &&
|
|
1152
|
+
Object.keys(x as object).length > 0 &&
|
|
1153
|
+
Object.values(x as object).every(
|
|
1154
|
+
(y) =>
|
|
1155
|
+
typeof y === "string" || (typeof y === "number" && Number.isFinite(y)),
|
|
1156
|
+
),
|
|
1157
|
+
)
|
|
1158
|
+
: 形 === "向き"
|
|
1159
|
+
? typeof 値 === "string" && ["up", "down", "left", "right"].includes(値)
|
|
1160
|
+
: typeof 値 === "string";
|
|
1050
1161
|
if (!型が合う) {
|
|
1051
1162
|
errors.push({
|
|
1052
1163
|
path: `${path}.${欄}`,
|
|
@@ -1140,6 +1251,278 @@ function validateReadouts(v: unknown, errors: JsonDslError[]): void {
|
|
|
1140
1251
|
});
|
|
1141
1252
|
}
|
|
1142
1253
|
|
|
1254
|
+
/**
|
|
1255
|
+
* 読む人が動かすつまみの並びを検査する (#1389)。
|
|
1256
|
+
*
|
|
1257
|
+
* **外側の形もここで見る**。 `欄の型表` は「並び」 とだけ宣言し、中身の検査は専用の検査に
|
|
1258
|
+
* 委ねる作りなので (`checkFieldType` の `case "並び"`)、ここで見ないと `inputs: 1` が
|
|
1259
|
+
* 素通りする。
|
|
1260
|
+
*
|
|
1261
|
+
* 種類ごとの欄は記法と同じ表 (`つまみの表`) で見る。 表は描画側の型定義から生成しており、
|
|
1262
|
+
* 種類が増えても書き足す場所が増えない。
|
|
1263
|
+
*/
|
|
1264
|
+
function validateInputs(v: unknown, errors: JsonDslError[]): void {
|
|
1265
|
+
if (v === undefined) return;
|
|
1266
|
+
if (!Array.isArray(v)) {
|
|
1267
|
+
errors.push({
|
|
1268
|
+
path: "$.inputs",
|
|
1269
|
+
message: "inputs must be an array of input objects",
|
|
1270
|
+
hint: `got ${v === null ? "null" : typeof v}`,
|
|
1271
|
+
});
|
|
1272
|
+
return;
|
|
1273
|
+
}
|
|
1274
|
+
v.forEach((r, i) => {
|
|
1275
|
+
const path = `$.inputs[${i}]`;
|
|
1276
|
+
if (!r || typeof r !== "object" || Array.isArray(r)) {
|
|
1277
|
+
errors.push({ path, message: "input must be a plain object", hint: `got ${typeof r}` });
|
|
1278
|
+
return;
|
|
1279
|
+
}
|
|
1280
|
+
const o = r as Record<string, unknown>;
|
|
1281
|
+
if (typeof o.id !== "string" || o.id === "") {
|
|
1282
|
+
errors.push({ path: `${path}.id`, message: "id is required", hint: "空でない文字列で書く" });
|
|
1283
|
+
}
|
|
1284
|
+
表で中身を検査する(o, つまみの表, path, "input", errors);
|
|
1285
|
+
});
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
/**
|
|
1289
|
+
* つまみの値から決まる値を検査する (#1391)。
|
|
1290
|
+
*
|
|
1291
|
+
* **外側の形もここで見る**。 `欄の型表` は「object」 とだけ宣言するため、
|
|
1292
|
+
* ここで見ないと `formulas: { a: 1 }` が素通りする。
|
|
1293
|
+
*
|
|
1294
|
+
* 式そのものは描画側の parser に通す = 自前で書き方を決めると、通ったのに描画側が
|
|
1295
|
+
* 解けない式を受けてしまう。
|
|
1296
|
+
*/
|
|
1297
|
+
function validateFormulas(v: unknown, inputs: unknown, errors: JsonDslError[]): void {
|
|
1298
|
+
if (v === undefined) return;
|
|
1299
|
+
if (!v || typeof v !== "object" || Array.isArray(v)) {
|
|
1300
|
+
errors.push({
|
|
1301
|
+
path: "$.formulas",
|
|
1302
|
+
message: "formulas must be an object of name to expression",
|
|
1303
|
+
hint: `got ${v === null ? "null" : Array.isArray(v) ? "array" : typeof v}`,
|
|
1304
|
+
});
|
|
1305
|
+
return;
|
|
1306
|
+
}
|
|
1307
|
+
const つまみ = new Map<string, string>();
|
|
1308
|
+
if (Array.isArray(inputs)) {
|
|
1309
|
+
for (const input of inputs) {
|
|
1310
|
+
if (input && typeof input === "object" && !Array.isArray(input)) {
|
|
1311
|
+
const candidate = input as { id?: unknown; kind?: unknown };
|
|
1312
|
+
if (typeof candidate.id === "string" && typeof candidate.kind === "string") {
|
|
1313
|
+
つまみ.set(candidate.id, candidate.kind);
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
const 先に書かれた式 = new Set<string>();
|
|
1319
|
+
for (const [名前, 式] of Object.entries(v as Record<string, unknown>)) {
|
|
1320
|
+
const path = `$.formulas.${名前}`;
|
|
1321
|
+
if (!isValueName(名前)) {
|
|
1322
|
+
errors.push({ path, ...valueNameIssue(名前) });
|
|
1323
|
+
continue;
|
|
1324
|
+
}
|
|
1325
|
+
if (typeof 式 !== "string" || 式.trim() === "") {
|
|
1326
|
+
errors.push({
|
|
1327
|
+
path,
|
|
1328
|
+
message: `${名前} must be a non-empty expression string`,
|
|
1329
|
+
hint: `got ${Array.isArray(式) ? "array" : 式 === null ? "null" : typeof 式}`,
|
|
1330
|
+
});
|
|
1331
|
+
continue;
|
|
1332
|
+
}
|
|
1333
|
+
try {
|
|
1334
|
+
const names = extractIdentifiers(parseFormula(式));
|
|
1335
|
+
if (つまみ.has(名前) || 先に書かれた式.has(名前)) {
|
|
1336
|
+
errors.push({
|
|
1337
|
+
path,
|
|
1338
|
+
message: `${名前} collides with an input or an earlier formula`,
|
|
1339
|
+
hint: "inputs と formulas では重ならない名前を使う",
|
|
1340
|
+
});
|
|
1341
|
+
continue;
|
|
1342
|
+
}
|
|
1343
|
+
let valid = true;
|
|
1344
|
+
for (const name of names) {
|
|
1345
|
+
if (先に書かれた式.has(name)) continue;
|
|
1346
|
+
const kind = つまみ.get(name);
|
|
1347
|
+
if (["slider", "number", "stepper", "timeline", "toggle"].includes(kind ?? "")) continue;
|
|
1348
|
+
valid = false;
|
|
1349
|
+
errors.push({
|
|
1350
|
+
path,
|
|
1351
|
+
message: `${名前} references an unavailable formula identifier`,
|
|
1352
|
+
hint: `${name} は数値/真偽の input にするか、この式より前の formula に書く`,
|
|
1353
|
+
});
|
|
1354
|
+
}
|
|
1355
|
+
if (valid) 先に書かれた式.add(名前);
|
|
1356
|
+
} catch (e) {
|
|
1357
|
+
errors.push({
|
|
1358
|
+
path,
|
|
1359
|
+
message: `${名前} is not a readable expression`,
|
|
1360
|
+
hint: (e as Error).message,
|
|
1361
|
+
});
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
/**
|
|
1367
|
+
* 押下などの出来事で動く仕掛けを検査する (#1393)。
|
|
1368
|
+
*
|
|
1369
|
+
* 相手の指し方は 4 つあり、**ちょうど 1 つだけ書く**。 2 つ書くとどちらを指したのか
|
|
1370
|
+
* 決まらず、0 なら相手がいない。 記法側の読み取りと同じ規則にする。
|
|
1371
|
+
*/
|
|
1372
|
+
function validateEvents(v: unknown, errors: JsonDslError[]): void {
|
|
1373
|
+
if (v === undefined) return;
|
|
1374
|
+
if (!Array.isArray(v)) {
|
|
1375
|
+
errors.push({
|
|
1376
|
+
path: "$.events",
|
|
1377
|
+
message: "events must be an array of event objects",
|
|
1378
|
+
hint: `got ${v === null ? "null" : typeof v}`,
|
|
1379
|
+
});
|
|
1380
|
+
return;
|
|
1381
|
+
}
|
|
1382
|
+
const 相手の鍵 = ["box", "lane", "arrow", "diagram"];
|
|
1383
|
+
v.forEach((e, i) => {
|
|
1384
|
+
const path = `$.events[${i}]`;
|
|
1385
|
+
if (!e || typeof e !== "object" || Array.isArray(e)) {
|
|
1386
|
+
errors.push({ path, message: "event must be a plain object", hint: `got ${typeof e}` });
|
|
1387
|
+
return;
|
|
1388
|
+
}
|
|
1389
|
+
const o = e as Record<string, unknown>;
|
|
1390
|
+
for (const k of Object.keys(o)) {
|
|
1391
|
+
if (k === "on" || k === "handler" || 相手の鍵.includes(k)) continue;
|
|
1392
|
+
errors.push({
|
|
1393
|
+
path: `${path}.${k}`,
|
|
1394
|
+
message: `unknown key "${k}"`,
|
|
1395
|
+
hint: `使える項目 = on, handler, ${相手の鍵.join(", ")}`,
|
|
1396
|
+
});
|
|
1397
|
+
}
|
|
1398
|
+
if (typeof o.on !== "string" || !(EVENT_KINDS as readonly string[]).includes(o.on)) {
|
|
1399
|
+
errors.push({
|
|
1400
|
+
path: `${path}.on`,
|
|
1401
|
+
message: "on must be a known event kind",
|
|
1402
|
+
hint: `使える種類 = ${EVENT_KINDS.join(", ")}`,
|
|
1403
|
+
});
|
|
1404
|
+
}
|
|
1405
|
+
if (typeof o.handler !== "string" || o.handler.trim() === "") {
|
|
1406
|
+
errors.push({
|
|
1407
|
+
path: `${path}.handler`,
|
|
1408
|
+
message: "handler is required",
|
|
1409
|
+
hint: "空でない文字列で書く",
|
|
1410
|
+
});
|
|
1411
|
+
}
|
|
1412
|
+
const 書いた = 相手の鍵.filter((k) => o[k] !== undefined);
|
|
1413
|
+
if (書いた.length !== 1) {
|
|
1414
|
+
errors.push({
|
|
1415
|
+
path,
|
|
1416
|
+
message:
|
|
1417
|
+
書いた.length === 0 ? "event target is required" : "event target must be written once",
|
|
1418
|
+
hint: `${相手の鍵.join(" / ")} のどれか 1 つだけを書く`,
|
|
1419
|
+
});
|
|
1420
|
+
return;
|
|
1421
|
+
}
|
|
1422
|
+
const 鍵 = 書いた[0]!;
|
|
1423
|
+
if (鍵 === "diagram") {
|
|
1424
|
+
if (o.diagram !== true) {
|
|
1425
|
+
errors.push({
|
|
1426
|
+
path: `${path}.diagram`,
|
|
1427
|
+
message: "diagram must be true",
|
|
1428
|
+
hint: "図全体を指す時だけ書く",
|
|
1429
|
+
});
|
|
1430
|
+
}
|
|
1431
|
+
return;
|
|
1432
|
+
}
|
|
1433
|
+
if (typeof o[鍵] !== "string" || (o[鍵] as string).trim() === "") {
|
|
1434
|
+
errors.push({ path: `${path}.${鍵}`, message: `${鍵} must be a non-empty string` });
|
|
1435
|
+
return;
|
|
1436
|
+
}
|
|
1437
|
+
if (鍵 === "arrow") {
|
|
1438
|
+
const 両端 = (o.arrow as string).split("->");
|
|
1439
|
+
if (両端.length !== 2 || 両端.some((x) => x.trim() === "")) {
|
|
1440
|
+
errors.push({
|
|
1441
|
+
path: `${path}.arrow`,
|
|
1442
|
+
message: "arrow must be `A -> B`",
|
|
1443
|
+
hint: "矢印の両端の名前を書く",
|
|
1444
|
+
});
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
});
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
/**
|
|
1451
|
+
* 巻き上げに応じて進む値を検査する (#1393)。
|
|
1452
|
+
*
|
|
1453
|
+
* 形は `{ 名前: { start, end, scrub, label } }`。 名前の規則は状態と揃える。
|
|
1454
|
+
*/
|
|
1455
|
+
function validateScrolls(
|
|
1456
|
+
v: unknown,
|
|
1457
|
+
inputs: unknown,
|
|
1458
|
+
formulas: unknown,
|
|
1459
|
+
errors: JsonDslError[],
|
|
1460
|
+
): void {
|
|
1461
|
+
if (v === undefined) return;
|
|
1462
|
+
if (!v || typeof v !== "object" || Array.isArray(v)) {
|
|
1463
|
+
errors.push({
|
|
1464
|
+
path: "$.scrolls",
|
|
1465
|
+
message: "scrolls must be an object of name to spec",
|
|
1466
|
+
hint: `got ${v === null ? "null" : Array.isArray(v) ? "array" : typeof v}`,
|
|
1467
|
+
});
|
|
1468
|
+
return;
|
|
1469
|
+
}
|
|
1470
|
+
const 既に値を作る名前 = new Set<string>();
|
|
1471
|
+
if (Array.isArray(inputs)) {
|
|
1472
|
+
for (const input of inputs) {
|
|
1473
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) continue;
|
|
1474
|
+
const id = (input as { id?: unknown }).id;
|
|
1475
|
+
if (typeof id === "string") 既に値を作る名前.add(id);
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
if (formulas && typeof formulas === "object" && !Array.isArray(formulas)) {
|
|
1479
|
+
for (const id of Object.keys(formulas)) 既に値を作る名前.add(id);
|
|
1480
|
+
}
|
|
1481
|
+
for (const [名前, spec] of Object.entries(v as Record<string, unknown>)) {
|
|
1482
|
+
const path = `$.scrolls.${名前}`;
|
|
1483
|
+
if (!isValueName(名前)) {
|
|
1484
|
+
errors.push({ path, ...valueNameIssue(名前) });
|
|
1485
|
+
continue;
|
|
1486
|
+
}
|
|
1487
|
+
if (既に値を作る名前.has(名前)) {
|
|
1488
|
+
errors.push({
|
|
1489
|
+
path,
|
|
1490
|
+
message: `${名前} collides with an input or formula`,
|
|
1491
|
+
hint: "inputs / formulas / scrolls では重ならない名前を使う",
|
|
1492
|
+
});
|
|
1493
|
+
}
|
|
1494
|
+
if (!spec || typeof spec !== "object" || Array.isArray(spec)) {
|
|
1495
|
+
errors.push({ path, message: `${名前} must be a plain object`, hint: `got ${typeof spec}` });
|
|
1496
|
+
continue;
|
|
1497
|
+
}
|
|
1498
|
+
for (const [k, x] of Object.entries(spec as Record<string, unknown>)) {
|
|
1499
|
+
if (k === "label") {
|
|
1500
|
+
if (typeof x !== "string") {
|
|
1501
|
+
errors.push({ path: `${path}.label`, message: "label must be a string" });
|
|
1502
|
+
}
|
|
1503
|
+
continue;
|
|
1504
|
+
}
|
|
1505
|
+
if (k !== "start" && k !== "end" && k !== "scrub") {
|
|
1506
|
+
errors.push({
|
|
1507
|
+
path: `${path}.${k}`,
|
|
1508
|
+
message: `unknown key "${k}"`,
|
|
1509
|
+
hint: "使える項目 = start, end, scrub, label",
|
|
1510
|
+
});
|
|
1511
|
+
continue;
|
|
1512
|
+
}
|
|
1513
|
+
if (typeof x !== "number" || !Number.isFinite(x)) {
|
|
1514
|
+
errors.push({ path: `${path}.${k}`, message: `${k} must be a finite number` });
|
|
1515
|
+
} else if (x < 0 || x > 1) {
|
|
1516
|
+
errors.push({
|
|
1517
|
+
path: `${path}.${k}`,
|
|
1518
|
+
message: `${k} must be between 0 and 1`,
|
|
1519
|
+
hint: "0 と 1 を含む範囲で書く",
|
|
1520
|
+
});
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1143
1526
|
/**
|
|
1144
1527
|
* 箱の中に描く図形を検査する (#1374)。
|
|
1145
1528
|
*
|
|
@@ -1478,6 +1861,13 @@ function validateJson(
|
|
|
1478
1861
|
checkUnknownKeys(j, "root", "$", errors);
|
|
1479
1862
|
// 値を見せる部品の中身を、記法と同じ表で見る (#1374)
|
|
1480
1863
|
validateReadouts(j.readouts, errors);
|
|
1864
|
+
// 読む人が動かすつまみの中身も、記法と同じ表で見る (#1389)
|
|
1865
|
+
validateInputs(j.inputs, errors);
|
|
1866
|
+
// 式は描画側の parser に通す (#1391)
|
|
1867
|
+
validateFormulas(j.formulas, j.inputs, errors);
|
|
1868
|
+
// 押下と巻き上げ (#1393)
|
|
1869
|
+
validateEvents(j.events, errors);
|
|
1870
|
+
validateScrolls(j.scrolls, j.inputs, j.formulas, errors);
|
|
1481
1871
|
|
|
1482
1872
|
// 値そのものの型は表が見る (#1304)。 図表の箱の上の小見出し (#1247) の空文字は
|
|
1483
1873
|
// 「書かなかった」 と同じ扱いにするため通す (記法側の `eyebrow:` と揃える。 落とすのは `jsonToDoc`)
|
|
@@ -1780,6 +2170,12 @@ export function jsonToDoc(json: DragonJson): DslDocument {
|
|
|
1780
2170
|
shape: isPart ? undefined : a.shape,
|
|
1781
2171
|
visibleIf: isPart ? undefined : a.visibleIf,
|
|
1782
2172
|
title: isPart ? undefined : a.title,
|
|
2173
|
+
// 値に追随する 5 欄 (#1392)。 記法と同じく見本には渡さない
|
|
2174
|
+
wBind: isPart ? undefined : a.wBind,
|
|
2175
|
+
hBind: isPart ? undefined : a.hBind,
|
|
2176
|
+
opacity: isPart ? undefined : a.opacity,
|
|
2177
|
+
renderOffsetX: isPart ? undefined : a.renderOffsetX,
|
|
2178
|
+
renderOffsetY: isPart ? undefined : a.renderOffsetY,
|
|
1783
2179
|
// 普通の箱にしか効かない欄は見本では落とす。 落とす欄の一覧は `見本に効かない欄` が
|
|
1784
2180
|
// 唯一の出どころで、検査 (#1308) も同じ表を見る = 「検査は通すが組み立てが捨てる」
|
|
1785
2181
|
// 状態が作れない
|
|
@@ -1831,6 +2227,10 @@ export function jsonToDoc(json: DragonJson): DslDocument {
|
|
|
1831
2227
|
style: s.style,
|
|
1832
2228
|
guard: s.guard,
|
|
1833
2229
|
cardinality: s.cardinality,
|
|
2230
|
+
// 値に追随する 3 欄 (#1396)
|
|
2231
|
+
widthBind: s.widthBind,
|
|
2232
|
+
strokeBind: s.strokeBind,
|
|
2233
|
+
dashOffsetBind: s.dashOffsetBind,
|
|
1834
2234
|
labelOffsetX: s.labelOffsetX,
|
|
1835
2235
|
labelOffsetY: s.labelOffsetY,
|
|
1836
2236
|
overlay: s.overlay,
|
|
@@ -1842,6 +2242,37 @@ export function jsonToDoc(json: DragonJson): DslDocument {
|
|
|
1842
2242
|
// 分けると `states` だけを書いた JSON で値が 1 つも届かない (記法側で起きていた形、 #1181)
|
|
1843
2243
|
// 値を見せる部品はそのまま渡す (#1374)。 形は描画側の型が縛る
|
|
1844
2244
|
const readouts: DslReadout[] | undefined = json.readouts ? [...json.readouts] : undefined;
|
|
2245
|
+
// つまみもそのまま渡す (#1389)。 形は描画側の型が縛る
|
|
2246
|
+
const inputs: DslInput[] | undefined = json.inputs ? [...json.inputs] : undefined;
|
|
2247
|
+
/*
|
|
2248
|
+
* 押下と巻き上げも記法側と同じ形へ写す (#1393)。
|
|
2249
|
+
*
|
|
2250
|
+
* 相手は名前のまま持ち、識別子への読み替えは組み立てが行う = 2 つの入口で同じ経路を通る。
|
|
2251
|
+
*/
|
|
2252
|
+
const events: DslEventBinding[] | undefined = json.events?.map((e) => ({
|
|
2253
|
+
event: e.on as DslEventBinding["event"],
|
|
2254
|
+
target:
|
|
2255
|
+
e.diagram === true
|
|
2256
|
+
? ({ kind: "diagram" } as const)
|
|
2257
|
+
: e.arrow !== undefined
|
|
2258
|
+
? ({
|
|
2259
|
+
kind: "edge" as const,
|
|
2260
|
+
from: e.arrow.split("->")[0]?.trim() ?? "",
|
|
2261
|
+
to: e.arrow.split("->")[1]?.trim() ?? "",
|
|
2262
|
+
} as const)
|
|
2263
|
+
: e.lane !== undefined
|
|
2264
|
+
? ({ kind: "lane" as const, name: e.lane } as const)
|
|
2265
|
+
: ({ kind: "node" as const, name: e.box ?? "" } as const),
|
|
2266
|
+
handlerId: e.handler.trim(),
|
|
2267
|
+
pos: p0,
|
|
2268
|
+
}));
|
|
2269
|
+
const scrolls: DslScrollTrigger[] | undefined = json.scrolls
|
|
2270
|
+
? Object.entries(json.scrolls).map(([id, spec]) => ({ id, ...spec }))
|
|
2271
|
+
: undefined;
|
|
2272
|
+
// 式は `{ 名前: "式" }` から並びへ写す (#1391)。 記法側と同じ形にして組み立てを 1 本にする
|
|
2273
|
+
const formulas: DslFormula[] | undefined = json.formulas
|
|
2274
|
+
? Object.entries(json.formulas).map(([id, expression]) => ({ id, expression, pos: p0 }))
|
|
2275
|
+
: undefined;
|
|
1845
2276
|
const states: DslState[] = Object.entries(json.states ?? {}).map(([name, initial]) => ({
|
|
1846
2277
|
name,
|
|
1847
2278
|
initial,
|
|
@@ -1916,6 +2347,10 @@ export function jsonToDoc(json: DragonJson): DslDocument {
|
|
|
1916
2347
|
)
|
|
1917
2348
|
: undefined,
|
|
1918
2349
|
readouts,
|
|
2350
|
+
inputs,
|
|
2351
|
+
formulas,
|
|
2352
|
+
events,
|
|
2353
|
+
scrolls,
|
|
1919
2354
|
pos: p0,
|
|
1920
2355
|
};
|
|
1921
2356
|
}
|