xiaodao-editor 0.1.20 → 0.1.21

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.
Files changed (36) hide show
  1. package/README.ZH.md +191 -68
  2. package/README.md +230 -78
  3. package/dist/block-editor.js +10444 -20908
  4. package/dist/block-editor.umd.cjs +30 -285
  5. package/dist/core/Editor.d.ts +32 -0
  6. package/dist/core/command/primitiveCommands.d.ts +1 -1
  7. package/dist/core/plugin/Plugin.d.ts +45 -5
  8. package/dist/core/state/EditorState.d.ts +5 -2
  9. package/dist/core/state/invert.d.ts +2 -2
  10. package/dist/core/types.d.ts +1 -1
  11. package/dist/extensions/Equation.d.ts +73 -8
  12. package/dist/extensions/Image.d.ts +54 -17
  13. package/dist/extensions/OrderedList.d.ts +1 -1
  14. package/dist/extensions/Paragraph.d.ts +1 -1
  15. package/dist/extensions/Table.d.ts +1 -1
  16. package/dist/extensions/math/ast.d.ts +140 -0
  17. package/dist/extensions/math/index.d.ts +18 -0
  18. package/dist/extensions/math/parser.d.ts +5 -0
  19. package/dist/extensions/math/renderHtml.d.ts +5 -0
  20. package/dist/extensions/math/renderTree.d.ts +16 -0
  21. package/dist/extensions/math/renderVNode.d.ts +4 -0
  22. package/dist/extensions/math/symbols.d.ts +37 -0
  23. package/dist/extensions/math/tokens.d.ts +22 -0
  24. package/dist/extensions/tableModel.d.ts +6 -6
  25. package/dist/i18n.d.ts +2 -2
  26. package/dist/index.d.ts +6 -3
  27. package/dist/style.css +1 -1
  28. package/dist/view/BlockEditor.vue.d.ts +0 -30
  29. package/dist/view/BlockList.vue.d.ts +2 -2
  30. package/dist/view/context.d.ts +11 -2
  31. package/dist/view/imageUpload.d.ts +20 -14
  32. package/dist/view/ui/icons.d.ts +22 -22
  33. package/dist/view/ui/inputRulesEngine.d.ts +2 -2
  34. package/dist/view/ui/popup.d.ts +1 -1
  35. package/dist/view/urlUtils.d.ts +2 -2
  36. package/package.json +2 -3
package/README.md CHANGED
@@ -17,19 +17,24 @@ by an **extension**, so the core never switches on a block type.
17
17
 
18
18
  ## Features
19
19
 
20
- - **12 built-in block types** paragraph, h1–h6 (heading), bullet list,
20
+ - **12 built-in block types**: paragraph, h1–h6 (heading), bullet list,
21
21
  ordered list, to-do, quote, code block, **image**, **equation** (LaTeX math),
22
22
  **divider**, **table**, **table of contents** (14 extensions total including
23
23
  Keymap and History behavior extensions)
24
- - **Equation (LaTeX math) block** renders LaTeX via KaTeX as a centered
25
- display formula. The document stores **only the raw `expression` string**
26
- KaTeX output is recomputed on the fly and never persisted, so serialization
27
- stays lean. Insert via the `/equation` slash command or the `+` menu; an empty
28
- block opens directly in edit mode. Click the block to select it; the floating
29
- button (or clicking an empty block) opens the source editor with a live
24
+ - **Equation (LaTeX math) block**: renders LaTeX as a centered display
25
+ formula via the **built-in zero-dependency math renderer** (a lightweight
26
+ LaTeX-math subset: fractions, roots, scripts, Greek letters, functions,
27
+ large operators, matrices, aligned rows). The document stores **only the raw
28
+ `expression` string**: rendered output is recomputed on the fly and never
29
+ persisted, so serialization stays lean. The renderer is **pluggable**: pass
30
+ `createEquationExtension({ renderer })` (compose it after `BuiltinExtensions`)
31
+ to swap in KaTeX, MathJax or any custom engine with full LaTeX support.
32
+ Insert via the `/equation` slash command or the `+` menu; an empty block
33
+ opens directly in edit mode. Click the block to select it; the floating ✎
34
+ button (or clicking an empty block) opens the source editor with a live
30
35
  preview. Supports block selection and **nesting as a child block** (indents
31
36
  to match its depth). Markdown export uses `$$$ … $$$` fenced blocks.
32
- - **Table block** `attrs`-based N×M grid; default 120 px column widths,
37
+ - **Table block**: `attrs`-based N×M grid; default 120 px column widths,
33
38
  new tables default to header row; row/column selection strips,
