@cardenelabs/dragon 0.12.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 +86 -58
- package/dist/index.cjs +10485 -6260
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +945 -4
- package/dist/index.d.ts +945 -4
- package/dist/index.js +10486 -6261
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/color.ts +31 -6
- package/src/compile.ts +724 -196
- package/src/input-size.ts +21 -1
- package/src/json-parser.ts +695 -2
- package/src/schemas/diagram.json +1341 -3
- package/src/types.ts +130 -1
- package/src/v05/input-table.generated.ts +136 -0
- package/src/v05/parser-types.ts +21 -0
- package/src/v05/parser.ts +1237 -26
- package/src/v05/readout-table.generated.ts +1088 -0
package/src/v05/parser.ts
CHANGED
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
*/
|
|
43
43
|
|
|
44
44
|
import type { NodeKind, Tone, EdgeStyle } from "@cardenelabs/cdl";
|
|
45
|
-
import { TONES, NODE_KINDS } from "@cardenelabs/cdl";
|
|
45
|
+
import { TONES, NODE_KINDS, parseFormula } from "@cardenelabs/cdl";
|
|
46
46
|
import { TONE_ALIAS, NODE_KIND_ALIAS } from "../keywords";
|
|
47
47
|
import { parseRelativePos, orderByDependency } from "../relative-pos";
|
|
48
48
|
import {
|
|
@@ -56,6 +56,12 @@ import type {
|
|
|
56
56
|
DslAxes,
|
|
57
57
|
DslDocument,
|
|
58
58
|
DslActor,
|
|
59
|
+
DslDynShape,
|
|
60
|
+
DslReadout,
|
|
61
|
+
DslInput,
|
|
62
|
+
DslFormula,
|
|
63
|
+
DslEventBinding,
|
|
64
|
+
DslScrollTrigger,
|
|
59
65
|
DslActorNodeOverride,
|
|
60
66
|
DslStep,
|
|
61
67
|
DslAnimate,
|
|
@@ -73,6 +79,9 @@ import type {
|
|
|
73
79
|
|
|
74
80
|
export type V05ParseResult = { ok: true; doc: DslDocument } | { ok: false; errors: DslError[] };
|
|
75
81
|
|
|
82
|
+
/** 矢印を出す辺。 Text DSL / JSON validator / 公開 schema の 3 経路で同じ 4 値を使う。 */
|
|
83
|
+
export const EDGE_SIDE_VALUES = ["top", "right", "bottom", "left"] as const;
|
|
84
|
+
|
|
76
85
|
/**
|
|
77
86
|
* 記法が受ける top-level の項目 (#1190)。
|
|
78
87
|
*
|
|
@@ -95,6 +104,16 @@ export const TOP_LEVEL_KEYS = [
|
|
|
95
104
|
"eyebrow",
|
|
96
105
|
// 2 軸で仕分ける図の軸の名前 (#1251)
|
|
97
106
|
"axes",
|
|
107
|
+
// 値を見せる部品 (#1374)
|
|
108
|
+
"readouts",
|
|
109
|
+
// 読む人が動かすつまみ (#1389)
|
|
110
|
+
"inputs",
|
|
111
|
+
// つまみの値から決まる値 (#1391)
|
|
112
|
+
"formulas",
|
|
113
|
+
// 押下などの出来事で動く仕掛け (#1393)
|
|
114
|
+
"events",
|
|
115
|
+
// 巻き上げに応じて進む値 (#1393)
|
|
116
|
+
"scrolls",
|
|
98
117
|
] as const;
|
|
99
118
|
|
|
100
119
|
/**
|
|
@@ -116,6 +135,101 @@ export const TOP_LEVEL_KEYS = [
|
|
|
116
135
|
*/
|
|
117
136
|
const LANE_ID_ENTRY = /^([\p{L}\p{N}_-]+)\s*:\s*\{([^}]*)\}\s*$/u;
|
|
118
137
|
|
|
138
|
+
/**
|
|
139
|
+
* `id: { ... }` の 1 行を、名前と中括弧の中身に割る (#1381)。
|
|
140
|
+
*
|
|
141
|
+
* `LANE_ID_ENTRY` は中括弧を `[^}]*` で読むため、中身に中括弧を含む形
|
|
142
|
+
* (`map: [{ value: "a" }]`) を受けられない。 部品の欄には組の並びを取るものがあるので、
|
|
143
|
+
* 引用符の内側を飛ばしつつ中括弧の深さを数えて割る。
|
|
144
|
+
*
|
|
145
|
+
* 割れない形は `undefined` を返す。 呼び手が行番号付きで知らせる。
|
|
146
|
+
*/
|
|
147
|
+
function 名前と中括弧に割る(行: string): [string, string] | undefined {
|
|
148
|
+
const t = 行.trim();
|
|
149
|
+
const c = t.indexOf(":");
|
|
150
|
+
if (c < 0) return undefined;
|
|
151
|
+
const 名前 = t.slice(0, c).trim();
|
|
152
|
+
if (!/^[\p{L}\p{N}_-]+$/u.test(名前)) return undefined;
|
|
153
|
+
const 残り = t.slice(c + 1).trim();
|
|
154
|
+
if (!残り.startsWith("{")) return undefined;
|
|
155
|
+
|
|
156
|
+
let 深さ = 0;
|
|
157
|
+
let 引用: string | null = null;
|
|
158
|
+
let 直前: string | null = null;
|
|
159
|
+
for (let i = 0; i < 残り.length; i += 1) {
|
|
160
|
+
const ch = 残り[i]!;
|
|
161
|
+
if (引用 !== null) {
|
|
162
|
+
if (引用 === '"' && ch === "\\" && i + 1 < 残り.length) {
|
|
163
|
+
i += 1;
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (ch === 引用) 引用 = null;
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
if ((ch === '"' || ch === "'") && (直前 === null || ":,{[".includes(直前))) {
|
|
170
|
+
引用 = ch;
|
|
171
|
+
直前 = ch;
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (ch === "{") 深さ += 1;
|
|
175
|
+
else if (ch === "}") {
|
|
176
|
+
深さ -= 1;
|
|
177
|
+
// 中括弧が閉じた後に文字が残る形は割らない (`{...} x`)
|
|
178
|
+
if (深さ === 0) return i === 残り.length - 1 ? [名前, 残り.slice(1, i)] : undefined;
|
|
179
|
+
}
|
|
180
|
+
if (!/\s/u.test(ch)) 直前 = ch;
|
|
181
|
+
}
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* 行の末尾にある中括弧の塊を切り出す (#1396)。
|
|
187
|
+
*
|
|
188
|
+
* **`{[^}]*}` では切れない**。 欄の値に中括弧が入る形 (`{ widthBind: "{flow}" }`) では
|
|
189
|
+
* 内側の `}` で止まってしまい、塊ごと読み落として **本文の一部として扱われる**
|
|
190
|
+
* (実測 = 矢印の説明文が `"x" { widthBind: "{flow}" }` のまま図に載った)。
|
|
191
|
+
*
|
|
192
|
+
* 箱の側は #1381 で同じ理由から深さを数える形に直してある。 矢印だけが古い形で
|
|
193
|
+
* 残っていた。
|
|
194
|
+
*
|
|
195
|
+
* 引用符の内側は数えない = `sub: "a } b"` の `}` で閉じたことにしない。
|
|
196
|
+
* 切り出せない形は `undefined` を返し、呼び手が従来どおり本文として扱う。
|
|
197
|
+
*/
|
|
198
|
+
function 末尾の中括弧を切り出す(rest: string): { 前: string; 中身: string } | undefined {
|
|
199
|
+
const t = rest.trimEnd();
|
|
200
|
+
if (!t.endsWith("}")) return undefined;
|
|
201
|
+
let 深さ = 0;
|
|
202
|
+
let 引用: string | null = null;
|
|
203
|
+
let 直前: string | null = null;
|
|
204
|
+
let 始 = -1;
|
|
205
|
+
for (let i = 0; i < t.length; i += 1) {
|
|
206
|
+
const c = t[i]!;
|
|
207
|
+
if (引用 !== null) {
|
|
208
|
+
if (引用 === '"' && c === "\\" && i + 1 < t.length) {
|
|
209
|
+
i += 1;
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
if (c === 引用) 引用 = null;
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
if ((c === '"' || c === "'") && (直前 === null || ":,{[".includes(直前))) {
|
|
216
|
+
引用 = c;
|
|
217
|
+
直前 = c;
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
if (c === "{") {
|
|
221
|
+
if (深さ === 0) 始 = i;
|
|
222
|
+
深さ += 1;
|
|
223
|
+
} else if (c === "}") {
|
|
224
|
+
深さ -= 1;
|
|
225
|
+
// 閉じた位置が末尾なら、そこが探していた塊
|
|
226
|
+
if (深さ === 0 && i === t.length - 1) return { 前: t.slice(0, 始), 中身: t.slice(始 + 1, i) };
|
|
227
|
+
}
|
|
228
|
+
if (!/\s/u.test(c)) 直前 = c;
|
|
229
|
+
}
|
|
230
|
+
return undefined;
|
|
231
|
+
}
|
|
232
|
+
|
|
119
233
|
function isTopLevelKey(key: string): key is (typeof TOP_LEVEL_KEYS)[number] {
|
|
120
234
|
return (TOP_LEVEL_KEYS as readonly string[]).includes(key);
|
|
121
235
|
}
|
|
@@ -278,6 +392,12 @@ export function parseTextDslV05(src: string): V05ParseResult {
|
|
|
278
392
|
const values: DslValue[] = [];
|
|
279
393
|
let viewport: DslViewport | undefined = undefined;
|
|
280
394
|
let lanesMap: Record<string, DslLane> | undefined = undefined;
|
|
395
|
+
let readoutsList: DslReadout[] | undefined = undefined;
|
|
396
|
+
let inputsList: DslInput[] | undefined = undefined;
|
|
397
|
+
let formulasList: DslFormula[] | undefined = undefined;
|
|
398
|
+
let eventsList: DslEventBinding[] | undefined = undefined;
|
|
399
|
+
let scrollsList: DslScrollTrigger[] | undefined = undefined;
|
|
400
|
+
let scrollLines = new Map<string, number>();
|
|
281
401
|
let groupsMap: Record<string, DslGroup> | undefined = undefined;
|
|
282
402
|
|
|
283
403
|
let i = 0;
|
|
@@ -539,6 +659,155 @@ export function parseTextDslV05(src: string): V05ParseResult {
|
|
|
539
659
|
i = next;
|
|
540
660
|
continue;
|
|
541
661
|
}
|
|
662
|
+
if (head.key === "readouts") {
|
|
663
|
+
// readouts:\n ring: { kind: percent-ring, source: total, max: 500, label: "..." }
|
|
664
|
+
// 1 行にまとめた形は受けない。 黙って空の並びにすると、書いた部品が全て消えた図になる。
|
|
665
|
+
if (head.value !== null && head.value.trim() !== "") {
|
|
666
|
+
errors.push({
|
|
667
|
+
line: line.no,
|
|
668
|
+
message: "readouts は 1 行にまとめて書けない",
|
|
669
|
+
hint: "次の行から字下げして `id: { kind: percent-ring, source: total, max: 100 }` の形で並べる",
|
|
670
|
+
});
|
|
671
|
+
i += 1;
|
|
672
|
+
continue;
|
|
673
|
+
}
|
|
674
|
+
// `collectIndentedList` は `:` の無い行を落とす。 部品を綴り違えた行も知らせるため、
|
|
675
|
+
// 字下げした行を全て読み手へ渡す。
|
|
676
|
+
const { items, next } = collectIndentedRaw(lines, i + 1, line.indent);
|
|
677
|
+
readoutsList = [];
|
|
678
|
+
for (const it of items) {
|
|
679
|
+
const m = 名前と中括弧に割る(it.trimmed);
|
|
680
|
+
if (m) {
|
|
681
|
+
const 読めた = 部品として読む(m[0], m[1], it.no, errors);
|
|
682
|
+
if (読めた) readoutsList.push(読めた);
|
|
683
|
+
} else {
|
|
684
|
+
errors.push({
|
|
685
|
+
line: it.no,
|
|
686
|
+
message: `invalid readout entry: "${it.trimmed}"`,
|
|
687
|
+
hint: "use `id: { kind: percent-ring, source: total, max: 500 }`",
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
i = next;
|
|
692
|
+
continue;
|
|
693
|
+
}
|
|
694
|
+
if (head.key === "inputs") {
|
|
695
|
+
// inputs:\n value: { kind: slider, min: 0, max: 100, defaultValue: 50, label: "Value" }
|
|
696
|
+
// 1 行にまとめた形は受けない。 黙って空の並びにすると、書いたつまみが全て消えた図になる。
|
|
697
|
+
if (head.value !== null && head.value.trim() !== "") {
|
|
698
|
+
errors.push({
|
|
699
|
+
line: line.no,
|
|
700
|
+
message: "inputs は 1 行にまとめて書けない",
|
|
701
|
+
hint: "次の行から字下げして `value: { kind: slider, min: 0, max: 100, defaultValue: 50 }` の形で並べる",
|
|
702
|
+
});
|
|
703
|
+
i += 1;
|
|
704
|
+
continue;
|
|
705
|
+
}
|
|
706
|
+
// `collectIndentedList` は `:` の無い行を落とす。 つまみを綴り違えた行も知らせるため、
|
|
707
|
+
// 字下げした行を全て読み手へ渡す。
|
|
708
|
+
const { items, next } = collectIndentedRaw(lines, i + 1, line.indent);
|
|
709
|
+
inputsList = [];
|
|
710
|
+
for (const it of items) {
|
|
711
|
+
const m = 名前と中括弧に割る(it.trimmed);
|
|
712
|
+
if (m) {
|
|
713
|
+
const 読めた = つまみとして読む(m[0], m[1], it.no, errors);
|
|
714
|
+
if (読めた) inputsList.push(読めた);
|
|
715
|
+
} else {
|
|
716
|
+
errors.push({
|
|
717
|
+
line: it.no,
|
|
718
|
+
message: `invalid input entry: "${it.trimmed}"`,
|
|
719
|
+
hint: "use `id: { kind: slider, min: 0, max: 100, defaultValue: 50 }`",
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
i = next;
|
|
724
|
+
continue;
|
|
725
|
+
}
|
|
726
|
+
if (head.key === "formulas") {
|
|
727
|
+
// formulas:\n doubled: "input * 2"
|
|
728
|
+
//
|
|
729
|
+
// 1 行にまとめた形は受けない (`values:` と同じ理由)。 式に `,` が入るため、
|
|
730
|
+
// 素朴な `,` 分割では式が壊れる。
|
|
731
|
+
if (head.value !== null && head.value.trim() !== "") {
|
|
732
|
+
errors.push({
|
|
733
|
+
line: line.no,
|
|
734
|
+
message: "formulas は 1 行にまとめて書けない",
|
|
735
|
+
hint: '式に `,` が入るため。 次の行から字下げして `doubled: "input * 2"` の形で並べる',
|
|
736
|
+
});
|
|
737
|
+
i += 1;
|
|
738
|
+
continue;
|
|
739
|
+
}
|
|
740
|
+
// 字下げした行を全て読み手へ渡す = 綴りを誤った行も知らせるため
|
|
741
|
+
const { items, next } = collectIndentedRaw(lines, i + 1, line.indent);
|
|
742
|
+
formulasList = [];
|
|
743
|
+
for (const it of items) {
|
|
744
|
+
const f = 式として読む(it.trimmed.replace(/^-\s*/, ""), it.no, errors);
|
|
745
|
+
if (f) formulasList.push(f);
|
|
746
|
+
}
|
|
747
|
+
i = next;
|
|
748
|
+
continue;
|
|
749
|
+
}
|
|
750
|
+
if (head.key === "events") {
|
|
751
|
+
// events:\n - { on: click, box: Button, handler: toggle-active }
|
|
752
|
+
if (head.value !== null && head.value.trim() !== "") {
|
|
753
|
+
errors.push({
|
|
754
|
+
line: line.no,
|
|
755
|
+
message: "events は 1 行にまとめて書けない",
|
|
756
|
+
hint: "次の行から字下げして `- { on: click, box: Button, handler: toggle }` の形で並べる",
|
|
757
|
+
});
|
|
758
|
+
i += 1;
|
|
759
|
+
continue;
|
|
760
|
+
}
|
|
761
|
+
const { items, next } = collectIndentedRaw(lines, i + 1, line.indent);
|
|
762
|
+
eventsList = [];
|
|
763
|
+
for (const it of items) {
|
|
764
|
+
const e = 出来事として読む(it.trimmed.replace(/^-\s*/, ""), it.no, errors);
|
|
765
|
+
if (e) eventsList.push(e);
|
|
766
|
+
}
|
|
767
|
+
i = next;
|
|
768
|
+
continue;
|
|
769
|
+
}
|
|
770
|
+
if (head.key === "scrolls") {
|
|
771
|
+
// scrolls:\n intro: { start: 0.9, end: 0.1, scrub: 1, label: "..." }
|
|
772
|
+
if (head.value !== null && head.value.trim() !== "") {
|
|
773
|
+
errors.push({
|
|
774
|
+
line: line.no,
|
|
775
|
+
message: "scrolls は 1 行にまとめて書けない",
|
|
776
|
+
hint: "次の行から字下げして `intro: { start: 0.9, end: 0.1 }` の形で並べる",
|
|
777
|
+
});
|
|
778
|
+
i += 1;
|
|
779
|
+
continue;
|
|
780
|
+
}
|
|
781
|
+
const { items, next } = collectIndentedRaw(lines, i + 1, line.indent);
|
|
782
|
+
scrollsList = [];
|
|
783
|
+
scrollLines = new Map();
|
|
784
|
+
for (const it of items) {
|
|
785
|
+
const m = 名前と中括弧に割る(it.trimmed);
|
|
786
|
+
if (m) {
|
|
787
|
+
const 読めた = 巻き上げとして読む(m[0], m[1], it.no, errors);
|
|
788
|
+
if (読めた) {
|
|
789
|
+
if (scrollLines.has(読めた.id)) {
|
|
790
|
+
errors.push({
|
|
791
|
+
line: it.no,
|
|
792
|
+
message: `巻き上げの名前 "${読めた.id}" が重複しています`,
|
|
793
|
+
hint: "scrolls の名前は 1 度だけ書く",
|
|
794
|
+
});
|
|
795
|
+
} else {
|
|
796
|
+
scrollsList.push(読めた);
|
|
797
|
+
scrollLines.set(読めた.id, it.no);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
} else {
|
|
801
|
+
errors.push({
|
|
802
|
+
line: it.no,
|
|
803
|
+
message: `invalid scroll entry: "${it.trimmed}"`,
|
|
804
|
+
hint: "use `id: { start: 0.9, end: 0.1, scrub: 1 }`",
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
i = next;
|
|
809
|
+
continue;
|
|
810
|
+
}
|
|
542
811
|
if (head.key === "groups") {
|
|
543
812
|
// groups:\n aws: { label: "AWS", lanes: [ecs, rds] }
|
|
544
813
|
const { items, next } = collectIndentedList(lines, i + 1, line.indent);
|
|
@@ -582,6 +851,20 @@ export function parseTextDslV05(src: string): V05ParseResult {
|
|
|
582
851
|
hint: "add `type: sequence|flow|swimlane|er|state|topology|solidity|gantt|class|pie|c4|mind`",
|
|
583
852
|
});
|
|
584
853
|
|
|
854
|
+
// つまみ・式・巻き上げは同じ名前空間で値を作る。 重なると後から作る値が効かない。
|
|
855
|
+
const 既に値を作る名前 = new Set([
|
|
856
|
+
...(inputsList ?? []).map((input) => input.id),
|
|
857
|
+
...(formulasList ?? []).map((formula) => formula.id),
|
|
858
|
+
]);
|
|
859
|
+
for (const scroll of scrollsList ?? []) {
|
|
860
|
+
if (!既に値を作る名前.has(scroll.id)) continue;
|
|
861
|
+
errors.push({
|
|
862
|
+
line: scrollLines.get(scroll.id) ?? 1,
|
|
863
|
+
message: `巻き上げの名前 "${scroll.id}" が inputs または formulas と重なっています`,
|
|
864
|
+
hint: "inputs / formulas / scrolls では重ならない名前を使う",
|
|
865
|
+
});
|
|
866
|
+
}
|
|
867
|
+
|
|
585
868
|
if (errors.length > 0) return { ok: false, errors };
|
|
586
869
|
|
|
587
870
|
return {
|
|
@@ -597,6 +880,11 @@ export function parseTextDslV05(src: string): V05ParseResult {
|
|
|
597
880
|
...(values.length > 0 ? { values } : {}),
|
|
598
881
|
viewport,
|
|
599
882
|
lanes: lanesMap,
|
|
883
|
+
readouts: readoutsList,
|
|
884
|
+
inputs: inputsList,
|
|
885
|
+
formulas: formulasList,
|
|
886
|
+
events: eventsList,
|
|
887
|
+
scrolls: scrollsList,
|
|
600
888
|
groups: groupsMap,
|
|
601
889
|
pos: { line: 1 },
|
|
602
890
|
},
|
|
@@ -937,6 +1225,732 @@ export const LANE_VALUE_KINDS = {
|
|
|
937
1225
|
lifeline: "真偽",
|
|
938
1226
|
} as const satisfies Record<string, 値の形>;
|
|
939
1227
|
|
|
1228
|
+
/**
|
|
1229
|
+
* 箱の中に描く図形 (`shape:`) と、値を見せる部品 (`readouts:`) の欄 (#1374)。
|
|
1230
|
+
*
|
|
1231
|
+
* どちらも描画側 (`CdlDynShape` / `CdlReadout`) の形をそのまま渡す。 記法の値は全て文字列で
|
|
1232
|
+
* 届くため、欄ごとに「数」 「文字列」 「数か文字列」 のどれとして読むかを表で持つ。
|
|
1233
|
+
*
|
|
1234
|
+
* **「数か文字列」 は状態を追いかける欄**。 `level: 80` のように数を直接書くこともできるし、
|
|
1235
|
+
* `level: "{s1}"` のように状態の名前を書いて段の中で動かすこともできる。 数として読めた時
|
|
1236
|
+
* だけ数にし、読めなければ文字列のまま渡す。
|
|
1237
|
+
*
|
|
1238
|
+
* ## 表を持つ理由
|
|
1239
|
+
*
|
|
1240
|
+
* 表が無いと「知らない欄を黙って捨てる」 か「何でも通す」 のどちらかになる。 前者は書いた
|
|
1241
|
+
* 指定が消え、後者は綴り違いがそのまま描画側へ流れて別の形で失敗する。 表があれば
|
|
1242
|
+
* 書いた場所と使える欄を添えて知らせられる。
|
|
1243
|
+
*/
|
|
1244
|
+
// 欄の形と定義の型は `parser-types.ts` が持つ (#1385)。 生成した部品の表がこの型を使うため、
|
|
1245
|
+
// `parser.ts` に置いたままだと 生成した表 → parser → 生成した表 の輪ができる
|
|
1246
|
+
export type { 欄の形, 図形の定義 } from "./parser-types";
|
|
1247
|
+
|
|
1248
|
+
/** 描ける図形と、その欄 (`CdlDynShape` の全 5 種を覆う) */
|
|
1249
|
+
export const 図形の表: Record<string, 図形の定義> = {
|
|
1250
|
+
rect: {
|
|
1251
|
+
必須: ["source", "fillMax"],
|
|
1252
|
+
欄: {
|
|
1253
|
+
source: "数か文字列",
|
|
1254
|
+
fillMax: "数",
|
|
1255
|
+
orient: "向き",
|
|
1256
|
+
fill: "文字列",
|
|
1257
|
+
stroke: "文字列",
|
|
1258
|
+
radius: "数",
|
|
1259
|
+
},
|
|
1260
|
+
},
|
|
1261
|
+
circle: {
|
|
1262
|
+
必須: [],
|
|
1263
|
+
欄: { radius: "数か文字列", fillProgress: "数か文字列", fill: "文字列", stroke: "文字列" },
|
|
1264
|
+
},
|
|
1265
|
+
arc: {
|
|
1266
|
+
必須: ["angle"],
|
|
1267
|
+
欄: {
|
|
1268
|
+
innerRadius: "数",
|
|
1269
|
+
outerRadius: "数",
|
|
1270
|
+
startAngle: "数",
|
|
1271
|
+
angle: "数か文字列",
|
|
1272
|
+
sweepMax: "数",
|
|
1273
|
+
fill: "文字列",
|
|
1274
|
+
stroke: "文字列",
|
|
1275
|
+
},
|
|
1276
|
+
},
|
|
1277
|
+
wave: {
|
|
1278
|
+
必須: ["level", "amplitude"],
|
|
1279
|
+
欄: {
|
|
1280
|
+
level: "数か文字列",
|
|
1281
|
+
amplitude: "数",
|
|
1282
|
+
frequency: "数",
|
|
1283
|
+
waveHeight: "数",
|
|
1284
|
+
fill: "文字列",
|
|
1285
|
+
stroke: "文字列",
|
|
1286
|
+
},
|
|
1287
|
+
},
|
|
1288
|
+
polygon: {
|
|
1289
|
+
必須: ["sides"],
|
|
1290
|
+
欄: {
|
|
1291
|
+
sides: "数",
|
|
1292
|
+
radius: "数か文字列",
|
|
1293
|
+
rotation: "数か文字列",
|
|
1294
|
+
fill: "文字列",
|
|
1295
|
+
stroke: "文字列",
|
|
1296
|
+
},
|
|
1297
|
+
},
|
|
1298
|
+
};
|
|
1299
|
+
|
|
1300
|
+
/**
|
|
1301
|
+
* 記法が受ける部品と、その欄。
|
|
1302
|
+
*
|
|
1303
|
+
* **手で書かず、描画側の型定義から生成する** (#1385)。 描画側は 107 種を持ち欄は 510 個あり、
|
|
1304
|
+
* 手で写すと写し間違いと描画側の変更への drift が残る (`rules/quality.md § 導出可能記述は
|
|
1305
|
+
* 人手で書かない`)。
|
|
1306
|
+
*
|
|
1307
|
+
* 作り直す = `node packages/dragon/scripts/gen-readout-table.mjs`
|
|
1308
|
+
* ずれの検知 = `packages/dragon/test/readout-table-generated.test.ts`
|
|
1309
|
+
*/
|
|
1310
|
+
export { 部品の表, 部品の組の表 } from "./readout-table.generated";
|
|
1311
|
+
import { 部品の表, 部品の組の表 } from "./readout-table.generated";
|
|
1312
|
+
|
|
1313
|
+
/**
|
|
1314
|
+
* 記法が受けるつまみと、その欄 (#1389)。
|
|
1315
|
+
*
|
|
1316
|
+
* **部品の表と同じく、描画側の型定義から生成する**。 14 種それぞれ欄が違い、手で写すと
|
|
1317
|
+
* 描画側が種類を足した時に drift が残る (`rules/quality.md § 導出可能記述は人手で書かない`)。
|
|
1318
|
+
*
|
|
1319
|
+
* 作り直す = `node packages/dragon/scripts/gen-input-table.mjs`
|
|
1320
|
+
* ずれの検知 = `packages/dragon/test/input-table-generated.test.ts`
|
|
1321
|
+
*/
|
|
1322
|
+
export { つまみの表 } from "./input-table.generated";
|
|
1323
|
+
import { つまみの表 } from "./input-table.generated";
|
|
1324
|
+
import type { 図形の定義 } from "./parser-types";
|
|
1325
|
+
|
|
1326
|
+
/** `[a, b]` の形を文字列の並びに読む */
|
|
1327
|
+
function 並びとして読む(raw: string): string[] {
|
|
1328
|
+
return raw
|
|
1329
|
+
.replace(/^\[|\]$/g, "")
|
|
1330
|
+
.split(/,(?![^[]*\])/)
|
|
1331
|
+
.map((x) => stripQuotes(x.trim()))
|
|
1332
|
+
.filter((x) => x !== "");
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
/**
|
|
1336
|
+
* `[{ a: 1 }, { b: 2 }]` の形を、中括弧ごとの組の並びに読む (#1381)。
|
|
1337
|
+
*
|
|
1338
|
+
* `並びとして読む` は葉の値しか想定していないため、中括弧を含む形を渡すと項目の途中で
|
|
1339
|
+
* 割れる。 中括弧の深さを数えて、深さ 0 の位置だけで割る。
|
|
1340
|
+
*
|
|
1341
|
+
* 読めない形は捨てずに `undefined` を返す。 呼び手が行番号付きで知らせる = 黙って捨てると
|
|
1342
|
+
* 「書いたのに出ない」 が手掛かりなしで起きる。
|
|
1343
|
+
*/
|
|
1344
|
+
function 組の並びとして読む(raw: string): Record<string, string>[] | undefined {
|
|
1345
|
+
const 中身 = raw.trim();
|
|
1346
|
+
if (!中身.startsWith("[") || !中身.endsWith("]")) return undefined;
|
|
1347
|
+
const 本体 = 中身.slice(1, -1);
|
|
1348
|
+
|
|
1349
|
+
const 塊: string[] = [];
|
|
1350
|
+
let i = 0;
|
|
1351
|
+
let 次は組 = true;
|
|
1352
|
+
while (i < 本体.length) {
|
|
1353
|
+
while (i < 本体.length && /\s/u.test(本体[i]!)) i += 1;
|
|
1354
|
+
if (i >= 本体.length) break;
|
|
1355
|
+
|
|
1356
|
+
if (!次は組) {
|
|
1357
|
+
if (本体[i] !== ",") return undefined;
|
|
1358
|
+
次は組 = true;
|
|
1359
|
+
i += 1;
|
|
1360
|
+
continue;
|
|
1361
|
+
}
|
|
1362
|
+
if (本体[i] !== "{") return undefined;
|
|
1363
|
+
|
|
1364
|
+
const 始まり = i + 1;
|
|
1365
|
+
let 引用: '"' | "'" | null = null;
|
|
1366
|
+
let 直前: string | null = "{";
|
|
1367
|
+
i += 1;
|
|
1368
|
+
for (; i < 本体.length; i += 1) {
|
|
1369
|
+
const c = 本体[i]!;
|
|
1370
|
+
if (引用 !== null) {
|
|
1371
|
+
if (引用 === '"' && c === "\\" && i + 1 < 本体.length) {
|
|
1372
|
+
i += 1;
|
|
1373
|
+
continue;
|
|
1374
|
+
}
|
|
1375
|
+
if (c === 引用) 引用 = null;
|
|
1376
|
+
continue;
|
|
1377
|
+
}
|
|
1378
|
+
if ((c === '"' || c === "'") && (直前 === null || ":,{[".includes(直前))) {
|
|
1379
|
+
引用 = c;
|
|
1380
|
+
直前 = c;
|
|
1381
|
+
continue;
|
|
1382
|
+
}
|
|
1383
|
+
// 組の中身は葉だけ。 引用符の外の入れ子は値の形を保てない。
|
|
1384
|
+
if (c === "{") return undefined;
|
|
1385
|
+
if (c === "}") {
|
|
1386
|
+
塊.push(本体.slice(始まり, i));
|
|
1387
|
+
i += 1;
|
|
1388
|
+
次は組 = false;
|
|
1389
|
+
break;
|
|
1390
|
+
}
|
|
1391
|
+
if (!/\s/u.test(c)) 直前 = c;
|
|
1392
|
+
}
|
|
1393
|
+
if (次は組) return undefined;
|
|
1394
|
+
}
|
|
1395
|
+
if (次は組 && 塊.length > 0) return undefined;
|
|
1396
|
+
if (塊.length === 0) return undefined;
|
|
1397
|
+
|
|
1398
|
+
const 出: Record<string, string>[] = [];
|
|
1399
|
+
for (const t of 塊) {
|
|
1400
|
+
const 組 = parseInlineMapping(t);
|
|
1401
|
+
if (Object.keys(組).length === 0) return undefined;
|
|
1402
|
+
出.push(組);
|
|
1403
|
+
}
|
|
1404
|
+
return 出;
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
function 部品の組を検査する(
|
|
1408
|
+
kind: string,
|
|
1409
|
+
読めた: Record<string, unknown>,
|
|
1410
|
+
line: number,
|
|
1411
|
+
errors: DslError[],
|
|
1412
|
+
): void {
|
|
1413
|
+
for (const [欄, 定義] of Object.entries(部品の組の表[kind] ?? {})) {
|
|
1414
|
+
const 並び = 読めた[欄];
|
|
1415
|
+
if (!Array.isArray(並び)) continue;
|
|
1416
|
+
並び.forEach((組, i) => {
|
|
1417
|
+
const o = 組 as Record<string, unknown>;
|
|
1418
|
+
for (const k of Object.keys(o)) {
|
|
1419
|
+
if (k in 定義.欄) continue;
|
|
1420
|
+
errors.push({
|
|
1421
|
+
line,
|
|
1422
|
+
message: `部品の ${欄}[${i}] の項目名が読めません: "${k}"`,
|
|
1423
|
+
hint: `使える項目 = ${Object.keys(定義.欄).join(", ")}`,
|
|
1424
|
+
});
|
|
1425
|
+
}
|
|
1426
|
+
for (const k of 定義.必須) {
|
|
1427
|
+
if (o[k] !== undefined) continue;
|
|
1428
|
+
errors.push({
|
|
1429
|
+
line,
|
|
1430
|
+
message: `部品の ${欄}[${i}].${k} は必ず書きます`,
|
|
1431
|
+
hint: `必須の項目 = ${定義.必須.join(", ")}`,
|
|
1432
|
+
});
|
|
1433
|
+
}
|
|
1434
|
+
});
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
/**
|
|
1439
|
+
* 中括弧の中身を、表に従って読む (#1374)。
|
|
1440
|
+
*
|
|
1441
|
+
* 知らない欄と足りない必須欄は行番号付きで知らせる。 読めた欄だけを返すため、
|
|
1442
|
+
* 知らせが出た欄は描画側へ渡らない。
|
|
1443
|
+
*/
|
|
1444
|
+
function 表に従って読む(
|
|
1445
|
+
定義: 図形の定義,
|
|
1446
|
+
opts: Record<string, string>,
|
|
1447
|
+
接頭: string,
|
|
1448
|
+
line: number,
|
|
1449
|
+
errors: DslError[],
|
|
1450
|
+
): Record<string, unknown> {
|
|
1451
|
+
const out: Record<string, unknown> = {};
|
|
1452
|
+
for (const [欄, 値] of Object.entries(opts)) {
|
|
1453
|
+
if (欄 === "kind") continue;
|
|
1454
|
+
const 形 = 定義.欄[欄];
|
|
1455
|
+
if (形 === undefined) {
|
|
1456
|
+
errors.push({
|
|
1457
|
+
line,
|
|
1458
|
+
message: `${接頭}の項目名が読めません: "${欄}"`,
|
|
1459
|
+
hint: `使える項目 = ${Object.keys(定義.欄).join(", ")}`,
|
|
1460
|
+
});
|
|
1461
|
+
continue;
|
|
1462
|
+
}
|
|
1463
|
+
/*
|
|
1464
|
+
* 空の値を落とすのは文字列以外だけ (#1385)。
|
|
1465
|
+
*
|
|
1466
|
+
* `unit: ""` は「単位を出さない」 という指定で、書いた人の意図がある。 落とすと
|
|
1467
|
+
* 書かなかった場合と同じになり、描画側の既定が出る = 書いたのに効かない。
|
|
1468
|
+
*
|
|
1469
|
+
* 値を書かなかった形 (`unit:`) はここに届かない = `parseInlineMapping` が値 1 文字以上を
|
|
1470
|
+
* 求めるため、名前ごと拾われない。 したがって空が来るのは引用符で明示した時だけ。
|
|
1471
|
+
*/
|
|
1472
|
+
if (値 === "" && 形 !== "文字列") continue;
|
|
1473
|
+
if (形 === "数") {
|
|
1474
|
+
const n = 数として読む(値, `${接頭}${欄}`, line, errors);
|
|
1475
|
+
if (n !== undefined) out[欄] = n;
|
|
1476
|
+
} else if (形 === "数か文字列") {
|
|
1477
|
+
const n = numberOrUndef(値);
|
|
1478
|
+
out[欄] = n !== undefined ? n : 値;
|
|
1479
|
+
} else if (形 === "文字列の並び") {
|
|
1480
|
+
out[欄] = 並びとして読む(値);
|
|
1481
|
+
} else if (形 === "数の並び") {
|
|
1482
|
+
/*
|
|
1483
|
+
* 数の並びは、1 つでも数として読めなければ欄ごと落とす (#1389)。
|
|
1484
|
+
*
|
|
1485
|
+
* 読めた分だけ渡すと並びの長さが変わり、番号で指す欄 (`defaultSpeedIdx`) が
|
|
1486
|
+
* 別の要素を指す。 書き間違いが「別の値が選ばれている図」 になって出るため、
|
|
1487
|
+
* 数え落としを黙って通さない。
|
|
1488
|
+
*/
|
|
1489
|
+
const 生 = 並びとして読む(値);
|
|
1490
|
+
const 数 = 生.map((x) => numberOrUndef(x));
|
|
1491
|
+
if (数.some((n) => n === undefined)) {
|
|
1492
|
+
errors.push({
|
|
1493
|
+
line,
|
|
1494
|
+
message: `${接頭}${欄} に数でない値があります: "${値}"`,
|
|
1495
|
+
hint: "`[0.5, 1, 2, 4]` の形で数だけを並べる",
|
|
1496
|
+
});
|
|
1497
|
+
} else {
|
|
1498
|
+
out[欄] = 数;
|
|
1499
|
+
}
|
|
1500
|
+
} else if (形 === "組の並び") {
|
|
1501
|
+
const 組 = 組の並びとして読む(値);
|
|
1502
|
+
if (組 !== undefined) out[欄] = 組;
|
|
1503
|
+
else {
|
|
1504
|
+
errors.push({
|
|
1505
|
+
line,
|
|
1506
|
+
message: `${接頭}${欄} の並びが読めません: "${値}"`,
|
|
1507
|
+
hint: '`[{ value: "online", color: "#22c55e" }]` の形で書く',
|
|
1508
|
+
});
|
|
1509
|
+
}
|
|
1510
|
+
} else if (形 === "真偽") {
|
|
1511
|
+
// 描画側は `boolean` を取る。 `"true"` / `"false"` の 2 語だけを受け、それ以外は
|
|
1512
|
+
// 黙って真に倒さず知らせる = `visibleIf` と違い、ここは真偽そのものを渡す欄
|
|
1513
|
+
if (値 === "true" || 値 === "false") out[欄] = 値 === "true";
|
|
1514
|
+
else {
|
|
1515
|
+
errors.push({
|
|
1516
|
+
line,
|
|
1517
|
+
message: `${接頭}${欄} の真偽が読めません: "${値}"`,
|
|
1518
|
+
hint: "使える値 = true, false",
|
|
1519
|
+
});
|
|
1520
|
+
}
|
|
1521
|
+
} else if (形 === "向き") {
|
|
1522
|
+
if (["up", "down", "left", "right"].includes(値)) out[欄] = 値;
|
|
1523
|
+
else {
|
|
1524
|
+
errors.push({
|
|
1525
|
+
line,
|
|
1526
|
+
message: `${接頭}${欄} の向きが読めません: "${値}"`,
|
|
1527
|
+
hint: "使える値 = up, down, left, right",
|
|
1528
|
+
});
|
|
1529
|
+
}
|
|
1530
|
+
} else {
|
|
1531
|
+
out[欄] = 値;
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
for (const 欄 of 定義.必須) {
|
|
1535
|
+
if (out[欄] === undefined) {
|
|
1536
|
+
errors.push({
|
|
1537
|
+
line,
|
|
1538
|
+
message: `${接頭}${欄} は必ず書きます`,
|
|
1539
|
+
hint: `必須の項目 = ${定義.必須.join(", ")}`,
|
|
1540
|
+
});
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
return out;
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
/**
|
|
1547
|
+
* その箱を出すかどうかの条件を読む (#1381)。
|
|
1548
|
+
*
|
|
1549
|
+
* 描画側は空文字を偽として扱うため、`visibleIf: ` と書くと「常に出ない箱」 になる。
|
|
1550
|
+
* 書いた本人はたいてい条件を書き忘れただけなので、空は捨てずに行番号付きで知らせる。
|
|
1551
|
+
*/
|
|
1552
|
+
function 出す条件として読む(
|
|
1553
|
+
raw: string | undefined,
|
|
1554
|
+
line: number,
|
|
1555
|
+
errors: DslError[],
|
|
1556
|
+
): string | undefined {
|
|
1557
|
+
if (raw === undefined) return undefined;
|
|
1558
|
+
const 値 = raw.trim();
|
|
1559
|
+
if (値 === "") {
|
|
1560
|
+
errors.push({
|
|
1561
|
+
line,
|
|
1562
|
+
message: "箱の visibleIf が空です",
|
|
1563
|
+
hint: '`visibleIf: "{flag}"` のように条件を書く。 常に隠すなら `visibleIf: "0"`',
|
|
1564
|
+
});
|
|
1565
|
+
return undefined;
|
|
1566
|
+
}
|
|
1567
|
+
return 値;
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
/**
|
|
1571
|
+
* 値に追随する欄を読む (#1392)。 数として読めれば数、読めなければ文字列のまま渡す。
|
|
1572
|
+
*
|
|
1573
|
+
* `箱の中に描く図形` の `source` と同じ読み方に揃える (`表に従って読む` の「数か文字列」)。
|
|
1574
|
+
* 状態の名前 (`"{barW}"`) を書く形と、数を直に書く形の両方を受ける。
|
|
1575
|
+
*
|
|
1576
|
+
* **空は捨てずに知らせる**。 描画側は空文字を「書かなかった」 と同じには扱わず、
|
|
1577
|
+
* `opacity: ""` は 0 として読まれて箱が消える。 書いた人はたいてい値を書き忘れただけ。
|
|
1578
|
+
*/
|
|
1579
|
+
function 値に追随する欄として読む(
|
|
1580
|
+
raw: string | undefined,
|
|
1581
|
+
接頭: string,
|
|
1582
|
+
line: number,
|
|
1583
|
+
errors: DslError[],
|
|
1584
|
+
): number | string | undefined {
|
|
1585
|
+
if (raw === undefined) return undefined;
|
|
1586
|
+
const 値 = raw.trim();
|
|
1587
|
+
if (値 === "") {
|
|
1588
|
+
errors.push({
|
|
1589
|
+
line,
|
|
1590
|
+
message: `${接頭}が空です`,
|
|
1591
|
+
hint: '`{状態の名前}` か数を書く (例 `opacity: "{fade}"` / `opacity: 0.5`)',
|
|
1592
|
+
});
|
|
1593
|
+
return undefined;
|
|
1594
|
+
}
|
|
1595
|
+
const n = numberOrUndef(値);
|
|
1596
|
+
return n !== undefined ? n : 値;
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
/**
|
|
1600
|
+
* 箱の幅と高さを値に追随させる欄を読む (#1392)。
|
|
1601
|
+
*
|
|
1602
|
+
* 描画側は文字列だけを受ける (`wBind?: string`)。 数を直に書いても追随のしようが無いので、
|
|
1603
|
+
* 数か文字列として読む欄とは分ける。
|
|
1604
|
+
*/
|
|
1605
|
+
function 追随する大きさとして読む(
|
|
1606
|
+
raw: string | undefined,
|
|
1607
|
+
接頭: string,
|
|
1608
|
+
line: number,
|
|
1609
|
+
errors: DslError[],
|
|
1610
|
+
): string | undefined {
|
|
1611
|
+
if (raw === undefined) return undefined;
|
|
1612
|
+
const 値 = raw.trim();
|
|
1613
|
+
if (値 === "") {
|
|
1614
|
+
errors.push({
|
|
1615
|
+
line,
|
|
1616
|
+
message: `${接頭}が空です`,
|
|
1617
|
+
hint: '`{状態の名前}` を書く (例 `wBind: "{barW}"`)',
|
|
1618
|
+
});
|
|
1619
|
+
return undefined;
|
|
1620
|
+
}
|
|
1621
|
+
return 値;
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
/**
|
|
1625
|
+
* 値に追随する箱の欄 (#1392)。
|
|
1626
|
+
*
|
|
1627
|
+
* `satisfies` で `DslActor` から欄を導く = 綴りを誤ると型検査が落ちる。
|
|
1628
|
+
*/
|
|
1629
|
+
const 値に追随する箱の欄 = [
|
|
1630
|
+
"wBind",
|
|
1631
|
+
"hBind",
|
|
1632
|
+
"opacity",
|
|
1633
|
+
"renderOffsetX",
|
|
1634
|
+
"renderOffsetY",
|
|
1635
|
+
] as const satisfies readonly (keyof DslActor)[];
|
|
1636
|
+
|
|
1637
|
+
/**
|
|
1638
|
+
* 縦に並べた時、値が空でも読み取りへ渡す欄 (#1392)。
|
|
1639
|
+
*
|
|
1640
|
+
* 他の欄は空を「書かなかった」 として落とすが、この 5 欄は描画側が空文字を数として
|
|
1641
|
+
* 読むため、落とすと書き忘れが「箱が消えた」 形で出る。
|
|
1642
|
+
*/
|
|
1643
|
+
const 空を知らせる箱の欄: ReadonlySet<string> = new Set(値に追随する箱の欄);
|
|
1644
|
+
|
|
1645
|
+
/** Object.prototype の持ち物を、表にある種類として扱わない。 */
|
|
1646
|
+
function 表から定義を引く(表: Record<string, 図形の定義>, kind: string): 図形の定義 | undefined {
|
|
1647
|
+
return Object.hasOwn(表, kind) ? 表[kind] : undefined;
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
/**
|
|
1651
|
+
* 箱の中に描く図形を読む (#1374)。 読めなければ `undefined` を返す。
|
|
1652
|
+
*
|
|
1653
|
+
* 種類が分からない形は捨てずに知らせる = 黙って捨てると、書いた図形が出ないのに
|
|
1654
|
+
* 手掛かりが 1 つも残らない。
|
|
1655
|
+
*/
|
|
1656
|
+
function 図形として読む(raw: string, line: number, errors: DslError[]): DslDynShape | undefined {
|
|
1657
|
+
const 中身 = raw.trim().replace(/^\{|\}$/g, "");
|
|
1658
|
+
const opts = parseInlineMapping(中身);
|
|
1659
|
+
const kind = (opts.kind ?? "").toLowerCase();
|
|
1660
|
+
const 定義 = 表から定義を引く(図形の表, kind);
|
|
1661
|
+
if (定義 === undefined) {
|
|
1662
|
+
errors.push({
|
|
1663
|
+
line,
|
|
1664
|
+
message: `図形の種類が読めません: "${opts.kind ?? ""}"`,
|
|
1665
|
+
hint: `使える種類 = ${Object.keys(図形の表).join(", ")}`,
|
|
1666
|
+
});
|
|
1667
|
+
return undefined;
|
|
1668
|
+
}
|
|
1669
|
+
const 読めた = 表に従って読む(定義, opts, `図形の `, line, errors);
|
|
1670
|
+
for (const 欄 of 定義.必須) if (読めた[欄] === undefined) return undefined;
|
|
1671
|
+
return { kind, ...読めた } as DslDynShape;
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
/**
|
|
1675
|
+
* 値を見せる部品を読む (#1374)。 読めなければ `undefined` を返す。
|
|
1676
|
+
*/
|
|
1677
|
+
function 部品として読む(
|
|
1678
|
+
id: string,
|
|
1679
|
+
raw: string,
|
|
1680
|
+
line: number,
|
|
1681
|
+
errors: DslError[],
|
|
1682
|
+
): DslReadout | undefined {
|
|
1683
|
+
const opts = parseInlineMapping(raw);
|
|
1684
|
+
const kind = (opts.kind ?? "").toLowerCase();
|
|
1685
|
+
const 定義 = 表から定義を引く(部品の表, kind);
|
|
1686
|
+
if (定義 === undefined) {
|
|
1687
|
+
errors.push({
|
|
1688
|
+
line,
|
|
1689
|
+
message: `部品の種類が読めません: "${opts.kind ?? ""}"`,
|
|
1690
|
+
hint: `使える種類 = ${Object.keys(部品の表).join(", ")}`,
|
|
1691
|
+
});
|
|
1692
|
+
return undefined;
|
|
1693
|
+
}
|
|
1694
|
+
const 読めた = 表に従って読む(定義, opts, `部品 ${id} の `, line, errors);
|
|
1695
|
+
部品の組を検査する(kind, 読めた, line, errors);
|
|
1696
|
+
for (const 欄 of 定義.必須) if (読めた[欄] === undefined) return undefined;
|
|
1697
|
+
return { id, kind, ...読めた } as DslReadout;
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
/**
|
|
1701
|
+
* 出来事の種類 (#1393)。 描画側の `CdlEventKind` と同じ語を並べる。
|
|
1702
|
+
*
|
|
1703
|
+
* **描画側から導けない**。 型は書き出されるが値の一覧は実行時に無いため、ここに書く。
|
|
1704
|
+
* 知らない語を書いた時の知らせがこの一覧をそのまま出すので、増えたら 1 行足す。
|
|
1705
|
+
*/
|
|
1706
|
+
export const EVENT_KINDS = [
|
|
1707
|
+
"click",
|
|
1708
|
+
"hover",
|
|
1709
|
+
"double-click",
|
|
1710
|
+
"long-press",
|
|
1711
|
+
"drag",
|
|
1712
|
+
"drop",
|
|
1713
|
+
"keydown",
|
|
1714
|
+
"focus",
|
|
1715
|
+
"blur",
|
|
1716
|
+
] as const;
|
|
1717
|
+
|
|
1718
|
+
/** 出来事の相手を指す書き方。 ちょうど 1 つだけ書く */
|
|
1719
|
+
const EVENT_TARGET_KEYS = ["box", "lane", "arrow", "diagram"] as const;
|
|
1720
|
+
|
|
1721
|
+
/**
|
|
1722
|
+
* 押下などの出来事で動く仕掛けを 1 件読む (#1393)。
|
|
1723
|
+
*
|
|
1724
|
+
* 形は `{ on: click, box: Button, handler: toggle }`。 相手の指し方は 4 つあり、
|
|
1725
|
+
* **ちょうど 1 つだけ書く** = 2 つ書くとどちらを指したのか決まらず、0 なら相手がいない。
|
|
1726
|
+
*
|
|
1727
|
+
* 相手は名前で書く。 識別子は記法で書けないため、名前から識別子への読み替えは
|
|
1728
|
+
* 組み立てが行う (`focus:` と同じ扱い)。
|
|
1729
|
+
*/
|
|
1730
|
+
function 出来事として読む(
|
|
1731
|
+
raw: string,
|
|
1732
|
+
line: number,
|
|
1733
|
+
errors: DslError[],
|
|
1734
|
+
): DslEventBinding | undefined {
|
|
1735
|
+
const t = raw.trim();
|
|
1736
|
+
if (!t.startsWith("{") || !t.endsWith("}")) {
|
|
1737
|
+
errors.push({
|
|
1738
|
+
line,
|
|
1739
|
+
message: `出来事の行が読めません: "${t}"`,
|
|
1740
|
+
hint: "`- { on: click, box: Button, handler: toggle }` の形で書く",
|
|
1741
|
+
});
|
|
1742
|
+
return undefined;
|
|
1743
|
+
}
|
|
1744
|
+
const opts = parseInlineMapping(t.slice(1, -1));
|
|
1745
|
+
for (const k of Object.keys(opts)) {
|
|
1746
|
+
if (k === "on" || k === "handler" || (EVENT_TARGET_KEYS as readonly string[]).includes(k))
|
|
1747
|
+
continue;
|
|
1748
|
+
errors.push({
|
|
1749
|
+
line,
|
|
1750
|
+
message: `出来事の項目名が読めません: "${k}"`,
|
|
1751
|
+
hint: `使える項目 = on, handler, ${EVENT_TARGET_KEYS.join(", ")}`,
|
|
1752
|
+
});
|
|
1753
|
+
return undefined;
|
|
1754
|
+
}
|
|
1755
|
+
const on = opts.on ?? "";
|
|
1756
|
+
if (!(EVENT_KINDS as readonly string[]).includes(on)) {
|
|
1757
|
+
errors.push({
|
|
1758
|
+
line,
|
|
1759
|
+
message: `出来事の種類が読めません: "${on}"`,
|
|
1760
|
+
hint: `使える種類 = ${EVENT_KINDS.join(", ")}`,
|
|
1761
|
+
});
|
|
1762
|
+
return undefined;
|
|
1763
|
+
}
|
|
1764
|
+
const handlerId = (opts.handler ?? "").trim();
|
|
1765
|
+
if (handlerId === "") {
|
|
1766
|
+
errors.push({
|
|
1767
|
+
line,
|
|
1768
|
+
message: "出来事の handler が空です",
|
|
1769
|
+
hint: "`handler: toggle-active` のように、呼び出す仕掛けの名前を書く",
|
|
1770
|
+
});
|
|
1771
|
+
return undefined;
|
|
1772
|
+
}
|
|
1773
|
+
const 書いた相手 = EVENT_TARGET_KEYS.filter((k) => opts[k] !== undefined);
|
|
1774
|
+
if (書いた相手.length !== 1) {
|
|
1775
|
+
errors.push({
|
|
1776
|
+
line,
|
|
1777
|
+
message:
|
|
1778
|
+
書いた相手.length === 0
|
|
1779
|
+
? "出来事の相手が書かれていません"
|
|
1780
|
+
: `出来事の相手を 2 つ以上書いています: ${書いた相手.join(", ")}`,
|
|
1781
|
+
hint: `${EVENT_TARGET_KEYS.join(" / ")} のどれか 1 つだけを書く`,
|
|
1782
|
+
});
|
|
1783
|
+
return undefined;
|
|
1784
|
+
}
|
|
1785
|
+
const 鍵 = 書いた相手[0]!;
|
|
1786
|
+
const 値 = (opts[鍵] ?? "").trim();
|
|
1787
|
+
let target: DslEventBinding["target"];
|
|
1788
|
+
if (鍵 === "diagram") {
|
|
1789
|
+
if (値 !== "true") {
|
|
1790
|
+
errors.push({
|
|
1791
|
+
line,
|
|
1792
|
+
message: `出来事の diagram が読めません: "${値}"`,
|
|
1793
|
+
hint: "図全体を指す時は `diagram: true` と書く",
|
|
1794
|
+
});
|
|
1795
|
+
return undefined;
|
|
1796
|
+
}
|
|
1797
|
+
target = { kind: "diagram" };
|
|
1798
|
+
} else if (鍵 === "arrow") {
|
|
1799
|
+
const m = 値.split("->");
|
|
1800
|
+
if (m.length !== 2 || m[0]!.trim() === "" || m[1]!.trim() === "") {
|
|
1801
|
+
errors.push({
|
|
1802
|
+
line,
|
|
1803
|
+
message: `出来事の矢印が読めません: "${値}"`,
|
|
1804
|
+
hint: "`arrow: A -> B` の形で、矢印の両端の名前を書く",
|
|
1805
|
+
});
|
|
1806
|
+
return undefined;
|
|
1807
|
+
}
|
|
1808
|
+
target = { kind: "edge", from: stripQuotes(m[0]!.trim()), to: stripQuotes(m[1]!.trim()) };
|
|
1809
|
+
} else {
|
|
1810
|
+
if (値 === "") {
|
|
1811
|
+
errors.push({
|
|
1812
|
+
line,
|
|
1813
|
+
message: `出来事の ${鍵} が空です`,
|
|
1814
|
+
hint: "指す相手の名前を書く",
|
|
1815
|
+
});
|
|
1816
|
+
return undefined;
|
|
1817
|
+
}
|
|
1818
|
+
target = { kind: 鍵 === "box" ? "node" : "lane", name: stripQuotes(値) };
|
|
1819
|
+
}
|
|
1820
|
+
return { event: on as DslEventBinding["event"], target, handlerId, pos: { line } };
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
/** 巻き上げに応じて進む値の欄 (#1393)。 描画側の `CdlScrollTrigger` を覆う */
|
|
1824
|
+
const SCROLL_VALUE_KINDS = {
|
|
1825
|
+
start: "数",
|
|
1826
|
+
end: "数",
|
|
1827
|
+
scrub: "数",
|
|
1828
|
+
} as const satisfies Record<string, 値の形>;
|
|
1829
|
+
|
|
1830
|
+
/**
|
|
1831
|
+
* 巻き上げに応じて進む値を 1 件読む (#1393)。
|
|
1832
|
+
*
|
|
1833
|
+
* 形は `intro: { start: 0.9, end: 0.1, scrub: 1, label: "..." }`。
|
|
1834
|
+
* 数の欄は表が読み、説明文はそのまま渡す。
|
|
1835
|
+
*/
|
|
1836
|
+
function 巻き上げとして読む(
|
|
1837
|
+
id: string,
|
|
1838
|
+
raw: string,
|
|
1839
|
+
line: number,
|
|
1840
|
+
errors: DslError[],
|
|
1841
|
+
): DslScrollTrigger | undefined {
|
|
1842
|
+
if (!isValueName(id)) {
|
|
1843
|
+
errors.push({ line, ...valueNameIssue(id) });
|
|
1844
|
+
return undefined;
|
|
1845
|
+
}
|
|
1846
|
+
const opts = parseInlineMapping(raw);
|
|
1847
|
+
const 使える = [...Object.keys(SCROLL_VALUE_KINDS), "label"];
|
|
1848
|
+
for (const k of Object.keys(opts)) {
|
|
1849
|
+
if (使える.includes(k)) continue;
|
|
1850
|
+
errors.push({
|
|
1851
|
+
line,
|
|
1852
|
+
message: `巻き上げ ${id} の項目名が読めません: "${k}"`,
|
|
1853
|
+
hint: `使える項目 = ${使える.join(", ")}`,
|
|
1854
|
+
});
|
|
1855
|
+
return undefined;
|
|
1856
|
+
}
|
|
1857
|
+
const 数 = 表で読む(SCROLL_VALUE_KINDS, opts, `巻き上げ ${id} の `, line, errors);
|
|
1858
|
+
for (const 欄 of ["start", "end", "scrub"] as const) {
|
|
1859
|
+
const 値 = 数[欄];
|
|
1860
|
+
if (値 === undefined || (値 >= 0 && 値 <= 1)) continue;
|
|
1861
|
+
errors.push({
|
|
1862
|
+
line,
|
|
1863
|
+
message: `巻き上げ ${id} の ${欄} は 0 から 1 の間で書きます: "${値}"`,
|
|
1864
|
+
hint: "0 は画面の上端または段階的な追随、1 は下端または連続追随",
|
|
1865
|
+
});
|
|
1866
|
+
}
|
|
1867
|
+
return {
|
|
1868
|
+
id,
|
|
1869
|
+
...(数.start !== undefined ? { start: 数.start } : {}),
|
|
1870
|
+
...(数.end !== undefined ? { end: 数.end } : {}),
|
|
1871
|
+
...(数.scrub !== undefined ? { scrub: 数.scrub } : {}),
|
|
1872
|
+
...(opts.label !== undefined ? { label: opts.label } : {}),
|
|
1873
|
+
};
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
/**
|
|
1877
|
+
* つまみの値から決まる値を読む (#1391)。 読めなければ `undefined` を返す。
|
|
1878
|
+
*
|
|
1879
|
+
* 形は `名前: "式"` の 1 行。 `values:` と同じ形で、**違うのは解かれる仕組み**。
|
|
1880
|
+
* あちらは段が動かす状態を読み、こちらはつまみが握る値を読む。
|
|
1881
|
+
*
|
|
1882
|
+
* 名前を中括弧で囲うかは自由 (実測 = 描画側の parser は `"{a} + 1"` と `"a + 1"` を
|
|
1883
|
+
* 同じ名前として読む)。 `values:` から式を書き写しても、そのまま通る。
|
|
1884
|
+
*
|
|
1885
|
+
* **式は描画側の parser に通す**。 自前で書き方を決めると、通ったのに描画側が
|
|
1886
|
+
* 解けない式を受けてしまう。 描画側が投げた誤りの本文をそのまま知らせに載せる。
|
|
1887
|
+
*/
|
|
1888
|
+
function 式として読む(行: string, line: number, errors: DslError[]): DslFormula | undefined {
|
|
1889
|
+
const c = 行.indexOf(":");
|
|
1890
|
+
if (c < 0) {
|
|
1891
|
+
errors.push({
|
|
1892
|
+
line,
|
|
1893
|
+
message: `式の行が読めません: "${行}"`,
|
|
1894
|
+
hint: '`名前: "式"` の形で書く (例 `doubled: "input * 2"`)',
|
|
1895
|
+
});
|
|
1896
|
+
return undefined;
|
|
1897
|
+
}
|
|
1898
|
+
const 名前 = 行.slice(0, c).trim();
|
|
1899
|
+
const 式 = stripQuotes(行.slice(c + 1).trim());
|
|
1900
|
+
if (!isValueName(名前)) {
|
|
1901
|
+
// 名前の規則は状態と揃える = 式の名前も `{名前}` で箱の文字に差し込める
|
|
1902
|
+
errors.push({ line, ...valueNameIssue(名前) });
|
|
1903
|
+
return undefined;
|
|
1904
|
+
}
|
|
1905
|
+
if (式 === "") {
|
|
1906
|
+
errors.push({
|
|
1907
|
+
line,
|
|
1908
|
+
message: `式 "${名前}" が空です`,
|
|
1909
|
+
hint: '`doubled: "input * 2"` のように式を書く',
|
|
1910
|
+
});
|
|
1911
|
+
return undefined;
|
|
1912
|
+
}
|
|
1913
|
+
try {
|
|
1914
|
+
parseFormula(式);
|
|
1915
|
+
} catch (e) {
|
|
1916
|
+
errors.push({
|
|
1917
|
+
line,
|
|
1918
|
+
message: `式 "${名前}" を読めません: ${(e as Error).message}`,
|
|
1919
|
+
hint: "使えるのは四則と括弧、比較、三項 (`a > 1 ? 2 : 3`)、`Math.min` などの関数",
|
|
1920
|
+
});
|
|
1921
|
+
return undefined;
|
|
1922
|
+
}
|
|
1923
|
+
return { id: 名前, expression: 式, pos: { line } };
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1926
|
+
/**
|
|
1927
|
+
* 読む人が動かすつまみを読む (#1389)。 読めなければ `undefined` を返す。
|
|
1928
|
+
*
|
|
1929
|
+
* 部品 (`readouts:`) と同じ経路で読む。 違いは組の並びを取る欄が無いことだけで、
|
|
1930
|
+
* 知らない欄と足りない必須欄の知らせ方は同じ。
|
|
1931
|
+
*/
|
|
1932
|
+
function つまみとして読む(
|
|
1933
|
+
id: string,
|
|
1934
|
+
raw: string,
|
|
1935
|
+
line: number,
|
|
1936
|
+
errors: DslError[],
|
|
1937
|
+
): DslInput | undefined {
|
|
1938
|
+
const opts = parseInlineMapping(raw);
|
|
1939
|
+
const kind = (opts.kind ?? "").toLowerCase();
|
|
1940
|
+
const 定義 = 表から定義を引く(つまみの表, kind);
|
|
1941
|
+
if (定義 === undefined) {
|
|
1942
|
+
errors.push({
|
|
1943
|
+
line,
|
|
1944
|
+
message: `つまみの種類が読めません: "${opts.kind ?? ""}"`,
|
|
1945
|
+
hint: `使える種類 = ${Object.keys(つまみの表).join(", ")}`,
|
|
1946
|
+
});
|
|
1947
|
+
return undefined;
|
|
1948
|
+
}
|
|
1949
|
+
const 読めた = 表に従って読む(定義, opts, `つまみ ${id} の `, line, errors);
|
|
1950
|
+
for (const 欄 of 定義.必須) if (読めた[欄] === undefined) return undefined;
|
|
1951
|
+
return { id, kind, ...読めた } as DslInput;
|
|
1952
|
+
}
|
|
1953
|
+
|
|
940
1954
|
/** 箱の中の要素の欄 (#1306)。 `DslActorNodeOverride` の全欄を覆う */
|
|
941
1955
|
export const ACTOR_NODE_VALUE_KINDS = {
|
|
942
1956
|
posX: "数",
|
|
@@ -1002,20 +2016,40 @@ function matchActorInlineMapping(raw: string): { name: string; inner: string } |
|
|
|
1002
2016
|
const name = raw.slice(0, colonIdx);
|
|
1003
2017
|
const rest = raw.slice(colonIdx + 1).trim();
|
|
1004
2018
|
if (!rest.startsWith("{")) return null;
|
|
1005
|
-
// depth count で対応 brace
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
2019
|
+
// depth count で対応 brace 探す。 正しい引用符の中の `}` は飛ばす。
|
|
2020
|
+
const 終わりを探す = (引用符を見る: boolean): number => {
|
|
2021
|
+
let depth = 0;
|
|
2022
|
+
let 引用符: '"' | "'" | null = null;
|
|
2023
|
+
let 直前: string | null = null;
|
|
2024
|
+
for (let i = 0; i < rest.length; i += 1) {
|
|
2025
|
+
const c = rest[i]!;
|
|
2026
|
+
if (引用符を見る) {
|
|
2027
|
+
if (引用符 !== null) {
|
|
2028
|
+
if (引用符 === '"' && c === "\\" && i + 1 < rest.length) {
|
|
2029
|
+
i += 1;
|
|
2030
|
+
continue;
|
|
2031
|
+
}
|
|
2032
|
+
if (c === 引用符) 引用符 = null;
|
|
2033
|
+
continue;
|
|
2034
|
+
}
|
|
2035
|
+
if ((c === '"' || c === "'") && (直前 === null || ":,{[".includes(直前))) {
|
|
2036
|
+
引用符 = c;
|
|
2037
|
+
直前 = c;
|
|
2038
|
+
continue;
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
if (c === "{") depth += 1;
|
|
2042
|
+
else if (c === "}") {
|
|
2043
|
+
depth -= 1;
|
|
2044
|
+
if (depth === 0) return i;
|
|
1016
2045
|
}
|
|
2046
|
+
if (!/\s/u.test(c)) 直前 = c;
|
|
1017
2047
|
}
|
|
1018
|
-
|
|
2048
|
+
return -1;
|
|
2049
|
+
};
|
|
2050
|
+
let endIdx = 終わりを探す(true);
|
|
2051
|
+
// 閉じない引用符は従来どおり単純分割へ戻す (#1367)。
|
|
2052
|
+
if (endIdx < 0) endIdx = 終わりを探す(false);
|
|
1019
2053
|
if (endIdx < 0) return null;
|
|
1020
2054
|
const inner = rest.slice(1, endIdx);
|
|
1021
2055
|
return { name, inner };
|
|
@@ -1278,6 +2312,12 @@ function applyContinuationLines(actor: DslActor, rest: Line[], errors: DslError[
|
|
|
1278
2312
|
const 図種ごとの欄 = new Map<string, string>();
|
|
1279
2313
|
// パーツでなければどこにも入らない項目。 パーツかどうかは block を読み終わるまで決まらない
|
|
1280
2314
|
const unknownKeys: Array<{ key: string; line: number }> = [];
|
|
2315
|
+
/**
|
|
2316
|
+
* 値に追随する 5 欄に書かれた字 (#1392)。
|
|
2317
|
+
*
|
|
2318
|
+
* 見本かどうかで読み方が変わるため、行を読む時点では振り分けない。
|
|
2319
|
+
*/
|
|
2320
|
+
const 追随する欄の生値 = new Map<string, { 値: string; line: number }>();
|
|
1281
2321
|
|
|
1282
2322
|
for (const ln of rest) {
|
|
1283
2323
|
const idx = ln.trimmed.indexOf(":");
|
|
@@ -1293,7 +2333,9 @@ function applyContinuationLines(actor: DslActor, rest: Line[], errors: DslError[
|
|
|
1293
2333
|
unknownKeys.push({ key, line: ln.no });
|
|
1294
2334
|
continue;
|
|
1295
2335
|
}
|
|
1296
|
-
|
|
2336
|
+
// 従来欄の空値は書かなかった扱いのままにする。 値に追随する 5 欄は、
|
|
2337
|
+
// 空文字を描画側が数として解釈するため、個別の読み取りへ渡して知らせる。
|
|
2338
|
+
if (!raw && !空を知らせる箱の欄.has(key)) continue;
|
|
1297
2339
|
|
|
1298
2340
|
if (COLOR_KEYS.has(key)) {
|
|
1299
2341
|
const { tone, hex } = splitColorValue(raw);
|
|
@@ -1326,6 +2368,35 @@ function applyContinuationLines(actor: DslActor, rest: Line[], errors: DslError[
|
|
|
1326
2368
|
case "値":
|
|
1327
2369
|
out.value = stripQuotes(raw);
|
|
1328
2370
|
break;
|
|
2371
|
+
case "shape":
|
|
2372
|
+
case "図形":
|
|
2373
|
+
out.shape = 図形として読む(raw, ln.no, errors);
|
|
2374
|
+
break;
|
|
2375
|
+
case "visibleIf":
|
|
2376
|
+
case "出す条件":
|
|
2377
|
+
out.visibleIf = 出す条件として読む(stripQuotes(raw), ln.no, errors);
|
|
2378
|
+
break;
|
|
2379
|
+
case "title":
|
|
2380
|
+
case "題":
|
|
2381
|
+
out.title = stripQuotes(raw);
|
|
2382
|
+
break;
|
|
2383
|
+
/*
|
|
2384
|
+
* 値に追随する 5 欄 (#1392)。 日本語の別名は置かない = 描画側の欄名がそのまま
|
|
2385
|
+
* 状態の名前と並ぶ場所なので、英字 1 種に絞って書き方の揺れを作らない。
|
|
2386
|
+
*
|
|
2387
|
+
* **ここでは読まずに書かれた字だけを控える**。 見本 (parts) では同じ名前が状態の
|
|
2388
|
+
* 上書きになり、中括弧の形は生の字を `coerceStateValue` に通す。 ここで先に読むと
|
|
2389
|
+
* `wBind: 120` が中括弧では数、縦に並べると文字列になって書き方で割れる。
|
|
2390
|
+
* 見本かどうかは `kind` で決まり、それが後ろの行に書かれることがあるため、
|
|
2391
|
+
* 全行を読み終えてから振り分ける。
|
|
2392
|
+
*/
|
|
2393
|
+
case "wBind":
|
|
2394
|
+
case "hBind":
|
|
2395
|
+
case "opacity":
|
|
2396
|
+
case "renderOffsetX":
|
|
2397
|
+
case "renderOffsetY":
|
|
2398
|
+
追随する欄の生値.set(key, { 値: stripQuotes(raw), line: ln.no });
|
|
2399
|
+
break;
|
|
1329
2400
|
case "rows":
|
|
1330
2401
|
case "行":
|
|
1331
2402
|
out.rows = raw
|
|
@@ -1425,11 +2496,7 @@ function applyContinuationLines(actor: DslActor, rest: Line[], errors: DslError[
|
|
|
1425
2496
|
// 名前の行と縦に並べた行の両方に倍率がある形では、後に書いた縦の行を採る。
|
|
1426
2497
|
// 知らせ (`scale-reserved`) は書かれた名前をすべて見るので、名前だけは足し合わせる
|
|
1427
2498
|
if (scaleWritten.size > 0) {
|
|
1428
|
-
const s = resolveScale(
|
|
1429
|
-
scaleWritten,
|
|
1430
|
-
(key) => scaleLines.get(key) ?? actor.pos.line,
|
|
1431
|
-
errors,
|
|
1432
|
-
);
|
|
2499
|
+
const s = resolveScale(scaleWritten, (key) => scaleLines.get(key) ?? actor.pos.line, errors);
|
|
1433
2500
|
out.scale = s.scale;
|
|
1434
2501
|
out.scaleKeys = [...new Set([...(actor.scaleKeys ?? []), ...s.keys])];
|
|
1435
2502
|
}
|
|
@@ -1447,9 +2514,31 @@ function applyContinuationLines(actor: DslActor, rest: Line[], errors: DslError[
|
|
|
1447
2514
|
}
|
|
1448
2515
|
// 状態も倍率も parts でだけ意味を持つ。 パーツなら知らせずに返す
|
|
1449
2516
|
if (out.partId !== undefined) {
|
|
2517
|
+
/*
|
|
2518
|
+
* 見本では 5 欄も他の名前と同じく状態の上書きになる。
|
|
2519
|
+
*
|
|
2520
|
+
* **中括弧の形と同じ `coerceStateValue` を通す**。 専用の読み取りを通した値を移すと、
|
|
2521
|
+
* `wBind: 120` が中括弧では数、縦に並べると文字列になって書き方で割れる (Round 2 の指摘)。
|
|
2522
|
+
*/
|
|
2523
|
+
for (const [欄, { 値 }] of 追随する欄の生値) {
|
|
2524
|
+
state[欄] = coerceStateValue(値);
|
|
2525
|
+
touchedState = true;
|
|
2526
|
+
}
|
|
1450
2527
|
if (touchedState) out.stateOverride = state;
|
|
1451
2528
|
return out;
|
|
1452
2529
|
}
|
|
2530
|
+
/*
|
|
2531
|
+
* 普通の箱では専用の欄として読む (#1392)。
|
|
2532
|
+
*
|
|
2533
|
+
* 見本かどうかが決まってから読むため、空の知らせも見本でない箱にだけ出る。
|
|
2534
|
+
*/
|
|
2535
|
+
for (const [欄, { 値, line }] of 追随する欄の生値) {
|
|
2536
|
+
if (欄 === "wBind" || 欄 === "hBind") {
|
|
2537
|
+
out[欄] = 追随する大きさとして読む(値, `箱の ${欄} `, line, errors);
|
|
2538
|
+
} else if (欄 === "opacity" || 欄 === "renderOffsetX" || 欄 === "renderOffsetY") {
|
|
2539
|
+
out[欄] = 値に追随する欄として読む(値, `箱の ${欄} `, line, errors);
|
|
2540
|
+
}
|
|
2541
|
+
}
|
|
1453
2542
|
// パーツでない箱に書かれた見知らぬ項目は、 どこにも入らずに消える。 黙って捨てると
|
|
1454
2543
|
// 「書いたのに図が変わらない」 が手掛かりなしで起きるので、 綴りの誤りとして知らせる
|
|
1455
2544
|
for (const u of unknownKeys) {
|
|
@@ -1477,6 +2566,21 @@ export const ACTOR_ITEM_KEYS: ReadonlySet<string> = new Set([
|
|
|
1477
2566
|
"値",
|
|
1478
2567
|
"rows",
|
|
1479
2568
|
"行",
|
|
2569
|
+
// 箱の中に描く図形 (#1374)
|
|
2570
|
+
"shape",
|
|
2571
|
+
"図形",
|
|
2572
|
+
// その箱を出すかどうかの条件 (#1381)
|
|
2573
|
+
"visibleIf",
|
|
2574
|
+
"出す条件",
|
|
2575
|
+
// 箱に出す題 (#1381)
|
|
2576
|
+
"title",
|
|
2577
|
+
"題",
|
|
2578
|
+
// 値に追随する 5 欄 (#1392)
|
|
2579
|
+
"wBind",
|
|
2580
|
+
"hBind",
|
|
2581
|
+
"opacity",
|
|
2582
|
+
"renderOffsetX",
|
|
2583
|
+
"renderOffsetY",
|
|
1480
2584
|
"位置",
|
|
1481
2585
|
"pos",
|
|
1482
2586
|
"posX",
|
|
@@ -1815,6 +2919,18 @@ export const INLINE_ACTOR_KEYS: ReadonlySet<string> = new Set([
|
|
|
1815
2919
|
"posY",
|
|
1816
2920
|
"posW",
|
|
1817
2921
|
"posH",
|
|
2922
|
+
// 箱の中に描く図形 (#1374)
|
|
2923
|
+
"shape",
|
|
2924
|
+
// その箱を出すかどうかの条件 (#1381)
|
|
2925
|
+
"visibleIf",
|
|
2926
|
+
// 箱に出す題 (#1381)。 名前と切り離して書ける
|
|
2927
|
+
"title",
|
|
2928
|
+
// 値に追随する 5 欄 (#1392)
|
|
2929
|
+
"wBind",
|
|
2930
|
+
"hBind",
|
|
2931
|
+
"opacity",
|
|
2932
|
+
"renderOffsetX",
|
|
2933
|
+
"renderOffsetY",
|
|
1818
2934
|
// 倍率は別経路 (`reportScaleOnNonPart`) が知らせる。 ここでも読める扱いにしないと
|
|
1819
2935
|
// 同じ名前で 2 度知らせることになる
|
|
1820
2936
|
"scale",
|
|
@@ -1834,10 +2950,28 @@ export const INLINE_ACTOR_KEYS: ReadonlySet<string> = new Set([
|
|
|
1834
2950
|
* 持つ一覧が実装と drift する = 欄を足しても誰も気付けない。 表を唯一の出どころにして、
|
|
1835
2951
|
* `FLOW_INLINE_KEYS` から一覧を導けるようにする。
|
|
1836
2952
|
*/
|
|
2953
|
+
const EDGE_BIND_INLINE_READERS = {
|
|
2954
|
+
widthBind: (v: string | undefined) => v,
|
|
2955
|
+
strokeBind: (v: string | undefined) => v,
|
|
2956
|
+
dashOffsetBind: (v: string | undefined) => v,
|
|
2957
|
+
} as const;
|
|
2958
|
+
|
|
1837
2959
|
const FLOW_INLINE_READERS = {
|
|
1838
2960
|
sub: (v: string | undefined) => v,
|
|
1839
2961
|
guard: (v: string | undefined) => v,
|
|
1840
2962
|
cardinality: (v: string | undefined) => v,
|
|
2963
|
+
// 矢印がどの辺から出るか (#1385)。 描画側は 4 方向を取り、書かなければ自動で選ぶ
|
|
2964
|
+
side: (v: string | undefined) =>
|
|
2965
|
+
v !== undefined && (EDGE_SIDE_VALUES as readonly string[]).includes(v)
|
|
2966
|
+
? (v as "top" | "right" | "bottom" | "left")
|
|
2967
|
+
: undefined,
|
|
2968
|
+
/*
|
|
2969
|
+
* 矢印を値に追随させる 3 欄 (#1396)。 箱の `wBind` (#1392) と同じく文字列だけを取る。
|
|
2970
|
+
*
|
|
2971
|
+
* ここでは字をそのまま通し、空かどうかは呼出側が知らせる (表の読み手は行番号を
|
|
2972
|
+
* 持たないため、知らせを出せる場所で見る)。
|
|
2973
|
+
*/
|
|
2974
|
+
...EDGE_BIND_INLINE_READERS,
|
|
1841
2975
|
// 数と真偽の欄は `FLOW_INLINE_VALUE_KINDS` の表が読む (#1306)。 ここでは名前だけを持つ =
|
|
1842
2976
|
// 読める欄の一覧 (`FLOW_INLINE_KEYS`) は本表から導くため、載せないと欄ごと消える
|
|
1843
2977
|
labelOffsetX: null,
|
|
@@ -1946,6 +3080,32 @@ function parseActor(line: Line, errors: DslError[]): DslActor | null {
|
|
|
1946
3080
|
.filter(Boolean)
|
|
1947
3081
|
: undefined,
|
|
1948
3082
|
lane: opts.lane,
|
|
3083
|
+
// 箱の中に描く図形 (#1374)。 パーツでは状態の上書きとして意味を持つため横取りしない
|
|
3084
|
+
shape:
|
|
3085
|
+
isPart || opts.shape === undefined
|
|
3086
|
+
? undefined
|
|
3087
|
+
: 図形として読む(opts.shape, line.no, errors),
|
|
3088
|
+
// 出す条件 (#1381)。 パーツでは状態の上書きとして意味を持つため横取りしない
|
|
3089
|
+
visibleIf: isPart ? undefined : 出す条件として読む(opts.visibleIf, line.no, errors),
|
|
3090
|
+
// 箱に出す題 (#1381)。 空文字も意味を持つ (題を出さない箱) ため undefined と分ける
|
|
3091
|
+
title: isPart ? undefined : opts.title,
|
|
3092
|
+
// 値に追随する 5 欄 (#1392)。 図形や出す条件と同じく、パーツでは状態の上書きとして
|
|
3093
|
+
// 意味を持つため横取りしない
|
|
3094
|
+
wBind: isPart
|
|
3095
|
+
? undefined
|
|
3096
|
+
: 追随する大きさとして読む(opts.wBind, "箱の wBind ", line.no, errors),
|
|
3097
|
+
hBind: isPart
|
|
3098
|
+
? undefined
|
|
3099
|
+
: 追随する大きさとして読む(opts.hBind, "箱の hBind ", line.no, errors),
|
|
3100
|
+
opacity: isPart
|
|
3101
|
+
? undefined
|
|
3102
|
+
: 値に追随する欄として読む(opts.opacity, "箱の opacity ", line.no, errors),
|
|
3103
|
+
renderOffsetX: isPart
|
|
3104
|
+
? undefined
|
|
3105
|
+
: 値に追随する欄として読む(opts.renderOffsetX, "箱の renderOffsetX ", line.no, errors),
|
|
3106
|
+
renderOffsetY: isPart
|
|
3107
|
+
? undefined
|
|
3108
|
+
: 値に追随する欄として読む(opts.renderOffsetY, "箱の renderOffsetY ", line.no, errors),
|
|
1949
3109
|
...表で読む(ACTOR_INLINE_VALUE_KINDS, opts, "箱の ", line.no, errors),
|
|
1950
3110
|
// parts では `tone` を状態の上書きとして従来から使えるため、 色として横取りしない
|
|
1951
3111
|
tone: isPart ? undefined : resolveTone(opts.tone),
|
|
@@ -2024,21 +3184,60 @@ function parseFlowStep(line: Line, no: number, errors: DslError[]): DslStep | nu
|
|
|
2024
3184
|
// 欄は `FLOW_INLINE_READERS` の表から読む。 個別に並べると一覧が実装と drift する
|
|
2025
3185
|
const 中括弧: Partial<Record<keyof typeof FLOW_INLINE_READERS, unknown>> = {};
|
|
2026
3186
|
// inline option (`{ ... }`) を末尾から抽出
|
|
2027
|
-
const
|
|
3187
|
+
const 塊 = 末尾の中括弧を切り出す(rest);
|
|
2028
3188
|
let 数と真偽: 読んだ結果<typeof FLOW_INLINE_VALUE_KINDS> | undefined;
|
|
2029
|
-
if (
|
|
2030
|
-
const opts = parseInlineMapping(
|
|
3189
|
+
if (塊) {
|
|
3190
|
+
const opts = parseInlineMapping(塊.中身);
|
|
3191
|
+
// `parseInlineMapping` は値が 1 文字もない `widthBind:` を拾わない。 3 欄は空を
|
|
3192
|
+
// 「書かなかった」扱いにせず知らせる契約なので、書かれた値を空も含めて上書きする。
|
|
3193
|
+
// 同じ欄を複数回書いた時は通常の mapping と同じく後勝ちにする。
|
|
3194
|
+
for (const field of splitInlineFields(塊.中身)) {
|
|
3195
|
+
const idx = field.indexOf(":");
|
|
3196
|
+
if (idx < 0) continue;
|
|
3197
|
+
const key = field.slice(0, idx).trim();
|
|
3198
|
+
if (!Object.hasOwn(EDGE_BIND_INLINE_READERS, key)) continue;
|
|
3199
|
+
opts[key] = stripQuotes(field.slice(idx + 1).trim());
|
|
3200
|
+
}
|
|
2031
3201
|
// 文字列の欄はそのまま入れ、数と真偽の欄は表が読んで読めない値を知らせる (#1306)
|
|
2032
3202
|
for (const k of FLOW_INLINE_KEYS) {
|
|
2033
|
-
|
|
2034
|
-
|
|
3203
|
+
const 読み手 = FLOW_INLINE_READERS[k];
|
|
3204
|
+
if (読み手 === null) continue;
|
|
3205
|
+
中括弧[k] = 読み手(opts[k]);
|
|
3206
|
+
}
|
|
3207
|
+
if (opts.side !== undefined && 中括弧.side === undefined) {
|
|
3208
|
+
errors.push({
|
|
3209
|
+
line: line.no,
|
|
3210
|
+
message: `矢印の side が読めません: "${opts.side}"`,
|
|
3211
|
+
hint: `使える値 = ${EDGE_SIDE_VALUES.join(", ")}`,
|
|
3212
|
+
});
|
|
2035
3213
|
}
|
|
2036
3214
|
数と真偽 = 表で読む(FLOW_INLINE_VALUE_KINDS, opts, "矢印の ", line.no, errors);
|
|
2037
|
-
rest =
|
|
3215
|
+
rest = 塊.前.trim();
|
|
2038
3216
|
}
|
|
2039
3217
|
const sub = 中括弧.sub as string | undefined;
|
|
2040
3218
|
const guard = 中括弧.guard as string | undefined;
|
|
2041
3219
|
const cardinality = 中括弧.cardinality as string | undefined;
|
|
3220
|
+
const side = 中括弧.side as "top" | "right" | "bottom" | "left" | undefined;
|
|
3221
|
+
// 値に追随する 3 欄 (#1396)。 空は捨てずに知らせる = 描画側は空文字を既定値へ落とさず
|
|
3222
|
+
// そのまま置換に使うため、書き忘れが「線が消えた」 形で出る
|
|
3223
|
+
const widthBind = 追随する大きさとして読む(
|
|
3224
|
+
中括弧.widthBind as string | undefined,
|
|
3225
|
+
"矢印の widthBind ",
|
|
3226
|
+
line.no,
|
|
3227
|
+
errors,
|
|
3228
|
+
);
|
|
3229
|
+
const strokeBind = 追随する大きさとして読む(
|
|
3230
|
+
中括弧.strokeBind as string | undefined,
|
|
3231
|
+
"矢印の strokeBind ",
|
|
3232
|
+
line.no,
|
|
3233
|
+
errors,
|
|
3234
|
+
);
|
|
3235
|
+
const dashOffsetBind = 追随する大きさとして読む(
|
|
3236
|
+
中括弧.dashOffsetBind as string | undefined,
|
|
3237
|
+
"矢印の dashOffsetBind ",
|
|
3238
|
+
line.no,
|
|
3239
|
+
errors,
|
|
3240
|
+
);
|
|
2042
3241
|
const labelOffsetX = 数と真偽?.labelOffsetX;
|
|
2043
3242
|
const labelOffsetY = 数と真偽?.labelOffsetY;
|
|
2044
3243
|
const overlay = 数と真偽?.overlay;
|
|
@@ -2098,6 +3297,10 @@ function parseFlowStep(line: Line, no: number, errors: DslError[]): DslStep | nu
|
|
|
2098
3297
|
sub,
|
|
2099
3298
|
guard,
|
|
2100
3299
|
cardinality,
|
|
3300
|
+
side,
|
|
3301
|
+
widthBind,
|
|
3302
|
+
strokeBind,
|
|
3303
|
+
dashOffsetBind,
|
|
2101
3304
|
labelOffsetX,
|
|
2102
3305
|
labelOffsetY,
|
|
2103
3306
|
overlay,
|
|
@@ -2220,7 +3423,15 @@ function ensureAnimate(a: DslAnimate | undefined, lineNo: number): DslAnimate {
|
|
|
2220
3423
|
* (#1330)。 段の項目と値は階層が違うので衝突しないが、同じ語が 2 つの意味で並ぶと
|
|
2221
3424
|
* 見本を読む人が階層から意味を判断することになる。
|
|
2222
3425
|
*/
|
|
2223
|
-
export const PHASE_ITEM_WORDS = [
|
|
3426
|
+
export const PHASE_ITEM_WORDS = [
|
|
3427
|
+
"focus",
|
|
3428
|
+
"badge",
|
|
3429
|
+
"body",
|
|
3430
|
+
"description",
|
|
3431
|
+
"tween",
|
|
3432
|
+
"set",
|
|
3433
|
+
"draw",
|
|
3434
|
+
] as const;
|
|
2224
3435
|
|
|
2225
3436
|
const 段の項目の英語 = PHASE_ITEM_WORDS;
|
|
2226
3437
|
const 段の項目の集合: ReadonlySet<string> = new Set(PHASE_ITEM_WORDS);
|