@cardenelabs/dragon 0.13.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -34
- package/dist/index.cjs +1511 -30
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +402 -3
- package/dist/index.d.ts +402 -3
- package/dist/index.js +1512 -31
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/color.ts +31 -6
- package/src/compile.ts +268 -9
- package/src/input-size.ts +12 -0
- package/src/json-parser.ts +456 -21
- package/src/schemas/diagram.json +356 -0
- package/src/types.ts +91 -0
- package/src/v05/input-table.generated.ts +136 -0
- package/src/v05/parser-types.ts +5 -1
- package/src/v05/parser.ts +695 -8
package/src/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 {
|
|
@@ -58,6 +58,10 @@ import type {
|
|
|
58
58
|
DslActor,
|
|
59
59
|
DslDynShape,
|
|
60
60
|
DslReadout,
|
|
61
|
+
DslInput,
|
|
62
|
+
DslFormula,
|
|
63
|
+
DslEventBinding,
|
|
64
|
+
DslScrollTrigger,
|
|
61
65
|
DslActorNodeOverride,
|
|
62
66
|
DslStep,
|
|
63
67
|
DslAnimate,
|
|
@@ -102,6 +106,14 @@ export const TOP_LEVEL_KEYS = [
|
|
|
102
106
|
"axes",
|
|
103
107
|
// 値を見せる部品 (#1374)
|
|
104
108
|
"readouts",
|
|
109
|
+
// 読む人が動かすつまみ (#1389)
|
|
110
|
+
"inputs",
|
|
111
|
+
// つまみの値から決まる値 (#1391)
|
|
112
|
+
"formulas",
|
|
113
|
+
// 押下などの出来事で動く仕掛け (#1393)
|
|
114
|
+
"events",
|
|
115
|
+
// 巻き上げに応じて進む値 (#1393)
|
|
116
|
+
"scrolls",
|
|
105
117
|
] as const;
|
|
106
118
|
|
|
107
119
|
/**
|
|
@@ -170,6 +182,54 @@ function 名前と中括弧に割る(行: string): [string, string] | undefined
|
|
|
170
182
|
return undefined;
|
|
171
183
|
}
|
|
172
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
|
+
|
|
173
233
|
function isTopLevelKey(key: string): key is (typeof TOP_LEVEL_KEYS)[number] {
|
|
174
234
|
return (TOP_LEVEL_KEYS as readonly string[]).includes(key);
|
|
175
235
|
}
|
|
@@ -333,6 +393,11 @@ export function parseTextDslV05(src: string): V05ParseResult {
|
|
|
333
393
|
let viewport: DslViewport | undefined = undefined;
|
|
334
394
|
let lanesMap: Record<string, DslLane> | undefined = undefined;
|
|
335
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>();
|
|
336
401
|
let groupsMap: Record<string, DslGroup> | undefined = undefined;
|
|
337
402
|
|
|
338
403
|
let i = 0;
|
|
@@ -626,6 +691,123 @@ export function parseTextDslV05(src: string): V05ParseResult {
|
|
|
626
691
|
i = next;
|
|
627
692
|
continue;
|
|
628
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
|
+
}
|
|
629
811
|
if (head.key === "groups") {
|
|
630
812
|
// groups:\n aws: { label: "AWS", lanes: [ecs, rds] }
|
|
631
813
|
const { items, next } = collectIndentedList(lines, i + 1, line.indent);
|
|
@@ -669,6 +851,20 @@ export function parseTextDslV05(src: string): V05ParseResult {
|
|
|
669
851
|
hint: "add `type: sequence|flow|swimlane|er|state|topology|solidity|gantt|class|pie|c4|mind`",
|
|
670
852
|
});
|
|
671
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
|
+
|
|
672
868
|
if (errors.length > 0) return { ok: false, errors };
|
|
673
869
|
|
|
674
870
|
return {
|
|
@@ -685,6 +881,10 @@ export function parseTextDslV05(src: string): V05ParseResult {
|
|
|
685
881
|
viewport,
|
|
686
882
|
lanes: lanesMap,
|
|
687
883
|
readouts: readoutsList,
|
|
884
|
+
inputs: inputsList,
|
|
885
|
+
formulas: formulasList,
|
|
886
|
+
events: eventsList,
|
|
887
|
+
scrolls: scrollsList,
|
|
688
888
|
groups: groupsMap,
|
|
689
889
|
pos: { line: 1 },
|
|
690
890
|
},
|
|
@@ -1109,6 +1309,18 @@ export const 図形の表: Record<string, 図形の定義> = {
|
|
|
1109
1309
|
*/
|
|
1110
1310
|
export { 部品の表, 部品の組の表 } from "./readout-table.generated";
|
|
1111
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";
|
|
1112
1324
|
import type { 図形の定義 } from "./parser-types";
|
|
1113
1325
|
|
|
1114
1326
|
/** `[a, b]` の形を文字列の並びに読む */
|
|
@@ -1266,6 +1478,25 @@ function 表に従って読む(
|
|
|
1266
1478
|
out[欄] = n !== undefined ? n : 値;
|
|
1267
1479
|
} else if (形 === "文字列の並び") {
|
|
1268
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
|
+
}
|
|
1269
1500
|
} else if (形 === "組の並び") {
|
|
1270
1501
|
const 組 = 組の並びとして読む(値);
|
|
1271
1502
|
if (組 !== undefined) out[欄] = 組;
|
|
@@ -1336,6 +1567,86 @@ function 出す条件として読む(
|
|
|
1336
1567
|
return 値;
|
|
1337
1568
|
}
|
|
1338
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
|
+
|
|
1339
1650
|
/**
|
|
1340
1651
|
* 箱の中に描く図形を読む (#1374)。 読めなければ `undefined` を返す。
|
|
1341
1652
|
*
|
|
@@ -1346,7 +1657,7 @@ function 図形として読む(raw: string, line: number, errors: DslError[]): D
|
|
|
1346
1657
|
const 中身 = raw.trim().replace(/^\{|\}$/g, "");
|
|
1347
1658
|
const opts = parseInlineMapping(中身);
|
|
1348
1659
|
const kind = (opts.kind ?? "").toLowerCase();
|
|
1349
|
-
const 定義 =
|
|
1660
|
+
const 定義 = 表から定義を引く(図形の表, kind);
|
|
1350
1661
|
if (定義 === undefined) {
|
|
1351
1662
|
errors.push({
|
|
1352
1663
|
line,
|
|
@@ -1371,7 +1682,7 @@ function 部品として読む(
|
|
|
1371
1682
|
): DslReadout | undefined {
|
|
1372
1683
|
const opts = parseInlineMapping(raw);
|
|
1373
1684
|
const kind = (opts.kind ?? "").toLowerCase();
|
|
1374
|
-
const 定義 =
|
|
1685
|
+
const 定義 = 表から定義を引く(部品の表, kind);
|
|
1375
1686
|
if (定義 === undefined) {
|
|
1376
1687
|
errors.push({
|
|
1377
1688
|
line,
|
|
@@ -1386,6 +1697,260 @@ function 部品として読む(
|
|
|
1386
1697
|
return { id, kind, ...読めた } as DslReadout;
|
|
1387
1698
|
}
|
|
1388
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
|
+
|
|
1389
1954
|
/** 箱の中の要素の欄 (#1306)。 `DslActorNodeOverride` の全欄を覆う */
|
|
1390
1955
|
export const ACTOR_NODE_VALUE_KINDS = {
|
|
1391
1956
|
posX: "数",
|
|
@@ -1747,6 +2312,12 @@ function applyContinuationLines(actor: DslActor, rest: Line[], errors: DslError[
|
|
|
1747
2312
|
const 図種ごとの欄 = new Map<string, string>();
|
|
1748
2313
|
// パーツでなければどこにも入らない項目。 パーツかどうかは block を読み終わるまで決まらない
|
|
1749
2314
|
const unknownKeys: Array<{ key: string; line: number }> = [];
|
|
2315
|
+
/**
|
|
2316
|
+
* 値に追随する 5 欄に書かれた字 (#1392)。
|
|
2317
|
+
*
|
|
2318
|
+
* 見本かどうかで読み方が変わるため、行を読む時点では振り分けない。
|
|
2319
|
+
*/
|
|
2320
|
+
const 追随する欄の生値 = new Map<string, { 値: string; line: number }>();
|
|
1750
2321
|
|
|
1751
2322
|
for (const ln of rest) {
|
|
1752
2323
|
const idx = ln.trimmed.indexOf(":");
|
|
@@ -1762,7 +2333,9 @@ function applyContinuationLines(actor: DslActor, rest: Line[], errors: DslError[
|
|
|
1762
2333
|
unknownKeys.push({ key, line: ln.no });
|
|
1763
2334
|
continue;
|
|
1764
2335
|
}
|
|
1765
|
-
|
|
2336
|
+
// 従来欄の空値は書かなかった扱いのままにする。 値に追随する 5 欄は、
|
|
2337
|
+
// 空文字を描画側が数として解釈するため、個別の読み取りへ渡して知らせる。
|
|
2338
|
+
if (!raw && !空を知らせる箱の欄.has(key)) continue;
|
|
1766
2339
|
|
|
1767
2340
|
if (COLOR_KEYS.has(key)) {
|
|
1768
2341
|
const { tone, hex } = splitColorValue(raw);
|
|
@@ -1807,6 +2380,23 @@ function applyContinuationLines(actor: DslActor, rest: Line[], errors: DslError[
|
|
|
1807
2380
|
case "題":
|
|
1808
2381
|
out.title = stripQuotes(raw);
|
|
1809
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;
|
|
1810
2400
|
case "rows":
|
|
1811
2401
|
case "行":
|
|
1812
2402
|
out.rows = raw
|
|
@@ -1924,9 +2514,31 @@ function applyContinuationLines(actor: DslActor, rest: Line[], errors: DslError[
|
|
|
1924
2514
|
}
|
|
1925
2515
|
// 状態も倍率も parts でだけ意味を持つ。 パーツなら知らせずに返す
|
|
1926
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
|
+
}
|
|
1927
2527
|
if (touchedState) out.stateOverride = state;
|
|
1928
2528
|
return out;
|
|
1929
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
|
+
}
|
|
1930
2542
|
// パーツでない箱に書かれた見知らぬ項目は、 どこにも入らずに消える。 黙って捨てると
|
|
1931
2543
|
// 「書いたのに図が変わらない」 が手掛かりなしで起きるので、 綴りの誤りとして知らせる
|
|
1932
2544
|
for (const u of unknownKeys) {
|
|
@@ -1963,6 +2575,12 @@ export const ACTOR_ITEM_KEYS: ReadonlySet<string> = new Set([
|
|
|
1963
2575
|
// 箱に出す題 (#1381)
|
|
1964
2576
|
"title",
|
|
1965
2577
|
"題",
|
|
2578
|
+
// 値に追随する 5 欄 (#1392)
|
|
2579
|
+
"wBind",
|
|
2580
|
+
"hBind",
|
|
2581
|
+
"opacity",
|
|
2582
|
+
"renderOffsetX",
|
|
2583
|
+
"renderOffsetY",
|
|
1966
2584
|
"位置",
|
|
1967
2585
|
"pos",
|
|
1968
2586
|
"posX",
|
|
@@ -2307,6 +2925,12 @@ export const INLINE_ACTOR_KEYS: ReadonlySet<string> = new Set([
|
|
|
2307
2925
|
"visibleIf",
|
|
2308
2926
|
// 箱に出す題 (#1381)。 名前と切り離して書ける
|
|
2309
2927
|
"title",
|
|
2928
|
+
// 値に追随する 5 欄 (#1392)
|
|
2929
|
+
"wBind",
|
|
2930
|
+
"hBind",
|
|
2931
|
+
"opacity",
|
|
2932
|
+
"renderOffsetX",
|
|
2933
|
+
"renderOffsetY",
|
|
2310
2934
|
// 倍率は別経路 (`reportScaleOnNonPart`) が知らせる。 ここでも読める扱いにしないと
|
|
2311
2935
|
// 同じ名前で 2 度知らせることになる
|
|
2312
2936
|
"scale",
|
|
@@ -2326,6 +2950,12 @@ export const INLINE_ACTOR_KEYS: ReadonlySet<string> = new Set([
|
|
|
2326
2950
|
* 持つ一覧が実装と drift する = 欄を足しても誰も気付けない。 表を唯一の出どころにして、
|
|
2327
2951
|
* `FLOW_INLINE_KEYS` から一覧を導けるようにする。
|
|
2328
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
|
+
|
|
2329
2959
|
const FLOW_INLINE_READERS = {
|
|
2330
2960
|
sub: (v: string | undefined) => v,
|
|
2331
2961
|
guard: (v: string | undefined) => v,
|
|
@@ -2335,6 +2965,13 @@ const FLOW_INLINE_READERS = {
|
|
|
2335
2965
|
v !== undefined && (EDGE_SIDE_VALUES as readonly string[]).includes(v)
|
|
2336
2966
|
? (v as "top" | "right" | "bottom" | "left")
|
|
2337
2967
|
: undefined,
|
|
2968
|
+
/*
|
|
2969
|
+
* 矢印を値に追随させる 3 欄 (#1396)。 箱の `wBind` (#1392) と同じく文字列だけを取る。
|
|
2970
|
+
*
|
|
2971
|
+
* ここでは字をそのまま通し、空かどうかは呼出側が知らせる (表の読み手は行番号を
|
|
2972
|
+
* 持たないため、知らせを出せる場所で見る)。
|
|
2973
|
+
*/
|
|
2974
|
+
...EDGE_BIND_INLINE_READERS,
|
|
2338
2975
|
// 数と真偽の欄は `FLOW_INLINE_VALUE_KINDS` の表が読む (#1306)。 ここでは名前だけを持つ =
|
|
2339
2976
|
// 読める欄の一覧 (`FLOW_INLINE_KEYS`) は本表から導くため、載せないと欄ごと消える
|
|
2340
2977
|
labelOffsetX: null,
|
|
@@ -2452,6 +3089,23 @@ function parseActor(line: Line, errors: DslError[]): DslActor | null {
|
|
|
2452
3089
|
visibleIf: isPart ? undefined : 出す条件として読む(opts.visibleIf, line.no, errors),
|
|
2453
3090
|
// 箱に出す題 (#1381)。 空文字も意味を持つ (題を出さない箱) ため undefined と分ける
|
|
2454
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),
|
|
2455
3109
|
...表で読む(ACTOR_INLINE_VALUE_KINDS, opts, "箱の ", line.no, errors),
|
|
2456
3110
|
// parts では `tone` を状態の上書きとして従来から使えるため、 色として横取りしない
|
|
2457
3111
|
tone: isPart ? undefined : resolveTone(opts.tone),
|
|
@@ -2530,10 +3184,20 @@ function parseFlowStep(line: Line, no: number, errors: DslError[]): DslStep | nu
|
|
|
2530
3184
|
// 欄は `FLOW_INLINE_READERS` の表から読む。 個別に並べると一覧が実装と drift する
|
|
2531
3185
|
const 中括弧: Partial<Record<keyof typeof FLOW_INLINE_READERS, unknown>> = {};
|
|
2532
3186
|
// inline option (`{ ... }`) を末尾から抽出
|
|
2533
|
-
const
|
|
3187
|
+
const 塊 = 末尾の中括弧を切り出す(rest);
|
|
2534
3188
|
let 数と真偽: 読んだ結果<typeof FLOW_INLINE_VALUE_KINDS> | undefined;
|
|
2535
|
-
if (
|
|
2536
|
-
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
|
+
}
|
|
2537
3201
|
// 文字列の欄はそのまま入れ、数と真偽の欄は表が読んで読めない値を知らせる (#1306)
|
|
2538
3202
|
for (const k of FLOW_INLINE_KEYS) {
|
|
2539
3203
|
const 読み手 = FLOW_INLINE_READERS[k];
|
|
@@ -2548,12 +3212,32 @@ function parseFlowStep(line: Line, no: number, errors: DslError[]): DslStep | nu
|
|
|
2548
3212
|
});
|
|
2549
3213
|
}
|
|
2550
3214
|
数と真偽 = 表で読む(FLOW_INLINE_VALUE_KINDS, opts, "矢印の ", line.no, errors);
|
|
2551
|
-
rest =
|
|
3215
|
+
rest = 塊.前.trim();
|
|
2552
3216
|
}
|
|
2553
3217
|
const sub = 中括弧.sub as string | undefined;
|
|
2554
3218
|
const guard = 中括弧.guard as string | undefined;
|
|
2555
3219
|
const cardinality = 中括弧.cardinality as string | undefined;
|
|
2556
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
|
+
);
|
|
2557
3241
|
const labelOffsetX = 数と真偽?.labelOffsetX;
|
|
2558
3242
|
const labelOffsetY = 数と真偽?.labelOffsetY;
|
|
2559
3243
|
const overlay = 数と真偽?.overlay;
|
|
@@ -2614,6 +3298,9 @@ function parseFlowStep(line: Line, no: number, errors: DslError[]): DslStep | nu
|
|
|
2614
3298
|
guard,
|
|
2615
3299
|
cardinality,
|
|
2616
3300
|
side,
|
|
3301
|
+
widthBind,
|
|
3302
|
+
strokeBind,
|
|
3303
|
+
dashOffsetBind,
|
|
2617
3304
|
labelOffsetX,
|
|
2618
3305
|
labelOffsetY,
|
|
2619
3306
|
overlay,
|