@one2x/medeo-rte 0.4.6 → 0.4.8

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,22 +1,17 @@
1
1
  # medeo-rte
2
2
 
3
- 自绘的文档渲染引擎。不使用 `contenteditable` —— 文档是一条字符流,排版自己算出每个字的坐标,绘制后端可替换。
4
-
5
- 同一份排版结果可以画到 DOM,也可以一键切到 Canvas,光标和选区不变。
3
+ 自绘的文档渲染引擎。不使用 `contenteditable`。文档是一条字符流,排版自己算出每个字的坐标,绘制后端可在 HTML 和 Canvas 之间切换,光标和选区不变。
6
4
 
7
5
  在线 demo:<https://editor-html-demo.pages.dev/>
8
6
 
9
- ## 仓库结构
10
-
11
7
  ```
12
8
  src/ SDK。发布出去的就是这部分
13
- demo/ 演示应用。同时是 SDK 的用法样例,不参与发布
9
+ demo/ 演示应用,也是用法样例,不参与发布
14
10
  ```
15
11
 
16
- SDK 只负责编辑区本身。工具栏、状态栏、小地图、基线网格这些**宿主外壳**全在 `demo/`,
17
- 产品接入时应自己实现——照着 `demo/main.ts` 抄即可。模板混排样例在 `demo/vblocks.ts`。
12
+ SDK 只负责编辑区。工具栏、状态栏、小地图由宿主自己做,可参考 `demo/main.ts`。
18
13
 
19
- ## 在其它项目里使用
14
+ ## 安装
20
15
 
21
16
  ```bash
22
17
  npm i @one2x/medeo-rte
@@ -27,19 +22,14 @@ import { Editor } from '@one2x/medeo-rte';
27
22
  import '@one2x/medeo-rte/style.css';
28
23
 
29
24
  const editor = new Editor(document.getElementById('app')!);
30
-
31
- // 下命令
32
25
  editor.commands.toggleBold();
33
-
34
- // 同步自己的工具栏
35
26
  editor.on('statechange', () => renderToolbar(editor.getState()));
36
27
 
37
- // 存取内容(Slice 可 JSON 序列化)
38
28
  const json = editor.getContent();
39
29
  editor.setContent(json);
40
30
  ```
41
31
 
42
- **公式需要单独引入**,因为它依赖 KaTeX + MathLive,是包体积的大头:
32
+ 公式依赖 KaTeX 和 MathLive,需单独引入,否则不会打进产物:
43
33
 
44
34
  ```ts
45
35
  import { builtinEmbedPlugins, Editor } from '@one2x/medeo-rte';
@@ -48,231 +38,330 @@ import { formulaPlugin } from '@one2x/medeo-rte/formula';
48
38
  new Editor(el, { plugins: [...builtinEmbedPlugins, formulaPlugin] });
49
39
  ```
50
40
 
51
- 不引入就完全不会打进产物(`katex` / `mathlive` 是可选 peer 依赖)。
41
+ ## 能力
42
+
43
+ - 文字与段落:加粗、斜体、下划线、删除线、字号、颜色、标题、对齐、列表、缩进、链接
44
+ - 整篇默认字号、字体、字重、行高(`textDefaults`)
45
+ - 表格、图片、视频、代码块、公式、图文绕排
46
+ - Markdown 边打边转、撤销重做、中文输入法
47
+ - 两种芯片:`internalSlot`(SDK 内置)、`hostSlot`(宿主自绘)。整颗换行
48
+ - `mark`:盖在正文上的样式区间,按字折行。底色、边框、圆角、内外边距、悬停、空态灰字、生成中状态
49
+ - 同步块:选区提成可复用内容,改一处全变
50
+ - 绘制后端:`html` / `canvas`
51
+
52
+ | | `internalSlot` | `hostSlot` | `mark` |
53
+ | --- | --- | --- | --- |
54
+ | 是什么 | SDK 内置芯片 | 宿主自绘芯片 | 样式叠层,字仍是正文 |
55
+ | 落在哪 | 流上一个 `*` | 同上 | 不占字符,区间 `[start, end)` |
56
+ | 折行 | 整颗盒子 | 整颗盒子 | 按字折行 |
57
+ | 插入 | `commands.insertSlot` | `insertHostEmbed` / `insertSlice` | `insertVirtualBlock` / `insertSlice` |
58
+
59
+ 更细的 mark 模型见 [docs/virtual-blocks.md](./docs/virtual-blocks.md)。
60
+
61
+ ## 构造参数
62
+
63
+ `new Editor(el, options?)`。
52
64
 
