@cardenelabs/dragon 0.30.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 +314 -12
- package/dist/index.cjs +6966 -1438
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2150 -118
- package/dist/index.d.ts +2150 -118
- package/dist/index.js +6950 -1440
- package/dist/index.js.map +1 -1
- package/package.json +12 -13
- package/src/compile/actor-ids.ts +6 -15
- package/src/compile/actor-option-notice.ts +379 -0
- package/src/compile/actors.ts +32 -3
- package/src/compile/c4.ts +0 -3
- package/src/compile/chain.ts +43 -4
- package/src/compile/chart-fields.ts +16 -1
- package/src/compile/direction.ts +50 -11
- package/src/compile/edge-option-notice.ts +112 -0
- package/src/compile/flowchart.ts +344 -0
- package/src/compile/funnel.ts +14 -2
- package/src/compile/gantt.ts +141 -36
- package/src/compile/generic.ts +473 -61
- package/src/compile/journey.ts +14 -5
- package/src/compile/kinds.ts +5 -13
- package/src/compile/lanes.ts +95 -13
- package/src/compile/metro.ts +271 -0
- package/src/compile/mind.ts +55 -5
- package/src/compile/notice.ts +51 -4
- package/src/compile/order.ts +62 -0
- package/src/compile/parts.ts +107 -33
- package/src/compile/placement-apply.ts +27 -11
- package/src/compile/quadrant.ts +171 -16
- package/src/compile/record.ts +75 -0
- package/src/compile/row-marks.ts +128 -0
- package/src/compile/sequence.ts +105 -26
- package/src/compile/slug.ts +9 -1
- package/src/compile/stages.ts +247 -0
- package/src/compile/subtitle.ts +15 -2
- package/src/compile/swimlane-marks.ts +87 -0
- package/src/compile/swimlane.ts +13 -14
- package/src/compile/timeline.ts +213 -0
- package/src/compile/topology.ts +3 -9
- package/src/compile/tree.ts +13 -4
- package/src/compile/value-chart.ts +179 -25
- package/src/compile.ts +1550 -370
- package/src/focus.ts +2 -2
- package/src/index.ts +34 -2
- package/src/json-parser.ts +1407 -48
- package/src/keywords.ts +251 -17
- package/src/notation-lint.ts +41 -64
- package/src/schemas/diagram.json +643 -20
- package/src/types.ts +336 -35
- package/src/v05/parser.ts +2451 -271
- package/src/compile/class.ts +0 -100
- package/src/compile/er.ts +0 -44
- package/src/compile/solidity.ts +0 -39
- package/src/compile/state.ts +0 -71
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";
|
|
93
|
+
import type { DslOrder } from "../keywords";
|
|
94
|
+
import type { DslDiagramShape, DslShape } from "../keywords";
|
|
50
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` には含まれない */
|
|
@@ -459,6 +771,20 @@ type Line = {
|
|
|
459
771
|
no: number;
|
|
460
772
|
};
|
|
461
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
|
+
|
|
462
788
|
export function parseTextDslV05(src: string): V05ParseResult {
|
|
463
789
|
const errors: DslError[] = [];
|
|
464
790
|
const lines = tokenize(src);
|
|
@@ -471,9 +797,79 @@ export function parseTextDslV05(src: string): V05ParseResult {
|
|
|
471
797
|
let relations: RelationFocus | null = null;
|
|
472
798
|
let direction: DslDirection | null = null;
|
|
473
799
|
let directionLine = 0;
|
|
474
|
-
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;
|
|
475
870
|
let axes: DslAxes | undefined = undefined;
|
|
476
871
|
let axesLine = 0;
|
|
872
|
+
let regions: DslRegions | undefined = undefined;
|
|
477
873
|
let actors: DslActor[] = [];
|
|
478
874
|
const flow: DslStep[] = [];
|
|
479
875
|
let animate: DslAnimate | undefined = undefined;
|
|
@@ -489,6 +885,11 @@ export function parseTextDslV05(src: string): V05ParseResult {
|
|
|
489
885
|
let scrollsList: DslScrollTrigger[] | undefined = undefined;
|
|
490
886
|
let scrollLines = new Map<string, number>();
|
|
491
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"];
|
|
492
893
|
|
|
493
894
|
let i = 0;
|
|
494
895
|
while (i < lines.length) {
|
|
@@ -497,12 +898,18 @@ export function parseTextDslV05(src: string): V05ParseResult {
|
|
|
497
898
|
i += 1;
|
|
498
899
|
continue;
|
|
499
900
|
}
|
|
500
|
-
const
|
|
901
|
+
const 書いた頭 = matchTopHeader(line.trimmed);
|
|
902
|
+
const head = 書いた頭
|
|
903
|
+
? {
|
|
904
|
+
...書いた頭,
|
|
905
|
+
key: TOP_LEVEL_KEY_ALIASES.get(書いた頭.key) ?? 書いた頭.key,
|
|
906
|
+
}
|
|
907
|
+
: null;
|
|
501
908
|
if (!head || !isTopLevelKey(head.key)) {
|
|
502
909
|
errors.push({
|
|
503
910
|
line: line.no,
|
|
504
|
-
message:
|
|
505
|
-
hint:
|
|
911
|
+
message: `最上位の項目名が読めません: "${最上位の項目名(line.trimmed)}"`,
|
|
912
|
+
hint: `使える項目 = ${TOP_LEVEL_KEYS.join(", ")}`,
|
|
506
913
|
});
|
|
507
914
|
i += 1;
|
|
508
915
|
continue;
|
|
@@ -510,7 +917,11 @@ export function parseTextDslV05(src: string): V05ParseResult {
|
|
|
510
917
|
if (head.key === "title") {
|
|
511
918
|
title = head.value ?? null;
|
|
512
919
|
if (!title) {
|
|
513
|
-
errors.push({
|
|
920
|
+
errors.push({
|
|
921
|
+
line: line.no,
|
|
922
|
+
message: "title が書かれていません",
|
|
923
|
+
hint: "`title: \"...\"` の形で書く",
|
|
924
|
+
});
|
|
514
925
|
}
|
|
515
926
|
i += 1;
|
|
516
927
|
continue;
|
|
@@ -542,7 +953,7 @@ export function parseTextDslV05(src: string): V05ParseResult {
|
|
|
542
953
|
/*
|
|
543
954
|
* 箱に触れると関係する線だけを光らせるか (#1757)。
|
|
544
955
|
*
|
|
545
|
-
*
|
|
956
|
+
* UML や表の関係を描く record は順番を持たない図で、段を追う見せ方は読み手の問い
|
|
546
957
|
* (この箱はどこと繋がっているか) に答えない。 `hover` と書くと、箱に触れた時に
|
|
547
958
|
* その箱 ・ 繋がる線 ・ 相手の箱だけが光る (`cdl#806`)。
|
|
548
959
|
*/
|
|
@@ -587,29 +998,134 @@ export function parseTextDslV05(src: string): V05ParseResult {
|
|
|
587
998
|
i += 1;
|
|
588
999
|
continue;
|
|
589
1000
|
}
|
|
590
|
-
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") {
|
|
591
1026
|
/*
|
|
592
|
-
*
|
|
1027
|
+
* 図の意匠 (#1553 / #2790)。
|
|
593
1028
|
*
|
|
594
1029
|
* 読めない語はその場で知らせる。 黙って既定に落とすと、書き手には「書いたのに効かない」
|
|
595
1030
|
* としか見えず、書き間違いか未対応かを分けられない。
|
|
596
1031
|
*/
|
|
597
1032
|
const v = (head.value ?? "").trim();
|
|
1033
|
+
const 書いた語 = 書いた頭?.key ?? head.key;
|
|
598
1034
|
if (v.length > 0) {
|
|
599
|
-
const 解けた =
|
|
1035
|
+
const 解けた = resolveTheme(v);
|
|
600
1036
|
if (解けた !== null) {
|
|
601
|
-
|
|
1037
|
+
if (書いた語 === "theme") {
|
|
1038
|
+
theme = 解けた;
|
|
1039
|
+
themeLine = line.no;
|
|
1040
|
+
} else {
|
|
1041
|
+
palette = 解けた;
|
|
1042
|
+
paletteLine = line.no;
|
|
1043
|
+
}
|
|
602
1044
|
} else {
|
|
603
1045
|
errors.push({
|
|
604
1046
|
line: line.no,
|
|
605
|
-
message:
|
|
606
|
-
hint: `使える語 = ${
|
|
1047
|
+
message: `${書いた語} が読めません (書いた値: ${v}。 使える名前 = ${THEMES.join(" / ")})`,
|
|
1048
|
+
hint: `使える語 = ${Object.keys(THEME_ALIAS).join(" / ")}`,
|
|
607
1049
|
});
|
|
608
1050
|
}
|
|
609
1051
|
}
|
|
610
1052
|
i += 1;
|
|
611
1053
|
continue;
|
|
612
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
|
+
}
|
|
613
1129
|
if (head.key === "eyebrow") {
|
|
614
1130
|
// 空で書いた形 (`eyebrow:`) は「書かなかった」 と同じにする。 空文字を残すと
|
|
615
1131
|
// 描画側が中身のない帯を出す
|
|
@@ -619,13 +1135,392 @@ export function parseTextDslV05(src: string): V05ParseResult {
|
|
|
619
1135
|
i += 1;
|
|
620
1136
|
continue;
|
|
621
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
|
+
}
|
|
622
1503
|
if (head.key === "type") {
|
|
623
1504
|
const v = (head.value ?? "").trim().toLowerCase();
|
|
624
|
-
|
|
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)) {
|
|
625
1520
|
errors.push({
|
|
626
1521
|
line: line.no,
|
|
627
|
-
message:
|
|
628
|
-
hint:
|
|
1522
|
+
message: `図種が読めません: "${v}"`,
|
|
1523
|
+
hint: `使える図種 = ${[...PRESET_TYPES, ...TYPE_ALIASES.keys()].join(", ")}`,
|
|
629
1524
|
});
|
|
630
1525
|
} else {
|
|
631
1526
|
type = v as PresetType;
|
|
@@ -635,15 +1530,23 @@ export function parseTextDslV05(src: string): V05ParseResult {
|
|
|
635
1530
|
}
|
|
636
1531
|
if (head.key === "actors") {
|
|
637
1532
|
// 1 行で書いた形と、 続く字下げ行に項目を並べた形の両方を受け付ける
|
|
638
|
-
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
|
+
}
|
|
639
1542
|
actors = [];
|
|
640
1543
|
for (const entry of items) {
|
|
641
1544
|
const base = parseActor(entry[0]!, errors);
|
|
642
1545
|
if (base === null) {
|
|
643
1546
|
errors.push({
|
|
644
1547
|
line: entry[0]!.no,
|
|
645
|
-
message:
|
|
646
|
-
hint: "
|
|
1548
|
+
message: `登場人物の行が読めません: "${entry[0]!.trimmed}"`,
|
|
1549
|
+
hint: "`- Client` または `- Client: storage` の形で書く",
|
|
647
1550
|
});
|
|
648
1551
|
continue;
|
|
649
1552
|
}
|
|
@@ -664,8 +1567,8 @@ export function parseTextDslV05(src: string): V05ParseResult {
|
|
|
664
1567
|
} else {
|
|
665
1568
|
errors.push({
|
|
666
1569
|
line: it.no,
|
|
667
|
-
message:
|
|
668
|
-
hint: '
|
|
1570
|
+
message: `矢印の行が読めません: "${it.trimmed}"`,
|
|
1571
|
+
hint: '`- A -> B: "label"` または `- A -> B: "label" (success)` の形で書く',
|
|
669
1572
|
});
|
|
670
1573
|
}
|
|
671
1574
|
}
|
|
@@ -679,7 +1582,7 @@ export function parseTextDslV05(src: string): V05ParseResult {
|
|
|
679
1582
|
const inner = inlineMatch.slice(1, -1).trim();
|
|
680
1583
|
animate = ensureAnimate(animate, line.no);
|
|
681
1584
|
for (const pair of splitTopLevelCommas(inner)) {
|
|
682
|
-
const st = parseStateEntry(pair, line.no);
|
|
1585
|
+
const st = parseStateEntry(pair, line.no, errors);
|
|
683
1586
|
if (st) animate.states.push(st);
|
|
684
1587
|
}
|
|
685
1588
|
i += 1;
|
|
@@ -688,14 +1591,10 @@ export function parseTextDslV05(src: string): V05ParseResult {
|
|
|
688
1591
|
const { items, next } = collectIndentedList(lines, i + 1, line.indent);
|
|
689
1592
|
animate = ensureAnimate(animate, line.no);
|
|
690
1593
|
for (const it of items) {
|
|
691
|
-
|
|
1594
|
+
// 断り方ごとの文は読み手が持つ (#2401)。 ここで `null` をまとめて叱ると、
|
|
1595
|
+
// 名前の規則を外れただけの行に形の案内が返る
|
|
1596
|
+
const st = parseStateEntry(it.trimmed.replace(/^-\s*/, ""), it.no, errors);
|
|
692
1597
|
if (st) animate.states.push(st);
|
|
693
|
-
else
|
|
694
|
-
errors.push({
|
|
695
|
-
line: it.no,
|
|
696
|
-
message: `invalid state entry: "${it.trimmed}"`,
|
|
697
|
-
hint: "use `name: initial`",
|
|
698
|
-
});
|
|
699
1598
|
}
|
|
700
1599
|
i = next;
|
|
701
1600
|
continue;
|
|
@@ -705,8 +1604,8 @@ export function parseTextDslV05(src: string): V05ParseResult {
|
|
|
705
1604
|
// 入る (`min({a}, {b})`) ため、 `states` が使う素朴な `,` 分割では式が壊れる。
|
|
706
1605
|
//
|
|
707
1606
|
// **`{` で始まるかを見てはいけない** (#1169)。 `values: a: "{b} + 1"` は `{` で始まらない
|
|
708
|
-
// ため判定を通り抜け、 その後 `
|
|
709
|
-
// 1 件も読まれずに黙って消える**。 書き間違いを黙って捨てないという `
|
|
1607
|
+
// ため判定を通り抜け、 その後 `collectIndentedList` が次行以降しか見ないので **値が
|
|
1608
|
+
// 1 件も読まれずに黙って消える**。 書き間違いを黙って捨てないという `collectIndentedList`
|
|
710
1609
|
// を自前で持った理由と矛盾する。
|
|
711
1610
|
//
|
|
712
1611
|
// 同じ行に何か書いてあれば形を問わず弾く = 1 行形は全て受けないという規則そのもの。
|
|
@@ -720,9 +1619,9 @@ export function parseTextDslV05(src: string): V05ParseResult {
|
|
|
720
1619
|
i += 1;
|
|
721
1620
|
continue;
|
|
722
1621
|
}
|
|
723
|
-
//
|
|
724
|
-
//
|
|
725
|
-
const { items, next } =
|
|
1622
|
+
// 字下げした行を 1 行も落とさずに集める。 捨てられると書き間違えた行が
|
|
1623
|
+
// 「書かなかった」 と同じになり、 値が 1 つ消えたことに気付けない
|
|
1624
|
+
const { items, next } = collectIndentedList(lines, i + 1, line.indent);
|
|
726
1625
|
for (const it of items) {
|
|
727
1626
|
const v = parseValueEntry(it.trimmed.replace(/^-\s*/, ""), it.no, errors);
|
|
728
1627
|
if (v) values.push(v);
|
|
@@ -732,8 +1631,16 @@ export function parseTextDslV05(src: string): V05ParseResult {
|
|
|
732
1631
|
}
|
|
733
1632
|
if (head.key === "animation") {
|
|
734
1633
|
// animation: は step を list で並べる
|
|
735
|
-
const { items: stepBlocks, next } = collectAnimationSteps(lines, i + 1, line.indent);
|
|
1634
|
+
const { items: stepBlocks, 届かない, next } = collectAnimationSteps(lines, i + 1, line.indent);
|
|
736
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
|
+
}
|
|
737
1644
|
for (const block of stepBlocks) {
|
|
738
1645
|
const ph = parsePhase(block, errors);
|
|
739
1646
|
if (ph) animate.phases.push(ph);
|
|
@@ -763,7 +1670,7 @@ export function parseTextDslV05(src: string): V05ParseResult {
|
|
|
763
1670
|
// block: viewport:\n width: 1400\n height: 900\n ...
|
|
764
1671
|
// **`collectIndentedList` を使わない** (#1968)。 あちらは `:` を含まない行を黙って捨てるため、
|
|
765
1672
|
// 書き間違えた行が「書かなかった」 と同じになる (`axes:` と同じ理由)
|
|
766
|
-
const { items, next } =
|
|
1673
|
+
const { items, next } = collectIndentedList(lines, i + 1, line.indent);
|
|
767
1674
|
const opts: Record<string, string> = {};
|
|
768
1675
|
// 知らせは **値を書いた行** を指す (#1306)。 `viewport:` の行を指すと、欄が縦に並ぶ形で
|
|
769
1676
|
// どの行を直せばよいか分からない
|
|
@@ -824,7 +1731,7 @@ export function parseTextDslV05(src: string): V05ParseResult {
|
|
|
824
1731
|
}
|
|
825
1732
|
// **`collectIndentedList` を使わない** (Round 1 の指摘)。 あちらは `:` を含まない行を
|
|
826
1733
|
// 黙って捨てるため、書き間違えた行が「書かなかった」 と同じになる
|
|
827
|
-
const { items, next } =
|
|
1734
|
+
const { items, next } = collectIndentedList(lines, i + 1, line.indent);
|
|
828
1735
|
axesLine = line.no;
|
|
829
1736
|
const 組み立て: DslAxes = {};
|
|
830
1737
|
for (const it of items) {
|
|
@@ -832,8 +1739,8 @@ export function parseTextDslV05(src: string): V05ParseResult {
|
|
|
832
1739
|
if (!m) {
|
|
833
1740
|
errors.push({
|
|
834
1741
|
line: it.no,
|
|
835
|
-
message:
|
|
836
|
-
hint: '
|
|
1742
|
+
message: `軸の行が読めません: "${it.trimmed}"`,
|
|
1743
|
+
hint: '`x: { left: "...", right: "..." }` または `y: { bottom: "...", top: "..." }` の形で書く',
|
|
837
1744
|
});
|
|
838
1745
|
continue;
|
|
839
1746
|
}
|
|
@@ -845,7 +1752,12 @@ export function parseTextDslV05(src: string): V05ParseResult {
|
|
|
845
1752
|
it.no,
|
|
846
1753
|
errors,
|
|
847
1754
|
);
|
|
848
|
-
|
|
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 };
|
|
849
1761
|
else 組み立て.y = { bottom: opts.bottom, top: opts.top };
|
|
850
1762
|
}
|
|
851
1763
|
// 1 本も読めなかった形は「書かなかった」 と同じにする。 空の軸を渡すと、
|
|
@@ -854,9 +1766,55 @@ export function parseTextDslV05(src: string): V05ParseResult {
|
|
|
854
1766
|
i = next;
|
|
855
1767
|
continue;
|
|
856
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
|
+
}
|
|
857
1814
|
if (head.key === "bands") {
|
|
858
1815
|
/*
|
|
859
|
-
* 動いている間の帯 (#1466)。 `- DB: 1..2`
|
|
1816
|
+
* 動いている間の帯 (#1466)。 `- DB: 1..2` の形で、言づての番号の区間を書く。
|
|
1817
|
+
* 番号は板に載る言づての行で 0 から数える (#2404 で実測、 段ではない)。
|
|
860
1818
|
*
|
|
861
1819
|
* 面ごとに 2 行以上書ける = 途中で手が空く面はそこで切れる。 書かない図は
|
|
862
1820
|
* 組み立て器が「最初に関わった段から最後まで」 の 1 本にする。
|
|
@@ -870,11 +1828,11 @@ export function parseTextDslV05(src: string): V05ParseResult {
|
|
|
870
1828
|
errors.push({
|
|
871
1829
|
line: it.no,
|
|
872
1830
|
message: `帯の書き方が読めません: "${it.trimmed}"`,
|
|
873
|
-
hint: "
|
|
1831
|
+
hint: "`- DB: 1..2` の形で書く (面の名前と、言づての番号の区間。 番号は 0 から数える)",
|
|
874
1832
|
});
|
|
875
1833
|
continue;
|
|
876
1834
|
}
|
|
877
|
-
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 } });
|
|
878
1836
|
}
|
|
879
1837
|
i = next;
|
|
880
1838
|
continue;
|
|
@@ -899,6 +1857,7 @@ export function parseTextDslV05(src: string): V05ParseResult {
|
|
|
899
1857
|
id,
|
|
900
1858
|
...表で読む(LANE_VALUE_KINDS, opts, `縦列 ${id} の `, it.no, errors),
|
|
901
1859
|
label: opts.label,
|
|
1860
|
+
subtitle: opts.subtitle,
|
|
902
1861
|
// 位置のずらし (#1971)
|
|
903
1862
|
layoutPos: ずらしにまとめる(
|
|
904
1863
|
表で読む(OFFSET_VALUE_KINDS, opts, `縦列 ${id} の `, it.no, errors),
|
|
@@ -908,8 +1867,8 @@ export function parseTextDslV05(src: string): V05ParseResult {
|
|
|
908
1867
|
} else {
|
|
909
1868
|
errors.push({
|
|
910
1869
|
line: it.no,
|
|
911
|
-
message:
|
|
912
|
-
hint: '
|
|
1870
|
+
message: `縦列の行が読めません: "${it.trimmed}"`,
|
|
1871
|
+
hint: '`id: { x: 0, width: 320, label: "..." }` の形で書く',
|
|
913
1872
|
});
|
|
914
1873
|
}
|
|
915
1874
|
}
|
|
@@ -930,7 +1889,7 @@ export function parseTextDslV05(src: string): V05ParseResult {
|
|
|
930
1889
|
}
|
|
931
1890
|
// `collectIndentedList` は `:` の無い行を落とす。 部品を綴り違えた行も知らせるため、
|
|
932
1891
|
// 字下げした行を全て読み手へ渡す。
|
|
933
|
-
const { items, next } =
|
|
1892
|
+
const { items, next } = collectIndentedList(lines, i + 1, line.indent);
|
|
934
1893
|
readoutsList = [];
|
|
935
1894
|
for (const it of items) {
|
|
936
1895
|
const m = 名前と中括弧に割る(it.trimmed);
|
|
@@ -940,8 +1899,8 @@ export function parseTextDslV05(src: string): V05ParseResult {
|
|
|
940
1899
|
} else {
|
|
941
1900
|
errors.push({
|
|
942
1901
|
line: it.no,
|
|
943
|
-
message:
|
|
944
|
-
hint: "
|
|
1902
|
+
message: `読み取り値の行が読めません: "${it.trimmed}"`,
|
|
1903
|
+
hint: "`id: { kind: percent-ring, source: total, max: 500 }` の形で書く",
|
|
945
1904
|
});
|
|
946
1905
|
}
|
|
947
1906
|
}
|
|
@@ -962,7 +1921,7 @@ export function parseTextDslV05(src: string): V05ParseResult {
|
|
|
962
1921
|
}
|
|
963
1922
|
// `collectIndentedList` は `:` の無い行を落とす。 つまみを綴り違えた行も知らせるため、
|
|
964
1923
|
// 字下げした行を全て読み手へ渡す。
|
|
965
|
-
const { items, next } =
|
|
1924
|
+
const { items, next } = collectIndentedList(lines, i + 1, line.indent);
|
|
966
1925
|
inputsList = [];
|
|
967
1926
|
for (const it of items) {
|
|
968
1927
|
const m = 名前と中括弧に割る(it.trimmed);
|
|
@@ -972,8 +1931,8 @@ export function parseTextDslV05(src: string): V05ParseResult {
|
|
|
972
1931
|
} else {
|
|
973
1932
|
errors.push({
|
|
974
1933
|
line: it.no,
|
|
975
|
-
message:
|
|
976
|
-
hint: "
|
|
1934
|
+
message: `つまみの行が読めません: "${it.trimmed}"`,
|
|
1935
|
+
hint: "`id: { kind: slider, min: 0, max: 100, defaultValue: 50 }` の形で書く",
|
|
977
1936
|
});
|
|
978
1937
|
}
|
|
979
1938
|
}
|
|
@@ -995,7 +1954,7 @@ export function parseTextDslV05(src: string): V05ParseResult {
|
|
|
995
1954
|
continue;
|
|
996
1955
|
}
|
|
997
1956
|
// 字下げした行を全て読み手へ渡す = 綴りを誤った行も知らせるため
|
|
998
|
-
const { items, next } =
|
|
1957
|
+
const { items, next } = collectIndentedList(lines, i + 1, line.indent);
|
|
999
1958
|
formulasList = [];
|
|
1000
1959
|
for (const it of items) {
|
|
1001
1960
|
const f = 式として読む(it.trimmed.replace(/^-\s*/, ""), it.no, errors);
|
|
@@ -1015,7 +1974,7 @@ export function parseTextDslV05(src: string): V05ParseResult {
|
|
|
1015
1974
|
i += 1;
|
|
1016
1975
|
continue;
|
|
1017
1976
|
}
|
|
1018
|
-
const { items, next } =
|
|
1977
|
+
const { items, next } = collectIndentedList(lines, i + 1, line.indent);
|
|
1019
1978
|
eventsList = [];
|
|
1020
1979
|
for (const it of items) {
|
|
1021
1980
|
const e = 出来事として読む(it.trimmed.replace(/^-\s*/, ""), it.no, errors);
|
|
@@ -1035,7 +1994,7 @@ export function parseTextDslV05(src: string): V05ParseResult {
|
|
|
1035
1994
|
i += 1;
|
|
1036
1995
|
continue;
|
|
1037
1996
|
}
|
|
1038
|
-
const { items, next } =
|
|
1997
|
+
const { items, next } = collectIndentedList(lines, i + 1, line.indent);
|
|
1039
1998
|
scrollsList = [];
|
|
1040
1999
|
scrollLines = new Map();
|
|
1041
2000
|
for (const it of items) {
|
|
@@ -1057,8 +2016,8 @@ export function parseTextDslV05(src: string): V05ParseResult {
|
|
|
1057
2016
|
} else {
|
|
1058
2017
|
errors.push({
|
|
1059
2018
|
line: it.no,
|
|
1060
|
-
message:
|
|
1061
|
-
hint: "
|
|
2019
|
+
message: `巻き上げの行が読めません: "${it.trimmed}"`,
|
|
2020
|
+
hint: "`id: { start: 0.9, end: 0.1, scrub: 1 }` の形で書く",
|
|
1062
2021
|
});
|
|
1063
2022
|
}
|
|
1064
2023
|
}
|
|
@@ -1089,8 +2048,8 @@ export function parseTextDslV05(src: string): V05ParseResult {
|
|
|
1089
2048
|
} else {
|
|
1090
2049
|
errors.push({
|
|
1091
2050
|
line: it.no,
|
|
1092
|
-
message:
|
|
1093
|
-
hint: '
|
|
2051
|
+
message: `組の行が読めません: "${it.trimmed}"`,
|
|
2052
|
+
hint: '`id: { label: "...", lanes: [a, b] }` の形で書く',
|
|
1094
2053
|
});
|
|
1095
2054
|
}
|
|
1096
2055
|
}
|
|
@@ -1101,12 +2060,21 @@ export function parseTextDslV05(src: string): V05ParseResult {
|
|
|
1101
2060
|
}
|
|
1102
2061
|
|
|
1103
2062
|
if (!title)
|
|
1104
|
-
errors.push({
|
|
2063
|
+
errors.push({
|
|
2064
|
+
line: 1,
|
|
2065
|
+
message: "title が書かれていません",
|
|
2066
|
+
hint: "`title: \"...\"` を最上位に書く",
|
|
2067
|
+
});
|
|
1105
2068
|
if (!type)
|
|
1106
2069
|
errors.push({
|
|
1107
2070
|
line: 1,
|
|
1108
|
-
message: "type
|
|
1109
|
-
|
|
2071
|
+
message: "type が書かれていません",
|
|
2072
|
+
/*
|
|
2073
|
+
* **図種の一覧を手で並べない** (#2341)。 並べていた間、ここは実装が受ける図種の
|
|
2074
|
+
* 半分しか挙げず、値を並べる図種 (棒 / 折れ線 / 木 など) がまるごと抜けていた。
|
|
2075
|
+
* すぐ上の `図種が読めません` は元から実装を読んでおり、2 つの一覧が食い違っていた。
|
|
2076
|
+
*/
|
|
2077
|
+
hint: `\`type: flow\` の形で最上位に書く (使える図種 = ${Array.from(PRESET_TYPES).join(", ")})`,
|
|
1110
2078
|
});
|
|
1111
2079
|
|
|
1112
2080
|
// つまみ・式・巻き上げは同じ名前空間で値を作る。 重なると後から作る値が効かない。
|
|
@@ -1123,7 +2091,25 @@ export function parseTextDslV05(src: string): V05ParseResult {
|
|
|
1123
2091
|
});
|
|
1124
2092
|
}
|
|
1125
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
|
+
|
|
1126
2111
|
if (errors.length > 0) return { ok: false, errors };
|
|
2112
|
+
const 選んだ意匠 = theme ?? palette;
|
|
1127
2113
|
|
|
1128
2114
|
return {
|
|
1129
2115
|
ok: true,
|
|
@@ -1134,8 +2120,80 @@ export function parseTextDslV05(src: string): V05ParseResult {
|
|
|
1134
2120
|
...(reveal !== null ? { reveal } : {}),
|
|
1135
2121
|
...(relations !== null ? { relations } : {}),
|
|
1136
2122
|
...(direction !== null ? { direction, directionPos: { line: directionLine } } : {}),
|
|
1137
|
-
...(
|
|
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
|
+
: {}),
|
|
1138
2195
|
...(axes !== undefined ? { axes, axesPos: { line: axesLine } } : {}),
|
|
2196
|
+
...(regions !== undefined ? { regions } : {}),
|
|
1139
2197
|
actors,
|
|
1140
2198
|
flow,
|
|
1141
2199
|
animate,
|
|
@@ -1170,10 +2228,125 @@ type TopHeader = { key: string; value: string | null };
|
|
|
1170
2228
|
|
|
1171
2229
|
function matchTopHeader(trimmed: string): TopHeader | null {
|
|
1172
2230
|
// 形式: `key:` or `key: value`
|
|
1173
|
-
const m = trimmed.match(/^([
|
|
2231
|
+
const m = trimmed.match(/^([^\s:]+)\s*:\s*(.*)$/u);
|
|
1174
2232
|
if (!m) return null;
|
|
1175
2233
|
const value = (m[2] ?? "").trim();
|
|
1176
|
-
|
|
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;
|
|
2290
|
+
}
|
|
2291
|
+
|
|
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
|
+
}
|
|
2310
|
+
|
|
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;
|
|
1177
2350
|
}
|
|
1178
2351
|
|
|
1179
2352
|
/**
|
|
@@ -1470,6 +2643,7 @@ function 表で読む<T extends Record<string, 値の形>>(
|
|
|
1470
2643
|
export const VIEWPORT_VALUE_KINDS = {
|
|
1471
2644
|
width: "数",
|
|
1472
2645
|
height: "数",
|
|
2646
|
+
contentOffsetX: "数",
|
|
1473
2647
|
scale: "数",
|
|
1474
2648
|
laneWidth: "数",
|
|
1475
2649
|
gap: "数",
|
|
@@ -1508,13 +2682,14 @@ export const LANE_VALUE_KINDS = {
|
|
|
1508
2682
|
|
|
1509
2683
|
/**
|
|
1510
2684
|
* 縦列の中括弧に書ける項目 (#1968)。 数と真偽の欄は `LANE_VALUE_KINDS` と `OFFSET_VALUE_KINDS` から
|
|
1511
|
-
* 導き、表に載せない文字列の `label` だけを足す。
|
|
2685
|
+
* 導き、表に載せない文字列の `label` と `subtitle` だけを足す。
|
|
1512
2686
|
*/
|
|
1513
2687
|
export const LANE_INLINE_KEYS: readonly (keyof DslLane | keyof typeof OFFSET_VALUE_KINDS)[] = [
|
|
1514
2688
|
...(Object.keys(LANE_VALUE_KINDS) as (keyof typeof LANE_VALUE_KINDS)[]),
|
|
1515
2689
|
// 位置のずらし (#1971)。 JSON の縦列の `pos` と同じ `layoutPos` に入る
|
|
1516
2690
|
...(Object.keys(OFFSET_VALUE_KINDS) as (keyof typeof OFFSET_VALUE_KINDS)[]),
|
|
1517
2691
|
"label",
|
|
2692
|
+
"subtitle",
|
|
1518
2693
|
];
|
|
1519
2694
|
|
|
1520
2695
|
/** 組の中括弧に書ける項目 (#1968)。 `groups:` の読み手が拾う欄と揃える */
|
|
@@ -1524,12 +2699,18 @@ export const GROUP_INLINE_KEYS = ["label", "lanes"] as const satisfies readonly
|
|
|
1524
2699
|
>[];
|
|
1525
2700
|
|
|
1526
2701
|
/** 軸の名前の中括弧に書ける項目 (#1968)。 横軸は左右、縦軸は下上 */
|
|
1527
|
-
export const AXIS_X_KEYS = ["left", "right"] as const satisfies readonly (
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
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
|
+
)[];
|
|
1533
2714
|
|
|
1534
2715
|
/**
|
|
1535
2716
|
* 箱の中に描く図形 (`shape:`) と、値を見せる部品 (`readouts:`) の欄 (#1374)。
|
|
@@ -1732,7 +2913,11 @@ function 選択肢の並びとして読む(
|
|
|
1732
2913
|
): (string | { value: string; label: string })[] | undefined {
|
|
1733
2914
|
const 中身 = 値.trim();
|
|
1734
2915
|
if (!中身.startsWith("[") || !中身.endsWith("]")) {
|
|
1735
|
-
errors.push({
|
|
2916
|
+
errors.push({
|
|
2917
|
+
line,
|
|
2918
|
+
message: `${名} の並びが読めません: "${値}"`,
|
|
2919
|
+
hint: '`["green", { value: "red", label: "失敗" }]` の形で書く',
|
|
2920
|
+
});
|
|
1736
2921
|
return undefined;
|
|
1737
2922
|
}
|
|
1738
2923
|
const 出: (string | { value: string; label: string })[] = [];
|
|
@@ -1745,19 +2930,35 @@ function 選択肢の並びとして読む(
|
|
|
1745
2930
|
return;
|
|
1746
2931
|
}
|
|
1747
2932
|
if (!要素.endsWith("}")) {
|
|
1748
|
-
errors.push({
|
|
2933
|
+
errors.push({
|
|
2934
|
+
line,
|
|
2935
|
+
message: `${名}[${i}] の組が読めません: "${要素}"`,
|
|
2936
|
+
hint: '`{ value: "red", label: "失敗" }` の形で書く',
|
|
2937
|
+
});
|
|
1749
2938
|
return;
|
|
1750
2939
|
}
|
|
1751
2940
|
const 組 = parseInlineMapping(要素.slice(1, -1));
|
|
1752
2941
|
for (const k of Object.keys(組)) {
|
|
1753
2942
|
if ((選択肢の組の項目 as readonly string[]).includes(k)) continue;
|
|
1754
|
-
errors.push({
|
|
2943
|
+
errors.push({
|
|
2944
|
+
line,
|
|
2945
|
+
message: `${名}[${i}] の項目名が読めません: "${k}"`,
|
|
2946
|
+
hint: `使える項目 = ${選択肢の組の項目.join(", ")}`,
|
|
2947
|
+
});
|
|
1755
2948
|
}
|
|
1756
2949
|
for (const k of 選択肢の組の項目) {
|
|
1757
2950
|
if (組[k] === undefined) {
|
|
1758
|
-
errors.push({
|
|
2951
|
+
errors.push({
|
|
2952
|
+
line,
|
|
2953
|
+
message: `${名}[${i}].${k} は必ず書きます`,
|
|
2954
|
+
hint: `必須の項目 = ${選択肢の組の項目.join(", ")}`,
|
|
2955
|
+
});
|
|
1759
2956
|
} else if (組[k] === "") {
|
|
1760
|
-
errors.push({
|
|
2957
|
+
errors.push({
|
|
2958
|
+
line,
|
|
2959
|
+
message: `${名}[${i}].${k} が空です`,
|
|
2960
|
+
hint: k === "label" ? "名前を書かないなら文字列だけを並べる" : "値を 1 文字以上書く",
|
|
2961
|
+
});
|
|
1761
2962
|
}
|
|
1762
2963
|
}
|
|
1763
2964
|
if (組.value && 組.label) 出.push({ value: 組.value, label: 組.label });
|
|
@@ -2059,7 +3260,7 @@ function 部品として読む(
|
|
|
2059
3260
|
const 読めた = 表に従って読む(定義, opts, `部品 ${id} の `, line, errors);
|
|
2060
3261
|
部品の組を検査する(kind, 読めた, line, errors);
|
|
2061
3262
|
for (const 欄 of 定義.必須) if (読めた[欄] === undefined) return undefined;
|
|
2062
|
-
return { id, kind,
|
|
3263
|
+
return { id, kind, ...読めた, pos: { line } } as DslReadout;
|
|
2063
3264
|
}
|
|
2064
3265
|
|
|
2065
3266
|
/**
|
|
@@ -2315,7 +3516,12 @@ function 式として読む(行: string, line: number, errors: DslError[]): DslF
|
|
|
2315
3516
|
});
|
|
2316
3517
|
return undefined;
|
|
2317
3518
|
}
|
|
2318
|
-
return {
|
|
3519
|
+
return {
|
|
3520
|
+
id: 名前,
|
|
3521
|
+
expression: 式,
|
|
3522
|
+
...(名札 !== undefined ? { label: 名札 } : {}),
|
|
3523
|
+
pos: { line },
|
|
3524
|
+
};
|
|
2319
3525
|
}
|
|
2320
3526
|
|
|
2321
3527
|
/** 組の中の項目名の形。 `parseInlineMapping` が項目名として受ける字と同じ */
|
|
@@ -2348,7 +3554,9 @@ function 式の組を読む(
|
|
|
2348
3554
|
): { expression: string; label?: string } | undefined {
|
|
2349
3555
|
const 使える = ["expression", "label"];
|
|
2350
3556
|
const 読めた: Record<string, string> = {};
|
|
2351
|
-
for (const 欄 of splitInlineFields(中身)
|
|
3557
|
+
for (const 欄 of splitInlineFields(中身)
|
|
3558
|
+
.map((s) => s.trim())
|
|
3559
|
+
.filter((s) => s !== "")) {
|
|
2352
3560
|
const m = 欄.match(組の項目名);
|
|
2353
3561
|
if (!m) {
|
|
2354
3562
|
errors.push({
|
|
@@ -2419,19 +3627,40 @@ export const ACTOR_INLINE_VALUE_KINDS = {
|
|
|
2419
3627
|
posY: "数",
|
|
2420
3628
|
posW: "数",
|
|
2421
3629
|
posH: "数",
|
|
3630
|
+
titleFontSize: "数",
|
|
3631
|
+
markGap: "数",
|
|
3632
|
+
milestone: "真偽",
|
|
3633
|
+
rowRules: "真偽",
|
|
3634
|
+
rowStripe: "真偽",
|
|
2422
3635
|
} as const satisfies Record<string, 値の形>;
|
|
2423
3636
|
|
|
2424
|
-
/**
|
|
3637
|
+
/**
|
|
3638
|
+
* 縦に並べる形で箱に書ける、数と真偽の欄 (#1306)。 `posW` / `posH` は `大きさ:` が受ける。
|
|
3639
|
+
*
|
|
3640
|
+
* 始まりと終わりの印 (`initial` / `final`) は #2346 で足した。 中括弧の表にはあったのに
|
|
3641
|
+
* こちらに無く、**段を分けた側には書き方が 1 つも無かった** = 書くと綴りの誤りとして落ちる
|
|
3642
|
+
*/
|
|
2425
3643
|
export const ACTOR_BLOCK_VALUE_KINDS = {
|
|
2426
3644
|
stack: "数",
|
|
2427
3645
|
posX: "数",
|
|
2428
3646
|
posY: "数",
|
|
3647
|
+
initial: "真偽",
|
|
3648
|
+
final: "真偽",
|
|
3649
|
+
titleFontSize: "数",
|
|
3650
|
+
markGap: "数",
|
|
3651
|
+
milestone: "真偽",
|
|
3652
|
+
rowRules: "真偽",
|
|
3653
|
+
rowStripe: "真偽",
|
|
2429
3654
|
} as const satisfies Record<string, 値の形>;
|
|
2430
3655
|
|
|
2431
3656
|
/** 矢印の中括弧に書ける、数と真偽の欄 (#1306) */
|
|
2432
3657
|
export const FLOW_INLINE_VALUE_KINDS = {
|
|
2433
3658
|
labelOffsetX: "数",
|
|
2434
3659
|
labelOffsetY: "数",
|
|
3660
|
+
posX1: "数",
|
|
3661
|
+
posY1: "数",
|
|
3662
|
+
posX2: "数",
|
|
3663
|
+
posY2: "数",
|
|
2435
3664
|
overlay: "真偽",
|
|
2436
3665
|
} as const satisfies Record<string, 値の形>;
|
|
2437
3666
|
|
|
@@ -2520,10 +3749,10 @@ function parseInlineMapping(inner: string): Record<string, string> {
|
|
|
2520
3749
|
// 読める名前を広げても、**知っている名前しか使われない**。 パーツの状態の上書きに
|
|
2521
3750
|
// 流れるのは `ACTOR_RESERVED_FIELDS` に無い名前だけで、日本語の項目名 (`位置` / `大きさ`
|
|
2522
3751
|
// 等) はそこに載せてあるため、これまでどおり落ちる
|
|
2523
|
-
const m = p.match(/^\s*([^\s:,{}[\]"']+)\s*:\s*(.+?)\s*$/);
|
|
3752
|
+
const m = p.match(/^\s*(?:"([^"]+)"|'([^']+)'|([^\s:,{}[\]"']+))\s*:\s*(.+?)\s*$/);
|
|
2524
3753
|
if (m) {
|
|
2525
|
-
const key = m[1]!;
|
|
2526
|
-
out[key] = stripQuotes(m[
|
|
3754
|
+
const key = m[1] ?? m[2] ?? m[3]!;
|
|
3755
|
+
out[key] = stripQuotes(m[4]!.trim());
|
|
2527
3756
|
}
|
|
2528
3757
|
}
|
|
2529
3758
|
return out;
|
|
@@ -2541,7 +3770,7 @@ function writtenScaleFields(inner: string): Map<string, string> {
|
|
|
2541
3770
|
for (const field of splitInlineFields(inner)) {
|
|
2542
3771
|
const idx = field.indexOf(":");
|
|
2543
3772
|
if (idx < 0) continue;
|
|
2544
|
-
const key = field.slice(0, idx).trim();
|
|
3773
|
+
const key = stripQuotes(field.slice(0, idx).trim());
|
|
2545
3774
|
if (!SCALE_KEYS.has(key)) continue;
|
|
2546
3775
|
// 同じ名前を 2 度書いたら後の値を採る
|
|
2547
3776
|
out.set(key, stripQuotes(field.slice(idx + 1).trim()));
|
|
@@ -2613,6 +3842,81 @@ function splitInlineFields(inner: string): string[] {
|
|
|
2613
3842
|
return 見た.閉じた ? 見た.parts : 引用符を見て割る(false).parts;
|
|
2614
3843
|
}
|
|
2615
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
|
+
*/
|
|
2616
3920
|
function collectIndentedList(
|
|
2617
3921
|
lines: Line[],
|
|
2618
3922
|
start: number,
|
|
@@ -2622,17 +3926,13 @@ function collectIndentedList(
|
|
|
2622
3926
|
let i = start;
|
|
2623
3927
|
while (i < lines.length) {
|
|
2624
3928
|
const ln = lines[i]!;
|
|
2625
|
-
if (!ln.trimmed) {
|
|
3929
|
+
if (!ln.trimmed || ln.trimmed.startsWith("#")) {
|
|
2626
3930
|
i += 1;
|
|
2627
3931
|
continue;
|
|
2628
3932
|
}
|
|
2629
3933
|
if (ln.indent <= parentIndent) break;
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
} else if (ln.trimmed.includes(":")) {
|
|
2633
|
-
// YAML 風 inline (key: value) は state 用 block で許容
|
|
2634
|
-
items.push(ln);
|
|
2635
|
-
}
|
|
3934
|
+
// 先頭の `- ` を外して渡す。 外した形と、 YAML 風の `key: value` を読み手が同じに読む
|
|
3935
|
+
items.push(ln.trimmed.startsWith("- ") ? { ...ln, trimmed: ln.trimmed.slice(2).trim() } : ln);
|
|
2636
3936
|
i += 1;
|
|
2637
3937
|
}
|
|
2638
3938
|
return { items, next: i };
|
|
@@ -2741,7 +4041,7 @@ function applyContinuationLines(actor: DslActor, rest: Line[], errors: DslError[
|
|
|
2741
4041
|
/** 倍率を名前ごとに最後に書いた行。 別名の優先順と行番号を取り違えないために保持する。 */
|
|
2742
4042
|
const scaleLines = new Map<string, number>();
|
|
2743
4043
|
/**
|
|
2744
|
-
*
|
|
4044
|
+
* 縦に並べて書かれたユーザージャーニーの欄 (#1251)。
|
|
2745
4045
|
*
|
|
2746
4046
|
* パーツでは状態の上書きとして意味を持つため、どちらに入れるかは block を読み終わってから
|
|
2747
4047
|
* 決める。 `kind:` の行が後ろに書かれることもあり、読んだ時点ではパーツか分からない。
|
|
@@ -2801,6 +4101,111 @@ function applyContinuationLines(actor: DslActor, rest: Line[], errors: DslError[
|
|
|
2801
4101
|
case "補足":
|
|
2802
4102
|
out.subtitle = stripQuotes(raw);
|
|
2803
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
|
+
}
|
|
2804
4209
|
case "value":
|
|
2805
4210
|
case "値":
|
|
2806
4211
|
out.value = stripQuotes(raw);
|
|
@@ -2848,20 +4253,20 @@ function applyContinuationLines(actor: DslActor, rest: Line[], errors: DslError[
|
|
|
2848
4253
|
.filter(Boolean);
|
|
2849
4254
|
break;
|
|
2850
4255
|
/*
|
|
2851
|
-
* 行頭の印 (#1466)。 `rows`
|
|
4256
|
+
* 行頭の印 (#1466)。 `rows` と同じ並びで書く。
|
|
4257
|
+
*
|
|
4258
|
+
* **語は dragon の 3 つ** (`鍵` / `外` / `条件`)。 印の 3 軸 (下線 / 形 / 塗り) に
|
|
4259
|
+
* 1 対 1 で当たる (`compile/row-marks.ts` が表を持つ)。
|
|
2852
4260
|
*
|
|
2853
|
-
*
|
|
2854
|
-
*
|
|
2855
|
-
*
|
|
4261
|
+
* 古い語 (`pk` / `fk` / `opt` / `entry` / `exit` / `do` / `internal`) はここで
|
|
4262
|
+
* 3 語へ読み替える (#2782)。 **読み替えは読み取りの入口 1 か所に閉じる** =
|
|
4263
|
+
* ここを通った後は古い語がどこにも残らないので、組み立て側は 3 語だけを扱う。
|
|
2856
4264
|
*
|
|
2857
4265
|
* 空の要素を捨てない (`filter(Boolean)` を掛けない) = 並びが `rows` とずれる。
|
|
2858
4266
|
*/
|
|
2859
4267
|
case "marks":
|
|
2860
4268
|
case "印":
|
|
2861
|
-
out.marks = raw
|
|
2862
|
-
.replace(/^\[|\]$/g, "")
|
|
2863
|
-
.split(/,(?![^[]*\])/)
|
|
2864
|
-
.map((x) => stripQuotes(x.trim()));
|
|
4269
|
+
out.marks = 行頭の印の並びとして読む(raw, ln.no, errors);
|
|
2865
4270
|
break;
|
|
2866
4271
|
case "位置":
|
|
2867
4272
|
case "pos": {
|
|
@@ -2909,6 +4314,11 @@ function applyContinuationLines(actor: DslActor, rest: Line[], errors: DslError[
|
|
|
2909
4314
|
case "posY":
|
|
2910
4315
|
out.posY = 数として読む(raw, "箱の posY", ln.no, errors);
|
|
2911
4316
|
break;
|
|
4317
|
+
case "at":
|
|
4318
|
+
case "点の位置":
|
|
4319
|
+
out.at = 四象限の座標として控える(raw);
|
|
4320
|
+
out.atPos = { line: ln.no };
|
|
4321
|
+
break;
|
|
2912
4322
|
// 位置のずらし (#1971)。 中括弧の形と同じ表で読み、片方だけ書いた時は残りを 0 とする
|
|
2913
4323
|
case "offsetX":
|
|
2914
4324
|
case "offsetY": {
|
|
@@ -2941,12 +4351,15 @@ function applyContinuationLines(actor: DslActor, rest: Line[], errors: DslError[
|
|
|
2941
4351
|
}
|
|
2942
4352
|
case "touchpoint":
|
|
2943
4353
|
case "opportunity":
|
|
4354
|
+
case "opportunityPosition":
|
|
2944
4355
|
case "owner":
|
|
2945
4356
|
case "end":
|
|
4357
|
+
case "stage":
|
|
4358
|
+
case "段階":
|
|
2946
4359
|
// **どちらに入れるかは block を読み終わるまで決まらない** (#1251 Round 1 の指摘)。
|
|
2947
4360
|
// パーツかどうかは `kind:` の行で決まり、それが後ろに書かれることもある。
|
|
2948
4361
|
// 倍率 (`scaleWritten`) と読めない項目名 (`unknownKeys`) が同じ理由で後回しにしている
|
|
2949
|
-
図種ごとの欄.set(key, stripQuotes(raw));
|
|
4362
|
+
図種ごとの欄.set(key === "段階" ? "stage" : key, stripQuotes(raw));
|
|
2950
4363
|
break;
|
|
2951
4364
|
case "lane":
|
|
2952
4365
|
out.lane = stripQuotes(raw);
|
|
@@ -2969,7 +4382,7 @@ function applyContinuationLines(actor: DslActor, rest: Line[], errors: DslError[
|
|
|
2969
4382
|
out.scale = s.scale;
|
|
2970
4383
|
out.scaleKeys = [...new Set([...(actor.scaleKeys ?? []), ...s.keys])];
|
|
2971
4384
|
}
|
|
2972
|
-
//
|
|
4385
|
+
// ユーザージャーニーの欄は、パーツなら状態の上書き、そうでなければ道筋の欄として入れる
|
|
2973
4386
|
for (const [key, v] of 図種ごとの欄) {
|
|
2974
4387
|
if (out.partId !== undefined) {
|
|
2975
4388
|
state[key] = coerceStateValue(v);
|
|
@@ -2978,8 +4391,20 @@ function applyContinuationLines(actor: DslActor, rest: Line[], errors: DslError[
|
|
|
2978
4391
|
}
|
|
2979
4392
|
if (key === "touchpoint") out.touchpoint = v;
|
|
2980
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
|
+
}
|
|
2981
4405
|
else if (key === "owner") out.owner = v;
|
|
2982
|
-
else out.end = v;
|
|
4406
|
+
else if (key === "end") out.end = v;
|
|
4407
|
+
else out.stage = v;
|
|
2983
4408
|
}
|
|
2984
4409
|
// 状態も倍率も parts でだけ意味を持つ。 パーツなら知らせずに返す
|
|
2985
4410
|
if (out.partId !== undefined) {
|
|
@@ -3019,7 +4444,7 @@ function applyContinuationLines(actor: DslActor, rest: Line[], errors: DslError[
|
|
|
3019
4444
|
errors.push({
|
|
3020
4445
|
line: u.line,
|
|
3021
4446
|
message: `項目名が読めません: "${u.key}"`,
|
|
3022
|
-
hint:
|
|
4447
|
+
hint: 弾いた項目の案内(u.key),
|
|
3023
4448
|
});
|
|
3024
4449
|
}
|
|
3025
4450
|
return out;
|
|
@@ -3035,6 +4460,22 @@ export const ACTOR_ITEM_KEYS: ReadonlySet<string> = new Set([
|
|
|
3035
4460
|
"kind",
|
|
3036
4461
|
"種類",
|
|
3037
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",
|
|
3038
4479
|
"補足",
|
|
3039
4480
|
"value",
|
|
3040
4481
|
"値",
|
|
@@ -3074,14 +4515,61 @@ export const ACTOR_ITEM_KEYS: ReadonlySet<string> = new Set([
|
|
|
3074
4515
|
"scale",
|
|
3075
4516
|
"lane",
|
|
3076
4517
|
"stack",
|
|
3077
|
-
|
|
4518
|
+
"stage",
|
|
4519
|
+
"段階",
|
|
4520
|
+
// ユーザージャーニーの欄 (#1251)
|
|
3078
4521
|
"touchpoint",
|
|
3079
4522
|
"opportunity",
|
|
4523
|
+
"opportunityPosition",
|
|
3080
4524
|
// 工程の並びの欄 (#1251)
|
|
3081
4525
|
"owner",
|
|
3082
4526
|
"end",
|
|
4527
|
+
"startLabel",
|
|
4528
|
+
"milestone",
|
|
4529
|
+
"emphasis",
|
|
4530
|
+
"at",
|
|
4531
|
+
"点の位置",
|
|
4532
|
+
// 箱の目次と、始まりと終わりの印 (#2346)。 中括弧にしか書き方が無かった 3 件
|
|
4533
|
+
"eyebrow",
|
|
4534
|
+
"initial",
|
|
4535
|
+
"final",
|
|
3083
4536
|
]);
|
|
3084
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
|
+
|
|
3085
4573
|
/**
|
|
3086
4574
|
* 相対で書かれた位置が解けるかを確かめる。
|
|
3087
4575
|
*
|
|
@@ -3159,14 +4647,16 @@ function collectActorEntries(
|
|
|
3159
4647
|
lines: Line[],
|
|
3160
4648
|
start: number,
|
|
3161
4649
|
parentIndent: number,
|
|
3162
|
-
): { items: Line[][]; next: number } {
|
|
4650
|
+
): { items: Line[][]; 届かない: Line[]; next: number } {
|
|
3163
4651
|
const items: Line[][] = [];
|
|
4652
|
+
/** 1 件の頭にも続きにもならなかった行 (#2400)。 呼出側が知らせにする */
|
|
4653
|
+
const 届かない: Line[] = [];
|
|
3164
4654
|
let cur: Line[] | null = null;
|
|
3165
4655
|
let headIndent = -1;
|
|
3166
4656
|
let i = start;
|
|
3167
4657
|
while (i < lines.length) {
|
|
3168
4658
|
const ln = lines[i]!;
|
|
3169
|
-
if (!ln.trimmed) {
|
|
4659
|
+
if (!ln.trimmed || ln.trimmed.startsWith("#")) {
|
|
3170
4660
|
i += 1;
|
|
3171
4661
|
continue;
|
|
3172
4662
|
}
|
|
@@ -3178,25 +4668,34 @@ function collectActorEntries(
|
|
|
3178
4668
|
} else if (cur && ln.indent > headIndent) {
|
|
3179
4669
|
// 頭より深い字下げは、 直前の 1 件の続き
|
|
3180
4670
|
cur.push(ln);
|
|
4671
|
+
} else {
|
|
4672
|
+
// 続きにも頭にもならない行を **捨てない** (#2400)。 呼出側が「読めません」 として知らせる。
|
|
4673
|
+
// 捨てると `- ` を書き落とした 1 行が知らせにも図にも現れない (実測 = 箱が 1 つ消えた)
|
|
4674
|
+
//
|
|
4675
|
+
// **ここで 1 件の頭にはしない**。 `- Web:` と同じ深さに書いた `補足: "..."` が
|
|
4676
|
+
// 登場人物 `補足` になり、書き間違いが別の意味に化ける (#1367 の検査が守っている形)
|
|
4677
|
+
届かない.push(ln);
|
|
3181
4678
|
}
|
|
3182
4679
|
i += 1;
|
|
3183
4680
|
}
|
|
3184
4681
|
if (cur) items.push(cur);
|
|
3185
|
-
return { items, next: i };
|
|
4682
|
+
return { items, 届かない, next: i };
|
|
3186
4683
|
}
|
|
3187
4684
|
|
|
3188
4685
|
function collectAnimationSteps(
|
|
3189
4686
|
lines: Line[],
|
|
3190
4687
|
start: number,
|
|
3191
4688
|
parentIndent: number,
|
|
3192
|
-
): { items: Line[][]; next: number } {
|
|
4689
|
+
): { items: Line[][]; 届かない: Line[]; next: number } {
|
|
3193
4690
|
// 各 `- step: "..."` 開始を 1 block の頭として識別、 後続の同 indent 以下を block 本文として吸収
|
|
3194
4691
|
const out: Line[][] = [];
|
|
4692
|
+
/** どの段にも属さなかった行 (#2400)。 呼出側が知らせにする */
|
|
4693
|
+
const 届かない: Line[] = [];
|
|
3195
4694
|
let i = start;
|
|
3196
4695
|
let cur: Line[] | null = null;
|
|
3197
4696
|
while (i < lines.length) {
|
|
3198
4697
|
const ln = lines[i]!;
|
|
3199
|
-
if (!ln.trimmed) {
|
|
4698
|
+
if (!ln.trimmed || ln.trimmed.startsWith("#")) {
|
|
3200
4699
|
i += 1;
|
|
3201
4700
|
continue;
|
|
3202
4701
|
}
|
|
@@ -3210,11 +4709,15 @@ function collectAnimationSteps(
|
|
|
3210
4709
|
} else if (cur) {
|
|
3211
4710
|
// 続く property line (focus / tween / set / badge / description)
|
|
3212
4711
|
cur.push(ln);
|
|
4712
|
+
} else {
|
|
4713
|
+
// 最初の段が始まる前の行を **捨てない** (#2400)。 呼出側が「読めません」 として知らせる。
|
|
4714
|
+
// 捨てると `- ` を書き落とした段が丸ごと黙って消える
|
|
4715
|
+
届かない.push(ln);
|
|
3213
4716
|
}
|
|
3214
4717
|
i += 1;
|
|
3215
4718
|
}
|
|
3216
4719
|
if (cur) out.push(cur);
|
|
3217
|
-
return { items: out, next: i };
|
|
4720
|
+
return { items: out, 届かない, next: i };
|
|
3218
4721
|
}
|
|
3219
4722
|
|
|
3220
4723
|
/**
|
|
@@ -3226,6 +4729,18 @@ function collectAnimationSteps(
|
|
|
3226
4729
|
export const ACTOR_RESERVED_FIELDS: ReadonlySet<string> = new Set([
|
|
3227
4730
|
"kind",
|
|
3228
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",
|
|
3229
4744
|
"eyebrow",
|
|
3230
4745
|
"value",
|
|
3231
4746
|
"previous",
|
|
@@ -3233,10 +4748,15 @@ export const ACTOR_RESERVED_FIELDS: ReadonlySet<string> = new Set([
|
|
|
3233
4748
|
"marks",
|
|
3234
4749
|
"lane",
|
|
3235
4750
|
"stack",
|
|
4751
|
+
"stage",
|
|
3236
4752
|
"initial",
|
|
3237
4753
|
"final",
|
|
4754
|
+
"startLabel",
|
|
4755
|
+
"milestone",
|
|
4756
|
+
"emphasis",
|
|
4757
|
+
"at",
|
|
3238
4758
|
"state",
|
|
3239
|
-
//
|
|
4759
|
+
// ユーザージャーニーの欄 (`touchpoint` / `opportunity`) はここに載せない (#1251 Round 1 の指摘)。
|
|
3240
4760
|
// 載せるとパーツで同じ名前の状態を書いた時に横取りされる = 既に動いている見本が静かに変わる。
|
|
3241
4761
|
// パーツでない箱でだけ道筋の欄として読む (`parseActor` / `applyContinuationLines` が分岐する)
|
|
3242
4762
|
// canvas pivot 新 spec = 絶対座標 4 field (dragon canvas pivot spec §layout-role-conversion)
|
|
@@ -3298,12 +4818,33 @@ function coerceStateValue(raw: string): number | string | boolean {
|
|
|
3298
4818
|
return stripped;
|
|
3299
4819
|
}
|
|
3300
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
|
+
|
|
3301
4834
|
/**
|
|
3302
4835
|
* パーツでない箱に倍率を書いた時に知らせる (#1026)。
|
|
3303
4836
|
*
|
|
3304
4837
|
* 倍率はパーツにしか効かない。 黙って捨てると「書いたのに大きさが変わらない」 が手掛かり
|
|
3305
4838
|
* なしで起きる。 3 つの書き方すべてで同じ知らせを出す (縦に並べた形だけ知らせて他が黙る、
|
|
3306
4839
|
* という状態を作らない)。
|
|
4840
|
+
*
|
|
4841
|
+
* **案内に語彙を並べない** (#2330)。 語彙には `倍率` と `scale` が入っているので、
|
|
4842
|
+
* 並べると **いま弾いた名前を「使える項目」 として出す** ことになる。 読み手は同じ名前を
|
|
4843
|
+
* 書き直して同じ知らせを受ける (実測 = 案内の 39 項目のうち、弾かれるのはこの 2 件だけ)。
|
|
4844
|
+
*
|
|
4845
|
+
* 弾く理由はここが知っている (`isPart === false`) ので、**どこでなら書けるか** を出す。
|
|
4846
|
+
* 綴りの誤りを知らせる側 (`unknownKeys`) は語彙を並べたままにする = そちらは弾いた名前が
|
|
4847
|
+
* 語彙に無いため食い違わない。
|
|
3307
4848
|
*/
|
|
3308
4849
|
function reportScaleOnNonPart(
|
|
3309
4850
|
isPart: boolean,
|
|
@@ -3314,54 +4855,40 @@ function reportScaleOnNonPart(
|
|
|
3314
4855
|
if (isPart || key === undefined) return;
|
|
3315
4856
|
errors.push({
|
|
3316
4857
|
line,
|
|
3317
|
-
message: `項目名が読めません: "${key}"`,
|
|
3318
|
-
hint:
|
|
3319
|
-
});
|
|
3320
|
-
}
|
|
3321
|
-
|
|
3322
|
-
/**
|
|
3323
|
-
* 中括弧の形で読める日本語と、その英語名 (#1301)。
|
|
3324
|
-
*
|
|
3325
|
-
* **英語が中括弧で読める欄だけを載せる**。 `位置` / `大きさ` / `色` は英語側
|
|
3326
|
-
* (`pos` / `size` / `color`) も中括弧では読めないため載せない = 英語で出来ないことを
|
|
3327
|
-
* 日本語で出来るようにはしない。
|
|
3328
|
-
*
|
|
3329
|
-
* 載せる前は、同じ意味の語が縦書きでは通り中括弧では「項目名が読めません」 になっていた。
|
|
3330
|
-
* 書き方によって日本語だけが落ちる状態を無くす。
|
|
3331
|
-
*/
|
|
3332
|
-
export const INLINE_ACTOR_ALIASES: Record<string, string> = {
|
|
3333
|
-
種類: "kind",
|
|
3334
|
-
補足: "subtitle",
|
|
3335
|
-
値: "value",
|
|
3336
|
-
前の値: "previous",
|
|
3337
|
-
行: "rows",
|
|
3338
|
-
印: "marks",
|
|
3339
|
-
};
|
|
3340
|
-
|
|
3341
|
-
/** 中括弧に書かれた日本語の項目名を、同じ意味の英語名に寄せる (#1301) */
|
|
3342
|
-
function 中括弧の別名を寄せる(opts: Record<string, string>): Record<string, string> {
|
|
3343
|
-
let 触った = false;
|
|
3344
|
-
const out: Record<string, string> = { ...opts };
|
|
3345
|
-
for (const [日, 英] of Object.entries(INLINE_ACTOR_ALIASES)) {
|
|
3346
|
-
if (!(日 in out)) continue;
|
|
3347
|
-
触った = true;
|
|
3348
|
-
// 英語を併記した時は英語を優先する (縦書き形が後勝ちなのと違い、こちらは 1 行に同居する)
|
|
3349
|
-
if (!(英 in out)) out[英] = out[日]!;
|
|
3350
|
-
delete out[日];
|
|
3351
|
-
}
|
|
3352
|
-
return 触った ? out : opts;
|
|
4858
|
+
message: `項目名が読めません: "${key}"`,
|
|
4859
|
+
hint: 弾いた項目の案内(key),
|
|
4860
|
+
});
|
|
3353
4861
|
}
|
|
3354
4862
|
|
|
3355
4863
|
/**
|
|
3356
|
-
*
|
|
4864
|
+
* 中括弧の形で読める英語の項目名 (#2344 で `INLINE_ACTOR_KEYS` から分けた)。
|
|
3357
4865
|
*
|
|
3358
4866
|
* ここに無い名前は、 パーツでない箱ではどこにも入らずに消える。 `ACTOR_ITEM_KEYS` (縦に
|
|
3359
4867
|
* 並べた形) とは別に持つ = 中括弧の形は位置や大きさ (`pos` / `size` / `scale`) を未対応にしてあり、
|
|
3360
4868
|
* 同じ集合にすると「知らせない」 側がずれる。
|
|
4869
|
+
*
|
|
4870
|
+
* **日本語の別名を混ぜない**。 別名は `ACTOR_ITEM_ALIASES` をこの集合で絞って導くため、
|
|
4871
|
+
* 混ぜると導く側と導かれる側が輪になる。 日本語も含む一覧は `INLINE_ACTOR_KEYS` が持つ。
|
|
3361
4872
|
*/
|
|
3362
|
-
|
|
4873
|
+
const INLINE_ACTOR_ENGLISH_KEYS: ReadonlySet<string> = new Set([
|
|
3363
4874
|
"kind",
|
|
3364
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",
|
|
3365
4892
|
"eyebrow",
|
|
3366
4893
|
"value",
|
|
3367
4894
|
"previous",
|
|
@@ -3369,18 +4896,24 @@ export const INLINE_ACTOR_KEYS: ReadonlySet<string> = new Set([
|
|
|
3369
4896
|
"marks",
|
|
3370
4897
|
"lane",
|
|
3371
4898
|
"stack",
|
|
4899
|
+
"stage",
|
|
3372
4900
|
"initial",
|
|
3373
4901
|
"final",
|
|
3374
4902
|
"tone",
|
|
3375
4903
|
// 色の欄 (#1969)。 縦に並べた形と JSON は `color` を受けるのに、中括弧の形だけが知らない
|
|
3376
4904
|
// 項目名として落としていた。 見本 (parts) では従来どおり状態の上書きとして読む
|
|
3377
4905
|
"color",
|
|
3378
|
-
//
|
|
4906
|
+
// ユーザージャーニーの欄 (#1251)。 他の図種では組み立て側が知らせる
|
|
3379
4907
|
"touchpoint",
|
|
3380
4908
|
"opportunity",
|
|
4909
|
+
"opportunityPosition",
|
|
3381
4910
|
// 工程の並びの欄 (#1251)
|
|
3382
4911
|
"owner",
|
|
3383
4912
|
"end",
|
|
4913
|
+
"startLabel",
|
|
4914
|
+
"milestone",
|
|
4915
|
+
"emphasis",
|
|
4916
|
+
"at",
|
|
3384
4917
|
"posX",
|
|
3385
4918
|
"posY",
|
|
3386
4919
|
"posW",
|
|
@@ -3404,7 +4937,72 @@ export const INLINE_ACTOR_KEYS: ReadonlySet<string> = new Set([
|
|
|
3404
4937
|
// 同じ名前で 2 度知らせることになる
|
|
3405
4938
|
"scale",
|
|
3406
4939
|
"倍率",
|
|
3407
|
-
|
|
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,
|
|
3408
5006
|
...Object.keys(INLINE_ACTOR_ALIASES),
|
|
3409
5007
|
]);
|
|
3410
5008
|
// `state` はパーツでだけ意味を持つ (`extractStateOverride` がパーツの時しか作らない)。
|
|
@@ -3447,7 +5045,7 @@ const EDGE_BIND_INLINE_READERS = {
|
|
|
3447
5045
|
|
|
3448
5046
|
const FLOW_INLINE_READERS = {
|
|
3449
5047
|
sub: (v: string | undefined) => v,
|
|
3450
|
-
//
|
|
5048
|
+
// UML の関係の出どころ側の多重度 (#1771 / #2783)。 `sub` (行き先の端) と対
|
|
3451
5049
|
tailSub: (v: string | undefined) => v,
|
|
3452
5050
|
guard: (v: string | undefined) => v,
|
|
3453
5051
|
cardinality: (v: string | undefined) => v,
|
|
@@ -3468,11 +5066,24 @@ const FLOW_INLINE_READERS = {
|
|
|
3468
5066
|
*/
|
|
3469
5067
|
labelPlate: (v: string | undefined) =>
|
|
3470
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,
|
|
3471
5074
|
// 矢印がどの辺から出るか (#1385)。 描画側は 4 方向を取り、書かなければ自動で選ぶ
|
|
3472
5075
|
side: (v: string | undefined) =>
|
|
3473
5076
|
v !== undefined && (EDGE_SIDE_VALUES as readonly string[]).includes(v)
|
|
3474
5077
|
? (v as "top" | "right" | "bottom" | "left")
|
|
3475
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,
|
|
3476
5087
|
// 矢印の先の形 (#1462)。 書かなければ描画側の既定 (塗った三角) になる
|
|
3477
5088
|
head: (v: string | undefined) =>
|
|
3478
5089
|
v !== undefined && EDGE_HEAD_VALUES.includes(v) ? (v as EdgeHead) : undefined,
|
|
@@ -3485,7 +5096,7 @@ const FLOW_INLINE_READERS = {
|
|
|
3485
5096
|
tailHeadFill: (v: string | undefined) =>
|
|
3486
5097
|
v !== undefined && EDGE_HEAD_FILL_VALUES.includes(v) ? (v as EdgeHeadFill) : undefined,
|
|
3487
5098
|
/*
|
|
3488
|
-
*
|
|
5099
|
+
* record の UML 関係の種類 (#1466 / #2783)。書くと **線と端の形と塗りと付く側** がまとめて決まる
|
|
3489
5100
|
* (`CLASS_RELATION_LOOK`)。
|
|
3490
5101
|
*
|
|
3491
5102
|
* 4 つを個別に書かせない = 組合せは 6 通りしか無く、1 つでも書き違えると読み手に別の意味で
|
|
@@ -3513,10 +5124,15 @@ const FLOW_INLINE_READERS = {
|
|
|
3513
5124
|
*/
|
|
3514
5125
|
fromPartNode: (v: string | undefined) => v,
|
|
3515
5126
|
toPartNode: (v: string | undefined) => v,
|
|
5127
|
+
waypoints: (v: string | undefined) => v,
|
|
3516
5128
|
// 数と真偽の欄は `FLOW_INLINE_VALUE_KINDS` の表が読む (#1306)。 ここでは名前だけを持つ =
|
|
3517
5129
|
// 読める欄の一覧 (`FLOW_INLINE_KEYS`) は本表から導くため、載せないと欄ごと消える
|
|
3518
5130
|
labelOffsetX: null,
|
|
3519
5131
|
labelOffsetY: null,
|
|
5132
|
+
posX1: null,
|
|
5133
|
+
posY1: null,
|
|
5134
|
+
posX2: null,
|
|
5135
|
+
posY2: null,
|
|
3520
5136
|
overlay: null,
|
|
3521
5137
|
} as const;
|
|
3522
5138
|
|
|
@@ -3553,7 +5169,7 @@ function reportUnknownInlineKeys(
|
|
|
3553
5169
|
for (const field of splitInlineFields(inner)) {
|
|
3554
5170
|
const idx = field.indexOf(":");
|
|
3555
5171
|
if (idx < 0) continue;
|
|
3556
|
-
const key = field.slice(0, idx).trim();
|
|
5172
|
+
const key = stripQuotes(field.slice(0, idx).trim());
|
|
3557
5173
|
if (!key) continue;
|
|
3558
5174
|
if (isPart) {
|
|
3559
5175
|
if (!PART_INLINE_UNREADABLE_KEYS.has(key)) continue;
|
|
@@ -3567,10 +5183,23 @@ function reportUnknownInlineKeys(
|
|
|
3567
5183
|
continue;
|
|
3568
5184
|
}
|
|
3569
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);
|
|
3570
5197
|
errors.push({
|
|
3571
5198
|
line,
|
|
3572
5199
|
message: `項目名が読めません: "${key}"`,
|
|
3573
|
-
hint:
|
|
5200
|
+
hint: 段を分ければ効く
|
|
5201
|
+
? `1 行の中括弧では読めません。名前の下に段を分けた形で書くと効きます`
|
|
5202
|
+
: `使える項目 = ${[...INLINE_ACTOR_KEYS].join(", ")}`,
|
|
3574
5203
|
});
|
|
3575
5204
|
}
|
|
3576
5205
|
}
|
|
@@ -3598,7 +5227,7 @@ function 中括弧の知らない項目名を知らせる(
|
|
|
3598
5227
|
for (const field of splitInlineFields(inner)) {
|
|
3599
5228
|
const idx = field.indexOf(":");
|
|
3600
5229
|
if (idx < 0) continue;
|
|
3601
|
-
const key = field.slice(0, idx).trim();
|
|
5230
|
+
const key = stripQuotes(field.slice(0, idx).trim());
|
|
3602
5231
|
if (!key || 使える.includes(key)) continue;
|
|
3603
5232
|
// 接頭は値の欄名に続ける形 (`縦列 l1 の `) で末尾に空白を持つ。 日本語へ続ける時は空白を落とす
|
|
3604
5233
|
errors.push({
|
|
@@ -3642,6 +5271,40 @@ function parseActor(line: Line, errors: DslError[]): DslActor | null {
|
|
|
3642
5271
|
if (!isPart && opts.tone !== undefined && resolveTone(opts.tone) === undefined) {
|
|
3643
5272
|
report読めない色(opts.tone, line.no, errors, { 線種も受ける: false });
|
|
3644
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
|
+
}
|
|
3645
5308
|
// `color` は縦に並べた形と JSON と同じ振り分けを通す (#1969)。 色の名前は箱の色、`#` で
|
|
3646
5309
|
// 始まる値は色番号になる。
|
|
3647
5310
|
//
|
|
@@ -3662,13 +5325,43 @@ function parseActor(line: Line, errors: DslError[]): DslActor | null {
|
|
|
3662
5325
|
// 「書かなかった」 と同じ扱いにする (#1058)
|
|
3663
5326
|
kindWritten: kindRaw !== "" && !isPart,
|
|
3664
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,
|
|
3665
5348
|
eyebrow: opts.eyebrow,
|
|
3666
5349
|
// パーツでは状態の上書きとして意味を持つため、道筋の欄として横取りしない (#1251)
|
|
3667
5350
|
touchpoint: isPart ? undefined : opts.touchpoint,
|
|
3668
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"],
|
|
3669
5357
|
// 見本では状態の上書きとして意味を持つため横取りしない (#1251)
|
|
3670
5358
|
owner: isPart ? undefined : opts.owner,
|
|
3671
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,
|
|
3672
5365
|
value: opts.value,
|
|
3673
5366
|
previous: opts.previous,
|
|
3674
5367
|
rows: opts.rows
|
|
@@ -3678,12 +5371,11 @@ function parseActor(line: Line, errors: DslError[]): DslActor | null {
|
|
|
3678
5371
|
.map((x) => stripQuotes(x.trim()))
|
|
3679
5372
|
.filter(Boolean)
|
|
3680
5373
|
: undefined,
|
|
3681
|
-
// 行頭の印 (#1466)。 `rows` と同じ並びなので
|
|
5374
|
+
// 行頭の印 (#1466)。 `rows` と同じ並びなので **空の要素を捨てない**。
|
|
5375
|
+
// 古い語の読み替えは縦に並べた形と同じ関数を通す (#2782) = 片方だけ通すと、
|
|
5376
|
+
// 中括弧で書いた `pk` が印を持たないまま通る
|
|
3682
5377
|
marks: opts.marks
|
|
3683
|
-
? opts.marks
|
|
3684
|
-
.replace(/^\[|\]$/g, "")
|
|
3685
|
-
.split(/,(?![^[]*\])/)
|
|
3686
|
-
.map((x) => stripQuotes(x.trim()))
|
|
5378
|
+
? 行頭の印の並びとして読む(opts.marks, line.no, errors)
|
|
3687
5379
|
: undefined,
|
|
3688
5380
|
lane: opts.lane,
|
|
3689
5381
|
// 箱の中に描く図形 (#1374)。 パーツでは状態の上書きとして意味を持つため横取りしない
|
|
@@ -3814,12 +5506,17 @@ function parseFlowStep(line: Line, no: number, errors: DslError[]): DslStep | nu
|
|
|
3814
5506
|
if (読み手 === null) continue;
|
|
3815
5507
|
中括弧[k] = 読み手(opts[k]);
|
|
3816
5508
|
}
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
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(", ")}` });
|
|
3823
5520
|
}
|
|
3824
5521
|
// 読めない語を黙って捨てない (#1462)。 捨てると「書いたのに端の形が変わらない」 が
|
|
3825
5522
|
// 手掛かりなしで起きる
|
|
@@ -3849,6 +5546,8 @@ function parseFlowStep(line: Line, no: number, errors: DslError[]): DslStep | nu
|
|
|
3849
5546
|
const guard = 中括弧.guard as string | undefined;
|
|
3850
5547
|
const cardinality = 中括弧.cardinality as string | undefined;
|
|
3851
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;
|
|
3852
5551
|
const head = 中括弧.head as EdgeHead | undefined;
|
|
3853
5552
|
const tailHead = 中括弧.tailHead as EdgeHead | undefined;
|
|
3854
5553
|
const headFill = 中括弧.headFill as EdgeHeadFill | undefined;
|
|
@@ -3858,6 +5557,19 @@ function parseFlowStep(line: Line, no: number, errors: DslError[]): DslStep | nu
|
|
|
3858
5557
|
// 辺の役目と名前の下地 (cdl#618)
|
|
3859
5558
|
const role = 中括弧.role as "main" | undefined;
|
|
3860
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
|
+
}
|
|
3861
5573
|
// 値に追随する 3 欄 (#1396)。 空は捨てずに知らせる = 描画側は空文字を既定値へ落とさず
|
|
3862
5574
|
// そのまま置換に使うため、書き忘れが「線が消えた」 形で出る
|
|
3863
5575
|
const widthBind = 追随する大きさとして読む(
|
|
@@ -3880,6 +5592,16 @@ function parseFlowStep(line: Line, no: number, errors: DslError[]): DslStep | nu
|
|
|
3880
5592
|
);
|
|
3881
5593
|
const labelOffsetX = 数と真偽?.labelOffsetX;
|
|
3882
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
|
+
);
|
|
3883
5605
|
const overlay = 数と真偽?.overlay;
|
|
3884
5606
|
// 空の名指し (`toPartNode: ""`) は書かなかったことにする = 要素 1 つの部品なら自動で繋ぐ
|
|
3885
5607
|
const fromPartNode = (中括弧.fromPartNode as string | undefined) || undefined;
|
|
@@ -3942,6 +5664,8 @@ function parseFlowStep(line: Line, no: number, errors: DslError[]): DslStep | nu
|
|
|
3942
5664
|
guard,
|
|
3943
5665
|
cardinality,
|
|
3944
5666
|
side,
|
|
5667
|
+
fromSide,
|
|
5668
|
+
toSide,
|
|
3945
5669
|
head,
|
|
3946
5670
|
tailHead,
|
|
3947
5671
|
headFill,
|
|
@@ -3955,6 +5679,13 @@ function parseFlowStep(line: Line, no: number, errors: DslError[]): DslStep | nu
|
|
|
3955
5679
|
dashOffsetBind,
|
|
3956
5680
|
labelOffsetX,
|
|
3957
5681
|
labelOffsetY,
|
|
5682
|
+
posX1,
|
|
5683
|
+
posY1,
|
|
5684
|
+
posX2,
|
|
5685
|
+
posY2,
|
|
5686
|
+
waypoints,
|
|
5687
|
+
labelCollision,
|
|
5688
|
+
labelSize,
|
|
3958
5689
|
overlay,
|
|
3959
5690
|
...(fromPartNode !== undefined ? { fromPartNode } : {}),
|
|
3960
5691
|
...(toPartNode !== undefined ? { toPartNode } : {}),
|
|
@@ -3962,12 +5693,35 @@ function parseFlowStep(line: Line, no: number, errors: DslError[]): DslStep | nu
|
|
|
3962
5693
|
};
|
|
3963
5694
|
}
|
|
3964
5695
|
|
|
3965
|
-
|
|
3966
|
-
|
|
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 {
|
|
3967
5711
|
const m = text.match(/^([^:]+?)\s*:\s*(.+)$/);
|
|
3968
|
-
if (!m)
|
|
5712
|
+
if (!m) {
|
|
5713
|
+
errors.push({
|
|
5714
|
+
line: lineNo,
|
|
5715
|
+
message: `状態の行が読めません: "${text}"`,
|
|
5716
|
+
hint: "`name: initial` の形で書く",
|
|
5717
|
+
});
|
|
5718
|
+
return null;
|
|
5719
|
+
}
|
|
3969
5720
|
const name = (m[1] ?? "").trim();
|
|
3970
|
-
if (!isValueName(name))
|
|
5721
|
+
if (!isValueName(name)) {
|
|
5722
|
+
errors.push({ line: lineNo, ...valueNameIssue(name) });
|
|
5723
|
+
return null;
|
|
5724
|
+
}
|
|
3971
5725
|
const raw = (m[2] ?? "").trim();
|
|
3972
5726
|
const stripped = stripQuotes(raw);
|
|
3973
5727
|
const asNum = Number(stripped);
|
|
@@ -3976,31 +5730,6 @@ function parseStateEntry(text: string, lineNo: number): DslState | null {
|
|
|
3976
5730
|
return { name, initial, pos: { line: lineNo } };
|
|
3977
5731
|
}
|
|
3978
5732
|
|
|
3979
|
-
/**
|
|
3980
|
-
* 字下げした行を 1 行も落とさずに集める。
|
|
3981
|
-
*
|
|
3982
|
-
* `collectIndentedList` は形が合わない行を黙って捨てるが、 `values` では捨てずに
|
|
3983
|
-
* 読み手 (`parseValueEntry`) へ渡して書き間違いとして報告させる。
|
|
3984
|
-
*/
|
|
3985
|
-
function collectIndentedRaw(
|
|
3986
|
-
lines: Line[],
|
|
3987
|
-
start: number,
|
|
3988
|
-
parentIndent: number,
|
|
3989
|
-
): { items: Line[]; next: number } {
|
|
3990
|
-
const items: Line[] = [];
|
|
3991
|
-
let i = start;
|
|
3992
|
-
while (i < lines.length) {
|
|
3993
|
-
const ln = lines[i];
|
|
3994
|
-
if (!ln || !ln.trimmed || ln.trimmed.startsWith("#")) {
|
|
3995
|
-
i += 1;
|
|
3996
|
-
continue;
|
|
3997
|
-
}
|
|
3998
|
-
if (ln.indent <= parentIndent) break;
|
|
3999
|
-
items.push(ln);
|
|
4000
|
-
i += 1;
|
|
4001
|
-
}
|
|
4002
|
-
return { items, next: i };
|
|
4003
|
-
}
|
|
4004
5733
|
|
|
4005
5734
|
/**
|
|
4006
5735
|
* `waiting: "{inflow} - {done}"` を 1 件の値として読む。
|
|
@@ -4013,7 +5742,7 @@ function parseValueEntry(text: string, lineNo: number, errors: DslError[]): DslV
|
|
|
4013
5742
|
if (!m) {
|
|
4014
5743
|
errors.push({
|
|
4015
5744
|
line: lineNo,
|
|
4016
|
-
message:
|
|
5745
|
+
message: `値の行が読めません: "${text}"`,
|
|
4017
5746
|
hint: '`waiting: "{inflow} - {done}"` の形で書く',
|
|
4018
5747
|
});
|
|
4019
5748
|
return null;
|
|
@@ -4044,7 +5773,7 @@ function parseValueEntry(text: string, lineNo: number, errors: DslError[]): DslV
|
|
|
4044
5773
|
if (expression === "") {
|
|
4045
5774
|
errors.push({
|
|
4046
5775
|
line: lineNo,
|
|
4047
|
-
message:
|
|
5776
|
+
message: `式が空です: "${name}"`,
|
|
4048
5777
|
hint: '`"{a} + {b}"` のように式を書く',
|
|
4049
5778
|
});
|
|
4050
5779
|
return null;
|
|
@@ -4057,6 +5786,397 @@ function parseValueEntry(text: string, lineNo: number, errors: DslError[]): DslV
|
|
|
4057
5786
|
return { name, expression, pos: { line: lineNo } };
|
|
4058
5787
|
}
|
|
4059
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
|
+
|
|
4060
6180
|
function splitTopLevelCommas(s: string): string[] {
|
|
4061
6181
|
// brace 内を考慮 ... 今回は単純 split (動作する範囲)
|
|
4062
6182
|
return s
|
|
@@ -4080,8 +6200,8 @@ function ensureAnimate(a: DslAnimate | undefined, lineNo: number): DslAnimate {
|
|
|
4080
6200
|
export const PHASE_ITEM_WORDS = [
|
|
4081
6201
|
"focus",
|
|
4082
6202
|
"badge",
|
|
4083
|
-
|
|
4084
|
-
|
|
6203
|
+
// 説明の語は `PHASE_BODY_KEYS` から導く (#2621)。 ここに書き写すと JSON の入口とずれる
|
|
6204
|
+
...PHASE_BODY_KEYS,
|
|
4085
6205
|
"tween",
|
|
4086
6206
|
"set",
|
|
4087
6207
|
"draw",
|
|
@@ -4097,8 +6217,8 @@ function parsePhase(block: Line[], errors: DslError[]): DslPhase | null {
|
|
|
4097
6217
|
if (!m) {
|
|
4098
6218
|
errors.push({
|
|
4099
6219
|
line: head.no,
|
|
4100
|
-
message:
|
|
4101
|
-
hint: '
|
|
6220
|
+
message: `段の見出しが読めません: "${head.trimmed}"`,
|
|
6221
|
+
hint: '`- step: "name" 1.5s` の形で書く',
|
|
4102
6222
|
});
|
|
4103
6223
|
return null;
|
|
4104
6224
|
}
|
|
@@ -4108,8 +6228,8 @@ function parsePhase(block: Line[], errors: DslError[]): DslPhase | null {
|
|
|
4108
6228
|
if (!headParse) {
|
|
4109
6229
|
errors.push({
|
|
4110
6230
|
line: head.no,
|
|
4111
|
-
message:
|
|
4112
|
-
hint: '
|
|
6231
|
+
message: `段の値が読めません: "${headRest}"`,
|
|
6232
|
+
hint: '`"name" 1.5s` の形で書く (長さは秒)',
|
|
4113
6233
|
});
|
|
4114
6234
|
return null;
|
|
4115
6235
|
}
|
|
@@ -4131,6 +6251,14 @@ function parsePhase(block: Line[], errors: DslError[]): DslPhase | null {
|
|
|
4131
6251
|
// 拾った上で、知らない名前は下で誤りとして知らせる
|
|
4132
6252
|
const propMatch = t.match(/^([^\s:]+)\s*:\s*(.*)$/);
|
|
4133
6253
|
if (!propMatch) {
|
|
6254
|
+
// **項目名として読めなかった行も捨てない** (#2403)。 すぐ下で「知らない項目名」 は
|
|
6255
|
+
// 知らせているのに、ここだけが黙っていた = コロンを書き落とすと、その行が
|
|
6256
|
+
// 知らせにも図にも現れない (実測 = 段の中に `focus` と書くと誤り 0 件で何も起きない)
|
|
6257
|
+
errors.push({
|
|
6258
|
+
line: ln.no,
|
|
6259
|
+
message: `段の行が項目名として読めません: "${t}"`,
|
|
6260
|
+
hint: `\`項目名: 値\` の形で書く (使える項目 = ${段の項目の英語.join(", ")})`,
|
|
6261
|
+
});
|
|
4134
6262
|
i += 1;
|
|
4135
6263
|
continue;
|
|
4136
6264
|
}
|
|
@@ -4174,7 +6302,7 @@ function parsePhase(block: Line[], errors: DslError[]): DslPhase | null {
|
|
|
4174
6302
|
errors.push({
|
|
4175
6303
|
line: ln.no,
|
|
4176
6304
|
message: `draw に書ける項目は語と割合の 2 つまでです: "${stripQuotes(value).trim()}"`,
|
|
4177
|
-
hint: "
|
|
6305
|
+
hint: "`draw: line 0.4` の形で書く",
|
|
4178
6306
|
});
|
|
4179
6307
|
} else {
|
|
4180
6308
|
phase.draw = 語;
|
|
@@ -4197,39 +6325,28 @@ function parsePhase(block: Line[], errors: DslError[]): DslPhase | null {
|
|
|
4197
6325
|
i += 1;
|
|
4198
6326
|
continue;
|
|
4199
6327
|
}
|
|
4200
|
-
if (
|
|
6328
|
+
if ((PHASE_BODY_KEYS as readonly string[]).includes(key)) {
|
|
4201
6329
|
phase.body = stripQuotes(value);
|
|
4202
6330
|
i += 1;
|
|
4203
6331
|
continue;
|
|
4204
6332
|
}
|
|
4205
6333
|
if (key === "tween") {
|
|
4206
|
-
//
|
|
6334
|
+
// 1 行にまとめた形 (tween: client_bal 100 -> 90) と縦に並べた形の両方を受ける。
|
|
6335
|
+
// 読めない行の文は読み手が持つ (#2403)
|
|
4207
6336
|
if (value) {
|
|
4208
|
-
const tw = parseTweenLine(value, ln.no);
|
|
6337
|
+
const tw = parseTweenLine(value, ln.no, errors);
|
|
4209
6338
|
if (tw) phase.tweens!.push(tw);
|
|
4210
|
-
else
|
|
4211
|
-
errors.push({
|
|
4212
|
-
line: ln.no,
|
|
4213
|
-
message: `invalid tween: "${value}"`,
|
|
4214
|
-
hint: "use `tween: name 100 -> 90`",
|
|
4215
|
-
});
|
|
4216
6339
|
i += 1;
|
|
4217
6340
|
continue;
|
|
4218
6341
|
}
|
|
4219
|
-
//
|
|
6342
|
+
// 縦に並べた形 ... 後続の同 indent + 1 以上の行を取り込む
|
|
4220
6343
|
const baseIndent = ln.indent;
|
|
4221
6344
|
let j = i + 1;
|
|
4222
6345
|
while (j < block.length) {
|
|
4223
6346
|
const nx = block[j]!;
|
|
4224
6347
|
if (nx.indent <= baseIndent) break;
|
|
4225
|
-
const tw = parseTweenLine(nx.trimmed, nx.no);
|
|
6348
|
+
const tw = parseTweenLine(nx.trimmed, nx.no, errors);
|
|
4226
6349
|
if (tw) phase.tweens!.push(tw);
|
|
4227
|
-
else
|
|
4228
|
-
errors.push({
|
|
4229
|
-
line: nx.no,
|
|
4230
|
-
message: `invalid tween entry: "${nx.trimmed}"`,
|
|
4231
|
-
hint: "use `name: 100 -> 90`",
|
|
4232
|
-
});
|
|
4233
6350
|
j += 1;
|
|
4234
6351
|
}
|
|
4235
6352
|
i = j;
|
|
@@ -4237,7 +6354,7 @@ function parsePhase(block: Line[], errors: DslError[]): DslPhase | null {
|
|
|
4237
6354
|
}
|
|
4238
6355
|
if (key === "set") {
|
|
4239
6356
|
if (value) {
|
|
4240
|
-
const st = parseSetLine(value, ln.no);
|
|
6357
|
+
const st = parseSetLine(value, ln.no, errors);
|
|
4241
6358
|
if (st) phase.sets!.push(st);
|
|
4242
6359
|
i += 1;
|
|
4243
6360
|
continue;
|
|
@@ -4247,7 +6364,7 @@ function parsePhase(block: Line[], errors: DslError[]): DslPhase | null {
|
|
|
4247
6364
|
while (j < block.length) {
|
|
4248
6365
|
const nx = block[j]!;
|
|
4249
6366
|
if (nx.indent <= baseIndent) break;
|
|
4250
|
-
const st = parseSetLine(nx.trimmed, nx.no);
|
|
6367
|
+
const st = parseSetLine(nx.trimmed, nx.no, errors);
|
|
4251
6368
|
if (st) phase.sets!.push(st);
|
|
4252
6369
|
j += 1;
|
|
4253
6370
|
}
|
|
@@ -4311,23 +6428,47 @@ function 段の項目のヒント(書いた名前: string): string {
|
|
|
4311
6428
|
*
|
|
4312
6429
|
* いまは語と図種が同じ綴りだが、**同じものとして扱わない**。 語は書き手が書く名前で、
|
|
4313
6430
|
* 図種は `type:` が取る値。 片方だけ別名を足したくなった時に、対応が表に残っている形にする。
|
|
6431
|
+
*
|
|
6432
|
+
* **数を描く図は形まで指す** (#2657)。 9 つの型を `chart` 1 つに畳んだので、型だけを
|
|
6433
|
+
* 指すと `draw: pie` を棒の図に書いても咎められなくなる = 語ごとの相手を型から形へ
|
|
6434
|
+
* 下ろして、畳む前と同じ細かさで判定する。
|
|
4314
6435
|
*/
|
|
4315
|
-
export const DRAW_TARGETS: ReadonlyMap<string, PresetType> = new Map<
|
|
4316
|
-
|
|
4317
|
-
|
|
4318
|
-
|
|
4319
|
-
["
|
|
4320
|
-
["
|
|
4321
|
-
["
|
|
4322
|
-
["
|
|
4323
|
-
["
|
|
4324
|
-
["
|
|
4325
|
-
["
|
|
4326
|
-
["
|
|
4327
|
-
["
|
|
4328
|
-
["
|
|
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" }],
|
|
4329
6453
|
]);
|
|
4330
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
|
+
|
|
4331
6472
|
/** `draw:` に書ける語。 表から導く (#1314) */
|
|
4332
6473
|
export const DRAW_WORDS: ReadonlySet<string> = new Set(DRAW_TARGETS.keys());
|
|
4333
6474
|
|
|
@@ -4453,13 +6594,33 @@ function parseFocusList(s: string): string[] {
|
|
|
4453
6594
|
return out;
|
|
4454
6595
|
}
|
|
4455
6596
|
|
|
4456
|
-
|
|
4457
|
-
|
|
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 {
|
|
4458
6609
|
const cleaned = s.replace(/^-\s*/, "").trim();
|
|
4459
6610
|
const m = cleaned.match(/^([^:\s]+)\s*[:\s]\s*(-?\d+(?:\.\d+)?)\s*->\s*(-?\d+(?:\.\d+)?)$/);
|
|
4460
|
-
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
|
+
}
|
|
4461
6619
|
const state = m[1] ?? "";
|
|
4462
|
-
if (!isValueName(state))
|
|
6620
|
+
if (!isValueName(state)) {
|
|
6621
|
+
errors.push({ line: lineNo, ...valueNameIssue(state) });
|
|
6622
|
+
return null;
|
|
6623
|
+
}
|
|
4463
6624
|
return {
|
|
4464
6625
|
state,
|
|
4465
6626
|
from: parseFloat(m[2] ?? "0"),
|
|
@@ -4468,13 +6629,32 @@ function parseTweenLine(s: string, lineNo: number): DslTween | null {
|
|
|
4468
6629
|
};
|
|
4469
6630
|
}
|
|
4470
6631
|
|
|
4471
|
-
|
|
4472
|
-
|
|
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 {
|
|
4473
6643
|
const cleaned = s.replace(/^-\s*/, "").trim();
|
|
4474
6644
|
const m = cleaned.match(/^([^:\s]+)\s*[:\s]\s*(.+)$/);
|
|
4475
|
-
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
|
+
}
|
|
4476
6653
|
const state = m[1] ?? "";
|
|
4477
|
-
if (!isValueName(state))
|
|
6654
|
+
if (!isValueName(state)) {
|
|
6655
|
+
errors.push({ line: lineNo, ...valueNameIssue(state) });
|
|
6656
|
+
return null;
|
|
6657
|
+
}
|
|
4478
6658
|
const raw = (m[2] ?? "").trim();
|
|
4479
6659
|
const stripped = stripQuotes(raw);
|
|
4480
6660
|
const asNum = Number(stripped);
|