@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/parser.ts
CHANGED
|
@@ -1,354 +1,386 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* 人が書く記法を、図の原本の JSON へ変える。
|
|
3
|
+
*
|
|
4
|
+
* 記法は 1 行が 1 本の線になる行ごとの文で、最初の段の見出しより前に図の種類と題と箱を宣言し、
|
|
5
|
+
* 段ごとに見出し (`#`) と本文 (`>`) の下へ、その段で起きることを起きる順に 1 行ずつ並べる。
|
|
6
|
+
*
|
|
7
|
+
* 時系列図 <題>
|
|
8
|
+
* 見せ方 <止める か 寄る> 書かなければ字の大きさで選ぶ
|
|
9
|
+
* 区画 <名札> = <名前>「<添え書き>」 [色 <色>] [印 <印>]
|
|
10
|
+
* <名札> = <名前>「<下の小さな字>」 [色 <色>] [印 <印>] [区画 <名札>]
|
|
11
|
+
* 帯 <題> = <欄>, <欄>
|
|
12
|
+
* # <段の題>
|
|
13
|
+
* > <段の本文>
|
|
14
|
+
* `<段で打つ命令>`
|
|
15
|
+
* <名札> -> <名札> : <線の名前> [札 <値>] [名前 上] 線。運ぶ物を書かなければ封筒が走る
|
|
16
|
+
* <名札> : <処理の名前> 箱の中の処理
|
|
17
|
+
* <名札>「<字>」 箱の下の小さな字の書き換え
|
|
18
|
+
* 帯 <欄> = <値> [<札>] 中身の帯の値
|
|
19
|
+
*
|
|
20
|
+
* 語は英語でも書ける (#2997)。`sequence` / `architecture` で始めた file は、語を `words.ts` の表の
|
|
21
|
+
* 英語の語で書き、`「…」` の代わりに `"…"` でも囲める。1 つの file の言葉は最初の行の図の種類の語で
|
|
22
|
+
* 決まり、違う言葉の語を書いた行は誤りにする。誤りの文も file の言葉で出す。
|
|
23
|
+
*
|
|
24
|
+
* 行の頭の語と同じ綴りの名札 (`帯` や `strip`) も書ける。行の頭の語と名札は行の形で見分け、
|
|
25
|
+
* 名札の次に `->` か `:` か `=` か囲みが来る行は箱の行と読む。
|
|
26
|
+
*
|
|
27
|
+
* 誤りは最初の 1 つで止め、行と列を示す。
|
|
5
28
|
*/
|
|
29
|
+
import { type Messages, messagesFor } from "./messages";
|
|
30
|
+
import { type Box, type Carry, type Color, COLORS, type Diagram, type DiagramEvent, type LabelSide, type Region, type Step, type ViewMode } from "./schema";
|
|
31
|
+
import { columnOf, type SourceLine, tokenize } from "./tokenize";
|
|
32
|
+
import { FORMAT_VERSION } from "./version";
|
|
33
|
+
import { languageOf, type NotationLang, otherLang, valueOf, wordOf, type WORDS } from "./words";
|
|
6
34
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
DslState,
|
|
13
|
-
DslPhase,
|
|
14
|
-
DslError,
|
|
15
|
-
PresetType,
|
|
16
|
-
} from "./types";
|
|
17
|
-
import type { NodeKind, Tone } from "@cardenelabs/cdl";
|
|
18
|
-
import {
|
|
19
|
-
PRESET_NAMES,
|
|
20
|
-
NODE_KIND_ALIAS,
|
|
21
|
-
TONE_ALIAS,
|
|
22
|
-
normalizeArrow,
|
|
23
|
-
resolveHeader,
|
|
24
|
-
resolveAnimSubkey,
|
|
25
|
-
parseDuration,
|
|
26
|
-
} from "./keywords";
|
|
35
|
+
export interface LineError {
|
|
36
|
+
line: number;
|
|
37
|
+
column: number;
|
|
38
|
+
message: string;
|
|
39
|
+
}
|
|
27
40
|
|
|
28
|
-
type
|
|
41
|
+
export type Parsed = { ok: true; diagram: Diagram } | { ok: false; errors: LineError[] };
|
|
29
42
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
if (c === " ") n += 1;
|
|
34
|
-
else if (c === "\t") n += 2;
|
|
35
|
-
else break;
|
|
36
|
-
}
|
|
37
|
-
return n;
|
|
43
|
+
export interface ParseOptions {
|
|
44
|
+
/** 最初の行が読めず file の言葉が決まらない時に、誤りの文に使う言葉。既定は日本語 */
|
|
45
|
+
lang?: NotationLang;
|
|
38
46
|
}
|
|
39
47
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
lineNo: i + 1,
|
|
49
|
-
};
|
|
50
|
-
});
|
|
48
|
+
class NotationError extends Error {
|
|
49
|
+
constructor(
|
|
50
|
+
readonly line: number,
|
|
51
|
+
readonly column: number,
|
|
52
|
+
message: string,
|
|
53
|
+
) {
|
|
54
|
+
super(message);
|
|
55
|
+
}
|
|
51
56
|
}
|
|
52
57
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
58
|
+
const fail = (source: SourceLine, index: number, message: string): NotationError =>
|
|
59
|
+
new NotationError(source.line, columnOf(source, index), message);
|
|
56
60
|
|
|
57
|
-
/**
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
61
|
+
/** `[鍵 値]` の指定。`raw` は括弧の中身そのまま、`at` は `[` の位置。 */
|
|
62
|
+
interface Attr {
|
|
63
|
+
raw: string;
|
|
64
|
+
key: string;
|
|
65
|
+
value: string;
|
|
66
|
+
at: number;
|
|
62
67
|
}
|
|
63
68
|
|
|
64
|
-
/** 行の
|
|
65
|
-
function
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
+
/** 行の `start` から後ろの終わりに並ぶ `[...]` を外す。`end` は残った字の終わり。 */
|
|
70
|
+
function trailingAttrs(source: SourceLine, start: number): { end: number; attrs: Attr[] } {
|
|
71
|
+
const attrs: Attr[] = [];
|
|
72
|
+
let end = source.text.length;
|
|
73
|
+
for (;;) {
|
|
74
|
+
const found = /\s*\[([^[\]]*)\]$/.exec(source.text.slice(start, end));
|
|
75
|
+
if (!found) break;
|
|
76
|
+
const raw = (found[1] ?? "").trim();
|
|
77
|
+
const [key = "", ...words] = raw.split(/\s+/);
|
|
78
|
+
attrs.unshift({ raw, key, value: words.join(" "), at: start + found.index + found[0].indexOf("[") });
|
|
79
|
+
end = start + found.index;
|
|
80
|
+
}
|
|
81
|
+
return { end, attrs };
|
|
69
82
|
}
|
|
70
83
|
|
|
71
|
-
|
|
72
|
-
* Parser 本体
|
|
73
|
-
*/
|
|
74
|
-
export type ParseResult = { ok: true; doc: DslDocument } | { ok: false; errors: DslError[] };
|
|
75
|
-
|
|
76
|
-
export function parseTextDsl(src: string): ParseResult {
|
|
77
|
-
const lines = splitLines(src);
|
|
78
|
-
const errors: DslError[] = [];
|
|
79
|
-
|
|
80
|
-
let title: string | undefined;
|
|
81
|
-
let type: PresetType | undefined;
|
|
82
|
-
const actors: DslActor[] = [];
|
|
83
|
-
const steps: DslStep[] = [];
|
|
84
|
-
let animate: DslAnimate | undefined;
|
|
84
|
+
const escape = (word: string): string => word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
85
85
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const header = resolveHeader(kv[0]);
|
|
95
|
-
if (header === "title") {
|
|
96
|
-
title = kv[1];
|
|
97
|
-
i += 1; continue;
|
|
98
|
-
}
|
|
99
|
-
if (header === "type") {
|
|
100
|
-
if (!(PRESET_NAMES as readonly string[]).includes(kv[1].toLowerCase())) {
|
|
101
|
-
errors.push({
|
|
102
|
-
line: l.lineNo,
|
|
103
|
-
message: `未知の種類 "${kv[1]}"`,
|
|
104
|
-
hint: `次から選んでください: ${PRESET_NAMES.join(" / ")}`,
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
type = kv[1].toLowerCase() as PresetType;
|
|
108
|
-
i += 1; continue;
|
|
109
|
-
}
|
|
110
|
-
if (header === "actors") {
|
|
111
|
-
// 次のブロック (indent > 0) を全部読む
|
|
112
|
-
i += 1;
|
|
113
|
-
while (i < lines.length && (lines[i]!.indent > 0 || isBlank(lines[i]!))) {
|
|
114
|
-
if (!isBlank(lines[i]!)) {
|
|
115
|
-
const a = parseActor(lines[i]!, errors);
|
|
116
|
-
if (a) actors.push(a);
|
|
117
|
-
}
|
|
118
|
-
i += 1;
|
|
119
|
-
}
|
|
120
|
-
continue;
|
|
121
|
-
}
|
|
122
|
-
if (header === "flow") {
|
|
123
|
-
i += 1;
|
|
124
|
-
while (i < lines.length && (lines[i]!.indent > 0 || isBlank(lines[i]!))) {
|
|
125
|
-
if (!isBlank(lines[i]!)) {
|
|
126
|
-
const s = parseStep(lines[i]!, errors, actors);
|
|
127
|
-
if (s) steps.push(s);
|
|
128
|
-
}
|
|
129
|
-
i += 1;
|
|
130
|
-
}
|
|
131
|
-
continue;
|
|
132
|
-
}
|
|
133
|
-
if (header === "animate") {
|
|
134
|
-
const r = parseAnimate(lines, i + 1, errors);
|
|
135
|
-
animate = r.anim;
|
|
136
|
-
i = r.nextIndex;
|
|
137
|
-
continue;
|
|
138
|
-
}
|
|
139
|
-
}
|
|
86
|
+
/** 字を囲む形。日本語の語の file は「」だけ、英語の語の file は「」と "" を読む */
|
|
87
|
+
const QUOTES: Record<NotationLang, readonly (readonly [string, string])[]> = {
|
|
88
|
+
ja: [["「", "」"]],
|
|
89
|
+
en: [
|
|
90
|
+
["「", "」"],
|
|
91
|
+
['"', '"'],
|
|
92
|
+
],
|
|
93
|
+
};
|
|
140
94
|
|
|
141
|
-
|
|
142
|
-
|
|
95
|
+
/** `<名前>「<添え書き>」` を名前と添え書きに分ける。 */
|
|
96
|
+
function nameAndNote(text: string, lang: NotationLang): { name: string; note?: string } {
|
|
97
|
+
for (const [open, close] of QUOTES[lang]) {
|
|
98
|
+
const found = new RegExp(`^(.*?)\\s*${escape(open)}(.*)${escape(close)}$`).exec(text);
|
|
99
|
+
if (found) return { name: (found[1] ?? "").trim(), note: found[2] ?? "" };
|
|
143
100
|
}
|
|
101
|
+
return { name: text.trim() };
|
|
102
|
+
}
|
|
144
103
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
104
|
+
/** `<名札>「<字>」` の書き換えの行。名札と字の位置を返す */
|
|
105
|
+
function swapLine(text: string, lang: NotationLang): RegExpExecArray | null {
|
|
106
|
+
const opens = QUOTES[lang].map(([o]) => escape(o)).join("");
|
|
107
|
+
for (const [open, close] of QUOTES[lang]) {
|
|
108
|
+
const found = new RegExp(`^([^\\s${opens}]+)\\s*${escape(open)}(.*)${escape(close)}$`, "d").exec(text);
|
|
109
|
+
if (found) return found;
|
|
110
|
+
}
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
151
113
|
|
|
114
|
+
/**
|
|
115
|
+
* 1 つの言葉の、行の頭の語で始まる行の形。
|
|
116
|
+
* `reading` は読んでいる file の言葉で、帯の値の行から外す箱の行 (名札の次に囲みが来る行) の形を決める。
|
|
117
|
+
*/
|
|
118
|
+
function headShapes(lang: NotationLang, reading: NotationLang) {
|
|
119
|
+
const head = (key: keyof typeof WORDS.head): string => escape(wordOf("head", key, lang));
|
|
120
|
+
const opens = QUOTES[reading].map(([open]) => `|${escape(open)}`).join("");
|
|
152
121
|
return {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
flow: steps,
|
|
159
|
-
animate,
|
|
160
|
-
pos: { line: 1 },
|
|
161
|
-
},
|
|
122
|
+
view: new RegExp(`^${head("view")}\\s+(\\S+)$`),
|
|
123
|
+
strip: new RegExp(`^${head("strip")}\\s+(.+?)\\s*=\\s*(.+)$`),
|
|
124
|
+
region: new RegExp(`^${head("region")}\\s+([^\\s=]+)\\s*=\\s*`, "d"),
|
|
125
|
+
// 空白を全部読んでから次の字を見る (`\s` を外すと、空白 2 つの `帯 -> 受け手` を帯の値と読む)
|
|
126
|
+
stripValue: new RegExp(`^${head("strip")}\\s+(?!\\s|->|:|=${opens})`),
|
|
162
127
|
};
|
|
163
128
|
}
|
|
164
129
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
let kind: NodeKind = "actor";
|
|
172
|
-
let kindWritten = false;
|
|
173
|
-
if (item.paren) {
|
|
174
|
-
const resolved = NODE_KIND_ALIAS[item.paren] ?? (item.paren as NodeKind);
|
|
175
|
-
kind = resolved;
|
|
176
|
-
kindWritten = true;
|
|
177
|
-
}
|
|
178
|
-
return { name: item.name, kind, kindWritten, pos: { line: l.lineNo } };
|
|
130
|
+
interface Building {
|
|
131
|
+
source: SourceLine;
|
|
132
|
+
title: string;
|
|
133
|
+
body: string[];
|
|
134
|
+
code?: string;
|
|
135
|
+
events: DiagramEvent[];
|
|
179
136
|
}
|
|
180
137
|
|
|
181
|
-
function
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
const m2 = rest.match(/^(.+?)\s*→\s*(.+?)\s*:\s*(.+)$/);
|
|
192
|
-
if (!m2) {
|
|
193
|
-
errors.push({ line: l.lineNo, message: `矢印または ラベル なし: "${l.trimmed}"`, hint: "`<from> → <to>: <ラベル>` 形式" });
|
|
194
|
-
return null;
|
|
195
|
-
}
|
|
196
|
-
const from = m2[1]!.trim();
|
|
197
|
-
const to = m2[2]!.trim();
|
|
198
|
-
let labelPart = m2[3]!.trim();
|
|
199
|
-
|
|
200
|
-
// tone in () at end
|
|
201
|
-
let tone: Tone | undefined;
|
|
202
|
-
const toneMatch = labelPart.match(/^(.+?)\s*\(\s*(.+?)\s*\)\s*$/);
|
|
203
|
-
if (toneMatch) {
|
|
204
|
-
const resolved = TONE_ALIAS[toneMatch[2]!];
|
|
205
|
-
if (resolved) {
|
|
206
|
-
tone = resolved;
|
|
207
|
-
labelPart = toneMatch[1]!.trim();
|
|
208
|
-
}
|
|
209
|
-
}
|
|
138
|
+
function parseLines(lines: SourceLine[], fallback: NotationLang): Diagram {
|
|
139
|
+
const [head, ...rest] = lines;
|
|
140
|
+
if (!head) throw new NotationError(1, 1, messagesFor(fallback).noHead);
|
|
141
|
+
const found = languageOf(head.text);
|
|
142
|
+
const m: Messages = messagesFor(found ?? fallback);
|
|
143
|
+
const header = /^(\S+)\s+(.+)$/.exec(head.text);
|
|
144
|
+
const kind = found && valueOf("kind", header?.[1] ?? "", found);
|
|
145
|
+
if (!found || !header || !kind) throw fail(head, 0, m.badHead);
|
|
146
|
+
const lang: NotationLang = found;
|
|
147
|
+
const title = header[2] ?? "";
|
|
210
148
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
}
|
|
149
|
+
const other = otherLang(lang);
|
|
150
|
+
const own = headShapes(lang, lang);
|
|
151
|
+
const foreign = headShapes(other, lang);
|
|
152
|
+
/** 違う言葉の語を書いた行の誤り。`group` の役割の、この file の言葉の語を示す */
|
|
153
|
+
const foreignWord = <G extends keyof typeof WORDS>(source: SourceLine, at: number, group: G, word: string): NotationError =>
|
|
154
|
+
fail(source, at, m.otherLanguageWord(word, wordOf(group, valueOf(group, word, other) as keyof (typeof WORDS)[G], lang)));
|
|
218
155
|
|
|
219
|
-
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
}
|
|
156
|
+
const regions: Region[] = [];
|
|
157
|
+
const boxes: Box[] = [];
|
|
158
|
+
const ids = new Set<string>();
|
|
159
|
+
let strip: { title: string; fields: string[] } | undefined;
|
|
160
|
+
let view: ViewMode | undefined;
|
|
161
|
+
const steps: Step[] = [];
|
|
162
|
+
let building: Building | undefined;
|
|
227
163
|
|
|
228
|
-
|
|
229
|
-
|
|
164
|
+
const declare = (source: SourceLine, id: string, at: number): void => {
|
|
165
|
+
if (ids.has(id)) throw fail(source, at, m.duplicate(id));
|
|
166
|
+
ids.add(id);
|
|
167
|
+
};
|
|
168
|
+
const boxAt = (source: SourceLine, id: string, at: number): string => {
|
|
169
|
+
if (!boxes.some((box) => box.id === id)) throw fail(source, at, m.undeclaredBox(id));
|
|
170
|
+
return id;
|
|
171
|
+
};
|
|
172
|
+
const finish = (): void => {
|
|
173
|
+
if (!building) return;
|
|
174
|
+
if (building.body.length === 0) throw fail(building.source, 0, m.noBody(building.title));
|
|
175
|
+
steps.push({
|
|
176
|
+
title: building.title,
|
|
177
|
+
body: building.body.join("\n"),
|
|
178
|
+
...(building.code !== undefined ? { code: building.code } : {}),
|
|
179
|
+
events: building.events,
|
|
180
|
+
});
|
|
181
|
+
};
|
|
230
182
|
|
|
231
|
-
|
|
183
|
+
for (const source of rest) {
|
|
184
|
+
const text = source.text;
|
|
185
|
+
if (text.startsWith("#")) {
|
|
186
|
+
finish();
|
|
187
|
+
const stepTitle = text.slice(1).trim();
|
|
188
|
+
if (stepTitle === "") throw fail(source, 0, m.emptyStepTitle);
|
|
189
|
+
building = { source, title: stepTitle, body: [], events: [] };
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
if (!building) {
|
|
193
|
+
parseDeclaration(source);
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
building.events.push(...parseStepLine(source, building));
|
|
197
|
+
}
|
|
198
|
+
finish();
|
|
232
199
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
const phases: DslPhase[] = [];
|
|
236
|
-
const anim: DslAnimate = { states, phases, pos: { line: startIdx + 1 } };
|
|
200
|
+
if (boxes.length === 0) throw fail(head, 0, m.noBoxes);
|
|
201
|
+
if (steps.length === 0) throw fail(head, 0, m.noSteps);
|
|
237
202
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
203
|
+
return {
|
|
204
|
+
version: FORMAT_VERSION,
|
|
205
|
+
kind,
|
|
206
|
+
title,
|
|
207
|
+
...(view ? { view } : {}),
|
|
208
|
+
...(regions.length > 0 ? { regions } : {}),
|
|
209
|
+
boxes,
|
|
210
|
+
...(strip ? { strip } : {}),
|
|
211
|
+
steps,
|
|
212
|
+
...(kind === "sequence" ? { sequence: { lifelines: boxes.map((box) => box.id) } } : {}),
|
|
213
|
+
};
|
|
244
214
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
if (
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
if (!m) {
|
|
257
|
-
errors.push({ line: l.lineNo, message: `状態 書式エラー: "${kv[1]}"`, hint: "`状態: <名前> = <初期値>`" });
|
|
258
|
-
} else {
|
|
259
|
-
const name = m[1]!.trim();
|
|
260
|
-
const raw = m[2]!.trim();
|
|
261
|
-
const num = parseFloat(raw);
|
|
262
|
-
const initial = isNaN(num) ? raw.replace(/^["']|["']$/g, "") : num;
|
|
263
|
-
states.push({ name, initial, pos: { line: l.lineNo } });
|
|
264
|
-
}
|
|
265
|
-
i += 1; continue;
|
|
215
|
+
function parseDeclaration(source: SourceLine): void {
|
|
216
|
+
const text = source.text;
|
|
217
|
+
const viewDecl = own.view.exec(text);
|
|
218
|
+
if (viewDecl) {
|
|
219
|
+
if (view) throw fail(source, 0, m.viewTwice);
|
|
220
|
+
const word = viewDecl[1] ?? "";
|
|
221
|
+
const at = text.indexOf(word);
|
|
222
|
+
const chosen = valueOf("view", word, lang);
|
|
223
|
+
if (!chosen) {
|
|
224
|
+
if (valueOf("view", word, other) !== undefined) throw foreignWord(source, at, "view", word);
|
|
225
|
+
throw fail(source, at, m.badView);
|
|
266
226
|
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
errors.push({ line: l.lineNo, message: `時間 解釈不能: "${durStr}"`, hint: "`1.5 秒` / `1500ms` / `2s` 等" });
|
|
285
|
-
i += 1; continue;
|
|
286
|
-
}
|
|
287
|
-
const phase: DslPhase = { name: phaseName, durationMs: durMs, pos: { line: l.lineNo } };
|
|
288
|
-
// sub block を読む
|
|
289
|
-
i += 1;
|
|
290
|
-
const phaseBaseIndent = l.indent;
|
|
291
|
-
while (i < lines.length && (lines[i]!.indent > phaseBaseIndent || isBlank(lines[i]!))) {
|
|
292
|
-
if (!isBlank(lines[i]!)) {
|
|
293
|
-
applyPhaseSubLine(lines[i]!, phase, errors);
|
|
294
|
-
}
|
|
295
|
-
i += 1;
|
|
296
|
-
}
|
|
297
|
-
phases.push(phase);
|
|
298
|
-
continue;
|
|
227
|
+
view = chosen;
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
const stripDecl = own.strip.exec(text);
|
|
231
|
+
if (stripDecl) {
|
|
232
|
+
if (strip) throw fail(source, 0, m.stripTwice);
|
|
233
|
+
const fields = (stripDecl[2] ?? "").split(",").map((field) => field.trim());
|
|
234
|
+
if (fields.some((field) => field === "")) throw fail(source, 0, m.emptyStripField);
|
|
235
|
+
strip = { title: stripDecl[1] ?? "", fields };
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
const regionDecl = own.region.exec(text);
|
|
239
|
+
const boxDecl = regionDecl ? undefined : /^([^\s=]+)\s*=(?!>)\s*/d.exec(text);
|
|
240
|
+
const decl = regionDecl ?? boxDecl;
|
|
241
|
+
if (!decl?.indices?.[1]) {
|
|
242
|
+
if (foreign.view.test(text) || foreign.strip.test(text) || foreign.region.test(text)) {
|
|
243
|
+
throw foreignWord(source, 0, "head", text.split(/\s+/)[0] ?? "");
|
|
299
244
|
}
|
|
245
|
+
if (looksLikeStepLine(text, lang)) throw fail(source, 0, m.stepLineBeforeStep);
|
|
246
|
+
throw fail(source, 0, m.unreadableDeclaration);
|
|
300
247
|
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
248
|
+
const id = decl[1] ?? "";
|
|
249
|
+
declare(source, id, decl.indices[1][0]);
|
|
250
|
+
const { end, attrs } = trailingAttrs(source, decl[0].length);
|
|
251
|
+
const { name, note } = nameAndNote(text.slice(decl[0].length, end), lang);
|
|
252
|
+
if (name === "") throw fail(source, decl[0].length, m.emptyName(id));
|
|
253
|
+
const look: { color?: Color; icon?: string; region?: string } = {};
|
|
254
|
+
for (const attr of attrs) {
|
|
255
|
+
const key = valueOf("attr", attr.key, lang);
|
|
256
|
+
if (key === "color") {
|
|
257
|
+
if (!COLORS.includes(attr.value as Color)) throw fail(source, attr.at, m.badColor);
|
|
258
|
+
look.color = attr.value as Color;
|
|
259
|
+
} else if (key === "icon" && attr.value !== "") {
|
|
260
|
+
look.icon = attr.value;
|
|
261
|
+
} else if (key === "region" && !regionDecl) {
|
|
262
|
+
if (!regions.some((region) => region.id === attr.value)) throw fail(source, attr.at, m.regionFirst(attr.value));
|
|
263
|
+
look.region = attr.value;
|
|
264
|
+
} else {
|
|
265
|
+
if (valueOf("attr", attr.key, other) !== undefined) throw foreignWord(source, attr.at, "attr", attr.key);
|
|
266
|
+
throw fail(source, attr.at, m.badAttr(attr.raw));
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
if (regionDecl) {
|
|
270
|
+
regions.push({
|
|
271
|
+
id,
|
|
272
|
+
name,
|
|
273
|
+
...(note !== undefined ? { note } : {}),
|
|
274
|
+
...(look.color ? { color: look.color } : {}),
|
|
275
|
+
...(look.icon ? { icon: look.icon } : {}),
|
|
276
|
+
});
|
|
321
277
|
return;
|
|
322
278
|
}
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
279
|
+
boxes.push({
|
|
280
|
+
id,
|
|
281
|
+
name,
|
|
282
|
+
...(note !== undefined ? { sub: note } : {}),
|
|
283
|
+
...(look.color ? { color: look.color } : {}),
|
|
284
|
+
...(look.icon ? { icon: look.icon } : {}),
|
|
285
|
+
...(look.region ? { region: look.region } : {}),
|
|
329
286
|
});
|
|
330
|
-
return;
|
|
331
287
|
}
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
const
|
|
335
|
-
if (
|
|
336
|
-
|
|
337
|
-
return;
|
|
288
|
+
|
|
289
|
+
function parseStepLine(source: SourceLine, step: Building): DiagramEvent[] {
|
|
290
|
+
const text = source.text;
|
|
291
|
+
if (text.startsWith(">")) {
|
|
292
|
+
step.body.push(text.slice(1).trim());
|
|
293
|
+
return [];
|
|
294
|
+
}
|
|
295
|
+
const code = /^`(.*)`$/.exec(text);
|
|
296
|
+
if (code) {
|
|
297
|
+
if (step.code !== undefined) throw fail(source, 0, m.codeTwice);
|
|
298
|
+
step.code = code[1] ?? "";
|
|
299
|
+
return [];
|
|
300
|
+
}
|
|
301
|
+
if (own.stripValue.test(text)) return [parseStripValue(source)];
|
|
302
|
+
if (foreign.stripValue.test(text)) throw foreignWord(source, 0, "head", text.split(/\s+/)[0] ?? "");
|
|
303
|
+
if (text.includes("->")) return [parseEdge(source)];
|
|
304
|
+
if (/^[^\s=]+\s*=(?!>)/.test(text)) throw fail(source, 0, m.declarationInStep);
|
|
305
|
+
const swap = swapLine(text, lang);
|
|
306
|
+
if (swap?.indices?.[1]) {
|
|
307
|
+
const box = boxAt(source, swap[1] ?? "", swap.indices[1][0]);
|
|
308
|
+
if ((swap[2] ?? "") === "") throw fail(source, swap.indices[1][1], m.emptySwap);
|
|
309
|
+
return [{ type: "swap", box, text: swap[2] ?? "" }];
|
|
310
|
+
}
|
|
311
|
+
const action = /^([^\s:]+)\s*:\s*(.+)$/d.exec(text);
|
|
312
|
+
if (action?.indices?.[1]) return [{ type: "action", box: boxAt(source, action[1] ?? "", action.indices[1][0]), label: action[2] ?? "" }];
|
|
313
|
+
throw fail(source, 0, m.unreadableStepLine);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function parseEdge(source: SourceLine): DiagramEvent {
|
|
317
|
+
const { end, attrs } = trailingAttrs(source, 0);
|
|
318
|
+
const edge = /^([^\s:]+)\s*->\s*([^\s:]+)(?:\s*:\s*(.+))?$/d.exec(source.text.slice(0, end));
|
|
319
|
+
if (!edge?.indices?.[1] || !edge.indices[2]) throw fail(source, 0, m.edgeShape);
|
|
320
|
+
const from = boxAt(source, edge[1] ?? "", edge.indices[1][0]);
|
|
321
|
+
const to = boxAt(source, edge[2] ?? "", edge.indices[2][0]);
|
|
322
|
+
let carry: Carry | undefined;
|
|
323
|
+
let labelSide: LabelSide | undefined;
|
|
324
|
+
for (const attr of attrs) {
|
|
325
|
+
const kind = valueOf("carry", attr.key, lang);
|
|
326
|
+
const label = valueOf("attr", attr.key, lang) === "label";
|
|
327
|
+
if (kind !== undefined) {
|
|
328
|
+
if (carry) throw fail(source, attr.at, m.carryTwice);
|
|
329
|
+
if (kind === "tag") {
|
|
330
|
+
if (attr.value === "") throw fail(source, attr.at, m.tagNeedsValue);
|
|
331
|
+
carry = { kind, text: attr.value };
|
|
332
|
+
} else {
|
|
333
|
+
if (attr.value !== "") throw fail(source, attr.at, m.carryTakesNoValue(attr.key));
|
|
334
|
+
carry = { kind };
|
|
335
|
+
}
|
|
336
|
+
} else if (label && valueOf("side", attr.value, lang) !== undefined) {
|
|
337
|
+
labelSide = valueOf("side", attr.value, lang);
|
|
338
|
+
} else {
|
|
339
|
+
if (label && valueOf("side", attr.value, other) !== undefined) throw foreignWord(source, attr.at, "side", attr.value);
|
|
340
|
+
if (valueOf("carry", attr.key, other) !== undefined) throw foreignWord(source, attr.at, "carry", attr.key);
|
|
341
|
+
if (valueOf("attr", attr.key, other) !== undefined) throw foreignWord(source, attr.at, "attr", attr.key);
|
|
342
|
+
throw fail(source, attr.at, m.badAttr(attr.raw));
|
|
343
|
+
}
|
|
338
344
|
}
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
+
return {
|
|
346
|
+
type: "edge",
|
|
347
|
+
from,
|
|
348
|
+
to,
|
|
349
|
+
...(edge[3] !== undefined ? { label: edge[3] } : {}),
|
|
350
|
+
carry: carry ?? { kind: "envelope" },
|
|
351
|
+
...(labelSide ? { labelSide } : {}),
|
|
352
|
+
};
|
|
345
353
|
}
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
354
|
+
|
|
355
|
+
function parseStripValue(source: SourceLine): DiagramEvent {
|
|
356
|
+
if (!strip) throw fail(source, 0, m.stripUndeclared);
|
|
357
|
+
const start = new RegExp(`^${escape(wordOf("head", "strip", lang))}\\s+`).exec(source.text)?.[0].length ?? 0;
|
|
358
|
+
const { end, attrs } = trailingAttrs(source, start);
|
|
359
|
+
if (attrs.length > 1) throw fail(source, attrs[1]?.at ?? 0, m.stripTagOnce);
|
|
360
|
+
const rest = source.text.slice(start, end);
|
|
361
|
+
const assigned = /^(.+?)\s*=\s*(.+)$/.exec(rest);
|
|
362
|
+
const field = (assigned ? assigned[1] : rest)?.trim() ?? "";
|
|
363
|
+
if (!strip.fields.includes(field)) throw fail(source, start, m.noSuchField(field));
|
|
364
|
+
const tag = attrs[0]?.raw;
|
|
365
|
+
if (!assigned && tag === undefined) throw fail(source, start, m.stripNeedsValue);
|
|
366
|
+
return {
|
|
367
|
+
type: "strip",
|
|
368
|
+
field,
|
|
369
|
+
...(assigned ? { value: assigned[2] ?? "" } : {}),
|
|
370
|
+
...(tag !== undefined ? { tag } : {}),
|
|
371
|
+
};
|
|
349
372
|
}
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function looksLikeStepLine(text: string, lang: NotationLang): boolean {
|
|
376
|
+
return text.startsWith(">") || text.startsWith("`") || text.includes("->") || swapLine(text, lang) !== null;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
export function parseNotation(notation: string, options: ParseOptions = {}): Parsed {
|
|
380
|
+
try {
|
|
381
|
+
return { ok: true, diagram: parseLines(tokenize(notation), options.lang ?? "ja") };
|
|
382
|
+
} catch (error) {
|
|
383
|
+
if (!(error instanceof NotationError)) throw error;
|
|
384
|
+
return { ok: false, errors: [{ line: error.line, column: error.column, message: error.message }] };
|
|
353
385
|
}
|
|
354
386
|
}
|