@cardenelabs/dragon 0.34.0 → 0.35.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 +39 -720
- package/dist/cli.cjs +4529 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.js +4523 -0
- package/dist/cli.js.map +1 -0
- package/dist/export.cjs +3912 -0
- package/dist/export.cjs.map +1 -0
- package/dist/export.d.cts +36 -0
- package/dist/export.d.ts +36 -0
- package/dist/export.js +3907 -0
- package/dist/export.js.map +1 -0
- package/dist/fonts.cjs +7 -0
- package/dist/fonts.cjs.map +1 -0
- package/dist/fonts.js +4 -0
- package/dist/fonts.js.map +1 -0
- package/dist/index.cjs +4105 -20356
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +227 -5522
- package/dist/index.d.ts +227 -5522
- package/dist/index.js +4091 -20283
- package/dist/index.js.map +1 -1
- package/dist/player.global.js +2 -0
- package/dist/schema-BBWfniwH.d.cts +112 -0
- package/dist/schema-BBWfniwH.d.ts +112 -0
- package/package.json +39 -24
- package/src/cli.ts +237 -0
- package/src/export/capture.ts +277 -0
- package/src/export/fonts.ts +68 -0
- package/src/export/html.ts +32 -0
- package/src/export/index.ts +146 -0
- package/src/field-messages.ts +104 -0
- package/src/figure-text.ts +42 -0
- package/src/index.ts +30 -283
- package/src/layout/architecture.ts +381 -0
- package/src/layout/measure.ts +21 -0
- package/src/layout/scene.ts +353 -0
- package/src/layout/sequence.ts +189 -0
- package/src/load-fonts.ts +8 -0
- package/src/look/fonts.ts +33 -0
- package/src/look/palettes.json +209 -0
- package/src/look/values.ts +155 -0
- package/src/markdown.ts +35 -0
- package/src/messages.ts +122 -0
- package/src/parser.ts +342 -310
- package/src/player-global.ts +4 -0
- package/src/player.ts +271 -0
- package/src/printer.ts +76 -0
- package/src/render/icons.ts +41 -0
- package/src/render/motion.ts +106 -0
- package/src/render/parts.ts +279 -0
- package/src/render/svg.ts +341 -0
- package/src/schema.ts +161 -16
- package/src/schemas/diagram.schema.json +150 -0
- package/src/schemas/validate-diagram.d.ts +12 -0
- package/src/schemas/validate-diagram.js +8 -0
- package/src/timeline.ts +166 -0
- package/src/tokenize.ts +20 -281
- package/src/version.ts +55 -0
- package/src/view.ts +16 -0
- package/src/words.ts +79 -0
- package/examples/quick-start.md +0 -142
- package/src/canvas-bounds.ts +0 -52
- package/src/color.ts +0 -197
- package/src/compile/actor-ids.ts +0 -297
- package/src/compile/actor-option-notice.ts +0 -379
- package/src/compile/actors.ts +0 -103
- package/src/compile/c4.ts +0 -74
- package/src/compile/chain.ts +0 -93
- package/src/compile/chart-fields.ts +0 -193
- package/src/compile/deep-rewrite.ts +0 -30
- package/src/compile/derived-source.ts +0 -18
- package/src/compile/direction.ts +0 -102
- package/src/compile/edge-option-notice.ts +0 -112
- package/src/compile/er-relation.ts +0 -200
- package/src/compile/flow.ts +0 -54
- package/src/compile/flowchart.ts +0 -344
- package/src/compile/funnel.ts +0 -87
- package/src/compile/gantt.ts +0 -337
- package/src/compile/generic.ts +0 -696
- package/src/compile/hierarchy.ts +0 -92
- package/src/compile/journey.ts +0 -93
- package/src/compile/kinds.ts +0 -139
- package/src/compile/lanes.ts +0 -138
- package/src/compile/metro.ts +0 -271
- package/src/compile/mind.ts +0 -393
- package/src/compile/node-title.ts +0 -24
- package/src/compile/notice.ts +0 -141
- package/src/compile/order.ts +0 -57
- package/src/compile/parts-lookup.ts +0 -17
- package/src/compile/parts.ts +0 -2709
- package/src/compile/placement-apply.ts +0 -689
- package/src/compile/placement.ts +0 -15
- package/src/compile/quadrant.ts +0 -265
- package/src/compile/record.ts +0 -75
- package/src/compile/row-marks.ts +0 -128
- package/src/compile/rows.ts +0 -40
- package/src/compile/sequence.ts +0 -305
- package/src/compile/slug.ts +0 -37
- package/src/compile/stages.ts +0 -247
- package/src/compile/subtitle.ts +0 -35
- package/src/compile/swimlane-marks.ts +0 -87
- package/src/compile/swimlane.ts +0 -82
- package/src/compile/timeline.ts +0 -213
- package/src/compile/topology.ts +0 -44
- package/src/compile/tree.ts +0 -69
- package/src/compile/value-chart.ts +0 -337
- package/src/compile/values.ts +0 -432
- package/src/compile/word-state.ts +0 -82
- package/src/compile.ts +0 -3168
- package/src/focus.ts +0 -46
- package/src/input-size.ts +0 -181
- package/src/json-line-map.ts +0 -30
- package/src/json-parser.ts +0 -4306
- package/src/keywords.ts +0 -417
- package/src/near-name.ts +0 -42
- package/src/notation-lint.ts +0 -342
- package/src/relative-pos.ts +0 -251
- package/src/schemas/diagram.json +0 -3088
- package/src/types.ts +0 -1009
- package/src/v05/index.ts +0 -9
- package/src/v05/input-table.generated.ts +0 -138
- package/src/v05/parser-types.ts +0 -32
- package/src/v05/parser.ts +0 -6668
- package/src/v05/readout-table.generated.ts +0 -1088
- package/src/v05/removed-syntax.ts +0 -115
- package/src/value-syntax.ts +0 -313
- package/src/write-position.ts +0 -270
package/src/json-parser.ts
DELETED
|
@@ -1,4306 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* LLM 向け JSON DSL parser。
|
|
3
|
-
*
|
|
4
|
-
* dragon の YAML DSL と 1:1 対応する JSON 記法を提供する。
|
|
5
|
-
* LLM (Anthropic Claude / OpenAI GPT) が structured output (tool call / response_format)
|
|
6
|
-
* で確実に生成できるよう、 flat な object array を優先した shape になっている。
|
|
7
|
-
*
|
|
8
|
-
* 使い方 (LLM):
|
|
9
|
-
* 1. `packages/dragon/src/schemas/diagram.json` の JSON Schema を LLM の tool schema に注入
|
|
10
|
-
* 2. LLM が JSON を返す
|
|
11
|
-
* 3. `jsonToDiagram(json)` で CdlDiagram に変換
|
|
12
|
-
* 4. validation error は throw、 retry loop で LLM に修正させる
|
|
13
|
-
*
|
|
14
|
-
* YAML との対応:
|
|
15
|
-
* YAML `title: "..."` ⇔ JSON `{title: "..."}`
|
|
16
|
-
* YAML `actors: [A, B: kind]` ⇔ JSON `{actors: [{name: "A"}, {name: "B", kind: "storage"}]}`
|
|
17
|
-
* YAML `flow: [- A -> B: "label"]` ⇔ JSON `{flow: [{from: "A", to: "B", label: "label"}]}`
|
|
18
|
-
* YAML `animation: [step: "..."]` ⇔ JSON `{animation: [{step: "...", duration: 1.4, focus: [...]}]}`
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
import {
|
|
22
|
-
DRAW_WORDS,
|
|
23
|
-
EDGE_SIDE_VALUES,
|
|
24
|
-
EDGE_HEAD_VALUES,
|
|
25
|
-
NODE_KIND_VALID,
|
|
26
|
-
PRESET_TYPES,
|
|
27
|
-
TYPE_ALIASES,
|
|
28
|
-
STYLE_VALID,
|
|
29
|
-
resolveNodeKind,
|
|
30
|
-
resolveTone,
|
|
31
|
-
splitColorValue,
|
|
32
|
-
書ける色名,
|
|
33
|
-
図形の表,
|
|
34
|
-
部品の表,
|
|
35
|
-
部品の組の表,
|
|
36
|
-
つまみの表,
|
|
37
|
-
EVENT_KINDS,
|
|
38
|
-
EVENT_TARGET_KEYS,
|
|
39
|
-
EVENT_REQUIRED_KEYS,
|
|
40
|
-
CHART_LINE_DASH_VALUES,
|
|
41
|
-
CHART_LINE_MARKER_VALUES,
|
|
42
|
-
CHART_PIE_TABLE_COLUMN_VALUES,
|
|
43
|
-
CHART_PIE_RING_WIDTH_VALUES,
|
|
44
|
-
CHART_STACKED_LEGEND_POSITION_VALUES,
|
|
45
|
-
CHART_WAFFLE_LEGEND_POSITION_VALUES,
|
|
46
|
-
FUNNEL_FORM_VALUES,
|
|
47
|
-
FUNNEL_RATE_VALUES,
|
|
48
|
-
QUADRANT_POINT_LABEL_SIDE_VALUES,
|
|
49
|
-
TREE_NODE_FORM_VALUES,
|
|
50
|
-
TREE_EDGE_TONE_VALUES,
|
|
51
|
-
TREE_EDGE_HEAD_VALUES,
|
|
52
|
-
MIND_FORM_VALUES,
|
|
53
|
-
JOURNEY_FORM_VALUES,
|
|
54
|
-
JOURNEY_LINE_FORM_VALUES,
|
|
55
|
-
JOURNEY_OPPORTUNITY_POSITION_VALUES,
|
|
56
|
-
JOURNEY_LABEL_KEYS,
|
|
57
|
-
CORNER_FORM_VALUES,
|
|
58
|
-
CHART_BAR_SHAPE_VALUES,
|
|
59
|
-
QUADRANT_LABEL_POSITION_VALUES,
|
|
60
|
-
LABEL_COLLISION_VALUES,
|
|
61
|
-
GANTT_TASK_LABEL_ALIGN_VALUES,
|
|
62
|
-
CDL_ICON_VALUES,
|
|
63
|
-
KIND_FORM_VALUES,
|
|
64
|
-
TITLE_ALIGN_VALUES,
|
|
65
|
-
LEGEND_LINE_STYLE_VALUES,
|
|
66
|
-
LEGEND_NOTE_ALIGN_VALUES,
|
|
67
|
-
ROW_MARK_FORM_VALUES,
|
|
68
|
-
ROW_MARK_TONE_VALUES,
|
|
69
|
-
ROW_DIVIDER_FORM_VALUES,
|
|
70
|
-
ROW_WELL_KEYS,
|
|
71
|
-
SEQUENCE_ACTOR_FORM_VALUES,
|
|
72
|
-
SEQUENCE_LABEL_FORM_VALUES,
|
|
73
|
-
SEQUENCE_RETURN_HEAD_VALUES,
|
|
74
|
-
SEQUENCE_KIND_LABEL_KEYS,
|
|
75
|
-
SEQUENCE_SELF_LOOP_KEYS,
|
|
76
|
-
type 図形の定義,
|
|
77
|
-
} from "./v05/parser";
|
|
78
|
-
// 図の意匠 (#1553 / #2790)。 記法の読み手と同じ解決を通す = 別名の受け方が
|
|
79
|
-
// 記法と JSON でずれない
|
|
80
|
-
import {
|
|
81
|
-
PHASE_BODY_KEYS,
|
|
82
|
-
DIAGRAM_SHAPES,
|
|
83
|
-
THEMES,
|
|
84
|
-
THEME_ALIAS,
|
|
85
|
-
resolveOrder,
|
|
86
|
-
resolveDiagramShape,
|
|
87
|
-
shapesForDiagramType,
|
|
88
|
-
resolveTheme,
|
|
89
|
-
resolveLegendMark,
|
|
90
|
-
} from "./keywords";
|
|
91
|
-
// 区画の語の表 (#2667)。 記法と同じ表から直す = 同じ区画を 2 通りで呼ばない
|
|
92
|
-
import { 区画 } from "./compile/word-state";
|
|
93
|
-
import { 外した図種の知らせ, 外した種類, 外した種類の知らせ } from "./v05/removed-syntax";
|
|
94
|
-
// 行頭の印の古い語の読み替え (#2782)。 記法と同じ表を通す = JSON だけ古い語が印を持たない
|
|
95
|
-
// 形にならない
|
|
96
|
-
import { 行頭の語へ読み替える } from "./compile/row-marks";
|
|
97
|
-
import type { DslDiagramShape, DslShape } from "./keywords";
|
|
98
|
-
import type { CompileToCdlOpts } from "./compile";
|
|
99
|
-
import type {
|
|
100
|
-
CdlDiagram,
|
|
101
|
-
CdlLegendItem,
|
|
102
|
-
NodeKind,
|
|
103
|
-
Tone,
|
|
104
|
-
EdgeStyle,
|
|
105
|
-
EdgeHead,
|
|
106
|
-
EdgeHeadFill,
|
|
107
|
-
EdgeReveal,
|
|
108
|
-
RelationFocus,
|
|
109
|
-
ClassRelationType,
|
|
110
|
-
SequenceMessageKind,
|
|
111
|
-
} from "@cardenelabs/cdl";
|
|
112
|
-
import { extractIdentifiers, parseFormula } from "@cardenelabs/cdl";
|
|
113
|
-
import type {
|
|
114
|
-
DslDocument,
|
|
115
|
-
DslStageHeader,
|
|
116
|
-
DslRegions,
|
|
117
|
-
DslActor,
|
|
118
|
-
DslStep,
|
|
119
|
-
DslAnimate,
|
|
120
|
-
DslPhase,
|
|
121
|
-
DslState,
|
|
122
|
-
PresetType,
|
|
123
|
-
LayoutPos,
|
|
124
|
-
DslReadout,
|
|
125
|
-
DslInput,
|
|
126
|
-
DslFormula,
|
|
127
|
-
DslEventBinding,
|
|
128
|
-
DslScrollTrigger,
|
|
129
|
-
DslDynShape,
|
|
130
|
-
} from "./types";
|
|
131
|
-
import { checkValueExpression, isValueName, valueNameIssue } from "./value-syntax";
|
|
132
|
-
import { 近い名前 } from "./near-name";
|
|
133
|
-
import { 書いた場所の鍵, type 書いた行の表 } from "./json-line-map";
|
|
134
|
-
import { compileToCdl } from "./compile";
|
|
135
|
-
import {
|
|
136
|
-
RELATIVE_DIRECTIONS,
|
|
137
|
-
findRelativeProblems,
|
|
138
|
-
type RelativeDirection,
|
|
139
|
-
type RelativePos,
|
|
140
|
-
} from "./relative-pos";
|
|
141
|
-
|
|
142
|
-
/** 古い綴りで書いた図種 (#2655 / #2657)。 受ける値は `TYPE_ALIASES` が SSOT */
|
|
143
|
-
type 図種の別名 = DslShape;
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* LLM 向け JSON DSL の入力 shape。 YAML DSL と 1:1 対応、 top-level は flat な object。
|
|
147
|
-
*/
|
|
148
|
-
export interface DragonJson {
|
|
149
|
-
/** 図の title (必須) */
|
|
150
|
-
title: string;
|
|
151
|
-
/**
|
|
152
|
-
* preset type (必須)。 受け付ける値は `PRESET_TYPES` と `TYPE_ALIASES` が SSOT。
|
|
153
|
-
*
|
|
154
|
-
* 別名 (#2655 / #2657) は読み取りで図種と語の 2 つに開く = `pie` と書いた JSON は
|
|
155
|
-
* 数を描く図 + 円の形 として読まれる。 外した図種 (`solidity`) は置き換え先を誤りとして返す
|
|
156
|
-
* (#2947、`v05/removed-syntax.ts`)。
|
|
157
|
-
*/
|
|
158
|
-
type: PresetType | 図種の別名;
|
|
159
|
-
/** 円グラフの見せ方。 */
|
|
160
|
-
form?: string;
|
|
161
|
-
/** `form` の和名。両方あれば `form` を採る。 */
|
|
162
|
-
見せ方?: string;
|
|
163
|
-
/** ガントチャートの目盛り。 */
|
|
164
|
-
ticks?: string[];
|
|
165
|
-
/** `ticks` の和名。両方あれば `ticks` を採る。 */
|
|
166
|
-
目盛り?: string[];
|
|
167
|
-
/** ガントチャートの今日の線の位置と札。 */
|
|
168
|
-
ganttToday?: { index: number; label: string };
|
|
169
|
-
/** 工程の端とは独立した目盛り名。 */
|
|
170
|
-
ganttTickLabels?: string[];
|
|
171
|
-
ganttBarEnd?: "position";
|
|
172
|
-
ganttBarThickness?: "thin";
|
|
173
|
-
chartSeriesSkipMuted?: DslDocument["chartSeriesSkipMuted"];
|
|
174
|
-
chartLineSeries?: DslDocument["chartLineSeries"];
|
|
175
|
-
chartPieCenterLabel?: DslDocument["chartPieCenterLabel"];
|
|
176
|
-
chartPieTableColumns?: DslDocument["chartPieTableColumns"];
|
|
177
|
-
chartPieRingWidth?: DslDocument["chartPieRingWidth"];
|
|
178
|
-
chartSlopePeriods?: DslDocument["chartSlopePeriods"];
|
|
179
|
-
chartSlopeEmphasisIds?: DslDocument["chartSlopeEmphasisIds"];
|
|
180
|
-
chartSlopeUnit?: DslDocument["chartSlopeUnit"];
|
|
181
|
-
chartStackedPeriods?: DslDocument["chartStackedPeriods"];
|
|
182
|
-
chartStackedRateId?: DslDocument["chartStackedRateId"];
|
|
183
|
-
chartStackedLegendPosition?: DslDocument["chartStackedLegendPosition"];
|
|
184
|
-
chartGaugeValue?: DslDocument["chartGaugeValue"];
|
|
185
|
-
chartWaffleLegendPosition?: DslDocument["chartWaffleLegendPosition"];
|
|
186
|
-
funnelForm?: DslDocument["funnelForm"];
|
|
187
|
-
funnelRate?: DslDocument["funnelRate"];
|
|
188
|
-
quadrantPointLabelSide?: DslDocument["quadrantPointLabelSide"];
|
|
189
|
-
treeNodeForm?: DslDocument["treeNodeForm"];
|
|
190
|
-
treeSize?: DslDocument["treeSize"];
|
|
191
|
-
treeEdgeTone?: DslDocument["treeEdgeTone"];
|
|
192
|
-
treeEdgeHead?: DslDocument["treeEdgeHead"];
|
|
193
|
-
mindForm?: DslDocument["mindForm"];
|
|
194
|
-
mindSize?: DslDocument["mindSize"];
|
|
195
|
-
edgeEndGap?: DslDocument["edgeEndGap"];
|
|
196
|
-
journeyForm?: DslDocument["journeyForm"];
|
|
197
|
-
journeyLineForm?: DslDocument["journeyLineForm"];
|
|
198
|
-
journeyLabels?: DslDocument["journeyLabels"];
|
|
199
|
-
journeySize?: DslDocument["journeySize"];
|
|
200
|
-
sequenceActorForm?: DslDocument["sequenceActorForm"];
|
|
201
|
-
sequenceLabelForm?: DslDocument["sequenceLabelForm"];
|
|
202
|
-
sequenceReturnHead?: DslDocument["sequenceReturnHead"];
|
|
203
|
-
sequenceKindLabels?: DslDocument["sequenceKindLabels"];
|
|
204
|
-
sequenceRowGap?: DslDocument["sequenceRowGap"];
|
|
205
|
-
sequenceGutter?: DslDocument["sequenceGutter"];
|
|
206
|
-
sequenceBandWidth?: DslDocument["sequenceBandWidth"];
|
|
207
|
-
sequenceSelfLoop?: DslDocument["sequenceSelfLoop"];
|
|
208
|
-
figureCard?: DslDocument["figureCard"];
|
|
209
|
-
figureSize?: DslDocument["figureSize"];
|
|
210
|
-
terminalDecoration?: DslDocument["terminalDecoration"];
|
|
211
|
-
cornerForm?: DslDocument["cornerForm"];
|
|
212
|
-
figureKindLabel?: DslDocument["figureKindLabel"];
|
|
213
|
-
figureInset?: DslDocument["figureInset"];
|
|
214
|
-
chartBarSize?: DslDocument["chartBarSize"];
|
|
215
|
-
chartBarShape?: DslDocument["chartBarShape"];
|
|
216
|
-
chartLineSize?: DslDocument["chartLineSize"];
|
|
217
|
-
chartPieSize?: DslDocument["chartPieSize"];
|
|
218
|
-
chartWaffleSize?: DslDocument["chartWaffleSize"];
|
|
219
|
-
chartSlopeLabelTemplate?: DslDocument["chartSlopeLabelTemplate"];
|
|
220
|
-
chartSlopeSize?: DslDocument["chartSlopeSize"];
|
|
221
|
-
chartStackedLayout?: DslDocument["chartStackedLayout"];
|
|
222
|
-
chartGaugeSize?: DslDocument["chartGaugeSize"];
|
|
223
|
-
ganttSize?: DslDocument["ganttSize"];
|
|
224
|
-
quadrantLabelPosition?: DslDocument["quadrantLabelPosition"];
|
|
225
|
-
quadrantSize?: DslDocument["quadrantSize"];
|
|
226
|
-
/**
|
|
227
|
-
* 図表の箱の上に出す小見出し (optional)。 記法の最上位 `eyebrow:` と同じ (#1247)。
|
|
228
|
-
*
|
|
229
|
-
* 効くのは図全体を 1 箱にする図種 (`chart` / `funnel` / `tree` / `journey` /
|
|
230
|
-
* `quadrant` / `mind` / `gantt`) だけ。 箱ごとに分かれる図種では相手が決まらないため、
|
|
231
|
-
* 組み立て側が知らせを出す。 そちらは `actors[].eyebrow` に書く。
|
|
232
|
-
*/
|
|
233
|
-
eyebrow?: string;
|
|
234
|
-
/**
|
|
235
|
-
* 2 軸で仕分ける図 (`type: quadrant`) の軸の名前 (#1294)。 記法の `axes:` と同じ。
|
|
236
|
-
*
|
|
237
|
-
* 書かないと「小さい / 大きい」 のままになり、何を判断する図か読めない。 区画の名前
|
|
238
|
-
* (`右上` 等) は軸の名前から決まる。
|
|
239
|
-
*
|
|
240
|
-
* 他の図種には軸が無いため、書かれていたら組み立て側が知らせる。
|
|
241
|
-
*/
|
|
242
|
-
axes?: JsonAxes;
|
|
243
|
-
/**
|
|
244
|
-
* 2 軸で仕分ける図の区画の名前 (#2667)。 記法の `regions:` と同じ。
|
|
245
|
-
*
|
|
246
|
-
* 鍵は項目の欄と同じ 4 語 (`左上` / `右上` / `左下` / `右下`)。 別の言い方を足すと
|
|
247
|
-
* 同じ区画を 2 通りで呼ぶことになる。
|
|
248
|
-
*/
|
|
249
|
-
regions?: JsonRegions;
|
|
250
|
-
/** 図の凡例の字の大きさ。 */
|
|
251
|
-
legendFontSize?: number;
|
|
252
|
-
legendOffset?: DslDocument["legendOffset"];
|
|
253
|
-
/** 段の箱で全意匠に共通して使う見出しの寸法。 */
|
|
254
|
-
stageHeader?: DslStageHeader;
|
|
255
|
-
stageLayout?: DslDocument["stageLayout"];
|
|
256
|
-
/** 登場人物 (必須): 文字列 or { name, kind, ... } object */
|
|
257
|
-
actors: (string | JsonActor)[];
|
|
258
|
-
/** flow step 配列 (必須): { from, to, label, ... } */
|
|
259
|
-
flow: JsonStep[];
|
|
260
|
-
/**
|
|
261
|
-
* 値を見せる部品 (optional、 #1374)。 記法の最上位 `readouts:` と同じ。
|
|
262
|
-
*
|
|
263
|
-
* 箱ではないので縦列に載らない。 図全体に 1 つの並びとして持つ。
|
|
264
|
-
*/
|
|
265
|
-
readouts?: DslReadout[];
|
|
266
|
-
/**
|
|
267
|
-
* 読む人が動かすつまみ (optional、 #1389)。 記法の最上位 `inputs:` と同じ。
|
|
268
|
-
*
|
|
269
|
-
* 部品と同じく箱ではないので縦列に載らない。 図全体に 1 つの並びとして持つ。
|
|
270
|
-
*/
|
|
271
|
-
inputs?: DslInput[];
|
|
272
|
-
/**
|
|
273
|
-
* つまみの値から決まる値 (optional、 #1391)。 記法の最上位 `formulas:` と同じ。
|
|
274
|
-
*
|
|
275
|
-
* 形は `{ 名前: "式" }`。 `values:` と経路が別で、式は名前を中括弧で囲わない。
|
|
276
|
-
*
|
|
277
|
-
* 名札を付ける時は `{ 名前: { expression: "式", label: "名札" } }` の組で書く (#1916)。
|
|
278
|
-
* 描画側は名札を操作部の式の名前に描く (cdl#853)。
|
|
279
|
-
*/
|
|
280
|
-
formulas?: Record<string, string | { expression: string; label?: string }>;
|
|
281
|
-
/**
|
|
282
|
-
* 押下などの出来事で動く仕掛け (optional、 #1393)。 記法の最上位 `events:` と同じ。
|
|
283
|
-
*
|
|
284
|
-
* 相手は名前で指す (`box` / `lane` / `arrow` / `diagram` のどれか 1 つ)。
|
|
285
|
-
*/
|
|
286
|
-
events?: {
|
|
287
|
-
on: string;
|
|
288
|
-
handler: string;
|
|
289
|
-
box?: string;
|
|
290
|
-
lane?: string;
|
|
291
|
-
arrow?: string;
|
|
292
|
-
diagram?: boolean;
|
|
293
|
-
}[];
|
|
294
|
-
/** 巻き上げに応じて進む値 (optional、 #1393)。 記法の最上位 `scrolls:` と同じ */
|
|
295
|
-
scrolls?: Record<string, { start?: number; end?: number; scrub?: number; label?: string }>;
|
|
296
|
-
/**
|
|
297
|
-
* 状態の初期値 (optional)。 記法の `states:` と同じ (#1181)。
|
|
298
|
-
*
|
|
299
|
-
* `{名前}` を箱の文字に置くと、ここに書いた値が描画側で置き換わる。 名前は英数字と `_`
|
|
300
|
-
* だけ (描画側が置き換える時に見る範囲と揃える)。
|
|
301
|
-
*
|
|
302
|
-
* ここに書けるのは初期値まで。 段で動かすのは `animation[].tween` / `animation[].set`
|
|
303
|
-
* (`#1186` で追加、記法の `tween:` / `set:` と同じ)。
|
|
304
|
-
*/
|
|
305
|
-
states?: Record<string, number | string>;
|
|
306
|
-
/**
|
|
307
|
-
* 他の値から自動で決まる値 (optional)。 記法の `values:` と同じ (#1181)。
|
|
308
|
-
*
|
|
309
|
-
* 式には四則 (`+ - * /`) と括弧、比較 (`> >= < <= == !=`)、`min` / `max` が書ける。
|
|
310
|
-
* 他の値は `{名前}` で読む。 解くのは描画側で、毎 frame 参照から順に決まる。
|
|
311
|
-
*/
|
|
312
|
-
values?: Record<string, string>;
|
|
313
|
-
/** animation phase 配列 (optional) */
|
|
314
|
-
animation?: JsonPhase[];
|
|
315
|
-
/** viewport (optional): 全体 canvas size / gap */
|
|
316
|
-
viewport?: {
|
|
317
|
-
width?: number;
|
|
318
|
-
height?: number;
|
|
319
|
-
laneWidth?: number;
|
|
320
|
-
gap?: number;
|
|
321
|
-
laneGap?: number;
|
|
322
|
-
nodeGap?: number;
|
|
323
|
-
labelMargin?: number;
|
|
324
|
-
};
|
|
325
|
-
/** lanes (optional): topology / swimlane preset で使う lane 宣言 */
|
|
326
|
-
lanes?: Record<
|
|
327
|
-
string,
|
|
328
|
-
{
|
|
329
|
-
x?: number;
|
|
330
|
-
width?: number;
|
|
331
|
-
label?: string;
|
|
332
|
-
/** 路線図の担当の名札で、名前の下へ出す補足。 */
|
|
333
|
-
subtitle?: string;
|
|
334
|
-
contain?: boolean;
|
|
335
|
-
lifeline?: boolean;
|
|
336
|
-
/**
|
|
337
|
-
* 縦列の位置のずらし (#1971)。 配置後の縦列の左上を (x, y) だけ動かす。 記法の `offsetX` / `offsetY`
|
|
338
|
-
* と同じで、読み方は `DslLane.layoutPos` が持つ。
|
|
339
|
-
*/
|
|
340
|
-
pos?: LayoutPos;
|
|
341
|
-
}
|
|
342
|
-
>;
|
|
343
|
-
/** groups (optional): topology preset で使う group 宣言 */
|
|
344
|
-
groups?: Record<
|
|
345
|
-
string,
|
|
346
|
-
{
|
|
347
|
-
label?: string;
|
|
348
|
-
lanes: string[];
|
|
349
|
-
}
|
|
350
|
-
>;
|
|
351
|
-
/** 順序図で面が動いている間の帯 (#1466)。 記法の最上位 `bands:` と同じ */
|
|
352
|
-
bands?: { actor: string; from: number; to: number }[];
|
|
353
|
-
/** 矢印をいつ出すか (#1470)。 記法の最上位 `reveal:` と同じ */
|
|
354
|
-
reveal?: EdgeReveal;
|
|
355
|
-
/** 箱に触れると関係する線だけを光らせるか (#1757)。 記法の最上位 `relations:` と同じ */
|
|
356
|
-
relations?: RelationFocus;
|
|
357
|
-
/** 図の並ぶ向き (#1494)。 記法の最上位 `direction:` と同じ。 JSON は英語の語で書く */
|
|
358
|
-
direction?: "vertical" | "horizontal";
|
|
359
|
-
/** 箱を並べ替える軸 (#2655)。 記法の最上位 `order:` と同じ。 JSON は英語の語で書く */
|
|
360
|
-
order?: "kind";
|
|
361
|
-
/** 図種ごとに選ぶ形 (#2657 / #2797)。 記法の最上位 `shape:` と同じ */
|
|
362
|
-
shape?: DslDiagramShape;
|
|
363
|
-
/** 図の意匠 (#1553 / #2790)。 記法の最上位 `theme:` と同じ。 */
|
|
364
|
-
theme?: string;
|
|
365
|
-
/** `theme` の別名。 両方書いた時は `theme` を使う。 */
|
|
366
|
-
palette?: string;
|
|
367
|
-
/** 図の下へ置く凡例。 印は記法と同じ英語名または和名で書く。 */
|
|
368
|
-
legend?: CdlLegendItem[];
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
/**
|
|
372
|
-
* 位置を他の要素からの相対で書く指定の、JSON 側の形 (#2039)。
|
|
373
|
-
*
|
|
374
|
-
* 記法の `位置: Web の右 200` を 3 つの欄に開いたもの。 内側の値は `RelativePos` と同じで、
|
|
375
|
-
* 向きの語も同じ 4 つ (`right` / `left` / `above` / `below`) を使う。
|
|
376
|
-
*
|
|
377
|
-
* **向きの語を並べ直さない**。 `RelativeDirection` をそのまま使うことで、本文側に向きを
|
|
378
|
-
* 足した時に JSON 側が黙って取り残される形を作らない。
|
|
379
|
-
*/
|
|
380
|
-
export interface JsonRelativePos {
|
|
381
|
-
/** 基準にする相手の名前。 `actors:` に書かれた名前をそのまま書く */
|
|
382
|
-
anchor: string;
|
|
383
|
-
/** 基準からどちら側に置くか */
|
|
384
|
-
dir: RelativeDirection;
|
|
385
|
-
/** 相手との間隔。 書かなければ `RELATIVE_GAP_DEFAULT` */
|
|
386
|
-
gap?: number;
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
export interface JsonActor {
|
|
390
|
-
name: string;
|
|
391
|
-
/**
|
|
392
|
-
* 箱の中に描く図形 (optional、 #1374)。 記法の `shape:` と同じ。
|
|
393
|
-
*
|
|
394
|
-
* 水位や角度を状態で動かす。 形は描画側の型が縛る。
|
|
395
|
-
*/
|
|
396
|
-
shape?: DslDynShape;
|
|
397
|
-
/**
|
|
398
|
-
* その箱を出すかどうかの条件 (optional、 #1381)。 記法の `visibleIf:` と同じ。
|
|
399
|
-
*/
|
|
400
|
-
visibleIf?: string;
|
|
401
|
-
/**
|
|
402
|
-
* 箱に出す題 (optional、 #1381)。 記法の `title:` と同じ。
|
|
403
|
-
*/
|
|
404
|
-
title?: string;
|
|
405
|
-
/**
|
|
406
|
-
* 値に追随する 5 欄 (optional、 #1392)。 記法の `wBind:` / `hBind:` / `opacity:` /
|
|
407
|
-
* `renderOffsetX:` / `renderOffsetY:` と同じ。
|
|
408
|
-
*
|
|
409
|
-
* 大きさは文字列だけ (数を書いても追随のしようが無い)、濃さとずらしは数も受ける。
|
|
410
|
-
*/
|
|
411
|
-
wBind?: string;
|
|
412
|
-
hBind?: string;
|
|
413
|
-
opacity?: number | string;
|
|
414
|
-
renderOffsetX?: number | string;
|
|
415
|
-
renderOffsetY?: number | string;
|
|
416
|
-
/**
|
|
417
|
-
* CAR-1657 unified syntax = 既存 NodeKind (一覧は `v05/parser.ts` の `NODE_KIND_VALID`) に
|
|
418
|
-
* 加えて parts identifier (arc-gauge 等) を accept する。
|
|
419
|
-
* 未知 kind 値は parts 候補として partId に格納、 compile 側 partsCatalog で解決。
|
|
420
|
-
* LLM structured output の typing 制約を緩めるため union に string 追加。
|
|
421
|
-
* `string & {}` = NodeKind の候補を IDE 補完で提示しつつ任意 string も許容する idiom。
|
|
422
|
-
* 素の `NodeKind | string` は no-redundant-type-constituents に抵触し補完も潰れる (#865)。
|
|
423
|
-
*/
|
|
424
|
-
kind?: NodeKind | (string & {});
|
|
425
|
-
subtitle?: string;
|
|
426
|
-
kindForm?: "icon";
|
|
427
|
-
icon?: DslActor["icon"];
|
|
428
|
-
emptyRowsNote?: string;
|
|
429
|
-
titleAlign?: "center";
|
|
430
|
-
rowRules?: boolean;
|
|
431
|
-
rowStripe?: boolean;
|
|
432
|
-
rowLayout?: DslActor["rowLayout"];
|
|
433
|
-
rowDividerForm?: DslActor["rowDividerForm"];
|
|
434
|
-
rowWell?: DslActor["rowWell"];
|
|
435
|
-
rowMarkForm?: "tone";
|
|
436
|
-
nestIn?: string;
|
|
437
|
-
figureCard?: DslActor["figureCard"];
|
|
438
|
-
stationNamePosition?: "bottom";
|
|
439
|
-
subtitlePlacement?: "right";
|
|
440
|
-
titleFontSize?: number;
|
|
441
|
-
markGap?: number;
|
|
442
|
-
eyebrow?: string;
|
|
443
|
-
value?: string;
|
|
444
|
-
/**
|
|
445
|
-
* 前の時点の値 (#1450)。 記法の `{ value: "320", previous: "280" }` と同じ。
|
|
446
|
-
*
|
|
447
|
-
* 内訳の変化を帯で示す図 (`type: stacked`) が 2 本目の帯として描き、
|
|
448
|
-
* 値 1 つを大きく示す図 (`type: stat`) が差として出す。 書かない図は 1 本のまま。
|
|
449
|
-
*/
|
|
450
|
-
previous?: string;
|
|
451
|
-
rows?: string[];
|
|
452
|
-
/** 段階ごとの箱で、この箱を入れる段階。 記法の `stage` / `段階` と同じ (#2797)。 */
|
|
453
|
-
stage?: string;
|
|
454
|
-
lane?: string;
|
|
455
|
-
stack?: number;
|
|
456
|
-
initial?: boolean;
|
|
457
|
-
final?: boolean;
|
|
458
|
-
/**
|
|
459
|
-
* 箱の位置のずらし (#1971)。 配置後の箱の中心を (x, y) だけ動かす。 記法の `offsetX` / `offsetY`
|
|
460
|
-
* と同じで、読み方は `DslActor.layoutPos` が持つ。
|
|
461
|
-
*/
|
|
462
|
-
pos?: LayoutPos;
|
|
463
|
-
/**
|
|
464
|
-
* 位置を他の要素からの相対で書く指定 (#2039)。 記法の `位置: Web の右 200` と同じ。
|
|
465
|
-
*
|
|
466
|
-
* 記法は 1 本の文字列に「誰の」「どちら側に」「どれだけ離して」 を詰めるが、JSON では
|
|
467
|
-
* 3 つの欄に分ける。 文字列を組み立てて渡す形にすると、書く側が記法の並び順と区切りを
|
|
468
|
-
* 覚える必要が出る = JSON を書くのは主に LLM で、読み取りの規則は欄の名前で示す方が短い。
|
|
469
|
-
*
|
|
470
|
-
* 組み立ての段階で基準の実座標から `posX` / `posY` に直すため、解決した後は座標を直接
|
|
471
|
-
* 書いた時と同じ経路を通る。 `pos` (配置後のずらし幅) とは別の欄。
|
|
472
|
-
*/
|
|
473
|
-
posRel?: JsonRelativePos;
|
|
474
|
-
/**
|
|
475
|
-
* CAR-1657 parts state override (kind = parts identifier 時のみ有効)。
|
|
476
|
-
* LLM JSON DSL では nested 明示 = `{ "state": { "v": 50 } }` が natural、 human 側の
|
|
477
|
-
* inline 拡散 pattern (`- arc1: { kind: arc-gauge, v: 50 }`) とは記述形式が分岐する
|
|
478
|
-
* (spec § 2.3 分岐設計、 human = YAML 手書き最適 / LLM = JSON structured 最適)。
|
|
479
|
-
*
|
|
480
|
-
* 見本でない箱に書くと誤りとして返す (#1294)。 記法側は読めない項目名として行番号付きで
|
|
481
|
-
* 知らせるため、黙って捨てると入口によって扱いが変わる。
|
|
482
|
-
*/
|
|
483
|
-
state?: Record<string, number | string | boolean>;
|
|
484
|
-
/**
|
|
485
|
-
* 箱の色 (#1294)。 記法の `tone:` と同じ。
|
|
486
|
-
*
|
|
487
|
-
* 色の名前と別名 (`成功` / `success` 等) を受ける。 見本 (parts) では色ではなく状態の
|
|
488
|
-
* 上書きとして意味を持つため、記法と同じく見本の箱には効かない。
|
|
489
|
-
*/
|
|
490
|
-
tone?: Tone | (string & {});
|
|
491
|
-
/**
|
|
492
|
-
* 色番号または色の名前 (#1294)。 記法の `色:` / `color:` と同じ。
|
|
493
|
-
*
|
|
494
|
-
* `#` で始まる値は色番号として `colorHex` に入り、見本の絵の色を変える。 それ以外は
|
|
495
|
-
* 色の名前として読む (記法の `splitColorValue` と同じ振り分け)。
|
|
496
|
-
*/
|
|
497
|
-
color?: string;
|
|
498
|
-
/**
|
|
499
|
-
* 工程の並び (`type: gantt`) で、その工程の担当 (#1294)。 記法の `owner:` と同じ。
|
|
500
|
-
*/
|
|
501
|
-
owner?: string;
|
|
502
|
-
/**
|
|
503
|
-
* 工程の並び (`type: gantt`) で、その工程が終わる時期 (#1294)。 記法の `end:` と同じ。
|
|
504
|
-
*
|
|
505
|
-
* 値は他の工程が書いた時期のどれかに一致させる。 一致しない値は始まりと同じ位置に落ちる
|
|
506
|
-
* (記法側と同じ扱い)。
|
|
507
|
-
*/
|
|
508
|
-
end?: string;
|
|
509
|
-
startLabel?: string;
|
|
510
|
-
milestone?: boolean;
|
|
511
|
-
emphasis?: "primary";
|
|
512
|
-
/** 四象限の座標 `[x, y]`。 */
|
|
513
|
-
at?: [number | string, number | string];
|
|
514
|
-
/** `at` の和名。両方あれば `at` を採る。 */
|
|
515
|
-
点の位置?: [number | string, number | string];
|
|
516
|
-
/**
|
|
517
|
-
* ユーザージャーニー (`type: journey`) で、その段階が起きる場所 (#1294)。 記法の `touchpoint:` と同じ。
|
|
518
|
-
*/
|
|
519
|
-
touchpoint?: string;
|
|
520
|
-
/**
|
|
521
|
-
* ユーザージャーニー (`type: journey`) で、その段階の改善の余地 (#1294)。 記法の `opportunity:` と同じ。
|
|
522
|
-
*/
|
|
523
|
-
opportunity?: string;
|
|
524
|
-
opportunityPosition?: DslActor["opportunityPosition"];
|
|
525
|
-
/**
|
|
526
|
-
* 箱を置く絶対座標と大きさ (#1294)。 記法の `posX:` / `posY:` / `posW:` / `posH:` と同じ。
|
|
527
|
-
*
|
|
528
|
-
* `pos` (ずらし幅) とは別物。 こちらは auto layout を使わずに位置そのものを決める。
|
|
529
|
-
*/
|
|
530
|
-
posX?: number;
|
|
531
|
-
posY?: number;
|
|
532
|
-
posW?: number;
|
|
533
|
-
posH?: number;
|
|
534
|
-
/**
|
|
535
|
-
* 見本を何倍で描くか (#1294)。 記法の `scale:` / `倍率:` と同じ。
|
|
536
|
-
*
|
|
537
|
-
* 見本 (parts) にしか効かない。 見本でない箱に書くと誤りとして返す (記法側も同じく
|
|
538
|
-
* 読めない項目名として知らせる)。
|
|
539
|
-
*/
|
|
540
|
-
scale?: number;
|
|
541
|
-
/**
|
|
542
|
-
* 見本が持つ段を使うか (#2125)。 記法の `{ kind: wave-gauge, phase: false }` と同じ。
|
|
543
|
-
*
|
|
544
|
-
* 既定 (書かない) は見本の段を書いた段と番号ごとに並べて再生する。 `false` を書くと
|
|
545
|
-
* 見本の段の中身 (点灯と値の変化) を空にし、値は書いた段だけが動かす。
|
|
546
|
-
*
|
|
547
|
-
* 見本 (parts) にしか効かない。 中身は状態の上書き (`state`) に `phase` として入る =
|
|
548
|
-
* `"state": { "phase": false }` と同じ所へ届く。 記法が中括弧の中に 1 つの名前として
|
|
549
|
-
* 書ける以上、JSON でも同じ名前で書けるようにする (入口で書き方が分かれない)。
|
|
550
|
-
*/
|
|
551
|
-
phase?: boolean;
|
|
552
|
-
/** 行頭の印 (#1466)。 行と対で読む。 語の意味は図の種類が決める */
|
|
553
|
-
marks?: Array<string | { mark: string; tone?: "primary" | "muted" }>;
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
/** 2 軸で仕分ける図の軸の名前 (#1294)。 記法の `axes:` と同じ形 */
|
|
557
|
-
export interface JsonAxes {
|
|
558
|
-
x?: { left?: string; right?: string; label?: never; direction?: never } | { label: string; direction: true };
|
|
559
|
-
y?: { bottom?: string; top?: string; label?: never; direction?: never } | { label: string; direction: true };
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
/** 2 軸で仕分ける図の区画の名前 (#2667)。 記法の `regions:` と同じ形 */
|
|
563
|
-
export interface JsonRegions {
|
|
564
|
-
左上?: string;
|
|
565
|
-
右上?: string;
|
|
566
|
-
左下?: string;
|
|
567
|
-
右下?: string;
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
export interface JsonStep {
|
|
571
|
-
from: string;
|
|
572
|
-
to: string;
|
|
573
|
-
label: string;
|
|
574
|
-
sub?: string;
|
|
575
|
-
/** 矢印がどの辺から出るか (#1385)。 記法の `side:` と同じ */
|
|
576
|
-
side?: "top" | "right" | "bottom" | "left";
|
|
577
|
-
fromSide?: "top" | "right" | "bottom" | "left";
|
|
578
|
-
toSide?: "top" | "right" | "bottom" | "left";
|
|
579
|
-
/**
|
|
580
|
-
* 矢印の先の形 (#1462)。 記法の `{ head: triangle }` と同じ。
|
|
581
|
-
*
|
|
582
|
-
* 三角 (継承・実装) / 菱 (集約・コンポジション) / 開いた矢 (関連・依存) / 鳥の足 (多)。
|
|
583
|
-
* 書かなければ従来どおり塗った三角になる。
|
|
584
|
-
*/
|
|
585
|
-
head?: EdgeHead;
|
|
586
|
-
/** 出どころ側の端の形と、両端の塗り (#1466)。 記法の `{ tailHead: diamond }` 等と同じ */
|
|
587
|
-
tailHead?: EdgeHead;
|
|
588
|
-
headFill?: EdgeHeadFill;
|
|
589
|
-
tailHeadFill?: EdgeHeadFill;
|
|
590
|
-
/** UML 関係の出どころ側の多重度 (#1771 / #2783)。記法の `{ tailSub: "1" }` と同じ */
|
|
591
|
-
tailSub?: string;
|
|
592
|
-
/**
|
|
593
|
-
* 辺の役目 (cdl#618)。 記法の `{ role: main }` と同じ。
|
|
594
|
-
*
|
|
595
|
-
* `main` を書いた辺だけ「いま」 の色で引く。 主となる 1 本 (または 1 続き) にだけ書く。
|
|
596
|
-
*/
|
|
597
|
-
role?: "main";
|
|
598
|
-
/** 名前の下地を敷くか (cdl#618)。 記法の `{ labelPlate: false }` と同じ。 既定は敷く */
|
|
599
|
-
labelPlate?: boolean;
|
|
600
|
-
/** record の UML 関係の語 (#1466 / #2783)。書くと端の形 / 塗り / 線種がまとめて決まる */
|
|
601
|
-
relation?: ClassRelationType;
|
|
602
|
-
/** 順序図の言づての種類 (#1466)。 `call` / `return` / `fire` */
|
|
603
|
-
kind?: SequenceMessageKind;
|
|
604
|
-
/**
|
|
605
|
-
* 矢印の色 (#1304)。 記法の `(成功)` と同じく別名 (`成功` / `neutral` 等) も受ける。
|
|
606
|
-
*
|
|
607
|
-
* `string & {}` は箱の `tone` と同じ idiom = 正規の色名を補完に出しつつ別名も通す。
|
|
608
|
-
*/
|
|
609
|
-
tone?: Tone | (string & {});
|
|
610
|
-
style?: EdgeStyle;
|
|
611
|
-
guard?: string;
|
|
612
|
-
cardinality?: string;
|
|
613
|
-
/**
|
|
614
|
-
* 矢印を値に追随させる 3 欄 (optional、 #1396)。 記法の `widthBind:` / `strokeBind:` /
|
|
615
|
-
* `dashOffsetBind:` と同じ。 描画側は文字列だけを取る。
|
|
616
|
-
*/
|
|
617
|
-
widthBind?: string;
|
|
618
|
-
strokeBind?: string;
|
|
619
|
-
dashOffsetBind?: string;
|
|
620
|
-
labelOffsetX?: number;
|
|
621
|
-
labelOffsetY?: number;
|
|
622
|
-
posX1?: number;
|
|
623
|
-
posY1?: number;
|
|
624
|
-
posX2?: number;
|
|
625
|
-
posY2?: number;
|
|
626
|
-
waypoints?: DslStep["waypoints"];
|
|
627
|
-
labelCollision?: DslStep["labelCollision"];
|
|
628
|
-
labelSize?: DslStep["labelSize"];
|
|
629
|
-
/** true で説明文を矢印の線の上に重ねる。 分岐図の条件ラベル用。 */
|
|
630
|
-
overlay?: boolean;
|
|
631
|
-
/**
|
|
632
|
-
* 矢印の位置のずらし (#1971)。 名前のずらし (`labelOffsetX` / `labelOffsetY`) に足して名前を動かす。
|
|
633
|
-
*/
|
|
634
|
-
pos?: LayoutPos;
|
|
635
|
-
/**
|
|
636
|
-
* 部品の端で矢印を繋ぐ部品の中の要素の id (#1979)。 記法の `{ fromPartNode: rC }` / `{ toPartNode: gC }` と同じ。
|
|
637
|
-
* 要素を 1 つだけ持つ部品は書かなくてもその要素に繋ぐ
|
|
638
|
-
*/
|
|
639
|
-
fromPartNode?: string;
|
|
640
|
-
toPartNode?: string;
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
export interface JsonPhase {
|
|
644
|
-
/** phase name (必須) */
|
|
645
|
-
step: string;
|
|
646
|
-
/** duration in seconds (default 1.4) */
|
|
647
|
-
duration?: number;
|
|
648
|
-
/** highlight 対象 (actor name / edge "A -> B") */
|
|
649
|
-
focus?: string[];
|
|
650
|
-
/** body 説明文 */
|
|
651
|
-
body?: string;
|
|
652
|
-
/**
|
|
653
|
-
* 説明文の別名 (#2621)。 記法が `description:` を受けるので JSON でも受ける。
|
|
654
|
-
*
|
|
655
|
-
* **正の語は `body`**。 両方書いた段は `body` を採る = 正の語を優先すると決めておくと、
|
|
656
|
-
* 読み手が「どちらが効くのか」 を実装を読まずに判断できる。
|
|
657
|
-
*/
|
|
658
|
-
description?: string;
|
|
659
|
-
/** badge label */
|
|
660
|
-
badge?: string;
|
|
661
|
-
/**
|
|
662
|
-
* その段で起点から描くもの (#1312 / #1314 / #1318)。 記法の `draw:` と同じ。
|
|
663
|
-
*
|
|
664
|
-
* 受ける 8 語は `DRAW_WORDS` が持つ。 語と同じ図種で、その段の間に図が起点から現れる。
|
|
665
|
-
*/
|
|
666
|
-
draw?: string;
|
|
667
|
-
/**
|
|
668
|
-
* 描き終えるまでに段の何割を使うか (#1441)。 記法の `draw: line 0.4` の後半と同じ。
|
|
669
|
-
*
|
|
670
|
-
* 0 より大きく 1 以下。 書かなければ段の終わりに描き終わる。 `draw` が無い段に書いても
|
|
671
|
-
* 何も起きない (描く相手が決まらないため、写す時に外す)。
|
|
672
|
-
*/
|
|
673
|
-
drawRatio?: number;
|
|
674
|
-
/**
|
|
675
|
-
* 段の中で値を動かす (#1186)。 記法の `tween: name 100 -> 90` と同じ。
|
|
676
|
-
*
|
|
677
|
-
* 足すまで JSON の入口は `states:` で初期値を書けても **動かす手段が無かった** ため、
|
|
678
|
-
* 同じ図を記法で書くと動き JSON で書くと静止する状態だった (#1181 で状態を足した時の残り)。
|
|
679
|
-
*/
|
|
680
|
-
tween?: Record<string, readonly [number, number]>;
|
|
681
|
-
/**
|
|
682
|
-
* 段の切替で値を差し替える (#1186)。 記法の `set: name value` と同じ。
|
|
683
|
-
*
|
|
684
|
-
* `tween` が段の中を補間するのに対し、こちらは段の切替時に 1 度だけ変える。
|
|
685
|
-
*/
|
|
686
|
-
set?: Record<string, number | string>;
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
/**
|
|
690
|
-
* JSON DSL error。 line 概念がないため、 field path (JSON pointer style) で位置を示す。
|
|
691
|
-
*/
|
|
692
|
-
export interface JsonDslError {
|
|
693
|
-
path: string;
|
|
694
|
-
message: string;
|
|
695
|
-
hint?: string;
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
/**
|
|
699
|
-
* 受け付ける種類 (`kind`)。 **記法側と同じ集合を使う** (`v05/parser.ts` の `NODE_KIND_VALID`)。
|
|
700
|
-
*
|
|
701
|
-
* ここに無い値は見本 (parts) の名前とみなし、`partId` へ退避して箱を `actor` に倒す
|
|
702
|
-
* (CAR-1657 の unified syntax)。 つまりこの集合が小さいほど、**書いた種類が箱の見た目に効かなくなる**。
|
|
703
|
-
* 部品を持つ一覧を渡した組み立てでは一覧に無い名前を知らせる (`part-not-found`、#2113) が、
|
|
704
|
-
* 一覧を渡さない組み立てと部品を持たない一覧の組み立てでは `console.warn` にしか残らない。
|
|
705
|
-
*
|
|
706
|
-
* 以前はここに一覧を写していたため、記法に種類が増えても JSON 経路が古い一覧のまま
|
|
707
|
-
* 見本の名前として扱った (実測 = 記法が知る 108 種のうち 77 種が該当、`shape-*` は全滅)。
|
|
708
|
-
* 当時はカタログに無い名前が `console.warn` にしか残らず `onNotice` を呼ばなかったため、
|
|
709
|
-
* 画面には何も出ないまま `actor` の箱が描かれていた (#1293)。
|
|
710
|
-
*
|
|
711
|
-
* 型が 2 箇所に散らばると、必ずどちらかが古くなる。 `VALID_PRESETS` (図種) と同じ扱いにする。
|
|
712
|
-
*/
|
|
713
|
-
const VALID_KIND_SET: ReadonlySet<string> = NODE_KIND_VALID;
|
|
714
|
-
|
|
715
|
-
/**
|
|
716
|
-
* 受け付ける図種。 **記法側と同じ集合を使う** (`v05/parser.ts` の `PRESET_TYPES`)。
|
|
717
|
-
*
|
|
718
|
-
* 以前はここに一覧を写していたため、 記法に型を足しても JSON 経路が古い一覧のまま弾いた
|
|
719
|
-
* (`bar` / `line` で実際に起きた)。 型が 3 箇所に散らばると、 必ずどれかが古くなる。
|
|
720
|
-
*/
|
|
721
|
-
const VALID_PRESETS: readonly PresetType[] = [...PRESET_TYPES];
|
|
722
|
-
|
|
723
|
-
/**
|
|
724
|
-
* 受け付ける図種の別名 (#2655)。 **記法側と同じ表を使う** (`v05/parser.ts` の `TYPE_ALIASES`)。
|
|
725
|
-
*
|
|
726
|
-
* 写すと片方だけ古くなる。 図種の一覧を 1 か所に寄せたのと同じ理由。
|
|
727
|
-
*/
|
|
728
|
-
const VALID_ALIASES: readonly string[] = [...TYPE_ALIASES.keys()];
|
|
729
|
-
|
|
730
|
-
/**
|
|
731
|
-
* 受け付ける項目の一覧 (#1295)。 **知らない項目を誤りにする判定と、公開 schema との
|
|
732
|
-
* 突き合わせが、どちらもここを見る**。
|
|
733
|
-
*
|
|
734
|
-
* 一覧を型 / 検査 / schema の 3 箇所に手で置くと必ずどれかが古くなる。 型は TypeScript が
|
|
735
|
-
* 見る宣言で実行時には残らないため、実行時の判定と schema はこの表を出どころにする。
|
|
736
|
-
*
|
|
737
|
-
* 公開している JSON Schema は元から全階層で `additionalProperties: false` を宣言していた。
|
|
738
|
-
* 知らない項目を弾くのは新しい方針ではなく、parser が自分の契約に追いついていなかった。
|
|
739
|
-
*
|
|
740
|
-
* `states` / `values` / `tween` / `set` / `lanes` / `groups` の **鍵は利用者が決める**
|
|
741
|
-
* (状態の名前 / 縦列の id)。 表が縛るのはその中の値の形で、鍵そのものではない。
|
|
742
|
-
*/
|
|
743
|
-
export const ACCEPTED_KEYS = {
|
|
744
|
-
root: [
|
|
745
|
-
"title",
|
|
746
|
-
"type",
|
|
747
|
-
"form",
|
|
748
|
-
"見せ方",
|
|
749
|
-
"ticks",
|
|
750
|
-
"目盛り",
|
|
751
|
-
"ganttToday",
|
|
752
|
-
"ganttTickLabels",
|
|
753
|
-
"ganttBarEnd",
|
|
754
|
-
"ganttBarThickness",
|
|
755
|
-
"chartSeriesSkipMuted",
|
|
756
|
-
"chartLineSeries",
|
|
757
|
-
"chartPieCenterLabel",
|
|
758
|
-
"chartPieTableColumns",
|
|
759
|
-
"chartPieRingWidth",
|
|
760
|
-
"chartSlopePeriods",
|
|
761
|
-
"chartSlopeEmphasisIds",
|
|
762
|
-
"chartSlopeUnit",
|
|
763
|
-
"chartStackedPeriods",
|
|
764
|
-
"chartStackedRateId",
|
|
765
|
-
"chartStackedLegendPosition",
|
|
766
|
-
"chartGaugeValue",
|
|
767
|
-
"chartWaffleLegendPosition",
|
|
768
|
-
"funnelForm",
|
|
769
|
-
"funnelRate",
|
|
770
|
-
"quadrantPointLabelSide",
|
|
771
|
-
"treeNodeForm",
|
|
772
|
-
"treeSize",
|
|
773
|
-
"treeEdgeTone",
|
|
774
|
-
"treeEdgeHead",
|
|
775
|
-
"mindForm",
|
|
776
|
-
"mindSize",
|
|
777
|
-
"edgeEndGap",
|
|
778
|
-
"journeyForm",
|
|
779
|
-
"journeyLineForm",
|
|
780
|
-
"journeyLabels",
|
|
781
|
-
"journeySize",
|
|
782
|
-
"sequenceActorForm",
|
|
783
|
-
"sequenceLabelForm",
|
|
784
|
-
"sequenceReturnHead",
|
|
785
|
-
"sequenceKindLabels",
|
|
786
|
-
"sequenceRowGap",
|
|
787
|
-
"sequenceGutter",
|
|
788
|
-
"sequenceBandWidth",
|
|
789
|
-
"sequenceSelfLoop",
|
|
790
|
-
"figureCard",
|
|
791
|
-
"figureSize",
|
|
792
|
-
"terminalDecoration",
|
|
793
|
-
"cornerForm",
|
|
794
|
-
"figureKindLabel",
|
|
795
|
-
"figureInset",
|
|
796
|
-
"chartBarSize",
|
|
797
|
-
"chartBarShape",
|
|
798
|
-
"chartLineSize",
|
|
799
|
-
"chartPieSize",
|
|
800
|
-
"chartWaffleSize",
|
|
801
|
-
"chartSlopeLabelTemplate",
|
|
802
|
-
"chartSlopeSize",
|
|
803
|
-
"chartStackedLayout",
|
|
804
|
-
"chartGaugeSize",
|
|
805
|
-
"ganttSize",
|
|
806
|
-
"quadrantLabelPosition",
|
|
807
|
-
"quadrantSize",
|
|
808
|
-
"eyebrow",
|
|
809
|
-
"axes",
|
|
810
|
-
"regions",
|
|
811
|
-
"actors",
|
|
812
|
-
"flow",
|
|
813
|
-
"states",
|
|
814
|
-
"values",
|
|
815
|
-
"animation",
|
|
816
|
-
"viewport",
|
|
817
|
-
"lanes",
|
|
818
|
-
"groups",
|
|
819
|
-
// 値を見せる部品 (#1374)
|
|
820
|
-
"readouts",
|
|
821
|
-
// 読む人が動かすつまみ (#1389)
|
|
822
|
-
"inputs",
|
|
823
|
-
// つまみの値から決まる値 (#1391)
|
|
824
|
-
"formulas",
|
|
825
|
-
// 押下などの出来事で動く仕掛けと、巻き上げに応じて進む値 (#1393)
|
|
826
|
-
"events",
|
|
827
|
-
"scrolls",
|
|
828
|
-
// 順序図で面が動いている間の帯 (#1466)
|
|
829
|
-
"bands",
|
|
830
|
-
// 矢印をいつ出すか (#1470)
|
|
831
|
-
"reveal",
|
|
832
|
-
// 箱に触れると関係する線だけを光らせるか (#1757)
|
|
833
|
-
"relations",
|
|
834
|
-
// 図の並ぶ向き (#1494)
|
|
835
|
-
"direction",
|
|
836
|
-
// 箱を並べ替える軸 (#2655)
|
|
837
|
-
"order",
|
|
838
|
-
// 数を描く図の形 (#2657)
|
|
839
|
-
"shape",
|
|
840
|
-
// 図の意匠とその別名 (#1553 / #2790)
|
|
841
|
-
"theme",
|
|
842
|
-
"palette",
|
|
843
|
-
"legend",
|
|
844
|
-
"legendFontSize",
|
|
845
|
-
"legendOffset",
|
|
846
|
-
"stageHeader",
|
|
847
|
-
"stageLayout",
|
|
848
|
-
],
|
|
849
|
-
actor: [
|
|
850
|
-
"name",
|
|
851
|
-
"kind",
|
|
852
|
-
"subtitle",
|
|
853
|
-
"kindForm",
|
|
854
|
-
"icon",
|
|
855
|
-
"emptyRowsNote",
|
|
856
|
-
"titleAlign",
|
|
857
|
-
"rowRules",
|
|
858
|
-
"rowStripe",
|
|
859
|
-
"rowLayout",
|
|
860
|
-
"rowDividerForm",
|
|
861
|
-
"rowWell",
|
|
862
|
-
"rowMarkForm",
|
|
863
|
-
"nestIn",
|
|
864
|
-
"figureCard",
|
|
865
|
-
"stationNamePosition",
|
|
866
|
-
"subtitlePlacement",
|
|
867
|
-
"titleFontSize",
|
|
868
|
-
"markGap",
|
|
869
|
-
"eyebrow",
|
|
870
|
-
"value",
|
|
871
|
-
// 前の時点の値 (#1450)。 内訳の変化を帯で示す図と、値 1 つを大きく示す図が読む
|
|
872
|
-
"previous",
|
|
873
|
-
"rows",
|
|
874
|
-
"stage",
|
|
875
|
-
"lane",
|
|
876
|
-
"stack",
|
|
877
|
-
"initial",
|
|
878
|
-
"final",
|
|
879
|
-
"tone",
|
|
880
|
-
"color",
|
|
881
|
-
"owner",
|
|
882
|
-
"end",
|
|
883
|
-
"startLabel",
|
|
884
|
-
"milestone",
|
|
885
|
-
"emphasis",
|
|
886
|
-
"at",
|
|
887
|
-
"点の位置",
|
|
888
|
-
"touchpoint",
|
|
889
|
-
"opportunity",
|
|
890
|
-
"opportunityPosition",
|
|
891
|
-
"posX",
|
|
892
|
-
"posY",
|
|
893
|
-
"posW",
|
|
894
|
-
"posH",
|
|
895
|
-
"scale",
|
|
896
|
-
"state",
|
|
897
|
-
"pos",
|
|
898
|
-
// 箱の中に描く図形 (#1374)
|
|
899
|
-
"shape",
|
|
900
|
-
// その箱を出すかどうかの条件 (#1381)
|
|
901
|
-
"visibleIf",
|
|
902
|
-
// 箱に出す題 (#1381)
|
|
903
|
-
"title",
|
|
904
|
-
// 値に追随する 5 欄 (#1392)
|
|
905
|
-
"wBind",
|
|
906
|
-
"hBind",
|
|
907
|
-
"opacity",
|
|
908
|
-
"renderOffsetX",
|
|
909
|
-
"renderOffsetY",
|
|
910
|
-
// 行頭の印 (#1466)。 行ごとに 1 つ、図の種類ごとの語で書く
|
|
911
|
-
"marks",
|
|
912
|
-
// 位置を他の要素からの相対で書く指定 (#2039)
|
|
913
|
-
"posRel",
|
|
914
|
-
// 見本が持つ段を使うか (#2125)
|
|
915
|
-
"phase",
|
|
916
|
-
],
|
|
917
|
-
step: [
|
|
918
|
-
"from",
|
|
919
|
-
"to",
|
|
920
|
-
"label",
|
|
921
|
-
"sub",
|
|
922
|
-
// 矢印がどの辺から出るか (#1385)
|
|
923
|
-
"side",
|
|
924
|
-
"fromSide",
|
|
925
|
-
"toSide",
|
|
926
|
-
// 矢印の先の形 (#1462)
|
|
927
|
-
"head",
|
|
928
|
-
// 端の印の残り 3 欄と、関係の語 / 言づての種類 (#1466)
|
|
929
|
-
"tailHead",
|
|
930
|
-
"headFill",
|
|
931
|
-
"tailHeadFill",
|
|
932
|
-
// UML 関係の出どころ側の多重度 (#1771 / #2783)
|
|
933
|
-
"tailSub",
|
|
934
|
-
"relation",
|
|
935
|
-
// 辺の役目と名前の下地 (cdl#618)
|
|
936
|
-
"role",
|
|
937
|
-
"labelPlate",
|
|
938
|
-
"kind",
|
|
939
|
-
"tone",
|
|
940
|
-
"style",
|
|
941
|
-
"guard",
|
|
942
|
-
"cardinality",
|
|
943
|
-
// 値に追随する 3 欄 (#1396)
|
|
944
|
-
"widthBind",
|
|
945
|
-
"strokeBind",
|
|
946
|
-
"dashOffsetBind",
|
|
947
|
-
"labelOffsetX",
|
|
948
|
-
"labelOffsetY",
|
|
949
|
-
"posX1",
|
|
950
|
-
"posY1",
|
|
951
|
-
"posX2",
|
|
952
|
-
"posY2",
|
|
953
|
-
"waypoints",
|
|
954
|
-
"labelCollision",
|
|
955
|
-
"labelSize",
|
|
956
|
-
"overlay",
|
|
957
|
-
"pos",
|
|
958
|
-
// 部品の端で繋ぐ要素の id (#1979)
|
|
959
|
-
"fromPartNode",
|
|
960
|
-
"toPartNode",
|
|
961
|
-
],
|
|
962
|
-
// 説明の語は `PHASE_BODY_KEYS` から導く (#2621)。 書き写すと記法の入口とずれる
|
|
963
|
-
phase: ["step", "duration", "focus", ...PHASE_BODY_KEYS, "badge", "tween", "set", "draw", "drawRatio"],
|
|
964
|
-
viewport: ["width", "height", "contentOffsetX", "scale", "laneWidth", "gap", "laneGap", "nodeGap", "labelMargin"],
|
|
965
|
-
lane: ["x", "width", "label", "subtitle", "contain", "lifeline", "pos"],
|
|
966
|
-
group: ["label", "lanes"],
|
|
967
|
-
axes: ["x", "y"],
|
|
968
|
-
axesX: ["left", "right", "label", "direction"],
|
|
969
|
-
axesY: ["bottom", "top", "label", "direction"],
|
|
970
|
-
// 区画を指す語は項目の欄と同じ 4 語 (#2667)
|
|
971
|
-
regions: ["左上", "右上", "左下", "右下"],
|
|
972
|
-
layoutPos: ["x", "y"],
|
|
973
|
-
// 位置を他の要素からの相対で書く指定 (#2039)。 間隔は書かなくてよい
|
|
974
|
-
posRel: ["anchor", "dir", "gap"],
|
|
975
|
-
} as const satisfies Record<string, readonly string[]>;
|
|
976
|
-
|
|
977
|
-
/** 受ける項目を持つ階層の名前 */
|
|
978
|
-
export type 階層 = keyof typeof ACCEPTED_KEYS;
|
|
979
|
-
|
|
980
|
-
/**
|
|
981
|
-
* 欄ごとの値の型 (#1304)。 **`ACCEPTED_KEYS` と同じ欄を必ず持つ**。
|
|
982
|
-
*
|
|
983
|
-
* 項目名の側は `#1295` で閉じたが、値の側は一部の欄にしか検査が無かった。 実測すると
|
|
984
|
-
* 20 欄が型違いの値をそのまま通し、`type: sequence` では 17 欄の値が図まで届いていた
|
|
985
|
-
* (`labelOffsetX: "q"` が矢印の中に `"q"` のまま入る、`tone: "bogus"` がそのまま色として載る)。
|
|
986
|
-
*
|
|
987
|
-
* 表を `ACCEPTED_KEYS` の隣に置き、下の `satisfies` で **欄が 1 つでも欠けたら型検査が落ちる**
|
|
988
|
-
* ようにする。 欄を足した時に「名前は受けるが値は見ない」 状態が作れない。
|
|
989
|
-
*
|
|
990
|
-
* ## 誰がどの欄を見るか
|
|
991
|
-
*
|
|
992
|
-
* | 型 | 見る場所 |
|
|
993
|
-
* |---|---|
|
|
994
|
-
* | 値そのものの型 (文字列 / 数 / 真偽 / 色 / 線種 等) | 本 file の `表で検査` |
|
|
995
|
-
* | `object` / `並び` | 欄ごとの専用の検査 (`validateViewport` 等) |
|
|
996
|
-
*
|
|
997
|
-
* 分けるのは、中身の形が欄ごとに違うから。 外側の形だけを表で見ても中身は見られないので、
|
|
998
|
-
* 専用の検査に任せて二重に誤りを出さない。 **専用の検査が抜けても表からは分からない** ため、
|
|
999
|
-
* 検査 (`test/json-value-types.test.ts`) が全欄に型違いの値を入れて誤りが返ることを確かめる。
|
|
1000
|
-
*/
|
|
1001
|
-
export type 欄の型 =
|
|
1002
|
-
| "必須の非空文字列"
|
|
1003
|
-
| "必須の文字列"
|
|
1004
|
-
| "非空の文字列"
|
|
1005
|
-
| "文字列"
|
|
1006
|
-
| "文字列の並び"
|
|
1007
|
-
| "数"
|
|
1008
|
-
| "必須の数"
|
|
1009
|
-
// 値に追随する欄 (#1392)。 数を直に書く形と、状態の名前を書く形の両方を受ける
|
|
1010
|
-
| "数か文字列"
|
|
1011
|
-
| "真偽"
|
|
1012
|
-
| "色"
|
|
1013
|
-
| "線種"
|
|
1014
|
-
| "辺"
|
|
1015
|
-
| "端の形"
|
|
1016
|
-
// 相対で置く時の向き (#2039)。 書けば必ず要る = 向きが無いとどちら側か決まらない
|
|
1017
|
-
| "必須の向き"
|
|
1018
|
-
| "色か色番号"
|
|
1019
|
-
| "描くもの"
|
|
1020
|
-
| "意匠"
|
|
1021
|
-
| "図の形"
|
|
1022
|
-
| "ガントの帯の終わり"
|
|
1023
|
-
| "ガントの帯の厚み"
|
|
1024
|
-
| "札の角"
|
|
1025
|
-
| "四象限の区画名位置"
|
|
1026
|
-
| "線札の衝突"
|
|
1027
|
-
| "主役"
|
|
1028
|
-
| "必須の図種"
|
|
1029
|
-
| "object"
|
|
1030
|
-
| "並び"
|
|
1031
|
-
| "必須の並び"
|
|
1032
|
-
| "必須の非空の並び";
|
|
1033
|
-
|
|
1034
|
-
export const 欄の型表 = {
|
|
1035
|
-
root: {
|
|
1036
|
-
title: "必須の非空文字列",
|
|
1037
|
-
type: "必須の図種",
|
|
1038
|
-
form: "非空の文字列",
|
|
1039
|
-
見せ方: "非空の文字列",
|
|
1040
|
-
ticks: "文字列の並び",
|
|
1041
|
-
目盛り: "文字列の並び",
|
|
1042
|
-
ganttToday: "object",
|
|
1043
|
-
ganttTickLabels: "文字列の並び",
|
|
1044
|
-
ganttBarEnd: "ガントの帯の終わり",
|
|
1045
|
-
ganttBarThickness: "ガントの帯の厚み",
|
|
1046
|
-
chartSeriesSkipMuted: "真偽",
|
|
1047
|
-
chartLineSeries: "並び",
|
|
1048
|
-
chartPieCenterLabel: "文字列",
|
|
1049
|
-
chartPieTableColumns: "文字列",
|
|
1050
|
-
chartPieRingWidth: "文字列",
|
|
1051
|
-
chartSlopePeriods: "文字列の並び",
|
|
1052
|
-
chartSlopeEmphasisIds: "文字列の並び",
|
|
1053
|
-
chartSlopeUnit: "文字列",
|
|
1054
|
-
chartStackedPeriods: "並び",
|
|
1055
|
-
chartStackedRateId: "文字列",
|
|
1056
|
-
chartStackedLegendPosition: "文字列",
|
|
1057
|
-
chartGaugeValue: "object",
|
|
1058
|
-
chartWaffleLegendPosition: "文字列",
|
|
1059
|
-
funnelForm: "文字列",
|
|
1060
|
-
funnelRate: "文字列",
|
|
1061
|
-
quadrantPointLabelSide: "文字列",
|
|
1062
|
-
treeNodeForm: "文字列",
|
|
1063
|
-
treeSize: "object",
|
|
1064
|
-
treeEdgeTone: "文字列",
|
|
1065
|
-
treeEdgeHead: "文字列",
|
|
1066
|
-
mindForm: "文字列",
|
|
1067
|
-
mindSize: "object",
|
|
1068
|
-
edgeEndGap: "数",
|
|
1069
|
-
journeyForm: "文字列",
|
|
1070
|
-
journeyLineForm: "文字列",
|
|
1071
|
-
journeyLabels: "object",
|
|
1072
|
-
journeySize: "object",
|
|
1073
|
-
sequenceActorForm: "非空の文字列",
|
|
1074
|
-
sequenceLabelForm: "非空の文字列",
|
|
1075
|
-
sequenceReturnHead: "非空の文字列",
|
|
1076
|
-
sequenceKindLabels: "object",
|
|
1077
|
-
sequenceRowGap: "数",
|
|
1078
|
-
sequenceGutter: "数",
|
|
1079
|
-
sequenceBandWidth: "数",
|
|
1080
|
-
sequenceSelfLoop: "object",
|
|
1081
|
-
figureCard: "object",
|
|
1082
|
-
figureSize: "object",
|
|
1083
|
-
terminalDecoration: "object",
|
|
1084
|
-
cornerForm: "札の角",
|
|
1085
|
-
figureKindLabel: "object",
|
|
1086
|
-
figureInset: "object",
|
|
1087
|
-
chartBarSize: "object",
|
|
1088
|
-
chartBarShape: "非空の文字列",
|
|
1089
|
-
chartLineSize: "object",
|
|
1090
|
-
chartPieSize: "object",
|
|
1091
|
-
chartWaffleSize: "object",
|
|
1092
|
-
chartSlopeLabelTemplate: "object",
|
|
1093
|
-
chartSlopeSize: "object",
|
|
1094
|
-
chartStackedLayout: "object",
|
|
1095
|
-
chartGaugeSize: "object",
|
|
1096
|
-
ganttSize: "object",
|
|
1097
|
-
quadrantLabelPosition: "四象限の区画名位置",
|
|
1098
|
-
quadrantSize: "object",
|
|
1099
|
-
eyebrow: "文字列",
|
|
1100
|
-
axes: "object",
|
|
1101
|
-
regions: "object",
|
|
1102
|
-
actors: "必須の非空の並び",
|
|
1103
|
-
flow: "必須の並び",
|
|
1104
|
-
states: "object",
|
|
1105
|
-
values: "object",
|
|
1106
|
-
animation: "並び",
|
|
1107
|
-
viewport: "object",
|
|
1108
|
-
lanes: "object",
|
|
1109
|
-
groups: "object",
|
|
1110
|
-
// 値を見せる部品 (#1374)。 中身は下の検査が種類ごとに見る
|
|
1111
|
-
readouts: "並び",
|
|
1112
|
-
// 読む人が動かすつまみ (#1389)。 部品と同じく中身は下の検査が種類ごとに見る
|
|
1113
|
-
inputs: "並び",
|
|
1114
|
-
// つまみの値から決まる値 (#1391)。 中身は下の検査が式ごとに見る
|
|
1115
|
-
formulas: "object",
|
|
1116
|
-
// 押下と巻き上げ (#1393)。 中身は下の検査が 1 件ずつ見る
|
|
1117
|
-
events: "並び",
|
|
1118
|
-
scrolls: "object",
|
|
1119
|
-
// 順序図で面が動いている間の帯 (#1466)
|
|
1120
|
-
bands: "並び",
|
|
1121
|
-
// 矢印をいつ出すか (#1470)
|
|
1122
|
-
reveal: "非空の文字列",
|
|
1123
|
-
// 箱に触れると関係する線だけを光らせるか (#1757)
|
|
1124
|
-
relations: "非空の文字列",
|
|
1125
|
-
// 図の並ぶ向き (#1494)
|
|
1126
|
-
direction: "非空の文字列",
|
|
1127
|
-
// 箱を並べ替える軸 (#2655)
|
|
1128
|
-
order: "非空の文字列",
|
|
1129
|
-
// 図種ごとの形 (#2657 / #2797)
|
|
1130
|
-
shape: "図の形",
|
|
1131
|
-
// 図の意匠とその別名 (#1553 / #2790)
|
|
1132
|
-
theme: "意匠",
|
|
1133
|
-
palette: "意匠",
|
|
1134
|
-
legend: "並び",
|
|
1135
|
-
legendFontSize: "数",
|
|
1136
|
-
legendOffset: "object",
|
|
1137
|
-
stageHeader: "object",
|
|
1138
|
-
stageLayout: "object",
|
|
1139
|
-
},
|
|
1140
|
-
actor: {
|
|
1141
|
-
name: "必須の非空文字列",
|
|
1142
|
-
kind: "非空の文字列",
|
|
1143
|
-
subtitle: "文字列",
|
|
1144
|
-
kindForm: "非空の文字列",
|
|
1145
|
-
icon: "非空の文字列",
|
|
1146
|
-
emptyRowsNote: "文字列",
|
|
1147
|
-
titleAlign: "非空の文字列",
|
|
1148
|
-
rowRules: "真偽",
|
|
1149
|
-
rowStripe: "真偽",
|
|
1150
|
-
rowLayout: "object",
|
|
1151
|
-
rowDividerForm: "非空の文字列",
|
|
1152
|
-
rowWell: "object",
|
|
1153
|
-
rowMarkForm: "非空の文字列",
|
|
1154
|
-
nestIn: "非空の文字列",
|
|
1155
|
-
figureCard: "object",
|
|
1156
|
-
stationNamePosition: "非空の文字列",
|
|
1157
|
-
subtitlePlacement: "非空の文字列",
|
|
1158
|
-
titleFontSize: "数",
|
|
1159
|
-
markGap: "数",
|
|
1160
|
-
eyebrow: "文字列",
|
|
1161
|
-
value: "文字列",
|
|
1162
|
-
previous: "文字列",
|
|
1163
|
-
rows: "文字列の並び",
|
|
1164
|
-
stage: "非空の文字列",
|
|
1165
|
-
lane: "文字列",
|
|
1166
|
-
stack: "数",
|
|
1167
|
-
initial: "真偽",
|
|
1168
|
-
final: "真偽",
|
|
1169
|
-
tone: "色",
|
|
1170
|
-
color: "色か色番号",
|
|
1171
|
-
owner: "文字列",
|
|
1172
|
-
end: "文字列",
|
|
1173
|
-
startLabel: "文字列",
|
|
1174
|
-
milestone: "真偽",
|
|
1175
|
-
emphasis: "主役",
|
|
1176
|
-
at: "並び",
|
|
1177
|
-
点の位置: "並び",
|
|
1178
|
-
touchpoint: "文字列",
|
|
1179
|
-
opportunity: "文字列",
|
|
1180
|
-
opportunityPosition: "文字列",
|
|
1181
|
-
posX: "数",
|
|
1182
|
-
posY: "数",
|
|
1183
|
-
posW: "数",
|
|
1184
|
-
posH: "数",
|
|
1185
|
-
scale: "数",
|
|
1186
|
-
state: "object",
|
|
1187
|
-
pos: "object",
|
|
1188
|
-
// 箱の中に描く図形 (#1374)。 中身は下の検査が種類ごとに見る
|
|
1189
|
-
shape: "object",
|
|
1190
|
-
// その箱を出すかどうかの条件 (#1381)
|
|
1191
|
-
visibleIf: "文字列",
|
|
1192
|
-
// 箱に出す題 (#1381)
|
|
1193
|
-
title: "文字列",
|
|
1194
|
-
// 値に追随する 5 欄 (#1392)。 大きさは文字列だけ、濃さとずらしは数も受ける
|
|
1195
|
-
wBind: "非空の文字列",
|
|
1196
|
-
hBind: "非空の文字列",
|
|
1197
|
-
opacity: "数か文字列",
|
|
1198
|
-
renderOffsetX: "数か文字列",
|
|
1199
|
-
renderOffsetY: "数か文字列",
|
|
1200
|
-
// 行頭の印 (#1466)
|
|
1201
|
-
marks: "並び",
|
|
1202
|
-
// 相対で置く指定 (#2039)。 中身の形は `validateRelativePos` が見る
|
|
1203
|
-
posRel: "object",
|
|
1204
|
-
// 見本が持つ段を使うか (#2125)
|
|
1205
|
-
phase: "真偽",
|
|
1206
|
-
},
|
|
1207
|
-
step: {
|
|
1208
|
-
from: "必須の文字列",
|
|
1209
|
-
to: "必須の文字列",
|
|
1210
|
-
label: "必須の文字列",
|
|
1211
|
-
sub: "文字列",
|
|
1212
|
-
// 矢印がどの辺から出るか (#1385)
|
|
1213
|
-
side: "辺",
|
|
1214
|
-
fromSide: "辺",
|
|
1215
|
-
toSide: "辺",
|
|
1216
|
-
head: "端の形",
|
|
1217
|
-
// 端の印の残り 3 欄と、関係の語 / 言づての種類 (#1466)
|
|
1218
|
-
tailHead: "端の形",
|
|
1219
|
-
headFill: "非空の文字列",
|
|
1220
|
-
tailHeadFill: "非空の文字列",
|
|
1221
|
-
// UML 関係の出どころ側の多重度 (#1771 / #2783)
|
|
1222
|
-
tailSub: "文字列",
|
|
1223
|
-
relation: "非空の文字列",
|
|
1224
|
-
// 辺の役目と名前の下地 (cdl#618)
|
|
1225
|
-
role: "非空の文字列",
|
|
1226
|
-
labelPlate: "真偽",
|
|
1227
|
-
kind: "非空の文字列",
|
|
1228
|
-
tone: "色",
|
|
1229
|
-
style: "線種",
|
|
1230
|
-
guard: "文字列",
|
|
1231
|
-
cardinality: "文字列",
|
|
1232
|
-
// 値に追随する 3 欄 (#1396)。 描画側は文字列だけを取る
|
|
1233
|
-
widthBind: "非空の文字列",
|
|
1234
|
-
strokeBind: "非空の文字列",
|
|
1235
|
-
dashOffsetBind: "非空の文字列",
|
|
1236
|
-
labelOffsetX: "数",
|
|
1237
|
-
labelOffsetY: "数",
|
|
1238
|
-
posX1: "数",
|
|
1239
|
-
posY1: "数",
|
|
1240
|
-
posX2: "数",
|
|
1241
|
-
posY2: "数",
|
|
1242
|
-
waypoints: "並び",
|
|
1243
|
-
labelCollision: "線札の衝突",
|
|
1244
|
-
labelSize: "object",
|
|
1245
|
-
overlay: "真偽",
|
|
1246
|
-
pos: "object",
|
|
1247
|
-
// 部品の端で繋ぐ要素の id (#1979)。 空の id は要素を指せないので受けない
|
|
1248
|
-
fromPartNode: "非空の文字列",
|
|
1249
|
-
toPartNode: "非空の文字列",
|
|
1250
|
-
},
|
|
1251
|
-
phase: {
|
|
1252
|
-
step: "必須の非空文字列",
|
|
1253
|
-
duration: "数",
|
|
1254
|
-
focus: "文字列の並び",
|
|
1255
|
-
// 説明は正の語と別名のどちらでも同じ型。 表の欄は `ACCEPTED_KEYS.phase` と揃える (#2621)
|
|
1256
|
-
body: "文字列",
|
|
1257
|
-
description: "文字列",
|
|
1258
|
-
badge: "文字列",
|
|
1259
|
-
tween: "object",
|
|
1260
|
-
set: "object",
|
|
1261
|
-
draw: "描くもの",
|
|
1262
|
-
drawRatio: "数",
|
|
1263
|
-
},
|
|
1264
|
-
viewport: {
|
|
1265
|
-
width: "数",
|
|
1266
|
-
height: "数",
|
|
1267
|
-
contentOffsetX: "数",
|
|
1268
|
-
scale: "数",
|
|
1269
|
-
laneWidth: "数",
|
|
1270
|
-
gap: "数",
|
|
1271
|
-
laneGap: "数",
|
|
1272
|
-
nodeGap: "数",
|
|
1273
|
-
labelMargin: "数",
|
|
1274
|
-
},
|
|
1275
|
-
lane: {
|
|
1276
|
-
x: "数",
|
|
1277
|
-
width: "数",
|
|
1278
|
-
label: "文字列",
|
|
1279
|
-
subtitle: "文字列",
|
|
1280
|
-
contain: "真偽",
|
|
1281
|
-
lifeline: "真偽",
|
|
1282
|
-
pos: "object",
|
|
1283
|
-
},
|
|
1284
|
-
group: { label: "文字列", lanes: "文字列の並び" },
|
|
1285
|
-
axes: { x: "object", y: "object" },
|
|
1286
|
-
axesX: { left: "文字列", right: "文字列", label: "文字列", direction: "真偽" },
|
|
1287
|
-
axesY: { bottom: "文字列", top: "文字列", label: "文字列", direction: "真偽" },
|
|
1288
|
-
regions: { 左上: "文字列", 右上: "文字列", 左下: "文字列", 右下: "文字列" },
|
|
1289
|
-
// 位置は書けば x と y の両方が要る。 片方だけでは寄せ幅が決まらない
|
|
1290
|
-
layoutPos: { x: "必須の数", y: "必須の数" },
|
|
1291
|
-
// 相対で置く指定 (#2039)。 相手と向きは必ず要り、間隔は書かなければ既定で置く
|
|
1292
|
-
posRel: { anchor: "必須の非空文字列", dir: "必須の向き", gap: "数" },
|
|
1293
|
-
} as const satisfies {
|
|
1294
|
-
[層 in 階層]: { [欄 in (typeof ACCEPTED_KEYS)[層][number]]: 欄の型 };
|
|
1295
|
-
};
|
|
1296
|
-
|
|
1297
|
-
/**
|
|
1298
|
-
* 色番号の形 (#1304)。 公開 schema の `pattern` と同じ形を実装側でも 1 箇所に持つ。
|
|
1299
|
-
*
|
|
1300
|
-
* 受けるのは 3 / 4 / 6 / 8 桁 (`#rgb` / `#rgba` / `#rrggbb` / `#rrggbbaa`)。 5 桁や 7 桁は
|
|
1301
|
-
* CSS の色として成立しないため通さない。
|
|
1302
|
-
*/
|
|
1303
|
-
const 色番号の形 = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;
|
|
1304
|
-
|
|
1305
|
-
/**
|
|
1306
|
-
* JSON で色名として受ける値か (#1304、 review Round 2 の指摘)。
|
|
1307
|
-
*
|
|
1308
|
-
* **記法の `resolveTone` は通さない**。 あちらは前後の空白と引用符を落として小文字に寄せるが、
|
|
1309
|
-
* それは記法の本文から語を切り出すために要る処理で、JSON には要らない。 JSON の値は既に
|
|
1310
|
-
* 切り出された文字列なので、`" success "` や `"\"success\""` は書き手の意図ではなく
|
|
1311
|
-
* 記法の癖が漏れた形になる。
|
|
1312
|
-
*
|
|
1313
|
-
* 公開 schema は色名を `enum` で宣言する。 `enum` は完全一致なので、**parser 側も完全一致に
|
|
1314
|
-
* 揃える**。 揃えないと「schema が拒む値を parser が受ける」 ずれが残り、本 file が閉じようと
|
|
1315
|
-
* している宣言と実装の食い違いを別の形で作ることになる。
|
|
1316
|
-
*
|
|
1317
|
-
* 揃え方は 2 通りあった。 schema を parser に合わせて広げる案は、大文字小文字と空白と引用符を
|
|
1318
|
-
* `pattern` で書くことになり、**色名の一覧が `enum` と `pattern` の 2 か所に写る**。 色が増えた
|
|
1319
|
-
* 時に片方だけ直る形を作るため採らなかった。 同じ理由で、この判定は `tone` (箱と矢印) と
|
|
1320
|
-
* `color` の 3 欄すべてが共有する。
|
|
1321
|
-
*/
|
|
1322
|
-
function JSONの色名か(v: string): boolean {
|
|
1323
|
-
return 書ける色名().includes(v);
|
|
1324
|
-
}
|
|
1325
|
-
|
|
1326
|
-
/**
|
|
1327
|
-
* 見本 (parts) にしか効かない欄 (#1294)。
|
|
1328
|
-
*
|
|
1329
|
-
* 普通の箱に書くと `jsonToDoc` が落とすため、書いても何も起きない。 #1294 で誤りにした。
|
|
1330
|
-
*
|
|
1331
|
-
* `見本に効かない欄` (#1308) と対になる。 2 つの表で「どちらの箱にしか効かないか」 を
|
|
1332
|
-
* 両方向から宣言する = 片方だけ増えると鏡の関係が崩れる。
|
|
1333
|
-
*/
|
|
1334
|
-
export const 見本にしか効かない欄 = [
|
|
1335
|
-
"state",
|
|
1336
|
-
"scale",
|
|
1337
|
-
// 見本が持つ段を使うか (#2125)。 普通の箱は自分の段を持たないので効かない
|
|
1338
|
-
"phase",
|
|
1339
|
-
] as const satisfies readonly (typeof ACCEPTED_KEYS.actor)[number][];
|
|
1340
|
-
|
|
1341
|
-
/**
|
|
1342
|
-
* 普通の箱にしか効かない欄 (#1308)。
|
|
1343
|
-
*
|
|
1344
|
-
* 見本 (parts) の箱に書くと `jsonToDoc` が丸ごと落とす。 検査を通ったうえで値が消えるため、
|
|
1345
|
-
* 書いた人には「書いたのに図が変わらない」 としか見えない (実測 = 5 欄すべてが消えていた)。
|
|
1346
|
-
*
|
|
1347
|
-
* 記法では同じ本文が状態の上書きに入る。 特別扱いしているのではなく、見本の中括弧に書いた
|
|
1348
|
-
* 名前を **すべて** 状態の上書きとして読むため、結果として届いている。 JSON は欄ごとに型を
|
|
1349
|
-
* 宣言する形なので同じ設計を持ち込めない。
|
|
1350
|
-
*
|
|
1351
|
-
* したがって **誤りとして返す**。 #1294 が決めた向き (見本にしか効かない `state` / `scale` を
|
|
1352
|
-
* 普通の箱に書いたら誤り) の鏡になる。 効く書き方は `state` が既に持つため、誤りにしても
|
|
1353
|
-
* 利用者の手段は失われない。
|
|
1354
|
-
*
|
|
1355
|
-
* **この表は検査と組み立ての両方が見る**。 落とす欄と誤りにする欄が別々に並ぶと、片方だけ
|
|
1356
|
-
* 増えた時に「検査は通すが組み立てが捨てる」 状態が戻る。
|
|
1357
|
-
*/
|
|
1358
|
-
export const 見本に効かない欄 = [
|
|
1359
|
-
"tone",
|
|
1360
|
-
"kindForm",
|
|
1361
|
-
"icon",
|
|
1362
|
-
"emptyRowsNote",
|
|
1363
|
-
"titleAlign",
|
|
1364
|
-
"rowRules",
|
|
1365
|
-
"rowStripe",
|
|
1366
|
-
"rowLayout",
|
|
1367
|
-
"rowDividerForm",
|
|
1368
|
-
"rowWell",
|
|
1369
|
-
"rowMarkForm",
|
|
1370
|
-
"nestIn",
|
|
1371
|
-
"figureCard",
|
|
1372
|
-
"at",
|
|
1373
|
-
"owner",
|
|
1374
|
-
"end",
|
|
1375
|
-
"startLabel",
|
|
1376
|
-
"milestone",
|
|
1377
|
-
"emphasis",
|
|
1378
|
-
"touchpoint",
|
|
1379
|
-
"opportunity",
|
|
1380
|
-
"opportunityPosition",
|
|
1381
|
-
"stage",
|
|
1382
|
-
// 箱の中に描く図形 (#1374)。 見本は自分の形を持つため、外から図形を差し替えられない
|
|
1383
|
-
"shape",
|
|
1384
|
-
// その箱を出すかどうかの条件 (#1381)。 見本は自分の出方を持つ
|
|
1385
|
-
"visibleIf",
|
|
1386
|
-
// 箱に出す題 (#1381)。 見本は自分の題を持つ
|
|
1387
|
-
"title",
|
|
1388
|
-
// 値に追随する 5 欄 (#1392)。 見本は自分の大きさと出方を持つ
|
|
1389
|
-
"wBind",
|
|
1390
|
-
"hBind",
|
|
1391
|
-
"opacity",
|
|
1392
|
-
"renderOffsetX",
|
|
1393
|
-
"renderOffsetY",
|
|
1394
|
-
] as const satisfies readonly (typeof ACCEPTED_KEYS.actor)[number][];
|
|
1395
|
-
|
|
1396
|
-
/**
|
|
1397
|
-
* 表に沿って 1 つの欄の値を見る (#1304)。
|
|
1398
|
-
*
|
|
1399
|
-
* `名前` は知らせの文に出す欄の呼び名 (`actor.tone` / `viewport.width`)。 `path` は直す場所を
|
|
1400
|
-
* 指す JSON pointer 風の文字列で、この 2 つは役割が違う (前者は「何の欄か」、後者は「どこか」)。
|
|
1401
|
-
*/
|
|
1402
|
-
function 値を検査(
|
|
1403
|
-
v: unknown,
|
|
1404
|
-
型: 欄の型,
|
|
1405
|
-
path: string,
|
|
1406
|
-
名前: string,
|
|
1407
|
-
errors: JsonDslError[],
|
|
1408
|
-
): void {
|
|
1409
|
-
const 型違い = (期待: string, 補足?: string): void => {
|
|
1410
|
-
errors.push({
|
|
1411
|
-
path,
|
|
1412
|
-
message: `${名前} must be ${期待}`,
|
|
1413
|
-
hint: 補足 ?? `got ${v === null ? "null" : Array.isArray(v) ? "array" : typeof v}`,
|
|
1414
|
-
});
|
|
1415
|
-
};
|
|
1416
|
-
switch (型) {
|
|
1417
|
-
case "必須の非空文字列":
|
|
1418
|
-
if (typeof v !== "string" || v.length === 0) 型違い("a non-empty string");
|
|
1419
|
-
return;
|
|
1420
|
-
case "必須の文字列":
|
|
1421
|
-
if (typeof v !== "string") 型違い("a string");
|
|
1422
|
-
return;
|
|
1423
|
-
case "非空の文字列":
|
|
1424
|
-
if (v === undefined) return;
|
|
1425
|
-
if (typeof v !== "string" || v.trim().length === 0) 型違い("a non-empty string if present");
|
|
1426
|
-
return;
|
|
1427
|
-
case "文字列":
|
|
1428
|
-
if (v === undefined) return;
|
|
1429
|
-
if (typeof v !== "string") 型違い("a string if present");
|
|
1430
|
-
return;
|
|
1431
|
-
case "文字列の並び":
|
|
1432
|
-
if (v === undefined) return;
|
|
1433
|
-
if (!Array.isArray(v)) {
|
|
1434
|
-
型違い("an array of strings if present");
|
|
1435
|
-
return;
|
|
1436
|
-
}
|
|
1437
|
-
// 要素の場所まで指す。 並び全体を指すと、どれを直せばよいか読めない
|
|
1438
|
-
v.forEach((要素, i) => {
|
|
1439
|
-
if (typeof 要素 !== "string") {
|
|
1440
|
-
errors.push({
|
|
1441
|
-
path: `${path}[${i}]`,
|
|
1442
|
-
message: `${名前}[${i}] must be a string`,
|
|
1443
|
-
hint: `got ${要素 === null ? "null" : typeof 要素}`,
|
|
1444
|
-
});
|
|
1445
|
-
}
|
|
1446
|
-
});
|
|
1447
|
-
return;
|
|
1448
|
-
case "数":
|
|
1449
|
-
if (v === undefined) return;
|
|
1450
|
-
if (typeof v !== "number" || !Number.isFinite(v)) {
|
|
1451
|
-
型違い(
|
|
1452
|
-
"a finite number if present",
|
|
1453
|
-
typeof v === "number" ? `got ${String(v)}` : undefined,
|
|
1454
|
-
);
|
|
1455
|
-
}
|
|
1456
|
-
return;
|
|
1457
|
-
case "必須の数":
|
|
1458
|
-
if (typeof v !== "number" || !Number.isFinite(v)) {
|
|
1459
|
-
型違い("a finite number", typeof v === "number" ? `got ${String(v)}` : undefined);
|
|
1460
|
-
}
|
|
1461
|
-
return;
|
|
1462
|
-
case "数か文字列":
|
|
1463
|
-
if (v === undefined) return;
|
|
1464
|
-
// 空文字は受けない。 描画側は 0 として読むため、書き忘れが「箱が消える」 形で出る
|
|
1465
|
-
if (typeof v === "string") {
|
|
1466
|
-
if (v.trim().length === 0) 型違い("a non-empty string if present");
|
|
1467
|
-
return;
|
|
1468
|
-
}
|
|
1469
|
-
if (typeof v !== "number" || !Number.isFinite(v)) {
|
|
1470
|
-
型違い(
|
|
1471
|
-
"a finite number or a non-empty string if present",
|
|
1472
|
-
typeof v === "number" ? `got ${String(v)}` : undefined,
|
|
1473
|
-
);
|
|
1474
|
-
}
|
|
1475
|
-
return;
|
|
1476
|
-
case "真偽":
|
|
1477
|
-
if (v === undefined) return;
|
|
1478
|
-
if (typeof v !== "boolean") 型違い("true or false if present");
|
|
1479
|
-
return;
|
|
1480
|
-
case "ガントの帯の終わり":
|
|
1481
|
-
if (v === undefined) return;
|
|
1482
|
-
if (v !== "position") 型違い('"position" if present');
|
|
1483
|
-
return;
|
|
1484
|
-
case "ガントの帯の厚み":
|
|
1485
|
-
if (v === undefined) return;
|
|
1486
|
-
if (v !== "thin") 型違い('"thin" if present');
|
|
1487
|
-
return;
|
|
1488
|
-
case "札の角":
|
|
1489
|
-
if (v === undefined) return;
|
|
1490
|
-
if (!(CORNER_FORM_VALUES as readonly unknown[]).includes(v)) {
|
|
1491
|
-
型違い(`one of: ${CORNER_FORM_VALUES.join(", ")}`);
|
|
1492
|
-
}
|
|
1493
|
-
return;
|
|
1494
|
-
case "四象限の区画名位置":
|
|
1495
|
-
if (v === undefined) return;
|
|
1496
|
-
if (!(QUADRANT_LABEL_POSITION_VALUES as readonly unknown[]).includes(v)) {
|
|
1497
|
-
型違い(`one of: ${QUADRANT_LABEL_POSITION_VALUES.join(", ")}`);
|
|
1498
|
-
}
|
|
1499
|
-
return;
|
|
1500
|
-
case "線札の衝突":
|
|
1501
|
-
if (v === undefined) return;
|
|
1502
|
-
if (!(LABEL_COLLISION_VALUES as readonly unknown[]).includes(v)) {
|
|
1503
|
-
型違い(`one of: ${LABEL_COLLISION_VALUES.join(", ")}`);
|
|
1504
|
-
}
|
|
1505
|
-
return;
|
|
1506
|
-
case "主役":
|
|
1507
|
-
if (v === undefined) return;
|
|
1508
|
-
if (v !== "primary") 型違い('"primary" if present');
|
|
1509
|
-
return;
|
|
1510
|
-
case "色":
|
|
1511
|
-
if (v === undefined) return;
|
|
1512
|
-
if (typeof v !== "string") {
|
|
1513
|
-
型違い("a color name if present");
|
|
1514
|
-
return;
|
|
1515
|
-
}
|
|
1516
|
-
// 別名 (`成功` / `neutral`) も受ける。 一覧は engine から取る
|
|
1517
|
-
if (!JSONの色名か(v)) {
|
|
1518
|
-
errors.push({
|
|
1519
|
-
path,
|
|
1520
|
-
message: `${名前} must be a known color name`,
|
|
1521
|
-
hint: `使える値 = ${書ける色名().join(", ")} (got ${JSON.stringify(v)})`,
|
|
1522
|
-
});
|
|
1523
|
-
}
|
|
1524
|
-
return;
|
|
1525
|
-
case "線種":
|
|
1526
|
-
if (v === undefined) return;
|
|
1527
|
-
if (typeof v !== "string" || !STYLE_VALID.has(v)) {
|
|
1528
|
-
errors.push({
|
|
1529
|
-
path,
|
|
1530
|
-
message: `${名前} must be one of: ${[...STYLE_VALID].join(", ")}`,
|
|
1531
|
-
hint: typeof v === "string" ? `got "${v}"` : `got ${typeof v}`,
|
|
1532
|
-
});
|
|
1533
|
-
}
|
|
1534
|
-
return;
|
|
1535
|
-
case "辺":
|
|
1536
|
-
if (v === undefined) return;
|
|
1537
|
-
if (typeof v !== "string" || !(EDGE_SIDE_VALUES as readonly string[]).includes(v)) {
|
|
1538
|
-
errors.push({
|
|
1539
|
-
path,
|
|
1540
|
-
message: `${名前} must be one of: ${EDGE_SIDE_VALUES.join(", ")}`,
|
|
1541
|
-
hint: typeof v === "string" ? `got "${v}"` : `got ${typeof v}`,
|
|
1542
|
-
});
|
|
1543
|
-
}
|
|
1544
|
-
return;
|
|
1545
|
-
case "必須の向き":
|
|
1546
|
-
// 受ける語は記法と同じ一覧を見る (`RELATIVE_DIRECTIONS`)。 写すと向きが増えた時に
|
|
1547
|
-
// 片方だけ古くなる
|
|
1548
|
-
if (typeof v !== "string" || !(RELATIVE_DIRECTIONS as readonly string[]).includes(v)) {
|
|
1549
|
-
errors.push({
|
|
1550
|
-
path,
|
|
1551
|
-
message: `${名前} must be one of: ${RELATIVE_DIRECTIONS.join(", ")}`,
|
|
1552
|
-
hint: typeof v === "string" ? `got "${v}"` : `got ${v === undefined ? "nothing" : typeof v}`,
|
|
1553
|
-
});
|
|
1554
|
-
}
|
|
1555
|
-
return;
|
|
1556
|
-
case "端の形":
|
|
1557
|
-
if (v === undefined) return;
|
|
1558
|
-
// 受ける語は記法と同じ一覧を見る (`EDGE_HEAD_VALUES`)。 写すと描画側が形を増やした時に
|
|
1559
|
-
// 片方だけ古くなる
|
|
1560
|
-
if (typeof v !== "string" || !EDGE_HEAD_VALUES.includes(v)) {
|
|
1561
|
-
errors.push({
|
|
1562
|
-
path,
|
|
1563
|
-
message: `${名前} must be one of: ${EDGE_HEAD_VALUES.join(", ")}`,
|
|
1564
|
-
hint: typeof v === "string" ? `got "${v}"` : `got ${typeof v}`,
|
|
1565
|
-
});
|
|
1566
|
-
}
|
|
1567
|
-
return;
|
|
1568
|
-
case "描くもの":
|
|
1569
|
-
if (v === undefined) return;
|
|
1570
|
-
// 受ける語は記法と同じ一覧を見る (`DRAW_WORDS`)。 写すと語が増えた時に片方だけ古くなる
|
|
1571
|
-
if (typeof v !== "string" || !DRAW_WORDS.has(v)) {
|
|
1572
|
-
errors.push({
|
|
1573
|
-
path,
|
|
1574
|
-
message: `${名前} must be one of: ${[...DRAW_WORDS].join(", ")}`,
|
|
1575
|
-
hint: typeof v === "string" ? `got "${v}"` : `got ${typeof v}`,
|
|
1576
|
-
});
|
|
1577
|
-
}
|
|
1578
|
-
return;
|
|
1579
|
-
case "意匠":
|
|
1580
|
-
if (v === undefined) return;
|
|
1581
|
-
if (typeof v !== "string" || resolveTheme(v) === null) {
|
|
1582
|
-
errors.push({
|
|
1583
|
-
path,
|
|
1584
|
-
message: `${名前} must be one of: ${THEMES.join(", ")}`,
|
|
1585
|
-
hint: `使える語 = ${Object.keys(THEME_ALIAS).join(" / ")} (got ${
|
|
1586
|
-
typeof v === "string" ? JSON.stringify(v) : typeof v
|
|
1587
|
-
})`,
|
|
1588
|
-
});
|
|
1589
|
-
}
|
|
1590
|
-
return;
|
|
1591
|
-
case "図の形":
|
|
1592
|
-
if (v === undefined) return;
|
|
1593
|
-
if (typeof v !== "string" || resolveDiagramShape(v) === null) {
|
|
1594
|
-
errors.push({
|
|
1595
|
-
path,
|
|
1596
|
-
message: `${名前} must be one of: ${Object.values(DIAGRAM_SHAPES).flat().join(", ")}`,
|
|
1597
|
-
hint: `使える語 = ${Object.values(DIAGRAM_SHAPES).flat().join(" / ")}`,
|
|
1598
|
-
});
|
|
1599
|
-
}
|
|
1600
|
-
return;
|
|
1601
|
-
case "色か色番号":
|
|
1602
|
-
if (v === undefined) return;
|
|
1603
|
-
if (typeof v !== "string") {
|
|
1604
|
-
型違い("a color name or a #hex value if present");
|
|
1605
|
-
return;
|
|
1606
|
-
}
|
|
1607
|
-
// 振り分けは記法と同じ (`splitColorValue`)。 `#` で始まれば色番号、それ以外は色の名前
|
|
1608
|
-
if (v.startsWith("#")) {
|
|
1609
|
-
if (!色番号の形.test(v)) {
|
|
1610
|
-
errors.push({
|
|
1611
|
-
path,
|
|
1612
|
-
message: `${名前} must be a #hex color`,
|
|
1613
|
-
hint: `\`#f59e0b\` の形で書く (got ${JSON.stringify(v)})`,
|
|
1614
|
-
});
|
|
1615
|
-
}
|
|
1616
|
-
return;
|
|
1617
|
-
}
|
|
1618
|
-
if (!JSONの色名か(v)) {
|
|
1619
|
-
errors.push({
|
|
1620
|
-
path,
|
|
1621
|
-
message: `${名前} must be a known color name or a #hex value`,
|
|
1622
|
-
hint: `使える値 = ${書ける色名().join(", ")} / \`#f59e0b\` (got ${JSON.stringify(v)})`,
|
|
1623
|
-
});
|
|
1624
|
-
}
|
|
1625
|
-
return;
|
|
1626
|
-
case "必須の図種": {
|
|
1627
|
-
// 外した図種は置き換え先を示す (#2947)。 「使える図種の一覧」 では書き換え先が分からない
|
|
1628
|
-
const 外した = typeof v === "string" ? 外した図種の知らせ(v) : undefined;
|
|
1629
|
-
if (外した !== undefined) {
|
|
1630
|
-
errors.push({ path, message: `${名前} "${String(v)}" was removed`, hint: 外した.hint });
|
|
1631
|
-
return;
|
|
1632
|
-
}
|
|
1633
|
-
if (
|
|
1634
|
-
typeof v !== "string" ||
|
|
1635
|
-
!(VALID_PRESETS.includes(v as PresetType) || VALID_ALIASES.includes(v))
|
|
1636
|
-
) {
|
|
1637
|
-
errors.push({
|
|
1638
|
-
path,
|
|
1639
|
-
message: `${名前} must be one of: ${[...VALID_PRESETS, ...VALID_ALIASES].join(", ")}`,
|
|
1640
|
-
hint: typeof v === "string" ? `got "${v}"` : undefined,
|
|
1641
|
-
});
|
|
1642
|
-
}
|
|
1643
|
-
return;
|
|
1644
|
-
}
|
|
1645
|
-
case "object":
|
|
1646
|
-
case "並び":
|
|
1647
|
-
case "必須の並び":
|
|
1648
|
-
case "必須の非空の並び":
|
|
1649
|
-
// 中身の形が欄ごとに違うため専用の検査が見る (`validateViewport` / `validateStates` 等)。
|
|
1650
|
-
// ここで外側の形も見ると、同じ入力に 2 つ誤りが出てどちらを直せばよいか読めなくなる
|
|
1651
|
-
return;
|
|
1652
|
-
}
|
|
1653
|
-
// 型を足して `case` を書き忘れると、その型の欄が黙って素通りする。 網羅を型検査で固定する
|
|
1654
|
-
型 satisfies never;
|
|
1655
|
-
}
|
|
1656
|
-
|
|
1657
|
-
/**
|
|
1658
|
-
* 1 つの階層の欄をまとめて見る (#1304)。
|
|
1659
|
-
*
|
|
1660
|
-
* `名前接頭` は知らせの文に出す呼び名の前半 (`actor` / `viewport` / `lanes.web`)。 空文字なら
|
|
1661
|
-
* 欄名だけを出す (最上位の `title` 等)。
|
|
1662
|
-
*/
|
|
1663
|
-
function 表で検査(
|
|
1664
|
-
o: Record<string, unknown>,
|
|
1665
|
-
層: 階層,
|
|
1666
|
-
path: string,
|
|
1667
|
-
名前接頭: string,
|
|
1668
|
-
errors: JsonDslError[],
|
|
1669
|
-
): void {
|
|
1670
|
-
// 表が `ACCEPTED_KEYS` と同じ欄を持つことは `欄の型表` の `satisfies` が固定する。 欄を
|
|
1671
|
-
// 足して型を書き忘れると型検査が落ちるため、ここでは欠落を扱わない
|
|
1672
|
-
for (const [欄, 型] of Object.entries<欄の型>(欄の型表[層])) {
|
|
1673
|
-
値を検査(o[欄], 型, `${path}.${欄}`, 名前接頭 === "" ? 欄 : `${名前接頭}.${欄}`, errors);
|
|
1674
|
-
}
|
|
1675
|
-
}
|
|
1676
|
-
|
|
1677
|
-
/**
|
|
1678
|
-
* 知らない項目を誤りとして積む (#1295)。
|
|
1679
|
-
*
|
|
1680
|
-
* 対象は plain object だけ。 形が違う入力は呼出側が別に誤りを積むため、ここでは何もしない
|
|
1681
|
-
* (同じ入力に 2 つの誤りを出すと、どちらを直せばよいか読めなくなる)。
|
|
1682
|
-
*/
|
|
1683
|
-
function checkUnknownKeys(v: unknown, 層: 階層, path: string, errors: JsonDslError[]): void {
|
|
1684
|
-
if (!v || typeof v !== "object" || Array.isArray(v)) return;
|
|
1685
|
-
const 受ける = ACCEPTED_KEYS[層] as readonly string[];
|
|
1686
|
-
for (const key of Object.keys(v)) {
|
|
1687
|
-
if (受ける.includes(key)) continue;
|
|
1688
|
-
const 候補 = 近い名前(key, 受ける);
|
|
1689
|
-
errors.push({
|
|
1690
|
-
path: `${path}.${key}`,
|
|
1691
|
-
message: `unknown key "${key}"`,
|
|
1692
|
-
hint: 候補 !== undefined ? `"${候補}" のことですか` : `使える項目 = ${受ける.join(", ")}`,
|
|
1693
|
-
});
|
|
1694
|
-
}
|
|
1695
|
-
}
|
|
1696
|
-
|
|
1697
|
-
/**
|
|
1698
|
-
* CAR-1693 Phase 1: DSL 表面 `pos: {x, y}` の型 check helper。 finite number pair を必須にし、
|
|
1699
|
-
* `NaN` / `Infinity` / non-number は reject する (組み立ての出口 `applyLayoutOffsets` が配置後の位置に足すため)。
|
|
1700
|
-
*/
|
|
1701
|
-
function validateLayoutPos(v: unknown, path: string, errors: JsonDslError[]): void {
|
|
1702
|
-
if (v === undefined) return;
|
|
1703
|
-
if (!v || typeof v !== "object" || Array.isArray(v)) {
|
|
1704
|
-
errors.push({ path, message: "pos must be an object with x and y numbers" });
|
|
1705
|
-
return;
|
|
1706
|
-
}
|
|
1707
|
-
const p = v as Record<string, unknown>;
|
|
1708
|
-
checkUnknownKeys(p, "layoutPos", path, errors);
|
|
1709
|
-
表で検査(p, "layoutPos", path, "pos", errors);
|
|
1710
|
-
}
|
|
1711
|
-
|
|
1712
|
-
/**
|
|
1713
|
-
* 相対で置く指定の形を見る (#2039)。
|
|
1714
|
-
*
|
|
1715
|
-
* 相手の名前が実在するかはここでは見ない。 組み立て側 (`resolveRelativeDoc`) が figure 全体を
|
|
1716
|
-
* 組んだ後に照合し、いない相手を指した時は知らせを出す。 入口ごとに別の場所で判定すると、
|
|
1717
|
-
* 本文で書いた時と JSON で書いた時で返るものが変わる。
|
|
1718
|
-
*/
|
|
1719
|
-
function validateRelativePos(v: unknown, path: string, errors: JsonDslError[]): void {
|
|
1720
|
-
if (v === undefined) return;
|
|
1721
|
-
if (!v || typeof v !== "object" || Array.isArray(v)) {
|
|
1722
|
-
errors.push({ path, message: "posRel must be an object with anchor and dir" });
|
|
1723
|
-
return;
|
|
1724
|
-
}
|
|
1725
|
-
const p = v as Record<string, unknown>;
|
|
1726
|
-
checkUnknownKeys(p, "posRel", path, errors);
|
|
1727
|
-
表で検査(p, "posRel", path, "posRel", errors);
|
|
1728
|
-
}
|
|
1729
|
-
|
|
1730
|
-
/**
|
|
1731
|
-
* 図全体として相対の指定が解けるかを見る (#2039)。
|
|
1732
|
-
*
|
|
1733
|
-
* 判定は `relative-pos.ts` の `findRelativeProblems` が持つ。 記法側も同じ判定を呼ぶので、
|
|
1734
|
-
* 同じ書き方が入口によって通ったり通らなかったりしない。
|
|
1735
|
-
*
|
|
1736
|
-
* 誤りの文だけをここで作る = 記法側は行番号を、こちらは欄の場所を添えるため。
|
|
1737
|
-
*/
|
|
1738
|
-
function validateRelativeAnchors(actors: readonly unknown[], errors: JsonDslError[]): void {
|
|
1739
|
-
const items: Array<{ name: string; rel?: RelativePos }> = [];
|
|
1740
|
-
const 場所 = new Map<string, number>();
|
|
1741
|
-
actors.forEach((a, i) => {
|
|
1742
|
-
if (!a || typeof a !== "object" || Array.isArray(a)) return;
|
|
1743
|
-
const o = a as Record<string, unknown>;
|
|
1744
|
-
if (typeof o.name !== "string") return;
|
|
1745
|
-
const rel = o.posRel;
|
|
1746
|
-
// 形の誤りは `validateRelativePos` が既に返している。 ここは形の整った指定だけを見る
|
|
1747
|
-
const 整った =
|
|
1748
|
-
rel !== null &&
|
|
1749
|
-
typeof rel === "object" &&
|
|
1750
|
-
!Array.isArray(rel) &&
|
|
1751
|
-
typeof (rel as Record<string, unknown>).anchor === "string" &&
|
|
1752
|
-
typeof (rel as Record<string, unknown>).dir === "string";
|
|
1753
|
-
items.push({ name: o.name, rel: 整った ? (rel as RelativePos) : undefined });
|
|
1754
|
-
if (!場所.has(o.name)) 場所.set(o.name, i);
|
|
1755
|
-
});
|
|
1756
|
-
|
|
1757
|
-
const named = items.map((i) => i.name);
|
|
1758
|
-
for (const p of findRelativeProblems(items)) {
|
|
1759
|
-
const i = 場所.get(p.name) ?? 0;
|
|
1760
|
-
errors.push({
|
|
1761
|
-
path: `$.actors[${i}].posRel`,
|
|
1762
|
-
...(p.kind === "self"
|
|
1763
|
-
? {
|
|
1764
|
-
message: `posRel.anchor must not be the actor itself: "${p.name}"`,
|
|
1765
|
-
hint: "別の登場人物の名前を書く",
|
|
1766
|
-
}
|
|
1767
|
-
: p.kind === "missing-anchor"
|
|
1768
|
-
? {
|
|
1769
|
-
message: `posRel.anchor must name an actor: "${p.anchor ?? ""}"`,
|
|
1770
|
-
hint:
|
|
1771
|
-
named.length > 0
|
|
1772
|
-
? `actors に書かれている名前 = ${named.join(", ")}`
|
|
1773
|
-
: "actors に基準にする登場人物を書く",
|
|
1774
|
-
}
|
|
1775
|
-
: {
|
|
1776
|
-
message: `posRel.anchor must not form a cycle: "${p.name}"`,
|
|
1777
|
-
hint: "どれか 1 つは posX / posY か自動配置にする",
|
|
1778
|
-
}),
|
|
1779
|
-
});
|
|
1780
|
-
}
|
|
1781
|
-
}
|
|
1782
|
-
|
|
1783
|
-
/**
|
|
1784
|
-
* 見本 (parts) の名前として扱う値かどうか (#1294)。
|
|
1785
|
-
*
|
|
1786
|
-
* 判定は `jsonToDoc` と同じにする。 別々に書くと、検査が「見本でない」 と見た箱を
|
|
1787
|
-
* 組み立て側が見本として扱う形ができ、見本にしか効かない項目の誤り判定がずれる。
|
|
1788
|
-
*/
|
|
1789
|
-
function 見本の名前か(kind: unknown): boolean {
|
|
1790
|
-
if (typeof kind !== "string" || kind.length === 0) return false;
|
|
1791
|
-
// 外した種類は見本の名前として扱わない (#2947)。 置き換え先の知らせだけを出す
|
|
1792
|
-
return kind !== "actor" && !外した種類.has(kind) && !VALID_KIND_SET.has(kind);
|
|
1793
|
-
}
|
|
1794
|
-
|
|
1795
|
-
/**
|
|
1796
|
-
* 図全体の大きさと間隔を見る (#1295)。
|
|
1797
|
-
*
|
|
1798
|
-
* これまで `viewport` は検査そのものが無く、中身を丸ごと写していた
|
|
1799
|
-
* (`{ ...json.viewport }`)。 型にも schema にも無い `scale` が素通しで効いていた一方、
|
|
1800
|
-
* 数でない値を書いても誰も止めなかった。
|
|
1801
|
-
*/
|
|
1802
|
-
function validateViewport(v: unknown, errors: JsonDslError[]): void {
|
|
1803
|
-
if (v === undefined) return;
|
|
1804
|
-
if (!v || typeof v !== "object" || Array.isArray(v)) {
|
|
1805
|
-
errors.push({ path: "$.viewport", message: "viewport must be a plain object" });
|
|
1806
|
-
return;
|
|
1807
|
-
}
|
|
1808
|
-
checkUnknownKeys(v, "viewport", "$.viewport", errors);
|
|
1809
|
-
表で検査(v as Record<string, unknown>, "viewport", "$.viewport", "viewport", errors);
|
|
1810
|
-
}
|
|
1811
|
-
|
|
1812
|
-
/**
|
|
1813
|
-
* 選択肢の組が受ける項目。 どれも必ず書く (#2258)。
|
|
1814
|
-
*
|
|
1815
|
-
* **判定と知らせが同じ集合を読む**。 別々に並べていた間、判定に項目を足しても
|
|
1816
|
-
* 知らせは古い一覧を教え続ける形だった。
|
|
1817
|
-
*/
|
|
1818
|
-
const 選択肢の項目 = ["value", "label"] as const;
|
|
1819
|
-
|
|
1820
|
-
/**
|
|
1821
|
-
* 選択肢の並び (`["green", { "value": "red", "label": "失敗" }]`) を要素ごとに検査する (#1920)。
|
|
1822
|
-
*
|
|
1823
|
-
* 要素は文字列か、`value` と `label` の 2 項目を持つ組。 誤りは要素の場所 (`options[1].label`) で
|
|
1824
|
-
* 返す = 並び全体の型違いとして返すと、どの組のどの項目を直せばよいかが読み手に届かない。
|
|
1825
|
-
*/
|
|
1826
|
-
function 選択肢の並びを検査する(値: unknown, path: string, errors: JsonDslError[]): void {
|
|
1827
|
-
if (!Array.isArray(値)) {
|
|
1828
|
-
errors.push({
|
|
1829
|
-
path,
|
|
1830
|
-
message: "options must be 選択肢の並び",
|
|
1831
|
-
hint: `got ${値 === null ? "null" : typeof 値}`,
|
|
1832
|
-
});
|
|
1833
|
-
return;
|
|
1834
|
-
}
|
|
1835
|
-
値.forEach((x, i) => {
|
|
1836
|
-
const here = `${path}[${i}]`;
|
|
1837
|
-
if (typeof x === "string") return;
|
|
1838
|
-
if (typeof x !== "object" || x === null || Array.isArray(x)) {
|
|
1839
|
-
errors.push({
|
|
1840
|
-
path: here,
|
|
1841
|
-
message: "option must be a string or { value, label }",
|
|
1842
|
-
hint: `got ${Array.isArray(x) ? "array" : x === null ? "null" : typeof x}`,
|
|
1843
|
-
});
|
|
1844
|
-
return;
|
|
1845
|
-
}
|
|
1846
|
-
const 組 = x as Record<string, unknown>;
|
|
1847
|
-
for (const k of Object.keys(組)) {
|
|
1848
|
-
if ((選択肢の項目 as readonly string[]).includes(k)) continue;
|
|
1849
|
-
errors.push({
|
|
1850
|
-
path: `${here}.${k}`,
|
|
1851
|
-
message: `unknown key "${k}"`,
|
|
1852
|
-
hint: `使える項目 = ${選択肢の項目.join(", ")}`,
|
|
1853
|
-
});
|
|
1854
|
-
}
|
|
1855
|
-
for (const k of 選択肢の項目) {
|
|
1856
|
-
const v = 組[k];
|
|
1857
|
-
if (v === undefined) {
|
|
1858
|
-
errors.push({
|
|
1859
|
-
path: `${here}.${k}`,
|
|
1860
|
-
message: `${k} is required`,
|
|
1861
|
-
hint: `必須の項目 = ${選択肢の項目.join(", ")}`,
|
|
1862
|
-
});
|
|
1863
|
-
} else if (typeof v !== "string") {
|
|
1864
|
-
errors.push({ path: `${here}.${k}`, message: `${k} must be 文字列`, hint: `got ${v === null ? "null" : typeof v}` });
|
|
1865
|
-
} else if (v === "") {
|
|
1866
|
-
errors.push({
|
|
1867
|
-
path: `${here}.${k}`,
|
|
1868
|
-
message: `${k} must not be empty`,
|
|
1869
|
-
hint: k === "label" ? "名前を書かないなら文字列だけを並べる" : "値を 1 文字以上書く",
|
|
1870
|
-
});
|
|
1871
|
-
}
|
|
1872
|
-
}
|
|
1873
|
-
});
|
|
1874
|
-
}
|
|
1875
|
-
|
|
1876
|
-
/**
|
|
1877
|
-
* 図形と部品の中身を、記法と **同じ表** で検査する (#1374)。
|
|
1878
|
-
*
|
|
1879
|
-
* 表を 2 つ持つと片方だけ直してずれる。 記法側 (`v05/parser.ts`) が持つ表をそのまま引く。
|
|
1880
|
-
*
|
|
1881
|
-
* 記法は値が全て文字列で届くため読み替えが要るが、JSON は型のまま届く。 ここでは
|
|
1882
|
-
* 「知らない種類」 「知らない欄」 「足りない必須の欄」 「欄の型違い」 の 4 つを見る。
|
|
1883
|
-
*/
|
|
1884
|
-
function 表で中身を検査する(
|
|
1885
|
-
o: Record<string, unknown>,
|
|
1886
|
-
表: Record<string, 図形の定義>,
|
|
1887
|
-
path: string,
|
|
1888
|
-
何: string,
|
|
1889
|
-
errors: JsonDslError[],
|
|
1890
|
-
): void {
|
|
1891
|
-
// JSON は公開 schema の enum と同じ正規名だけを受ける。 ここだけ小文字化すると、
|
|
1892
|
-
// schema が拒む種類を validator が通した上、種類別の追加検査も回避できてしまう。
|
|
1893
|
-
const kind = typeof o.kind === "string" ? o.kind : "";
|
|
1894
|
-
// 通常の object を表に使うため、継承した名前を own kind として扱わない。
|
|
1895
|
-
const 定義 = Object.hasOwn(表, kind) ? 表[kind] : undefined;
|
|
1896
|
-
if (定義 === undefined) {
|
|
1897
|
-
errors.push({
|
|
1898
|
-
path: `${path}.kind`,
|
|
1899
|
-
// **文字列でない値をそのまま文にしない**。 object を混ぜると `[object Object]` になり、
|
|
1900
|
-
// 何を書いたのかが読み手に届かない
|
|
1901
|
-
message: `unknown ${何} kind ${typeof o.kind === "string" ? `"${o.kind}"` : JSON.stringify(o.kind ?? null)}`,
|
|
1902
|
-
hint: `使える種類 = ${Object.keys(表).join(", ")}`,
|
|
1903
|
-
});
|
|
1904
|
-
return;
|
|
1905
|
-
}
|
|
1906
|
-
for (const [欄, 値] of Object.entries(o)) {
|
|
1907
|
-
if (欄 === "kind" || 欄 === "id") continue;
|
|
1908
|
-
const 形 = 定義.欄[欄];
|
|
1909
|
-
if (形 === undefined) {
|
|
1910
|
-
errors.push({
|
|
1911
|
-
path: `${path}.${欄}`,
|
|
1912
|
-
message: `unknown key "${欄}"`,
|
|
1913
|
-
hint: `使える項目 = ${Object.keys(定義.欄).join(", ")}`,
|
|
1914
|
-
});
|
|
1915
|
-
continue;
|
|
1916
|
-
}
|
|
1917
|
-
if (形 === "選択肢の並び") {
|
|
1918
|
-
選択肢の並びを検査する(値, `${path}.${欄}`, errors);
|
|
1919
|
-
continue;
|
|
1920
|
-
}
|
|
1921
|
-
const 型が合う =
|
|
1922
|
-
形 === "数"
|
|
1923
|
-
? typeof 値 === "number" && Number.isFinite(値)
|
|
1924
|
-
: 形 === "数か文字列"
|
|
1925
|
-
? (typeof 値 === "number" && Number.isFinite(値)) || typeof 値 === "string"
|
|
1926
|
-
: 形 === "文字列の並び"
|
|
1927
|
-
? Array.isArray(値) && 値.every((x) => typeof x === "string")
|
|
1928
|
-
: 形 === "数の並び"
|
|
1929
|
-
? Array.isArray(値) && 値.every((x) => typeof x === "number" && Number.isFinite(x))
|
|
1930
|
-
: 形 === "真偽"
|
|
1931
|
-
? typeof 値 === "boolean"
|
|
1932
|
-
: 形 === "組の並び"
|
|
1933
|
-
? Array.isArray(値) &&
|
|
1934
|
-
値.length > 0 &&
|
|
1935
|
-
値.every(
|
|
1936
|
-
(x) =>
|
|
1937
|
-
typeof x === "object" &&
|
|
1938
|
-
x !== null &&
|
|
1939
|
-
!Array.isArray(x) &&
|
|
1940
|
-
Object.keys(x as object).length > 0 &&
|
|
1941
|
-
Object.values(x as object).every(
|
|
1942
|
-
(y) =>
|
|
1943
|
-
typeof y === "string" || (typeof y === "number" && Number.isFinite(y)),
|
|
1944
|
-
),
|
|
1945
|
-
)
|
|
1946
|
-
: 形 === "向き"
|
|
1947
|
-
? typeof 値 === "string" && ["up", "down", "left", "right"].includes(値)
|
|
1948
|
-
: typeof 値 === "string";
|
|
1949
|
-
if (!型が合う) {
|
|
1950
|
-
errors.push({
|
|
1951
|
-
path: `${path}.${欄}`,
|
|
1952
|
-
message: `${欄} must be ${形}`,
|
|
1953
|
-
hint: `got ${Array.isArray(値) ? "array" : 値 === null ? "null" : typeof 値}`,
|
|
1954
|
-
});
|
|
1955
|
-
}
|
|
1956
|
-
}
|
|
1957
|
-
for (const 欄 of 定義.必須) {
|
|
1958
|
-
if (o[欄] === undefined) {
|
|
1959
|
-
errors.push({
|
|
1960
|
-
path: `${path}.${欄}`,
|
|
1961
|
-
message: `${欄} is required for ${何} kind "${kind}"`,
|
|
1962
|
-
hint: `必須の項目 = ${定義.必須.join(", ")}`,
|
|
1963
|
-
});
|
|
1964
|
-
}
|
|
1965
|
-
}
|
|
1966
|
-
}
|
|
1967
|
-
|
|
1968
|
-
/** 帯に書く相手の項目。 値が文字列で、他の 2 つと型が違う */
|
|
1969
|
-
const 帯の相手の項目 = ["actor"] as const;
|
|
1970
|
-
/** 帯に書く段の項目。 値が 0 以上の整数 */
|
|
1971
|
-
const 帯の段の項目 = ["from", "to"] as const;
|
|
1972
|
-
/**
|
|
1973
|
-
* 帯が受ける項目。 判定と知らせの両方がここから作る (#2258)。
|
|
1974
|
-
*
|
|
1975
|
-
* 公開 schema の `$.bands` も同じ 3 語を閉じた集合として宣言しており、
|
|
1976
|
-
* `closed-level-coverage.test.ts` が両者の一致を見る。
|
|
1977
|
-
*/
|
|
1978
|
-
export const 帯の項目: readonly string[] = [...帯の相手の項目, ...帯の段の項目];
|
|
1979
|
-
|
|
1980
|
-
/**
|
|
1981
|
-
* 順序図の帯の並びを検査する (#1466)。
|
|
1982
|
-
*
|
|
1983
|
-
* **外側の形もここで見る**。 `欄の型表` は「並び」 とだけ宣言し、中身の検査は専用の検査に
|
|
1984
|
-
* 委ねる作りなので、ここで見ないと `bands: 1` が素通りする。
|
|
1985
|
-
*
|
|
1986
|
-
* ## 知らない項目も見る (#2262)
|
|
1987
|
-
*
|
|
1988
|
-
* 書いてある 3 つの型だけを見て、**知らない項目があるかを確かめていなかった**。
|
|
1989
|
-
* 公開 schema は `$.bands` を閉じた階層として宣言しているので、
|
|
1990
|
-
* schema を信じて書いた入力がこちらでは通り、schema 検証器では落ちる状態だった。
|
|
1991
|
-
* 公開 schema が閉じた階層のうち、素通りしていたのはここだけだった。
|
|
1992
|
-
*/
|
|
1993
|
-
function validateBands(v: unknown, errors: JsonDslError[]): void {
|
|
1994
|
-
if (v === undefined) return;
|
|
1995
|
-
if (!Array.isArray(v)) {
|
|
1996
|
-
errors.push({
|
|
1997
|
-
path: "$.bands",
|
|
1998
|
-
message: "bands must be an array of band objects",
|
|
1999
|
-
hint: `got ${v === null ? "null" : typeof v}`,
|
|
2000
|
-
});
|
|
2001
|
-
return;
|
|
2002
|
-
}
|
|
2003
|
-
v.forEach((b, i) => {
|
|
2004
|
-
const path = `$.bands[${i}]`;
|
|
2005
|
-
if (typeof b !== "object" || b === null || Array.isArray(b)) {
|
|
2006
|
-
errors.push({ path, message: "band must be an object", hint: "{ actor, from, to } の形で書く" });
|
|
2007
|
-
return;
|
|
2008
|
-
}
|
|
2009
|
-
const o = b as Record<string, unknown>;
|
|
2010
|
-
for (const k of Object.keys(o)) {
|
|
2011
|
-
if (帯の項目.includes(k)) continue;
|
|
2012
|
-
errors.push({
|
|
2013
|
-
path: `${path}.${k}`,
|
|
2014
|
-
message: `unknown key "${k}"`,
|
|
2015
|
-
hint: `使える項目 = ${帯の項目.join(", ")}`,
|
|
2016
|
-
});
|
|
2017
|
-
}
|
|
2018
|
-
for (const k of 帯の相手の項目) {
|
|
2019
|
-
if (typeof o[k] !== "string" || o[k] === "") {
|
|
2020
|
-
errors.push({ path: `${path}.${k}`, message: `band.${k} must be a non-empty string` });
|
|
2021
|
-
}
|
|
2022
|
-
}
|
|
2023
|
-
for (const k of 帯の段の項目) {
|
|
2024
|
-
if (typeof o[k] !== "number" || !Number.isInteger(o[k]) || o[k] < 0) {
|
|
2025
|
-
errors.push({ path: `${path}.${k}`, message: `band.${k} must be a non-negative integer` });
|
|
2026
|
-
}
|
|
2027
|
-
}
|
|
2028
|
-
});
|
|
2029
|
-
}
|
|
2030
|
-
|
|
2031
|
-
const LEGEND_KEYS = ["mark", "text", "tone", "lineStyle", "align", "lead"] as const;
|
|
2032
|
-
const LEGEND_LINE_MARKS = ["line-triangle-hollow", "line-diamond-hollow", "line-diamond-solid"] as const;
|
|
2033
|
-
|
|
2034
|
-
/** JSON の凡例を、外側の並びから各項目の必須値まで検査する。 */
|
|
2035
|
-
function validateLegend(v: unknown, errors: JsonDslError[]): void {
|
|
2036
|
-
if (v === undefined) return;
|
|
2037
|
-
if (!Array.isArray(v)) {
|
|
2038
|
-
errors.push({ path: "$.legend", message: "legend must be an array" });
|
|
2039
|
-
return;
|
|
2040
|
-
}
|
|
2041
|
-
v.forEach((item, index) => {
|
|
2042
|
-
const path = `$.legend[${index}]`;
|
|
2043
|
-
if (!item || typeof item !== "object" || Array.isArray(item)) {
|
|
2044
|
-
errors.push({ path, message: "legend item must be an object" });
|
|
2045
|
-
return;
|
|
2046
|
-
}
|
|
2047
|
-
const o = item as Record<string, unknown>;
|
|
2048
|
-
for (const key of Object.keys(o)) {
|
|
2049
|
-
if ((LEGEND_KEYS as readonly string[]).includes(key)) continue;
|
|
2050
|
-
errors.push({
|
|
2051
|
-
path: `${path}.${key}`,
|
|
2052
|
-
message: `unknown key "${key}"`,
|
|
2053
|
-
hint: `使える項目 = ${LEGEND_KEYS.join(", ")}`,
|
|
2054
|
-
});
|
|
2055
|
-
}
|
|
2056
|
-
const mark = typeof o.mark === "string" ? resolveLegendMark(o.mark) : undefined;
|
|
2057
|
-
if (o.mark !== undefined && (typeof o.mark !== "string" || mark === null)) {
|
|
2058
|
-
errors.push({ path: `${path}.mark`, message: "legend mark is not recognized" });
|
|
2059
|
-
}
|
|
2060
|
-
if (typeof o.text !== "string" || o.text.trim().length === 0) {
|
|
2061
|
-
errors.push({ path: `${path}.text`, message: "legend text must be a non-empty string" });
|
|
2062
|
-
}
|
|
2063
|
-
if (o.mark === undefined) {
|
|
2064
|
-
if (o.tone !== undefined || o.lineStyle !== undefined) errors.push({ path, message: "legend note cannot have tone or lineStyle" });
|
|
2065
|
-
if (o.align !== undefined && !(LEGEND_NOTE_ALIGN_VALUES as readonly unknown[]).includes(o.align)) errors.push({ path: `${path}.align`, message: `align must be ${LEGEND_NOTE_ALIGN_VALUES.join(" or ")}` });
|
|
2066
|
-
if (o.lead !== undefined && typeof o.lead !== "string") errors.push({ path: `${path}.lead`, message: "lead must be a string" });
|
|
2067
|
-
} else {
|
|
2068
|
-
if (o.align !== undefined || o.lead !== undefined) errors.push({ path, message: "marked legend item cannot have align or lead" });
|
|
2069
|
-
if (o.tone !== undefined && (typeof o.tone !== "string" || resolveTone(o.tone) === undefined)) errors.push({ path: `${path}.tone`, message: "tone is not recognized" });
|
|
2070
|
-
if (o.lineStyle !== undefined) {
|
|
2071
|
-
if (!(LEGEND_LINE_STYLE_VALUES as readonly unknown[]).includes(o.lineStyle)) errors.push({ path: `${path}.lineStyle`, message: `lineStyle must be ${LEGEND_LINE_STYLE_VALUES.join(" or ")}` });
|
|
2072
|
-
if (mark === undefined || mark === null || !(LEGEND_LINE_MARKS as readonly string[]).includes(mark)) errors.push({ path: `${path}.lineStyle`, message: "lineStyle requires a line-end legend mark" });
|
|
2073
|
-
}
|
|
2074
|
-
}
|
|
2075
|
-
});
|
|
2076
|
-
}
|
|
2077
|
-
|
|
2078
|
-
const STAGE_HEADER_KEYS = ["leftPad", "topPad", "numberSize", "gap", "nameSize", "bottomPad"] as const;
|
|
2079
|
-
const REQUIRED_STAGE_HEADER_KEYS = ["topPad", "numberSize", "gap", "nameSize", "bottomPad"] as const;
|
|
2080
|
-
|
|
2081
|
-
/** JSON の段の見出し寸法を、閉じた欄・必須の正数まで検査する。 */
|
|
2082
|
-
function validateStageHeader(v: unknown, errors: JsonDslError[]): void {
|
|
2083
|
-
if (v === undefined) return;
|
|
2084
|
-
if (!v || typeof v !== "object" || Array.isArray(v)) {
|
|
2085
|
-
errors.push({ path: "$.stageHeader", message: "stageHeader must be an object" });
|
|
2086
|
-
return;
|
|
2087
|
-
}
|
|
2088
|
-
const path = "$.stageHeader";
|
|
2089
|
-
const o = v as Record<string, unknown>;
|
|
2090
|
-
for (const key of Object.keys(o)) {
|
|
2091
|
-
if ((STAGE_HEADER_KEYS as readonly string[]).includes(key)) continue;
|
|
2092
|
-
errors.push({
|
|
2093
|
-
path: `${path}.${key}`,
|
|
2094
|
-
message: `unknown key "${key}"`,
|
|
2095
|
-
hint: `使える項目 = ${STAGE_HEADER_KEYS.join(", ")}`,
|
|
2096
|
-
});
|
|
2097
|
-
}
|
|
2098
|
-
for (const key of REQUIRED_STAGE_HEADER_KEYS) {
|
|
2099
|
-
if (o[key] === undefined) {
|
|
2100
|
-
errors.push({ path: `${path}.${key}`, message: `${key} is required` });
|
|
2101
|
-
}
|
|
2102
|
-
}
|
|
2103
|
-
for (const key of STAGE_HEADER_KEYS) {
|
|
2104
|
-
const value = o[key];
|
|
2105
|
-
if (value === undefined) continue;
|
|
2106
|
-
const zeroAllowed = key === "leftPad" || key === "topPad" || key === "gap" || key === "bottomPad";
|
|
2107
|
-
if (typeof value !== "number" || !Number.isFinite(value) || (zeroAllowed ? value < 0 : value <= 0)) {
|
|
2108
|
-
errors.push({
|
|
2109
|
-
path: `${path}.${key}`,
|
|
2110
|
-
message: `${key} must be ${zeroAllowed ? "a non-negative" : "a positive"} number`,
|
|
2111
|
-
});
|
|
2112
|
-
}
|
|
2113
|
-
}
|
|
2114
|
-
}
|
|
2115
|
-
|
|
2116
|
-
/**
|
|
2117
|
-
* 値を見せる部品の並びを検査する (#1374)。
|
|
2118
|
-
*
|
|
2119
|
-
* **外側の形もここで見る**。 `欄の型表` は「並び」 とだけ宣言し、中身の検査は専用の検査に
|
|
2120
|
-
* 委ねる作りなので (`checkFieldType` の `case "並び"`)、ここで見ないと `readouts: 1` が
|
|
2121
|
-
* 素通りする。
|
|
2122
|
-
*/
|
|
2123
|
-
function validateReadouts(v: unknown, errors: JsonDslError[]): void {
|
|
2124
|
-
if (v === undefined) return;
|
|
2125
|
-
if (!Array.isArray(v)) {
|
|
2126
|
-
errors.push({
|
|
2127
|
-
path: "$.readouts",
|
|
2128
|
-
message: "readouts must be an array of readout objects",
|
|
2129
|
-
hint: `got ${v === null ? "null" : typeof v}`,
|
|
2130
|
-
});
|
|
2131
|
-
return;
|
|
2132
|
-
}
|
|
2133
|
-
v.forEach((r, i) => {
|
|
2134
|
-
const path = `$.readouts[${i}]`;
|
|
2135
|
-
if (!r || typeof r !== "object" || Array.isArray(r)) {
|
|
2136
|
-
errors.push({ path, message: "readout must be a plain object", hint: `got ${typeof r}` });
|
|
2137
|
-
return;
|
|
2138
|
-
}
|
|
2139
|
-
const o = r as Record<string, unknown>;
|
|
2140
|
-
if (typeof o.id !== "string" || o.id === "") {
|
|
2141
|
-
errors.push({ path: `${path}.id`, message: "id is required", hint: "空でない文字列で書く" });
|
|
2142
|
-
}
|
|
2143
|
-
表で中身を検査する(o, 部品の表, path, "readout", errors);
|
|
2144
|
-
|
|
2145
|
-
/*
|
|
2146
|
-
* 組の並びを取る欄の中身を、記法と同じ表で見る (#1385)。
|
|
2147
|
-
*
|
|
2148
|
-
* **種類を手で並べない**。 元は `status-dot` と `status-timeline` を直書きしていたが、
|
|
2149
|
-
* 表は描画側の型定義から生成しており、種類が増えるたびに書き足す形になる。
|
|
2150
|
-
*/
|
|
2151
|
-
const 組の定義 = 部品の組の表[String(o.kind)];
|
|
2152
|
-
for (const [欄名, 定義] of Object.entries(組の定義 ?? {})) {
|
|
2153
|
-
if (!Array.isArray(o[欄名])) continue;
|
|
2154
|
-
(o[欄名] as unknown[]).forEach((entry, j) => {
|
|
2155
|
-
const entryPath = `${path}.${欄名}[${j}]`;
|
|
2156
|
-
if (!entry || typeof entry !== "object" || Array.isArray(entry)) return;
|
|
2157
|
-
const item = entry as Record<string, unknown>;
|
|
2158
|
-
for (const key of Object.keys(item)) {
|
|
2159
|
-
if (key in 定義.欄) continue;
|
|
2160
|
-
errors.push({
|
|
2161
|
-
path: `${entryPath}.${key}`,
|
|
2162
|
-
message: `${key} is not allowed for readout kind "${String(o.kind)}"`,
|
|
2163
|
-
hint: `使える項目 = ${Object.keys(定義.欄).join(", ")}`,
|
|
2164
|
-
});
|
|
2165
|
-
}
|
|
2166
|
-
for (const key of 定義.必須) {
|
|
2167
|
-
if (item[key] !== undefined) continue;
|
|
2168
|
-
errors.push({
|
|
2169
|
-
path: `${entryPath}.${key}`,
|
|
2170
|
-
message: `${key} is required for readout kind "${String(o.kind)}"`,
|
|
2171
|
-
});
|
|
2172
|
-
}
|
|
2173
|
-
for (const [key, 形] of Object.entries(定義.欄)) {
|
|
2174
|
-
const 値 = item[key];
|
|
2175
|
-
if (値 === undefined) continue;
|
|
2176
|
-
const 合う =
|
|
2177
|
-
形 === "数" ? typeof 値 === "number" && Number.isFinite(値) : typeof 値 === "string";
|
|
2178
|
-
if (合う) continue;
|
|
2179
|
-
errors.push({
|
|
2180
|
-
path: `${entryPath}.${key}`,
|
|
2181
|
-
message: `${key} must be ${形}`,
|
|
2182
|
-
hint: `got ${Array.isArray(値) ? "array" : 値 === null ? "null" : typeof 値}`,
|
|
2183
|
-
});
|
|
2184
|
-
}
|
|
2185
|
-
});
|
|
2186
|
-
}
|
|
2187
|
-
});
|
|
2188
|
-
}
|
|
2189
|
-
|
|
2190
|
-
/**
|
|
2191
|
-
* 読む人が動かすつまみの並びを検査する (#1389)。
|
|
2192
|
-
*
|
|
2193
|
-
* **外側の形もここで見る**。 `欄の型表` は「並び」 とだけ宣言し、中身の検査は専用の検査に
|
|
2194
|
-
* 委ねる作りなので (`checkFieldType` の `case "並び"`)、ここで見ないと `inputs: 1` が
|
|
2195
|
-
* 素通りする。
|
|
2196
|
-
*
|
|
2197
|
-
* 種類ごとの欄は記法と同じ表 (`つまみの表`) で見る。 表は描画側の型定義から生成しており、
|
|
2198
|
-
* 種類が増えても書き足す場所が増えない。
|
|
2199
|
-
*/
|
|
2200
|
-
function validateInputs(v: unknown, errors: JsonDslError[]): void {
|
|
2201
|
-
if (v === undefined) return;
|
|
2202
|
-
if (!Array.isArray(v)) {
|
|
2203
|
-
errors.push({
|
|
2204
|
-
path: "$.inputs",
|
|
2205
|
-
message: "inputs must be an array of input objects",
|
|
2206
|
-
hint: `got ${v === null ? "null" : typeof v}`,
|
|
2207
|
-
});
|
|
2208
|
-
return;
|
|
2209
|
-
}
|
|
2210
|
-
v.forEach((r, i) => {
|
|
2211
|
-
const path = `$.inputs[${i}]`;
|
|
2212
|
-
if (!r || typeof r !== "object" || Array.isArray(r)) {
|
|
2213
|
-
errors.push({ path, message: "input must be a plain object", hint: `got ${typeof r}` });
|
|
2214
|
-
return;
|
|
2215
|
-
}
|
|
2216
|
-
const o = r as Record<string, unknown>;
|
|
2217
|
-
if (typeof o.id !== "string" || o.id === "") {
|
|
2218
|
-
errors.push({ path: `${path}.id`, message: "id is required", hint: "空でない文字列で書く" });
|
|
2219
|
-
}
|
|
2220
|
-
表で中身を検査する(o, つまみの表, path, "input", errors);
|
|
2221
|
-
});
|
|
2222
|
-
}
|
|
2223
|
-
|
|
2224
|
-
/**
|
|
2225
|
-
* つまみの値から決まる値を検査する (#1391)。
|
|
2226
|
-
*
|
|
2227
|
-
* **外側の形もここで見る**。 `欄の型表` は「object」 とだけ宣言するため、
|
|
2228
|
-
* ここで見ないと `formulas: { a: 1 }` が素通りする。
|
|
2229
|
-
*
|
|
2230
|
-
* 式そのものは描画側の parser に通す = 自前で書き方を決めると、通ったのに描画側が
|
|
2231
|
-
* 解けない式を受けてしまう。
|
|
2232
|
-
*/
|
|
2233
|
-
function validateFormulas(v: unknown, inputs: unknown, errors: JsonDslError[]): void {
|
|
2234
|
-
if (v === undefined) return;
|
|
2235
|
-
if (!v || typeof v !== "object" || Array.isArray(v)) {
|
|
2236
|
-
errors.push({
|
|
2237
|
-
path: "$.formulas",
|
|
2238
|
-
message: "formulas must be an object of name to expression",
|
|
2239
|
-
hint: `got ${v === null ? "null" : Array.isArray(v) ? "array" : typeof v}`,
|
|
2240
|
-
});
|
|
2241
|
-
return;
|
|
2242
|
-
}
|
|
2243
|
-
const つまみ = new Map<string, string>();
|
|
2244
|
-
if (Array.isArray(inputs)) {
|
|
2245
|
-
for (const input of inputs) {
|
|
2246
|
-
if (input && typeof input === "object" && !Array.isArray(input)) {
|
|
2247
|
-
const candidate = input as { id?: unknown; kind?: unknown };
|
|
2248
|
-
if (typeof candidate.id === "string" && typeof candidate.kind === "string") {
|
|
2249
|
-
つまみ.set(candidate.id, candidate.kind);
|
|
2250
|
-
}
|
|
2251
|
-
}
|
|
2252
|
-
}
|
|
2253
|
-
}
|
|
2254
|
-
const 先に書かれた式 = new Set<string>();
|
|
2255
|
-
for (const [名前, 値] of Object.entries(v as Record<string, unknown>)) {
|
|
2256
|
-
const path = `$.formulas.${名前}`;
|
|
2257
|
-
if (!isValueName(名前)) {
|
|
2258
|
-
errors.push({ path, ...valueNameIssue(名前) });
|
|
2259
|
-
continue;
|
|
2260
|
-
}
|
|
2261
|
-
const 検査結果 = 式の組を検査する(値, path, errors);
|
|
2262
|
-
if (!検査結果.読めた) continue;
|
|
2263
|
-
const 式 = 検査結果.値;
|
|
2264
|
-
if (typeof 式 !== "string" || 式.trim() === "") {
|
|
2265
|
-
errors.push({
|
|
2266
|
-
path,
|
|
2267
|
-
message: `${名前} must be a non-empty expression string`,
|
|
2268
|
-
hint: `got ${Array.isArray(式) ? "array" : 式 === null ? "null" : typeof 式}`,
|
|
2269
|
-
});
|
|
2270
|
-
continue;
|
|
2271
|
-
}
|
|
2272
|
-
try {
|
|
2273
|
-
const names = extractIdentifiers(parseFormula(式));
|
|
2274
|
-
if (つまみ.has(名前) || 先に書かれた式.has(名前)) {
|
|
2275
|
-
errors.push({
|
|
2276
|
-
path,
|
|
2277
|
-
message: `${名前} collides with an input or an earlier formula`,
|
|
2278
|
-
hint: "inputs と formulas では重ならない名前を使う",
|
|
2279
|
-
});
|
|
2280
|
-
continue;
|
|
2281
|
-
}
|
|
2282
|
-
let valid = true;
|
|
2283
|
-
for (const name of names) {
|
|
2284
|
-
if (先に書かれた式.has(name)) continue;
|
|
2285
|
-
const kind = つまみ.get(name);
|
|
2286
|
-
if (["slider", "number", "stepper", "timeline", "toggle"].includes(kind ?? "")) continue;
|
|
2287
|
-
valid = false;
|
|
2288
|
-
errors.push({
|
|
2289
|
-
path,
|
|
2290
|
-
message: `${名前} references an unavailable formula identifier`,
|
|
2291
|
-
hint: `${name} は数値/真偽の input にするか、この式より前の formula に書く`,
|
|
2292
|
-
});
|
|
2293
|
-
}
|
|
2294
|
-
if (valid) 先に書かれた式.add(名前);
|
|
2295
|
-
} catch (e) {
|
|
2296
|
-
errors.push({
|
|
2297
|
-
path,
|
|
2298
|
-
message: `${名前} is not a readable expression`,
|
|
2299
|
-
hint: (e as Error).message,
|
|
2300
|
-
});
|
|
2301
|
-
}
|
|
2302
|
-
}
|
|
2303
|
-
}
|
|
2304
|
-
|
|
2305
|
-
/**
|
|
2306
|
-
* 式の組を検査した結果 (#1916 / #2006)。
|
|
2307
|
-
*
|
|
2308
|
-
* **読めたかどうかを別の欄に持つ**。 値そのものに合図を混ぜると、`{ a: null }` の `null` が
|
|
2309
|
-
* 合図と区別が付かず、呼び手が「組の誤りは知らせ済み」 と読んで何も知らせずに通してしまう (実測)。
|
|
2310
|
-
*
|
|
2311
|
-
* 印 (`unique symbol`) を値に混ぜる形も採らない。 返す型が `unknown` を含むと印が飲み込まれ、
|
|
2312
|
-
* 呼び手は型で見分けられないまま等値比較に頼ることになる。
|
|
2313
|
-
*/
|
|
2314
|
-
type 式の検査結果 = { readonly 読めた: true; readonly 値: unknown } | { readonly 読めた: false };
|
|
2315
|
-
|
|
2316
|
-
/**
|
|
2317
|
-
* 式の組が受ける項目 (#2258)。 判定と知らせが同じ集合を読む。
|
|
2318
|
-
*/
|
|
2319
|
-
const 式の組の項目 = ["expression", "label"] as const;
|
|
2320
|
-
|
|
2321
|
-
/**
|
|
2322
|
-
* 式の値が組の形 (`{ expression, label }`) なら組を検査し、式の文字列を返す (#1916)。
|
|
2323
|
-
*
|
|
2324
|
-
* 文字列やそれ以外の形は **そのまま返す** = 式の文字列の検査は呼び手が文字列の形と同じ経路で行う。
|
|
2325
|
-
* 組に誤りがあれば知らせて `読めた: false` を返す。
|
|
2326
|
-
*/
|
|
2327
|
-
function 式の組を検査する(値: unknown, path: string, errors: JsonDslError[]): 式の検査結果 {
|
|
2328
|
-
if (!値 || typeof 値 !== "object" || Array.isArray(値)) return { 読めた: true, 値 };
|
|
2329
|
-
const 組 = 値 as Record<string, unknown>;
|
|
2330
|
-
let 読めた = true;
|
|
2331
|
-
for (const 鍵 of Object.keys(組)) {
|
|
2332
|
-
if ((式の組の項目 as readonly string[]).includes(鍵)) continue;
|
|
2333
|
-
読めた = false;
|
|
2334
|
-
errors.push({
|
|
2335
|
-
path: `${path}.${鍵}`,
|
|
2336
|
-
message: `unknown formula field: ${鍵}`,
|
|
2337
|
-
hint: `使える項目 = ${式の組の項目.join(", ")}`,
|
|
2338
|
-
});
|
|
2339
|
-
}
|
|
2340
|
-
if (typeof 組.expression !== "string" || 組.expression.trim() === "") {
|
|
2341
|
-
読めた = false;
|
|
2342
|
-
errors.push({
|
|
2343
|
-
path: `${path}.expression`,
|
|
2344
|
-
message: "expression must be a non-empty expression string",
|
|
2345
|
-
hint: `got ${組.expression === undefined ? "nothing" : JSON.stringify(組.expression)}`,
|
|
2346
|
-
});
|
|
2347
|
-
}
|
|
2348
|
-
if (組.label !== undefined && (typeof 組.label !== "string" || 組.label.trim() === "")) {
|
|
2349
|
-
読めた = false;
|
|
2350
|
-
errors.push({
|
|
2351
|
-
path: `${path}.label`,
|
|
2352
|
-
message: "label must be a non-empty string",
|
|
2353
|
-
hint: "名札を付けないなら label を書かない",
|
|
2354
|
-
});
|
|
2355
|
-
}
|
|
2356
|
-
return 読めた ? { 読めた: true, 値: 組.expression } : { 読めた: false };
|
|
2357
|
-
}
|
|
2358
|
-
|
|
2359
|
-
/**
|
|
2360
|
-
* 押下などの出来事で動く仕掛けを検査する (#1393)。
|
|
2361
|
-
*
|
|
2362
|
-
* 相手の指し方は 4 つあり、**ちょうど 1 つだけ書く**。 2 つ書くとどちらを指したのか
|
|
2363
|
-
* 決まらず、0 なら相手がいない。 記法側の読み取りと同じ規則にする。
|
|
2364
|
-
*/
|
|
2365
|
-
function validateEvents(v: unknown, errors: JsonDslError[]): void {
|
|
2366
|
-
if (v === undefined) return;
|
|
2367
|
-
if (!Array.isArray(v)) {
|
|
2368
|
-
errors.push({
|
|
2369
|
-
path: "$.events",
|
|
2370
|
-
message: "events must be an array of event objects",
|
|
2371
|
-
hint: `got ${v === null ? "null" : typeof v}`,
|
|
2372
|
-
});
|
|
2373
|
-
return;
|
|
2374
|
-
}
|
|
2375
|
-
// 相手の指し方は記法の側が 1 か所で決める (#2256)。 同じ 4 語をここにも並べていた間、
|
|
2376
|
-
// 1:1 を名乗りながら受ける項目が両側で別々に決まっていた。
|
|
2377
|
-
// 必ず書く 2 つも同じ定数から取る (#2258) = 知らせが判定と別に並べる形をやめる
|
|
2378
|
-
const 相手の鍵: readonly string[] = EVENT_TARGET_KEYS;
|
|
2379
|
-
const 出来事の項目: readonly string[] = [...EVENT_REQUIRED_KEYS, ...EVENT_TARGET_KEYS];
|
|
2380
|
-
v.forEach((e, i) => {
|
|
2381
|
-
const path = `$.events[${i}]`;
|
|
2382
|
-
if (!e || typeof e !== "object" || Array.isArray(e)) {
|
|
2383
|
-
errors.push({ path, message: "event must be a plain object", hint: `got ${typeof e}` });
|
|
2384
|
-
return;
|
|
2385
|
-
}
|
|
2386
|
-
const o = e as Record<string, unknown>;
|
|
2387
|
-
for (const k of Object.keys(o)) {
|
|
2388
|
-
if (出来事の項目.includes(k)) continue;
|
|
2389
|
-
errors.push({
|
|
2390
|
-
path: `${path}.${k}`,
|
|
2391
|
-
message: `unknown key "${k}"`,
|
|
2392
|
-
hint: `使える項目 = ${出来事の項目.join(", ")}`,
|
|
2393
|
-
});
|
|
2394
|
-
}
|
|
2395
|
-
if (typeof o.on !== "string" || !(EVENT_KINDS as readonly string[]).includes(o.on)) {
|
|
2396
|
-
errors.push({
|
|
2397
|
-
path: `${path}.on`,
|
|
2398
|
-
message: "on must be a known event kind",
|
|
2399
|
-
hint: `使える種類 = ${EVENT_KINDS.join(", ")}`,
|
|
2400
|
-
});
|
|
2401
|
-
}
|
|
2402
|
-
if (typeof o.handler !== "string" || o.handler.trim() === "") {
|
|
2403
|
-
errors.push({
|
|
2404
|
-
path: `${path}.handler`,
|
|
2405
|
-
message: "handler is required",
|
|
2406
|
-
hint: "空でない文字列で書く",
|
|
2407
|
-
});
|
|
2408
|
-
}
|
|
2409
|
-
const 書いた = 相手の鍵.filter((k) => o[k] !== undefined);
|
|
2410
|
-
if (書いた.length !== 1) {
|
|
2411
|
-
errors.push({
|
|
2412
|
-
path,
|
|
2413
|
-
message:
|
|
2414
|
-
書いた.length === 0 ? "event target is required" : "event target must be written once",
|
|
2415
|
-
hint: `${相手の鍵.join(" / ")} のどれか 1 つだけを書く`,
|
|
2416
|
-
});
|
|
2417
|
-
return;
|
|
2418
|
-
}
|
|
2419
|
-
const 鍵 = 書いた[0]!;
|
|
2420
|
-
if (鍵 === "diagram") {
|
|
2421
|
-
if (o.diagram !== true) {
|
|
2422
|
-
errors.push({
|
|
2423
|
-
path: `${path}.diagram`,
|
|
2424
|
-
message: "diagram must be true",
|
|
2425
|
-
hint: "図全体を指す時だけ書く",
|
|
2426
|
-
});
|
|
2427
|
-
}
|
|
2428
|
-
return;
|
|
2429
|
-
}
|
|
2430
|
-
if (typeof o[鍵] !== "string" || o[鍵].trim() === "") {
|
|
2431
|
-
errors.push({ path: `${path}.${鍵}`, message: `${鍵} must be a non-empty string` });
|
|
2432
|
-
return;
|
|
2433
|
-
}
|
|
2434
|
-
if (鍵 === "arrow") {
|
|
2435
|
-
const 両端 = (o.arrow as string).split("->");
|
|
2436
|
-
if (両端.length !== 2 || 両端.some((x) => x.trim() === "")) {
|
|
2437
|
-
errors.push({
|
|
2438
|
-
path: `${path}.arrow`,
|
|
2439
|
-
message: "arrow must be `A -> B`",
|
|
2440
|
-
hint: "矢印の両端の名前を書く",
|
|
2441
|
-
});
|
|
2442
|
-
}
|
|
2443
|
-
}
|
|
2444
|
-
});
|
|
2445
|
-
}
|
|
2446
|
-
|
|
2447
|
-
/**
|
|
2448
|
-
* 巻き上げに応じて進む値が受ける項目 (#2258)。
|
|
2449
|
-
*
|
|
2450
|
-
* **数を書く項目と文字を書く項目を分けて持つ** = 受け方が違うので判定が分かれるが、
|
|
2451
|
-
* 知らせは 2 つを繋いで 1 つの一覧として出す。 別々に並べていた間、判定が 3 語で
|
|
2452
|
-
* 知らせが 4 語という食い違いが同じ関数の中にあった。
|
|
2453
|
-
*/
|
|
2454
|
-
const 送りの数の項目 = ["start", "end", "scrub"] as const;
|
|
2455
|
-
const 送りの文の項目 = ["label"] as const;
|
|
2456
|
-
|
|
2457
|
-
/**
|
|
2458
|
-
* 巻き上げに応じて進む値を検査する (#1393)。
|
|
2459
|
-
*
|
|
2460
|
-
* 形は `{ 名前: { start, end, scrub, label } }`。 名前の規則は状態と揃える。
|
|
2461
|
-
*/
|
|
2462
|
-
function validateScrolls(
|
|
2463
|
-
v: unknown,
|
|
2464
|
-
inputs: unknown,
|
|
2465
|
-
formulas: unknown,
|
|
2466
|
-
errors: JsonDslError[],
|
|
2467
|
-
): void {
|
|
2468
|
-
if (v === undefined) return;
|
|
2469
|
-
if (!v || typeof v !== "object" || Array.isArray(v)) {
|
|
2470
|
-
errors.push({
|
|
2471
|
-
path: "$.scrolls",
|
|
2472
|
-
message: "scrolls must be an object of name to spec",
|
|
2473
|
-
hint: `got ${v === null ? "null" : Array.isArray(v) ? "array" : typeof v}`,
|
|
2474
|
-
});
|
|
2475
|
-
return;
|
|
2476
|
-
}
|
|
2477
|
-
const 既に値を作る名前 = new Set<string>();
|
|
2478
|
-
if (Array.isArray(inputs)) {
|
|
2479
|
-
for (const input of inputs) {
|
|
2480
|
-
if (!input || typeof input !== "object" || Array.isArray(input)) continue;
|
|
2481
|
-
const id = (input as { id?: unknown }).id;
|
|
2482
|
-
if (typeof id === "string") 既に値を作る名前.add(id);
|
|
2483
|
-
}
|
|
2484
|
-
}
|
|
2485
|
-
if (formulas && typeof formulas === "object" && !Array.isArray(formulas)) {
|
|
2486
|
-
for (const id of Object.keys(formulas)) 既に値を作る名前.add(id);
|
|
2487
|
-
}
|
|
2488
|
-
for (const [名前, spec] of Object.entries(v as Record<string, unknown>)) {
|
|
2489
|
-
const path = `$.scrolls.${名前}`;
|
|
2490
|
-
if (!isValueName(名前)) {
|
|
2491
|
-
errors.push({ path, ...valueNameIssue(名前) });
|
|
2492
|
-
continue;
|
|
2493
|
-
}
|
|
2494
|
-
if (既に値を作る名前.has(名前)) {
|
|
2495
|
-
errors.push({
|
|
2496
|
-
path,
|
|
2497
|
-
message: `${名前} collides with an input or formula`,
|
|
2498
|
-
hint: "inputs / formulas / scrolls では重ならない名前を使う",
|
|
2499
|
-
});
|
|
2500
|
-
}
|
|
2501
|
-
if (!spec || typeof spec !== "object" || Array.isArray(spec)) {
|
|
2502
|
-
errors.push({ path, message: `${名前} must be a plain object`, hint: `got ${typeof spec}` });
|
|
2503
|
-
continue;
|
|
2504
|
-
}
|
|
2505
|
-
for (const [k, x] of Object.entries(spec as Record<string, unknown>)) {
|
|
2506
|
-
if ((送りの文の項目 as readonly string[]).includes(k)) {
|
|
2507
|
-
if (typeof x !== "string") {
|
|
2508
|
-
errors.push({ path: `${path}.${k}`, message: `${k} must be a string` });
|
|
2509
|
-
}
|
|
2510
|
-
continue;
|
|
2511
|
-
}
|
|
2512
|
-
if (!(送りの数の項目 as readonly string[]).includes(k)) {
|
|
2513
|
-
errors.push({
|
|
2514
|
-
path: `${path}.${k}`,
|
|
2515
|
-
message: `unknown key "${k}"`,
|
|
2516
|
-
hint: `使える項目 = ${[...送りの数の項目, ...送りの文の項目].join(", ")}`,
|
|
2517
|
-
});
|
|
2518
|
-
continue;
|
|
2519
|
-
}
|
|
2520
|
-
if (typeof x !== "number" || !Number.isFinite(x)) {
|
|
2521
|
-
errors.push({ path: `${path}.${k}`, message: `${k} must be a finite number` });
|
|
2522
|
-
} else if (x < 0 || x > 1) {
|
|
2523
|
-
errors.push({
|
|
2524
|
-
path: `${path}.${k}`,
|
|
2525
|
-
message: `${k} must be between 0 and 1`,
|
|
2526
|
-
hint: "0 と 1 を含む範囲で書く",
|
|
2527
|
-
});
|
|
2528
|
-
}
|
|
2529
|
-
}
|
|
2530
|
-
}
|
|
2531
|
-
}
|
|
2532
|
-
|
|
2533
|
-
/**
|
|
2534
|
-
* 箱の中に描く図形を検査する (#1374)。
|
|
2535
|
-
*
|
|
2536
|
-
* `validateReadouts` と同じ理由で外側の形もここで見る。
|
|
2537
|
-
*/
|
|
2538
|
-
function validateActorShape(v: unknown, path: string, errors: JsonDslError[]): void {
|
|
2539
|
-
if (v === undefined) return;
|
|
2540
|
-
if (!v || typeof v !== "object" || Array.isArray(v)) {
|
|
2541
|
-
errors.push({
|
|
2542
|
-
path,
|
|
2543
|
-
message: "shape must be a plain object",
|
|
2544
|
-
hint: `got ${v === null ? "null" : Array.isArray(v) ? "array" : typeof v}`,
|
|
2545
|
-
});
|
|
2546
|
-
return;
|
|
2547
|
-
}
|
|
2548
|
-
表で中身を検査する(v as Record<string, unknown>, 図形の表, path, "shape", errors);
|
|
2549
|
-
}
|
|
2550
|
-
|
|
2551
|
-
/**
|
|
2552
|
-
* 鍵を利用者が決める入れ物 (縦列 / 群) を見る (#1304)。
|
|
2553
|
-
*
|
|
2554
|
-
* `lanes` / `groups` は id を鍵に持つため、鍵そのものは縛れない。 縛れるのは
|
|
2555
|
-
* 「入れ物が plain object か」 と「各 id の中身の項目名と値の型」 の 2 つ。
|
|
2556
|
-
*
|
|
2557
|
-
* #1304 まで外側の形が違う入力 (`lanes: 5`) は走査ごと飛ばされ、誤りが 1 件も返らなかった。
|
|
2558
|
-
* 形が違うものを黙って捨てると、書いた縦列が 1 つも効かない図が知らせなしで出る。
|
|
2559
|
-
*/
|
|
2560
|
-
function validateIdMap(
|
|
2561
|
-
v: unknown,
|
|
2562
|
-
欄: "lanes" | "groups",
|
|
2563
|
-
層: 階層,
|
|
2564
|
-
errors: JsonDslError[],
|
|
2565
|
-
中身を見る: (o: Record<string, unknown>, id: string, path: string) => void,
|
|
2566
|
-
): void {
|
|
2567
|
-
if (v === undefined) return;
|
|
2568
|
-
const path = `$.${欄}`;
|
|
2569
|
-
if (!v || typeof v !== "object" || Array.isArray(v)) {
|
|
2570
|
-
errors.push({
|
|
2571
|
-
path,
|
|
2572
|
-
message: `${欄} must be a plain object of id -> settings`,
|
|
2573
|
-
hint: `got ${v === null ? "null" : Array.isArray(v) ? "array" : typeof v}`,
|
|
2574
|
-
});
|
|
2575
|
-
return;
|
|
2576
|
-
}
|
|
2577
|
-
for (const [id, 中] of Object.entries(v as Record<string, unknown>)) {
|
|
2578
|
-
const idPath = `${path}.${id}`;
|
|
2579
|
-
if (!中 || typeof 中 !== "object" || Array.isArray(中)) {
|
|
2580
|
-
errors.push({
|
|
2581
|
-
path: idPath,
|
|
2582
|
-
message: `${欄}.${id} must be a plain object`,
|
|
2583
|
-
hint: `got ${中 === null ? "null" : Array.isArray(中) ? "array" : typeof 中}`,
|
|
2584
|
-
});
|
|
2585
|
-
continue;
|
|
2586
|
-
}
|
|
2587
|
-
const o = 中 as Record<string, unknown>;
|
|
2588
|
-
checkUnknownKeys(o, 層, idPath, errors);
|
|
2589
|
-
中身を見る(o, id, idPath);
|
|
2590
|
-
}
|
|
2591
|
-
}
|
|
2592
|
-
|
|
2593
|
-
/** 2 軸で仕分ける図の軸の名前を見る (#1294) */
|
|
2594
|
-
function validateAxes(v: unknown, errors: JsonDslError[]): void {
|
|
2595
|
-
if (v === undefined) return;
|
|
2596
|
-
if (!v || typeof v !== "object" || Array.isArray(v)) {
|
|
2597
|
-
errors.push({
|
|
2598
|
-
path: "$.axes",
|
|
2599
|
-
message: "axes must be a plain object like { x: { left, right }, y: { bottom, top } }",
|
|
2600
|
-
});
|
|
2601
|
-
return;
|
|
2602
|
-
}
|
|
2603
|
-
const 軸 = v as Record<string, unknown>;
|
|
2604
|
-
checkUnknownKeys(軸, "axes", "$.axes", errors);
|
|
2605
|
-
const 層の名 = { x: "axesX", y: "axesY" } as const;
|
|
2606
|
-
for (const 名 of ["x", "y"] as const) {
|
|
2607
|
-
const 一方 = 軸[名];
|
|
2608
|
-
if (一方 === undefined) continue;
|
|
2609
|
-
if (!一方 || typeof 一方 !== "object" || Array.isArray(一方)) {
|
|
2610
|
-
errors.push({ path: `$.axes.${名}`, message: `axes.${名} must be a plain object` });
|
|
2611
|
-
continue;
|
|
2612
|
-
}
|
|
2613
|
-
const o = 一方 as Record<string, unknown>;
|
|
2614
|
-
checkUnknownKeys(o, 層の名[名], `$.axes.${名}`, errors);
|
|
2615
|
-
表で検査(o, 層の名[名], `$.axes.${名}`, `axes.${名}`, errors);
|
|
2616
|
-
if (o.direction === true) {
|
|
2617
|
-
if (typeof o.label !== "string") {
|
|
2618
|
-
errors.push({ path: `$.axes.${名}.label`, message: `axes.${名}.label must be a string for a directional axis` });
|
|
2619
|
-
}
|
|
2620
|
-
} else if (o.label !== undefined || o.direction !== undefined) {
|
|
2621
|
-
errors.push({ path: `$.axes.${名}.direction`, message: `axes.${名}.direction must be true when label is used` });
|
|
2622
|
-
}
|
|
2623
|
-
}
|
|
2624
|
-
}
|
|
2625
|
-
|
|
2626
|
-
/** #2854 で足した図表の複合欄と閉じた語を検査する。 */
|
|
2627
|
-
function validateChartPresentationFields(j: Record<string, unknown>, errors: JsonDslError[]): void {
|
|
2628
|
-
const enumField = (name: string, allowed: readonly string[]): void => {
|
|
2629
|
-
const value = j[name];
|
|
2630
|
-
if (value === undefined || typeof value !== "string") return;
|
|
2631
|
-
if (!allowed.includes(value)) {
|
|
2632
|
-
errors.push({ path: `$.${name}`, message: `${name} must be one of: ${allowed.join(", ")}` });
|
|
2633
|
-
}
|
|
2634
|
-
};
|
|
2635
|
-
enumField("chartPieTableColumns", CHART_PIE_TABLE_COLUMN_VALUES);
|
|
2636
|
-
enumField("chartPieRingWidth", CHART_PIE_RING_WIDTH_VALUES);
|
|
2637
|
-
enumField("chartStackedLegendPosition", CHART_STACKED_LEGEND_POSITION_VALUES);
|
|
2638
|
-
enumField("chartWaffleLegendPosition", CHART_WAFFLE_LEGEND_POSITION_VALUES);
|
|
2639
|
-
enumField("funnelForm", FUNNEL_FORM_VALUES);
|
|
2640
|
-
enumField("funnelRate", FUNNEL_RATE_VALUES);
|
|
2641
|
-
enumField("quadrantPointLabelSide", QUADRANT_POINT_LABEL_SIDE_VALUES);
|
|
2642
|
-
enumField("treeNodeForm", TREE_NODE_FORM_VALUES);
|
|
2643
|
-
enumField("treeEdgeTone", TREE_EDGE_TONE_VALUES);
|
|
2644
|
-
enumField("treeEdgeHead", TREE_EDGE_HEAD_VALUES);
|
|
2645
|
-
enumField("mindForm", MIND_FORM_VALUES);
|
|
2646
|
-
enumField("journeyForm", JOURNEY_FORM_VALUES);
|
|
2647
|
-
enumField("journeyLineForm", JOURNEY_LINE_FORM_VALUES);
|
|
2648
|
-
enumField("sequenceActorForm", SEQUENCE_ACTOR_FORM_VALUES);
|
|
2649
|
-
enumField("sequenceLabelForm", SEQUENCE_LABEL_FORM_VALUES);
|
|
2650
|
-
enumField("sequenceReturnHead", SEQUENCE_RETURN_HEAD_VALUES);
|
|
2651
|
-
enumField("cornerForm", CORNER_FORM_VALUES);
|
|
2652
|
-
enumField("chartBarShape", CHART_BAR_SHAPE_VALUES);
|
|
2653
|
-
enumField("quadrantLabelPosition", QUADRANT_LABEL_POSITION_VALUES);
|
|
2654
|
-
|
|
2655
|
-
const closedObject = (
|
|
2656
|
-
name: "journeyLabels" | "figureCard" | "figureSize" | "treeSize" | "mindSize" |
|
|
2657
|
-
"terminalDecoration" | "figureKindLabel" | "figureInset" | "journeySize" | "chartBarSize" |
|
|
2658
|
-
"chartLineSize" | "chartPieSize" | "chartWaffleSize" | "chartSlopeLabelTemplate" | "chartSlopeSize" |
|
|
2659
|
-
"chartStackedLayout" | "chartGaugeSize" | "ganttSize" | "quadrantSize" | "legendOffset" | "stageLayout",
|
|
2660
|
-
keys: readonly string[],
|
|
2661
|
-
): Record<string, unknown> | undefined => {
|
|
2662
|
-
const value = j[name];
|
|
2663
|
-
if (value === undefined) return undefined;
|
|
2664
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
2665
|
-
errors.push({ path: `$.${name}`, message: `${name} must be an object` });
|
|
2666
|
-
return undefined;
|
|
2667
|
-
}
|
|
2668
|
-
const object = value as Record<string, unknown>;
|
|
2669
|
-
for (const key of Object.keys(object)) {
|
|
2670
|
-
if (!keys.includes(key)) errors.push({ path: `$.${name}.${key}`, message: `unknown key "${key}"` });
|
|
2671
|
-
}
|
|
2672
|
-
return object;
|
|
2673
|
-
};
|
|
2674
|
-
const labels = closedObject("journeyLabels", JOURNEY_LABEL_KEYS);
|
|
2675
|
-
if (labels !== undefined) {
|
|
2676
|
-
for (const key of JOURNEY_LABEL_KEYS) {
|
|
2677
|
-
if (typeof labels[key] !== "string" || labels[key].trim() === "") {
|
|
2678
|
-
errors.push({ path: `$.journeyLabels.${key}`, message: `${key} must be a non-empty string` });
|
|
2679
|
-
}
|
|
2680
|
-
}
|
|
2681
|
-
}
|
|
2682
|
-
if (j.sequenceKindLabels !== undefined) {
|
|
2683
|
-
if (!j.sequenceKindLabels || typeof j.sequenceKindLabels !== "object" || Array.isArray(j.sequenceKindLabels)) {
|
|
2684
|
-
errors.push({ path: "$.sequenceKindLabels", message: "sequenceKindLabels must be an object" });
|
|
2685
|
-
} else {
|
|
2686
|
-
const labels = j.sequenceKindLabels as Record<string, unknown>;
|
|
2687
|
-
for (const [key, value] of Object.entries(labels)) {
|
|
2688
|
-
if (!(SEQUENCE_KIND_LABEL_KEYS as readonly string[]).includes(key)) errors.push({ path: `$.sequenceKindLabels.${key}`, message: `unknown key "${key}"` });
|
|
2689
|
-
else if (typeof value !== "string") errors.push({ path: `$.sequenceKindLabels.${key}`, message: `${key} must be a string` });
|
|
2690
|
-
}
|
|
2691
|
-
}
|
|
2692
|
-
}
|
|
2693
|
-
if (
|
|
2694
|
-
j.sequenceRowGap !== undefined &&
|
|
2695
|
-
(typeof j.sequenceRowGap !== "number" || !Number.isFinite(j.sequenceRowGap) || j.sequenceRowGap <= 0)
|
|
2696
|
-
) {
|
|
2697
|
-
errors.push({ path: "$.sequenceRowGap", message: "sequenceRowGap must be a positive finite number" });
|
|
2698
|
-
}
|
|
2699
|
-
for (const key of ["sequenceGutter", "sequenceBandWidth"] as const) {
|
|
2700
|
-
if (j[key] !== undefined && (typeof j[key] !== "number" || !Number.isFinite(j[key]) || j[key] <= 0)) {
|
|
2701
|
-
errors.push({ path: `$.${key}`, message: `${key} must be a positive finite number` });
|
|
2702
|
-
}
|
|
2703
|
-
}
|
|
2704
|
-
if (j.sequenceSelfLoop !== undefined) {
|
|
2705
|
-
if (!j.sequenceSelfLoop || typeof j.sequenceSelfLoop !== "object" || Array.isArray(j.sequenceSelfLoop)) {
|
|
2706
|
-
errors.push({ path: "$.sequenceSelfLoop", message: "sequenceSelfLoop must be an object" });
|
|
2707
|
-
} else {
|
|
2708
|
-
const loop = j.sequenceSelfLoop as Record<string, unknown>;
|
|
2709
|
-
for (const key of Object.keys(loop)) {
|
|
2710
|
-
if (!(SEQUENCE_SELF_LOOP_KEYS as readonly string[]).includes(key)) errors.push({ path: `$.sequenceSelfLoop.${key}`, message: `unknown key "${key}"` });
|
|
2711
|
-
}
|
|
2712
|
-
if (loop.form !== undefined && loop.form !== "orthogonal") errors.push({ path: "$.sequenceSelfLoop.form", message: "form must be orthogonal" });
|
|
2713
|
-
for (const key of ["width", "drop"] as const) {
|
|
2714
|
-
if (loop[key] !== undefined && (typeof loop[key] !== "number" || !Number.isFinite(loop[key]) || loop[key] <= 0)) errors.push({ path: `$.sequenceSelfLoop.${key}`, message: "must be a positive finite number" });
|
|
2715
|
-
}
|
|
2716
|
-
}
|
|
2717
|
-
}
|
|
2718
|
-
const card = closedObject("figureCard", ["label", "note"]);
|
|
2719
|
-
if (card !== undefined) {
|
|
2720
|
-
if (typeof card.label !== "string")
|
|
2721
|
-
errors.push({ path: "$.figureCard.label", message: "label must be a string" });
|
|
2722
|
-
if (card.note !== undefined && typeof card.note !== "string")
|
|
2723
|
-
errors.push({ path: "$.figureCard.note", message: "note must be a string" });
|
|
2724
|
-
}
|
|
2725
|
-
const size = closedObject("figureSize", ["width", "height"]);
|
|
2726
|
-
if (size !== undefined) {
|
|
2727
|
-
for (const key of ["width", "height"] as const) {
|
|
2728
|
-
if (typeof size[key] !== "number" || !Number.isFinite(size[key]) || size[key] <= 0)
|
|
2729
|
-
errors.push({ path: `$.figureSize.${key}`, message: `${key} must be a positive finite number` });
|
|
2730
|
-
}
|
|
2731
|
-
}
|
|
2732
|
-
const positiveNumber = (value: unknown, path: string): boolean => {
|
|
2733
|
-
if (typeof value === "number" && Number.isFinite(value) && value > 0) return true;
|
|
2734
|
-
errors.push({ path, message: "must be a positive finite number" });
|
|
2735
|
-
return false;
|
|
2736
|
-
};
|
|
2737
|
-
const nonNegativeNumber = (value: unknown, path: string): boolean => {
|
|
2738
|
-
if (typeof value === "number" && Number.isFinite(value) && value >= 0) return true;
|
|
2739
|
-
errors.push({ path, message: "must be a non-negative finite number" });
|
|
2740
|
-
return false;
|
|
2741
|
-
};
|
|
2742
|
-
if (j.edgeEndGap !== undefined) nonNegativeNumber(j.edgeEndGap, "$.edgeEndGap");
|
|
2743
|
-
const treeSize = closedObject("treeSize", [
|
|
2744
|
-
"nodeWidths",
|
|
2745
|
-
"nodeHeight",
|
|
2746
|
-
"titleFontSize",
|
|
2747
|
-
"subtitleFontSize",
|
|
2748
|
-
"siblingGap",
|
|
2749
|
-
"levelGap",
|
|
2750
|
-
]);
|
|
2751
|
-
if (treeSize !== undefined) {
|
|
2752
|
-
if (treeSize.nodeWidths !== undefined) {
|
|
2753
|
-
if (!Array.isArray(treeSize.nodeWidths) || treeSize.nodeWidths.length === 0) {
|
|
2754
|
-
errors.push({ path: "$.treeSize.nodeWidths", message: "must be a non-empty array" });
|
|
2755
|
-
} else {
|
|
2756
|
-
treeSize.nodeWidths.forEach((value, index) => {
|
|
2757
|
-
positiveNumber(value, `$.treeSize.nodeWidths[${index}]`);
|
|
2758
|
-
});
|
|
2759
|
-
}
|
|
2760
|
-
}
|
|
2761
|
-
for (const key of ["nodeHeight", "titleFontSize", "subtitleFontSize"] as const) {
|
|
2762
|
-
if (treeSize[key] !== undefined) positiveNumber(treeSize[key], `$.treeSize.${key}`);
|
|
2763
|
-
}
|
|
2764
|
-
for (const key of ["siblingGap", "levelGap"] as const) {
|
|
2765
|
-
if (treeSize[key] !== undefined) nonNegativeNumber(treeSize[key], `$.treeSize.${key}`);
|
|
2766
|
-
}
|
|
2767
|
-
}
|
|
2768
|
-
const mindSize = closedObject("mindSize", [
|
|
2769
|
-
"contentOffset",
|
|
2770
|
-
"root",
|
|
2771
|
-
"branch",
|
|
2772
|
-
"leaf",
|
|
2773
|
-
"rootBranchGap",
|
|
2774
|
-
"branchLeafGap",
|
|
2775
|
-
"branchRowGap",
|
|
2776
|
-
"branchRowOffset",
|
|
2777
|
-
"leafRowGap",
|
|
2778
|
-
"leafRowOffset",
|
|
2779
|
-
]);
|
|
2780
|
-
if (mindSize !== undefined) {
|
|
2781
|
-
if (mindSize.contentOffset !== undefined) {
|
|
2782
|
-
const value = mindSize.contentOffset;
|
|
2783
|
-
const path = "$.mindSize.contentOffset";
|
|
2784
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
2785
|
-
errors.push({ path, message: "contentOffset must be an object" });
|
|
2786
|
-
} else {
|
|
2787
|
-
const offset = value as Record<string, unknown>;
|
|
2788
|
-
for (const key of Object.keys(offset)) {
|
|
2789
|
-
if (key !== "x" && key !== "y")
|
|
2790
|
-
errors.push({ path: `${path}.${key}`, message: `unknown key "${key}"` });
|
|
2791
|
-
}
|
|
2792
|
-
for (const key of ["x", "y"] as const) {
|
|
2793
|
-
if (typeof offset[key] !== "number" || !Number.isFinite(offset[key]))
|
|
2794
|
-
errors.push({ path: `${path}.${key}`, message: "must be a finite number" });
|
|
2795
|
-
}
|
|
2796
|
-
}
|
|
2797
|
-
}
|
|
2798
|
-
const nestedSize = (
|
|
2799
|
-
key: "root" | "branch" | "leaf",
|
|
2800
|
-
allowed: readonly string[],
|
|
2801
|
-
): Record<string, unknown> | undefined => {
|
|
2802
|
-
const value = mindSize[key];
|
|
2803
|
-
if (value === undefined) return undefined;
|
|
2804
|
-
const path = `$.mindSize.${key}`;
|
|
2805
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
2806
|
-
errors.push({ path, message: `${key} must be an object` });
|
|
2807
|
-
return undefined;
|
|
2808
|
-
}
|
|
2809
|
-
const object = value as Record<string, unknown>;
|
|
2810
|
-
for (const nestedKey of Object.keys(object)) {
|
|
2811
|
-
if (!allowed.includes(nestedKey))
|
|
2812
|
-
errors.push({ path: `${path}.${nestedKey}`, message: `unknown key "${nestedKey}"` });
|
|
2813
|
-
}
|
|
2814
|
-
return object;
|
|
2815
|
-
};
|
|
2816
|
-
for (const [key, allowed] of [
|
|
2817
|
-
["root", ["width", "height", "fontSize"]],
|
|
2818
|
-
["branch", ["width", "height", "fontSize"]],
|
|
2819
|
-
["leaf", ["fontSize"]],
|
|
2820
|
-
] as const) {
|
|
2821
|
-
const nested = nestedSize(key, allowed);
|
|
2822
|
-
if (nested === undefined) continue;
|
|
2823
|
-
for (const nestedKey of allowed) {
|
|
2824
|
-
if (nested[nestedKey] !== undefined)
|
|
2825
|
-
positiveNumber(nested[nestedKey], `$.mindSize.${key}.${nestedKey}`);
|
|
2826
|
-
}
|
|
2827
|
-
}
|
|
2828
|
-
for (const key of ["rootBranchGap", "branchLeafGap", "branchRowGap", "leafRowGap"] as const) {
|
|
2829
|
-
if (mindSize[key] !== undefined) nonNegativeNumber(mindSize[key], `$.mindSize.${key}`);
|
|
2830
|
-
}
|
|
2831
|
-
for (const key of ["branchRowOffset", "leafRowOffset"] as const) {
|
|
2832
|
-
const value = mindSize[key];
|
|
2833
|
-
if (value !== undefined && (typeof value !== "number" || !Number.isFinite(value))) {
|
|
2834
|
-
errors.push({ path: `$.mindSize.${key}`, message: "must be a finite number" });
|
|
2835
|
-
}
|
|
2836
|
-
}
|
|
2837
|
-
}
|
|
2838
|
-
|
|
2839
|
-
const specs = {
|
|
2840
|
-
terminalDecoration: ["windowControls", "cursor", "rowOrdinal"],
|
|
2841
|
-
figureKindLabel: ["text", "x", "y", "fontSize", "fontWeight", "letterSpacing"],
|
|
2842
|
-
figureInset: ["top", "right", "bottom", "left", "radius"],
|
|
2843
|
-
journeySize: ["plotPadding", "pointPadding"],
|
|
2844
|
-
chartBarSize: ["tickFontSize", "valueFontSize", "labelFontSize", "plotPadding", "barWidth", "cornerRadius"],
|
|
2845
|
-
chartLineSize: ["valueLabelOffset", "tickFontSize", "periodFontSize", "pointRadius", "plotPadding", "tickGap", "pointPadding", "periodY"],
|
|
2846
|
-
chartPieSize: ["tableWidth", "tableLabelFontSize", "tableValueFontSize", "tableRowGap", "tablePaddingInline", "outerRadius", "centerX"],
|
|
2847
|
-
chartWaffleSize: ["gridLeft", "cellPitch"],
|
|
2848
|
-
chartSlopeLabelTemplate: ["left", "right"],
|
|
2849
|
-
chartSlopeSize: ["pointRadius", "periodFontSize", "labelFontSize", "leftAxisX", "axisGap", "labelAxisGap", "plotPadding", "pointPadding"],
|
|
2850
|
-
chartStackedLayout: ["legendColumns", "legendColumnGap", "segmentGap", "barThickness", "barWidth", "legendMarkSize", "legendFontSize", "periodFontSize", "rateFontSize", "legendOffsetY"],
|
|
2851
|
-
chartGaugeSize: ["arcThickness", "outerRadius", "plotTop", "valueFontSize"],
|
|
2852
|
-
ganttSize: ["taskLabelFontSize", "taskLabelAlign", "taskLabelX", "tickFontSize", "tickCount", "barThickness", "milestoneFontSize", "milestoneRadius", "dependencyStrokeWidth", "taskColumnWidth", "timelinePaddingRight"],
|
|
2853
|
-
quadrantSize: ["plotOffset", "plotSize", "pointRadius", "pointFontSize", "pointLabelGap", "quadrantFontSize", "axisFontSize"],
|
|
2854
|
-
legendOffset: ["x", "y"],
|
|
2855
|
-
stageLayout: ["columnWidths", "columnGap", "backDetourTopGap", "backDetourSideGap", "backDetourTargetInset"],
|
|
2856
|
-
} as const;
|
|
2857
|
-
const objects = Object.fromEntries(Object.entries(specs).map(([name, keys]) => [name, closedObject(name as keyof typeof specs, keys)])) as Record<keyof typeof specs, Record<string, unknown> | undefined>;
|
|
2858
|
-
const positiveKeys: Partial<Record<keyof typeof specs, readonly string[]>> = {
|
|
2859
|
-
figureKindLabel: ["fontSize", "fontWeight"],
|
|
2860
|
-
chartBarSize: ["tickFontSize", "valueFontSize", "labelFontSize", "barWidth"],
|
|
2861
|
-
chartLineSize: ["tickFontSize", "periodFontSize", "pointRadius"],
|
|
2862
|
-
chartPieSize: ["tableWidth", "tableLabelFontSize", "tableValueFontSize", "outerRadius"],
|
|
2863
|
-
chartWaffleSize: ["cellPitch"],
|
|
2864
|
-
chartSlopeSize: ["pointRadius", "periodFontSize", "labelFontSize", "axisGap"],
|
|
2865
|
-
chartStackedLayout: ["barThickness", "barWidth", "legendMarkSize", "legendFontSize", "periodFontSize", "rateFontSize"],
|
|
2866
|
-
chartGaugeSize: ["arcThickness", "outerRadius", "valueFontSize"],
|
|
2867
|
-
ganttSize: ["taskLabelFontSize", "tickFontSize", "barThickness", "milestoneFontSize", "milestoneRadius", "dependencyStrokeWidth"],
|
|
2868
|
-
quadrantSize: ["plotSize", "pointRadius", "pointFontSize", "quadrantFontSize", "axisFontSize"],
|
|
2869
|
-
};
|
|
2870
|
-
const nonNegativeKeys: Partial<Record<keyof typeof specs, readonly string[]>> = {
|
|
2871
|
-
figureKindLabel: ["letterSpacing"], figureInset: ["top", "right", "bottom", "left", "radius"],
|
|
2872
|
-
chartBarSize: ["cornerRadius"], chartLineSize: ["tickGap", "periodY"], chartPieSize: ["tableRowGap", "tablePaddingInline", "centerX"], chartWaffleSize: ["gridLeft"], chartSlopeSize: ["leftAxisX", "labelAxisGap"],
|
|
2873
|
-
chartStackedLayout: ["legendColumnGap", "segmentGap"], chartGaugeSize: ["plotTop"],
|
|
2874
|
-
ganttSize: ["taskLabelX", "taskColumnWidth", "timelinePaddingRight"], quadrantSize: ["pointLabelGap"],
|
|
2875
|
-
stageLayout: ["columnGap", "backDetourTopGap", "backDetourSideGap", "backDetourTargetInset"],
|
|
2876
|
-
};
|
|
2877
|
-
for (const [name, object] of Object.entries(objects) as [keyof typeof specs, Record<string, unknown> | undefined][]) {
|
|
2878
|
-
if (object === undefined) continue;
|
|
2879
|
-
for (const key of positiveKeys[name] ?? []) if (object[key] !== undefined) positiveNumber(object[key], `$.${name}.${key}`);
|
|
2880
|
-
for (const key of nonNegativeKeys[name] ?? []) if (object[key] !== undefined) nonNegativeNumber(object[key], `$.${name}.${key}`);
|
|
2881
|
-
}
|
|
2882
|
-
const finite = (value: unknown, path: string): void => { if (value !== undefined && (typeof value !== "number" || !Number.isFinite(value))) errors.push({ path, message: "must be a finite number" }); };
|
|
2883
|
-
if (objects.terminalDecoration) for (const key of specs.terminalDecoration) if (objects.terminalDecoration[key] !== undefined && typeof objects.terminalDecoration[key] !== "boolean") errors.push({ path: `$.terminalDecoration.${key}`, message: "must be boolean" });
|
|
2884
|
-
if (objects.figureKindLabel) {
|
|
2885
|
-
if (typeof objects.figureKindLabel.text !== "string" || objects.figureKindLabel.text.trim() === "") errors.push({ path: "$.figureKindLabel.text", message: "must be a non-empty string" });
|
|
2886
|
-
finite(objects.figureKindLabel.x, "$.figureKindLabel.x"); finite(objects.figureKindLabel.y, "$.figureKindLabel.y");
|
|
2887
|
-
}
|
|
2888
|
-
if (objects.chartSlopeLabelTemplate) for (const key of specs.chartSlopeLabelTemplate) if (objects.chartSlopeLabelTemplate[key] !== undefined && typeof objects.chartSlopeLabelTemplate[key] !== "string") errors.push({ path: `$.chartSlopeLabelTemplate.${key}`, message: "must be a string" });
|
|
2889
|
-
if (objects.chartStackedLayout) finite(objects.chartStackedLayout.legendOffsetY, "$.chartStackedLayout.legendOffsetY");
|
|
2890
|
-
if (objects.legendOffset) {
|
|
2891
|
-
finite(objects.legendOffset.x, "$.legendOffset.x");
|
|
2892
|
-
finite(objects.legendOffset.y, "$.legendOffset.y");
|
|
2893
|
-
}
|
|
2894
|
-
const nestedNumberObject = (parent: keyof typeof specs, key: string, keys: readonly string[], positive: boolean): void => {
|
|
2895
|
-
const value = objects[parent]?.[key]; if (value === undefined) return;
|
|
2896
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) { errors.push({ path: `$.${parent}.${key}`, message: "must be an object" }); return; }
|
|
2897
|
-
const object = value as Record<string, unknown>;
|
|
2898
|
-
for (const child of Object.keys(object)) if (!keys.includes(child)) errors.push({ path: `$.${parent}.${key}.${child}`, message: `unknown key "${child}"` });
|
|
2899
|
-
for (const child of keys) if (object[child] !== undefined) (positive ? positiveNumber : nonNegativeNumber)(object[child], `$.${parent}.${key}.${child}`);
|
|
2900
|
-
};
|
|
2901
|
-
nestedNumberObject("chartBarSize", "plotPadding", ["top", "right", "bottom", "left"], false);
|
|
2902
|
-
nestedNumberObject("chartLineSize", "valueLabelOffset", ["above", "below"], false);
|
|
2903
|
-
nestedNumberObject("chartLineSize", "plotPadding", ["top", "right", "bottom", "left"], false);
|
|
2904
|
-
nestedNumberObject("chartLineSize", "pointPadding", ["left", "right"], false);
|
|
2905
|
-
nestedNumberObject("chartSlopeSize", "plotPadding", ["top", "bottom"], false);
|
|
2906
|
-
nestedNumberObject("chartSlopeSize", "pointPadding", ["top", "bottom"], false);
|
|
2907
|
-
nestedNumberObject("journeySize", "plotPadding", ["left", "right"], false);
|
|
2908
|
-
nestedNumberObject("journeySize", "pointPadding", ["left", "right"], false);
|
|
2909
|
-
nestedNumberObject("quadrantSize", "plotOffset", ["x", "y"], false);
|
|
2910
|
-
const columnWidths = objects.stageLayout?.columnWidths;
|
|
2911
|
-
if (columnWidths !== undefined) {
|
|
2912
|
-
if (!columnWidths || typeof columnWidths !== "object" || Array.isArray(columnWidths)) errors.push({ path: "$.stageLayout.columnWidths", message: "must be an object" });
|
|
2913
|
-
else for (const [key, value] of Object.entries(columnWidths)) nonNegativeNumber(value, `$.stageLayout.columnWidths.${key}`);
|
|
2914
|
-
}
|
|
2915
|
-
const stackedColumns = objects.chartStackedLayout?.legendColumns;
|
|
2916
|
-
if (stackedColumns !== undefined && (!Number.isInteger(stackedColumns) || typeof stackedColumns !== "number" || stackedColumns <= 0)) errors.push({ path: "$.chartStackedLayout.legendColumns", message: "must be a positive integer" });
|
|
2917
|
-
if (
|
|
2918
|
-
objects.ganttSize?.taskLabelAlign !== undefined &&
|
|
2919
|
-
!(GANTT_TASK_LABEL_ALIGN_VALUES as readonly unknown[]).includes(objects.ganttSize.taskLabelAlign)
|
|
2920
|
-
) errors.push({ path: "$.ganttSize.taskLabelAlign", message: "must be start or end" });
|
|
2921
|
-
const tickCount = objects.ganttSize?.tickCount;
|
|
2922
|
-
if (tickCount !== undefined && (!Number.isInteger(tickCount) || typeof tickCount !== "number" || tickCount < 2)) errors.push({ path: "$.ganttSize.tickCount", message: "must be an integer greater than or equal to 2" });
|
|
2923
|
-
|
|
2924
|
-
if (Array.isArray(j.chartSlopePeriods) &&
|
|
2925
|
-
(j.chartSlopePeriods.length !== 2 || j.chartSlopePeriods.some((v) => typeof v !== "string" || v.trim() === ""))) {
|
|
2926
|
-
errors.push({ path: "$.chartSlopePeriods", message: "chartSlopePeriods must contain exactly two non-empty strings" });
|
|
2927
|
-
}
|
|
2928
|
-
if (Array.isArray(j.chartSlopeEmphasisIds) && j.chartSlopeEmphasisIds.length === 0) {
|
|
2929
|
-
errors.push({ path: "$.chartSlopeEmphasisIds", message: "chartSlopeEmphasisIds must contain at least one id" });
|
|
2930
|
-
}
|
|
2931
|
-
|
|
2932
|
-
const boundNumber = (value: unknown): boolean =>
|
|
2933
|
-
(typeof value === "number" && Number.isFinite(value)) ||
|
|
2934
|
-
(typeof value === "string" && value.trim() !== "");
|
|
2935
|
-
if (j.chartLineSeries !== undefined && !Array.isArray(j.chartLineSeries)) {
|
|
2936
|
-
errors.push({ path: "$.chartLineSeries", message: "chartLineSeries must be an array" });
|
|
2937
|
-
} else if (Array.isArray(j.chartLineSeries)) {
|
|
2938
|
-
if (j.chartLineSeries.length === 0) {
|
|
2939
|
-
errors.push({ path: "$.chartLineSeries", message: "chartLineSeries must contain at least one series" });
|
|
2940
|
-
}
|
|
2941
|
-
j.chartLineSeries.forEach((series, i) => {
|
|
2942
|
-
const path = `$.chartLineSeries[${i}]`;
|
|
2943
|
-
if (!series || typeof series !== "object" || Array.isArray(series)) {
|
|
2944
|
-
errors.push({ path, message: "chartLineSeries item must be an object" });
|
|
2945
|
-
return;
|
|
2946
|
-
}
|
|
2947
|
-
const value = series as Record<string, unknown>;
|
|
2948
|
-
for (const key of Object.keys(value)) {
|
|
2949
|
-
if (!["label", "points", "tone", "dash", "marker", "valueIndexes"].includes(key))
|
|
2950
|
-
errors.push({ path: `${path}.${key}`, message: `unknown key "${key}"` });
|
|
2951
|
-
}
|
|
2952
|
-
if (value.label !== undefined && typeof value.label !== "string")
|
|
2953
|
-
errors.push({ path: `${path}.label`, message: "label must be a string" });
|
|
2954
|
-
if (value.tone !== undefined && (typeof value.tone !== "string" || resolveTone(value.tone) === undefined))
|
|
2955
|
-
errors.push({ path: `${path}.tone`, message: "tone must be a known tone" });
|
|
2956
|
-
if (
|
|
2957
|
-
value.dash !== undefined &&
|
|
2958
|
-
(typeof value.dash !== "string" ||
|
|
2959
|
-
!(CHART_LINE_DASH_VALUES as readonly string[]).includes(value.dash))
|
|
2960
|
-
)
|
|
2961
|
-
errors.push({ path: `${path}.dash`, message: "dash must be solid or dotted" });
|
|
2962
|
-
if (
|
|
2963
|
-
value.marker !== undefined &&
|
|
2964
|
-
(typeof value.marker !== "string" ||
|
|
2965
|
-
!(CHART_LINE_MARKER_VALUES as readonly string[]).includes(value.marker))
|
|
2966
|
-
)
|
|
2967
|
-
errors.push({ path: `${path}.marker`, message: "marker must be dot, hollow, or none" });
|
|
2968
|
-
if (value.valueIndexes !== undefined &&
|
|
2969
|
-
(!Array.isArray(value.valueIndexes) || value.valueIndexes.some((v) => typeof v !== "number" || !Number.isInteger(v) || v < 0)))
|
|
2970
|
-
errors.push({ path: `${path}.valueIndexes`, message: "valueIndexes must contain non-negative integers" });
|
|
2971
|
-
if (!Array.isArray(value.points) || value.points.length === 0) {
|
|
2972
|
-
errors.push({ path: `${path}.points`, message: "points must be a non-empty array" });
|
|
2973
|
-
} else value.points.forEach((point, p) => {
|
|
2974
|
-
const pointPath = `${path}.points[${p}]`;
|
|
2975
|
-
if (!point || typeof point !== "object" || Array.isArray(point)) {
|
|
2976
|
-
errors.push({ path: pointPath, message: "point must be an object" });
|
|
2977
|
-
return;
|
|
2978
|
-
}
|
|
2979
|
-
const item = point as Record<string, unknown>;
|
|
2980
|
-
for (const key of Object.keys(item)) if (key !== "label" && key !== "value")
|
|
2981
|
-
errors.push({ path: `${pointPath}.${key}`, message: `unknown key "${key}"` });
|
|
2982
|
-
if (typeof item.label !== "string") errors.push({ path: `${pointPath}.label`, message: "label must be a string" });
|
|
2983
|
-
if (!boundNumber(item.value)) errors.push({ path: `${pointPath}.value`, message: "value must be a finite number or non-empty string" });
|
|
2984
|
-
});
|
|
2985
|
-
});
|
|
2986
|
-
}
|
|
2987
|
-
|
|
2988
|
-
if (j.chartStackedPeriods !== undefined && !Array.isArray(j.chartStackedPeriods)) {
|
|
2989
|
-
errors.push({ path: "$.chartStackedPeriods", message: "chartStackedPeriods must be an array" });
|
|
2990
|
-
} else if (Array.isArray(j.chartStackedPeriods)) {
|
|
2991
|
-
if (j.chartStackedPeriods.length === 0)
|
|
2992
|
-
errors.push({ path: "$.chartStackedPeriods", message: "chartStackedPeriods must contain at least one period" });
|
|
2993
|
-
j.chartStackedPeriods.forEach((period, i) => {
|
|
2994
|
-
const path = `$.chartStackedPeriods[${i}]`;
|
|
2995
|
-
if (!period || typeof period !== "object" || Array.isArray(period)) {
|
|
2996
|
-
errors.push({ path, message: "chartStackedPeriods item must be an object" });
|
|
2997
|
-
return;
|
|
2998
|
-
}
|
|
2999
|
-
const value = period as Record<string, unknown>;
|
|
3000
|
-
for (const key of Object.keys(value)) if (key !== "label" && key !== "values")
|
|
3001
|
-
errors.push({ path: `${path}.${key}`, message: `unknown key "${key}"` });
|
|
3002
|
-
if (typeof value.label !== "string") errors.push({ path: `${path}.label`, message: "label must be a string" });
|
|
3003
|
-
if (!Array.isArray(value.values) || value.values.some((entry) => !boundNumber(entry)))
|
|
3004
|
-
errors.push({ path: `${path}.values`, message: "values must be an array of finite numbers or non-empty strings" });
|
|
3005
|
-
});
|
|
3006
|
-
}
|
|
3007
|
-
|
|
3008
|
-
if (
|
|
3009
|
-
j.chartGaugeValue !== undefined &&
|
|
3010
|
-
(!j.chartGaugeValue || typeof j.chartGaugeValue !== "object" || Array.isArray(j.chartGaugeValue))
|
|
3011
|
-
) {
|
|
3012
|
-
errors.push({ path: "$.chartGaugeValue", message: "chartGaugeValue must be an object" });
|
|
3013
|
-
} else if (j.chartGaugeValue && typeof j.chartGaugeValue === "object") {
|
|
3014
|
-
const value = j.chartGaugeValue as Record<string, unknown>;
|
|
3015
|
-
for (const key of Object.keys(value)) if (!["max", "current", "target", "previous", "previousLabel"].includes(key))
|
|
3016
|
-
errors.push({ path: `$.chartGaugeValue.${key}`, message: `unknown key "${key}"` });
|
|
3017
|
-
const finite = (entry: unknown): boolean => typeof entry === "number" && Number.isFinite(entry);
|
|
3018
|
-
if (!finite(value.max) || Number(value.max) <= 0) errors.push({ path: "$.chartGaugeValue.max", message: "max must be a positive finite number" });
|
|
3019
|
-
if (!boundNumber(value.current)) errors.push({ path: "$.chartGaugeValue.current", message: "current must be a finite number or non-empty string" });
|
|
3020
|
-
for (const key of ["target", "previous"] as const) if (value[key] !== undefined && !boundNumber(value[key]))
|
|
3021
|
-
errors.push({ path: `$.chartGaugeValue.${key}`, message: `${key} must be a finite number or non-empty string` });
|
|
3022
|
-
if (value.previousLabel !== undefined && typeof value.previousLabel !== "string")
|
|
3023
|
-
errors.push({ path: "$.chartGaugeValue.previousLabel", message: "previousLabel must be a string" });
|
|
3024
|
-
}
|
|
3025
|
-
}
|
|
3026
|
-
|
|
3027
|
-
/**
|
|
3028
|
-
* JSON の区画の名前を、図の側の綴りへ直す (#2667)。
|
|
3029
|
-
*
|
|
3030
|
-
* 記法と JSON はどちらも `左上` のような語で書き、図の側は `topLeft` で受ける。
|
|
3031
|
-
* 直す先の表は項目の欄と共有する (`区画`) = 同じ区画を 2 通りで呼ばない。
|
|
3032
|
-
*
|
|
3033
|
-
* 中身の無い形は「書かなかった」 と同じにする (`axes` と同じ扱い)。
|
|
3034
|
-
*/
|
|
3035
|
-
function 区画の名前を直す(r: JsonRegions | undefined): DslRegions | undefined {
|
|
3036
|
-
if (r === undefined) return undefined;
|
|
3037
|
-
const 出: DslRegions = {};
|
|
3038
|
-
for (const [語, key] of 区画) {
|
|
3039
|
-
const v = (r as Record<string, unknown>)[語];
|
|
3040
|
-
if (typeof v === "string" && v.trim() !== "") 出[key] = v;
|
|
3041
|
-
}
|
|
3042
|
-
return Object.keys(出).length > 0 ? 出 : undefined;
|
|
3043
|
-
}
|
|
3044
|
-
|
|
3045
|
-
/** 2 軸で仕分ける図の区画の名前を見る (#2667) */
|
|
3046
|
-
function validateRegions(v: unknown, errors: JsonDslError[]): void {
|
|
3047
|
-
if (v === undefined) return;
|
|
3048
|
-
if (!v || typeof v !== "object" || Array.isArray(v)) {
|
|
3049
|
-
errors.push({
|
|
3050
|
-
path: "$.regions",
|
|
3051
|
-
message: 'regions must be a plain object like { "左上": "すぐやる" }',
|
|
3052
|
-
});
|
|
3053
|
-
return;
|
|
3054
|
-
}
|
|
3055
|
-
const o = v as Record<string, unknown>;
|
|
3056
|
-
checkUnknownKeys(o, "regions", "$.regions", errors);
|
|
3057
|
-
表で検査(o, "regions", "$.regions", "regions", errors);
|
|
3058
|
-
}
|
|
3059
|
-
|
|
3060
|
-
/**
|
|
3061
|
-
* 写しを作る時の入れ子の深さの上限。
|
|
3062
|
-
*
|
|
3063
|
-
* 枠の並びの長さが入れ子の深さで決まる。 図の入れ子は深くても数段で、 64 に届く形は書けない。
|
|
3064
|
-
*/
|
|
3065
|
-
const 写しの最大の深さ = 64;
|
|
3066
|
-
|
|
3067
|
-
/**
|
|
3068
|
-
* 写しを作る時に触る値の数の上限。
|
|
3069
|
-
*
|
|
3070
|
-
* **書式の規則ではなく、 資源を使い切らないための歯止め**。 図の書式は値の数を制限していないので、
|
|
3071
|
-
* ここで拒むのは「構造としては正しいが大きすぎる」 入力になる。 だから **正当な入力が届かない
|
|
3072
|
-
* 高さ** に置く。
|
|
3073
|
-
*
|
|
3074
|
-
* 500 万は、 記法の入力の大きさの上限 (`input-size.ts` の 512KB) を全て 2 文字の値で埋めても
|
|
3075
|
-
* 届かない数になる。 JSON でも同じ規模の図が 500 万個の値を持つことはない。
|
|
3076
|
-
*
|
|
3077
|
-
* **数を数えないと守れない** (Round 4 の指摘)。 一度は「写しの大きさは元の入力の大きさで決まる
|
|
3078
|
-
* から数える意味が無い」 として外したが、 これは誤りだった。 Proxy は読まれるたびに新しい object
|
|
3079
|
-
* を返せるため、 **小さな入力から枝を生やせる** (実測 = 1 個の Proxy が深さ 6 / 6 分岐で
|
|
3080
|
-
* 55,987 個の object に膨らんだ)。 深さの上限だけでは横の広がりを止められない。
|
|
3081
|
-
*/
|
|
3082
|
-
const 写しの最大の項目数 = 5_000_000;
|
|
3083
|
-
|
|
3084
|
-
/** 写しを作れなかった理由 (path 付き) */
|
|
3085
|
-
class 写せない extends Error {
|
|
3086
|
-
constructor(
|
|
3087
|
-
readonly path: string,
|
|
3088
|
-
readonly 理由: string,
|
|
3089
|
-
) {
|
|
3090
|
-
super(`${path}: ${理由}`);
|
|
3091
|
-
}
|
|
3092
|
-
}
|
|
3093
|
-
|
|
3094
|
-
/**
|
|
3095
|
-
* 検査の前に 1 度だけ読んで作る、 素のデータの複製 (#1217)。
|
|
3096
|
-
*
|
|
3097
|
-
* 入口は検査する時と図に写す時で同じ項目を 2 度読んでいた。 渡された object が値を返す関数
|
|
3098
|
-
* (getter) を持っていると、 2 度目の読み取りで別の値を返せる = **検査を通った値と図に届く値が
|
|
3099
|
-
* 別物になり、 検査が意味を持たない** (実測 = `animation[0].tween` を 6 回目から
|
|
3100
|
-
* `[NaN, Infinity]` を返す getter にすると、 検査を通って図に `from: null` が届いた)。
|
|
3101
|
-
*
|
|
3102
|
-
* ここで 1 度だけ読んで写しを作り、 以降は写しだけを読む。 各項目の読み取りは 1 回で、
|
|
3103
|
-
* 項目の名前も添字も同じ値を 2 度取りに行かない。
|
|
3104
|
-
*
|
|
3105
|
-
* **`structuredClone` は使わない**。 関数や symbol を含む入力で `DataCloneError` を投げるため、
|
|
3106
|
-
* `validateDragonJson` が約束している「誤りは `{ ok: false, errors }` で返す」 が破れる。
|
|
3107
|
-
* 自前で写せば、 写せない値もそのまま持ち越して検査側の型の判定に落とせる。
|
|
3108
|
-
*
|
|
3109
|
-
* **再帰では書かない** (Round 1 の指摘)。 検査が見ない項目も含めて写すため、 深い入れ子を渡すと
|
|
3110
|
-
* 呼び出しの積み上げが溢れる (実測 = 使わない項目に 20,000 段の入れ子を付けると
|
|
3111
|
-
* `RangeError: Maximum call stack size exceeded`)。 枠を自前で積んで回す。
|
|
3112
|
-
*
|
|
3113
|
-
* **読む順は書いた順のまま、 深さ優先で降りる** (Round 2 / 3 の指摘)。 値を返す関数が副作用を
|
|
3114
|
-
* 持つ入力では読む順が結果に出るため、 再帰で書いた時と同じ順を保つ。 幅優先で回すと、 先に
|
|
3115
|
-
* 書いた兄弟の深い所より後の兄弟の浅い所を先に読む。
|
|
3116
|
-
*
|
|
3117
|
-
* **読み取りの例外も外に出さない** (Round 1 の指摘)。 項目の名前を数える所も値を読む所も、
|
|
3118
|
-
* getter や Proxy が投げれば `validateDragonJson` 自体が throw して約束が破れる。 投げた場所を
|
|
3119
|
-
* path として拾い、 検査の誤りに変える。
|
|
3120
|
-
*
|
|
3121
|
-
* 書き込みは `Object.defineProperty` で行う = `__proto__` を項目名に持つ入力で代入が
|
|
3122
|
-
* prototype の setter に落ちるのを避ける (`JSON.parse` と同じく普通の項目として持つ)。
|
|
3123
|
-
* `__proto__` を書いた時の扱いそのものは `#1184` が持つ。
|
|
3124
|
-
*
|
|
3125
|
-
* 輪 (自分を指す入れ子) は同じ写しを返して止める。 JSON からは作れないが、 object を直接
|
|
3126
|
-
* 渡す経路では作れる。
|
|
3127
|
-
*
|
|
3128
|
-
* ## 守る範囲 (Round 6 で線を引いた)
|
|
3129
|
-
*
|
|
3130
|
-
* この関数が守るのは **自分が確保する量** = 写しの入れ物と、 名前の一覧と、 枠の並び。 いずれも
|
|
3131
|
-
* 上限 (深さ / 数) を見てから作る。
|
|
3132
|
-
*
|
|
3133
|
-
* **渡された側が自分で確保する量は守れない**。 Proxy の `ownKeys` は「名前の並びを返す」 のが
|
|
3134
|
-
* 仕事で、 その並びは trap の中で作られる。 こちらが受け取った時点で既に在るため、 長さを見て
|
|
3135
|
-
* 拒んでも確保そのものは起きた後になる。 これは呼ぶ側の code が確保するもので、 同じ process に
|
|
3136
|
-
* 任意の object を渡せる相手は、 この関数を通さずに同じことができる。
|
|
3137
|
-
*
|
|
3138
|
-
* 線を引くのは、 5 round にわたって「読む前に量を作れる経路」 を潰し続けた末に、 残りが
|
|
3139
|
-
* 呼ぶ側の code の中に移ったため。 潰す対象が自分の外に出た時点で、 この関数の責務ではない。
|
|
3140
|
-
*
|
|
3141
|
-
* **test のために export する** (#1295)。 写しの性質 (深さ / 数の上限 / 読む順 / 値を返す
|
|
3142
|
-
* 関数の扱い) を確かめる検査は、以前は「検査が見ない項目」 に構造をぶら下げて
|
|
3143
|
-
* `validateDragonJson` 越しに見ていた。 知らない項目を誤りにしたことでその足場が無くなり、
|
|
3144
|
-
* かつ写しは検査より前に走るため、検査の結果からは写しの中身を取り出せない。
|
|
3145
|
-
* 検査の対象そのものを直接呼ぶ形にする。
|
|
3146
|
-
*/
|
|
3147
|
-
export function 素のデータに写す(
|
|
3148
|
-
root: unknown,
|
|
3149
|
-
): { ok: true; value: unknown } | { ok: false; error: JsonDslError } {
|
|
3150
|
-
const 写し済 = new WeakMap<object, unknown[] | Record<string, unknown>>();
|
|
3151
|
-
let 項目数 = 0;
|
|
3152
|
-
// 例外を拾った時に「どこを読んでいたか」 を言うために持つ。 投げるのは値を読む所と名前を
|
|
3153
|
-
// 数える所の両方で、 どちらも path を持たないまま外へ出ると `$` としか言えない
|
|
3154
|
-
let 読んでいる場所 = "$";
|
|
3155
|
-
|
|
3156
|
-
/**
|
|
3157
|
-
* まだ中身を埋めていない入れ物と、 その進み具合。
|
|
3158
|
-
*
|
|
3159
|
-
* 配列は名前の並びを持たず長さだけを持つ (Round 6 の指摘)。 添字を文字の並びとして実体化すると、
|
|
3160
|
-
* **上限を見る前にその並びを作ってしまう** (実測 = `new Array(5_000_001)` で 500 万個の添字を
|
|
3161
|
-
* 作ろうとした)。 添字は数から導けるので持つ必要がない。
|
|
3162
|
-
*/
|
|
3163
|
-
type 枠 = {
|
|
3164
|
-
元: object;
|
|
3165
|
-
器: unknown[] | Record<string, unknown>;
|
|
3166
|
-
/** object の時だけ持つ。 配列は `長さ` を使う */
|
|
3167
|
-
名前の並び: string[] | null;
|
|
3168
|
-
長さ: number;
|
|
3169
|
-
次: number;
|
|
3170
|
-
深さ: number;
|
|
3171
|
-
path: string;
|
|
3172
|
-
};
|
|
3173
|
-
|
|
3174
|
-
/** 入れ物だけ作る (ここでは降りない)。 新しく作った時だけ枠を返す */
|
|
3175
|
-
const 器を作る = (v: unknown, 深さ: number, path: string): { 値: unknown; 枠: 枠 | null } => {
|
|
3176
|
-
項目数 += 1;
|
|
3177
|
-
if (項目数 > 写しの最大の項目数) {
|
|
3178
|
-
throw new 写せない(path, `項目が多すぎる (上限 ${写しの最大の項目数})`);
|
|
3179
|
-
}
|
|
3180
|
-
if (v === null || typeof v !== "object") return { 値: v, 枠: null };
|
|
3181
|
-
|
|
3182
|
-
const 既にある = 写し済.get(v);
|
|
3183
|
-
if (既にある !== undefined) return { 値: 既にある, 枠: null };
|
|
3184
|
-
|
|
3185
|
-
if (深さ >= 写しの最大の深さ) {
|
|
3186
|
-
throw new 写せない(path, `入れ子が深すぎる (上限 ${写しの最大の深さ})`);
|
|
3187
|
-
}
|
|
3188
|
-
読んでいる場所 = path;
|
|
3189
|
-
const 並びか = Array.isArray(v);
|
|
3190
|
-
const 器: unknown[] | Record<string, unknown> = 並びか ? [] : {};
|
|
3191
|
-
写し済.set(v, 器);
|
|
3192
|
-
|
|
3193
|
-
if (並びか) {
|
|
3194
|
-
// **長さを先に見てから降りる** (Round 6 の指摘)。 添字を文字の並びとして作ると、 上限を
|
|
3195
|
-
// 見る前にその並びを作ってしまう。 長さは数を読むだけなので何も作らない。
|
|
3196
|
-
//
|
|
3197
|
-
// **長さは正規化してから使う** (Round 7 の指摘)。 `Array.from({ length })` は仕様の
|
|
3198
|
-
// `ToLength` を通しており、 生の値をそのまま使うと 2 つの形で壊れる。
|
|
3199
|
-
//
|
|
3200
|
-
// | `length` が返す値 | 正規化しないと |
|
|
3201
|
-
// |---|---|
|
|
3202
|
-
// | `2.5` | 3 回読む (`Array.from` は 2 要素) |
|
|
3203
|
-
// | `NaN` | 数の合計が `NaN` になり、 上限も終わりも判定できず読み続ける |
|
|
3204
|
-
//
|
|
3205
|
-
// `ToLength` と同じく 0 へ丸め、 0 以上 2^53-1 以下に収める。
|
|
3206
|
-
//
|
|
3207
|
-
// **数に直すのは単項 `+`** (Round 8 の指摘)。 `Number()` は `BigInt` を通してしまうが、
|
|
3208
|
-
// 仕様の `ToNumber` は `TypeError` を投げる = `Array.from({ length: 2n })` は投げる。
|
|
3209
|
-
// 単項 `+` は `ToNumber` そのものなので、 投げる形も含めて元の挙動と揃う (投げた分は
|
|
3210
|
-
// 下の `catch` が検査の誤りに変える)。
|
|
3211
|
-
const 生の長さ = +(v as unknown[]).length;
|
|
3212
|
-
const 長さ = Number.isNaN(生の長さ)
|
|
3213
|
-
? 0
|
|
3214
|
-
: Math.min(Math.max(Math.trunc(生の長さ), 0), Number.MAX_SAFE_INTEGER);
|
|
3215
|
-
項目数 += 長さ;
|
|
3216
|
-
if (項目数 > 写しの最大の項目数) {
|
|
3217
|
-
throw new 写せない(path, `項目が多すぎる (上限 ${写しの最大の項目数})`);
|
|
3218
|
-
}
|
|
3219
|
-
return { 値: 器, 枠: { 元: v, 器, 名前の並び: null, 長さ, 次: 0, 深さ, path } };
|
|
3220
|
-
}
|
|
3221
|
-
|
|
3222
|
-
// **名前も数に入れる** (Round 5 の指摘)。 値を読む前に名前の一覧を作るため、 値だけを
|
|
3223
|
-
// 数えると「名前が 20,000 個ある段を 63 回降りる」 形で 126 万個を並べられる = 上限を
|
|
3224
|
-
// 見る前に資源を使い切れる
|
|
3225
|
-
const 名前の並び = Object.keys(v);
|
|
3226
|
-
項目数 += 名前の並び.length;
|
|
3227
|
-
if (項目数 > 写しの最大の項目数) {
|
|
3228
|
-
throw new 写せない(path, `項目が多すぎる (上限 ${写しの最大の項目数})`);
|
|
3229
|
-
}
|
|
3230
|
-
return { 値: 器, 枠: { 元: v, 器, 名前の並び, 長さ: 名前の並び.length, 次: 0, 深さ, path } };
|
|
3231
|
-
};
|
|
3232
|
-
|
|
3233
|
-
try {
|
|
3234
|
-
const 先頭 = 器を作る(root, 0, "$");
|
|
3235
|
-
const 積み: 枠[] = 先頭.枠 ? [先頭.枠] : [];
|
|
3236
|
-
|
|
3237
|
-
while (積み.length > 0) {
|
|
3238
|
-
const 今 = 積み[積み.length - 1]!;
|
|
3239
|
-
if (今.次 >= 今.長さ) {
|
|
3240
|
-
積み.pop();
|
|
3241
|
-
continue;
|
|
3242
|
-
}
|
|
3243
|
-
// 配列は添字をその場で作る (並びとして持たない)
|
|
3244
|
-
const key = 今.名前の並び === null ? String(今.次) : 今.名前の並び[今.次]!;
|
|
3245
|
-
今.次 += 1;
|
|
3246
|
-
const 子のpath = 今.名前の並び === null ? `${今.path}[${key}]` : `${今.path}.${key}`;
|
|
3247
|
-
|
|
3248
|
-
// 読む直前に場所を控える = 値の読み取りそのものが投げるため、 読んだ後では遅い
|
|
3249
|
-
読んでいる場所 = 子のpath;
|
|
3250
|
-
const 生の値 = (今.元 as Record<string, unknown>)[key];
|
|
3251
|
-
|
|
3252
|
-
const 子 = 器を作る(生の値, 今.深さ + 1, 子のpath);
|
|
3253
|
-
if (Array.isArray(今.器)) 今.器.push(子.値);
|
|
3254
|
-
else {
|
|
3255
|
-
Object.defineProperty(今.器, key, {
|
|
3256
|
-
value: 子.値,
|
|
3257
|
-
enumerable: true,
|
|
3258
|
-
writable: true,
|
|
3259
|
-
configurable: true,
|
|
3260
|
-
});
|
|
3261
|
-
}
|
|
3262
|
-
// 深さ優先で降りる = 次の兄弟を読む前に、 この子の中身を全部読む
|
|
3263
|
-
if (子.枠) 積み.push(子.枠);
|
|
3264
|
-
}
|
|
3265
|
-
return { ok: true, value: 先頭.値 };
|
|
3266
|
-
} catch (e) {
|
|
3267
|
-
if (e instanceof 写せない) {
|
|
3268
|
-
return { ok: false, error: { path: e.path, message: e.理由 } };
|
|
3269
|
-
}
|
|
3270
|
-
// getter / Proxy が投げた形。 約束どおり誤りとして返す (throw しない)
|
|
3271
|
-
return {
|
|
3272
|
-
ok: false,
|
|
3273
|
-
error: {
|
|
3274
|
-
path: 読んでいる場所,
|
|
3275
|
-
message: "入力を読み取れない",
|
|
3276
|
-
hint: e instanceof Error ? e.message : String(e),
|
|
3277
|
-
},
|
|
3278
|
-
};
|
|
3279
|
-
}
|
|
3280
|
-
}
|
|
3281
|
-
|
|
3282
|
-
/**
|
|
3283
|
-
* shape validation。 layer 1 = 必須 field + 型 check、 layer 2 は compile 側の validation に委譲。
|
|
3284
|
-
* fail-fast ではなく全 error 収集して返す (LLM に一括で修正させるため)。
|
|
3285
|
-
*/
|
|
3286
|
-
function validateJson(
|
|
3287
|
-
json: unknown,
|
|
3288
|
-
): { ok: true; data: DragonJson } | { ok: false; errors: JsonDslError[] } {
|
|
3289
|
-
const errors: JsonDslError[] = [];
|
|
3290
|
-
// root の形は写しより先に見る = 形が違う入力には従来どおり `root must be a JSON object` を
|
|
3291
|
-
// 返すため。 写した後に見ると、 root が配列の入力で中の getter が先に動き、 別の誤りに化ける
|
|
3292
|
-
// (Round 3 の指摘)。
|
|
3293
|
-
//
|
|
3294
|
-
// ただし `Array.isArray` は失効した Proxy で `TypeError` を投げる (Round 2 の指摘)。 判定
|
|
3295
|
-
// そのものを受けて、 投げた形は「読み取れない」 として返す。
|
|
3296
|
-
let rootがobjectか: boolean;
|
|
3297
|
-
try {
|
|
3298
|
-
rootがobjectか = !!json && typeof json === "object" && !Array.isArray(json);
|
|
3299
|
-
} catch (e) {
|
|
3300
|
-
return {
|
|
3301
|
-
ok: false,
|
|
3302
|
-
errors: [
|
|
3303
|
-
{
|
|
3304
|
-
path: "$",
|
|
3305
|
-
message: "入力を読み取れない",
|
|
3306
|
-
hint: e instanceof Error ? e.message : String(e),
|
|
3307
|
-
},
|
|
3308
|
-
],
|
|
3309
|
-
};
|
|
3310
|
-
}
|
|
3311
|
-
if (!rootがobjectか) {
|
|
3312
|
-
return { ok: false, errors: [{ path: "$", message: "root must be a JSON object" }] };
|
|
3313
|
-
}
|
|
3314
|
-
|
|
3315
|
-
// 以降は写しだけを読む。 元の object には二度と触らない (#1217)
|
|
3316
|
-
const 写し = 素のデータに写す(json);
|
|
3317
|
-
if (!写し.ok) return { ok: false, errors: [写し.error] };
|
|
3318
|
-
const j = 写し.value as Record<string, unknown>;
|
|
3319
|
-
|
|
3320
|
-
// 知らない項目を先に見る (#1295)。 綴り違いは「書いた項目が効かない」 形で表に出るため、
|
|
3321
|
-
// 個々の型の誤りより先に伝える方が直しやすい
|
|
3322
|
-
checkUnknownKeys(j, "root", "$", errors);
|
|
3323
|
-
// 値を見せる部品の中身を、記法と同じ表で見る (#1374)
|
|
3324
|
-
validateReadouts(j.readouts, errors);
|
|
3325
|
-
validateBands(j.bands, errors);
|
|
3326
|
-
validateLegend(j.legend, errors);
|
|
3327
|
-
validateStageHeader(j.stageHeader, errors);
|
|
3328
|
-
// 読む人が動かすつまみの中身も、記法と同じ表で見る (#1389)
|
|
3329
|
-
validateInputs(j.inputs, errors);
|
|
3330
|
-
// 式は描画側の parser に通す (#1391)
|
|
3331
|
-
validateFormulas(j.formulas, j.inputs, errors);
|
|
3332
|
-
// 押下と巻き上げ (#1393)
|
|
3333
|
-
validateEvents(j.events, errors);
|
|
3334
|
-
validateScrolls(j.scrolls, j.inputs, j.formulas, errors);
|
|
3335
|
-
|
|
3336
|
-
// 値そのものの型は表が見る (#1304)。 図表の箱の上の小見出し (#1247) の空文字は
|
|
3337
|
-
// 「書かなかった」 と同じ扱いにするため通す (記法側の `eyebrow:` と揃える。 落とすのは `jsonToDoc`)
|
|
3338
|
-
表で検査(j, "root", "$", "", errors);
|
|
3339
|
-
validateDiagramShape(j, errors);
|
|
3340
|
-
for (const 欄 of ["ticks", "目盛り"] as const) {
|
|
3341
|
-
if (Array.isArray(j[欄]) && j[欄].length === 0) {
|
|
3342
|
-
errors.push({ path: `$.${欄}`, message: `${欄} must contain at least one label` });
|
|
3343
|
-
}
|
|
3344
|
-
}
|
|
3345
|
-
if (j.ganttToday !== undefined) {
|
|
3346
|
-
const today = j.ganttToday;
|
|
3347
|
-
if (!today || typeof today !== "object" || Array.isArray(today)) {
|
|
3348
|
-
errors.push({ path: "$.ganttToday", message: "ganttToday must be an object" });
|
|
3349
|
-
} else {
|
|
3350
|
-
const value = today as Record<string, unknown>;
|
|
3351
|
-
for (const key of Object.keys(value)) {
|
|
3352
|
-
if (key !== "index" && key !== "label") errors.push({ path: `$.ganttToday.${key}`, message: `unknown key "${key}"` });
|
|
3353
|
-
}
|
|
3354
|
-
if (typeof value.index !== "number" || !Number.isFinite(value.index)) {
|
|
3355
|
-
errors.push({ path: "$.ganttToday.index", message: "ganttToday.index must be a finite number" });
|
|
3356
|
-
}
|
|
3357
|
-
if (typeof value.label !== "string") {
|
|
3358
|
-
errors.push({ path: "$.ganttToday.label", message: "ganttToday.label must be a string" });
|
|
3359
|
-
}
|
|
3360
|
-
}
|
|
3361
|
-
}
|
|
3362
|
-
|
|
3363
|
-
if (!Array.isArray(j.actors) || j.actors.length === 0) {
|
|
3364
|
-
errors.push({ path: "$.actors", message: "actors must be a non-empty array" });
|
|
3365
|
-
} else {
|
|
3366
|
-
j.actors.forEach((a, i) => {
|
|
3367
|
-
if (typeof a === "string") return;
|
|
3368
|
-
if (!a || typeof a !== "object" || Array.isArray(a)) {
|
|
3369
|
-
errors.push({ path: `$.actors[${i}]`, message: "actor must be string or object" });
|
|
3370
|
-
return;
|
|
3371
|
-
}
|
|
3372
|
-
const ao = a as Record<string, unknown>;
|
|
3373
|
-
checkUnknownKeys(ao, "actor", `$.actors[${i}]`, errors);
|
|
3374
|
-
// 外した種類は置き換え先を示す (#2947)。 記法と同じ表を引く
|
|
3375
|
-
const 外した = typeof ao.kind === "string" ? 外した種類の知らせ(ao.kind) : undefined;
|
|
3376
|
-
if (外した !== undefined) {
|
|
3377
|
-
errors.push({ path: `$.actors[${i}].kind`, message: `kind "${String(ao.kind)}" was removed`, hint: 外した.hint });
|
|
3378
|
-
}
|
|
3379
|
-
// 値そのものの型は表が見る (#1304)。 `kind` は見本 (parts) の名前も受けるため
|
|
3380
|
-
// 非空の文字列までしか縛らない (CAR-1657 の unified syntax)
|
|
3381
|
-
表で検査(ao, "actor", `$.actors[${i}]`, "actor", errors);
|
|
3382
|
-
const actorEnum = (key: string, allowed: readonly string[]): void => {
|
|
3383
|
-
const value = ao[key];
|
|
3384
|
-
if (value === undefined || typeof value !== "string") return;
|
|
3385
|
-
if (!allowed.includes(value)) errors.push({ path: `$.actors[${i}].${key}`, message: `${key} must be one of: ${allowed.join(", ")}` });
|
|
3386
|
-
};
|
|
3387
|
-
actorEnum("kindForm", KIND_FORM_VALUES);
|
|
3388
|
-
actorEnum("icon", CDL_ICON_VALUES);
|
|
3389
|
-
actorEnum("titleAlign", TITLE_ALIGN_VALUES);
|
|
3390
|
-
actorEnum("rowMarkForm", ROW_MARK_FORM_VALUES);
|
|
3391
|
-
actorEnum("rowDividerForm", ROW_DIVIDER_FORM_VALUES);
|
|
3392
|
-
if (ao.rowWell !== undefined) {
|
|
3393
|
-
const path = `$.actors[${i}].rowWell`;
|
|
3394
|
-
if (!ao.rowWell || typeof ao.rowWell !== "object" || Array.isArray(ao.rowWell)) {
|
|
3395
|
-
errors.push({ path, message: "rowWell must be an object" });
|
|
3396
|
-
} else {
|
|
3397
|
-
const well = ao.rowWell as Record<string, unknown>;
|
|
3398
|
-
for (const key of Object.keys(well)) {
|
|
3399
|
-
if (!(ROW_WELL_KEYS as readonly string[]).includes(key)) errors.push({ path: `${path}.${key}`, message: `unknown key "${key}"` });
|
|
3400
|
-
}
|
|
3401
|
-
for (const key of ROW_WELL_KEYS) {
|
|
3402
|
-
const value = well[key];
|
|
3403
|
-
if (value !== undefined && (typeof value !== "number" || !Number.isFinite(value) || value < 0)) errors.push({ path: `${path}.${key}`, message: "must be a non-negative finite number" });
|
|
3404
|
-
}
|
|
3405
|
-
}
|
|
3406
|
-
}
|
|
3407
|
-
if (ao.rowLayout !== undefined) {
|
|
3408
|
-
const path = `$.actors[${i}].rowLayout`;
|
|
3409
|
-
if (!ao.rowLayout || typeof ao.rowLayout !== "object" || Array.isArray(ao.rowLayout)) {
|
|
3410
|
-
errors.push({ path, message: "rowLayout must be an object" });
|
|
3411
|
-
} else {
|
|
3412
|
-
const layout = ao.rowLayout as Record<string, unknown>;
|
|
3413
|
-
for (const key of Object.keys(layout)) if (!["titleBaseline", "rowTop", "rowPitch"].includes(key)) errors.push({ path: `${path}.${key}`, message: `unknown key "${key}"` });
|
|
3414
|
-
for (const key of ["titleBaseline", "rowTop", "rowPitch"] as const) {
|
|
3415
|
-
const value = layout[key];
|
|
3416
|
-
if (value !== undefined && (typeof value !== "number" || !Number.isFinite(value) || (key === "rowPitch" ? value <= 0 : value < 0))) errors.push({ path: `${path}.${key}`, message: key === "rowPitch" ? "must be a positive finite number" : "must be a non-negative finite number" });
|
|
3417
|
-
}
|
|
3418
|
-
}
|
|
3419
|
-
}
|
|
3420
|
-
if (ao.figureCard !== undefined) {
|
|
3421
|
-
const path = `$.actors[${i}].figureCard`;
|
|
3422
|
-
if (!ao.figureCard || typeof ao.figureCard !== "object" || Array.isArray(ao.figureCard)) errors.push({ path, message: "figureCard must be an object" });
|
|
3423
|
-
else {
|
|
3424
|
-
const card = ao.figureCard as Record<string, unknown>;
|
|
3425
|
-
for (const key of Object.keys(card)) if (key !== "label" && key !== "note") errors.push({ path: `${path}.${key}`, message: `unknown key "${key}"` });
|
|
3426
|
-
if (typeof card.label !== "string") errors.push({ path: `${path}.label`, message: "label must be a string" });
|
|
3427
|
-
if (card.note !== undefined && typeof card.note !== "string") errors.push({ path: `${path}.note`, message: "note must be a string" });
|
|
3428
|
-
}
|
|
3429
|
-
}
|
|
3430
|
-
if (ao.marks !== undefined && !Array.isArray(ao.marks)) {
|
|
3431
|
-
errors.push({ path: `$.actors[${i}].marks`, message: "marks must be an array" });
|
|
3432
|
-
} else if (Array.isArray(ao.marks)) {
|
|
3433
|
-
ao.marks.forEach((mark, markIndex) => {
|
|
3434
|
-
if (typeof mark === "string") return;
|
|
3435
|
-
const path = `$.actors[${i}].marks[${markIndex}]`;
|
|
3436
|
-
if (!mark || typeof mark !== "object" || Array.isArray(mark)) {
|
|
3437
|
-
errors.push({ path, message: "mark must be a string or object" });
|
|
3438
|
-
return;
|
|
3439
|
-
}
|
|
3440
|
-
const entry = mark as Record<string, unknown>;
|
|
3441
|
-
for (const key of Object.keys(entry)) if (key !== "mark" && key !== "tone") errors.push({ path: `${path}.${key}`, message: `unknown key "${key}"` });
|
|
3442
|
-
if (typeof entry.mark !== "string") errors.push({ path: `${path}.mark`, message: "mark must be a string" });
|
|
3443
|
-
if (entry.tone !== undefined && !(ROW_MARK_TONE_VALUES as readonly unknown[]).includes(entry.tone)) errors.push({ path: `${path}.tone`, message: `tone must be one of: ${ROW_MARK_TONE_VALUES.join(", ")}` });
|
|
3444
|
-
});
|
|
3445
|
-
}
|
|
3446
|
-
if (
|
|
3447
|
-
typeof ao.opportunityPosition === "string" &&
|
|
3448
|
-
!(JOURNEY_OPPORTUNITY_POSITION_VALUES as readonly string[]).includes(ao.opportunityPosition)
|
|
3449
|
-
) {
|
|
3450
|
-
errors.push({
|
|
3451
|
-
path: `$.actors[${i}].opportunityPosition`,
|
|
3452
|
-
message: `opportunityPosition must be one of: ${JOURNEY_OPPORTUNITY_POSITION_VALUES.join(", ")}`,
|
|
3453
|
-
});
|
|
3454
|
-
}
|
|
3455
|
-
for (const 欄 of ["at", "点の位置"] as const) {
|
|
3456
|
-
if (ao[欄] === undefined) continue;
|
|
3457
|
-
const at = ao[欄];
|
|
3458
|
-
if (
|
|
3459
|
-
!Array.isArray(at) ||
|
|
3460
|
-
at.length !== 2 ||
|
|
3461
|
-
at.some((v) => typeof v !== "number" && typeof v !== "string")
|
|
3462
|
-
) {
|
|
3463
|
-
errors.push({
|
|
3464
|
-
path: `$.actors[${i}].${欄}`,
|
|
3465
|
-
message: `actor.${欄} must be [x, y] with number or state reference values`,
|
|
3466
|
-
});
|
|
3467
|
-
}
|
|
3468
|
-
}
|
|
3469
|
-
// 箱の中に描く図形の中身を、記法と同じ表で見る (#1374)
|
|
3470
|
-
validateActorShape(ao.shape, `$.actors[${i}].shape`, errors);
|
|
3471
|
-
// 見本 (parts) にしか効かない項目は、見本でない箱に書かれたら誤りにする (#1294)。
|
|
3472
|
-
// 記法側は読めない項目名として行番号付きで知らせるため、黙って捨てると入口で扱いが変わる。
|
|
3473
|
-
const 見本か = 見本の名前か(ao.kind);
|
|
3474
|
-
// 見本にしか効かない欄を普通の箱に書いた形 (#1294)。 一覧は表が持つ
|
|
3475
|
-
const 代わりに使う欄: Record<(typeof 見本にしか効かない欄)[number], string> = {
|
|
3476
|
-
state: "箱の見た目を変えるなら tone / color を使う",
|
|
3477
|
-
scale: "大きさを変えるなら posW / posH を使う",
|
|
3478
|
-
phase: "普通の箱は自分の段を持たない。 動かす段は animation に書く",
|
|
3479
|
-
};
|
|
3480
|
-
if (!見本か) {
|
|
3481
|
-
for (const 欄 of 見本にしか効かない欄) {
|
|
3482
|
-
if (ao[欄] === undefined) continue;
|
|
3483
|
-
errors.push({
|
|
3484
|
-
path: `$.actors[${i}].${欄}`,
|
|
3485
|
-
message: `actor.${欄} is only for parts (kind must be a parts identifier)`,
|
|
3486
|
-
hint: 代わりに使う欄[欄],
|
|
3487
|
-
});
|
|
3488
|
-
}
|
|
3489
|
-
}
|
|
3490
|
-
// 逆向きも同じく誤りにする (#1308)。 普通の箱にしか効かない欄を見本に書くと、
|
|
3491
|
-
// `jsonToDoc` が丸ごと落として何も起きない
|
|
3492
|
-
if (見本か) {
|
|
3493
|
-
for (const 欄 of 見本に効かない欄) {
|
|
3494
|
-
if (ao[欄] === undefined) continue;
|
|
3495
|
-
errors.push({
|
|
3496
|
-
path: `$.actors[${i}].${欄}`,
|
|
3497
|
-
message: `actor.${欄} is not for parts (kind is a parts identifier)`,
|
|
3498
|
-
hint: `見本の状態を変えるなら state を使う (\`"state": { "${欄}": ... }\`)`,
|
|
3499
|
-
});
|
|
3500
|
-
}
|
|
3501
|
-
}
|
|
3502
|
-
// 値の上書き (`state`) は素の object に限り、中の値も数 / 文字 / 真偽のいずれかに限る。
|
|
3503
|
-
// `{ v: {} }` のような入れ子や null を通すと、値の初期値に扱えない型が入って組み立てが崩れる。
|
|
3504
|
-
if (ao.state !== undefined) {
|
|
3505
|
-
if (!ao.state || typeof ao.state !== "object" || Array.isArray(ao.state)) {
|
|
3506
|
-
errors.push({
|
|
3507
|
-
path: `$.actors[${i}].state`,
|
|
3508
|
-
message: "actor.state must be a plain object",
|
|
3509
|
-
});
|
|
3510
|
-
} else {
|
|
3511
|
-
for (const [sk, sv] of Object.entries(ao.state as Record<string, unknown>)) {
|
|
3512
|
-
const svType = typeof sv;
|
|
3513
|
-
if (svType !== "number" && svType !== "string" && svType !== "boolean") {
|
|
3514
|
-
errors.push({
|
|
3515
|
-
path: `$.actors[${i}].state.${sk}`,
|
|
3516
|
-
message: `actor.state.${sk} must be number / string / boolean (got ${sv === null ? "null" : svType})`,
|
|
3517
|
-
});
|
|
3518
|
-
}
|
|
3519
|
-
}
|
|
3520
|
-
}
|
|
3521
|
-
}
|
|
3522
|
-
// CAR-1693 Phase 1: actor DSL 表面 pos の validation
|
|
3523
|
-
validateLayoutPos(ao.pos, `$.actors[${i}].pos`, errors);
|
|
3524
|
-
// 相対で置く指定 (#2039)
|
|
3525
|
-
validateRelativePos(ao.posRel, `$.actors[${i}].posRel`, errors);
|
|
3526
|
-
});
|
|
3527
|
-
// 解けない相対の指定 (#2039)。 欄ごとの形を見た後に、図全体として解けるかを見る
|
|
3528
|
-
validateRelativeAnchors(j.actors, errors);
|
|
3529
|
-
}
|
|
3530
|
-
if (!Array.isArray(j.flow)) {
|
|
3531
|
-
errors.push({ path: "$.flow", message: "flow must be an array" });
|
|
3532
|
-
} else {
|
|
3533
|
-
j.flow.forEach((s, i) => {
|
|
3534
|
-
if (!s || typeof s !== "object" || Array.isArray(s)) {
|
|
3535
|
-
errors.push({ path: `$.flow[${i}]`, message: "step must be an object" });
|
|
3536
|
-
return;
|
|
3537
|
-
}
|
|
3538
|
-
const so = s as Record<string, unknown>;
|
|
3539
|
-
checkUnknownKeys(so, "step", `$.flow[${i}]`, errors);
|
|
3540
|
-
// 値そのものの型は表が見る (#1304)。 色と線種は engine の一覧と突き合わせる
|
|
3541
|
-
表で検査(so, "step", `$.flow[${i}]`, "step", errors);
|
|
3542
|
-
if (so.labelCollision !== undefined && !(LABEL_COLLISION_VALUES as readonly unknown[]).includes(so.labelCollision)) {
|
|
3543
|
-
errors.push({ path: `$.flow[${i}].labelCollision`, message: `labelCollision must be one of: ${LABEL_COLLISION_VALUES.join(", ")}` });
|
|
3544
|
-
}
|
|
3545
|
-
if (
|
|
3546
|
-
so.labelSize !== undefined &&
|
|
3547
|
-
(!so.labelSize || typeof so.labelSize !== "object" || Array.isArray(so.labelSize))
|
|
3548
|
-
) {
|
|
3549
|
-
errors.push({ path: `$.flow[${i}].labelSize`, message: "labelSize must be an object" });
|
|
3550
|
-
} else if (so.labelSize !== undefined) {
|
|
3551
|
-
const size = so.labelSize as Record<string, unknown>;
|
|
3552
|
-
for (const key of Object.keys(size)) if (!["width", "height", "fontSize"].includes(key)) errors.push({ path: `$.flow[${i}].labelSize.${key}`, message: `unknown key "${key}"` });
|
|
3553
|
-
for (const key of ["width", "height", "fontSize"]) if (size[key] !== undefined && (typeof size[key] !== "number" || !Number.isFinite(size[key]) || size[key] <= 0)) errors.push({ path: `$.flow[${i}].labelSize.${key}`, message: "must be a positive finite number" });
|
|
3554
|
-
}
|
|
3555
|
-
if (so.waypoints !== undefined) {
|
|
3556
|
-
const path = `$.flow[${i}].waypoints`;
|
|
3557
|
-
if (!Array.isArray(so.waypoints)) errors.push({ path, message: "waypoints must be an array" });
|
|
3558
|
-
else {
|
|
3559
|
-
const points: Array<{ x: number; y: number }> = [];
|
|
3560
|
-
so.waypoints.forEach((point, pointIndex) => {
|
|
3561
|
-
const pointPath = `${path}[${pointIndex}]`;
|
|
3562
|
-
if (!point || typeof point !== "object" || Array.isArray(point)) {
|
|
3563
|
-
errors.push({ path: pointPath, message: "waypoint must be an object" });
|
|
3564
|
-
return;
|
|
3565
|
-
}
|
|
3566
|
-
const value = point as Record<string, unknown>;
|
|
3567
|
-
for (const key of Object.keys(value)) if (key !== "x" && key !== "y") errors.push({ path: `${pointPath}.${key}`, message: `unknown key "${key}"` });
|
|
3568
|
-
if (typeof value.x !== "number" || !Number.isFinite(value.x) || typeof value.y !== "number" || !Number.isFinite(value.y)) errors.push({ path: pointPath, message: "waypoint x and y must be finite numbers" });
|
|
3569
|
-
else points.push({ x: value.x, y: value.y });
|
|
3570
|
-
});
|
|
3571
|
-
const ends = [so.posX1, so.posY1, so.posX2, so.posY2];
|
|
3572
|
-
if (ends.every((value) => typeof value === "number" && Number.isFinite(value)) && points.length === so.waypoints.length) {
|
|
3573
|
-
const all = [{ x: so.posX1 as number, y: so.posY1 as number }, ...points, { x: so.posX2 as number, y: so.posY2 as number }];
|
|
3574
|
-
all.forEach((point, pointIndex) => {
|
|
3575
|
-
if (pointIndex > 0 && point.x !== all[pointIndex - 1]!.x && point.y !== all[pointIndex - 1]!.y) errors.push({ path: `${path}[${Math.min(pointIndex - 1, points.length - 1)}]`, message: "each waypoint segment must be orthogonal" });
|
|
3576
|
-
});
|
|
3577
|
-
}
|
|
3578
|
-
}
|
|
3579
|
-
}
|
|
3580
|
-
// CAR-1693 Phase 1: step DSL 表面 pos の validation
|
|
3581
|
-
validateLayoutPos(so.pos, `$.flow[${i}].pos`, errors);
|
|
3582
|
-
});
|
|
3583
|
-
}
|
|
3584
|
-
validateViewport(j.viewport, errors);
|
|
3585
|
-
// 縦列と群は **鍵を利用者が決める** (id)。 表が縛るのはその中の項目
|
|
3586
|
-
//
|
|
3587
|
-
// #1304 まで外側の形すら見ておらず、`lanes: 5` のような値が誤りにならないまま素通りしていた
|
|
3588
|
-
// (形が違えば中の走査ごと飛ばす書き方だったため)。 形が違う値は黙って捨てない
|
|
3589
|
-
validateIdMap(j.lanes, "lanes", "lane", errors, (lane, laneId, path) => {
|
|
3590
|
-
表で検査(lane, "lane", path, `lanes.${laneId}`, errors);
|
|
3591
|
-
// CAR-1693 Phase 1: lane DSL 表面 pos の validation
|
|
3592
|
-
validateLayoutPos(lane.pos, `${path}.pos`, errors);
|
|
3593
|
-
});
|
|
3594
|
-
validateIdMap(j.groups, "groups", "group", errors, (group, groupId, path) => {
|
|
3595
|
-
表で検査(group, "group", path, `groups.${groupId}`, errors);
|
|
3596
|
-
});
|
|
3597
|
-
if (j.animation !== undefined) {
|
|
3598
|
-
if (!Array.isArray(j.animation)) {
|
|
3599
|
-
errors.push({ path: "$.animation", message: "animation must be an array if present" });
|
|
3600
|
-
} else {
|
|
3601
|
-
j.animation.forEach((p, i) => {
|
|
3602
|
-
if (!p || typeof p !== "object" || Array.isArray(p)) {
|
|
3603
|
-
errors.push({ path: `$.animation[${i}]`, message: "phase must be an object" });
|
|
3604
|
-
return;
|
|
3605
|
-
}
|
|
3606
|
-
const po = p as Record<string, unknown>;
|
|
3607
|
-
checkUnknownKeys(po, "phase", `$.animation[${i}]`, errors);
|
|
3608
|
-
// 値そのものの型は表が見る (#1304)
|
|
3609
|
-
表で検査(po, "phase", `$.animation[${i}]`, "phase", errors);
|
|
3610
|
-
validatePhaseMotion(po, i, errors);
|
|
3611
|
-
});
|
|
3612
|
-
}
|
|
3613
|
-
}
|
|
3614
|
-
validateStates(j.states, errors);
|
|
3615
|
-
validateValues(j.values, errors);
|
|
3616
|
-
validateChartPresentationFields(j, errors);
|
|
3617
|
-
validateAxes(j.axes, errors);
|
|
3618
|
-
validateRegions(j.regions, errors);
|
|
3619
|
-
if (errors.length > 0) return { ok: false, errors };
|
|
3620
|
-
return { ok: true, data: j as unknown as DragonJson };
|
|
3621
|
-
}
|
|
3622
|
-
|
|
3623
|
-
/** 最上位の `shape` を記法と同じ図種別の集合で検査する (#2797)。 */
|
|
3624
|
-
function validateDiagramShape(j: Record<string, unknown>, errors: JsonDslError[]): void {
|
|
3625
|
-
if (j.shape === undefined) return;
|
|
3626
|
-
if (typeof j.shape !== "string" || typeof j.type !== "string") return;
|
|
3627
|
-
const 別名 = TYPE_ALIASES.get(j.type);
|
|
3628
|
-
const type = 別名?.type ?? (PRESET_TYPES.has(j.type as PresetType) ? (j.type as PresetType) : null);
|
|
3629
|
-
if (type === null) return;
|
|
3630
|
-
const shape = resolveDiagramShape(j.shape);
|
|
3631
|
-
const 図種の語 = shapesForDiagramType(type);
|
|
3632
|
-
// 語そのものの誤りは `欄の型表` が同じ集合から伝える。 ここでは図種との組だけを見る。
|
|
3633
|
-
if (shape === null) return;
|
|
3634
|
-
if (shape !== null && (type !== "chart" || 図種の語.includes(shape))) return;
|
|
3635
|
-
const hint =
|
|
3636
|
-
図種の語.length > 0
|
|
3637
|
-
? `使える語 = ${図種の語.join(" / ")}`
|
|
3638
|
-
: `shape を書ける図種と語 = ${Object.entries(DIAGRAM_SHAPES)
|
|
3639
|
-
.map(([図種, 語]) => `type: ${図種} (${語.join(" / ")})`)
|
|
3640
|
-
.join(" / ")}`;
|
|
3641
|
-
errors.push({
|
|
3642
|
-
path: "$.shape",
|
|
3643
|
-
message: `shape is not valid for type: ${type}`,
|
|
3644
|
-
hint,
|
|
3645
|
-
});
|
|
3646
|
-
}
|
|
3647
|
-
|
|
3648
|
-
/**
|
|
3649
|
-
* 段の中で値を動かす指定を見る (#1186)。
|
|
3650
|
-
*
|
|
3651
|
-
* 状態名の記法は `states:` と同じ判定を使う。 参照先はこの JSON の `states` だけでは決めない。
|
|
3652
|
-
* 見本や preset が持つ状態を動かす指定もあるためで、記法側と同じく compile 後の図で解決する。
|
|
3653
|
-
*/
|
|
3654
|
-
function validatePhaseMotion(po: Record<string, unknown>, i: number, errors: JsonDslError[]): void {
|
|
3655
|
-
if (po.tween !== undefined) {
|
|
3656
|
-
if (!po.tween || typeof po.tween !== "object" || Array.isArray(po.tween)) {
|
|
3657
|
-
errors.push({
|
|
3658
|
-
path: `$.animation[${i}].tween`,
|
|
3659
|
-
message: "tween must be a plain object of state -> [from, to]",
|
|
3660
|
-
});
|
|
3661
|
-
} else {
|
|
3662
|
-
for (const [name, range] of Object.entries(po.tween as Record<string, unknown>)) {
|
|
3663
|
-
const path = `$.animation[${i}].tween.${name}`;
|
|
3664
|
-
if (!isValueName(name)) errors.push({ path, ...valueNameIssue(name) });
|
|
3665
|
-
if (!Array.isArray(range) || range.length !== 2) {
|
|
3666
|
-
errors.push({ path, message: "tween value must be [from, to]" });
|
|
3667
|
-
continue;
|
|
3668
|
-
}
|
|
3669
|
-
// 補間は数どうしでしか成り立たない。 文字列を通すと描画側が数として読めず
|
|
3670
|
-
// 段の途中が壊れる (記法側も数だけを受ける)
|
|
3671
|
-
for (const v of range) {
|
|
3672
|
-
if (typeof v !== "number" || !Number.isFinite(v)) {
|
|
3673
|
-
errors.push({
|
|
3674
|
-
path,
|
|
3675
|
-
message: "tween value must be finite numbers",
|
|
3676
|
-
hint: `got ${typeof v}`,
|
|
3677
|
-
});
|
|
3678
|
-
break;
|
|
3679
|
-
}
|
|
3680
|
-
}
|
|
3681
|
-
}
|
|
3682
|
-
}
|
|
3683
|
-
}
|
|
3684
|
-
|
|
3685
|
-
if (po.set !== undefined) {
|
|
3686
|
-
if (!po.set || typeof po.set !== "object" || Array.isArray(po.set)) {
|
|
3687
|
-
errors.push({
|
|
3688
|
-
path: `$.animation[${i}].set`,
|
|
3689
|
-
message: "set must be a plain object of state -> value",
|
|
3690
|
-
});
|
|
3691
|
-
} else {
|
|
3692
|
-
for (const [name, value] of Object.entries(po.set as Record<string, unknown>)) {
|
|
3693
|
-
const path = `$.animation[${i}].set.${name}`;
|
|
3694
|
-
if (!isValueName(name)) errors.push({ path, ...valueNameIssue(name) });
|
|
3695
|
-
const t = typeof value;
|
|
3696
|
-
if (t !== "number" && t !== "string") {
|
|
3697
|
-
errors.push({ path, message: "set value must be a number or string", hint: `got ${t}` });
|
|
3698
|
-
} else if (t === "number" && !Number.isFinite(value)) {
|
|
3699
|
-
errors.push({ path, message: "set value must be a finite number" });
|
|
3700
|
-
}
|
|
3701
|
-
}
|
|
3702
|
-
}
|
|
3703
|
-
}
|
|
3704
|
-
}
|
|
3705
|
-
|
|
3706
|
-
/**
|
|
3707
|
-
* 状態の初期値を見る (#1181)。
|
|
3708
|
-
*
|
|
3709
|
-
* 名前の判定は記法と同じものを使う (`value-syntax.ts`)。 別々に持つと、YAML では弾かれる
|
|
3710
|
-
* 名前が JSON では通る形ができ、描画側が `{名前}` を置き換えられない図が生まれる。
|
|
3711
|
-
*/
|
|
3712
|
-
function validateStates(v: unknown, errors: JsonDslError[]): void {
|
|
3713
|
-
if (v === undefined) return;
|
|
3714
|
-
if (!v || typeof v !== "object" || Array.isArray(v)) {
|
|
3715
|
-
errors.push({
|
|
3716
|
-
path: "$.states",
|
|
3717
|
-
message: "states must be a plain object of name -> initial value",
|
|
3718
|
-
});
|
|
3719
|
-
return;
|
|
3720
|
-
}
|
|
3721
|
-
for (const [name, initial] of Object.entries(v as Record<string, unknown>)) {
|
|
3722
|
-
if (!isValueName(name)) {
|
|
3723
|
-
errors.push({ path: `$.states.${name}`, ...valueNameIssue(name) });
|
|
3724
|
-
}
|
|
3725
|
-
const t = typeof initial;
|
|
3726
|
-
if (t !== "number" && t !== "string") {
|
|
3727
|
-
errors.push({
|
|
3728
|
-
path: `$.states.${name}`,
|
|
3729
|
-
message: "state initial must be a number or string",
|
|
3730
|
-
hint: `got ${t}`,
|
|
3731
|
-
});
|
|
3732
|
-
} else if (t === "number" && !Number.isFinite(initial)) {
|
|
3733
|
-
// `NaN` / `Infinity` は JSON には書けないが、object を直接渡す経路では届く。
|
|
3734
|
-
// 描画側は文字列に直して式に流すため、そのまま通すと計算が全て壊れる
|
|
3735
|
-
errors.push({ path: `$.states.${name}`, message: "state initial must be a finite number" });
|
|
3736
|
-
}
|
|
3737
|
-
}
|
|
3738
|
-
}
|
|
3739
|
-
|
|
3740
|
-
/**
|
|
3741
|
-
* 他の値から決まる値を見る (#1181)。
|
|
3742
|
-
*
|
|
3743
|
-
* 名前と式の判定は記法と同じものを使う。 式が文法として正しいかまでは見ない (描画側が
|
|
3744
|
-
* 評価する時に判定して、その値だけを止める = spec § 4.2)。
|
|
3745
|
-
*/
|
|
3746
|
-
function validateValues(v: unknown, errors: JsonDslError[]): void {
|
|
3747
|
-
if (v === undefined) return;
|
|
3748
|
-
if (!v || typeof v !== "object" || Array.isArray(v)) {
|
|
3749
|
-
errors.push({
|
|
3750
|
-
path: "$.values",
|
|
3751
|
-
message: "values must be a plain object of name -> expression",
|
|
3752
|
-
});
|
|
3753
|
-
return;
|
|
3754
|
-
}
|
|
3755
|
-
for (const [name, expression] of Object.entries(v as Record<string, unknown>)) {
|
|
3756
|
-
if (!isValueName(name)) {
|
|
3757
|
-
errors.push({ path: `$.values.${name}`, ...valueNameIssue(name) });
|
|
3758
|
-
}
|
|
3759
|
-
if (typeof expression !== "string" || expression.trim() === "") {
|
|
3760
|
-
errors.push({
|
|
3761
|
-
path: `$.values.${name}`,
|
|
3762
|
-
message: "value expression must be a non-empty string",
|
|
3763
|
-
hint: '`"{inflow} - {done}"` の形で書く',
|
|
3764
|
-
});
|
|
3765
|
-
continue;
|
|
3766
|
-
}
|
|
3767
|
-
for (const issue of checkValueExpression(expression, name)) {
|
|
3768
|
-
errors.push({ path: `$.values.${name}`, ...issue });
|
|
3769
|
-
}
|
|
3770
|
-
}
|
|
3771
|
-
}
|
|
3772
|
-
|
|
3773
|
-
/**
|
|
3774
|
-
* 段の説明を取り出す (#2621)。 受ける語は `PHASE_BODY_KEYS` が持つ。
|
|
3775
|
-
*
|
|
3776
|
-
* **正の語 (`body`) を先に見る**。 表の先頭が正の語なので、並び順のまま最初に値を持つ語を
|
|
3777
|
-
* 採れば「どちらが効くか」 の規則を別に書かなくて済む。
|
|
3778
|
-
*/
|
|
3779
|
-
function 段の説明(p: JsonPhase): string | undefined {
|
|
3780
|
-
for (const key of PHASE_BODY_KEYS) {
|
|
3781
|
-
const v = p[key];
|
|
3782
|
-
if (typeof v === "string") return v;
|
|
3783
|
-
}
|
|
3784
|
-
return undefined;
|
|
3785
|
-
}
|
|
3786
|
-
|
|
3787
|
-
/**
|
|
3788
|
-
* 図表の箱の上の小見出しを、 記法側と同じ形に整える (#1247)。
|
|
3789
|
-
*
|
|
3790
|
-
* 記法は値を `trim()` してから空かどうかを見る。 JSON でも同じ順で見ないと、 空白だけの値が
|
|
3791
|
-
* 入口によって別の意味になる (記法は書かなかった扱い、 JSON は中身のない帯)。
|
|
3792
|
-
*/
|
|
3793
|
-
function 整えた小見出し(v: string | undefined): string | undefined {
|
|
3794
|
-
if (v === undefined) return undefined;
|
|
3795
|
-
const t = v.trim();
|
|
3796
|
-
return t.length > 0 ? t : undefined;
|
|
3797
|
-
}
|
|
3798
|
-
|
|
3799
|
-
/**
|
|
3800
|
-
* 見本の状態の上書きに、段を使うかどうか (`phase`) を混ぜる (#2125)。
|
|
3801
|
-
*
|
|
3802
|
-
* 組み立て側は段を外す指定を状態の上書きの中の `phase` として読む
|
|
3803
|
-
* (`compile/parts.ts` の `段を外す`)。 記法は中括弧に書いた名前をすべて上書きへ入れるため
|
|
3804
|
-
* `{ kind: wave-gauge, phase: false }` がそのまま届くが、JSON は欄ごとに型を宣言する形なので、
|
|
3805
|
-
* 欄として受けたものを同じ場所へ入れ直す。
|
|
3806
|
-
*
|
|
3807
|
-
* **書かなかった時は欄を作らない**。 `{ phase: undefined }` を混ぜると、上書きの有無で
|
|
3808
|
-
* 分岐する側から見て「書いた」 ことになる。
|
|
3809
|
-
*/
|
|
3810
|
-
function 段を含めた上書き(a: JsonActor): Record<string, number | string | boolean> | undefined {
|
|
3811
|
-
if (a.phase === undefined) return a.state;
|
|
3812
|
-
return { ...(a.state ?? {}), phase: a.phase };
|
|
3813
|
-
}
|
|
3814
|
-
|
|
3815
|
-
/**
|
|
3816
|
-
* JSON DSL → DslDocument (AST) 変換。
|
|
3817
|
-
*
|
|
3818
|
-
* 行番号は `行の表` を渡した時だけ入る (#2117)。 JSON そのものは書いた場所を持たないため、
|
|
3819
|
-
* 本文の行が分かる入口 (編集画面の YAML 欄) が場所ごとの行を集めて渡す。 渡さない呼出は
|
|
3820
|
-
* 全ての要素が 0 行になる = 「行が分からない」 ことを 0 で表す。
|
|
3821
|
-
*
|
|
3822
|
-
* CAR-1693 Phase 1: DSL 表面 `pos: {x, y}` → 内部 AST `layoutPos:` の 2 層 mapping の実装 core。
|
|
3823
|
-
* test で mapping logic を実 execute するため export する (pos-field.test.ts の regression guard)。
|
|
3824
|
-
*/
|
|
3825
|
-
export function jsonToDoc(json: DragonJson, 行の表?: 書いた行の表): DslDocument {
|
|
3826
|
-
// 書いた場所から行を引く。 引けない場所は 0 行 = 表を渡さない呼出と同じ状態になる
|
|
3827
|
-
const 位置 = (...道: readonly (string | number)[]): { line: number } => ({
|
|
3828
|
-
line: 行の表?.get(書いた場所の鍵(...道)) ?? 0,
|
|
3829
|
-
});
|
|
3830
|
-
const actors: DslActor[] = json.actors.map((a, i) => {
|
|
3831
|
-
if (typeof a === "string") {
|
|
3832
|
-
return { name: a, kind: "actor" as NodeKind, kindWritten: false, pos: 位置("actors", i) };
|
|
3833
|
-
}
|
|
3834
|
-
// CAR-1657 = kind が既存 NodeKind に無い値なら parts identifier 候補、 partId に格納
|
|
3835
|
-
const kindStr = a.kind ?? "actor";
|
|
3836
|
-
const isPart = 見本の名前か(kindStr);
|
|
3837
|
-
// 外した種類 (#2947) は検査が誤りとして返す。 検査を通さずに呼ばれても描けない種類を
|
|
3838
|
-
// 描画側へ渡さないよう、書かなかった箱と同じ扱いにする
|
|
3839
|
-
const 外した = 外した種類.has(kindStr);
|
|
3840
|
-
// 色は記法と同じ振り分けを通す (#1294)。 `#` で始まれば色番号、それ以外は色の名前。
|
|
3841
|
-
// 別々に書くと、同じ値が入口によって色番号にも色名にもなる
|
|
3842
|
-
const 色 = a.color !== undefined ? splitColorValue(a.color) : {};
|
|
3843
|
-
const 書いた座標 = a.at ?? a.点の位置;
|
|
3844
|
-
return {
|
|
3845
|
-
name: a.name,
|
|
3846
|
-
kind: isPart || 外した ? "actor" : resolveNodeKind(kindStr),
|
|
3847
|
-
// parts 候補は `kind` を `actor` に倒して `partId` へ退避するため、 名札に載せる種類としては
|
|
3848
|
-
// 「書かなかった」 と同じ扱いにする (#1058)
|
|
3849
|
-
kindWritten: a.kind !== undefined && !isPart && !外した,
|
|
3850
|
-
subtitle: a.subtitle,
|
|
3851
|
-
kindForm: isPart ? undefined : a.kindForm,
|
|
3852
|
-
icon: isPart ? undefined : a.icon,
|
|
3853
|
-
emptyRowsNote: isPart ? undefined : a.emptyRowsNote,
|
|
3854
|
-
titleAlign: isPart ? undefined : a.titleAlign,
|
|
3855
|
-
rowRules: isPart ? undefined : a.rowRules,
|
|
3856
|
-
rowStripe: isPart ? undefined : a.rowStripe,
|
|
3857
|
-
rowLayout: isPart ? undefined : a.rowLayout,
|
|
3858
|
-
rowDividerForm: isPart ? undefined : a.rowDividerForm,
|
|
3859
|
-
rowWell: isPart ? undefined : a.rowWell,
|
|
3860
|
-
rowMarkForm: isPart ? undefined : a.rowMarkForm,
|
|
3861
|
-
nestIn: isPart ? undefined : a.nestIn,
|
|
3862
|
-
figureCard: isPart ? undefined : a.figureCard,
|
|
3863
|
-
stationNamePosition: a.stationNamePosition,
|
|
3864
|
-
subtitlePlacement: a.subtitlePlacement,
|
|
3865
|
-
titleFontSize: a.titleFontSize,
|
|
3866
|
-
markGap: a.markGap,
|
|
3867
|
-
eyebrow: a.eyebrow,
|
|
3868
|
-
value: a.value,
|
|
3869
|
-
previous: a.previous,
|
|
3870
|
-
rows: a.rows,
|
|
3871
|
-
// 行頭の印 (#1466)。 行と対で読む。 古い語は記法と同じ関数で読み替える (#2782)
|
|
3872
|
-
marks: a.marks?.map((m) =>
|
|
3873
|
-
typeof m === "string"
|
|
3874
|
-
? 行頭の語へ読み替える(m)
|
|
3875
|
-
: { mark: 行頭の語へ読み替える(m.mark), ...(m.tone === undefined ? {} : { tone: m.tone }) },
|
|
3876
|
-
),
|
|
3877
|
-
stage: isPart ? undefined : a.stage,
|
|
3878
|
-
lane: a.lane,
|
|
3879
|
-
stack: a.stack,
|
|
3880
|
-
initial: a.initial,
|
|
3881
|
-
final: a.final,
|
|
3882
|
-
// 箱の中に描く図形 (#1374)。 見本では状態の上書きが効くため、通常の箱にだけ渡す
|
|
3883
|
-
shape: isPart ? undefined : a.shape,
|
|
3884
|
-
visibleIf: isPart ? undefined : a.visibleIf,
|
|
3885
|
-
title: isPart ? undefined : a.title,
|
|
3886
|
-
// 値に追随する 5 欄 (#1392)。 記法と同じく見本には渡さない
|
|
3887
|
-
wBind: isPart ? undefined : a.wBind,
|
|
3888
|
-
hBind: isPart ? undefined : a.hBind,
|
|
3889
|
-
opacity: isPart ? undefined : a.opacity,
|
|
3890
|
-
renderOffsetX: isPart ? undefined : a.renderOffsetX,
|
|
3891
|
-
renderOffsetY: isPart ? undefined : a.renderOffsetY,
|
|
3892
|
-
// 普通の箱にしか効かない欄は見本では落とす。 落とす欄の一覧は `見本に効かない欄` が
|
|
3893
|
-
// 唯一の出どころで、検査 (#1308) も同じ表を見る = 「検査は通すが組み立てが捨てる」
|
|
3894
|
-
// 状態が作れない
|
|
3895
|
-
...(isPart
|
|
3896
|
-
? {}
|
|
3897
|
-
: {
|
|
3898
|
-
tone: resolveTone(a.tone) ?? 色.tone,
|
|
3899
|
-
owner: a.owner,
|
|
3900
|
-
end: a.end,
|
|
3901
|
-
startLabel: a.startLabel,
|
|
3902
|
-
milestone: a.milestone,
|
|
3903
|
-
emphasis: a.emphasis,
|
|
3904
|
-
at:
|
|
3905
|
-
書いた座標 === undefined
|
|
3906
|
-
? undefined
|
|
3907
|
-
: { x: 書いた座標[0], y: 書いた座標[1], raw: `[${書いた座標.join(", ")}]` },
|
|
3908
|
-
atPos: 書いた座標 === undefined ? undefined : 位置("actors", i, a.at ? "at" : "点の位置"),
|
|
3909
|
-
touchpoint: a.touchpoint,
|
|
3910
|
-
opportunity: a.opportunity,
|
|
3911
|
-
opportunityPosition: a.opportunityPosition,
|
|
3912
|
-
}),
|
|
3913
|
-
colorHex: 色.hex,
|
|
3914
|
-
// 部品に書いた色の名前は効かない。 記法と同じく書いた名前として残し、組み立て側が知らせる (#1973)
|
|
3915
|
-
partColorName: isPart ? 色.tone : undefined,
|
|
3916
|
-
// 絶対座標と大きさ (#1294)。 `pos` (ずらし幅) とは別経路
|
|
3917
|
-
posX: a.posX,
|
|
3918
|
-
posY: a.posY,
|
|
3919
|
-
posW: a.posW,
|
|
3920
|
-
posH: a.posH,
|
|
3921
|
-
// 倍率は見本にしか効かない (検査が見本でない箱を弾く)。 書かれた名前は JSON では
|
|
3922
|
-
// 常に `scale` で、記法の別名 (`倍率`) は JSON に持ち込まない
|
|
3923
|
-
scale: isPart ? a.scale : undefined,
|
|
3924
|
-
scaleKeys: isPart && a.scale !== undefined ? ["scale"] : undefined,
|
|
3925
|
-
partId: isPart ? kindStr : undefined,
|
|
3926
|
-
stateOverride: isPart ? 段を含めた上書き(a) : undefined,
|
|
3927
|
-
// CAR-1693 Phase 1: DSL 表面 pos → 内部 AST layoutPos の 2 層 mapping (naming collision 回避)
|
|
3928
|
-
layoutPos: a.pos,
|
|
3929
|
-
// 相対で置く指定 (#2039)。 欄の名前が内部と同じなのでそのまま渡す
|
|
3930
|
-
posRel: a.posRel,
|
|
3931
|
-
pos: 位置("actors", i),
|
|
3932
|
-
};
|
|
3933
|
-
});
|
|
3934
|
-
// 図の意匠 (#1553 / #2790)。 正の語と別名を別々に解き、順番に依らず `theme` を優先する
|
|
3935
|
-
const 意匠 = json.theme === undefined ? null : resolveTheme(json.theme);
|
|
3936
|
-
const 別名の意匠 = json.palette === undefined ? null : resolveTheme(json.palette);
|
|
3937
|
-
const 選んだ意匠 = 意匠 ?? 別名の意匠;
|
|
3938
|
-
const 並び順 = json.order === undefined ? null : resolveOrder(json.order);
|
|
3939
|
-
const 形 = json.shape === undefined ? null : resolveDiagramShape(json.shape);
|
|
3940
|
-
const legend = json.legend?.flatMap((item) => {
|
|
3941
|
-
if (item.mark === undefined) return [item];
|
|
3942
|
-
const mark = resolveLegendMark(item.mark);
|
|
3943
|
-
return mark === null ? [] : [{ ...item, mark } as CdlLegendItem];
|
|
3944
|
-
});
|
|
3945
|
-
|
|
3946
|
-
const flow: DslStep[] = json.flow.map((s, i) => ({
|
|
3947
|
-
no: i + 1,
|
|
3948
|
-
from: s.from,
|
|
3949
|
-
to: s.to,
|
|
3950
|
-
label: s.label,
|
|
3951
|
-
sub: s.sub,
|
|
3952
|
-
side: s.side,
|
|
3953
|
-
fromSide: s.fromSide,
|
|
3954
|
-
toSide: s.toSide,
|
|
3955
|
-
// 矢印の先の形 (#1462)。 読めない語は組み立てが落とす
|
|
3956
|
-
head: s.head,
|
|
3957
|
-
// 端の印の残り 3 欄と、関係の語 / 言づての種類 (#1466)
|
|
3958
|
-
tailHead: s.tailHead,
|
|
3959
|
-
headFill: s.headFill,
|
|
3960
|
-
tailHeadFill: s.tailHeadFill,
|
|
3961
|
-
// UML 関係の出どころ側の多重度 (#1771 / #2783)
|
|
3962
|
-
tailSub: s.tailSub,
|
|
3963
|
-
relation: s.relation,
|
|
3964
|
-
// 辺の役目と名前の下地 (cdl#618)
|
|
3965
|
-
role: s.role,
|
|
3966
|
-
labelPlate: s.labelPlate,
|
|
3967
|
-
msgKind: s.kind,
|
|
3968
|
-
// 箱と同じ読み替えを通す (#1304)。 通さないと `tone: "成功"` が色名として解決されないまま
|
|
3969
|
-
// 図に届き、同じ値が箱では色になり矢印では色にならない
|
|
3970
|
-
tone: resolveTone(s.tone),
|
|
3971
|
-
style: s.style,
|
|
3972
|
-
guard: s.guard,
|
|
3973
|
-
cardinality: s.cardinality,
|
|
3974
|
-
// 値に追随する 3 欄 (#1396)
|
|
3975
|
-
widthBind: s.widthBind,
|
|
3976
|
-
strokeBind: s.strokeBind,
|
|
3977
|
-
dashOffsetBind: s.dashOffsetBind,
|
|
3978
|
-
labelOffsetX: s.labelOffsetX,
|
|
3979
|
-
labelOffsetY: s.labelOffsetY,
|
|
3980
|
-
posX1: s.posX1,
|
|
3981
|
-
posY1: s.posY1,
|
|
3982
|
-
posX2: s.posX2,
|
|
3983
|
-
posY2: s.posY2,
|
|
3984
|
-
waypoints: s.waypoints,
|
|
3985
|
-
labelCollision: s.labelCollision,
|
|
3986
|
-
labelSize: s.labelSize,
|
|
3987
|
-
overlay: s.overlay,
|
|
3988
|
-
// 部品の端で繋ぐ要素の id (#1979)。 書かない矢印には欄を足さない = 記法と同じ形になる
|
|
3989
|
-
...(s.fromPartNode !== undefined ? { fromPartNode: s.fromPartNode } : {}),
|
|
3990
|
-
...(s.toPartNode !== undefined ? { toPartNode: s.toPartNode } : {}),
|
|
3991
|
-
// CAR-1693 Phase 1: DSL 表面 pos → 内部 AST layoutPos
|
|
3992
|
-
layoutPos: s.pos,
|
|
3993
|
-
pos: 位置("flow", i),
|
|
3994
|
-
}));
|
|
3995
|
-
// 状態は段が無くても図に載る (#1162 で組み立ての出口が載せる)。 **段の有無で分けない** =
|
|
3996
|
-
// 分けると `states` だけを書いた JSON で値が 1 つも届かない (記法側で起きていた形、 #1181)
|
|
3997
|
-
// 値を見せる部品はそのまま渡す (#1374)。 形は描画側の型が縛る
|
|
3998
|
-
const readouts: DslReadout[] | undefined = json.readouts ? [...json.readouts] : undefined;
|
|
3999
|
-
// つまみもそのまま渡す (#1389)。 形は描画側の型が縛る
|
|
4000
|
-
const inputs: DslInput[] | undefined = json.inputs ? [...json.inputs] : undefined;
|
|
4001
|
-
/*
|
|
4002
|
-
* 押下と巻き上げも記法側と同じ形へ写す (#1393)。
|
|
4003
|
-
*
|
|
4004
|
-
* 相手は名前のまま持ち、識別子への読み替えは組み立てが行う = 2 つの入口で同じ経路を通る。
|
|
4005
|
-
*/
|
|
4006
|
-
const events: DslEventBinding[] | undefined = json.events?.map((e, i) => ({
|
|
4007
|
-
event: e.on as DslEventBinding["event"],
|
|
4008
|
-
target:
|
|
4009
|
-
e.diagram === true
|
|
4010
|
-
? ({ kind: "diagram" } as const)
|
|
4011
|
-
: e.arrow !== undefined
|
|
4012
|
-
? ({
|
|
4013
|
-
kind: "edge" as const,
|
|
4014
|
-
from: e.arrow.split("->")[0]?.trim() ?? "",
|
|
4015
|
-
to: e.arrow.split("->")[1]?.trim() ?? "",
|
|
4016
|
-
} as const)
|
|
4017
|
-
: e.lane !== undefined
|
|
4018
|
-
? ({ kind: "lane" as const, name: e.lane } as const)
|
|
4019
|
-
: ({ kind: "node" as const, name: e.box ?? "" } as const),
|
|
4020
|
-
handlerId: e.handler.trim(),
|
|
4021
|
-
pos: 位置("events", i),
|
|
4022
|
-
}));
|
|
4023
|
-
const scrolls: DslScrollTrigger[] | undefined = json.scrolls
|
|
4024
|
-
? Object.entries(json.scrolls).map(([id, spec]) => ({ id, ...spec }))
|
|
4025
|
-
: undefined;
|
|
4026
|
-
// 式は `{ 名前: "式" }` から並びへ写す (#1391)。 記法側と同じ形にして組み立てを 1 本にする。
|
|
4027
|
-
// 組の形 (`{ expression, label }`) は名札を持ったまま写す (#1916)
|
|
4028
|
-
const formulas: DslFormula[] | undefined = json.formulas
|
|
4029
|
-
? Object.entries(json.formulas).map(([id, 値]) =>
|
|
4030
|
-
typeof 値 === "string"
|
|
4031
|
-
? { id, expression: 値, pos: 位置("formulas", id) }
|
|
4032
|
-
: {
|
|
4033
|
-
id,
|
|
4034
|
-
expression: 値.expression,
|
|
4035
|
-
...(値.label !== undefined ? { label: 値.label } : {}),
|
|
4036
|
-
pos: 位置("formulas", id),
|
|
4037
|
-
},
|
|
4038
|
-
)
|
|
4039
|
-
: undefined;
|
|
4040
|
-
const states: DslState[] = Object.entries(json.states ?? {}).map(([name, initial]) => ({
|
|
4041
|
-
name,
|
|
4042
|
-
initial,
|
|
4043
|
-
pos: 位置("states", name),
|
|
4044
|
-
}));
|
|
4045
|
-
const phases: DslPhase[] = (json.animation ?? []).map((p, i) => ({
|
|
4046
|
-
name: p.step,
|
|
4047
|
-
durationMs: Math.round((p.duration ?? 1.4) * 1000),
|
|
4048
|
-
highlight: p.focus,
|
|
4049
|
-
body: 段の説明(p),
|
|
4050
|
-
badge: p.badge,
|
|
4051
|
-
// 書いた段だけが欄を持つ。 空文字を置くと「書いた」 と「書いていない」 が同じ形になる
|
|
4052
|
-
...(p.draw !== undefined ? { draw: p.draw, drawPos: 位置("animation", i, "draw") } : {}),
|
|
4053
|
-
// 割合は `draw` がある段にだけ写す。 単独で書いても描く相手が決まらず何も起きないので、
|
|
4054
|
-
// 記法側 (同じ行に書かせる形) と同じ状態に揃える
|
|
4055
|
-
...(p.draw !== undefined && p.drawRatio !== undefined ? { drawRatio: p.drawRatio } : {}),
|
|
4056
|
-
// 段の中で動かす分 (#1186)。 記法側の `tweens` / `sets` と同じ形に写す。
|
|
4057
|
-
// 空の配列を置かないのは、記法側が「無ければ field ごと持たない」 形だから
|
|
4058
|
-
...(p.tween && Object.keys(p.tween).length > 0
|
|
4059
|
-
? {
|
|
4060
|
-
tweens: Object.entries(p.tween).map(([state, [from, to]]) => ({
|
|
4061
|
-
state,
|
|
4062
|
-
from,
|
|
4063
|
-
to,
|
|
4064
|
-
pos: 位置("animation", i, "tween", state),
|
|
4065
|
-
})),
|
|
4066
|
-
}
|
|
4067
|
-
: {}),
|
|
4068
|
-
...(p.set && Object.keys(p.set).length > 0
|
|
4069
|
-
? {
|
|
4070
|
-
sets: Object.entries(p.set).map(([state, value]) => ({
|
|
4071
|
-
state,
|
|
4072
|
-
value,
|
|
4073
|
-
pos: 位置("animation", i, "set", state),
|
|
4074
|
-
})),
|
|
4075
|
-
}
|
|
4076
|
-
: {}),
|
|
4077
|
-
pos: 位置("animation", i),
|
|
4078
|
-
}));
|
|
4079
|
-
const animate: DslAnimate | undefined =
|
|
4080
|
-
states.length > 0 || phases.length > 0
|
|
4081
|
-
? // 動きの塊そのものには書いた場所が無い。 段がある文書は `animation`、無い文書は
|
|
4082
|
-
// `states` を指す = どちらも「動きを書き始めた場所」 になる
|
|
4083
|
-
{ states, phases, pos: phases.length > 0 ? 位置("animation") : 位置("states") }
|
|
4084
|
-
: undefined;
|
|
4085
|
-
// **別名は記法と同じ表で開く** (#2655 / #2657)。 開いた後は古い綴りが残らない。
|
|
4086
|
-
//
|
|
4087
|
-
// **開く語を 1 つずつ書く** = 表の値をそのまま広げると、持たない語が `undefined` の
|
|
4088
|
-
// まま欄に載り、後ろの `...(形 !== null ? ...)` を打ち消す (実測 = `type: "pie"` の
|
|
4089
|
-
// JSON が形を持たず、書かない時の形 (棒) の図になった)
|
|
4090
|
-
const 別名 = TYPE_ALIASES.get(json.type);
|
|
4091
|
-
const form = json.form ?? json.見せ方;
|
|
4092
|
-
const ticks = json.ticks ?? json.目盛り;
|
|
4093
|
-
return {
|
|
4094
|
-
title: json.title,
|
|
4095
|
-
type: 別名?.type ?? (json.type as PresetType),
|
|
4096
|
-
...(form !== undefined
|
|
4097
|
-
? { form, formPos: 位置(json.form !== undefined ? "form" : "見せ方") }
|
|
4098
|
-
: {}),
|
|
4099
|
-
...(ticks !== undefined
|
|
4100
|
-
? { ticks: [...ticks], ticksPos: 位置(json.ticks !== undefined ? "ticks" : "目盛り") }
|
|
4101
|
-
: {}),
|
|
4102
|
-
...(json.ganttToday !== undefined ? { ganttToday: json.ganttToday } : {}),
|
|
4103
|
-
...(json.ganttTickLabels !== undefined ? { ganttTickLabels: [...json.ganttTickLabels] } : {}),
|
|
4104
|
-
...(json.ganttBarEnd !== undefined ? { ganttBarEnd: json.ganttBarEnd } : {}),
|
|
4105
|
-
...(json.ganttBarThickness !== undefined ? { ganttBarThickness: json.ganttBarThickness } : {}),
|
|
4106
|
-
...(json.chartSeriesSkipMuted !== undefined ? { chartSeriesSkipMuted: json.chartSeriesSkipMuted } : {}),
|
|
4107
|
-
...(json.chartLineSeries !== undefined ? { chartLineSeries: json.chartLineSeries } : {}),
|
|
4108
|
-
...(json.chartPieCenterLabel !== undefined ? { chartPieCenterLabel: json.chartPieCenterLabel } : {}),
|
|
4109
|
-
...(json.chartPieTableColumns !== undefined ? { chartPieTableColumns: json.chartPieTableColumns } : {}),
|
|
4110
|
-
...(json.chartPieRingWidth !== undefined ? { chartPieRingWidth: json.chartPieRingWidth } : {}),
|
|
4111
|
-
...(json.chartSlopePeriods !== undefined ? { chartSlopePeriods: json.chartSlopePeriods } : {}),
|
|
4112
|
-
...(json.chartSlopeEmphasisIds !== undefined ? { chartSlopeEmphasisIds: json.chartSlopeEmphasisIds } : {}),
|
|
4113
|
-
...(json.chartSlopeUnit !== undefined ? { chartSlopeUnit: json.chartSlopeUnit } : {}),
|
|
4114
|
-
...(json.chartStackedPeriods !== undefined ? { chartStackedPeriods: json.chartStackedPeriods } : {}),
|
|
4115
|
-
...(json.chartStackedRateId !== undefined ? { chartStackedRateId: json.chartStackedRateId } : {}),
|
|
4116
|
-
...(json.chartStackedLegendPosition !== undefined ? { chartStackedLegendPosition: json.chartStackedLegendPosition } : {}),
|
|
4117
|
-
...(json.chartGaugeValue !== undefined ? { chartGaugeValue: json.chartGaugeValue } : {}),
|
|
4118
|
-
...(json.chartWaffleLegendPosition !== undefined ? { chartWaffleLegendPosition: json.chartWaffleLegendPosition } : {}),
|
|
4119
|
-
...(json.funnelForm !== undefined ? { funnelForm: json.funnelForm } : {}),
|
|
4120
|
-
...(json.funnelRate !== undefined ? { funnelRate: json.funnelRate } : {}),
|
|
4121
|
-
...(json.quadrantPointLabelSide !== undefined ? { quadrantPointLabelSide: json.quadrantPointLabelSide } : {}),
|
|
4122
|
-
...(json.treeNodeForm !== undefined ? { treeNodeForm: json.treeNodeForm } : {}),
|
|
4123
|
-
...(json.treeSize !== undefined ? { treeSize: json.treeSize } : {}),
|
|
4124
|
-
...(json.treeEdgeTone !== undefined ? { treeEdgeTone: json.treeEdgeTone } : {}),
|
|
4125
|
-
...(json.treeEdgeHead !== undefined ? { treeEdgeHead: json.treeEdgeHead } : {}),
|
|
4126
|
-
...(json.mindForm !== undefined ? { mindForm: json.mindForm } : {}),
|
|
4127
|
-
...(json.mindSize !== undefined ? { mindSize: json.mindSize } : {}),
|
|
4128
|
-
...(json.edgeEndGap !== undefined ? { edgeEndGap: json.edgeEndGap } : {}),
|
|
4129
|
-
...(json.journeyForm !== undefined ? { journeyForm: json.journeyForm } : {}),
|
|
4130
|
-
...(json.journeyLineForm !== undefined ? { journeyLineForm: json.journeyLineForm } : {}),
|
|
4131
|
-
...(json.journeyLabels !== undefined ? { journeyLabels: json.journeyLabels } : {}),
|
|
4132
|
-
...(json.journeySize !== undefined ? { journeySize: json.journeySize } : {}),
|
|
4133
|
-
...(json.sequenceActorForm !== undefined ? { sequenceActorForm: json.sequenceActorForm } : {}),
|
|
4134
|
-
...(json.sequenceLabelForm !== undefined ? { sequenceLabelForm: json.sequenceLabelForm } : {}),
|
|
4135
|
-
...(json.sequenceReturnHead !== undefined ? { sequenceReturnHead: json.sequenceReturnHead } : {}),
|
|
4136
|
-
...(json.sequenceKindLabels !== undefined ? { sequenceKindLabels: json.sequenceKindLabels } : {}),
|
|
4137
|
-
...(json.sequenceRowGap !== undefined ? { sequenceRowGap: json.sequenceRowGap } : {}),
|
|
4138
|
-
...(json.sequenceGutter !== undefined ? { sequenceGutter: json.sequenceGutter } : {}),
|
|
4139
|
-
...(json.sequenceBandWidth !== undefined ? { sequenceBandWidth: json.sequenceBandWidth } : {}),
|
|
4140
|
-
...(json.sequenceSelfLoop !== undefined ? { sequenceSelfLoop: json.sequenceSelfLoop } : {}),
|
|
4141
|
-
...(json.figureCard !== undefined ? { figureCard: json.figureCard } : {}),
|
|
4142
|
-
...(json.figureSize !== undefined ? { figureSize: json.figureSize } : {}),
|
|
4143
|
-
...(json.terminalDecoration !== undefined ? { terminalDecoration: json.terminalDecoration } : {}),
|
|
4144
|
-
...(json.cornerForm !== undefined ? { cornerForm: json.cornerForm } : {}),
|
|
4145
|
-
...(json.figureKindLabel !== undefined ? { figureKindLabel: json.figureKindLabel } : {}),
|
|
4146
|
-
...(json.figureInset !== undefined ? { figureInset: json.figureInset } : {}),
|
|
4147
|
-
...(json.chartBarSize !== undefined ? { chartBarSize: json.chartBarSize } : {}),
|
|
4148
|
-
...(json.chartBarShape !== undefined ? { chartBarShape: json.chartBarShape } : {}),
|
|
4149
|
-
...(json.chartLineSize !== undefined ? { chartLineSize: json.chartLineSize } : {}),
|
|
4150
|
-
...(json.chartPieSize !== undefined ? { chartPieSize: json.chartPieSize } : {}),
|
|
4151
|
-
...(json.chartWaffleSize !== undefined ? { chartWaffleSize: json.chartWaffleSize } : {}),
|
|
4152
|
-
...(json.chartSlopeLabelTemplate !== undefined ? { chartSlopeLabelTemplate: json.chartSlopeLabelTemplate } : {}),
|
|
4153
|
-
...(json.chartSlopeSize !== undefined ? { chartSlopeSize: json.chartSlopeSize } : {}),
|
|
4154
|
-
...(json.chartStackedLayout !== undefined ? { chartStackedLayout: json.chartStackedLayout } : {}),
|
|
4155
|
-
...(json.chartGaugeSize !== undefined ? { chartGaugeSize: json.chartGaugeSize } : {}),
|
|
4156
|
-
...(json.ganttSize !== undefined ? { ganttSize: json.ganttSize } : {}),
|
|
4157
|
-
...(json.quadrantLabelPosition !== undefined ? { quadrantLabelPosition: json.quadrantLabelPosition } : {}),
|
|
4158
|
-
...(json.quadrantSize !== undefined ? { quadrantSize: json.quadrantSize } : {}),
|
|
4159
|
-
...(別名?.shape !== undefined ? { shape: 別名.shape, shapePos: 位置("type") } : {}),
|
|
4160
|
-
// 前後の空白を落としてから見る。 記法側 (`v05/parser.ts`) が `trim()` してから
|
|
4161
|
-
// 空かどうかを判定するため、 揃えないと **空白だけの値で入口ごとに図が変わる**
|
|
4162
|
-
// (記法は書かなかった扱い、 JSON は中身のない帯を描く。 Round 2 の指摘で実測)
|
|
4163
|
-
...(整えた小見出し(json.eyebrow) !== undefined
|
|
4164
|
-
? { eyebrow: 整えた小見出し(json.eyebrow), eyebrowPos: 位置("eyebrow") }
|
|
4165
|
-
: {}),
|
|
4166
|
-
// 2 軸で仕分ける図の軸の名前 (#1294)。 中身の無い形は「書かなかった」 と同じにする =
|
|
4167
|
-
// 空の軸を渡すと、書いていない側の名前が空文字で描かれる (記法側と同じ扱い)
|
|
4168
|
-
...(json.axes && (json.axes.x !== undefined || json.axes.y !== undefined)
|
|
4169
|
-
? { axes: json.axes, axesPos: 位置("axes") }
|
|
4170
|
-
: {}),
|
|
4171
|
-
// 区画の名前 (#2667)。 鍵は記法と同じ 4 語なので、描画側の綴りへここで直す
|
|
4172
|
-
...(区画の名前を直す(json.regions) === undefined
|
|
4173
|
-
? {}
|
|
4174
|
-
: { regions: 区画の名前を直す(json.regions) }),
|
|
4175
|
-
...(json.legendFontSize !== undefined ? { legendFontSize: json.legendFontSize } : {}),
|
|
4176
|
-
...(json.legendOffset !== undefined ? { legendOffset: json.legendOffset } : {}),
|
|
4177
|
-
...(json.stageHeader !== undefined ? { stageHeader: json.stageHeader } : {}),
|
|
4178
|
-
...(json.stageLayout !== undefined ? { stageLayout: json.stageLayout } : {}),
|
|
4179
|
-
actors,
|
|
4180
|
-
flow,
|
|
4181
|
-
animate,
|
|
4182
|
-
// 他の値から決まる値 (#1181)。 書いた順に並べる = 解く順は参照から決まるので順序に
|
|
4183
|
-
// 意味は無いが、知らせの並びが書いた順になる
|
|
4184
|
-
values: json.values
|
|
4185
|
-
? Object.entries(json.values).map(([name, expression]) => ({
|
|
4186
|
-
name,
|
|
4187
|
-
expression,
|
|
4188
|
-
pos: 位置("values", name),
|
|
4189
|
-
}))
|
|
4190
|
-
: undefined,
|
|
4191
|
-
viewport: json.viewport ? { ...json.viewport, pos: 位置("viewport") } : undefined,
|
|
4192
|
-
lanes: json.lanes
|
|
4193
|
-
? Object.fromEntries(
|
|
4194
|
-
Object.entries(json.lanes).map(([id, l]) => {
|
|
4195
|
-
// CAR-1693 Phase 1: DSL 表面 pos → 内部 AST layoutPos の 2 層 mapping。
|
|
4196
|
-
// JSON input の { pos, x, width, ... } を分離し、 pos のみ layoutPos に rename する。
|
|
4197
|
-
const { pos: layoutPos, ...laneRest } = l;
|
|
4198
|
-
return [id, { id, ...laneRest, layoutPos, pos: 位置("lanes", id) }];
|
|
4199
|
-
}),
|
|
4200
|
-
)
|
|
4201
|
-
: undefined,
|
|
4202
|
-
groups: json.groups
|
|
4203
|
-
? Object.fromEntries(
|
|
4204
|
-
Object.entries(json.groups).map(([id, g]) => [
|
|
4205
|
-
id,
|
|
4206
|
-
{ id, label: g.label, lanes: g.lanes, pos: 位置("groups", id) },
|
|
4207
|
-
]),
|
|
4208
|
-
)
|
|
4209
|
-
: undefined,
|
|
4210
|
-
readouts,
|
|
4211
|
-
inputs,
|
|
4212
|
-
formulas,
|
|
4213
|
-
events,
|
|
4214
|
-
scrolls,
|
|
4215
|
-
// 順序図で面が動いている間の帯 (#1466)
|
|
4216
|
-
bands: json.bands,
|
|
4217
|
-
// 矢印をいつ出すか (#1470)
|
|
4218
|
-
reveal: json.reveal,
|
|
4219
|
-
// 箱に触れると関係する線だけを光らせるか (#1757)
|
|
4220
|
-
relations: json.relations,
|
|
4221
|
-
// 図の並ぶ向き (#1494)。 JSON は英語で書くので、記法と同じ語に直してから渡す。
|
|
4222
|
-
// 書いた行も記法と同じく持つ = 持たないと、向きの知らせが文書の行 (1 行目) を指す
|
|
4223
|
-
...(json.direction !== undefined
|
|
4224
|
-
? {
|
|
4225
|
-
direction: json.direction === "horizontal" ? ("横" as const) : ("縦" as const),
|
|
4226
|
-
directionPos: 位置("direction"),
|
|
4227
|
-
}
|
|
4228
|
-
: {}),
|
|
4229
|
-
// 箱を並べ替える軸 (#2655)。 向きと同じく英語で書くので、記法と同じ解決を通して日本語に直す。
|
|
4230
|
-
// 読めない語は渡さない = 公開 schema の語の一覧が絞っているので、ここに来るのは書き間違いだけ
|
|
4231
|
-
...(並び順 !== null ? { order: 並び順, orderPos: 位置("order") } : {}),
|
|
4232
|
-
// 数を描く図の形 (#2657)。 記法と同じ解決を通す = 受ける語がずれない
|
|
4233
|
-
...(形 !== null ? { shape: 形, shapePos: 位置("shape") } : {}),
|
|
4234
|
-
// 図の意匠 (#1553 / #2790)。 dragon の文書では `theme`、cdl の図へ載せる時は `palette` とする
|
|
4235
|
-
...(選んだ意匠 !== null ? { theme: 選んだ意匠 } : {}),
|
|
4236
|
-
...(legend && legend.length > 0 ? { legend } : {}),
|
|
4237
|
-
...(意匠 !== null && 別名の意匠 !== null
|
|
4238
|
-
? {
|
|
4239
|
-
themeAlsoPalettePos: {
|
|
4240
|
-
themeLine: 位置("theme").line,
|
|
4241
|
-
paletteLine: 位置("palette").line,
|
|
4242
|
-
},
|
|
4243
|
-
}
|
|
4244
|
-
: {}),
|
|
4245
|
-
pos: 位置(),
|
|
4246
|
-
};
|
|
4247
|
-
}
|
|
4248
|
-
|
|
4249
|
-
/**
|
|
4250
|
-
* LLM 向け JSON DSL の parse + compile 一発変換。
|
|
4251
|
-
*
|
|
4252
|
-
* @param json - DragonJson shape の object (parsed JSON、 not string)
|
|
4253
|
-
* @returns CdlDiagram (@cardenelabs/cdl の CdlDiagramView 等に渡せる)
|
|
4254
|
-
* @throws Error - validation error + hint 付きの詳細メッセージ、 LLM に retry させるための情報を含む
|
|
4255
|
-
*
|
|
4256
|
-
* @example
|
|
4257
|
-
* const diagram = jsonToDiagram({
|
|
4258
|
-
* title: "ログインAPI",
|
|
4259
|
-
* type: "sequence",
|
|
4260
|
-
* actors: ["User", "API", { name: "DB", kind: "storage" }],
|
|
4261
|
-
* flow: [
|
|
4262
|
-
* { from: "User", to: "API", label: "login" },
|
|
4263
|
-
* { from: "API", to: "DB", label: "SELECT" },
|
|
4264
|
-
* ],
|
|
4265
|
-
* animation: [
|
|
4266
|
-
* { step: "call", duration: 1.4, focus: ["User", "API"] },
|
|
4267
|
-
* ],
|
|
4268
|
-
* });
|
|
4269
|
-
*/
|
|
4270
|
-
export function jsonToDiagram(
|
|
4271
|
-
json: unknown,
|
|
4272
|
-
// **`onNotice` も通す**。 記法経路だけに通知を付けていたため、 同じ型を受ける JSON / YAML
|
|
4273
|
-
// 経路では読めない値や捨てた矢印が利用者へ届かなかった (review 指摘)。 エディタの YAML タブは
|
|
4274
|
-
// ここを通る
|
|
4275
|
-
opts?: {
|
|
4276
|
-
partsCatalog?: Record<string, CdlDiagram>;
|
|
4277
|
-
onNotice?: CompileToCdlOpts["onNotice"];
|
|
4278
|
-
/**
|
|
4279
|
-
* 本文の場所から書いた行を引く表 (#2117)。 渡すと知らせが書いた行を指す。
|
|
4280
|
-
*
|
|
4281
|
-
* JSON そのものは書いた場所を持たないため、本文の行が分かる入口 (編集画面の YAML 欄) が
|
|
4282
|
-
* 集めて渡す。 渡さない呼出は今までどおり全ての知らせが 0 行になる。
|
|
4283
|
-
*/
|
|
4284
|
-
行の表?: 書いた行の表;
|
|
4285
|
-
},
|
|
4286
|
-
): CdlDiagram {
|
|
4287
|
-
const v = validateJson(json);
|
|
4288
|
-
if (!v.ok) {
|
|
4289
|
-
const msg = v.errors
|
|
4290
|
-
.map((e) => ` ${e.path}: ${e.message}${e.hint ? ` (${e.hint})` : ""}`)
|
|
4291
|
-
.join("\n");
|
|
4292
|
-
throw new Error(`Dragon JSON DSL validation error:\n${msg}`);
|
|
4293
|
-
}
|
|
4294
|
-
const doc = jsonToDoc(v.data, opts?.行の表);
|
|
4295
|
-
return compileToCdl(doc, opts);
|
|
4296
|
-
}
|
|
4297
|
-
|
|
4298
|
-
/**
|
|
4299
|
-
* JSON DSL を validate だけ実施 (compile しない)。 error 詳細を配列で取得したい場合に使う。
|
|
4300
|
-
* LLM の structured output の retry loop で、 error path を prompt に注入する用途。
|
|
4301
|
-
*/
|
|
4302
|
-
export function validateDragonJson(
|
|
4303
|
-
json: unknown,
|
|
4304
|
-
): { ok: true; data: DragonJson } | { ok: false; errors: JsonDslError[] } {
|
|
4305
|
-
return validateJson(json);
|
|
4306
|
-
}
|