34
39
  corner-handle to select the whole table; insert dots between rows/cols;
35
40
  floating action bar with merge/split cells, **toggle header row** (sets
@@ -40,58 +45,58 @@ by an **extension**, so the core never switches on a block type.
40
45
  newline), Escape blurs; internal horizontal scrollbar à la Arco Design;
41
46
  full-rect merge-cell selection expansion so you can never select half
42
47
  of a merged cell.
43
- - **Inline marks** bold, italic, underline, strikethrough, inline code,
48
+ - **Inline marks**: bold, italic, underline, strikethrough, inline code,
44
49
  **link** (`Mod-K`, URL pasting, auto-link, popover with view/edit/copy/remove,
45
50
  href sanitization to block `javascript:` / XSS), per-selection text color
46
51
  and background color
47
- - **Block-level attrs** alignment (left/center/right/justify), text color,
52
+ - **Block-level attrs**: alignment (left/center/right/justify), text color,
48
53
  background color, indentation (0–10); image additionally carries
49
54
  `src`, `alt`, `title`, `width`, `height`, `caption`, `fileId`
50
- - **Slash menu** `/` opens a searchable command palette; input rules
55
+ - **Slash menu**: `/` opens a searchable command palette; input rules
51
56
  (`# `, `> `, `[] `, ``` ``` ````) convert blocks on the fly; `/image`
52
57
  opens the file picker
53
- - **Block manipulation** drag handle, hover toolbar, `+` insert button,
58
+ - **Block manipulation**: drag handle, hover toolbar, `+` insert button,
54
59
  grip menu with duplicate / copy / cut / move up / move down / delete;