53
- ## 主要能力
65
+ | 字段 | 默认 | 含义 |
66
+ | --- | --- | --- |
67
+ | `doc` | 空文档 | 初始 `DocumentModel` |
68
+ | `paint` | `'html'` | 绘制后端,`'html'` 或 `'canvas'` |
69
+ | `plugins` | `builtinEmbedPlugins` | 嵌入插件。公式要另外追加 `formulaPlugin` |
70
+ | `width` | `'fill'` | `'fill'` 撑满容器并随容器重排;数字是固定内容宽(像素) |
71
+ | `padding` | `{ x: 72, y: 56 }` | 版心内边距。紧凑输入框可传 `{ x: 0, y: 0 }` |
72
+ | `hostEmbed` | 无 | 宿主渲染器。提供后才能插入 `hostSlot` |
73
+ | `placeholder` | 无 | 空文档占位文案。组合输入一开始就隐藏 |
74
+ | `textDefaults` | 16px / `#1f2329` | 没写文字样式时的默认属性,见下表 |
75
+ | `shortcuts` | `SHORTCUTS` | 快捷键表。输入框用 `inputShortcuts()` |
76
+ | `autofocus` | `true` | 创建后是否聚焦。嵌在页面里应传 `false` |
77
+ | `chrome` | 实心光标、扁平选区 | 编辑器装饰,见「全局 chrome」。不是 mark 的皮 |
78
+ | `perfPanel` | `false` | 左下角性能面板。也可用 `setPerfPanel` |
79
+
80
+ ### textDefaults
81
+
82
+ 写在文档上,主题切换用 `setTextDefaults`。`fontFamily` 不要用 `system-ui`。`lineHeight` 只在这里生效,写在单个文字 span 上会被忽略。
83
+
84
+ | 字段 | 含义 |
85
+ | --- | --- |
86
+ | `fontSize` | 字号,像素。默认 16 |
87
+ | `fontFamily` | 字体栈。会写成 `--mini-font-family`,测宽和绘制用同一套 |
88
+ | `fontWeight` | 字重。默认 400。`bold: true` 时绘制仍用 700 |
89
+ | `color` | 默认字色 `#1f2329` |
90
+ | `bold` / `italic` | 默认是否加粗、斜体 |
91
+ | `lineHeight` | 数字是字号倍数;`'24px'` 是固定像素。不传按 `round(字号 × 1.6)` |
92
+ | `lineGap` | 行与行之间的额外间距,像素。留在行盒外面,下一行整行下移。不传是 0。用来让折行后的边框不再贴在一起 |
54
93
 
55
- 文字与段落样式、列表、表格、链接、图文绕排、Markdown 边打边转、撤销重做、中文 IME。
94
+ ## CSS 变量
56
95
 
57
- 嵌入对象走同一套插件接口:图片、视频、代码块、公式、同步块引用,以及两种芯片。**名字都带 slot,用途完全不同,不要混用。** 模板里可点、可生成、按字折行的那截文字用 **`mark`**,不是芯片。
96
+ 都写在 `.mini-editor` 上,不要写到 `:root`。
58
97
 
59
- ### 两种芯片对照(接入必读)
98
+ 全局 chrome 和 `textDefaults` 会改其中一部分。光标高度、选区高度只有 CSS,没有 chrome 字段:宿主自己设在 `.mini-editor` 上。
60
99
 
61
- | | `internalSlot` | `hostSlot` |
100
+ | 变量 | 谁来写 | 含义 |
62
101
  | --- | --- | --- |
63
- | 是什么 | SDK 内置芯片(`slotPlugin`) | 宿主自绘芯片 |
64
- | 落在哪 | `EmbedEntity`,流上一个 `*` | 同上 |
65
- | 内容 | 实体字段(label / slotValue 等) | `hostType` + `data`,DOM 由宿主画 |
66
- | 折行 | 整颗盒子,放不下换行 | 同左 |
67
- | 谁用 | demo / 简单内置控件 | **产品业务胶囊**(下拉、`@`、上传等) |
68
- | 插入 | `commands.insertSlot('text'\|'image'\|'select')` | `commands.insertHostEmbed(...)` 或 `insertSlice` 的 `entities` |
102
+ | `--mini-caret-color` | `chrome.caretColor` | 实心光标颜色。默认 `#09090b` |
103
+ | `--mini-caret-width` | `chrome.caretWidth` | 实心光标宽度。默认 `2px` |
104
+ | `--mini-caret-height` | 只能 CSS | 实心光标高度,像素。不设则铺满行高,设了就在行内居中收短。彩虹光标不用 |
105
+ | `--mini-sel-color` | `chrome.selectionColor` | 选区高亮色。默认 `rgba(43, 108, 255, 0.22)` |
106
+ | `--mini-sel-radius` | `chrome.selectionRadius` | 选区圆角。默认 `0px` |
107
+ | `--mini-sel-height` | 只能 CSS | 选区高亮和选区焦点标记的高度,像素。不设则铺满行高,设了就居中收短。焦点圆点贴着收短后的顶 |
108
+ | `--mini-sel-focus-color` | SDK 固定写入 | 选区焦点标记颜色,固定 `#2b6cff`。chrome 不能改 |
109
+ | `--mini-ime-underline-color` | `chrome.composeUnderlineColor` | 中文输入中间态下划线颜色。默认 `#1f2329` |
110
+ | `--mini-ime-underline-width` | `chrome.composeUnderlineWidth` | 下划线粗细。默认 `2px` |
111
+ | `--mini-font-family` | `textDefaults.fontFamily` | 编辑器默认字体栈 |
112
+ | `--mini-font-weight` | `textDefaults.fontWeight` | 编辑器默认字重 |
113
+ | `--mini-content-width` | SDK 按排版写入 | 内容宽。宿主不要自己设 |
69
114
 
70
- 另外还有 **同步块**(fragment / ref):选区提成可复用内容,改一处全变。芯片整颗换行;要按字折行、点击出菜单、展示生成中,用下面的 `mark`。
115
+ 另外:`data-caret="solid"|"rainbow"` 由 `chrome.caret` 写入;`data-surface="paper"|"transparent"` 由 `chrome.surfaceBackground` 写入。
71
116
 
72
- ## 虚拟块怎么用(业务侧)
117
+ 画布和 `.mini-page-wrap` 左右各多留 5px(负 margin 抵消,文字位置不变),给贴边的 mark 内边距用。覆盖 `.mini-page-wrap` 的 `padding` 时保留 `padding-inline: 5px; margin-inline: -5px;`,否则文字会左移 5px,贴边的内边距会被裁。
73
118
 
74
- 虚拟块是盖在字符流上的假块:负责皮(背景、圆角)和「整段选中」交互。区间里的字仍是文档正文。用户打字**不会**新建虚拟块,只能由业务 API 插入。
119
+ ## 全局 chrome
75
120
 
76
- 两种 kind:
121
+ `EditorChrome`,构造时的 `chrome` 或之后的 `setChrome(patch)`。`setChrome` 只覆盖传入的字段。
77
122
 
