@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/README.md +74 -58
- package/dist/index.cjs +2863 -119
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +544 -2
- package/dist/index.d.ts +544 -2
- package/dist/index.js +2863 -119
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/compile.ts +457 -188
- package/src/input-size.ts +9 -1
- package/src/json-parser.ts +259 -1
- package/src/schemas/diagram.json +985 -3
- package/src/types.ts +39 -1
- package/src/v05/parser-types.ts +17 -0
- package/src/v05/parser.ts +544 -20
- package/src/v05/readout-table.generated.ts +1088 -0
package/src/v05/parser.ts
CHANGED
|
@@ -56,6 +56,8 @@ import type {
|
|
|
56
56
|
DslAxes,
|
|
57
57
|
DslDocument,
|
|
58
58
|
DslActor,
|
|
59
|
+
DslDynShape,
|
|
60
|
+
DslReadout,
|
|
59
61
|
DslActorNodeOverride,
|
|
60
62
|
DslStep,
|
|
61
63
|
DslAnimate,
|
|
@@ -73,6 +75,9 @@ import type {
|
|
|
73
75
|
|
|
74
76
|
export type V05ParseResult = { ok: true; doc: DslDocument } | { ok: false; errors: DslError[] };
|
|
75
77
|
|
|
78
|
+
/** 矢印を出す辺。 Text DSL / JSON validator / 公開 schema の 3 経路で同じ 4 値を使う。 */
|
|
79
|
+
export const EDGE_SIDE_VALUES = ["top", "right", "bottom", "left"] as const;
|
|
80
|
+
|
|
76
81
|
/**
|
|
77
82
|
* 記法が受ける top-level の項目 (#1190)。
|
|
78
83
|
*
|
|
@@ -95,6 +100,8 @@ export const TOP_LEVEL_KEYS = [
|
|
|
95
100
|
"eyebrow",
|
|
96
101
|
// 2 軸で仕分ける図の軸の名前 (#1251)
|
|
97
102
|
"axes",
|
|
103
|
+
// 値を見せる部品 (#1374)
|
|
104
|
+
"readouts",
|
|
98
105
|
] as const;
|
|
99
106
|
|
|
100
107
|
/**
|
|
@@ -116,6 +123,53 @@ export const TOP_LEVEL_KEYS = [
|
|
|
116
123
|
*/
|
|
117
124
|
const LANE_ID_ENTRY = /^([\p{L}\p{N}_-]+)\s*:\s*\{([^}]*)\}\s*$/u;
|
|
118
125
|
|
|
126
|
+
/**
|
|
127
|
+
* `id: { ... }` の 1 行を、名前と中括弧の中身に割る (#1381)。
|
|
128
|
+
*
|
|
129
|
+
* `LANE_ID_ENTRY` は中括弧を `[^}]*` で読むため、中身に中括弧を含む形
|
|
130
|
+
* (`map: [{ value: "a" }]`) を受けられない。 部品の欄には組の並びを取るものがあるので、
|
|
131
|
+
* 引用符の内側を飛ばしつつ中括弧の深さを数えて割る。
|
|
132
|
+
*
|
|
133
|
+
* 割れない形は `undefined` を返す。 呼び手が行番号付きで知らせる。
|
|
134
|
+
*/
|
|
135
|
+
function 名前と中括弧に割る(行: string): [string, string] | undefined {
|
|
136
|
+
const t = 行.trim();
|
|
137
|
+
const c = t.indexOf(":");
|
|
138
|
+
if (c < 0) return undefined;
|
|
139
|
+
const 名前 = t.slice(0, c).trim();
|
|
140
|
+
if (!/^[\p{L}\p{N}_-]+$/u.test(名前)) return undefined;
|
|
141
|
+
const 残り = t.slice(c + 1).trim();
|
|
142
|
+
if (!残り.startsWith("{")) return undefined;
|
|
143
|
+
|
|
144
|
+
let 深さ = 0;
|
|
145
|
+
let 引用: string | null = null;
|
|
146
|
+
let 直前: string | null = null;
|
|
147
|
+
for (let i = 0; i < 残り.length; i += 1) {
|
|
148
|
+
const ch = 残り[i]!;
|
|
149
|
+
if (引用 !== null) {
|
|
150
|
+
if (引用 === '"' && ch === "\\" && i + 1 < 残り.length) {
|
|
151
|
+
i += 1;
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (ch === 引用) 引用 = null;
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if ((ch === '"' || ch === "'") && (直前 === null || ":,{[".includes(直前))) {
|
|
158
|
+
引用 = ch;
|
|
159
|
+
直前 = ch;
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (ch === "{") 深さ += 1;
|
|
163
|
+
else if (ch === "}") {
|
|
164
|
+
深さ -= 1;
|
|
165
|
+
// 中括弧が閉じた後に文字が残る形は割らない (`{...} x`)
|
|
166
|
+
if (深さ === 0) return i === 残り.length - 1 ? [名前, 残り.slice(1, i)] : undefined;
|
|
167
|
+
}
|
|
168
|
+
if (!/\s/u.test(ch)) 直前 = ch;
|
|
169
|
+
}
|
|
170
|
+
return undefined;
|
|
171
|
+
}
|
|
172
|
+
|
|
119
173
|
function isTopLevelKey(key: string): key is (typeof TOP_LEVEL_KEYS)[number] {
|
|
120
174
|
return (TOP_LEVEL_KEYS as readonly string[]).includes(key);
|
|
121
175
|
}
|
|
@@ -278,6 +332,7 @@ export function parseTextDslV05(src: string): V05ParseResult {
|
|
|
278
332
|
const values: DslValue[] = [];
|
|
279
333
|
let viewport: DslViewport | undefined = undefined;
|
|
280
334
|
let lanesMap: Record<string, DslLane> | undefined = undefined;
|
|
335
|
+
let readoutsList: DslReadout[] | undefined = undefined;
|
|
281
336
|
let groupsMap: Record<string, DslGroup> | undefined = undefined;
|
|
282
337
|
|
|
283
338
|
let i = 0;
|
|
@@ -539,6 +594,38 @@ export function parseTextDslV05(src: string): V05ParseResult {
|
|
|
539
594
|
i = next;
|
|
540
595
|
continue;
|
|
541
596
|
}
|
|
597
|
+
if (head.key === "readouts") {
|
|
598
|
+
// readouts:\n ring: { kind: percent-ring, source: total, max: 500, label: "..." }
|
|
599
|
+
// 1 行にまとめた形は受けない。 黙って空の並びにすると、書いた部品が全て消えた図になる。
|
|
600
|
+
if (head.value !== null && head.value.trim() !== "") {
|
|
601
|
+
errors.push({
|
|
602
|
+
line: line.no,
|
|
603
|
+
message: "readouts は 1 行にまとめて書けない",
|
|
604
|
+
hint: "次の行から字下げして `id: { kind: percent-ring, source: total, max: 100 }` の形で並べる",
|
|
605
|
+
});
|
|
606
|
+
i += 1;
|
|
607
|
+
continue;
|
|
608
|
+
}
|
|
609
|
+
// `collectIndentedList` は `:` の無い行を落とす。 部品を綴り違えた行も知らせるため、
|
|
610
|
+
// 字下げした行を全て読み手へ渡す。
|
|
611
|
+
const { items, next } = collectIndentedRaw(lines, i + 1, line.indent);
|
|
612
|
+
readoutsList = [];
|
|
613
|
+
for (const it of items) {
|
|
614
|
+
const m = 名前と中括弧に割る(it.trimmed);
|
|
615
|
+
if (m) {
|
|
616
|
+
const 読めた = 部品として読む(m[0], m[1], it.no, errors);
|
|
617
|
+
if (読めた) readoutsList.push(読めた);
|
|
618
|
+
} else {
|
|
619
|
+
errors.push({
|
|
620
|
+
line: it.no,
|
|
621
|
+
message: `invalid readout entry: "${it.trimmed}"`,
|
|
622
|
+
hint: "use `id: { kind: percent-ring, source: total, max: 500 }`",
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
i = next;
|
|
627
|
+
continue;
|
|
628
|
+
}
|
|
542
629
|
if (head.key === "groups") {
|
|
543
630
|
// groups:\n aws: { label: "AWS", lanes: [ecs, rds] }
|
|
544
631
|
const { items, next } = collectIndentedList(lines, i + 1, line.indent);
|
|
@@ -597,6 +684,7 @@ export function parseTextDslV05(src: string): V05ParseResult {
|
|
|
597
684
|
...(values.length > 0 ? { values } : {}),
|
|
598
685
|
viewport,
|
|
599
686
|
lanes: lanesMap,
|
|
687
|
+
readouts: readoutsList,
|
|
600
688
|
groups: groupsMap,
|
|
601
689
|
pos: { line: 1 },
|
|
602
690
|
},
|
|
@@ -937,6 +1025,367 @@ export const LANE_VALUE_KINDS = {
|
|
|
937
1025
|
lifeline: "真偽",
|
|
938
1026
|
} as const satisfies Record<string, 値の形>;
|
|
939
1027
|
|
|
1028
|
+
/**
|
|
1029
|
+
* 箱の中に描く図形 (`shape:`) と、値を見せる部品 (`readouts:`) の欄 (#1374)。
|
|
1030
|
+
*
|
|
1031
|
+
* どちらも描画側 (`CdlDynShape` / `CdlReadout`) の形をそのまま渡す。 記法の値は全て文字列で
|
|
1032
|
+
* 届くため、欄ごとに「数」 「文字列」 「数か文字列」 のどれとして読むかを表で持つ。
|
|
1033
|
+
*
|
|
1034
|
+
* **「数か文字列」 は状態を追いかける欄**。 `level: 80` のように数を直接書くこともできるし、
|
|
1035
|
+
* `level: "{s1}"` のように状態の名前を書いて段の中で動かすこともできる。 数として読めた時
|
|
1036
|
+
* だけ数にし、読めなければ文字列のまま渡す。
|
|
1037
|
+
*
|
|
1038
|
+
* ## 表を持つ理由
|
|
1039
|
+
*
|
|
1040
|
+
* 表が無いと「知らない欄を黙って捨てる」 か「何でも通す」 のどちらかになる。 前者は書いた
|
|
1041
|
+
* 指定が消え、後者は綴り違いがそのまま描画側へ流れて別の形で失敗する。 表があれば
|
|
1042
|
+
* 書いた場所と使える欄を添えて知らせられる。
|
|
1043
|
+
*/
|
|
1044
|
+
// 欄の形と定義の型は `parser-types.ts` が持つ (#1385)。 生成した部品の表がこの型を使うため、
|
|
1045
|
+
// `parser.ts` に置いたままだと 生成した表 → parser → 生成した表 の輪ができる
|
|
1046
|
+
export type { 欄の形, 図形の定義 } from "./parser-types";
|
|
1047
|
+
|
|
1048
|
+
/** 描ける図形と、その欄 (`CdlDynShape` の全 5 種を覆う) */
|
|
1049
|
+
export const 図形の表: Record<string, 図形の定義> = {
|
|
1050
|
+
rect: {
|
|
1051
|
+
必須: ["source", "fillMax"],
|
|
1052
|
+
欄: {
|
|
1053
|
+
source: "数か文字列",
|
|
1054
|
+
fillMax: "数",
|
|
1055
|
+
orient: "向き",
|
|
1056
|
+
fill: "文字列",
|
|
1057
|
+
stroke: "文字列",
|
|
1058
|
+
radius: "数",
|
|
1059
|
+
},
|
|
1060
|
+
},
|
|
1061
|
+
circle: {
|
|
1062
|
+
必須: [],
|
|
1063
|
+
欄: { radius: "数か文字列", fillProgress: "数か文字列", fill: "文字列", stroke: "文字列" },
|
|
1064
|
+
},
|
|
1065
|
+
arc: {
|
|
1066
|
+
必須: ["angle"],
|
|
1067
|
+
欄: {
|
|
1068
|
+
innerRadius: "数",
|
|
1069
|
+
outerRadius: "数",
|
|
1070
|
+
startAngle: "数",
|
|
1071
|
+
angle: "数か文字列",
|
|
1072
|
+
sweepMax: "数",
|
|
1073
|
+
fill: "文字列",
|
|
1074
|
+
stroke: "文字列",
|
|
1075
|
+
},
|
|
1076
|
+
},
|
|
1077
|
+
wave: {
|
|
1078
|
+
必須: ["level", "amplitude"],
|
|
1079
|
+
欄: {
|
|
1080
|
+
level: "数か文字列",
|
|
1081
|
+
amplitude: "数",
|
|
1082
|
+
frequency: "数",
|
|
1083
|
+
waveHeight: "数",
|
|
1084
|
+
fill: "文字列",
|
|
1085
|
+
stroke: "文字列",
|
|
1086
|
+
},
|
|
1087
|
+
},
|
|
1088
|
+
polygon: {
|
|
1089
|
+
必須: ["sides"],
|
|
1090
|
+
欄: {
|
|
1091
|
+
sides: "数",
|
|
1092
|
+
radius: "数か文字列",
|
|
1093
|
+
rotation: "数か文字列",
|
|
1094
|
+
fill: "文字列",
|
|
1095
|
+
stroke: "文字列",
|
|
1096
|
+
},
|
|
1097
|
+
},
|
|
1098
|
+
};
|
|
1099
|
+
|
|
1100
|
+
/**
|
|
1101
|
+
* 記法が受ける部品と、その欄。
|
|
1102
|
+
*
|
|
1103
|
+
* **手で書かず、描画側の型定義から生成する** (#1385)。 描画側は 107 種を持ち欄は 510 個あり、
|
|
1104
|
+
* 手で写すと写し間違いと描画側の変更への drift が残る (`rules/quality.md § 導出可能記述は
|
|
1105
|
+
* 人手で書かない`)。
|
|
1106
|
+
*
|
|
1107
|
+
* 作り直す = `node packages/dragon/scripts/gen-readout-table.mjs`
|
|
1108
|
+
* ずれの検知 = `packages/dragon/test/readout-table-generated.test.ts`
|
|
1109
|
+
*/
|
|
1110
|
+
export { 部品の表, 部品の組の表 } from "./readout-table.generated";
|
|
1111
|
+
import { 部品の表, 部品の組の表 } from "./readout-table.generated";
|
|
1112
|
+
import type { 図形の定義 } from "./parser-types";
|
|
1113
|
+
|
|
1114
|
+
/** `[a, b]` の形を文字列の並びに読む */
|
|
1115
|
+
function 並びとして読む(raw: string): string[] {
|
|
1116
|
+
return raw
|
|
1117
|
+
.replace(/^\[|\]$/g, "")
|
|
1118
|
+
.split(/,(?![^[]*\])/)
|
|
1119
|
+
.map((x) => stripQuotes(x.trim()))
|
|
1120
|
+
.filter((x) => x !== "");
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
/**
|
|
1124
|
+
* `[{ a: 1 }, { b: 2 }]` の形を、中括弧ごとの組の並びに読む (#1381)。
|
|
1125
|
+
*
|
|
1126
|
+
* `並びとして読む` は葉の値しか想定していないため、中括弧を含む形を渡すと項目の途中で
|
|
1127
|
+
* 割れる。 中括弧の深さを数えて、深さ 0 の位置だけで割る。
|
|
1128
|
+
*
|
|
1129
|
+
* 読めない形は捨てずに `undefined` を返す。 呼び手が行番号付きで知らせる = 黙って捨てると
|
|
1130
|
+
* 「書いたのに出ない」 が手掛かりなしで起きる。
|
|
1131
|
+
*/
|
|
1132
|
+
function 組の並びとして読む(raw: string): Record<string, string>[] | undefined {
|
|
1133
|
+
const 中身 = raw.trim();
|
|
1134
|
+
if (!中身.startsWith("[") || !中身.endsWith("]")) return undefined;
|
|
1135
|
+
const 本体 = 中身.slice(1, -1);
|
|
1136
|
+
|
|
1137
|
+
const 塊: string[] = [];
|
|
1138
|
+
let i = 0;
|
|
1139
|
+
let 次は組 = true;
|
|
1140
|
+
while (i < 本体.length) {
|
|
1141
|
+
while (i < 本体.length && /\s/u.test(本体[i]!)) i += 1;
|
|
1142
|
+
if (i >= 本体.length) break;
|
|
1143
|
+
|
|
1144
|
+
if (!次は組) {
|
|
1145
|
+
if (本体[i] !== ",") return undefined;
|
|
1146
|
+
次は組 = true;
|
|
1147
|
+
i += 1;
|
|
1148
|
+
continue;
|
|
1149
|
+
}
|
|
1150
|
+
if (本体[i] !== "{") return undefined;
|
|
1151
|
+
|
|
1152
|
+
const 始まり = i + 1;
|
|
1153
|
+
let 引用: '"' | "'" | null = null;
|
|
1154
|
+
let 直前: string | null = "{";
|
|
1155
|
+
i += 1;
|
|
1156
|
+
for (; i < 本体.length; i += 1) {
|
|
1157
|
+
const c = 本体[i]!;
|
|
1158
|
+
if (引用 !== null) {
|
|
1159
|
+
if (引用 === '"' && c === "\\" && i + 1 < 本体.length) {
|
|
1160
|
+
i += 1;
|
|
1161
|
+
continue;
|
|
1162
|
+
}
|
|
1163
|
+
if (c === 引用) 引用 = null;
|
|
1164
|
+
continue;
|
|
1165
|
+
}
|
|
1166
|
+
if ((c === '"' || c === "'") && (直前 === null || ":,{[".includes(直前))) {
|
|
1167
|
+
引用 = c;
|
|
1168
|
+
直前 = c;
|
|
1169
|
+
continue;
|
|
1170
|
+
}
|
|
1171
|
+
// 組の中身は葉だけ。 引用符の外の入れ子は値の形を保てない。
|
|
1172
|
+
if (c === "{") return undefined;
|
|
1173
|
+
if (c === "}") {
|
|
1174
|
+
塊.push(本体.slice(始まり, i));
|
|
1175
|
+
i += 1;
|
|
1176
|
+
次は組 = false;
|
|
1177
|
+
break;
|
|
1178
|
+
}
|
|
1179
|
+
if (!/\s/u.test(c)) 直前 = c;
|
|
1180
|
+
}
|
|
1181
|
+
if (次は組) return undefined;
|
|
1182
|
+
}
|
|
1183
|
+
if (次は組 && 塊.length > 0) return undefined;
|
|
1184
|
+
if (塊.length === 0) return undefined;
|
|
1185
|
+
|
|
1186
|
+
const 出: Record<string, string>[] = [];
|
|
1187
|
+
for (const t of 塊) {
|
|
1188
|
+
const 組 = parseInlineMapping(t);
|
|
1189
|
+
if (Object.keys(組).length === 0) return undefined;
|
|
1190
|
+
出.push(組);
|
|
1191
|
+
}
|
|
1192
|
+
return 出;
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
function 部品の組を検査する(
|
|
1196
|
+
kind: string,
|
|
1197
|
+
読めた: Record<string, unknown>,
|
|
1198
|
+
line: number,
|
|
1199
|
+
errors: DslError[],
|
|
1200
|
+
): void {
|
|
1201
|
+
for (const [欄, 定義] of Object.entries(部品の組の表[kind] ?? {})) {
|
|
1202
|
+
const 並び = 読めた[欄];
|
|
1203
|
+
if (!Array.isArray(並び)) continue;
|
|
1204
|
+
並び.forEach((組, i) => {
|
|
1205
|
+
const o = 組 as Record<string, unknown>;
|
|
1206
|
+
for (const k of Object.keys(o)) {
|
|
1207
|
+
if (k in 定義.欄) continue;
|
|
1208
|
+
errors.push({
|
|
1209
|
+
line,
|
|
1210
|
+
message: `部品の ${欄}[${i}] の項目名が読めません: "${k}"`,
|
|
1211
|
+
hint: `使える項目 = ${Object.keys(定義.欄).join(", ")}`,
|
|
1212
|
+
});
|
|
1213
|
+
}
|
|
1214
|
+
for (const k of 定義.必須) {
|
|
1215
|
+
if (o[k] !== undefined) continue;
|
|
1216
|
+
errors.push({
|
|
1217
|
+
line,
|
|
1218
|
+
message: `部品の ${欄}[${i}].${k} は必ず書きます`,
|
|
1219
|
+
hint: `必須の項目 = ${定義.必須.join(", ")}`,
|
|
1220
|
+
});
|
|
1221
|
+
}
|
|
1222
|
+
});
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
/**
|
|
1227
|
+
* 中括弧の中身を、表に従って読む (#1374)。
|
|
1228
|
+
*
|
|
1229
|
+
* 知らない欄と足りない必須欄は行番号付きで知らせる。 読めた欄だけを返すため、
|
|
1230
|
+
* 知らせが出た欄は描画側へ渡らない。
|
|
1231
|
+
*/
|
|
1232
|
+
function 表に従って読む(
|
|
1233
|
+
定義: 図形の定義,
|
|
1234
|
+
opts: Record<string, string>,
|
|
1235
|
+
接頭: string,
|
|
1236
|
+
line: number,
|
|
1237
|
+
errors: DslError[],
|
|
1238
|
+
): Record<string, unknown> {
|
|
1239
|
+
const out: Record<string, unknown> = {};
|
|
1240
|
+
for (const [欄, 値] of Object.entries(opts)) {
|
|
1241
|
+
if (欄 === "kind") continue;
|
|
1242
|
+
const 形 = 定義.欄[欄];
|
|
1243
|
+
if (形 === undefined) {
|
|
1244
|
+
errors.push({
|
|
1245
|
+
line,
|
|
1246
|
+
message: `${接頭}の項目名が読めません: "${欄}"`,
|
|
1247
|
+
hint: `使える項目 = ${Object.keys(定義.欄).join(", ")}`,
|
|
1248
|
+
});
|
|
1249
|
+
continue;
|
|
1250
|
+
}
|
|
1251
|
+
/*
|
|
1252
|
+
* 空の値を落とすのは文字列以外だけ (#1385)。
|
|
1253
|
+
*
|
|
1254
|
+
* `unit: ""` は「単位を出さない」 という指定で、書いた人の意図がある。 落とすと
|
|
1255
|
+
* 書かなかった場合と同じになり、描画側の既定が出る = 書いたのに効かない。
|
|
1256
|
+
*
|
|
1257
|
+
* 値を書かなかった形 (`unit:`) はここに届かない = `parseInlineMapping` が値 1 文字以上を
|
|
1258
|
+
* 求めるため、名前ごと拾われない。 したがって空が来るのは引用符で明示した時だけ。
|
|
1259
|
+
*/
|
|
1260
|
+
if (値 === "" && 形 !== "文字列") continue;
|
|
1261
|
+
if (形 === "数") {
|
|
1262
|
+
const n = 数として読む(値, `${接頭}${欄}`, line, errors);
|
|
1263
|
+
if (n !== undefined) out[欄] = n;
|
|
1264
|
+
} else if (形 === "数か文字列") {
|
|
1265
|
+
const n = numberOrUndef(値);
|
|
1266
|
+
out[欄] = n !== undefined ? n : 値;
|
|
1267
|
+
} else if (形 === "文字列の並び") {
|
|
1268
|
+
out[欄] = 並びとして読む(値);
|
|
1269
|
+
} else if (形 === "組の並び") {
|
|
1270
|
+
const 組 = 組の並びとして読む(値);
|
|
1271
|
+
if (組 !== undefined) out[欄] = 組;
|
|
1272
|
+
else {
|
|
1273
|
+
errors.push({
|
|
1274
|
+
line,
|
|
1275
|
+
message: `${接頭}${欄} の並びが読めません: "${値}"`,
|
|
1276
|
+
hint: '`[{ value: "online", color: "#22c55e" }]` の形で書く',
|
|
1277
|
+
});
|
|
1278
|
+
}
|
|
1279
|
+
} else if (形 === "真偽") {
|
|
1280
|
+
// 描画側は `boolean` を取る。 `"true"` / `"false"` の 2 語だけを受け、それ以外は
|
|
1281
|
+
// 黙って真に倒さず知らせる = `visibleIf` と違い、ここは真偽そのものを渡す欄
|
|
1282
|
+
if (値 === "true" || 値 === "false") out[欄] = 値 === "true";
|
|
1283
|
+
else {
|
|
1284
|
+
errors.push({
|
|
1285
|
+
line,
|
|
1286
|
+
message: `${接頭}${欄} の真偽が読めません: "${値}"`,
|
|
1287
|
+
hint: "使える値 = true, false",
|
|
1288
|
+
});
|
|
1289
|
+
}
|
|
1290
|
+
} else if (形 === "向き") {
|
|
1291
|
+
if (["up", "down", "left", "right"].includes(値)) out[欄] = 値;
|
|
1292
|
+
else {
|
|
1293
|
+
errors.push({
|
|
1294
|
+
line,
|
|
1295
|
+
message: `${接頭}${欄} の向きが読めません: "${値}"`,
|
|
1296
|
+
hint: "使える値 = up, down, left, right",
|
|
1297
|
+
});
|
|
1298
|
+
}
|
|
1299
|
+
} else {
|
|
1300
|
+
out[欄] = 値;
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
for (const 欄 of 定義.必須) {
|
|
1304
|
+
if (out[欄] === undefined) {
|
|
1305
|
+
errors.push({
|
|
1306
|
+
line,
|
|
1307
|
+
message: `${接頭}${欄} は必ず書きます`,
|
|
1308
|
+
hint: `必須の項目 = ${定義.必須.join(", ")}`,
|
|
1309
|
+
});
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
return out;
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
/**
|
|
1316
|
+
* その箱を出すかどうかの条件を読む (#1381)。
|
|
1317
|
+
*
|
|
1318
|
+
* 描画側は空文字を偽として扱うため、`visibleIf: ` と書くと「常に出ない箱」 になる。
|
|
1319
|
+
* 書いた本人はたいてい条件を書き忘れただけなので、空は捨てずに行番号付きで知らせる。
|
|
1320
|
+
*/
|
|
1321
|
+
function 出す条件として読む(
|
|
1322
|
+
raw: string | undefined,
|
|
1323
|
+
line: number,
|
|
1324
|
+
errors: DslError[],
|
|
1325
|
+
): string | undefined {
|
|
1326
|
+
if (raw === undefined) return undefined;
|
|
1327
|
+
const 値 = raw.trim();
|
|
1328
|
+
if (値 === "") {
|
|
1329
|
+
errors.push({
|
|
1330
|
+
line,
|
|
1331
|
+
message: "箱の visibleIf が空です",
|
|
1332
|
+
hint: '`visibleIf: "{flag}"` のように条件を書く。 常に隠すなら `visibleIf: "0"`',
|
|
1333
|
+
});
|
|
1334
|
+
return undefined;
|
|
1335
|
+
}
|
|
1336
|
+
return 値;
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
/**
|
|
1340
|
+
* 箱の中に描く図形を読む (#1374)。 読めなければ `undefined` を返す。
|
|
1341
|
+
*
|
|
1342
|
+
* 種類が分からない形は捨てずに知らせる = 黙って捨てると、書いた図形が出ないのに
|
|
1343
|
+
* 手掛かりが 1 つも残らない。
|
|
1344
|
+
*/
|
|
1345
|
+
function 図形として読む(raw: string, line: number, errors: DslError[]): DslDynShape | undefined {
|
|
1346
|
+
const 中身 = raw.trim().replace(/^\{|\}$/g, "");
|
|
1347
|
+
const opts = parseInlineMapping(中身);
|
|
1348
|
+
const kind = (opts.kind ?? "").toLowerCase();
|
|
1349
|
+
const 定義 = 図形の表[kind];
|
|
1350
|
+
if (定義 === undefined) {
|
|
1351
|
+
errors.push({
|
|
1352
|
+
line,
|
|
1353
|
+
message: `図形の種類が読めません: "${opts.kind ?? ""}"`,
|
|
1354
|
+
hint: `使える種類 = ${Object.keys(図形の表).join(", ")}`,
|
|
1355
|
+
});
|
|
1356
|
+
return undefined;
|
|
1357
|
+
}
|
|
1358
|
+
const 読めた = 表に従って読む(定義, opts, `図形の `, line, errors);
|
|
1359
|
+
for (const 欄 of 定義.必須) if (読めた[欄] === undefined) return undefined;
|
|
1360
|
+
return { kind, ...読めた } as DslDynShape;
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
/**
|
|
1364
|
+
* 値を見せる部品を読む (#1374)。 読めなければ `undefined` を返す。
|
|
1365
|
+
*/
|
|
1366
|
+
function 部品として読む(
|
|
1367
|
+
id: string,
|
|
1368
|
+
raw: string,
|
|
1369
|
+
line: number,
|
|
1370
|
+
errors: DslError[],
|
|
1371
|
+
): DslReadout | undefined {
|
|
1372
|
+
const opts = parseInlineMapping(raw);
|
|
1373
|
+
const kind = (opts.kind ?? "").toLowerCase();
|
|
1374
|
+
const 定義 = 部品の表[kind];
|
|
1375
|
+
if (定義 === undefined) {
|
|
1376
|
+
errors.push({
|
|
1377
|
+
line,
|
|
1378
|
+
message: `部品の種類が読めません: "${opts.kind ?? ""}"`,
|
|
1379
|
+
hint: `使える種類 = ${Object.keys(部品の表).join(", ")}`,
|
|
1380
|
+
});
|
|
1381
|
+
return undefined;
|
|
1382
|
+
}
|
|
1383
|
+
const 読めた = 表に従って読む(定義, opts, `部品 ${id} の `, line, errors);
|
|
1384
|
+
部品の組を検査する(kind, 読めた, line, errors);
|
|
1385
|
+
for (const 欄 of 定義.必須) if (読めた[欄] === undefined) return undefined;
|
|
1386
|
+
return { id, kind, ...読めた } as DslReadout;
|
|
1387
|
+
}
|
|
1388
|
+
|
|
940
1389
|
/** 箱の中の要素の欄 (#1306)。 `DslActorNodeOverride` の全欄を覆う */
|
|
941
1390
|
export const ACTOR_NODE_VALUE_KINDS = {
|
|
942
1391
|
posX: "数",
|
|
@@ -1002,20 +1451,40 @@ function matchActorInlineMapping(raw: string): { name: string; inner: string } |
|
|
|
1002
1451
|
const name = raw.slice(0, colonIdx);
|
|
1003
1452
|
const rest = raw.slice(colonIdx + 1).trim();
|
|
1004
1453
|
if (!rest.startsWith("{")) return null;
|
|
1005
|
-
// depth count で対応 brace
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1454
|
+
// depth count で対応 brace 探す。 正しい引用符の中の `}` は飛ばす。
|
|
1455
|
+
const 終わりを探す = (引用符を見る: boolean): number => {
|
|
1456
|
+
let depth = 0;
|
|
1457
|
+
let 引用符: '"' | "'" | null = null;
|
|
1458
|
+
let 直前: string | null = null;
|
|
1459
|
+
for (let i = 0; i < rest.length; i += 1) {
|
|
1460
|
+
const c = rest[i]!;
|
|
1461
|
+
if (引用符を見る) {
|
|
1462
|
+
if (引用符 !== null) {
|
|
1463
|
+
if (引用符 === '"' && c === "\\" && i + 1 < rest.length) {
|
|
1464
|
+
i += 1;
|
|
1465
|
+
continue;
|
|
1466
|
+
}
|
|
1467
|
+
if (c === 引用符) 引用符 = null;
|
|
1468
|
+
continue;
|
|
1469
|
+
}
|
|
1470
|
+
if ((c === '"' || c === "'") && (直前 === null || ":,{[".includes(直前))) {
|
|
1471
|
+
引用符 = c;
|
|
1472
|
+
直前 = c;
|
|
1473
|
+
continue;
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
if (c === "{") depth += 1;
|
|
1477
|
+
else if (c === "}") {
|
|
1478
|
+
depth -= 1;
|
|
1479
|
+
if (depth === 0) return i;
|
|
1016
1480
|
}
|
|
1481
|
+
if (!/\s/u.test(c)) 直前 = c;
|
|
1017
1482
|
}
|
|
1018
|
-
|
|
1483
|
+
return -1;
|
|
1484
|
+
};
|
|
1485
|
+
let endIdx = 終わりを探す(true);
|
|
1486
|
+
// 閉じない引用符は従来どおり単純分割へ戻す (#1367)。
|
|
1487
|
+
if (endIdx < 0) endIdx = 終わりを探す(false);
|
|
1019
1488
|
if (endIdx < 0) return null;
|
|
1020
1489
|
const inner = rest.slice(1, endIdx);
|
|
1021
1490
|
return { name, inner };
|
|
@@ -1326,6 +1795,18 @@ function applyContinuationLines(actor: DslActor, rest: Line[], errors: DslError[
|
|
|
1326
1795
|
case "値":
|
|
1327
1796
|
out.value = stripQuotes(raw);
|
|
1328
1797
|
break;
|
|
1798
|
+
case "shape":
|
|
1799
|
+
case "図形":
|
|
1800
|
+
out.shape = 図形として読む(raw, ln.no, errors);
|
|
1801
|
+
break;
|
|
1802
|
+
case "visibleIf":
|
|
1803
|
+
case "出す条件":
|
|
1804
|
+
out.visibleIf = 出す条件として読む(stripQuotes(raw), ln.no, errors);
|
|
1805
|
+
break;
|
|
1806
|
+
case "title":
|
|
1807
|
+
case "題":
|
|
1808
|
+
out.title = stripQuotes(raw);
|
|
1809
|
+
break;
|
|
1329
1810
|
case "rows":
|
|
1330
1811
|
case "行":
|
|
1331
1812
|
out.rows = raw
|
|
@@ -1425,11 +1906,7 @@ function applyContinuationLines(actor: DslActor, rest: Line[], errors: DslError[
|
|
|
1425
1906
|
// 名前の行と縦に並べた行の両方に倍率がある形では、後に書いた縦の行を採る。
|
|
1426
1907
|
// 知らせ (`scale-reserved`) は書かれた名前をすべて見るので、名前だけは足し合わせる
|
|
1427
1908
|
if (scaleWritten.size > 0) {
|
|
1428
|
-
const s = resolveScale(
|
|
1429
|
-
scaleWritten,
|
|
1430
|
-
(key) => scaleLines.get(key) ?? actor.pos.line,
|
|
1431
|
-
errors,
|
|
1432
|
-
);
|
|
1909
|
+
const s = resolveScale(scaleWritten, (key) => scaleLines.get(key) ?? actor.pos.line, errors);
|
|
1433
1910
|
out.scale = s.scale;
|
|
1434
1911
|
out.scaleKeys = [...new Set([...(actor.scaleKeys ?? []), ...s.keys])];
|
|
1435
1912
|
}
|
|
@@ -1477,6 +1954,15 @@ export const ACTOR_ITEM_KEYS: ReadonlySet<string> = new Set([
|
|
|
1477
1954
|
"値",
|
|
1478
1955
|
"rows",
|
|
1479
1956
|
"行",
|
|
1957
|
+
// 箱の中に描く図形 (#1374)
|
|
1958
|
+
"shape",
|
|
1959
|
+
"図形",
|
|
1960
|
+
// その箱を出すかどうかの条件 (#1381)
|
|
1961
|
+
"visibleIf",
|
|
1962
|
+
"出す条件",
|
|
1963
|
+
// 箱に出す題 (#1381)
|
|
1964
|
+
"title",
|
|
1965
|
+
"題",
|
|
1480
1966
|
"位置",
|
|
1481
1967
|
"pos",
|
|
1482
1968
|
"posX",
|
|
@@ -1815,6 +2301,12 @@ export const INLINE_ACTOR_KEYS: ReadonlySet<string> = new Set([
|
|
|
1815
2301
|
"posY",
|
|
1816
2302
|
"posW",
|
|
1817
2303
|
"posH",
|
|
2304
|
+
// 箱の中に描く図形 (#1374)
|
|
2305
|
+
"shape",
|
|
2306
|
+
// その箱を出すかどうかの条件 (#1381)
|
|
2307
|
+
"visibleIf",
|
|
2308
|
+
// 箱に出す題 (#1381)。 名前と切り離して書ける
|
|
2309
|
+
"title",
|
|
1818
2310
|
// 倍率は別経路 (`reportScaleOnNonPart`) が知らせる。 ここでも読める扱いにしないと
|
|
1819
2311
|
// 同じ名前で 2 度知らせることになる
|
|
1820
2312
|
"scale",
|
|
@@ -1838,6 +2330,11 @@ const FLOW_INLINE_READERS = {
|
|
|
1838
2330
|
sub: (v: string | undefined) => v,
|
|
1839
2331
|
guard: (v: string | undefined) => v,
|
|
1840
2332
|
cardinality: (v: string | undefined) => v,
|
|
2333
|
+
// 矢印がどの辺から出るか (#1385)。 描画側は 4 方向を取り、書かなければ自動で選ぶ
|
|
2334
|
+
side: (v: string | undefined) =>
|
|
2335
|
+
v !== undefined && (EDGE_SIDE_VALUES as readonly string[]).includes(v)
|
|
2336
|
+
? (v as "top" | "right" | "bottom" | "left")
|
|
2337
|
+
: undefined,
|
|
1841
2338
|
// 数と真偽の欄は `FLOW_INLINE_VALUE_KINDS` の表が読む (#1306)。 ここでは名前だけを持つ =
|
|
1842
2339
|
// 読める欄の一覧 (`FLOW_INLINE_KEYS`) は本表から導くため、載せないと欄ごと消える
|
|
1843
2340
|
labelOffsetX: null,
|
|
@@ -1946,6 +2443,15 @@ function parseActor(line: Line, errors: DslError[]): DslActor | null {
|
|
|
1946
2443
|
.filter(Boolean)
|
|
1947
2444
|
: undefined,
|
|
1948
2445
|
lane: opts.lane,
|
|
2446
|
+
// 箱の中に描く図形 (#1374)。 パーツでは状態の上書きとして意味を持つため横取りしない
|
|
2447
|
+
shape:
|
|
2448
|
+
isPart || opts.shape === undefined
|
|
2449
|
+
? undefined
|
|
2450
|
+
: 図形として読む(opts.shape, line.no, errors),
|
|
2451
|
+
// 出す条件 (#1381)。 パーツでは状態の上書きとして意味を持つため横取りしない
|
|
2452
|
+
visibleIf: isPart ? undefined : 出す条件として読む(opts.visibleIf, line.no, errors),
|
|
2453
|
+
// 箱に出す題 (#1381)。 空文字も意味を持つ (題を出さない箱) ため undefined と分ける
|
|
2454
|
+
title: isPart ? undefined : opts.title,
|
|
1949
2455
|
...表で読む(ACTOR_INLINE_VALUE_KINDS, opts, "箱の ", line.no, errors),
|
|
1950
2456
|
// parts では `tone` を状態の上書きとして従来から使えるため、 色として横取りしない
|
|
1951
2457
|
tone: isPart ? undefined : resolveTone(opts.tone),
|
|
@@ -2030,8 +2536,16 @@ function parseFlowStep(line: Line, no: number, errors: DslError[]): DslStep | nu
|
|
|
2030
2536
|
const opts = parseInlineMapping(mapMatch[1]!);
|
|
2031
2537
|
// 文字列の欄はそのまま入れ、数と真偽の欄は表が読んで読めない値を知らせる (#1306)
|
|
2032
2538
|
for (const k of FLOW_INLINE_KEYS) {
|
|
2033
|
-
|
|
2034
|
-
|
|
2539
|
+
const 読み手 = FLOW_INLINE_READERS[k];
|
|
2540
|
+
if (読み手 === null) continue;
|
|
2541
|
+
中括弧[k] = 読み手(opts[k]);
|
|
2542
|
+
}
|
|
2543
|
+
if (opts.side !== undefined && 中括弧.side === undefined) {
|
|
2544
|
+
errors.push({
|
|
2545
|
+
line: line.no,
|
|
2546
|
+
message: `矢印の side が読めません: "${opts.side}"`,
|
|
2547
|
+
hint: `使える値 = ${EDGE_SIDE_VALUES.join(", ")}`,
|
|
2548
|
+
});
|
|
2035
2549
|
}
|
|
2036
2550
|
数と真偽 = 表で読む(FLOW_INLINE_VALUE_KINDS, opts, "矢印の ", line.no, errors);
|
|
2037
2551
|
rest = rest.slice(0, mapMatch.index ?? 0).trim();
|
|
@@ -2039,6 +2553,7 @@ function parseFlowStep(line: Line, no: number, errors: DslError[]): DslStep | nu
|
|
|
2039
2553
|
const sub = 中括弧.sub as string | undefined;
|
|
2040
2554
|
const guard = 中括弧.guard as string | undefined;
|
|
2041
2555
|
const cardinality = 中括弧.cardinality as string | undefined;
|
|
2556
|
+
const side = 中括弧.side as "top" | "right" | "bottom" | "left" | undefined;
|
|
2042
2557
|
const labelOffsetX = 数と真偽?.labelOffsetX;
|
|
2043
2558
|
const labelOffsetY = 数と真偽?.labelOffsetY;
|
|
2044
2559
|
const overlay = 数と真偽?.overlay;
|
|
@@ -2098,6 +2613,7 @@ function parseFlowStep(line: Line, no: number, errors: DslError[]): DslStep | nu
|
|
|
2098
2613
|
sub,
|
|
2099
2614
|
guard,
|
|
2100
2615
|
cardinality,
|
|
2616
|
+
side,
|
|
2101
2617
|
labelOffsetX,
|
|
2102
2618
|
labelOffsetY,
|
|
2103
2619
|
overlay,
|
|
@@ -2220,7 +2736,15 @@ function ensureAnimate(a: DslAnimate | undefined, lineNo: number): DslAnimate {
|
|
|
2220
2736
|
* (#1330)。 段の項目と値は階層が違うので衝突しないが、同じ語が 2 つの意味で並ぶと
|
|
2221
2737
|
* 見本を読む人が階層から意味を判断することになる。
|
|
2222
2738
|
*/
|
|
2223
|
-
export const PHASE_ITEM_WORDS = [
|
|
2739
|
+
export const PHASE_ITEM_WORDS = [
|
|
2740
|
+
"focus",
|
|
2741
|
+
"badge",
|
|
2742
|
+
"body",
|
|
2743
|
+
"description",
|
|
2744
|
+
"tween",
|
|
2745
|
+
"set",
|
|
2746
|
+
"draw",
|
|
2747
|
+
] as const;
|
|
2224
2748
|
|
|
2225
2749
|
const 段の項目の英語 = PHASE_ITEM_WORDS;
|
|
2226
2750
|
const 段の項目の集合: ReadonlySet<string> = new Set(PHASE_ITEM_WORDS);
|