55
60
  **real nesting** (Tab/Shift-Tab indent/outdent builds a parent–child tree;
56
61
  drag-and-drop supports before/after sibling insert plus a **drop-into**
57
- mode pause over a block's center to nest under it as its first child);
62
+ mode: pause over a block's center to nest under it as its first child);
58
63
  duplicate clones the whole subtree; image additionally exposes replace /
59
64
  remove / drag-resize corner handle with locked aspect ratio and editable
60
65
  caption
61
- - **Fixed toolbar** persistent action bar with a contextual
66
+ - **Fixed toolbar**: persistent action bar with a contextual
62
67
  **HoverToolbar** embedded inline (so text selection is preserved when
63
68
  clicking formatting buttons). Supports four placement modes via the
64
- `toolbarPosition` prop: `'auto'` (default top on desktop, bottom on
69
+ `toolbarPosition` prop: `'auto'` (default: top on desktop, bottom on
65
70
  mobile), `'top'` (always top), `'bottom'` (always bottom), or `'float'`
66
- (desktop only hides the FixedToolbar and shows a floating selection
71
+ (desktop only: hides the FixedToolbar and shows a floating selection
67
72
  toolbar that follows the text/table selection; falls back to the FixedToolbar
68
73
  on mobile). Menus (PlusMenu / BlockSettingsMenu) open downward when the
69
74
  toolbar is at the top.
70
- - **Sizing & internal scrolling** constrain the editor with `width`
75
+ - **Sizing & internal scrolling**: constrain the editor with `width`
71
76
  and `height` props (numbers are treated as pixels). The content area
72
77
  scrolls vertically inside the editor instead of growing unbounded,
73
78
  so embedding layouts stay in control of overflow.
74
- - **Clipboard** clean copy/cut/paste of HTML and plain text; multi-block
79
+ - **Clipboard**: clean copy/cut/paste of HTML and plain text; multi-block
75
80
  selection overlay; **HTML `<img>` / image-file paste + drag-and-drop
76
81
  automatically create image blocks** and dispatch the upload; selecting text
77
82
  and pasting a URL wraps it as a link
78
- - **Mobile support** long-press to start text selection, then drag your
83
+ - **Mobile support**: long-press to start text selection, then drag your
79
84
  finger to select **across multiple independent `contenteditable` blocks**
80
85
  via a hit-tested overlay (the native Selection API cannot cross block
81
86
  boundaries). The fixed toolbar auto-drops to the bottom above the virtual
82
87
  keyboard.
83
- - **History** undo/redo with typing grouping (`Mod-Z` / `Mod-Shift-Z`);
88
+ - **History**: undo/redo with typing grouping (`Mod-Z` / `Mod-Shift-Z`);
84
89
  undo restores blocks but never resurrects transient upload state
85
- - **i18n** `zh-CN` (default) and `en-US` via the `locale` prop; zero-dep
90
+ - **i18n**: `zh-CN` (default) and `en-US` via the `locale` prop; zero-dep
86
91
  translation module (no `vue-i18n`)
87
- - **Theming** `light` (default) and `dark` via the `theme` prop; CSS
92
+ - **Theming**: `light` (default) and `dark` via the `theme` prop; CSS
88
93
  variables for all design tokens
89
- - **Accessible** keyboard navigation throughout, ARIA roles on menus
90
- - **Table of contents** a live, non-editable block that renders a
94
+ - **Accessible**: keyboard navigation throughout, ARIA roles on menus
95
+ - **Table of contents**: a live, non-editable block that renders a
91
96
  hierarchical list of every heading in the document; stays in sync as
92
97
  headings are added, removed, or edited; click an entry to jump to the
93
98
  heading; insert via slash menu `/table of contents`
94
- - **Markdown import / export** the `Editor` instance exposes
99
+ - **Markdown import / export**: the `Editor` instance exposes
95
100
  `toMarkdown()` and `setDocFromMarkdown(string)`. Round-trips are
96
101
  implemented natively on top of the live `DocState` (no intermediate
97
102
  `BlockData` or external converter), so heading/list nesting, inline
@@ -106,12 +111,12 @@ npm install xiaodao-editor
106
111
 
107
112
  ```vue
108
113
  <script setup lang="ts">
109
- import { ref } from 'vue'
110
- import { BlockEditor } from 'xiaodao-editor'
111
- import type { DocumentData } from 'xiaodao-editor'
112
- import 'xiaodao-editor/style.css'
114
+ import { ref } from 'vue';
115
+ import { BlockEditor } from 'xiaodao-editor';
116
+ import type { DocumentData } from 'xiaodao-editor';
117
+ import 'xiaodao-editor/style.css';
113
118
 
114
- const doc = ref<DocumentData>({ blocks: [] })
119
+ const doc = ref<DocumentData>({ blocks: [] });
115
120
  </script>
116
121
 
117
122
  <template>
@@ -119,36 +124,181 @@ const doc = ref<DocumentData>({ blocks: [] })
119
124
  </template>
120
125
  ```
121
126
 
122
- The editor ships with all 14 built-in extensions by default no need to pass
127
+ The editor ships with all 14 built-in extensions by default: no need to pass
123
128
  `extensions` unless you want a custom set.
124
129
 
130
+ ## Pluggable equation renderer
131
+
132
+ The equation block stores only the raw LaTeX `expression` string. How that
133
+ string becomes pixels is up to a pluggable renderer:
134
+
135
+ ```ts
136
+ interface EquationRenderer {
137
+ render(expression: string, options?: { displayMode?: boolean }): EquationRenderResult;
138
+ }
139
+
140
+ interface EquationRenderResult {
141
+ /** Safe HTML string, always available (export / SSR / non-Vue consumers). */
142
+ html: string;
143
+ /** Optional Vue VNode tree; when present the view renders it directly (no innerHTML). */
144
+ vnode: VNode | VNode[] | null;
145
+ /** True when the expression has at least one error diagnostic. */
146
+ error: boolean;
147
+ diagnostics: readonly EquationDiagnostic[];
148
+ }
149
+ ```
150
+
151
+ By default the editor uses the **built-in math renderer**, a zero-dependency
152
+ implementation of a lightweight LaTeX-math subset (tokenizer → parser → AST →
153
+ DOM): numbers/identifiers, operators (`\pm \times \div \cdot \le \ge \neq …`),
154
+ superscripts/subscripts, `\frac`, `\sqrt` / `\sqrt[n]`, Greek letters,
155
+ `\sin \cos \tan \log \ln \exp \lim \min \max`, large operators
156
+ (`\sum \prod \int` with display limits), `\begin{matrix}` and
157
+ `\begin{aligned}`. Unknown commands degrade gracefully (rendered literally)
158
+ and syntax errors show an inline warning instead of crashing: the source is
159
+ always preserved and re-parses automatically once fixed. It is *not* a full
160
+ TeX engine; for that, inject an external renderer via the
161
+ `createEquationExtension({ renderer })` factory; `<BlockEditor>` itself has no
162
+ `equationRenderer` prop, because the boundary lives at the extension layer:
163
+
164
+ ```vue
165
+ <script setup lang="ts">
166
+ // KaTeX itself is NOT a dependency of xiaodao-editor, install it yourself:
167
+ // pnpm add katex
168
+ import katex from 'katex';
169
+ // ★ KaTeX's CSS MUST be imported. KaTeX produces a flat HTML tree whose
170
+ // positioning (superscripts, subscripts, integral limits, fraction bars,
171
+ // combined glyphs like ∫ with upper/lower limit, etc.) is done entirely
172
+ // by the `.katex` / `.strut` / `<sup>` / `<sub>` / `.mord` … classes. If
173
+ // the CSS is missing, every span lays out inline and "garbled" output
174
+ // like `∫ab`, `αx3`, `e−λx` (and any upper/lower limit, fraction) is the
175
+ // symptom you'll see. Load it once, in your app entry (putting it next
176
+ // to the renderer keeps the demo self-contained.
177
+ import 'katex/dist/katex.min.css';
178
+ import { createEquationExtension, BuiltinExtensions, type EquationRenderer } from 'xiaodao-editor';
179
+
180
+ const katexRenderer: EquationRenderer = {
181
+ render(expression, options) {
182
+ const src = expression ?? '';
183
+ try {
184
+ // `output: 'htmlAndMathml'` matches the project's pre-`built-in math
185
+ // engine` version (KaTeX v0.16 default): visually identical to HTML,
186
+ // and the inline `<math>` helps a11y / SSR consumers. `'html'` would
187
+ // work too but loses the MathML branch.
188
+ const html = katex.renderToString(src, {
189
+ displayMode: options?.displayMode ?? true,
190
+ throwOnError: false, // never throw: KaTeX wraps the bad fragment
191
+ // in `<span class="katex-error">…</span>` instead
192
+ trust: false, // REQUIRED: `trust: true` allows `\href` /
193
+ // `\url` to inject raw HTML (XSS). Keep off.
194
+ strict: false, // lenient: unknown commands warn but still render
195
+ output: 'htmlAndMathml',
196
+ });
197
+ // KaTeX's error sentinel class is `katex-error` (not `merror`; that
198
+ // one is from later KaTeX versions). Surface it as `error: true` so
199
+ // the equation block shows the ⚠ badge.
200
+ const error = /class="katex-error"/.test(html);
201
+ return { html, vnode: null, error, diagnostics: [] };
202
+ } catch {
203
+ return { html: '', vnode: null, error: true, diagnostics: [] };
204
+ }
205
+ },
206
+ };
207
+
208
+ // Append your renderer-bearing extension AFTER `BuiltinExtensions`. The
209
+ // extension registry is name-based deduplicated (last entry wins), so the
210
+ // appended `createEquationExtension({ renderer })` replaces the built-in's
211
+ // `EquationExtension`. Do NOT add both; pick one.
212
+ const extensions = [
213
+ ...BuiltinExtensions,
214
+ createEquationExtension({ renderer: katexRenderer }),
215
+ ];
216
+ </script>
217
+
218
+ <template>
219
+ <BlockEditor v-model="doc" :extensions="extensions" />
220
+ </template>
221
+ ```
222
+
223
+ `createEquationExtension({ renderer })` is the only supported way to override
224
+ the equation renderer; there is no component-level prop for this. The
225
+ registry deduplicates by extension `name` so the override extension must
226
+ appear **after** `BuiltinExtensions` (or anywhere later in the array) to
227
+ take effect. You can also import the built-in engine pieces (`parseMath`,
228
+ `renderMathToHtml`, …) from `xiaodao-editor` to build custom renderers on
229
+ top of the AST.
230
+
125
231
  ## Props
126
232
 
127
- | Prop | Type | Default | Description |
128
- | ----------------- | -------------------------------------- | ------------------ | ----------------------------------------------------------------------------- |
129
- | `modelValue` | `DocumentData` | `{ blocks: [] }` | The document JSON (two-way via `v-model`). |
130
- | `extensions` | `readonly Extension[]` | `BuiltinExtensions`| Extensions to register. Override to add custom blocks or strip built-ins. |
131
- | `editable` | `boolean` | `true` | Read-only mode when `false`. |
132
- | `placeholder` | `string` | locale-aware | Placeholder for the first empty block. Defaults to a localized string. |
133
- | `theme` | `'light' \| 'dark'` | `'light'` | Color theme. The class is applied to `.block-editor` and synced to `<body>`. |
134
- | `locale` | `'zh-CN' \| 'en-US'` | `'zh-CN'` | UI language. Any non-empty value other than `'zh-CN'` ⇒ `'en-US'`. |
135
- | `uploadImage` | `UploadImageHandler` | in-memory mock | Hook for image uploads. Signature: `(name, file, controller, onProgress) => Promise<ImageUploadResult>`. Consumers **must** provide this if they intend to persist documents (the default mock stores `blob:` URLs which are not serialisable). |
136
- | `width` | `string \| number` | `undefined` | Optional fixed width. A number is interpreted as CSS pixels; a string is used as-is (e.g. `'800px'`, `'100%'`). When unset, the editor fills its container (`width: 100%`). |
137
- | `height` | `string \| number` | `undefined` | Optional fixed height. When set, the editor scrolls its content area **internally** rather than growing unbounded; when unset the editor grows with content and the host page scrolls. |
233
+ | Prop | Type | Default | Description |
234
+ | ----------------- | ---------------------------------------- | ------------------ | ----------------------------------------------------------------------------- |
235
+ | `modelValue` | `DocumentData` | `{ blocks: [] }` | The document JSON (two-way via `v-model`). |
236
+ | `extensions` | `readonly Extension[]` | `BuiltinExtensions`| Extensions to register. Override to add custom blocks, replace the equation renderer (`createEquationExtension({ renderer })`), or replace the image upload pipeline (`createImageExtension({ upload, onFileCleanup })`). |
237
+ | `editable` | `boolean` | `true` | Read-only mode when `false`. |
238
+ | `placeholder` | `string` | locale-aware | Placeholder for the first empty block. Defaults to a localized string. |
239
+ | `theme` | `'light' \| 'dark'` | `'light'` | Color theme. The class is applied to `.block-editor` and synced to `<body>`. |
240
+ | `locale` | `'zh-CN' \| 'en-US'` | `'zh-CN'` | UI language. Any non-empty value other than `'zh-CN'` ⇒ `'en-US'`. |
241
+ | `width` | `string \| number` | `undefined` | Optional fixed width. A number is interpreted as CSS pixels; a string is used as-is (e.g. `'800px'`, `'100%'`). When unset, the editor fills its container (`width: 100%`). |
242
+ | `height` | `string \| number` | `undefined` | Optional fixed height. When set, the editor scrolls its content area **internally** rather than growing unbounded; when unset the editor grows with content and the host page scrolls. |
138
243
  | `toolbarPosition` | `'auto' \| 'top' \| 'bottom' \| 'float'` | `'auto'` | Placement of the toolbar / action bar. `'auto'` = top on desktop, bottom on mobile (above the virtual keyboard). `'float'` (desktop only) hides the FixedToolbar and uses a floating selection toolbar (HoverToolbar) that follows the text selection; on mobile it falls back to the auto FixedToolbar. |
139
244
 
245
+ ### Pluggable image upload
246
+
247
+ Image uploads are injected by composing `createImageExtension({ upload,
248
+ onFileCleanup })` AFTER the built-in extensions. This replaces the default
249
+ mock upload (which produces `blob:` URLs that don't survive a page reload),
250
+ giving the host full control over the upload pipeline.
251
+
252
+ ```ts
253
+ import {
254
+ BuiltinExtensions,
255
+ createImageExtension,
256
+ type UploadImageHandler,
257
+ } from 'xiaodao-editor';
258
+
259
+ const upload: UploadImageHandler = async (name, file, controller, onProgress) => {
260
+ // 1. request a signed URL from your backend
261
+ const { url, fields } = await api.presign(name);
262
+
263
+ // 2. PUT the file (with abort signal + progress)
264
+ const xhr = new XMLHttpRequest();
265
+ xhr.upload.addEventListener('progress', (e) => {
266
+ if (e.lengthComputable) onProgress(Math.round((e.loaded / e.total) * 100));
267
+ });
268
+ // ...wire controller.signal.abort into xhr.abort()...
269
+
270
+ // 3. resolve with the public URL + a stable fileId so cleanup can fire
271
+ return {
272
+ url: `${CDN}/${name}`,
273
+ width: 0, height: 0,
274
+ fileId: hashOf(name + size), // host-chosen stable id; 0 disables cleanup
275
+ };
276
+ };
277
+
278
+ const extensions = [
279
+ ...BuiltinExtensions.filter((e) => e.name !== 'image'),
280
+ createImageExtension({
281
+ upload,
282
+ onFileCleanup: (fileId) => api.deleteCloudFile(fileId),
283
+ }),
284
+ ];
285
+ ```
286
+
287
+ The default extension bundled with `BuiltinExtensions` uses an in-memory mock
288
+ upload (with a small random failure rate) that lives only as a demo and
289
+ NOT for persisted documents.
290
+
140
291
  ### Emits
141
292
 
142
293
  | Event | Payload | When |
143
294
  | ------------------------ | -------------- | ------------------------------------------------------------------------------------- |
144
295
  | `update:modelValue` | `DocumentData` | Document changed (debounced on blur). |
145
- | `cleanup:image-file` | `number` | `fileId` reference count dropped to 0 (last image block referencing it was removed or its src replaced). Payload is the `fileId`; 0 is never emitted. Consumer may reclaim cloud storage.
146
296
 
147
297
  ### Expose
148
298
 
149
299
  | Member | Type | Description |
150
300
  | -------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
151
- | `editor` | `Editor` | The framework-agnostic `Editor` instance. Useful methods: <br>`toData(): DocumentData` export JSON. <br>`setDocument(json: DocumentData)` replace JSON. <br>`toMarkdown(): string` export native Markdown. <br>`setDocFromMarkdown(md: string)` import native Markdown (resets history). |
301
+ | `editor` | `Editor` | The framework-agnostic `Editor` instance. Useful methods: <br>`toData(): DocumentData`: export JSON. <br>`setDocument(json: DocumentData)`: replace JSON. <br>`toMarkdown(): string`: export native Markdown. <br>`setDocFromMarkdown(md: string)`: import native Markdown (resets history). |
152
302
 
153
303
  ## Theming
154
304
 
@@ -164,7 +314,7 @@ latter so `<Teleport>`-ed popovers inherit them too).
164
314
  }
165
315
  ```