78
- - **`mark`**:纯样式叠层(高亮底色、边框、文字缺省色等)。点击落在字上,块内方向键逐字走。从外侧朝块内按方向键不会走进去,而是先进入选中态(高亮整段文字,焦点留在来的这一侧)。从内侧走到边界再往外按,同样先进入选中态,焦点留在要出去的这一侧。选中态再朝块内按一次,光标进入这一侧的内侧;朝外侧再按一次,才离开到这一侧的外面。拖选或 Shift 拉出的选区不是选中态,左右键仍收到选区两端。选中态打字按选区替换整段。上下键不参与选中态,按原来的方式换行。同一条边上有多块重叠时,从外往里撞上选范围最大的,从里往外离开选范围最小的。不参与嵌套(`parentId` 恒为 `null`),可与其他块任意叠放。光标停在左右边界时都分内外两侧:从块内走到边界或点在底色里是**内侧**,输入算区间内(背景跟着延伸,遇到换行就断);选中态再朝外侧按一次,或点在底色外,是**外侧**(光标画在底色外),输入的文字留在块外、同一行。其他方式落到边界(`setSelection`、撤销等)时,右边界默认内侧、左边界默认外侧。没有 `placeholder` 时,区间内文字删光后块自动移除;有 `placeholder` 时,只删这块里的字会收成零宽,灰字由 SDK 画出来,不进正文,方向键也不把它当成整段选中。选区比这块更大(含全选)再删除会把这块拆掉;已经是零宽灰字时,光标在它上面再按退格或删除也会拆掉。样式只需配 `chrome.background`、`chrome.radius`(圆角,默认 3;折行时首行的上两角和末行的下两角是圆角,倒数第二行右缘超出最后一行时它的右下角也圆,其余角为直角,单行自然四角全圆)、`chrome.paddingX`(水平内边距,不传按 4px,传 `0` 表示不留)、`chrome.hoverBackground`(鼠标悬停时整段含折行各行换成这个底色)和 `chrome.placeholderColor`(灰字颜色,不传是 `#c5ccd6`):
123
+ 这是编辑器装饰:光标、选区、输入法下划线、空文档占位、纸张底。和 mark 的 `chrome` 不是一回事。mark 的皮不能通过这里设,也不能通过上面的 CSS 变量设。
79
124
 
80
- ```ts
81
- editor.insertVirtualBlock({
82
- kind: 'mark',
83
- chrome: { background: '#fff3bf', hoverBackground: '#ffe066', radius: 4, paddingX: 5 }
84
- });
85
- ```
125
+ | 字段 | 默认 | 含义 |
126
+ | --- | --- | --- |
127
+ | `caret` | `'solid'` | `'solid'` 实心光标;`'rainbow'` 彩虹光标 |
128
+ | `caretColor` | `#09090b` | 实心光标颜色 |
129
+ | `caretWidth` | `2` | 实心光标宽度,像素 |
130
+ | `caretTrail` | `false` | 是否画光标残影 |
131
+ | `selectionColor` | `rgba(43, 108, 255, 0.22)` | 选区高亮色。彩虹光标且未指定时,demo 用渐变选区 |
132
+ | `selectionRadius` | `0` | 选区圆角,像素 |
133
+ | `composeUnderlineColor` | `#1f2329` | 中文输入中间态下划线颜色 |
134
+ | `composeUnderlineWidth` | `2` | 下划线粗细,像素 |
135
+ | `placeholderColor` | | 空文档占位颜色 |
136
+ | `placeholderOpacity` | | 空文档占位透明度,0–1 |
137
+ | `surfaceBackground` | `'paper'` | `'paper'` 纸张白;`'transparent'` 透出宿主背景 |
86
138
 
87
- `paddingX` 留在底色里面,每一行的字都按它缩进。mark 真正的起点前、终点后,这段空白还占排版宽度,前后文字和相邻 mark 不会压在底色上,光标在内外侧的位置也正好是下一个字落下的位置。还没写完的行,底色铺到内容区右缘。SDK 默认让画布和 `.mini-page-wrap` 左右各多出 5px(负 margin 抵消,文字位置不变),所以贴边的 `paddingX` 在 5px 以内开箱即用。**宿主若覆盖 `.mini-page-wrap` 的 `padding`,请保留 `padding-inline: 5px; margin-inline: -5px;`,否则文字会左移 5px、贴边的内边距会被裁**。不支持垂直内边距。
139
+ 光标高度、选区高度不在这张表里,用 `--mini-caret-height`、`--mini-sel-height`。
88
140
 
89
- **空态。** `placeholder` 不是正文,复制、保存、`getContent()` 里都没有这段字。只删这块里的字,或 `replaceMarkText(id, '')`,块留在原下标,两种光标亲和打字都会写进块里。选区超出这块再删,或光标停在灰字上再按退格、删除,块会被拆掉。没传 `placeholder` 的 mark 不能建成零宽,删光即拆掉。
141
+ ## mark 的 chrome
90
142
 
91
- ```ts
92
- const id = editor.insertVirtualBlock({
93
- kind: 'mark',
94
- placeholder: 'Describe a scene',
95
- chrome: { background: '#fff3bf', placeholderColor: '#c5ccd6' }
96
- });
97
- // 光标在这块里(含空块、内侧边界)时:
98
- editor.on('markenter', ({ id }) => {});
99
- editor.on('markleave', ({ id }) => {});
100
- // 点击这块。锁定中不发,但光标仍会落下。
101
- editor.on('markclick', ({ id }) => openMenu(id));
102
- ```
143
+ 写在每个 mark 的 `chrome` 上。`updateVirtualBlock(id, chrome)` 按字段合并,没传的字段保持原值。
103
144
 
