@cardenelabs/dragon 0.28.0 → 0.32.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 +391 -16
- package/dist/index.cjs +13479 -7403
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2360 -208
- package/dist/index.d.ts +2360 -208
- package/dist/index.js +13462 -7405
- package/dist/index.js.map +1 -1
- package/package.json +12 -13
- package/src/compile/actor-ids.ts +297 -0
- package/src/compile/actor-option-notice.ts +379 -0
- package/src/compile/actors.ts +103 -0
- package/src/compile/c4.ts +74 -0
- package/src/compile/chain.ts +93 -0
- package/src/compile/chart-fields.ts +193 -0
- package/src/compile/deep-rewrite.ts +30 -0
- package/src/compile/derived-source.ts +18 -0
- package/src/compile/direction.ts +102 -0
- package/src/compile/edge-option-notice.ts +112 -0
- package/src/compile/er-relation.ts +200 -0
- package/src/compile/flow.ts +54 -0
- package/src/compile/flowchart.ts +344 -0
- package/src/compile/funnel.ts +87 -0
- package/src/compile/gantt.ts +337 -0
- package/src/compile/generic.ts +696 -0
- package/src/compile/hierarchy.ts +92 -0
- package/src/compile/journey.ts +93 -0
- package/src/compile/kinds.ts +148 -0
- package/src/compile/lanes.ts +138 -0
- package/src/compile/metro.ts +271 -0
- package/src/compile/mind.ts +393 -0
- package/src/compile/node-title.ts +24 -0
- package/src/compile/notice.ts +141 -0
- package/src/compile/order.ts +62 -0
- package/src/compile/parts-lookup.ts +17 -0
- package/src/compile/parts.ts +2709 -0
- package/src/compile/placement-apply.ts +689 -0
- package/src/compile/placement.ts +15 -0
- package/src/compile/quadrant.ts +265 -0
- package/src/compile/record.ts +75 -0
- package/src/compile/row-marks.ts +128 -0
- package/src/compile/rows.ts +40 -0
- package/src/compile/sequence.ts +305 -0
- package/src/compile/slug.ts +37 -0
- package/src/compile/stages.ts +247 -0
- package/src/compile/subtitle.ts +35 -0
- package/src/compile/swimlane-marks.ts +87 -0
- package/src/compile/swimlane.ts +82 -0
- package/src/compile/timeline.ts +213 -0
- package/src/compile/topology.ts +44 -0
- package/src/compile/tree.ts +69 -0
- package/src/compile/value-chart.ts +337 -0
- package/src/compile/values.ts +432 -0
- package/src/compile/word-state.ts +82 -0
- package/src/compile.ts +1806 -6971
- package/src/focus.ts +2 -2
- package/src/index.ts +42 -9
- package/src/json-line-map.ts +30 -0
- package/src/json-parser.ts +1760 -168
- package/src/keywords.ts +251 -17
- package/src/near-name.ts +42 -0
- package/src/notation-lint.ts +41 -64
- package/src/relative-pos.ts +71 -2
- package/src/schema.ts +1 -1
- package/src/schemas/diagram.json +680 -23
- package/src/types.ts +337 -36
- package/src/v05/parser.ts +2553 -354
package/src/v05/parser.ts
CHANGED
|
@@ -41,13 +41,102 @@
|
|
|
41
41
|
* 出力は v0.4 と同じ DslDocument。 既存 compile.ts で CdlDiagram に変換できる。
|
|
42
42
|
*/
|
|
43
43
|
|
|
44
|
-
import type {
|
|
45
|
-
|
|
44
|
+
import type {
|
|
45
|
+
CdlLegendItem,
|
|
46
|
+
LegendMark,
|
|
47
|
+
NodeKind,
|
|
48
|
+
Tone,
|
|
49
|
+
EdgeStyle,
|
|
50
|
+
EdgeHead,
|
|
51
|
+
EdgeHeadFill,
|
|
52
|
+
ClassRelationType,
|
|
53
|
+
SequenceMessageKind,
|
|
54
|
+
} from "@cardenelabs/cdl";
|
|
55
|
+
import {
|
|
56
|
+
TONES,
|
|
57
|
+
LEGEND_MARKS,
|
|
58
|
+
NODE_KINDS,
|
|
59
|
+
EDGE_HEADS,
|
|
60
|
+
EDGE_HEAD_FILLS,
|
|
61
|
+
EDGE_STYLES,
|
|
62
|
+
EDGE_REVEALS,
|
|
63
|
+
RELATION_FOCUSES,
|
|
64
|
+
CLASS_RELATION_LOOK,
|
|
65
|
+
SEQUENCE_MESSAGE_LOOK,
|
|
66
|
+
parseFormula,
|
|
67
|
+
} from "@cardenelabs/cdl";
|
|
46
68
|
import type { EdgeReveal, RelationFocus } from "@cardenelabs/cdl";
|
|
47
|
-
import {
|
|
48
|
-
|
|
69
|
+
import {
|
|
70
|
+
TONE_ALIAS,
|
|
71
|
+
NODE_KIND_ALIAS,
|
|
72
|
+
DIRECTIONS,
|
|
73
|
+
resolveDirection,
|
|
74
|
+
ORDERS,
|
|
75
|
+
resolveOrder,
|
|
76
|
+
SHAPES,
|
|
77
|
+
DIAGRAM_SHAPES,
|
|
78
|
+
resolveDiagramShape,
|
|
79
|
+
shapesForDiagramType,
|
|
80
|
+
THEMES,
|
|
81
|
+
THEME_ALIAS,
|
|
82
|
+
resolveTheme,
|
|
83
|
+
LEGEND_MARK_ALIAS,
|
|
84
|
+
resolveLegendMark,
|
|
85
|
+
} from "../keywords";
|
|
86
|
+
import { 区画 } from "../compile/word-state";
|
|
87
|
+
// 行頭の印の古い語の読み替え (#2782)。 印を作る側と同じ file が表を持つ = 縦に並べた形と
|
|
88
|
+
// 中括弧の形と JSON の 3 経路が同じ表を通る
|
|
89
|
+
import { 行頭の語へ読み替える } from "../compile/row-marks";
|
|
90
|
+
import { PHASE_BODY_KEYS } from "../keywords";
|
|
91
|
+
import type { DslTheme } from "../keywords";
|
|
49
92
|
import type { DslDirection } from "../keywords";
|
|
50
|
-
import {
|
|
93
|
+
import type { DslOrder } from "../keywords";
|
|
94
|
+
import type { DslDiagramShape, DslShape } from "../keywords";
|
|
95
|
+
import { parseRelativePos, findRelativeProblems, type RelativeProblem } from "../relative-pos";
|
|
96
|
+
|
|
97
|
+
/** #2854 で記法に足した図表欄の閉じた語。JSON / schema と共有する。 */
|
|
98
|
+
export const CHART_LINE_DASH_VALUES = ["solid", "dotted"] as const;
|
|
99
|
+
export const CHART_LINE_MARKER_VALUES = ["dot", "hollow", "none"] as const;
|
|
100
|
+
export const CHART_PIE_TABLE_COLUMN_VALUES = ["share-value", "value"] as const;
|
|
101
|
+
export const CHART_PIE_RING_WIDTH_VALUES = ["thick", "thin"] as const;
|
|
102
|
+
export const CHART_STACKED_LEGEND_POSITION_VALUES = ["top", "bottom"] as const;
|
|
103
|
+
export const CHART_WAFFLE_LEGEND_POSITION_VALUES = ["left", "right"] as const;
|
|
104
|
+
export const FUNNEL_FORM_VALUES = ["trapezoid", "proportional-bars"] as const;
|
|
105
|
+
export const FUNNEL_RATE_VALUES = ["drop", "conversion", "none"] as const;
|
|
106
|
+
export const QUADRANT_POINT_LABEL_SIDE_VALUES = ["auto", "right"] as const;
|
|
107
|
+
export const TREE_NODE_FORM_VALUES = ["accent", "frame"] as const;
|
|
108
|
+
export const TREE_EDGE_TONE_VALUES = ["accent", "depth"] as const;
|
|
109
|
+
export const TREE_EDGE_HEAD_VALUES = ["none", "triangle"] as const;
|
|
110
|
+
export const MIND_FORM_VALUES = ["filled", "outline"] as const;
|
|
111
|
+
export const JOURNEY_FORM_VALUES = ["bands", "rules"] as const;
|
|
112
|
+
export const JOURNEY_LINE_FORM_VALUES = ["curve", "straight"] as const;
|
|
113
|
+
export const JOURNEY_OPPORTUNITY_POSITION_VALUES = ["below-chart", "below-point"] as const;
|
|
114
|
+
export const JOURNEY_LABEL_KEYS = ["delighted", "happy", "neutral", "frustrated", "angry"] as const;
|
|
115
|
+
export const CORNER_FORM_VALUES = ["theme-variants"] as const;
|
|
116
|
+
export const CHART_BAR_SHAPE_VALUES = ["split", "top-rounded"] as const;
|
|
117
|
+
export const QUADRANT_LABEL_POSITION_VALUES = ["outside-corners"] as const;
|
|
118
|
+
export const LABEL_COLLISION_VALUES = ["auto", "author"] as const;
|
|
119
|
+
export const GANTT_TASK_LABEL_ALIGN_VALUES = ["start", "end"] as const;
|
|
120
|
+
const TREE_SIZE_KEYS = [
|
|
121
|
+
"nodeWidths",
|
|
122
|
+
"nodeHeight",
|
|
123
|
+
"titleFontSize",
|
|
124
|
+
"subtitleFontSize",
|
|
125
|
+
"siblingGap",
|
|
126
|
+
"levelGap",
|
|
127
|
+
] as const;
|
|
128
|
+
const MIND_SIZE_KEYS = [
|
|
129
|
+
"contentOffset",
|
|
130
|
+
"root",
|
|
131
|
+
"branch",
|
|
132
|
+
"leaf",
|
|
133
|
+
"rootBranchGap",
|
|
134
|
+
"branchLeafGap",
|
|
135
|
+
"branchRowGap",
|
|
136
|
+
"branchRowOffset",
|
|
137
|
+
"leafRowGap",
|
|
138
|
+
"leafRowOffset",
|
|
139
|
+
] as const;
|
|
51
140
|
import {
|
|
52
141
|
checkValueExpression,
|
|
53
142
|
isTriggerBody,
|
|
@@ -57,7 +146,9 @@ import {
|
|
|
57
146
|
} from "../value-syntax";
|
|
58
147
|
import type {
|
|
59
148
|
DslAxes,
|
|
149
|
+
DslRegions,
|
|
60
150
|
DslDocument,
|
|
151
|
+
DslStageHeader,
|
|
61
152
|
DslActor,
|
|
62
153
|
DslNodeKind,
|
|
63
154
|
DslDynShape,
|
|
@@ -97,6 +188,110 @@ export const EDGE_HEAD_VALUES: readonly string[] = EDGE_HEADS;
|
|
|
97
188
|
/** 端の印の塗り方 (#1466)。 描画側の一覧から導く = 描画側が増やせば書ける */
|
|
98
189
|
export const EDGE_HEAD_FILL_VALUES: readonly string[] = EDGE_HEAD_FILLS;
|
|
99
190
|
|
|
191
|
+
/** cdl 0.130.0 の共通部品箱へ描ける線画。 */
|
|
192
|
+
export const CDL_ICON_VALUES = ["person", "box", "database", "lightning", "cloud", "terminal"] as const satisfies readonly NonNullable<DslActor["icon"]>[];
|
|
193
|
+
|
|
194
|
+
/** cdl 0.130.0 の表・部品箱と順序図が受ける閉じた値。 */
|
|
195
|
+
export const KIND_FORM_VALUES = ["icon"] as const;
|
|
196
|
+
export const TITLE_ALIGN_VALUES = ["center"] as const;
|
|
197
|
+
export const ROW_MARK_FORM_VALUES = ["tone"] as const;
|
|
198
|
+
export const ROW_MARK_TONE_VALUES = ["primary", "muted"] as const;
|
|
199
|
+
export const ROW_DIVIDER_FORM_VALUES = ["full"] as const;
|
|
200
|
+
export const SEQUENCE_SELF_LOOP_FORM_VALUES = ["orthogonal"] as const;
|
|
201
|
+
export const SEQUENCE_ACTOR_FORM_VALUES = ["box"] as const;
|
|
202
|
+
export const SEQUENCE_LABEL_FORM_VALUES = ["kind"] as const;
|
|
203
|
+
export const SEQUENCE_RETURN_HEAD_VALUES = ["solid"] as const;
|
|
204
|
+
export const SEQUENCE_KIND_LABEL_KEYS = ["call", "return", "fire"] as const;
|
|
205
|
+
export const SEQUENCE_SELF_LOOP_KEYS = ["form", "width", "drop"] as const;
|
|
206
|
+
export const ROW_WELL_KEYS = ["top", "right", "bottom", "left", "radius"] as const;
|
|
207
|
+
export const ROW_LAYOUT_KEYS = ["titleBaseline", "rowTop", "rowPitch"] as const;
|
|
208
|
+
export const LEGEND_LINE_STYLE_VALUES = ["solid", "dotted"] as const;
|
|
209
|
+
export const LEGEND_NOTE_ALIGN_VALUES = ["start", "end"] as const;
|
|
210
|
+
|
|
211
|
+
/** 表の行の受け皿を閉じた object として読み、寸法を 0 以上に限る。 */
|
|
212
|
+
function 行の受け皿として読む(
|
|
213
|
+
raw: string | undefined,
|
|
214
|
+
line: number,
|
|
215
|
+
errors: DslError[],
|
|
216
|
+
): DslActor["rowWell"] {
|
|
217
|
+
const value = JSONの欄を読む(raw ?? null, "rowWell", line, errors);
|
|
218
|
+
const unknown = 図表の知らない項目を知らせる(
|
|
219
|
+
value,
|
|
220
|
+
ROW_WELL_KEYS,
|
|
221
|
+
"rowWell",
|
|
222
|
+
line,
|
|
223
|
+
errors,
|
|
224
|
+
);
|
|
225
|
+
if (unknown || value === undefined) return undefined;
|
|
226
|
+
if (!objectか(value)) {
|
|
227
|
+
errors.push({ line, message: "rowWell の値の形が読めません" });
|
|
228
|
+
return undefined;
|
|
229
|
+
}
|
|
230
|
+
for (const key of ROW_WELL_KEYS) {
|
|
231
|
+
const item = value[key];
|
|
232
|
+
if (item !== undefined && (typeof item !== "number" || !Number.isFinite(item) || item < 0)) {
|
|
233
|
+
errors.push({ line, message: `rowWell.${key} が読めません`, hint: "0 以上の数で書く" });
|
|
234
|
+
return undefined;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return value;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** 表の題と行の baseline を閉じた object として読む。 */
|
|
241
|
+
function 行の配置として読む(
|
|
242
|
+
raw: string | undefined,
|
|
243
|
+
line: number,
|
|
244
|
+
errors: DslError[],
|
|
245
|
+
): DslActor["rowLayout"] {
|
|
246
|
+
const value = JSONの欄を読む(raw ?? null, "rowLayout", line, errors);
|
|
247
|
+
const unknown = 図表の知らない項目を知らせる(value, ROW_LAYOUT_KEYS, "rowLayout", line, errors);
|
|
248
|
+
if (unknown || value === undefined) return undefined;
|
|
249
|
+
if (!objectか(value)) {
|
|
250
|
+
errors.push({ line, message: "rowLayout の値の形が読めません" });
|
|
251
|
+
return undefined;
|
|
252
|
+
}
|
|
253
|
+
for (const key of ROW_LAYOUT_KEYS) {
|
|
254
|
+
const item = value[key];
|
|
255
|
+
const valid = typeof item === "number" && Number.isFinite(item) &&
|
|
256
|
+
(key === "rowPitch" ? item > 0 : item >= 0);
|
|
257
|
+
if (item !== undefined && !valid) {
|
|
258
|
+
errors.push({ line, message: `rowLayout.${key} が読めません`, hint: key === "rowPitch" ? "正の数で書く" : "0 以上の数で書く" });
|
|
259
|
+
return undefined;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return value;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** 固定線の途中の点を読み、始点から終点までの各区間を直角に限る。 */
|
|
266
|
+
function 線の途中の点として読む(
|
|
267
|
+
raw: string | undefined,
|
|
268
|
+
ends: readonly [number | undefined, number | undefined, number | undefined, number | undefined],
|
|
269
|
+
line: number,
|
|
270
|
+
errors: DslError[],
|
|
271
|
+
): DslStep["waypoints"] {
|
|
272
|
+
if (raw === undefined) return undefined;
|
|
273
|
+
const value = JSONの欄を読む(raw ?? null, "waypoints", line, errors);
|
|
274
|
+
if (value === undefined) return undefined;
|
|
275
|
+
if (Array.isArray(value)) {
|
|
276
|
+
const unknown = value.some((point, index) =>
|
|
277
|
+
図表の知らない項目を知らせる(point, ["x", "y"], `waypoints[${index}]`, line, errors));
|
|
278
|
+
if (unknown) return undefined;
|
|
279
|
+
}
|
|
280
|
+
if (!Array.isArray(value) || value.some((point) => !objectか(point) || typeof point.x !== "number" || !Number.isFinite(point.x) || typeof point.y !== "number" || !Number.isFinite(point.y))) {
|
|
281
|
+
errors.push({ line, message: "waypoints の値の形が読めません", hint: '`[{"x":232,"y":293.45}]` の形で書く' });
|
|
282
|
+
return undefined;
|
|
283
|
+
}
|
|
284
|
+
const [x1, y1, x2, y2] = ends;
|
|
285
|
+
if ([x1, y1, x2, y2].every((item) => item !== undefined)) {
|
|
286
|
+
const points = [{ x: x1!, y: y1! }, ...value, { x: x2!, y: y2! }];
|
|
287
|
+
if (points.some((point, index) => index > 0 && point.x !== points[index - 1]!.x && point.y !== points[index - 1]!.y)) {
|
|
288
|
+
errors.push({ line, message: "waypoints の各区間は縦か横にする" });
|
|
289
|
+
return undefined;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return value;
|
|
293
|
+
}
|
|
294
|
+
|
|
100
295
|
/**
|
|
101
296
|
* 辺の役目として書ける語 (#1559)。
|
|
102
297
|
*
|
|
@@ -114,7 +309,7 @@ export const EDGE_ROLE_VALUES = ["main"] as const;
|
|
|
114
309
|
export const SHAPE_ORIENT_VALUES = ["up", "down", "left", "right"] as const;
|
|
115
310
|
|
|
116
311
|
/**
|
|
117
|
-
*
|
|
312
|
+
* record に書ける UML の関係の種類 (#1466 / #2783)。描画側の表 (`CLASS_RELATION_LOOK`) の key から導く。
|
|
118
313
|
*
|
|
119
314
|
* 手で並べると、描画側が種類を足した時にここだけ取り残されて書けないままになる。
|
|
120
315
|
*/
|
|
@@ -145,6 +340,8 @@ export const TOP_LEVEL_KEYS = [
|
|
|
145
340
|
"eyebrow",
|
|
146
341
|
// 2 軸で仕分ける図の軸の名前 (#1251)
|
|
147
342
|
"axes",
|
|
343
|
+
// 2 軸で仕分ける図の区画の名前 (#2667)
|
|
344
|
+
"regions",
|
|
148
345
|
// 値を見せる部品 (#1374)
|
|
149
346
|
"readouts",
|
|
150
347
|
// 読む人が動かすつまみ (#1389)
|
|
@@ -173,24 +370,110 @@ export const TOP_LEVEL_KEYS = [
|
|
|
173
370
|
*/
|
|
174
371
|
"direction",
|
|
175
372
|
/*
|
|
176
|
-
*
|
|
373
|
+
* 箱を並べる順 (#2655)。 並べ替えの鍵しか持たない図種を畳んだ先で、書かなければ
|
|
374
|
+
* 書いた順のまま。 最上位の語は英語にする決まりに従う (向きの語と同じ)
|
|
375
|
+
*/
|
|
376
|
+
"order",
|
|
377
|
+
/*
|
|
378
|
+
* 数を描く図の形 (#2657)。 形しか違わない図種 9 つを畳んだ先で、`type: chart` だけが読む。
|
|
379
|
+
* 最上位の語は英語にする決まりに従う (向きの語と同じ)
|
|
380
|
+
*/
|
|
381
|
+
"shape",
|
|
382
|
+
// 円グラフの見せ方 (#2837)
|
|
383
|
+
"form",
|
|
384
|
+
// ガントチャートの目盛り (#2837)
|
|
385
|
+
"ticks",
|
|
386
|
+
"ganttToday",
|
|
387
|
+
"ganttTickLabels",
|
|
388
|
+
"ganttBarEnd",
|
|
389
|
+
"ganttBarThickness",
|
|
390
|
+
"chartSeriesSkipMuted",
|
|
391
|
+
"chartLineSeries",
|
|
392
|
+
"chartPieCenterLabel",
|
|
393
|
+
"chartPieTableColumns",
|
|
394
|
+
"chartPieRingWidth",
|
|
395
|
+
"chartSlopePeriods",
|
|
396
|
+
"chartSlopeEmphasisIds",
|
|
397
|
+
"chartSlopeUnit",
|
|
398
|
+
"chartStackedPeriods",
|
|
399
|
+
"chartStackedRateId",
|
|
400
|
+
"chartStackedLegendPosition",
|
|
401
|
+
"chartGaugeValue",
|
|
402
|
+
"chartWaffleLegendPosition",
|
|
403
|
+
"funnelForm",
|
|
404
|
+
"funnelRate",
|
|
405
|
+
"quadrantPointLabelSide",
|
|
406
|
+
"treeNodeForm",
|
|
407
|
+
"treeSize",
|
|
408
|
+
"treeEdgeTone",
|
|
409
|
+
"treeEdgeHead",
|
|
410
|
+
"mindForm",
|
|
411
|
+
"mindSize",
|
|
412
|
+
"journeyForm",
|
|
413
|
+
"journeyLineForm",
|
|
414
|
+
"journeyLabels",
|
|
415
|
+
"journeySize",
|
|
416
|
+
"sequenceActorForm",
|
|
417
|
+
"sequenceLabelForm",
|
|
418
|
+
"sequenceReturnHead",
|
|
419
|
+
"sequenceKindLabels",
|
|
420
|
+
"sequenceRowGap",
|
|
421
|
+
"sequenceGutter",
|
|
422
|
+
"sequenceBandWidth",
|
|
423
|
+
"sequenceSelfLoop",
|
|
424
|
+
"figureCard",
|
|
425
|
+
"figureSize",
|
|
426
|
+
"terminalDecoration",
|
|
427
|
+
"cornerForm",
|
|
428
|
+
"figureKindLabel",
|
|
429
|
+
"figureInset",
|
|
430
|
+
"chartBarSize",
|
|
431
|
+
"chartBarShape",
|
|
432
|
+
"chartLineSize",
|
|
433
|
+
"chartPieSize",
|
|
434
|
+
"chartWaffleSize",
|
|
435
|
+
"chartSlopeLabelTemplate",
|
|
436
|
+
"chartSlopeSize",
|
|
437
|
+
"chartStackedLayout",
|
|
438
|
+
"chartGaugeSize",
|
|
439
|
+
"ganttSize",
|
|
440
|
+
"quadrantLabelPosition",
|
|
441
|
+
"quadrantSize",
|
|
442
|
+
"stageLayout",
|
|
443
|
+
/*
|
|
444
|
+
* 図の意匠 (#1553 / #2790)。
|
|
177
445
|
*
|
|
178
446
|
* cdl は色を持たないので、名前だけを図に載せる。 消費側 (`cdl-theme.css`) が名前を見て
|
|
179
447
|
* 7 つの口 (台 / 行の面 / 縞 / 枠 / 字 / 型名 / 線) に色を当てる。
|
|
180
448
|
*
|
|
181
|
-
*
|
|
182
|
-
* (行頭の印 + 左に名前 + 右に型) で、名前と型が離れて並ぶため、行を横に追う目印
|
|
183
|
-
* (行の縞) が要る。 縞の色は配色からしか来ないので、既定が無いと縞が箱の面と同じ色に
|
|
184
|
-
* 落ちて 1 本も出ない。 書き手が `palette:` を書いた時はそちらが勝つ。
|
|
449
|
+
* 意匠は dragon で読み、cdl へは `palette` として渡す。
|
|
185
450
|
*/
|
|
186
|
-
"
|
|
451
|
+
"theme",
|
|
452
|
+
"legend",
|
|
453
|
+
"legendOffset",
|
|
454
|
+
"legendFontSize",
|
|
455
|
+
"edgeEndGap",
|
|
456
|
+
"stageHeader",
|
|
187
457
|
] as const;
|
|
188
458
|
|
|
459
|
+
/**
|
|
460
|
+
* 最上位の語の別名 (#2790)。
|
|
461
|
+
*
|
|
462
|
+
* 一覧は正の語だけを持つ。 README と変更履歴の検査が一覧を読むため、別名を混ぜると
|
|
463
|
+
* 公開する正の語が 2 つに見える。
|
|
464
|
+
*/
|
|
465
|
+
export const TOP_LEVEL_KEY_ALIASES: ReadonlyMap<string, (typeof TOP_LEVEL_KEYS)[number]> = new Map([
|
|
466
|
+
["palette", "theme"],
|
|
467
|
+
["凡例", "legend"],
|
|
468
|
+
["見せ方", "form"],
|
|
469
|
+
["目盛り", "ticks"],
|
|
470
|
+
]);
|
|
471
|
+
|
|
189
472
|
/**
|
|
190
473
|
* `lanes:` / `groups:` の 1 行を読む形 (#1241)。
|
|
191
474
|
*
|
|
192
475
|
* **id は英数字と下線に限らない**。 組み立て側は登場人物の名前から縦列 id を作るため、
|
|
193
|
-
* hyphen と日本語が入る (実測 = `type:
|
|
476
|
+
* hyphen と日本語が入る (実測 = `type: record` で `lane-idle` / `lane-待機`、
|
|
194
477
|
* `type: swimlane` で `lane-sign-up`)。 英数字と下線だけを受けていた間、
|
|
195
478
|
* **自動で作られた縦列の幅や見出しを書き直す手段が無かった**。
|
|
196
479
|
*
|
|
@@ -308,22 +591,12 @@ function isTopLevelKey(key: string): key is (typeof TOP_LEVEL_KEYS)[number] {
|
|
|
308
591
|
export const PRESET_TYPES: ReadonlySet<PresetType> = new Set([
|
|
309
592
|
"sequence",
|
|
310
593
|
"flow",
|
|
594
|
+
"flowchart",
|
|
311
595
|
"swimlane",
|
|
312
|
-
"
|
|
313
|
-
"state",
|
|
596
|
+
"record",
|
|
314
597
|
"topology",
|
|
315
|
-
"solidity",
|
|
316
598
|
"gantt",
|
|
317
|
-
"
|
|
318
|
-
"pie",
|
|
319
|
-
"bar",
|
|
320
|
-
"line",
|
|
321
|
-
"gauge",
|
|
322
|
-
"radial",
|
|
323
|
-
"stat",
|
|
324
|
-
"waffle",
|
|
325
|
-
"stacked",
|
|
326
|
-
"slope",
|
|
599
|
+
"chart",
|
|
327
600
|
"funnel",
|
|
328
601
|
"tree",
|
|
329
602
|
"journey",
|
|
@@ -332,6 +605,41 @@ export const PRESET_TYPES: ReadonlySet<PresetType> = new Set([
|
|
|
332
605
|
"mind",
|
|
333
606
|
]);
|
|
334
607
|
|
|
608
|
+
/**
|
|
609
|
+
* 図種の別名 (#2655 / #2657)。
|
|
610
|
+
*
|
|
611
|
+
* **差が語 1 つしかない図種を、畳んだ先の型とその語に読み替える**。
|
|
612
|
+
*
|
|
613
|
+
* | 古い綴り | 読み替え先 | 差 |
|
|
614
|
+
* |---|---|---|
|
|
615
|
+
* | `solidity` | `sequence` + `order: 種類` | 箱を種類で並べ替えるだけで、独自の組み立てを持たない |
|
|
616
|
+
* | 数を描く 9 つ | `chart` + `shape: <綴り>` | 同じ組み立て関数に渡す文字列 2 つしか違わない |
|
|
617
|
+
*
|
|
618
|
+
* 型として持つと、同じ扱いをする条件と表が型の数だけ増える。 `solidity` は
|
|
619
|
+
* 「順序図 または その型」 という条件を 12 か所に増やし、数を描く 9 つは 4 つの表に
|
|
620
|
+
* 同じ値の行を 9 行ずつ並べていた (足す人が 1 か所書き忘れると黙って外れる)。
|
|
621
|
+
*
|
|
622
|
+
* **読み替えは読み取りの入口 1 か所に閉じる**。 ここを通った後は古い綴りが
|
|
623
|
+
* どこにも残らないので、組み立て側は畳んだ先の型として 1 通りに扱える。
|
|
624
|
+
*
|
|
625
|
+
* 古い名前で書いた記法はそのまま動く = 別名を消すのは別の回にする。
|
|
626
|
+
*/
|
|
627
|
+
type 読み替え先 = { type: PresetType; order?: DslOrder; shape?: DslShape };
|
|
628
|
+
|
|
629
|
+
export const TYPE_ALIASES: ReadonlyMap<string, 読み替え先> = new Map<string, 読み替え先>([
|
|
630
|
+
["solidity", { type: "sequence", order: "種類" }],
|
|
631
|
+
...SHAPES.map((s): [string, 読み替え先] => [s, { type: "chart", shape: s }]),
|
|
632
|
+
]);
|
|
633
|
+
|
|
634
|
+
/** `shape:` の綴り違いへ、図種ごとに書ける語を案内する (#2797)。 */
|
|
635
|
+
function shapeの案内(type: PresetType | null): string {
|
|
636
|
+
const その図種 = type === null ? [] : shapesForDiagramType(type);
|
|
637
|
+
if (その図種.length > 0) return `使える語 = ${その図種.join(" / ")}`;
|
|
638
|
+
return `shape を書ける図種と語 = ${Object.entries(DIAGRAM_SHAPES)
|
|
639
|
+
.map(([図種, 語]) => `type: ${図種} (${語.join(" / ")})`)
|
|
640
|
+
.join(" / ")}`;
|
|
641
|
+
}
|
|
642
|
+
|
|
335
643
|
const NODE_KIND_DEFAULT: NodeKind = "actor";
|
|
336
644
|
|
|
337
645
|
/**
|
|
@@ -353,6 +661,10 @@ export const DSL_ONLY_KINDS = [
|
|
|
353
661
|
"proxy",
|
|
354
662
|
"library",
|
|
355
663
|
"interface",
|
|
664
|
+
// 繰り返しの箱 (#2523)。 描画側は分かれ道の図の形としてだけ持ち、種類としては持たないため
|
|
665
|
+
// ここに置く。 分かれ道の図では形に読み替わり (`compile/flowchart.ts`)、それ以外の図種では
|
|
666
|
+
// `記法だけの種類の読み替え` が札に落とす
|
|
667
|
+
"loop",
|
|
356
668
|
] as const;
|
|
357
669
|
|
|
358
670
|
/** 記法だけが持つ種類。 描画側の `NodeKind` には含まれない */
|
|
@@ -383,14 +695,12 @@ const INFRA_KIND_ALIAS: Record<string, NodeKind> = {
|
|
|
383
695
|
};
|
|
384
696
|
|
|
385
697
|
/**
|
|
386
|
-
*
|
|
698
|
+
* 記法が受理する種類の全体。 これに載っていない種類は見本 (パーツ) の候補になる。
|
|
387
699
|
*
|
|
388
|
-
*
|
|
389
|
-
*
|
|
700
|
+
* 描画できる種類 (cdl の `NODE_KINDS`) に、 記法だけが持つ種類と基盤の別名を足す。
|
|
701
|
+
* 手書きの一覧だった頃は、 描画側に種類が増えても記法から書けず、 部品名として扱われて
|
|
702
|
+
* 「そんな部品は無い」 と警告が出るだけだった。 描画側を出所に加えることで
|
|
390
703
|
* 「描画できるものは書ける」 が成立する。
|
|
391
|
-
*/
|
|
392
|
-
/**
|
|
393
|
-
* 記法が受理する種類の全体。 これに載っていない種類は見本 (パーツ) の候補になる。
|
|
394
704
|
*
|
|
395
705
|
* 画面側が「本文が見本を使っているか」 を判定するのに使う (#1022)。 手書きの一覧を
|
|
396
706
|
* 別に持つと、種類が増えた時にそちらだけ取り残されて余分な読み込みが起きる。
|
|
@@ -461,6 +771,20 @@ type Line = {
|
|
|
461
771
|
no: number;
|
|
462
772
|
};
|
|
463
773
|
|
|
774
|
+
/**
|
|
775
|
+
* 読めなかった最上位の項目名を、行から取り出す (#2341)。
|
|
776
|
+
*
|
|
777
|
+
* **行ではなく名前だけを引用する**。 行をまるごと引用すると、値に引用符がある時に
|
|
778
|
+
* `"題: "t""` のように入れ子になって、どこまでが名前か読めない (実測)。
|
|
779
|
+
*
|
|
780
|
+
* 箱 / 矢印 / 縦列の読めない項目名はどれも名前だけを引用しており、最上位だけが
|
|
781
|
+
* 揃っていなかった。 `:` を持たない行はそのまま返す = その時は行全体が名前の位置にある。
|
|
782
|
+
*/
|
|
783
|
+
function 最上位の項目名(trimmed: string): string {
|
|
784
|
+
const idx = trimmed.indexOf(":");
|
|
785
|
+
return (idx < 0 ? trimmed : trimmed.slice(0, idx)).trim();
|
|
786
|
+
}
|
|
787
|
+
|
|
464
788
|
export function parseTextDslV05(src: string): V05ParseResult {
|
|
465
789
|
const errors: DslError[] = [];
|
|
466
790
|
const lines = tokenize(src);
|
|
@@ -473,9 +797,79 @@ export function parseTextDslV05(src: string): V05ParseResult {
|
|
|
473
797
|
let relations: RelationFocus | null = null;
|
|
474
798
|
let direction: DslDirection | null = null;
|
|
475
799
|
let directionLine = 0;
|
|
476
|
-
let
|
|
800
|
+
let order: DslOrder | null = null;
|
|
801
|
+
let orderLine = 0;
|
|
802
|
+
// `shape:` は `type:` より前にも書ける。 図種が決まるまで語と行だけを控える (#2797)。
|
|
803
|
+
let shapeWritten: { value: string; line: number } | null = null;
|
|
804
|
+
let formWritten: { value: string; line: number } | null = null;
|
|
805
|
+
let ticks: string[] | undefined;
|
|
806
|
+
let ticksLine = 0;
|
|
807
|
+
let ganttToday: DslDocument["ganttToday"];
|
|
808
|
+
let ganttTickLabels: string[] | undefined;
|
|
809
|
+
let ganttBarEnd: DslDocument["ganttBarEnd"];
|
|
810
|
+
let ganttBarThickness: DslDocument["ganttBarThickness"];
|
|
811
|
+
let chartSeriesSkipMuted: DslDocument["chartSeriesSkipMuted"];
|
|
812
|
+
let chartLineSeries: DslDocument["chartLineSeries"];
|
|
813
|
+
let chartPieCenterLabel: DslDocument["chartPieCenterLabel"];
|
|
814
|
+
let chartPieTableColumns: DslDocument["chartPieTableColumns"];
|
|
815
|
+
let chartPieRingWidth: DslDocument["chartPieRingWidth"];
|
|
816
|
+
let chartSlopePeriods: DslDocument["chartSlopePeriods"];
|
|
817
|
+
let chartSlopeEmphasisIds: DslDocument["chartSlopeEmphasisIds"];
|
|
818
|
+
let chartSlopeUnit: DslDocument["chartSlopeUnit"];
|
|
819
|
+
let chartStackedPeriods: DslDocument["chartStackedPeriods"];
|
|
820
|
+
let chartStackedRateId: DslDocument["chartStackedRateId"];
|
|
821
|
+
let chartStackedLegendPosition: DslDocument["chartStackedLegendPosition"];
|
|
822
|
+
let chartGaugeValue: DslDocument["chartGaugeValue"];
|
|
823
|
+
let chartWaffleLegendPosition: DslDocument["chartWaffleLegendPosition"];
|
|
824
|
+
let funnelForm: DslDocument["funnelForm"];
|
|
825
|
+
let funnelRate: DslDocument["funnelRate"];
|
|
826
|
+
let quadrantPointLabelSide: DslDocument["quadrantPointLabelSide"];
|
|
827
|
+
let treeNodeForm: DslDocument["treeNodeForm"];
|
|
828
|
+
let treeSize: DslDocument["treeSize"];
|
|
829
|
+
let treeEdgeTone: DslDocument["treeEdgeTone"];
|
|
830
|
+
let treeEdgeHead: DslDocument["treeEdgeHead"];
|
|
831
|
+
let mindForm: DslDocument["mindForm"];
|
|
832
|
+
let mindSize: DslDocument["mindSize"];
|
|
833
|
+
let journeyForm: DslDocument["journeyForm"];
|
|
834
|
+
let journeyLineForm: DslDocument["journeyLineForm"];
|
|
835
|
+
let journeyLabels: DslDocument["journeyLabels"];
|
|
836
|
+
let journeySize: DslDocument["journeySize"];
|
|
837
|
+
let sequenceActorForm: DslDocument["sequenceActorForm"];
|
|
838
|
+
let sequenceLabelForm: DslDocument["sequenceLabelForm"];
|
|
839
|
+
let sequenceReturnHead: DslDocument["sequenceReturnHead"];
|
|
840
|
+
let sequenceKindLabels: DslDocument["sequenceKindLabels"];
|
|
841
|
+
let sequenceRowGap: DslDocument["sequenceRowGap"];
|
|
842
|
+
let sequenceGutter: DslDocument["sequenceGutter"];
|
|
843
|
+
let sequenceBandWidth: DslDocument["sequenceBandWidth"];
|
|
844
|
+
let sequenceSelfLoop: DslDocument["sequenceSelfLoop"];
|
|
845
|
+
let figureCard: DslDocument["figureCard"];
|
|
846
|
+
let figureSize: DslDocument["figureSize"];
|
|
847
|
+
let terminalDecoration: DslDocument["terminalDecoration"];
|
|
848
|
+
let cornerForm: DslDocument["cornerForm"];
|
|
849
|
+
let figureKindLabel: DslDocument["figureKindLabel"];
|
|
850
|
+
let figureInset: DslDocument["figureInset"];
|
|
851
|
+
let chartBarSize: DslDocument["chartBarSize"];
|
|
852
|
+
let chartBarShape: DslDocument["chartBarShape"];
|
|
853
|
+
let chartLineSize: DslDocument["chartLineSize"];
|
|
854
|
+
let chartPieSize: DslDocument["chartPieSize"];
|
|
855
|
+
let chartWaffleSize: DslDocument["chartWaffleSize"];
|
|
856
|
+
let chartSlopeLabelTemplate: DslDocument["chartSlopeLabelTemplate"];
|
|
857
|
+
let chartSlopeSize: DslDocument["chartSlopeSize"];
|
|
858
|
+
let chartStackedLayout: DslDocument["chartStackedLayout"];
|
|
859
|
+
let chartGaugeSize: DslDocument["chartGaugeSize"];
|
|
860
|
+
let ganttSize: DslDocument["ganttSize"];
|
|
861
|
+
let quadrantLabelPosition: DslDocument["quadrantLabelPosition"];
|
|
862
|
+
let quadrantSize: DslDocument["quadrantSize"];
|
|
863
|
+
let stageLayout: DslDocument["stageLayout"];
|
|
864
|
+
let aliasShape: DslShape | null = null;
|
|
865
|
+
let aliasShapeLine = 0;
|
|
866
|
+
let theme: DslTheme | null = null;
|
|
867
|
+
let themeLine = 0;
|
|
868
|
+
let palette: DslTheme | null = null;
|
|
869
|
+
let paletteLine = 0;
|
|
477
870
|
let axes: DslAxes | undefined = undefined;
|
|
478
871
|
let axesLine = 0;
|
|
872
|
+
let regions: DslRegions | undefined = undefined;
|
|
479
873
|
let actors: DslActor[] = [];
|
|
480
874
|
const flow: DslStep[] = [];
|
|
481
875
|
let animate: DslAnimate | undefined = undefined;
|
|
@@ -491,6 +885,11 @@ export function parseTextDslV05(src: string): V05ParseResult {
|
|
|
491
885
|
let scrollsList: DslScrollTrigger[] | undefined = undefined;
|
|
492
886
|
let scrollLines = new Map<string, number>();
|
|
493
887
|
let groupsMap: Record<string, DslGroup> | undefined = undefined;
|
|
888
|
+
const legend: CdlLegendItem[] = [];
|
|
889
|
+
let legendOffset: DslDocument["legendOffset"];
|
|
890
|
+
let legendFontSize: number | undefined;
|
|
891
|
+
let edgeEndGap: DslDocument["edgeEndGap"];
|
|
892
|
+
let stageHeader: DslDocument["stageHeader"];
|
|
494
893
|
|
|
495
894
|
let i = 0;
|
|
496
895
|
while (i < lines.length) {
|
|
@@ -499,12 +898,18 @@ export function parseTextDslV05(src: string): V05ParseResult {
|
|
|
499
898
|
i += 1;
|
|
500
899
|
continue;
|
|
501
900
|
}
|
|
502
|
-
const
|
|
901
|
+
const 書いた頭 = matchTopHeader(line.trimmed);
|
|
902
|
+
const head = 書いた頭
|
|
903
|
+
? {
|
|
904
|
+
...書いた頭,
|
|
905
|
+
key: TOP_LEVEL_KEY_ALIASES.get(書いた頭.key) ?? 書いた頭.key,
|
|
906
|
+
}
|
|
907
|
+
: null;
|
|
503
908
|
if (!head || !isTopLevelKey(head.key)) {
|
|
504
909
|
errors.push({
|
|
505
910
|
line: line.no,
|
|
506
|
-
message:
|
|
507
|
-
hint:
|
|
911
|
+
message: `最上位の項目名が読めません: "${最上位の項目名(line.trimmed)}"`,
|
|
912
|
+
hint: `使える項目 = ${TOP_LEVEL_KEYS.join(", ")}`,
|
|
508
913
|
});
|
|
509
914
|
i += 1;
|
|
510
915
|
continue;
|
|
@@ -512,7 +917,11 @@ export function parseTextDslV05(src: string): V05ParseResult {
|
|
|
512
917
|
if (head.key === "title") {
|
|
513
918
|
title = head.value ?? null;
|
|
514
919
|
if (!title) {
|
|
515
|
-
errors.push({
|
|
920
|
+
errors.push({
|
|
921
|
+
line: line.no,
|
|
922
|
+
message: "title が書かれていません",
|
|
923
|
+
hint: "`title: \"...\"` の形で書く",
|
|
924
|
+
});
|
|
516
925
|
}
|
|
517
926
|
i += 1;
|
|
518
927
|
continue;
|
|
@@ -544,7 +953,7 @@ export function parseTextDslV05(src: string): V05ParseResult {
|
|
|
544
953
|
/*
|
|
545
954
|
* 箱に触れると関係する線だけを光らせるか (#1757)。
|
|
546
955
|
*
|
|
547
|
-
*
|
|
956
|
+
* UML や表の関係を描く record は順番を持たない図で、段を追う見せ方は読み手の問い
|
|
548
957
|
* (この箱はどこと繋がっているか) に答えない。 `hover` と書くと、箱に触れた時に
|
|
549
958
|
* その箱 ・ 繋がる線 ・ 相手の箱だけが光る (`cdl#806`)。
|
|
550
959
|
*/
|
|
@@ -589,29 +998,134 @@ export function parseTextDslV05(src: string): V05ParseResult {
|
|
|
589
998
|
i += 1;
|
|
590
999
|
continue;
|
|
591
1000
|
}
|
|
592
|
-
if (head.key === "
|
|
1001
|
+
if (head.key === "order") {
|
|
1002
|
+
/*
|
|
1003
|
+
* 箱を並べる順 (#2655)。
|
|
1004
|
+
*
|
|
1005
|
+
* 書かなければ書いた順。 `種類` と書くと箱の種類で並べ替えてから置く。
|
|
1006
|
+
* 向きと同じく **書いた行を覚える** = 効かない図種に書いた時の知らせが場所を指せる。
|
|
1007
|
+
*/
|
|
1008
|
+
const v = (head.value ?? "").trim();
|
|
1009
|
+
if (v.length > 0) {
|
|
1010
|
+
const 解けた = resolveOrder(v);
|
|
1011
|
+
if (解けた !== null) {
|
|
1012
|
+
order = 解けた;
|
|
1013
|
+
orderLine = line.no;
|
|
1014
|
+
} else {
|
|
1015
|
+
errors.push({
|
|
1016
|
+
line: line.no,
|
|
1017
|
+
message: `order が読めません (書いた値: ${v})`,
|
|
1018
|
+
hint: `使える語 = ${ORDERS.join(" / ")} / kind`,
|
|
1019
|
+
});
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
i += 1;
|
|
1023
|
+
continue;
|
|
1024
|
+
}
|
|
1025
|
+
if (head.key === "theme") {
|
|
593
1026
|
/*
|
|
594
|
-
*
|
|
1027
|
+
* 図の意匠 (#1553 / #2790)。
|
|
595
1028
|
*
|
|
596
1029
|
* 読めない語はその場で知らせる。 黙って既定に落とすと、書き手には「書いたのに効かない」
|
|
597
1030
|
* としか見えず、書き間違いか未対応かを分けられない。
|
|
598
1031
|
*/
|
|
599
1032
|
const v = (head.value ?? "").trim();
|
|
1033
|
+
const 書いた語 = 書いた頭?.key ?? head.key;
|
|
600
1034
|
if (v.length > 0) {
|
|
601
|
-
const 解けた =
|
|
1035
|
+
const 解けた = resolveTheme(v);
|
|
602
1036
|
if (解けた !== null) {
|
|
603
|
-
|
|
1037
|
+
if (書いた語 === "theme") {
|
|
1038
|
+
theme = 解けた;
|
|
1039
|
+
themeLine = line.no;
|
|
1040
|
+
} else {
|
|
1041
|
+
palette = 解けた;
|
|
1042
|
+
paletteLine = line.no;
|
|
1043
|
+
}
|
|
604
1044
|
} else {
|
|
605
1045
|
errors.push({
|
|
606
1046
|
line: line.no,
|
|
607
|
-
message:
|
|
608
|
-
hint: `使える語 = ${
|
|
1047
|
+
message: `${書いた語} が読めません (書いた値: ${v}。 使える名前 = ${THEMES.join(" / ")})`,
|
|
1048
|
+
hint: `使える語 = ${Object.keys(THEME_ALIAS).join(" / ")}`,
|
|
609
1049
|
});
|
|
610
1050
|
}
|
|
611
1051
|
}
|
|
612
1052
|
i += 1;
|
|
613
1053
|
continue;
|
|
614
1054
|
}
|
|
1055
|
+
if (head.key === "legend") {
|
|
1056
|
+
const inline = head.value?.trim();
|
|
1057
|
+
if (inline) {
|
|
1058
|
+
errors.push({
|
|
1059
|
+
line: line.no,
|
|
1060
|
+
message: "legend は 1 行にまとめて書けない",
|
|
1061
|
+
hint: '`legend:` の次の行から `{ mark: diamond, text: "分かれ道" }` の形で並べる',
|
|
1062
|
+
});
|
|
1063
|
+
i += 1;
|
|
1064
|
+
continue;
|
|
1065
|
+
}
|
|
1066
|
+
const { items, next } = collectIndentedList(lines, i + 1, line.indent);
|
|
1067
|
+
for (const item of items) {
|
|
1068
|
+
const parsed = parseLegendItem(item, errors);
|
|
1069
|
+
if (parsed !== null) legend.push(parsed);
|
|
1070
|
+
}
|
|
1071
|
+
i = next;
|
|
1072
|
+
continue;
|
|
1073
|
+
}
|
|
1074
|
+
if (head.key === "legendFontSize") {
|
|
1075
|
+
const value = Number((head.value ?? "").trim());
|
|
1076
|
+
if (Number.isFinite(value) && value > 0) legendFontSize = value;
|
|
1077
|
+
else {
|
|
1078
|
+
errors.push({
|
|
1079
|
+
line: line.no,
|
|
1080
|
+
message: `legendFontSize が読めません: "${head.value ?? ""}"`,
|
|
1081
|
+
hint: "0 より大きい数を書く",
|
|
1082
|
+
});
|
|
1083
|
+
}
|
|
1084
|
+
i += 1;
|
|
1085
|
+
continue;
|
|
1086
|
+
}
|
|
1087
|
+
if (head.key === "edgeEndGap") {
|
|
1088
|
+
const written = (head.value ?? "").trim();
|
|
1089
|
+
const value = Number(written);
|
|
1090
|
+
if (written !== "" && Number.isFinite(value) && value >= 0) edgeEndGap = value;
|
|
1091
|
+
else {
|
|
1092
|
+
errors.push({
|
|
1093
|
+
line: line.no,
|
|
1094
|
+
message: `edgeEndGap が読めません: "${head.value ?? ""}"`,
|
|
1095
|
+
hint: "0 以上の数を書く",
|
|
1096
|
+
});
|
|
1097
|
+
}
|
|
1098
|
+
i += 1;
|
|
1099
|
+
continue;
|
|
1100
|
+
}
|
|
1101
|
+
if (head.key === "stageHeader") {
|
|
1102
|
+
const written = (head.value ?? "").trim();
|
|
1103
|
+
const isInlineMapping = written.startsWith("{") && written.endsWith("}");
|
|
1104
|
+
const inner = isInlineMapping ? written.slice(1, -1) : "";
|
|
1105
|
+
const opts = parseInlineMapping(inner);
|
|
1106
|
+
const keys = ["leftPad", "topPad", "numberSize", "gap", "nameSize", "bottomPad"] as const;
|
|
1107
|
+
if (isInlineMapping) {
|
|
1108
|
+
中括弧の知らない項目名を知らせる(inner, keys, "段の見出しの ", line.no, errors);
|
|
1109
|
+
}
|
|
1110
|
+
const values = Object.fromEntries(
|
|
1111
|
+
keys.map((key) => [key, opts[key] === undefined ? undefined : Number(opts[key])]),
|
|
1112
|
+
) as Record<(typeof keys)[number], number | undefined>;
|
|
1113
|
+
const required = ["topPad", "numberSize", "gap", "nameSize", "bottomPad"] as const;
|
|
1114
|
+
if (
|
|
1115
|
+
required.some((key) => !Number.isFinite(values[key])) ||
|
|
1116
|
+
(values.leftPad !== undefined && !Number.isFinite(values.leftPad))
|
|
1117
|
+
) {
|
|
1118
|
+
errors.push({
|
|
1119
|
+
line: line.no,
|
|
1120
|
+
message: "段の見出しの寸法が読めません",
|
|
1121
|
+
hint: "{ leftPad: 26, topPad: 20, numberSize: 16, gap: 10, nameSize: 29, bottomPad: 12 } の形で書く",
|
|
1122
|
+
});
|
|
1123
|
+
} else {
|
|
1124
|
+
stageHeader = values as DslStageHeader;
|
|
1125
|
+
}
|
|
1126
|
+
i += 1;
|
|
1127
|
+
continue;
|
|
1128
|
+
}
|
|
615
1129
|
if (head.key === "eyebrow") {
|
|
616
1130
|
// 空で書いた形 (`eyebrow:`) は「書かなかった」 と同じにする。 空文字を残すと
|
|
617
1131
|
// 描画側が中身のない帯を出す
|
|
@@ -621,13 +1135,392 @@ export function parseTextDslV05(src: string): V05ParseResult {
|
|
|
621
1135
|
i += 1;
|
|
622
1136
|
continue;
|
|
623
1137
|
}
|
|
1138
|
+
if (head.key === "shape") {
|
|
1139
|
+
/*
|
|
1140
|
+
* 数を描く図の形 (#2657)。
|
|
1141
|
+
*
|
|
1142
|
+
* 並べ替えの語と同じ形にする。 **書いた行を覚える** = 効かない図種に書いた時の
|
|
1143
|
+
* 知らせが場所を指せる。
|
|
1144
|
+
*/
|
|
1145
|
+
const v = (head.value ?? "").trim();
|
|
1146
|
+
if (v.length > 0) {
|
|
1147
|
+
shapeWritten = { value: v, line: line.no };
|
|
1148
|
+
}
|
|
1149
|
+
i += 1;
|
|
1150
|
+
continue;
|
|
1151
|
+
}
|
|
1152
|
+
if (head.key === "form") {
|
|
1153
|
+
const v = stripQuotes((head.value ?? "").trim());
|
|
1154
|
+
if (v.length > 0) formWritten = { value: v, line: line.no };
|
|
1155
|
+
i += 1;
|
|
1156
|
+
continue;
|
|
1157
|
+
}
|
|
1158
|
+
if (head.key === "ticks") {
|
|
1159
|
+
const raw = (head.value ?? "").trim();
|
|
1160
|
+
if (!raw.startsWith("[") || !raw.endsWith("]")) {
|
|
1161
|
+
errors.push({
|
|
1162
|
+
line: line.no,
|
|
1163
|
+
message: `ticks の書き方が読めません: "${raw}"`,
|
|
1164
|
+
hint: "`ticks: [6月, 7月, 8月]` の形で書く",
|
|
1165
|
+
});
|
|
1166
|
+
} else {
|
|
1167
|
+
const values = splitTopLevelCommas(raw.slice(1, -1))
|
|
1168
|
+
.map((value) => stripQuotes(value.trim()))
|
|
1169
|
+
.filter((value) => value.length > 0);
|
|
1170
|
+
if (values.length === 0) {
|
|
1171
|
+
errors.push({
|
|
1172
|
+
line: line.no,
|
|
1173
|
+
message: "ticks に目盛りがありません",
|
|
1174
|
+
hint: "`ticks: [6月, 7月, 8月]` の形で 1 つ以上書く",
|
|
1175
|
+
});
|
|
1176
|
+
} else {
|
|
1177
|
+
ticks = values;
|
|
1178
|
+
ticksLine = line.no;
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
i += 1;
|
|
1182
|
+
continue;
|
|
1183
|
+
}
|
|
1184
|
+
if (head.key === "ganttToday") {
|
|
1185
|
+
const raw = (head.value ?? "").trim();
|
|
1186
|
+
if (raw.startsWith("{") && raw.endsWith("}")) {
|
|
1187
|
+
中括弧の知らない項目名を知らせる(
|
|
1188
|
+
raw.slice(1, -1),
|
|
1189
|
+
["index", "label"],
|
|
1190
|
+
"ganttToday の ",
|
|
1191
|
+
line.no,
|
|
1192
|
+
errors,
|
|
1193
|
+
);
|
|
1194
|
+
}
|
|
1195
|
+
const inner = raw.startsWith("{") && raw.endsWith("}")
|
|
1196
|
+
? parseInlineMapping(raw.slice(1, -1))
|
|
1197
|
+
: {};
|
|
1198
|
+
const index = Number(inner.index);
|
|
1199
|
+
const label = inner.label === undefined ? undefined : stripQuotes(inner.label);
|
|
1200
|
+
if (!Number.isFinite(index) || label === undefined) {
|
|
1201
|
+
errors.push({
|
|
1202
|
+
line: line.no,
|
|
1203
|
+
message: `ganttToday の書き方が読めません: "${raw}"`,
|
|
1204
|
+
hint: '`ganttToday: { index: 3.3, label: "今日" }` の形で書く',
|
|
1205
|
+
});
|
|
1206
|
+
} else {
|
|
1207
|
+
ganttToday = { index, label };
|
|
1208
|
+
}
|
|
1209
|
+
i += 1;
|
|
1210
|
+
continue;
|
|
1211
|
+
}
|
|
1212
|
+
if (head.key === "ganttTickLabels") {
|
|
1213
|
+
const raw = (head.value ?? "").trim();
|
|
1214
|
+
const values = raw.startsWith("[") && raw.endsWith("]")
|
|
1215
|
+
? splitTopLevelCommas(raw.slice(1, -1)).map((value) => stripQuotes(value.trim()))
|
|
1216
|
+
: [];
|
|
1217
|
+
if (values.length === 0 || values.some((value) => value.length === 0)) {
|
|
1218
|
+
errors.push({
|
|
1219
|
+
line: line.no,
|
|
1220
|
+
message: `ganttTickLabels の書き方が読めません: "${raw}"`,
|
|
1221
|
+
hint: "`ganttTickLabels: [6月, 7月, 8月]` の形で 1 つ以上書く",
|
|
1222
|
+
});
|
|
1223
|
+
} else {
|
|
1224
|
+
ganttTickLabels = values;
|
|
1225
|
+
}
|
|
1226
|
+
i += 1;
|
|
1227
|
+
continue;
|
|
1228
|
+
}
|
|
1229
|
+
if (head.key === "ganttBarEnd") {
|
|
1230
|
+
const value = stripQuotes((head.value ?? "").trim());
|
|
1231
|
+
if (value === "position") ganttBarEnd = value;
|
|
1232
|
+
else errors.push({ line: line.no, message: `ganttBarEnd が読めません: "${value}"`, hint: "使える値 = position" });
|
|
1233
|
+
i += 1;
|
|
1234
|
+
continue;
|
|
1235
|
+
}
|
|
1236
|
+
if (head.key === "ganttBarThickness") {
|
|
1237
|
+
const value = stripQuotes((head.value ?? "").trim());
|
|
1238
|
+
if (value === "thin") ganttBarThickness = value;
|
|
1239
|
+
else errors.push({ line: line.no, message: `ganttBarThickness が読めません: "${value}"`, hint: "使える値 = thin" });
|
|
1240
|
+
i += 1;
|
|
1241
|
+
continue;
|
|
1242
|
+
}
|
|
1243
|
+
if (head.key === "chartSeriesSkipMuted") {
|
|
1244
|
+
const value = stripQuotes((head.value ?? "").trim());
|
|
1245
|
+
if (value === "true") chartSeriesSkipMuted = true;
|
|
1246
|
+
else errors.push({ line: line.no, message: `chartSeriesSkipMuted が読めません: "${value}"`, hint: "使える値 = true" });
|
|
1247
|
+
i += 1;
|
|
1248
|
+
continue;
|
|
1249
|
+
}
|
|
1250
|
+
if (head.key === "chartLineSeries") {
|
|
1251
|
+
const value = JSONの欄を読む(head.value, "chartLineSeries", line.no, errors);
|
|
1252
|
+
const unknown = chartLineSeriesの知らない項目を知らせる(value, line.no, errors);
|
|
1253
|
+
if (!unknown && chartLineSeriesの形(value)) chartLineSeries = value;
|
|
1254
|
+
else if (value !== undefined) errors.push({ line: line.no, message: "chartLineSeries の系列または点の形が読めません" });
|
|
1255
|
+
i += 1;
|
|
1256
|
+
continue;
|
|
1257
|
+
}
|
|
1258
|
+
if (head.key === "chartStackedPeriods") {
|
|
1259
|
+
const value = JSONの欄を読む(head.value, "chartStackedPeriods", line.no, errors);
|
|
1260
|
+
const unknown = chartStackedPeriodsの知らない項目を知らせる(value, line.no, errors);
|
|
1261
|
+
if (!unknown && chartStackedPeriodsの形(value)) chartStackedPeriods = value;
|
|
1262
|
+
else if (value !== undefined) errors.push({ line: line.no, message: "chartStackedPeriods の期間または値の形が読めません" });
|
|
1263
|
+
i += 1;
|
|
1264
|
+
continue;
|
|
1265
|
+
}
|
|
1266
|
+
if (head.key === "chartGaugeValue") {
|
|
1267
|
+
const value = JSONの欄を読む(head.value, "chartGaugeValue", line.no, errors);
|
|
1268
|
+
const unknown = chartGaugeValueの知らない項目を知らせる(value, line.no, errors);
|
|
1269
|
+
if (!unknown && chartGaugeValueの形(value)) chartGaugeValue = value;
|
|
1270
|
+
else if (value !== undefined) errors.push({ line: line.no, message: "chartGaugeValue の値の形が読めません" });
|
|
1271
|
+
i += 1;
|
|
1272
|
+
continue;
|
|
1273
|
+
}
|
|
1274
|
+
if (head.key === "chartSlopePeriods" || head.key === "chartSlopeEmphasisIds") {
|
|
1275
|
+
const raw = (head.value ?? "").trim();
|
|
1276
|
+
const values = raw.startsWith("[") && raw.endsWith("]")
|
|
1277
|
+
? splitTopLevelCommas(raw.slice(1, -1)).map((value) => stripQuotes(value.trim()))
|
|
1278
|
+
: [];
|
|
1279
|
+
if (values.length === 0 || values.some((value) => value.length === 0)) {
|
|
1280
|
+
errors.push({ line: line.no, message: `${head.key} の書き方が読めません`, hint: `${head.key}: ["先月", "今月"] の形で書く` });
|
|
1281
|
+
} else if (head.key === "chartSlopePeriods") {
|
|
1282
|
+
if (values.length === 2) chartSlopePeriods = [values[0]!, values[1]!];
|
|
1283
|
+
else errors.push({ line: line.no, message: "chartSlopePeriods は時点名を 2 つ書く" });
|
|
1284
|
+
} else chartSlopeEmphasisIds = values;
|
|
1285
|
+
i += 1;
|
|
1286
|
+
continue;
|
|
1287
|
+
}
|
|
1288
|
+
if (head.key === "chartPieCenterLabel" || head.key === "chartSlopeUnit" || head.key === "chartStackedRateId") {
|
|
1289
|
+
const value = stripQuotes((head.value ?? "").trim());
|
|
1290
|
+
if (head.key === "chartPieCenterLabel") chartPieCenterLabel = value;
|
|
1291
|
+
else if (head.key === "chartSlopeUnit") chartSlopeUnit = value;
|
|
1292
|
+
else chartStackedRateId = value;
|
|
1293
|
+
i += 1;
|
|
1294
|
+
continue;
|
|
1295
|
+
}
|
|
1296
|
+
if (head.key === "chartPieTableColumns") {
|
|
1297
|
+
const value = stripQuotes((head.value ?? "").trim());
|
|
1298
|
+
if ((CHART_PIE_TABLE_COLUMN_VALUES as readonly string[]).includes(value)) chartPieTableColumns = value as typeof chartPieTableColumns;
|
|
1299
|
+
else errors.push({ line: line.no, message: `chartPieTableColumns が読めません: "${value}"`, hint: `使える値 = ${CHART_PIE_TABLE_COLUMN_VALUES.join(", ")}` });
|
|
1300
|
+
i += 1;
|
|
1301
|
+
continue;
|
|
1302
|
+
}
|
|
1303
|
+
if (head.key === "chartPieRingWidth") {
|
|
1304
|
+
const value = stripQuotes((head.value ?? "").trim());
|
|
1305
|
+
if ((CHART_PIE_RING_WIDTH_VALUES as readonly string[]).includes(value)) chartPieRingWidth = value as typeof chartPieRingWidth;
|
|
1306
|
+
else errors.push({ line: line.no, message: `chartPieRingWidth が読めません: "${value}"`, hint: `使える値 = ${CHART_PIE_RING_WIDTH_VALUES.join(", ")}` });
|
|
1307
|
+
i += 1;
|
|
1308
|
+
continue;
|
|
1309
|
+
}
|
|
1310
|
+
if (head.key === "chartStackedLegendPosition") {
|
|
1311
|
+
const value = stripQuotes((head.value ?? "").trim());
|
|
1312
|
+
if ((CHART_STACKED_LEGEND_POSITION_VALUES as readonly string[]).includes(value)) chartStackedLegendPosition = value as typeof chartStackedLegendPosition;
|
|
1313
|
+
else errors.push({ line: line.no, message: `chartStackedLegendPosition が読めません: "${value}"`, hint: `使える値 = ${CHART_STACKED_LEGEND_POSITION_VALUES.join(", ")}` });
|
|
1314
|
+
i += 1;
|
|
1315
|
+
continue;
|
|
1316
|
+
}
|
|
1317
|
+
if (
|
|
1318
|
+
head.key === "sequenceActorForm" || head.key === "sequenceLabelForm" ||
|
|
1319
|
+
head.key === "sequenceReturnHead"
|
|
1320
|
+
) {
|
|
1321
|
+
const value = stripQuotes((head.value ?? "").trim());
|
|
1322
|
+
const values = head.key === "sequenceActorForm"
|
|
1323
|
+
? SEQUENCE_ACTOR_FORM_VALUES
|
|
1324
|
+
: head.key === "sequenceLabelForm" ? SEQUENCE_LABEL_FORM_VALUES : SEQUENCE_RETURN_HEAD_VALUES;
|
|
1325
|
+
if ((values as readonly string[]).includes(value)) {
|
|
1326
|
+
if (head.key === "sequenceActorForm") sequenceActorForm = value as "box";
|
|
1327
|
+
else if (head.key === "sequenceLabelForm") sequenceLabelForm = value as "kind";
|
|
1328
|
+
else sequenceReturnHead = value as "solid";
|
|
1329
|
+
} else errors.push({ line: line.no, message: `${head.key} が読めません: "${value}"`, hint: `使える値 = ${values.join(", ")}` });
|
|
1330
|
+
i += 1;
|
|
1331
|
+
continue;
|
|
1332
|
+
}
|
|
1333
|
+
if (head.key === "sequenceKindLabels") {
|
|
1334
|
+
const value = JSONの欄を読む(head.value, head.key, line.no, errors);
|
|
1335
|
+
const unknown = 図表の知らない項目を知らせる(value, SEQUENCE_KIND_LABEL_KEYS, head.key, line.no, errors);
|
|
1336
|
+
if (!unknown && objectか(value) && Object.values(value).every((label) => typeof label === "string")) {
|
|
1337
|
+
sequenceKindLabels = value;
|
|
1338
|
+
} else if (value !== undefined && !unknown) errors.push({ line: line.no, message: "sequenceKindLabels の値の形が読めません" });
|
|
1339
|
+
i += 1;
|
|
1340
|
+
continue;
|
|
1341
|
+
}
|
|
1342
|
+
if (head.key === "sequenceRowGap" || head.key === "sequenceGutter" || head.key === "sequenceBandWidth") {
|
|
1343
|
+
const value = Number(stripQuotes((head.value ?? "").trim()));
|
|
1344
|
+
if (Number.isFinite(value) && value > 0) {
|
|
1345
|
+
if (head.key === "sequenceRowGap") sequenceRowGap = value;
|
|
1346
|
+
else if (head.key === "sequenceGutter") sequenceGutter = value;
|
|
1347
|
+
else sequenceBandWidth = value;
|
|
1348
|
+
} else errors.push({ line: line.no, message: `${head.key} が読めません: "${head.value ?? ""}"`, hint: "正の数で書く" });
|
|
1349
|
+
i += 1;
|
|
1350
|
+
continue;
|
|
1351
|
+
}
|
|
1352
|
+
if (head.key === "sequenceSelfLoop") {
|
|
1353
|
+
const value = JSONの欄を読む(head.value, head.key, line.no, errors);
|
|
1354
|
+
const unknown = 図表の知らない項目を知らせる(value, SEQUENCE_SELF_LOOP_KEYS, head.key, line.no, errors);
|
|
1355
|
+
if (!unknown && objectか(value)) {
|
|
1356
|
+
const form = value.form;
|
|
1357
|
+
const width = value.width;
|
|
1358
|
+
const drop = value.drop;
|
|
1359
|
+
const validForm =
|
|
1360
|
+
form === undefined ||
|
|
1361
|
+
(SEQUENCE_SELF_LOOP_FORM_VALUES as readonly unknown[]).includes(form);
|
|
1362
|
+
const validWidth = width === undefined || (typeof width === "number" && Number.isFinite(width) && width > 0);
|
|
1363
|
+
const validDrop = drop === undefined || (typeof drop === "number" && Number.isFinite(drop) && drop > 0);
|
|
1364
|
+
if (validForm && validWidth && validDrop) sequenceSelfLoop = value;
|
|
1365
|
+
else errors.push({ line: line.no, message: "sequenceSelfLoop の値の形が読めません", hint: '`{"form":"orthogonal","width":127,"drop":46}` の形で書く' });
|
|
1366
|
+
} else if (value !== undefined && !unknown) errors.push({ line: line.no, message: "sequenceSelfLoop の値の形が読めません" });
|
|
1367
|
+
i += 1;
|
|
1368
|
+
continue;
|
|
1369
|
+
}
|
|
1370
|
+
if (head.key === "chartWaffleLegendPosition") {
|
|
1371
|
+
const value = stripQuotes((head.value ?? "").trim());
|
|
1372
|
+
if ((CHART_WAFFLE_LEGEND_POSITION_VALUES as readonly string[]).includes(value)) chartWaffleLegendPosition = value as typeof chartWaffleLegendPosition;
|
|
1373
|
+
else errors.push({ line: line.no, message: `chartWaffleLegendPosition が読めません: "${value}"`, hint: `使える値 = ${CHART_WAFFLE_LEGEND_POSITION_VALUES.join(", ")}` });
|
|
1374
|
+
i += 1;
|
|
1375
|
+
continue;
|
|
1376
|
+
}
|
|
1377
|
+
if (head.key === "funnelForm") {
|
|
1378
|
+
const value = stripQuotes((head.value ?? "").trim());
|
|
1379
|
+
if ((FUNNEL_FORM_VALUES as readonly string[]).includes(value)) funnelForm = value as typeof funnelForm;
|
|
1380
|
+
else errors.push({ line: line.no, message: `funnelForm が読めません: "${value}"`, hint: `使える値 = ${FUNNEL_FORM_VALUES.join(", ")}` });
|
|
1381
|
+
i += 1;
|
|
1382
|
+
continue;
|
|
1383
|
+
}
|
|
1384
|
+
if (head.key === "funnelRate") {
|
|
1385
|
+
const value = stripQuotes((head.value ?? "").trim());
|
|
1386
|
+
if ((FUNNEL_RATE_VALUES as readonly string[]).includes(value)) funnelRate = value as typeof funnelRate;
|
|
1387
|
+
else errors.push({ line: line.no, message: `funnelRate が読めません: "${value}"`, hint: `使える値 = ${FUNNEL_RATE_VALUES.join(", ")}` });
|
|
1388
|
+
i += 1;
|
|
1389
|
+
continue;
|
|
1390
|
+
}
|
|
1391
|
+
if (head.key === "quadrantPointLabelSide") {
|
|
1392
|
+
const value = stripQuotes((head.value ?? "").trim());
|
|
1393
|
+
if ((QUADRANT_POINT_LABEL_SIDE_VALUES as readonly string[]).includes(value)) quadrantPointLabelSide = value as typeof quadrantPointLabelSide;
|
|
1394
|
+
else errors.push({ line: line.no, message: `quadrantPointLabelSide が読めません: "${value}"`, hint: `使える値 = ${QUADRANT_POINT_LABEL_SIDE_VALUES.join(", ")}` });
|
|
1395
|
+
i += 1;
|
|
1396
|
+
continue;
|
|
1397
|
+
}
|
|
1398
|
+
if (
|
|
1399
|
+
head.key === "treeNodeForm" || head.key === "treeEdgeTone" || head.key === "treeEdgeHead" ||
|
|
1400
|
+
head.key === "mindForm" || head.key === "journeyForm" || head.key === "journeyLineForm"
|
|
1401
|
+
) {
|
|
1402
|
+
const value = stripQuotes((head.value ?? "").trim());
|
|
1403
|
+
const values = {
|
|
1404
|
+
treeNodeForm: TREE_NODE_FORM_VALUES,
|
|
1405
|
+
treeEdgeTone: TREE_EDGE_TONE_VALUES,
|
|
1406
|
+
treeEdgeHead: TREE_EDGE_HEAD_VALUES,
|
|
1407
|
+
mindForm: MIND_FORM_VALUES,
|
|
1408
|
+
journeyForm: JOURNEY_FORM_VALUES,
|
|
1409
|
+
journeyLineForm: JOURNEY_LINE_FORM_VALUES,
|
|
1410
|
+
}[head.key];
|
|
1411
|
+
if ((values as readonly string[]).includes(value)) {
|
|
1412
|
+
if (head.key === "treeNodeForm") treeNodeForm = value as typeof treeNodeForm;
|
|
1413
|
+
else if (head.key === "treeEdgeTone") treeEdgeTone = value as typeof treeEdgeTone;
|
|
1414
|
+
else if (head.key === "treeEdgeHead") treeEdgeHead = value as typeof treeEdgeHead;
|
|
1415
|
+
else if (head.key === "mindForm") mindForm = value as typeof mindForm;
|
|
1416
|
+
else if (head.key === "journeyForm") journeyForm = value as typeof journeyForm;
|
|
1417
|
+
else journeyLineForm = value as typeof journeyLineForm;
|
|
1418
|
+
} else {
|
|
1419
|
+
errors.push({ line: line.no, message: `${head.key} が読めません: "${value}"`, hint: `使える値 = ${values.join(", ")}` });
|
|
1420
|
+
}
|
|
1421
|
+
i += 1;
|
|
1422
|
+
continue;
|
|
1423
|
+
}
|
|
1424
|
+
if (head.key === "treeSize" || head.key === "mindSize") {
|
|
1425
|
+
const value = JSONの欄を読む(head.value, head.key, line.no, errors);
|
|
1426
|
+
if (value !== undefined) {
|
|
1427
|
+
if (head.key === "treeSize" && 木の寸法を読む(value, line.no, errors)) treeSize = value;
|
|
1428
|
+
if (head.key === "mindSize" && 放射の寸法を読む(value, line.no, errors)) mindSize = value;
|
|
1429
|
+
}
|
|
1430
|
+
i += 1;
|
|
1431
|
+
continue;
|
|
1432
|
+
}
|
|
1433
|
+
if (head.key === "cornerForm" || head.key === "quadrantLabelPosition" || head.key === "chartBarShape") {
|
|
1434
|
+
const value = stripQuotes((head.value ?? "").trim());
|
|
1435
|
+
const values = head.key === "cornerForm"
|
|
1436
|
+
? CORNER_FORM_VALUES
|
|
1437
|
+
: head.key === "chartBarShape" ? CHART_BAR_SHAPE_VALUES : QUADRANT_LABEL_POSITION_VALUES;
|
|
1438
|
+
if ((values as readonly string[]).includes(value)) {
|
|
1439
|
+
if (head.key === "cornerForm") cornerForm = value as typeof cornerForm;
|
|
1440
|
+
else if (head.key === "chartBarShape") chartBarShape = value as typeof chartBarShape;
|
|
1441
|
+
else quadrantLabelPosition = value as typeof quadrantLabelPosition;
|
|
1442
|
+
} else errors.push({ line: line.no, message: `${head.key} が読めません: "${value}"`, hint: `使える値 = ${values.join(", ")}` });
|
|
1443
|
+
i += 1;
|
|
1444
|
+
continue;
|
|
1445
|
+
}
|
|
1446
|
+
if (head.key === "stageLayout") {
|
|
1447
|
+
const value = JSONの欄を読む(head.value, head.key, line.no, errors);
|
|
1448
|
+
if (value !== undefined && cdl0132の欄を読む(head.key, value, line.no, errors)) {
|
|
1449
|
+
stageLayout = value as NonNullable<typeof stageLayout>;
|
|
1450
|
+
}
|
|
1451
|
+
i += 1;
|
|
1452
|
+
continue;
|
|
1453
|
+
}
|
|
1454
|
+
if ((CDL_0132_OBJECT_FIELDS as readonly string[]).includes(head.key)) {
|
|
1455
|
+
const value = JSONの欄を読む(head.value, head.key, line.no, errors);
|
|
1456
|
+
if (value !== undefined && cdl0132の欄を読む(head.key as Cdl0132ObjectField, value, line.no, errors)) {
|
|
1457
|
+
if (head.key === "terminalDecoration") terminalDecoration = value as NonNullable<typeof terminalDecoration>;
|
|
1458
|
+
else if (head.key === "figureKindLabel") figureKindLabel = value as NonNullable<typeof figureKindLabel>;
|
|
1459
|
+
else if (head.key === "figureInset") figureInset = value as NonNullable<typeof figureInset>;
|
|
1460
|
+
else if (head.key === "journeySize") journeySize = value as NonNullable<typeof journeySize>;
|
|
1461
|
+
else if (head.key === "chartBarSize") chartBarSize = value as NonNullable<typeof chartBarSize>;
|
|
1462
|
+
else if (head.key === "chartLineSize") chartLineSize = value as NonNullable<typeof chartLineSize>;
|
|
1463
|
+
else if (head.key === "chartPieSize") chartPieSize = value as NonNullable<typeof chartPieSize>;
|
|
1464
|
+
else if (head.key === "chartWaffleSize") chartWaffleSize = value as NonNullable<typeof chartWaffleSize>;
|
|
1465
|
+
else if (head.key === "chartSlopeLabelTemplate") chartSlopeLabelTemplate = value as NonNullable<typeof chartSlopeLabelTemplate>;
|
|
1466
|
+
else if (head.key === "chartSlopeSize") chartSlopeSize = value as NonNullable<typeof chartSlopeSize>;
|
|
1467
|
+
else if (head.key === "chartStackedLayout") chartStackedLayout = value as NonNullable<typeof chartStackedLayout>;
|
|
1468
|
+
else if (head.key === "chartGaugeSize") chartGaugeSize = value as NonNullable<typeof chartGaugeSize>;
|
|
1469
|
+
else if (head.key === "ganttSize") ganttSize = value as NonNullable<typeof ganttSize>;
|
|
1470
|
+
else if (head.key === "quadrantSize") quadrantSize = value as NonNullable<typeof quadrantSize>;
|
|
1471
|
+
else if (head.key === "legendOffset") legendOffset = value as NonNullable<typeof legendOffset>;
|
|
1472
|
+
else stageLayout = value as NonNullable<typeof stageLayout>;
|
|
1473
|
+
}
|
|
1474
|
+
i += 1;
|
|
1475
|
+
continue;
|
|
1476
|
+
}
|
|
1477
|
+
if (head.key === "journeyLabels" || head.key === "figureCard" || head.key === "figureSize") {
|
|
1478
|
+
const value = JSONの欄を読む(head.value, head.key, line.no, errors);
|
|
1479
|
+
const keys = head.key === "journeyLabels"
|
|
1480
|
+
? JOURNEY_LABEL_KEYS
|
|
1481
|
+
: head.key === "figureCard" ? (["label", "note"] as const) : (["width", "height"] as const);
|
|
1482
|
+
const unknown = 図表の知らない項目を知らせる(value, keys, head.key, line.no, errors);
|
|
1483
|
+
if (!unknown && objectか(value)) {
|
|
1484
|
+
if (
|
|
1485
|
+
head.key === "journeyLabels" &&
|
|
1486
|
+
JOURNEY_LABEL_KEYS.every((key) => typeof value[key] === "string" && value[key].trim() !== "")
|
|
1487
|
+
) journeyLabels = value as typeof journeyLabels;
|
|
1488
|
+
else if (
|
|
1489
|
+
head.key === "figureCard" && typeof value.label === "string" &&
|
|
1490
|
+
(value.note === undefined || typeof value.note === "string")
|
|
1491
|
+
) figureCard = value as typeof figureCard;
|
|
1492
|
+
else if (
|
|
1493
|
+
head.key === "figureSize" && typeof value.width === "number" && Number.isFinite(value.width) && value.width > 0 &&
|
|
1494
|
+
typeof value.height === "number" && Number.isFinite(value.height) && value.height > 0
|
|
1495
|
+
) figureSize = value as typeof figureSize;
|
|
1496
|
+
else errors.push({ line: line.no, message: `${head.key} の値の形が読めません` });
|
|
1497
|
+
} else if (value !== undefined && !unknown) {
|
|
1498
|
+
errors.push({ line: line.no, message: `${head.key} は object で書きます` });
|
|
1499
|
+
}
|
|
1500
|
+
i += 1;
|
|
1501
|
+
continue;
|
|
1502
|
+
}
|
|
624
1503
|
if (head.key === "type") {
|
|
625
1504
|
const v = (head.value ?? "").trim().toLowerCase();
|
|
626
|
-
|
|
1505
|
+
// **別名を先に引く** (#2655)。 引けたら図種と並び順の 2 つに開いて、以降は
|
|
1506
|
+
// その語が 1 か所も残らないようにする
|
|
1507
|
+
const 別名 = TYPE_ALIASES.get(v);
|
|
1508
|
+
if (別名 !== undefined) {
|
|
1509
|
+
type = 別名.type;
|
|
1510
|
+
// 書き手が語も書いていればそちらが勝つ = 別名は既定を入れるだけ
|
|
1511
|
+
if (order === null && 別名.order !== undefined) {
|
|
1512
|
+
order = 別名.order;
|
|
1513
|
+
orderLine = line.no;
|
|
1514
|
+
}
|
|
1515
|
+
if (別名.shape !== undefined) {
|
|
1516
|
+
aliasShape = 別名.shape;
|
|
1517
|
+
aliasShapeLine = line.no;
|
|
1518
|
+
}
|
|
1519
|
+
} else if (!PRESET_TYPES.has(v as PresetType)) {
|
|
627
1520
|
errors.push({
|
|
628
1521
|
line: line.no,
|
|
629
|
-
message:
|
|
630
|
-
hint:
|
|
1522
|
+
message: `図種が読めません: "${v}"`,
|
|
1523
|
+
hint: `使える図種 = ${[...PRESET_TYPES, ...TYPE_ALIASES.keys()].join(", ")}`,
|
|
631
1524
|
});
|
|
632
1525
|
} else {
|
|
633
1526
|
type = v as PresetType;
|
|
@@ -637,15 +1530,23 @@ export function parseTextDslV05(src: string): V05ParseResult {
|
|
|
637
1530
|
}
|
|
638
1531
|
if (head.key === "actors") {
|
|
639
1532
|
// 1 行で書いた形と、 続く字下げ行に項目を並べた形の両方を受け付ける
|
|
640
|
-
const { items, next } = collectActorEntries(lines, i + 1, line.indent);
|
|
1533
|
+
const { items, 届かない, next } = collectActorEntries(lines, i + 1, line.indent);
|
|
1534
|
+
// 1 件の頭にも続きにもならなかった行を知らせる (#2400)。 捨てると箱が黙って消える
|
|
1535
|
+
for (const ln of 届かない) {
|
|
1536
|
+
errors.push({
|
|
1537
|
+
line: ln.no,
|
|
1538
|
+
message: `登場人物の行が読めません: "${ln.trimmed}"`,
|
|
1539
|
+
hint: "`- Client` の形で書く (先頭の `- ` を落とすと 1 件として読めない)",
|
|
1540
|
+
});
|
|
1541
|
+
}
|
|
641
1542
|
actors = [];
|
|
642
1543
|
for (const entry of items) {
|
|
643
1544
|
const base = parseActor(entry[0]!, errors);
|
|
644
1545
|
if (base === null) {
|
|
645
1546
|
errors.push({
|
|
646
1547
|
line: entry[0]!.no,
|
|
647
|
-
message:
|
|
648
|
-
hint: "
|
|
1548
|
+
message: `登場人物の行が読めません: "${entry[0]!.trimmed}"`,
|
|
1549
|
+
hint: "`- Client` または `- Client: storage` の形で書く",
|
|
649
1550
|
});
|
|
650
1551
|
continue;
|
|
651
1552
|
}
|
|
@@ -666,8 +1567,8 @@ export function parseTextDslV05(src: string): V05ParseResult {
|
|
|
666
1567
|
} else {
|
|
667
1568
|
errors.push({
|
|
668
1569
|
line: it.no,
|
|
669
|
-
message:
|
|
670
|
-
hint: '
|
|
1570
|
+
message: `矢印の行が読めません: "${it.trimmed}"`,
|
|
1571
|
+
hint: '`- A -> B: "label"` または `- A -> B: "label" (success)` の形で書く',
|
|
671
1572
|
});
|
|
672
1573
|
}
|
|
673
1574
|
}
|
|
@@ -681,7 +1582,7 @@ export function parseTextDslV05(src: string): V05ParseResult {
|
|
|
681
1582
|
const inner = inlineMatch.slice(1, -1).trim();
|
|
682
1583
|
animate = ensureAnimate(animate, line.no);
|
|
683
1584
|
for (const pair of splitTopLevelCommas(inner)) {
|
|
684
|
-
const st = parseStateEntry(pair, line.no);
|
|
1585
|
+
const st = parseStateEntry(pair, line.no, errors);
|
|
685
1586
|
if (st) animate.states.push(st);
|
|
686
1587
|
}
|
|
687
1588
|
i += 1;
|
|
@@ -690,14 +1591,10 @@ export function parseTextDslV05(src: string): V05ParseResult {
|
|
|
690
1591
|
const { items, next } = collectIndentedList(lines, i + 1, line.indent);
|
|
691
1592
|
animate = ensureAnimate(animate, line.no);
|
|
692
1593
|
for (const it of items) {
|
|
693
|
-
|
|
1594
|
+
// 断り方ごとの文は読み手が持つ (#2401)。 ここで `null` をまとめて叱ると、
|
|
1595
|
+
// 名前の規則を外れただけの行に形の案内が返る
|
|
1596
|
+
const st = parseStateEntry(it.trimmed.replace(/^-\s*/, ""), it.no, errors);
|
|
694
1597
|
if (st) animate.states.push(st);
|
|
695
|
-
else
|
|
696
|
-
errors.push({
|
|
697
|
-
line: it.no,
|
|
698
|
-
message: `invalid state entry: "${it.trimmed}"`,
|
|
699
|
-
hint: "use `name: initial`",
|
|
700
|
-
});
|
|
701
1598
|
}
|
|
702
1599
|
i = next;
|
|
703
1600
|
continue;
|
|
@@ -707,8 +1604,8 @@ export function parseTextDslV05(src: string): V05ParseResult {
|
|
|
707
1604
|
// 入る (`min({a}, {b})`) ため、 `states` が使う素朴な `,` 分割では式が壊れる。
|
|
708
1605
|
//
|
|
709
1606
|
// **`{` で始まるかを見てはいけない** (#1169)。 `values: a: "{b} + 1"` は `{` で始まらない
|
|
710
|
-
// ため判定を通り抜け、 その後 `
|
|
711
|
-
// 1 件も読まれずに黙って消える**。 書き間違いを黙って捨てないという `
|
|
1607
|
+
// ため判定を通り抜け、 その後 `collectIndentedList` が次行以降しか見ないので **値が
|
|
1608
|
+
// 1 件も読まれずに黙って消える**。 書き間違いを黙って捨てないという `collectIndentedList`
|
|
712
1609
|
// を自前で持った理由と矛盾する。
|
|
713
1610
|
//
|
|
714
1611
|
// 同じ行に何か書いてあれば形を問わず弾く = 1 行形は全て受けないという規則そのもの。
|
|
@@ -722,9 +1619,9 @@ export function parseTextDslV05(src: string): V05ParseResult {
|
|
|
722
1619
|
i += 1;
|
|
723
1620
|
continue;
|
|
724
1621
|
}
|
|
725
|
-
//
|
|
726
|
-
//
|
|
727
|
-
const { items, next } =
|
|
1622
|
+
// 字下げした行を 1 行も落とさずに集める。 捨てられると書き間違えた行が
|
|
1623
|
+
// 「書かなかった」 と同じになり、 値が 1 つ消えたことに気付けない
|
|
1624
|
+
const { items, next } = collectIndentedList(lines, i + 1, line.indent);
|
|
728
1625
|
for (const it of items) {
|
|
729
1626
|
const v = parseValueEntry(it.trimmed.replace(/^-\s*/, ""), it.no, errors);
|
|
730
1627
|
if (v) values.push(v);
|
|
@@ -734,8 +1631,16 @@ export function parseTextDslV05(src: string): V05ParseResult {
|
|
|
734
1631
|
}
|
|
735
1632
|
if (head.key === "animation") {
|
|
736
1633
|
// animation: は step を list で並べる
|
|
737
|
-
const { items: stepBlocks, next } = collectAnimationSteps(lines, i + 1, line.indent);
|
|
1634
|
+
const { items: stepBlocks, 届かない, next } = collectAnimationSteps(lines, i + 1, line.indent);
|
|
738
1635
|
animate = ensureAnimate(animate, line.no);
|
|
1636
|
+
// どの段にも属さなかった行を知らせる (#2400)。 捨てると段が丸ごと黙って消える
|
|
1637
|
+
for (const ln of 届かない) {
|
|
1638
|
+
errors.push({
|
|
1639
|
+
line: ln.no,
|
|
1640
|
+
message: `段の行が読めません: "${ln.trimmed}"`,
|
|
1641
|
+
hint: '`- step: "名前" 1.2s` の形で書く (先頭の `- ` を落とすと段として読めない)',
|
|
1642
|
+
});
|
|
1643
|
+
}
|
|
739
1644
|
for (const block of stepBlocks) {
|
|
740
1645
|
const ph = parsePhase(block, errors);
|
|
741
1646
|
if (ph) animate.phases.push(ph);
|
|
@@ -765,7 +1670,7 @@ export function parseTextDslV05(src: string): V05ParseResult {
|
|
|
765
1670
|
// block: viewport:\n width: 1400\n height: 900\n ...
|
|
766
1671
|
// **`collectIndentedList` を使わない** (#1968)。 あちらは `:` を含まない行を黙って捨てるため、
|
|
767
1672
|
// 書き間違えた行が「書かなかった」 と同じになる (`axes:` と同じ理由)
|
|
768
|
-
const { items, next } =
|
|
1673
|
+
const { items, next } = collectIndentedList(lines, i + 1, line.indent);
|
|
769
1674
|
const opts: Record<string, string> = {};
|
|
770
1675
|
// 知らせは **値を書いた行** を指す (#1306)。 `viewport:` の行を指すと、欄が縦に並ぶ形で
|
|
771
1676
|
// どの行を直せばよいか分からない
|
|
@@ -826,7 +1731,7 @@ export function parseTextDslV05(src: string): V05ParseResult {
|
|
|
826
1731
|
}
|
|
827
1732
|
// **`collectIndentedList` を使わない** (Round 1 の指摘)。 あちらは `:` を含まない行を
|
|
828
1733
|
// 黙って捨てるため、書き間違えた行が「書かなかった」 と同じになる
|
|
829
|
-
const { items, next } =
|
|
1734
|
+
const { items, next } = collectIndentedList(lines, i + 1, line.indent);
|
|
830
1735
|
axesLine = line.no;
|
|
831
1736
|
const 組み立て: DslAxes = {};
|
|
832
1737
|
for (const it of items) {
|
|
@@ -834,8 +1739,8 @@ export function parseTextDslV05(src: string): V05ParseResult {
|
|
|
834
1739
|
if (!m) {
|
|
835
1740
|
errors.push({
|
|
836
1741
|
line: it.no,
|
|
837
|
-
message:
|
|
838
|
-
hint: '
|
|
1742
|
+
message: `軸の行が読めません: "${it.trimmed}"`,
|
|
1743
|
+
hint: '`x: { left: "...", right: "..." }` または `y: { bottom: "...", top: "..." }` の形で書く',
|
|
839
1744
|
});
|
|
840
1745
|
continue;
|
|
841
1746
|
}
|
|
@@ -847,7 +1752,12 @@ export function parseTextDslV05(src: string): V05ParseResult {
|
|
|
847
1752
|
it.no,
|
|
848
1753
|
errors,
|
|
849
1754
|
);
|
|
850
|
-
|
|
1755
|
+
const direction = opts.direction === "true";
|
|
1756
|
+
if (direction && opts.label !== undefined) {
|
|
1757
|
+
const axis = { label: stripQuotes(opts.label), direction: true } as const;
|
|
1758
|
+
if (m[1] === "x") 組み立て.x = axis;
|
|
1759
|
+
else 組み立て.y = axis;
|
|
1760
|
+
} else if (m[1] === "x") 組み立て.x = { left: opts.left, right: opts.right };
|
|
851
1761
|
else 組み立て.y = { bottom: opts.bottom, top: opts.top };
|
|
852
1762
|
}
|
|
853
1763
|
// 1 本も読めなかった形は「書かなかった」 と同じにする。 空の軸を渡すと、
|
|
@@ -856,9 +1766,55 @@ export function parseTextDslV05(src: string): V05ParseResult {
|
|
|
856
1766
|
i = next;
|
|
857
1767
|
continue;
|
|
858
1768
|
}
|
|
1769
|
+
if (head.key === "regions") {
|
|
1770
|
+
// regions:\n 左上: "すぐやる"\n 右上: "計画してやる"
|
|
1771
|
+
//
|
|
1772
|
+
// 1 行にまとめて書く形は受けない (`axes:` と同じ理由)。 受けないなら黙って捨てず、
|
|
1773
|
+
// その場で伝える = 捨てると名前を書いたつもりの本文が既定のまま描かれる
|
|
1774
|
+
if (head.value !== null && head.value.trim() !== "") {
|
|
1775
|
+
errors.push({
|
|
1776
|
+
line: line.no,
|
|
1777
|
+
message: "regions は 1 行にまとめて書けない",
|
|
1778
|
+
hint: '次の行から字下げして `左上: "すぐやる"` の形で並べる',
|
|
1779
|
+
});
|
|
1780
|
+
i += 1;
|
|
1781
|
+
continue;
|
|
1782
|
+
}
|
|
1783
|
+
const { items, next } = collectIndentedList(lines, i + 1, line.indent);
|
|
1784
|
+
const 組み立て: DslRegions = {};
|
|
1785
|
+
for (const it of items) {
|
|
1786
|
+
const m = it.trimmed.match(/^(.+?)\s*:\s*(.+)$/);
|
|
1787
|
+
if (!m) {
|
|
1788
|
+
errors.push({
|
|
1789
|
+
line: it.no,
|
|
1790
|
+
message: `区画の行が読めません: "${it.trimmed}"`,
|
|
1791
|
+
hint: '`左上: "すぐやる"` の形で書く',
|
|
1792
|
+
});
|
|
1793
|
+
continue;
|
|
1794
|
+
}
|
|
1795
|
+
// 区画を指す語は項目の欄と同じ 4 語。 別の言い方を足すと同じ区画を 2 通りで呼ぶ
|
|
1796
|
+
const key = 区画.get((m[1] ?? "").trim());
|
|
1797
|
+
if (key === undefined) {
|
|
1798
|
+
// 知らせの形は他の欄と揃える (#1968)。 揃えないと、知らない項目名を
|
|
1799
|
+
// 走査する検査がこの欄だけ見つけられない
|
|
1800
|
+
errors.push({
|
|
1801
|
+
line: it.no,
|
|
1802
|
+
message: `regions の項目名が読めません: "${(m[1] ?? "").trim()}"`,
|
|
1803
|
+
hint: `使える項目 = ${[...区画.keys()].join(", ")}`,
|
|
1804
|
+
});
|
|
1805
|
+
continue;
|
|
1806
|
+
}
|
|
1807
|
+
組み立て[key] = stripQuotes((m[2] ?? "").trim());
|
|
1808
|
+
}
|
|
1809
|
+
// 1 つも読めなかった形は「書かなかった」 と同じにする (`axes:` と同じ)
|
|
1810
|
+
regions = Object.keys(組み立て).length > 0 ? 組み立て : undefined;
|
|
1811
|
+
i = next;
|
|
1812
|
+
continue;
|
|
1813
|
+
}
|
|
859
1814
|
if (head.key === "bands") {
|
|
860
1815
|
/*
|
|
861
|
-
* 動いている間の帯 (#1466)。 `- DB: 1..2`
|
|
1816
|
+
* 動いている間の帯 (#1466)。 `- DB: 1..2` の形で、言づての番号の区間を書く。
|
|
1817
|
+
* 番号は板に載る言づての行で 0 から数える (#2404 で実測、 段ではない)。
|
|
862
1818
|
*
|
|
863
1819
|
* 面ごとに 2 行以上書ける = 途中で手が空く面はそこで切れる。 書かない図は
|
|
864
1820
|
* 組み立て器が「最初に関わった段から最後まで」 の 1 本にする。
|
|
@@ -872,11 +1828,11 @@ export function parseTextDslV05(src: string): V05ParseResult {
|
|
|
872
1828
|
errors.push({
|
|
873
1829
|
line: it.no,
|
|
874
1830
|
message: `帯の書き方が読めません: "${it.trimmed}"`,
|
|
875
|
-
hint: "
|
|
1831
|
+
hint: "`- DB: 1..2` の形で書く (面の名前と、言づての番号の区間。 番号は 0 から数える)",
|
|
876
1832
|
});
|
|
877
1833
|
continue;
|
|
878
1834
|
}
|
|
879
|
-
bands.push({ actor: m[1]!, from: Number(m[2]), to: Number(m[3]) });
|
|
1835
|
+
bands.push({ actor: m[1]!, from: Number(m[2]), to: Number(m[3]), pos: { line: it.no } });
|
|
880
1836
|
}
|
|
881
1837
|
i = next;
|
|
882
1838
|
continue;
|
|
@@ -901,6 +1857,7 @@ export function parseTextDslV05(src: string): V05ParseResult {
|
|
|
901
1857
|
id,
|
|
902
1858
|
...表で読む(LANE_VALUE_KINDS, opts, `縦列 ${id} の `, it.no, errors),
|
|
903
1859
|
label: opts.label,
|
|
1860
|
+
subtitle: opts.subtitle,
|
|
904
1861
|
// 位置のずらし (#1971)
|
|
905
1862
|
layoutPos: ずらしにまとめる(
|
|
906
1863
|
表で読む(OFFSET_VALUE_KINDS, opts, `縦列 ${id} の `, it.no, errors),
|
|
@@ -910,8 +1867,8 @@ export function parseTextDslV05(src: string): V05ParseResult {
|
|
|
910
1867
|
} else {
|
|
911
1868
|
errors.push({
|
|
912
1869
|
line: it.no,
|
|
913
|
-
message:
|
|
914
|
-
hint: '
|
|
1870
|
+
message: `縦列の行が読めません: "${it.trimmed}"`,
|
|
1871
|
+
hint: '`id: { x: 0, width: 320, label: "..." }` の形で書く',
|
|
915
1872
|
});
|
|
916
1873
|
}
|
|
917
1874
|
}
|
|
@@ -932,7 +1889,7 @@ export function parseTextDslV05(src: string): V05ParseResult {
|
|
|
932
1889
|
}
|
|
933
1890
|
// `collectIndentedList` は `:` の無い行を落とす。 部品を綴り違えた行も知らせるため、
|
|
934
1891
|
// 字下げした行を全て読み手へ渡す。
|
|
935
|
-
const { items, next } =
|
|
1892
|
+
const { items, next } = collectIndentedList(lines, i + 1, line.indent);
|
|
936
1893
|
readoutsList = [];
|
|
937
1894
|
for (const it of items) {
|
|
938
1895
|
const m = 名前と中括弧に割る(it.trimmed);
|
|
@@ -942,8 +1899,8 @@ export function parseTextDslV05(src: string): V05ParseResult {
|
|
|
942
1899
|
} else {
|
|
943
1900
|
errors.push({
|
|
944
1901
|
line: it.no,
|
|
945
|
-
message:
|
|
946
|
-
hint: "
|
|
1902
|
+
message: `読み取り値の行が読めません: "${it.trimmed}"`,
|
|
1903
|
+
hint: "`id: { kind: percent-ring, source: total, max: 500 }` の形で書く",
|
|
947
1904
|
});
|
|
948
1905
|
}
|
|
949
1906
|
}
|
|
@@ -964,7 +1921,7 @@ export function parseTextDslV05(src: string): V05ParseResult {
|
|
|
964
1921
|
}
|
|
965
1922
|
// `collectIndentedList` は `:` の無い行を落とす。 つまみを綴り違えた行も知らせるため、
|
|
966
1923
|
// 字下げした行を全て読み手へ渡す。
|
|
967
|
-
const { items, next } =
|
|
1924
|
+
const { items, next } = collectIndentedList(lines, i + 1, line.indent);
|
|
968
1925
|
inputsList = [];
|
|
969
1926
|
for (const it of items) {
|
|
970
1927
|
const m = 名前と中括弧に割る(it.trimmed);
|
|
@@ -974,8 +1931,8 @@ export function parseTextDslV05(src: string): V05ParseResult {
|
|
|
974
1931
|
} else {
|
|
975
1932
|
errors.push({
|
|
976
1933
|
line: it.no,
|
|
977
|
-
message:
|
|
978
|
-
hint: "
|
|
1934
|
+
message: `つまみの行が読めません: "${it.trimmed}"`,
|
|
1935
|
+
hint: "`id: { kind: slider, min: 0, max: 100, defaultValue: 50 }` の形で書く",
|
|
979
1936
|
});
|
|
980
1937
|
}
|
|
981
1938
|
}
|
|
@@ -997,7 +1954,7 @@ export function parseTextDslV05(src: string): V05ParseResult {
|
|
|
997
1954
|
continue;
|
|
998
1955
|
}
|
|
999
1956
|
// 字下げした行を全て読み手へ渡す = 綴りを誤った行も知らせるため
|
|
1000
|
-
const { items, next } =
|
|
1957
|
+
const { items, next } = collectIndentedList(lines, i + 1, line.indent);
|
|
1001
1958
|
formulasList = [];
|
|
1002
1959
|
for (const it of items) {
|
|
1003
1960
|
const f = 式として読む(it.trimmed.replace(/^-\s*/, ""), it.no, errors);
|
|
@@ -1017,7 +1974,7 @@ export function parseTextDslV05(src: string): V05ParseResult {
|
|
|
1017
1974
|
i += 1;
|
|
1018
1975
|
continue;
|
|
1019
1976
|
}
|
|
1020
|
-
const { items, next } =
|
|
1977
|
+
const { items, next } = collectIndentedList(lines, i + 1, line.indent);
|
|
1021
1978
|
eventsList = [];
|
|
1022
1979
|
for (const it of items) {
|
|
1023
1980
|
const e = 出来事として読む(it.trimmed.replace(/^-\s*/, ""), it.no, errors);
|
|
@@ -1037,7 +1994,7 @@ export function parseTextDslV05(src: string): V05ParseResult {
|
|
|
1037
1994
|
i += 1;
|
|
1038
1995
|
continue;
|
|
1039
1996
|
}
|
|
1040
|
-
const { items, next } =
|
|
1997
|
+
const { items, next } = collectIndentedList(lines, i + 1, line.indent);
|
|
1041
1998
|
scrollsList = [];
|
|
1042
1999
|
scrollLines = new Map();
|
|
1043
2000
|
for (const it of items) {
|
|
@@ -1059,8 +2016,8 @@ export function parseTextDslV05(src: string): V05ParseResult {
|
|
|
1059
2016
|
} else {
|
|
1060
2017
|
errors.push({
|
|
1061
2018
|
line: it.no,
|
|
1062
|
-
message:
|
|
1063
|
-
hint: "
|
|
2019
|
+
message: `巻き上げの行が読めません: "${it.trimmed}"`,
|
|
2020
|
+
hint: "`id: { start: 0.9, end: 0.1, scrub: 1 }` の形で書く",
|
|
1064
2021
|
});
|
|
1065
2022
|
}
|
|
1066
2023
|
}
|
|
@@ -1091,8 +2048,8 @@ export function parseTextDslV05(src: string): V05ParseResult {
|
|
|
1091
2048
|
} else {
|
|
1092
2049
|
errors.push({
|
|
1093
2050
|
line: it.no,
|
|
1094
|
-
message:
|
|
1095
|
-
hint: '
|
|
2051
|
+
message: `組の行が読めません: "${it.trimmed}"`,
|
|
2052
|
+
hint: '`id: { label: "...", lanes: [a, b] }` の形で書く',
|
|
1096
2053
|
});
|
|
1097
2054
|
}
|
|
1098
2055
|
}
|
|
@@ -1103,12 +2060,21 @@ export function parseTextDslV05(src: string): V05ParseResult {
|
|
|
1103
2060
|
}
|
|
1104
2061
|
|
|
1105
2062
|
if (!title)
|
|
1106
|
-
errors.push({
|
|
2063
|
+
errors.push({
|
|
2064
|
+
line: 1,
|
|
2065
|
+
message: "title が書かれていません",
|
|
2066
|
+
hint: "`title: \"...\"` を最上位に書く",
|
|
2067
|
+
});
|
|
1107
2068
|
if (!type)
|
|
1108
2069
|
errors.push({
|
|
1109
2070
|
line: 1,
|
|
1110
|
-
message: "type
|
|
1111
|
-
|
|
2071
|
+
message: "type が書かれていません",
|
|
2072
|
+
/*
|
|
2073
|
+
* **図種の一覧を手で並べない** (#2341)。 並べていた間、ここは実装が受ける図種の
|
|
2074
|
+
* 半分しか挙げず、値を並べる図種 (棒 / 折れ線 / 木 など) がまるごと抜けていた。
|
|
2075
|
+
* すぐ上の `図種が読めません` は元から実装を読んでおり、2 つの一覧が食い違っていた。
|
|
2076
|
+
*/
|
|
2077
|
+
hint: `\`type: flow\` の形で最上位に書く (使える図種 = ${Array.from(PRESET_TYPES).join(", ")})`,
|
|
1112
2078
|
});
|
|
1113
2079
|
|
|
1114
2080
|
// つまみ・式・巻き上げは同じ名前空間で値を作る。 重なると後から作る値が効かない。
|
|
@@ -1125,7 +2091,25 @@ export function parseTextDslV05(src: string): V05ParseResult {
|
|
|
1125
2091
|
});
|
|
1126
2092
|
}
|
|
1127
2093
|
|
|
2094
|
+
let shape: DslDiagramShape | null = aliasShape;
|
|
2095
|
+
let shapeLine = aliasShapeLine;
|
|
2096
|
+
if (shapeWritten !== null) {
|
|
2097
|
+
const 解けた = resolveDiagramShape(shapeWritten.value);
|
|
2098
|
+
const 図種の語 = type === null ? [] : shapesForDiagramType(type);
|
|
2099
|
+
if (解けた === null || (type === "chart" && !図種の語.includes(解けた))) {
|
|
2100
|
+
errors.push({
|
|
2101
|
+
line: shapeWritten.line,
|
|
2102
|
+
message: `shape が読めません (書いた値: ${shapeWritten.value})`,
|
|
2103
|
+
hint: shapeの案内(type),
|
|
2104
|
+
});
|
|
2105
|
+
} else {
|
|
2106
|
+
shape = 解けた;
|
|
2107
|
+
shapeLine = shapeWritten.line;
|
|
2108
|
+
}
|
|
2109
|
+
}
|
|
2110
|
+
|
|
1128
2111
|
if (errors.length > 0) return { ok: false, errors };
|
|
2112
|
+
const 選んだ意匠 = theme ?? palette;
|
|
1129
2113
|
|
|
1130
2114
|
return {
|
|
1131
2115
|
ok: true,
|
|
@@ -1136,8 +2120,80 @@ export function parseTextDslV05(src: string): V05ParseResult {
|
|
|
1136
2120
|
...(reveal !== null ? { reveal } : {}),
|
|
1137
2121
|
...(relations !== null ? { relations } : {}),
|
|
1138
2122
|
...(direction !== null ? { direction, directionPos: { line: directionLine } } : {}),
|
|
1139
|
-
...(
|
|
2123
|
+
...(order !== null ? { order, orderPos: { line: orderLine } } : {}),
|
|
2124
|
+
...(shape !== null ? { shape, shapePos: { line: shapeLine } } : {}),
|
|
2125
|
+
...(formWritten !== null
|
|
2126
|
+
? { form: formWritten.value, formPos: { line: formWritten.line } }
|
|
2127
|
+
: {}),
|
|
2128
|
+
...(ticks !== undefined ? { ticks, ticksPos: { line: ticksLine } } : {}),
|
|
2129
|
+
...(ganttToday !== undefined ? { ganttToday } : {}),
|
|
2130
|
+
...(ganttTickLabels !== undefined ? { ganttTickLabels } : {}),
|
|
2131
|
+
...(ganttBarEnd !== undefined ? { ganttBarEnd } : {}),
|
|
2132
|
+
...(ganttBarThickness !== undefined ? { ganttBarThickness } : {}),
|
|
2133
|
+
...(chartSeriesSkipMuted !== undefined ? { chartSeriesSkipMuted } : {}),
|
|
2134
|
+
...(chartLineSeries !== undefined ? { chartLineSeries } : {}),
|
|
2135
|
+
...(chartPieCenterLabel !== undefined ? { chartPieCenterLabel } : {}),
|
|
2136
|
+
...(chartPieTableColumns !== undefined ? { chartPieTableColumns } : {}),
|
|
2137
|
+
...(chartPieRingWidth !== undefined ? { chartPieRingWidth } : {}),
|
|
2138
|
+
...(chartSlopePeriods !== undefined ? { chartSlopePeriods } : {}),
|
|
2139
|
+
...(chartSlopeEmphasisIds !== undefined ? { chartSlopeEmphasisIds } : {}),
|
|
2140
|
+
...(chartSlopeUnit !== undefined ? { chartSlopeUnit } : {}),
|
|
2141
|
+
...(chartStackedPeriods !== undefined ? { chartStackedPeriods } : {}),
|
|
2142
|
+
...(chartStackedRateId !== undefined ? { chartStackedRateId } : {}),
|
|
2143
|
+
...(chartStackedLegendPosition !== undefined ? { chartStackedLegendPosition } : {}),
|
|
2144
|
+
...(chartGaugeValue !== undefined ? { chartGaugeValue } : {}),
|
|
2145
|
+
...(chartWaffleLegendPosition !== undefined ? { chartWaffleLegendPosition } : {}),
|
|
2146
|
+
...(funnelForm !== undefined ? { funnelForm } : {}),
|
|
2147
|
+
...(funnelRate !== undefined ? { funnelRate } : {}),
|
|
2148
|
+
...(quadrantPointLabelSide !== undefined ? { quadrantPointLabelSide } : {}),
|
|
2149
|
+
...(treeNodeForm !== undefined ? { treeNodeForm } : {}),
|
|
2150
|
+
...(treeSize !== undefined ? { treeSize } : {}),
|
|
2151
|
+
...(treeEdgeTone !== undefined ? { treeEdgeTone } : {}),
|
|
2152
|
+
...(treeEdgeHead !== undefined ? { treeEdgeHead } : {}),
|
|
2153
|
+
...(mindForm !== undefined ? { mindForm } : {}),
|
|
2154
|
+
...(mindSize !== undefined ? { mindSize } : {}),
|
|
2155
|
+
...(journeyForm !== undefined ? { journeyForm } : {}),
|
|
2156
|
+
...(journeyLineForm !== undefined ? { journeyLineForm } : {}),
|
|
2157
|
+
...(journeyLabels !== undefined ? { journeyLabels } : {}),
|
|
2158
|
+
...(journeySize !== undefined ? { journeySize } : {}),
|
|
2159
|
+
...(sequenceActorForm !== undefined ? { sequenceActorForm } : {}),
|
|
2160
|
+
...(sequenceLabelForm !== undefined ? { sequenceLabelForm } : {}),
|
|
2161
|
+
...(sequenceReturnHead !== undefined ? { sequenceReturnHead } : {}),
|
|
2162
|
+
...(sequenceKindLabels !== undefined ? { sequenceKindLabels } : {}),
|
|
2163
|
+
...(sequenceRowGap !== undefined ? { sequenceRowGap } : {}),
|
|
2164
|
+
...(sequenceGutter !== undefined ? { sequenceGutter } : {}),
|
|
2165
|
+
...(sequenceBandWidth !== undefined ? { sequenceBandWidth } : {}),
|
|
2166
|
+
...(sequenceSelfLoop !== undefined ? { sequenceSelfLoop } : {}),
|
|
2167
|
+
...(figureCard !== undefined ? { figureCard } : {}),
|
|
2168
|
+
...(figureSize !== undefined ? { figureSize } : {}),
|
|
2169
|
+
...(terminalDecoration !== undefined ? { terminalDecoration } : {}),
|
|
2170
|
+
...(cornerForm !== undefined ? { cornerForm } : {}),
|
|
2171
|
+
...(figureKindLabel !== undefined ? { figureKindLabel } : {}),
|
|
2172
|
+
...(figureInset !== undefined ? { figureInset } : {}),
|
|
2173
|
+
...(chartBarSize !== undefined ? { chartBarSize } : {}),
|
|
2174
|
+
...(chartBarShape !== undefined ? { chartBarShape } : {}),
|
|
2175
|
+
...(chartLineSize !== undefined ? { chartLineSize } : {}),
|
|
2176
|
+
...(chartPieSize !== undefined ? { chartPieSize } : {}),
|
|
2177
|
+
...(chartWaffleSize !== undefined ? { chartWaffleSize } : {}),
|
|
2178
|
+
...(chartSlopeLabelTemplate !== undefined ? { chartSlopeLabelTemplate } : {}),
|
|
2179
|
+
...(chartSlopeSize !== undefined ? { chartSlopeSize } : {}),
|
|
2180
|
+
...(chartStackedLayout !== undefined ? { chartStackedLayout } : {}),
|
|
2181
|
+
...(chartGaugeSize !== undefined ? { chartGaugeSize } : {}),
|
|
2182
|
+
...(ganttSize !== undefined ? { ganttSize } : {}),
|
|
2183
|
+
...(quadrantLabelPosition !== undefined ? { quadrantLabelPosition } : {}),
|
|
2184
|
+
...(quadrantSize !== undefined ? { quadrantSize } : {}),
|
|
2185
|
+
...(stageLayout !== undefined ? { stageLayout } : {}),
|
|
2186
|
+
...(選んだ意匠 !== null ? { theme: 選んだ意匠 } : {}),
|
|
2187
|
+
...(legend.length > 0 ? { legend } : {}),
|
|
2188
|
+
...(legendOffset !== undefined ? { legendOffset } : {}),
|
|
2189
|
+
...(legendFontSize !== undefined ? { legendFontSize } : {}),
|
|
2190
|
+
...(edgeEndGap !== undefined ? { edgeEndGap } : {}),
|
|
2191
|
+
...(stageHeader !== undefined ? { stageHeader } : {}),
|
|
2192
|
+
...(theme !== null && palette !== null
|
|
2193
|
+
? { themeAlsoPalettePos: { themeLine, paletteLine } }
|
|
2194
|
+
: {}),
|
|
1140
2195
|
...(axes !== undefined ? { axes, axesPos: { line: axesLine } } : {}),
|
|
2196
|
+
...(regions !== undefined ? { regions } : {}),
|
|
1141
2197
|
actors,
|
|
1142
2198
|
flow,
|
|
1143
2199
|
animate,
|
|
@@ -1156,26 +2212,141 @@ export function parseTextDslV05(src: string): V05ParseResult {
|
|
|
1156
2212
|
};
|
|
1157
2213
|
}
|
|
1158
2214
|
|
|
1159
|
-
function tokenize(src: string): Line[] {
|
|
1160
|
-
const out: Line[] = [];
|
|
1161
|
-
const raw = src.split("\n");
|
|
1162
|
-
for (let i = 0; i < raw.length; i += 1) {
|
|
1163
|
-
const r = raw[i] ?? "";
|
|
1164
|
-
const trimmed = r.trim();
|
|
1165
|
-
const indent = r.length - r.trimStart().length;
|
|
1166
|
-
out.push({ raw: r, trimmed, indent, no: i + 1 });
|
|
2215
|
+
function tokenize(src: string): Line[] {
|
|
2216
|
+
const out: Line[] = [];
|
|
2217
|
+
const raw = src.split("\n");
|
|
2218
|
+
for (let i = 0; i < raw.length; i += 1) {
|
|
2219
|
+
const r = raw[i] ?? "";
|
|
2220
|
+
const trimmed = r.trim();
|
|
2221
|
+
const indent = r.length - r.trimStart().length;
|
|
2222
|
+
out.push({ raw: r, trimmed, indent, no: i + 1 });
|
|
2223
|
+
}
|
|
2224
|
+
return out;
|
|
2225
|
+
}
|
|
2226
|
+
|
|
2227
|
+
type TopHeader = { key: string; value: string | null };
|
|
2228
|
+
|
|
2229
|
+
function matchTopHeader(trimmed: string): TopHeader | null {
|
|
2230
|
+
// 形式: `key:` or `key: value`
|
|
2231
|
+
const m = trimmed.match(/^([^\s:]+)\s*:\s*(.*)$/u);
|
|
2232
|
+
if (!m) return null;
|
|
2233
|
+
const value = (m[2] ?? "").trim();
|
|
2234
|
+
const writtenKey = m[1] ?? "";
|
|
2235
|
+
const lowerKey = writtenKey.toLowerCase();
|
|
2236
|
+
const canonicalKey = TOP_LEVEL_KEYS.find((key) => key.toLowerCase() === lowerKey);
|
|
2237
|
+
return { key: canonicalKey ?? lowerKey, value: value.length ? stripQuotes(value) : null };
|
|
2238
|
+
}
|
|
2239
|
+
|
|
2240
|
+
const LEGEND_ITEM_KEYS = ["mark", "text", "tone", "lineStyle", "align", "lead", "印", "説明"] as const;
|
|
2241
|
+
const LEGEND_LINE_MARKS = ["line-triangle-hollow", "line-diamond-hollow", "line-diamond-solid"] as const;
|
|
2242
|
+
|
|
2243
|
+
/** 凡例の 1 行を読み、描画側へ渡せる印と文字へ揃える。 */
|
|
2244
|
+
function parseLegendItem(
|
|
2245
|
+
line: Line,
|
|
2246
|
+
errors: DslError[],
|
|
2247
|
+
): CdlLegendItem | null {
|
|
2248
|
+
const match = line.trimmed.match(/^\{([\s\S]*)\}$/u);
|
|
2249
|
+
if (!match) {
|
|
2250
|
+
errors.push({
|
|
2251
|
+
line: line.no,
|
|
2252
|
+
message: "凡例の行が読めません",
|
|
2253
|
+
hint: '`{ mark: diamond, text: "分かれ道" }` の形で書く',
|
|
2254
|
+
});
|
|
2255
|
+
return null;
|
|
2256
|
+
}
|
|
2257
|
+
|
|
2258
|
+
const fields = new Map<string, string>();
|
|
2259
|
+
let unreadable = false;
|
|
2260
|
+
for (const field of splitInlineFields(match[1] ?? "")) {
|
|
2261
|
+
const separator = field.indexOf(":");
|
|
2262
|
+
if (separator < 0) {
|
|
2263
|
+
unreadable = true;
|
|
2264
|
+
continue;
|
|
2265
|
+
}
|
|
2266
|
+
const key = field.slice(0, separator).trim();
|
|
2267
|
+
const value = field.slice(separator + 1).trim();
|
|
2268
|
+
if (!LEGEND_ITEM_KEYS.includes(key as (typeof LEGEND_ITEM_KEYS)[number])) {
|
|
2269
|
+
errors.push({
|
|
2270
|
+
line: line.no,
|
|
2271
|
+
message: `凡例の項目名が読めません: "${key}"`,
|
|
2272
|
+
hint: `使える項目 = ${LEGEND_ITEM_KEYS.join(", ")}`,
|
|
2273
|
+
});
|
|
2274
|
+
unreadable = true;
|
|
2275
|
+
continue;
|
|
2276
|
+
}
|
|
2277
|
+
fields.set(key, value);
|
|
2278
|
+
}
|
|
2279
|
+
|
|
2280
|
+
const markRaw = fields.get("mark") ?? fields.get("印");
|
|
2281
|
+
const markValue = markRaw === undefined ? undefined : stripQuotes(markRaw);
|
|
2282
|
+
const mark = markValue === undefined ? undefined : resolveLegendMark(markValue);
|
|
2283
|
+
if (markValue !== undefined && mark === null) {
|
|
2284
|
+
errors.push({
|
|
2285
|
+
line: line.no,
|
|
2286
|
+
message: `凡例の印が読めません: "${markValue}"`,
|
|
2287
|
+
hint: `使える印 = ${[...LEGEND_MARKS, ...Object.keys(LEGEND_MARK_ALIAS)].join(" / ")}`,
|
|
2288
|
+
});
|
|
2289
|
+
unreadable = true;
|
|
1167
2290
|
}
|
|
1168
|
-
return out;
|
|
1169
|
-
}
|
|
1170
2291
|
|
|
1171
|
-
|
|
2292
|
+
const textRaw = fields.get("text") ?? fields.get("説明") ?? "";
|
|
2293
|
+
const quoted =
|
|
2294
|
+
(textRaw.startsWith('"') && textRaw.endsWith('"')) ||
|
|
2295
|
+
(textRaw.startsWith("'") && textRaw.endsWith("'"));
|
|
2296
|
+
const text = stripQuotes(textRaw).trim();
|
|
2297
|
+
const bracketed =
|
|
2298
|
+
textRaw.startsWith("[") ||
|
|
2299
|
+
textRaw.startsWith("{") ||
|
|
2300
|
+
textRaw.endsWith("]") ||
|
|
2301
|
+
textRaw.endsWith("}");
|
|
2302
|
+
if (text.length === 0 || (!quoted && bracketed)) {
|
|
2303
|
+
errors.push({
|
|
2304
|
+
line: line.no,
|
|
2305
|
+
message: "凡例の説明は文字で書く",
|
|
2306
|
+
hint: '`text: "分かれ道"` または `説明: "分かれ道"` の形で書く',
|
|
2307
|
+
});
|
|
2308
|
+
unreadable = true;
|
|
2309
|
+
}
|
|
1172
2310
|
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
2311
|
+
const toneRaw = fields.get("tone");
|
|
2312
|
+
const tone = toneRaw === undefined ? undefined : resolveTone(stripQuotes(toneRaw));
|
|
2313
|
+
if (toneRaw !== undefined && tone === undefined) {
|
|
2314
|
+
errors.push({ line: line.no, message: `凡例の tone が読めません: "${stripQuotes(toneRaw)}"`, hint: `使える値 = ${書ける色名().join(", ")}` });
|
|
2315
|
+
unreadable = true;
|
|
2316
|
+
}
|
|
2317
|
+
const lineStyleRaw = fields.get("lineStyle");
|
|
2318
|
+
const lineStyle = lineStyleRaw === undefined ? undefined : stripQuotes(lineStyleRaw);
|
|
2319
|
+
if (lineStyle !== undefined && !(LEGEND_LINE_STYLE_VALUES as readonly string[]).includes(lineStyle)) {
|
|
2320
|
+
errors.push({ line: line.no, message: `凡例の lineStyle が読めません: "${lineStyle}"`, hint: `使える値 = ${LEGEND_LINE_STYLE_VALUES.join(", ")}` });
|
|
2321
|
+
unreadable = true;
|
|
2322
|
+
}
|
|
2323
|
+
const alignRaw = fields.get("align");
|
|
2324
|
+
const align = alignRaw === undefined ? undefined : stripQuotes(alignRaw);
|
|
2325
|
+
if (align !== undefined && !(LEGEND_NOTE_ALIGN_VALUES as readonly string[]).includes(align)) {
|
|
2326
|
+
errors.push({ line: line.no, message: `凡例の align が読めません: "${align}"`, hint: `使える値 = ${LEGEND_NOTE_ALIGN_VALUES.join(", ")}` });
|
|
2327
|
+
unreadable = true;
|
|
2328
|
+
}
|
|
2329
|
+
const lead = fields.get("lead") === undefined ? undefined : stripQuotes(fields.get("lead")!);
|
|
2330
|
+
if (mark === undefined && (tone !== undefined || lineStyle !== undefined)) {
|
|
2331
|
+
errors.push({ line: line.no, message: "印のない凡例に tone / lineStyle は書けません", hint: "注記には text / align / lead を使う" });
|
|
2332
|
+
unreadable = true;
|
|
2333
|
+
}
|
|
2334
|
+
if (mark !== undefined && (align !== undefined || lead !== undefined)) {
|
|
2335
|
+
errors.push({ line: line.no, message: "印を持つ凡例に align / lead は書けません", hint: "align / lead は印のない注記に使う" });
|
|
2336
|
+
unreadable = true;
|
|
2337
|
+
}
|
|
2338
|
+
if (lineStyle !== undefined && (mark === undefined || mark === null || !(LEGEND_LINE_MARKS as readonly LegendMark[]).includes(mark))) {
|
|
2339
|
+
errors.push({ line: line.no, message: "lineStyle は線端を持つ凡例の印にだけ書けます", hint: `使える印 = ${LEGEND_LINE_MARKS.join(", ")}` });
|
|
2340
|
+
unreadable = true;
|
|
2341
|
+
}
|
|
2342
|
+
if (unreadable || mark === null) return null;
|
|
2343
|
+
if (mark === undefined) return { text, ...(align === undefined ? {} : { align: align as "start" | "end" }), ...(lead === undefined ? {} : { lead }) };
|
|
2344
|
+
return {
|
|
2345
|
+
mark,
|
|
2346
|
+
...(tone === undefined ? {} : { tone }),
|
|
2347
|
+
...(lineStyle === undefined ? {} : { lineStyle }),
|
|
2348
|
+
text,
|
|
2349
|
+
} as CdlLegendItem;
|
|
1179
2350
|
}
|
|
1180
2351
|
|
|
1181
2352
|
/**
|
|
@@ -1472,6 +2643,7 @@ function 表で読む<T extends Record<string, 値の形>>(
|
|
|
1472
2643
|
export const VIEWPORT_VALUE_KINDS = {
|
|
1473
2644
|
width: "数",
|
|
1474
2645
|
height: "数",
|
|
2646
|
+
contentOffsetX: "数",
|
|
1475
2647
|
scale: "数",
|
|
1476
2648
|
laneWidth: "数",
|
|
1477
2649
|
gap: "数",
|
|
@@ -1510,13 +2682,14 @@ export const LANE_VALUE_KINDS = {
|
|
|
1510
2682
|
|
|
1511
2683
|
/**
|
|
1512
2684
|
* 縦列の中括弧に書ける項目 (#1968)。 数と真偽の欄は `LANE_VALUE_KINDS` と `OFFSET_VALUE_KINDS` から
|
|
1513
|
-
* 導き、表に載せない文字列の `label` だけを足す。
|
|
2685
|
+
* 導き、表に載せない文字列の `label` と `subtitle` だけを足す。
|
|
1514
2686
|
*/
|
|
1515
2687
|
export const LANE_INLINE_KEYS: readonly (keyof DslLane | keyof typeof OFFSET_VALUE_KINDS)[] = [
|
|
1516
2688
|
...(Object.keys(LANE_VALUE_KINDS) as (keyof typeof LANE_VALUE_KINDS)[]),
|
|
1517
2689
|
// 位置のずらし (#1971)。 JSON の縦列の `pos` と同じ `layoutPos` に入る
|
|
1518
2690
|
...(Object.keys(OFFSET_VALUE_KINDS) as (keyof typeof OFFSET_VALUE_KINDS)[]),
|
|
1519
2691
|
"label",
|
|
2692
|
+
"subtitle",
|
|
1520
2693
|
];
|
|
1521
2694
|
|
|
1522
2695
|
/** 組の中括弧に書ける項目 (#1968)。 `groups:` の読み手が拾う欄と揃える */
|
|
@@ -1526,12 +2699,18 @@ export const GROUP_INLINE_KEYS = ["label", "lanes"] as const satisfies readonly
|
|
|
1526
2699
|
>[];
|
|
1527
2700
|
|
|
1528
2701
|
/** 軸の名前の中括弧に書ける項目 (#1968)。 横軸は左右、縦軸は下上 */
|
|
1529
|
-
export const AXIS_X_KEYS = ["left", "right"] as const satisfies readonly (
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
2702
|
+
export const AXIS_X_KEYS = ["left", "right", "label", "direction"] as const satisfies readonly (
|
|
2703
|
+
| "left"
|
|
2704
|
+
| "right"
|
|
2705
|
+
| "label"
|
|
2706
|
+
| "direction"
|
|
2707
|
+
)[];
|
|
2708
|
+
export const AXIS_Y_KEYS = ["bottom", "top", "label", "direction"] as const satisfies readonly (
|
|
2709
|
+
| "bottom"
|
|
2710
|
+
| "top"
|
|
2711
|
+
| "label"
|
|
2712
|
+
| "direction"
|
|
2713
|
+
)[];
|
|
1535
2714
|
|
|
1536
2715
|
/**
|
|
1537
2716
|
* 箱の中に描く図形 (`shape:`) と、値を見せる部品 (`readouts:`) の欄 (#1374)。
|
|
@@ -1553,7 +2732,7 @@ export const AXIS_Y_KEYS = ["bottom", "top"] as const satisfies readonly (keyof
|
|
|
1553
2732
|
// `parser.ts` に置いたままだと 生成した表 → parser → 生成した表 の輪ができる
|
|
1554
2733
|
export type { 欄の形, 図形の定義 } from "./parser-types";
|
|
1555
2734
|
|
|
1556
|
-
/** 描ける図形と、その欄 (`CdlDynShape`
|
|
2735
|
+
/** 描ける図形と、その欄 (`CdlDynShape` の全種を覆う) */
|
|
1557
2736
|
export const 図形の表: Record<string, 図形の定義> = {
|
|
1558
2737
|
rect: {
|
|
1559
2738
|
必須: ["source", "fillMax"],
|
|
@@ -1608,9 +2787,9 @@ export const 図形の表: Record<string, 図形の定義> = {
|
|
|
1608
2787
|
/**
|
|
1609
2788
|
* 記法が受ける部品と、その欄。
|
|
1610
2789
|
*
|
|
1611
|
-
* **手で書かず、描画側の型定義から生成する** (#1385)。
|
|
1612
|
-
*
|
|
1613
|
-
* 人手で書かない`)。
|
|
2790
|
+
* **手で書かず、描画側の型定義から生成する** (#1385)。 描画側が持つ種類も、種類ごとの欄も
|
|
2791
|
+
* 多く、手で写すと写し間違いと描画側の変更への drift が残る (`rules/quality.md § 導出可能記述は
|
|
2792
|
+
* 人手で書かない`)。 いくつあるかは `readout-table.generated.ts` を数えれば出る。
|
|
1614
2793
|
*
|
|
1615
2794
|
* 作り直す = `node packages/dragon/scripts/gen-readout-table.mjs`
|
|
1616
2795
|
* ずれの検知 = `packages/dragon/test/readout-table-generated.test.ts`
|
|
@@ -1621,8 +2800,9 @@ import { 部品の表, 部品の組の表 } from "./readout-table.generated";
|
|
|
1621
2800
|
/**
|
|
1622
2801
|
* 記法が受けるつまみと、その欄 (#1389)。
|
|
1623
2802
|
*
|
|
1624
|
-
* **部品の表と同じく、描画側の型定義から生成する**。
|
|
2803
|
+
* **部品の表と同じく、描画側の型定義から生成する**。 種類ごとに欄が違い、手で写すと
|
|
1625
2804
|
* 描画側が種類を足した時に drift が残る (`rules/quality.md § 導出可能記述は人手で書かない`)。
|
|
2805
|
+
* いくつあるかは `input-table.generated.ts` を数えれば出る。
|
|
1626
2806
|
*
|
|
1627
2807
|
* 作り直す = `node packages/dragon/scripts/gen-input-table.mjs`
|
|
1628
2808
|
* ずれの検知 = `packages/dragon/test/input-table-generated.test.ts`
|
|
@@ -1733,7 +2913,11 @@ function 選択肢の並びとして読む(
|
|
|
1733
2913
|
): (string | { value: string; label: string })[] | undefined {
|
|
1734
2914
|
const 中身 = 値.trim();
|
|
1735
2915
|
if (!中身.startsWith("[") || !中身.endsWith("]")) {
|
|
1736
|
-
errors.push({
|
|
2916
|
+
errors.push({
|
|
2917
|
+
line,
|
|
2918
|
+
message: `${名} の並びが読めません: "${値}"`,
|
|
2919
|
+
hint: '`["green", { value: "red", label: "失敗" }]` の形で書く',
|
|
2920
|
+
});
|
|
1737
2921
|
return undefined;
|
|
1738
2922
|
}
|
|
1739
2923
|
const 出: (string | { value: string; label: string })[] = [];
|
|
@@ -1746,19 +2930,35 @@ function 選択肢の並びとして読む(
|
|
|
1746
2930
|
return;
|
|
1747
2931
|
}
|
|
1748
2932
|
if (!要素.endsWith("}")) {
|
|
1749
|
-
errors.push({
|
|
2933
|
+
errors.push({
|
|
2934
|
+
line,
|
|
2935
|
+
message: `${名}[${i}] の組が読めません: "${要素}"`,
|
|
2936
|
+
hint: '`{ value: "red", label: "失敗" }` の形で書く',
|
|
2937
|
+
});
|
|
1750
2938
|
return;
|
|
1751
2939
|
}
|
|
1752
2940
|
const 組 = parseInlineMapping(要素.slice(1, -1));
|
|
1753
2941
|
for (const k of Object.keys(組)) {
|
|
1754
2942
|
if ((選択肢の組の項目 as readonly string[]).includes(k)) continue;
|
|
1755
|
-
errors.push({
|
|
2943
|
+
errors.push({
|
|
2944
|
+
line,
|
|
2945
|
+
message: `${名}[${i}] の項目名が読めません: "${k}"`,
|
|
2946
|
+
hint: `使える項目 = ${選択肢の組の項目.join(", ")}`,
|
|
2947
|
+
});
|
|
1756
2948
|
}
|
|
1757
2949
|
for (const k of 選択肢の組の項目) {
|
|
1758
2950
|
if (組[k] === undefined) {
|
|
1759
|
-
errors.push({
|
|
2951
|
+
errors.push({
|
|
2952
|
+
line,
|
|
2953
|
+
message: `${名}[${i}].${k} は必ず書きます`,
|
|
2954
|
+
hint: `必須の項目 = ${選択肢の組の項目.join(", ")}`,
|
|
2955
|
+
});
|
|
1760
2956
|
} else if (組[k] === "") {
|
|
1761
|
-
errors.push({
|
|
2957
|
+
errors.push({
|
|
2958
|
+
line,
|
|
2959
|
+
message: `${名}[${i}].${k} が空です`,
|
|
2960
|
+
hint: k === "label" ? "名前を書かないなら文字列だけを並べる" : "値を 1 文字以上書く",
|
|
2961
|
+
});
|
|
1762
2962
|
}
|
|
1763
2963
|
}
|
|
1764
2964
|
if (組.value && 組.label) 出.push({ value: 組.value, label: 組.label });
|
|
@@ -2060,7 +3260,7 @@ function 部品として読む(
|
|
|
2060
3260
|
const 読めた = 表に従って読む(定義, opts, `部品 ${id} の `, line, errors);
|
|
2061
3261
|
部品の組を検査する(kind, 読めた, line, errors);
|
|
2062
3262
|
for (const 欄 of 定義.必須) if (読めた[欄] === undefined) return undefined;
|
|
2063
|
-
return { id, kind,
|
|
3263
|
+
return { id, kind, ...読めた, pos: { line } } as DslReadout;
|
|
2064
3264
|
}
|
|
2065
3265
|
|
|
2066
3266
|
/**
|
|
@@ -2081,8 +3281,26 @@ export const EVENT_KINDS = [
|
|
|
2081
3281
|
"blur",
|
|
2082
3282
|
] as const;
|
|
2083
3283
|
|
|
2084
|
-
/**
|
|
2085
|
-
|
|
3284
|
+
/**
|
|
3285
|
+
* 出来事の相手を指す書き方。 ちょうど 1 つだけ書く。
|
|
3286
|
+
*
|
|
3287
|
+
* **ここが唯一の置き場所** (#2256)。 同じ 4 語が JSON の記法にも書かれていた間、
|
|
3288
|
+
* 1:1 を名乗りながら受ける項目が両側で別々に決まっており、片方に 5 つ目を足すと
|
|
3289
|
+
* 記法では通るのに JSON では弾かれる状態が黙って生まれる形だった。
|
|
3290
|
+
* 公開 schema の項目の鍵とは `event-target-keys-parity.test.ts` が突き合わせる。
|
|
3291
|
+
*/
|
|
3292
|
+
export const EVENT_TARGET_KEYS = ["box", "lane", "arrow", "diagram"] as const;
|
|
3293
|
+
|
|
3294
|
+
/** 出来事に必ず書く項目。 相手の指し方と合わせて schema の項目の鍵になる */
|
|
3295
|
+
export const EVENT_REQUIRED_KEYS = ["on", "handler"] as const;
|
|
3296
|
+
|
|
3297
|
+
/**
|
|
3298
|
+
* 出来事が受ける項目の全体 (#2258)。
|
|
3299
|
+
*
|
|
3300
|
+
* **判定と知らせが同じ集合を読む**。 知らせが先頭 2 語だけを手で並べていた間、
|
|
3301
|
+
* 必ず書く項目を増やしても知らせは古い一覧を教え続ける形だった。
|
|
3302
|
+
*/
|
|
3303
|
+
const EVENT_ITEM_KEYS: readonly string[] = [...EVENT_REQUIRED_KEYS, ...EVENT_TARGET_KEYS];
|
|
2086
3304
|
|
|
2087
3305
|
/**
|
|
2088
3306
|
* 押下などの出来事で動く仕掛けを 1 件読む (#1393)。
|
|
@@ -2109,12 +3327,11 @@ function 出来事として読む(
|
|
|
2109
3327
|
}
|
|
2110
3328
|
const opts = parseInlineMapping(t.slice(1, -1));
|
|
2111
3329
|
for (const k of Object.keys(opts)) {
|
|
2112
|
-
if (
|
|
2113
|
-
continue;
|
|
3330
|
+
if (EVENT_ITEM_KEYS.includes(k)) continue;
|
|
2114
3331
|
errors.push({
|
|
2115
3332
|
line,
|
|
2116
3333
|
message: `出来事の項目名が読めません: "${k}"`,
|
|
2117
|
-
hint: `使える項目 =
|
|
3334
|
+
hint: `使える項目 = ${EVENT_ITEM_KEYS.join(", ")}`,
|
|
2118
3335
|
});
|
|
2119
3336
|
return undefined;
|
|
2120
3337
|
}
|
|
@@ -2299,7 +3516,12 @@ function 式として読む(行: string, line: number, errors: DslError[]): DslF
|
|
|
2299
3516
|
});
|
|
2300
3517
|
return undefined;
|
|
2301
3518
|
}
|
|
2302
|
-
return {
|
|
3519
|
+
return {
|
|
3520
|
+
id: 名前,
|
|
3521
|
+
expression: 式,
|
|
3522
|
+
...(名札 !== undefined ? { label: 名札 } : {}),
|
|
3523
|
+
pos: { line },
|
|
3524
|
+
};
|
|
2303
3525
|
}
|
|
2304
3526
|
|
|
2305
3527
|
/** 組の中の項目名の形。 `parseInlineMapping` が項目名として受ける字と同じ */
|
|
@@ -2332,7 +3554,9 @@ function 式の組を読む(
|
|
|
2332
3554
|
): { expression: string; label?: string } | undefined {
|
|
2333
3555
|
const 使える = ["expression", "label"];
|
|
2334
3556
|
const 読めた: Record<string, string> = {};
|
|
2335
|
-
for (const 欄 of splitInlineFields(中身)
|
|
3557
|
+
for (const 欄 of splitInlineFields(中身)
|
|
3558
|
+
.map((s) => s.trim())
|
|
3559
|
+
.filter((s) => s !== "")) {
|
|
2336
3560
|
const m = 欄.match(組の項目名);
|
|
2337
3561
|
if (!m) {
|
|
2338
3562
|
errors.push({
|
|
@@ -2403,19 +3627,40 @@ export const ACTOR_INLINE_VALUE_KINDS = {
|
|
|
2403
3627
|
posY: "数",
|
|
2404
3628
|
posW: "数",
|
|
2405
3629
|
posH: "数",
|
|
3630
|
+
titleFontSize: "数",
|
|
3631
|
+
markGap: "数",
|
|
3632
|
+
milestone: "真偽",
|
|
3633
|
+
rowRules: "真偽",
|
|
3634
|
+
rowStripe: "真偽",
|
|
2406
3635
|
} as const satisfies Record<string, 値の形>;
|
|
2407
3636
|
|
|
2408
|
-
/**
|
|
3637
|
+
/**
|
|
3638
|
+
* 縦に並べる形で箱に書ける、数と真偽の欄 (#1306)。 `posW` / `posH` は `大きさ:` が受ける。
|
|
3639
|
+
*
|
|
3640
|
+
* 始まりと終わりの印 (`initial` / `final`) は #2346 で足した。 中括弧の表にはあったのに
|
|
3641
|
+
* こちらに無く、**段を分けた側には書き方が 1 つも無かった** = 書くと綴りの誤りとして落ちる
|
|
3642
|
+
*/
|
|
2409
3643
|
export const ACTOR_BLOCK_VALUE_KINDS = {
|
|
2410
3644
|
stack: "数",
|
|
2411
3645
|
posX: "数",
|
|
2412
3646
|
posY: "数",
|
|
3647
|
+
initial: "真偽",
|
|
3648
|
+
final: "真偽",
|
|
3649
|
+
titleFontSize: "数",
|
|
3650
|
+
markGap: "数",
|
|
3651
|
+
milestone: "真偽",
|
|
3652
|
+
rowRules: "真偽",
|
|
3653
|
+
rowStripe: "真偽",
|
|
2413
3654
|
} as const satisfies Record<string, 値の形>;
|
|
2414
3655
|
|
|
2415
3656
|
/** 矢印の中括弧に書ける、数と真偽の欄 (#1306) */
|
|
2416
3657
|
export const FLOW_INLINE_VALUE_KINDS = {
|
|
2417
3658
|
labelOffsetX: "数",
|
|
2418
3659
|
labelOffsetY: "数",
|
|
3660
|
+
posX1: "数",
|
|
3661
|
+
posY1: "数",
|
|
3662
|
+
posX2: "数",
|
|
3663
|
+
posY2: "数",
|
|
2419
3664
|
overlay: "真偽",
|
|
2420
3665
|
} as const satisfies Record<string, 値の形>;
|
|
2421
3666
|
|
|
@@ -2504,10 +3749,10 @@ function parseInlineMapping(inner: string): Record<string, string> {
|
|
|
2504
3749
|
// 読める名前を広げても、**知っている名前しか使われない**。 パーツの状態の上書きに
|
|
2505
3750
|
// 流れるのは `ACTOR_RESERVED_FIELDS` に無い名前だけで、日本語の項目名 (`位置` / `大きさ`
|
|
2506
3751
|
// 等) はそこに載せてあるため、これまでどおり落ちる
|
|
2507
|
-
const m = p.match(/^\s*([^\s:,{}[\]"']+)\s*:\s*(.+?)\s*$/);
|
|
3752
|
+
const m = p.match(/^\s*(?:"([^"]+)"|'([^']+)'|([^\s:,{}[\]"']+))\s*:\s*(.+?)\s*$/);
|
|
2508
3753
|
if (m) {
|
|
2509
|
-
const key = m[1]!;
|
|
2510
|
-
out[key] = stripQuotes(m[
|
|
3754
|
+
const key = m[1] ?? m[2] ?? m[3]!;
|
|
3755
|
+
out[key] = stripQuotes(m[4]!.trim());
|
|
2511
3756
|
}
|
|
2512
3757
|
}
|
|
2513
3758
|
return out;
|
|
@@ -2525,7 +3770,7 @@ function writtenScaleFields(inner: string): Map<string, string> {
|
|
|
2525
3770
|
for (const field of splitInlineFields(inner)) {
|
|
2526
3771
|
const idx = field.indexOf(":");
|
|
2527
3772
|
if (idx < 0) continue;
|
|
2528
|
-
const key = field.slice(0, idx).trim();
|
|
3773
|
+
const key = stripQuotes(field.slice(0, idx).trim());
|
|
2529
3774
|
if (!SCALE_KEYS.has(key)) continue;
|
|
2530
3775
|
// 同じ名前を 2 度書いたら後の値を採る
|
|
2531
3776
|
out.set(key, stripQuotes(field.slice(idx + 1).trim()));
|
|
@@ -2597,6 +3842,81 @@ function splitInlineFields(inner: string): string[] {
|
|
|
2597
3842
|
return 見た.閉じた ? 見た.parts : 引用符を見て割る(false).parts;
|
|
2598
3843
|
}
|
|
2599
3844
|
|
|
3845
|
+
/** 行頭の印を、従来の語または色の段を持つ組として読む。 */
|
|
3846
|
+
function 行頭の印の並びとして読む(
|
|
3847
|
+
raw: string,
|
|
3848
|
+
line: number,
|
|
3849
|
+
errors: DslError[],
|
|
3850
|
+
): DslActor["marks"] {
|
|
3851
|
+
if (!raw.includes("{")) {
|
|
3852
|
+
return raw
|
|
3853
|
+
.replace(/^\[|\]$/g, "")
|
|
3854
|
+
.split(/,(?![^[]*\])/)
|
|
3855
|
+
.map((x) => 行頭の語へ読み替える(stripQuotes(x.trim())));
|
|
3856
|
+
}
|
|
3857
|
+
const value = JSONの欄を読む(raw, "marks", line, errors);
|
|
3858
|
+
if (!Array.isArray(value)) {
|
|
3859
|
+
errors.push({ line, message: "marks は並びで書きます" });
|
|
3860
|
+
return undefined;
|
|
3861
|
+
}
|
|
3862
|
+
const out: NonNullable<DslActor["marks"]> = [];
|
|
3863
|
+
value.forEach((item, index) => {
|
|
3864
|
+
if (typeof item === "string") {
|
|
3865
|
+
out.push(行頭の語へ読み替える(item));
|
|
3866
|
+
return;
|
|
3867
|
+
}
|
|
3868
|
+
if (!objectか(item)) {
|
|
3869
|
+
errors.push({ line, message: `marks[${index}] の値の形が読めません` });
|
|
3870
|
+
return;
|
|
3871
|
+
}
|
|
3872
|
+
const 受ける項目 = ["mark", "tone"] as const;
|
|
3873
|
+
const unknown = Object.keys(item).filter((key) => !(受ける項目 as readonly string[]).includes(key));
|
|
3874
|
+
if (unknown.length > 0) {
|
|
3875
|
+
for (const key of unknown) errors.push({ line, message: `marks[${index}] の項目名が読めません: "${key}"`, hint: `使える項目 = ${受ける項目.join(", ")}` });
|
|
3876
|
+
return;
|
|
3877
|
+
}
|
|
3878
|
+
if (typeof item.mark !== "string") {
|
|
3879
|
+
errors.push({ line, message: `marks[${index}].mark は文字で書きます` });
|
|
3880
|
+
return;
|
|
3881
|
+
}
|
|
3882
|
+
if (item.tone !== undefined && !(ROW_MARK_TONE_VALUES as readonly unknown[]).includes(item.tone)) {
|
|
3883
|
+
const shown = typeof item.tone === "string" ? item.tone : (JSON.stringify(item.tone) ?? typeof item.tone);
|
|
3884
|
+
errors.push({ line, message: `marks[${index}].tone が読めません: "${shown}"`, hint: `使える値 = ${ROW_MARK_TONE_VALUES.join(", ")}` });
|
|
3885
|
+
return;
|
|
3886
|
+
}
|
|
3887
|
+
out.push({
|
|
3888
|
+
mark: 行頭の語へ読み替える(item.mark),
|
|
3889
|
+
...(item.tone === undefined ? {} : { tone: item.tone as "primary" | "muted" }),
|
|
3890
|
+
});
|
|
3891
|
+
});
|
|
3892
|
+
return out;
|
|
3893
|
+
}
|
|
3894
|
+
|
|
3895
|
+
/**
|
|
3896
|
+
* 字下げした行を 1 行も落とさずに集める (#2400)。
|
|
3897
|
+
*
|
|
3898
|
+
* 形が合うかを **ここでは見ない**。 集めた行は呼出側の読み手 (`parseFlowStep` /
|
|
3899
|
+
* `parseStateEntry` / `parseLane` 等) へ渡し、読めない行は読み手が「…の行が読めません」 として
|
|
3900
|
+
* 報告する。
|
|
3901
|
+
*
|
|
3902
|
+
* **形の合わない行をここで捨てない**。 捨てると知らせにも図にも現れず、書いた人には
|
|
3903
|
+
* 「1 行書いたのに増えない」 だけが残る (実測 = `states:` に `progress 0` と書くと
|
|
3904
|
+
* 知らせ 0 件でその状態が消えた)。 判定を読み手 1 か所に寄せると、受ける形を広げた日に
|
|
3905
|
+
* 集める側を直し忘れて黙る形も生まれない。
|
|
3906
|
+
*
|
|
3907
|
+
* **記法が受ける全ての節がここを通る** (#2406)。 以前は先頭の `- ` を外す版と外さない版の
|
|
3908
|
+
* 2 つに分かれていたが、 形の合わない行を捨てない振る舞いが #2400 で揃った時点で違いが
|
|
3909
|
+
* 「`- ` を外すかどうか」 の 1 行だけになり、 外す側へ寄せて 1 つにした。
|
|
3910
|
+
*
|
|
3911
|
+
* **外す側へ寄せると、 受ける形が増える節がある**。 まとめる前に測ると、 11 節のうち 8 節は
|
|
3912
|
+
* 既に `- ` を受けており (5 節はここで外し、 3 節は読み手が自分で外していた)、 受けないのは
|
|
3913
|
+
* `readouts` / `inputs` / `scrolls` の 3 節だけだった。 寄せると この 3 節も受ける =
|
|
3914
|
+
* **既にある図の意味は 1 つも変わらず**、 節によって受ける形が違う不揃いが消える。
|
|
3915
|
+
*
|
|
3916
|
+
* **空行とコメント行はここで飛ばす**。 読み手はどれも「書いた指定」 として読むため、
|
|
3917
|
+
* 渡すとコメントが誤りとして報告される (実測 = コロンを含むコメント `# めも: あ` が
|
|
3918
|
+
* `状態の行が読めません` になっていた)。
|
|
3919
|
+
*/
|
|
2600
3920
|
function collectIndentedList(
|
|
2601
3921
|
lines: Line[],
|
|
2602
3922
|
start: number,
|
|
@@ -2606,36 +3926,18 @@ function collectIndentedList(
|
|
|
2606
3926
|
let i = start;
|
|
2607
3927
|
while (i < lines.length) {
|
|
2608
3928
|
const ln = lines[i]!;
|
|
2609
|
-
if (!ln.trimmed) {
|
|
3929
|
+
if (!ln.trimmed || ln.trimmed.startsWith("#")) {
|
|
2610
3930
|
i += 1;
|
|
2611
3931
|
continue;
|
|
2612
3932
|
}
|
|
2613
3933
|
if (ln.indent <= parentIndent) break;
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
} else if (ln.trimmed.includes(":")) {
|
|
2617
|
-
// YAML 風 inline (key: value) は state 用 block で許容
|
|
2618
|
-
items.push(ln);
|
|
2619
|
-
}
|
|
3934
|
+
// 先頭の `- ` を外して渡す。 外した形と、 YAML 風の `key: value` を読み手が同じに読む
|
|
3935
|
+
items.push(ln.trimmed.startsWith("- ") ? { ...ln, trimmed: ln.trimmed.slice(2).trim() } : ln);
|
|
2620
3936
|
i += 1;
|
|
2621
3937
|
}
|
|
2622
3938
|
return { items, next: i };
|
|
2623
3939
|
}
|
|
2624
3940
|
|
|
2625
|
-
/**
|
|
2626
|
-
* 登場人物を 1 件ずつ集める。 続く字下げ行は同じ 1 件にまとめる。
|
|
2627
|
-
*
|
|
2628
|
-
* 項目が少なければ 1 行で書け、 多ければ縦に並べられる。 縦に並べた方が、 何を指定できるかが
|
|
2629
|
-
* 見える。
|
|
2630
|
-
*
|
|
2631
|
-
* ```
|
|
2632
|
-
* - Client
|
|
2633
|
-
* - API: service
|
|
2634
|
-
* - Web:
|
|
2635
|
-
* kind: service
|
|
2636
|
-
* 色: 失敗
|
|
2637
|
-
* ```
|
|
2638
|
-
*/
|
|
2639
3941
|
/**
|
|
2640
3942
|
* 色の指定を振り分ける。
|
|
2641
3943
|
*
|
|
@@ -2739,7 +4041,7 @@ function applyContinuationLines(actor: DslActor, rest: Line[], errors: DslError[
|
|
|
2739
4041
|
/** 倍率を名前ごとに最後に書いた行。 別名の優先順と行番号を取り違えないために保持する。 */
|
|
2740
4042
|
const scaleLines = new Map<string, number>();
|
|
2741
4043
|
/**
|
|
2742
|
-
*
|
|
4044
|
+
* 縦に並べて書かれたユーザージャーニーの欄 (#1251)。
|
|
2743
4045
|
*
|
|
2744
4046
|
* パーツでは状態の上書きとして意味を持つため、どちらに入れるかは block を読み終わってから
|
|
2745
4047
|
* 決める。 `kind:` の行が後ろに書かれることもあり、読んだ時点ではパーツか分からない。
|
|
@@ -2799,6 +4101,111 @@ function applyContinuationLines(actor: DslActor, rest: Line[], errors: DslError[
|
|
|
2799
4101
|
case "補足":
|
|
2800
4102
|
out.subtitle = stripQuotes(raw);
|
|
2801
4103
|
break;
|
|
4104
|
+
case "kindForm": {
|
|
4105
|
+
const value = stripQuotes(raw);
|
|
4106
|
+
if (value === "icon") out.kindForm = value;
|
|
4107
|
+
else errors.push({ line: ln.no, message: `箱の kindForm が読めません: "${value}"`, hint: `使える値 = ${KIND_FORM_VALUES.join(", ")}` });
|
|
4108
|
+
break;
|
|
4109
|
+
}
|
|
4110
|
+
case "icon": {
|
|
4111
|
+
const value = stripQuotes(raw);
|
|
4112
|
+
if ((CDL_ICON_VALUES as readonly string[]).includes(value)) out.icon = value as NonNullable<DslActor["icon"]>;
|
|
4113
|
+
else errors.push({ line: ln.no, message: `箱の icon が読めません: "${value}"`, hint: `使える値 = ${CDL_ICON_VALUES.join(", ")}` });
|
|
4114
|
+
break;
|
|
4115
|
+
}
|
|
4116
|
+
case "emptyRowsNote":
|
|
4117
|
+
out.emptyRowsNote = stripQuotes(raw);
|
|
4118
|
+
break;
|
|
4119
|
+
case "titleAlign": {
|
|
4120
|
+
const value = stripQuotes(raw);
|
|
4121
|
+
if (value === "center") out.titleAlign = value;
|
|
4122
|
+
else errors.push({ line: ln.no, message: `箱の titleAlign が読めません: "${value}"`, hint: `使える値 = ${TITLE_ALIGN_VALUES.join(", ")}` });
|
|
4123
|
+
break;
|
|
4124
|
+
}
|
|
4125
|
+
case "rowRules": {
|
|
4126
|
+
const read = 表で読む(ACTOR_BLOCK_VALUE_KINDS, { rowRules: raw }, "箱の ", ln.no, errors);
|
|
4127
|
+
out.rowRules = read.rowRules;
|
|
4128
|
+
break;
|
|
4129
|
+
}
|
|
4130
|
+
case "rowStripe": {
|
|
4131
|
+
const read = 表で読む(ACTOR_BLOCK_VALUE_KINDS, { rowStripe: raw }, "箱の ", ln.no, errors);
|
|
4132
|
+
out.rowStripe = read.rowStripe;
|
|
4133
|
+
break;
|
|
4134
|
+
}
|
|
4135
|
+
case "rowLayout":
|
|
4136
|
+
out.rowLayout = 行の配置として読む(raw, ln.no, errors);
|
|
4137
|
+
break;
|
|
4138
|
+
case "rowDividerForm": {
|
|
4139
|
+
const value = stripQuotes(raw);
|
|
4140
|
+
if (value === "full") out.rowDividerForm = value;
|
|
4141
|
+
else errors.push({ line: ln.no, message: `箱の rowDividerForm が読めません: "${value}"`, hint: `使える値 = ${ROW_DIVIDER_FORM_VALUES.join(", ")}` });
|
|
4142
|
+
break;
|
|
4143
|
+
}
|
|
4144
|
+
case "rowWell":
|
|
4145
|
+
out.rowWell = 行の受け皿として読む(raw, ln.no, errors);
|
|
4146
|
+
break;
|
|
4147
|
+
case "rowMarkForm": {
|
|
4148
|
+
const value = stripQuotes(raw);
|
|
4149
|
+
if (value === "tone") out.rowMarkForm = value;
|
|
4150
|
+
else errors.push({ line: ln.no, message: `箱の rowMarkForm が読めません: "${value}"`, hint: `使える値 = ${ROW_MARK_FORM_VALUES.join(", ")}` });
|
|
4151
|
+
break;
|
|
4152
|
+
}
|
|
4153
|
+
case "nestIn":
|
|
4154
|
+
out.nestIn = stripQuotes(raw);
|
|
4155
|
+
break;
|
|
4156
|
+
case "figureCard": {
|
|
4157
|
+
const value = JSONの欄を読む(raw, "figureCard", ln.no, errors);
|
|
4158
|
+
const unknown = 図表の知らない項目を知らせる(value, ["label", "note"] as const, "figureCard", ln.no, errors);
|
|
4159
|
+
if (!unknown && objectか(value) && typeof value.label === "string" && (value.note === undefined || typeof value.note === "string")) out.figureCard = value as NonNullable<DslActor["figureCard"]>;
|
|
4160
|
+
else if (!unknown && value !== undefined) errors.push({ line: ln.no, message: "figureCard の値の形が読めません" });
|
|
4161
|
+
break;
|
|
4162
|
+
}
|
|
4163
|
+
case "stationNamePosition":
|
|
4164
|
+
if (stripQuotes(raw) === "bottom") out.stationNamePosition = "bottom";
|
|
4165
|
+
break;
|
|
4166
|
+
case "subtitlePlacement":
|
|
4167
|
+
if (stripQuotes(raw) === "right") out.subtitlePlacement = "right";
|
|
4168
|
+
break;
|
|
4169
|
+
case "titleFontSize":
|
|
4170
|
+
case "markGap": {
|
|
4171
|
+
const read = 表で読む(ACTOR_BLOCK_VALUE_KINDS, { [key]: raw }, "箱の ", ln.no, errors);
|
|
4172
|
+
if (key === "titleFontSize") out.titleFontSize = read.titleFontSize;
|
|
4173
|
+
else out.markGap = read.markGap;
|
|
4174
|
+
break;
|
|
4175
|
+
}
|
|
4176
|
+
case "milestone": {
|
|
4177
|
+
const read = 表で読む(ACTOR_BLOCK_VALUE_KINDS, { milestone: raw }, "箱の ", ln.no, errors);
|
|
4178
|
+
out.milestone = read.milestone;
|
|
4179
|
+
break;
|
|
4180
|
+
}
|
|
4181
|
+
case "startLabel":
|
|
4182
|
+
out.startLabel = stripQuotes(raw);
|
|
4183
|
+
break;
|
|
4184
|
+
case "emphasis": {
|
|
4185
|
+
const value = stripQuotes(raw);
|
|
4186
|
+
if (value === "primary") out.emphasis = value;
|
|
4187
|
+
else errors.push({ line: ln.no, message: `箱の emphasis が読めません: "${value}"`, hint: "使える値 = primary" });
|
|
4188
|
+
break;
|
|
4189
|
+
}
|
|
4190
|
+
/*
|
|
4191
|
+
* 箱の目次と、始まりと終わりの印 (#2346)。
|
|
4192
|
+
*
|
|
4193
|
+
* 中括弧の形にしか無く、段を分けて書くと綴りの誤りとして落ちていた。
|
|
4194
|
+
* 中括弧と同じく **見本 (parts) でも横取りしない** = この 3 つは
|
|
4195
|
+
* `ACTOR_RESERVED_FIELDS` が状態の上書きから外しているので、どちらの形で書いても
|
|
4196
|
+
* 見本の状態にはならない。
|
|
4197
|
+
*/
|
|
4198
|
+
case "eyebrow":
|
|
4199
|
+
out.eyebrow = stripQuotes(raw);
|
|
4200
|
+
break;
|
|
4201
|
+
case "initial":
|
|
4202
|
+
case "final": {
|
|
4203
|
+
// 中括弧と同じ表で読む。 読めない値の知らせも同じ文になる
|
|
4204
|
+
const 読めた = 表で読む(ACTOR_BLOCK_VALUE_KINDS, { [key]: raw }, "箱の ", ln.no, errors);
|
|
4205
|
+
if (key === "initial") out.initial = 読めた.initial;
|
|
4206
|
+
else out.final = 読めた.final;
|
|
4207
|
+
break;
|
|
4208
|
+
}
|
|
2802
4209
|
case "value":
|
|
2803
4210
|
case "値":
|
|
2804
4211
|
out.value = stripQuotes(raw);
|
|
@@ -2846,20 +4253,20 @@ function applyContinuationLines(actor: DslActor, rest: Line[], errors: DslError[
|
|
|
2846
4253
|
.filter(Boolean);
|
|
2847
4254
|
break;
|
|
2848
4255
|
/*
|
|
2849
|
-
* 行頭の印 (#1466)。 `rows`
|
|
4256
|
+
* 行頭の印 (#1466)。 `rows` と同じ並びで書く。
|
|
2850
4257
|
*
|
|
2851
|
-
*
|
|
2852
|
-
*
|
|
2853
|
-
*
|
|
4258
|
+
* **語は dragon の 3 つ** (`鍵` / `外` / `条件`)。 印の 3 軸 (下線 / 形 / 塗り) に
|
|
4259
|
+
* 1 対 1 で当たる (`compile/row-marks.ts` が表を持つ)。
|
|
4260
|
+
*
|
|
4261
|
+
* 古い語 (`pk` / `fk` / `opt` / `entry` / `exit` / `do` / `internal`) はここで
|
|
4262
|
+
* 3 語へ読み替える (#2782)。 **読み替えは読み取りの入口 1 か所に閉じる** =
|
|
4263
|
+
* ここを通った後は古い語がどこにも残らないので、組み立て側は 3 語だけを扱う。
|
|
2854
4264
|
*
|
|
2855
4265
|
* 空の要素を捨てない (`filter(Boolean)` を掛けない) = 並びが `rows` とずれる。
|
|
2856
4266
|
*/
|
|
2857
4267
|
case "marks":
|
|
2858
4268
|
case "印":
|
|
2859
|
-
out.marks = raw
|
|
2860
|
-
.replace(/^\[|\]$/g, "")
|
|
2861
|
-
.split(/,(?![^[]*\])/)
|
|
2862
|
-
.map((x) => stripQuotes(x.trim()));
|
|
4269
|
+
out.marks = 行頭の印の並びとして読む(raw, ln.no, errors);
|
|
2863
4270
|
break;
|
|
2864
4271
|
case "位置":
|
|
2865
4272
|
case "pos": {
|
|
@@ -2907,6 +4314,11 @@ function applyContinuationLines(actor: DslActor, rest: Line[], errors: DslError[
|
|
|
2907
4314
|
case "posY":
|
|
2908
4315
|
out.posY = 数として読む(raw, "箱の posY", ln.no, errors);
|
|
2909
4316
|
break;
|
|
4317
|
+
case "at":
|
|
4318
|
+
case "点の位置":
|
|
4319
|
+
out.at = 四象限の座標として控える(raw);
|
|
4320
|
+
out.atPos = { line: ln.no };
|
|
4321
|
+
break;
|
|
2910
4322
|
// 位置のずらし (#1971)。 中括弧の形と同じ表で読み、片方だけ書いた時は残りを 0 とする
|
|
2911
4323
|
case "offsetX":
|
|
2912
4324
|
case "offsetY": {
|
|
@@ -2939,12 +4351,15 @@ function applyContinuationLines(actor: DslActor, rest: Line[], errors: DslError[
|
|
|
2939
4351
|
}
|
|
2940
4352
|
case "touchpoint":
|
|
2941
4353
|
case "opportunity":
|
|
4354
|
+
case "opportunityPosition":
|
|
2942
4355
|
case "owner":
|
|
2943
4356
|
case "end":
|
|
4357
|
+
case "stage":
|
|
4358
|
+
case "段階":
|
|
2944
4359
|
// **どちらに入れるかは block を読み終わるまで決まらない** (#1251 Round 1 の指摘)。
|
|
2945
4360
|
// パーツかどうかは `kind:` の行で決まり、それが後ろに書かれることもある。
|
|
2946
4361
|
// 倍率 (`scaleWritten`) と読めない項目名 (`unknownKeys`) が同じ理由で後回しにしている
|
|
2947
|
-
図種ごとの欄.set(key, stripQuotes(raw));
|
|
4362
|
+
図種ごとの欄.set(key === "段階" ? "stage" : key, stripQuotes(raw));
|
|
2948
4363
|
break;
|
|
2949
4364
|
case "lane":
|
|
2950
4365
|
out.lane = stripQuotes(raw);
|
|
@@ -2967,7 +4382,7 @@ function applyContinuationLines(actor: DslActor, rest: Line[], errors: DslError[
|
|
|
2967
4382
|
out.scale = s.scale;
|
|
2968
4383
|
out.scaleKeys = [...new Set([...(actor.scaleKeys ?? []), ...s.keys])];
|
|
2969
4384
|
}
|
|
2970
|
-
//
|
|
4385
|
+
// ユーザージャーニーの欄は、パーツなら状態の上書き、そうでなければ道筋の欄として入れる
|
|
2971
4386
|
for (const [key, v] of 図種ごとの欄) {
|
|
2972
4387
|
if (out.partId !== undefined) {
|
|
2973
4388
|
state[key] = coerceStateValue(v);
|
|
@@ -2976,8 +4391,20 @@ function applyContinuationLines(actor: DslActor, rest: Line[], errors: DslError[
|
|
|
2976
4391
|
}
|
|
2977
4392
|
if (key === "touchpoint") out.touchpoint = v;
|
|
2978
4393
|
else if (key === "opportunity") out.opportunity = v;
|
|
4394
|
+
else if (key === "opportunityPosition") {
|
|
4395
|
+
if ((JOURNEY_OPPORTUNITY_POSITION_VALUES as readonly string[]).includes(v)) {
|
|
4396
|
+
out.opportunityPosition = v as DslActor["opportunityPosition"];
|
|
4397
|
+
} else {
|
|
4398
|
+
errors.push({
|
|
4399
|
+
line: actor.pos.line,
|
|
4400
|
+
message: `opportunityPosition が読めません: "${v}"`,
|
|
4401
|
+
hint: `使える値 = ${JOURNEY_OPPORTUNITY_POSITION_VALUES.join(", ")}`,
|
|
4402
|
+
});
|
|
4403
|
+
}
|
|
4404
|
+
}
|
|
2979
4405
|
else if (key === "owner") out.owner = v;
|
|
2980
|
-
else out.end = v;
|
|
4406
|
+
else if (key === "end") out.end = v;
|
|
4407
|
+
else out.stage = v;
|
|
2981
4408
|
}
|
|
2982
4409
|
// 状態も倍率も parts でだけ意味を持つ。 パーツなら知らせずに返す
|
|
2983
4410
|
if (out.partId !== undefined) {
|
|
@@ -3017,7 +4444,7 @@ function applyContinuationLines(actor: DslActor, rest: Line[], errors: DslError[
|
|
|
3017
4444
|
errors.push({
|
|
3018
4445
|
line: u.line,
|
|
3019
4446
|
message: `項目名が読めません: "${u.key}"`,
|
|
3020
|
-
hint:
|
|
4447
|
+
hint: 弾いた項目の案内(u.key),
|
|
3021
4448
|
});
|
|
3022
4449
|
}
|
|
3023
4450
|
return out;
|
|
@@ -3033,6 +4460,22 @@ export const ACTOR_ITEM_KEYS: ReadonlySet<string> = new Set([
|
|
|
3033
4460
|
"kind",
|
|
3034
4461
|
"種類",
|
|
3035
4462
|
"subtitle",
|
|
4463
|
+
"kindForm",
|
|
4464
|
+
"icon",
|
|
4465
|
+
"emptyRowsNote",
|
|
4466
|
+
"titleAlign",
|
|
4467
|
+
"rowRules",
|
|
4468
|
+
"rowStripe",
|
|
4469
|
+
"rowLayout",
|
|
4470
|
+
"rowDividerForm",
|
|
4471
|
+
"rowWell",
|
|
4472
|
+
"rowMarkForm",
|
|
4473
|
+
"nestIn",
|
|
4474
|
+
"figureCard",
|
|
4475
|
+
"stationNamePosition",
|
|
4476
|
+
"subtitlePlacement",
|
|
4477
|
+
"titleFontSize",
|
|
4478
|
+
"markGap",
|
|
3036
4479
|
"補足",
|
|
3037
4480
|
"value",
|
|
3038
4481
|
"値",
|
|
@@ -3072,14 +4515,61 @@ export const ACTOR_ITEM_KEYS: ReadonlySet<string> = new Set([
|
|
|
3072
4515
|
"scale",
|
|
3073
4516
|
"lane",
|
|
3074
4517
|
"stack",
|
|
3075
|
-
|
|
4518
|
+
"stage",
|
|
4519
|
+
"段階",
|
|
4520
|
+
// ユーザージャーニーの欄 (#1251)
|
|
3076
4521
|
"touchpoint",
|
|
3077
4522
|
"opportunity",
|
|
4523
|
+
"opportunityPosition",
|
|
3078
4524
|
// 工程の並びの欄 (#1251)
|
|
3079
4525
|
"owner",
|
|
3080
4526
|
"end",
|
|
4527
|
+
"startLabel",
|
|
4528
|
+
"milestone",
|
|
4529
|
+
"emphasis",
|
|
4530
|
+
"at",
|
|
4531
|
+
"点の位置",
|
|
4532
|
+
// 箱の目次と、始まりと終わりの印 (#2346)。 中括弧にしか書き方が無かった 3 件
|
|
4533
|
+
"eyebrow",
|
|
4534
|
+
"initial",
|
|
4535
|
+
"final",
|
|
3081
4536
|
]);
|
|
3082
4537
|
|
|
4538
|
+
/**
|
|
4539
|
+
* 部品の箱にだけ効く項目 (#1026)。
|
|
4540
|
+
*
|
|
4541
|
+
* 普通の箱に書いた時は綴りの誤りとして知らせる。 黙って捨てると
|
|
4542
|
+
* 「書いたのに大きさが変わらない」 が手掛かりなしで起きるため。
|
|
4543
|
+
*/
|
|
4544
|
+
export const PARTS_ONLY_ITEM_KEYS: ReadonlySet<string> = new Set(["倍率", "scale"]);
|
|
4545
|
+
|
|
4546
|
+
/**
|
|
4547
|
+
* 項目名を弾いた時の案内 (#2330)。
|
|
4548
|
+
*
|
|
4549
|
+
* **いま弾いた名前を「使える項目」 として並べない**。 語彙には `倍率` と `scale` が
|
|
4550
|
+
* 入っているので、そのまま並べると読み手は同じ名前を書き直して同じ知らせを受ける
|
|
4551
|
+
* (実測 = 語彙のうち普通の箱で弾かれるのはこの 2 件だけ)。
|
|
4552
|
+
*
|
|
4553
|
+
* 部品にだけ効く項目は **どこでなら書けるか** を出す。 綴りの誤りは語彙を並べる =
|
|
4554
|
+
* そちらは弾いた名前が語彙に無いので食い違わない。
|
|
4555
|
+
*/
|
|
4556
|
+
function 弾いた項目の案内(key: string): string {
|
|
4557
|
+
if (PARTS_ONLY_ITEM_KEYS.has(key)) {
|
|
4558
|
+
return "`倍率` と `scale` は部品の箱 (`kind` に部品の名前を書いた箱) にだけ効きます";
|
|
4559
|
+
}
|
|
4560
|
+
/*
|
|
4561
|
+
* 1 行の中括弧でだけ読める名前 (#2346)。
|
|
4562
|
+
*
|
|
4563
|
+
* 語彙を並べるだけだと、**同じ名前が別の書き方で効くこと**が伝わらない =
|
|
4564
|
+
* 読み手は綴りを誤ったと受け取り、正しい名前を書いたまま直しようがなくなる。
|
|
4565
|
+
* #2344 で足した逆向きの案内 (中括弧で弾いた名前に段を分けた形を案内する) と対にする。
|
|
4566
|
+
*/
|
|
4567
|
+
if (INLINE_ACTOR_KEYS.has(key)) {
|
|
4568
|
+
return `段を分けた形では読めません。 \`- 名前: { ${key}: 値 }\` のように 1 行の中括弧に書くと効きます`;
|
|
4569
|
+
}
|
|
4570
|
+
return `使える項目 = ${[...ACTOR_ITEM_KEYS].filter((k) => k !== key).join(", ")}`;
|
|
4571
|
+
}
|
|
4572
|
+
|
|
3083
4573
|
/**
|
|
3084
4574
|
* 相対で書かれた位置が解けるかを確かめる。
|
|
3085
4575
|
*
|
|
@@ -3091,63 +4581,82 @@ export const ACTOR_ITEM_KEYS: ReadonlySet<string> = new Set([
|
|
|
3091
4581
|
*/
|
|
3092
4582
|
function validateRelativePositions(actors: DslActor[], errors: DslError[]): void {
|
|
3093
4583
|
const named = new Set(actors.map((a) => a.name));
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
for (const a of actors) {
|
|
3097
|
-
const rel = a.posRel;
|
|
3098
|
-
if (!rel) continue;
|
|
3099
|
-
if (rel.anchor === a.name) {
|
|
3100
|
-
errors.push({
|
|
3101
|
-
line: a.pos.line,
|
|
3102
|
-
message: `位置の基準が自分自身です: "${a.name}"`,
|
|
3103
|
-
hint: "別の登場人物の名前を書く",
|
|
3104
|
-
});
|
|
3105
|
-
broken.add(a.name);
|
|
3106
|
-
continue;
|
|
3107
|
-
}
|
|
3108
|
-
if (!named.has(rel.anchor)) {
|
|
3109
|
-
errors.push({
|
|
3110
|
-
line: a.pos.line,
|
|
3111
|
-
message: `位置の基準が見つかりません: "${rel.anchor}"`,
|
|
3112
|
-
hint:
|
|
3113
|
-
named.size > 0
|
|
3114
|
-
? `actors: に書かれている名前 = ${[...named].join(", ")}`
|
|
3115
|
-
: "actors: に基準にする登場人物を書く",
|
|
3116
|
-
});
|
|
3117
|
-
broken.add(a.name);
|
|
3118
|
-
}
|
|
3119
|
-
}
|
|
4584
|
+
// 解けない書き方の判定は `relative-pos.ts` が持つ。 JSON の入口も同じ判定を呼ぶ (#2039)
|
|
4585
|
+
const problems = findRelativeProblems(actors.map((a) => ({ name: a.name, rel: a.posRel })));
|
|
3120
4586
|
|
|
3121
|
-
const
|
|
3122
|
-
actors.
|
|
3123
|
-
);
|
|
3124
|
-
for (const name of cyclic) {
|
|
3125
|
-
const a = actors.find((x) => x.name === name);
|
|
4587
|
+
for (const p of problems) {
|
|
4588
|
+
const a = actors.find((x) => x.name === p.name);
|
|
3126
4589
|
errors.push({
|
|
3127
4590
|
line: a?.pos.line ?? 1,
|
|
3128
|
-
|
|
3129
|
-
hint: "どれか 1 つは座標 (`位置: 300,200`) か自動配置にする",
|
|
4591
|
+
...相対の誤り(p, named),
|
|
3130
4592
|
});
|
|
3131
|
-
broken.add(name);
|
|
3132
4593
|
}
|
|
3133
4594
|
|
|
4595
|
+
const broken = new Set(problems.map((p) => p.name));
|
|
3134
4596
|
for (const a of actors) {
|
|
3135
4597
|
if (broken.has(a.name)) a.posRel = undefined;
|
|
3136
4598
|
}
|
|
3137
4599
|
}
|
|
3138
4600
|
|
|
4601
|
+
/**
|
|
4602
|
+
* 解けない相対の指定に対する、記法側の誤りの文 (#2039)。
|
|
4603
|
+
*
|
|
4604
|
+
* 判定は `relative-pos.ts` が持ち、文はここが持つ。 JSON 側は欄の場所を添えた別の文を作る。
|
|
4605
|
+
*/
|
|
4606
|
+
function 相対の誤り(
|
|
4607
|
+
p: RelativeProblem,
|
|
4608
|
+
named: ReadonlySet<string>,
|
|
4609
|
+
): { message: string; hint: string } {
|
|
4610
|
+
switch (p.kind) {
|
|
4611
|
+
case "self":
|
|
4612
|
+
return {
|
|
4613
|
+
message: `位置の基準が自分自身です: "${p.name}"`,
|
|
4614
|
+
hint: "別の登場人物の名前を書く",
|
|
4615
|
+
};
|
|
4616
|
+
case "missing-anchor":
|
|
4617
|
+
return {
|
|
4618
|
+
message: `位置の基準が見つかりません: "${p.anchor ?? ""}"`,
|
|
4619
|
+
hint:
|
|
4620
|
+
named.size > 0
|
|
4621
|
+
? `actors: に書かれている名前 = ${[...named].join(", ")}`
|
|
4622
|
+
: "actors: に基準にする登場人物を書く",
|
|
4623
|
+
};
|
|
4624
|
+
case "cyclic":
|
|
4625
|
+
return {
|
|
4626
|
+
message: `位置の基準が互いを指しています: "${p.name}"`,
|
|
4627
|
+
hint: "どれか 1 つは座標 (`位置: 300,200`) か自動配置にする",
|
|
4628
|
+
};
|
|
4629
|
+
}
|
|
4630
|
+
}
|
|
4631
|
+
|
|
4632
|
+
/**
|
|
4633
|
+
* 登場人物を 1 件ずつ集める。 続く字下げ行は同じ 1 件にまとめる。
|
|
4634
|
+
*
|
|
4635
|
+
* 項目が少なければ 1 行で書け、 多ければ縦に並べられる。 縦に並べた方が、 何を指定できるかが
|
|
4636
|
+
* 見える。
|
|
4637
|
+
*
|
|
4638
|
+
* ```
|
|
4639
|
+
* - Client
|
|
4640
|
+
* - API: service
|
|
4641
|
+
* - Web:
|
|
4642
|
+
* kind: service
|
|
4643
|
+
* 色: 失敗
|
|
4644
|
+
* ```
|
|
4645
|
+
*/
|
|
3139
4646
|
function collectActorEntries(
|
|
3140
4647
|
lines: Line[],
|
|
3141
4648
|
start: number,
|
|
3142
4649
|
parentIndent: number,
|
|
3143
|
-
): { items: Line[][]; next: number } {
|
|
4650
|
+
): { items: Line[][]; 届かない: Line[]; next: number } {
|
|
3144
4651
|
const items: Line[][] = [];
|
|
4652
|
+
/** 1 件の頭にも続きにもならなかった行 (#2400)。 呼出側が知らせにする */
|
|
4653
|
+
const 届かない: Line[] = [];
|
|
3145
4654
|
let cur: Line[] | null = null;
|
|
3146
4655
|
let headIndent = -1;
|
|
3147
4656
|
let i = start;
|
|
3148
4657
|
while (i < lines.length) {
|
|
3149
4658
|
const ln = lines[i]!;
|
|
3150
|
-
if (!ln.trimmed) {
|
|
4659
|
+
if (!ln.trimmed || ln.trimmed.startsWith("#")) {
|
|
3151
4660
|
i += 1;
|
|
3152
4661
|
continue;
|
|
3153
4662
|
}
|
|
@@ -3159,25 +4668,34 @@ function collectActorEntries(
|
|
|
3159
4668
|
} else if (cur && ln.indent > headIndent) {
|
|
3160
4669
|
// 頭より深い字下げは、 直前の 1 件の続き
|
|
3161
4670
|
cur.push(ln);
|
|
4671
|
+
} else {
|
|
4672
|
+
// 続きにも頭にもならない行を **捨てない** (#2400)。 呼出側が「読めません」 として知らせる。
|
|
4673
|
+
// 捨てると `- ` を書き落とした 1 行が知らせにも図にも現れない (実測 = 箱が 1 つ消えた)
|
|
4674
|
+
//
|
|
4675
|
+
// **ここで 1 件の頭にはしない**。 `- Web:` と同じ深さに書いた `補足: "..."` が
|
|
4676
|
+
// 登場人物 `補足` になり、書き間違いが別の意味に化ける (#1367 の検査が守っている形)
|
|
4677
|
+
届かない.push(ln);
|
|
3162
4678
|
}
|
|
3163
4679
|
i += 1;
|
|
3164
4680
|
}
|
|
3165
4681
|
if (cur) items.push(cur);
|
|
3166
|
-
return { items, next: i };
|
|
4682
|
+
return { items, 届かない, next: i };
|
|
3167
4683
|
}
|
|
3168
4684
|
|
|
3169
4685
|
function collectAnimationSteps(
|
|
3170
4686
|
lines: Line[],
|
|
3171
4687
|
start: number,
|
|
3172
4688
|
parentIndent: number,
|
|
3173
|
-
): { items: Line[][]; next: number } {
|
|
4689
|
+
): { items: Line[][]; 届かない: Line[]; next: number } {
|
|
3174
4690
|
// 各 `- step: "..."` 開始を 1 block の頭として識別、 後続の同 indent 以下を block 本文として吸収
|
|
3175
4691
|
const out: Line[][] = [];
|
|
4692
|
+
/** どの段にも属さなかった行 (#2400)。 呼出側が知らせにする */
|
|
4693
|
+
const 届かない: Line[] = [];
|
|
3176
4694
|
let i = start;
|
|
3177
4695
|
let cur: Line[] | null = null;
|
|
3178
4696
|
while (i < lines.length) {
|
|
3179
4697
|
const ln = lines[i]!;
|
|
3180
|
-
if (!ln.trimmed) {
|
|
4698
|
+
if (!ln.trimmed || ln.trimmed.startsWith("#")) {
|
|
3181
4699
|
i += 1;
|
|
3182
4700
|
continue;
|
|
3183
4701
|
}
|
|
@@ -3191,11 +4709,15 @@ function collectAnimationSteps(
|
|
|
3191
4709
|
} else if (cur) {
|
|
3192
4710
|
// 続く property line (focus / tween / set / badge / description)
|
|
3193
4711
|
cur.push(ln);
|
|
4712
|
+
} else {
|
|
4713
|
+
// 最初の段が始まる前の行を **捨てない** (#2400)。 呼出側が「読めません」 として知らせる。
|
|
4714
|
+
// 捨てると `- ` を書き落とした段が丸ごと黙って消える
|
|
4715
|
+
届かない.push(ln);
|
|
3194
4716
|
}
|
|
3195
4717
|
i += 1;
|
|
3196
4718
|
}
|
|
3197
4719
|
if (cur) out.push(cur);
|
|
3198
|
-
return { items: out, next: i };
|
|
4720
|
+
return { items: out, 届かない, next: i };
|
|
3199
4721
|
}
|
|
3200
4722
|
|
|
3201
4723
|
/**
|
|
@@ -3207,6 +4729,18 @@ function collectAnimationSteps(
|
|
|
3207
4729
|
export const ACTOR_RESERVED_FIELDS: ReadonlySet<string> = new Set([
|
|
3208
4730
|
"kind",
|
|
3209
4731
|
"subtitle",
|
|
4732
|
+
"kindForm",
|
|
4733
|
+
"icon",
|
|
4734
|
+
"emptyRowsNote",
|
|
4735
|
+
"titleAlign",
|
|
4736
|
+
"rowRules",
|
|
4737
|
+
"rowStripe",
|
|
4738
|
+
"rowLayout",
|
|
4739
|
+
"rowDividerForm",
|
|
4740
|
+
"rowWell",
|
|
4741
|
+
"rowMarkForm",
|
|
4742
|
+
"nestIn",
|
|
4743
|
+
"figureCard",
|
|
3210
4744
|
"eyebrow",
|
|
3211
4745
|
"value",
|
|
3212
4746
|
"previous",
|
|
@@ -3214,10 +4748,15 @@ export const ACTOR_RESERVED_FIELDS: ReadonlySet<string> = new Set([
|
|
|
3214
4748
|
"marks",
|
|
3215
4749
|
"lane",
|
|
3216
4750
|
"stack",
|
|
4751
|
+
"stage",
|
|
3217
4752
|
"initial",
|
|
3218
4753
|
"final",
|
|
4754
|
+
"startLabel",
|
|
4755
|
+
"milestone",
|
|
4756
|
+
"emphasis",
|
|
4757
|
+
"at",
|
|
3219
4758
|
"state",
|
|
3220
|
-
//
|
|
4759
|
+
// ユーザージャーニーの欄 (`touchpoint` / `opportunity`) はここに載せない (#1251 Round 1 の指摘)。
|
|
3221
4760
|
// 載せるとパーツで同じ名前の状態を書いた時に横取りされる = 既に動いている見本が静かに変わる。
|
|
3222
4761
|
// パーツでない箱でだけ道筋の欄として読む (`parseActor` / `applyContinuationLines` が分岐する)
|
|
3223
4762
|
// canvas pivot 新 spec = 絶対座標 4 field (dragon canvas pivot spec §layout-role-conversion)
|
|
@@ -3279,12 +4818,33 @@ function coerceStateValue(raw: string): number | string | boolean {
|
|
|
3279
4818
|
return stripped;
|
|
3280
4819
|
}
|
|
3281
4820
|
|
|
4821
|
+
/** 四象限の座標を `[x, y]` の順で控える。 妥当性は CompileNotice を出せる組み立て側で見る。 */
|
|
4822
|
+
function 四象限の座標として控える(raw: string): DslActor["at"] {
|
|
4823
|
+
const value = raw.trim();
|
|
4824
|
+
const inner = value.startsWith("[") && value.endsWith("]") ? value.slice(1, -1) : value;
|
|
4825
|
+
const parts = splitTopLevelCommas(inner).map((part) => stripQuotes(part.trim()));
|
|
4826
|
+
const 読む = (part: string | undefined): number | string | undefined => {
|
|
4827
|
+
if (part === undefined || part === "") return undefined;
|
|
4828
|
+
const n = Number(part);
|
|
4829
|
+
return Number.isFinite(n) ? n : part;
|
|
4830
|
+
};
|
|
4831
|
+
return { x: 読む(parts[0]), y: 読む(parts[1]), raw: value };
|
|
4832
|
+
}
|
|
4833
|
+
|
|
3282
4834
|
/**
|
|
3283
4835
|
* パーツでない箱に倍率を書いた時に知らせる (#1026)。
|
|
3284
4836
|
*
|
|
3285
4837
|
* 倍率はパーツにしか効かない。 黙って捨てると「書いたのに大きさが変わらない」 が手掛かり
|
|
3286
4838
|
* なしで起きる。 3 つの書き方すべてで同じ知らせを出す (縦に並べた形だけ知らせて他が黙る、
|
|
3287
4839
|
* という状態を作らない)。
|
|
4840
|
+
*
|
|
4841
|
+
* **案内に語彙を並べない** (#2330)。 語彙には `倍率` と `scale` が入っているので、
|
|
4842
|
+
* 並べると **いま弾いた名前を「使える項目」 として出す** ことになる。 読み手は同じ名前を
|
|
4843
|
+
* 書き直して同じ知らせを受ける (実測 = 案内の 39 項目のうち、弾かれるのはこの 2 件だけ)。
|
|
4844
|
+
*
|
|
4845
|
+
* 弾く理由はここが知っている (`isPart === false`) ので、**どこでなら書けるか** を出す。
|
|
4846
|
+
* 綴りの誤りを知らせる側 (`unknownKeys`) は語彙を並べたままにする = そちらは弾いた名前が
|
|
4847
|
+
* 語彙に無いため食い違わない。
|
|
3288
4848
|
*/
|
|
3289
4849
|
function reportScaleOnNonPart(
|
|
3290
4850
|
isPart: boolean,
|
|
@@ -3296,53 +4856,39 @@ function reportScaleOnNonPart(
|
|
|
3296
4856
|
errors.push({
|
|
3297
4857
|
line,
|
|
3298
4858
|
message: `項目名が読めません: "${key}"`,
|
|
3299
|
-
hint:
|
|
4859
|
+
hint: 弾いた項目の案内(key),
|
|
3300
4860
|
});
|
|
3301
4861
|
}
|
|
3302
4862
|
|
|
3303
4863
|
/**
|
|
3304
|
-
*
|
|
3305
|
-
*
|
|
3306
|
-
* ここに無い名前は、 パーツでない箱ではどこにも入らずに消える。 `ACTOR_ITEM_KEYS` (縦に
|
|
3307
|
-
* 並べた形) とは別に持つ =
|
|
3308
|
-
*
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
*
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
* (`pos` / `size` / `color`) も中括弧では読めないため載せない = 英語で出来ないことを
|
|
3315
|
-
* 日本語で出来るようにはしない。
|
|
3316
|
-
*
|
|
3317
|
-
* 載せる前は、同じ意味の語が縦書きでは通り中括弧では「項目名が読めません」 になっていた。
|
|
3318
|
-
* 書き方によって日本語だけが落ちる状態を無くす。
|
|
3319
|
-
*/
|
|
3320
|
-
export const INLINE_ACTOR_ALIASES: Record<string, string> = {
|
|
3321
|
-
種類: "kind",
|
|
3322
|
-
補足: "subtitle",
|
|
3323
|
-
値: "value",
|
|
3324
|
-
前の値: "previous",
|
|
3325
|
-
行: "rows",
|
|
3326
|
-
印: "marks",
|
|
3327
|
-
};
|
|
3328
|
-
|
|
3329
|
-
/** 中括弧に書かれた日本語の項目名を、同じ意味の英語名に寄せる (#1301) */
|
|
3330
|
-
function 中括弧の別名を寄せる(opts: Record<string, string>): Record<string, string> {
|
|
3331
|
-
let 触った = false;
|
|
3332
|
-
const out: Record<string, string> = { ...opts };
|
|
3333
|
-
for (const [日, 英] of Object.entries(INLINE_ACTOR_ALIASES)) {
|
|
3334
|
-
if (!(日 in out)) continue;
|
|
3335
|
-
触った = true;
|
|
3336
|
-
// 英語を併記した時は英語を優先する (縦書き形が後勝ちなのと違い、こちらは 1 行に同居する)
|
|
3337
|
-
if (!(英 in out)) out[英] = out[日]!;
|
|
3338
|
-
delete out[日];
|
|
3339
|
-
}
|
|
3340
|
-
return 触った ? out : opts;
|
|
3341
|
-
}
|
|
3342
|
-
|
|
3343
|
-
export const INLINE_ACTOR_KEYS: ReadonlySet<string> = new Set([
|
|
4864
|
+
* 中括弧の形で読める英語の項目名 (#2344 で `INLINE_ACTOR_KEYS` から分けた)。
|
|
4865
|
+
*
|
|
4866
|
+
* ここに無い名前は、 パーツでない箱ではどこにも入らずに消える。 `ACTOR_ITEM_KEYS` (縦に
|
|
4867
|
+
* 並べた形) とは別に持つ = 中括弧の形は位置や大きさ (`pos` / `size` / `scale`) を未対応にしてあり、
|
|
4868
|
+
* 同じ集合にすると「知らせない」 側がずれる。
|
|
4869
|
+
*
|
|
4870
|
+
* **日本語の別名を混ぜない**。 別名は `ACTOR_ITEM_ALIASES` をこの集合で絞って導くため、
|
|
4871
|
+
* 混ぜると導く側と導かれる側が輪になる。 日本語も含む一覧は `INLINE_ACTOR_KEYS` が持つ。
|
|
4872
|
+
*/
|
|
4873
|
+
const INLINE_ACTOR_ENGLISH_KEYS: ReadonlySet<string> = new Set([
|
|
3344
4874
|
"kind",
|
|
3345
4875
|
"subtitle",
|
|
4876
|
+
"kindForm",
|
|
4877
|
+
"icon",
|
|
4878
|
+
"emptyRowsNote",
|
|
4879
|
+
"titleAlign",
|
|
4880
|
+
"rowRules",
|
|
4881
|
+
"rowStripe",
|
|
4882
|
+
"rowLayout",
|
|
4883
|
+
"rowDividerForm",
|
|
4884
|
+
"rowWell",
|
|
4885
|
+
"rowMarkForm",
|
|
4886
|
+
"nestIn",
|
|
4887
|
+
"figureCard",
|
|
4888
|
+
"stationNamePosition",
|
|
4889
|
+
"subtitlePlacement",
|
|
4890
|
+
"titleFontSize",
|
|
4891
|
+
"markGap",
|
|
3346
4892
|
"eyebrow",
|
|
3347
4893
|
"value",
|
|
3348
4894
|
"previous",
|
|
@@ -3350,18 +4896,24 @@ export const INLINE_ACTOR_KEYS: ReadonlySet<string> = new Set([
|
|
|
3350
4896
|
"marks",
|
|
3351
4897
|
"lane",
|
|
3352
4898
|
"stack",
|
|
4899
|
+
"stage",
|
|
3353
4900
|
"initial",
|
|
3354
4901
|
"final",
|
|
3355
4902
|
"tone",
|
|
3356
4903
|
// 色の欄 (#1969)。 縦に並べた形と JSON は `color` を受けるのに、中括弧の形だけが知らない
|
|
3357
4904
|
// 項目名として落としていた。 見本 (parts) では従来どおり状態の上書きとして読む
|
|
3358
4905
|
"color",
|
|
3359
|
-
//
|
|
4906
|
+
// ユーザージャーニーの欄 (#1251)。 他の図種では組み立て側が知らせる
|
|
3360
4907
|
"touchpoint",
|
|
3361
4908
|
"opportunity",
|
|
4909
|
+
"opportunityPosition",
|
|
3362
4910
|
// 工程の並びの欄 (#1251)
|
|
3363
4911
|
"owner",
|
|
3364
4912
|
"end",
|
|
4913
|
+
"startLabel",
|
|
4914
|
+
"milestone",
|
|
4915
|
+
"emphasis",
|
|
4916
|
+
"at",
|
|
3365
4917
|
"posX",
|
|
3366
4918
|
"posY",
|
|
3367
4919
|
"posW",
|
|
@@ -3385,7 +4937,72 @@ export const INLINE_ACTOR_KEYS: ReadonlySet<string> = new Set([
|
|
|
3385
4937
|
// 同じ名前で 2 度知らせることになる
|
|
3386
4938
|
"scale",
|
|
3387
4939
|
"倍率",
|
|
3388
|
-
|
|
4940
|
+
]);
|
|
4941
|
+
|
|
4942
|
+
/**
|
|
4943
|
+
* 箱の項目名の、日本語と英語の対応 (#1301 / #2344)。
|
|
4944
|
+
*
|
|
4945
|
+
* **段を分けた形が読む日本語名をすべて載せる**。 中括弧で読めるかはここでは決めず、
|
|
4946
|
+
* `INLINE_ACTOR_ALIASES` が英語側の可否で絞る。
|
|
4947
|
+
*
|
|
4948
|
+
* 載せる場所を 1 つにしたのは、**中括弧の別名を別に手で並べていた間、英語の欄を
|
|
4949
|
+
* 中括弧に足した日に日本語名だけが取り残された**ため (`title` は #1381、`tone` は #1969、
|
|
4950
|
+
* `shape` は #1374 で足り、日本語名は 3 件とも置き去りになっていた)。
|
|
4951
|
+
*
|
|
4952
|
+
* `倍率` は箱の欄を持たない (部品にだけ効く、#1026)。 ここには載せず、
|
|
4953
|
+
* `PARTS_ONLY_ITEM_KEYS` と `reportScaleOnNonPart` が受け持つ。
|
|
4954
|
+
*/
|
|
4955
|
+
export const ACTOR_ITEM_ALIASES: Record<string, string> = {
|
|
4956
|
+
種類: "kind",
|
|
4957
|
+
題: "title",
|
|
4958
|
+
補足: "subtitle",
|
|
4959
|
+
色: "tone",
|
|
4960
|
+
行: "rows",
|
|
4961
|
+
印: "marks",
|
|
4962
|
+
大きさ: "size",
|
|
4963
|
+
図形: "shape",
|
|
4964
|
+
値: "value",
|
|
4965
|
+
前の値: "previous",
|
|
4966
|
+
位置: "pos",
|
|
4967
|
+
出す条件: "visibleIf",
|
|
4968
|
+
段階: "stage",
|
|
4969
|
+
点の位置: "at",
|
|
4970
|
+
};
|
|
4971
|
+
|
|
4972
|
+
/**
|
|
4973
|
+
* 中括弧の形で読める日本語と、その英語名 (#1301 / #2344)。
|
|
4974
|
+
*
|
|
4975
|
+
* **英語が中括弧で読める欄だけに絞る** = 英語で出来ないことを日本語で出来るようにはしない。
|
|
4976
|
+
* `位置` (`pos`) と `大きさ` (`size`) はここで落ちる。
|
|
4977
|
+
*
|
|
4978
|
+
* **絞り込みは実装から導く**。 手で並べていた間、`題` / `色` / `図形` / `出す条件` の 4 件が
|
|
4979
|
+
* 英語側は中括弧で読めるのに日本語だけ落ちていた (実測)。
|
|
4980
|
+
*/
|
|
4981
|
+
export const INLINE_ACTOR_ALIASES: Record<string, string> = Object.fromEntries(
|
|
4982
|
+
Object.entries(ACTOR_ITEM_ALIASES).filter(([, 英]) => INLINE_ACTOR_ENGLISH_KEYS.has(英)),
|
|
4983
|
+
);
|
|
4984
|
+
|
|
4985
|
+
/** 中括弧に書かれた日本語の項目名を、同じ意味の英語名に寄せる (#1301) */
|
|
4986
|
+
function 中括弧の別名を寄せる(opts: Record<string, string>): Record<string, string> {
|
|
4987
|
+
let 触った = false;
|
|
4988
|
+
const out: Record<string, string> = { ...opts };
|
|
4989
|
+
for (const [日, 英] of Object.entries(INLINE_ACTOR_ALIASES)) {
|
|
4990
|
+
if (!(日 in out)) continue;
|
|
4991
|
+
触った = true;
|
|
4992
|
+
// 英語を併記した時は英語を優先する (縦書き形が後勝ちなのと違い、こちらは 1 行に同居する)
|
|
4993
|
+
if (!(英 in out)) out[英] = out[日]!;
|
|
4994
|
+
delete out[日];
|
|
4995
|
+
}
|
|
4996
|
+
return 触った ? out : opts;
|
|
4997
|
+
}
|
|
4998
|
+
|
|
4999
|
+
/**
|
|
5000
|
+
* 中括弧の形で読める項目名 (英語と、英語が読める欄の日本語別名)。
|
|
5001
|
+
*
|
|
5002
|
+
* 別名は `INLINE_ACTOR_ALIASES` が `ACTOR_ITEM_ALIASES` から導く (#1301 / #2344)。
|
|
5003
|
+
*/
|
|
5004
|
+
export const INLINE_ACTOR_KEYS: ReadonlySet<string> = new Set([
|
|
5005
|
+
...INLINE_ACTOR_ENGLISH_KEYS,
|
|
3389
5006
|
...Object.keys(INLINE_ACTOR_ALIASES),
|
|
3390
5007
|
]);
|
|
3391
5008
|
// `state` はパーツでだけ意味を持つ (`extractStateOverride` がパーツの時しか作らない)。
|
|
@@ -3428,7 +5045,7 @@ const EDGE_BIND_INLINE_READERS = {
|
|
|
3428
5045
|
|
|
3429
5046
|
const FLOW_INLINE_READERS = {
|
|
3430
5047
|
sub: (v: string | undefined) => v,
|
|
3431
|
-
//
|
|
5048
|
+
// UML の関係の出どころ側の多重度 (#1771 / #2783)。 `sub` (行き先の端) と対
|
|
3432
5049
|
tailSub: (v: string | undefined) => v,
|
|
3433
5050
|
guard: (v: string | undefined) => v,
|
|
3434
5051
|
cardinality: (v: string | undefined) => v,
|
|
@@ -3449,11 +5066,24 @@ const FLOW_INLINE_READERS = {
|
|
|
3449
5066
|
*/
|
|
3450
5067
|
labelPlate: (v: string | undefined) =>
|
|
3451
5068
|
v === undefined ? undefined : v !== "false" && v !== "なし",
|
|
5069
|
+
labelCollision: (v: string | undefined) =>
|
|
5070
|
+
v !== undefined && (LABEL_COLLISION_VALUES as readonly string[]).includes(v)
|
|
5071
|
+
? (v as "auto" | "author")
|
|
5072
|
+
: undefined,
|
|
5073
|
+
labelSize: (v: string | undefined) => v,
|
|
3452
5074
|
// 矢印がどの辺から出るか (#1385)。 描画側は 4 方向を取り、書かなければ自動で選ぶ
|
|
3453
5075
|
side: (v: string | undefined) =>
|
|
3454
5076
|
v !== undefined && (EDGE_SIDE_VALUES as readonly string[]).includes(v)
|
|
3455
5077
|
? (v as "top" | "right" | "bottom" | "left")
|
|
3456
5078
|
: undefined,
|
|
5079
|
+
fromSide: (v: string | undefined) =>
|
|
5080
|
+
v !== undefined && (EDGE_SIDE_VALUES as readonly string[]).includes(v)
|
|
5081
|
+
? (v as "top" | "right" | "bottom" | "left")
|
|
5082
|
+
: undefined,
|
|
5083
|
+
toSide: (v: string | undefined) =>
|
|
5084
|
+
v !== undefined && (EDGE_SIDE_VALUES as readonly string[]).includes(v)
|
|
5085
|
+
? (v as "top" | "right" | "bottom" | "left")
|
|
5086
|
+
: undefined,
|
|
3457
5087
|
// 矢印の先の形 (#1462)。 書かなければ描画側の既定 (塗った三角) になる
|
|
3458
5088
|
head: (v: string | undefined) =>
|
|
3459
5089
|
v !== undefined && EDGE_HEAD_VALUES.includes(v) ? (v as EdgeHead) : undefined,
|
|
@@ -3466,7 +5096,7 @@ const FLOW_INLINE_READERS = {
|
|
|
3466
5096
|
tailHeadFill: (v: string | undefined) =>
|
|
3467
5097
|
v !== undefined && EDGE_HEAD_FILL_VALUES.includes(v) ? (v as EdgeHeadFill) : undefined,
|
|
3468
5098
|
/*
|
|
3469
|
-
*
|
|
5099
|
+
* record の UML 関係の種類 (#1466 / #2783)。書くと **線と端の形と塗りと付く側** がまとめて決まる
|
|
3470
5100
|
* (`CLASS_RELATION_LOOK`)。
|
|
3471
5101
|
*
|
|
3472
5102
|
* 4 つを個別に書かせない = 組合せは 6 通りしか無く、1 つでも書き違えると読み手に別の意味で
|
|
@@ -3494,10 +5124,15 @@ const FLOW_INLINE_READERS = {
|
|
|
3494
5124
|
*/
|
|
3495
5125
|
fromPartNode: (v: string | undefined) => v,
|
|
3496
5126
|
toPartNode: (v: string | undefined) => v,
|
|
5127
|
+
waypoints: (v: string | undefined) => v,
|
|
3497
5128
|
// 数と真偽の欄は `FLOW_INLINE_VALUE_KINDS` の表が読む (#1306)。 ここでは名前だけを持つ =
|
|
3498
5129
|
// 読める欄の一覧 (`FLOW_INLINE_KEYS`) は本表から導くため、載せないと欄ごと消える
|
|
3499
5130
|
labelOffsetX: null,
|
|
3500
5131
|
labelOffsetY: null,
|
|
5132
|
+
posX1: null,
|
|
5133
|
+
posY1: null,
|
|
5134
|
+
posX2: null,
|
|
5135
|
+
posY2: null,
|
|
3501
5136
|
overlay: null,
|
|
3502
5137
|
} as const;
|
|
3503
5138
|
|
|
@@ -3534,7 +5169,7 @@ function reportUnknownInlineKeys(
|
|
|
3534
5169
|
for (const field of splitInlineFields(inner)) {
|
|
3535
5170
|
const idx = field.indexOf(":");
|
|
3536
5171
|
if (idx < 0) continue;
|
|
3537
|
-
const key = field.slice(0, idx).trim();
|
|
5172
|
+
const key = stripQuotes(field.slice(0, idx).trim());
|
|
3538
5173
|
if (!key) continue;
|
|
3539
5174
|
if (isPart) {
|
|
3540
5175
|
if (!PART_INLINE_UNREADABLE_KEYS.has(key)) continue;
|
|
@@ -3548,10 +5183,23 @@ function reportUnknownInlineKeys(
|
|
|
3548
5183
|
continue;
|
|
3549
5184
|
}
|
|
3550
5185
|
if (INLINE_ACTOR_KEYS.has(key)) continue;
|
|
5186
|
+
/*
|
|
5187
|
+
* **段を分けて書けば効く名前には、その行き先を添える** (#2344)。
|
|
5188
|
+
*
|
|
5189
|
+
* `位置` / `大きさ` は段を分けた形では読める (英語の `pos` / `size` も同じ)。
|
|
5190
|
+
* 使える項目の一覧だけを返すと、書いた人はその名前を諦めて別の書き方を探すことになる。
|
|
5191
|
+
* 部品の箱は同じ状況を #1996 で既にこの文言で知らせており、こちらだけが外れていた。
|
|
5192
|
+
*
|
|
5193
|
+
* 判定は段を分けた形が読む一覧 (`ACTOR_ITEM_KEYS`) で行う = 日本語と英語を分けない。
|
|
5194
|
+
* `size` を中括弧に書いた人も、同じ行き先を知りたい。
|
|
5195
|
+
*/
|
|
5196
|
+
const 段を分ければ効く = ACTOR_ITEM_KEYS.has(key);
|
|
3551
5197
|
errors.push({
|
|
3552
5198
|
line,
|
|
3553
5199
|
message: `項目名が読めません: "${key}"`,
|
|
3554
|
-
hint:
|
|
5200
|
+
hint: 段を分ければ効く
|
|
5201
|
+
? `1 行の中括弧では読めません。名前の下に段を分けた形で書くと効きます`
|
|
5202
|
+
: `使える項目 = ${[...INLINE_ACTOR_KEYS].join(", ")}`,
|
|
3555
5203
|
});
|
|
3556
5204
|
}
|
|
3557
5205
|
}
|
|
@@ -3579,7 +5227,7 @@ function 中括弧の知らない項目名を知らせる(
|
|
|
3579
5227
|
for (const field of splitInlineFields(inner)) {
|
|
3580
5228
|
const idx = field.indexOf(":");
|
|
3581
5229
|
if (idx < 0) continue;
|
|
3582
|
-
const key = field.slice(0, idx).trim();
|
|
5230
|
+
const key = stripQuotes(field.slice(0, idx).trim());
|
|
3583
5231
|
if (!key || 使える.includes(key)) continue;
|
|
3584
5232
|
// 接頭は値の欄名に続ける形 (`縦列 l1 の `) で末尾に空白を持つ。 日本語へ続ける時は空白を落とす
|
|
3585
5233
|
errors.push({
|
|
@@ -3623,6 +5271,40 @@ function parseActor(line: Line, errors: DslError[]): DslActor | null {
|
|
|
3623
5271
|
if (!isPart && opts.tone !== undefined && resolveTone(opts.tone) === undefined) {
|
|
3624
5272
|
report読めない色(opts.tone, line.no, errors, { 線種も受ける: false });
|
|
3625
5273
|
}
|
|
5274
|
+
if (!isPart && opts.emphasis !== undefined && opts.emphasis !== "primary") {
|
|
5275
|
+
errors.push({
|
|
5276
|
+
line: line.no,
|
|
5277
|
+
message: `箱の emphasis が読めません: "${opts.emphasis}"`,
|
|
5278
|
+
hint: "使える値 = primary",
|
|
5279
|
+
});
|
|
5280
|
+
}
|
|
5281
|
+
if (
|
|
5282
|
+
!isPart && opts.opportunityPosition !== undefined &&
|
|
5283
|
+
!(JOURNEY_OPPORTUNITY_POSITION_VALUES as readonly string[]).includes(opts.opportunityPosition)
|
|
5284
|
+
) {
|
|
5285
|
+
errors.push({
|
|
5286
|
+
line: line.no,
|
|
5287
|
+
message: `opportunityPosition が読めません: "${opts.opportunityPosition}"`,
|
|
5288
|
+
hint: `使える値 = ${JOURNEY_OPPORTUNITY_POSITION_VALUES.join(", ")}`,
|
|
5289
|
+
});
|
|
5290
|
+
}
|
|
5291
|
+
const 閉じた箱の値 = <T extends string>(
|
|
5292
|
+
key: string,
|
|
5293
|
+
values: readonly T[],
|
|
5294
|
+
): T | undefined => {
|
|
5295
|
+
const value = opts[key];
|
|
5296
|
+
if (value === undefined || isPart) return undefined;
|
|
5297
|
+
if ((values as readonly string[]).includes(value)) return value as T;
|
|
5298
|
+
errors.push({ line: line.no, message: `箱の ${key} が読めません: "${value}"`, hint: `使える値 = ${values.join(", ")}` });
|
|
5299
|
+
return undefined;
|
|
5300
|
+
};
|
|
5301
|
+
let actorFigureCard: DslActor["figureCard"];
|
|
5302
|
+
if (!isPart && opts.figureCard !== undefined) {
|
|
5303
|
+
const value = JSONの欄を読む(opts.figureCard, "figureCard", line.no, errors);
|
|
5304
|
+
const unknown = 図表の知らない項目を知らせる(value, ["label", "note"] as const, "figureCard", line.no, errors);
|
|
5305
|
+
if (!unknown && objectか(value) && typeof value.label === "string" && (value.note === undefined || typeof value.note === "string")) actorFigureCard = value as NonNullable<DslActor["figureCard"]>;
|
|
5306
|
+
else if (!unknown && value !== undefined) errors.push({ line: line.no, message: "figureCard の値の形が読めません" });
|
|
5307
|
+
}
|
|
3626
5308
|
// `color` は縦に並べた形と JSON と同じ振り分けを通す (#1969)。 色の名前は箱の色、`#` で
|
|
3627
5309
|
// 始まる値は色番号になる。
|
|
3628
5310
|
//
|
|
@@ -3643,13 +5325,43 @@ function parseActor(line: Line, errors: DslError[]): DslActor | null {
|
|
|
3643
5325
|
// 「書かなかった」 と同じ扱いにする (#1058)
|
|
3644
5326
|
kindWritten: kindRaw !== "" && !isPart,
|
|
3645
5327
|
subtitle: opts.subtitle,
|
|
5328
|
+
kindForm: 閉じた箱の値("kindForm", KIND_FORM_VALUES),
|
|
5329
|
+
icon: 閉じた箱の値("icon", CDL_ICON_VALUES),
|
|
5330
|
+
emptyRowsNote: isPart ? undefined : opts.emptyRowsNote,
|
|
5331
|
+
titleAlign: 閉じた箱の値("titleAlign", TITLE_ALIGN_VALUES),
|
|
5332
|
+
rowMarkForm: 閉じた箱の値("rowMarkForm", ROW_MARK_FORM_VALUES),
|
|
5333
|
+
rowDividerForm: 閉じた箱の値("rowDividerForm", ROW_DIVIDER_FORM_VALUES),
|
|
5334
|
+
rowLayout:
|
|
5335
|
+
isPart || opts.rowLayout === undefined
|
|
5336
|
+
? undefined
|
|
5337
|
+
: 行の配置として読む(opts.rowLayout, line.no, errors),
|
|
5338
|
+
rowWell:
|
|
5339
|
+
isPart || opts.rowWell === undefined
|
|
5340
|
+
? undefined
|
|
5341
|
+
: 行の受け皿として読む(opts.rowWell, line.no, errors),
|
|
5342
|
+
nestIn: isPart ? undefined : opts.nestIn,
|
|
5343
|
+
figureCard: actorFigureCard,
|
|
5344
|
+
stationNamePosition:
|
|
5345
|
+
opts.stationNamePosition === "bottom" ? opts.stationNamePosition : undefined,
|
|
5346
|
+
subtitlePlacement:
|
|
5347
|
+
opts.subtitlePlacement === "right" ? opts.subtitlePlacement : undefined,
|
|
3646
5348
|
eyebrow: opts.eyebrow,
|
|
3647
5349
|
// パーツでは状態の上書きとして意味を持つため、道筋の欄として横取りしない (#1251)
|
|
3648
5350
|
touchpoint: isPart ? undefined : opts.touchpoint,
|
|
3649
5351
|
opportunity: isPart ? undefined : opts.opportunity,
|
|
5352
|
+
opportunityPosition:
|
|
5353
|
+
isPart || opts.opportunityPosition === undefined ||
|
|
5354
|
+
!(JOURNEY_OPPORTUNITY_POSITION_VALUES as readonly string[]).includes(opts.opportunityPosition)
|
|
5355
|
+
? undefined
|
|
5356
|
+
: opts.opportunityPosition as DslActor["opportunityPosition"],
|
|
3650
5357
|
// 見本では状態の上書きとして意味を持つため横取りしない (#1251)
|
|
3651
5358
|
owner: isPart ? undefined : opts.owner,
|
|
3652
5359
|
end: isPart ? undefined : opts.end,
|
|
5360
|
+
startLabel: isPart ? undefined : opts.startLabel,
|
|
5361
|
+
emphasis: isPart || opts.emphasis !== "primary" ? undefined : "primary",
|
|
5362
|
+
at: isPart || opts.at === undefined ? undefined : 四象限の座標として控える(opts.at),
|
|
5363
|
+
atPos: isPart || opts.at === undefined ? undefined : { line: line.no },
|
|
5364
|
+
stage: isPart ? undefined : opts.stage,
|
|
3653
5365
|
value: opts.value,
|
|
3654
5366
|
previous: opts.previous,
|
|
3655
5367
|
rows: opts.rows
|
|
@@ -3659,12 +5371,11 @@ function parseActor(line: Line, errors: DslError[]): DslActor | null {
|
|
|
3659
5371
|
.map((x) => stripQuotes(x.trim()))
|
|
3660
5372
|
.filter(Boolean)
|
|
3661
5373
|
: undefined,
|
|
3662
|
-
// 行頭の印 (#1466)。 `rows` と同じ並びなので
|
|
5374
|
+
// 行頭の印 (#1466)。 `rows` と同じ並びなので **空の要素を捨てない**。
|
|
5375
|
+
// 古い語の読み替えは縦に並べた形と同じ関数を通す (#2782) = 片方だけ通すと、
|
|
5376
|
+
// 中括弧で書いた `pk` が印を持たないまま通る
|
|
3663
5377
|
marks: opts.marks
|
|
3664
|
-
? opts.marks
|
|
3665
|
-
.replace(/^\[|\]$/g, "")
|
|
3666
|
-
.split(/,(?![^[]*\])/)
|
|
3667
|
-
.map((x) => stripQuotes(x.trim()))
|
|
5378
|
+
? 行頭の印の並びとして読む(opts.marks, line.no, errors)
|
|
3668
5379
|
: undefined,
|
|
3669
5380
|
lane: opts.lane,
|
|
3670
5381
|
// 箱の中に描く図形 (#1374)。 パーツでは状態の上書きとして意味を持つため横取りしない
|
|
@@ -3795,12 +5506,17 @@ function parseFlowStep(line: Line, no: number, errors: DslError[]): DslStep | nu
|
|
|
3795
5506
|
if (読み手 === null) continue;
|
|
3796
5507
|
中括弧[k] = 読み手(opts[k]);
|
|
3797
5508
|
}
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
5509
|
+
for (const field of ["side", "fromSide", "toSide"] as const) {
|
|
5510
|
+
if (opts[field] !== undefined && 中括弧[field] === undefined) {
|
|
5511
|
+
errors.push({
|
|
5512
|
+
line: line.no,
|
|
5513
|
+
message: `矢印の ${field} が読めません: "${opts[field]}"`,
|
|
5514
|
+
hint: `使える値 = ${EDGE_SIDE_VALUES.join(", ")}`,
|
|
5515
|
+
});
|
|
5516
|
+
}
|
|
5517
|
+
}
|
|
5518
|
+
if (opts.labelCollision !== undefined && 中括弧.labelCollision === undefined) {
|
|
5519
|
+
errors.push({ line: line.no, message: `矢印の labelCollision が読めません: "${opts.labelCollision}"`, hint: `使える値 = ${LABEL_COLLISION_VALUES.join(", ")}` });
|
|
3804
5520
|
}
|
|
3805
5521
|
// 読めない語を黙って捨てない (#1462)。 捨てると「書いたのに端の形が変わらない」 が
|
|
3806
5522
|
// 手掛かりなしで起きる
|
|
@@ -3830,6 +5546,8 @@ function parseFlowStep(line: Line, no: number, errors: DslError[]): DslStep | nu
|
|
|
3830
5546
|
const guard = 中括弧.guard as string | undefined;
|
|
3831
5547
|
const cardinality = 中括弧.cardinality as string | undefined;
|
|
3832
5548
|
const side = 中括弧.side as "top" | "right" | "bottom" | "left" | undefined;
|
|
5549
|
+
const fromSide = 中括弧.fromSide as "top" | "right" | "bottom" | "left" | undefined;
|
|
5550
|
+
const toSide = 中括弧.toSide as "top" | "right" | "bottom" | "left" | undefined;
|
|
3833
5551
|
const head = 中括弧.head as EdgeHead | undefined;
|
|
3834
5552
|
const tailHead = 中括弧.tailHead as EdgeHead | undefined;
|
|
3835
5553
|
const headFill = 中括弧.headFill as EdgeHeadFill | undefined;
|
|
@@ -3839,6 +5557,19 @@ function parseFlowStep(line: Line, no: number, errors: DslError[]): DslStep | nu
|
|
|
3839
5557
|
// 辺の役目と名前の下地 (cdl#618)
|
|
3840
5558
|
const role = 中括弧.role as "main" | undefined;
|
|
3841
5559
|
const labelPlate = 中括弧.labelPlate as boolean | undefined;
|
|
5560
|
+
const labelCollision = 中括弧.labelCollision as DslStep["labelCollision"];
|
|
5561
|
+
let labelSize: DslStep["labelSize"];
|
|
5562
|
+
if (中括弧.labelSize !== undefined) {
|
|
5563
|
+
const value = JSONの欄を読む(中括弧.labelSize as string, "labelSize", line.no, errors);
|
|
5564
|
+
if (objectか(value)) {
|
|
5565
|
+
let invalid = 図表の知らない項目を知らせる(value, ["width", "height", "fontSize"], "labelSize", line.no, errors);
|
|
5566
|
+
for (const key of ["width", "height", "fontSize"] as const) {
|
|
5567
|
+
if (value[key] !== undefined && !正の有限数か(value[key])) invalid = true;
|
|
5568
|
+
}
|
|
5569
|
+
if (!invalid) labelSize = value;
|
|
5570
|
+
else errors.push({ line: line.no, message: "labelSize の値の形が読めません" });
|
|
5571
|
+
} else if (value !== undefined) errors.push({ line: line.no, message: "labelSize は object で書きます" });
|
|
5572
|
+
}
|
|
3842
5573
|
// 値に追随する 3 欄 (#1396)。 空は捨てずに知らせる = 描画側は空文字を既定値へ落とさず
|
|
3843
5574
|
// そのまま置換に使うため、書き忘れが「線が消えた」 形で出る
|
|
3844
5575
|
const widthBind = 追随する大きさとして読む(
|
|
@@ -3861,6 +5592,16 @@ function parseFlowStep(line: Line, no: number, errors: DslError[]): DslStep | nu
|
|
|
3861
5592
|
);
|
|
3862
5593
|
const labelOffsetX = 数と真偽?.labelOffsetX;
|
|
3863
5594
|
const labelOffsetY = 数と真偽?.labelOffsetY;
|
|
5595
|
+
const posX1 = 数と真偽?.posX1;
|
|
5596
|
+
const posY1 = 数と真偽?.posY1;
|
|
5597
|
+
const posX2 = 数と真偽?.posX2;
|
|
5598
|
+
const posY2 = 数と真偽?.posY2;
|
|
5599
|
+
const waypoints = 線の途中の点として読む(
|
|
5600
|
+
中括弧.waypoints as string | undefined,
|
|
5601
|
+
[posX1, posY1, posX2, posY2],
|
|
5602
|
+
line.no,
|
|
5603
|
+
errors,
|
|
5604
|
+
);
|
|
3864
5605
|
const overlay = 数と真偽?.overlay;
|
|
3865
5606
|
// 空の名指し (`toPartNode: ""`) は書かなかったことにする = 要素 1 つの部品なら自動で繋ぐ
|
|
3866
5607
|
const fromPartNode = (中括弧.fromPartNode as string | undefined) || undefined;
|
|
@@ -3923,6 +5664,8 @@ function parseFlowStep(line: Line, no: number, errors: DslError[]): DslStep | nu
|
|
|
3923
5664
|
guard,
|
|
3924
5665
|
cardinality,
|
|
3925
5666
|
side,
|
|
5667
|
+
fromSide,
|
|
5668
|
+
toSide,
|
|
3926
5669
|
head,
|
|
3927
5670
|
tailHead,
|
|
3928
5671
|
headFill,
|
|
@@ -3936,6 +5679,13 @@ function parseFlowStep(line: Line, no: number, errors: DslError[]): DslStep | nu
|
|
|
3936
5679
|
dashOffsetBind,
|
|
3937
5680
|
labelOffsetX,
|
|
3938
5681
|
labelOffsetY,
|
|
5682
|
+
posX1,
|
|
5683
|
+
posY1,
|
|
5684
|
+
posX2,
|
|
5685
|
+
posY2,
|
|
5686
|
+
waypoints,
|
|
5687
|
+
labelCollision,
|
|
5688
|
+
labelSize,
|
|
3939
5689
|
overlay,
|
|
3940
5690
|
...(fromPartNode !== undefined ? { fromPartNode } : {}),
|
|
3941
5691
|
...(toPartNode !== undefined ? { toPartNode } : {}),
|
|
@@ -3943,12 +5693,35 @@ function parseFlowStep(line: Line, no: number, errors: DslError[]): DslStep | nu
|
|
|
3943
5693
|
};
|
|
3944
5694
|
}
|
|
3945
5695
|
|
|
3946
|
-
|
|
3947
|
-
|
|
5696
|
+
/**
|
|
5697
|
+
* `client_bal: 100` / `status: "idle"` を 1 件の状態として読む。
|
|
5698
|
+
*
|
|
5699
|
+
* **2 つの断り方を分けて自分で報告する** (#2401)。 どちらも `null` にまとめると呼出側は
|
|
5700
|
+
* 区別できず、名前の規則を外れただけの行にも形の案内が返る。 書いた本文は案内どおりの形なので
|
|
5701
|
+
* **案内に従って書き直しても同じ案内が返り続ける** (実測 = `すすみ: 0` に
|
|
5702
|
+
* 「`name: initial` の形で書く」 が返った)。
|
|
5703
|
+
*
|
|
5704
|
+
* 名前の判定と案内は `value-syntax.ts` が持つ。 値の節 (`parseValueEntry`) と同じ文を使うことで、
|
|
5705
|
+
* 状態と値 × 記法と JSON の 4 経路で同じ理由が返る。
|
|
5706
|
+
*
|
|
5707
|
+
* 報告をここに置くと、呼出しを足した日に書き忘れても黙らない。 1 行にまとめた形の呼出しは
|
|
5708
|
+
* 実際に分岐を 1 つも持たず、規則外の名前の状態が知らせも無く消えていた。
|
|
5709
|
+
*/
|
|
5710
|
+
function parseStateEntry(text: string, lineNo: number, errors: DslError[]): DslState | null {
|
|
3948
5711
|
const m = text.match(/^([^:]+?)\s*:\s*(.+)$/);
|
|
3949
|
-
if (!m)
|
|
5712
|
+
if (!m) {
|
|
5713
|
+
errors.push({
|
|
5714
|
+
line: lineNo,
|
|
5715
|
+
message: `状態の行が読めません: "${text}"`,
|
|
5716
|
+
hint: "`name: initial` の形で書く",
|
|
5717
|
+
});
|
|
5718
|
+
return null;
|
|
5719
|
+
}
|
|
3950
5720
|
const name = (m[1] ?? "").trim();
|
|
3951
|
-
if (!isValueName(name))
|
|
5721
|
+
if (!isValueName(name)) {
|
|
5722
|
+
errors.push({ line: lineNo, ...valueNameIssue(name) });
|
|
5723
|
+
return null;
|
|
5724
|
+
}
|
|
3952
5725
|
const raw = (m[2] ?? "").trim();
|
|
3953
5726
|
const stripped = stripQuotes(raw);
|
|
3954
5727
|
const asNum = Number(stripped);
|
|
@@ -3957,31 +5730,6 @@ function parseStateEntry(text: string, lineNo: number): DslState | null {
|
|
|
3957
5730
|
return { name, initial, pos: { line: lineNo } };
|
|
3958
5731
|
}
|
|
3959
5732
|
|
|
3960
|
-
/**
|
|
3961
|
-
* 字下げした行を 1 行も落とさずに集める。
|
|
3962
|
-
*
|
|
3963
|
-
* `collectIndentedList` は形が合わない行を黙って捨てるが、 `values` では捨てずに
|
|
3964
|
-
* 読み手 (`parseValueEntry`) へ渡して書き間違いとして報告させる。
|
|
3965
|
-
*/
|
|
3966
|
-
function collectIndentedRaw(
|
|
3967
|
-
lines: Line[],
|
|
3968
|
-
start: number,
|
|
3969
|
-
parentIndent: number,
|
|
3970
|
-
): { items: Line[]; next: number } {
|
|
3971
|
-
const items: Line[] = [];
|
|
3972
|
-
let i = start;
|
|
3973
|
-
while (i < lines.length) {
|
|
3974
|
-
const ln = lines[i];
|
|
3975
|
-
if (!ln || !ln.trimmed || ln.trimmed.startsWith("#")) {
|
|
3976
|
-
i += 1;
|
|
3977
|
-
continue;
|
|
3978
|
-
}
|
|
3979
|
-
if (ln.indent <= parentIndent) break;
|
|
3980
|
-
items.push(ln);
|
|
3981
|
-
i += 1;
|
|
3982
|
-
}
|
|
3983
|
-
return { items, next: i };
|
|
3984
|
-
}
|
|
3985
5733
|
|
|
3986
5734
|
/**
|
|
3987
5735
|
* `waiting: "{inflow} - {done}"` を 1 件の値として読む。
|
|
@@ -3994,7 +5742,7 @@ function parseValueEntry(text: string, lineNo: number, errors: DslError[]): DslV
|
|
|
3994
5742
|
if (!m) {
|
|
3995
5743
|
errors.push({
|
|
3996
5744
|
line: lineNo,
|
|
3997
|
-
message:
|
|
5745
|
+
message: `値の行が読めません: "${text}"`,
|
|
3998
5746
|
hint: '`waiting: "{inflow} - {done}"` の形で書く',
|
|
3999
5747
|
});
|
|
4000
5748
|
return null;
|
|
@@ -4025,7 +5773,7 @@ function parseValueEntry(text: string, lineNo: number, errors: DslError[]): DslV
|
|
|
4025
5773
|
if (expression === "") {
|
|
4026
5774
|
errors.push({
|
|
4027
5775
|
line: lineNo,
|
|
4028
|
-
message:
|
|
5776
|
+
message: `式が空です: "${name}"`,
|
|
4029
5777
|
hint: '`"{a} + {b}"` のように式を書く',
|
|
4030
5778
|
});
|
|
4031
5779
|
return null;
|
|
@@ -4038,6 +5786,397 @@ function parseValueEntry(text: string, lineNo: number, errors: DslError[]): DslV
|
|
|
4038
5786
|
return { name, expression, pos: { line: lineNo } };
|
|
4039
5787
|
}
|
|
4040
5788
|
|
|
5789
|
+
/** JSON と同じ 1 行の配列 / object を、図表の複合欄で読む。 */
|
|
5790
|
+
function JSONの欄を読む(
|
|
5791
|
+
source: string | null,
|
|
5792
|
+
name: string,
|
|
5793
|
+
line: number,
|
|
5794
|
+
errors: DslError[],
|
|
5795
|
+
): unknown {
|
|
5796
|
+
const raw = (source ?? "").trim();
|
|
5797
|
+
try {
|
|
5798
|
+
return JSON.parse(raw) as unknown;
|
|
5799
|
+
} catch {
|
|
5800
|
+
errors.push({
|
|
5801
|
+
line,
|
|
5802
|
+
message: `${name} の JSON の形が読めません`,
|
|
5803
|
+
hint: `${name}: [...] または ${name}: {...} の形で 1 行に書く`,
|
|
5804
|
+
});
|
|
5805
|
+
return undefined;
|
|
5806
|
+
}
|
|
5807
|
+
}
|
|
5808
|
+
|
|
5809
|
+
const objectか = (value: unknown): value is Record<string, unknown> =>
|
|
5810
|
+
value !== null && typeof value === "object" && !Array.isArray(value);
|
|
5811
|
+
const 図表の数か = (value: unknown): value is number | string =>
|
|
5812
|
+
(typeof value === "number" && Number.isFinite(value)) ||
|
|
5813
|
+
(typeof value === "string" && value.trim().length > 0);
|
|
5814
|
+
|
|
5815
|
+
function 図表の知らない項目を知らせる(
|
|
5816
|
+
value: unknown,
|
|
5817
|
+
使える: readonly string[],
|
|
5818
|
+
名: string,
|
|
5819
|
+
line: number,
|
|
5820
|
+
errors: DslError[],
|
|
5821
|
+
): boolean {
|
|
5822
|
+
if (!objectか(value)) return false;
|
|
5823
|
+
const 知らない = Object.keys(value).filter((key) => !使える.includes(key));
|
|
5824
|
+
for (const key of 知らない) {
|
|
5825
|
+
errors.push({
|
|
5826
|
+
line,
|
|
5827
|
+
message: `${名} の項目名が読めません: "${key}"`,
|
|
5828
|
+
hint: `使える項目 = ${使える.join(", ")}`,
|
|
5829
|
+
});
|
|
5830
|
+
}
|
|
5831
|
+
return 知らない.length > 0;
|
|
5832
|
+
}
|
|
5833
|
+
|
|
5834
|
+
const 正の有限数か = (value: unknown): value is number =>
|
|
5835
|
+
typeof value === "number" && Number.isFinite(value) && value > 0;
|
|
5836
|
+
const 負でない有限数か = (value: unknown): value is number =>
|
|
5837
|
+
typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
5838
|
+
|
|
5839
|
+
const CDL_0132_OBJECT_FIELDS = [
|
|
5840
|
+
"terminalDecoration", "figureKindLabel", "figureInset", "journeySize", "chartBarSize", "chartLineSize",
|
|
5841
|
+
"chartPieSize", "chartWaffleSize", "chartSlopeLabelTemplate", "chartSlopeSize", "chartStackedLayout",
|
|
5842
|
+
"chartGaugeSize", "ganttSize", "quadrantSize", "legendOffset", "stageLayout",
|
|
5843
|
+
] as const;
|
|
5844
|
+
type Cdl0132ObjectField = (typeof CDL_0132_OBJECT_FIELDS)[number];
|
|
5845
|
+
|
|
5846
|
+
const CDL_0132_KEYS: Record<Cdl0132ObjectField, readonly string[]> = {
|
|
5847
|
+
terminalDecoration: ["windowControls", "cursor", "rowOrdinal"],
|
|
5848
|
+
figureKindLabel: ["text", "x", "y", "fontSize", "fontWeight", "letterSpacing"],
|
|
5849
|
+
figureInset: ["top", "right", "bottom", "left", "radius"],
|
|
5850
|
+
journeySize: ["plotPadding", "pointPadding"],
|
|
5851
|
+
chartBarSize: ["tickFontSize", "valueFontSize", "labelFontSize", "plotPadding", "barWidth", "cornerRadius"],
|
|
5852
|
+
chartLineSize: ["valueLabelOffset", "tickFontSize", "periodFontSize", "pointRadius", "plotPadding", "tickGap", "pointPadding", "periodY"],
|
|
5853
|
+
chartPieSize: ["tableWidth", "tableLabelFontSize", "tableValueFontSize", "tableRowGap", "tablePaddingInline", "outerRadius", "centerX"],
|
|
5854
|
+
chartWaffleSize: ["gridLeft", "cellPitch"],
|
|
5855
|
+
chartSlopeLabelTemplate: ["left", "right"],
|
|
5856
|
+
chartSlopeSize: ["pointRadius", "periodFontSize", "labelFontSize", "leftAxisX", "axisGap", "labelAxisGap", "plotPadding", "pointPadding"],
|
|
5857
|
+
chartStackedLayout: ["legendColumns", "legendColumnGap", "segmentGap", "barThickness", "barWidth", "legendMarkSize", "legendFontSize", "periodFontSize", "rateFontSize", "legendOffsetY"],
|
|
5858
|
+
chartGaugeSize: ["arcThickness", "outerRadius", "plotTop", "valueFontSize"],
|
|
5859
|
+
ganttSize: ["taskLabelFontSize", "taskLabelAlign", "taskLabelX", "tickFontSize", "tickCount", "barThickness", "milestoneFontSize", "milestoneRadius", "dependencyStrokeWidth", "taskColumnWidth", "timelinePaddingRight"],
|
|
5860
|
+
quadrantSize: ["plotOffset", "plotSize", "pointRadius", "pointFontSize", "pointLabelGap", "quadrantFontSize", "axisFontSize"],
|
|
5861
|
+
legendOffset: ["x", "y"],
|
|
5862
|
+
stageLayout: ["columnWidths", "columnGap", "backDetourTopGap", "backDetourSideGap", "backDetourTargetInset"],
|
|
5863
|
+
};
|
|
5864
|
+
|
|
5865
|
+
function cdl0132の欄を読む(
|
|
5866
|
+
name: Cdl0132ObjectField,
|
|
5867
|
+
value: unknown,
|
|
5868
|
+
line: number,
|
|
5869
|
+
errors: DslError[],
|
|
5870
|
+
): value is NonNullable<DslDocument[Cdl0132ObjectField]> {
|
|
5871
|
+
if (!objectか(value)) {
|
|
5872
|
+
errors.push({ line, message: `${name} は object で書きます` });
|
|
5873
|
+
return false;
|
|
5874
|
+
}
|
|
5875
|
+
let invalid = 図表の知らない項目を知らせる(value, CDL_0132_KEYS[name], name, line, errors);
|
|
5876
|
+
const positive = (key: string): void => {
|
|
5877
|
+
if (value[key] !== undefined && !正の有限数か(value[key])) invalid = true;
|
|
5878
|
+
};
|
|
5879
|
+
const nonNegative = (key: string): void => {
|
|
5880
|
+
if (value[key] !== undefined && !負でない有限数か(value[key])) invalid = true;
|
|
5881
|
+
};
|
|
5882
|
+
const nested = (key: string, keys: readonly string[], kind: "positive" | "nonNegative"): void => {
|
|
5883
|
+
const item = value[key];
|
|
5884
|
+
if (item === undefined) return;
|
|
5885
|
+
if (!objectか(item)) { invalid = true; return; }
|
|
5886
|
+
invalid = 図表の知らない項目を知らせる(item, keys, `${name}.${key}`, line, errors) || invalid;
|
|
5887
|
+
for (const nestedKey of keys) {
|
|
5888
|
+
if (item[nestedKey] === undefined) continue;
|
|
5889
|
+
if (kind === "positive" ? !正の有限数か(item[nestedKey]) : !負でない有限数か(item[nestedKey])) invalid = true;
|
|
5890
|
+
}
|
|
5891
|
+
};
|
|
5892
|
+
if (name === "terminalDecoration") {
|
|
5893
|
+
for (const key of CDL_0132_KEYS[name]) if (value[key] !== undefined && typeof value[key] !== "boolean") invalid = true;
|
|
5894
|
+
} else if (name === "figureKindLabel") {
|
|
5895
|
+
if (typeof value.text !== "string" || value.text.trim() === "") invalid = true;
|
|
5896
|
+
for (const key of ["x", "y"]) if (value[key] !== undefined && (typeof value[key] !== "number" || !Number.isFinite(value[key]))) invalid = true;
|
|
5897
|
+
for (const key of ["fontSize", "fontWeight"]) positive(key);
|
|
5898
|
+
nonNegative("letterSpacing");
|
|
5899
|
+
} else if (name === "figureInset") {
|
|
5900
|
+
for (const key of ["top", "right", "bottom", "left", "radius"]) nonNegative(key);
|
|
5901
|
+
} else if (name === "journeySize") {
|
|
5902
|
+
nested("plotPadding", ["left", "right"], "nonNegative");
|
|
5903
|
+
nested("pointPadding", ["left", "right"], "nonNegative");
|
|
5904
|
+
} else if (name === "chartBarSize") {
|
|
5905
|
+
for (const key of ["tickFontSize", "valueFontSize", "labelFontSize", "barWidth"]) positive(key);
|
|
5906
|
+
nested("plotPadding", ["top", "right", "bottom", "left"], "nonNegative");
|
|
5907
|
+
nonNegative("cornerRadius");
|
|
5908
|
+
} else if (name === "chartLineSize") {
|
|
5909
|
+
for (const key of ["tickFontSize", "periodFontSize", "pointRadius"]) positive(key);
|
|
5910
|
+
nested("valueLabelOffset", ["above", "below"], "nonNegative");
|
|
5911
|
+
nested("plotPadding", ["top", "right", "bottom", "left"], "nonNegative");
|
|
5912
|
+
nested("pointPadding", ["left", "right"], "nonNegative");
|
|
5913
|
+
nonNegative("tickGap");
|
|
5914
|
+
nonNegative("periodY");
|
|
5915
|
+
} else if (name === "chartPieSize") {
|
|
5916
|
+
for (const key of ["tableWidth", "tableLabelFontSize", "tableValueFontSize", "outerRadius"]) positive(key);
|
|
5917
|
+
for (const key of ["tableRowGap", "tablePaddingInline"]) nonNegative(key);
|
|
5918
|
+
nonNegative("centerX");
|
|
5919
|
+
} else if (name === "chartWaffleSize") {
|
|
5920
|
+
nonNegative("gridLeft");
|
|
5921
|
+
positive("cellPitch");
|
|
5922
|
+
} else if (name === "chartSlopeLabelTemplate") {
|
|
5923
|
+
for (const key of CDL_0132_KEYS[name]) if (value[key] !== undefined && typeof value[key] !== "string") invalid = true;
|
|
5924
|
+
} else if (name === "chartSlopeSize") {
|
|
5925
|
+
for (const key of ["pointRadius", "periodFontSize", "labelFontSize", "axisGap"]) positive(key);
|
|
5926
|
+
for (const key of ["leftAxisX", "labelAxisGap"]) nonNegative(key);
|
|
5927
|
+
nested("plotPadding", ["top", "bottom"], "nonNegative");
|
|
5928
|
+
nested("pointPadding", ["top", "bottom"], "nonNegative");
|
|
5929
|
+
} else if (name === "chartStackedLayout") {
|
|
5930
|
+
for (const key of ["barThickness", "barWidth", "legendMarkSize", "legendFontSize", "periodFontSize", "rateFontSize"]) positive(key);
|
|
5931
|
+
for (const key of ["legendColumnGap", "segmentGap"]) nonNegative(key);
|
|
5932
|
+
if (value.legendOffsetY !== undefined && (typeof value.legendOffsetY !== "number" || !Number.isFinite(value.legendOffsetY))) invalid = true;
|
|
5933
|
+
if (value.legendColumns !== undefined && (!Number.isInteger(value.legendColumns) || !正の有限数か(value.legendColumns))) invalid = true;
|
|
5934
|
+
} else if (name === "chartGaugeSize") {
|
|
5935
|
+
for (const key of ["arcThickness", "outerRadius", "valueFontSize"]) positive(key);
|
|
5936
|
+
nonNegative("plotTop");
|
|
5937
|
+
} else if (name === "ganttSize") {
|
|
5938
|
+
for (const key of ["taskLabelFontSize", "tickFontSize", "barThickness", "milestoneFontSize", "milestoneRadius", "dependencyStrokeWidth"]) positive(key);
|
|
5939
|
+
nonNegative("taskLabelX");
|
|
5940
|
+
nonNegative("taskColumnWidth");
|
|
5941
|
+
nonNegative("timelinePaddingRight");
|
|
5942
|
+
if (
|
|
5943
|
+
value.taskLabelAlign !== undefined &&
|
|
5944
|
+
!(GANTT_TASK_LABEL_ALIGN_VALUES as readonly unknown[]).includes(value.taskLabelAlign)
|
|
5945
|
+
) invalid = true;
|
|
5946
|
+
if (value.tickCount !== undefined && (!Number.isInteger(value.tickCount) || typeof value.tickCount !== "number" || value.tickCount < 2)) invalid = true;
|
|
5947
|
+
} else if (name === "quadrantSize") {
|
|
5948
|
+
nested("plotOffset", ["x", "y"], "nonNegative");
|
|
5949
|
+
for (const key of ["plotSize", "pointRadius", "pointFontSize", "quadrantFontSize", "axisFontSize"]) positive(key);
|
|
5950
|
+
nonNegative("pointLabelGap");
|
|
5951
|
+
} else if (name === "legendOffset") {
|
|
5952
|
+
for (const key of ["x", "y"])
|
|
5953
|
+
if (value[key] !== undefined && (typeof value[key] !== "number" || !Number.isFinite(value[key]))) invalid = true;
|
|
5954
|
+
} else if (name === "stageLayout") {
|
|
5955
|
+
nested("columnWidths", Object.keys(objectか(value.columnWidths) ? value.columnWidths : {}), "nonNegative");
|
|
5956
|
+
for (const key of ["columnGap", "backDetourTopGap", "backDetourSideGap", "backDetourTargetInset"]) nonNegative(key);
|
|
5957
|
+
}
|
|
5958
|
+
if (invalid) errors.push({ line, message: `${name} の値の形が読めません` });
|
|
5959
|
+
return !invalid;
|
|
5960
|
+
}
|
|
5961
|
+
|
|
5962
|
+
function 木の寸法を読む(
|
|
5963
|
+
value: unknown,
|
|
5964
|
+
line: number,
|
|
5965
|
+
errors: DslError[],
|
|
5966
|
+
): value is NonNullable<DslDocument["treeSize"]> {
|
|
5967
|
+
if (!objectか(value)) {
|
|
5968
|
+
errors.push({ line, message: "treeSize は object で書きます" });
|
|
5969
|
+
return false;
|
|
5970
|
+
}
|
|
5971
|
+
let invalid = 図表の知らない項目を知らせる(value, TREE_SIZE_KEYS, "treeSize", line, errors);
|
|
5972
|
+
if (
|
|
5973
|
+
value.nodeWidths !== undefined &&
|
|
5974
|
+
(!Array.isArray(value.nodeWidths) ||
|
|
5975
|
+
value.nodeWidths.length === 0 ||
|
|
5976
|
+
value.nodeWidths.some((width) => !正の有限数か(width)))
|
|
5977
|
+
)
|
|
5978
|
+
invalid = true;
|
|
5979
|
+
for (const key of ["nodeHeight", "titleFontSize", "subtitleFontSize"] as const) {
|
|
5980
|
+
if (value[key] !== undefined && !正の有限数か(value[key])) invalid = true;
|
|
5981
|
+
}
|
|
5982
|
+
for (const key of ["siblingGap", "levelGap"] as const) {
|
|
5983
|
+
if (value[key] !== undefined && !負でない有限数か(value[key])) invalid = true;
|
|
5984
|
+
}
|
|
5985
|
+
if (invalid) errors.push({ line, message: "treeSize の値の形が読めません" });
|
|
5986
|
+
return !invalid;
|
|
5987
|
+
}
|
|
5988
|
+
|
|
5989
|
+
function 放射の寸法を読む(
|
|
5990
|
+
value: unknown,
|
|
5991
|
+
line: number,
|
|
5992
|
+
errors: DslError[],
|
|
5993
|
+
): value is NonNullable<DslDocument["mindSize"]> {
|
|
5994
|
+
if (!objectか(value)) {
|
|
5995
|
+
errors.push({ line, message: "mindSize は object で書きます" });
|
|
5996
|
+
return false;
|
|
5997
|
+
}
|
|
5998
|
+
let invalid = 図表の知らない項目を知らせる(value, MIND_SIZE_KEYS, "mindSize", line, errors);
|
|
5999
|
+
const contentOffset = value.contentOffset;
|
|
6000
|
+
if (contentOffset !== undefined) {
|
|
6001
|
+
if (!objectか(contentOffset)) invalid = true;
|
|
6002
|
+
else {
|
|
6003
|
+
invalid =
|
|
6004
|
+
図表の知らない項目を知らせる(
|
|
6005
|
+
contentOffset,
|
|
6006
|
+
["x", "y"],
|
|
6007
|
+
"mindSize.contentOffset",
|
|
6008
|
+
line,
|
|
6009
|
+
errors,
|
|
6010
|
+
) || invalid;
|
|
6011
|
+
for (const key of ["x", "y"] as const) {
|
|
6012
|
+
if (typeof contentOffset[key] !== "number" || !Number.isFinite(contentOffset[key])) {
|
|
6013
|
+
invalid = true;
|
|
6014
|
+
}
|
|
6015
|
+
}
|
|
6016
|
+
}
|
|
6017
|
+
}
|
|
6018
|
+
for (const [key, allowed] of [
|
|
6019
|
+
["root", ["width", "height", "fontSize"]],
|
|
6020
|
+
["branch", ["width", "height", "fontSize"]],
|
|
6021
|
+
["leaf", ["fontSize"]],
|
|
6022
|
+
] as const) {
|
|
6023
|
+
const nested = value[key];
|
|
6024
|
+
if (nested === undefined) continue;
|
|
6025
|
+
if (!objectか(nested)) {
|
|
6026
|
+
invalid = true;
|
|
6027
|
+
continue;
|
|
6028
|
+
}
|
|
6029
|
+
invalid =
|
|
6030
|
+
図表の知らない項目を知らせる(nested, allowed, `mindSize.${key}`, line, errors) || invalid;
|
|
6031
|
+
for (const nestedKey of allowed) {
|
|
6032
|
+
if (nested[nestedKey] !== undefined && !正の有限数か(nested[nestedKey])) invalid = true;
|
|
6033
|
+
}
|
|
6034
|
+
}
|
|
6035
|
+
for (const key of ["rootBranchGap", "branchLeafGap", "branchRowGap", "leafRowGap"] as const) {
|
|
6036
|
+
if (value[key] !== undefined && !負でない有限数か(value[key])) invalid = true;
|
|
6037
|
+
}
|
|
6038
|
+
for (const key of ["branchRowOffset", "leafRowOffset"] as const) {
|
|
6039
|
+
if (
|
|
6040
|
+
value[key] !== undefined &&
|
|
6041
|
+
(typeof value[key] !== "number" || !Number.isFinite(value[key]))
|
|
6042
|
+
)
|
|
6043
|
+
invalid = true;
|
|
6044
|
+
}
|
|
6045
|
+
if (invalid) errors.push({ line, message: "mindSize の値の形が読めません" });
|
|
6046
|
+
return !invalid;
|
|
6047
|
+
}
|
|
6048
|
+
|
|
6049
|
+
function chartLineSeriesの知らない項目を知らせる(
|
|
6050
|
+
value: unknown,
|
|
6051
|
+
line: number,
|
|
6052
|
+
errors: DslError[],
|
|
6053
|
+
): boolean {
|
|
6054
|
+
if (!Array.isArray(value)) return false;
|
|
6055
|
+
let unknown = false;
|
|
6056
|
+
value.forEach((series, index) => {
|
|
6057
|
+
unknown =
|
|
6058
|
+
図表の知らない項目を知らせる(
|
|
6059
|
+
series,
|
|
6060
|
+
["label", "points", "tone", "dash", "marker", "valueIndexes"],
|
|
6061
|
+
`chartLineSeries[${index}]`,
|
|
6062
|
+
line,
|
|
6063
|
+
errors,
|
|
6064
|
+
) || unknown;
|
|
6065
|
+
if (!objectか(series) || !Array.isArray(series.points)) return;
|
|
6066
|
+
series.points.forEach((point, pointIndex) => {
|
|
6067
|
+
unknown =
|
|
6068
|
+
図表の知らない項目を知らせる(
|
|
6069
|
+
point,
|
|
6070
|
+
["label", "value"],
|
|
6071
|
+
`chartLineSeries[${index}].points[${pointIndex}]`,
|
|
6072
|
+
line,
|
|
6073
|
+
errors,
|
|
6074
|
+
) || unknown;
|
|
6075
|
+
});
|
|
6076
|
+
});
|
|
6077
|
+
return unknown;
|
|
6078
|
+
}
|
|
6079
|
+
|
|
6080
|
+
function chartStackedPeriodsの知らない項目を知らせる(
|
|
6081
|
+
value: unknown,
|
|
6082
|
+
line: number,
|
|
6083
|
+
errors: DslError[],
|
|
6084
|
+
): boolean {
|
|
6085
|
+
if (!Array.isArray(value)) return false;
|
|
6086
|
+
let unknown = false;
|
|
6087
|
+
value.forEach((period, index) => {
|
|
6088
|
+
unknown =
|
|
6089
|
+
図表の知らない項目を知らせる(
|
|
6090
|
+
period,
|
|
6091
|
+
["label", "values"],
|
|
6092
|
+
`chartStackedPeriods[${index}]`,
|
|
6093
|
+
line,
|
|
6094
|
+
errors,
|
|
6095
|
+
) || unknown;
|
|
6096
|
+
});
|
|
6097
|
+
return unknown;
|
|
6098
|
+
}
|
|
6099
|
+
|
|
6100
|
+
function chartGaugeValueの知らない項目を知らせる(
|
|
6101
|
+
value: unknown,
|
|
6102
|
+
line: number,
|
|
6103
|
+
errors: DslError[],
|
|
6104
|
+
): boolean {
|
|
6105
|
+
return 図表の知らない項目を知らせる(
|
|
6106
|
+
value,
|
|
6107
|
+
["max", "current", "target", "previous", "previousLabel"],
|
|
6108
|
+
"chartGaugeValue",
|
|
6109
|
+
line,
|
|
6110
|
+
errors,
|
|
6111
|
+
);
|
|
6112
|
+
}
|
|
6113
|
+
|
|
6114
|
+
function chartLineSeriesの形(
|
|
6115
|
+
value: unknown,
|
|
6116
|
+
): value is NonNullable<DslDocument["chartLineSeries"]> {
|
|
6117
|
+
if (!Array.isArray(value) || value.length === 0) return false;
|
|
6118
|
+
return value.every((series) => {
|
|
6119
|
+
if (!objectか(series) || !Array.isArray(series.points) || series.points.length === 0) return false;
|
|
6120
|
+
if (series.label !== undefined && typeof series.label !== "string") return false;
|
|
6121
|
+
if (
|
|
6122
|
+
series.tone !== undefined &&
|
|
6123
|
+
(typeof series.tone !== "string" || resolveTone(series.tone) === undefined)
|
|
6124
|
+
)
|
|
6125
|
+
return false;
|
|
6126
|
+
if (
|
|
6127
|
+
series.dash !== undefined &&
|
|
6128
|
+
(typeof series.dash !== "string" ||
|
|
6129
|
+
!(CHART_LINE_DASH_VALUES as readonly string[]).includes(series.dash))
|
|
6130
|
+
)
|
|
6131
|
+
return false;
|
|
6132
|
+
if (
|
|
6133
|
+
series.marker !== undefined &&
|
|
6134
|
+
(typeof series.marker !== "string" ||
|
|
6135
|
+
!(CHART_LINE_MARKER_VALUES as readonly string[]).includes(series.marker))
|
|
6136
|
+
) return false;
|
|
6137
|
+
if (
|
|
6138
|
+
series.valueIndexes !== undefined &&
|
|
6139
|
+
(!Array.isArray(series.valueIndexes) ||
|
|
6140
|
+
series.valueIndexes.some((index) => !Number.isInteger(index) || Number(index) < 0))
|
|
6141
|
+
) return false;
|
|
6142
|
+
return series.points.every(
|
|
6143
|
+
(point) => objectか(point) && typeof point.label === "string" && 図表の数か(point.value),
|
|
6144
|
+
);
|
|
6145
|
+
});
|
|
6146
|
+
}
|
|
6147
|
+
|
|
6148
|
+
function chartStackedPeriodsの形(
|
|
6149
|
+
value: unknown,
|
|
6150
|
+
): value is NonNullable<DslDocument["chartStackedPeriods"]> {
|
|
6151
|
+
return (
|
|
6152
|
+
Array.isArray(value) &&
|
|
6153
|
+
value.length > 0 &&
|
|
6154
|
+
value.every(
|
|
6155
|
+
(period) =>
|
|
6156
|
+
objectか(period) &&
|
|
6157
|
+
typeof period.label === "string" &&
|
|
6158
|
+
Array.isArray(period.values) &&
|
|
6159
|
+
period.values.every(図表の数か),
|
|
6160
|
+
)
|
|
6161
|
+
);
|
|
6162
|
+
}
|
|
6163
|
+
|
|
6164
|
+
function chartGaugeValueの形(
|
|
6165
|
+
value: unknown,
|
|
6166
|
+
): value is NonNullable<DslDocument["chartGaugeValue"]> {
|
|
6167
|
+
if (!objectか(value)) return false;
|
|
6168
|
+
const finite = (entry: unknown): entry is number =>
|
|
6169
|
+
typeof entry === "number" && Number.isFinite(entry);
|
|
6170
|
+
return (
|
|
6171
|
+
finite(value.max) &&
|
|
6172
|
+
value.max > 0 &&
|
|
6173
|
+
図表の数か(value.current) &&
|
|
6174
|
+
(value.target === undefined || 図表の数か(value.target)) &&
|
|
6175
|
+
(value.previous === undefined || 図表の数か(value.previous)) &&
|
|
6176
|
+
(value.previousLabel === undefined || typeof value.previousLabel === "string")
|
|
6177
|
+
);
|
|
6178
|
+
}
|
|
6179
|
+
|
|
4041
6180
|
function splitTopLevelCommas(s: string): string[] {
|
|
4042
6181
|
// brace 内を考慮 ... 今回は単純 split (動作する範囲)
|
|
4043
6182
|
return s
|
|
@@ -4061,8 +6200,8 @@ function ensureAnimate(a: DslAnimate | undefined, lineNo: number): DslAnimate {
|
|
|
4061
6200
|
export const PHASE_ITEM_WORDS = [
|
|
4062
6201
|
"focus",
|
|
4063
6202
|
"badge",
|
|
4064
|
-
|
|
4065
|
-
|
|
6203
|
+
// 説明の語は `PHASE_BODY_KEYS` から導く (#2621)。 ここに書き写すと JSON の入口とずれる
|
|
6204
|
+
...PHASE_BODY_KEYS,
|
|
4066
6205
|
"tween",
|
|
4067
6206
|
"set",
|
|
4068
6207
|
"draw",
|
|
@@ -4078,8 +6217,8 @@ function parsePhase(block: Line[], errors: DslError[]): DslPhase | null {
|
|
|
4078
6217
|
if (!m) {
|
|
4079
6218
|
errors.push({
|
|
4080
6219
|
line: head.no,
|
|
4081
|
-
message:
|
|
4082
|
-
hint: '
|
|
6220
|
+
message: `段の見出しが読めません: "${head.trimmed}"`,
|
|
6221
|
+
hint: '`- step: "name" 1.5s` の形で書く',
|
|
4083
6222
|
});
|
|
4084
6223
|
return null;
|
|
4085
6224
|
}
|
|
@@ -4089,8 +6228,8 @@ function parsePhase(block: Line[], errors: DslError[]): DslPhase | null {
|
|
|
4089
6228
|
if (!headParse) {
|
|
4090
6229
|
errors.push({
|
|
4091
6230
|
line: head.no,
|
|
4092
|
-
message:
|
|
4093
|
-
hint: '
|
|
6231
|
+
message: `段の値が読めません: "${headRest}"`,
|
|
6232
|
+
hint: '`"name" 1.5s` の形で書く (長さは秒)',
|
|
4094
6233
|
});
|
|
4095
6234
|
return null;
|
|
4096
6235
|
}
|
|
@@ -4112,6 +6251,14 @@ function parsePhase(block: Line[], errors: DslError[]): DslPhase | null {
|
|
|
4112
6251
|
// 拾った上で、知らない名前は下で誤りとして知らせる
|
|
4113
6252
|
const propMatch = t.match(/^([^\s:]+)\s*:\s*(.*)$/);
|
|
4114
6253
|
if (!propMatch) {
|
|
6254
|
+
// **項目名として読めなかった行も捨てない** (#2403)。 すぐ下で「知らない項目名」 は
|
|
6255
|
+
// 知らせているのに、ここだけが黙っていた = コロンを書き落とすと、その行が
|
|
6256
|
+
// 知らせにも図にも現れない (実測 = 段の中に `focus` と書くと誤り 0 件で何も起きない)
|
|
6257
|
+
errors.push({
|
|
6258
|
+
line: ln.no,
|
|
6259
|
+
message: `段の行が項目名として読めません: "${t}"`,
|
|
6260
|
+
hint: `\`項目名: 値\` の形で書く (使える項目 = ${段の項目の英語.join(", ")})`,
|
|
6261
|
+
});
|
|
4115
6262
|
i += 1;
|
|
4116
6263
|
continue;
|
|
4117
6264
|
}
|
|
@@ -4155,7 +6302,7 @@ function parsePhase(block: Line[], errors: DslError[]): DslPhase | null {
|
|
|
4155
6302
|
errors.push({
|
|
4156
6303
|
line: ln.no,
|
|
4157
6304
|
message: `draw に書ける項目は語と割合の 2 つまでです: "${stripQuotes(value).trim()}"`,
|
|
4158
|
-
hint: "
|
|
6305
|
+
hint: "`draw: line 0.4` の形で書く",
|
|
4159
6306
|
});
|
|
4160
6307
|
} else {
|
|
4161
6308
|
phase.draw = 語;
|
|
@@ -4178,39 +6325,28 @@ function parsePhase(block: Line[], errors: DslError[]): DslPhase | null {
|
|
|
4178
6325
|
i += 1;
|
|
4179
6326
|
continue;
|
|
4180
6327
|
}
|
|
4181
|
-
if (
|
|
6328
|
+
if ((PHASE_BODY_KEYS as readonly string[]).includes(key)) {
|
|
4182
6329
|
phase.body = stripQuotes(value);
|
|
4183
6330
|
i += 1;
|
|
4184
6331
|
continue;
|
|
4185
6332
|
}
|
|
4186
6333
|
if (key === "tween") {
|
|
4187
|
-
//
|
|
6334
|
+
// 1 行にまとめた形 (tween: client_bal 100 -> 90) と縦に並べた形の両方を受ける。
|
|
6335
|
+
// 読めない行の文は読み手が持つ (#2403)
|
|
4188
6336
|
if (value) {
|
|
4189
|
-
const tw = parseTweenLine(value, ln.no);
|
|
6337
|
+
const tw = parseTweenLine(value, ln.no, errors);
|
|
4190
6338
|
if (tw) phase.tweens!.push(tw);
|
|
4191
|
-
else
|
|
4192
|
-
errors.push({
|
|
4193
|
-
line: ln.no,
|
|
4194
|
-
message: `invalid tween: "${value}"`,
|
|
4195
|
-
hint: "use `tween: name 100 -> 90`",
|
|
4196
|
-
});
|
|
4197
6339
|
i += 1;
|
|
4198
6340
|
continue;
|
|
4199
6341
|
}
|
|
4200
|
-
//
|
|
6342
|
+
// 縦に並べた形 ... 後続の同 indent + 1 以上の行を取り込む
|
|
4201
6343
|
const baseIndent = ln.indent;
|
|
4202
6344
|
let j = i + 1;
|
|
4203
6345
|
while (j < block.length) {
|
|
4204
6346
|
const nx = block[j]!;
|
|
4205
6347
|
if (nx.indent <= baseIndent) break;
|
|
4206
|
-
const tw = parseTweenLine(nx.trimmed, nx.no);
|
|
6348
|
+
const tw = parseTweenLine(nx.trimmed, nx.no, errors);
|
|
4207
6349
|
if (tw) phase.tweens!.push(tw);
|
|
4208
|
-
else
|
|
4209
|
-
errors.push({
|
|
4210
|
-
line: nx.no,
|
|
4211
|
-
message: `invalid tween entry: "${nx.trimmed}"`,
|
|
4212
|
-
hint: "use `name: 100 -> 90`",
|
|
4213
|
-
});
|
|
4214
6350
|
j += 1;
|
|
4215
6351
|
}
|
|
4216
6352
|
i = j;
|
|
@@ -4218,7 +6354,7 @@ function parsePhase(block: Line[], errors: DslError[]): DslPhase | null {
|
|
|
4218
6354
|
}
|
|
4219
6355
|
if (key === "set") {
|
|
4220
6356
|
if (value) {
|
|
4221
|
-
const st = parseSetLine(value, ln.no);
|
|
6357
|
+
const st = parseSetLine(value, ln.no, errors);
|
|
4222
6358
|
if (st) phase.sets!.push(st);
|
|
4223
6359
|
i += 1;
|
|
4224
6360
|
continue;
|
|
@@ -4228,7 +6364,7 @@ function parsePhase(block: Line[], errors: DslError[]): DslPhase | null {
|
|
|
4228
6364
|
while (j < block.length) {
|
|
4229
6365
|
const nx = block[j]!;
|
|
4230
6366
|
if (nx.indent <= baseIndent) break;
|
|
4231
|
-
const st = parseSetLine(nx.trimmed, nx.no);
|
|
6367
|
+
const st = parseSetLine(nx.trimmed, nx.no, errors);
|
|
4232
6368
|
if (st) phase.sets!.push(st);
|
|
4233
6369
|
j += 1;
|
|
4234
6370
|
}
|
|
@@ -4292,23 +6428,47 @@ function 段の項目のヒント(書いた名前: string): string {
|
|
|
4292
6428
|
*
|
|
4293
6429
|
* いまは語と図種が同じ綴りだが、**同じものとして扱わない**。 語は書き手が書く名前で、
|
|
4294
6430
|
* 図種は `type:` が取る値。 片方だけ別名を足したくなった時に、対応が表に残っている形にする。
|
|
6431
|
+
*
|
|
6432
|
+
* **数を描く図は形まで指す** (#2657)。 9 つの型を `chart` 1 つに畳んだので、型だけを
|
|
6433
|
+
* 指すと `draw: pie` を棒の図に書いても咎められなくなる = 語ごとの相手を型から形へ
|
|
6434
|
+
* 下ろして、畳む前と同じ細かさで判定する。
|
|
4295
6435
|
*/
|
|
4296
|
-
export const DRAW_TARGETS: ReadonlyMap<string, PresetType> = new Map<
|
|
4297
|
-
|
|
4298
|
-
|
|
4299
|
-
|
|
4300
|
-
["
|
|
4301
|
-
["
|
|
4302
|
-
["
|
|
4303
|
-
["
|
|
4304
|
-
["
|
|
4305
|
-
["
|
|
4306
|
-
["
|
|
4307
|
-
["
|
|
4308
|
-
["
|
|
4309
|
-
["
|
|
6436
|
+
export const DRAW_TARGETS: ReadonlyMap<string, { type: PresetType; shape?: DslShape }> = new Map<
|
|
6437
|
+
string,
|
|
6438
|
+
{ type: PresetType; shape?: DslShape }
|
|
6439
|
+
>([
|
|
6440
|
+
["line", { type: "chart", shape: "line" }],
|
|
6441
|
+
["bar", { type: "chart", shape: "bar" }],
|
|
6442
|
+
["pie", { type: "chart", shape: "pie" }],
|
|
6443
|
+
["journey", { type: "journey" }],
|
|
6444
|
+
["mind", { type: "mind" }],
|
|
6445
|
+
["tree", { type: "tree" }],
|
|
6446
|
+
["gantt", { type: "gantt" }],
|
|
6447
|
+
["funnel", { type: "funnel" }],
|
|
6448
|
+
["slope", { type: "chart", shape: "slope" }],
|
|
6449
|
+
["gauge", { type: "chart", shape: "gauge" }],
|
|
6450
|
+
["radial", { type: "chart", shape: "radial" }],
|
|
6451
|
+
["stacked", { type: "chart", shape: "stacked" }],
|
|
6452
|
+
["waffle", { type: "chart", shape: "waffle" }],
|
|
4310
6453
|
]);
|
|
4311
6454
|
|
|
6455
|
+
/**
|
|
6456
|
+
* 段に書いた描く語が、その図に効くか (#2657)。
|
|
6457
|
+
*
|
|
6458
|
+
* 型が合うだけでは足りない = 数を描く図は 1 つの型に 9 つの形が入るので、形まで見ないと
|
|
6459
|
+
* `draw: pie` を棒の図に書いた形が通る。 形を持たない語は型の一致だけで決まる。
|
|
6460
|
+
*/
|
|
6461
|
+
export function 描く語がその図を指すか(
|
|
6462
|
+
語: string,
|
|
6463
|
+
type: PresetType,
|
|
6464
|
+
shape: DslShape | undefined,
|
|
6465
|
+
): boolean {
|
|
6466
|
+
const 相手 = DRAW_TARGETS.get(語);
|
|
6467
|
+
if (相手 === undefined) return false;
|
|
6468
|
+
if (相手.type !== type) return false;
|
|
6469
|
+
return 相手.shape === undefined || 相手.shape === shape;
|
|
6470
|
+
}
|
|
6471
|
+
|
|
4312
6472
|
/** `draw:` に書ける語。 表から導く (#1314) */
|
|
4313
6473
|
export const DRAW_WORDS: ReadonlySet<string> = new Set(DRAW_TARGETS.keys());
|
|
4314
6474
|
|
|
@@ -4434,13 +6594,33 @@ function parseFocusList(s: string): string[] {
|
|
|
4434
6594
|
return out;
|
|
4435
6595
|
}
|
|
4436
6596
|
|
|
4437
|
-
|
|
4438
|
-
|
|
6597
|
+
/**
|
|
6598
|
+
* `client_bal 100 -> 90` / `client_bal: 100 -> 90` を 1 件の変化として読む。
|
|
6599
|
+
*
|
|
6600
|
+
* **2 つの断り方を分けて自分で報告する** (#2403、 #2401 と同じ形)。 どちらも `null` に
|
|
6601
|
+
* まとめると、名前の規則を外れただけの行にも形の案内が返り、案内に従っても直らない
|
|
6602
|
+
* (実測 = `すすみ 0 -> 1` に「`tween: name 100 -> 90` の形で書く」 が返った)。
|
|
6603
|
+
*
|
|
6604
|
+
* **形の案内には 2 つの書き方を両方載せる**。 呼出側は 1 行にまとめた形と縦に並べた形の
|
|
6605
|
+
* 2 つあり、文を呼出側へ戻すと「呼出しを足した日に書き忘れる」 形が戻る。 文を 1 か所に
|
|
6606
|
+
* 置いたまま、どちらで書いた人にも直し方が伝わるようにする。
|
|
6607
|
+
*/
|
|
6608
|
+
function parseTweenLine(s: string, lineNo: number, errors: DslError[]): DslTween | null {
|
|
4439
6609
|
const cleaned = s.replace(/^-\s*/, "").trim();
|
|
4440
6610
|
const m = cleaned.match(/^([^:\s]+)\s*[:\s]\s*(-?\d+(?:\.\d+)?)\s*->\s*(-?\d+(?:\.\d+)?)$/);
|
|
4441
|
-
if (!m)
|
|
6611
|
+
if (!m) {
|
|
6612
|
+
errors.push({
|
|
6613
|
+
line: lineNo,
|
|
6614
|
+
message: `変化の行が読めません: "${cleaned}"`,
|
|
6615
|
+
hint: "`tween: name 100 -> 90` の形で書く (縦に並べる時は `name: 100 -> 90`)",
|
|
6616
|
+
});
|
|
6617
|
+
return null;
|
|
6618
|
+
}
|
|
4442
6619
|
const state = m[1] ?? "";
|
|
4443
|
-
if (!isValueName(state))
|
|
6620
|
+
if (!isValueName(state)) {
|
|
6621
|
+
errors.push({ line: lineNo, ...valueNameIssue(state) });
|
|
6622
|
+
return null;
|
|
6623
|
+
}
|
|
4444
6624
|
return {
|
|
4445
6625
|
state,
|
|
4446
6626
|
from: parseFloat(m[2] ?? "0"),
|
|
@@ -4449,13 +6629,32 @@ function parseTweenLine(s: string, lineNo: number): DslTween | null {
|
|
|
4449
6629
|
};
|
|
4450
6630
|
}
|
|
4451
6631
|
|
|
4452
|
-
|
|
4453
|
-
|
|
6632
|
+
/**
|
|
6633
|
+
* `status: "loading"` / `status loading` を 1 件の書き換えとして読む。
|
|
6634
|
+
*
|
|
6635
|
+
* **2 つの断り方を分けて自分で報告する** (#2403)。 直す前は断り方を分けないどころか、
|
|
6636
|
+
* 呼出側が `null` を握り潰していた = 4 通りの書き損じ (1 行 / 縦 × 名前が規則外 / 区切りが無い)
|
|
6637
|
+
* すべてで知らせが 0 件になり、段に書き換えも入らなかった。
|
|
6638
|
+
*
|
|
6639
|
+
* 隣に並ぶ `tween` は知らせを出しており、**同じ段の中で片方だけが黙っていた**。
|
|
6640
|
+
* 文の置き場所を読み手 1 か所にすると、呼出しを足した日に書き忘れても黙らない。
|
|
6641
|
+
*/
|
|
6642
|
+
function parseSetLine(s: string, lineNo: number, errors: DslError[]): DslSet | null {
|
|
4454
6643
|
const cleaned = s.replace(/^-\s*/, "").trim();
|
|
4455
6644
|
const m = cleaned.match(/^([^:\s]+)\s*[:\s]\s*(.+)$/);
|
|
4456
|
-
if (!m)
|
|
6645
|
+
if (!m) {
|
|
6646
|
+
errors.push({
|
|
6647
|
+
line: lineNo,
|
|
6648
|
+
message: `値の書き換えが読めません: "${cleaned}"`,
|
|
6649
|
+
hint: '`set: name "done"` の形で書く (縦に並べる時は `name: "done"`)',
|
|
6650
|
+
});
|
|
6651
|
+
return null;
|
|
6652
|
+
}
|
|
4457
6653
|
const state = m[1] ?? "";
|
|
4458
|
-
if (!isValueName(state))
|
|
6654
|
+
if (!isValueName(state)) {
|
|
6655
|
+
errors.push({ line: lineNo, ...valueNameIssue(state) });
|
|
6656
|
+
return null;
|
|
6657
|
+
}
|
|
4459
6658
|
const raw = (m[2] ?? "").trim();
|
|
4460
6659
|
const stripped = stripQuotes(raw);
|
|
4461
6660
|
const asNum = Number(stripped);
|