pm-canvas-viewer 0.1.1 → 0.7.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/README.md +147 -48
- package/bin/cli.js +8 -0
- package/dist/frontend/assets/index-BGlLkKpJ.js +847 -0
- package/dist/frontend/assets/{index-DuoTxNrI.css → index-CQryX8ee.css} +1 -1
- package/dist/frontend/assets/{main-BVykNxDI.js → main-Ac6ZQwWa.js} +1 -1
- package/dist/frontend/index.html +2 -2
- package/lib/inject-pm-ids-rules.js +52 -0
- package/lib/inject-pm-ids.js +275 -0
- package/lib/inject-pm-ids.test.js +303 -0
- package/lib/scan-and-inject.js +65 -0
- package/package.json +10 -5
- package/server/api/textPatch.js +595 -0
- package/server/api/textPatch.test.js +248 -0
- package/server/index.js +3 -1
- package/server/lib/jsAstUtils.js +179 -0
- package/server/lib/jsAstUtils.test.js +60 -0
- package/server/patchers/HtmlAttrPatcher.js +111 -0
- package/server/patchers/HtmlAttrPatcher.test.js +149 -0
- package/server/patchers/HtmlTextPatcher.js +143 -0
- package/server/patchers/HtmlTextPatcher.test.js +226 -0
- package/server/patchers/JsLiteralPatcher.js +154 -0
- package/server/patchers/JsLiteralPatcher.test.js +269 -0
- package/server/patchers/index.js +24 -0
- package/dist/frontend/assets/index-wIsEd6Hm.js +0 -353
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# pm-canvas-viewer
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
本地 **HTML 原型 + 配套文档** 查看编辑器。一键启动,浏览器操作。
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
打开任意"含 HTML 文件 + 可选 docs/ 文档"的目录,左侧选项 / 切页,中间预览,右侧 Markdown / Excalidraw / draw.io 三种文档编辑器。完整兼容 [pm-canvas skill](../skill/) 生成的画板(含锚点系统)。
|
|
6
6
|
|
|
7
7
|
## 安装
|
|
8
8
|
|
|
@@ -14,70 +14,156 @@ npm i -g pm-canvas-viewer
|
|
|
14
14
|
|
|
15
15
|
## 使用
|
|
16
16
|
|
|
17
|
+
打开一个**文件夹**(viewer 不接受单文件路径——它的定位是"HTML + 配套文档"组合):
|
|
18
|
+
|
|
17
19
|
```bash
|
|
18
|
-
canvas open
|
|
20
|
+
canvas open ./我的原型/
|
|
21
|
+
canvas open # 等价于 canvas open .
|
|
22
|
+
canvas # 从历史记录里检索
|
|
23
|
+
canvas list # 查看运行中的实例
|
|
24
|
+
canvas stop # 停止实例
|
|
19
25
|
```
|
|
20
26
|
|
|
21
|
-
|
|
27
|
+
浏览器自动打开。viewer 会扫目录识别"展示项",然后按情况渲染界面。
|
|
28
|
+
|
|
29
|
+
### viewer 识别哪些目录
|
|
30
|
+
|
|
31
|
+
每个目录独立判断:
|
|
32
|
+
|
|
33
|
+
| 类型 | 判定 | viewer 内表现 |
|
|
34
|
+
|---|---|---|
|
|
35
|
+
| **画板项** | 含 `canvas.json` 且 `rows[]` 是数组 | 完整画板能力 + 锚点系统 |
|
|
36
|
+
| **html 项** | 直接含 ≥1 个 `.html` 且无 canvas.json | 普通预览 + iframe 跳转拦截 |
|
|
37
|
+
| **容器目录** | 自身非项,子孙含项 | 在左栏作为分组节点展开 |
|
|
38
|
+
| 其他 | 无 html 无 canvas.json | 完全忽略 |
|
|
22
39
|
|
|
23
|
-
|
|
40
|
+
过滤名单:`node_modules` / `.git` / `dist` / `docs` / `.DS_Store` / `assets`。深度上限 8 层。
|
|
41
|
+
|
|
42
|
+
### 界面布局
|
|
24
43
|
|
|
25
44
|
```
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
│
|
|
31
|
-
│
|
|
32
|
-
|
|
33
|
-
├── 需求规格.md
|
|
34
|
-
└── 业务流程.excalidraw
|
|
45
|
+
┌────────────┬──────────────────┬────────────┐
|
|
46
|
+
│ 左栏 │ 中间画框 │ 右栏 │
|
|
47
|
+
│ │ (iframe) │ │
|
|
48
|
+
│ 项列表 ▾ │ │ 文档面板 │
|
|
49
|
+
│ ──────── │ │ + 编辑器 │
|
|
50
|
+
│ 页列表 │ │ │
|
|
51
|
+
└────────────┴──────────────────┴────────────┘
|
|
35
52
|
```
|
|
36
53
|
|
|
37
|
-
|
|
54
|
+
- **左栏自适应显隐**:项数 ≥2 显示项列表(树状);当前 html 项 ≥2 页时显示页列表;两者都无显示时左栏整体隐藏(沉浸预览)
|
|
55
|
+
- **页序**:`index.html > home.html > A-Z`,支持拖动调整并按项记忆
|
|
56
|
+
- **html 项的跳转策略**:同源同项放行 + 同步页列表选中态;外链或跨项拦截 + toast 提示
|
|
57
|
+
|
|
58
|
+
### 几种典型场景
|
|
38
59
|
|
|
39
60
|
```bash
|
|
40
|
-
|
|
61
|
+
# 1. 标准画板(向后兼容,体验和老版本完全一致)
|
|
62
|
+
canvas open ./画板目录/
|
|
63
|
+
|
|
64
|
+
# 2. 多版本画板管理
|
|
65
|
+
canvas open ./我的产品/
|
|
66
|
+
# ↑ 目录里有 v1/canvas.json、v2/canvas.json,左栏显示项树
|
|
41
67
|
|
|
42
|
-
#
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
68
|
+
# 3. 多页 HTML 原型
|
|
69
|
+
canvas open ./用户中心原型/
|
|
70
|
+
# ↑ 目录直接含 home.html / profile.html / settings.html,左下显示页列表
|
|
71
|
+
|
|
72
|
+
# 4. 混合(画板 + 普通 HTML 共存)
|
|
73
|
+
canvas open ./prototypes/
|
|
74
|
+
# ↑ 含画板子目录 + 散户 html 目录,左栏按物理目录树状展开
|
|
46
75
|
```
|
|
47
76
|
|
|
48
77
|
## 功能
|
|
49
78
|
|
|
50
|
-
###
|
|
51
|
-
|
|
79
|
+
### 中间画框
|
|
80
|
+
|
|
81
|
+
- **画板项**:保留 pm-canvas shell 的无限画布、缩放、平移;可挂载锚点
|
|
82
|
+
- **html 项**:iframe 加载选中页,`sandbox="allow-scripts allow-same-origin"`,onload 注入跳转拦截脚本(同源放行 + URL 变化同步左下页列表;外链拦截 toast)
|
|
52
83
|
|
|
53
|
-
###
|
|
84
|
+
### 右栏:文档面板
|
|
85
|
+
|
|
86
|
+
支持三种文档(都按"当前选中项的 `docs/`"动态绑定):
|
|
54
87
|
|
|
55
88
|
**Markdown 编辑器**
|
|
56
|
-
- Tiptap 3
|
|
57
|
-
-
|
|
58
|
-
- `⌘B` 加粗、`⌘I` 斜体
|
|
59
|
-
- `⌘S` 保存到本地文件
|
|
89
|
+
- Tiptap 3 WYSIWYG,工具栏:标题、加粗、斜体、列表、代码、表格
|
|
90
|
+
- `⌘B` 加粗、`⌘I` 斜体、`⌘S` 保存
|
|
60
91
|
|
|
61
92
|
**Excalidraw 编辑器**
|
|
62
|
-
- 完整 Excalidraw
|
|
63
|
-
- `⌘S` 保存为 .excalidraw
|
|
93
|
+
- 完整 Excalidraw(流程图、架构图等)
|
|
94
|
+
- `⌘S` 保存为 .excalidraw
|
|
95
|
+
|
|
96
|
+
**draw.io 编辑器**
|
|
97
|
+
- 集成 diagrams.net iframe,`⌘S` 保存为 .drawio
|
|
64
98
|
|
|
65
99
|
**文件管理**
|
|
66
|
-
-
|
|
67
|
-
-
|
|
68
|
-
|
|
100
|
+
- 标签页切换、拖动排序(按项独立记忆)
|
|
101
|
+
- 空状态点 + 新建文件,触发 `docs/` 按需创建(不会无 docs 文件就生成空 `docs/` 目录)
|
|
102
|
+
|
|
103
|
+
### 锚点系统(仅画板项)
|
|
104
|
+
|
|
105
|
+
- 双击画板创建锚点 pin(自定义颜色 + 标签)
|
|
106
|
+
- 锚点可关联到文档文件、或文档内的标题(marker)
|
|
107
|
+
- pin ↔ Tiptap chip 双向跳转
|
|
108
|
+
- 数据存 `docs/anchors.json`,按项隔离
|
|
109
|
+
- 切到 html 项时**彻底卸载**(无 iframe DOM 监听残留)
|
|
110
|
+
|
|
111
|
+
### 文案就地编辑(v0.5+)
|
|
112
|
+
|
|
113
|
+
无需翻代码即可改原型/画板里的可见文字,写回源 HTML/JS 文件。
|
|
114
|
+
|
|
115
|
+
**5 类可编辑范围**:
|
|
116
|
+
|
|
117
|
+
| 类别 | 例 | 引入 |
|
|
118
|
+
|------|---|------|
|
|
119
|
+
| HTML 静态文本 | `<p>文字</p>` 里的"文字" | v1.0 |
|
|
120
|
+
| HTML mixed content | `<div>市值<b>14.5万</b>仓位</div>` 里的"市值"或"仓位"(每段独立可改) | v1.5 |
|
|
121
|
+
| HTML 属性白名单 | `<input placeholder=>`、`<img alt=>`、`title` / `aria-label` / `aria-description` | v1.5 |
|
|
122
|
+
| JS 字符串字面量 | `const STOCK = 'AI芯片';` 里的 `'AI芯片'`(含单/双引号 + 纯反引号字符串) | v1.5 |
|
|
123
|
+
| 模板字符串静态片段 | `` `hello ${name}` `` 里的 `hello ` | v1.5 |
|
|
124
|
+
|
|
125
|
+
**入口**(任一):按 `E` 键 / canvas 项 iframe toolbar 「开启文案编辑」 / html 项右上角浮层 toggle。
|
|
126
|
+
|
|
127
|
+
**不可编辑提示**:灰色高亮 + hover 200ms tooltip,10 类原因(`no-locator` / `pm-id-not-found` / `text-mismatch` / `no-text-child` / `whitespace-only` / `computed-text` / `template-expression` / `attribute-not-editable` / `script-or-style-internal` / `source-position-ambiguous`)。
|
|
128
|
+
|
|
129
|
+
**实现要点**:
|
|
130
|
+
- 后端 Patcher Strategy(HtmlText / HtmlAttr / JsLiteral),用 parse5 + acorn 解析定位 + 字符 offset 切片重写
|
|
131
|
+
- JS 字面量首次保存自动插 sentinel 注释(`/*pm-js-id:lit-xxx*/`)便于下次精确反查;多处同字面量返 `source-position-ambiguous`
|
|
132
|
+
- 保存走 fileHash 校验 + 冲突弹窗(覆盖 / 取消)
|
|
133
|
+
- 详见 [`ai/specs/2026-06-03-inline-text-editing-v1.5-design.md`](../ai/specs/2026-06-03-inline-text-editing-v1.5-design.md)
|
|
134
|
+
|
|
135
|
+
**已知限制**(v1.5 → 后续):
|
|
136
|
+
- JS 字面量改完后 DOM 不自动重算(DOM 是 JS innerHTML 拼出)→ toast 提示「刷新页面看效果」;根治需 skill 模板侧为动态节点注入 `data-pm-id`
|
|
137
|
+
- 模板字符串 quasi 不插 sentinel(会破坏模板字符串)→ 同 stringValue quasi 多处时 `source-position-ambiguous`
|
|
138
|
+
- 见 `ai/ROADMAP.md` 技术债清单
|
|
139
|
+
|
|
140
|
+
### 命令行
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
canvas open [目录] -p <端口> # 默认 4800
|
|
144
|
+
canvas # 历史检索
|
|
145
|
+
canvas list # 列运行实例
|
|
146
|
+
canvas stop [port|--all] # 停止实例
|
|
147
|
+
canvas --version / --help
|
|
148
|
+
```
|
|
69
149
|
|
|
70
150
|
## 技术栈
|
|
71
151
|
|
|
72
152
|
| 层 | 技术 |
|
|
73
153
|
|---|---|
|
|
74
154
|
| CLI | Commander.js |
|
|
75
|
-
| 服务端 | Express
|
|
155
|
+
| 服务端 | Express(ItemRegistry + routeId 协议 + 路径穿越防御) |
|
|
76
156
|
| 前端框架 | React 18 + Vite |
|
|
77
157
|
| MD 编辑器 | Tiptap 3 WYSIWYG(@tiptap/markdown) |
|
|
78
|
-
| 流程图编辑 | @excalidraw/excalidraw |
|
|
158
|
+
| 流程图编辑 | @excalidraw/excalidraw + diagrams.net embed |
|
|
79
159
|
| 构建 | Vite 5 |
|
|
80
160
|
|
|
161
|
+
## 安全
|
|
162
|
+
|
|
163
|
+
- 服务端只接受 scanner 产出的 routeId(sha1 前 8 位),不接受任意路径
|
|
164
|
+
- 路径穿越二段防御:filename 层拒绝 `\0` / `/` / `..` / `.` 开头;resolve 后强制落在项目录内
|
|
165
|
+
- iframe sandbox(html 项)+ 跳转拦截脚本(拦 `<a>` / `<form>` / `window.open` / `history.pushState` / 越界 location)
|
|
166
|
+
|
|
81
167
|
## 开发
|
|
82
168
|
|
|
83
169
|
```bash
|
|
@@ -88,25 +174,38 @@ npm run dev
|
|
|
88
174
|
npm run build
|
|
89
175
|
|
|
90
176
|
# 用开发版直接测试
|
|
91
|
-
node bin/cli.js open /path/to/
|
|
177
|
+
node bin/cli.js open /path/to/dir --port 4800
|
|
92
178
|
```
|
|
93
179
|
|
|
94
180
|
### 项目结构
|
|
95
181
|
|
|
96
182
|
```
|
|
97
183
|
pm-canvas-viewer/
|
|
98
|
-
├── bin/cli.js
|
|
184
|
+
├── bin/cli.js ← CLI 入口
|
|
99
185
|
├── server/
|
|
100
|
-
│ ├── index.js
|
|
101
|
-
│
|
|
102
|
-
|
|
103
|
-
│
|
|
104
|
-
│
|
|
105
|
-
|
|
106
|
-
│
|
|
107
|
-
│
|
|
108
|
-
│
|
|
109
|
-
│ └──
|
|
110
|
-
├──
|
|
111
|
-
|
|
186
|
+
│ ├── index.js ← Express 服务器 + /item/:routeId/* 静态路由
|
|
187
|
+
│ ├── itemRegistry.js ← 单例 routeId → Item 映射
|
|
188
|
+
│ └── api/
|
|
189
|
+
│ ├── files.js ← docs/ CRUD(routeId 协议 + 按需 mkdir)
|
|
190
|
+
│ └── anchors.js ← anchors.json CRUD(仅画板项)
|
|
191
|
+
├── lib/
|
|
192
|
+
│ ├── scanner.js ← 展示项扫描(画板 / html / 容器 / 过滤)
|
|
193
|
+
│ ├── history.js ← 启动过的根目录历史
|
|
194
|
+
│ ├── port.js ← 端口管理
|
|
195
|
+
│ └── prompts.js ← 终端 TUI(fuzzy 选择)
|
|
196
|
+
├── frontend/src/
|
|
197
|
+
│ ├── App.jsx ← 主布局 + 导航状态机 + 切项清理
|
|
198
|
+
│ ├── apiClient.js ← 统一注入 routeId 的 fetch 封装
|
|
199
|
+
│ ├── Sidebar/ ← 左栏(项树 + 页列表)
|
|
200
|
+
│ ├── CanvasView/
|
|
201
|
+
│ │ ├── index.jsx ← iframe 双模式渲染
|
|
202
|
+
│ │ └── iframeInterceptor.js ← html 项跳转拦截脚本生成器
|
|
203
|
+
│ ├── DocPanel/ ← 右栏 + 新建文件
|
|
204
|
+
│ ├── MdEditor/ ← Tiptap MD
|
|
205
|
+
│ ├── ExcalidrawEditor/ ← Excalidraw
|
|
206
|
+
│ ├── DrawioEditor/ ← draw.io iframe
|
|
207
|
+
│ ├── AnchorSystem/ ← 锚点 overlay + store + dialog
|
|
208
|
+
│ └── Toast.jsx ← 拦截反馈
|
|
209
|
+
├── dist/ ← 前端构建产物(gitignored,发包时打入)
|
|
210
|
+
└── package.json
|
|
112
211
|
```
|
package/bin/cli.js
CHANGED
|
@@ -152,6 +152,14 @@ async function startServerForRoot(rootDir) {
|
|
|
152
152
|
stats: scanResult.stats,
|
|
153
153
|
});
|
|
154
154
|
|
|
155
|
+
// v0.5: 编辑文案功能需要每个元素有稳定 data-pm-id,启动时自动注入(幂等)
|
|
156
|
+
const { injectAllCanvasItems } = await import('../lib/scan-and-inject.js');
|
|
157
|
+
const injectResults = await injectAllCanvasItems(scanResult.items);
|
|
158
|
+
const changed = injectResults.filter(r => r.changed).length;
|
|
159
|
+
if (changed > 0) {
|
|
160
|
+
console.log(` ✓ 已给 ${changed} 个 HTML 文件注入 data-pm-id(首次迁移)`);
|
|
161
|
+
}
|
|
162
|
+
|
|
155
163
|
const port = await resolvePort(portOverride || 4800);
|
|
156
164
|
if (port === null) process.exit(0); // 用户在端口冲突 prompt 中选了 reuse
|
|
157
165
|
|