@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.
Files changed (127) hide show
  1. package/README.md +39 -720
  2. package/dist/cli.cjs +4529 -0
  3. package/dist/cli.cjs.map +1 -0
  4. package/dist/cli.js +4523 -0
  5. package/dist/cli.js.map +1 -0
  6. package/dist/export.cjs +3912 -0
  7. package/dist/export.cjs.map +1 -0
  8. package/dist/export.d.cts +36 -0
  9. package/dist/export.d.ts +36 -0
  10. package/dist/export.js +3907 -0
  11. package/dist/export.js.map +1 -0
  12. package/dist/fonts.cjs +7 -0
  13. package/dist/fonts.cjs.map +1 -0
  14. package/dist/fonts.js +4 -0
  15. package/dist/fonts.js.map +1 -0
  16. package/dist/index.cjs +4105 -20356
  17. package/dist/index.cjs.map +1 -1
  18. package/dist/index.d.cts +227 -5522
  19. package/dist/index.d.ts +227 -5522
  20. package/dist/index.js +4091 -20283
  21. package/dist/index.js.map +1 -1
  22. package/dist/player.global.js +2 -0
  23. package/dist/schema-BBWfniwH.d.cts +112 -0
  24. package/dist/schema-BBWfniwH.d.ts +112 -0
  25. package/package.json +39 -24
  26. package/src/cli.ts +237 -0
  27. package/src/export/capture.ts +277 -0
  28. package/src/export/fonts.ts +68 -0
  29. package/src/export/html.ts +32 -0
  30. package/src/export/index.ts +146 -0
  31. package/src/field-messages.ts +104 -0
  32. package/src/figure-text.ts +42 -0
  33. package/src/index.ts +30 -283
  34. package/src/layout/architecture.ts +381 -0
  35. package/src/layout/measure.ts +21 -0
  36. package/src/layout/scene.ts +353 -0
  37. package/src/layout/sequence.ts +189 -0
  38. package/src/load-fonts.ts +8 -0
  39. package/src/look/fonts.ts +33 -0
  40. package/src/look/palettes.json +209 -0
  41. package/src/look/values.ts +155 -0
  42. package/src/markdown.ts +35 -0
  43. package/src/messages.ts +122 -0
  44. package/src/parser.ts +342 -310
  45. package/src/player-global.ts +4 -0
  46. package/src/player.ts +271 -0
  47. package/src/printer.ts +76 -0
  48. package/src/render/icons.ts +41 -0
  49. package/src/render/motion.ts +106 -0
  50. package/src/render/parts.ts +279 -0
  51. package/src/render/svg.ts +341 -0
  52. package/src/schema.ts +161 -16
  53. package/src/schemas/diagram.schema.json +150 -0
  54. package/src/schemas/validate-diagram.d.ts +12 -0
  55. package/src/schemas/validate-diagram.js +8 -0
  56. package/src/timeline.ts +166 -0
  57. package/src/tokenize.ts +20 -281
  58. package/src/version.ts +55 -0
  59. package/src/view.ts +16 -0
  60. package/src/words.ts +79 -0
  61. package/examples/quick-start.md +0 -142
  62. package/src/canvas-bounds.ts +0 -52
  63. package/src/color.ts +0 -197
  64. package/src/compile/actor-ids.ts +0 -297
  65. package/src/compile/actor-option-notice.ts +0 -379
  66. package/src/compile/actors.ts +0 -103
  67. package/src/compile/c4.ts +0 -74
  68. package/src/compile/chain.ts +0 -93
  69. package/src/compile/chart-fields.ts +0 -193
  70. package/src/compile/deep-rewrite.ts +0 -30
  71. package/src/compile/derived-source.ts +0 -18
  72. package/src/compile/direction.ts +0 -102
  73. package/src/compile/edge-option-notice.ts +0 -112
  74. package/src/compile/er-relation.ts +0 -200
  75. package/src/compile/flow.ts +0 -54
  76. package/src/compile/flowchart.ts +0 -344
  77. package/src/compile/funnel.ts +0 -87
  78. package/src/compile/gantt.ts +0 -337
  79. package/src/compile/generic.ts +0 -696
  80. package/src/compile/hierarchy.ts +0 -92
  81. package/src/compile/journey.ts +0 -93
  82. package/src/compile/kinds.ts +0 -139
  83. package/src/compile/lanes.ts +0 -138
  84. package/src/compile/metro.ts +0 -271
  85. package/src/compile/mind.ts +0 -393
  86. package/src/compile/node-title.ts +0 -24
  87. package/src/compile/notice.ts +0 -141
  88. package/src/compile/order.ts +0 -57
  89. package/src/compile/parts-lookup.ts +0 -17
  90. package/src/compile/parts.ts +0 -2709
  91. package/src/compile/placement-apply.ts +0 -689
  92. package/src/compile/placement.ts +0 -15
  93. package/src/compile/quadrant.ts +0 -265
  94. package/src/compile/record.ts +0 -75
  95. package/src/compile/row-marks.ts +0 -128
  96. package/src/compile/rows.ts +0 -40
  97. package/src/compile/sequence.ts +0 -305
  98. package/src/compile/slug.ts +0 -37
  99. package/src/compile/stages.ts +0 -247
  100. package/src/compile/subtitle.ts +0 -35
  101. package/src/compile/swimlane-marks.ts +0 -87
  102. package/src/compile/swimlane.ts +0 -82
  103. package/src/compile/timeline.ts +0 -213
  104. package/src/compile/topology.ts +0 -44
  105. package/src/compile/tree.ts +0 -69
  106. package/src/compile/value-chart.ts +0 -337
  107. package/src/compile/values.ts +0 -432
  108. package/src/compile/word-state.ts +0 -82
  109. package/src/compile.ts +0 -3168
  110. package/src/focus.ts +0 -46
  111. package/src/input-size.ts +0 -181
  112. package/src/json-line-map.ts +0 -30
  113. package/src/json-parser.ts +0 -4306
  114. package/src/keywords.ts +0 -417
  115. package/src/near-name.ts +0 -42
  116. package/src/notation-lint.ts +0 -342
  117. package/src/relative-pos.ts +0 -251
  118. package/src/schemas/diagram.json +0 -3088
  119. package/src/types.ts +0 -1009
  120. package/src/v05/index.ts +0 -9
  121. package/src/v05/input-table.generated.ts +0 -138
  122. package/src/v05/parser-types.ts +0 -32
  123. package/src/v05/parser.ts +0 -6668
  124. package/src/v05/readout-table.generated.ts +0 -1088
  125. package/src/v05/removed-syntax.ts +0 -115
  126. package/src/value-syntax.ts +0 -313
  127. package/src/write-position.ts +0 -270
