@cardenelabs/dragon 0.28.0 → 0.30.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 (57) hide show
  1. package/README.md +78 -5
  2. package/dist/index.cjs +9246 -8698
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +186 -66
  5. package/dist/index.d.ts +186 -66
  6. package/dist/index.js +9247 -8700
  7. package/dist/index.js.map +1 -1
  8. package/package.json +1 -1
  9. package/src/compile/actor-ids.ts +306 -0
  10. package/src/compile/actors.ts +74 -0
  11. package/src/compile/c4.ts +77 -0
  12. package/src/compile/chain.ts +54 -0
  13. package/src/compile/chart-fields.ts +178 -0
  14. package/src/compile/class.ts +100 -0
  15. package/src/compile/deep-rewrite.ts +30 -0
  16. package/src/compile/derived-source.ts +18 -0
  17. package/src/compile/direction.ts +63 -0
  18. package/src/compile/er-relation.ts +200 -0
  19. package/src/compile/er.ts +44 -0
  20. package/src/compile/flow.ts +54 -0
  21. package/src/compile/funnel.ts +75 -0
  22. package/src/compile/gantt.ts +232 -0
  23. package/src/compile/generic.ts +284 -0
  24. package/src/compile/hierarchy.ts +92 -0
  25. package/src/compile/journey.ts +84 -0
  26. package/src/compile/kinds.ts +156 -0
  27. package/src/compile/lanes.ts +56 -0
  28. package/src/compile/mind.ts +343 -0
  29. package/src/compile/node-title.ts +24 -0
  30. package/src/compile/notice.ts +94 -0
  31. package/src/compile/parts-lookup.ts +17 -0
  32. package/src/compile/parts.ts +2635 -0
  33. package/src/compile/placement-apply.ts +673 -0
  34. package/src/compile/placement.ts +15 -0
  35. package/src/compile/quadrant.ts +110 -0
  36. package/src/compile/rows.ts +40 -0
  37. package/src/compile/sequence.ts +226 -0
  38. package/src/compile/slug.ts +29 -0
  39. package/src/compile/solidity.ts +39 -0
  40. package/src/compile/state.ts +71 -0
  41. package/src/compile/subtitle.ts +22 -0
  42. package/src/compile/swimlane.ts +83 -0
  43. package/src/compile/topology.ts +50 -0
  44. package/src/compile/tree.ts +60 -0
  45. package/src/compile/value-chart.ts +183 -0
  46. package/src/compile/values.ts +432 -0
  47. package/src/compile/word-state.ts +82 -0
  48. package/src/compile.ts +513 -6858
  49. package/src/index.ts +8 -7
  50. package/src/json-line-map.ts +30 -0
  51. package/src/json-parser.ts +356 -123
  52. package/src/near-name.ts +42 -0
  53. package/src/relative-pos.ts +71 -2
  54. package/src/schema.ts +1 -1
  55. package/src/schemas/diagram.json +38 -4
  56. package/src/types.ts +1 -1
  57. package/src/v05/parser.ts +91 -72
@@ -0,0 +1,42 @@
1
+ /**
2
+ * 書き間違えた名前に綴りの近い候補を探す (#1295 で JSON の項目名に作り、#2113 で部品の名前と共有した)。
3
+ *
4
+ * 「知らない名前です」 だけだと、書いた人は正しい綴りを探しに行く必要がある。
5
+ * 2 文字の違い (足りない / 多い / 別の字) までを候補とする。 隣どうしの入れ替わりは 2 文字の違いに数える。
6
+ *
7
+ * 遠い名前は勧めない。 無関係な名前を勧めると、書いた人がそちらへ直して二度手間になる。
8
+ * 大文字と小文字は区別しない。
9
+ */
10
+ export function 近い名前(語: string, 候補: Iterable<string>): string | undefined {
11
+ const 小文字 = 語.toLowerCase();
12
+ let 最短: { 名: string; 距離: number } | undefined;
13
+ for (const c of 候補) {
14
+ const d = 編集距離(小文字, c.toLowerCase(), 2);
15
+ if (d <= 2 && (最短 === undefined || d < 最短.距離)) 最短 = { 名: c, 距離: d };
16
+ }
17
+ return 最短?.名;
18
+ }
19
+
20
+ /**
21
+ * 2 つの語の編集距離 (上限付き)。
22
+ *
23
+ * 上限を持つのは、長い語どうしで表を全部埋めないため。 上限を超えた時点で打ち切る。
24
+ */
25
+ function 編集距離(a: string, b: string, 上限: number): number {
26
+ if (Math.abs(a.length - b.length) > 上限) return 上限 + 1;
27
+ let 前 = Array.from({ length: b.length + 1 }, (_, i) => i);
28
+ for (let i = 1; i <= a.length; i += 1) {
29
+ const 今: number[] = [i];
30
+ let 行の最小 = i;
31
+ for (let j = 1; j <= b.length; j += 1) {
32
+ const 費用 = a[i - 1] === b[j - 1] ? 0 : 1;
33
+ const v = Math.min(今[j - 1]! + 1, 前[j]! + 1, 前[j - 1]! + 費用);
34
+ 今.push(v);
35
+ if (v < 行の最小) 行の最小 = v;
36
+ }
37
+ // その行の最小が上限を超えたら、以降どう進んでも上限以下にはならない
38
+ if (行の最小 > 上限) return 上限 + 1;
39
+ 前 = 今;
40
+ }
41
+ return 前[b.length]!;
42
+ }
@@ -12,8 +12,20 @@
12
12
  * 直らない事故を、 規則を共有することで構造的に防ぐ。
