@wscytz/dsl 0.0.0-stage → 0.1.1

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,134 @@
1
+ /**
2
+ * dsl-grammar — cys-dsl 语法的单一可信源。
3
+ *
4
+ * 持有所有「声明性事实」:版号、指令种类、颜色枚举、规范文字描述(给 prompt/help)。
5
+ * parser(dsl-parser.ts)和 serializer(canvas-dsl.ts)的代码级语法从此 import KINDS/COLORS;
6
+ * AI prompt 和用户向语法帮助 import DSL_GRAMMAR_REFERENCE。
7
+ *
8
+ * 加新指令 = 改这里 + parser/serializer 各一处 + bump DSL_VERSION;prompt 自动跟随。
9
+ * 这是为 (c2) prompt 加固做的联动准备——改 prompt 届时只改 REFERENCE 一处。
10
+ *
11
+ * bump 规则:增删指令种类 / 增删属性 / 改颜色枚举 → bump DSL_VERSION。
12
+ * 纯改 prompt 措辞、改 parser 正则细节(不动语法)→ 不 bump。
13
+ */
14
+ export declare const DSL_VERSION = 9;
15
+ /**
16
+ * v9 组锚保留字:锚位裸名 `@词` 与内建指令同名时,sanitize 产诊断并剥 rel
17
+ * (防"组名漏写/误吞指令"静默成错误组锚)。引号形式 `@"gap"` 是显式意图,不受此限。
18
+ * 与指令清单手动同步——新增指令时补这里。
19
+ */
20
+ export declare const DSL_RESERVED_GROUP_ANCHOR_WORDS: readonly string[];
21
+ /**
22
+ * `@text("...")` / `@label("...")` / `@title("...")` 值的最大字符数(int 级)。
23
+ *
24
+ * 防护:AI 输出不可信,可能产超长文本(幻觉 / 错误重复 / token 失控)→ 渲染溢出 / 存储
25
+ * 膨胀 / 应用卡顿(DoS)。parser 在解析时把超长值**静默截断**到上限(不报错不 warn ——
26
+ * parser 要 robust,坏一行不该整块丢)。
27
+ *
28
+ * 不 bump DSL_VERSION:这是 parser 防护,不改语法形态(语法层面 @text 仍接任意字符串,
29
+ * 只是 parser 不接受超长)。200 覆盖正常画布标签/文本需求(标签通常 <20 字,文本标题 <50)。
30
+ */
31
+ export declare const DSL_MAX_TEXT_LEN = 200;
32
+ /**
33
+ * `@content("...")` 值的最大字符数(v5:卡片正文 markdown,long 级)。
34
+ *
35
+ * 与 @title/@text 的 DSL_MAX_TEXT_LEN(int 级 200)区分:@content 是 long 级 —— 卡片正文是
36
+ * markdown body,远长于标签/标题。8000 覆盖正常长笔记(~1000 词),同时防 LLM 失控膨胀(DoS)。
37
+ * parser + sanitize 都按此截断(静默,不报错)。
38
+ *
39
+ * 不 bump DSL_VERSION:parser 防护,不改语法形态。
40
+ */
41
+ export declare const DSL_MAX_CONTENT_LEN = 8000;
42
+ /**
43
+ * `@href(#a;#b;…)` 目标 id 的最大个数(v7:卡片显式语义引用列表)。
44
+ *
45
+ * 防护:AI 输出不可信,可能产超长引用列表(幻觉 / token 失控)→ 存储膨胀 / 应用卡顿(DoS)。
46
+ * parser 解析 @href 时把目标数**静默截断**到上限(保前 N 个,不报错)。20 远超正常一张卡的
47
+ * 出链需求(典型 <5)。
48
+ *
49
+ * 不 bump DSL_VERSION:parser 防护,不改语法形态(语法层面 @href 仍接任意长 `;` 列表)。
50
+ */
51
+ export declare const DSL_MAX_HREF_TARGETS = 20;
52
+ /**
53
+ * `@tags(a;b;c)` 标签最大个数(v8:卡片标签列表)。
54
+ *
55
+ * 防护:AI 输出不可信,可能产超长标签列表 → 存储膨胀 / 渲染卡顿(DoS)。parser 解析 @tags
56
+ * 时把个数**静默截断**到上限(保前 N 个,不报错)。20 远超正常一张卡的标签数(典型 <5)。
57
+ *
58
+ * 不 bump DSL_VERSION:parser 防护,不改语法形态(语法层面 @tags 仍接任意长 `;` 列表)。
59
+ */
60
+ export declare const DSL_MAX_TAG_COUNT = 20;
61
+ /**
62
+ * `@links(<url>;…)` 链接最大个数(v8:卡片外链列表,仅 URL)。
63
+ *
64
+ * 防护同 @tags。10 覆盖正常一张卡的外链数(典型 1-3)。
65
+ *
66
+ * 不 bump DSL_VERSION:parser 防护,不改语法形态。
67
+ */
68
+ export declare const DSL_MAX_LINK_COUNT = 10;
69
+ /**
70
+ * `@code(…)` 代码块最大个数(v8:卡片代码片段,可重复指令)。
71
+ *
72
+ * 防护:多 @code 指令累积,上限防 LLM 失控膨胀。8 远超正常一张卡的代码块数(典型 1)。
73
+ *
74
+ * 不 bump DSL_VERSION:parser 防护,不改语法形态。
75
+ */
76
+ export declare const DSL_MAX_CODE_BLOCKS = 8;
77
+ /**
78
+ * `@quote(…)` 引文最大个数(v8:卡片引文,可重复指令)。
79
+ *
80
+ * 防护同 @code。8 远超正常一张卡的引文数(典型 1)。
81
+ *
82
+ * 不 bump DSL_VERSION:parser 防护,不改语法形态。
83
+ */
84
+ export declare const DSL_MAX_QUOTES = 8;
85
+ /**
86
+ * 单块 DSL 的元素(card/arrow/free)总数上限 —— parse 层 DoS 防御。
87
+ *
88
+ * 防「粘贴炸弹」/恶意超大输入:无上限时无限行 → 无限 op 数组 → sanitize 全扫 +
89
+ * applyLayout 逐个 upsert + (含 relational card 时) solveRelational O(N³) 碰撞避让,
90
+ * 主线程卡死。sanitize 已钳每 op 的字段(size/coord/text/tag/code…),但未限 op 总数 ——
91
+ * 本常量补这层(parse 层,在 sanitize 之前)。
92
+ *
93
+ * 值取 1000:正常单画布远不触达(典型几十到几百);卡在 solveRelational 的安全区边缘
94
+ * (N=1000 → 约 1e9,几秒降级,用户得到截断 diagnostic 而非无限卡死)。彻底把 solver
95
+ * 降到 O(N) 单调栈是后续独立工作(relational-solver.ts,高风险,另立 plan)。
96
+ *
97
+ * 不 bump DSL_VERSION:parse 防护,不改语法形态。
98
+ */
99
+ export declare const DSL_MAX_OPS = 1000;
100
+ /**
101
+ * `@type(…)` 合法值(v8:卡片语义类型,镜像 domain CardType)。
102
+ *
103
+ * 声明性事实,放语法单一可信源。parser(validEnum 收窄)与 sanitize(二次枚举校验)共用,
104
+ * 消除两处重复列表。media 不进 DSL,故 image 卡只往返 type。
105
+ *
106
+ * 不 bump DSL_VERSION 于本常量本身:它是 @type 指令的值域(增删此枚举值 = 改语法 → bump,见上)。
107
+ */
108
+ export declare const DSL_CARD_TYPES: readonly ["note", "image", "link", "code", "quote"];
109
+ export type DslCardType = (typeof DSL_CARD_TYPES)[number];
110
+ /**
111
+ * 文本截断到 max 个 UTF-16 码元,**不劈开代理对**(emoji / 增补平面字符占 2 码元)。
112
+ *
113
+ * 若切点正好落在高代理位(其配对的低代理位在 max 处、会被切掉),回退一位 —— 避免产生
114
+ * 孤立代理位(渲染成坏字符、JSON.stringify 出 `\udxxx` 孤值)。parser(dsl-parser.ts)与
115
+ * sanitize(dsl-sanitize.ts)共用这一处实现(消除两处重复 truncateTo;H 代理对安全)。
116
+ *
117
+ * max 为 0 / 空串安全:charCodeAt(-1)=NaN,不满足高代理位区间,slice(0,0)=''。
118
+ */
119
+ export declare function truncateDslText(v: string, max: number): string;
120
+ /** 指令种类(parser 识别 + serializer 序列化的集合)。freedraw 不在 DSL——程序自管(R2 + 渲染),
121
+ * 非 DSL 可表达(点序列重、意义低、隐私);serialize 按 DSL_KINDS 过滤,freedraw 同 legacy 被丢。 */
122
+ export declare const DSL_KINDS: readonly ["card", "rect", "frame", "text", "arrow"];
123
+ export type DslKind = (typeof DSL_KINDS)[number];
124
+ /** Bauhaus-6 颜色(canonical)。grey 是 gray 的输入别名。 */
125
+ export declare const DSL_COLORS: readonly ["red", "yellow", "blue", "black", "white", "gray"];
126
+ export type DslColor = (typeof DSL_COLORS)[number];
127
+ /** 输入别名(parser 接受 canonical ∪ 这些别名键)。 */
128
+ export declare const DSL_COLOR_ALIASES: Record<string, DslColor>;
129
+ /**
130
+ * cys-dsl 语法的规范文字描述。给所有 AI prompt 和用户向语法帮助 import。
131
+ * 指令 shape 取自原 canvas-prompt.ts 的 GRAMMAR(权威,与 parser 一致)+ 版号行。
132
+ * 故意不含 [freedraw #id]——freedraw 不在 DSL(程序自管:R2 + 渲染;点序列重/意义低/隐私),AI 不该也不产。
133
+ */
134
+ export declare const DSL_GRAMMAR_REFERENCE: string;
@@ -0,0 +1,227 @@
1
+ /**
2
+ * dsl-grammar — cys-dsl 语法的单一可信源。
3
+ *
4
+ * 持有所有「声明性事实」:版号、指令种类、颜色枚举、规范文字描述(给 prompt/help)。
5
+ * parser(dsl-parser.ts)和 serializer(canvas-dsl.ts)的代码级语法从此 import KINDS/COLORS;
6
+ * AI prompt 和用户向语法帮助 import DSL_GRAMMAR_REFERENCE。
7
+ *
8
+ * 加新指令 = 改这里 + parser/serializer 各一处 + bump DSL_VERSION;prompt 自动跟随。
9
+ * 这是为 (c2) prompt 加固做的联动准备——改 prompt 届时只改 REFERENCE 一处。
10
+ *
11
+ * bump 规则:增删指令种类 / 增删属性 / 改颜色枚举 → bump DSL_VERSION。
12
+ * 纯改 prompt 措辞、改 parser 正则细节(不动语法)→ 不 bump。
13
+ */
14
+ export const DSL_VERSION = 9;
15
+ /**
16
+ * v9 组锚保留字:锚位裸名 `@词` 与内建指令同名时,sanitize 产诊断并剥 rel
17
+ * (防"组名漏写/误吞指令"静默成错误组锚)。引号形式 `@"gap"` 是显式意图,不受此限。
18
+ * 与指令清单手动同步——新增指令时补这里。
19
+ */
20
+ export const DSL_RESERVED_GROUP_ANCHOR_WORDS = [
21
+ 'pos',
22
+ 'size',
23
+ 'color',
24
+ 'title',
25
+ 'content',
26
+ 'type',
27
+ 'tags',
28
+ 'links',
29
+ 'code',
30
+ 'quote',
31
+ 'group',
32
+ 'href',
33
+ 'compute',
34
+ 'gap',
35
+ 'create',
36
+ 'from',
37
+ 'to',
38
+ 'arrowhead',
39
+ 'route',
40
+ 'curve',
41
+ 'elbow',
42
+ 'wikilink',
43
+ 'dash',
44
+ 'label',
45
+ 'text',
46
+ 'right-of',
47
+ 'below',
48
+ ];
49
+ /**
50
+ * `@text("...")` / `@label("...")` / `@title("...")` 值的最大字符数(int 级)。
51
+ *
52
+ * 防护:AI 输出不可信,可能产超长文本(幻觉 / 错误重复 / token 失控)→ 渲染溢出 / 存储
53
+ * 膨胀 / 应用卡顿(DoS)。parser 在解析时把超长值**静默截断**到上限(不报错不 warn ——
54
+ * parser 要 robust,坏一行不该整块丢)。
55
+ *
56
+ * 不 bump DSL_VERSION:这是 parser 防护,不改语法形态(语法层面 @text 仍接任意字符串,
57
+ * 只是 parser 不接受超长)。200 覆盖正常画布标签/文本需求(标签通常 <20 字,文本标题 <50)。
58
+ */
59
+ export const DSL_MAX_TEXT_LEN = 200;
60
+ /**
61
+ * `@content("...")` 值的最大字符数(v5:卡片正文 markdown,long 级)。
62
+ *
63
+ * 与 @title/@text 的 DSL_MAX_TEXT_LEN(int 级 200)区分:@content 是 long 级 —— 卡片正文是
64
+ * markdown body,远长于标签/标题。8000 覆盖正常长笔记(~1000 词),同时防 LLM 失控膨胀(DoS)。
65
+ * parser + sanitize 都按此截断(静默,不报错)。
66
+ *
67
+ * 不 bump DSL_VERSION:parser 防护,不改语法形态。
68
+ */
69
+ export const DSL_MAX_CONTENT_LEN = 8000;
70
+ /**
71
+ * `@href(#a;#b;…)` 目标 id 的最大个数(v7:卡片显式语义引用列表)。
72
+ *
73
+ * 防护:AI 输出不可信,可能产超长引用列表(幻觉 / token 失控)→ 存储膨胀 / 应用卡顿(DoS)。
74
+ * parser 解析 @href 时把目标数**静默截断**到上限(保前 N 个,不报错)。20 远超正常一张卡的
75
+ * 出链需求(典型 <5)。
76
+ *
77
+ * 不 bump DSL_VERSION:parser 防护,不改语法形态(语法层面 @href 仍接任意长 `;` 列表)。
78
+ */
79
+ export const DSL_MAX_HREF_TARGETS = 20;
80
+ /**
81
+ * `@tags(a;b;c)` 标签最大个数(v8:卡片标签列表)。
82
+ *
83
+ * 防护:AI 输出不可信,可能产超长标签列表 → 存储膨胀 / 渲染卡顿(DoS)。parser 解析 @tags
84
+ * 时把个数**静默截断**到上限(保前 N 个,不报错)。20 远超正常一张卡的标签数(典型 <5)。
85
+ *
86
+ * 不 bump DSL_VERSION:parser 防护,不改语法形态(语法层面 @tags 仍接任意长 `;` 列表)。
87
+ */
88
+ export const DSL_MAX_TAG_COUNT = 20;
89
+ /**
90
+ * `@links(<url>;…)` 链接最大个数(v8:卡片外链列表,仅 URL)。
91
+ *
92
+ * 防护同 @tags。10 覆盖正常一张卡的外链数(典型 1-3)。
93
+ *
94
+ * 不 bump DSL_VERSION:parser 防护,不改语法形态。
95
+ */
96
+ export const DSL_MAX_LINK_COUNT = 10;
97
+ /**
98
+ * `@code(…)` 代码块最大个数(v8:卡片代码片段,可重复指令)。
99
+ *
100
+ * 防护:多 @code 指令累积,上限防 LLM 失控膨胀。8 远超正常一张卡的代码块数(典型 1)。
101
+ *
102
+ * 不 bump DSL_VERSION:parser 防护,不改语法形态。
103
+ */
104
+ export const DSL_MAX_CODE_BLOCKS = 8;
105
+ /**
106
+ * `@quote(…)` 引文最大个数(v8:卡片引文,可重复指令)。
107
+ *
108
+ * 防护同 @code。8 远超正常一张卡的引文数(典型 1)。
109
+ *
110
+ * 不 bump DSL_VERSION:parser 防护,不改语法形态。
111
+ */
112
+ export const DSL_MAX_QUOTES = 8;
113
+ /**
114
+ * 单块 DSL 的元素(card/arrow/free)总数上限 —— parse 层 DoS 防御。
115
+ *
116
+ * 防「粘贴炸弹」/恶意超大输入:无上限时无限行 → 无限 op 数组 → sanitize 全扫 +
117
+ * applyLayout 逐个 upsert + (含 relational card 时) solveRelational O(N³) 碰撞避让,
118
+ * 主线程卡死。sanitize 已钳每 op 的字段(size/coord/text/tag/code…),但未限 op 总数 ——
119
+ * 本常量补这层(parse 层,在 sanitize 之前)。
120
+ *
121
+ * 值取 1000:正常单画布远不触达(典型几十到几百);卡在 solveRelational 的安全区边缘
122
+ * (N=1000 → 约 1e9,几秒降级,用户得到截断 diagnostic 而非无限卡死)。彻底把 solver
123
+ * 降到 O(N) 单调栈是后续独立工作(relational-solver.ts,高风险,另立 plan)。
124
+ *
125
+ * 不 bump DSL_VERSION:parse 防护,不改语法形态。
126
+ */
127
+ export const DSL_MAX_OPS = 1000;
128
+ /**
129
+ * `@type(…)` 合法值(v8:卡片语义类型,镜像 domain CardType)。
130
+ *
131
+ * 声明性事实,放语法单一可信源。parser(validEnum 收窄)与 sanitize(二次枚举校验)共用,
132
+ * 消除两处重复列表。media 不进 DSL,故 image 卡只往返 type。
133
+ *
134
+ * 不 bump DSL_VERSION 于本常量本身:它是 @type 指令的值域(增删此枚举值 = 改语法 → bump,见上)。
135
+ */
136
+ export const DSL_CARD_TYPES = ['note', 'image', 'link', 'code', 'quote'];
137
+ /**
138
+ * 文本截断到 max 个 UTF-16 码元,**不劈开代理对**(emoji / 增补平面字符占 2 码元)。
139
+ *
140
+ * 若切点正好落在高代理位(其配对的低代理位在 max 处、会被切掉),回退一位 —— 避免产生
141
+ * 孤立代理位(渲染成坏字符、JSON.stringify 出 `\udxxx` 孤值)。parser(dsl-parser.ts)与
142
+ * sanitize(dsl-sanitize.ts)共用这一处实现(消除两处重复 truncateTo;H 代理对安全)。
143
+ *
144
+ * max 为 0 / 空串安全:charCodeAt(-1)=NaN,不满足高代理位区间,slice(0,0)=''。
145
+ */
146
+ export function truncateDslText(v, max) {
147
+ if (v.length <= max)
148
+ return v;
149
+ let end = max;
150
+ const code = v.charCodeAt(end - 1);
151
+ if (code >= 0xd800 && code <= 0xdbff)
152
+ end -= 1; // 高代理位 → 回退,不劈开
153
+ return v.slice(0, end);
154
+ }
155
+ /** 指令种类(parser 识别 + serializer 序列化的集合)。freedraw 不在 DSL——程序自管(R2 + 渲染),
156
+ * 非 DSL 可表达(点序列重、意义低、隐私);serialize 按 DSL_KINDS 过滤,freedraw 同 legacy 被丢。 */
157
+ export const DSL_KINDS = ['card', 'rect', 'frame', 'text', 'arrow'];
158
+ /** Bauhaus-6 颜色(canonical)。grey 是 gray 的输入别名。 */
159
+ export const DSL_COLORS = ['red', 'yellow', 'blue', 'black', 'white', 'gray'];
160
+ /** 输入别名(parser 接受 canonical ∪ 这些别名键)。 */
161
+ export const DSL_COLOR_ALIASES = { grey: 'gray' };
162
+ /**
163
+ * cys-dsl 语法的规范文字描述。给所有 AI prompt 和用户向语法帮助 import。
164
+ * 指令 shape 取自原 canvas-prompt.ts 的 GRAMMAR(权威,与 parser 一致)+ 版号行。
165
+ * 故意不含 [freedraw #id]——freedraw 不在 DSL(程序自管:R2 + 渲染;点序列重/意义低/隐私),AI 不该也不产。
166
+ */
167
+ export const DSL_GRAMMAR_REFERENCE = `cys-dsl grammar v${DSL_VERSION} (one element per line):
168
+ [card #id] @pos(x, y) @size(w, h) @color(red|yellow|blue|black|white|gray|grey) [@title("…")] [@content("…")] [@group("…")] [@href(#a;#b)] [@type(…)] [@tags(…)] [@links(…)] [@code(…)]… [@quote(…)]…
169
+ [card #id create] @pos(x, y) @size(w, h) @color(c) [@title("…")] [@content("…")] [@type(…)] [@tags(…)] [@links(…)] [@code(…)]… [@quote(…)]… # create a card; id must not exist
170
+ # @title: short card title (≤200 chars, int-tier). @content: long markdown body (≤8000 chars, long-tier).
171
+ # Quoted-string escapes: \\" = quote, \\\\ = backslash, \\n = newline, \\\` = backtick (so @content/@code carry multi-line markdown or code on one DSL line).
172
+ # @group("name") (v7): tag the card with a semantic group name (grouping/styling is a view concern, not DSL state).
173
+ # Assign a group to an EXISTING card without moving it: [card #id] @group("name") (no @pos needed).
174
+ # @href(#a;#b) (v7): declare explicit semantic links from this card to other card ids — a knowledge-graph edge
175
+ # with NO arrow drawn (distinct from body [[wikilinks]] which auto-build reference arrows). Semicolon-separated,
176
+ # leading # optional, deduped, ≤20 targets.
177
+ # @type(note|image|link|code|quote) (v8): the card's semantic type. One value. image carries no binary (media is
178
+ # excluded from DSL — heavy/privacy), so an image card round-trips its type only.
179
+ # @tags(a;b;c) (v8): the card's tags as one semicolon-separated list (each value URL-encoded, ≤20). Single directive.
180
+ # @links(<url>;…) (v8): the card's external links as one semicolon-separated list of URL-ENCODED urls (≤10).
181
+ # Only the URL round-trips (link-preview title/image are fetch-derived state, not DSL). Single directive.
182
+ # @code(lang,"code"[,"caption"]) (v8): one code block — lang is a bare token (ts/py/js/…), code is a quoted string
183
+ # (escapes carry newlines/quotes/backticks). REPEATABLE: emit one @code per block (≤8). caption optional.
184
+ # @quote("text"[,"attribution"[,"sourceUrl"]]) (v8): one quotation — text quoted, attribution/sourceUrl optional quoted.
185
+ # REPEATABLE: emit one @quote per quotation (≤8). Trailing args may be "" when a later one is present.
186
+ # Edit an EXISTING card's type/tags/links/code/quote in place WITHOUT moving it — omit @pos (geometry kept), same as @title/@content.
187
+ # relational placement — PREFER for structured layouts (trees, lists, grids, hierarchies;
188
+ # anything row/column-shaped). The engine computes coords AND avoids overlaps, so you skip
189
+ # error-prone coordinate math. Reserve @pos for free/scattered positioning only:
190
+ # [card #id] right-of #anchor @gap(20) # right of anchor, same row (x=anchor.x+w+gap; y=anchor.y)
191
+ # [card #id] below #anchor @gap(20) # below anchor, same column (y=anchor.y+h+gap; x=anchor.x)
192
+ # (@gap defaults to 20 and is limited to 0..2000; anchor must be placed earlier or already exist)
193
+ # Group anchors (v9): anchor a card to a whole GROUP's bounding box with @groupname:
194
+ # [card #id] right-of @methods @gap(30) # right of the methods group (x=bbox.maxX+gap; y=bbox.minY)
195
+ # [card #id] below @axes @gap(30) # below the axes group (y=bbox.maxY+gap; x=bbox.minX)
196
+ # Anchor forms: @name (simple name: letters/digits/_:/-/CJK), @"any name" (spaces etc.),
197
+ # or anchor a member #id. A group declared as @group("Q3 规划") is anchored via
198
+ # @"Q3 规划" or via one of its member ids. A bare @word that collides with a built-in
199
+ # directive (e.g. @gap/@pos) is REJECTED with a diagnostic — put @gap(n) AFTER the
200
+ # anchor, never at the anchor slot. The bbox covers group members resolved SO FAR
201
+ # (write group members before their group anchor; a group with no members yet →
202
+ # diagnostic).
203
+ [rect #id] @pos(x, y) @size(w, h) @color(c) [@group("…")]
204
+ [text #id] @pos(x, y) @text("...") @color(c) [@group("…")] [@compute("…")]
205
+ # @compute("expr") (v7, text only): a SAFE formula evaluated into the text's displayed value.
206
+ # Language: numbers, + - * / , parentheses, min/max/abs/round, and geometry refs #id.x|y|w|h.
207
+ # Example: [text #total] @pos(x,y) @compute("#a.w + #b.w"). NO code/eval; only element geometry
208
+ # (never card content). Recomputed on each apply (not live-reactive). On a failed formula the last text is kept.
209
+ [frame #id] @pos(x, y) @size(w, h) @text("title") @color(c) [@group("…")] # themed group/section container
210
+ [arrow #id] from #a to #b @label("...") @color(c) @dash(solid|dashed|dotted) @arrowhead(arrow|triangle|none) [@wikilink]
211
+ [arrow #id] @pos(x, y) @size(w, h) @color(c) # free arrow (no from/to)
212
+ # arrow route (optional, to bend or elbow around obstacles):
213
+ # @route(curve) @curve(cx,cy) # smooth quadratic curve via one control point
214
+ # @route(elbow) @elbow(x,y;x,y) # 1-2 corner points (semicolon-separated)
215
+ # (omit @route for a straight line)
216
+ # @wikilink (optional, relation/free arrow): only on wikilink-auto arrows
217
+ # (meta.wikilink===true); distinguishes auto-built wikilink arrows from
218
+ # manual references arrows so the marker survives DSL round-trip.
219
+ Rules: card updates are the default; explicit create persists a new card before its host element (title/content optional).
220
+ @title/@content (v5) update an existing card's title/body; "" clears it, omit to leave unchanged.
221
+ @type/@tags/@links/@code/@quote (v8) update an existing card's structured fields the same way (omit to leave unchanged).
222
+ To edit an EXISTING card's content/color/size WITHOUT moving it, omit @pos (geometry is kept):
223
+ [card #id] @title("…") @content("…") # content-only edit, position preserved
224
+ [card #id] @color(c) # recolor in place
225
+ (@pos is still required to MOVE a card or to CREATE one.)
226
+ IDs use letters, digits, underscore, hyphen, and colon. Lines starting with # are comments and ignored;
227
+ colors are the ${DSL_COLORS.length} Bauhaus tokens only.`;
@@ -0,0 +1,199 @@
1
+ /**
2
+ * DSL Parser — parses AI output (or pasted/edited DSL) for canvas layout.
3
+ *
4
+ * 架构(正则 → PEG 升级,2026-07):
5
+ * dsl.peggy(Peggy 语法,结构 tokenizer)—— `pnpm --filter @wscytz/dsl gen` → dsl-parser.gen.js
6
+ * 本文件(TS 包装层)—— 按行调用生成 parser,做语义 coercion:
7
+ * Number / finite 守卫 / escape unescape / 颜色枚举校验 / elbow 拆分 / DSL_MAX_TEXT_LEN 截断
8
+ * 永不抛错 + 坐标恒有限 + byte-equal round-trip 契约不变(见 dsl-* 测试)。
9
+ *
10
+ * 行级解析:按 \n 切行,逐行 parse Line 规则 → {kind, ds} 结构 → build{Card,Arrow,Free}
11
+ * 组装 DslOp / 记 diagnostic。`#id` 也是 directive(等价旧"整行任意位置提取");directive*
12
+ * 内的 skipChunk 吃未知残余 → 复刻旧正则的任意位置/任意顺序容错。
13
+ */
14
+ import type { CardId, CardType, CodeBlock, Quote } from './types.js';
15
+ export type DslCardOp = {
16
+ type: 'card';
17
+ cardId: CardId;
18
+ x: number;
19
+ y: number;
20
+ w?: number;
21
+ h?: number;
22
+ color?: string;
23
+ create?: boolean;
24
+ /** v5:卡片标题(短,≤DSL_MAX_TEXT_LEN)。serialize 由消费者注入;DSL apply 写回 Card.title。 */
25
+ title?: string;
26
+ /** v5:卡片正文 markdown(长,≤DSL_MAX_CONTENT_LEN)。serialize 由消费者注入;DSL apply 写回 Card.body。 */
27
+ content?: string;
28
+ /** B工程 pilot:关系式坐标。有 rel 时 x/y 是占位(0,0),求解器 solveRelational 填真值。
29
+ * right-of #anchor:x = anchor.x + anchor.w + gap;y = anchor.y
30
+ * below #anchor:y = anchor.y + anchor.h + gap;x = anchor.x
31
+ * v9 组级相对锚:anchorGroup=true 时 anchor 是组名(锚位三形态:@裸名 / @"含空格名" /
32
+ * 或退回 #组员id)。anchorQuoted 标记引号形式(保留字检查豁免)。锚位裸名撞保留指令词由
33
+ * sanitize 产诊断并剥 rel。组锚语义(lazy 组包围盒、顺序敏感)见 solveRelational。
34
+ * 仅 AI 输入路径;serializeCanvas 永不 emit rel(rel 解决后画布存绝对坐标)。 */
35
+ rel?: {
36
+ dir: 'right-of' | 'below';
37
+ anchor: string;
38
+ gap: number;
39
+ anchorGroup?: boolean;
40
+ anchorQuoted?: boolean;
41
+ };
42
+ /** v5(E):无 @pos 的"纯属性/内容编辑"标志。无 @pos(且非 create、非 rel)但携带
43
+ * title/content/color/size 之一时置真 —— apply 时(planCard)沿用现有卡几何,只更那些字段;
44
+ * x/y 为占位(0,0),apply 期忽略。serializeCanvas 永不 emit(始终绝对坐标)→ 输入专用。 */
45
+ keepExistingPos?: boolean;
46
+ /** v7:语义分组名(directive,非新 kind)。落 element.meta.group;组样式/折叠是视图层,不进 DSL。 */
47
+ group?: string;
48
+ /** v7:卡片显式语义引用目标 id 列表(KG 边,不画线)。落 element.meta.href。
49
+ * 区别于正文 [[...]]→自动 references 箭头:这是 DSL 里直接声明的语义边。 */
50
+ href?: string[];
51
+ /** v8:卡片语义类型(@type)。media 不进 DSL,故 image 卡只往返 type。apply 写回 Card.type。 */
52
+ cardType?: CardType;
53
+ /** v8:卡片标签**值**列表(@tags;颜色由 apply 层按 stableTagColor 指派,parser 不知色)。
54
+ * apply 映射成 TagRef[]。 */
55
+ tags?: string[];
56
+ /** v8:卡片外链 **URL** 列表(@links;仅 URL,预览 title/image 是抓取派生态不往返)。
57
+ * apply 映射成 LinkPreview[](fetchedAt=new Date())。 */
58
+ links?: string[];
59
+ /** v8:卡片代码块(@code,可重复)。apply 写回 Card.codeSnippets。 */
60
+ code?: CodeBlock[];
61
+ /** v8:卡片引文(@quote,可重复)。apply 写回 Card.quotes。 */
62
+ quotes?: Quote[];
63
+ };
64
+ export type DslFreeOp = {
65
+ type: 'free';
66
+ shape: 'rect';
67
+ /** Element id (round-trip with serializeCanvas's `[rect #id]`). */
68
+ id?: string;
69
+ x: number;
70
+ y: number;
71
+ w?: number;
72
+ h?: number;
73
+ color?: string;
74
+ /** v7:语义分组名。落 element.meta.group。 */
75
+ group?: string;
76
+ } | {
77
+ type: 'free';
78
+ shape: 'text';
79
+ /** Element id (round-trip with serializeCanvas's `[text #id]`). */
80
+ id?: string;
81
+ x: number;
82
+ y: number;
83
+ w?: number;
84
+ h?: number;
85
+ text?: string;
86
+ color?: string;
87
+ /** v7:语义分组名。落 element.meta.group。 */
88
+ group?: string;
89
+ /** v7:安全公式原文(仅 text 元素)。apply 时用受限递归下降求值器算(禁裸 eval),
90
+ * 只引用元素几何 #id.x/y/w/h,结果写 element.text,原式存 element.meta.compute。 */
91
+ compute?: string;
92
+ } | {
93
+ type: 'free';
94
+ shape: 'frame';
95
+ /** Element id (round-trip with serializeCanvas's `[frame #id]`). */
96
+ id?: string;
97
+ x: number;
98
+ y: number;
99
+ w?: number;
100
+ h?: number;
101
+ text?: string;
102
+ color?: string;
103
+ /** v7:语义分组名。落 element.meta.group。 */
104
+ group?: string;
105
+ };
106
+ export type DslArrowOp = {
107
+ type: 'arrow';
108
+ /** Arrow element id (round-trip with serializeCanvas's `[arrow #id]`). When
109
+ * present and the host already has this arrow, applyArrowOp updates it in
110
+ * place (changing its relation signature) instead of creating a new one. */
111
+ id?: string;
112
+ from: string;
113
+ to: string;
114
+ label?: string;
115
+ color?: string;
116
+ /** Relation signature line style (semantics): solid/dashed/dotted. */
117
+ dash?: 'solid' | 'dashed' | 'dotted';
118
+ /** Relation signature terminal (semantics): arrow/triangle/none. */
119
+ arrowhead?: 'arrow' | 'triangle' | 'none';
120
+ /** 自由箭头标记:无 from/to,pos+size 编码线段 bbox。 */
121
+ freeArrow?: boolean;
122
+ /** 自由箭头 bbox(仅 freeArrow=true 时有意义)。 */
123
+ x?: number;
124
+ y?: number;
125
+ w?: number;
126
+ h?: number;
127
+ /** 弯曲控制点(二次贝塞尔,绝对页坐标)。关系/自由箭头均可。 */
128
+ curve?: {
129
+ cx: number;
130
+ cy: number;
131
+ };
132
+ /** 箭头路由形态:straight(直线)/curve(弯曲)/elbow(折线)。缺省 straight。
133
+ * 向后兼容:无 route 但有 curve → 当 curve(serialize 不为 straight 主动输出 route,
134
+ * 除非显式切过)。 */
135
+ route?: 'straight' | 'curve' | 'elbow';
136
+ /** 折线折点(1-2 个,绝对页坐标)。route='elbow' 时用。 */
137
+ elbow?: {
138
+ x: number;
139
+ y: number;
140
+ }[];
141
+ /** 显式 wikilink 标记:仅 meta.wikilink===true 的箭头序列化时 emit `@wikilink`。
142
+ * 区分自动建(wikilink)箭头与手动 references 箭头,让标记在 DSL round-trip 中存活。
143
+ * 应用时(applyArrowOp)→ host 元素 meta.wikilink=true。 */
144
+ wikilink?: boolean;
145
+ };
146
+ export type DslOp = DslCardOp | DslFreeOp | DslArrowOp;
147
+ /** A diagnostic describing a single malformed DSL line that was dropped. */
148
+ export type DslDiagnostic = {
149
+ /** 1-based original line number in the source text. */
150
+ line: number;
151
+ /** The trimmed source line that failed to parse. */
152
+ text: string;
153
+ /** Short technical reason (English, dev-facing — dialog prefixes i18n). */
154
+ message: string;
155
+ };
156
+ /**
157
+ * Parse a DSL block into ops AND per-line diagnostics for dropped lines.
158
+ *
159
+ * 行分类(保留 1-based 原始行号,编辑器告诉用户哪行被丢、为什么 —— 转义核心卖点的反信任问题):
160
+ *
161
+ * - empty / `# comment` / 非 `[` 散文 / 围栏 → 静默 skip(无 error)
162
+ * - `[` 行解析后缺关键字段 → 记 {@link DslDiagnostic}
163
+ *
164
+ * 永不抛错:grammar 本身因尾部 `.*` 对任何 `[` 行都 succeed;极端输入(控制字符等)若仍
165
+ * 抛 → 该行静默跳过(不崩整块)。坐标恒 finite(finiteNum 守卫)。
166
+ */
167
+ export declare function parseDslWithDiagnostics(dslText: string): {
168
+ ops: DslOp[];
169
+ errors: DslDiagnostic[];
170
+ /** 错误型残余软信号(全角标点/未知 @指令/拼错关系词)。恢复型残余(如 #id:1 的 :1)不告警。 */
171
+ warnings: DslDiagnostic[];
172
+ };
173
+ /** grammar 全部 @指令名(来源:dsl.peggy 逐条 Dir 规则的 '@' 字面量,实测 22 个)。
174
+ * 注意 from/to/create/关系词是**裸词**(fromDir='from' sp id 等,不带 @);
175
+ * adversarial.test.ts 与 DSL_RESERVED_GROUP_ANCHOR_WORDS 做双向闭合锁,漂移即炸。 */
176
+ export declare const AT_DIRECTIVES: ReadonlySet<string>;
177
+ /** 保留词表里的裸关键词(非 @ 指令):createDir / relationDir / fromDir / toDir。 */
178
+ export declare const BARE_KEYWORDS: ReadonlySet<string>;
179
+ /**
180
+ * Strict AI mode. User-pasted legacy text keeps using the compatibility parser,
181
+ * while model output must contain only complete directives. Unknown residual
182
+ * chunks and duplicate directives are errors instead of being swallowed by
183
+ * Peggy's compatibility skipChunk rule.
184
+ */
185
+ export declare function parseDslStrictWithDiagnostics(dslText: string): {
186
+ ops: DslOp[];
187
+ errors: DslDiagnostic[];
188
+ };
189
+ /**
190
+ * Parse a DSL block (the AI's output) into a list of layout operations.
191
+ *
192
+ * Graceful: unrecognized lines are skipped (no throw). The DSL is
193
+ * intended for AI-to-machine communication — if the AI produces
194
+ * invalid syntax, we silently ignore bad lines.
195
+ *
196
+ * Thin wrapper over {@link parseDslWithDiagnostics}: returns only the ops
197
+ * (the diagnostics are irrelevant for the AI path). Behavior is unchanged.
198
+ */
199
+ export declare function parseDsl(dslText: string): DslOp[];
@@ -0,0 +1,4 @@
1
+ // Peggy 生成 parser 的类型垫片(dsl-parser.gen.js 由 `pnpm --filter @wscytz/dsl gen` 产出,无内建类型)。
2
+ // parse 的返回值结构对应 dsl.peggy 的 Line 规则;dsl-parser.ts 按 LineResult 联合类型解释。
3
+ // 重生 parser 不必改本文件(签名稳定)。
4
+ export function parse(input: string, options?: { startRule?: string }): unknown