@@ -1,115 +0,0 @@
1
- /**
2
- * 外した書き方と置き換え先 (#2947)。
3
- *
4
- * dragon はどの分野にも寄らずに図を描く道具なので、特定の技術や会社に向けた書き方を外した。
5
- * 外した書き方を黙って受けないために、**書いた人に置き換え先を誤りとして返す**。
6
- *
7
- * 外した語を受理集合 (`NODE_KIND_VALID` / `TYPE_ALIASES`) から消すだけだと、箱の種類は
8
- * 見本 (parts) の名前として扱われて「そんな部品は無い」 と出て、図種は「図種が読めません」
9
- * と出る。 どちらも何に書き換えればよいかが読み取れない。
10
- *
11
- * 記法 (`v05/parser.ts`) と JSON (`json-parser.ts`) の両方の入口がこの表を引く。
12
- * 入口ごとに表を持つと、片方だけ受け付ける形が残る。
13
- */
14
-
15
- /** 外した図種と、同じ図を描く書き方 */
16
- export const 外した図種: ReadonlyMap<string, string> = new Map<string, string>([
17
- ["solidity", "`type: sequence` と `order: 種類`"],
18
- ]);
19
-
20
- /**
21
- * cdl から外した挿絵の種類 (cardene777/cdl#1256)。
22
- *
23
- * 業界ごとに手で描いた形で、どこまで足しても偏るため cdl 0.140.0 で外した。
24
- * 外す前の形は cdl の tag `archive/illustration-shapes` から取り出せる。
25
- */
26
- export const 外した挿絵の種類: readonly string[] = [
27
- "shape-kanban-card",
28
- "shape-message-bubble",
29
- "shape-gear",
30
- "shape-network-node",
31
- "shape-iot-sensor",
32
- "shape-robot-arm",
33
- "shape-satellite",
34
- "shape-website",
35
- "shape-cdn-edge",
36
- "shape-api-gateway",
37
- "shape-storefront",
38
- "shape-warehouse",
39
- "shape-online-shop",
40
- "shape-customer-service",
41
- "shape-lawyer",
42
- "shape-auditor",
43
- "shape-regulator",
44
- "shape-notary",
45
- "shape-trader",
46
- "shape-smart-contract",
47
- "shape-blockchain-block",
48
- "shape-blockchain",
49
- "shape-bitcoin-chain",
50
- "shape-ethereum-chain",
51
- "shape-blockchain-node",
52
- "shape-rpc-node",
53
- "shape-wallet",
54
- "shape-nft",
55
- "shape-token",
56
- "shape-bank",
57
- "shape-trust-bank",
58
- "shape-payment-provider",
59
- "shape-credit-card",
60
- "shape-brokerage",
61
- "shape-exchange",
62
- "shape-atm",
63
- ];
64
-
65
- /** 外した挿絵の代わり。 1 つに決まらないので、選べる 2 つの道を示す */
66
- const 挿絵の代わり =
67
- "基本の形 (`shape-file` / `shape-cylinder` など) を使うか、箱に `kindForm: icon` と `icon` を書く";
68
-
69
- /**
70
- * 外した箱の種類と、置き換え先。
71
- *
72
- * | 外した種類 | 置き換え先 | 元の役目 |
73
- * |---|---|---|
74
- * | `contract` / `proxy` / `library` / `interface` | `card` | Solidity の図の契約 |
75
- * | `eoa` | `person` | Solidity の図の人が持つ財布 |
76
- * | `multisig` | `signer` | Solidity の図の複数人の署名 |
77
- * | `alb` / `ecs` / `iam` / `kms` / `lambda` / `rds` / `s3` | 同じ役目の汎用の種類 | AWS の名前の読み替え |
78
- * | cdl から外した挿絵 | 基本の形か `kindForm: icon` | 業界ごとの挿絵 |
79
- *
80
- * 置き換え先は外す前に読み替えていた先と同じ種類にした (`alb` だけは読み替え先の挿絵が
81
- * 外れたので `api` にした)。 書き換えた図は外す前と同じ形で描かれる。
82
- */
83
- export const 外した種類: ReadonlyMap<string, string> = new Map<string, string>([
84
- ["contract", "`card`"],
85
- ["proxy", "`card`"],
86
- ["library", "`card`"],
87
- ["interface", "`card`"],
88
- ["eoa", "`person`"],
89
- ["multisig", "`signer`"],
90
- ["alb", "`api`"],
91
- ["ecs", "`microservice`"],
92
- ["iam", "`admin`"],
93
- ["kms", "`admin`"],
94
- ["lambda", "`function`"],
95
- ["rds", "`database`"],
96
- ["s3", "`storage`"],
97
- ...外した挿絵の種類.map((k): [string, string] => [k, 挿絵の代わり]),
98
- ]);
99
-
100
- /** 外した箱の種類なら、知らせの文面を返す */
101
- export function 外した種類の知らせ(kind: string): { message: string; hint: string } | undefined {
102
- const 代わり = 外した種類.get(kind);
103
- if (代わり === undefined) return undefined;
104
- return {
105
- message: `箱の種類 "${kind}" は外しました`,
106
- hint: 外した挿絵の種類.includes(kind) ? 代わり : `代わりに ${代わり} を書く`,
107
- };
108
- }
109
-
110
- /** 外した図種なら、知らせの文面を返す */
111
- export function 外した図種の知らせ(type: string): { message: string; hint: string } | undefined {
112
- const 代わり = 外した図種.get(type);
113
- if (代わり === undefined) return undefined;
114
- return { message: `図種 "${type}" は外しました`, hint: `代わりに ${代わり} を書く` };
115
- }
@@ -1,313 +0,0 @@
1
- /**
2
- * 値 (`values`) と状態 (`states`) の名前と式が、記法として書ける範囲に収まっているかの判定。
3
- *
4
- * **入口が 2 つあるので 1 か所に置く** (#1181)。 記法 (`v05/parser.ts`) と JSON
5
- * (`json-parser.ts`) が同じ判定を使う。 別々に持つと片方だけが受け付ける形ができ、
6
- * 「YAML では弾かれるのに JSON では通る」 (またはその逆) が生まれる。
7
- *
8
- * 位置の付け方だけが入口で違う (記法は行番号、JSON は field の path)。 そのため本 file は
9
- * 位置を持たない指摘だけを返し、呼出側が自分の形に包む。
10
- */
11
-
12
- /** 値と状態の名前。 描画側が `{名前}` を置き換える時に見る範囲と揃える (英数字と `_` のみ) */
13
- const VALUE_NAME_RE = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
14
-
15
- /** 式に書ける関数。 spec が「関数呼び出しは入れない」 としつつ例外にしている 2 つだけ */
16
- const VALUE_FNS: ReadonlySet<string> = new Set(["min", "max"]);
17
-
18
- /** 位置を持たない指摘。 呼出側が行番号 (記法) か path (JSON) を付けて報告する */
19
- export type ValueSyntaxIssue = {
20
- message: string;
21
- hint?: string;
22
- };
23
-
24
- /** 値 / 状態の名前として使えるか */
25
- export function isValueName(name: string): boolean {
26
- return VALUE_NAME_RE.test(name);
27
- }
28
-
29
- /** 名前が使えない時の指摘 */
30
- export function valueNameIssue(name: string): ValueSyntaxIssue {
31
- return {
32
- message: `invalid value name: "${name}"`,
33
- hint: "英字か _ で始め、 英数字と _ だけを使う (states と同じ規則)",
34
- };
35
- }
36
-
37
- /** きっかけ形の読み取り結果。 位置は呼出側が付ける (本 file の方針) */
38
- export type ParsedValueTrigger = {
39
- trigger:
40
- | { kind: "step"; step: string }
41
- | { kind: "reaches"; source: string; op: TriggerOp; threshold: number };
42
- to: number;
43
- durationMs: number;
44
- };
45
-
46
- export type TriggerOp = ">=" | ">" | "<=" | "<" | "==" | "!=";
47
-
48
- /** 比較の記号。 **2 文字を先に見る** = `>=` を `>` と読むと境目が 1 つずれる */
49
- const TRIGGER_OPS: readonly TriggerOp[] = [">=", "<=", "==", "!=", ">", "<"];
50
-
51
- /**
52
- * 動く長さを ms に直す。 段の頭 (`"取込み" 2s`) と同じ書き方を受ける。
53
- *
54
- * 単位を書かない形は秒として読む (段の頭と揃える)。 0 以下は動かないので受けない。
55
- */
56
- export function parseDurationMs(text: string): number | null {
57
- const m = text.trim().match(/^(\d+(?:\.\d+)?)\s*(ms|s)?$/);
58
- if (!m) return null;
59
- const n = parseFloat(m[1] ?? "0");
60
- if (!Number.isFinite(n) || n <= 0) return null;
61
- const unit = m[2] ?? "s";
62
- const durationMs = unit === "ms" ? Math.round(n) : Math.round(n * 1000);
63
- // 描画側の時計は ms の整数。 正の小数でも丸めた結果が 0ms なら、 後段で傾斜の式が
64
- // 0 除算になるため、この入口で「動く長さではない」として弾く。
65
- return durationMs > 0 ? durationMs : null;
66
- }
67
-
68
- /**
69
- * 引用の外にある `,` だけで切る。
70
- *
71
- * 素朴な `split(",")` を使えない。 段の名前に `,` を書ける (`step "取込み, 検証"`) ため、
72
- * 引用の中で切ると名前が 2 つに割れる。
73
- */
74
- function splitOutsideQuotes(body: string): string[] {
75
- const out: string[] = [];
76
- let cur = "";
77
- let quote: string | null = null;
78
- for (const ch of body) {
79
- if (quote) {
80
- if (ch === quote) quote = null;
81
- cur += ch;
82
- continue;
83
- }
84
- if (ch === '"' || ch === "'") {
85
- quote = ch;
86
- cur += ch;
87
- continue;
88
- }
89
- if (ch === ",") {
90
- out.push(cur);
91
- cur = "";
92
- continue;
93
- }
94
- cur += ch;
95
- }
96
- out.push(cur);
97
- return out.map((s) => s.trim()).filter((s) => s !== "");
98
- }
99
-
100
- /** 引用を外す。 引用の中の `,` を守るために引用を残したまま切っているので、 ここで落とす */
101
- function unquote(s: string): string {
102
- const t = s.trim();
103
- if (t.length >= 2 && (t[0] === '"' || t[0] === "'") && t[t.length - 1] === t[0]) {
104
- return t.slice(1, -1);
105
- }
106
- return t;
107
- }
108
-
109
- /**
110
- * きっかけの本体を読む (`step "取込み"` / `vDone >= 100`)。
111
- *
112
- * **`step` を先に見る**。 後に回すと `step "x"` が比較の記号を含まない形として
113
- * 「相手の値の名前」 に落ち、 名前として不正という別の理由で弾かれる = 直し方が伝わらない。
114
- */
115
- function parseTriggerExpression(raw: string, name: string): ParsedValueTrigger["trigger"] | ValueSyntaxIssue {
116
- const text = raw.trim();
117
- const stepMatch = text.match(/^step\s+(.+)$/);
118
- if (stepMatch) {
119
- const step = unquote(stepMatch[1] ?? "");
120
- if (step === "") {
121
- return { message: `trigger の段名が空です ("${name}")`, hint: '`trigger: step "取込み"` の形で段の名前を書く' };
122
- }
123
- return { kind: "step", step };
124
- }
125
- for (const op of TRIGGER_OPS) {
126
- const at = text.indexOf(op);
127
- if (at < 0) continue;
128
- const source = text.slice(0, at).trim();
129
- const rhs = text.slice(at + op.length).trim();
130
- if (!isValueName(source)) {
131
- return {
132
- message: `trigger が見張る値の名前が使えません: "${source}" ("${name}")`,
133
- hint: "英字か _ で始め、 英数字と _ だけを使う",
134
- };
135
- }
136
- const threshold = Number(rhs);
137
- if (rhs === "" || !Number.isFinite(threshold)) {
138
- return {
139
- message: `trigger の境目が数ではありません: "${rhs}" ("${name}")`,
140
- hint: "`trigger: vDone >= 100` のように数で書く",
141
- };
142
- }
143
- return { kind: "reaches", source, op, threshold };
144
- }
145
- return {
146
- message: `trigger の形が読めません: "${text}" ("${name}")`,
147
- hint: '`step "取込み"` か `vDone >= 100` の形で書く',
148
- };
149
- }
150
-
151
- /**
152
- * 中括弧で囲まれた本体が「きっかけ形」 かを見分ける。
153
- *
154
- * 式も中括弧で始まり中括弧で終わることがある (`{a} + {b}` / `{a}`)。 見分けるのは
155
- * **引用の外に `:` があるか**で、 式の中括弧は値の名前しか包まないため `:` を持たない。
156
- *
157
- * 名前で見分けない (`trigger` を含むか等)。 `{trigger} + 1` のように `trigger` という名前の値を
158
- * 読む式が書けるため、 名前で見ると式をきっかけ形と読み違える。
159
- */
160
- export function isTriggerBody(raw: string): boolean {
161
- const t = raw.trim();
162
- if (!t.startsWith("{") || !t.endsWith("}") || t.length < 2) return false;
163
- const inner = t.slice(1, -1);
164
- let quote: string | null = null;
165
- for (const ch of inner) {
166
- if (quote) {
167
- if (ch === quote) quote = null;
168
- continue;
169
- }
170
- if (ch === '"' || ch === "'") {
171
- quote = ch;
172
- continue;
173
- }
174
- if (ch === ":") return true;
175
- }
176
- return false;
177
- }
178
-
179
- /**
180
- * きっかけ形の中身を読む (`{ trigger: step "取込み", to: 100, dur: 2s }`)。
181
- *
182
- * 3 つとも必須にする。 既定値を置くと「書かなかった」 と「その値を書いた」 が同じになり、
183
- * 動かない時に何を直せばよいか分からなくなる。
184
- */
185
- export function parseValueTriggerBody(
186
- body: string,
187
- name: string,
188
- ): { spec: ParsedValueTrigger; issues: [] } | { spec: null; issues: ValueSyntaxIssue[] } {
189
- const issues: ValueSyntaxIssue[] = [];
190
- const seen = new Map<string, string>();
191
- for (const part of splitOutsideQuotes(body)) {
192
- const at = part.indexOf(":");
193
- if (at < 0) {
194
- issues.push({ message: `"${part}" は key: value の形ではありません ("${name}")`, hint: "`trigger: ... , to: 100, dur: 2s` の形で書く" });
195
- continue;
196
- }
197
- const key = part.slice(0, at).trim().toLowerCase();
198
- const value = part.slice(at + 1).trim();
199
- if (seen.has(key)) {
200
- issues.push({ message: `"${key}" を 2 度書いています ("${name}")`, hint: "同じ項目は 1 度だけ書く" });
201
- continue;
202
- }
203
- seen.set(key, value);
204
- }
205
-
206
- const known = new Set(["trigger", "to", "dur"]);
207
- for (const key of seen.keys()) {
208
- if (known.has(key)) continue;
209
- issues.push({
210
- message: `"${key}" は values に書けません ("${name}")`,
211
- hint: `書けるのは ${[...known].join(" / ")} だけ`,
212
- });
213
- }
214
-
215
- const triggerRaw = seen.get("trigger");
216
- let trigger: ParsedValueTrigger["trigger"] | null = null;
217
- if (triggerRaw === undefined) {
218
- issues.push({ message: `trigger がありません ("${name}")`, hint: '`trigger: step "取込み"` か `trigger: vDone >= 100` を書く' });
219
- } else {
220
- const parsed = parseTriggerExpression(triggerRaw, name);
221
- if ("message" in parsed) issues.push(parsed);
222
- else trigger = parsed;
223
- }
224
-
225
- const toRaw = seen.get("to");
226
- let to = 0;
227
- if (toRaw === undefined) {
228
- issues.push({ message: `to がありません ("${name}")`, hint: "`to: 100` のように動いた先の値を書く" });
229
- } else {
230
- to = Number(unquote(toRaw));
231
- if (!Number.isFinite(to)) {
232
- issues.push({ message: `to が数ではありません: "${toRaw}" ("${name}")`, hint: "`to: 100` のように数で書く" });
233
- }
234
- }
235
-
236
- const durRaw = seen.get("dur");
237
- let durationMs = 0;
238
- if (durRaw === undefined) {
239
- issues.push({ message: `dur がありません ("${name}")`, hint: "`dur: 2s` のように動く長さを書く" });
240
- } else {
241
- const parsedDur = parseDurationMs(unquote(durRaw));
242
- if (parsedDur === null) {
243
- issues.push({ message: `dur が読めません: "${durRaw}" ("${name}")`, hint: "`2s` / `1.5s` / `500ms` の形で、 0 より大きい長さを書く" });
244
- } else {
245
- durationMs = parsedDur;
246
- }
247
- }
248
-
249
- if (issues.length > 0 || trigger === null) return { spec: null, issues };
250
- return { spec: { trigger, to, durationMs }, issues: [] };
251
- }
252
-
253
- /**
254
- * 式が記法として書ける範囲に収まっているかを見る。
255
- *
256
- * 式が文法として正しいかは見ない。 そこは描画側が評価する時に判定して、その値だけを止める
257
- * (spec § 4.2 = 1 箇所の壊れで図全体を止めない)。 ここで見るのは **記法として書ける範囲に
258
- * 収まっているか**で、描画側が受け付けるが記法としては認めない書き方 (余り / 条件分岐 /
259
- * `min` `max` 以外の関数) を弾く。
260
- *
261
- * `{名前}` の中身と外側を分けて見る。 分けないと、名前に紛れた記号を式の記号と読み違える。
262
- */
263
- export function checkValueExpression(expression: string, name: string): ValueSyntaxIssue[] {
264
- const issues: ValueSyntaxIssue[] = [];
265
-
266
- // `{名前}` の中身は名前の規則で見る
267
- const refRe = /\{([^}]*)\}/g;
268
- let m: RegExpExecArray | null;
269
- while ((m = refRe.exec(expression)) !== null) {
270
- const ref = (m[1] ?? "").trim();
271
- if (!VALUE_NAME_RE.test(ref)) {
272
- issues.push({
273
- message: `invalid reference "{${ref}}" in "${name}"`,
274
- hint: "英字か _ で始め、 英数字と _ だけを使う",
275
- });
276
- }
277
- }
278
- if (expression.includes("{") && !expression.includes("}")) {
279
- issues.push({ message: `unclosed "{" in "${name}"`, hint: "`{名前}` の形で閉じる" });
280
- }
281
-
282
- // 名前を外した残りが式の骨格。 ここに記法外の記号や関数が無いかを見る
283
- const outside = expression.replace(/\{[^}]*\}/g, " ");
284
- if (outside.includes("%")) {
285
- issues.push({ message: `"%" は式に書けない ("${name}")`, hint: "四則 (+ - * /) だけを使う" });
286
- }
287
- if (outside.includes("?")) {
288
- issues.push({
289
- message: `条件分岐 (?:) は式に書けない ("${name}")`,
290
- hint: "比較の結果は真 = 1 / 偽 = 0 の数になるので、 掛け算で切り替える",
291
- });
292
- }
293
- for (const fn of outside.matchAll(/[a-zA-Z_][a-zA-Z0-9_.]*/g)) {
294
- const word = fn[0];
295
- if (VALUE_FNS.has(word)) continue;
296
- issues.push({
297
- message: `"${word}" は式に書けない ("${name}")`,
298
- hint:
299
- word.startsWith("Math.")
300
- ? "min / max は Math. を付けずに書く"
301
- : `使えるのは ${[...VALUE_FNS].join(" / ")} だけ。 値は {名前} で読む`,
302
- });
303
- }
304
- const stray = outside.replace(/[a-zA-Z_][a-zA-Z0-9_.]*/g, " ").match(/[^0-9.,+\-*/()<>=!\s]/g);
305
- if (stray) {
306
- issues.push({
307
- message: `"${[...new Set(stray)].join("")}" は式に書けない ("${name}")`,
308
- hint: "四則 (+ - * /) / 括弧 / 比較 (> >= < <= == !=) / min / max だけを使う",
309
- });
310
- }
311
-
312
- return issues;
313
- }
@@ -1,270 +0,0 @@
1
- /**
2
- * 登場人物 1 人の位置を DSL 本文に書き込む。
3
- *
4
- * editor の画面で見えている座標を、 そのまま記法に落とすために使う。 書く人は図を見ながら
5
- * 数値を決められないので、 今の位置を出発点として渡す経路が要る。
6
- *
7
- * 記法を知っているのは本 package なので、 本文の書き換えもここに置く。 画面側に置くと
8
- * 記法が増えた時に画面側だけが取り残される。
9
- *
10
- * 書き方は 4 通りあり、 元の形を保ったまま位置だけを差し替える。 形を勝手に揃えると、
11
- * 書いた人の見た目が変わって差分が読めなくなる。
12
- */
13
-
14
- /** 位置の書き込み結果。 対象が見つからなければ null。 */
15
- export function writeActorPosition(
16
- src: string,
17
- actorName: string,
18
- posX: number,
19
- posY: number,
20
- ): string | null {
21
- if (!Number.isFinite(posX) || !Number.isFinite(posY)) return null;
22
- const x = Math.round(posX);
23
- const y = Math.round(posY);
24
-
25
- // 改行コードは行ごとに元のまま残す。 buffer 全体で一括判定すると、 混在した本文で
26
- // 無関係な行の改行まで書き換わって差分が変更箇所の外に広がる
27
- const seg = src.split(/(\r\n|\n)/);
28
- const lineAt = (i: number): string => seg[i * 2] ?? "";
29
- const lineCount = Math.ceil(seg.length / 2);
30
-
31
- // 探すのは `actors:` の中だけ。 全文を走ると、 別の項目に同じ名前で並ぶ行
32
- // (`notes:` の下の `- API: service` 等) を先に掴んで書き換える (実測)
33
- const span = actorsSpan(lineAt, lineCount);
34
- if (!span) return null;
35
-
36
- for (let i = span.from; i < span.to; i += 1) {
37
- const head = matchActorHead(lineAt(i));
38
- if (!head || head.name !== actorName) continue;
39
-
40
- // 1. `- API: { kind: service }` = 中括弧の中に座標を入れる
41
- if (head.rest.startsWith("{") && head.rest.endsWith("}")) {
42
- seg[i * 2] = `${head.prefix}{ ${upsertInlineFields(head.rest.slice(1, -1), x, y)} }`;
43
- return seg.join("");
44
- }
45
-
46
- // 2. `- API:` だけの行 = 続く字下げ行に `位置:` を入れる
47
- if (head.hasColon && head.rest === "") {
48
- return writeVerticalForm(seg, i, span.to, head.indent, x, y);
49
- }
50
-
51
- // 3. `- API: service` = 値の並びに `@x,y` を入れる
52
- if (head.hasColon) {
53
- seg[i * 2] = `${head.prefix}${upsertAtToken(head.rest, x, y)}`;
54
- return seg.join("");
55
- }
56
-
57
- // 4. `- API` = 名前だけ。 値を書ける形にしてから座標を足す
58
- seg[i * 2] = `${head.indentText}- ${head.rawName}: @${x},${y}`;
59
- return seg.join("");
60
- }
61
- return null;
62
- }
63
-
64
- /**
65
- * `actors:` block の行範囲。 見つからなければ null。
66
- *
67
- * 開始は行頭の `actors:` に限る。 終わりは次の行頭項目 (字下げなしの `key:`) の直前。
68
- *
69
- * `actors:` が複数ある本文では最後のものを返す。 記法側は同じ項目が 2 度現れると後の方で
70
- * 上書きするため、 最初のものを見ると parser が採らない箱を書き換えることになる
71
- * (実測 = 後の block に居る箱の書き戻しが `null` になった)。
72
- */
73
- function actorsSpan(
74
- lineAt: (i: number) => string,
75
- lineCount: number,
76
- ): { from: number; to: number } | null {
77
- let from = -1;
78
- let to = lineCount;
79
- for (let i = 0; i < lineCount; i += 1) {
80
- const line = lineAt(i);
81
- if (/^actors[ \t]*:[ \t]*$/.test(line)) {
82
- from = i + 1;
83
- to = lineCount;
84
- continue;
85
- }
86
- // 字下げのない `key:` は次の項目の始まり。 直前の block はそこで閉じる
87
- if (from >= 0 && to === lineCount && /^[^\s#][^:]*:/.test(line)) to = i;
88
- }
89
- return from < 0 ? null : { from, to };
90
- }
91
-
92
- type ActorHead = {
93
- /** 行頭から `:` までをそのまま保つ。 元の空白の入れ方を壊さないために持つ */
94
- prefix: string;
95
- indentText: string;
96
- indent: number;
97
- rawName: string;
98
- name: string;
99
- hasColon: boolean;
100
- rest: string;
101
- };
102
-
103
- /**
104
- * ` - API: service` の形を、 前置き / 名前 / 残りに分ける。
105
- *
106
- * `:` を任意にした 1 本の正規表現では書けない。 名前の部分を控えめに取ると `A` で切って
107
- * 残りを `PI: service` と読んでしまう (実測)。 `:` のある形を先に当てる。
108
- */
109
- function matchActorHead(line: string): ActorHead | null {
110
- const NAME = String.raw`"(?:[^"\\]|\\.)*"|[^:\s][^:]*?`;
111
- const withColon = line.match(new RegExp(String.raw`^(\s*)-[ \t]+(${NAME})[ \t]*:[ \t]*(.*)$`));
112
- if (withColon) {
113
- const [, indentText = "", rawName = "", rest = ""] = withColon;
114
- return {
115
- // 元の空白の入れ方を壊さないよう、 行頭から値の直前までをそのまま持つ
116
- prefix: line.slice(0, line.length - rest.length),
117
- indentText,
118
- indent: indentText.length,
119
- rawName,
120
- name: unquote(rawName),
121
- hasColon: true,
122
- rest: rest.trimEnd(),
123
- };
124
- }
125
- const nameOnly = line.match(new RegExp(String.raw`^(\s*)-[ \t]+("(?:[^"\\]|\\.)*"|\S+)[ \t]*$`));
126
- if (!nameOnly) return null;
127
- const [, indentText = "", rawName = ""] = nameOnly;
128
- return {
129
- prefix: line,
130
- indentText,
131
- indent: indentText.length,
132
- rawName,
133
- name: unquote(rawName),
134
- hasColon: false,
135
- rest: "",
136
- };
137
- }
138
-
139
- function unquote(raw: string): string {
140
- if (!raw.startsWith('"') || !raw.endsWith('"') || raw.length < 2) return raw;
141
- return raw.slice(1, -1).replace(/\\(.)/g, "$1");
142
- }
143
-
144
- /**
145
- * 空白区切りの値の並びに `@x,y` を差し込む。 既に書かれていれば差し替える。
146
- *
147
- * 引用符の中は触らない。 補足 (`"@1,2 の話"`) を座標と取り違えないため。
148
- */
149
- function upsertAtToken(rest: string, x: number, y: number): string {
150
- const kept = splitOutsideQuotes(rest).filter((t) => !/^@-?\d+(\.\d+)?\s*[,、]\s*-?\d+(\.\d+)?$/.test(t));
151
- kept.push(`@${x},${y}`);
152
- return kept.join(" ");
153
- }
154
-
155
- /** 引用符と角括弧の外の空白で区切る。 */
156
- function splitOutsideQuotes(s: string): string[] {
157
- const out: string[] = [];
158
- let buf = "";
159
- let quote: '"' | "'" | null = null;
160
- let depth = 0;
161
- for (let i = 0; i < s.length; i += 1) {
162
- const c = s[i]!;
163
- if (quote) {
164
- buf += c;
165
- if (quote === '"' && c === "\\") {
166
- buf += s[i + 1] ?? "";
167
- i += 1;
168
- continue;
169
- }
170
- if (c === quote) quote = null;
171
- continue;
172
- }
173
- if (c === '"' || c === "'") {
174
- quote = c;
175
- buf += c;
176
- continue;
177
- }
178
- if (c === "[") depth += 1;
179
- if (c === "]") depth -= 1;
180
- if (/\s/.test(c) && depth === 0) {
181
- if (buf) out.push(buf);
182
- buf = "";
183
- continue;
184
- }
185
- buf += c;
186
- }
187
- if (buf) out.push(buf);
188
- return out;
189
- }
190
-
191
- /** 中括弧の中の `posX` / `posY` を差し替える。 他の項目は順番ごと残す。 */
192
- function upsertInlineFields(inner: string, x: number, y: number): string {
193
- const kept = splitTopLevel(inner).filter((f) => {
194
- const key = f.slice(0, f.indexOf(":")).trim();
195
- return key !== "posX" && key !== "posY";
196
- });
197
- kept.push(`posX: ${x}`, `posY: ${y}`);
198
- return kept.join(", ");
199
- }
200
-
201
- /** 中括弧と角括弧の深さ 0 の `,` で区切る。 入れ子の中の項目を top-level と取り違えない。 */
202
- function splitTopLevel(inner: string): string[] {
203
- const out: string[] = [];
204
- let depth = 0;
205
- let start = 0;
206
- let quote: '"' | "'" | null = null;
207
- for (let i = 0; i < inner.length; i += 1) {
208
- const c = inner[i]!;
209
- if (quote) {
210
- if (quote === '"' && c === "\\") {
211
- i += 1;
212
- continue;
213
- }
214
- if (c === quote) quote = null;
215
- continue;
216
- }
217
- if (c === '"' || c === "'") quote = c;
218
- else if (c === "{" || c === "[") depth += 1;
219
- else if (c === "}" || c === "]") depth -= 1;
220
- else if (c === "," && depth === 0) {
221
- out.push(inner.slice(start, i));
222
- start = i + 1;
223
- }
224
- }
225
- out.push(inner.slice(start));
226
- return out.map((f) => f.trim()).filter((f) => f.length > 0);
227
- }
228
-
229
- /**
230
- * 縦に並べた形に `位置:` を入れる。
231
- *
232
- * 既にあれば値だけ差し替える。 無ければ最後の項目の次に足す。 字下げは既にある項目に
233
- * 合わせ、 項目が 1 つも無ければ頭の字下げに 4 を足す。
234
- *
235
- * 差し替える対象は登場人物の直下の項目だけ。 深さを見ないと、 入れ子の中の `位置:`
236
- * (`nodes: → header: → 位置:`) を掴んで書き換える (実測)。
237
- */
238
- function writeVerticalForm(
239
- seg: string[],
240
- headIdx: number,
241
- lineCount: number,
242
- headIndent: number,
243
- x: number,
244
- y: number,
245
- ): string {
246
- let lastChild = -1;
247
- let childIndent = -1;
248
- for (let j = headIdx + 1; j < lineCount; j += 1) {
249
- const line = seg[j * 2] ?? "";
250
- if (line.trim() === "") continue;
251
- const indent = line.length - line.trimStart().length;
252
- if (indent <= headIndent) break;
253
- if (childIndent < 0) childIndent = indent;
254
- lastChild = j;
255
- // 位置 / pos の行が既にあれば、 その行の値だけ差し替える。 直下の深さのものだけを見る
256
- const key = indent === childIndent ? line.trim().match(/^(位置|pos)\s*:/) : null;
257
- if (key) {
258
- seg[j * 2] = `${" ".repeat(indent)}${key[1]}: ${x},${y}`;
259
- return seg.join("");
260
- }
261
- }
262
- const indent = childIndent >= 0 ? childIndent : headIndent + 4;
263
- const newLine = `${" ".repeat(indent)}位置: ${x},${y}`;
264
- const insertAt = (lastChild >= 0 ? lastChild : headIdx) + 1;
265
- // 挿入位置の直前で実際に使われている改行に合わせる。 末尾に足す時は最後の区切りを見る
266
- const sep = seg[insertAt * 2 - 1] ?? seg[seg.length - 2] ?? "\n";
267
- if (insertAt * 2 >= seg.length) seg.push(sep, newLine);
268
- else seg.splice(insertAt * 2, 0, newLine, sep);
269
- return seg.join("");
270
- }