@cardenelabs/dragon 0.17.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cardenelabs/dragon",
3
- "version": "0.17.0",
3
+ "version": "0.18.0",
4
4
  "description": "Dragon — Mermaid 感覚で animated SVG を生成する Text DSL。 cdl engine を内部利用。",
5
5
  "license": "MIT",
6
6
  "author": "cardene777",
@@ -46,8 +46,18 @@
46
46
  "node": ">=18"
47
47
  },
48
48
  "sideEffects": false,
49
+ "scripts": {
50
+ "build": "tsup",
51
+ "dev": "tsup --watch",
52
+ "typecheck": "tsc --noEmit",
53
+ "typecheck:test": "tsc --noEmit -p tsconfig.test.json",
54
+ "test": "cd ../.. && vitest run packages/dragon",
55
+ "test:watch": "cd ../.. && vitest packages/dragon",
56
+ "lint": "tsc --noEmit",
57
+ "prepublishOnly": "pnpm run typecheck && pnpm run test && pnpm run build"
58
+ },
49
59
  "dependencies": {
50
- "@cardenelabs/cdl": "^0.18.1"
60
+ "@cardenelabs/cdl": "^0.20.1"
51
61
  },
52
62
  "peerDependencies": {
53
63
  "react": "^19.0.0",
@@ -66,14 +76,5 @@
66
76
  "react": "^19.1.0",
67
77
  "tsup": "^8.5.1",
68
78
  "typescript": "^5.8.0"
69
- },
70
- "scripts": {
71
- "build": "tsup",
72
- "dev": "tsup --watch",
73
- "typecheck": "tsc --noEmit",
74
- "typecheck:test": "tsc --noEmit -p tsconfig.test.json",
75
- "test": "cd ../.. && vitest run packages/dragon",
76
- "test:watch": "cd ../.. && vitest packages/dragon",
77
- "lint": "tsc --noEmit"
78
79
  }