13
13
  */
14
14
 
15
- /** 基準からどちら側に置くか。 */
16
- export type RelativeDirection = "right" | "left" | "above" | "below";
15
+ /**
16
+ * 基準からどちら側に置くか。
17
+ *
18
+ * 語の一覧は下の `RELATIVE_DIRECTIONS` が持つ。 型を並べ直さず、一覧から導く。
19
+ */
20
+ export type RelativeDirection = (typeof RELATIVE_DIRECTIONS)[number];
21
+
22
+ /**
23
+ * 向きとして受ける語の一覧。
24
+ *
25
+ * 読み取りの入口が 2 つある (本文の `位置: Web の右 200` と JSON の `posRel.dir`) ため、
26
+ * 一覧を 1 箇所に置いて両方から引く。 2 度書くと、向きを足した時に片方だけが取り残される。
27
+ */
28
+ export const RELATIVE_DIRECTIONS = ["right", "left", "above", "below"] as const;
17
29
 
18
30
  /** 相対で書かれた位置の指定。 */
19
31
  export type RelativePos = {
@@ -180,3 +192,60 @@ export function orderByDependency(
180
192
  }
181
193
  return { order, cyclic: [...cyclic] };
182
194
  }
195
+
196
+ /** 解けない書き方の種類。 誤りの文は入口ごとに作るので、ここでは種類だけを返す */
197
+ export type RelativeProblemKind = "self" | "missing-anchor" | "cyclic";
198
+
199
+ /** 解けない書き方 1 件。 `anchor` は相手が居ない時だけ持つ */
200
+ export type RelativeProblem = {
201
+ name: string;
202
+ kind: RelativeProblemKind;
203
+ anchor?: string;
204
+ };
205
+
206
+ /**
207
+ * 解けない相対の指定を挙げる (#2039)。
208
+ *
209
+ * 解けない書き方は 3 通りある。 相手が居ない / 自分を基準にした / 基準が輪になっている。
210
+ * どれも「書いたのに図が変わらない」 形で表に出るため、図を出す前に知らせる。
211
+ *
212
+ * **判定は本 file に 1 つだけ置く**。 読み取りの入口が 2 つある (本文と JSON) ため、
213
+ * 片方だけに判定を持つと、同じ書き方が入口によって通ったり通らなかったりする。
214
+ * 実際 JSON 側には判定が無く、相対で置く欄そのものが無かった。
215
+ *
216
+ * 誤りの文は返さない。 本文側は行番号を、JSON 側は欄の場所を添えるため、文の組み立ては
217
+ * 入口に任せる。 ここが返すのは「誰の」「どの種類の」 解けなさか だけ。
218
+ *
219
+ * 呼ぶ側は、挙がった名前から相対の指定を外す。 残したままだと、誤りを無視して読み込んだ
220
+ * 経路で解決できない指定が組み立てまで届く。
221
+ */
222
+ export function findRelativeProblems(
223
+ items: ReadonlyArray<{ name: string; rel?: RelativePos }>,
224
+ ): RelativeProblem[] {
225
+ const named = new Set(items.map((i) => i.name));
226
+ const broken = new Set<string>();
227
+ const out: RelativeProblem[] = [];
228
+
229
+ for (const it of items) {
230
+ const rel = it.rel;
231
+ if (!rel) continue;
232
+ if (rel.anchor === it.name) {
233
+ out.push({ name: it.name, kind: "self" });
234
+ broken.add(it.name);
235
+ continue;
236
+ }
237
+ if (!named.has(rel.anchor)) {
238
+ out.push({ name: it.name, kind: "missing-anchor", anchor: rel.anchor });
239
+ broken.add(it.name);
240
+ }
241
+ }
242
+
243
+ // 輪は、既に解けないと判った分を外してから探す。 外さないと「相手が居ない」 1 件が
244
+ // 「輪になっている」 としても挙がり、同じ 1 つの誤りが 2 通りの直し方で出る
245
+ const { cyclic } = orderByDependency(
246
+ items.map((i) => ({ name: i.name, rel: broken.has(i.name) ? undefined : i.rel })),
247
+ );
248
+ for (const name of cyclic) out.push({ name, kind: "cyclic" });
249
+
250
+ return out;
251
+ }
package/src/schema.ts CHANGED
@@ -13,7 +13,7 @@
13
13
  * });
