@olierjs/fugue-cli 0.1.2 → 0.1.3

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/README.md CHANGED
@@ -52,6 +52,8 @@ $fugue-show-me 检查取消与重试交错时的状态变化,画出可能的
52
52
 
53
53
  需要可视化解释时,总览路由到 [fugue-show-me](skills/fugue-show-me/SKILL.md):由 `show-me` 选择通用表达形式,需要分析状态变化、检查行为规则或展示缺陷路径时配合 `statechart`。适配维护在独立目录中;更新上游时整体替换原版目录并更新来源记录。
54
54
 
55
+ `fugue-show-me` 根据这些方法组织全局视图和必要的交互、状态细节,再通过本层的 [Pele 输出脚本](skills/fugue-show-me/scripts/render-html.mjs) 将 Mermaid 统一展示为单文件 HTML。图在正文直接可见,采用 Flexoki 浅色与深色主题,按信息角色分色,支持自适应和缩放。首次生成页面会将 `pele@1.12.0` 安装到独立用户缓存,生成的页面可离线打开;该展示链路不导入或修改上游的内部渲染模块。
56
+
55
57
  ## 状态图依赖
56
58
 
57
59
  `statechart` 使用 Node.js 和 npm;模型检查还需要 [mise](https://mise.jdx.dev/getting-started.html)。macOS 可先安装:
@@ -75,7 +77,7 @@ node ~/.agents/skills/statechart/scripts/statechart.mjs --help
75
77
 
76
78
  ```sh
77
79
  npm pack
78
- npm install -g ./olierjs-fugue-cli-0.1.2.tgz
80
+ npm install -g ./olierjs-fugue-cli-0.1.3.tgz
79
81
  fugue-cli --version
80
82
  fugue-cli guide
81
83
  ```
@@ -94,7 +96,7 @@ npm pack
94
96
  `npm pack` 的 `prepack` 自动构建各平台二进制,并将版本写入命令。发布目标固定为 npmjs,包名为 `@olierjs/fugue-cli`。发布验收后的包:
95
97
 
96
98
  ```sh
97
- npm publish ./olierjs-fugue-cli-0.1.2.tgz --access public
99
+ npm publish ./olierjs-fugue-cli-0.1.3.tgz --access public
98
100
  ```
99
101
 
100
102
  Go 命令入口位于 [cmd/fugue-cli](cmd/fugue-cli/main.go),指南源码位于 [guide.md](internal/cli/guide.md),入口技能位于 [SKILL.md](skills/fugue-stack/SKILL.md)。
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@olierjs/fugue-cli",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Fugue 需求交付工作指南与 Agent 技能",
5
5
  "type": "module",
6
6
  "bin": { "fugue-cli": "bin/fugue-cli.mjs" },
@@ -1,16 +1,44 @@
1
1
  ---
2
2
  name: fugue-show-me
3
- description: 使用 show-me 和 statechart 可视化解释当前实现、代码变更、运行结果或状态变化;当用户需要图解、结构示意或缺陷路径时使用。
3
+ description: 组合 show-me 与 statechart 的分析视角,用 Pele 将 Mermaid 展示为可读的 HTML 图解;解释实现、代码变更、运行结果、状态变化或缺陷路径时使用。
4
4
  ---
5
5
 
6
6
  # 可视化解释
7
7
 
8
- 先阅读并遵循[上游 show-me](../show-me/SKILL.md),由它指导表达形式的选择。
8
+ 上游 skill 保持独立。本层负责按问题选择分析方法、组织面向人的视图,再用 Pele 统一展示;不修改上游,也不调用它们的内部渲染模块。
9
9
 
10
- 需要分析状态转换、并发、重试、取消、恢复或缺陷路径,并检查行为规则、对比修复时,阅读并遵循[上游 statechart](../statechart/SKILL.md)的建模、模型检查、出图与交付要求。用 `show-me` 解释整体结构,用 `statechart` 展示状态变化和缺陷路径,按当前问题需要组合。
10
+ ## 获取信息与组织视图
11
11
 
12
- 原版脚本安装的 TLC 使用 ASCII 模型标识符和字符串值,中文状态与步骤标签写在映射文件中,再从模型生成中文图示。
12
+ 先阅读并遵循[上游 show-me](../show-me/SKILL.md),由它指导表达形式的选择。技能提供分析方法,事实来自当前源码、差异、文档和实际运行结果。
13
13
 
14
- 说明使用用户的语言,依据当前源码、差异或已有材料,只展示本次问题需要的内容。
14
+ | 问题需要的视角 | 方法与信息 | 适合的图 |
15
+ | --- | --- | --- |
16
+ | 系统全貌、职责边界、数据流 | 按 `show-me` 从材料中提取组件、输入输出与关系 | 流程图、分组结构图 |
17
+ | 谁先做什么、谁调用谁 | 按 `show-me` 跟随真实调用或事件顺序 | 时序图 |
18
+ | 状态、并发、重试、取消、恢复、缺陷路径 | 阅读并遵循[上游 statechart](../statechart/SKILL.md),获取模型状态、转换、规则、检查结果和复现证据 | 上游生成的状态图与缺陷路径图 |
19
+
20
+ 按问题选用必要的方法;普通结构解释不强制建模。任务中其他已选技能的输出也可作为材料,使用前核对其依据和适用范围。
21
+
22
+ 模型负责组织信息:统一不同材料的对象名称、版本和粒度;区分已确认行为、模型假设与待确认关系。涉及多个组件或分支时先给出边界和主路径的总览,再按问题展开关键交互或状态细节。一张图回答一个问题,避免把全部实现细节堆进总览;说明使用用户的语言,标签优先写读者关心的行为。
23
+
24
+ 图中按职责或状态区分颜色,沿用 [Pele 展示说明](references/pele.md)中的颜色角色;由模型明确标注输入、处理、判断、外部交互、完成、失败和跳过,保留对应文字标签。不要按节点顺序轮流染色,也不让脚本从节点名称猜业务含义。浅色和深色使用同一套颜色含义,原模型的规则结果和缺陷路径标记优先。
25
+
26
+ 使用 `statechart` 时保留上游的建模、模型检查、复现及交付要求。原版 TLC 使用 ASCII 模型标识符和字符串值,中文状态与步骤标签写在映射文件中。将它生成的 `.mmd` 直接交给展示层,保留状态、边、步骤编号和缺陷标记;另画的简洁总览不能替代经检查的模型图,Pele 的渲染成功也不证明行为正确。
27
+
28
+ ## 用 Pele 交付图示
29
+
30
+ 选择 Mermaid 表达并需要 HTML 时,用本层的 [Pele 输出脚本](scripts/render-html.mjs) 渲染。输入可以同时包含模型编写的总览、时序图和 `statechart` 生成的状态图,顺序就是页面阅读顺序。用易懂的文件名作为各图标题。
31
+
32
+ ```bash
33
+ node <本 skill 目录>/scripts/render-html.mjs --title "发布流程说明" --out /绝对路径/说明.html /绝对路径/全局视图.mmd /绝对路径/状态流转.mmd
34
+ ```
35
+
36
+ 脚本通过 Pele 的公开 API 生成内嵌 SVG,并将 Pele 的浏览器包一并嵌入 HTML,用它提供自适应、中文字体测量和缩放。采用官方站点的 Flexoki 浅色与深色配色,页面内可切换,`--theme light|dark` 指定初始主题。首次安装固定版本依赖,后续复用本层的独立缓存;页面打开时无需联网或启动服务。集成到已有 HTML、调整主题或定位兼容问题时,阅读 [Pele 展示说明](references/pele.md)。
37
+
38
+ 核心图放在对应说明旁、默认可见,简短说明补充结论、范围与必要证据。源码和模型检查材料可放附录,下载入口仅作补充。在支持 Mermaid 的对话或文档中可直接使用会渲染的 `mermaid` 代码块;表达形式更适合树、伪代码或表格时,继续遵循 `show-me`,无需强制转成图。
39
+
40
+ ## 交付检查
41
+
42
+ 打开最终交付的 HTML 或图片,检查实际图示;不能用“文件已生成”、HTML 源码或下载链接存在代替检查。HTML 检查桌面和窄屏,确认核心图已显示、中文标签及连线可读、大图可查看完整内容;本地静态页面还需确认离线可显示。渲染失败就修复后重新检查,不把下载源码当成完成。无法预览时明确说明未验证的范围。
15
43
 
16
44
  将上游的 `Bash(open ...)` 对应为当前宿主可用的终端或文件预览工具,按实际操作系统打开 HTML 或图片。交付本地文件时提供绝对路径链接;仅在实际打开成功后说明已打开。
@@ -0,0 +1,77 @@
1
+ /* Flexoki light/dark palettes from https://pele.run/theming and the official site. */
2
+ :root {
3
+ color-scheme: light;
4
+ --bg: #fffcf0; --surface: #f2f0e5; --surface-alt: #e6e4d9;
5
+ --text: #100f0f; --muted: #6f6e69; --border: #cecdc3; --accent: #205ea6;
6
+ --page-muted: #575653; --link: #205ea6;
7
+ --role-input: #24837b; --role-process: #205ea6; --role-decision: #ad8301;
8
+ --role-external: #5e409d; --role-success: #66800b; --role-failure: #af3029; --role-skipped: #6f6e69;
9
+ --role-strength: 18%;
10
+ --pele-series-1: #af3029; --pele-series-2: #bc5215; --pele-series-3: #ad8301; --pele-series-4: #66800b;
11
+ --pele-series-5: #24837b; --pele-series-6: #205ea6; --pele-series-7: #5e409d; --pele-series-8: #a02f6f;
12
+ }
13
+ html[data-theme="dark"] {
14
+ color-scheme: dark;
15
+ --bg: #100f0f; --surface: #1c1b1a; --surface-alt: #282726;
16
+ --text: #cecdc3; --muted: #878580; --border: #403e3c; --accent: #4385be;
17
+ --page-muted: #878580; --link: #66a0c8;
18
+ --role-input: #3aa99f; --role-process: #4385be; --role-decision: #d0a215;
19
+ --role-external: #8b7ec8; --role-success: #879a39; --role-failure: #d14d41; --role-skipped: #878580;
20
+ --role-strength: 24%;
21
+ --pele-series-1: #d14d41; --pele-series-2: #da702c; --pele-series-3: #d0a215; --pele-series-4: #879a39;
22
+ --pele-series-5: #3aa99f; --pele-series-6: #4385be; --pele-series-7: #8b7ec8; --pele-series-8: #ce5d97;
23
+ }
24
+ * { box-sizing: border-box; }
25
+ body { margin: 0; background: var(--surface); color: var(--text); font: 16px/1.65 var(--font); }
26
+ main { max-width: 1200px; margin: auto; padding: 40px 28px; }
27
+ .page-heading { display: flex; justify-content: space-between; align-items: start; gap: 20px; }
28
+ h1 { font-size: clamp(26px,4vw,38px); line-height: 1.3; margin: 0 0 12px; }
29
+ h2 { font-size: 20px; margin: 0 0 16px; }
30
+ header p { color: var(--page-muted); font-size: 14px; }
31
+ .theme-controls { display: flex; flex-shrink: 0; padding: 3px; gap: 3px; border: 1px solid var(--border); border-radius: 9px; background: var(--bg); }
32
+ .theme-controls button { cursor: pointer; font: inherit; font-size: 13px; min-height: 36px; padding: 5px 14px; border: 0; border-radius: 6px; background: transparent; color: var(--muted); }
33
+ .theme-controls button[aria-pressed="true"] { background: var(--surface-alt); color: var(--text); font-weight: 600; }
34
+ nav { display: flex; flex-wrap: wrap; gap: 10px 20px; margin: 20px 0; }
35
+ a { color: var(--link); text-decoration: underline; text-underline-offset: 4px; overflow-wrap: anywhere; }
36
+ .legend { display: flex; flex-wrap: wrap; gap: 10px 22px; padding: 0; margin: 22px 0 32px; list-style: none; font-size: 13px; color: var(--page-muted); }
37
+ .legend li { display: inline-flex; align-items: center; gap: 8px; }
38
+ .legend i { width: 12px; height: 12px; border-radius: 3px; background: var(--role-color); }
39
+ section { min-width: 0; margin: 24px 0; padding: 24px; background: var(--bg); border: 1px solid var(--border); border-radius: 12px; }
40
+ .diagram {
41
+ width: 100%; min-width: 0;
42
+ --pele-bg: var(--bg); --pele-fg: var(--text); --pele-fg-on-light: #100f0f; --pele-fg-on-dark: #fffcf0;
43
+ --pele-muted: var(--muted); --pele-line: var(--muted); --pele-surface: var(--surface);
44
+ --pele-surface-alt: var(--surface-alt); --pele-border: var(--border); --pele-accent: var(--accent);
45
+ --pele-critical: var(--role-failure); --pele-error: var(--role-failure); --pele-font: var(--font);
46
+ }
47
+ .diagram > svg { display: block; max-width: 100%; height: auto; margin-inline: auto; }
48
+ .fugueInput { --role-color: var(--role-input); }
49
+ .fugueProcess { --role-color: var(--role-process); }
50
+ .fugueDecision { --role-color: var(--role-decision); }
51
+ .fugueExternal { --role-color: var(--role-external); }
52
+ .fugueSuccess { --role-color: var(--role-success); }
53
+ .fugueFailure { --role-color: var(--role-failure); }
54
+ .fugueSkipped { --role-color: var(--role-skipped); }
55
+
56
+ /* Neutral model states follow the theme; rule outcomes keep their source colors. */
57
+ .diagram .pele-state:is(.works,.worksPath) > :is(rect,path,circle,ellipse,polygon) { fill: var(--surface) !important; }
58
+ .diagram .pele-state:is(.works,.worksPath) .pele-label { fill: var(--text) !important; }
59
+
60
+ /* Roles are explicit Mermaid classes, never guessed from labels or source order. */
61
+ .diagram .pele-node:is(.fugueInput,.fugueProcess,.fugueDecision,.fugueExternal,.fugueSuccess,.fugueFailure,.fugueSkipped):not([class*="wrong" i]):not([class*="fixed" i]):not([class*="mixed" i]):not([class*="after" i]):not([class*="worse" i]) > :is(rect,path,circle,ellipse,polygon) {
62
+ fill: color-mix(in srgb, var(--role-color) var(--role-strength), var(--bg)) !important;
63
+ stroke: var(--path-border, var(--role-color)) !important;
64
+ }
65
+ .diagram .pele-node:is(.fugueInput,.fugueProcess,.fugueDecision,.fugueExternal,.fugueSuccess,.fugueFailure,.fugueSkipped):not([class*="wrong" i]):not([class*="fixed" i]):not([class*="mixed" i]):not([class*="after" i]):not([class*="worse" i]) .pele-label { fill: var(--text) !important; }
66
+
67
+ /* Blue path borders remain blue, with their original width and step numbers. */
68
+ .diagram .pele-state[class*="Path"] { --path-border: var(--role-process); }
69
+ .diagram .pele-state[class*="Path"] > :is(rect,path,circle,ellipse,polygon) { stroke: var(--role-process) !important; }
70
+ a:focus-visible,button:focus-visible,.diagram:focus-visible { outline: 2px solid var(--accent); outline-offset: 4px; }
71
+ noscript { color: var(--page-muted); }
72
+ @media(max-width:640px) {
73
+ main { padding: 24px 14px; }
74
+ .page-heading { flex-direction: column; gap: 8px; }
75
+ section { padding: 16px 10px; }
76
+ h2 { font-size: 18px; }
77
+ }
@@ -0,0 +1,68 @@
1
+ # Pele 展示层
2
+
3
+ Pele 只负责把 Mermaid 画成 SVG。模型决定要解释什么、采用哪个视角和保留哪些关系;源码、运行结果与上游检查结果提供事实依据。展示层不重新判断这些事实。
4
+
5
+ ## 生成单文件 HTML
6
+
7
+ ```bash
8
+ node <本 skill 目录>/scripts/render-html.mjs \
9
+ --title "取消与恢复的实现" \
10
+ --out /绝对路径/说明.html \
11
+ /绝对路径/全局视图.mmd \
12
+ /绝对路径/调用顺序.mmd \
13
+ /绝对路径/状态与缺陷.mmd
14
+ ```
15
+
16
+ 仅传入本次解释需要的图,不要求凑齐示例中的三种视角。输入与产物放在目标源码仓库之外,除非用户明确要求保存到仓库。
17
+
18
+ 脚本使用 `pele@1.12.0`,首次通过 npm 安装到用户缓存目录的 `fugue-show-me/pele-1.12.0`,遵循 `XDG_CACHE_HOME`,默认在 `~/.cache` 下。缓存独立于 `statechart`;不读取上游缓存、导入上游私有模块或依赖上游 PNG 渲染实现。
19
+
20
+ 所有输入先调用 `render()` 检查并生成 SVG,成功后才写 HTML;错误会使命令失败,不生成仅剩下载入口的页面。HTML 同时内嵌 SVG、Mermaid 源码和 Pele 的浏览器包。浏览器通过 `mount()` 使用实际字体和容器宽度布局,缩放、拖动、复位及键盘操作复用 Pele 自带能力。
21
+
22
+ 打开后查看每张图,而不是仅检查命令成功。浏览器发生绘图错误时,Pele 会在图旁显示错误;这仍是不完整的交付,需要修复并重新检查。
23
+
24
+ ## 主题与颜色含义
25
+
26
+ 使用 Pele 官方站点的 Flexoki 浅色与深色配色:浅色为暖白 `#fffcf0`,深色为墨黑 `#100f0f`。页面内提供切换按钮;命令加 `--theme dark` 可指定初始深色,默认浅色。颜色通过同一份 CSS 变量切换,不重新布局图,也不改变缩放位置。
27
+
28
+ | 信息角色 | Mermaid 类名 | 色系 |
29
+ | --- | --- | --- |
30
+ | 输入、上下文与事实 | `fugueInput` | 青色 |
31
+ | 程序处理步骤 | `fugueProcess` | 蓝色 |
32
+ | 判断、确认与等待 | `fugueDecision` | 黄色 |
33
+ | 外部系统交互 | `fugueExternal` | 紫色 |
34
+ | 完成与目标达成 | `fugueSuccess` | 绿色 |
35
+ | 失败与阻断 | `fugueFailure` | 红色 |
36
+ | 跳过与未执行 | `fugueSkipped` | 灰色 |
37
+
38
+ 流程图用标准 Mermaid 类标注,类的主题样式由本层 HTML 提供:
39
+
40
+ ```mermaid
41
+ flowchart LR
42
+ input[任务事实]:::fugueInput --> check{来源是否完整}:::fugueDecision
43
+ check -->|完整| run[执行处理]:::fugueProcess
44
+ check -->|缺失| stop[失败并保留事实]:::fugueFailure
45
+ run --> done[交付结果]:::fugueSuccess
46
+ ```
47
+
48
+ 状态图在展示副本中增加 `class s0 fugueInput` 这样的颜色角色即可,保留原 `class`、状态、边、编号和规则标记。已标记的错误或修复状态保持原有结果配色;缺陷路径继续使用蓝框及原有线宽。不得为了颜色修改模型或把失败状态改成通过。
49
+
50
+ 时序图用标准 `box rgba(...) 分组名` 区分参与方,透明填色能与浅、深背景叠合;分组名写明职责。需要突出某段交互时可使用 `rect rgba(...)`。采用上表的对应色系,不按参与方顺序随机分色。
51
+
52
+ 页面图例只列出图中实际使用的颜色角色。颜色增强信息分组,文字标签继续承载含义。两种主题均需检查实际节点填色、标签和连线,测量文字与其真实背景的对比度。
53
+
54
+ 配色来源:[官方站点 Flexoki 主题定义](https://github.com/obsidianmd/pele/blob/bdbad5df4dea13b9a14a83a92aed62c4769e49f5/website/src/styles/pele-theme.scss)。
55
+
56
+ ## 集成到已有 HTML
57
+
58
+ 可以调整生成页面的布局、说明和 CSS,保留每张图的容器、源码数据与初始化脚本。也可按以下原则自行组装:
59
+
60
+ - 使用 Pele 的公开 `render()` / `mount()` API;浏览器单文件使用 `pele/global` 导出的包并内嵌它,不依赖 CDN。
61
+ - 把 SVG 放在相应说明旁。多张图各用独立的 `idPrefix`,避免箭头、渐变、标题和描述的 ID 冲突。
62
+ - 图示 CSS 位于 `assets/diagram.css`,生成时内嵌到 HTML。用 `--pele-bg`、`--pele-fg`、`--pele-muted`、`--pele-line`、`--pele-surface`、`--pele-surface-alt`、`--pele-border` 等 CSS 变量匹配宿主页面。源图的 `classDef` 可能表达错误、修复和缺陷路径,不为统一配色覆盖这些语义。
63
+ - 用 `mount()` 读取实际中文字体并随容器宽度重排;需要缩放时使用它的 `zoom` 选项,不自行实现图布局或缩放系统。仍然过密的图应先拆分视角,而不是继续缩小文字。
64
+ - 内嵌源码、标题和说明时正确转义 HTML 与脚本结束标签。图中禁用外部图片以保持离线显示,保留 Pele 的许可文本。
65
+
66
+ Pele 使用自己的布局与主题;Mermaid 的 `themeVariables` 不生效,任意 HTML 标签也不受支持。遇到不支持的语法,按原含义调整或拆图后重新验证;状态与缺陷路径的含义必须保持,不静默改用另一套渲染器。
67
+
68
+ 官方依据:[API](https://pele.run/api)、[主题](https://pele.run/theming)、[兼容性](https://pele.run/compatibility)、[安全与嵌入](https://pele.run/security)。
@@ -0,0 +1,116 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { existsSync } from 'node:fs';
3
+ import { mkdir, mkdtemp, readFile, rename, rm, writeFile } from 'node:fs/promises';
4
+ import { createRequire } from 'node:module';
5
+ import { homedir } from 'node:os';
6
+ import { basename, dirname, extname, join, resolve } from 'node:path';
7
+ import { pathToFileURL } from 'node:url';
8
+ import { parseArgs } from 'node:util';
9
+
10
+ const version = '1.12.0';
11
+ const fontFamily = "-apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif";
12
+ const html = text => text.replace(/[&<>"']/g, char => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[char]);
13
+ const json = value => JSON.stringify(value).replace(/</g, '\\u003c');
14
+ const roles = [
15
+ ['fugueInput', '输入事实'], ['fugueProcess', '处理步骤'], ['fugueDecision', '判断与等待'],
16
+ ['fugueExternal', '外部交互'], ['fugueSuccess', '完成'], ['fugueFailure', '失败与阻断'], ['fugueSkipped', '跳过'],
17
+ ];
18
+
19
+ async function loadPele() {
20
+ const cache = join(process.env.XDG_CACHE_HOME || join(homedir(), '.cache'), 'fugue-show-me', `pele-${version}`);
21
+ if (!existsSync(join(cache, 'node_modules', 'pele', 'package.json'))) {
22
+ await mkdir(dirname(cache), { recursive: true });
23
+ const staging = await mkdtemp(`${cache}-`);
24
+ try {
25
+ await writeFile(join(staging, 'package.json'), '{"private":true}\n');
26
+ console.error(`首次安装 pele@${version},缓存位置:${cache}`);
27
+ const result = spawnSync('npm', ['install', '--no-audit', '--no-fund', '--ignore-scripts', '--save-exact', `pele@${version}`], {
28
+ cwd: staging,
29
+ encoding: 'utf8',
30
+ env: { ...process.env, PATH: `${dirname(process.execPath)}${process.platform === 'win32' ? ';' : ':'}${process.env.PATH || ''}` },
31
+ shell: process.platform === 'win32',
32
+ });
33
+ if (result.status !== 0) throw new Error(result.error?.message || result.stderr || 'Pele 安装失败');
34
+ try { await rename(staging, cache); }
35
+ catch (error) { if (!existsSync(join(cache, 'node_modules', 'pele', 'package.json'))) throw error; }
36
+ } finally {
37
+ await rm(staging, { recursive: true, force: true });
38
+ }
39
+ }
40
+ const require = createRequire(join(cache, 'package.json'));
41
+ const { render } = await import(pathToFileURL(require.resolve('pele')).href);
42
+ const bundle = await readFile(require.resolve('pele/global'), 'utf8');
43
+ const license = await readFile(join(dirname(require.resolve('pele/package.json')), 'LICENSE'), 'utf8');
44
+ return { render, bundle, license };
45
+ }
46
+
47
+ async function main() {
48
+ const { values, positionals } = parseArgs({ options: {
49
+ out: { type: 'string', short: 'o' }, title: { type: 'string' }, theme: { type: 'string', default: 'light' }, help: { type: 'boolean', short: 'h' },
50
+ }, allowPositionals: true });
51
+ const usage = '用法:node render-html.mjs --out 说明.html [--title 标题] [--theme light|dark] 总览.mmd [细节.mmd ...]';
52
+ if (values.help) { console.log(usage); return; }
53
+ if (!values.out || !positionals.length) throw new Error(usage);
54
+ if (!['light', 'dark'].includes(values.theme)) throw new Error('主题必须是 light 或 dark');
55
+ const sources = await Promise.all(positionals.map(async file => ({
56
+ title: basename(file, extname(file)), source: await readFile(file, 'utf8'),
57
+ })));
58
+ const { render, bundle, license } = await loadPele();
59
+ const diagrams = sources.map((diagram, index) => {
60
+ const id = `diagram-${index + 1}`;
61
+ try {
62
+ const { svg } = render(diagram.source, { idPrefix: id, fontFamily, images: false });
63
+ return { ...diagram, id, svg };
64
+ } catch (error) { throw new Error(`${positionals[index]}:${error.message}`, { cause: error }); }
65
+ });
66
+ const title = values.title || '图解说明';
67
+ const css = await readFile(new URL('../assets/diagram.css', import.meta.url), 'utf8');
68
+ const nodeClasses = diagrams.flatMap(({ svg }) => [...svg.matchAll(/<g\b[^>]*\bclass="([^"]*)"/g)].map(match => match[1].split(/\s+/)))
69
+ .filter(classes => classes.includes('pele-node') && !classes.some(name => /wrong|fixed|mixed|after|worse/i.test(name)));
70
+ const usedRoles = roles.filter(([name]) => nodeClasses.some(classes => classes.includes(name)));
71
+ const legend = usedRoles.length ? `<ul class="legend" aria-label="颜色含义">${usedRoles.map(([name, label]) => `<li class="${name}"><i aria-hidden="true"></i>${label}</li>`).join('')}</ul>` : '';
72
+ const sections = diagrams.map(({ title, id, svg }) => `<section aria-labelledby="${id}-heading">
73
+ <h2 id="${id}-heading">${html(title)}</h2>
74
+ <div class="diagram" id="${id}" role="region" aria-label="${html(title)}">${svg}</div>
75
+ </section>`).join('\n');
76
+ const page = `<!doctype html>
77
+ <html lang="zh-CN" data-theme="${values.theme}"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
78
+ <title>${html(title)}</title><style>
79
+ :root { --font: ${fontFamily}; }
80
+ ${css}
81
+ </style></head><body><main>
82
+ <header><div class="page-heading"><h1>${html(title)}</h1><div class="theme-controls" role="group" aria-label="页面主题"><button type="button" data-theme-option="light" aria-pressed="${values.theme === 'light'}">浅色</button><button type="button" data-theme-option="dark" aria-pressed="${values.theme === 'dark'}">深色</button></div></div><p>先看全局,再查看需要的细节。图内可缩放、拖动和复位;聚焦图后也可使用 +、−、0 和方向键。</p></header>
83
+ <nav aria-label="图示目录">${diagrams.map(({ id, title }) => `<a href="#${id}-heading">${html(title)}</a>`).join('')}</nav>
84
+ ${legend}
85
+ ${sections}
86
+ <noscript>当前展示静态图;启用 JavaScript 后可使用自适应布局和缩放。</noscript>
87
+ </main>
88
+ <script id="diagram-sources" type="application/json">${json(diagrams.map(({ id, source }) => ({ id, source })))}</script>
89
+ <script>/* Pele ${version}\n${license.replace(/\*\//g, '* /')}\n*/\n${bundle.replace(/<\/script/gi, '<\\/script')}</script>
90
+ <script>
91
+ for (const button of document.querySelectorAll('[data-theme-option]')) {
92
+ button.addEventListener('click', () => {
93
+ document.documentElement.dataset.theme = button.dataset.themeOption;
94
+ for (const option of document.querySelectorAll('[data-theme-option]')) {
95
+ option.setAttribute('aria-pressed', String(option === button));
96
+ }
97
+ });
98
+ }
99
+ for (const { id, source } of JSON.parse(document.getElementById('diagram-sources').textContent)) {
100
+ const container = document.getElementById(id);
101
+ pele.mount(container, source, {
102
+ idPrefix: id,
103
+ images: false,
104
+ zoom: { always: true, labels: { zoomIn: '放大', zoomOut: '缩小', reset: '显示全图' } },
105
+ onRender() { container.dataset.rendered = 'true'; delete container.dataset.renderError; },
106
+ onError(error) { container.dataset.renderError = String(error); },
107
+ });
108
+ }
109
+ </script></body></html>`;
110
+ const output = resolve(values.out);
111
+ await mkdir(dirname(output), { recursive: true });
112
+ await writeFile(output, page);
113
+ console.log(`已生成 ${diagrams.length} 张图:${output}`);
114
+ }
115
+
116
+ main().catch(error => { console.error(error.message); process.exitCode = 1; });