166
316
 
167
- The `.block-editor` element intentionally has **no background** the host
317
+ The `.block-editor` element intentionally has **no background**: the host
168
318
  page controls the editor's background so it blends into the surrounding UI.
169
319
  Set it explicitly if needed:
170
320
 
@@ -186,14 +336,14 @@ Set it explicitly if needed:
186
336
  | `OrderedListExtension`| `orderedList` | Auto-numbered; `attrs.startNumber` for explicit override. |
187
337
  | `TodoListExtension` | `todoList` | Checkbox via `attrs.checked`. |
188
338
  | `QuoteExtension` | `quote` | Blockquote. No inline italic (disabled by schema). |
189
- | `CodeBlockExtension` | `codeBlock` | `attrs.language`; isolating Enter inserts a newline. |
190
- | `ImageExtension` | `image` | `content: 'none'`; attrs `src/alt/title/width/height/caption/fileId`; serialize → HTML `<figure>`/`<img>` + Markdown `![alt](url "title")`; replace + drag-resize handle + editable caption; upload side-channel via `uploadImage` prop + `cleanup:image-file`. |
191
- | `EquationExtension` | `equation` | `content: 'none'`; isolated block stores only `attrs.expression` (raw LaTeX). KaTeX renders a centered display formula on the fly (output never persisted). Insert via `/equation` or `+`; empty block auto-opens in edit mode; floating ✎ button edits the source with live preview. Supports block selection and nesting (indents as a child; `attrs.indent` mirrors depth). Markdown export uses `$$$ … $$$` fenced blocks. |
339
+ | `CodeBlockExtension` | `codeBlock` | `attrs.language`; isolating: Enter inserts a newline. |
340
+ | `ImageExtension` | `image` | `content: 'none'`; attrs `src/alt/title/width/height/caption/fileId`; serialize → HTML `<figure>`/`<img>` + Markdown `![alt](url "title")`; replace + drag-resize handle + editable caption; upload side-channel via `createImageExtension({ upload, onFileCleanup })` (see *Pluggable image upload*). The default `ImageExtension` bundled with `BuiltinExtensions` uses an in-memory mock upload (object URLs that don't survive reload) and never invokes an `onFileCleanup` callback. |
341
+ | `EquationExtension` | `equation` | `content: 'none'`; isolated block that stores only `attrs.expression` (raw LaTeX). A pluggable renderer produces the centered display formula on the fly (output never persisted); the default is the built-in **zero-dependency math renderer** (lightweight LaTeX subset, see the *Pluggable equation renderer* section), and KaTeX/MathJax can be injected via `createEquationExtension({ renderer })` composed after `BuiltinExtensions`. Insert via `/equation` or `+`; empty block auto-opens in edit mode; floating ✎ button edits the source with live preview. Supports block selection and nesting (indents as a child; `attrs.indent` mirrors depth). Markdown export uses `$$$ … $$$` fenced blocks. |
192
342
  | `TableExtension` | `table` | `content: 'none'`; attrs `rows/cols/cells/colWidths/headerRow`; cell InlineSeq per cell with cellType/align/bgColor/rowspan/colspan; row/col selection strips + corner handle; floating toolbar with merge/split, **toggle header row**, delete row/col/table; row/col insert dots; full-rect selection expansion for merged cells. Default column width 120 px; new tables default to `headerRow: true`. |
193
343
  | `DividerExtension` | `divider` | Isolating horizontal rule. |
194
- | `TableOfContentsExtension` | `tableOfContents` | `content: 'none'`; empty attrs the heading list is a **dynamic view** computed from the editor state on every render. Non-editable block (`editable: false`); collects all `heading` blocks in document order (table-cell headings excluded automatically); click an entry to scroll the heading into view. Serialize emits empty string (the real headings are exported by their own blocks). |
195
- | `KeymapExtension` | | Enter / Backspace / ArrowUp / ArrowDown bindings. |
196
- | `HistoryExtension` | | `Mod-Z` / `Mod-Shift-Z` / `Mod-Y` undo/redo keymap. |
344
+ | `TableOfContentsExtension` | `tableOfContents` | `content: 'none'`; empty attrs: the heading list is a **dynamic view** computed from the editor state on every render. Non-editable block (`editable: false`); collects all `heading` blocks in document order (table-cell headings excluded automatically); click an entry to scroll the heading into view. Serialize emits empty string (the real headings are exported by their own blocks). |
345
+ | `KeymapExtension` | n/a | Enter / Backspace / ArrowUp / ArrowDown bindings. |
346
+ | `HistoryExtension` | n/a | `Mod-Z` / `Mod-Shift-Z` / `Mod-Y` undo/redo keymap. |
197
347
 
198
348
  To use a **custom subset**, pass `extensions` explicitly:
199
349
 
@@ -201,31 +351,31 @@ To use a **custom subset**, pass `extensions` explicitly:
201
351
  import {
202
352
  ParagraphExtension, HeadingExtension,
203
353
  KeymapExtension, HistoryExtension,
204
- } from 'xiaodao-editor'
354
+ } from 'xiaodao-editor';
205
355
 
