@cardenelabs/dragon 0.27.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 (60) hide show
  1. package/README.md +119 -12
  2. package/dist/index.cjs +9619 -8023
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +407 -175
  5. package/dist/index.d.ts +407 -175
  6. package/dist/index.js +9617 -8024
  7. package/dist/index.js.map +1 -1
  8. package/package.json +2 -2
  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 +596 -5672
  49. package/src/index.ts +13 -10
  50. package/src/json-line-map.ts +30 -0
  51. package/src/json-parser.ts +509 -176
  52. package/src/near-name.ts +42 -0
  53. package/src/notation-lint.ts +112 -74
  54. package/src/relative-pos.ts +71 -2
  55. package/src/schema.ts +1 -1
  56. package/src/schemas/diagram.json +130 -34
  57. package/src/types.ts +60 -41
  58. package/src/v05/input-table.generated.ts +6 -4
  59. package/src/v05/parser-types.ts +12 -1
  60. package/src/v05/parser.ts +503 -149
package/src/v05/parser.ts CHANGED
@@ -42,12 +42,12 @@
42
42
  */
43
43
 
44
44
  import type { NodeKind, Tone, EdgeStyle, EdgeHead, EdgeHeadFill, ClassRelationType, SequenceMessageKind } from "@cardenelabs/cdl";