104
- **中间态(生成中)。** `setMarkStatus` 只活在编辑器里,不进文档、撤销栈、复制和 `getContent()`。`label` 盖住正文;比正文宽时这一行临时撑开,清掉状态后宽度恢复。`locked: true` 时,块内和内侧边界上的输入、删除、粘贴、回车不生效,点击也不发 `markclick`;外侧打字仍然落在块外。生成结束后先清状态,再用 `replaceMarkText` 把结果写回去,这一步单独占一次撤销。
145
+ 静态底色和静态边框每一层都画。悬停时,只有指针下最上面、并且配了悬停底色或悬停边框的那一块换皮,它的每一行一起换。压在下面的 mark 继续画静态底色和静态边框,包括露在外面的其它行。灰字颜色仍是指针下每一层一起换。指针落在 mark 里的 `internalSlot` 或 `hostSlot` 上时,mark 不换悬停皮,只留芯片自己的悬停。
105
146
 
106
- ```ts
107
- editor.setMarkStatus(id, { label: 'Generating…', locked: true, text: 'rainbow', border: 'pulse' });
108
- editor.setMarkStatus(id, null);
109
- editor.replaceMarkText(id, 'a red bicycle');
110
- ```
147
+ 折行后的边:上下边每段都画;左边只在首段,右边只在末段。圆角是首段左侧两角、末段右侧两角;只有一段时四角都圆。
111
148
 
112
- `text: 'rainbow'` 只作用于 `label`。`border: 'pulse'` 是底色边缘的呼吸描边。HTML 用 CSS 动画,canvas 由 SDK 逐帧重画。块被拆掉或 `setContent` 换文档后,状态自动丢掉。
149
+ | 字段 | 默认 | 含义 |
150
+ | --- | --- | --- |
151
+ | `background` | 无 | 静态底色 |
152
+ | `hoverBackground` | 无 | 悬停底色。只换指针下最上面那一块,这一块的每一行一起换。不进撤销 |
153
+ | `border` | 无 | 静态边,`{ color, width? }`。`width` 不传是 1。不传就没有边 |
154
+ | `hoverBorder` | 沿用 `border` | 悬停边,形状同 `border`。只换指针下最上面那一块,这一块的每一行一起换。不进撤销 |
155
+ | `selectedBackground` | 无 | 方向键整段选中这块时的底色。光标只是停在里面不换。没传就仍用 `background`。不进撤销 |
156
+ | `selectedBorder` | 沿用 `border` | 同上时的边。指针正悬停在这块上时,悬停皮盖过选中皮。不进撤销 |
157
+ | `radius` | `3` | 圆角,像素 |
158
+ | `paddingX` | `4` | 水平内边距,留在底色里面。每一行文字都缩进;区间真正的首尾这段空白还占排版宽度。传 `0` 表示不留 |
159
+ | `paddingY` | `0` | 上下内边距,留在这块自己的底色里。盖住的行按同一行里最大的值变高,文字居中。没配的内层仍贴着文字。不画进相邻行 |
160
+ | `marginX` | `0` | 底色边缘到外面文字的间距,前后同一段。只加在区间真正的首尾 |
161
+ | `color` | | 文字颜色。这个字自己有颜色时用正文,否则用这里 |
162
+ | `fontSize` | | 字号。传了就盖过正文,没传才继承。占位灰字和生成中的文案同样 |
163
+ | `fontFamily` | | 字体栈。优先级同 `fontSize` |
164
+ | `fontWeight` | | 字重。优先级同 `fontSize` |
165
+ | `bold` | | 加粗。优先级同 `fontSize` |
166
+ | `italic` | | 斜体。优先级同 `fontSize` |
167
+ | `placeholderColor` | `#c5ccd6` | 空态灰字颜色。透明度写在颜色里 |
168
+ | `hoverPlaceholderColor` | 无 | 悬停时灰字颜色。不进撤销。生成中的文案不换 |
169
+ | `selectable` | `true` | 光标能否进入。`false` 时左右、上下和点击都不会把光标留在里面:mark 整段跳过,芯片不选中、停在旁边 |
113
170
 
114
- 点击后的菜单位置用 `getVirtualBlockRect(id)`,返回的是屏幕坐标,不用再向 SDK 要点击点。
171
+ 多层重叠时,字体字段是内层盖过外层:内层写了的用内层,没写的用外层。
115
172
 
116
- `mark` 是目前唯一的虚拟块。`parentId` 恒为 `null`,多块可以重叠。日常编辑里业务**不用维护** `[start, end)`,插删字时 SDK 自动挪区间。
173
+ ### mark 怎么用
117
174
 
118
- ### 路径 A:整段模板一次插入(推荐)
175
+ 虚拟块目前只有 `mark`。`parentId` 恒为 `null`,可以重叠。用户打字不会新建 mark。日常编辑不用自己维护 `[start, end)`,插删字时 SDK 会挪区间。
119
176
 
120
- 文案 + mark + 胶囊一起走 `insertSlice`。`virtualBlocks` 里的下标是**相对这段 `text`** 的半开区间:
177
+ 从外侧朝块内按方向键,只有配了 `selectedBackground` 或 `selectedBorder` 的 mark 会先整段选中,焦点留在来的这一侧;再朝内按一次才进入。没配选中皮的 mark 跳过这一步,方向键直接走进或走出。从内侧走到边界再往外,同样只对配了选中皮的块先整段选中。拖选或 Shift 不是这种选中态。上下键不走这套。重叠时,从外往里选范围最大、且配了选中皮的那一块,从里往外选范围最小、且配了选中皮的那一块。这种选中态不画选区高亮和焦点竖线,只换 mark 自己的选中皮。拖选和 Shift 仍画选区。光标只是停在 mark 里面时不换选中皮。
121
178
 