14
14
  * const diagram = jsonToDiagram(res.content[0].input);
15
15
  *
16
- * schema JSON は `packages/dragon/schemas/diagram.json` が SSOT、 本 file は import して再 export するだけ。
16
+ * schema JSON は `packages/dragon/src/schemas/diagram.json` が SSOT、 本 file は import して再 export するだけ。
17
17
  */
18
18
 
19
19
  import schemaJson from "./schemas/diagram.json" with { type: "json" };
@@ -106,7 +106,7 @@
106
106
  },
107
107
  "kind": {
108
108
  "type": "string",
109
- "description": "node kind。 shape-* を指定して 49 shape 中から選ぶ。 例: 'shape-wallet' / 'shape-smart-contract' / 'shape-cloud' / 'shape-file' / 'actor' (default) / 'function' / 'storage' / 'event' 等"
109
+ "description": "node kind。 shape-* を指定して 49 shape 中から選ぶ。 例: 'shape-wallet' / 'shape-smart-contract' / 'shape-cloud' / 'shape-file' / 'actor' (default) / 'function' / 'storage' / 'event' 等。 種別に無い名前は部品の名前として部品の一覧 (partsCatalog) から引き、一覧にも無ければ種別を書かなかった箱になる。 部品を持つ一覧を渡した時は知らせを出し、綴りの近い名前を案内する。"
110
110
  },
111
111
  "subtitle": {
112
112
  "type": "string",
@@ -215,7 +215,7 @@
215
215
  },
216
216
  "end": {
217
217
  "type": "string",
218
- "description": "gantt の工程が終わる時期。"
218
+ "description": "gantt の工程が終わる時期。 始まりより前の時期は始まりと同じに倒し、知らせを出す。"
219
219
  },
220
220
  "touchpoint": {
221
221
  "type": "string",
@@ -245,6 +245,10 @@
245
245
  "type": "number",
246
246
  "description": "parts の描画倍率。 kind に parts identifier を指定した時だけ使える。"
247
247
  },
248
+ "phase": {
249
+ "type": "boolean",
250
+ "description": "parts が持つ段を使うか。 既定は書いた段と番号ごとに並べて再生する。 false を書くと parts の段の中身を空にし、値は animation に書いた段だけが動かす。 kind に parts identifier を指定した時だけ使える。"
251
+ },
248
252
  "pos": {
249
253
  "type": "object",
250
254
  "additionalProperties": false,
@@ -262,6 +266,36 @@
262
266
  }
263
267
  }
264
268
  },