206
356
  const extensions = [
207
357
  ParagraphExtension, HeadingExtension,
208
358
  KeymapExtension, HistoryExtension,
209
- ]
359
+ ];
210
360
  ```
211
361
 
212
362
  ## Document model
213
363
 
214
364
  ```ts
215
365
  interface Block {
216
- id: BlockId
217
- type: BlockType
218
- attrs: Attrs // e.g. { level: 2, align: 'center', color: 'red' }
219
- content: InlineSeq // text runs with optional marks
220
- children: BlockId[] // child block ids real nesting:
366
+ id: BlockId;
367
+ type: BlockType;
368
+ attrs: Attrs; // e.g. { level: 2, align: 'center', color: 'red' }
369
+ content: InlineSeq; // text runs with optional marks
370
+ children: BlockId[]; // child block ids, real nesting:
221
371
  // paragraph/heading + the 3 list kinds can be parents;
222
372
  // any block type can be a child. `attrs.indent` is a
223
373
  // derived mirror of the nesting depth.
224
374
  }
225
375
 
226
376
  interface DocumentData {
227
- id?: string
228
- blocks: BlockData[] // nested JSON; normalized on import
377
+ id?: string;
378
+ blocks: BlockData[]; // nested JSON; normalized on import
229
379
  }
230
380
  ```
231
381
 
@@ -245,7 +395,7 @@ const doc: DocumentData = {
245
395
  { type: 'codeBlock', attrs: { language: 'ts' }, content: [{ type: 'text', text: 'const x = 1' }] },
246
396
  { type: 'image', attrs: {
247
397
  src: 'https://cdn.example.com/hero.png', alt: 'Hero',
248
- width: 1200, height: 630, caption: 'Fig. 1 Architecture overview', fileId: 42,
398
+ width: 1200, height: 630, caption: 'Fig. 1: Architecture overview', fileId: 42,
249
399
  }, content: [] },
250
400
  { type: 'divider' },
251
401
  { type: 'table', attrs: {
@@ -263,7 +413,7 @@ const doc: DocumentData = {
263
413
  }, content: [] },
264
414
  { type: 'equation', attrs: { expression: 'E = mc^2' }, content: [] },
265
415
  ],
266
- }
416
+ };
267
417
  ```
