@cardenelabs/dragon 0.7.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.
@@ -0,0 +1,142 @@
1
+ # dragon quick-start examples
2
+
3
+ ## 1. YAML DSL (人向け、 5 ブロック箇条書き)
4
+
5
+ ```yaml
6
+ # examples/auth-flow.yaml
7
+ タイトル: User Login
8
+ 種類: シーケンス
9
+ 登場人物: User, API, DB
10
+ 流れ:
11
+ - User → API: POST /login
12
+ - API → DB: SELECT credentials
13
+ - DB → API: rows (成功)
14
+ - API → User: 200 OK (成功)
15
+ ```
16
+
17
+ TypeScript から:
18
+
19
+ ```tsx
20
+ import { compileText, CdlDiagramView } from "@cardenelabs/dragon";
21
+ import { readFileSync } from "node:fs";
22
+
23
+ const dsl = readFileSync("examples/auth-flow.yaml", "utf-8");
24
+ const diagram = compileText(dsl);
25
+
26
+ export default function AuthFlow() {
27
+ return <CdlDiagramView diagram={diagram} />;
28
+ }
29
+ ```
30
+
31
+ ## 2. JSON DSL (LLM / structured output 向け)
32
+
33
+ ```ts
34
+ import { compileJson, CdlDiagramView } from "@cardenelabs/dragon";
35
+
36
+ const spec = {
37
+ id: "auth-flow",
38
+ topic: "User Login",
39
+ kind: "sequence",
40
+ actors: ["User", "API", "DB"],
41
+ steps: [
42
+ { from: "User", to: "API", label: "POST /login" },
43
+ { from: "API", to: "DB", label: "SELECT credentials" },
44
+ { from: "DB", to: "API", label: "rows", tone: "success" },
45
+ { from: "API", to: "User", label: "200 OK", tone: "success" },
46
+ ],
47
+ };
48
+
49
+ const diagram = compileJson(spec);
50
+ ```
51
+
52
+ ## 3. flow (フローチャート)
53
+
54
+ ```yaml
55
+ タイトル: 決済フロー
56
+ 種類: フロー
57
+ ノード: 受注, 与信, 決済, 完了
58
+ 流れ:
59
+ - 受注 → 与信: カード確認
60
+ - 与信 → 決済: OK
61
+ - 決済 → 完了: 成功 (成功)
62
+ ```
63
+
64
+ ## 4. state machine (状態遷移)
65
+
66
+ ```yaml
67
+ タイトル: TCP コネクション
68
+ 種類: 状態遷移
69
+ 状態: CLOSED, LISTEN, ESTABLISHED, CLOSE_WAIT
70
+ 遷移:
71
+ - CLOSED → LISTEN: passive open
72
+ - LISTEN → ESTABLISHED: SYN + ACK
73
+ - ESTABLISHED → CLOSE_WAIT: FIN
74
+ - CLOSE_WAIT → CLOSED: close
75
+ ```
76
+
77
+ ## 5. animated pipeline (rich layered animation)
78
+
79
+ builder API を直接使う (cdl 経路)、 dragon YAML より細かい制御可能:
80
+
81
+ ```ts
82
+ import { diagram, CdlDiagramView } from "@cardenelabs/cdl";
83
+
84
+ const pipeline = diagram("pipeline", { topic: "ETL パイプライン" })
85
+ .lane("l1", { x: 0, width: 200 })
86
+ .lane("l2", { x: 220, width: 200 })
87
+ .lane("l3", { x: 440, width: 200 })
88
+ .state("p1", { initial: 0 })
89
+ .state("p2", { initial: 0 })
90
+ .state("p3", { initial: 0 })
91
+ .node("extract", {
92
+ lane: "l1", stack: 0, kind: "dyn-wave", title: "抽出",
93
+ subtitle: "{p1}%", w: 180, h: 240,
94
+ shape: { kind: "wave", level: "{p1}", amplitude: 100, frequency: 2.5, waveHeight: 10, fill: "#4e9dc4" },
95
+ })
96
+ .node("transform", {
97
+ lane: "l2", stack: 0, kind: "dyn-wave", title: "変換",
98
+ subtitle: "{p2}%", w: 180, h: 240,
99
+ shape: { kind: "wave", level: "{p2}", amplitude: 100, frequency: 2.5, waveHeight: 10, fill: "#4e9dc4" },
100
+ })
101
+ .node("load", {
102
+ lane: "l3", stack: 0, kind: "dyn-wave", title: "ロード",
103
+ subtitle: "{p3}%", w: 180, h: 240,
104
+ shape: { kind: "wave", level: "{p3}", amplitude: 100, frequency: 2.5, waveHeight: 10, fill: "#22c55e" },
105
+ })
106
+ .edge("extract", "transform", { label: "raw" })
107
+ .edge("transform", "load", { label: "clean" })
108
+ .phase("p1", { duration: 1500, title: "抽出中", body: "" }, (p) =>
109
+ p.activate("extract").tween("p1", 0, 100).badge("抽出")
110
+ )
111
+ .phase("p2", { duration: 1500, title: "変換中", body: "" }, (p) =>
112
+ p.activate("extract", "transform").tween("p2", 0, 100).badge("変換")
113
+ )
114
+ .phase("p3", { duration: 1500, title: "ロード完了", body: "" }, (p) =>
115
+ p.activate("extract", "transform", "load").tween("p3", 0, 100).badge("完了")
116
+ )
117
+ .build();
118
+
119
+ export default function ETLPipeline() {
120
+ return <CdlDiagramView diagram={pipeline} />;
121
+ }
122
+ ```
123
+
124
+ ## 6. catalog SPA で全実例閲覧
125
+
126
+ https://github.com/cardene777/dragon の `apps/playground-spa` で 300+ 例が閲覧可能:
127
+
128
+ - `/catalog/presets` — 定型テンプレート
129
+ - `/catalog/cookbook` — 頻出レシピ
130
+ - `/catalog/patterns` — 汎用構成パターン
131
+ - `/catalog/primitives` — cdl の最小構成
132
+ - `/catalog/text-dsl` — YAML/JSON 記法
133
+ - `/catalog/animation` — 時間軸の物語
134
+ - `/catalog/parts` — 合成用小部品
135
+ - `/catalog/styles` — 線と色
136
+ - `/catalog/interactive` — user 操作で動く要素
137
+
138
+ ## 参考
139
+
140
+ - README: 全機能 overview
141
+ - CHANGELOG: version 履歴
142
+ - ISSUES: バグ報告 / 機能要望 https://github.com/cardene777/dragon/issues
package/package.json ADDED
@@ -0,0 +1,79 @@
1
+ {
2
+ "name": "@cardenelabs/dragon",
3
+ "version": "0.7.0",
4
+ "description": "Dragon — Mermaid 感覚で animated SVG を生成する Text DSL。 cdl engine を内部利用。",
5
+ "license": "MIT",
6
+ "author": "cardene777",
7
+ "homepage": "https://github.com/cardene777/dragon",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/cardene777/dragon.git",
11
+ "directory": "packages/dragon"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/cardene777/dragon/issues"
15
+ },
16
+ "keywords": [
17
+ "dragon",
18
+ "diagram",
19
+ "animation",
20
+ "text-dsl",
21
+ "mermaid-alternative",
22
+ "svg-animation"
23
+ ],
24
+ "type": "module",
25
+ "main": "./dist/index.cjs",
26
+ "module": "./dist/index.js",
27
+ "types": "./dist/index.d.ts",
28
+ "exports": {
29
+ ".": {
30
+ "types": "./dist/index.d.ts",
31
+ "import": "./dist/index.js",
32
+ "require": "./dist/index.cjs"
33
+ },
34
+ "./schemas/diagram.json": "./src/schemas/diagram.json"
35
+ },
36
+ "files": [
37
+ "dist",
38
+ "src",
39
+ "examples",
40
+ "README.md"
41
+ ],
42
+ "publishConfig": {
43
+ "access": "public"
44
+ },
45
+ "engines": {
46
+ "node": ">=18"
47
+ },
48
+ "sideEffects": false,
49
+ "scripts": {
50
+ "build": "tsup",
51
+ "dev": "tsup --watch",
52
+ "typecheck": "tsc --noEmit",
53
+ "test": "cd ../.. && vitest run packages/dragon",
54
+ "test:watch": "cd ../.. && vitest packages/dragon",
55
+ "lint": "tsc --noEmit",
56
+ "prepublishOnly": "pnpm run typecheck && pnpm run test && pnpm run build"
57
+ },
58
+ "dependencies": {
59
+ "@cardenelabs/cdl": "^0.5.0"
60
+ },
61
+ "peerDependencies": {
62
+ "react": "^19.0.0",
63
+ "react-dom": "^19.0.0"
64
+ },
65
+ "peerDependenciesMeta": {
66
+ "react": {
67
+ "optional": true
68
+ },
69
+ "react-dom": {
70
+ "optional": true
71
+ }
72
+ },
73
+ "devDependencies": {
74
+ "@types/react": "^19.1.0",
75
+ "react": "^19.1.0",
76
+ "tsup": "^8.5.1",
77
+ "typescript": "^5.8.0"
78
+ }
79
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * canvas pivot 新 spec 図境界計算 helper (dragon canvas pivot spec §diagram-boundary)。
3
+ *
4
+ * 図の bounding box (点線四角) を「構成パーツの外接矩形 + 余白」 で計算する SSOT。
5
+ * CDL の layout() を呼んで LaidDiagram.viewBox から外接矩形を取得し、 spec 準拠の 20px 余白を加える。
6
+ *
7
+ * 用途:
8
+ * - dragon editor で drag 中の overlay が「図の中」 に入ったか判定 (spec §4 自動調整発動)
9
+ * - dragon renderer で図 hover 時に dashed rect を描画 (spec 項目 4 の視覚 UI)
10
+ * - PR-B 以降で drag / resize / snap 判定の base rect として参照
11
+ */
12
+
13
+ import type { CdlDiagram, LaidDiagram } from "@cardenelabs/cdl";
14
+ import { layout } from "@cardenelabs/cdl";
15
+
16
+ /** 図境界の padding (SVG unit)、 spec §diagram-boundary で 20 と定めた */
17
+ export const DIAGRAM_BOUNDARY_PADDING = 20;
18
+
19
+ export type DiagramBoundingBox = {
20
+ x: number;
21
+ y: number;
22
+ width: number;
23
+ height: number;
24
+ };
25
+
26
+ /**
27
+ * CdlDiagram の外接矩形 + 20px 余白を計算する。
28
+ * layout() が返す LaidDiagram.viewBox は既に全 element を包む rect (右端 +80 / 下端 +80 の CDL 既定余白付き)。
29
+ * それにさらに spec 準拠の 20px を加えて figure 判定 buffer とする。
30
+ */
31
+ export function computeDiagramBoundingBox(diag: CdlDiagram): DiagramBoundingBox {
32
+ const laid: LaidDiagram = layout(diag);
33
+ const vb = laid.viewBox;
34
+ return {
35
+ x: vb.x - DIAGRAM_BOUNDARY_PADDING,
36
+ y: vb.y - DIAGRAM_BOUNDARY_PADDING,
37
+ width: vb.w + DIAGRAM_BOUNDARY_PADDING * 2,
38
+ height: vb.h + DIAGRAM_BOUNDARY_PADDING * 2,
39
+ };
40
+ }
41
+
42
+ /**
43
+ * bbox 同士の重なり判定 (dragon canvas pivot spec §4 図内 drag 判定 SSOT)。
44
+ * user 明示 「1 部でも重なれば中」 = 交差面積 > 0 を判定。
45
+ */
46
+ export function rectsOverlap(a: DiagramBoundingBox, b: DiagramBoundingBox): boolean {
47
+ const aRight = a.x + a.width;
48
+ const aBottom = a.y + a.height;
49
+ const bRight = b.x + b.width;
50
+ const bBottom = b.y + b.height;
51
+ return a.x < bRight && aRight > b.x && a.y < bBottom && aBottom > b.y;
52
+ }
package/src/color.ts ADDED
@@ -0,0 +1,172 @@
1
+ /**
2
+ * 色として読める値かの判定と、 図から外部参照を落とす処理 (#1004)。
3
+ *
4
+ * 図の中の文字列は最終的に SVG の属性になる。 色を塗る位置 (`fill` / `stroke` 等) に
5
+ * `url(https://example.invalid/x)` が入ると、 図を開いた人の環境からその URL へ要求が飛ぶ。
6
+ * 書き出した SVG を配布しても同じことが起きる。
7
+ *
8
+ * 入口は 1 つではない (状態の上書き / phase の `set` / 画面が直接書く背景色 / 埋め込んだ JSON)。
9
+ * 入口ごとに塞ぐと 1 つ見落とした時に穴が残るため、 **組み立ての最後に図全体を走査する**
10
+ * 出口の検査を置く。 入口側の判定 (`isColorValue`) は「正しい図を保つ」 ため、
11
+ * 出口の検査 (`stripExternalPaint`) は「漏れを塞ぐ」 ための二重の構えになっている。
12
+ */
13
+
14
+ /**
15
+ * CSS の標準色名。
16
+ *
17
+ * 色名を色として扱わないと、 初期値に `red` を持つ状態が「色ではない」 判定になり、
18
+ * その状態への上書きが無検査で通る (実測で `fill="url(...)"` が生成された)。
19
+ * 逆に 16 進の初期値へ正当な `red` を書いた時に元の色へ戻る退行も起きる。
20
+ *
21
+ * 完全一致で照合する。 部分一致にすると `red; background:url(x)` のような形が通る。
22
+ */
23
+ const CSS_COLOR_NAMES: ReadonlySet<string> = new Set([
24
+ "aliceblue", "antiquewhite", "aqua", "aquamarine", "azure", "beige", "bisque", "black",
25
+ "blanchedalmond", "blue", "blueviolet", "brown", "burlywood", "cadetblue", "chartreuse",
26
+ "chocolate", "coral", "cornflowerblue", "cornsilk", "crimson", "cyan", "darkblue", "darkcyan",
27
+ "darkgoldenrod", "darkgray", "darkgreen", "darkgrey", "darkkhaki", "darkmagenta",
28
+ "darkolivegreen", "darkorange", "darkorchid", "darkred", "darksalmon", "darkseagreen",
29
+ "darkslateblue", "darkslategray", "darkslategrey", "darkturquoise", "darkviolet", "deeppink",
30
+ "deepskyblue", "dimgray", "dimgrey", "dodgerblue", "firebrick", "floralwhite", "forestgreen",
31
+ "fuchsia", "gainsboro", "ghostwhite", "gold", "goldenrod", "gray", "green", "greenyellow",
32
+ "grey", "honeydew", "hotpink", "indianred", "indigo", "ivory", "khaki", "lavender",
33
+ "lavenderblush", "lawngreen", "lemonchiffon", "lightblue", "lightcoral", "lightcyan",
34
+ "lightgoldenrodyellow", "lightgray", "lightgreen", "lightgrey", "lightpink", "lightsalmon",
35
+ "lightseagreen", "lightskyblue", "lightslategray", "lightslategrey", "lightsteelblue",
36
+ "lightyellow", "lime", "limegreen", "linen", "magenta", "maroon", "mediumaquamarine",
37
+ "mediumblue", "mediumorchid", "mediumpurple", "mediumseagreen", "mediumslateblue",
38
+ "mediumspringgreen", "mediumturquoise", "mediumvioletred", "midnightblue", "mintcream",
39
+ "mistyrose", "moccasin", "navajowhite", "navy", "oldlace", "olive", "olivedrab", "orange",
40
+ "orangered", "orchid", "palegoldenrod", "palegreen", "paleturquoise", "palevioletred",
41
+ "papayawhip", "peachpuff", "peru", "pink", "plum", "powderblue", "purple", "rebeccapurple",
42
+ "red", "rosybrown", "royalblue", "saddlebrown", "salmon", "sandybrown", "seagreen", "seashell",
43
+ "sienna", "silver", "skyblue", "slateblue", "slategray", "slategrey", "snow", "springgreen",
44
+ "steelblue", "tan", "teal", "thistle", "tomato", "transparent", "turquoise", "violet", "wheat",
45
+ "white", "whitesmoke", "yellow", "yellowgreen",
46
+ // 塗らないことを表す値。 色ではないが、 色を書く位置に置ける正当な値
47
+ "none", "currentcolor",
48
+ ]);
49
+
50
+ /**
51
+ * 色として読める値か。
52
+ *
53
+ * 状態の値は node の `fill` にそのまま入る。 そのため「色の状態を探す」 判定と
54
+ * 「上書きを受け入れるか」 の判定は同じ物差しでなければならない。 別々に持つと、
55
+ * 片方だけ直した時に片方が通してしまう。
56
+ *
57
+ * 通すのは 16 進の 3 形 (`#rgb` / `#rrggbb` / `#rrggbbaa`) と、 標準の色名。
58
+ * 桁数を絞るのは、 `#1234` のような半端な形を色として扱うと描画側の解釈に委ねる範囲が
59
+ * 広がるため。 見本のすべての色が 3 形に収まることは `parts-color-hex-format.test.ts` が検査している。
60
+ *
61
+ * `rgb(...)` / `hsl(...)` は通さない。 括弧を含む形を許すと、 括弧の中身を見る判定が要る。
62
+ * 見本のどのパーツも使っておらず、 通す理由が無い。
63
+ */
64
+ export function isColorValue(v: unknown): v is string {
65
+ if (typeof v !== "string") return false;
66
+ const s = v.trim();
67
+ if (/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/.test(s)) return true;
68
+ return CSS_COLOR_NAMES.has(s.toLowerCase());
69
+ }
70
+
71
+ /**
72
+ * 図の外側を指す値か。
73
+ *
74
+ * SVG の `url(...)` は図の中の定義 (`url(#gradient-1)`) も指せる。 これは正当な用途なので、
75
+ * 括弧の中が `#` で始まる形だけを残し、 それ以外を外向きとみなす。
76
+ *
77
+ * 空白と大文字小文字は無視する。 `URL( https://... )` のような書き方で判定を抜けられないため。
78
+ */
79
+ export function pointsOutside(v: unknown): boolean {
80
+ if (typeof v !== "string") return false;
81
+ // 空白をすべて落としてから見る。 `url ( http...)` のように間に空白を挟む形も同じ扱いにする
82
+ const compact = v.replace(/\s+/g, "").toLowerCase();
83
+ const m = compact.match(/url\(([^)]*)/);
84
+ if (m) {
85
+ const inner = (m[1] ?? "").replace(/^["']/, "");
86
+ // 図の中の定義を指すものだけ残す
87
+ return !inner.startsWith("#");
88
+ }
89
+ // `url(` を伴わない直書きの参照。 属性によってはこの形でも読み込まれる
90
+ return /^(https?:)?\/\//.test(compact) || compact.startsWith("data:");
91
+ }
92
+
93
+ /** 色を塗る位置に使われる key。 ここに入る値は SVG の paint 属性になる */
94
+ const PAINT_KEYS: ReadonlySet<string> = new Set([
95
+ "fill", "stroke", "color", "bg", "background",
96
+ "fillbind", "strokebind", "colorbind",
97
+ "stfill", "strokecolor", "fillcolor",
98
+ ]);
99
+
100
+ /** 落とした時に入れる値。 「塗らない」 を表す SVG の正当な値 */
101
+ const SAFE_PAINT = "none";
102
+
103
+ /** 落とした場所と値。 呼出側が書いた人に知らせるために使う */
104
+ export type StrippedPaint = { path: string; value: string };
105
+
106
+ /**
107
+ * 図の中から、 色を塗る位置に入った外部参照を落とす。
108
+ *
109
+ * 対象は 2 種類ある。
110
+ *
111
+ * - 色を塗る key (`fill` / `stroke` / `bg` 等) の値
112
+ * - 状態の値 (`states[].initial` と、 phase が状態へ入れる値)。 状態は `{名前}` の形で
113
+ * `fill` に差し込まれるため、 色を塗る位置に届く
114
+ *
115
+ * 説明文 (`title` / `subtitle` / `value` / `rows`) は対象外。 文字として出るだけで
116
+ * 属性にはならないため、 URL を書く正当な用途を壊さない。
117
+ *
118
+ * 図を直接書き換える (返り値ではなく引数を変える)。 組み立ての最後に 1 度だけ呼ぶ前提。
119
+ */
120
+ export function stripExternalPaint(diagram: unknown): StrippedPaint[] {
121
+ const stripped: StrippedPaint[] = [];
122
+ walk(diagram, "", false, stripped);
123
+ return stripped;
124
+ }
125
+
126
+ /**
127
+ * 図の中を辿って外部参照を落とす。
128
+ *
129
+ * `inStateValue` = 今見ている場所が状態の値かどうか。 状態は key の名前が `initial` や
130
+ * 状態名そのもの (phase の `sets`) になるため、 key の名前だけでは色かどうか分からない。
131
+ * 「状態を入れる箱の中にいる」 ことを引き継いで判断する。
132
+ */
133
+ function walk(node: unknown, path: string, inStateValue: boolean, out: StrippedPaint[]): void {
134
+ if (node === null || typeof node !== "object") return;
135
+
136
+ if (Array.isArray(node)) {
137
+ for (let i = 0; i < node.length; i++) {
138
+ walk(node[i], `${path}[${i}]`, inStateValue, out);
139
+ }
140
+ return;
141
+ }
142
+
143
+ const obj = node as Record<string, unknown>;
144
+ for (const [key, value] of Object.entries(obj)) {
145
+ const here = path ? `${path}.${key}` : key;
146
+ const lower = key.toLowerCase();
147
+ // 状態を入れる箱に入ったら、 その中の値はすべて状態の値として扱う
148
+ const nextInState = inStateValue || lower === "states" || lower === "sets" || lower === "tweens";
149
+
150
+ if (typeof value === "string") {
151
+ const isPaint = PAINT_KEYS.has(lower) || (nextInState && (lower === "initial" || lower === "to" || lower === "from" || !isReservedStateKey(lower)));
152
+ if (isPaint && pointsOutside(value)) {
153
+ obj[key] = SAFE_PAINT;
154
+ out.push({ path: here, value });
155
+ }
156
+ continue;
157
+ }
158
+ walk(value, here, nextInState, out);
159
+ }
160
+ }
161
+
162
+ /**
163
+ * 状態を入れる箱の中で、 値ではなく仕組みを表す key。
164
+ *
165
+ * phase の `sets` は「状態名: 値」 の形なので、 key の名前を列挙して除外できない。
166
+ * 代わりに、 状態の箱が持つ決まった名前 (`id` / `stateId` / `duration` 等) を除いた残りを
167
+ * 値とみなす。
168
+ */
169
+ function isReservedStateKey(lowerKey: string): boolean {
170
+ return lowerKey === "id" || lowerKey === "stateid" || lowerKey === "duration" ||
171
+ lowerKey === "title" || lowerKey === "body" || lowerKey === "badge" || lowerKey === "kind";
172
+ }