269
+ "posRel": {
270
+ "type": "object",
271
+ "additionalProperties": false,
272
+ "description": "位置を他の要素からの相対で書く指定。 座標を知らなくても「誰の」「どちら側に」「どれだけ離して」 で置ける。 解決した後は posX / posY を書いた時と同じ経路を通る。",
273
+ "required": [
274
+ "anchor",
275
+ "dir"
276
+ ],
277
+ "properties": {
278
+ "anchor": {
279
+ "type": "string",
280
+ "minLength": 1,
281
+ "description": "基準にする相手の名前。 actors に書かれた名前をそのまま書く。"
282
+ },
283
+ "dir": {
284
+ "type": "string",
285
+ "enum": [
286
+ "right",
287
+ "left",
288
+ "above",
289
+ "below"
290
+ ],
291
+ "description": "基準からどちら側に置くか。"
292
+ },
293
+ "gap": {
294
+ "type": "number",
295
+ "description": "相手の縁との間隔。 書かなければ 160 で置く。"
296
+ }
297
+ }
298
+ },
265
299
  "visibleIf": {
266
300
  "type": "string",
267
301
  "description": "その箱を出すかどうかの条件。 状態を差し込める文字列で、\"0\" / \"false\" / 空文字が偽、それ以外は真。"
@@ -538,7 +572,7 @@
538
572
  },
539
573
  "cardinality": {
540
574
  "type": "string",
541
- "description": "ER の多重度 (1..N 等、 kind: er 用)"
575
+ "description": "ER の多重度 (type: er 用)。 1:1 / 1:N / N:1 / N:M / 0..1 / 1..* のどれかを書くと、 両端の形がその語から決まり (tailHead / head を書けばそちらが勝つ)、 語は説明文の下の行に出る。 説明文に書いた同じ語は説明文から外す。 6 語以外を書くと説明文に (語) と添えるだけで、 端の形は決まらない (tailHead と head を両方書かなければ、 描かない端を知らせる)。 type: er 以外の図種では描かず、 そのことを知らせる"
542
576
  },
543
577
  "side": {
544
578
  "type": "string",
@@ -2321,7 +2355,7 @@
2321
2355
  },