79
- }
80
+ }
package/src/compile.ts CHANGED
@@ -146,6 +146,8 @@ export type CompileNotice = {
146
146
  | "external-paint-dropped"
147
147
  // 図の中に描く部品を持たない見本を重ねた (#1017)
148
148
  | "part-not-drawn"
149
+ // 向きが効かない形で `向き:` を書いた (#1494)
150
+ | "direction-not-honored"
149
151
  // `倍率:` を書いた見本が、同じ名前の状態も持っていた (#1026)
150
152
  | "scale-reserved"
151
153
  // 値で描く図 (`pie` / `bar` / `line`) で値を読めなかった (#1154)
@@ -317,6 +319,7 @@ export function compileToCdl(doc: DslDocument, opts?: CompileToCdlOpts): CdlDiag
317
319
  reportActorKindNotHonored(書いたまま, opts?.onNotice);
318
320
  reportMessageOptionNotHonored(書いたまま, opts?.onNotice);
319
321
  reportDocEyebrowNotHonored(書いたまま, opts?.onNotice);
322
+ reportDirectionNotHonored(書いたまま, opts?.onNotice);
320
323
  reportDrawNotHonored(書いたまま, opts?.onNotice);
321
324
  reportChartFieldsNotHonored(書いたまま, opts?.onNotice);
322
325
  reportAxesNotHonored(書いたまま, opts?.onNotice);
@@ -6215,7 +6218,13 @@ function compileFlow(doc: DslDocument): CdlDiagram {
6215
6218
  // v0.4 ... animation あり時 builder 直接経路で複数 phase 注入
6216
6219
  // **縦列を書いた形は動きの有無に関わらず generic 経路へ** (#1263)。 動く図だけで効かせると、
6217
6220
  // 同じ記法でも静止図では指定が黙って消える (実測 = 縦列 3 本のはずが 1 本になり知らせも出ない)
6218
- if ((doc.animate && doc.animate.phases.length > 0) || 書いた縦列に置く("flow", doc)) {
6221
+ // **向きを書いた形も generic 経路へ** (#1494) 静止図の経路は並びを固定で持つので、
6222
+ // ここを通さないと書いた向きが黙って消える (縦列を書いた形と同じ理由)
6223
+ if (
6224
+ (doc.animate && doc.animate.phases.length > 0) ||
6225
+ 書いた縦列に置く("flow", doc) ||
6226
+ doc.direction !== undefined
6227
+ ) {
6219
6228
  return compileGenericWithAnimate(doc, { kind: "flow", laneId: "main", laneWidth: 400 });
6220
6229
  }
6221
6230
  // 登場人物が 0 人なら枠も作らない。 描画側の `flow()` は枠を必ず 1 つ作るため、 そのまま
@@ -6251,7 +6260,13 @@ function compileSwimlane(doc: DslDocument): CdlDiagram {
6251
6260
  // v0.4 ... animation あり時 builder 直接経路 (各 actor 別 lane で配置)
6252
6261
  // **縦列を書いた形は動きの有無に関わらず generic 経路へ** (#1263)。 動く図だけで効かせると、
6253
6262
  // 同じ記法でも静止図では指定が黙って消える (実測 = 縦列 3 本のはずが 1 本になり知らせも出ない)
6254
- if ((doc.animate && doc.animate.phases.length > 0) || 書いた縦列に置く("swimlane", doc)) {
6263
+ // **向きを書いた形も generic 経路へ** (#1494) 静止図の経路は並びを固定で持つので、
6264
+ // ここを通さないと書いた向きが黙って消える (縦列を書いた形と同じ理由)
6265
+ if (
6266
+ (doc.animate && doc.animate.phases.length > 0) ||
6267
+ 書いた縦列に置く("swimlane", doc) ||
6268
+ doc.direction !== undefined
6269
+ ) {
6255
6270
  return compileGenericWithAnimate(doc, { kind: "swimlane", laneWidth: 400 });
6256
6271
  }
6257
6272
  // swimlane preset は lane 配置 + 自由 node/edge。
@@ -6514,6 +6529,65 @@ function 後ろへ戻る矢印か(
6514
6529
  return 先 < 元;
6515
6530
  }
6516
6531
 
6532
+ /**
6533
+ * 向きを選べる図種 (#1494)。
6534
+ *
6535
+ * **並び方そのものが読み方を担う図種は外す**。 表の図は「1 縦列 1 表」、クラス図と状態の図は
6536
+ * 設計が格子に置く形、順序図は 1 枚の板で、どれも向きを入れ替えると図の意味が変わる。
6537
+ *
6538
+ * `topology` も外す = 入れ物 (`contain`) を持つ図で、縦列の中に箱を囲む作りが向きと結びついている。
6539
+ */
6540
+ const 向きを選べる図種: ReadonlySet<PresetType> = new Set<PresetType>(["flow", "swimlane"]);
6541
+
6542
+ /** その図種の既定の向き。 書かなかった時は今までどおりの並びになる。 */
6543
+ function 既定の向き(kind: PresetType): "縦" | "横" {
6544
+ return kind === "flow" || kind === "topology" ? "縦" : "横";
6545
+ }
6546
+
6547
+ /**
6548
+ * 実際に使う向き (#1494)。
6549
+ *
6550
+ * 書いていない図と、効かない図種に書いた図は既定のまま = **書かない図の並びは 1 つも動かない**。
6551
+ */
6552
+ function 並べる向き(kind: PresetType, doc: DslDocument): "縦" | "横" {
6553
+ if (doc.direction === undefined) return 既定の向き(kind);
6554
+ if (!向きを選べる図種.has(kind)) return 既定の向き(kind);
6555
+ return doc.direction;
6556
+ }
6557
+
6558
+ /**
6559
+ * 書いた向きが効かない時に伝える (#1494)。
6560
+ *
6561
+ * 効かない形は 2 つある。 向きを選べない図種に書いた形と、全ての箱が縦列を書いた形。
6562
+ * 後者は書いた縦列が勝つので、向きだけが黙って捨てられる。
6563
+ *
6564
+ * 黙って捨てると「書いたのに変わらない」 が手掛かりなしで起きる。
6565
+ */
6566
+ function reportDirectionNotHonored(doc: DslDocument, onNotice?: (n: CompileNotice) => void): void {
6567
+ if (!onNotice) return;
6568
+ if (doc.direction === undefined) return;
6569
+ const 行 = doc.directionPos?.line ?? doc.pos?.line ?? 0;
6570
+ if (!向きを選べる図種.has(doc.type)) {
6571
+ onNotice({
6572
+ kind: "direction-not-honored",
6573
+ actor: doc.title,
6574
+ line: 行,
6575
+ message: `書いた direction は効きません (type: ${doc.type} は並び方そのものが読み方を決めます)`,
6576
+ hint: `direction を書けるのは ${[...向きを選べる図種].join(" / ")} です`,
6577
+ });
6578
+ return;
6579
+ }
6580
+ if (書いた縦列に置く(doc.type, doc)) {
6581
+ onNotice({
6582
+ kind: "direction-not-honored",
6583
+ actor: doc.title,
6584
+ line: 行,
6585
+ message: "書いた direction は効きません (全ての箱が縦列を書いているので、そちらが優先されます)",
6586
+ hint: "direction で並べたい時は箱の `lane` を外してください",
6587
+ });
6588
+ }
6589
+ }
6590
+
6517
6591
  /**
6518
6592
  * 縦列を選べる図種 (#1263)。
6519
6593
  *
@@ -6616,12 +6690,18 @@ function compileGenericWithAnimate(doc: DslDocument, opts: GenericOpts): CdlDiag
6616
6690
  title: 箱の題(a),
6617
6691
  });
6618
6692
  });
6619
- } else if (kind === "flow" || kind === "topology") {
6693
+ } else if (並べる向き(kind, doc) === "") {
6620
6694
  // 1 lane に全 actor を縦 stack
6621
6695
  const lid = opts.laneId ?? "main";
6622
6696
  b.lane(lid, {
6623
6697
  width: laneWidth,
6624
- label: doc.title,
6698
+ /*
6699
+ * **見出しは自然に縦へ積む図種だけ** (#1494)。
6700
+ *
6701
+ * `direction: 縦` を書いた泳法図がここへ来るようになった。 その図に題を渡すと、図の題が
6702
+ * 縦列の見出しとしてもう 1 度出る (実測 = 「認証の流れ」 が題と見出しの 2 箇所に並んだ)。
6703
+ */
6704
+ ...(kind === "flow" || kind === "topology" ? { label: doc.title } : {}),
6625
6705
  ...(kind === "topology" ? { contain: true } : {}),
6626
6706
  });
6627
6707
  doc.actors.forEach((a, idx) => {
@@ -6635,7 +6715,8 @@ function compileGenericWithAnimate(doc: DslDocument, opts: GenericOpts): CdlDiag
6635
6715
  });
6636
6716
  });
6637
6717
  } else {
6638
- // swimlane / er / state ... actor ごとに 1 lane (横並び)
6718
+ // actor ごとに 1 lane (横並び)。 `swimlane` / `er` / `state` の既定と、
6719
+ // `向き: 横` を書いた流れ図がここに来る (#1494)
6639
6720
  //
6640
6721
  // **見出しを付けるのは `swimlane` だけ** (#1241)。 3 図種とも箱を 1 つずつ持ち、
6641
6722
  // その箱が既に名前を描く。 縦列にも同じ名前を渡すと **同じ字が縦に 2 つ並ぶ**
@@ -187,6 +187,8 @@ export interface DragonJson {
187
187
  bands?: { actor: string; from: number; to: number }[];
188
188
  /** 矢印をいつ出すか (#1470)。 記法の最上位 `reveal:` と同じ */
189
189
  reveal?: EdgeReveal;
190
+ /** 図の並ぶ向き (#1494)。 記法の最上位 `direction:` と同じ。 JSON は英語の語で書く */
191
+ direction?: "vertical" | "horizontal";
190
192
  }
191
193
 
192
194
  export interface JsonActor {
@@ -487,6 +489,8 @@ export const ACCEPTED_KEYS = {
487
489
  "bands",
488
490
  // 矢印をいつ出すか (#1470)
489
491
  "reveal",
492
+ // 図の並ぶ向き (#1494)
493
+ "direction",
490
494
  ],
491
495
  actor: [
492
496
  "name",
@@ -643,6 +647,8 @@ export const 欄の型表 = {
643
647
  bands: "並び",
644
648
  // 矢印をいつ出すか (#1470)
645
649
  reveal: "非空の文字列",
650
+ // 図の並ぶ向き (#1494)
651
+ direction: "非空の文字列",
646
652
  },
647
653
  actor: {
648
654
  name: "必須の非空文字列",
@@ -2494,6 +2500,8 @@ export function jsonToDoc(json: DragonJson): DslDocument {
2494
2500
  bands: json.bands,
2495
2501
  // 矢印をいつ出すか (#1470)
2496
2502
  reveal: json.reveal,
2503
+ // 図の並ぶ向き (#1494)。 JSON は英語で書くので、記法と同じ語に直してから渡す
2504
+ ...(json.direction !== undefined ? { direction: json.direction === "horizontal" ? ("横" as const) : ("縦" as const) } : {}),
2497
2505
  pos: p0,
2498
2506
  };
2499
2507
  }
package/src/keywords.ts CHANGED
@@ -17,6 +17,34 @@ export const HEADERS = {
17
17
  /** preset 名 */
18
18
  export const PRESET_NAMES = ["sequence", "flow", "swimlane", "er", "state", "topology"] as const;
19
19
 
20
+ /**
21
+ * 図の並ぶ向き (`direction:`、 #1494)。
22
+ *
23
+ * 縦列 (`lane`) は横、段 (`stack`) は縦に置かれるので、向きは「1 つの縦列に積む」 か
24
+ * 「1 人ずつ縦列を作る」 かの選択になる。 どちらも組み立て側に既にある経路で、
25
+ * 記法から名指しできる言葉が無かった。
26
+ */
27
+ export const DIRECTIONS = ["縦", "横"] as const;
28
+ export type DslDirection = (typeof DIRECTIONS)[number];
29
+
30
+ /**
31
+ * 向きの別名。 色 (`色` / `color` / `tone`) と同じく日本語と英語の両方で書ける。
32
+ *
33
+ * 正規の語を鍵にも入れておく = 引く側が別名かどうかを気にせず 1 度で解決できる。
34
+ */
35
+ export const DIRECTION_ALIAS: Record<string, DslDirection> = {
36
+ 縦: "縦",
37
+ 横: "横",
38
+ vertical: "縦",
39
+ horizontal: "横",
40
+ };
41
+
42
+ /** 書いた向きを正規の語に直す。 読めない語は `null`。 */
43
+ export function resolveDirection(s: string): DslDirection | null {
44
+ const k = s.trim().toLowerCase();
45
+ return Object.hasOwn(DIRECTION_ALIAS, k) ? DIRECTION_ALIAS[k]! : null;
46
+ }
47
+
20
48
  /** NodeKind 別名 (日本語 → English) */
21
49
  export const NODE_KIND_ALIAS: Record<string, NodeKind> = {
22
50
  // 日本語
@@ -1796,6 +1796,11 @@
1796
1796
  "pattern": "^[A-Za-z_][A-Za-z0-9_]*$"
1797
1797
  }
1798
1798
  },
1799
+ "direction": {
1800
+ "type": "string",
1801
+ "enum": ["vertical", "horizontal"],
1802
+ "description": "図の並ぶ向き。 vertical = 1 つの縦列に積む。 horizontal = 1 人ずつ縦列を作る。 効くのは flow と swimlane だけ。"
1803
+ },
1799
1804
  "reveal": {
1800
1805
  "type": "string",
1801
1806
  "enum": ["phase", "all"],
package/src/types.ts CHANGED
@@ -5,6 +5,7 @@
5
5
 
6
6
  import type { CdlDiagram, NodeKind, Tone, EdgeStyle, EdgeHead, EdgeHeadFill, EdgeReveal, ClassRelationType, SequenceMessageKind } from "@cardenelabs/cdl";
7
7
  import type { DslOnlyKind } from "./v05/parser";
8
+ import type { DslDirection } from "./keywords";
8
9
 
9
10
  /**
10
11
  * 記法が書ける箱の種類 (#1420)。
@@ -166,6 +167,17 @@ export type DslDocument = {
166
167
  * 段に関わらず最初から全部描く。 描き手の `CdlDiagram.edgeReveal` にそのまま渡る。
167
168
  */
168
169
  reveal?: EdgeReveal;
170
+ /**
171
+ * 図の並ぶ向き (`direction:`、 #1494)。
172
+ *
173
+ * `縦` は 1 つの縦列に積み、`横` は 1 人ずつ縦列を作る。 効くのは流れ図と泳法図だけで、
174
+ * 他の図種は並び方そのものが読み方を担うため書いても効かない (知らせを出す)。
175
+ *
176
+ * 全ての箱が縦列を書いた形では、書いた縦列が勝つ (同じく知らせを出す)。
177
+ */
178
+ direction?: DslDirection;
179
+ /** `direction:` を書いた行。 効かない時の知らせで、書いた場所を指すために持つ */
180
+ directionPos?: { line: number };
169
181
  groups?: Record<string, DslGroup>;
170
182
  /**
171
183
  * 値を見せる部品 (`readouts:`、 #1374)。 割合の輪や数え上げを図の脇に出す。
package/src/v05/parser.ts CHANGED
@@ -44,7 +44,8 @@
44
44
  import type { NodeKind, Tone, EdgeStyle, EdgeHead, EdgeHeadFill, ClassRelationType, SequenceMessageKind } from "@cardenelabs/cdl";
45
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
46
  import type { EdgeReveal } from "@cardenelabs/cdl";
47
- import { TONE_ALIAS, NODE_KIND_ALIAS } from "../keywords";
47
+ import { TONE_ALIAS, NODE_KIND_ALIAS, DIRECTIONS, resolveDirection } from "../keywords";
48
+ import type { DslDirection } from "../keywords";
48
49
  import { parseRelativePos, orderByDependency } from "../relative-pos";
49
50
  import {
50
51
  checkValueExpression,
@@ -141,6 +142,17 @@ export const TOP_LEVEL_KEYS = [
141
142
  "bands",
142
143
  // 矢印をいつ出すか (#1470)
143
144
  "reveal",
145
+ /*
146
+ * 図の並ぶ向き (#1494)。
147
+ *
148
+ * **最上位の語は英語にする**。 日本語の見出し (`タイトル:` / `種類:` 等) は v0.4 の
149
+ * 書き方で、2026-12-31 に廃止する側にある。 ここで日本語の語を足すと、消す予定の形を
150
+ * 増やすことになる。
151
+ *
152
+ * 値は日本語と英語の両方を受ける。 箱の項目 (`色` / `種類` / `位置` / `大きさ`) は v0.5 でも
153
+ * 日本語で書けるので、書き手が自然に言う語 (`縦` / `横`) を残せる。
154
+ */
155
+ "direction",
144
156
  ] as const;
145
157
 
146
158
  /**
@@ -426,6 +438,8 @@ export function parseTextDslV05(src: string): V05ParseResult {
426
438
  let eyebrow: string | null = null;
427
439
  let eyebrowLine = 0;
428
440
  let reveal: EdgeReveal | null = null;
441
+ let direction: DslDirection | null = null;
442
+ let directionLine = 0;
429
443
  let axes: DslAxes | undefined = undefined;
430
444
  let axesLine = 0;
431
445
  let actors: DslActor[] = [];
@@ -492,6 +506,32 @@ export function parseTextDslV05(src: string): V05ParseResult {
492
506
  i += 1;
493
507
  continue;
494
508
  }
509
+ if (head.key === "direction") {
510
+ /*
511
+ * 図の並ぶ向き (#1494)。
512
+ *
513
+ * `縦` は 1 つの縦列に積み、`横` は 1 人ずつ縦列を作る。 どちらも組み立て側に既にある
514
+ * 経路で、ここで足すのは名指しする言葉だけ。
515
+ *
516
+ * **書いた行を覚える**。 効かない図種に書いた時の知らせが、書いた場所を指せるようにする。
517
+ */
518
+ const v = (head.value ?? "").trim();
519
+ if (v.length > 0) {
520
+ const 解けた = resolveDirection(v);
521
+ if (解けた !== null) {
522
+ direction = 解けた;
523
+ directionLine = line.no;
524
+ } else {
525
+ errors.push({
526
+ line: line.no,
527
+ message: `direction が読めません (書いた値: ${v})`,
528
+ hint: `使える語 = ${DIRECTIONS.join(" / ")} / vertical / horizontal`,
529
+ });
530
+ }
531
+ }
532
+ i += 1;
533
+ continue;
534
+ }
495
535
  if (head.key === "eyebrow") {
496
536
  // 空で書いた形 (`eyebrow:`) は「書かなかった」 と同じにする。 空文字を残すと
497
537
  // 描画側が中身のない帯を出す
@@ -966,6 +1006,7 @@ export function parseTextDslV05(src: string): V05ParseResult {
966
1006
  type: type!,
967
1007
  ...(eyebrow !== null ? { eyebrow, eyebrowPos: { line: eyebrowLine } } : {}),
968
1008
  ...(reveal !== null ? { reveal } : {}),
1009
+ ...(direction !== null ? { direction, directionPos: { line: directionLine } } : {}),
969
1010
  ...(axes !== undefined ? { axes, axesPos: { line: axesLine } } : {}),
970
1011
  actors,
971
1012
  flow,