122
- ```ts
123
- editor.insertSlice({
124
- text: 'Write a prompt for a red bicycle.\n',
125
- styles: [],
126
- entities: [],
127
- virtualBlocks: [
128
- {
129
- id: 'phrase',
130
- parentId: null,
131
- kind: 'mark',
132
- start: 19,
133
- end: 33,
134
- placeholder: 'a red bicycle',
135
- chrome: { background: '#fff3bf', placeholderColor: '#c5ccd6' }
136
- }
137
- ]
138
- });
139
- ```
179
+ 光标落在块内的芯片上时,芯片用自己实体上的 `chrome` 换选中皮。选中变化发 `markselect` 或 `slotselect`。
140
180
 
141
- 可点击生成的样例见 `demo/vblocks.ts` 和 demo 右下角「插入可生成 mark」。
181
+ 边界分内外侧。内侧输入算在块里,外侧输入留在块外。选区正好盖住一块 mark 时,焦点竖线和普通选区一样,停在焦点那一端的光标位置。
142
182
 
143
- ### 路径 B:给已有字套皮
183
+ 没有 `placeholder` 时,区间里的字删光,块就拆掉。有 `placeholder` 时,只删这块里的字会收成零宽灰字,灰字不进正文。配了选中皮的零宽 mark,方向键会先停在选中态,再进灰字;没配的直接进出。选区比这块更大再删,或光标已经在灰字上、或正停在这块选中态时再按退格、删除,块会被拆掉。
144
184
 
145
185
  ```ts
146
- // 不传 start/end → 用当前选区
147
- editor.insertVirtualBlock({
186
+ const id = editor.insertVirtualBlock({
148
187
  kind: 'mark',
149
188
  placeholder: 'Describe a scene',
150
- chrome: { background: '#fff3bf', placeholderColor: '#c5ccd6' }
189
+ chrome: {
190
+ background: '#fff3bf',
191
+ hoverBackground: '#ffe066',
192
+ border: { color: '#d4a012', width: 1 },
193
+ hoverBorder: { color: '#926000', width: 1 },
194
+ selectedBackground: 'rgba(43, 108, 255, 0.16)',
195
+ selectedBorder: { color: '#2b6cff', width: 1 },
196
+ radius: 4,
197
+ paddingX: 5,
198
+ paddingY: 2,
199
+ marginX: 8,
200
+ placeholderColor: '#c5ccd6'
201
+ }
151
202
  });
152
-
153
- // 也可显式下标
154
- editor.insertVirtualBlock({ kind: 'mark', start: 0, end: 20, chrome: { background: '#fff3bf' } });
155
203
  ```
156
204
 
157
- 可配的 `chrome` 字段:`background`、`hoverBackground`(仅 mark)、`radius`、`borderColor` / `borderWidth`、`paddingX`(仅水平)、文字缺省(`color`、`fontSize`、`fontFamily`、`fontWeight`、`bold`、`italic`)、`placeholderColor`(mark 的灰字)、`hoverPlaceholderColor`(悬停时的灰字颜色,不进撤销)。
205
+ 生成中用 `setMarkStatus`,不进文档和撤销。有 `label` 就是锁定:有字的 mark 整块换成 `label`,里面的字、芯片、内层 mark 都不显示,宽度按 `label` 算,光标进不去,不能改,点击也不发 `markclick`;`locked` 已忽略。结束后先清状态,再用 `replaceMarkText` 写回,这一步单独占一次撤销。
158
206
 
159
- 改皮 / 拆皮:`updateVirtualBlock(id, chrome)`、`removeVirtualBlock(id)`(只拆皮,字和胶囊留下)。
160
-
161
- ### 业务浮层(Sample / AI 等)
162
-
163
- SDK 不画业务菜单。宿主自己:
164
-
165
- 1. 听 `markclick`,参数是 `{ id }`。锁定中的 mark 不发这个事件
166
- 2. `editor.getVirtualBlockRect(id)` 拿屏幕坐标(clientX/Y,含滚动)定位浮层:`left/top/width/height` 是外接矩形,折行的块还有 `lines`;下拉放在 `left` / `top + height` 即可。滚动或重排后(`layoutchange`)重新取。不需要点击点坐标
167
- 3. 生成中:`setMarkStatus(id, { label, locked: true, text: 'rainbow', border: 'pulse' })`
168
- 4. 写回:`setMarkStatus(id, null)` 然后 `replaceMarkText(id, text)`,这一步单独占一次撤销
207
+ ```ts
208
+ editor.setMarkStatus(id, { label: 'Generating…', locked: true, text: 'rainbow', border: 'pulse' });
209
+ editor.setMarkStatus(id, null);
210
+ editor.replaceMarkText(id, 'a red bicycle');
211
+ ```
169
212
 
170
- ### 和 host 胶囊一起用
213
+ `text: 'rainbow'` 只作用于 `label`。`border: 'pulse'` 是底色边缘的呼吸描边。块被拆掉或 `setContent` 之后,状态自动丢掉。
171
214
 
172
- 胶囊是流上的 `*`,可以落在 mark 覆盖范围内,但**不是**虚拟块的子节点。自定义芯片:
215
+ 整段模板用 `insertSlice`,`virtualBlocks` 里的下标相对这段 `text`。已有文字用 `insertVirtualBlock`,不传 `start` / `end` 就是当前选区。
173
216
 
174
- ```ts
175
- const editor = new Editor(el, {
176
- hostEmbed: {
177
- mount(container, ctx) {
178
- // 按 ctx.entity.hostType 画自己的组件;实体 kind 为 'hostSlot'
179
- return { update(next) {}, destroy() {} };
180
- }
181
- }
182
- });
183
- editor.commands.insertHostEmbed('Upscale', { label: '@Upscale' }, { width: 88, height: 24 });
184
- ```
217
+ 点击后的菜单位置用 `getVirtualBlockRect(id)`,返回屏幕坐标。
185
218
 