2322
2356
  "events": {
2323
2357
  "type": "array",
2324
- "description": "押下などの出来事で動く仕掛け。 相手は名前で指す (box / lane / arrow / diagram のどれか 1 つ)。",
2358
+ "description": "押下などの出来事で動く仕掛け。 相手は名前で指し、指し方の項目をちょうど 1 つだけ書く。",
2325
2359
  "items": {
2326
2360
  "type": "object",
2327
2361
  "required": [
package/src/types.ts CHANGED
@@ -637,10 +637,10 @@ export type DslValue = {
637
637
  }
638
638
  );
639
639
 
640
- /** ステップ (phase) */
641
640
  /** 動いている間の帯 (#1466)。 順序図で、面がいつ動いているかを段の番号で持つ */
642
641
  export type DslBand = { actor: string; from: number; to: number };
643
642
 
643
+ /** ステップ (phase) */
644
644
  export type DslPhase = {
645
645
  name: string;
646
646
  durationMs: number;
package/src/v05/parser.ts CHANGED
@@ -47,7 +47,7 @@ import type { EdgeReveal, RelationFocus } from "@cardenelabs/cdl";
47
47
  import { TONE_ALIAS, NODE_KIND_ALIAS, DIRECTIONS, resolveDirection, PALETTES, resolvePalette } from "../keywords";
48
48
  import type { DslPalette } from "../keywords";
49
49
  import type { DslDirection } from "../keywords";
50
- import { parseRelativePos, orderByDependency } from "../relative-pos";
50
+ import { parseRelativePos, findRelativeProblems, type RelativeProblem } from "../relative-pos";
51
51
  import {
52
52
  checkValueExpression,
53
53
  isTriggerBody,
@@ -383,14 +383,12 @@ const INFRA_KIND_ALIAS: Record<string, NodeKind> = {
383
383
  };
384
384
 
385
385
  /**
386
- * 受け付ける箱の種類。 描画できる種類 (cdl の `NODE_KINDS`) に、 記法だけが持つ種類を足す。
386
+ * 記法が受理する種類の全体。 これに載っていない種類は見本 (パーツ) の候補になる。
387
387
  *
388
- * 以前は手書きの 31 種だった。 描画できる 90 種のうち 78 種が記法から書けず、 部品名として
389
- * 扱われて「そんな部品は無い」 と警告が出るだけだった。 描画側を出所に加えることで
388
+ * 描画できる種類 (cdl の `NODE_KINDS`) に、 記法だけが持つ種類と基盤の別名を足す。
389
+ * 手書きの一覧だった頃は、 描画側に種類が増えても記法から書けず、 部品名として扱われて
390
+ * 「そんな部品は無い」 と警告が出るだけだった。 描画側を出所に加えることで
390
391
  * 「描画できるものは書ける」 が成立する。
391
- */
392
- /**
393
- * 記法が受理する種類の全体。 これに載っていない種類は見本 (パーツ) の候補になる。
394
392
  *
395
393
  * 画面側が「本文が見本を使っているか」 を判定するのに使う (#1022)。 手書きの一覧を
396
394
  * 別に持つと、種類が増えた時にそちらだけ取り残されて余分な読み込みが起きる。
@@ -1553,7 +1551,7 @@ export const AXIS_Y_KEYS = ["bottom", "top"] as const satisfies readonly (keyof
1553
1551
  // `parser.ts` に置いたままだと 生成した表 → parser → 生成した表 の輪ができる
1554
1552
  export type { 欄の形, 図形の定義 } from "./parser-types";
1555
1553
 
1556
- /** 描ける図形と、その欄 (`CdlDynShape` の全 5 種を覆う) */
1554
+ /** 描ける図形と、その欄 (`CdlDynShape` の全種を覆う) */
1557
1555
  export const 図形の表: Record<string, 図形の定義> = {
1558
1556
  rect: {
1559
1557
  必須: ["source", "fillMax"],
@@ -1608,9 +1606,9 @@ export const 図形の表: Record<string, 図形の定義> = {
1608
1606
  /**
1609
1607
  * 記法が受ける部品と、その欄。
1610
1608
  *
1611
- * **手で書かず、描画側の型定義から生成する** (#1385)。 描画側は 107 種を持ち欄は 510 個あり、
1612
- * 手で写すと写し間違いと描画側の変更への drift が残る (`rules/quality.md § 導出可能記述は
1613
- * 人手で書かない`)。
1609
+ * **手で書かず、描画側の型定義から生成する** (#1385)。 描画側が持つ種類も、種類ごとの欄も
1610
+ * 多く、手で写すと写し間違いと描画側の変更への drift が残る (`rules/quality.md § 導出可能記述は
1611
+ * 人手で書かない`)。 いくつあるかは `readout-table.generated.ts` を数えれば出る。
1614
1612
  *
1615
1613
  * 作り直す = `node packages/dragon/scripts/gen-readout-table.mjs`
1616
1614
  * ずれの検知 = `packages/dragon/test/readout-table-generated.test.ts`
@@ -1621,8 +1619,9 @@ import { 部品の表, 部品の組の表 } from "./readout-table.generated";
1621
1619
  /**
1622
1620
  * 記法が受けるつまみと、その欄 (#1389)。
1623
1621
  *
1624
- * **部品の表と同じく、描画側の型定義から生成する**。 14 種それぞれ欄が違い、手で写すと
1622
+ * **部品の表と同じく、描画側の型定義から生成する**。 種類ごとに欄が違い、手で写すと
1625
1623
  * 描画側が種類を足した時に drift が残る (`rules/quality.md § 導出可能記述は人手で書かない`)。
1624
+ * いくつあるかは `input-table.generated.ts` を数えれば出る。
1626
1625
  *
1627
1626
  * 作り直す = `node packages/dragon/scripts/gen-input-table.mjs`
1628
1627
  * ずれの検知 = `packages/dragon/test/input-table-generated.test.ts`
@@ -2081,8 +2080,26 @@ export const EVENT_KINDS = [
2081
2080
  "blur",
2082
2081
  ] as const;
2083
2082
 
2084
- /** 出来事の相手を指す書き方。 ちょうど 1 つだけ書く */
2085
- const EVENT_TARGET_KEYS = ["box", "lane", "arrow", "diagram"] as const;
2083
+ /**
2084
+ * 出来事の相手を指す書き方。 ちょうど 1 つだけ書く。
2085
+ *
2086
+ * **ここが唯一の置き場所** (#2256)。 同じ 4 語が JSON の記法にも書かれていた間、
2087
+ * 1:1 を名乗りながら受ける項目が両側で別々に決まっており、片方に 5 つ目を足すと
2088
+ * 記法では通るのに JSON では弾かれる状態が黙って生まれる形だった。
2089
+ * 公開 schema の項目の鍵とは `event-target-keys-parity.test.ts` が突き合わせる。
2090
+ */
2091
+ export const EVENT_TARGET_KEYS = ["box", "lane", "arrow", "diagram"] as const;
2092
+
2093
+ /** 出来事に必ず書く項目。 相手の指し方と合わせて schema の項目の鍵になる */
2094
+ export const EVENT_REQUIRED_KEYS = ["on", "handler"] as const;
2095
+
2096
+ /**
2097
+ * 出来事が受ける項目の全体 (#2258)。
2098
+ *
2099
+ * **判定と知らせが同じ集合を読む**。 知らせが先頭 2 語だけを手で並べていた間、
2100
+ * 必ず書く項目を増やしても知らせは古い一覧を教え続ける形だった。
2101
+ */
2102
+ const EVENT_ITEM_KEYS: readonly string[] = [...EVENT_REQUIRED_KEYS, ...EVENT_TARGET_KEYS];
2086
2103
 
2087
2104
  /**
2088
2105
  * 押下などの出来事で動く仕掛けを 1 件読む (#1393)。
@@ -2109,12 +2126,11 @@ function 出来事として読む(
2109
2126
  }
2110
2127
  const opts = parseInlineMapping(t.slice(1, -1));
2111
2128
  for (const k of Object.keys(opts)) {
2112
- if (k === "on" || k === "handler" || (EVENT_TARGET_KEYS as readonly string[]).includes(k))
2113
- continue;
2129
+ if (EVENT_ITEM_KEYS.includes(k)) continue;
2114
2130
  errors.push({
2115
2131
  line,
2116
2132
  message: `出来事の項目名が読めません: "${k}"`,
2117
- hint: `使える項目 = on, handler, ${EVENT_TARGET_KEYS.join(", ")}`,
2133
+ hint: `使える項目 = ${EVENT_ITEM_KEYS.join(", ")}`,
2118
2134
  });
2119
2135
  return undefined;
2120
2136
  }
@@ -2622,20 +2638,6 @@ function collectIndentedList(
2622
2638
  return { items, next: i };
2623
2639
  }
2624
2640
 
2625
- /**
2626
- * 登場人物を 1 件ずつ集める。 続く字下げ行は同じ 1 件にまとめる。
2627
- *
2628
- * 項目が少なければ 1 行で書け、 多ければ縦に並べられる。 縦に並べた方が、 何を指定できるかが
2629
- * 見える。
2630
- *
2631
- * ```
2632
- * - Client
2633
- * - API: service
2634
- * - Web:
2635
- * kind: service
2636
- * 色: 失敗
2637
- * ```
2638
- */
2639
2641
  /**
2640
2642
  * 色の指定を振り分ける。
2641
2643
  *
@@ -3091,51 +3093,68 @@ export const ACTOR_ITEM_KEYS: ReadonlySet<string> = new Set([
3091
3093
  */
3092
3094
  function validateRelativePositions(actors: DslActor[], errors: DslError[]): void {
3093
3095
  const named = new Set(actors.map((a) => a.name));
3094
- const broken = new Set<string>();
3096
+ // 解けない書き方の判定は `relative-pos.ts` が持つ。 JSON の入口も同じ判定を呼ぶ (#2039)
3097
+ const problems = findRelativeProblems(actors.map((a) => ({ name: a.name, rel: a.posRel })));
3095
3098
 
3096
- for (const a of actors) {
3097
- const rel = a.posRel;
3098
- if (!rel) continue;
3099
- if (rel.anchor === a.name) {
3100
- errors.push({
3101
- line: a.pos.line,
3102
- message: `位置の基準が自分自身です: "${a.name}"`,
3103
- hint: "別の登場人物の名前を書く",
3104
- });
3105
- broken.add(a.name);
3106
- continue;
3107
- }
3108
- if (!named.has(rel.anchor)) {
3109
- errors.push({
3110
- line: a.pos.line,
3111
- message: `位置の基準が見つかりません: "${rel.anchor}"`,
3112
- hint:
3113
- named.size > 0
3114
- ? `actors: に書かれている名前 = ${[...named].join(", ")}`
3115
- : "actors: に基準にする登場人物を書く",
3116
- });
3117
- broken.add(a.name);
3118
- }
3119
- }
3120
-
3121
- const { cyclic } = orderByDependency(
3122
- actors.map((a) => ({ name: a.name, rel: broken.has(a.name) ? undefined : a.posRel })),
3123
- );
3124
- for (const name of cyclic) {
3125
- const a = actors.find((x) => x.name === name);
3099
+ for (const p of problems) {
3100
+ const a = actors.find((x) => x.name === p.name);
3126
3101
  errors.push({
3127
3102
  line: a?.pos.line ?? 1,
3128
- message: `位置の基準が互いを指しています: "${name}"`,
3129
- hint: "どれか 1 つは座標 (`位置: 300,200`) か自動配置にする",
3103
+ ...相対の誤り(p, named),
3130
3104
  });
3131
- broken.add(name);
3132
3105
  }
3133
3106
 
3107
+ const broken = new Set(problems.map((p) => p.name));
3134
3108
  for (const a of actors) {
3135
3109
  if (broken.has(a.name)) a.posRel = undefined;
3136
3110
  }
3137
3111
  }
3138
3112
 
3113
+ /**
3114
+ * 解けない相対の指定に対する、記法側の誤りの文 (#2039)。
3115
+ *
3116
+ * 判定は `relative-pos.ts` が持ち、文はここが持つ。 JSON 側は欄の場所を添えた別の文を作る。
3117
+ */
3118
+ function 相対の誤り(
3119
+ p: RelativeProblem,
3120
+ named: ReadonlySet<string>,
3121
+ ): { message: string; hint: string } {
3122
+ switch (p.kind) {
3123
+ case "self":
3124
+ return {
3125
+ message: `位置の基準が自分自身です: "${p.name}"`,
3126
+ hint: "別の登場人物の名前を書く",
3127
+ };
3128
+ case "missing-anchor":
3129
+ return {
3130
+ message: `位置の基準が見つかりません: "${p.anchor ?? ""}"`,
3131
+ hint:
3132
+ named.size > 0
3133
+ ? `actors: に書かれている名前 = ${[...named].join(", ")}`
3134
+ : "actors: に基準にする登場人物を書く",
3135
+ };
3136
+ case "cyclic":
3137
+ return {
3138
+ message: `位置の基準が互いを指しています: "${p.name}"`,
3139
+ hint: "どれか 1 つは座標 (`位置: 300,200`) か自動配置にする",
3140
+ };
3141
+ }
3142
+ }
3143
+
3144
+ /**
3145
+ * 登場人物を 1 件ずつ集める。 続く字下げ行は同じ 1 件にまとめる。
3146
+ *
3147
+ * 項目が少なければ 1 行で書け、 多ければ縦に並べられる。 縦に並べた方が、 何を指定できるかが
3148
+ * 見える。
3149
+ *
3150
+ * ```
3151
+ * - Client
3152
+ * - API: service
3153
+ * - Web:
3154
+ * kind: service
3155
+ * 色: 失敗
3156
+ * ```
3157
+ */
3139
3158
  function collectActorEntries(
3140
3159
  lines: Line[],
3141
3160
  start: number,
@@ -3300,13 +3319,6 @@ function reportScaleOnNonPart(
3300
3319
  });
3301
3320
  }
3302
3321
 
3303
- /**
3304
- * 中括弧の形で読める項目名。
3305
- *
3306
- * ここに無い名前は、 パーツでない箱ではどこにも入らずに消える。 `ACTOR_ITEM_KEYS` (縦に
3307
- * 並べた形) とは別に持つ = 中括弧の形は位置や大きさを未対応にしてあり、 同じ集合にすると
3308
- * 「知らせない」 側がずれる。
3309
- */
3310
3322
  /**
3311
3323
  * 中括弧の形で読める日本語と、その英語名 (#1301)。
3312
3324
  *
@@ -3340,6 +3352,13 @@ function 中括弧の別名を寄せる(opts: Record<string, string>): Record<st
3340
3352
  return 触った ? out : opts;
3341
3353
  }
3342
3354
 
3355
+ /**
3356
+ * 中括弧の形で読める項目名。
3357
+ *
3358
+ * ここに無い名前は、 パーツでない箱ではどこにも入らずに消える。 `ACTOR_ITEM_KEYS` (縦に
3359
+ * 並べた形) とは別に持つ = 中括弧の形は位置や大きさ (`pos` / `size` / `scale`) を未対応にしてあり、
3360
+ * 同じ集合にすると「知らせない」 側がずれる。
3361
+ */
3343
3362
  export const INLINE_ACTOR_KEYS: ReadonlySet<string> = new Set([
3344
3363
  "kind",
3345
3364
  "subtitle",