@cardenelabs/dragon 0.11.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 +2891 -120
- 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 +2891 -120
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- 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 +598 -35
- 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
|
+
}
|
|
1016
1475
|
}
|
|
1476
|
+
if (c === "{") depth += 1;
|
|
1477
|
+
else if (c === "}") {
|
|
1478
|
+
depth -= 1;
|
|
1479
|
+
if (depth === 0) return i;
|
|
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 };
|
|
@@ -1069,24 +1538,63 @@ function writtenScaleFields(inner: string): Map<string, string> {
|
|
|
1069
1538
|
*
|
|
1070
1539
|
* `parseInlineMapping` と、倍率の「書かれた名前」 を拾う経路 (#1026) で共用する。
|
|
1071
1540
|
* 割り方を 2 つ持つと、片方だけが拾える項目という食い違いが生まれる。
|
|
1541
|
+
*
|
|
1542
|
+
* **引用符の中のカンマでは割らない** (#1367)。 見ていなかった間、`{ subtitle: "a, b" }` の
|
|
1543
|
+
* 補足が `"a` に切れて図に出ていた (実測で見本 1 件が踏んでいた)。 配列
|
|
1544
|
+
* (`{ initial: '["入力", "確認"]' }`) が壊れていなかったのは角括弧の深さで守られていたためで、
|
|
1545
|
+
* 引用符だけで守られる値は守られていなかった。
|
|
1072
1546
|
*/
|
|
1073
1547
|
function splitInlineFields(inner: string): string[] {
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1548
|
+
const 引用符を見て割る = (見る: boolean): { parts: string[]; 閉じた: boolean } => {
|
|
1549
|
+
let depth = 0;
|
|
1550
|
+
let 引用符: '"' | "'" | null = null;
|
|
1551
|
+
let 直前: string | null = null;
|
|
1552
|
+
let buf = "";
|
|
1553
|
+
const parts: string[] = [];
|
|
1554
|
+
for (let i = 0; i < inner.length; i += 1) {
|
|
1555
|
+
const c = inner[i]!;
|
|
1556
|
+
if (見る) {
|
|
1557
|
+
if (引用符 !== null) {
|
|
1558
|
+
// 二重引用の中では、逃がした引用符を終端と取り違えない
|
|
1559
|
+
if (引用符 === '"' && c === "\\" && i + 1 < inner.length) {
|
|
1560
|
+
buf += c;
|
|
1561
|
+
i += 1;
|
|
1562
|
+
buf += inner[i]!;
|
|
1563
|
+
continue;
|
|
1564
|
+
}
|
|
1565
|
+
// **開いた記号と同じものだけが閉じる**。 `"Guns N' Roses"` の `'` は文字として残す
|
|
1566
|
+
if (c === 引用符) 引用符 = null;
|
|
1567
|
+
buf += c;
|
|
1568
|
+
continue;
|
|
1569
|
+
}
|
|
1570
|
+
// 値の途中の apostrophe まで開始記号にすると、別 field まで引用内として飲み込む
|
|
1571
|
+
if ((c === '"' || c === "'") && (直前 === null || ":,{[".includes(直前))) {
|
|
1572
|
+
引用符 = c;
|
|
1573
|
+
直前 = c;
|
|
1574
|
+
buf += c;
|
|
1575
|
+
continue;
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
if (c === "[" || c === "{") depth += 1;
|
|
1579
|
+
else if (c === "]" || c === "}") depth -= 1;
|
|
1580
|
+
if (c === "," && depth === 0) {
|
|
1581
|
+
parts.push(buf);
|
|
1582
|
+
buf = "";
|
|
1583
|
+
直前 = c;
|
|
1584
|
+
continue;
|
|
1585
|
+
}
|
|
1586
|
+
buf += c;
|
|
1587
|
+
if (c !== " " && c !== "\t") 直前 = c;
|
|
1085
1588
|
}
|
|
1086
|
-
buf
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1589
|
+
if (buf.trim()) parts.push(buf);
|
|
1590
|
+
return { parts, 閉じた: 引用符 === null };
|
|
1591
|
+
};
|
|
1592
|
+
|
|
1593
|
+
const 見た = 引用符を見て割る(true);
|
|
1594
|
+
// **閉じない引用符が残ったらこれまでどおりに割る** (#1367)。 壊れた入力で挙動が変わると、
|
|
1595
|
+
// 今まで通っていた書き方が黙って別の結果になる。 実測では見本 9832 件の中括弧のうち
|
|
1596
|
+
// 閉じない形は 0 件で、この経路は保険
|
|
1597
|
+
return 見た.閉じた ? 見た.parts : 引用符を見て割る(false).parts;
|
|
1090
1598
|
}
|
|
1091
1599
|
|
|
1092
1600
|
function collectIndentedList(
|
|
@@ -1287,6 +1795,18 @@ function applyContinuationLines(actor: DslActor, rest: Line[], errors: DslError[
|
|
|
1287
1795
|
case "値":
|
|
1288
1796
|
out.value = stripQuotes(raw);
|
|
1289
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;
|
|
1290
1810
|
case "rows":
|
|
1291
1811
|
case "行":
|
|
1292
1812
|
out.rows = raw
|
|
@@ -1386,11 +1906,7 @@ function applyContinuationLines(actor: DslActor, rest: Line[], errors: DslError[
|
|
|
1386
1906
|
// 名前の行と縦に並べた行の両方に倍率がある形では、後に書いた縦の行を採る。
|
|
1387
1907
|
// 知らせ (`scale-reserved`) は書かれた名前をすべて見るので、名前だけは足し合わせる
|
|
1388
1908
|
if (scaleWritten.size > 0) {
|
|
1389
|
-
const s = resolveScale(
|
|
1390
|
-
scaleWritten,
|
|
1391
|
-
(key) => scaleLines.get(key) ?? actor.pos.line,
|
|
1392
|
-
errors,
|
|
1393
|
-
);
|
|
1909
|
+
const s = resolveScale(scaleWritten, (key) => scaleLines.get(key) ?? actor.pos.line, errors);
|
|
1394
1910
|
out.scale = s.scale;
|
|
1395
1911
|
out.scaleKeys = [...new Set([...(actor.scaleKeys ?? []), ...s.keys])];
|
|
1396
1912
|
}
|
|
@@ -1438,6 +1954,15 @@ export const ACTOR_ITEM_KEYS: ReadonlySet<string> = new Set([
|
|
|
1438
1954
|
"値",
|
|
1439
1955
|
"rows",
|
|
1440
1956
|
"行",
|
|
1957
|
+
// 箱の中に描く図形 (#1374)
|
|
1958
|
+
"shape",
|
|
1959
|
+
"図形",
|
|
1960
|
+
// その箱を出すかどうかの条件 (#1381)
|
|
1961
|
+
"visibleIf",
|
|
1962
|
+
"出す条件",
|
|
1963
|
+
// 箱に出す題 (#1381)
|
|
1964
|
+
"title",
|
|
1965
|
+
"題",
|
|
1441
1966
|
"位置",
|
|
1442
1967
|
"pos",
|
|
1443
1968
|
"posX",
|
|
@@ -1776,6 +2301,12 @@ export const INLINE_ACTOR_KEYS: ReadonlySet<string> = new Set([
|
|
|
1776
2301
|
"posY",
|
|
1777
2302
|
"posW",
|
|
1778
2303
|
"posH",
|
|
2304
|
+
// 箱の中に描く図形 (#1374)
|
|
2305
|
+
"shape",
|
|
2306
|
+
// その箱を出すかどうかの条件 (#1381)
|
|
2307
|
+
"visibleIf",
|
|
2308
|
+
// 箱に出す題 (#1381)。 名前と切り離して書ける
|
|
2309
|
+
"title",
|
|
1779
2310
|
// 倍率は別経路 (`reportScaleOnNonPart`) が知らせる。 ここでも読める扱いにしないと
|
|
1780
2311
|
// 同じ名前で 2 度知らせることになる
|
|
1781
2312
|
"scale",
|
|
@@ -1799,6 +2330,11 @@ const FLOW_INLINE_READERS = {
|
|
|
1799
2330
|
sub: (v: string | undefined) => v,
|
|
1800
2331
|
guard: (v: string | undefined) => v,
|
|
1801
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,
|
|
1802
2338
|
// 数と真偽の欄は `FLOW_INLINE_VALUE_KINDS` の表が読む (#1306)。 ここでは名前だけを持つ =
|
|
1803
2339
|
// 読める欄の一覧 (`FLOW_INLINE_KEYS`) は本表から導くため、載せないと欄ごと消える
|
|
1804
2340
|
labelOffsetX: null,
|
|
@@ -1907,6 +2443,15 @@ function parseActor(line: Line, errors: DslError[]): DslActor | null {
|
|
|
1907
2443
|
.filter(Boolean)
|
|
1908
2444
|
: undefined,
|
|
1909
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,
|
|
1910
2455
|
...表で読む(ACTOR_INLINE_VALUE_KINDS, opts, "箱の ", line.no, errors),
|
|
1911
2456
|
// parts では `tone` を状態の上書きとして従来から使えるため、 色として横取りしない
|
|
1912
2457
|
tone: isPart ? undefined : resolveTone(opts.tone),
|
|
@@ -1991,8 +2536,16 @@ function parseFlowStep(line: Line, no: number, errors: DslError[]): DslStep | nu
|
|
|
1991
2536
|
const opts = parseInlineMapping(mapMatch[1]!);
|
|
1992
2537
|
// 文字列の欄はそのまま入れ、数と真偽の欄は表が読んで読めない値を知らせる (#1306)
|
|
1993
2538
|
for (const k of FLOW_INLINE_KEYS) {
|
|
1994
|
-
|
|
1995
|
-
|
|
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
|
+
});
|
|
1996
2549
|
}
|
|
1997
2550
|
数と真偽 = 表で読む(FLOW_INLINE_VALUE_KINDS, opts, "矢印の ", line.no, errors);
|
|
1998
2551
|
rest = rest.slice(0, mapMatch.index ?? 0).trim();
|
|
@@ -2000,6 +2553,7 @@ function parseFlowStep(line: Line, no: number, errors: DslError[]): DslStep | nu
|
|
|
2000
2553
|
const sub = 中括弧.sub as string | undefined;
|
|
2001
2554
|
const guard = 中括弧.guard as string | undefined;
|
|
2002
2555
|
const cardinality = 中括弧.cardinality as string | undefined;
|
|
2556
|
+
const side = 中括弧.side as "top" | "right" | "bottom" | "left" | undefined;
|
|
2003
2557
|
const labelOffsetX = 数と真偽?.labelOffsetX;
|
|
2004
2558
|
const labelOffsetY = 数と真偽?.labelOffsetY;
|
|
2005
2559
|
const overlay = 数と真偽?.overlay;
|
|
@@ -2059,6 +2613,7 @@ function parseFlowStep(line: Line, no: number, errors: DslError[]): DslStep | nu
|
|
|
2059
2613
|
sub,
|
|
2060
2614
|
guard,
|
|
2061
2615
|
cardinality,
|
|
2616
|
+
side,
|
|
2062
2617
|
labelOffsetX,
|
|
2063
2618
|
labelOffsetY,
|
|
2064
2619
|
overlay,
|
|
@@ -2181,7 +2736,15 @@ function ensureAnimate(a: DslAnimate | undefined, lineNo: number): DslAnimate {
|
|
|
2181
2736
|
* (#1330)。 段の項目と値は階層が違うので衝突しないが、同じ語が 2 つの意味で並ぶと
|
|
2182
2737
|
* 見本を読む人が階層から意味を判断することになる。
|
|
2183
2738
|
*/
|
|
2184
|
-
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;
|
|
2185
2748
|
|
|
2186
2749
|
const 段の項目の英語 = PHASE_ITEM_WORDS;
|
|
2187
2750
|
const 段の項目の集合: ReadonlySet<string> = new Set(PHASE_ITEM_WORDS);
|