186
- 更细的模型见 [virtual-blocks.md](./docs/virtual-blocks.md)。
219
+ ## 编辑器 API
187
220
 
188
- ### API 一览
221
+ 下面是 `Editor` 上宿主会用到的全部公开成员。
189
222
 
190
223
  | 成员 | 说明 |
191
224
  | --- | --- |
192
- | `new Editor(el, options?)` | `doc` / `paint` / `plugins` / `hostEmbed` / `width` / `padding` / `placeholder` / `textDefaults` / `shortcuts` / `autofocus` / `chrome` |
193
- | `commands` | 公开编辑命令(加粗、插表、插图、`insertHostEmbed`、同步块等),宿主工具栏调这些 |
194
- | `on(event, fn)` | `change` / `selectionchange` / `statechange` / `layoutchange` 无参数;`markclick` / `markenter` / `markleave` 收到 `{ id }`。返回退订函数 |
195
- | `getState()` | 当前状态快照,用于同步 UI |
196
- | `getContent()` / `setContent()` | 整篇内容读写,`Slice` 结构(含 `virtualBlocks`) |
197
- | `insertSlice(slice, at?)` | 插入切片;模板混排走这条 |
198
- | `insertVirtualBlock` / `updateVirtualBlock` / `removeVirtualBlock` | 套皮、改 chrome、拆皮。mark 带 `placeholder` 时 `start === end` 可以,用来放一块空灰字 |
199
- | `setMarkStatus(id, status \| null)` | mark 的中间态:`label` / `locked` / `text: 'rainbow'` / `border: 'pulse'`。不进文档和撤销 |
200
- | `replaceMarkText(id, text)` | 换掉 mark 里的字,块和 id 留下,整步一次撤销。空串按有没有 placeholder 收成灰字或拆块 |
201
- | `insertHostEmbed(hostType, data?, size?, at?)` | 插入宿主自绘胶囊 |
202
- | `getSelection()` / `setSelection()` | 选区读写 |
203
- | `getLayout()` | 排版几何(只读),做标尺、小地图、槽上浮层用 |
204
- | `getDocument()` | 文档模型,做批量操作或自定义命令用 |
205
- | `setPaintMode('html'\|'canvas')` | 切换绘制后端 |
206
- | `setChrome` / `setTextDefaults` | 光标选区外观;默认字号/颜色(主题切换) |
207
- | `root` / `scroller` / `pageElement` | 根节点 / 滚动容器 / 内容定位上下文 |
208
- | `focus()` / `destroy()` | |
209
- | `setPlaceholder(text)` | 空态占位;组合输入一开始即隐藏 |
210
- | `composing()` / `setDisabled()` | 组合态查询;禁用输入与快捷键 |
211
- | `INPUT_SHORTCUTS` / `inputShortcuts()` | 输入框快捷键;默认 Enter 换行,Cmd+Enter 留给宿主 |
212
-
213
- 脚本化改文档也可 `import { operations, runOperation } from '@one2x/medeo-rte'`,不要直接改 `DocumentModel` 的内部表。
225
+ | `root` | 根节点 `.mini-editor` |
226
+ | `scroller` | 滚动容器 |
227
+ | `pageElement` | 内容定位上下文 |
228
+ | `paint` | 当前绘制后端,`'html'` 或 `'canvas'` |
229
+ | `commands` | 工具栏命令,见下表 |
230
+ | `perf` | 性能探针。`start()` / `report()` / `stop()`,默认关闭 |
231
+ | `on(event, fn)` | 订阅。返回退订函数。事件见下表 |
232
+ | `getState()` | 工具栏用的状态快照 |
233
+ | `getContent()` / `setContent(slice)` | 整篇读写。`setContent` 清空撤销栈,并丢掉全部 `setMarkStatus` |
234
+ | `getDocument()` | 文档模型。改文档走 `operations` + `runOperation`,不要直接改内部表 |
235
+ | `getLayout()` | 只读排版几何 |
236
+ | `getSelection()` / `setSelection(sel)` | 选区读写 |
237
+ | `insertSlice(slice, at?)` | 插入切片。不传 `at` 用当前光标 |
238
+ | `insertVirtualBlock(spec)` | 套一层 mark。返回 id;建不成(没有范围、也没有 placeholder)时返回 `null` |
239
+ | `updateVirtualBlock(id, chrome)` | 按字段合并这块的 chrome,进撤销 |
240
+ | `removeVirtualBlock(id)` | 拆掉皮,字留下 |
241
+ | `replaceMarkText(id, text)` | 换掉 mark 里的字,块和 id 留下,整步一次撤销。返回块是否存在 |
242
+ | `setMarkStatus(id, status \| null)` | 生成中状态。不进文档、撤销和 `change` |
243
+ | `getVirtualBlockRect(id)` | 屏幕坐标。`left/top/width/height` 是外接矩形,`lines` 是每一行。没有这块时返回 `null` |
244
+ | `insertHostEmbed(hostType, data?, size?, at?, chrome?)` | 插入宿主芯片。`at` 为下标、`'start'`、`'end'` 或 `'cursor'`(默认)。`chrome` 与 mark 同一套 `VirtualBlockChrome`。SDK 在芯片外框上画底色、边框、圆角和字体;悬停、选中的优先级和 mark 一样。`paddingX` / `paddingY` / `marginX` 和占位灰字只参与 mark 的排版 |
245
+ | `listEmbeds()` | 文档里全部嵌入对象,按位置排序 |
246
+ | `setPaintMode('html' \| 'canvas')` | 切换绘制后端 |
247
+ | `setChrome(patch)` | 合并全局 chrome |
248
+ | `setTextDefaults(patch)` | 合并默认文字属性 |
249
+ | `setPlaceholder(text)` | 改空文档占位 |
250
+ | `setPerfPanel(on)` | 开关性能面板 |
251
+ | `setDisabled(disabled)` | 禁用输入和快捷键 |
252
+ | `focus()` | 聚焦 |
253
+ | `composing()` | 是否正在中文输入组合 |
254
+ | `destroy()` | 卸下编辑器 |
255
+
256
+ ### commands
257
+
258
+ 宿主工具栏调这些。方向键、删除、全选也在 `commands` 上,但通常留给键盘。
259
+
260
+ | 命令 | 作用 |
261
+ | --- | --- |
262
+ | `toggleBold` / `toggleItalic` / `toggleUnderline` / `toggleStrike` | 切换文字样式 |
263
+ | `setFontSize(size)` / `setColor(color)` | 字号、颜色 |
264
+ | `setHeading(0\|1\|2\|3)` | 标题。`0` 是正文 |
265
+ | `setAlign('left'\|'center'\|'right')` | 对齐 |
266
+ | `toggleList('ul'\|'ol')` | 列表 |
267
+ | `indent(delta)` | 缩进。`delta` 为 `1` 或 `-1` |
268
+ | `insertImage(file)` / `insertVideo(file)` | 插入本地文件 |
269
+ | `insertCode()` / `insertFormula()` | 代码块、公式 |
270
+ | `insertSlot('text'\|'image'\|'select')` | 内置芯片 |
271
+ | `insertHostEmbed(hostType, data?, size?)` | 宿主芯片。要指定落点用编辑器上的 `insertHostEmbed` |
272
+ | `insertTable` / `addRow` / `addCol` / `removeRow` / `removeCol` | 表格 |
273
+ | `toggleLink` | 链接 |
274
+ | `promoteRef` / `insertRef` / `unlinkRef` | 把选区收成同步块、再插入一处、解除 |
275
+ | `undo` / `redo` | 撤销、重做 |
276
+
277
+ 默认快捷键(`SHORTCUTS`):Cmd/Ctrl+Z 撤销,Shift+Cmd/Ctrl+Z 或 Cmd/Ctrl+Y 重做,Cmd/Ctrl+B/I/U/K/A 为加粗、斜体、下划线、链接、全选。Enter 换段,Backspace / Delete 删除,Tab 在列表里改层级、在表格里跳格。左右是字符,Option 是词,Cmd 左右是行首行尾,Option 上下是段,Cmd 上下是文首文尾。Shift 扩选。
278
+
279
+ 输入框用 `inputShortcuts()`:保留撤销、选区、删除和方向键,吞掉加粗和链接。默认 Enter 换行,Cmd/Ctrl+Enter 不登记,留给宿主提交。
280
+
281
+ ### 事件
282
+
283
+ | 事件 | 参数 | 何时 |
284
+ | --- | --- | --- |
285
+ | `change` | 无 | 文档内容变化 |
286
+ | `selectionchange` | 无 | 选区或光标移动 |
287
+ | `statechange` | 无 | `getState()` 里的字段可能变了,用来同步工具栏 |
288
+ | `layoutchange` | 无 | 重新排版完成 |
289
+ | `markclick` | `{ id }` | 点击 mark。锁定中不发,光标仍会落下 |
290
+ | `markenter` | `{ id }` | 光标进入 mark,含空块和内侧边界 |
291
+ | `markleave` | `{ id }` | 光标离开 mark |
292
+ | `markselect` | `{ id }` | 光标所在最上层 mark 变了。`id` 为 `null` 表示当前没有 mark 选中。不进撤销 |
293
+ | `slotselect` | `{ id }` | 光标落到 `internalSlot` 或 `hostSlot` 上,或从芯片上离开。芯片比盖住它的 mark 更靠上,这时 mark 不换选中皮 |
294
+
295
+ ### getState()
296
+
297
+ | 字段 | 含义 |
298
+ | --- | --- |
299
+ | `bold` / `italic` / `underline` / `strikethrough` | 光标处是否已有该样式 |
300
+ | `fontSize` / `color` | 光标处字号、颜色 |
301
+ | `heading` | `0`–`3` |
302
+ | `align` | `'left'` / `'center'` / `'right'` |
303
+ | `list` | `'none'` / `'ul'` / `'ol'` |
304
+ | `inTable` | 光标是否在表格里 |
305
+ | `canUndo` / `canRedo` | 能否撤销、重做 |
306
+ | `paintMode` | `'html'` 或 `'canvas'` |
307
+ | `hasLink` | 光标是否在链接里 |
308
+ | `canPromote` / `canInsertRef` / `canUnlink` | 同步块三个按钮是否可用 |
309
+ | `selection` | 主文档选区 |
310
+ | `docLength` | 文档长度(spacer 数) |
311
+ | `inRefEdit` | 是否正在编辑同步块内部 |
312
+ | `refSelection` | 同步块内部选区。只在 `inRefEdit` 时有意义 |
313
+
314
+ 脚本化改文档用 `import { operations, runOperation } from '@one2x/medeo-rte'`。
214
315
 
