@andares/pdeditor 0.4.0 → 0.5.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 CHANGED
@@ -1,13 +1,23 @@
1
1
  # @andares/pdeditor
2
2
 
3
- **Headless 提示词输入框组件**:在任意框架(React/Vue/Svelte/原生)中嵌入一个支持多格式语法高亮的输入框。基于 [Yace](https://github.com/petersolopov/yace)(~2KB、零依赖)。
3
+ **低 UI 提示词输入框**:一个能嵌进聊天框、侧栏、表单、弹窗的文本编辑器。
4
+ 核心只负责文本、光标与语法高亮;边框、背景、工具栏、发送按钮全部由宿主决定。
4
5
 
5
- ## 定位
6
+ - **低 UI**:默认不画任何 chrome;字体、字号、颜色全部继承宿主,只有 `line-height: 1.5` 是温和默认值
7
+ - **精确优先**:文本模型就是原始字符串(不做富文本/文档模型),高亮服务于“看清结构”,不改变内容
8
+ - **任意高度**:内容自适应;宿主给 `height` / `max-height` 时自动切换为内滚 + 覆盖层同步
9
+ - **三种用法**:原生工厂 `createPdEditor`、自定义元素 `<pd-editor>`、React 薄包装 `<PdEditor>`
10
+ - **主题即变量**:token 配色默认由 `currentColor` 派生;需要品牌色时覆盖 CSS 变量即可
11
+ - **可访问**:覆盖层 `aria-hidden`、label 转发、焦点环、`Tab` 可配置、`Escape` 释放焦点
6
12
 
7
- - **Headless**:只渲染输入框内容(高亮层 + 原生 textarea),无 UI chrome;核心维护覆盖层排版不变量,外部定义容器外观与 token 配色
8
- - **精确优先**:pd 是纯代码文本,高亮服务于精确(看清结构),不改变文本;编辑模型保留原始文本
9
- - **语言切换是 API 行为**(`setLanguage`),不做 UI 切换器
10
- - 支持格式:`pd`(自研 tokenizer,语义与主包 lexer 一致)/ `md` / `xml` / `json` / `yaml`(Prism)
13
+ 支持格式:`pd`(内置 tokenizer,语义与 `@andares/pdfoundation` lexer 一致)/ `md` / `xml` / `json` / `yaml`(Prism,主入口内置)。
14
+
15
+ > 🤖 **AI agent 集成本项目时**:先读 [`skill/pdeditor/SKILL.md`](https://github.com/andares/promptdown/blob/master/skill/pdeditor/SKILL.md)
16
+ > (选入口 / 各框架可复制代码 / 低 UI 嵌入契约 / 禁止事项 / 集成后自测清单),
17
+ > 该 skill 随 npm 包 `@andares/promptdown` 发布,装过全局包即可被 agent 自动发现。
18
+ > 本文档是完整的 API/主题/浏览器支持手册。
19
+
20
+ ---
11
21
 
12
22
  ## 安装
13
23
 
@@ -15,120 +25,390 @@
15
25
  npm install @andares/pdeditor
16
26
  ```
17
27
 
18
- ## 入口选择与 tree-shaking
28
+ `@andares/pdfoundation`(共享语义核心)会作为依赖自动安装。
19
29
 
20
- 同一个包两个入口,**选择入口即完成裁剪**——高亮/运行时依赖(yace、es-toolkit、Prism)均预打包进产物(`sideEffects: false`),消费方 bundler 无需做模块级摇树;仅**共享语义包 `@andares/pdfoundation` 保持 external**——两个入口都 re-export `format` / `jsonToPdText` / `pdToJsonText`(另含自研 `highlightPd`),运行时由消费方按 peerDependencies 提供,语义单一来源、零漂移:
30
+ ## 入口与体积
21
31
 
22
- | 入口 | import | 内置高亮 | 产物体积(min 前 / gzip) | 适用 |
23
- | --- | --- | --- | --- | --- |
24
- | 全量(默认) | `@andares/pdeditor` | pd(自研)+ md/xml/json/yaml(Prism) | ~84 kB / ~23 kB | 需要多语言内置高亮 |
25
- | pd-only 精简 | `@andares/pdeditor/pd` | 仅 pd;其余语言纯文本渲染(功能完好) | ~17 kB / ~5.5 kB | 只用 pd(多数宿主场景) |
32
+ 每个入口都是**自包含产物**(引擎/插件/tokenizer 已内联),选入口即完成裁剪:
26
33
 
27
- ```ts
28
- // pd-only:不把 Prism(~80 kB)打进你的 bundle
29
- import { createPdEditor } from "@andares/pdeditor/pd";
34
+ | 入口 | import | 内容 | 体积(min / gzip) |
35
+ | --- | --- | --- | --- |
36
+ | 全量 | `@andares/pdeditor` | 工厂 + pd 自研高亮 + Prism(md/json/yaml/xml) | ~88.8 kB / ~24.1 kB |
37
+ | pd-only | `@andares/pdeditor/pd` | 工厂 + pd 高亮;其余语言纯文本 | ~21.7 kB / ~6.7 kB |
38
+ | 元素 | `@andares/pdeditor/element` | `<pd-editor>` + pd 高亮(**不含 Prism**) | ~35.0 kB / ~9.7 kB |
39
+ | 元素自动注册 | `@andares/pdeditor/element/auto` | 同上 + import 即 `customElements.define` | ~35.0 kB / ~9.7 kB |
40
+ | React | `@andares/pdeditor/react` | React 包装 + 元素 + pd 高亮 | ~37.1 kB / ~10.4 kB |
41
+
42
+ - 元素/React 入口默认只带 pd tokenizer;需要 md/json/yaml/xml 时,把主入口的 `prismHighlighter` 注入 `highlight`(见下文),避免所有元素用户都背 Prism。
43
+ - 所有入口都 re-export `format` / `jsonToPdText` / `pdToJsonText`;`/pd` 与主入口另导出 `highlightPd`。
44
+ - React 是 **optional peerDependency**(`>=17`);不用 React 不会装上它。
45
+
46
+ ---
47
+
48
+ ## 快速开始
49
+
50
+ ### 1. 自定义元素(推荐)
51
+
52
+ ```html
53
+ <script type="module">
54
+ import "@andares/pdeditor/element/auto";
55
+ </script>
56
+
57
+ <pd-editor
58
+ language="pd"
59
+ placeholder="描述你想让模型做的事…"
60
+ aria-label="提示词"
61
+ ></pd-editor>
62
+
63
+ <script type="module">
64
+ const el = document.querySelector("pd-editor");
65
+ el.addEventListener("pd-input", (e) => {
66
+ console.log(e.detail.value);
67
+ });
68
+ el.value = "任务: 写一个防抖搜索";
69
+ </script>
30
70
  ```
31
71
 
32
- - 两个入口的 API 完全一致(同一 `createPdEditor` 工厂,仅内置高亮管线不同)
33
- - pd-only 入口下 `setLanguage("md")` 不报错:内容以纯文本渲染,无高亮
34
- - 任何时候都可用 `options.highlight` 自带高亮器(BYO)覆盖内置管线——精简入口 + BYO 即可按需补其它语言
35
- - 消费验证:vite 项目引 `/pd` 入口构建产物 ~13 kB(gzip ~4.7 kB)、无 Prism 痕迹;除 peer `@andares/pdfoundation` 外的依赖均已内联进 dist(仅开发期使用)
72
+ 也可以显式注册(SSR 安全):
36
73
 
37
- ## 用法
74
+ ```ts
75
+ import { definePdEditor } from "@andares/pdeditor/element";
76
+ definePdEditor(); // 或 definePdEditor("my-pd")
77
+ ```
78
+
79
+ ### 2. 工厂(无 UI / 自己控制 DOM)
38
80
 
39
81
  ```ts
40
82
  import { createPdEditor } from "@andares/pdeditor";
41
83
 
42
84
  const editor = createPdEditor(document.querySelector("#editor")!, {
43
- value: "//!pd 基础设定\n角色: 资深工程师\n",
44
- language: "pd",
45
- styles: {
46
- fontSize: "14px",
47
- lineHeight: "20px",
48
- padding: "12px",
49
- },
50
- onValueChange: (v) => console.log(v.length),
85
+ value: "//!pd 任务\n角色: 资深工程师\n",
86
+ language: "pd",
87
+ placeholder: "写点什么…",
88
+ styles: { padding: "12px" },
89
+ onValueChange: (v) => console.log(v.length),
51
90
  });
52
91
 
53
- editor.setLanguage("json"); // 语言切换 = API
54
- editor.setValue('{"角色": "资深工程师"}');
55
- console.log(editor.getValue());
92
+ editor.setValue("任务: 改一下");
93
+ editor.setLanguage("json");
94
+ editor.insertText("{}");
56
95
  editor.destroy();
57
96
  ```
58
97
 
98
+ 工厂**不会修改挂载点的样式**:它在挂载点内追加 `.pd-root`,宿主布局/边框/padding 全归宿主。
99
+
100
+ ### 3. React
101
+
102
+ ```tsx
103
+ import { PdEditor } from "@andares/pdeditor/react";
104
+
105
+ <PdEditor
106
+ defaultValue="任务: 写一个防抖搜索"
107
+ placeholder="描述任务…"
108
+ onValueChange={setValue}
109
+ />
110
+ ```
111
+
112
+ 受控用法:传 `value` + `onValueChange`;当 DOM 当前值与 prop 相同则不会回写(不打断光标/撤销)。
113
+ 非受控:只传 `defaultValue`。需要 md/json 高亮时:
114
+
115
+ ```tsx
116
+ import { prismHighlighter } from "@andares/pdeditor";
117
+ <PdEditor highlight={prismHighlighter} language="json" value={json} />
118
+ ```
119
+
120
+ ---
121
+
122
+ ## 低 UI 契约
123
+
124
+ ### 样式继承
125
+
126
+ 不传任何样式时:
127
+
128
+ - `font-family` / `font-size` / `letter-spacing` / `color` 全部 `inherit`
129
+ - `background` 透明,无边框/圆角/阴影
130
+ - `line-height` 默认 `1.5`(可用 `--pd-line-height` 或 `styles.lineHeight` 覆盖)
131
+ - 唯一“意见”是等宽对齐所需的不变量(见下)
132
+
133
+ ```css
134
+ /* 宿主可以只用一个变量把编辑器融进页面 */
135
+ pd-editor {
136
+ --pd-font-size: 14px;
137
+ --pd-line-height: 1.6;
138
+ --pd-placeholder-opacity: 0.45;
139
+ }
140
+ ```
141
+
142
+ ### 高度与滚动
143
+
144
+ | 宿主布局 | 行为 |
145
+ | --- | --- |
146
+ | 不给高度 | **auto-grow**:内容多高,编辑器多高 |
147
+ | `height: 200px` / `height: 100%`(父级有确定高度) | **内滚模式**:textarea 滚动,高亮层/行号层同步 `scrollTop` |
148
+ | `max-height: 200px` | 先 auto-grow,到上限后内滚 |
149
+
150
+ 固定高度示例:
151
+
152
+ ```css
153
+ .chat-sidebar { height: 360px; }
154
+ .chat-sidebar pd-editor { height: 100%; }
155
+ ```
156
+
157
+ > 元素默认 `height: auto`(不主动填满父级),这样在聊天框里不会被拉伸。要填满一个已知高度的容器,显式给它 `height: 100%`。
158
+
159
+ ### 排版不变量(不可破坏)
160
+
161
+ 覆盖层方案要求高亮 `pre` 与透明 `textarea` 逐字符对齐。以下由组件统一设置,**不要**在宿主 CSS 里覆盖:
162
+
163
+ - `font-*`、`line-height`、`letter-spacing`、`tab-size`
164
+ - `white-space: pre-wrap`、`overflow-wrap`、`word-break`
165
+ - `box-sizing`、`padding`(通过 `styles` / CSS 变量统一传)
166
+ - `font-variant-ligatures: none`(跨 span 的连字会改变宽度)
167
+
168
+ token 样式只应改**颜色**(或 `opacity`/`text-decoration`)。不要给 token 加 `font-size`、`padding`、`margin`、`border`、`font-weight`、`font-style`——在非严格等宽字体下会破坏对齐。
169
+
170
+ ---
171
+
59
172
  ## API
60
173
 
174
+ ### `createPdEditor(el, options?)`
175
+
176
+ | 选项 | 默认 | 说明 |
177
+ | --- | --- | --- |
178
+ | `value` | `""` | 初始值(CRLF/CR 会归一为 LF) |
179
+ | `language` | `"pd"` | `pd` / `md` / `xml` / `json` / `yaml`,或自定义字符串 |
180
+ | `lineNumbers` | `false` | 行号 gutter(会多一层透明文本,用于保持换行高度) |
181
+ | `styles` | `{}` | 排版样式(camelCase 或 `--pd-*`);`padding` 会应用到覆盖层而不是 root |
182
+ | `indentUnit` | `" "` | Tab 整行缩进 / 续行缩进单位 |
183
+ | `highlight` | — | BYO 高亮器 `(source, lang) => html`,覆盖内置管线 |
184
+ | `onValueChange` | — | 值变化(用户输入、撤销/重做、insertText;`setValue` 不触发) |
185
+ | `placeholder` | `""` | 覆盖层占位符(不依赖原生 placeholder) |
186
+ | `readOnly` / `disabled` | `false` | 只读 / 禁用 |
187
+ | `maxLength` | 不限 | 负数/未设置 = 不限;`setValue`/`insertText` 也会截断 |
188
+ | `spellcheck` | `false` | 原生拼写检查 |
189
+ | `tabBehavior` | `"indent"` | `indent`(pd 编辑语义)或 `focus`(无障碍优先);`indent` 下按 `Escape` 后下一次 `Tab` 移动焦点 |
190
+ | `historyLimit` | `5` | 撤销记忆步数(不含初始值) |
191
+ | `historyDebounce` | `1000` | 连续输入的合并窗口(毫秒) |
192
+ | `highlightThreshold` | `100000` | 超过字符数降级纯文本,避免超大文档每键全量 tokenize |
193
+ | `ariaLabel` / `ariaLabelledby` / `ariaDescribedby` | — | 转发给内部 textarea |
194
+ | `dir` | — | 文本方向 |
195
+
196
+ 返回实例:
197
+
61
198
  ```ts
62
- createPdEditor(el, {
63
- value?: string; // 初始值
64
- language?: "pd" | "md" | "xml" | "json" | "yaml";
65
- lineNumbers?: boolean; // 行号(默认 false)
66
- styles?: Record<string, string>; // 覆盖层共用的根节点排版样式
67
- highlight?: (source, lang) => string; // BYO 高亮函数(覆盖内置)
68
- onValueChange?: (value: string) => void;
69
- }): {
70
- setValue(v): void;
199
+ interface PdEditorInstance {
200
+ setValue(value: string, options?: { resetHistory?: boolean }): void;
71
201
  getValue(): string;
72
- setLanguage(lang): void; // 语言切换
202
+ setLanguage(lang: string): void;
203
+ setPlaceholder(text: string): void;
204
+ setReadOnly(v: boolean): void;
205
+ setDisabled(v: boolean): void;
206
+ setLineNumbers(v: boolean): void;
207
+ update(options: Partial<PdEditorOptions>): void;
208
+ focus(): void;
209
+ blur(): void;
210
+ undo(): void;
211
+ redo(): void;
212
+ insertText(text: string): void;
213
+ getSelection(): { start: number; end: number };
214
+ setSelection(start: number, end?: number): void;
73
215
  destroy(): void;
74
- textarea: HTMLTextAreaElement; // 原生元素(外部可监听)
216
+ textarea: HTMLTextAreaElement;
217
+ root: HTMLElement;
218
+ pre: HTMLElement;
75
219
  }
76
220
  ```
77
221
 
78
- ## 高亮样式
222
+ - `setValue` 默认**重置撤销历史**(程序化回填不应留下旧输入的撤销洞);需要保留历史时传 `{ resetHistory: false }`。
223
+ - `undo` / `redo` 也会触发 `onValueChange`(值确实变了),方便受控宿主同步。
224
+ - `destroy` 只移除引擎自己的 `.pd-root`,不动挂载点的其它子节点。
225
+
226
+ ### `<pd-editor>`
227
+
228
+ **属性**(声明式,字符串):
229
+
230
+ ```html
231
+ <pd-editor
232
+ value="初始值"
233
+ language="pd"
234
+ placeholder="…"
235
+ indent-unit=" "
236
+ line-numbers
237
+ tab-behavior="indent" <!-- 或 focus -->
238
+ readonly
239
+ disabled
240
+ maxlength="2000"
241
+ name="prompt"
242
+ spellcheck="true"
243
+ inputmode="text"
244
+ enterkeyhint="send"
245
+ dir="ltr"
246
+ shadow="false" <!-- light DOM(默认 shadow DOM) -->
247
+ aria-label="提示词"
248
+ ></pd-editor>
249
+ ```
250
+
251
+ **属性 / property**:`value`、`language`、`placeholder`、`readOnly`、`disabled`、`lineNumbers`、`indentUnit`、`tabBehavior`、`maxLength`、`name`、`highlight`。
252
+
253
+ ```ts
254
+ const el = document.querySelector("pd-editor");
255
+ el.value = "任务: x"; // 程序化设置(重置历史)
256
+ el.highlight = prismHighlighter; // 注入 Prism 高亮
257
+ el.insertText("...");
258
+ el.undo();
259
+ ```
260
+
261
+ **事件**(均 `bubbles` + `composed`):
262
+
263
+ | 事件 | detail | 说明 |
264
+ | --- | --- | --- |
265
+ | `pd-input` | `{ value }` | 值变化(用户输入/撤销/insertText) |
266
+ | `pd-change` | `{ value }` | 失焦且值相对聚焦时有变化 |
267
+ | `pd-focus` / `pd-blur` | `{ value }` | 焦点 |
268
+ | `pd-language-change` | `{ value }` | 语言属性变化 |
269
+ | `pd-ready` | `{ editor }` | 元素完成挂载(微任务派发) |
270
+
271
+ **Part**:`container` / `root` / `highlight` / `input` / `gutter` / `placeholder`。
79
272
 
80
- 高亮输出带 class 的 HTML,外部提供 token 配色。字体、行高、字距和 padding 等几何样式必须通过 `styles` 从 Yace 根节点统一传入,不要分别设置内部 `textarea` / `pre`。token 样式也不能使用会改变字形宽度或盒模型的 `font-size`、`padding`、`margin`、`border`;非严格等宽字体下不要使用粗体或斜体。
273
+ **表单关联**:元素是 `formAssociated`,支持 `name`、`value`、`formResetCallback`、`formDisabledCallback`(浏览器支持 `ElementInternals` 时)。
274
+
275
+ **SSR**:`element` 模块导入不触碰 `HTMLElement`/`customElements`;`definePdEditor()` 在 Node 下安全返回 `null`。React 包装在 layout effect 中注册元素,SSR 端渲染为空标签。
276
+
277
+ ---
278
+
279
+ ## 主题
280
+
281
+ 默认 token 颜色由 `currentColor` 派生(`color-mix`),在浅色/深色背景上都能自然融合;不支持 `color-mix` 时退回透明度。所有可配置项都是 CSS 变量:
282
+
283
+ | 变量 | 作用 |
284
+ | --- | --- |
285
+ | `--pd-font-family` / `--pd-font-size` / `--pd-line-height` / `--pd-letter-spacing` | 排版 |
286
+ | `--pd-color` / `--pd-background` | 前景/背景 |
287
+ | `--pd-caret-color` | 光标 |
288
+ | `--pd-placeholder-opacity` | 占位符透明度(默认 0.5) |
289
+ | `--pd-gutter-color` / `--pd-gutter-opacity` | 行号 |
290
+ | `--pd-token-key` / `--pd-token-punct` / `--pd-token-item` / `--pd-token-section` / `--pd-token-sep` / `--pd-token-ref` / `--pd-token-code` / `--pd-token-fence` | pd token |
291
+ | `--pd-token-comment` / `--pd-token-string` / `--pd-token-keyword` / `--pd-token-number` / `--pd-token-property` / `--pd-token-punctuation` | Prism token(by 常用类) |
292
+ | `--pd-focus-ring` / `--pd-focus-ring-offset` / `--pd-focus-radius` | 焦点环(`--pd-focus-ring: none` 可关) |
293
+ | `--pd-disabled-opacity` | 禁用态 |
81
294
 
82
295
  ```css
83
- .pd-key { color: #9cdcfe; } /* 键名 */
84
- .pd-key-punct { color: #d4d4d4; } /* 冒号 */
85
- .pd-item { color: #569cd6; } /* - 序列标记 */
86
- .pd-section { color: #c586c0; } /* //!pd 段标记 */
87
- .pd-sep { color: #569cd6; } /* --- 分隔线 */
88
- .pd-ref { color: #ce9178; } /* :refname 引用 */
89
- .pd-inline-code { color: #dcdcaa; } /* `行内代码` */
90
- .pd-fence { color: #6a9955; } /* ``` 围栏行 */
91
- /* Prism token 类(md/xml/json/yaml):.token.* */
296
+ /* 一个 VS Code 风格的暗色主题只要这样 */
297
+ pd-editor.dark {
298
+ --pd-background: #1e1e1e;
299
+ --pd-color: #d4d4d4;
300
+ --pd-token-key: #9cdcfe;
301
+ --pd-token-punct: #d4d4d4;
302
+ --pd-token-item: #569cd6;
303
+ --pd-token-section: #c586c0;
304
+ --pd-token-ref: #ce9178;
305
+ --pd-token-code: #dcdcaa;
306
+ --pd-token-fence: #6a9955;
307
+ --pd-focus-ring: 2px solid #007acc;
308
+ --pd-focus-ring-offset: 2px;
309
+ }
92
310
  ```
93
311
 
94
- 参考 [demo/index.html](./demo/index.html) 的完整配色示例。
312
+ ### shadow vs light
95
313
 
96
- 当前版本按内容自动增高;不要给编辑器设置会触发内部滚动的固定高度。固定高度滚动与高亮层同步留待后续版本实现。
314
+ - **默认 shadow DOM**:几何与 token 样式完全隔离,宿主全局 CSS 不会破坏覆盖层;token 用 CSS 变量上色。
315
+ - **`shadow="false"`(light DOM)**:宿主可以直接写 `pd-editor .pd-key { color: … }`;组件会向文档 head 注入一次 scoped 默认主题(不覆盖宿主更高优先级的规则)。
97
316
 
98
- ## 内置编辑行为
317
+ ---
99
318
 
100
- - **回车续行**:序列项行(`-` 开头)回车 → 新行继承缩进并自动补 `-`;非序列项行回车保留缩进
101
- - **Tab / Shift+Tab**:序列项行(`-` 开头,允许缩进)按 Tab 整行右缩进一个单位、Shift+Tab 整行左缩出一个单位(多行选区整体操作,空行跳过);非序列项行 Tab 插入缩进单位
102
- - **缩进单位**:默认两个空格,可用 `indentUnit` 选项配置
103
- - **中文输入法**:组合期(拼音/五笔选字前)输入字符实时渲染(覆盖层同步),选字确认后正常触发 `onValueChange`(组合期不触发,避免宿主收到中间态)
104
- - **撤销/重做**:Ctrl+Z 撤销 / Ctrl+Shift+Z 与 Ctrl+Y 重做,5 步记忆;快照含光标选区(恢复时光标一并回到当时位置,撤回到初始内容时光标回到首次变更点而非末尾);保存节点有 1s 防抖(连续打字合并为一步,不逐字还原、不浪费记忆步数);初始内容恒在栈底(可退回初始态,超限只挤掉最旧的非初始快照);程序化 `setValue` 不入栈;新输入使重做失效
105
- - 原生 textarea 行为保留(IME、selection、移动端触屏、粘贴)
319
+ ## 可访问性
106
320
 
107
- ## Demo
321
+ - 覆盖层(高亮 `pre`、行号、占位符)全部 `aria-hidden`,读屏只读内部 `textarea`。
322
+ - `aria-label` / `aria-labelledby` / `aria-describedby` 会转发到 textarea;`placeholder` 同时写 `aria-placeholder`。
323
+ - 焦点环默认画在宿主上(`:focus-within`),可用 `--pd-focus-ring: none` 关闭。
324
+ - `Tab` 默认用于缩进(pd 编辑语义);按一下 `Escape` 后的下一次 `Tab` 会移动焦点。表单/无障碍优先场景直接设 `tab-behavior="focus"`。
325
+ - 支持 `readOnly` / `disabled` / `maxLength`;`inputmode` / `enterkeyhint` 转发到内部 textarea(移动端键盘/发送键);`prefers-color-scheme` 由宿主主题决定。
326
+ - 移动端若宿主字号 < 16px,iOS 聚焦会缩放页面;可在 `@media (pointer: coarse)` 里设 `--pd-font-size: 16px`。
327
+
328
+ ---
329
+
330
+ ## 排版性能
331
+
332
+ - 输入热路径不做强制布局读取(高度/滚动同步交给 `ResizeObserver`、`scroll` 事件与程序化操作)。
333
+ - 2000 行 / 31k 字符的 pd 文档,单次输入渲染约 **10–13 ms**(Chromium,本机);输入框场景通常远小于此。
334
+ - 超过 `highlightThreshold`(默认 10 万字符)自动降级为纯文本渲染,避免每键全量 tokenize。
335
+ - 行号是可选层;开启后会多一层透明文本以保持软换行高度(仅在需要时付费)。
108
336
 
109
- 两种方式:
337
+ ---
338
+
339
+ ## Demo
110
340
 
111
341
  ```bash
112
- # ① 预构建(推荐:直接打开,无需服务)
113
- pnpm --filter @andares/pdeditor build:demo # 产物 demo-dist/(index.html + assets/)
114
- # 直接双击 demo-dist/index.html(或 firefox.localhost 打开)即可
342
+ # 单文件、双击即可打开(file:// 可用,无需服务器)
343
+ pnpm --filter @andares/pdeditor build:demo
344
+ # → packages/editor/demo-dist/index.html
115
345
 
116
- # ② vite dev(开发迭代:热更新)
117
- pnpm --filter @andares/pdeditor dev # 访问终端输出的端口(默认 http://localhost:5173/)
346
+ # 开发热更新
347
+ pnpm --filter @andares/pdeditor dev
118
348
  ```
119
349
 
120
- ⚠️ 端口被占用时 vite 自动换端口并提示;若浏览器报脚本 MIME 拦截/404,先确认访问的是 vite 输出的端口(其他项目的 dev server 可能占用了默认端口)。
350
+ demo 包含:低 UI 的聊天框/侧栏/衬线字体嵌入、一层基础工具栏 UI、各框架用法、以及**浏览器几何自检**(覆盖层对齐、滚动同步、占位符、aria、行号)。
121
351
 
122
- ## 开发
352
+ ---
123
353
 
124
- ```bash
125
- pnpm install # workspace 安装
126
- pnpm --filter @andares/pdeditor test # vitest + jsdom
127
- pnpm --filter @andares/pdeditor build # vite lib(ESM + CJS + d.ts)
354
+ ## 各框架
355
+
356
+ React 用上面的 `/react` 入口。其余框架直接绑定 property / 事件:
357
+
358
+ ```jsx
359
+ // Preact(原生 input 事件是 composed,能穿过 shadow;复杂属性用 ref 赋值)
360
+ import "@andares/pdeditor/element/auto";
361
+ <pd-editor ref={ref} onInput={() => setValue(ref.current.value)} />
362
+ ```
363
+
364
+ ```jsx
365
+ // Solid
366
+ import "@andares/pdeditor/element/auto";
367
+ <pd-editor ref={el} prop:value={value()} on:pd-input={(e) => setValue(e.detail.value)} />
368
+ ```
369
+
370
+ ```vue
371
+ <!-- Vue 3:需要把 pd-editor 标记为自定义元素 -->
372
+ <!-- vite.config.ts: vue({ template: { compilerOptions: { isCustomElement: t => t === "pd-editor" } } }) -->
373
+ <pd-editor :value="value" @pd-input="value = $event.detail.value" />
374
+ ```
375
+
376
+ ```svelte
377
+ <script>
378
+ import "@andares/pdeditor/element/auto";
379
+ let el;
380
+ </script>
381
+ <pd-editor bind:this={el} on:pd-input={(e) => (value = e.detail.value)} />
128
382
  ```
129
383
 
130
- ## 未来方向(不在本包当前范围)
384
+ > 复杂值(`highlight` 函数、对象)在 React 18 的自定义元素上会被字符串化;用官方 React 包装或 `ref` + property 赋值。
385
+
386
+ ---
387
+
388
+ ## 浏览器支持
389
+
390
+ - 现代 evergreen:Chrome/Edge、Firefox、Safari。
391
+ - `color-mix`:Chrome 111+ / Firefox 113+ / Safari 16.2+(不支持时退回透明度,不影响功能)。
392
+ - `scrollbar-gutter`:Chrome 94+ / Firefox 97+ / Safari 18.2+(旧 Safari 在出现滚动条时可能有 ~15px 换行差异,可用 overlay 滚动条或固定宽度规避)。
393
+ - `adoptedStyleSheets` 不可用时退回 `<style>`;`ElementInternals` 不可用时表单关联自动降级。
394
+ - 不支持 IE。
395
+
396
+ ## CSP
397
+
398
+ - 引擎的几何样式通过内联 style 属性设置(CSSOM),shadow 默认优先 constructable stylesheet。
399
+ - 严格 CSP 环境可改用 light 模式并自备 token CSS;或允许 `style-src 'unsafe-inline'`。
400
+ - Demo 是单文件内联脚本,仅用于展示。
401
+
402
+ ---
403
+
404
+ ## 从 0.4.x 升级
405
+
406
+ - 移除 Yace,改为内置 overlay 引擎;`createPdEditor` API 保持兼容(新增可选键)。
407
+ - 默认排版从「16px 等宽」改为 **inherit**;若原来依赖默认字号,请显式传 `styles.fontSize` / `fontFamily`。
408
+ - `padding` 现在应用到覆盖层(`styles.padding` 行为更符合“无 host 污染”),不再写挂载点。
409
+ - `setValue` 默认重置撤销历史;CRLF 归一为 LF;`onValueChange` 在撤销/重做时也会触发。
410
+ - 新增 `<pd-editor>` / `/element` / `/element/auto` / `/react` 入口;见上文。
411
+
412
+ ## 变更记录
131
413
 
132
- - 成品输入框(headless 核心 + UI 层):格式切换器、Ctrl+G 放大模式、历史记录、工具栏
133
- - 主题系统(CSS 变量)
134
- - 移动端/触屏专项、协同(Yjs)
414
+ - [`CHANGELOG.md`](./CHANGELOG.md)(开发期条目写 `CHANGELOG.dev.md`,`pnpm release-editor` 归档)
package/dist/core.d.ts CHANGED
@@ -1,12 +1,18 @@
1
- import { type Highlighter } from "yace";
2
- export type EditorLang = "pd" | "md" | "xml" | "json" | "yaml";
1
+ /**
2
+ * 语言标识。保留常用值补全,同时允许自定义语言名(BYO highlighter 场景):
3
+ * `language` 本身是字符串,内置高亮器只识别 pd/md/xml/json/yaml。
4
+ */
5
+ export type EditorLang = "pd" | "md" | "xml" | "json" | "yaml" | (string & {});
6
+ /** Tab 行为:indent = 插入/整行缩进(pd 编辑语义);focus = 原生焦点移动(表单/无障碍优先) */
7
+ export type TabBehavior = "indent" | "focus";
3
8
  export interface PdEditorOptions {
4
9
  value?: string;
5
10
  language?: EditorLang;
6
11
  lineNumbers?: boolean;
7
12
  /**
8
- * Yace 根节点样式。字体、行高、字距和 padding 等几何样式必须从根节点统一传入,
9
- * 以保证透明 textarea 与高亮 pre 使用同一套排版参数。
13
+ * 排版样式(camelCase 或 CSS 变量)。字体、行高、字距、padding 等几何样式
14
+ * 由引擎统一应用到覆盖层,**不要**再分别设置内部 textarea / pre。
15
+ * `padding` 类样式会同时应用到高亮层/输入层(不会被挂载点吞掉)。
10
16
  */
11
17
  styles?: Record<string, string>;
12
18
  /** 缩进单位(Tab 整行缩进 / 续行 `- ` 用;默认两个空格) */
@@ -14,23 +20,72 @@ export interface PdEditorOptions {
14
20
  /** 外部注入的 highlighter(BYO,覆盖内置)——lang 参数由内部传入 */
15
21
  highlight?: (source: string, lang: EditorLang) => string;
16
22
  onValueChange?: (value: string) => void;
23
+ /** 空值占位符(由覆盖层渲染;不依赖 textarea 的透明文字) */
24
+ placeholder?: string;
25
+ readOnly?: boolean;
26
+ disabled?: boolean;
27
+ maxLength?: number;
28
+ /** 默认 false(代码/提示词输入不参与拼写检查) */
29
+ spellcheck?: boolean;
30
+ /**
31
+ * Tab 行为。默认 indent(保持 pd 编辑语义);按下 Escape 后的下一次 Tab
32
+ * 会移动焦点(键盘用户的逃生通道)。focus = Tab 始终移动焦点。
33
+ */
34
+ tabBehavior?: TabBehavior;
35
+ /** 撤销记忆步数(不含初始值;默认 5) */
36
+ historyLimit?: number;
37
+ /** 撤销快照落栈防抖毫秒(默认 1000;连续打字合并为一步) */
38
+ historyDebounce?: number;
39
+ /** 超过该字符数降级为纯文本高亮(避免超大文档每键全量 tokenize;默认 100000) */
40
+ highlightThreshold?: number;
41
+ ariaLabel?: string;
42
+ ariaLabelledby?: string;
43
+ ariaDescribedby?: string;
44
+ dir?: string;
17
45
  }
18
46
  export interface PdEditorInstance {
19
- setValue(value: string): void;
47
+ setValue(value: string, options?: {
48
+ resetHistory?: boolean;
49
+ }): void;
20
50
  getValue(): string;
21
51
  setLanguage(lang: EditorLang): void;
52
+ setPlaceholder(text: string): void;
53
+ setReadOnly(readOnly: boolean): void;
54
+ setDisabled(disabled: boolean): void;
55
+ setLineNumbers(enabled: boolean): void;
56
+ /** 运行时更新选项(样式/padding/语言/只读/行号/缩进等) */
57
+ update(options: Partial<PdEditorOptions>): void;
58
+ focus(): void;
59
+ blur(): void;
60
+ undo(): void;
61
+ redo(): void;
62
+ insertText(text: string): void;
63
+ getSelection(): {
64
+ start: number;
65
+ end: number;
66
+ };
67
+ setSelection(start: number, end?: number): void;
22
68
  destroy(): void;
23
69
  /** 原生 textarea 元素(外部可自行监听事件) */
24
70
  textarea: HTMLTextAreaElement;
71
+ /** 引擎根节点(宿主样式/事件挂载点;自定义元素据此暴露 part) */
72
+ root: HTMLElement;
73
+ /** 高亮覆盖层(调试/自定义渲染用) */
74
+ pre: HTMLElement;
25
75
  }
26
- /** HTML 转义(纯文本高亮回退 / 各入口共用) */
76
+ /** HTML 转义(纯文本高亮回退 / sizer / 各入口共用) */
27
77
  export declare function escapeHtml(s: string): string;
78
+ /** 换行归一化:textarea 的 value setter 会把 CRLF/CR 归一为 LF,引擎内部保持一致 */
79
+ export declare function normalizeEol(s: string): string;
28
80
  /**
29
- * Headless 提示词输入框核心(基于 Yace)——各入口的公共实现。
30
- * 高亮管线由入口注入(getHighlighter):全量入口 pd 自研 + 其余 Prism,
31
- * pd-only 入口仅 pd 高亮 + 其余纯文本——由此实现按入口裁剪依赖(Prism 隔离)。
32
- * - 只渲染输入框内容(高亮层 + 原生 textarea),无任何 UI/chrome
33
- * - 语言切换是 API 行为(setLanguage),不做 UI 切换器
34
- * - 核心维护覆盖层排版不变量;外部框架定义容器外观与 token 配色
81
+ * Headless 低 UI 编辑器核心——DOM/排版/输入管线。
82
+ *
83
+ * 设计:
84
+ * - **不修改挂载点样式**:只在挂载点内追加 `.pd-root`,宿主布局/边框/padding 全归宿主;
85
+ * - **纯 CSS 自适应高度**:in-flow 的隐藏 sizer 决定 root 高度;textarea/pre 绝对铺满。
86
+ * 宿主给 `height/max-height` 时自动切换为“内滚 + 覆盖层同步”,无需 JS 测高;
87
+ * - **低 UI 默认**:字体/字号/颜色全部 inherit,只有 line-height 1.5 是温和默认值;
88
+ * - **主题**走 CSS 变量(--pd-caret-color / --pd-placeholder-opacity / --pd-gutter-color),
89
+ * token 配色由外部 CSS 决定(element 入口提供中性默认主题)。
35
90
  */
36
- export declare function createCoreEditor(el: HTMLElement, options: PdEditorOptions, getHighlighter: (lang: EditorLang) => Highlighter): PdEditorInstance;
91
+ export declare function createCoreEditor(el: HTMLElement, options: PdEditorOptions, getHighlighter: (lang: EditorLang) => (value: string) => string): PdEditorInstance;