268
418
 
269
419
  ## Custom extensions
@@ -276,9 +426,9 @@ serialization. A minimal block-type extension provides a schema and a Vue
276
426
  renderer:
277
427
 
278
428
  ```ts
279
- import { defineComponent, h } from 'vue'
280
- import type { Extension } from 'xiaodao-editor'
281
- import { BlockContent } from 'xiaodao-editor'
429
+ import { defineComponent, h } from 'vue';
430
+ import type { Extension } from 'xiaodao-editor';
431
+ import { BlockContent } from 'xiaodao-editor';
282
432
 
283
433
  const CalloutBlock = defineComponent({
284
434
  props: ['block', 'placeholder'],
@@ -287,9 +437,9 @@ const CalloutBlock = defineComponent({
287
437
  block: props.block,
288
438
  placeholder: props.placeholder,
289
439
  class: 'block-callout',
290
- })
440
+ });
291
441
  },
292
- })
442
+ });
293
443
 
294
444
  export const CalloutExtension: Extension = {
295
445
  name: 'callout',
@@ -302,31 +452,31 @@ export const CalloutExtension: Extension = {
302
452
  },
303
453
  },
304
454
  renderer: { component: CalloutBlock },
305
- }
455
+ };
306
456
  ```
307
457
 
308
458
  Register it alongside the built-ins:
