xiaodao-editor 0.1.19 → 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.
- package/README.ZH.md +191 -68
- package/README.md +230 -78
- package/dist/block-editor.js +10444 -20908
- package/dist/block-editor.umd.cjs +30 -285
- package/dist/core/Editor.d.ts +32 -0
- package/dist/core/command/primitiveCommands.d.ts +1 -1
- package/dist/core/plugin/Plugin.d.ts +45 -5
- package/dist/core/state/EditorState.d.ts +5 -2
- package/dist/core/state/invert.d.ts +2 -2
- package/dist/core/types.d.ts +1 -1
- package/dist/extensions/Equation.d.ts +73 -8
- package/dist/extensions/Image.d.ts +54 -17
- package/dist/extensions/OrderedList.d.ts +1 -1
- package/dist/extensions/Paragraph.d.ts +1 -1
- package/dist/extensions/Table.d.ts +1 -1
- package/dist/extensions/math/ast.d.ts +140 -0
- package/dist/extensions/math/index.d.ts +18 -0
- package/dist/extensions/math/parser.d.ts +5 -0
- package/dist/extensions/math/renderHtml.d.ts +5 -0
- package/dist/extensions/math/renderTree.d.ts +16 -0
- package/dist/extensions/math/renderVNode.d.ts +4 -0
- package/dist/extensions/math/symbols.d.ts +37 -0
- package/dist/extensions/math/tokens.d.ts +22 -0
- package/dist/extensions/tableModel.d.ts +6 -6
- package/dist/i18n.d.ts +2 -2
- package/dist/index.d.ts +6 -3
- package/dist/style.css +1 -1
- package/dist/view/BlockEditor.vue.d.ts +0 -30
- package/dist/view/BlockList.vue.d.ts +2 -2
- package/dist/view/context.d.ts +11 -2
- package/dist/view/imageUpload.d.ts +20 -14
- package/dist/view/ui/icons.d.ts +22 -22
- package/dist/view/ui/inputRulesEngine.d.ts +2 -2
- package/dist/view/ui/popup.d.ts +1 -1
- package/dist/view/urlUtils.d.ts +2 -2
- 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
|
|
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
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
69
|
+
`toolbarPosition` prop: `'auto'` (default: top on desktop, bottom on
|
|
65
70
|
mobile), `'top'` (always top), `'bottom'` (always bottom), or `'float'`
|
|
66
|
-
(desktop only
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
90
|
+
- **i18n**: `zh-CN` (default) and `en-US` via the `locale` prop; zero-dep
|
|
86
91
|
translation module (no `vue-i18n`)
|
|
87
|
-
- **Theming
|
|
92
|
+
- **Theming**: `light` (default) and `dark` via the `theme` prop; CSS
|
|
88
93
|
variables for all design tokens
|
|
89
|
-
- **Accessible
|
|
90
|
-
- **Table of contents
|
|
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
|
|
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
|
|
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
|
|
128
|
-
| ----------------- |
|
|
129
|
-
| `modelValue` | `DocumentData`
|
|
130
|
-
| `extensions` | `readonly Extension[]`
|
|
131
|
-
| `editable` | `boolean`
|
|
132
|
-
| `placeholder` | `string`
|
|
133
|
-
| `theme` | `'light' \| 'dark'`
|
|
134
|
-
| `locale` | `'zh-CN' \| 'en-US'`
|
|
135
|
-
| `
|
|
136
|
-
| `
|
|
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
|
|
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
|
|
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
|
|
190
|
-
| `ImageExtension` | `image` | `content: 'none'`; attrs `src/alt/title/width/height/caption/fileId`; serialize → HTML `<figure>`/`<img>` + Markdown ``; replace + drag-resize handle + editable caption; upload side-channel via `
|
|
191
|
-
| `EquationExtension` | `equation` | `content: 'none'`; isolated block
|
|
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 ``; 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
|
|
195
|
-
| `KeymapExtension` |
|
|
196
|
-
| `HistoryExtension` |
|
|
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
|
|
219
|
-
content: InlineSeq
|
|
220
|
-
children: BlockId[]
|
|
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[]
|
|
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
|
|
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
|
|
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()
|
|
472
|
+
(`Editor.toMarkdown()` / `Editor.setDocFromMarkdown()`: operates straight
|
|
323
473
|
on `DocState`, no intermediate `BlockData`).
|
|
324
|
-
- **`src/view
|
|
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
|
|
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
|
|
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
|
-
-
|
|
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
|