@cardenelabs/dragon 0.8.0 → 0.9.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 +8 -4
- package/dist/index.cjs +6378 -5214
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +340 -8
- package/dist/index.d.ts +340 -8
- package/dist/index.js +6374 -5215
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/compile.ts +65 -1
- package/src/index.ts +26 -15
- package/src/json-parser.ts +928 -99
- package/src/schemas/diagram.json +368 -44
- package/src/tokenize.ts +289 -0
- package/src/types.ts +19 -0
- package/src/v05/parser.ts +670 -138
package/src/v05/parser.ts
CHANGED
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
|
|
44
44
|
import type { NodeKind, Tone, EdgeStyle } from "@cardenelabs/cdl";
|
|
45
45
|
import { TONES, NODE_KINDS } from "@cardenelabs/cdl";
|
|
46
|
-
import { TONE_ALIAS } from "../keywords";
|
|
46
|
+
import { TONE_ALIAS, NODE_KIND_ALIAS } from "../keywords";
|
|
47
47
|
import { parseRelativePos, orderByDependency } from "../relative-pos";
|
|
48
48
|
import {
|
|
49
49
|
checkValueExpression,
|
|
@@ -71,9 +71,7 @@ import type {
|
|
|
71
71
|
DslViewport,
|
|
72
72
|
} from "../types";
|
|
73
73
|
|
|
74
|
-
export type V05ParseResult =
|
|
75
|
-
| { ok: true; doc: DslDocument }
|
|
76
|
-
| { ok: false; errors: DslError[] };
|
|
74
|
+
export type V05ParseResult = { ok: true; doc: DslDocument } | { ok: false; errors: DslError[] };
|
|
77
75
|
|
|
78
76
|
/**
|
|
79
77
|
* 記法が受ける top-level の項目 (#1190)。
|
|
@@ -83,7 +81,16 @@ export type V05ParseResult =
|
|
|
83
81
|
* 一覧に 1 件も無い状態で放置されていた)。
|
|
84
82
|
*/
|
|
85
83
|
export const TOP_LEVEL_KEYS = [
|
|
86
|
-
"title",
|
|
84
|
+
"title",
|
|
85
|
+
"type",
|
|
86
|
+
"actors",
|
|
87
|
+
"flow",
|
|
88
|
+
"states",
|
|
89
|
+
"values",
|
|
90
|
+
"animation",
|
|
91
|
+
"viewport",
|
|
92
|
+
"lanes",
|
|
93
|
+
"groups",
|
|
87
94
|
// 図全体を 1 箱にする図種で、 その箱の上に出す小見出し (#1247)
|
|
88
95
|
"eyebrow",
|
|
89
96
|
// 2 軸で仕分ける図の軸の名前 (#1251)
|
|
@@ -145,8 +152,14 @@ const NODE_KIND_DEFAULT: NodeKind = "actor";
|
|
|
145
152
|
* 種類に置き換わるため、 そのまま描画側に渡ることはない。
|
|
146
153
|
*/
|
|
147
154
|
const DSL_ONLY_KINDS = [
|
|
148
|
-
"entity",
|
|
149
|
-
"
|
|
155
|
+
"entity",
|
|
156
|
+
"state",
|
|
157
|
+
"contract",
|
|
158
|
+
"eoa",
|
|
159
|
+
"multisig",
|
|
160
|
+
"proxy",
|
|
161
|
+
"library",
|
|
162
|
+
"interface",
|
|
150
163
|
] as const;
|
|
151
164
|
|
|
152
165
|
/**
|
|
@@ -160,17 +173,17 @@ const DSL_ONLY_KINDS = [
|
|
|
160
173
|
* 保管する役)。 見た目が同じになるが、 役割が同じなので嘘にはならない。
|
|
161
174
|
*/
|
|
162
175
|
const INFRA_KIND_ALIAS: Record<string, NodeKind> = {
|
|
163
|
-
alb: "shape-api-gateway",
|
|
164
|
-
browser: "frontend",
|
|
165
|
-
ecs: "microservice",
|
|
166
|
-
iam: "admin",
|
|
167
|
-
kms: "admin",
|
|
168
|
-
lambda: "function",
|
|
169
|
-
rds: "database",
|
|
170
|
-
s3: "storage",
|
|
171
|
-
secret: "storage",
|
|
172
|
-
user: "person",
|
|
173
|
-
container: "service",
|
|
176
|
+
alb: "shape-api-gateway", // 入口で振り分ける
|
|
177
|
+
browser: "frontend", // 画面側
|
|
178
|
+
ecs: "microservice", // コンテナ群
|
|
179
|
+
iam: "admin", // 権限を守る
|
|
180
|
+
kms: "admin", // 鍵を守る
|
|
181
|
+
lambda: "function", // 呼ぶと動く
|
|
182
|
+
rds: "database", // 表を持つ
|
|
183
|
+
s3: "storage", // 置き場
|
|
184
|
+
secret: "storage", // 機密の置き場
|
|
185
|
+
user: "person", // 人
|
|
186
|
+
container: "service", // 動かす単位 (C4 の container)
|
|
174
187
|
};
|
|
175
188
|
|
|
176
189
|
/**
|
|
@@ -195,7 +208,52 @@ export const NODE_KIND_VALID: ReadonlySet<string> = new Set<string>([
|
|
|
195
208
|
// 受理する色名は cdl 側の一覧をそのまま使う。 手書きすると cdl に色が増えた時に取り残される。
|
|
196
209
|
const TONE_VALID: ReadonlySet<string> = new Set<string>(TONES);
|
|
197
210
|
|
|
198
|
-
|
|
211
|
+
/**
|
|
212
|
+
* 受理する線種。 `EdgeStyle` は型だけで実体を持たないため、 実行時の一覧はここが唯一の出どころ。
|
|
213
|
+
*
|
|
214
|
+
* JSON 経路も同じ集合を読む (#1304)。 別に持つと、 線種が増えた時に片方だけ取り残される。
|
|
215
|
+
*/
|
|
216
|
+
export const STYLE_VALID: ReadonlySet<string> = new Set<string>(["solid", "dotted-flow"]);
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* 色の名前として書ける語の一覧 (#1304)。 知らせの `hint` に出す。
|
|
220
|
+
*
|
|
221
|
+
* 正規の色名 (`TONES`) と別名 (`TONE_ALIAS` の鍵) を合わせる。 手で並べると色が増えた時に
|
|
222
|
+
* 取り残されるため、 どちらも実装の集合から導く。
|
|
223
|
+
*/
|
|
224
|
+
export function 書ける色名(): string[] {
|
|
225
|
+
return [...new Set<string>([...TONES, ...Object.keys(TONE_ALIAS)])];
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* 色名として読めない値を知らせる (#1304)。
|
|
230
|
+
*
|
|
231
|
+
* 線種を受ける場所 (矢印) と受けない場所 (箱) で hint を変える。 箱に `solid` と書いても
|
|
232
|
+
* 効かないため、 使える語として案内しない。
|
|
233
|
+
*
|
|
234
|
+
* 矢印の丸括弧には、 説明文の一部が入り込むことがある
|
|
235
|
+
* (`- A -> B: 呼び出し (非同期)` の `非同期`)。 これまでは黙って捨てられ、 **説明文から
|
|
236
|
+
* 括弧の中だけが消えた図** が出ていた。 直し方が「別の語に変える」 とは限らないため、
|
|
237
|
+
* 引用符で囲む道も併せて案内する。
|
|
238
|
+
*/
|
|
239
|
+
function report読めない色(
|
|
240
|
+
値: string,
|
|
241
|
+
line: number,
|
|
242
|
+
errors: DslError[],
|
|
243
|
+
opts: { 線種も受ける: boolean },
|
|
244
|
+
): void {
|
|
245
|
+
const 語 = stripQuotes(値.trim());
|
|
246
|
+
const 使える = opts.線種も受ける ? [...書ける色名(), ...STYLE_VALID] : 書ける色名();
|
|
247
|
+
errors.push({
|
|
248
|
+
line,
|
|
249
|
+
message: opts.線種も受ける
|
|
250
|
+
? `色名か線種が読めません: "${語}"`
|
|
251
|
+
: `色の名前が読めません: "${語}"`,
|
|
252
|
+
hint: opts.線種も受ける
|
|
253
|
+
? `使える値 = ${使える.join(", ")}。 説明文に括弧を含めるなら \`"…"\` で囲む`
|
|
254
|
+
: `使える値 = ${使える.join(", ")}`,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
199
257
|
|
|
200
258
|
type Line = {
|
|
201
259
|
raw: string;
|
|
@@ -280,7 +338,7 @@ export function parseTextDslV05(src: string): V05ParseResult {
|
|
|
280
338
|
errors.push({
|
|
281
339
|
line: entry[0]!.no,
|
|
282
340
|
message: `invalid actor entry: "${entry[0]!.trimmed}"`,
|
|
283
|
-
hint:
|
|
341
|
+
hint: "use `- Client` or `- Client: storage`",
|
|
284
342
|
});
|
|
285
343
|
continue;
|
|
286
344
|
}
|
|
@@ -294,7 +352,7 @@ export function parseTextDslV05(src: string): V05ParseResult {
|
|
|
294
352
|
const { items, next } = collectIndentedList(lines, i + 1, line.indent);
|
|
295
353
|
let stepNo = 1;
|
|
296
354
|
for (const it of items) {
|
|
297
|
-
const step = parseFlowStep(it, stepNo);
|
|
355
|
+
const step = parseFlowStep(it, stepNo, errors);
|
|
298
356
|
if (step) {
|
|
299
357
|
flow.push(step);
|
|
300
358
|
stepNo += 1;
|
|
@@ -327,7 +385,12 @@ export function parseTextDslV05(src: string): V05ParseResult {
|
|
|
327
385
|
for (const it of items) {
|
|
328
386
|
const st = parseStateEntry(it.trimmed.replace(/^-\s*/, ""), it.no);
|
|
329
387
|
if (st) animate.states.push(st);
|
|
330
|
-
else
|
|
388
|
+
else
|
|
389
|
+
errors.push({
|
|
390
|
+
line: it.no,
|
|
391
|
+
message: `invalid state entry: "${it.trimmed}"`,
|
|
392
|
+
hint: "use `name: initial`",
|
|
393
|
+
});
|
|
331
394
|
}
|
|
332
395
|
i = next;
|
|
333
396
|
continue;
|
|
@@ -379,14 +442,7 @@ export function parseTextDslV05(src: string): V05ParseResult {
|
|
|
379
442
|
if (inline && inline.startsWith("{") && inline.endsWith("}")) {
|
|
380
443
|
const opts = parseInlineMapping(inline.slice(1, -1));
|
|
381
444
|
viewport = {
|
|
382
|
-
|
|
383
|
-
height: numberOrUndef(opts.height),
|
|
384
|
-
laneWidth: numberOrUndef(opts.laneWidth),
|
|
385
|
-
gap: numberOrUndef(opts.gap),
|
|
386
|
-
laneGap: numberOrUndef(opts.laneGap),
|
|
387
|
-
nodeGap: numberOrUndef(opts.nodeGap),
|
|
388
|
-
scale: numberOrUndef(opts.scale),
|
|
389
|
-
labelMargin: numberOrUndef(opts.labelMargin),
|
|
445
|
+
...表で読む(VIEWPORT_VALUE_KINDS, opts, "viewport の ", line.no, errors),
|
|
390
446
|
pos: { line: line.no },
|
|
391
447
|
};
|
|
392
448
|
i += 1;
|
|
@@ -395,19 +451,24 @@ export function parseTextDslV05(src: string): V05ParseResult {
|
|
|
395
451
|
// block: viewport:\n width: 1400\n height: 900\n ...
|
|
396
452
|
const { items, next } = collectIndentedList(lines, i + 1, line.indent);
|
|
397
453
|
const opts: Record<string, string> = {};
|
|
454
|
+
// 知らせは **値を書いた行** を指す (#1306)。 `viewport:` の行を指すと、欄が縦に並ぶ形で
|
|
455
|
+
// どの行を直せばよいか分からない
|
|
456
|
+
const optLines: Record<string, number> = {};
|
|
398
457
|
for (const it of items) {
|
|
399
458
|
const m = it.trimmed.match(/^([a-zA-Z][a-zA-Z0-9_]*)\s*:\s*(.+)$/);
|
|
400
|
-
if (m)
|
|
459
|
+
if (!m) continue;
|
|
460
|
+
const 欄 = m[1] ?? "";
|
|
461
|
+
opts[欄] = stripQuotes((m[2] ?? "").trim());
|
|
462
|
+
optLines[欄] = it.no;
|
|
401
463
|
}
|
|
402
464
|
viewport = {
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
labelMargin: numberOrUndef(opts.labelMargin),
|
|
465
|
+
...表で読む(
|
|
466
|
+
VIEWPORT_VALUE_KINDS,
|
|
467
|
+
opts,
|
|
468
|
+
"viewport の ",
|
|
469
|
+
(欄) => optLines[欄] ?? line.no,
|
|
470
|
+
errors,
|
|
471
|
+
),
|
|
411
472
|
pos: { line: line.no },
|
|
412
473
|
};
|
|
413
474
|
i = next;
|
|
@@ -463,11 +524,8 @@ export function parseTextDslV05(src: string): V05ParseResult {
|
|
|
463
524
|
const opts = parseInlineMapping(m[2]!);
|
|
464
525
|
lanesMap[id] = {
|
|
465
526
|
id,
|
|
466
|
-
|
|
467
|
-
width: numberOrUndef(opts.width),
|
|
527
|
+
...表で読む(LANE_VALUE_KINDS, opts, `縦列 ${id} の `, it.no, errors),
|
|
468
528
|
label: opts.label,
|
|
469
|
-
contain: boolOrUndef(opts.contain),
|
|
470
|
-
lifeline: boolOrUndef(opts.lifeline),
|
|
471
529
|
pos: { line: it.no },
|
|
472
530
|
};
|
|
473
531
|
} else {
|
|
@@ -515,8 +573,14 @@ export function parseTextDslV05(src: string): V05ParseResult {
|
|
|
515
573
|
i += 1;
|
|
516
574
|
}
|
|
517
575
|
|
|
518
|
-
if (!title)
|
|
519
|
-
|
|
576
|
+
if (!title)
|
|
577
|
+
errors.push({ line: 1, message: "title is required", hint: 'add `title: "..."` at top' });
|
|
578
|
+
if (!type)
|
|
579
|
+
errors.push({
|
|
580
|
+
line: 1,
|
|
581
|
+
message: "type is required",
|
|
582
|
+
hint: "add `type: sequence|flow|swimlane|er|state|topology|solidity|gantt|class|pie|c4|mind`",
|
|
583
|
+
});
|
|
520
584
|
|
|
521
585
|
if (errors.length > 0) return { ok: false, errors };
|
|
522
586
|
|
|
@@ -590,7 +654,10 @@ function lastTopLevelColon(s: string): number {
|
|
|
590
654
|
if (c === quote) quote = "";
|
|
591
655
|
continue;
|
|
592
656
|
}
|
|
593
|
-
if (c === '"' || c === "'") {
|
|
657
|
+
if (c === '"' || c === "'") {
|
|
658
|
+
quote = c;
|
|
659
|
+
continue;
|
|
660
|
+
}
|
|
594
661
|
if (c === "[" || c === "{") depth += 1;
|
|
595
662
|
else if (c === "]" || c === "}") depth -= 1;
|
|
596
663
|
else if (c === ":" && depth === 0) last = i;
|
|
@@ -615,11 +682,26 @@ function splitValues(s: string): string[] {
|
|
|
615
682
|
if (c === quote) quote = "";
|
|
616
683
|
continue;
|
|
617
684
|
}
|
|
618
|
-
if (c === '"' || c === "'") {
|
|
619
|
-
|
|
620
|
-
|
|
685
|
+
if (c === '"' || c === "'") {
|
|
686
|
+
quote = c;
|
|
687
|
+
buf += c;
|
|
688
|
+
continue;
|
|
689
|
+
}
|
|
690
|
+
if (c === "[" || c === "{") {
|
|
691
|
+
depth += 1;
|
|
692
|
+
buf += c;
|
|
693
|
+
continue;
|
|
694
|
+
}
|
|
695
|
+
if (c === "]" || c === "}") {
|
|
696
|
+
depth -= 1;
|
|
697
|
+
buf += c;
|
|
698
|
+
continue;
|
|
699
|
+
}
|
|
621
700
|
if (/\s/.test(c) && depth === 0) {
|
|
622
|
-
if (buf) {
|
|
701
|
+
if (buf) {
|
|
702
|
+
out.push(buf);
|
|
703
|
+
buf = "";
|
|
704
|
+
}
|
|
623
705
|
continue;
|
|
624
706
|
}
|
|
625
707
|
buf += c;
|
|
@@ -650,13 +732,16 @@ type ActorValues = {
|
|
|
650
732
|
* 振り分けは値の形で決まる。 引用符付きは補足 (2 つ目は値)、 角括弧は行、 色名は色、
|
|
651
733
|
* 残りが種類。 形が違うので取り違えない。
|
|
652
734
|
*/
|
|
653
|
-
function classifyValues(values: string[]): ActorValues {
|
|
735
|
+
function classifyValues(values: string[], line: number, errors: DslError[]): ActorValues {
|
|
654
736
|
const out: ActorValues = { kind: "" };
|
|
655
737
|
/** 書かれた倍率。 同じ名前が 2 度出たら後の値で上書きする */
|
|
656
738
|
const scaleWritten = new Map<string, string>();
|
|
657
739
|
const kindWords: string[] = [];
|
|
658
740
|
for (const v of values) {
|
|
659
|
-
if (
|
|
741
|
+
if (
|
|
742
|
+
(v.startsWith('"') && v.endsWith('"') && v.length > 1) ||
|
|
743
|
+
(v.startsWith("'") && v.endsWith("'") && v.length > 1)
|
|
744
|
+
) {
|
|
660
745
|
// 1 つ目の引用符は補足、 2 つ目は値 (`storage` の右側に出る数値等)
|
|
661
746
|
if (out.subtitle === undefined) out.subtitle = stripQuotes(v);
|
|
662
747
|
else if (out.value === undefined) out.value = stripQuotes(v);
|
|
@@ -672,7 +757,11 @@ function classifyValues(values: string[]): ActorValues {
|
|
|
672
757
|
}
|
|
673
758
|
// `@300,200` は位置。 2 つ揃わないと効かないので、 1 つの値としてまとめて書く
|
|
674
759
|
const at = v.match(/^@(-?\d+(?:\.\d+)?)\s*[,、]\s*(-?\d+(?:\.\d+)?)$/);
|
|
675
|
-
if (at) {
|
|
760
|
+
if (at) {
|
|
761
|
+
out.posX = Number(at[1]);
|
|
762
|
+
out.posY = Number(at[2]);
|
|
763
|
+
continue;
|
|
764
|
+
}
|
|
676
765
|
// `名前=値` は parts の状態の上書き。 状態名は自由なので、 形では見分けられない。
|
|
677
766
|
// 等号を書いてもらう。
|
|
678
767
|
const eq = v.indexOf("=");
|
|
@@ -690,12 +779,15 @@ function classifyValues(values: string[]): ActorValues {
|
|
|
690
779
|
continue;
|
|
691
780
|
}
|
|
692
781
|
}
|
|
693
|
-
const tone =
|
|
694
|
-
if (tone) {
|
|
782
|
+
const tone = resolveTone(v);
|
|
783
|
+
if (tone) {
|
|
784
|
+
out.tone = tone;
|
|
785
|
+
continue;
|
|
786
|
+
}
|
|
695
787
|
kindWords.push(v);
|
|
696
788
|
}
|
|
697
789
|
out.kind = kindWords.join(" ").toLowerCase();
|
|
698
|
-
const s = resolveScale(scaleWritten);
|
|
790
|
+
const s = resolveScale(scaleWritten, line, errors);
|
|
699
791
|
out.scale = s.scale;
|
|
700
792
|
out.scaleKeys = s.keys;
|
|
701
793
|
return out;
|
|
@@ -706,7 +798,7 @@ function classifyValues(values: string[]): ActorValues {
|
|
|
706
798
|
*
|
|
707
799
|
* 固有名 (`lambda` / `rds` 等) は読み替え表を通す。 それ以外はそのまま返す。
|
|
708
800
|
*/
|
|
709
|
-
function
|
|
801
|
+
export function resolveNodeKind(raw: string): NodeKind {
|
|
710
802
|
if (raw === "") return NODE_KIND_DEFAULT;
|
|
711
803
|
// `Object.hasOwn` で引く。 素の添字だと `toString` 等の既定の持ち物が引けてしまい、
|
|
712
804
|
// 種類として関数が返る。 呼ぶ前に受理集合で弾いてはいるが、 表を引く側でも閉じておく。
|
|
@@ -727,6 +819,157 @@ function boolOrUndef(s: string | undefined): boolean | undefined {
|
|
|
727
819
|
return undefined;
|
|
728
820
|
}
|
|
729
821
|
|
|
822
|
+
/**
|
|
823
|
+
* 欄が期待する値の形 (#1306)。
|
|
824
|
+
*
|
|
825
|
+
* 記法の値はすべて文字列なので「型」 は無いが、**欄ごとに読める形は決まっている**
|
|
826
|
+
* (`posX` は数、`overlay` は真偽)。 その形を表に並べ、読めない値を行番号付きで知らせる。
|
|
827
|
+
*/
|
|
828
|
+
type 値の形 = "数" | "真偽";
|
|
829
|
+
|
|
830
|
+
/** 表から作る、欄の名前と読んだ結果の対応 */
|
|
831
|
+
type 読んだ結果<T extends Record<string, 値の形>> = {
|
|
832
|
+
[K in keyof T]: T[K] extends "数" ? number | undefined : boolean | undefined;
|
|
833
|
+
};
|
|
834
|
+
|
|
835
|
+
/**
|
|
836
|
+
* 数として読む。 読めない値は行番号付きで知らせる (#1306)。
|
|
837
|
+
*
|
|
838
|
+
* `numberOrUndef` は読めない値を黙って `undefined` に落とすため、書いた欄が無かったことに
|
|
839
|
+
* なる。 誤りも警告も出ないので、書いた人には「書いたのに図が変わらない」 としか見えない。
|
|
840
|
+
*
|
|
841
|
+
* 値を書かなかった形 (`posX:` の右が空) は「書かなかった」 と同じ扱いのままにする。
|
|
842
|
+
* こちらは黙って消えているわけではなく、書いていないものが効かないだけ。
|
|
843
|
+
*/
|
|
844
|
+
function 数として読む(
|
|
845
|
+
raw: string | undefined,
|
|
846
|
+
欄: string,
|
|
847
|
+
line: number,
|
|
848
|
+
errors: DslError[],
|
|
849
|
+
): number | undefined {
|
|
850
|
+
if (raw === undefined || raw === "") return undefined;
|
|
851
|
+
const n = numberOrUndef(raw);
|
|
852
|
+
if (n !== undefined) return n;
|
|
853
|
+
errors.push({
|
|
854
|
+
line,
|
|
855
|
+
message: `${欄} は数で書きます: "${raw}"`,
|
|
856
|
+
hint: "`300` / `-8` / `1.5` の形で書く",
|
|
857
|
+
});
|
|
858
|
+
return undefined;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
/**
|
|
862
|
+
* 真偽として読む。 読めない値は行番号付きで知らせる (#1306)。
|
|
863
|
+
*
|
|
864
|
+
* 受けるのは `true` と `false` だけ。 `yes` / `1` / `はい` は読めないため、使える値を
|
|
865
|
+
* 添えて知らせる (読めない値を捨てるだけだと、別の綴りを試し続けることになる)。
|
|
866
|
+
*/
|
|
867
|
+
function 真偽として読む(
|
|
868
|
+
raw: string | undefined,
|
|
869
|
+
欄: string,
|
|
870
|
+
line: number,
|
|
871
|
+
errors: DslError[],
|
|
872
|
+
): boolean | undefined {
|
|
873
|
+
if (raw === undefined || raw === "") return undefined;
|
|
874
|
+
const b = boolOrUndef(raw);
|
|
875
|
+
if (b !== undefined) return b;
|
|
876
|
+
errors.push({
|
|
877
|
+
line,
|
|
878
|
+
message: `${欄} は true か false で書きます: "${raw}"`,
|
|
879
|
+
hint: "使える値 = true, false",
|
|
880
|
+
});
|
|
881
|
+
return undefined;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
/**
|
|
885
|
+
* 表に並べた欄をまとめて読む (#1306)。
|
|
886
|
+
*
|
|
887
|
+
* **呼出側に欄名を並べない**。 並べると欄を足した時に知らせだけが漏れる (JSON 入口が
|
|
888
|
+
* #1304 で踏んだ形と同じ)。 表を 1 つ置き、読む側も検査もそこから導く。
|
|
889
|
+
*
|
|
890
|
+
* `接頭` は知らせに出す欄の呼び名の前半 (`viewport.` / `縦列 l1 の `)。 同じ欄名が別の
|
|
891
|
+
* 場所に出る (`width` は図全体と縦列、`posX` は箱と箱の中の要素) ため、どこの欄かが
|
|
892
|
+
* 分かる形にする。
|
|
893
|
+
*/
|
|
894
|
+
function 表で読む<T extends Record<string, 値の形>>(
|
|
895
|
+
表: T,
|
|
896
|
+
opts: Record<string, string | undefined>,
|
|
897
|
+
接頭: string,
|
|
898
|
+
// 欄ごとに行が違う書き方 (縦に並べる形) では関数で渡す。 1 行に収まる書き方 (中括弧) は数で渡す
|
|
899
|
+
line: number | ((欄: string) => number),
|
|
900
|
+
errors: DslError[],
|
|
901
|
+
): 読んだ結果<T> {
|
|
902
|
+
const out: Record<string, number | boolean | undefined> = {};
|
|
903
|
+
const 行を引く = (欄: string): number => (typeof line === "number" ? line : line(欄));
|
|
904
|
+
for (const [欄, 形] of Object.entries<値の形>(表)) {
|
|
905
|
+
out[欄] =
|
|
906
|
+
形 === "数"
|
|
907
|
+
? 数として読む(opts[欄], `${接頭}${欄}`, 行を引く(欄), errors)
|
|
908
|
+
: 真偽として読む(opts[欄], `${接頭}${欄}`, 行を引く(欄), errors);
|
|
909
|
+
}
|
|
910
|
+
return out as 読んだ結果<T>;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
/**
|
|
914
|
+
* 図全体の大きさと間隔の欄 (#1306)。 `DslViewport` の数の欄をすべて覆う。
|
|
915
|
+
*
|
|
916
|
+
* `satisfies` で `DslViewport` から欄を導く = 欄を足して表に書き忘れると型検査が落ちる。
|
|
917
|
+
*/
|
|
918
|
+
export const VIEWPORT_VALUE_KINDS = {
|
|
919
|
+
width: "数",
|
|
920
|
+
height: "数",
|
|
921
|
+
scale: "数",
|
|
922
|
+
laneWidth: "数",
|
|
923
|
+
gap: "数",
|
|
924
|
+
laneGap: "数",
|
|
925
|
+
nodeGap: "数",
|
|
926
|
+
labelMargin: "数",
|
|
927
|
+
} as const satisfies Record<Exclude<keyof DslViewport, "pos">, 値の形>;
|
|
928
|
+
|
|
929
|
+
/**
|
|
930
|
+
* 縦列の欄 (#1306)。 `label` は文字列なので表に載せない (記法の値は全て文字列で、
|
|
931
|
+
* 文字列の欄には読めない値という状態が無い)。
|
|
932
|
+
*/
|
|
933
|
+
export const LANE_VALUE_KINDS = {
|
|
934
|
+
x: "数",
|
|
935
|
+
width: "数",
|
|
936
|
+
contain: "真偽",
|
|
937
|
+
lifeline: "真偽",
|
|
938
|
+
} as const satisfies Record<string, 値の形>;
|
|
939
|
+
|
|
940
|
+
/** 箱の中の要素の欄 (#1306)。 `DslActorNodeOverride` の全欄を覆う */
|
|
941
|
+
export const ACTOR_NODE_VALUE_KINDS = {
|
|
942
|
+
posX: "数",
|
|
943
|
+
posY: "数",
|
|
944
|
+
posW: "数",
|
|
945
|
+
posH: "数",
|
|
946
|
+
} as const satisfies Record<keyof DslActorNodeOverride, 値の形>;
|
|
947
|
+
|
|
948
|
+
/** 中括弧の形で箱に書ける、数と真偽の欄 (#1306) */
|
|
949
|
+
export const ACTOR_INLINE_VALUE_KINDS = {
|
|
950
|
+
stack: "数",
|
|
951
|
+
initial: "真偽",
|
|
952
|
+
final: "真偽",
|
|
953
|
+
posX: "数",
|
|
954
|
+
posY: "数",
|
|
955
|
+
posW: "数",
|
|
956
|
+
posH: "数",
|
|
957
|
+
} as const satisfies Record<string, 値の形>;
|
|
958
|
+
|
|
959
|
+
/** 縦に並べる形で箱に書ける、数と真偽の欄 (#1306)。 `posW` / `posH` は `大きさ:` が受ける */
|
|
960
|
+
export const ACTOR_BLOCK_VALUE_KINDS = {
|
|
961
|
+
stack: "数",
|
|
962
|
+
posX: "数",
|
|
963
|
+
posY: "数",
|
|
964
|
+
} as const satisfies Record<string, 値の形>;
|
|
965
|
+
|
|
966
|
+
/** 矢印の中括弧に書ける、数と真偽の欄 (#1306) */
|
|
967
|
+
export const FLOW_INLINE_VALUE_KINDS = {
|
|
968
|
+
labelOffsetX: "数",
|
|
969
|
+
labelOffsetY: "数",
|
|
970
|
+
overlay: "真偽",
|
|
971
|
+
} as const satisfies Record<string, 値の形>;
|
|
972
|
+
|
|
730
973
|
/**
|
|
731
974
|
* 色名を解決する。 別名 (`成功` / `neutral` 等) も受け付ける。
|
|
732
975
|
*
|
|
@@ -736,7 +979,7 @@ function boolOrUndef(s: string | undefined): boolean | undefined {
|
|
|
736
979
|
* `valueOf` / `__proto__` が JavaScript の既定の持ち物として引けてしまい、 色名として
|
|
737
980
|
* 関数やオブジェクトが通る (実測)。 最後に解決結果が正規の色名かも確かめる。
|
|
738
981
|
*/
|
|
739
|
-
function
|
|
982
|
+
export function resolveTone(s: string | undefined): Tone | undefined {
|
|
740
983
|
if (s === undefined) return undefined;
|
|
741
984
|
const raw = stripQuotes(s.trim());
|
|
742
985
|
const lower = raw.toLowerCase();
|
|
@@ -846,7 +1089,11 @@ function splitInlineFields(inner: string): string[] {
|
|
|
846
1089
|
return parts;
|
|
847
1090
|
}
|
|
848
1091
|
|
|
849
|
-
function collectIndentedList(
|
|
1092
|
+
function collectIndentedList(
|
|
1093
|
+
lines: Line[],
|
|
1094
|
+
start: number,
|
|
1095
|
+
parentIndent: number,
|
|
1096
|
+
): { items: Line[]; next: number } {
|
|
850
1097
|
const items: Line[] = [];
|
|
851
1098
|
let i = start;
|
|
852
1099
|
while (i < lines.length) {
|
|
@@ -887,13 +1134,43 @@ function collectIndentedList(lines: Line[], start: number, parentIndent: number)
|
|
|
887
1134
|
* 書く人は「色を変えたい」 としか思わないので、 項目は `色:` 1 つにまとめる。 意味の色
|
|
888
1135
|
* (`失敗`) と色番号 (`#f59e0b`) は形で見分ける。 前者は箱の色、 後者はパーツの塗りになる。
|
|
889
1136
|
*/
|
|
890
|
-
function splitColorValue(raw: string): { tone?: Tone; hex?: string } {
|
|
1137
|
+
export function splitColorValue(raw: string): { tone?: Tone; hex?: string } {
|
|
891
1138
|
const v = stripQuotes(raw.trim());
|
|
892
1139
|
if (v.startsWith("#")) return { hex: v };
|
|
893
|
-
const tone =
|
|
1140
|
+
const tone = resolveTone(v);
|
|
894
1141
|
return tone ? { tone } : {};
|
|
895
1142
|
}
|
|
896
1143
|
|
|
1144
|
+
/**
|
|
1145
|
+
* v0.4 で使えた箱の種類の名前 (#1301)。
|
|
1146
|
+
*
|
|
1147
|
+
* v0.5 の受理集合 (`NODE_KIND_VALID`) に無いため、書くと見本 (parts) の名前として扱われ、
|
|
1148
|
+
* 見本帳に無ければ `actor` に潰れて **黙って消えていた**。 見本の名前と区別が付かないので、
|
|
1149
|
+
* 「v0.4 で種類として使えた語」 であることを根拠に誤りとして知らせる。
|
|
1150
|
+
*
|
|
1151
|
+
* 対応は `keywords.ts` の `NODE_KIND_ALIAS` が持つ (日本語 → 英語の種類名)。
|
|
1152
|
+
*/
|
|
1153
|
+
function v04の種類名(値: string): string | undefined {
|
|
1154
|
+
if (!Object.hasOwn(NODE_KIND_ALIAS, 値)) return undefined;
|
|
1155
|
+
if (NODE_KIND_VALID.has(値)) return undefined; // v0.5 でも受ける名前は対象外
|
|
1156
|
+
return NODE_KIND_ALIAS[値];
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
/**
|
|
1160
|
+
* 箱の種類に v0.4 の日本語を書いた時に知らせる (#1301)。
|
|
1161
|
+
*
|
|
1162
|
+
* 黙って見本の名前として扱うと、見本帳に無い場合に `actor` へ潰れて手掛かりが残らない。
|
|
1163
|
+
*/
|
|
1164
|
+
function reportV04Kind(kindRaw: string, line: number, errors: DslError[]): void {
|
|
1165
|
+
const 英語 = v04の種類名(kindRaw);
|
|
1166
|
+
if (英語 === undefined) return;
|
|
1167
|
+
errors.push({
|
|
1168
|
+
line,
|
|
1169
|
+
message: `箱の種類に v0.4 の名前は使えません: "${kindRaw}"`,
|
|
1170
|
+
hint: `v0.5 では英語で書く (\`${英語}\`)`,
|
|
1171
|
+
});
|
|
1172
|
+
}
|
|
1173
|
+
|
|
897
1174
|
/** `色` / `color` のどちらでも書ける。 */
|
|
898
1175
|
const COLOR_KEYS = new Set(["色", "color", "tone"]);
|
|
899
1176
|
|
|
@@ -921,11 +1198,20 @@ const SCALE_ORDER = ["scale", "倍率"] as const;
|
|
|
921
1198
|
* `keys` は書かれた名前そのもの。 値が読めたかに関わらず入る。 見本が同じ名前の状態を
|
|
922
1199
|
* 持つ時の知らせ (`scale-reserved`) が、値の読めなさに左右されないようにするため。
|
|
923
1200
|
*/
|
|
924
|
-
function resolveScale(
|
|
1201
|
+
function resolveScale(
|
|
1202
|
+
written: Map<string, string>,
|
|
1203
|
+
line: number | ((key: string) => number),
|
|
1204
|
+
errors: DslError[],
|
|
1205
|
+
): { scale?: number; keys: string[] } {
|
|
925
1206
|
const keys = [...written.keys()];
|
|
926
1207
|
for (const key of SCALE_ORDER) {
|
|
927
1208
|
const raw = written.get(key);
|
|
928
|
-
|
|
1209
|
+
// 読めない値は黙って捨てず知らせる (#1306)。 書かれた名前 (`keys`) は値の読めなさに
|
|
1210
|
+
// 関わらず残す = 見本が同じ名前の状態を持つ時の知らせが消えないようにするため
|
|
1211
|
+
if (raw !== undefined) {
|
|
1212
|
+
const 当該行 = typeof line === "number" ? line : line(key);
|
|
1213
|
+
return { scale: 数として読む(raw, `箱の ${key}`, 当該行, errors), keys };
|
|
1214
|
+
}
|
|
929
1215
|
}
|
|
930
1216
|
return { keys };
|
|
931
1217
|
}
|
|
@@ -942,6 +1228,8 @@ function applyContinuationLines(actor: DslActor, rest: Line[], errors: DslError[
|
|
|
942
1228
|
let touchedState = false;
|
|
943
1229
|
/** 縦に並べて書かれた倍率。 同じ名前が 2 度出たら後の値で上書きする */
|
|
944
1230
|
const scaleWritten = new Map<string, string>();
|
|
1231
|
+
/** 倍率を名前ごとに最後に書いた行。 別名の優先順と行番号を取り違えないために保持する。 */
|
|
1232
|
+
const scaleLines = new Map<string, number>();
|
|
945
1233
|
/**
|
|
946
1234
|
* 縦に並べて書かれた体験の道筋の欄 (#1251)。
|
|
947
1235
|
*
|
|
@@ -962,6 +1250,7 @@ function applyContinuationLines(actor: DslActor, rest: Line[], errors: DslError[
|
|
|
962
1250
|
// 予約の知らせが消え、別名 (`倍率`) に降りて別の値が効いてしまう
|
|
963
1251
|
if (SCALE_KEYS.has(key)) {
|
|
964
1252
|
scaleWritten.set(key, stripQuotes(raw));
|
|
1253
|
+
scaleLines.set(key, ln.no);
|
|
965
1254
|
unknownKeys.push({ key, line: ln.no });
|
|
966
1255
|
continue;
|
|
967
1256
|
}
|
|
@@ -972,14 +1261,18 @@ function applyContinuationLines(actor: DslActor, rest: Line[], errors: DslError[
|
|
|
972
1261
|
if (tone) out.tone = tone;
|
|
973
1262
|
// 色番号を入れる状態の名前はパーツごとに違う。 組み立て時に解決する
|
|
974
1263
|
if (hex) out.colorHex = hex;
|
|
1264
|
+
// 色名としても色番号としても読めない値は黙って捨てない (#1304)。 捨てると
|
|
1265
|
+
// 既定色のまま描かれ、 手掛かりが 1 つも残らない
|
|
1266
|
+
if (!tone && !hex) report読めない色(raw, ln.no, errors, { 線種も受ける: false });
|
|
975
1267
|
continue;
|
|
976
1268
|
}
|
|
977
1269
|
switch (key) {
|
|
978
1270
|
case "kind":
|
|
979
1271
|
case "種類": {
|
|
980
1272
|
const k = stripQuotes(raw).toLowerCase();
|
|
1273
|
+
reportV04Kind(k, ln.no, errors);
|
|
981
1274
|
const isPart = k !== "" && !NODE_KIND_VALID.has(k);
|
|
982
|
-
out.kind = isPart ? NODE_KIND_DEFAULT :
|
|
1275
|
+
out.kind = isPart ? NODE_KIND_DEFAULT : resolveNodeKind(k);
|
|
983
1276
|
// parts 候補は `kind` を既定に倒して `partId` へ退避するため、 名札に載せる種類としては
|
|
984
1277
|
// 「書かなかった」 と同じ扱いにする (#1058)
|
|
985
1278
|
out.kindWritten = k !== "" && !isPart;
|
|
@@ -996,7 +1289,11 @@ function applyContinuationLines(actor: DslActor, rest: Line[], errors: DslError[
|
|
|
996
1289
|
break;
|
|
997
1290
|
case "rows":
|
|
998
1291
|
case "行":
|
|
999
|
-
out.rows = raw
|
|
1292
|
+
out.rows = raw
|
|
1293
|
+
.replace(/^\[|\]$/g, "")
|
|
1294
|
+
.split(/,(?![^[]*\])/)
|
|
1295
|
+
.map((x) => stripQuotes(x.trim()))
|
|
1296
|
+
.filter(Boolean);
|
|
1000
1297
|
break;
|
|
1001
1298
|
case "位置":
|
|
1002
1299
|
case "pos": {
|
|
@@ -1025,7 +1322,8 @@ function applyContinuationLines(actor: DslActor, rest: Line[], errors: DslError[
|
|
|
1025
1322
|
//
|
|
1026
1323
|
// 負の間隔 (`Web の右 -200`) もここに来る。 向きを書いた上で裏返す指定は、
|
|
1027
1324
|
// 書いた人の意図と図が食い違うので誤りとして返す
|
|
1028
|
-
const negative =
|
|
1325
|
+
const negative =
|
|
1326
|
+
/^(.+?)\s*(?:の\s*(?:右|左|上|下)|\s(?:right|left|above|below))\s*-\s*[\d.]/i.test(value);
|
|
1029
1327
|
errors.push({
|
|
1030
1328
|
line: ln.no,
|
|
1031
1329
|
message: negative
|
|
@@ -1038,10 +1336,10 @@ function applyContinuationLines(actor: DslActor, rest: Line[], errors: DslError[
|
|
|
1038
1336
|
break;
|
|
1039
1337
|
}
|
|
1040
1338
|
case "posX":
|
|
1041
|
-
out.posX =
|
|
1339
|
+
out.posX = 数として読む(raw, "箱の posX", ln.no, errors);
|
|
1042
1340
|
break;
|
|
1043
1341
|
case "posY":
|
|
1044
|
-
out.posY =
|
|
1342
|
+
out.posY = 数として読む(raw, "箱の posY", ln.no, errors);
|
|
1045
1343
|
break;
|
|
1046
1344
|
case "大きさ":
|
|
1047
1345
|
case "size": {
|
|
@@ -1075,7 +1373,7 @@ function applyContinuationLines(actor: DslActor, rest: Line[], errors: DslError[
|
|
|
1075
1373
|
out.lane = stripQuotes(raw);
|
|
1076
1374
|
break;
|
|
1077
1375
|
case "stack":
|
|
1078
|
-
out.stack =
|
|
1376
|
+
out.stack = 数として読む(raw, "箱の stack", ln.no, errors);
|
|
1079
1377
|
break;
|
|
1080
1378
|
default:
|
|
1081
1379
|
// 残りはパーツの状態の上書き
|
|
@@ -1088,7 +1386,11 @@ function applyContinuationLines(actor: DslActor, rest: Line[], errors: DslError[
|
|
|
1088
1386
|
// 名前の行と縦に並べた行の両方に倍率がある形では、後に書いた縦の行を採る。
|
|
1089
1387
|
// 知らせ (`scale-reserved`) は書かれた名前をすべて見るので、名前だけは足し合わせる
|
|
1090
1388
|
if (scaleWritten.size > 0) {
|
|
1091
|
-
const s = resolveScale(
|
|
1389
|
+
const s = resolveScale(
|
|
1390
|
+
scaleWritten,
|
|
1391
|
+
(key) => scaleLines.get(key) ?? actor.pos.line,
|
|
1392
|
+
errors,
|
|
1393
|
+
);
|
|
1092
1394
|
out.scale = s.scale;
|
|
1093
1395
|
out.scaleKeys = [...new Set([...(actor.scaleKeys ?? []), ...s.keys])];
|
|
1094
1396
|
}
|
|
@@ -1128,18 +1430,30 @@ function applyContinuationLines(actor: DslActor, rest: Line[], errors: DslError[
|
|
|
1128
1430
|
*/
|
|
1129
1431
|
export const ACTOR_ITEM_KEYS: ReadonlySet<string> = new Set([
|
|
1130
1432
|
...COLOR_KEYS,
|
|
1131
|
-
"kind",
|
|
1132
|
-
"
|
|
1133
|
-
"
|
|
1134
|
-
"
|
|
1135
|
-
"
|
|
1136
|
-
"
|
|
1137
|
-
"
|
|
1138
|
-
"
|
|
1433
|
+
"kind",
|
|
1434
|
+
"種類",
|
|
1435
|
+
"subtitle",
|
|
1436
|
+
"補足",
|
|
1437
|
+
"value",
|
|
1438
|
+
"値",
|
|
1439
|
+
"rows",
|
|
1440
|
+
"行",
|
|
1441
|
+
"位置",
|
|
1442
|
+
"pos",
|
|
1443
|
+
"posX",
|
|
1444
|
+
"posY",
|
|
1445
|
+
"大きさ",
|
|
1446
|
+
"size",
|
|
1447
|
+
"倍率",
|
|
1448
|
+
"scale",
|
|
1449
|
+
"lane",
|
|
1450
|
+
"stack",
|
|
1139
1451
|
// 体験の道筋の欄 (#1251)
|
|
1140
|
-
"touchpoint",
|
|
1452
|
+
"touchpoint",
|
|
1453
|
+
"opportunity",
|
|
1141
1454
|
// 工程の並びの欄 (#1251)
|
|
1142
|
-
"owner",
|
|
1455
|
+
"owner",
|
|
1456
|
+
"end",
|
|
1143
1457
|
]);
|
|
1144
1458
|
|
|
1145
1459
|
/**
|
|
@@ -1198,14 +1512,21 @@ function validateRelativePositions(actors: DslActor[], errors: DslError[]): void
|
|
|
1198
1512
|
}
|
|
1199
1513
|
}
|
|
1200
1514
|
|
|
1201
|
-
function collectActorEntries(
|
|
1515
|
+
function collectActorEntries(
|
|
1516
|
+
lines: Line[],
|
|
1517
|
+
start: number,
|
|
1518
|
+
parentIndent: number,
|
|
1519
|
+
): { items: Line[][]; next: number } {
|
|
1202
1520
|
const items: Line[][] = [];
|
|
1203
1521
|
let cur: Line[] | null = null;
|
|
1204
1522
|
let headIndent = -1;
|
|
1205
1523
|
let i = start;
|
|
1206
1524
|
while (i < lines.length) {
|
|
1207
1525
|
const ln = lines[i]!;
|
|
1208
|
-
if (!ln.trimmed) {
|
|
1526
|
+
if (!ln.trimmed) {
|
|
1527
|
+
i += 1;
|
|
1528
|
+
continue;
|
|
1529
|
+
}
|
|
1209
1530
|
if (ln.indent <= parentIndent) break;
|
|
1210
1531
|
if (ln.trimmed.startsWith("- ")) {
|
|
1211
1532
|
if (cur) items.push(cur);
|
|
@@ -1221,7 +1542,11 @@ function collectActorEntries(lines: Line[], start: number, parentIndent: number)
|
|
|
1221
1542
|
return { items, next: i };
|
|
1222
1543
|
}
|
|
1223
1544
|
|
|
1224
|
-
function collectAnimationSteps(
|
|
1545
|
+
function collectAnimationSteps(
|
|
1546
|
+
lines: Line[],
|
|
1547
|
+
start: number,
|
|
1548
|
+
parentIndent: number,
|
|
1549
|
+
): { items: Line[][]; next: number } {
|
|
1225
1550
|
// 各 `- step: "..."` 開始を 1 block の頭として識別、 後続の同 indent 以下を block 本文として吸収
|
|
1226
1551
|
const out: Line[][] = [];
|
|
1227
1552
|
let i = start;
|
|
@@ -1233,7 +1558,10 @@ function collectAnimationSteps(lines: Line[], start: number, parentIndent: numbe
|
|
|
1233
1558
|
continue;
|
|
1234
1559
|
}
|
|
1235
1560
|
if (ln.indent <= parentIndent) break;
|
|
1236
|
-
|
|
1561
|
+
// v0.4 の日本語の段名も block の頭として拾い、parsePhase で英語の `step` を案内する
|
|
1562
|
+
// (#1301)。ここで英語だけに絞ると `- ステップ:` は block 自体が作られず、段全体が
|
|
1563
|
+
// 誤りなしで黙って消える。
|
|
1564
|
+
if (ln.trimmed.startsWith("- step") || ln.trimmed.startsWith("- ステップ")) {
|
|
1237
1565
|
if (cur) out.push(cur);
|
|
1238
1566
|
cur = [{ ...ln, trimmed: ln.trimmed.slice(2).trim() }];
|
|
1239
1567
|
} else if (cur) {
|
|
@@ -1288,7 +1616,9 @@ const ACTOR_RESERVED_FIELDS: ReadonlySet<string> = new Set([
|
|
|
1288
1616
|
"色",
|
|
1289
1617
|
]);
|
|
1290
1618
|
|
|
1291
|
-
function extractStateOverride(
|
|
1619
|
+
function extractStateOverride(
|
|
1620
|
+
opts: Record<string, string>,
|
|
1621
|
+
): Record<string, number | string | boolean> | undefined {
|
|
1292
1622
|
const out: Record<string, number | string | boolean> = {};
|
|
1293
1623
|
let count = 0;
|
|
1294
1624
|
// 明示 `state: {...}` fallback がある場合はそちらを優先 (nested map parse)
|
|
@@ -1317,7 +1647,11 @@ function extractStateOverride(opts: Record<string, string>): Record<string, numb
|
|
|
1317
1647
|
* key: sub-map ペアに再 split → 各 sub-map を parseInlineMapping で解いて posX/Y/W/H に coerce」 する。
|
|
1318
1648
|
* 未 field or 空 object なら undefined 返し (caller は actor.nodes を set しない)。
|
|
1319
1649
|
*/
|
|
1320
|
-
function parseActorNodesField(
|
|
1650
|
+
function parseActorNodesField(
|
|
1651
|
+
raw: string | undefined,
|
|
1652
|
+
line: number,
|
|
1653
|
+
errors: DslError[],
|
|
1654
|
+
): Record<string, DslActorNodeOverride> | undefined {
|
|
1321
1655
|
if (!raw) return undefined;
|
|
1322
1656
|
const trimmed = raw.trim();
|
|
1323
1657
|
if (!trimmed.startsWith("{") || !trimmed.endsWith("}")) return undefined;
|
|
@@ -1347,12 +1681,7 @@ function parseActorNodesField(raw: string | undefined): Record<string, DslActorN
|
|
|
1347
1681
|
const val = p.slice(colonIdx + 1).trim();
|
|
1348
1682
|
if (!key || !val.startsWith("{") || !val.endsWith("}")) continue;
|
|
1349
1683
|
const nodeOpts = parseInlineMapping(val.slice(1, -1));
|
|
1350
|
-
out[key] = {
|
|
1351
|
-
posX: numberOrUndef(nodeOpts.posX),
|
|
1352
|
-
posY: numberOrUndef(nodeOpts.posY),
|
|
1353
|
-
posW: numberOrUndef(nodeOpts.posW),
|
|
1354
|
-
posH: numberOrUndef(nodeOpts.posH),
|
|
1355
|
-
};
|
|
1684
|
+
out[key] = 表で読む(ACTOR_NODE_VALUE_KINDS, nodeOpts, `nodes の ${key} の `, line, errors);
|
|
1356
1685
|
}
|
|
1357
1686
|
return Object.keys(out).length > 0 ? out : undefined;
|
|
1358
1687
|
}
|
|
@@ -1394,17 +1723,65 @@ function reportScaleOnNonPart(
|
|
|
1394
1723
|
* 並べた形) とは別に持つ = 中括弧の形は位置や大きさを未対応にしてあり、 同じ集合にすると
|
|
1395
1724
|
* 「知らせない」 側がずれる。
|
|
1396
1725
|
*/
|
|
1726
|
+
/**
|
|
1727
|
+
* 中括弧の形で読める日本語と、その英語名 (#1301)。
|
|
1728
|
+
*
|
|
1729
|
+
* **英語が中括弧で読める欄だけを載せる**。 `位置` / `大きさ` / `色` は英語側
|
|
1730
|
+
* (`pos` / `size` / `color`) も中括弧では読めないため載せない = 英語で出来ないことを
|
|
1731
|
+
* 日本語で出来るようにはしない。
|
|
1732
|
+
*
|
|
1733
|
+
* 載せる前は、同じ意味の語が縦書きでは通り中括弧では「項目名が読めません」 になっていた。
|
|
1734
|
+
* 書き方によって日本語だけが落ちる状態を無くす。
|
|
1735
|
+
*/
|
|
1736
|
+
export const INLINE_ACTOR_ALIASES: Record<string, string> = {
|
|
1737
|
+
種類: "kind",
|
|
1738
|
+
補足: "subtitle",
|
|
1739
|
+
値: "value",
|
|
1740
|
+
行: "rows",
|
|
1741
|
+
};
|
|
1742
|
+
|
|
1743
|
+
/** 中括弧に書かれた日本語の項目名を、同じ意味の英語名に寄せる (#1301) */
|
|
1744
|
+
function 中括弧の別名を寄せる(opts: Record<string, string>): Record<string, string> {
|
|
1745
|
+
let 触った = false;
|
|
1746
|
+
const out: Record<string, string> = { ...opts };
|
|
1747
|
+
for (const [日, 英] of Object.entries(INLINE_ACTOR_ALIASES)) {
|
|
1748
|
+
if (!(日 in out)) continue;
|
|
1749
|
+
触った = true;
|
|
1750
|
+
// 英語を併記した時は英語を優先する (縦書き形が後勝ちなのと違い、こちらは 1 行に同居する)
|
|
1751
|
+
if (!(英 in out)) out[英] = out[日]!;
|
|
1752
|
+
delete out[日];
|
|
1753
|
+
}
|
|
1754
|
+
return 触った ? out : opts;
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1397
1757
|
export const INLINE_ACTOR_KEYS: ReadonlySet<string> = new Set([
|
|
1398
|
-
"kind",
|
|
1399
|
-
"
|
|
1758
|
+
"kind",
|
|
1759
|
+
"subtitle",
|
|
1760
|
+
"eyebrow",
|
|
1761
|
+
"value",
|
|
1762
|
+
"rows",
|
|
1763
|
+
"lane",
|
|
1764
|
+
"stack",
|
|
1765
|
+
"initial",
|
|
1766
|
+
"final",
|
|
1767
|
+
"tone",
|
|
1768
|
+
"nodes",
|
|
1400
1769
|
// 体験の道筋の欄 (#1251)。 他の図種では組み立て側が知らせる
|
|
1401
|
-
"touchpoint",
|
|
1770
|
+
"touchpoint",
|
|
1771
|
+
"opportunity",
|
|
1402
1772
|
// 工程の並びの欄 (#1251)
|
|
1403
|
-
"owner",
|
|
1404
|
-
"
|
|
1773
|
+
"owner",
|
|
1774
|
+
"end",
|
|
1775
|
+
"posX",
|
|
1776
|
+
"posY",
|
|
1777
|
+
"posW",
|
|
1778
|
+
"posH",
|
|
1405
1779
|
// 倍率は別経路 (`reportScaleOnNonPart`) が知らせる。 ここでも読める扱いにしないと
|
|
1406
1780
|
// 同じ名前で 2 度知らせることになる
|
|
1407
|
-
"scale",
|
|
1781
|
+
"scale",
|
|
1782
|
+
"倍率",
|
|
1783
|
+
// 英語が読める欄の日本語別名 (#1301)。 一覧は `INLINE_ACTOR_ALIASES` が持つ
|
|
1784
|
+
...Object.keys(INLINE_ACTOR_ALIASES),
|
|
1408
1785
|
]);
|
|
1409
1786
|
// `state` はパーツでだけ意味を持つ (`extractStateOverride` がパーツの時しか作らない)。
|
|
1410
1787
|
// 通常の箱で読める扱いにすると `- A: { state: { foo: 1 } }` が黙って消え、 本 file が塞ごうと
|
|
@@ -1422,13 +1799,17 @@ const FLOW_INLINE_READERS = {
|
|
|
1422
1799
|
sub: (v: string | undefined) => v,
|
|
1423
1800
|
guard: (v: string | undefined) => v,
|
|
1424
1801
|
cardinality: (v: string | undefined) => v,
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1802
|
+
// 数と真偽の欄は `FLOW_INLINE_VALUE_KINDS` の表が読む (#1306)。 ここでは名前だけを持つ =
|
|
1803
|
+
// 読める欄の一覧 (`FLOW_INLINE_KEYS`) は本表から導くため、載せないと欄ごと消える
|
|
1804
|
+
labelOffsetX: null,
|
|
1805
|
+
labelOffsetY: null,
|
|
1806
|
+
overlay: null,
|
|
1428
1807
|
} as const;
|
|
1429
1808
|
|
|
1430
1809
|
/** 矢印の中括弧に書ける欄の名前。 README の一覧と突き合わせる (#1275) */
|
|
1431
|
-
export const FLOW_INLINE_KEYS = Object.keys(
|
|
1810
|
+
export const FLOW_INLINE_KEYS = Object.keys(
|
|
1811
|
+
FLOW_INLINE_READERS,
|
|
1812
|
+
) as readonly (keyof typeof FLOW_INLINE_READERS)[];
|
|
1432
1813
|
|
|
1433
1814
|
/**
|
|
1434
1815
|
* 中括弧に書かれた読めない項目名を知らせる (#1090)。
|
|
@@ -1481,18 +1862,28 @@ function parseActor(line: Line, errors: DslError[]): DslActor | null {
|
|
|
1481
1862
|
if (mapMatch) {
|
|
1482
1863
|
const namePart = stripQuotes(mapMatch.name.trim());
|
|
1483
1864
|
if (!namePart) return null;
|
|
1484
|
-
|
|
1865
|
+
// 日本語の項目名を英語名に寄せてから読む (#1301)。 寄せないと、同じ意味の語が
|
|
1866
|
+
// 縦書きでは通り中括弧では落ちる
|
|
1867
|
+
const opts = 中括弧の別名を寄せる(parseInlineMapping(mapMatch.inner));
|
|
1485
1868
|
const kindRaw = (opts.kind ?? "").toLowerCase();
|
|
1486
1869
|
// CAR-1657 = kind が既存 NODE_KIND_VALID に無い場合 parts identifier 候補として partId に格納、
|
|
1487
1870
|
// kind は actor default fallback。 compile 側 partsCatalog lookup で解決する。
|
|
1488
1871
|
const isPart = kindRaw !== "" && !NODE_KIND_VALID.has(kindRaw);
|
|
1489
1872
|
// 倍率はパーツにしか効かない。 書いたのに効かない状態を黙って作らない (#1026)。
|
|
1490
1873
|
// 値が空の形でも名前を残すため、`opts` ではなく中身から直接拾う
|
|
1491
|
-
const inlineScale = resolveScale(writtenScaleFields(mapMatch.inner));
|
|
1874
|
+
const inlineScale = resolveScale(writtenScaleFields(mapMatch.inner), line.no, errors);
|
|
1492
1875
|
reportScaleOnNonPart(isPart, inlineScale.keys[0], line.no, errors);
|
|
1493
1876
|
// 中括弧に書いた読めない項目名も知らせる (#1090)。 縦に並べた形だけが知らせていた
|
|
1494
1877
|
reportUnknownInlineKeys(isPart, mapMatch.inner, line.no, errors);
|
|
1495
|
-
|
|
1878
|
+
reportV04Kind(kindRaw, line.no, errors);
|
|
1879
|
+
// 中括弧に書いた読めない色名も知らせる (#1304)。 パーツでは `tone` が状態の上書きとして
|
|
1880
|
+
// 意味を持つため対象外 = 色として読もうとしない値を色として叱らない
|
|
1881
|
+
if (!isPart && opts.tone !== undefined && resolveTone(opts.tone) === undefined) {
|
|
1882
|
+
report読めない色(opts.tone, line.no, errors, { 線種も受ける: false });
|
|
1883
|
+
}
|
|
1884
|
+
const kind = isPart
|
|
1885
|
+
? NODE_KIND_DEFAULT
|
|
1886
|
+
: resolveNodeKind(NODE_KIND_VALID.has(kindRaw) ? kindRaw : "");
|
|
1496
1887
|
return {
|
|
1497
1888
|
name: namePart,
|
|
1498
1889
|
kind,
|
|
@@ -1516,23 +1907,17 @@ function parseActor(line: Line, errors: DslError[]): DslActor | null {
|
|
|
1516
1907
|
.filter(Boolean)
|
|
1517
1908
|
: undefined,
|
|
1518
1909
|
lane: opts.lane,
|
|
1519
|
-
|
|
1520
|
-
initial: boolOrUndef(opts.initial),
|
|
1521
|
-
final: boolOrUndef(opts.final),
|
|
1910
|
+
...表で読む(ACTOR_INLINE_VALUE_KINDS, opts, "箱の ", line.no, errors),
|
|
1522
1911
|
// parts では `tone` を状態の上書きとして従来から使えるため、 色として横取りしない
|
|
1523
|
-
tone: isPart ? undefined :
|
|
1912
|
+
tone: isPart ? undefined : resolveTone(opts.tone),
|
|
1524
1913
|
partId: isPart ? kindRaw : undefined,
|
|
1525
1914
|
stateOverride: isPart ? extractStateOverride(opts) : undefined,
|
|
1526
|
-
// canvas pivot 新 spec = 絶対座標 field
|
|
1527
|
-
posX: numberOrUndef(opts.posX),
|
|
1528
|
-
posY: numberOrUndef(opts.posY),
|
|
1529
|
-
posW: numberOrUndef(opts.posW),
|
|
1530
|
-
posH: numberOrUndef(opts.posH),
|
|
1915
|
+
// canvas pivot 新 spec = 絶対座標 field は `ACTOR_INLINE_VALUE_KINDS` の表が読む (#1306)
|
|
1531
1916
|
// 図形の倍率 (#1026)。 どれが効くかは `resolveScale` が 1 箇所で決める
|
|
1532
1917
|
scale: inlineScale.scale,
|
|
1533
1918
|
scaleKeys: inlineScale.keys.length ? inlineScale.keys : undefined,
|
|
1534
1919
|
// canvas pivot UX 修正 (B1) = sub-node 単位 override map (`nodes: { header: {posX:..., ...}, ...}`)
|
|
1535
|
-
nodes: parseActorNodesField(opts.nodes),
|
|
1920
|
+
nodes: parseActorNodesField(opts.nodes, line.no, errors),
|
|
1536
1921
|
pos: { line: line.no },
|
|
1537
1922
|
};
|
|
1538
1923
|
}
|
|
@@ -1547,13 +1932,16 @@ function parseActor(line: Line, errors: DslError[]): DslActor | null {
|
|
|
1547
1932
|
//
|
|
1548
1933
|
// 値は形で見分ける。 引用符付きは補足、 角括弧は行、 色名は色、 残りが種類。
|
|
1549
1934
|
// 種類と色は語の集合が閉じているので取り違えない。
|
|
1550
|
-
const v = classifyValues(splitValues(rest));
|
|
1935
|
+
const v = classifyValues(splitValues(rest), line.no, errors);
|
|
1551
1936
|
|
|
1552
1937
|
// CAR-1657 = short form (`arc1: arc-gauge`) でも parts kind 対応、 未知 kind は partId 経路
|
|
1553
1938
|
const isPart = v.kind !== "" && !NODE_KIND_VALID.has(v.kind);
|
|
1939
|
+
reportV04Kind(v.kind, line.no, errors);
|
|
1554
1940
|
// 倍率はパーツにしか効かない (#1026)
|
|
1555
1941
|
reportScaleOnNonPart(isPart, v.scaleKeys?.[0], line.no, errors);
|
|
1556
|
-
const kind = isPart
|
|
1942
|
+
const kind = isPart
|
|
1943
|
+
? NODE_KIND_DEFAULT
|
|
1944
|
+
: resolveNodeKind(NODE_KIND_VALID.has(v.kind) ? v.kind : "");
|
|
1557
1945
|
return {
|
|
1558
1946
|
name: namePart,
|
|
1559
1947
|
kind,
|
|
@@ -1579,7 +1967,7 @@ function parseActor(line: Line, errors: DslError[]): DslActor | null {
|
|
|
1579
1967
|
return { name: namePart, kind: NODE_KIND_DEFAULT, kindWritten: false, pos: { line: line.no } };
|
|
1580
1968
|
}
|
|
1581
1969
|
|
|
1582
|
-
function parseFlowStep(line: Line, no: number): DslStep | null {
|
|
1970
|
+
function parseFlowStep(line: Line, no: number, errors: DslError[]): DslStep | null {
|
|
1583
1971
|
// 形式 (順序自由、 部分省略可):
|
|
1584
1972
|
// 1. `Client -> API` ... label / option なし
|
|
1585
1973
|
// 2. `Client -> API: "deposit"` ... label
|
|
@@ -1598,17 +1986,23 @@ function parseFlowStep(line: Line, no: number): DslStep | null {
|
|
|
1598
1986
|
const 中括弧: Partial<Record<keyof typeof FLOW_INLINE_READERS, unknown>> = {};
|
|
1599
1987
|
// inline option (`{ ... }`) を末尾から抽出
|
|
1600
1988
|
const mapMatch = rest.match(/\s*\{([^}]*)\}\s*$/);
|
|
1989
|
+
let 数と真偽: 読んだ結果<typeof FLOW_INLINE_VALUE_KINDS> | undefined;
|
|
1601
1990
|
if (mapMatch) {
|
|
1602
1991
|
const opts = parseInlineMapping(mapMatch[1]!);
|
|
1603
|
-
|
|
1992
|
+
// 文字列の欄はそのまま入れ、数と真偽の欄は表が読んで読めない値を知らせる (#1306)
|
|
1993
|
+
for (const k of FLOW_INLINE_KEYS) {
|
|
1994
|
+
if (FLOW_INLINE_READERS[k] === null) continue;
|
|
1995
|
+
中括弧[k] = opts[k];
|
|
1996
|
+
}
|
|
1997
|
+
数と真偽 = 表で読む(FLOW_INLINE_VALUE_KINDS, opts, "矢印の ", line.no, errors);
|
|
1604
1998
|
rest = rest.slice(0, mapMatch.index ?? 0).trim();
|
|
1605
1999
|
}
|
|
1606
2000
|
const sub = 中括弧.sub as string | undefined;
|
|
1607
2001
|
const guard = 中括弧.guard as string | undefined;
|
|
1608
2002
|
const cardinality = 中括弧.cardinality as string | undefined;
|
|
1609
|
-
const labelOffsetX =
|
|
1610
|
-
const labelOffsetY =
|
|
1611
|
-
const overlay =
|
|
2003
|
+
const labelOffsetX = 数と真偽?.labelOffsetX;
|
|
2004
|
+
const labelOffsetY = 数と真偽?.labelOffsetY;
|
|
2005
|
+
const overlay = 数と真偽?.overlay;
|
|
1612
2006
|
// 色と線種を末尾から取る。 括弧 (`(成功)`) と空白区切り (`成功`) の両方を受け付ける。
|
|
1613
2007
|
//
|
|
1614
2008
|
// 括弧は従来の書き方で、 catalog が使っている。 空白区切りは登場人物と揃えた形。
|
|
@@ -1616,9 +2010,13 @@ function parseFlowStep(line: Line, no: number): DslStep | null {
|
|
|
1616
2010
|
if (optMatch) {
|
|
1617
2011
|
const opts = (optMatch[1] ?? "").split(",").map((s) => s.trim());
|
|
1618
2012
|
for (const opt of opts) {
|
|
1619
|
-
const resolvedTone =
|
|
2013
|
+
const resolvedTone = resolveTone(opt);
|
|
1620
2014
|
if (resolvedTone !== undefined) tone = resolvedTone;
|
|
1621
2015
|
else if (STYLE_VALID.has(opt.toLowerCase())) style = opt.toLowerCase() as EdgeStyle;
|
|
2016
|
+
// 丸括弧に書けるのは色名と線種だけ。 読めない語を黙って捨てると、 書いた人には
|
|
2017
|
+
// 「書いたのに色が変わらない」 としか見えない (#1304)。 空の語 (`( )` / `(a,,b)`) は
|
|
2018
|
+
// 書き間違いというより余分な区切りなので知らせない
|
|
2019
|
+
else if (opt !== "") report読めない色(opt, line.no, errors, { 線種も受ける: true });
|
|
1622
2020
|
}
|
|
1623
2021
|
rest = rest.slice(0, optMatch.index ?? 0).trim();
|
|
1624
2022
|
} else {
|
|
@@ -1629,9 +2027,17 @@ function parseFlowStep(line: Line, no: number): DslStep | null {
|
|
|
1629
2027
|
const last = words[words.length - 1]!;
|
|
1630
2028
|
// 引用符付きは説明文なので取らない
|
|
1631
2029
|
if (last.startsWith('"') || last.startsWith("'")) break;
|
|
1632
|
-
const resolvedTone =
|
|
1633
|
-
if (resolvedTone !== undefined) {
|
|
1634
|
-
|
|
2030
|
+
const resolvedTone = resolveTone(last);
|
|
2031
|
+
if (resolvedTone !== undefined) {
|
|
2032
|
+
tone = resolvedTone;
|
|
2033
|
+
words.pop();
|
|
2034
|
+
continue;
|
|
2035
|
+
}
|
|
2036
|
+
if (STYLE_VALID.has(last.toLowerCase())) {
|
|
2037
|
+
style = last.toLowerCase() as EdgeStyle;
|
|
2038
|
+
words.pop();
|
|
2039
|
+
continue;
|
|
2040
|
+
}
|
|
1635
2041
|
break;
|
|
1636
2042
|
}
|
|
1637
2043
|
rest = words.join(" ");
|
|
@@ -1669,7 +2075,8 @@ function parseStateEntry(text: string, lineNo: number): DslState | null {
|
|
|
1669
2075
|
const raw = (m[2] ?? "").trim();
|
|
1670
2076
|
const stripped = stripQuotes(raw);
|
|
1671
2077
|
const asNum = Number(stripped);
|
|
1672
|
-
const initial: number | string =
|
|
2078
|
+
const initial: number | string =
|
|
2079
|
+
Number.isFinite(asNum) && stripped !== "" && !isNaN(asNum) ? asNum : stripped;
|
|
1673
2080
|
return { name, initial, pos: { line: lineNo } };
|
|
1674
2081
|
}
|
|
1675
2082
|
|
|
@@ -1729,11 +2136,21 @@ function parseValueEntry(text: string, lineNo: number, errors: DslError[]): DslV
|
|
|
1729
2136
|
for (const issue of issues) errors.push({ line: lineNo, ...issue });
|
|
1730
2137
|
return null;
|
|
1731
2138
|
}
|
|
1732
|
-
return {
|
|
2139
|
+
return {
|
|
2140
|
+
name,
|
|
2141
|
+
trigger: spec.trigger,
|
|
2142
|
+
to: spec.to,
|
|
2143
|
+
durationMs: spec.durationMs,
|
|
2144
|
+
pos: { line: lineNo },
|
|
2145
|
+
};
|
|
1733
2146
|
}
|
|
1734
2147
|
const expression = stripQuotes(rest);
|
|
1735
2148
|
if (expression === "") {
|
|
1736
|
-
errors.push({
|
|
2149
|
+
errors.push({
|
|
2150
|
+
line: lineNo,
|
|
2151
|
+
message: `empty expression for "${name}"`,
|
|
2152
|
+
hint: '`"{a} + {b}"` のように式を書く',
|
|
2153
|
+
});
|
|
1737
2154
|
return null;
|
|
1738
2155
|
}
|
|
1739
2156
|
const issues = checkValueExpression(expression, name);
|
|
@@ -1746,7 +2163,10 @@ function parseValueEntry(text: string, lineNo: number, errors: DslError[]): DslV
|
|
|
1746
2163
|
|
|
1747
2164
|
function splitTopLevelCommas(s: string): string[] {
|
|
1748
2165
|
// brace 内を考慮 ... 今回は単純 split (動作する範囲)
|
|
1749
|
-
return s
|
|
2166
|
+
return s
|
|
2167
|
+
.split(",")
|
|
2168
|
+
.map((x) => x.trim())
|
|
2169
|
+
.filter(Boolean);
|
|
1750
2170
|
}
|
|
1751
2171
|
|
|
1752
2172
|
function ensureAnimate(a: DslAnimate | undefined, lineNo: number): DslAnimate {
|
|
@@ -1759,14 +2179,22 @@ function parsePhase(block: Line[], errors: DslError[]): DslPhase | null {
|
|
|
1759
2179
|
const head = block[0]!;
|
|
1760
2180
|
const m = head.trimmed.match(/^step\s*:\s*(.+)$/);
|
|
1761
2181
|
if (!m) {
|
|
1762
|
-
errors.push({
|
|
2182
|
+
errors.push({
|
|
2183
|
+
line: head.no,
|
|
2184
|
+
message: `invalid step header: "${head.trimmed}"`,
|
|
2185
|
+
hint: 'use `- step: "name" 1.5s`',
|
|
2186
|
+
});
|
|
1763
2187
|
return null;
|
|
1764
2188
|
}
|
|
1765
2189
|
const headRest = (m[1] ?? "").trim();
|
|
1766
2190
|
// `"request" 1.5s` 形式 ... quote 後の duration 抽出
|
|
1767
2191
|
const headParse = parseStepHead(headRest);
|
|
1768
2192
|
if (!headParse) {
|
|
1769
|
-
errors.push({
|
|
2193
|
+
errors.push({
|
|
2194
|
+
line: head.no,
|
|
2195
|
+
message: `invalid step value: "${headRest}"`,
|
|
2196
|
+
hint: 'use `"name" 1.5s` (duration in s)',
|
|
2197
|
+
});
|
|
1770
2198
|
return null;
|
|
1771
2199
|
}
|
|
1772
2200
|
const phase: DslPhase = {
|
|
@@ -1782,7 +2210,10 @@ function parsePhase(block: Line[], errors: DslError[]): DslPhase | null {
|
|
|
1782
2210
|
while (i < block.length) {
|
|
1783
2211
|
const ln = block[i]!;
|
|
1784
2212
|
const t = ln.trimmed;
|
|
1785
|
-
|
|
2213
|
+
// **英数字以外の項目名も拾う** (#1301)。 以前は `[a-zA-Z]` で始まる名前しか見ておらず、
|
|
2214
|
+
// 日本語の項目名 (`強調` / `説明` 等) は match そのものが外れて **黙って捨てられていた**。
|
|
2215
|
+
// 拾った上で、知らない名前は下で誤りとして知らせる
|
|
2216
|
+
const propMatch = t.match(/^([^\s:]+)\s*:\s*(.*)$/);
|
|
1786
2217
|
if (!propMatch) {
|
|
1787
2218
|
i += 1;
|
|
1788
2219
|
continue;
|
|
@@ -1799,6 +2230,23 @@ function parsePhase(block: Line[], errors: DslError[]): DslPhase | null {
|
|
|
1799
2230
|
i += 1;
|
|
1800
2231
|
continue;
|
|
1801
2232
|
}
|
|
2233
|
+
if (key === "draw") {
|
|
2234
|
+
const 語 = stripQuotes(value).trim();
|
|
2235
|
+
// **読めない語を黙って捨てない** (#1304 / #1306 と同じ扱い)。 受ける語は 1 つだけで、
|
|
2236
|
+
// 書き間違いはその段が何も描かない形になって手掛かりが残らない
|
|
2237
|
+
if (!DRAW_WORDS.has(語)) {
|
|
2238
|
+
errors.push({
|
|
2239
|
+
line: ln.no,
|
|
2240
|
+
message: `draw に書けない語です: "${語}"`,
|
|
2241
|
+
hint: `使える語 = ${[...DRAW_WORDS].join(", ")}`,
|
|
2242
|
+
});
|
|
2243
|
+
} else {
|
|
2244
|
+
phase.draw = 語;
|
|
2245
|
+
phase.drawPos = { line: ln.no };
|
|
2246
|
+
}
|
|
2247
|
+
i += 1;
|
|
2248
|
+
continue;
|
|
2249
|
+
}
|
|
1802
2250
|
if (key === "description" || key === "body") {
|
|
1803
2251
|
phase.body = stripQuotes(value);
|
|
1804
2252
|
i += 1;
|
|
@@ -1809,7 +2257,12 @@ function parsePhase(block: Line[], errors: DslError[]): DslPhase | null {
|
|
|
1809
2257
|
if (value) {
|
|
1810
2258
|
const tw = parseTweenLine(value, ln.no);
|
|
1811
2259
|
if (tw) phase.tweens!.push(tw);
|
|
1812
|
-
else
|
|
2260
|
+
else
|
|
2261
|
+
errors.push({
|
|
2262
|
+
line: ln.no,
|
|
2263
|
+
message: `invalid tween: "${value}"`,
|
|
2264
|
+
hint: "use `tween: name 100 -> 90`",
|
|
2265
|
+
});
|
|
1813
2266
|
i += 1;
|
|
1814
2267
|
continue;
|
|
1815
2268
|
}
|
|
@@ -1821,7 +2274,12 @@ function parsePhase(block: Line[], errors: DslError[]): DslPhase | null {
|
|
|
1821
2274
|
if (nx.indent <= baseIndent) break;
|
|
1822
2275
|
const tw = parseTweenLine(nx.trimmed, nx.no);
|
|
1823
2276
|
if (tw) phase.tweens!.push(tw);
|
|
1824
|
-
else
|
|
2277
|
+
else
|
|
2278
|
+
errors.push({
|
|
2279
|
+
line: nx.no,
|
|
2280
|
+
message: `invalid tween entry: "${nx.trimmed}"`,
|
|
2281
|
+
hint: "use `name: 100 -> 90`",
|
|
2282
|
+
});
|
|
1825
2283
|
j += 1;
|
|
1826
2284
|
}
|
|
1827
2285
|
i = j;
|
|
@@ -1846,11 +2304,81 @@ function parsePhase(block: Line[], errors: DslError[]): DslPhase | null {
|
|
|
1846
2304
|
i = j;
|
|
1847
2305
|
continue;
|
|
1848
2306
|
}
|
|
2307
|
+
// ここに来るのは上のどれにも当たらなかった名前 (#1301)。 黙って捨てると
|
|
2308
|
+
// 「書いたのに段が変わらない」 が手掛かりなしで起きる
|
|
2309
|
+
errors.push({
|
|
2310
|
+
line: ln.no,
|
|
2311
|
+
message: `段の項目名が読めません: "${propMatch[1] ?? ""}"`,
|
|
2312
|
+
hint: 段の項目のヒント(propMatch[1] ?? ""),
|
|
2313
|
+
});
|
|
1849
2314
|
i += 1;
|
|
1850
2315
|
}
|
|
1851
2316
|
return phase;
|
|
1852
2317
|
}
|
|
1853
2318
|
|
|
2319
|
+
/**
|
|
2320
|
+
* 段の項目名が読めない時のヒント (#1301)。
|
|
2321
|
+
*
|
|
2322
|
+
* 日本語の名前は v0.4 の記法では使えたため、**同じ意味の英語を勧める**。
|
|
2323
|
+
* 「使えません」 だけだと、書いた人は代わりに何を書けばよいか分からない。
|
|
2324
|
+
*/
|
|
2325
|
+
function 段の項目のヒント(書いた名前: string): string {
|
|
2326
|
+
const 英語 = 段の項目の日本語[書いた名前];
|
|
2327
|
+
return 英語 !== undefined
|
|
2328
|
+
? `v0.5 では英語で書く (\`${英語}\`)`
|
|
2329
|
+
: `使える項目 = ${段の項目の英語.join(", ")}`;
|
|
2330
|
+
}
|
|
2331
|
+
|
|
2332
|
+
/** 段に書ける項目の英語名。 `parsePhase` の分岐から導く一覧 */
|
|
2333
|
+
const 段の項目の英語 = ["focus", "badge", "body", "description", "tween", "set", "draw"] as const;
|
|
2334
|
+
|
|
2335
|
+
/**
|
|
2336
|
+
* `draw:` に書ける語と、その語が効く図種 (#1312 / #1314)。
|
|
2337
|
+
*
|
|
2338
|
+
* **語と図種の対応をここ 1 箇所で持つ**。 受ける語の一覧 (`DRAW_WORDS`) も、組み立て側が見る
|
|
2339
|
+
* 図種の一覧 (`DRAWABLE_DOC_TYPES`) も、この表から導く。 3 つを別々に並べると、語を足した時に
|
|
2340
|
+
* どれかが古いまま残る (#1310 / #1304 で 3 度直した形)。
|
|
2341
|
+
*
|
|
2342
|
+
* | 語 | 図種 | 起点 |
|
|
2343
|
+
* |---|---|---|
|
|
2344
|
+
* | `line` | `line` | 左端から右へ線が伸びる |
|
|
2345
|
+
* | `bar` | `bar` | 横軸から上へ棒が伸びる |
|
|
2346
|
+
* | `pie` | `pie` | 12 時から時計回りに扇が開く |
|
|
2347
|
+
*
|
|
2348
|
+
* いまは語と図種が同じ綴りだが、**同じものとして扱わない**。 語は書き手が書く名前で、
|
|
2349
|
+
* 図種は `type:` が取る値。 片方だけ別名を足したくなった時に、対応が表に残っている形にする。
|
|
2350
|
+
*/
|
|
2351
|
+
export const DRAW_TARGETS: ReadonlyMap<string, PresetType> = new Map<string, PresetType>([
|
|
2352
|
+
["line", "line"],
|
|
2353
|
+
["bar", "bar"],
|
|
2354
|
+
["pie", "pie"],
|
|
2355
|
+
]);
|
|
2356
|
+
|
|
2357
|
+
/** `draw:` に書ける語。 表から導く (#1314) */
|
|
2358
|
+
export const DRAW_WORDS: ReadonlySet<string> = new Set(DRAW_TARGETS.keys());
|
|
2359
|
+
|
|
2360
|
+
/**
|
|
2361
|
+
* v0.4 で使えた段の項目名と、v0.5 での書き方 (#1301)。
|
|
2362
|
+
*
|
|
2363
|
+
* **`ANIM_SUBKEYS` からは導けない**。 あの表は v0.4 の日本語と v0.4 の英語を組にしており、
|
|
2364
|
+
* v0.5 が使う名前とは一致しない (`強調` の相手は v0.4 では `highlight`、v0.5 では `focus`)。
|
|
2365
|
+
* 2 つの記法の間の翻訳なので、対応は手で書く。
|
|
2366
|
+
*
|
|
2367
|
+
* 表に語が増えた時に取り残されないよう、`ANIM_SUBKEYS` の日本語を全て覆っていることを
|
|
2368
|
+
* 検査が確かめる (`v05-japanese-scope.test.ts`)。
|
|
2369
|
+
*/
|
|
2370
|
+
export const 段の項目の日本語: Record<string, string> = {
|
|
2371
|
+
強調: "focus",
|
|
2372
|
+
説明: "body",
|
|
2373
|
+
バッジ: "badge",
|
|
2374
|
+
遷移: "tween",
|
|
2375
|
+
切替: "set",
|
|
2376
|
+
// 状態は段の中ではなく最上位に書く (`states:`)
|
|
2377
|
+
状態: "states (最上位に書く)",
|
|
2378
|
+
// 段そのものの名前
|
|
2379
|
+
ステップ: "step",
|
|
2380
|
+
};
|
|
2381
|
+
|
|
1854
2382
|
function parseStepHead(s: string): { name: string; durationMs: number } | null {
|
|
1855
2383
|
// 例: `"request" 1.5s` / `"step1" 1500ms` / `step1 2s`
|
|
1856
2384
|
let rest = s.trim();
|
|
@@ -1906,7 +2434,7 @@ function parseFocusList(s: string): string[] {
|
|
|
1906
2434
|
buf += ch;
|
|
1907
2435
|
continue;
|
|
1908
2436
|
}
|
|
1909
|
-
if (ch === "
|
|
2437
|
+
if (ch === '"' || ch === "'") {
|
|
1910
2438
|
// item の途中にある引用符は名前の一部。 空白または区切りの直後だけ囲みを開始する。
|
|
1911
2439
|
if (!buf || /\s$/.test(buf)) {
|
|
1912
2440
|
pushFragment(false);
|
|
@@ -1926,7 +2454,10 @@ function parseFocusList(s: string): string[] {
|
|
|
1926
2454
|
// quote 外 item は依然として space split (旧挙動、 「Client API」 が 2 item として解釈される互換維持)。
|
|
1927
2455
|
const out: string[] = [];
|
|
1928
2456
|
for (const fragments of groups) {
|
|
1929
|
-
const whole = fragments
|
|
2457
|
+
const whole = fragments
|
|
2458
|
+
.map(({ text }) => text)
|
|
2459
|
+
.join("")
|
|
2460
|
+
.trim();
|
|
1930
2461
|
if (fragments.every(({ quoted }) => !quoted) && /[-→][>]?/.test(whole) && /\s/.test(whole)) {
|
|
1931
2462
|
// arrow を含む非引用区間は「A -> B」パターン。 空白で分割しない。
|
|
1932
2463
|
out.push(whole);
|
|
@@ -1973,6 +2504,7 @@ function parseSetLine(s: string, lineNo: number): DslSet | null {
|
|
|
1973
2504
|
const raw = (m[2] ?? "").trim();
|
|
1974
2505
|
const stripped = stripQuotes(raw);
|
|
1975
2506
|
const asNum = Number(stripped);
|
|
1976
|
-
const value: number | string =
|
|
2507
|
+
const value: number | string =
|
|
2508
|
+
Number.isFinite(asNum) && stripped !== "" && !isNaN(asNum) ? asNum : stripped;
|
|
1977
2509
|
return { state, value, pos: { line: lineNo } };
|
|
1978
2510
|
}
|