309
459
 
310
460
  ```ts
311
- import { BuiltinExtensions, BlockEditor } from 'xiaodao-editor'
312
- import { CalloutExtension } from './callout'
461
+ import { BuiltinExtensions, BlockEditor } from 'xiaodao-editor';
462
+ import { CalloutExtension } from './callout';
313
463
 
314
- const extensions = [...BuiltinExtensions, CalloutExtension]
464
+ const extensions = [...BuiltinExtensions, CalloutExtension];
315
465
  ```
316
466
 
317
467
  ## Architecture
318
468
 
319
- - **`src/core/`** framework-agnostic engine (zero Vue imports, enforced by
469
+ - **`src/core/`**: framework-agnostic engine (zero Vue imports, enforced by
320
470
  ESLint). Owns the document model, transactions, history, commands, schema,
321
471
  extension registries, and **native Markdown import/export**
322
- (`Editor.toMarkdown()` / `Editor.setDocFromMarkdown()` operates straight
472
+ (`Editor.toMarkdown()` / `Editor.setDocFromMarkdown()`: operates straight
323
473
  on `DocState`, no intermediate `BlockData`).
324
- - **`src/view/`** Vue bridge: `BlockEditor.vue` (root), `BlockList`,
474
+ - **`src/view/`**: Vue bridge: `BlockEditor.vue` (root), `BlockList`,
325
475
  `BlockHost`, `BlockContent` (per-block `contenteditable`), and the UI
326
476
  components (`BlockHandle`, `BlockSettingsMenu`, `HoverToolbar`, `PlusMenu`,
327
477
  `OrderedListMenu`, `NumberPicker`, `CodeLangPicker`, `LinkPopover`,
328
478
  `FixedToolbar`).
329
- - **`src/extensions/`** the 14 built-in extensions plus `_commonAttrs.ts`
479
+ - **`src/extensions/`**: the 14 built-in extensions plus `_commonAttrs.ts`
330
480
  (shared align/color/bgColor/indent specs and color presets, plus
331
481
  `ImageExtension`'s upload-side-channel renderer logic). **Table** lives in
332
482
  `Table.ts` (Vue renderer + command registrations) and `tableModel.ts` (pure
@@ -334,10 +484,12 @@ const extensions = [...BuiltinExtensions, CalloutExtension]
334
484
  selection expansion, header row toggle, column width helpers, HTML/Markdown
335
485
  serialization, attrs validation/coercion). **Divider** lives in `Divider.ts`.
336
486
  **Table of contents** lives in `TableOfContents.ts` (non-editable block that
337
- renders a live heading list). **Equation** lives in `Equation.ts` (LaTeX/KaTeX
487
+ renders a live heading list). **Equation** lives in `Equation.ts` (LaTeX math
338
488
  block; stores only `attrs.expression`, renders a centered display formula; its
339
- `attrs.indent` mirrors nesting depth so it indents as a child block).
340
- - **`src/i18n.ts`** locale + theme module; provides `t(key)` via Vue's
489
+ `attrs.indent` mirrors nesting depth so it indents as a child block). The
490
+ built-in math engine lives in `extensions/math/` (tokenizer parser AST →
491
+ render tree → DOM/HTML), with zero third-party dependencies.
492
+ - **`src/i18n.ts`**: locale + theme module; provides `t(key)` via Vue's
341
493
  provide/inject so popovers rendered through `<Teleport>` stay reactive.
342
494
 
343
495
  ## Development