@wscytz/dsl 0.0.0-stage → 0.1.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,74 @@
1
+ /**
2
+ * to-json-canvas — cys-dsl ops → JSON Canvas 1.0(jsoncanvas.org)适配器。
3
+ *
4
+ * 用途(执行意向书 Step 2「顺手」):让陌生画布用户把 .sift 一键转成他们已在用的格式
5
+ * (Obsidian .canvas / React Flow / Excalidraw 生态均支持 JSON Canvas 导入)。
6
+ *
7
+ * 映射(v0.1,明确取舍):
8
+ * - card → node(type:'text'),title 落 node.text;
9
+ * - free text → node(type:'text');
10
+ * - 关系箭头(from/to)→ edge,label 落 edge.label;
11
+ * - rect / frame → 跳过(JSON Canvas 无对应语义,数据仍在 .sift 源);
12
+ * - 自由箭头(freeArrow)/ keepExistingPos 卡(无独立几何,独立转换无现有画布可沿用)→ 跳过;
13
+ * - 其余 cys 字段(color/content/@type/@tags/@links/@code/@quote/@group/@href/compute/
14
+ * dash/arrowhead/curve/route/elbow/wikilink)整体落节点的 `cys` 附加属性 —— JSON Canvas
15
+ * 允许节点/边携带任意附加属性,round-trip 回 DSL 时不丢。
16
+ *
17
+ * 颜色**故意不**映射到 node.color:JSON Canvas 1.0 的 color 只接受 "1".."6" 或 #hex,
18
+ * DSL 的色 token('blue' 等)不是合法值,塞进去会产出不合规文档 —— 保存在 cys.color。
19
+ * curve/route/elbow 同理保进 cys(消费端画直线,数据不丢)。
20
+ *
21
+ * 纯函数、零依赖(与包整体纪律一致);本模块进公开 barrel,cli.ts 不进(避免 node:fs
22
+ * 进 web bundle)。
23
+ */
24
+ import type { DslOp } from './dsl-parser.js';
25
+ /** 与 apply-layout / relational-solver 一致的缺省卡尺寸。 */
26
+ export declare const JSON_CANVAS_DEFAULT_CARD_W = 240;
27
+ export declare const JSON_CANVAS_DEFAULT_CARD_H = 120;
28
+ /** 自由 text 元素的转换缺省尺寸(仅当 op 未带 @size 时)。 */
29
+ export declare const JSON_CANVAS_DEFAULT_TEXT_W = 200;
30
+ export declare const JSON_CANVAS_DEFAULT_TEXT_H = 60;
31
+ /** JSON Canvas 1.0 节点。附加字段全部收在 `cys` 命名空间(spec 允许任意附加属性)。 */
32
+ export type JsonCanvasNode = {
33
+ id: string;
34
+ type: 'text' | 'group';
35
+ x: number;
36
+ y: number;
37
+ width: number;
38
+ height: number;
39
+ color?: string;
40
+ text?: string;
41
+ cys?: Record<string, unknown>;
42
+ };
43
+ /** JSON Canvas 1.0 边。 */
44
+ export type JsonCanvasEdge = {
45
+ id: string;
46
+ fromNode: string;
47
+ toNode: string;
48
+ color?: string;
49
+ label?: string;
50
+ cys?: Record<string, unknown>;
51
+ };
52
+ /** JSON Canvas 1.0 文档根。 */
53
+ export type JsonCanvasFile = {
54
+ nodes: JsonCanvasNode[];
55
+ edges: JsonCanvasEdge[];
56
+ };
57
+ /** 被跳过的 op 及原因(转换是**诚实的**:跳过必留痕,不静默丢)。 */
58
+ export type SkippedOp = {
59
+ /** 源 op 在输入数组中的下标。 */
60
+ opIndex: number;
61
+ /** op 种类名(card / rect / frame / arrow)。 */
62
+ op: string;
63
+ reason: string;
64
+ };
65
+ /**
66
+ * 将(已求解的)DSL ops 转 JSON Canvas 1.0 文档。
67
+ *
68
+ * 输入应是 solveRelational 之后的 ops(关系式已解析为绝对坐标);converter 自身不求解。
69
+ * 箭头延迟到节点集齐后处理(DSL 允许前向引用:arrow 行可出现在目标 card 行之前)。
70
+ */
71
+ export declare function toJsonCanvas(ops: DslOp[]): {
72
+ canvas: JsonCanvasFile;
73
+ skipped: SkippedOp[];
74
+ };
@@ -0,0 +1,149 @@
1
+ /** 与 apply-layout / relational-solver 一致的缺省卡尺寸。 */
2
+ export const JSON_CANVAS_DEFAULT_CARD_W = 240;
3
+ export const JSON_CANVAS_DEFAULT_CARD_H = 120;
4
+ /** 自由 text 元素的转换缺省尺寸(仅当 op 未带 @size 时)。 */
5
+ export const JSON_CANVAS_DEFAULT_TEXT_W = 200;
6
+ export const JSON_CANVAS_DEFAULT_TEXT_H = 60;
7
+ /**
8
+ * 将(已求解的)DSL ops 转 JSON Canvas 1.0 文档。
9
+ *
10
+ * 输入应是 solveRelational 之后的 ops(关系式已解析为绝对坐标);converter 自身不求解。
11
+ * 箭头延迟到节点集齐后处理(DSL 允许前向引用:arrow 行可出现在目标 card 行之前)。
12
+ */
13
+ export function toJsonCanvas(ops) {
14
+ const nodes = [];
15
+ const edges = [];
16
+ const skipped = [];
17
+ const deferredArrows = [];
18
+ const pushCys = (node_or_edge, cys) => {
19
+ if (Object.keys(cys).length > 0)
20
+ node_or_edge.cys = cys;
21
+ };
22
+ // id 全局唯一(JSON Canvas spec:node/edge id 各自 unique)。显式重复 → 跳过留痕
23
+ // (保首个,后续引用边仍解析到首节点);兜底生成 id 撞车 → 静默改名(边 id 无引用语义)。
24
+ const usedIds = new Set();
25
+ function claimId(candidate, explicit, opIndex, opKind) {
26
+ if (!usedIds.has(candidate)) {
27
+ usedIds.add(candidate);
28
+ return candidate;
29
+ }
30
+ if (explicit) {
31
+ skipped.push({ opIndex, op: opKind, reason: `id ${candidate} 与更早元素重复,仅首个进转换` });
32
+ return null;
33
+ }
34
+ let n = 2;
35
+ while (usedIds.has(`${candidate}-${n}`))
36
+ n++;
37
+ const renamed = `${candidate}-${n}`;
38
+ usedIds.add(renamed);
39
+ return renamed;
40
+ }
41
+ function addCard(op, opIndex) {
42
+ if (op.keepExistingPos === true) {
43
+ skipped.push({
44
+ opIndex,
45
+ op: 'card',
46
+ reason: '纯属性编辑 op(keepExistingPos,无独立几何)——独立转换没有现有画布坐标可沿用',
47
+ });
48
+ return;
49
+ }
50
+ const id = claimId(op.cardId, true, opIndex, 'card');
51
+ if (id === null)
52
+ return;
53
+ // spec:x/y/width/height 为 required integer —— solver/parse 可产小数,此处取整。
54
+ // text 型节点 text 为 required —— 无 @title 时落空串。
55
+ const node = {
56
+ id,
57
+ type: 'text',
58
+ x: Math.round(op.x),
59
+ y: Math.round(op.y),
60
+ width: Math.round(op.w ?? JSON_CANVAS_DEFAULT_CARD_W),
61
+ height: Math.round(op.h ?? JSON_CANVAS_DEFAULT_CARD_H),
62
+ text: op.title ?? '',
63
+ };
64
+ pushCys(node, {
65
+ ...(op.color !== undefined ? { color: op.color } : {}),
66
+ ...(op.content !== undefined ? { content: op.content } : {}),
67
+ ...(op.cardType !== undefined ? { cardType: op.cardType } : {}),
68
+ ...(op.tags?.length ? { tags: op.tags } : {}),
69
+ ...(op.links?.length ? { links: op.links } : {}),
70
+ ...(op.code?.length ? { code: op.code } : {}),
71
+ ...(op.quotes?.length ? { quotes: op.quotes } : {}),
72
+ ...(op.group !== undefined ? { group: op.group } : {}),
73
+ ...(op.href?.length ? { href: op.href } : {}),
74
+ });
75
+ nodes.push(node);
76
+ }
77
+ function addFree(op, opIndex) {
78
+ if (op.shape !== 'text') {
79
+ skipped.push({
80
+ opIndex,
81
+ op: op.shape,
82
+ reason: `JSON Canvas 无 ${op.shape} 语义(v0.1 不映射;源 .sift 中保留)`,
83
+ });
84
+ return;
85
+ }
86
+ const id = claimId(op.id ?? `n${opIndex}`, op.id !== undefined, opIndex, op.shape);
87
+ if (id === null)
88
+ return;
89
+ const node = {
90
+ id,
91
+ type: 'text',
92
+ x: Math.round(op.x),
93
+ y: Math.round(op.y),
94
+ width: Math.round(op.w ?? JSON_CANVAS_DEFAULT_TEXT_W),
95
+ height: Math.round(op.h ?? JSON_CANVAS_DEFAULT_TEXT_H),
96
+ text: op.text ?? '',
97
+ };
98
+ pushCys(node, {
99
+ ...(op.color !== undefined ? { color: op.color } : {}),
100
+ ...(op.group !== undefined ? { group: op.group } : {}),
101
+ ...(op.compute !== undefined ? { compute: op.compute } : {}),
102
+ });
103
+ nodes.push(node);
104
+ }
105
+ function addArrow(op, opIndex, nodeIds) {
106
+ const missing = !nodeIds.has(op.from) ? op.from : !nodeIds.has(op.to) ? op.to : null;
107
+ if (missing !== null) {
108
+ skipped.push({
109
+ opIndex,
110
+ op: 'arrow',
111
+ reason: `端点 #${missing} 不在可转换节点中(元素缺失或其 op 被跳过),边随之跳过`,
112
+ });
113
+ return;
114
+ }
115
+ const edgeId = claimId(op.id ?? `e${opIndex}`, op.id !== undefined, opIndex, 'arrow');
116
+ if (edgeId === null)
117
+ return;
118
+ const edge = { id: edgeId, fromNode: op.from, toNode: op.to };
119
+ if (op.label !== undefined)
120
+ edge.label = op.label;
121
+ pushCys(edge, {
122
+ ...(op.color !== undefined ? { color: op.color } : {}),
123
+ ...(op.dash !== undefined ? { dash: op.dash } : {}),
124
+ ...(op.arrowhead !== undefined ? { arrowhead: op.arrowhead } : {}),
125
+ ...(op.wikilink !== undefined ? { wikilink: op.wikilink } : {}),
126
+ ...(op.curve !== undefined ? { curve: op.curve } : {}),
127
+ ...(op.route !== undefined ? { route: op.route } : {}),
128
+ ...(op.elbow !== undefined ? { elbow: op.elbow } : {}),
129
+ });
130
+ edges.push(edge);
131
+ }
132
+ ops.forEach((op, opIndex) => {
133
+ if (op.type === 'card')
134
+ return addCard(op, opIndex);
135
+ if (op.type === 'free')
136
+ return addFree(op, opIndex);
137
+ // 箭头:freeArrow(或 from/to 为空串)无端点,JSON Canvas 边必须有节点端点。
138
+ if (op.freeArrow || !op.from || !op.to) {
139
+ skipped.push({ opIndex, op: 'arrow', reason: '自由箭头无 from/to 端点,JSON Canvas 边需要节点端点' });
140
+ return;
141
+ }
142
+ deferredArrows.push({ op, opIndex });
143
+ });
144
+ const nodeIds = new Set(nodes.map((n) => n.id));
145
+ for (const { op, opIndex } of deferredArrows)
146
+ addArrow(op, opIndex, nodeIds);
147
+ skipped.sort((a, b) => a.opIndex - b.opIndex);
148
+ return { canvas: { nodes, edges }, skipped };
149
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * to-svg — cys-dsl ops → 独立 SVG(零依赖,预览级)。
3
+ *
4
+ * 执行意向书 Step 1 的 render 承诺:CLI `render --svg` 输出 SVG(默认输出 JSON Canvas)。
5
+ * 自包含:色值直接落设计 token 实值(packages/ui tokens.css 浅色主题取值,2026-10),
6
+ * 不 import canvas-engine(web 版 elements-to-svg 依赖宿主 token 注入,搬不动)。
7
+ *
8
+ * 保真边界(如实声明,不冒充完整渲染):
9
+ * - 卡片 = 描边矩形 + 标题 + 最多 3 行内容(超出省略号);不渲染 media/tag chips/组色带。
10
+ * - @compute 不在此求值(CLI pipeline 只 parse→sanitize→solve):text 只有 compute 无 text 时
11
+ * 渲染公式原文前缀 '='。
12
+ * - 关系箭头端点按 id 解析到卡/自由元素几何,贴边交点连线;端点缺失 → 跳过并留原因。
13
+ * - 默认路由自动避障:直连段穿过卡/矩形时自动绕行走廊(L/Z 形候选,端点重贴边,全清才采纳);
14
+ * 显式 route/curve/elbow 不干预。route=elbow 未给折点 = 正交自动路由(中线 Z / 避障绕行)。
15
+ * frame 是容器、text 无边界,都不算障碍。折线转角圆角化(渲染层美化,不改几何)。
16
+ * - 路由 straight/curve/elbow 都画;arrowhead arrow=开口 V / triangle=实心三角 / none=无线帽。
17
+ */
18
+ import type { DslOp } from './dsl-parser.js';
19
+ export interface SvgSkip {
20
+ opIndex: number;
21
+ op: string;
22
+ reason: string;
23
+ }
24
+ interface Rect {
25
+ x: number;
26
+ y: number;
27
+ w: number;
28
+ h: number;
29
+ }
30
+ interface Pt {
31
+ x: number;
32
+ y: number;
33
+ }
34
+ /**
35
+ * 默认路由自动避障:直连段穿过障碍时,生成绕行路标再整段验证。候选四族:
36
+ * L 形 2 个 / 命中包围盒外扩 Z 形 4 个 / 障碍间隙走廊 Z 形(两轴各若干——
37
+ * 直连线同时擦到两簇障碍时,包围盒外扩走廊会全撞墙,列间走廊才是活路)/
38
+ * 全部障碍外包盒四向兜底(前面全灭时宁可绕远不可穿模)。
39
+ * 每个候选都把端点按新方向重新贴边(borderPoint),返回**整条路径**(含新端点)——
40
+ * 调用方直接用,不再拼原端点(否则末端沿卡边滑行,箭头顺着边飘)。
41
+ * 全部候选撞墙则退回 null(直连,诚实降级不硬凑)。margin=走廊与障碍的间距。
42
+ */
43
+ export declare function routeAround(p1: Pt, p2: Pt, src: Rect, dst: Rect, obstacles: Rect[], margin?: number): Pt[] | null;
44
+ export declare function toSvg(ops: DslOp[]): {
45
+ svg: string;
46
+ skipped: SvgSkip[];
47
+ };
48
+ export {};