@cardenelabs/cdl 0.5.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/LICENSE +21 -0
- package/README.md +343 -0
- package/SPEC.md +374 -0
- package/dist/index.cjs +28085 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +3415 -0
- package/dist/index.d.ts +3415 -0
- package/dist/index.js +27962 -0
- package/dist/index.js.map +1 -0
- package/dist/react.cjs +23043 -0
- package/dist/react.cjs.map +1 -0
- package/dist/react.d.cts +2 -0
- package/dist/react.d.ts +2 -0
- package/dist/react.js +23041 -0
- package/dist/react.js.map +1 -0
- package/dist/render-C78lIXeC.d.cts +2449 -0
- package/dist/render-C78lIXeC.d.ts +2449 -0
- package/examples/quick-start.md +88 -0
- package/package.json +79 -0
- package/src/anim/core/easing.ts +77 -0
- package/src/anim/core/timeline.ts +335 -0
- package/src/anim/core/types.ts +42 -0
- package/src/anim/index.ts +20 -0
- package/src/anim/react/index.ts +3 -0
- package/src/anim/react/useReducedMotion.ts +27 -0
- package/src/anim/react/useTimeline.ts +48 -0
- package/src/assert-never.ts +16 -0
- package/src/author-intent-verify.ts +587 -0
- package/src/builder.ts +3740 -0
- package/src/compile.ts +12 -0
- package/src/dom-verify-core.ts +121 -0
- package/src/dom-verify-types.ts +21 -0
- package/src/dom-verify.ts +948 -0
- package/src/event-handler/index.ts +184 -0
- package/src/formula/ast.ts +46 -0
- package/src/formula/evaluator.ts +163 -0
- package/src/formula/formula-computeds.ts +83 -0
- package/src/formula/index.ts +5 -0
- package/src/formula/parser.ts +399 -0
- package/src/index.ts +284 -0
- package/src/input-signals.ts +74 -0
- package/src/kinds/actor.tsx +79 -0
- package/src/kinds/card.tsx +61 -0
- package/src/kinds/chart-bar.tsx +121 -0
- package/src/kinds/chart-line.tsx +163 -0
- package/src/kinds/chart-pie.tsx +107 -0
- package/src/kinds/compact-title.ts +164 -0
- package/src/kinds/dyn-shape.tsx +394 -0
- package/src/kinds/event.tsx +70 -0
- package/src/kinds/function.tsx +53 -0
- package/src/kinds/funnel.tsx +122 -0
- package/src/kinds/gantt.tsx +193 -0
- package/src/kinds/generic.tsx +368 -0
- package/src/kinds/mind-map.tsx +367 -0
- package/src/kinds/mind-radial.tsx +209 -0
- package/src/kinds/node-tone.ts +18 -0
- package/src/kinds/quadrant.tsx +164 -0
- package/src/kinds/row-align.ts +179 -0
- package/src/kinds/shape-basement.tsx +683 -0
- package/src/kinds/shape-blockchain.tsx +750 -0
- package/src/kinds/shape-commerce.tsx +553 -0
- package/src/kinds/shape-finance.tsx +706 -0
- package/src/kinds/shape-hardware.tsx +862 -0
- package/src/kinds/shape-people.tsx +439 -0
- package/src/kinds/shape-region.tsx +383 -0
- package/src/kinds/shape-software.tsx +859 -0
- package/src/kinds/storage.tsx +180 -0
- package/src/kinds/text-width.ts +73 -0
- package/src/kinds/tree.tsx +275 -0
- package/src/kinds/user-journey.tsx +240 -0
- package/src/label-text.ts +71 -0
- package/src/layout/clearance-constants.ts +47 -0
- package/src/layout/collisions.ts +2078 -0
- package/src/layout/edges.ts +2498 -0
- package/src/layout/footer-shape.ts +97 -0
- package/src/layout/geometry.ts +135 -0
- package/src/layout/label-shift.ts +28 -0
- package/src/layout/lanes.ts +328 -0
- package/src/layout/nodes.ts +190 -0
- package/src/layout/predict-bbox.ts +76 -0
- package/src/layout/px-projection.ts +176 -0
- package/src/layout/spec.ts +872 -0
- package/src/layout/text-width.ts +133 -0
- package/src/layout/tokens.ts +181 -0
- package/src/layout/viewbox.ts +47 -0
- package/src/layout-with-validation.ts +175 -0
- package/src/layout.ts +381 -0
- package/src/presets.ts +2108 -0
- package/src/reactive/batch.ts +48 -0
- package/src/reactive/computed.ts +85 -0
- package/src/reactive/effect.ts +145 -0
- package/src/reactive/index.ts +6 -0
- package/src/reactive/internal.ts +109 -0
- package/src/reactive/signal.ts +113 -0
- package/src/render/edges.tsx +318 -0
- package/src/render/header.tsx +146 -0
- package/src/render/interactive-panel.tsx +9620 -0
- package/src/render/nodes.tsx +319 -0
- package/src/render/stage.tsx +377 -0
- package/src/render/tone.ts +64 -0
- package/src/render/utils.ts +238 -0
- package/src/render.tsx +221 -0
- package/src/scroll-trigger/index.ts +109 -0
- package/src/scroll-trigger/progress.ts +49 -0
- package/src/thumbnail.tsx +114 -0
- package/src/types.ts +2371 -0
- package/src/validate.ts +268 -0
- package/src/visual-validate.ts +4381 -0
package/src/validate.ts
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import type { CdlDiagram } from "./types";
|
|
2
|
+
import { TONES as TONE_NAMES, NODE_KINDS } from "./types";
|
|
3
|
+
|
|
4
|
+
const TONES = new Set<string>(TONE_NAMES);
|
|
5
|
+
const KINDS = new Set<string>(NODE_KINDS);
|
|
6
|
+
const ENG_BANNED = /\b(FUNCTION|STORAGE|EVENT LOG|BALANCES MAPPING|READ|WRITE|EMIT|CALL)\b/;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* cdl が compile される前に通る validator。
|
|
10
|
+
* 構造的破綻 (重複 id / 不存在 id 参照 / 必須要素 0 件 等) は throw、
|
|
11
|
+
* 文体警告 (japanese-only 違反) は console.warn で build を止めない。
|
|
12
|
+
*
|
|
13
|
+
* error 統一 format ... `[cdl] {category}: {detail}` で grep / 著者の locate が楽に。
|
|
14
|
+
*/
|
|
15
|
+
export function validate(diag: CdlDiagram): void {
|
|
16
|
+
const errors: string[] = [];
|
|
17
|
+
const warnings: string[] = [];
|
|
18
|
+
|
|
19
|
+
// ───────────────────────────────────────────────────────────
|
|
20
|
+
// 1. 必須要素の存在
|
|
21
|
+
// ───────────────────────────────────────────────────────────
|
|
22
|
+
if (diag.lanes.length === 0) {
|
|
23
|
+
errors.push(
|
|
24
|
+
`[cdl] empty: lane が 0 件です\n → 修正例: diagram("${diag.id}", { topic }).lane("col1", { x: 0, width: 400 }).node(...).build()`,
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
if (diag.nodes.length === 0) {
|
|
28
|
+
errors.push(
|
|
29
|
+
`[cdl] empty: node が 0 件です\n → 修正例: .node("a", { lane: "col1", stack: 0, kind: "actor", title: "Client" })`,
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
if (diag.phases.length === 0) {
|
|
33
|
+
errors.push(
|
|
34
|
+
`[cdl] empty: phase が 0 件です (animation を駆動するための最低 1 phase が必要)\n → 修正例: .phase("p1", { duration: 1800, title: "step 1", body: "..." }, p => p.activate("a"))`,
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ───────────────────────────────────────────────────────────
|
|
39
|
+
// 2. 重複 id 検出 (lane / node / edge / state / phase)
|
|
40
|
+
// ───────────────────────────────────────────────────────────
|
|
41
|
+
const checkDup = (kind: string, items: Array<{ id: string }>) => {
|
|
42
|
+
const seen = new Map<string, number>();
|
|
43
|
+
items.forEach((item, idx) => {
|
|
44
|
+
const prev = seen.get(item.id);
|
|
45
|
+
if (prev !== undefined) {
|
|
46
|
+
errors.push(`[cdl] duplicate-id: ${kind} "${item.id}" が ${prev} 件目 + ${idx} 件目で重複`);
|
|
47
|
+
} else {
|
|
48
|
+
seen.set(item.id, idx);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
};
|
|
52
|
+
checkDup("lane", diag.lanes);
|
|
53
|
+
checkDup("node", diag.nodes);
|
|
54
|
+
checkDup("edge", diag.edges);
|
|
55
|
+
checkDup("state", diag.states);
|
|
56
|
+
checkDup("phase", diag.phases);
|
|
57
|
+
|
|
58
|
+
// ───────────────────────────────────────────────────────────
|
|
59
|
+
// 3. tone / kind 妥当性
|
|
60
|
+
// ───────────────────────────────────────────────────────────
|
|
61
|
+
for (const e of diag.edges) {
|
|
62
|
+
if (!TONES.has(e.tone)) {
|
|
63
|
+
errors.push(`[cdl] unknown-tone: edge "${e.id}" tone "${e.tone}" は未対応 (${[...TONES].join(" / ")} のいずれか)`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
for (const n of diag.nodes) {
|
|
67
|
+
if (!KINDS.has(n.kind)) {
|
|
68
|
+
errors.push(`[cdl] unknown-kind: node "${n.id}" kind "${n.kind}" は未対応 (${[...KINDS].join(" / ")} のいずれか)`);
|
|
69
|
+
}
|
|
70
|
+
// 未知の tone は描画側が無言で既定色に落とすので、 typo が気付かれずに残る。
|
|
71
|
+
if (n.tone !== undefined && !TONES.has(n.tone)) {
|
|
72
|
+
errors.push(`[cdl] unknown-tone: node "${n.id}" tone "${n.tone}" は未対応 (${[...TONES].join(" / ")} のいずれか)`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ───────────────────────────────────────────────────────────
|
|
77
|
+
// 4. 不存在 id 参照 (lane / node / edge / state)
|
|
78
|
+
// ───────────────────────────────────────────────────────────
|
|
79
|
+
const laneIds = new Set(diag.lanes.map((l) => l.id));
|
|
80
|
+
const nodeIds = new Set(diag.nodes.map((n) => n.id));
|
|
81
|
+
const edgeIds = new Set(diag.edges.map((e) => e.id));
|
|
82
|
+
const stateIds = new Set(diag.states.map((s) => s.id));
|
|
83
|
+
|
|
84
|
+
// 「もしかして」 サジェスト (typo 検出): edit distance 1 で最も近い候補 1 件
|
|
85
|
+
const suggest = (target: string, candidates: Set<string>): string => {
|
|
86
|
+
const list = [...candidates];
|
|
87
|
+
if (list.length === 0) return "";
|
|
88
|
+
let best = "";
|
|
89
|
+
let bestD = Infinity;
|
|
90
|
+
for (const c of list) {
|
|
91
|
+
const d = levenshtein(target, c);
|
|
92
|
+
if (d < bestD) {
|
|
93
|
+
bestD = d;
|
|
94
|
+
best = c;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return bestD <= 2 ? ` (もしかして "${best}"?)` : ` (定義済 lane: ${list.slice(0, 5).join(", ")}${list.length > 5 ? "..." : ""})`;
|
|
98
|
+
};
|
|
99
|
+
for (const n of diag.nodes) {
|
|
100
|
+
if (!laneIds.has(n.lane)) {
|
|
101
|
+
errors.push(
|
|
102
|
+
`[cdl] unknown-ref: node "${n.id}" の lane "${n.lane}" が未定義です${suggest(n.lane, laneIds)}\n → 修正例: .lane("${n.lane}", { x: 0, width: 400 }) を node 宣言前に追加`,
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
for (const e of diag.edges) {
|
|
107
|
+
if (!nodeIds.has(e.from)) {
|
|
108
|
+
errors.push(
|
|
109
|
+
`[cdl] unknown-ref: edge "${e.id}" の from "${e.from}" が node に存在しません${suggest(e.from, nodeIds)}\n → 修正例: .node("${e.from}", { lane: "...", stack: 0, kind: "actor", title: "..." }) を edge 宣言前に追加`,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
if (!nodeIds.has(e.to)) {
|
|
113
|
+
errors.push(
|
|
114
|
+
`[cdl] unknown-ref: edge "${e.id}" の to "${e.to}" が node に存在しません${suggest(e.to, nodeIds)}\n → 修正例: .node("${e.to}", { lane: "...", stack: 0, kind: "...", title: "..." }) を edge 宣言前に追加`,
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
if (e.from === e.to) {
|
|
118
|
+
errors.push(
|
|
119
|
+
`[cdl] self-loop: edge "${e.id}" の from と to が同じ node "${e.from}" です (self-loop は現状未対応)\n → 別 node を作って 2 段に分けるか、 後続 PR の self-loop support を待ってください`,
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ───────────────────────────────────────────────────────────
|
|
125
|
+
// 5. phase activate / tween の id 参照 (error 昇格)
|
|
126
|
+
// ───────────────────────────────────────────────────────────
|
|
127
|
+
for (const p of diag.phases) {
|
|
128
|
+
for (const id of p.activate) {
|
|
129
|
+
if (!nodeIds.has(id) && !edgeIds.has(id)) {
|
|
130
|
+
errors.push(`[cdl] unknown-ref: phase "${p.id}" の activate "${id}" は node でも edge でもない (typo の可能性)`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
for (const t of p.tweens) {
|
|
134
|
+
if (!stateIds.has(t.stateId)) {
|
|
135
|
+
errors.push(`[cdl] unknown-ref: phase "${p.id}" tween 対象 state "${t.stateId}" 未定義`);
|
|
136
|
+
}
|
|
137
|
+
if (typeof t.from === "number" && typeof t.to === "number" && t.from === t.to) {
|
|
138
|
+
warnings.push(`phase "${p.id}" tween state "${t.stateId}" の from === to (${t.from})、 動きなし`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
for (const s of p.sets) {
|
|
142
|
+
if (!stateIds.has(s.stateId)) {
|
|
143
|
+
errors.push(`[cdl] unknown-ref: phase "${p.id}" set 対象 state "${s.stateId}" 未定義`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (p.duration <= 0) {
|
|
147
|
+
errors.push(`[cdl] invalid-duration: phase "${p.id}" duration "${p.duration}" は正値必須`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ───────────────────────────────────────────────────────────
|
|
152
|
+
// 6. state 参照 (node.value / node.rows の {stateId}) → 未定義は warning
|
|
153
|
+
// ───────────────────────────────────────────────────────────
|
|
154
|
+
const refRe = /\{(\w+)\}/g;
|
|
155
|
+
for (const n of diag.nodes) {
|
|
156
|
+
const checks: string[] = [];
|
|
157
|
+
if (n.value) checks.push(n.value);
|
|
158
|
+
if (n.rows) checks.push(...n.rows);
|
|
159
|
+
for (const text of checks) {
|
|
160
|
+
let m: RegExpExecArray | null;
|
|
161
|
+
while ((m = refRe.exec(text))) {
|
|
162
|
+
const id = m[1];
|
|
163
|
+
if (id && !stateIds.has(id)) {
|
|
164
|
+
warnings.push(`node "${n.id}" が未定義 state "${id}" を参照`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ───────────────────────────────────────────────────────────
|
|
171
|
+
// 6.5. unused detection (warn のみ、 build を止めない)
|
|
172
|
+
// ───────────────────────────────────────────────────────────
|
|
173
|
+
// どの phase でも activate されない node / edge は warn (catalog で OK だが本番では typo の可能性)
|
|
174
|
+
const allActivated = new Set<string>();
|
|
175
|
+
for (const p of diag.phases) {
|
|
176
|
+
for (const id of p.activate) allActivated.add(id);
|
|
177
|
+
}
|
|
178
|
+
for (const n of diag.nodes) {
|
|
179
|
+
if (!allActivated.has(n.id)) {
|
|
180
|
+
warnings.push(`node "${n.id}" がどの phase でも activate されていない (unused、 typo の可能性)`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
for (const e of diag.edges) {
|
|
184
|
+
if (!allActivated.has(e.id)) {
|
|
185
|
+
warnings.push(`edge "${e.id}" がどの phase でも activate されていない (unused、 typo の可能性)`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
// 使われない state も warn (tween / set / node.value / node.rows のいずれにも参照されない)
|
|
189
|
+
const stateUsed = new Set<string>();
|
|
190
|
+
for (const p of diag.phases) {
|
|
191
|
+
for (const t of p.tweens) stateUsed.add(t.stateId);
|
|
192
|
+
for (const s of p.sets) stateUsed.add(s.stateId);
|
|
193
|
+
}
|
|
194
|
+
for (const n of diag.nodes) {
|
|
195
|
+
const texts = [n.value, ...(n.rows ?? [])].filter(Boolean) as string[];
|
|
196
|
+
for (const text of texts) {
|
|
197
|
+
let m: RegExpExecArray | null;
|
|
198
|
+
const re = /\{(\w+)\}/g;
|
|
199
|
+
while ((m = re.exec(text))) {
|
|
200
|
+
if (m[1]) stateUsed.add(m[1]);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
for (const s of diag.states) {
|
|
205
|
+
if (!stateUsed.has(s.id)) {
|
|
206
|
+
warnings.push(`state "${s.id}" がどこでも参照されていない (unused)`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
// 使われない lane も warn (どの node も配置されない)
|
|
210
|
+
const laneUsed = new Set<string>();
|
|
211
|
+
for (const n of diag.nodes) laneUsed.add(n.lane);
|
|
212
|
+
for (const l of diag.lanes) {
|
|
213
|
+
if (!laneUsed.has(l.id)) {
|
|
214
|
+
warnings.push(`lane "${l.id}" にどの node も配置されていない (unused)`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ───────────────────────────────────────────────────────────
|
|
219
|
+
// 7. japanese-only (uppercase 英単語ラベル禁止) → warning
|
|
220
|
+
// ───────────────────────────────────────────────────────────
|
|
221
|
+
const texts: Array<{ where: string; text: string }> = [];
|
|
222
|
+
for (const l of diag.lanes) if (l.label) texts.push({ where: `lane "${l.id}".label`, text: l.label });
|
|
223
|
+
for (const n of diag.nodes) {
|
|
224
|
+
texts.push({ where: `node "${n.id}".title`, text: n.title });
|
|
225
|
+
if (n.eyebrow) texts.push({ where: `node "${n.id}".eyebrow`, text: n.eyebrow });
|
|
226
|
+
if (n.subtitle) texts.push({ where: `node "${n.id}".subtitle`, text: n.subtitle });
|
|
227
|
+
}
|
|
228
|
+
for (const e of diag.edges) {
|
|
229
|
+
texts.push({ where: `edge "${e.id}".label`, text: e.label });
|
|
230
|
+
if (e.sub) texts.push({ where: `edge "${e.id}".sub`, text: e.sub });
|
|
231
|
+
}
|
|
232
|
+
for (const { where, text } of texts) {
|
|
233
|
+
if (ENG_BANNED.test(text)) {
|
|
234
|
+
warnings.push(`${where} に uppercase 英単語が検出: "${text}"`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ───────────────────────────────────────────────────────────
|
|
239
|
+
// 出力
|
|
240
|
+
// ───────────────────────────────────────────────────────────
|
|
241
|
+
if (warnings.length > 0) {
|
|
242
|
+
for (const w of warnings) console.warn(`[cdl validate] warn: ${w}`);
|
|
243
|
+
}
|
|
244
|
+
if (errors.length > 0) {
|
|
245
|
+
throw new Error(`cdl validate failed:\n - ${errors.join("\n - ")}`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Levenshtein distance (typo サジェスト用)。
|
|
251
|
+
* 規模が小さい (各 string 数十文字程度) のため素朴な DP で十分。
|
|
252
|
+
*/
|
|
253
|
+
function levenshtein(a: string, b: string): number {
|
|
254
|
+
const m = a.length;
|
|
255
|
+
const n = b.length;
|
|
256
|
+
if (m === 0) return n;
|
|
257
|
+
if (n === 0) return m;
|
|
258
|
+
const dp: number[][] = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
|
|
259
|
+
for (let i = 0; i <= m; i++) dp[i]![0] = i;
|
|
260
|
+
for (let j = 0; j <= n; j++) dp[0]![j] = j;
|
|
261
|
+
for (let i = 1; i <= m; i++) {
|
|
262
|
+
for (let j = 1; j <= n; j++) {
|
|
263
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
264
|
+
dp[i]![j] = Math.min(dp[i - 1]![j]! + 1, dp[i]![j - 1]! + 1, dp[i - 1]![j - 1]! + cost);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return dp[m]![n]!;
|
|
268
|
+
}
|