From a4cec3d70863ebf250cf2cb517f44e1b0b2e5b10 Mon Sep 17 00:00:00 2001 From: moranzhi Date: Thu, 24 Sep 2026 00:58:48 +0800 Subject: [PATCH] =?UTF-8?q?=E5=BC=95=E5=85=A5=E6=AD=A3=E6=96=87=E5=AE=89?= =?UTF-8?q?=E5=85=A8=E5=AD=90=E9=9B=86=E5=B9=B6=E6=94=B6=E7=B4=A7=E5=91=88?= =?UTF-8?q?=E7=8E=B0=E5=88=9B=E4=BD=9C=E5=8F=A3=E5=BE=84=EF=BC=8C=E9=81=BF?= =?UTF-8?q?=E5=85=8D=E6=9C=AC=E5=B1=80=E4=BB=BB=E6=84=8F=E8=84=9A=E6=9C=AC?= =?UTF-8?q?=E7=BE=8E=E5=8C=96=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 创作只选壳与分区,渲染与提示共用同一份白名单语法。 Co-authored-by: Cursor --- docs/context-fragment-design.md | 4 +- docs/play-presentation-shells.md | 3 +- docs/progressive-data-design.md | 2 +- docs/world-simulator-modules.md | 6 +- .../markdown-safe-subset/catalog.yaml | 49 +++++++ .../modules/reply-format/prompt.md | 107 +++++++-------- src/skills/markdown-safe-subset.ts | 126 ++++++++++++++++++ tests/markdown-render.test.ts | 38 ++++++ tests/markdown-safe-subset.test.ts | 50 +++++++ web/markdown.js | 79 ++++++++++- 10 files changed, 399 insertions(+), 65 deletions(-) create mode 100644 skills/dialogue/world-simulator/markdown-safe-subset/catalog.yaml create mode 100644 src/skills/markdown-safe-subset.ts create mode 100644 tests/markdown-render.test.ts create mode 100644 tests/markdown-safe-subset.test.ts diff --git a/docs/context-fragment-design.md b/docs/context-fragment-design.md index d51f0bb..e9bc300 100644 --- a/docs/context-fragment-design.md +++ b/docs/context-fragment-design.md @@ -63,11 +63,11 @@ 叙事指南与故事推进(工序编排)正文:一份全文(纲领/遣词/笔墨/禁忌/推进等);**整份**双挂转述与主世界层。artifact=`设计.叙事指南与故事推进`。 对话落盘拆成两份:`设计.叙事指南`(遣词/笔墨/禁忌/写法档)与 `设计.故事推进`(推进 + 用户输入用法);各自双挂,条件独立落盘。 回复呈现:每轮终稿给用户看什么(正文/字数/日期/变量);artifact=`设计.监控栏`(旧称设计监控栏/状态栏)。 -正文组成:把回复呈现清单展示给用户(选壳、美化,可写 CSS/HTML/JS);artifact=`设计.正文组成`。见 `docs/play-presentation-shells.md`。 +正文组成:把回复呈现清单展示给用户(选壳、分区、【正文安全子集】);artifact=`设计.正文组成`。见 `docs/play-presentation-shells.md`。 变量设计:真值 + **维护语句** + **Data映射索引**(指向具体实例)+ side_effects。 变量控制上下文:各槽视野 + **旁观汇总**;mount 含主世界层与旁观维护。 生成规则:合同挂主世界层 + 旁观(旁观读 rule_id/格式/必要性,描写长文可 summary)。 -开场白与开场变量:每条开场 = meta(用户角色+初值)+ 正文(present.v1,壳与正文组成一致);可经 opening-generator 落库。 +开场白与开场变量:开场正文(present.v1,壳与正文组成一致)+ 开场变量初值;用户代入名片优先「主角设定」。可经 opening-generator 落库。 具体实例正文:`rule_id` + `records[]`(始终数组;单层,键随规则;条数服从规则「一次生成」);自评=合规模(只按规则执行)。 diff --git a/docs/play-presentation-shells.md b/docs/play-presentation-shells.md index ec05ffe..173302b 100644 --- a/docs/play-presentation-shells.md +++ b/docs/play-presentation-shells.md @@ -4,7 +4,8 @@ > 上游:游玩管线 `gm → perspective? → narrator → auditor`。 > 创作挂钩:`设计.正文组成`(怎么展示)/ `设计.监控栏`(每轮看什么,技能名「回复呈现」);终稿 tag 通常 `输出.用户展示`。 > 各壳介绍与规格:`skills/dialogue/world-simulator/present-shells/`(正文组成执行时动态注入)。 -> **创作定位:按回复呈现清单选壳并美化,可写 CSS/HTML/JS。** +> **创作定位:按回复呈现清单选壳并分区;正文语法见【正文安全子集】(程序动态装载)。** +> 日常折叠/表格走 Markdown 安全子集;一般不写本局任意 CSS/HTML/JS。 > 预览:`/shells.html` ## 1. 创作怎么做(权威口径) diff --git a/docs/progressive-data-design.md b/docs/progressive-data-design.md index ebe6cf8..1edebcc 100644 --- a/docs/progressive-data-design.md +++ b/docs/progressive-data-design.md @@ -122,7 +122,7 @@ Progressive 投影 本轮主世界层只看见「当前档」;未 | **生成规则 / 具体实例** | 分档表 schema 与行数据(如好感→性格);生成后由变量设计索引 | | **正文组成** | 隐藏段定界(供解析维护语句);用户可见块序 | | **设计监控栏** | 用户可见的会变字段子集 | -| **开场白与开场变量** | 开场正文(present.v1,跟正文组成)+ 与开场同真相的初值 / 用户角色 | +| **开场白与开场变量** | 开场正文(present.v1,跟正文组成)+ 与开场同真相的初值;用户角色优先「主角设定」 | | **游玩拓扑 / 上下文投影排序 / 细化终稿** | 固定槽;扁平投影序;tables 收成 | 编排口诀:体验与机制 → 要不要真值 → 映射表用生成规则/实例 → 维护语句与旁观汇总 → 开场同真相 → 收成。 diff --git a/docs/world-simulator-modules.md b/docs/world-simulator-modules.md index e9edc23..ef5dd6b 100644 --- a/docs/world-simulator-modules.md +++ b/docs/world-simulator-modules.md @@ -172,7 +172,7 @@ recipes/world-simulator|expand-assistant/recipe.yaml | 能力 | id | 状态 | |------|-----|------| | 美学纲领与交互范式 | `aesthetics-interaction` | **范例已写**;前端 mosaic 视图 | -| 主角设定 | `protagonist` | **已写**:有产物则挂在当前选用的用户角色卡下(不进全局列表);关掉这局就不加载 | +| 主角设定 | `protagonist` | **已写**:代入名片(名字/背景/特殊设定);挂在当前选用的用户角色卡下;关掉这局就不加载 | | 实现机制 | `mechanism` | **已写**:`context-fragment.v1`(支撑点正文 + 自评 + 追问);`feeds: gm`;**专用卡已接** | | 舞台骨架 | `world-blueprint` | **已写**:`context-fragment.v1`;社会结构 + 世界状况;**专用卡已接** | | 生成规则 | `generation-rules` | **已写**:`context-fragment.v1`;方向池优先、元素池仅封闭集合;可反复;**〔先验产物〕**;规划字段 `target`;挂 gm+auditor;**专用卡已接** | @@ -183,10 +183,10 @@ recipes/world-simulator|expand-assistant/recipe.yaml | 拓扑图谱 | `topology` | 必须生成且不适合走生成规则→实例时,用拓扑结构写出(升级路径、地图、人物关系等) | | 回复呈现 | `status-bar` | **已写**:每轮终稿看什么(正文/字数/日期/变量);旧称设计监控栏 | | 随机范围整理 | `random-range` | **已写**:汇总检定/对抗等随机项与范围;正文组成之前;替代机遇裁定槽 | -| 正文组成 | `reply-format` | **已写**:把回复呈现清单展示出来;选壳+美化,可写 CSS/HTML/JS;可接隐藏备用随机区 | +| 正文组成 | `reply-format` | **已写**:选壳+分区;正文语法【正文安全子集】动态注入;可接隐藏备用随机区 | | 变量设计与更新规则 | `variable-design` | **已写**;专用/结构化卡 | | 变量控制上下文 | `variable-context` | **已写**:旁观汇总;mount 含 auditor;通用 fragment 卡 | -| 开场白与开场变量 | `opening-setup` | **已写**:每条 = meta(初值/用户角色)+ present.v1 正文(壳跟正文组成);**〔收口〕**选定后落库并保存定稿 | +| 开场白与开场变量 | `opening-setup` | **已写**:每条 = meta(初值)+ present.v1 正文;用户角色优先主角设定;**〔收口〕**选定后落库并保存定稿 | 共用收成(池内保留,按需): diff --git a/skills/dialogue/world-simulator/markdown-safe-subset/catalog.yaml b/skills/dialogue/world-simulator/markdown-safe-subset/catalog.yaml new file mode 100644 index 0000000..83f6041 --- /dev/null +++ b/skills/dialogue/world-simulator/markdown-safe-subset/catalog.yaml @@ -0,0 +1,49 @@ +# 正文 Markdown 安全子集(程序动态装载进「正文组成」) +# 实现:web/markdown.js · 先转义再解析;禁止裸 HTML / 任意 CSS / JS +version: 1 +intro: > + 用户开启 Markdown 渲染时,游玩正文(及壳区域主读)只吃下列语法。 + 美化靠程序预制 class(.md-*),不要写本局任意 CSS/HTML/JS。 + 目录可增条目;技能 prompt 勿背死清单。 +features: + - id: paragraph + name: 段落与换行 + syntax: 空行分段;段内换行保留 + - id: heading + name: 标题 + syntax: "# ~ ###### 标题" + - id: emphasis + name: 强调 + syntax: "**粗** *斜* ~~删~~ `行内代码`" + - id: link + name: 链接 + syntax: "[文案](https://…)(仅 http/https)" + - id: list + name: 列表 + syntax: '- 项 / 1. 项' + - id: quote + name: 引用 + syntax: '> 引用行' + - id: fence + name: 代码块 + syntax: '三反引号围栏代码块' + - id: hr + name: 分隔线 + syntax: '---' + - id: table + name: 表格 + syntax: | + | 列甲 | 列乙 | + | --- | --- | + | 值1 | 值2 | + - id: details + name: 折叠 + syntax: | + :::details 标题 + 折叠正文(可含上表其它语法) + ::: + 默认展开::::details open 标题 +restrictions: + - 禁止裸 HTML 标签、事件属性、script + - 禁止本局任意 CSS / JS(壳微调 tone_chrome 等另走呈现壳) + - 链接协议仅 http / https diff --git a/skills/dialogue/world-simulator/modules/reply-format/prompt.md b/skills/dialogue/world-simulator/modules/reply-format/prompt.md index 706ca9b..fec25e6 100644 --- a/skills/dialogue/world-simulator/modules/reply-format/prompt.md +++ b/skills/dialogue/world-simulator/modules/reply-format/prompt.md @@ -1,7 +1,7 @@ # 正文组成 > 技能文档。程序只切割下方 **fence 块**;`##` 标题仅供人读。 -> **创作定位:把「回复呈现」已钉的内容展示给用户**(选壳、美化排版,可写 CSS/HTML/JS)。壳介绍与规格写在各壳自己的文档,程序动态装载。 +> **创作定位:把「回复呈现」已钉的内容展示给用户**(选壳、分区、用安全子集美化)。壳与【正文安全子集】由程序动态装载。 > 产物外壳:`docs/context-fragment-design.md`。 > 旧称:设计回复格式。 @@ -12,7 +12,7 @@ name: 正文组成 id: reply-format artifact: 设计.正文组成 declaration: > - 把「回复呈现」已钉的内容展示给用户:选壳、美化排版,可写 CSS/HTML/JS;有「随机范围整理」则把隐藏备用随机区接到消息;排在二者之后、开场白之前 + 把「回复呈现」已钉的内容展示给用户:选壳、分区、按【正文安全子集】做折叠/表格等美化;有「随机范围整理」则把隐藏备用随机区接到消息;排在二者之后、开场白之前 when: | 已有「回复呈现」(或等价:每轮终稿看什么已钉)后排。几乎每局都要。 若本局有随机需求:须已有「随机范围整理」(或确认无随机)后再排本步。 @@ -23,7 +23,7 @@ when_not: | 不要排在开场白之后。几乎不跳过。 只谈文风遣词 → 「叙事指南」(工序编排:「叙事指南与故事推进」)。只改呈现区域与内容规则(字数/选项槽/监控键)→ 「回复呈现」。 boundary: | - 本技能:前端展示——从【可选呈现壳】选壳、把回复呈现的各区域落到版式、美化排版(可含 CSS/HTML/JS);若有随机范围表,预留/接到隐藏备用随机区(用户看不见、发给 AI 仍在上下文)。 + 本技能:前端展示——从【可选呈现壳】选壳、把回复呈现的各区域落到版式;正文语法以【正文安全子集】为准(折叠、表格等)。 回复呈现:必选/可选区域 + 各区简单内容规则——本步不重议清单与规则,只负责怎么显示。 随机范围整理:要插哪些数、范围是什么——本步不改范围表,只负责怎么藏进消息。 叙事指南:文风。变量设计:真值规则。开场白:第一屏——本步先定展示。 @@ -33,11 +33,11 @@ feeds: narrator ## opening ```opening -本局把「回复呈现」里已钉的内容展示给用户(选壳、美化排版)。壳清单由程序动态装载,不要背死几种。 +本局把「回复呈现」里已钉的内容展示给用户(选壳、分区、安全子集美化)。壳与安全子集由程序动态装载,不要背死。 1. 对照【可选呈现壳】:哪一个最适合放下已钉的那些东西? -2. 各项落到壳的哪一区?显示名叫什么? -3. 要不要额外的 CSS/HTML/JS 来美化(没有就说没有)? +2. 各项落到壳的哪一区?显示名叫什么?渲染提示用纯文本还是 Markdown? +3. 需要折叠/表格等时,对照【正文安全子集】(一般够用;html/css/js 默认空)。 4. 定稿时会用前面情节架空一幕灌进壳里,专供你看版式/美化(内容随便,不当正史)。 ``` @@ -46,17 +46,18 @@ feeds: narrator ```task 你正在执行「正文组成」。产物必须是 **context-fragment.v1** JSON,写入「设计.正文组成」。 -本步 = **前端展示**:按「回复呈现」的清单,从【可选呈现壳】里选一个,把内容落到区域并美化排版。可写本局需要的 CSS/HTML/JS。 -禁止重议「看什么、正文多少字」;禁止自造目录没有的 shell_id。 +本步 = **前端展示**:按「回复呈现」的清单,从【可选呈现壳】里选一个,把内容落到区域。 +正文美化优先【正文安全子集】(用户开 Markdown 渲染时生效)。日常折叠、表格等走子集即可;「渲染」里 html/css/js 默认空字符串。 +禁止重议「看什么、正文多少字」;禁止自造目录没有的 shell_id;禁止发明子集没有的语法。 执行顺序: 1. 读「回复呈现」与美学/叙事:清单是权威;不重开「看什么」问卷。 2. **选 shell_id**(必须是【可选呈现壳】里的 id);说明为何这个壳放得下清单里的项。 -3. 可见块:把清单各项挂到该壳已有区域;写显示名与职责。 -4. 需要时写「渲染」:本局 CSS/HTML/JS 片段(美化排版,不是新后端协议)。 +3. 可见块:把清单各项挂到该壳已有区域;写显示名与职责;渲染提示选「纯文本」或「Markdown」。 +4. 「渲染」html/css/js:默认全空;仅当用户点名要超出安全子集的交互时再议(本步通常不写)。 5. 隐藏段(若清单要求):定界与用途。 6. 填「呈现壳.微调」;分区必须与壳一致。 -7. **示例**:用前面已定情节架空一幕灌进 `present.v1`,专供验收版式/美化;字数以回复呈现为准,本步不重钉篇幅。 +7. **示例**:用前面已定情节架空一幕灌进 `present.v1`;body 可演示子集语法(如折叠/表格);字数以回复呈现为准。 8. 自评 + 追问;禁止写 context `order`。 summary:`正文组成 · 适配 {shell_id} · …`。 @@ -66,14 +67,14 @@ summary:`正文组成 · 适配 {shell_id} · …`。 ```principles 1. 创作 = 把「回复呈现」的清单展示出来 - 选壳、分区、显示名、CSS/HTML/JS 都为那份清单服务。本步验收重点是「看起来对不对」。 + 选壳、分区、显示名、安全子集语法都为那份清单服务。本步验收重点是「看起来对不对」。 「按意见修改」:以最新产物为底;用户要求按先后都有效,冲突时以较新的为准。用户说整体可以、或只提美化 → 不换 shell_id。换壳必须用户点名。 2. 清单权威在「回复呈现」 正文篇幅、要不要日期、展示哪些变量,以那份产物为准。本步只决定怎么显示。 -3. 壳是底,渲染可加 - 从【可选呈现壳】选底。需要时写本局 CSS/HTML/JS 做美化;不要发明新的后端协议或目录没有的壳。 +3. 壳是底,子集是语法 + 从【可选呈现壳】选底;Markdown 区只使用【正文安全子集】。不要发明新后端协议、目录没有的壳、或子集没有的语法。 4. 隐藏段服务维护,不污染阅读 @@ -90,8 +91,8 @@ summary:`正文组成 · 适配 {shell_id} · …`。 ```probe 每轮 1~2 点。 -优先:壳能否放下回复呈现的各项;分区与显示名;要不要 CSS/HTML/JS。 -不追问:重议字数/变量清单、事件池 schema、文风遣词、示例文笔好坏。 +优先:壳能否放下回复呈现的各项;分区与显示名;渲染提示纯文本还是 Markdown;要不要子集里的折叠/表格。 +不追问:重议字数/变量清单、事件池 schema、文风遣词、示例文笔好坏、本局任意 CSS/JS。 ``` ## output @@ -100,46 +101,38 @@ summary:`正文组成 · 适配 {shell_id} · …`。 { "schema": "context-fragment.v1", "技能": "正文组成", - "brief": "一句话:壳 + 主要块序", + "brief": "一句话:选了哪壳、关键区怎么挂", "mount": ["narrator"], "稳变": "stable", "正文": { - "依据的体验与呈现": [ - "从回复呈现继承的清单 + 美学/叙事呈现要点" - ], + "依据的体验与呈现": { + "回复呈现要点": "从设计.监控栏摘:必选区 + 字数等", + "美学或叙事可映到版式的一句": "可空" + }, "呈现壳": { - "shell_id": "必须与【可选呈现壳】某一 id 一致", - "为何选它": "一句话", + "shell_id": "必须是【可选呈现壳】里的 id", + "为何选它": "对照回复呈现要同时看见什么", "微调": { - "tone_chrome": "messenger|terminal|book|default", - "show_suggested_actions": true, + "monitor_fields": [], "block_labels": {}, + "show_suggested_actions": true, + "tone_chrome": "default|messenger|book|terminal", "empty_states": {} } }, - "版式隐喻": "给人读的比喻;程序以 shell_id 为准", + "版式隐喻": "可空;一句气质,不是新壳", "可见块": [ { - "块id": "英文 kebab 或中文短名,同局稳定", - "区域": "monitor|header|body|footer|aside", - "显示名": "用户可理解的标题;可无标题则空", - "职责": "这块给用户什么信息", - "顺序": 1, - "可空": true, - "填充方": "叙事转述|程序拼装|回复呈现变量|其它", - "内容形态": "散文|短列表|键值行|其它", - "示例": "可选:该块一句占位(总预览见正文.示例)" + "块id": "与回复呈现一致的短名", + "挂到区域": "monitor|header|body|footer|aside", + "显示名": "可空", + "职责": "一句", + "来源提示": "转述填|程序填|混合" } ], - "呈现清单落实": { - "依据的回复呈现": "brief 或要点", - "各项落到": [ - { - "清单项": "正文|日期|某变量|建议行动|…", - "区域": "monitor|header|body|footer|aside|hidden", - "显示名": "可空" - } - ] + "监控栏锚点": { + "启用": true, + "字段": ["与回复呈现一致;无则 []"] }, "隐藏段": [ { @@ -159,20 +152,20 @@ summary:`正文组成 · 适配 {shell_id} · …`。 "区域id": "monitor|header|body|footer|aside", "对应块id": ["…"], "意图": "固定顶栏|主滚动|折叠文末|…", - "渲染提示": "纯文本|Markdown|自定义" + "渲染提示": "纯文本|Markdown" } ], "渲染": { - "html": "可选:本局结构片段;无则空字符串", - "css": "可选:本局样式;无则空字符串", - "js": "可选:本局交互;无则空字符串" + "html": "", + "css": "", + "js": "" }, - "说明": "无分区则写「单流渲染」(通常 prose)" + "说明": "Markdown 区语法见【正文安全子集】;渲染三字段默认空。无分区则写「单流渲染」(通常 prose)" }, "拼装与终稿": { "终稿tag": "通常 输出.用户展示", "拼装方": "叙事转述一次写出各块|程序按块拼接|混合", - "推荐灌数": "分块壳优先 present.v1;prose 可为纯 Markdown", + "推荐灌数": "分块壳优先 present.v1;prose 可为纯 Markdown(安全子集)", "单轮正文篇幅": "以回复呈现的字数为准,本步不重钉", "与正文关系": "转述读主世界「输出.用户展示」再填各块;可补残稿与版式,不改剧情;隐藏段可供旁观解析维护语句" }, @@ -186,7 +179,7 @@ summary:`正文组成 · 适配 {shell_id} · …`。 "blocks": { "monitor": "若该壳有 monitor:短键值;否则省略", "header": "若有则短字;无则省略", - "body": "架空主读文(短段即可,勿写成章)", + "body": "架空主读文;需要时可演示 :::details / 表格(短段即可)", "aside": "若有则短线索/备注;无则省略", "footer": "若有则可空或一句" }, @@ -211,7 +204,7 @@ summary:`正文组成 · 适配 {shell_id} · …`。 { "名": "可实现", "分数": 8, - "说明": "shell_id 是否在【可选呈现壳】内;分区与隐藏段是否可交给转述/程序" + "说明": "shell_id 是否在【可选呈现壳】内;Markdown 是否只用【正文安全子集】" }, { "名": "版式可感", @@ -241,7 +234,7 @@ summary:`正文组成 · 适配 {shell_id} · …`。 3. `可见块` 至少 1 条(通常含 body);禁止自造区域 id / 目录没有的壳。 4. 正文须含 `示例`:`性质` 标明非正史;`灌数` 为 `present.v1`,`shell` 与本步 shell_id 一致;只填该壳已有区域。 5. 示例内容可糙:只为验收壳与美化;禁止当开场白/正史;禁止为示例新造设定。 -6. 禁止程序 API schema、context `order`。需要美化时写本局 CSS/HTML/JS,不要另起后端协议。 +6. 禁止程序 API schema、context `order`。Markdown 区只使用【正文安全子集】;渲染 html/css/js 默认空。 7. 自评:可见完备 / 清单落实 / 可实现 / 版式可感。 8. summary:`正文组成 · 适配 {shell_id} · {brief 缩略}`。 9. 兼容旧称产物 tag「设计.回复格式」。 @@ -252,7 +245,7 @@ summary:`正文组成 · 适配 {shell_id} · …`。 - [ ] 是把「回复呈现」的清单展示出来,而不是重议看什么? - [ ] shell_id 在【可选呈现壳】内?可见块未超出该壳分区? - [ ] 清单各项已落到区域?字数未在本步另定一套? -- [ ] 隐藏段(若有)定界清楚?CSS/HTML/JS 只服务本局展示? +- [ ] Markdown 区语法落在【正文安全子集】内?渲染三字段默认空? - [ ] 有示例灌数(present.v1),壳一致、各区撑开,且标明非正史? - [ ] 未把示例写成开场白或打磨成长文? ``` @@ -261,13 +254,15 @@ summary:`正文组成 · 适配 {shell_id} · …`。 ```examples 好: -- 回复呈现要正文+好感:选 chat_monitor,好感进 monitor,正文进 body;可加短 CSS 美化芯片。 -- 回复呈现只要正文:选 prose;渲染可空。 -- 回复呈现要选项:选 choice_dock,示例 meta 带占位行动。 +- 回复呈现要正文+好感:选 chat_monitor,好感进 monitor,正文进 body;渲染提示 Markdown。 +- 需要折叠说明:body 用 :::details 标题 … :::(见安全子集)。 +- 需要对照表:body 用 Markdown 表格。 +- 回复呈现只要正文:选 prose;渲染三字段空。 坏: - 本步重钉「正文 300 字」而回复呈现已经写了 800~1500。 - 不写 shell_id,只写「要好看的 UI」。 - 示例写成第一章正史,或为写好看新造未定设定。 - 示例 shell 与呈现壳不一致,或灌了壳没有的区域。 +- 写本局任意 CSS/JS,却只是要折叠或表格。 ``` diff --git a/src/skills/markdown-safe-subset.ts b/src/skills/markdown-safe-subset.ts new file mode 100644 index 0000000..fcd4a02 --- /dev/null +++ b/src/skills/markdown-safe-subset.ts @@ -0,0 +1,126 @@ +/** + * 正文 Markdown 安全子集目录。 + * 正文组成执行时注入【正文安全子集】,与 web/markdown.js 实现对照维护。 + */ +import { readFile } from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { parse as parseYaml } from "yaml"; + +const DEFAULT_SKILLS_ROOT = path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + "../../skills", +); + +export const MARKDOWN_SAFE_SUBSET_FILENAME = + "markdown-safe-subset/catalog.yaml"; + +export type MarkdownSafeFeature = { + id: string; + name: string; + syntax: string; +}; + +export type MarkdownSafeSubsetCatalog = { + version: number; + intro: string; + features: MarkdownSafeFeature[]; + restrictions: string[]; +}; + +function asString(v: unknown): string { + return typeof v === "string" ? v.trim() : ""; +} + +export function parseMarkdownSafeSubsetCatalog( + raw: string, +): MarkdownSafeSubsetCatalog | null { + let doc: unknown; + try { + doc = parseYaml(raw); + } catch { + return null; + } + if (!doc || typeof doc !== "object" || Array.isArray(doc)) return null; + const row = doc as Record; + const featuresRaw = row.features; + if (!Array.isArray(featuresRaw)) return null; + const features: MarkdownSafeFeature[] = []; + for (const item of featuresRaw) { + if (!item || typeof item !== "object" || Array.isArray(item)) continue; + const f = item as Record; + const id = asString(f.id); + const name = asString(f.name); + const syntax = asString(f.syntax); + if (!id || !name || !syntax) continue; + features.push({ id, name, syntax }); + } + if (!features.length) return null; + const restrictions: string[] = []; + if (Array.isArray(row.restrictions)) { + for (const r of row.restrictions) { + const s = asString(r); + if (s) restrictions.push(s); + } + } + const version = + typeof row.version === "number" && Number.isFinite(row.version) + ? row.version + : 1; + return { + version, + intro: asString(row.intro), + features, + restrictions, + }; +} + +export async function loadMarkdownSafeSubsetCatalog( + skillPackRoot: string, + skillsRoot = DEFAULT_SKILLS_ROOT, +): Promise { + const catalogPath = path.join( + skillsRoot, + skillPackRoot, + MARKDOWN_SAFE_SUBSET_FILENAME, + ); + let raw: string; + try { + raw = await readFile(catalogPath, "utf8"); + } catch { + return null; + } + return parseMarkdownSafeSubsetCatalog(raw); +} + +export function formatMarkdownSafeSubsetForPrompt( + catalog: MarkdownSafeSubsetCatalog, +): string { + const lines = [ + "## 【正文安全子集】程序从仓库动态装载", + "", + catalog.intro || + "用户开启 Markdown 渲染时,正文只吃下列语法;美化靠程序 class,勿写本局任意 CSS/HTML/JS。", + "", + ]; + for (const f of catalog.features) { + lines.push(`- **${f.name}**(\`${f.id}\`)`); + const syn = f.syntax.trim(); + if (syn.includes("\n")) { + lines.push("", "```", syn, "```", ""); + } else { + lines.push(` \`${syn}\``); + } + } + if (catalog.restrictions.length) { + lines.push("", "硬限制:"); + for (const r of catalog.restrictions) { + lines.push(`- ${r}`); + } + } + lines.push( + "", + "区域「渲染提示」选 Markdown 时,只使用上表;日常折叠/表格走子集即可,一般不必填「渲染」里的 html/css/js。", + ); + return lines.join("\n").trim(); +} diff --git a/tests/markdown-render.test.ts b/tests/markdown-render.test.ts new file mode 100644 index 0000000..013bd58 --- /dev/null +++ b/tests/markdown-render.test.ts @@ -0,0 +1,38 @@ +import { describe, expect, it } from "vitest"; +import { renderMarkdownToHtml } from "../web/markdown.js"; + +describe("markdown safe subset render", () => { + it("renders GFM tables", () => { + const html = renderMarkdownToHtml( + ["| 甲 | 乙 |", "| --- | --- |", "| 1 | **粗** |"].join("\n"), + ); + expect(html).toContain(''); + expect(html).toContain("
"); + expect(html).toContain(""); + expect(html).toContain("粗"); + expect(html).not.toContain("| 甲 |"); + }); + + it("renders details folds", () => { + const html = renderMarkdownToHtml( + [":::details 规则说明", "里面有 *斜体*", ":::"].join("\n"), + ); + expect(html).toContain('
'); + expect(html).toContain(''); + expect(html).toContain("规则说明"); + expect(html).toContain("斜体"); + }); + + it("supports open details", () => { + const html = renderMarkdownToHtml( + [":::details open 已展开", "内容", ":::"].join("\n"), + ); + expect(html).toContain("
"); + }); + + it("escapes raw html tags", () => { + const html = renderMarkdownToHtml(""); + expect(html).not.toContain("