45
- import { TONES, NODE_KINDS, EDGE_HEADS, EDGE_HEAD_FILLS, EDGE_STYLES, EDGE_REVEALS, CLASS_RELATION_LOOK, SEQUENCE_MESSAGE_LOOK, parseFormula } from "@cardenelabs/cdl";
46
- import type { EdgeReveal } from "@cardenelabs/cdl";
45
+ import { TONES, NODE_KINDS, EDGE_HEADS, EDGE_HEAD_FILLS, EDGE_STYLES, EDGE_REVEALS, RELATION_FOCUSES, CLASS_RELATION_LOOK, SEQUENCE_MESSAGE_LOOK, parseFormula } from "@cardenelabs/cdl";
46
+ 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,
@@ -66,7 +66,6 @@ import type {
66
66
  DslFormula,
67
67
  DslEventBinding,
68
68
  DslScrollTrigger,
69
- DslActorNodeOverride,
70
69
  DslStep,
71
70
  DslAnimate,
72
71
  DslState,
@@ -80,6 +79,7 @@ import type {
80
79
  DslBand,
81
80
  DslGroup,
82
81
  DslViewport,
82
+ LayoutPos,
83
83
  } from "../types";
84
84
 
85
85
  export type V05ParseResult = { ok: true; doc: DslDocument } | { ok: false; errors: DslError[] };
@@ -159,6 +159,8 @@ export const TOP_LEVEL_KEYS = [
159
159
  "bands",
160
160
  // 矢印をいつ出すか (#1470)
161
161
  "reveal",
162
+ // 箱に触れると関係する線だけを光らせるか (#1757)
163
+ "relations",
162
164
  /*
163
165
  * 図の並ぶ向き (#1494)。
164
166
  *
@@ -381,14 +383,12 @@ const INFRA_KIND_ALIAS: Record<string, NodeKind> = {
381
383
  };
382
384
 
383
385
  /**
384
- * 受け付ける箱の種類。 描画できる種類 (cdl の `NODE_KINDS`) に、 記法だけが持つ種類を足す。
386
+ * 記法が受理する種類の全体。 これに載っていない種類は見本 (パーツ) の候補になる。
385
387
  *
386
- * 以前は手書きの 31 種だった。 描画できる 90 種のうち 78 種が記法から書けず、 部品名として
387
- * 扱われて「そんな部品は無い」 と警告が出るだけだった。 描画側を出所に加えることで
388
+ * 描画できる種類 (cdl の `NODE_KINDS`) に、 記法だけが持つ種類と基盤の別名を足す。
389
+ * 手書きの一覧だった頃は、 描画側に種類が増えても記法から書けず、 部品名として扱われて
390
+ * 「そんな部品は無い」 と警告が出るだけだった。 描画側を出所に加えることで
388
391
  * 「描画できるものは書ける」 が成立する。
389
- */
390
- /**
391
- * 記法が受理する種類の全体。 これに載っていない種類は見本 (パーツ) の候補になる。
392
392
  *
393
393
  * 画面側が「本文が見本を使っているか」 を判定するのに使う (#1022)。 手書きの一覧を
394
394
  * 別に持つと、種類が増えた時にそちらだけ取り残されて余分な読み込みが起きる。
@@ -468,6 +468,7 @@ export function parseTextDslV05(src: string): V05ParseResult {
468
468
  let eyebrow: string | null = null;
469
469
  let eyebrowLine = 0;
470
470
  let reveal: EdgeReveal | null = null;
471
+ let relations: RelationFocus | null = null;
471
472
  let direction: DslDirection | null = null;
472
473
  let directionLine = 0;
473
474
  let palette: DslPalette | null = null;
@@ -537,6 +538,29 @@ export function parseTextDslV05(src: string): V05ParseResult {
537
538
  i += 1;
538
539
  continue;
539
540
  }
541
+ if (head.key === "relations") {
542
+ /*
543
+ * 箱に触れると関係する線だけを光らせるか (#1757)。
544
+ *
545
+ * クラス図と ER 図は順番を持たない図で、段を追う見せ方は読み手の問い
546
+ * (この箱はどこと繋がっているか) に答えない。 `hover` と書くと、箱に触れた時に
547
+ * その箱 ・ 繋がる線 ・ 相手の箱だけが光る (`cdl#806`)。
548
+ */
549
+ const v = (head.value ?? "").trim();
550
+ if (v.length > 0) {
551
+ if ((RELATION_FOCUSES as readonly string[]).includes(v)) {
552
+ relations = v as RelationFocus;
553
+ } else {
554
+ errors.push({
555
+ line: line.no,
556
+ message: `relations が読めません (書いた値: ${v})`,
557
+ hint: `使える語 = ${RELATION_FOCUSES.join(" / ")}`,
558
+ });
559
+ }
560
+ }
561
+ i += 1;
562
+ continue;
563
+ }
540
564
  if (head.key === "direction") {
541
565
  /*
542
566
  * 図の並ぶ向き (#1494)。
@@ -722,6 +746,13 @@ export function parseTextDslV05(src: string): V05ParseResult {
722
746
  const inline = head.value?.trim();
723
747
  if (inline && inline.startsWith("{") && inline.endsWith("}")) {
724
748
  const opts = parseInlineMapping(inline.slice(1, -1));
749
+ 中括弧の知らない項目名を知らせる(
750
+ inline.slice(1, -1),
751
+ Object.keys(VIEWPORT_VALUE_KINDS),
752
+ "viewport の ",
753
+ line.no,
754
+ errors,
755
+ );
725
756
  viewport = {
726
757
  ...表で読む(VIEWPORT_VALUE_KINDS, opts, "viewport の ", line.no, errors),
727
758
  pos: { line: line.no },
@@ -730,15 +761,37 @@ export function parseTextDslV05(src: string): V05ParseResult {
730
761
  continue;
731
762
  }
732
763
  // block: viewport:\n width: 1400\n height: 900\n ...
733
- const { items, next } = collectIndentedList(lines, i + 1, line.indent);
764
+ // **`collectIndentedList` を使わない** (#1968)。 あちらは `:` を含まない行を黙って捨てるため、
765
+ // 書き間違えた行が「書かなかった」 と同じになる (`axes:` と同じ理由)
766
+ const { items, next } = collectIndentedRaw(lines, i + 1, line.indent);
734
767
  const opts: Record<string, string> = {};
735
768
  // 知らせは **値を書いた行** を指す (#1306)。 `viewport:` の行を指すと、欄が縦に並ぶ形で
736
769
  // どの行を直せばよいか分からない
737
770
  const optLines: Record<string, number> = {};
771
+ const 使える = Object.keys(VIEWPORT_VALUE_KINDS);
738
772
  for (const it of items) {
739
- const m = it.trimmed.match(/^([a-zA-Z][a-zA-Z0-9_]*)\s*:\s*(.+)$/);
740
- if (!m) continue;
773
+ // 先頭の `- ` を付けて並べる形も受ける (`collectIndentedList` が落としていた分を揃える)
774
+ const 行 = it.trimmed.replace(/^-\s+/, "");
775
+ const m = 行.match(/^([a-zA-Z][a-zA-Z0-9_]*)\s*:\s*(.*)$/);
776
+ if (!m) {
777
+ errors.push({
778
+ line: it.no,
779
+ message: `viewport の行が読めません: "${it.trimmed}"`,
780
+ hint: `use \`width: 1400\` (使える項目 = ${使える.join(", ")})`,
781
+ });
782
+ continue;
783
+ }
741
784
  const 欄 = m[1] ?? "";
785
+ if (!使える.includes(欄)) {
786
+ errors.push({
787
+ line: it.no,
788
+ message: `viewport の項目名が読めません: "${欄}"`,
789
+ hint: `使える項目 = ${使える.join(", ")}`,
790
+ });
791
+ continue;
792
+ }
793
+ // 値を書かなかった形 (`width:`) は「書かなかった」 と同じに扱う (1 行の形と揃える)
794
+ if ((m[2] ?? "").trim() === "") continue;
742
795
  opts[欄] = stripQuotes((m[2] ?? "").trim());
743
796
  optLines[欄] = it.no;
744
797
  }
@@ -785,6 +838,13 @@ export function parseTextDslV05(src: string): V05ParseResult {
785
838
  continue;
786
839
  }
787
840
  const opts = parseInlineMapping(m[2] ?? "");
841
+ 中括弧の知らない項目名を知らせる(
842
+ m[2] ?? "",
843
+ m[1] === "x" ? AXIS_X_KEYS : AXIS_Y_KEYS,
844
+ `軸 ${m[1]} の `,
845
+ it.no,
846
+ errors,
847
+ );
788
848
  if (m[1] === "x") 組み立て.x = { left: opts.left, right: opts.right };
789
849
  else 組み立て.y = { bottom: opts.bottom, top: opts.top };
790
850
  }
@@ -828,10 +888,21 @@ export function parseTextDslV05(src: string): V05ParseResult {
828
888
  if (m) {
829
889
  const id = m[1]!;
830
890
  const opts = parseInlineMapping(m[2]!);
891
+ 中括弧の知らない項目名を知らせる(
892
+ m[2]!,
893
+ LANE_INLINE_KEYS,
894
+ `縦列 ${id} の `,
895
+ it.no,
896
+ errors,
897
+ );
831
898
  lanesMap[id] = {
832
899
  id,
833
900
  ...表で読む(LANE_VALUE_KINDS, opts, `縦列 ${id} の `, it.no, errors),
834
901
  label: opts.label,
902
+ // 位置のずらし (#1971)
903
+ layoutPos: ずらしにまとめる(
904
+ 表で読む(OFFSET_VALUE_KINDS, opts, `縦列 ${id} の `, it.no, errors),
905
+ ),
835
906
  pos: { line: it.no },
836
907
  };
837
908
  } else {
@@ -1003,6 +1074,7 @@ export function parseTextDslV05(src: string): V05ParseResult {
1003
1074
  if (m) {
1004
1075
  const id = m[1]!;
1005
1076
  const opts = parseInlineMapping(m[2]!);
1077
+ 中括弧の知らない項目名を知らせる(m[2]!, GROUP_INLINE_KEYS, `組 ${id} の `, it.no, errors);
1006
1078
  const lanesList = (opts.lanes ?? "")
1007
1079
  .replace(/^\[|\]$/g, "")
1008
1080
  .split(",")
@@ -1060,6 +1132,7 @@ export function parseTextDslV05(src: string): V05ParseResult {
1060
1132
  type: type!,
1061
1133
  ...(eyebrow !== null ? { eyebrow, eyebrowPos: { line: eyebrowLine } } : {}),
1062
1134
  ...(reveal !== null ? { reveal } : {}),
1135
+ ...(relations !== null ? { relations } : {}),
1063
1136
  ...(direction !== null ? { direction, directionPos: { line: directionLine } } : {}),
1064
1137
  ...(palette !== null ? { palette } : {}),
1065
1138
  ...(axes !== undefined ? { axes, axesPos: { line: axesLine } } : {}),
@@ -1405,6 +1478,23 @@ export const VIEWPORT_VALUE_KINDS = {
1405
1478
  labelMargin: "数",
1406
1479
  } as const satisfies Record<Exclude<keyof DslViewport, "pos">, 値の形>;
1407
1480
 
1481
+ /**
1482
+ * 位置のずらしの 2 欄 (#1971)。 箱 (縦に並べた形と中括弧の形) と縦列 (中括弧の形) で同じ読み方をする。
1483
+ *
1484
+ * JSON の `pos: { x, y }` と同じ `layoutPos` に入る。 記法の `pos:` は座標 (`posX` / `posY`) の意味で
1485
+ * 使われているため、ずらしは別の名前にする。
1486
+ */
1487
+ export const OFFSET_VALUE_KINDS = {
1488
+ offsetX: "数",
1489
+ offsetY: "数",
1490
+ } as const satisfies Record<string, 値の形>;
1491
+
1492
+ /** 読めたずらしを `layoutPos` にまとめる。 片方だけ書いた時は残りを 0 とする (JSON は 2 欄とも必須) */
1493
+ function ずらしにまとめる(v: { offsetX?: number; offsetY?: number }): LayoutPos | undefined {
1494
+ if (v.offsetX === undefined && v.offsetY === undefined) return undefined;
1495
+ return { x: v.offsetX ?? 0, y: v.offsetY ?? 0 };
1496
+ }
1497
+
1408
1498
  /**
1409
1499
  * 縦列の欄 (#1306)。 `label` は文字列なので表に載せない (記法の値は全て文字列で、
1410
1500
  * 文字列の欄には読めない値という状態が無い)。
@@ -1416,6 +1506,31 @@ export const LANE_VALUE_KINDS = {
1416
1506
  lifeline: "真偽",
1417
1507
  } as const satisfies Record<string, 値の形>;
1418
1508
 
1509
+ /**
1510
+ * 縦列の中括弧に書ける項目 (#1968)。 数と真偽の欄は `LANE_VALUE_KINDS` と `OFFSET_VALUE_KINDS` から
1511
+ * 導き、表に載せない文字列の `label` だけを足す。
1512
+ */
1513
+ export const LANE_INLINE_KEYS: readonly (keyof DslLane | keyof typeof OFFSET_VALUE_KINDS)[] = [
1514
+ ...(Object.keys(LANE_VALUE_KINDS) as (keyof typeof LANE_VALUE_KINDS)[]),
1515
+ // 位置のずらし (#1971)。 JSON の縦列の `pos` と同じ `layoutPos` に入る
1516
+ ...(Object.keys(OFFSET_VALUE_KINDS) as (keyof typeof OFFSET_VALUE_KINDS)[]),
1517
+ "label",
1518
+ ];
1519
+
1520
+ /** 組の中括弧に書ける項目 (#1968)。 `groups:` の読み手が拾う欄と揃える */
1521
+ export const GROUP_INLINE_KEYS = ["label", "lanes"] as const satisfies readonly Exclude<
1522
+ keyof DslGroup,
1523
+ "id" | "pos"
1524
+ >[];
1525
+
1526
+ /** 軸の名前の中括弧に書ける項目 (#1968)。 横軸は左右、縦軸は下上 */
1527
+ export const AXIS_X_KEYS = ["left", "right"] as const satisfies readonly (keyof NonNullable<
1528
+ DslAxes["x"]
1529
+ >)[];
1530
+ export const AXIS_Y_KEYS = ["bottom", "top"] as const satisfies readonly (keyof NonNullable<
1531
+ DslAxes["y"]
1532
+ >)[];
1533
+
1419
1534
  /**
1420
1535
  * 箱の中に描く図形 (`shape:`) と、値を見せる部品 (`readouts:`) の欄 (#1374)。
1421
1536
  *
@@ -1436,7 +1551,7 @@ export const LANE_VALUE_KINDS = {
1436
1551
  // `parser.ts` に置いたままだと 生成した表 → parser → 生成した表 の輪ができる
1437
1552
  export type { 欄の形, 図形の定義 } from "./parser-types";
1438
1553
 
1439
- /** 描ける図形と、その欄 (`CdlDynShape` の全 5 種を覆う) */
1554
+ /** 描ける図形と、その欄 (`CdlDynShape` の全種を覆う) */
1440
1555
  export const 図形の表: Record<string, 図形の定義> = {
1441
1556
  rect: {
1442
1557
  必須: ["source", "fillMax"],
@@ -1491,9 +1606,9 @@ export const 図形の表: Record<string, 図形の定義> = {
1491
1606
  /**
1492
1607
  * 記法が受ける部品と、その欄。
1493
1608
  *
1494
- * **手で書かず、描画側の型定義から生成する** (#1385)。 描画側は 107 種を持ち欄は 510 個あり、
1495
- * 手で写すと写し間違いと描画側の変更への drift が残る (`rules/quality.md § 導出可能記述は
1496
- * 人手で書かない`)。
1609
+ * **手で書かず、描画側の型定義から生成する** (#1385)。 描画側が持つ種類も、種類ごとの欄も
1610
+ * 多く、手で写すと写し間違いと描画側の変更への drift が残る (`rules/quality.md § 導出可能記述は
1611
+ * 人手で書かない`)。 いくつあるかは `readout-table.generated.ts` を数えれば出る。
1497
1612
  *
1498
1613
  * 作り直す = `node packages/dragon/scripts/gen-readout-table.mjs`
1499
1614
  * ずれの検知 = `packages/dragon/test/readout-table-generated.test.ts`
@@ -1504,8 +1619,9 @@ import { 部品の表, 部品の組の表 } from "./readout-table.generated";
1504
1619
  /**
1505
1620
  * 記法が受けるつまみと、その欄 (#1389)。
1506
1621
  *
1507
- * **部品の表と同じく、描画側の型定義から生成する**。 14 種それぞれ欄が違い、手で写すと
1622
+ * **部品の表と同じく、描画側の型定義から生成する**。 種類ごとに欄が違い、手で写すと
1508
1623
  * 描画側が種類を足した時に drift が残る (`rules/quality.md § 導出可能記述は人手で書かない`)。
1624
+ * いくつあるかは `input-table.generated.ts` を数えれば出る。
1509
1625
  *
1510
1626
  * 作り直す = `node packages/dragon/scripts/gen-input-table.mjs`
1511
1627
  * ずれの検知 = `packages/dragon/test/input-table-generated.test.ts`
@@ -1595,6 +1711,60 @@ function 組の並びとして読む(raw: string): Record<string, string>[] | un
1595
1711
  return 出;
1596
1712
  }
1597
1713
 
1714
+ /** 選択肢の組が持てる項目。 描画側の `CdlInputOption` の組と同じ 2 つ */
1715
+ const 選択肢の組の項目 = ["value", "label"] as const;
1716
+
1717
+ /**
1718
+ * `["green", { value: "red", label: "失敗" }]` の形を選択肢の並びに読む (#1920)。
1719
+ *
1720
+ * 要素は文字列 (値と名前を兼ねる) か、`value` と `label` を持つ組。 2 つを混ぜて書ける。
1721
+ * 割るのは `splitInlineFields` = 引用符と括弧の中の `,` を区切りと取り違えない
1722
+ * (`並びとして読む` は組の中の `,` で割るので使えない)。
1723
+ *
1724
+ * 組の誤り (項目の欠け / 知らない項目 / 空の値) は行番号付きで知らせ、並びごと落とす。
1725
+ * 読めた組だけを渡すと選択肢の数が変わり、既定値が選択肢に無い図になる。
1726
+ */
1727
+ function 選択肢の並びとして読む(
1728
+ 値: string,
1729
+ 名: string,
1730
+ line: number,
1731
+ errors: DslError[],
1732
+ ): (string | { value: string; label: string })[] | undefined {
1733
+ const 中身 = 値.trim();
1734
+ if (!中身.startsWith("[") || !中身.endsWith("]")) {
1735
+ errors.push({ line, message: `${名} の並びが読めません: "${値}"`, hint: '`["green", { value: "red", label: "失敗" }]` の形で書く' });
1736
+ return undefined;
1737
+ }
1738
+ const 出: (string | { value: string; label: string })[] = [];
1739
+ const 誤りの数 = errors.length;
1740
+ splitInlineFields(中身.slice(1, -1)).forEach((生, i) => {
1741
+ const 要素 = 生.trim();
1742
+ if (!要素.startsWith("{")) {
1743
+ const 文字列 = stripQuotes(要素);
1744
+ if (文字列 !== "") 出.push(文字列);
1745
+ return;
1746
+ }
1747
+ if (!要素.endsWith("}")) {
1748
+ errors.push({ line, message: `${名}[${i}] の組が読めません: "${要素}"`, hint: '`{ value: "red", label: "失敗" }` の形で書く' });
1749
+ return;
1750
+ }
1751
+ const 組 = parseInlineMapping(要素.slice(1, -1));
1752
+ for (const k of Object.keys(組)) {
1753
+ if ((選択肢の組の項目 as readonly string[]).includes(k)) continue;
1754
+ errors.push({ line, message: `${名}[${i}] の項目名が読めません: "${k}"`, hint: `使える項目 = ${選択肢の組の項目.join(", ")}` });
1755
+ }
1756
+ for (const k of 選択肢の組の項目) {
1757
+ if (組[k] === undefined) {
1758
+ errors.push({ line, message: `${名}[${i}].${k} は必ず書きます`, hint: `必須の項目 = ${選択肢の組の項目.join(", ")}` });
1759
+ } else if (組[k] === "") {
1760
+ errors.push({ line, message: `${名}[${i}].${k} が空です`, hint: k === "label" ? "名前を書かないなら文字列だけを並べる" : "値を 1 文字以上書く" });
1761
+ }
1762
+ }
1763
+ if (組.value && 組.label) 出.push({ value: 組.value, label: 組.label });
1764
+ });
1765
+ return errors.length === 誤りの数 ? 出 : undefined;
1766
+ }
1767
+
1598
1768
  function 部品の組を検査する(
1599
1769
  kind: string,
1600
1770
  読めた: Record<string, unknown>,
@@ -1646,7 +1816,8 @@ function 表に従って読む(
1646
1816
  if (形 === undefined) {
1647
1817
  errors.push({
1648
1818
  line,
1649
- message: `${接頭}の項目名が読めません: "${欄}"`,
1819
+ // 接頭は呼び手が「の」 まで書く (`図形の ` / `部品 ring の `)。 ここで足すと「の の」 に重なる (#1968)
1820
+ message: `${接頭.trimEnd()}項目名が読めません: "${欄}"`,
1650
1821
  hint: `使える項目 = ${Object.keys(定義.欄).join(", ")}`,
1651
1822
  });
1652
1823
  continue;
@@ -1688,6 +1859,9 @@ function 表に従って読む(
1688
1859
  } else {
1689
1860
  out[欄] = 数;
1690
1861
  }
1862
+ } else if (形 === "選択肢の並び") {
1863
+ const 選択肢 = 選択肢の並びとして読む(値, `${接頭}${欄}`, line, errors);
1864
+ if (選択肢 !== undefined) out[欄] = 選択肢;
1691
1865
  } else if (形 === "組の並び") {
1692
1866
  const 組 = 組の並びとして読む(値);
1693
1867
  if (組 !== undefined) out[欄] = 組;
@@ -1906,8 +2080,26 @@ export const EVENT_KINDS = [
1906
2080
  "blur",
1907
2081
  ] as const;
1908
2082
 
1909
- /** 出来事の相手を指す書き方。 ちょうど 1 つだけ書く */
1910
- 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];
1911
2103
 
1912
2104
  /**
1913
2105
  * 押下などの出来事で動く仕掛けを 1 件読む (#1393)。
@@ -1934,12 +2126,11 @@ function 出来事として読む(
1934
2126
  }
1935
2127
  const opts = parseInlineMapping(t.slice(1, -1));
1936
2128
  for (const k of Object.keys(opts)) {
1937
- if (k === "on" || k === "handler" || (EVENT_TARGET_KEYS as readonly string[]).includes(k))
1938
- continue;
2129
+ if (EVENT_ITEM_KEYS.includes(k)) continue;
1939
2130
  errors.push({
1940
2131
  line,
1941
2132
  message: `出来事の項目名が読めません: "${k}"`,
1942
- hint: `使える項目 = on, handler, ${EVENT_TARGET_KEYS.join(", ")}`,
2133
+ hint: `使える項目 = ${EVENT_ITEM_KEYS.join(", ")}`,
1943
2134
  });
1944
2135
  return undefined;
1945
2136
  }
@@ -2075,6 +2266,9 @@ function 巻き上げとして読む(
2075
2266
  *
2076
2267
  * **式は描画側の parser に通す**。 自前で書き方を決めると、通ったのに描画側が
2077
2268
  * 解けない式を受けてしまう。 描画側が投げた誤りの本文をそのまま知らせに載せる。
2269
+ *
2270
+ * 名札を付ける時は `名前: { expression: "式", label: "名札" }` の組で書く (#1916)。
2271
+ * 描画側は名札を操作部の名前に描く (cdl#853)。 巻き取り (`scrolls:`) と同じ組の形にそろえる。
2078
2272
  */
2079
2273
  function 式として読む(行: string, line: number, errors: DslError[]): DslFormula | undefined {
2080
2274
  const c = 行.indexOf(":");
@@ -2087,12 +2281,22 @@ function 式として読む(行: string, line: number, errors: DslError[]): DslF
2087
2281
  return undefined;
2088
2282
  }
2089
2283
  const 名前 = 行.slice(0, c).trim();
2090
- const 式 = stripQuotes(行.slice(c + 1).trim());
2091
2284
  if (!isValueName(名前)) {
2092
2285
  // 名前の規則は状態と揃える = 式の名前も `{名前}` で箱の文字に差し込める
2093
2286
  errors.push({ line, ...valueNameIssue(名前) });
2094
2287
  return undefined;
2095
2288
  }
2289
+ const 組 = 式の組に割る(行);
2290
+ let 式: string;
2291
+ let 名札: string | undefined;
2292
+ if (組 === undefined) {
2293
+ 式 = stripQuotes(行.slice(c + 1).trim());
2294
+ } else {
2295
+ const 読めた = 式の組を読む(名前, 組, line, errors);
2296
+ if (読めた === undefined) return undefined;
2297
+ 式 = 読めた.expression;
2298
+ 名札 = 読めた.label;
2299
+ }
2096
2300
  if (式 === "") {
2097
2301
  errors.push({
2098
2302
  line,
@@ -2111,7 +2315,71 @@ function 式として読む(行: string, line: number, errors: DslError[]): DslF
2111
2315
  });
2112
2316
  return undefined;
2113
2317
  }
2114
- return { id: 名前, expression: 式, pos: { line } };
2318
+ return { id: 名前, expression: 式, ...(名札 !== undefined ? { label: 名札 } : {}), pos: { line } };
2319
+ }
2320
+
2321
+ /** 組の中の項目名の形。 `parseInlineMapping` が項目名として受ける字と同じ */
2322
+ const 組の項目名 = /^([^\s:,{}[\]"']+)\s*:\s*(.*)$/s;
2323
+
2324
+ /**
2325
+ * 式の行が組の形 (`{ expression: ..., label: ... }`) なら中括弧の内側を返す (#1916)。
2326
+ *
2327
+ * **中括弧で名前を囲った式と取り違えない**。 式は `{input} * 2` のように名前を中括弧で
2328
+ * 囲ってよく (#1391)、`v: {input}` は中括弧だけの行になる。 組とみなすのは、行全体が
2329
+ * 1 つの中括弧の塊で、**最初の項目が `名前:` の形を持つ** 時だけ。
2330
+ */
2331
+ function 式の組に割る(行: string): string | undefined {
2332
+ const 割れた = 名前と中括弧に割る(行);
2333
+ if (割れた === undefined) return undefined;
2334
+ const 最初 = splitInlineFields(割れた[1])[0]?.trim() ?? "";
2335
+ return 組の項目名.test(最初) ? 割れた[1] : undefined;
2336
+ }
2337
+
2338
+ /**
2339
+ * 式の組の中身を読む (#1916)。 読めなければ知らせて `undefined` を返す。
2340
+ *
2341
+ * 式の中身と空の式の検査は呼び手が文字列の形と同じ経路で行う。 ここで見るのは組の形だけ。
2342
+ */
2343
+ function 式の組を読む(
2344
+ 名前: string,
2345
+ 中身: string,
2346
+ line: number,
2347
+ errors: DslError[],
2348
+ ): { expression: string; label?: string } | undefined {
2349
+ const 使える = ["expression", "label"];
2350
+ const 読めた: Record<string, string> = {};
2351
+ for (const 欄 of splitInlineFields(中身).map((s) => s.trim()).filter((s) => s !== "")) {
2352
+ const m = 欄.match(組の項目名);
2353
+ if (!m) {
2354
+ errors.push({
2355
+ line,
2356
+ message: `式 "${名前}" の項目が読めません: "${欄}"`,
2357
+ hint: '`{ expression: "input * 2", label: "2 倍" }` の形で書く。 `,` を含む式は引用符で囲む',
2358
+ });
2359
+ return undefined;
2360
+ }
2361
+ if (!使える.includes(m[1]!)) {
2362
+ errors.push({
2363
+ line,
2364
+ message: `式 "${名前}" の項目名が読めません: "${m[1]}"`,
2365
+ hint: `使える項目 = ${使える.join(", ")}`,
2366
+ });
2367
+ return undefined;
2368
+ }
2369
+ 読めた[m[1]!] = stripQuotes(m[2]!.trim());
2370
+ }
2371
+ if (読めた.label !== undefined && 読めた.label.trim() === "") {
2372
+ errors.push({
2373
+ line,
2374
+ message: `式 "${名前}" の名札が空です`,
2375
+ hint: '名札を付けないなら `label` を書かない。 付けるなら `label: "2 倍"` のように書く',
2376
+ });
2377
+ return undefined;
2378
+ }
2379
+ return {
2380
+ expression: 読めた.expression ?? "",
2381
+ ...(読めた.label !== undefined ? { label: 読めた.label } : {}),
2382
+ };
2115
2383
  }
2116
2384
 
2117
2385
  /**
@@ -2142,14 +2410,6 @@ function つまみとして読む(
2142
2410
  return { id, kind, ...読めた } as DslInput;
2143
2411
  }
2144
2412
 
2145
- /** 箱の中の要素の欄 (#1306)。 `DslActorNodeOverride` の全欄を覆う */
2146
- export const ACTOR_NODE_VALUE_KINDS = {
2147
- posX: "数",
2148
- posY: "数",
2149
- posW: "数",
2150
- posH: "数",
2151
- } as const satisfies Record<keyof DslActorNodeOverride, 値の形>;
2152
-
2153
2413
  /** 中括弧の形で箱に書ける、数と真偽の欄 (#1306) */
2154
2414
  export const ACTOR_INLINE_VALUE_KINDS = {
2155
2415
  stack: "数",
@@ -2378,20 +2638,6 @@ function collectIndentedList(
2378
2638
  return { items, next: i };
2379
2639
  }
2380
2640
 
2381
- /**
2382
- * 登場人物を 1 件ずつ集める。 続く字下げ行は同じ 1 件にまとめる。
2383
- *
2384
- * 項目が少なければ 1 行で書け、 多ければ縦に並べられる。 縦に並べた方が、 何を指定できるかが
2385
- * 見える。
2386
- *
2387
- * ```
2388
- * - Client
2389
- * - API: service
2390
- * - Web:
2391
- * kind: service
2392
- * 色: 失敗
2393
- * ```
2394
- */
2395
2641
  /**
2396
2642
  * 色の指定を振り分ける。
2397
2643
  *
@@ -2409,7 +2655,7 @@ export function splitColorValue(raw: string): { tone?: Tone; hex?: string } {
2409
2655
  * v0.4 で使えた箱の種類の名前 (#1301)。
2410
2656
  *
2411
2657
  * v0.5 の受理集合 (`NODE_KIND_VALID`) に無いため、書くと見本 (parts) の名前として扱われ、
2412
- * 見本帳に無ければ `actor` に潰れて **黙って消えていた**。 見本の名前と区別が付かないので、
2658
+ * カタログに無ければ `actor` に潰れて **黙って消えていた**。 見本の名前と区別が付かないので、
2413
2659
  * 「v0.4 で種類として使えた語」 であることを根拠に誤りとして知らせる。
2414
2660
  *
2415
2661
  * 対応は `keywords.ts` の `NODE_KIND_ALIAS` が持つ (日本語 → 英語の種類名)。
@@ -2423,7 +2669,7 @@ function v04の種類名(値: string): string | undefined {
2423
2669
  /**
2424
2670
  * 箱の種類に v0.4 の日本語を書いた時に知らせる (#1301)。
2425
2671
  *
2426
- * 黙って見本の名前として扱うと、見本帳に無い場合に `actor` へ潰れて手掛かりが残らない。
2672
+ * 黙って見本の名前として扱うと、カタログに無い場合に `actor` へ潰れて手掛かりが残らない。
2427
2673
  */
2428
2674
  function reportV04Kind(kindRaw: string, line: number, errors: DslError[]): void {
2429
2675
  const 英語 = v04の種類名(kindRaw);
@@ -2663,6 +2909,17 @@ function applyContinuationLines(actor: DslActor, rest: Line[], errors: DslError[
2663
2909
  case "posY":
2664
2910
  out.posY = 数として読む(raw, "箱の posY", ln.no, errors);
2665
2911
  break;
2912
+ // 位置のずらし (#1971)。 中括弧の形と同じ表で読み、片方だけ書いた時は残りを 0 とする
2913
+ case "offsetX":
2914
+ case "offsetY": {
2915
+ const 読めた = 表で読む(OFFSET_VALUE_KINDS, { [key]: raw }, "箱の ", ln.no, errors);
2916
+ const 前 = out.layoutPos;
2917
+ out.layoutPos = ずらしにまとめる({
2918
+ offsetX: key === "offsetX" ? 読めた.offsetX : 前?.x,
2919
+ offsetY: key === "offsetY" ? 読めた.offsetY : 前?.y,
2920
+ });
2921
+ break;
2922
+ }
2666
2923
  case "大きさ":
2667
2924
  case "size": {
2668
2925
  // `大きさ: 400,200` の形。 位置と揃える
@@ -2737,6 +2994,11 @@ function applyContinuationLines(actor: DslActor, rest: Line[], errors: DslError[
2737
2994
  touchedState = true;
2738
2995
  }
2739
2996
  if (touchedState) out.stateOverride = state;
2997
+ // 色の名前は部品に効かない。 中括弧の形と同じく箱の色から外し、書いた名前として残す (#1973)
2998
+ if (out.tone !== undefined) {
2999
+ out.partColorName = out.tone;
3000
+ delete out.tone;
3001
+ }
2740
3002
  return out;
2741
3003
  }
2742
3004
  /*
@@ -2803,6 +3065,9 @@ export const ACTOR_ITEM_KEYS: ReadonlySet<string> = new Set([
2803
3065
  "pos",
2804
3066
  "posX",
2805
3067
  "posY",
3068
+ // 位置のずらし (#1971)
3069
+ "offsetX",
3070
+ "offsetY",
2806
3071
  "大きさ",
2807
3072
  "size",
2808
3073
  "倍率",
@@ -2828,51 +3093,68 @@ export const ACTOR_ITEM_KEYS: ReadonlySet<string> = new Set([
2828
3093
  */
2829
3094
  function validateRelativePositions(actors: DslActor[], errors: DslError[]): void {
2830
3095
  const named = new Set(actors.map((a) => a.name));
2831
- const broken = new Set<string>();
2832
-
2833
- for (const a of actors) {
2834
- const rel = a.posRel;
2835
- if (!rel) continue;
2836
- if (rel.anchor === a.name) {
2837
- errors.push({
2838
- line: a.pos.line,
2839
- message: `位置の基準が自分自身です: "${a.name}"`,
2840
- hint: "別の登場人物の名前を書く",
2841
- });
2842
- broken.add(a.name);
2843
- continue;
2844
- }
2845
- if (!named.has(rel.anchor)) {
2846
- errors.push({
2847
- line: a.pos.line,
2848
- message: `位置の基準が見つかりません: "${rel.anchor}"`,
2849
- hint:
2850
- named.size > 0
2851
- ? `actors: に書かれている名前 = ${[...named].join(", ")}`
2852
- : "actors: に基準にする登場人物を書く",
2853
- });
2854
- broken.add(a.name);
2855
- }
2856
- }
3096
+ // 解けない書き方の判定は `relative-pos.ts` が持つ。 JSON の入口も同じ判定を呼ぶ (#2039)
3097
+ const problems = findRelativeProblems(actors.map((a) => ({ name: a.name, rel: a.posRel })));
2857
3098
 
2858
- const { cyclic } = orderByDependency(
2859
- actors.map((a) => ({ name: a.name, rel: broken.has(a.name) ? undefined : a.posRel })),
2860
- );
2861
- for (const name of cyclic) {
2862
- const a = actors.find((x) => x.name === name);
3099
+ for (const p of problems) {
3100
+ const a = actors.find((x) => x.name === p.name);
2863
3101
  errors.push({
2864
3102
  line: a?.pos.line ?? 1,
2865
- message: `位置の基準が互いを指しています: "${name}"`,
2866
- hint: "どれか 1 つは座標 (`位置: 300,200`) か自動配置にする",
3103
+ ...相対の誤り(p, named),
2867
3104
  });
2868
- broken.add(name);
2869
3105
  }
2870
3106
 
3107
+ const broken = new Set(problems.map((p) => p.name));
2871
3108
  for (const a of actors) {
2872
3109
  if (broken.has(a.name)) a.posRel = undefined;
2873
3110
  }
2874
3111
  }
2875
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
+ */
2876
3158
  function collectActorEntries(
2877
3159
  lines: Line[],
2878
3160
  start: number,
@@ -2941,7 +3223,7 @@ function collectAnimationSteps(
2941
3223
  * `{ v: 50, count: 100 }` state override map に集約する経路。
2942
3224
  * 予約語衝突時は `state: { v: 50 }` 明示 fallback を使う (別 field で処理)。
2943
3225
  */
2944
- const ACTOR_RESERVED_FIELDS: ReadonlySet<string> = new Set([
3226
+ export const ACTOR_RESERVED_FIELDS: ReadonlySet<string> = new Set([
2945
3227
  "kind",
2946
3228
  "subtitle",
2947
3229
  "eyebrow",
@@ -2962,11 +3244,15 @@ const ACTOR_RESERVED_FIELDS: ReadonlySet<string> = new Set([
2962
3244
  "posY",
2963
3245
  "posW",
2964
3246
  "posH",
2965
- // canvas pivot UX 修正 (B1) = sub-node 単位 override map (nested `nodes: { header: {...} }`)
2966
- "nodes",
3247
+ // 位置のずらし (#1971)。 見本では見本 1 つ分をまとめてずらす欄で、状態の名前としては読まない
3248
+ "offsetX",
3249
+ "offsetY",
2967
3250
  // 図形の倍率 (#1026)。 状態の名前としては読まない
2968
3251
  "scale",
2969
3252
  "倍率",
3253
+ // 色番号 (#1973)。 見本の色の状態へまとめて入れる欄で、状態の名前としては読まない。
3254
+ // 縦に並べた形 (`COLOR_KEYS`) と JSON は元から色として読んでいた
3255
+ "color",
2970
3256
  // 日本語の項目名 (#1026)。 中括弧の形が日本語の項目名を読めるようになったため、
2971
3257
  // ここに載せないと状態の名前として拾われる。 縦に並べた形での意味 (位置 / 大きさ 等) は
2972
3258
  // 中括弧の形では未対応なので、これまでどおり落とす方に揃える
@@ -3003,52 +3289,6 @@ function extractStateOverride(
3003
3289
  return count > 0 ? out : undefined;
3004
3290
  }
3005
3291
 
3006
- /**
3007
- * canvas pivot UX 修正 (B1) = actor entry の inline map から `nodes: { header: { posX: ..., ... }, ... }`
3008
- * 形式の nested override を抽出する。 outer parseInlineMapping が opts.nodes を string としてそのまま
3009
- * 保持 (value 内 nested `{ }` は depth-aware で保護済) しているので、 本 fn で「outer `{...}` を剥がして
3010
- * key: sub-map ペアに再 split → 各 sub-map を parseInlineMapping で解いて posX/Y/W/H に coerce」 する。
3011
- * 未 field or 空 object なら undefined 返し (caller は actor.nodes を set しない)。
3012
- */
3013
- function parseActorNodesField(
3014
- raw: string | undefined,
3015
- line: number,
3016
- errors: DslError[],
3017
- ): Record<string, DslActorNodeOverride> | undefined {
3018
- if (!raw) return undefined;
3019
- const trimmed = raw.trim();
3020
- if (!trimmed.startsWith("{") || !trimmed.endsWith("}")) return undefined;
3021
- const inner = trimmed.slice(1, -1).trim();
3022
- if (!inner) return undefined;
3023
- // depth-aware split (parseInlineMapping と同じ logic を local reuse、 nested `{ }` / `[ ]` 保護)
3024
- const parts: string[] = [];
3025
- let depth = 0;
3026
- let buf = "";
3027
- for (let i = 0; i < inner.length; i += 1) {
3028
- const c = inner[i]!;
3029
- if (c === "[" || c === "{") depth += 1;
3030
- else if (c === "]" || c === "}") depth -= 1;
3031
- if (c === "," && depth === 0) {
3032
- parts.push(buf);
3033
- buf = "";
3034
- continue;
3035
- }
3036
- buf += c;
3037
- }
3038
- if (buf.trim()) parts.push(buf);
3039
- const out: Record<string, DslActorNodeOverride> = {};
3040
- for (const p of parts) {
3041
- const colonIdx = p.indexOf(":");
3042
- if (colonIdx < 0) continue;
3043
- const key = p.slice(0, colonIdx).trim();
3044
- const val = p.slice(colonIdx + 1).trim();
3045
- if (!key || !val.startsWith("{") || !val.endsWith("}")) continue;
3046
- const nodeOpts = parseInlineMapping(val.slice(1, -1));
3047
- out[key] = 表で読む(ACTOR_NODE_VALUE_KINDS, nodeOpts, `nodes の ${key} の `, line, errors);
3048
- }
3049
- return Object.keys(out).length > 0 ? out : undefined;
3050
- }
3051
-
3052
3292
  function coerceStateValue(raw: string): number | string | boolean {
3053
3293
  const stripped = stripQuotes(raw);
3054
3294
  if (stripped === "true") return true;
@@ -3079,13 +3319,6 @@ function reportScaleOnNonPart(
3079
3319
  });
3080
3320
  }
3081
3321
 
3082
- /**
3083
- * 中括弧の形で読める項目名。
3084
- *
3085
- * ここに無い名前は、 パーツでない箱ではどこにも入らずに消える。 `ACTOR_ITEM_KEYS` (縦に
3086
- * 並べた形) とは別に持つ = 中括弧の形は位置や大きさを未対応にしてあり、 同じ集合にすると
3087
- * 「知らせない」 側がずれる。
3088
- */
3089
3322
  /**
3090
3323
  * 中括弧の形で読める日本語と、その英語名 (#1301)。
3091
3324
  *
@@ -3119,6 +3352,13 @@ function 中括弧の別名を寄せる(opts: Record<string, string>): Record<st
3119
3352
  return 触った ? out : opts;
3120
3353
  }
3121
3354
 
3355
+ /**
3356
+ * 中括弧の形で読める項目名。
3357
+ *
3358
+ * ここに無い名前は、 パーツでない箱ではどこにも入らずに消える。 `ACTOR_ITEM_KEYS` (縦に
3359
+ * 並べた形) とは別に持つ = 中括弧の形は位置や大きさ (`pos` / `size` / `scale`) を未対応にしてあり、
3360
+ * 同じ集合にすると「知らせない」 側がずれる。
3361
+ */
3122
3362
  export const INLINE_ACTOR_KEYS: ReadonlySet<string> = new Set([
3123
3363
  "kind",
3124
3364
  "subtitle",
@@ -3132,7 +3372,9 @@ export const INLINE_ACTOR_KEYS: ReadonlySet<string> = new Set([
3132
3372
  "initial",
3133
3373
  "final",
3134
3374
  "tone",
3135
- "nodes",
3375
+ // 色の欄 (#1969)。 縦に並べた形と JSON は `color` を受けるのに、中括弧の形だけが知らない
3376
+ // 項目名として落としていた。 見本 (parts) では従来どおり状態の上書きとして読む
3377
+ "color",
3136
3378
  // 体験の道筋の欄 (#1251)。 他の図種では組み立て側が知らせる
3137
3379
  "touchpoint",
3138
3380
  "opportunity",
@@ -3143,6 +3385,9 @@ export const INLINE_ACTOR_KEYS: ReadonlySet<string> = new Set([
3143
3385
  "posY",
3144
3386
  "posW",
3145
3387
  "posH",
3388
+ // 位置のずらし (#1971)
3389
+ "offsetX",
3390
+ "offsetY",
3146
3391
  // 箱の中に描く図形 (#1374)
3147
3392
  "shape",
3148
3393
  // その箱を出すかどうかの条件 (#1381)
@@ -3164,8 +3409,28 @@ export const INLINE_ACTOR_KEYS: ReadonlySet<string> = new Set([
3164
3409
  ]);
3165
3410
  // `state` はパーツでだけ意味を持つ (`extractStateOverride` がパーツの時しか作らない)。
3166
3411
  // 通常の箱で読める扱いにすると `- A: { state: { foo: 1 } }` が黙って消え、 本 file が塞ごうと
3167
- // している経路が予約語で残る (Round 1 review の指摘、 実測で確認)。 パーツ側は `isPart` の
3168
- // 早期 return が先に効くのでここに載せる必要が無い
3412
+ // している経路が予約語で残る (Round 1 review の指摘、 実測で確認)。 パーツ側は
3413
+ // `PART_INLINE_UNREADABLE_KEYS` が `state` を外すのでここに載せる必要が無い
3414
+
3415
+ /**
3416
+ * パーツの中括弧で、予約されているのにどの欄も読まない項目名 (#1996)。
3417
+ *
3418
+ * パーツでは中括弧に書いた名前が状態の上書きになる (`extractStateOverride`) ため、読めない
3419
+ * 名前をまとめて知らせることはできない。 一方 `ACTOR_RESERVED_FIELDS` に載る名前は状態の
3420
+ * 上書きから外れるので、`INLINE_ACTOR_KEYS` にも無い名前はどこにも行かずに消える。
3421
+ *
3422
+ * 実測 = `- 甲: { kind: edge-chain, 大きさ: 900,400 }` は書かない時と 1 ピクセルも変わらず
3423
+ * (どちらも `viewBox 1951x552` / パーツの幅 896)、知らせも 1 件も出なかった。 同じ語を縦に
3424
+ * 並べた形で書けば効き、パーツでない箱に中括弧で書けば知らせが出る = 同じ語の扱いが
3425
+ * 「パーツかどうか」 と「書く場所」 で 3 通りに割れていた。
3426
+ *
3427
+ * **2 つの集合の差から導く**。 手で並べると、予約の一覧に名前を足した時に知らせだけが
3428
+ * 取り残される。 `state` だけは差から外す = `extractStateOverride` が明示欄として読むため、
3429
+ * 予約されていても消える名前ではない。
3430
+ */
3431
+ export const PART_INLINE_UNREADABLE_KEYS: ReadonlySet<string> = new Set(
3432
+ [...ACTOR_RESERVED_FIELDS].filter((key) => !INLINE_ACTOR_KEYS.has(key) && key !== "state"),
3433
+ );
3169
3434
 
3170
3435
  /**
3171
3436
  * 矢印の中括弧に書ける欄と、その読み方 (#1275)。
@@ -3182,6 +3447,8 @@ const EDGE_BIND_INLINE_READERS = {
3182
3447
 
3183
3448
  const FLOW_INLINE_READERS = {
3184
3449
  sub: (v: string | undefined) => v,
3450
+ // クラス図の出どころ側の多重度 (#1771)。 `sub` (行き先の端) と対
3451
+ tailSub: (v: string | undefined) => v,
3185
3452
  guard: (v: string | undefined) => v,
3186
3453
  cardinality: (v: string | undefined) => v,
3187
3454
  /*
@@ -3212,7 +3479,7 @@ const FLOW_INLINE_READERS = {
3212
3479
  // 出どころ側の端の形 (#1466)。 ER は端ごとに違う個数を示すので両端に要る
3213
3480
  tailHead: (v: string | undefined) =>
3214
3481
  v !== undefined && EDGE_HEAD_VALUES.includes(v) ? (v as EdgeHead) : undefined,
3215
- // 端の印の塗り (#1466)。 白抜きの菱が「持つ」、塗った菱が「抱える」
3482
+ // 端の印の塗り (#1466)。 白抜きの菱が集約、塗った菱がコンポジション
3216
3483
  headFill: (v: string | undefined) =>
3217
3484
  v !== undefined && EDGE_HEAD_FILL_VALUES.includes(v) ? (v as EdgeHeadFill) : undefined,
3218
3485
  tailHeadFill: (v: string | undefined) =>
@@ -3240,6 +3507,12 @@ const FLOW_INLINE_READERS = {
3240
3507
  * 持たないため、知らせを出せる場所で見る)。
3241
3508
  */
3242
3509
  ...EDGE_BIND_INLINE_READERS,
3510
+ /*
3511
+ * 部品の端で矢印を繋ぐ要素の id (#1979)。 部品かどうか、要素が在るかは組み立てで部品の図を
3512
+ * 引いてから決まるため、ここでは字をそのまま通す
3513
+ */
3514
+ fromPartNode: (v: string | undefined) => v,
3515
+ toPartNode: (v: string | undefined) => v,
3243
3516
  // 数と真偽の欄は `FLOW_INLINE_VALUE_KINDS` の表が読む (#1306)。 ここでは名前だけを持つ =
3244
3517
  // 読める欄の一覧 (`FLOW_INLINE_KEYS`) は本表から導くため、載せないと欄ごと消える
3245
3518
  labelOffsetX: null,
@@ -3262,7 +3535,10 @@ export const FLOW_INLINE_KEYS = Object.keys(
3262
3535
  * 5 つの箱すべてで題が捨てられて識別子 (`root` 等) が出ていた。 知らせも出ないため、 書いた
3263
3536
  * 人には「書いたのに図が変わらない」 としか見えない。
3264
3537
  *
3265
- * パーツでは知らせない。 中括弧に書いた名前は状態の上書きとして意味を持つ (`extractStateOverride`)。
3538
+ * パーツでは名前の大半を知らせない。 中括弧に書いた名前は状態の上書きとして意味を持つ
3539
+ * (`extractStateOverride`)。 ただし状態の上書きからも外れる名前 (`PART_INLINE_UNREADABLE_KEYS`)
3540
+ * だけは知らせる = どの欄も読まず状態にも入らないため、黙ると「書いたのに図が変わらない」 が
3541
+ * 手掛かりなしで起きる (#1996)。
3266
3542
  */
3267
3543
  function reportUnknownInlineKeys(
3268
3544
  isPart: boolean,
@@ -3270,7 +3546,6 @@ function reportUnknownInlineKeys(
3270
3546
  line: number,
3271
3547
  errors: DslError[],
3272
3548
  ): void {
3273
- if (isPart) return;
3274
3549
  // 値が空の形 (`{ title: }`) も見る。 `parseInlineMapping` は値が 1 文字以上ある項目しか
3275
3550
  // 拾わないため、 その結果を走査すると空白の有無で知らせが消える (実測 = `{title:}` と
3276
3551
  // `{ title:}` は黙って通り、 `{ title: }` だけ知らせが出た)。 契約が入力の整形に依存する
@@ -3280,6 +3555,17 @@ function reportUnknownInlineKeys(
3280
3555
  if (idx < 0) continue;
3281
3556
  const key = field.slice(0, idx).trim();
3282
3557
  if (!key) continue;
3558
+ if (isPart) {
3559
+ if (!PART_INLINE_UNREADABLE_KEYS.has(key)) continue;
3560
+ // 行き先を添える。 「読めません」 だけだと、どこにも書けないのか書く場所が違うのかが
3561
+ // 分からない。 この 3 つは縦に並べた形では効く
3562
+ errors.push({
3563
+ line,
3564
+ message: `項目名が読めません: "${key}"`,
3565
+ hint: `1 行の中括弧では読めません。名前の下に段を分けた形で書くと効きます`,
3566
+ });
3567
+ continue;
3568
+ }
3283
3569
  if (INLINE_ACTOR_KEYS.has(key)) continue;
3284
3570
  errors.push({
3285
3571
  line,
@@ -3289,6 +3575,40 @@ function reportUnknownInlineKeys(
3289
3575
  }
3290
3576
  }
3291
3577
 
3578
+ /**
3579
+ * 箱以外の中括弧に書かれた読めない項目名を知らせる (#1968)。
3580
+ *
3581
+ * レーン / 視点 / 矢印 / 組 / 軸の名前 の中括弧は、読む欄だけを拾って残りを黙って捨てていた。
3582
+ * 実測 = レーンに `widht: 280` と書くと幅が既定のまま描かれ、 知らせは 1 件も出なかった。
3583
+ * 箱 (`reportUnknownInlineKeys`) と段は知らせるので、 同じ記法の中で書く場所によって扱いが割れていた。
3584
+ *
3585
+ * 項目名は `splitInlineFields` で拾う。 `parseInlineMapping` の結果を見ると値が空の項目
3586
+ * (`{ widht: }`) を拾えず、 空白の有無で知らせが消える (箱の側が #1090 で踏んだ形)。
3587
+ *
3588
+ * `使える` は呼出側が各欄の表から導いて渡す。 ここに一覧を持つと、 表に欄を足した時に
3589
+ * 知らせだけが取り残される。
3590
+ */
3591
+ function 中括弧の知らない項目名を知らせる(
3592
+ inner: string,
3593
+ 使える: readonly string[],
3594
+ 接頭: string,
3595
+ line: number,
3596
+ errors: DslError[],
3597
+ ): void {
3598
+ for (const field of splitInlineFields(inner)) {
3599
+ const idx = field.indexOf(":");
3600
+ if (idx < 0) continue;
3601
+ const key = field.slice(0, idx).trim();
3602
+ if (!key || 使える.includes(key)) continue;
3603
+ // 接頭は値の欄名に続ける形 (`縦列 l1 の `) で末尾に空白を持つ。 日本語へ続ける時は空白を落とす
3604
+ errors.push({
3605
+ line,
3606
+ message: `${接頭.trimEnd()}項目名が読めません: "${key}"`,
3607
+ hint: `使える項目 = ${使える.join(", ")}`,
3608
+ });
3609
+ }
3610
+ }
3611
+
3292
3612
  function parseActor(line: Line, errors: DslError[]): DslActor | null {
3293
3613
  // 5 形式 サポート:
3294
3614
  // 1. `Client` ... name のみ、 kind=actor default
@@ -3322,6 +3642,16 @@ function parseActor(line: Line, errors: DslError[]): DslActor | null {
3322
3642
  if (!isPart && opts.tone !== undefined && resolveTone(opts.tone) === undefined) {
3323
3643
  report読めない色(opts.tone, line.no, errors, { 線種も受ける: false });
3324
3644
  }
3645
+ // `color` は縦に並べた形と JSON と同じ振り分けを通す (#1969)。 色の名前は箱の色、`#` で
3646
+ // 始まる値は色番号になる。
3647
+ //
3648
+ // **見本でも色として読む** (#1973)。 縦に並べた形と JSON は見本の `color` を色番号として
3649
+ // 見本の色の状態へ入れる。 中括弧の形だけが状態の名前 `color` として拾い、 その名前の状態を
3650
+ // 持つ見本が無いため黙って捨てていた (実測 = `state-indicator` の塗りが既定の緑のまま)
3651
+ const 色 = opts.color !== undefined ? splitColorValue(opts.color) : {};
3652
+ if (opts.color !== undefined && !色.tone && !色.hex) {
3653
+ report読めない色(opts.color, line.no, errors, { 線種も受ける: false });
3654
+ }
3325
3655
  const kind = isPart
3326
3656
  ? NODE_KIND_DEFAULT
3327
3657
  : resolveNodeKind(NODE_KIND_VALID.has(kindRaw) ? kindRaw : "");
@@ -3384,15 +3714,18 @@ function parseActor(line: Line, errors: DslError[]): DslActor | null {
3384
3714
  : 値に追随する欄として読む(opts.renderOffsetY, "箱の renderOffsetY ", line.no, errors),
3385
3715
  ...表で読む(ACTOR_INLINE_VALUE_KINDS, opts, "箱の ", line.no, errors),
3386
3716
  // parts では `tone` を状態の上書きとして従来から使えるため、 色として横取りしない
3387
- tone: isPart ? undefined : resolveTone(opts.tone),
3717
+ // `tone` と `color` を両方書いた時は `tone` を採る (JSON の読み方と揃える)
3718
+ tone: isPart ? undefined : (resolveTone(opts.tone) ?? 色.tone),
3719
+ colorHex: 色.hex,
3720
+ partColorName: isPart ? 色.tone : undefined,
3388
3721
  partId: isPart ? kindRaw : undefined,
3389
3722
  stateOverride: isPart ? extractStateOverride(opts) : undefined,
3390
3723
  // canvas pivot 新 spec = 絶対座標 field は `ACTOR_INLINE_VALUE_KINDS` の表が読む (#1306)
3391
3724
  // 図形の倍率 (#1026)。 どれが効くかは `resolveScale` が 1 箇所で決める
3392
3725
  scale: inlineScale.scale,
3393
3726
  scaleKeys: inlineScale.keys.length ? inlineScale.keys : undefined,
3394
- // canvas pivot UX 修正 (B1) = sub-node 単位 override map (`nodes: { header: {posX:..., ...}, ...}`)
3395
- nodes: parseActorNodesField(opts.nodes, line.no, errors),
3727
+ // 位置のずらし (#1971)。 見本 (parts) でも見本 1 つ分をまとめてずらす
3728
+ layoutPos: ずらしにまとめる(表で読む(OFFSET_VALUE_KINDS, opts, "箱の ", line.no, errors)),
3396
3729
  pos: { line: line.no },
3397
3730
  };
3398
3731
  }
@@ -3464,6 +3797,7 @@ function parseFlowStep(line: Line, no: number, errors: DslError[]): DslStep | nu
3464
3797
  let 数と真偽: 読んだ結果<typeof FLOW_INLINE_VALUE_KINDS> | undefined;
3465
3798
  if (塊) {
3466
3799
  const opts = parseInlineMapping(塊.中身);
3800
+ 中括弧の知らない項目名を知らせる(塊.中身, FLOW_INLINE_KEYS, "矢印の ", line.no, errors);
3467
3801
  // `parseInlineMapping` は値が 1 文字もない `widthBind:` を拾わない。 3 欄は空を
3468
3802
  // 「書かなかった」扱いにせず知らせる契約なので、書かれた値を空も含めて上書きする。
3469
3803
  // 同じ欄を複数回書いた時は通常の mapping と同じく後勝ちにする。
@@ -3511,6 +3845,7 @@ function parseFlowStep(line: Line, no: number, errors: DslError[]): DslStep | nu
3511
3845
  rest = 塊.前.trim();
3512
3846
  }
3513
3847
  const sub = 中括弧.sub as string | undefined;
3848
+ const tailSub = 中括弧.tailSub as string | undefined;
3514
3849
  const guard = 中括弧.guard as string | undefined;
3515
3850
  const cardinality = 中括弧.cardinality as string | undefined;
3516
3851
  const side = 中括弧.side as "top" | "right" | "bottom" | "left" | undefined;
@@ -3546,6 +3881,9 @@ function parseFlowStep(line: Line, no: number, errors: DslError[]): DslStep | nu
3546
3881
  const labelOffsetX = 数と真偽?.labelOffsetX;
3547
3882
  const labelOffsetY = 数と真偽?.labelOffsetY;
3548
3883
  const overlay = 数と真偽?.overlay;
3884
+ // 空の名指し (`toPartNode: ""`) は書かなかったことにする = 要素 1 つの部品なら自動で繋ぐ
3885
+ const fromPartNode = (中括弧.fromPartNode as string | undefined) || undefined;
3886
+ const toPartNode = (中括弧.toPartNode as string | undefined) || undefined;
3549
3887
  // 色と線種を末尾から取る。 括弧 (`(成功)`) と空白区切り (`成功`) の両方を受け付ける。
3550
3888
  //
3551
3889
  // 括弧は従来の書き方で、 catalog が使っている。 空白区切りは登場人物と揃えた形。
@@ -3600,6 +3938,7 @@ function parseFlowStep(line: Line, no: number, errors: DslError[]): DslStep | nu
3600
3938
  tone,
3601
3939
  style,
3602
3940
  sub,
3941
+ tailSub,
3603
3942
  guard,
3604
3943
  cardinality,
3605
3944
  side,
@@ -3617,6 +3956,8 @@ function parseFlowStep(line: Line, no: number, errors: DslError[]): DslStep | nu
3617
3956
  labelOffsetX,
3618
3957
  labelOffsetY,
3619
3958
  overlay,
3959
+ ...(fromPartNode !== undefined ? { fromPartNode } : {}),
3960
+ ...(toPartNode !== undefined ? { toPartNode } : {}),
3620
3961
  pos: { line: line.no },
3621
3962
  };
3622
3963
  }
@@ -3955,10 +4296,19 @@ function 段の項目のヒント(書いた名前: string): string {
3955
4296
  * | `tree` | `tree` | 根から下へ枝が伸びる |
3956
4297
  * | `gantt` | `gantt` | 各帯の始端から右へ帯が伸びる |
3957
4298
  * | `funnel` | `funnel` | 上端から順に段が積まれる |
4299
+ * | `slope` | `slope` | 左の軸から右の軸へ線が伸びる |
4300
+ * | `gauge` | `gauge` | 9 時から上を通って 3 時へ弧が伸びる |
4301
+ * | `radial` | `radial` | 各輪が 12 時から時計回りに開く |
4302
+ * | `stacked` | `stacked` | 左端から右へ帯が伸びる |
4303
+ * | `waffle` | `waffle` | 左上から読む向きに印が 1 個ずつ埋まる |
3958
4304
  *
3959
4305
  * `quadrant` は入れない。 4 つの区画に項目を置く図で **項目に順序が無く**、起点を決められない
3960
4306
  * (順番を書いた順で決めると、動きが図の意味を持たない)。
3961
4307
  *
4308
+ * `stat` も入れない。 値 1 つを大きく示す図で、描画側が起点から描く動きを持たない
4309
+ * (`cdl` の `DRAW_KINDS`)。 `waffle` は 1 個 = 1% の印を読む向きに埋める動きを
4310
+ * 持つようになったので入れた (cdl#719)。
4311
+ *
3962
4312
  * いまは語と図種が同じ綴りだが、**同じものとして扱わない**。 語は書き手が書く名前で、
3963
4313
  * 図種は `type:` が取る値。 片方だけ別名を足したくなった時に、対応が表に残っている形にする。
3964
4314
  */
@@ -3972,6 +4322,10 @@ export const DRAW_TARGETS: ReadonlyMap<string, PresetType> = new Map<string, Pre
3972
4322
  ["gantt", "gantt"],
3973
4323
  ["funnel", "funnel"],
3974
4324
  ["slope", "slope"],
4325
+ ["gauge", "gauge"],
4326
+ ["radial", "radial"],
4327
+ ["stacked", "stacked"],
4328
+ ["waffle", "waffle"],
3975
4329
  ]);
3976
4330
 
3977
4331
  /** `draw:` に書ける語。 表から導く (#1314) */