215
316
  ## 性能面板
216
317
 
217
- 常驻在页面左下角,每次输入上屏后显示这一次的**上屏延迟**(按键 → 下一帧画完)、**编辑器耗时**(编辑器自身同步 JS)和**排队时间**(事件等主线程的时间),每项后面的圆点按阈值显示绿 / 黄 / 红。3 秒没有输入显示 `—`。不存历史,关闭时零开销。
218
-
219
318
  ```ts
220
- new Editor(el, { perfPanel: true }); // 或运行时 editor.setPerfPanel(true / false)
319
+ new Editor(el, { perfPanel: true });
320
+ editor.setPerfPanel(false);
221
321
  ```
222
322
 
223
- 上屏延迟高而编辑器耗时低,说明慢的不是编辑器(机器、其他页面逻辑或渲染)。demo 默认开启。面板可拖动;单击缩小/展开。
224
-
225
- ## 性能探针(开发用)
323
+ 每次输入上屏后显示上屏延迟(按键到下一帧画完)、编辑器耗时(同步 JS)、排队时间(事件等主线程)。绿 / 黄 / 红按阈值。3 秒没有输入显示 `—`。demo 默认开启。
226
324
 
227
- 默认关闭、零开销。在浏览器控制台里:
325
+ 开发探针默认关闭:
228
326
 
229
327
  ```js
230
- editor.perf.start() // demo 里 window.editor 已暴露;业务里用自己的 editor 实例
231
- // 连续打字 / 按住方向键 / 中文输入法连续上屏 / 按快捷键……
232
- editor.perf.report() // 输出各阶段耗时(平均 / p95 / 最大 ms)和帧率
328
+ editor.perf.start()
329
+ editor.perf.report()
233
330
  editor.perf.stop()
234
331
  ```
235
332
 
236
- 详细读法与历次优化记录见 [docs/performance.md](./docs/performance.md)。输出包含:`insertText`、`handleComposition`、`moveCaret` 等入口的同步耗时;`relayout`、`paintDoc`、`paintChrome`、`scrollCaret` 等阶段耗时;`xxx→下一帧`(处理开始到下一帧开始,近似输入延迟);以及帧率、帧间隔 p95、掉帧数。120Hz 屏的帧预算是 8.3ms。
333
+ 读法见 [docs/performance.md](./docs/performance.md)。
237
334
 
238
335
  ## 设计文档
239
336
 
240
- `docs/` 下是分层架构与技术方案:
241
-
242
337
  | 文档 | 内容 |
243
338
  | --- | --- |
244
- | [architecture.md](./docs/architecture.md) | 分层结构、五条不变式、样式模型 |
245
- | [mutations.md](./docs/mutations.md) | 原子操作的类型、坐标语义、三套 handler 契约 |
246
- | [operations.md](./docs/operations.md) | 编排层规范与 Operation 清单 |
247
- | [edits.md](./docs/edits.md) | 事务边界、撤销栈、片段坐标空间、协同预留 |
248
- | [rendering.md](./docs/rendering.md) | 块级增量排版、脏区带、绘制后端 |
249
- | [virtual-blocks.md](./docs/virtual-blocks.md) | 虚拟块:模板混排、文本槽、按层选区 |
250
- | [agent-segment-mapping.md](./docs/agent-segment-mapping.md) | 面向 Agent:用户的 segment 概念如何对应到真实结构与 API |
251
- | [performance.md](./docs/performance.md) | 性能目标、探针用法、长文档优化记录与待办 |
252
-
253
- 改动模型前先读 architecture.md 的不变式那一节。
339
+ | [architecture.md](./docs/architecture.md) | 分层结构、不变式、样式模型 |
340
+ | [mutations.md](./docs/mutations.md) | 原子操作 |
341
+ | [operations.md](./docs/operations.md) | 编排层与 Operation 清单 |
342
+ | [edits.md](./docs/edits.md) | 事务、撤销栈 |
343
+ | [rendering.md](./docs/rendering.md) | 增量排版与绘制后端 |
344
+ | [virtual-blocks.md](./docs/virtual-blocks.md) | mark 的模型和交互 |
345
+ | [agent-segment-mapping.md](./docs/agent-segment-mapping.md) | 业务 segment 怎么对应到 API |
346
+ | [performance.md](./docs/performance.md) | 性能探针与优化记录 |
254
347
 
255
348
  ## 本地开发
256
349
 
257
350
  ```bash
258
351
  npm install
259
- npm run dev # http://localhost:5273/ 跑 demo
352
+ npm run dev # http://localhost:5273/
260
353
  ```
261
354
 
262
- | 命令 | 作用 |
263
- | ------------------- | ----------------------------- |
264
- | `npm run dev` | 起 demo 开发服务器 |
265
- | `npm run build` | 构建 demo 站点 → `dist-demo/` |
266
- | `npm run build:lib` | 构建 SDK → `dist/`(含 `.d.ts`) |
267
- | `npm run selfcheck` | 分层自检:编辑语义、变换算术、撤销栈、快捷键表、增量排版 |
268
-
269
- ## 发布到 npm
355
+ | 命令 | 作用 |
356
+ | --- | --- |
357
+ | `npm run dev` | demo 开发服务器 |
358
+ | `npm run build` | 构建 demo → `dist-demo/` |
359
+ | `npm run build:lib` | 构建 SDK → `dist/` |
360
+ | `npm run selfcheck` | 分层自检 |
270
361
 
271
- 包名 `@one2x/medeo-rte`。在 **main** 上打与版本号一致的 tag,Actions 会自动发布。
362
+ ## 发布
272
363
 
273
- 1. 把要发的改动合进 **main**
274
- 2. 改 `package.json` 的 `version`(例如 `0.1.1`),一并合进 main
275
- 3. 在 **main** tip 打同名 tag 并推送:
364
+ 包名 `@one2x/medeo-rte`。在 main 上打与 `package.json` 的 `version` 一致的 tag,Actions 会发布。
276
365
 
277
366
  ```bash
278
367
  git checkout main && git pull
@@ -280,4 +369,4 @@ git tag v0.1.1
280
369
  git push origin v0.1.1
281
370
  ```
282
371
 
283
- tag 必须指向 main 上的提交,且 `v*` 与 `package.json` 的 `version` 一致,否则 Publish 会失败。
372
+ tag 必须指向 main,且和版本号一致,否则发布失败。