@dou-ui/editor 0.0.0-stage → 0.1.0-alpha.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/LICENSE +21 -0
- package/README.md +10 -2
- package/dist/catalog.js +18 -0
- package/dist/catalog.js.map +1 -0
- package/dist/code/code-editor-block.js +6 -0
- package/dist/code/code-editor-block.js.map +1 -0
- package/dist/code/code-editor-block.vue_vue_type_script_setup_true_lang.js +116 -0
- package/dist/code/code-editor-block.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/code/index.js +69 -0
- package/dist/code/index.js.map +1 -0
- package/dist/code/languages.js +51 -0
- package/dist/code/languages.js.map +1 -0
- package/dist/code/view.js +255 -0
- package/dist/code/view.js.map +1 -0
- package/dist/collaboration/binding.js +381 -0
- package/dist/collaboration/binding.js.map +1 -0
- package/dist/collaboration/cursors.js +150 -0
- package/dist/collaboration/cursors.js.map +1 -0
- package/dist/collaboration/index.js +108 -0
- package/dist/collaboration/index.js.map +1 -0
- package/dist/collaboration/order.js +88 -0
- package/dist/collaboration/order.js.map +1 -0
- package/dist/collaboration/table.js +234 -0
- package/dist/collaboration/table.js.map +1 -0
- package/dist/collaboration/text.js +91 -0
- package/dist/collaboration/text.js.map +1 -0
- package/dist/components/block-handle.js +6 -0
- package/dist/components/block-handle.js.map +1 -0
- package/dist/components/block-handle.vue_vue_type_script_setup_true_lang.js +474 -0
- package/dist/components/block-handle.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/bubble-toolbar.js +6 -0
- package/dist/components/bubble-toolbar.js.map +1 -0
- package/dist/components/bubble-toolbar.vue_vue_type_script_setup_true_lang.js +381 -0
- package/dist/components/bubble-toolbar.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/comment-button.js +6 -0
- package/dist/components/comment-button.js.map +1 -0
- package/dist/components/comment-button.vue_vue_type_script_setup_true_lang.js +83 -0
- package/dist/components/comment-button.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/comment-card.js +6 -0
- package/dist/components/comment-card.js.map +1 -0
- package/dist/components/comment-card.vue_vue_type_script_setup_true_lang.js +104 -0
- package/dist/components/comment-card.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/menu-escape.js +25 -0
- package/dist/components/menu-escape.js.map +1 -0
- package/dist/components/search-bar.js +6 -0
- package/dist/components/search-bar.js.map +1 -0
- package/dist/components/search-bar.vue_vue_type_script_setup_true_lang.js +192 -0
- package/dist/components/search-bar.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/slash-menu.js +6 -0
- package/dist/components/slash-menu.js.map +1 -0
- package/dist/components/slash-menu.vue_vue_type_script_setup_true_lang.js +164 -0
- package/dist/components/slash-menu.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/core/align.js +94 -0
- package/dist/core/align.js.map +1 -0
- package/dist/core/block-selection.js +155 -0
- package/dist/core/block-selection.js.map +1 -0
- package/dist/core/blocks.js +132 -0
- package/dist/core/blocks.js.map +1 -0
- package/dist/core/changes.js +85 -0
- package/dist/core/changes.js.map +1 -0
- package/dist/core/colors.js +16 -0
- package/dist/core/colors.js.map +1 -0
- package/dist/core/commands.js +379 -0
- package/dist/core/commands.js.map +1 -0
- package/dist/core/comments.js +186 -0
- package/dist/core/comments.js.map +1 -0
- package/dist/core/compare.js +106 -0
- package/dist/core/compare.js.map +1 -0
- package/dist/core/convert.js +28 -0
- package/dist/core/convert.js.map +1 -0
- package/dist/core/diff.js +141 -0
- package/dist/core/diff.js.map +1 -0
- package/dist/core/editor.js +138 -0
- package/dist/core/editor.js.map +1 -0
- package/dist/core/extension.js +99 -0
- package/dist/core/extension.js.map +1 -0
- package/dist/core/file-drop.js +56 -0
- package/dist/core/file-drop.js.map +1 -0
- package/dist/core/highlight.js +120 -0
- package/dist/core/highlight.js.map +1 -0
- package/dist/core/input-rules.js +61 -0
- package/dist/core/input-rules.js.map +1 -0
- package/dist/core/keymap.js +38 -0
- package/dist/core/keymap.js.map +1 -0
- package/dist/core/model.js +196 -0
- package/dist/core/model.js.map +1 -0
- package/dist/core/output.js +263 -0
- package/dist/core/output.js.map +1 -0
- package/dist/core/plugins.js +102 -0
- package/dist/core/plugins.js.map +1 -0
- package/dist/core/prepare.js +102 -0
- package/dist/core/prepare.js.map +1 -0
- package/dist/core/schema.js +158 -0
- package/dist/core/schema.js.map +1 -0
- package/dist/core/search.js +197 -0
- package/dist/core/search.js.map +1 -0
- package/dist/core/selection.js +16 -0
- package/dist/core/selection.js.map +1 -0
- package/dist/core/slash.js +83 -0
- package/dist/core/slash.js.map +1 -0
- package/dist/core/transaction.js +311 -0
- package/dist/core/transaction.js.map +1 -0
- package/dist/define-block.js +177 -0
- package/dist/define-block.js.map +1 -0
- package/dist/document.js +157 -0
- package/dist/document.js.map +1 -0
- package/dist/dom.js +48 -0
- package/dist/dom.js.map +1 -0
- package/dist/editor.js +33 -0
- package/dist/editor.js.map +1 -0
- package/dist/editor.vue_vue_type_script_setup_true_lang.js +564 -0
- package/dist/editor.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/editor2.js +6 -0
- package/dist/editor2.js.map +1 -0
- package/dist/extensions/align/align-menu.js +6 -0
- package/dist/extensions/align/align-menu.js.map +1 -0
- package/dist/extensions/align/align-menu.vue_vue_type_script_setup_true_lang.js +82 -0
- package/dist/extensions/align/align-menu.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/extensions/align/align-sub.js +6 -0
- package/dist/extensions/align/align-sub.js.map +1 -0
- package/dist/extensions/align/align-sub.vue_vue_type_script_setup_true_lang.js +57 -0
- package/dist/extensions/align/align-sub.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/extensions/align/commands.js +40 -0
- package/dist/extensions/align/commands.js.map +1 -0
- package/dist/extensions/align/index.js +27 -0
- package/dist/extensions/align/index.js.map +1 -0
- package/dist/extensions/attachment/attachment-block.js +6 -0
- package/dist/extensions/attachment/attachment-block.js.map +1 -0
- package/dist/extensions/attachment/attachment-block.vue_vue_type_script_setup_true_lang.js +119 -0
- package/dist/extensions/attachment/attachment-block.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/extensions/attachment/files.js +66 -0
- package/dist/extensions/attachment/files.js.map +1 -0
- package/dist/extensions/attachment/index.js +132 -0
- package/dist/extensions/attachment/index.js.map +1 -0
- package/dist/extensions/blocks.js +170 -0
- package/dist/extensions/blocks.js.map +1 -0
- package/dist/extensions/callout/callout-block.js +6 -0
- package/dist/extensions/callout/callout-block.js.map +1 -0
- package/dist/extensions/callout/callout-block.vue_vue_type_script_setup_true_lang.js +71 -0
- package/dist/extensions/callout/callout-block.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/extensions/callout/html.js +61 -0
- package/dist/extensions/callout/html.js.map +1 -0
- package/dist/extensions/callout/index.js +52 -0
- package/dist/extensions/callout/index.js.map +1 -0
- package/dist/extensions/callout/tones.js +12 -0
- package/dist/extensions/callout/tones.js.map +1 -0
- package/dist/extensions/code-block/code-block-view.js +6 -0
- package/dist/extensions/code-block/code-block-view.js.map +1 -0
- package/dist/extensions/code-block/code-block-view.vue_vue_type_script_setup_true_lang.js +87 -0
- package/dist/extensions/code-block/code-block-view.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/extensions/code-block/index.js +98 -0
- package/dist/extensions/code-block/index.js.map +1 -0
- package/dist/extensions/code-block/languages.js +116 -0
- package/dist/extensions/code-block/languages.js.map +1 -0
- package/dist/extensions/colors/color-menu.js +6 -0
- package/dist/extensions/colors/color-menu.js.map +1 -0
- package/dist/extensions/colors/color-menu.vue_vue_type_script_setup_true_lang.js +113 -0
- package/dist/extensions/colors/color-menu.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/extensions/colors/index.js +49 -0
- package/dist/extensions/colors/index.js.map +1 -0
- package/dist/extensions/colors/palette.js +26 -0
- package/dist/extensions/colors/palette.js.map +1 -0
- package/dist/extensions/editor-kit.js +35 -0
- package/dist/extensions/editor-kit.js.map +1 -0
- package/dist/extensions/embed/embed-block.js +6 -0
- package/dist/extensions/embed/embed-block.js.map +1 -0
- package/dist/extensions/embed/embed-block.vue_vue_type_script_setup_true_lang.js +210 -0
- package/dist/extensions/embed/embed-block.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/extensions/embed/index.js +118 -0
- package/dist/extensions/embed/index.js.map +1 -0
- package/dist/extensions/embed/targets.js +79 -0
- package/dist/extensions/embed/targets.js.map +1 -0
- package/dist/extensions/formatting.js +153 -0
- package/dist/extensions/formatting.js.map +1 -0
- package/dist/extensions/image/image-block.js +6 -0
- package/dist/extensions/image/image-block.js.map +1 -0
- package/dist/extensions/image/image-block.vue_vue_type_script_setup_true_lang.js +206 -0
- package/dist/extensions/image/image-block.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/extensions/image/index.js +125 -0
- package/dist/extensions/image/index.js.map +1 -0
- package/dist/extensions/image/uploads.js +96 -0
- package/dist/extensions/image/uploads.js.map +1 -0
- package/dist/extensions/list-html.js +58 -0
- package/dist/extensions/list-html.js.map +1 -0
- package/dist/extensions/lists.js +182 -0
- package/dist/extensions/lists.js.map +1 -0
- package/dist/extensions/mention/index.js +56 -0
- package/dist/extensions/mention/index.js.map +1 -0
- package/dist/extensions/mention/mention-menu.js +6 -0
- package/dist/extensions/mention/mention-menu.js.map +1 -0
- package/dist/extensions/mention/mention-menu.vue_vue_type_script_setup_true_lang.js +132 -0
- package/dist/extensions/mention/mention-menu.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/extensions/mention/state.js +113 -0
- package/dist/extensions/mention/state.js.map +1 -0
- package/dist/extensions/table/commands.js +356 -0
- package/dist/extensions/table/commands.js.map +1 -0
- package/dist/extensions/table/html.js +134 -0
- package/dist/extensions/table/html.js.map +1 -0
- package/dist/extensions/table/index.js +56 -0
- package/dist/extensions/table/index.js.map +1 -0
- package/dist/extensions/table/plugin.js +81 -0
- package/dist/extensions/table/plugin.js.map +1 -0
- package/dist/extensions/table/schema.js +258 -0
- package/dist/extensions/table/schema.js.map +1 -0
- package/dist/extensions/table/table-controls.js +6 -0
- package/dist/extensions/table/table-controls.js.map +1 -0
- package/dist/extensions/table/table-controls.vue_vue_type_script_setup_true_lang.js +698 -0
- package/dist/extensions/table/table-controls.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/extensions/table/view.js +19 -0
- package/dist/extensions/table/view.js.map +1 -0
- package/dist/extensions/toggle/index.js +87 -0
- package/dist/extensions/toggle/index.js.map +1 -0
- package/dist/extensions/toggle/state.js +120 -0
- package/dist/extensions/toggle/state.js.map +1 -0
- package/dist/extensions/toggle/toggle-block.js +6 -0
- package/dist/extensions/toggle/toggle-block.js.map +1 -0
- package/dist/extensions/toggle/toggle-block.vue_vue_type_script_setup_true_lang.js +42 -0
- package/dist/extensions/toggle/toggle-block.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/index.js +49 -0
- package/dist/index.js.map +1 -0
- package/dist/locale.js +522 -0
- package/dist/locale.js.map +1 -0
- package/dist/markdown/index.js +24 -0
- package/dist/markdown/index.js.map +1 -0
- package/dist/style.css +1 -0
- package/dist/style.d.ts +1 -0
- package/dist/style.js +8 -0
- package/dist/types/catalog.d.ts +8 -0
- package/dist/types/code/code-editor-block.vue.d.ts +17 -0
- package/dist/types/code/index.d.ts +18 -0
- package/dist/types/code/languages.d.ts +7 -0
- package/dist/types/code/types.d.ts +37 -0
- package/dist/types/code/view.d.ts +12 -0
- package/dist/types/collaboration/binding.d.ts +32 -0
- package/dist/types/collaboration/cursors.d.ts +26 -0
- package/dist/types/collaboration/index.d.ts +47 -0
- package/dist/types/collaboration/order.d.ts +20 -0
- package/dist/types/collaboration/table.d.ts +25 -0
- package/dist/types/collaboration/text.d.ts +9 -0
- package/dist/types/components/block-handle.vue.d.ts +3 -0
- package/dist/types/components/bubble-toolbar.vue.d.ts +7 -0
- package/dist/types/components/comment-button.vue.d.ts +3 -0
- package/dist/types/components/comment-card.vue.d.ts +3 -0
- package/dist/types/components/menu-escape.d.ts +7 -0
- package/dist/types/components/search-bar.vue.d.ts +22 -0
- package/dist/types/components/slash-menu.vue.d.ts +11 -0
- package/dist/types/core/align.d.ts +16 -0
- package/dist/types/core/block-selection.d.ts +22 -0
- package/dist/types/core/blocks.d.ts +52 -0
- package/dist/types/core/changes.d.ts +14 -0
- package/dist/types/core/colors.d.ts +4 -0
- package/dist/types/core/commands.d.ts +61 -0
- package/dist/types/core/comments.d.ts +44 -0
- package/dist/types/core/compare.d.ts +10 -0
- package/dist/types/core/convert.d.ts +12 -0
- package/dist/types/core/diff.d.ts +49 -0
- package/dist/types/core/editor.d.ts +64 -0
- package/dist/types/core/extension.d.ts +167 -0
- package/dist/types/core/file-drop.d.ts +19 -0
- package/dist/types/core/highlight.d.ts +32 -0
- package/dist/types/core/input-rules.d.ts +21 -0
- package/dist/types/core/keymap.d.ts +8 -0
- package/dist/types/core/model.d.ts +158 -0
- package/dist/types/core/output.d.ts +64 -0
- package/dist/types/core/plugins.d.ts +24 -0
- package/dist/types/core/prepare.d.ts +23 -0
- package/dist/types/core/schema.d.ts +38 -0
- package/dist/types/core/search.d.ts +83 -0
- package/dist/types/core/selection.d.ts +8 -0
- package/dist/types/core/slash.d.ts +21 -0
- package/dist/types/core/transaction.d.ts +97 -0
- package/dist/types/define-block.d.ts +66 -0
- package/dist/types/document.d.ts +46 -0
- package/dist/types/dom.d.ts +17 -0
- package/dist/types/editor.d.ts +252 -0
- package/dist/types/editor.vue.d.ts +210 -0
- package/dist/types/extensions/align/align-menu.vue.d.ts +3 -0
- package/dist/types/extensions/align/align-sub.vue.d.ts +7 -0
- package/dist/types/extensions/align/commands.d.ts +13 -0
- package/dist/types/extensions/align/index.d.ts +7 -0
- package/dist/types/extensions/attachment/attachment-block.vue.d.ts +17 -0
- package/dist/types/extensions/attachment/files.d.ts +6 -0
- package/dist/types/extensions/attachment/index.d.ts +20 -0
- package/dist/types/extensions/blocks.d.ts +8 -0
- package/dist/types/extensions/callout/callout-block.vue.d.ts +14 -0
- package/dist/types/extensions/callout/html.d.ts +4 -0
- package/dist/types/extensions/callout/index.d.ts +3 -0
- package/dist/types/extensions/callout/tones.d.ts +8 -0
- package/dist/types/extensions/code-block/code-block-view.vue.d.ts +16 -0
- package/dist/types/extensions/code-block/index.d.ts +19 -0
- package/dist/types/extensions/code-block/languages.d.ts +9 -0
- package/dist/types/extensions/colors/color-menu.vue.d.ts +3 -0
- package/dist/types/extensions/colors/index.d.ts +7 -0
- package/dist/types/extensions/colors/palette.d.ts +7 -0
- package/dist/types/extensions/editor-kit.d.ts +37 -0
- package/dist/types/extensions/embed/embed-block.vue.d.ts +16 -0
- package/dist/types/extensions/embed/index.d.ts +18 -0
- package/dist/types/extensions/embed/targets.d.ts +32 -0
- package/dist/types/extensions/formatting.d.ts +8 -0
- package/dist/types/extensions/image/image-block.vue.d.ts +16 -0
- package/dist/types/extensions/image/index.d.ts +16 -0
- package/dist/types/extensions/image/uploads.d.ts +35 -0
- package/dist/types/extensions/list-html.d.ts +11 -0
- package/dist/types/extensions/lists.d.ts +6 -0
- package/dist/types/extensions/mention/index.d.ts +11 -0
- package/dist/types/extensions/mention/mention-menu.vue.d.ts +7 -0
- package/dist/types/extensions/mention/state.d.ts +28 -0
- package/dist/types/extensions/table/commands.d.ts +69 -0
- package/dist/types/extensions/table/html.d.ts +4 -0
- package/dist/types/extensions/table/index.d.ts +8 -0
- package/dist/types/extensions/table/plugin.d.ts +4 -0
- package/dist/types/extensions/table/schema.d.ts +23 -0
- package/dist/types/extensions/table/table-controls.vue.d.ts +3 -0
- package/dist/types/extensions/table/view.d.ts +13 -0
- package/dist/types/extensions/toggle/index.d.ts +7 -0
- package/dist/types/extensions/toggle/state.d.ts +17 -0
- package/dist/types/extensions/toggle/toggle-block.vue.d.ts +14 -0
- package/dist/types/index.d.ts +266 -0
- package/dist/types/locale.d.ts +195 -0
- package/dist/types/markdown/index.d.ts +14 -0
- package/dist/types/use-comments.d.ts +17 -0
- package/dist/use-comments.js +141 -0
- package/dist/use-comments.js.map +1 -0
- package/package.json +98 -4
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { docToBlocks } from "./model.js";
|
|
2
|
+
import { diffBlocks, diffText, inlineText } from "./diff.js";
|
|
3
|
+
import { Plugin, PluginKey } from "prosemirror-state";
|
|
4
|
+
import { Decoration, DecorationSet } from "prosemirror-view";
|
|
5
|
+
// src/core/compare.ts
|
|
6
|
+
var compareKey = new PluginKey("compare");
|
|
7
|
+
var CLS = "dou-editor__diff";
|
|
8
|
+
/** The text of a removed block, as a line of its own. */
|
|
9
|
+
function removedBlock(block, label) {
|
|
10
|
+
const el = document.createElement("div");
|
|
11
|
+
el.className = `${CLS}-removed-block`;
|
|
12
|
+
el.dataset.diff = "removed";
|
|
13
|
+
const text = inlineText(block.content).replaceAll("", "");
|
|
14
|
+
const { name, caption, alt } = block.props;
|
|
15
|
+
const named = [
|
|
16
|
+
name,
|
|
17
|
+
caption,
|
|
18
|
+
alt
|
|
19
|
+
].find((value) => typeof value === "string" && value);
|
|
20
|
+
el.textContent = text || named || label(block);
|
|
21
|
+
return el;
|
|
22
|
+
}
|
|
23
|
+
function removedText(text) {
|
|
24
|
+
const el = document.createElement("del");
|
|
25
|
+
el.className = `${CLS}-removed`;
|
|
26
|
+
el.dataset.diff = "removed";
|
|
27
|
+
el.textContent = text.replaceAll("", "");
|
|
28
|
+
return el;
|
|
29
|
+
}
|
|
30
|
+
function decorate(doc, before, label) {
|
|
31
|
+
const { changes } = diffBlocks(before, docToBlocks(doc));
|
|
32
|
+
if (!changes.length) return DecorationSet.empty;
|
|
33
|
+
const byId = /* @__PURE__ */ new Map();
|
|
34
|
+
/** Removed blocks, by the id of the block they stood before (`null`: the end). */
|
|
35
|
+
const removedAt = /* @__PURE__ */ new Map();
|
|
36
|
+
for (const change of changes) if (change.type !== "removed") byId.set(change.id, change);
|
|
37
|
+
else {
|
|
38
|
+
const list = removedAt.get(change.next) ?? [];
|
|
39
|
+
list.push(change.before);
|
|
40
|
+
removedAt.set(change.next, list);
|
|
41
|
+
}
|
|
42
|
+
const decorations = [];
|
|
43
|
+
const putRemoved = (pos, id) => {
|
|
44
|
+
for (const [index, block] of (removedAt.get(id) ?? []).entries()) decorations.push(Decoration.widget(pos, () => removedBlock(block, label), {
|
|
45
|
+
side: -1,
|
|
46
|
+
key: `removed-${block.id}-${index}`,
|
|
47
|
+
ignoreSelection: true
|
|
48
|
+
}));
|
|
49
|
+
};
|
|
50
|
+
const ids = /* @__PURE__ */ new Set([...byId.keys(), ...removedAt.keys()]);
|
|
51
|
+
doc.descendants((node, pos) => {
|
|
52
|
+
if (node.type.name !== "block") return true;
|
|
53
|
+
const id = node.attrs.id;
|
|
54
|
+
if (!ids.has(id)) return true;
|
|
55
|
+
putRemoved(pos, id);
|
|
56
|
+
const change = byId.get(id);
|
|
57
|
+
if (change?.type === "added") decorations.push(Decoration.node(pos, pos + node.nodeSize, { class: `${CLS}-added` }));
|
|
58
|
+
else if (change?.type === "updated") {
|
|
59
|
+
const content = node.firstChild;
|
|
60
|
+
const sameKind = change.before.type === change.after.type && JSON.stringify(change.before.props) === JSON.stringify(change.after.props);
|
|
61
|
+
const inline = content.inlineContent && !change.after.rows;
|
|
62
|
+
const parts = inline ? diffText(inlineText(change.before.content), inlineText(change.after.content)) : [];
|
|
63
|
+
if (!sameKind || !inline || parts.every((part) => part.type === "same")) decorations.push(Decoration.node(pos, pos + node.nodeSize, { class: `${CLS}-changed` }));
|
|
64
|
+
if (inline) {
|
|
65
|
+
const start = pos + 2;
|
|
66
|
+
let offset = 0;
|
|
67
|
+
for (const [index, part] of parts.entries()) {
|
|
68
|
+
const at = start + offset;
|
|
69
|
+
if (part.type === "same") offset += part.text.length;
|
|
70
|
+
else if (part.type === "added") {
|
|
71
|
+
decorations.push(Decoration.inline(at, at + part.text.length, {
|
|
72
|
+
class: `${CLS}-added-text`,
|
|
73
|
+
"data-diff": "added"
|
|
74
|
+
}));
|
|
75
|
+
offset += part.text.length;
|
|
76
|
+
} else decorations.push(Decoration.widget(at, () => removedText(part.text), {
|
|
77
|
+
side: -1,
|
|
78
|
+
key: `removed-${id}-${index}-${part.text}`,
|
|
79
|
+
ignoreSelection: true
|
|
80
|
+
}));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return true;
|
|
85
|
+
});
|
|
86
|
+
putRemoved(doc.content.size, null);
|
|
87
|
+
return DecorationSet.create(doc, decorations);
|
|
88
|
+
}
|
|
89
|
+
/** Draws the changes from `before()`; `null` draws none. */
|
|
90
|
+
function comparePlugin(options) {
|
|
91
|
+
const build = (doc) => {
|
|
92
|
+
const before = options.before();
|
|
93
|
+
return before ? decorate(doc, before, options.label) : DecorationSet.empty;
|
|
94
|
+
};
|
|
95
|
+
return new Plugin({
|
|
96
|
+
key: compareKey,
|
|
97
|
+
state: {
|
|
98
|
+
init: (_, state) => build(state.doc),
|
|
99
|
+
apply: (tr, value) => tr.docChanged || tr.getMeta(compareKey) ? build(tr.doc) : value.map(tr.mapping, tr.doc)
|
|
100
|
+
},
|
|
101
|
+
props: { decorations: (state) => compareKey.getState(state) }
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
export { compareKey, comparePlugin };
|
|
105
|
+
|
|
106
|
+
//# sourceMappingURL=compare.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compare.js","names":[],"sources":["../../src/core/compare.ts"],"sourcesContent":["import type { Node as PMNode } from 'prosemirror-model'\nimport { Plugin, PluginKey } from 'prosemirror-state'\nimport { Decoration, DecorationSet } from 'prosemirror-view'\nimport { diffBlocks, diffText, inlineText, type BlockVersionChange } from './diff'\nimport { docToBlocks, type Block } from './model'\n\n/*\n * The document compared with an older version, drawn over the text: blocks\n * added since have a bar beside them, blocks removed show where they were,\n * struck through, and in a changed block the text added is underlined and\n * the text removed shown struck through where it was. Colors aside, each\n * kind has its own line, so they read the same to everyone.\n */\n\nexport const compareKey = new PluginKey<DecorationSet>('compare')\n\nconst CLS = 'dou-editor__diff'\n\n/** The text of a removed block, as a line of its own. */\nfunction removedBlock(block: Block, label: (block: Block) => string) {\n const el = document.createElement('div')\n el.className = `${CLS}-removed-block`\n el.dataset.diff = 'removed'\n const text = inlineText(block.content).replaceAll('', '')\n // a file, an embed or an image has no text: its name or caption stands for it\n const { name, caption, alt } = block.props as Record<string, unknown>\n const named = [name, caption, alt].find((value) => typeof value === 'string' && value)\n el.textContent = text || (named as string | undefined) || label(block)\n return el\n}\n\nfunction removedText(text: string) {\n const el = document.createElement('del')\n el.className = `${CLS}-removed`\n el.dataset.diff = 'removed'\n el.textContent = text.replaceAll('', '')\n return el\n}\n\nfunction decorate(doc: PMNode, before: readonly Block[], label: (block: Block) => string) {\n const { changes } = diffBlocks(before, docToBlocks(doc))\n if (!changes.length) return DecorationSet.empty\n const byId = new Map<string, BlockVersionChange>()\n /** Removed blocks, by the id of the block they stood before (`null`: the end). */\n const removedAt = new Map<string | null, Block[]>()\n for (const change of changes) {\n if (change.type !== 'removed') byId.set(change.id, change)\n else {\n const list = removedAt.get(change.next) ?? []\n list.push(change.before)\n removedAt.set(change.next, list)\n }\n }\n\n const decorations: Decoration[] = []\n const putRemoved = (pos: number, id: string | null) => {\n for (const [index, block] of (removedAt.get(id) ?? []).entries())\n decorations.push(\n Decoration.widget(pos, () => removedBlock(block, label), {\n side: -1,\n key: `removed-${block.id}-${index}`,\n ignoreSelection: true,\n }),\n )\n }\n // a block's id is where its changes and the removed blocks before it are found\n const ids = new Set([...byId.keys(), ...removedAt.keys()])\n doc.descendants((node, pos) => {\n if (node.type.name !== 'block') return true\n const id = node.attrs.id as string\n if (!ids.has(id)) return true\n putRemoved(pos, id)\n const change = byId.get(id)\n if (change?.type === 'added')\n decorations.push(Decoration.node(pos, pos + node.nodeSize, { class: `${CLS}-added` }))\n else if (change?.type === 'updated') {\n const content = node.firstChild!\n const sameKind =\n change.before.type === change.after.type &&\n JSON.stringify(change.before.props) === JSON.stringify(change.after.props)\n const inline = content.inlineContent && !change.after.rows\n const parts = inline\n ? diffText(inlineText(change.before.content), inlineText(change.after.content))\n : []\n // the kind, a table or a file changed, or only the formatting of the same text (a color, a link)\n if (!sameKind || !inline || parts.every((part) => part.type === 'same'))\n decorations.push(Decoration.node(pos, pos + node.nodeSize, { class: `${CLS}-changed` }))\n if (inline) {\n // the content node starts inside the block: one for the block, one for the content\n const start = pos + 2\n let offset = 0\n for (const [index, part] of parts.entries()) {\n const at = start + offset\n if (part.type === 'same') offset += part.text.length\n else if (part.type === 'added') {\n decorations.push(\n Decoration.inline(at, at + part.text.length, {\n class: `${CLS}-added-text`,\n 'data-diff': 'added',\n }),\n )\n offset += part.text.length\n } else\n decorations.push(\n Decoration.widget(at, () => removedText(part.text), {\n side: -1,\n key: `removed-${id}-${index}-${part.text}`,\n ignoreSelection: true,\n }),\n )\n }\n }\n }\n return true\n })\n putRemoved(doc.content.size, null)\n return DecorationSet.create(doc, decorations)\n}\n\n/** Draws the changes from `before()`; `null` draws none. */\nexport function comparePlugin(options: {\n before: () => readonly Block[] | null | undefined\n /** What to show for a removed block without text, such as a divider. */\n label: (block: Block) => string\n}) {\n const build = (doc: PMNode) => {\n const before = options.before()\n return before ? decorate(doc, before, options.label) : DecorationSet.empty\n }\n return new Plugin<DecorationSet>({\n key: compareKey,\n state: {\n init: (_, state) => build(state.doc),\n apply: (tr, value) =>\n tr.docChanged || tr.getMeta(compareKey) ? build(tr.doc) : value.map(tr.mapping, tr.doc),\n },\n props: {\n decorations: (state) => compareKey.getState(state),\n },\n })\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { resolveEditorExtensions } from "./extension.js";
|
|
2
|
+
import { parseHTML } from "./prepare.js";
|
|
3
|
+
import { writeHTML } from "./output.js";
|
|
4
|
+
// src/core/convert.ts
|
|
5
|
+
var resolved = /* @__PURE__ */ new WeakMap();
|
|
6
|
+
/** Resolve a list of extensions once; the same array gives the same result. */
|
|
7
|
+
function kitOf(extensions) {
|
|
8
|
+
let kit = resolved.get(extensions);
|
|
9
|
+
if (!kit) {
|
|
10
|
+
kit = resolveEditorExtensions(extensions);
|
|
11
|
+
resolved.set(extensions, kit);
|
|
12
|
+
}
|
|
13
|
+
return kit;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Write blocks as HTML with the given extensions, e.g. to show a document
|
|
17
|
+
* outside the editor. Works without a browser.
|
|
18
|
+
*/
|
|
19
|
+
function blocksToHTML(blocks, extensions) {
|
|
20
|
+
return writeHTML(blocks, kitOf(extensions));
|
|
21
|
+
}
|
|
22
|
+
/** Read HTML as blocks with the given extensions. Needs `DOMParser` (a browser, or a DOM library). */
|
|
23
|
+
function htmlToBlocks(html, extensions) {
|
|
24
|
+
return parseHTML(html, kitOf(extensions));
|
|
25
|
+
}
|
|
26
|
+
export { blocksToHTML, htmlToBlocks, kitOf };
|
|
27
|
+
|
|
28
|
+
//# sourceMappingURL=convert.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"convert.js","names":[],"sources":["../../src/core/convert.ts"],"sourcesContent":["import {\n resolveEditorExtensions,\n type EditorExtension,\n type ResolvedEditorExtensions,\n} from './extension'\nimport type { Block, PartialBlock } from './model'\nimport { writeHTML } from './output'\nimport { parseHTML } from './prepare'\n\nexport type EditorExtensionList = readonly (EditorExtension | EditorExtension[])[]\n\nconst resolved = new WeakMap<EditorExtensionList, ResolvedEditorExtensions>()\n\n/** Resolve a list of extensions once; the same array gives the same result. */\nexport function kitOf(extensions: EditorExtensionList): ResolvedEditorExtensions {\n let kit = resolved.get(extensions)\n if (!kit) {\n kit = resolveEditorExtensions(extensions)\n resolved.set(extensions, kit)\n }\n return kit\n}\n\n/**\n * Write blocks as HTML with the given extensions, e.g. to show a document\n * outside the editor. Works without a browser.\n */\nexport function blocksToHTML(blocks: PartialBlock[], extensions: EditorExtensionList): string {\n return writeHTML(blocks, kitOf(extensions))\n}\n\n/** Read HTML as blocks with the given extensions. Needs `DOMParser` (a browser, or a DOM library). */\nexport function htmlToBlocks(html: string, extensions: EditorExtensionList): Block[] {\n return parseHTML(html, kitOf(extensions))\n}\n"],"mappings":";;;;AAWA,IAAM,2BAAW,IAAI,QAAuD;;AAG5E,SAAgB,MAAM,YAA2D;CAC/E,IAAI,MAAM,SAAS,IAAI,UAAU;CACjC,IAAI,CAAC,KAAK;EACR,MAAM,wBAAwB,UAAU;EACxC,SAAS,IAAI,YAAY,GAAG;CAC9B;CACA,OAAO;AACT;;;;;AAMA,SAAgB,aAAa,QAAwB,YAAyC;CAC5F,OAAO,UAAU,QAAQ,MAAM,UAAU,CAAC;AAC5C;;AAGA,SAAgB,aAAa,MAAc,YAA0C;CACnF,OAAO,UAAU,MAAM,MAAM,UAAU,CAAC;AAC1C"}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// src/core/diff.ts
|
|
2
|
+
/** Every block of a tree, parents before their children. */
|
|
3
|
+
function flatten(blocks, out = []) {
|
|
4
|
+
for (const block of blocks) {
|
|
5
|
+
out.push(block);
|
|
6
|
+
flatten(block.children, out);
|
|
7
|
+
}
|
|
8
|
+
return out;
|
|
9
|
+
}
|
|
10
|
+
var withoutComments = (content) => content.map((item) => {
|
|
11
|
+
if (item.type !== "text" || !item.comments) return item;
|
|
12
|
+
const { comments: _, ...rest } = item;
|
|
13
|
+
return rest;
|
|
14
|
+
});
|
|
15
|
+
/** What a block shows, without its children and comments. */
|
|
16
|
+
var own = (block) => JSON.stringify([
|
|
17
|
+
block.type,
|
|
18
|
+
block.props,
|
|
19
|
+
withoutComments(block.content),
|
|
20
|
+
block.rows?.map((row) => row.cells.map((cell) => [
|
|
21
|
+
withoutComments(cell.content),
|
|
22
|
+
cell.colspan,
|
|
23
|
+
cell.rowspan
|
|
24
|
+
]))
|
|
25
|
+
]);
|
|
26
|
+
/** The blocks added, removed and changed from `before` to `after`. */
|
|
27
|
+
function diffBlocks(before, after) {
|
|
28
|
+
const old = flatten(before);
|
|
29
|
+
const now = flatten(after);
|
|
30
|
+
const oldById = new Map(old.map((block) => [block.id, block]));
|
|
31
|
+
const nowIds = new Set(now.map((block) => block.id));
|
|
32
|
+
const changes = [];
|
|
33
|
+
const added = [];
|
|
34
|
+
const removed = [];
|
|
35
|
+
const updated = [];
|
|
36
|
+
const removedBefore = /* @__PURE__ */ new Map();
|
|
37
|
+
let pending = [];
|
|
38
|
+
for (const block of old) if (nowIds.has(block.id)) {
|
|
39
|
+
if (pending.length) removedBefore.set(block.id, pending);
|
|
40
|
+
pending = [];
|
|
41
|
+
} else pending.push(block);
|
|
42
|
+
if (pending.length) removedBefore.set(null, pending);
|
|
43
|
+
const putRemoved = (next) => {
|
|
44
|
+
for (const block of removedBefore.get(next) ?? []) {
|
|
45
|
+
changes.push({
|
|
46
|
+
type: "removed",
|
|
47
|
+
id: block.id,
|
|
48
|
+
before: block,
|
|
49
|
+
next
|
|
50
|
+
});
|
|
51
|
+
removed.push(block.id);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
for (const block of now) {
|
|
55
|
+
putRemoved(block.id);
|
|
56
|
+
const was = oldById.get(block.id);
|
|
57
|
+
if (!was) {
|
|
58
|
+
changes.push({
|
|
59
|
+
type: "added",
|
|
60
|
+
id: block.id,
|
|
61
|
+
after: block
|
|
62
|
+
});
|
|
63
|
+
added.push(block.id);
|
|
64
|
+
} else if (own(was) !== own(block)) {
|
|
65
|
+
changes.push({
|
|
66
|
+
type: "updated",
|
|
67
|
+
id: block.id,
|
|
68
|
+
before: was,
|
|
69
|
+
after: block
|
|
70
|
+
});
|
|
71
|
+
updated.push(block.id);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
putRemoved(null);
|
|
75
|
+
return {
|
|
76
|
+
changes,
|
|
77
|
+
added,
|
|
78
|
+
removed,
|
|
79
|
+
updated
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* A block's inline content as text, one character for each position it
|
|
84
|
+
* takes in the editor: a line break and an inline node (a mention) are
|
|
85
|
+
* one each.
|
|
86
|
+
*/
|
|
87
|
+
function inlineText(content) {
|
|
88
|
+
let text = "";
|
|
89
|
+
for (const item of content) text += item.type === "text" ? item.text : item.type === "break" ? "\n" : "";
|
|
90
|
+
return text;
|
|
91
|
+
}
|
|
92
|
+
/** Above this many cells the texts are compared as one replaced middle, not character by character. */
|
|
93
|
+
var MAX_CELLS = 4e6;
|
|
94
|
+
/**
|
|
95
|
+
* The characters `after` added to `before` and those it removed: their
|
|
96
|
+
* longest common subsequence kept, the rest added or removed. Shared
|
|
97
|
+
* starts and ends are taken off first, so a small edit in a long text is
|
|
98
|
+
* cheap.
|
|
99
|
+
*/
|
|
100
|
+
function diffText(before, after) {
|
|
101
|
+
let start = 0;
|
|
102
|
+
while (start < before.length && start < after.length && before[start] === after[start]) start++;
|
|
103
|
+
let end = 0;
|
|
104
|
+
while (end < before.length - start && end < after.length - start && before[before.length - 1 - end] === after[after.length - 1 - end]) end++;
|
|
105
|
+
const a = before.slice(start, before.length - end);
|
|
106
|
+
const b = after.slice(start, after.length - end);
|
|
107
|
+
const parts = [];
|
|
108
|
+
const push = (type, text) => {
|
|
109
|
+
if (!text) return;
|
|
110
|
+
const last = parts[parts.length - 1];
|
|
111
|
+
if (last?.type === type) last.text += text;
|
|
112
|
+
else parts.push({
|
|
113
|
+
type,
|
|
114
|
+
text
|
|
115
|
+
});
|
|
116
|
+
};
|
|
117
|
+
push("same", before.slice(0, start));
|
|
118
|
+
if (!a.length || !b.length || a.length * b.length > MAX_CELLS) {
|
|
119
|
+
push("removed", a);
|
|
120
|
+
push("added", b);
|
|
121
|
+
} else {
|
|
122
|
+
const width = b.length + 1;
|
|
123
|
+
const table = new Uint32Array((a.length + 1) * width);
|
|
124
|
+
for (let i = a.length - 1; i >= 0; i--) for (let j = b.length - 1; j >= 0; j--) table[i * width + j] = a[i] === b[j] ? table[(i + 1) * width + j + 1] + 1 : Math.max(table[(i + 1) * width + j], table[i * width + j + 1]);
|
|
125
|
+
let i = 0;
|
|
126
|
+
let j = 0;
|
|
127
|
+
while (i < a.length && j < b.length) if (a[i] === b[j]) {
|
|
128
|
+
push("same", a[i]);
|
|
129
|
+
i++;
|
|
130
|
+
j++;
|
|
131
|
+
} else if (table[(i + 1) * width + j] >= table[i * width + j + 1]) push("removed", a[i++]);
|
|
132
|
+
else push("added", b[j++]);
|
|
133
|
+
push("removed", a.slice(i));
|
|
134
|
+
push("added", b.slice(j));
|
|
135
|
+
}
|
|
136
|
+
push("same", before.slice(before.length - end));
|
|
137
|
+
return parts;
|
|
138
|
+
}
|
|
139
|
+
export { diffBlocks, diffText, inlineText };
|
|
140
|
+
|
|
141
|
+
//# sourceMappingURL=diff.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"diff.js","names":[],"sources":["../../src/core/diff.ts"],"sourcesContent":["import type { Block, InlineContent } from './model'\n\n/*\n * What changed between two versions of a document, block by block: blocks\n * are matched by id, so a block that was edited is \"updated\" wherever it\n * moved, and one whose id is only in one version was added or removed.\n * Comments don't count as changes: they come and go apart from the text.\n */\n\n/** One block that differs between two versions. */\nexport type BlockVersionChange =\n | { type: 'added'; id: string; after: Block }\n /** `next`: the block of the newer version it stood before; `null` at the end. */\n | { type: 'removed'; id: string; before: Block; next: string | null }\n /** Its text, type, attributes or rows changed. */\n | { type: 'updated'; id: string; before: Block; after: Block }\n\nexport interface BlocksDiff {\n /** The changed blocks, in the order of the newer version, removed ones where they were. */\n changes: BlockVersionChange[]\n /** The ids of the blocks added, removed and updated, as the `transaction` event's `changes` has them. */\n added: string[]\n removed: string[]\n updated: string[]\n}\n\n/** Every block of a tree, parents before their children. */\nfunction flatten(blocks: readonly Block[], out: Block[] = []) {\n for (const block of blocks) {\n out.push(block)\n flatten(block.children, out)\n }\n return out\n}\n\nconst withoutComments = (content: readonly InlineContent[]) =>\n content.map((item) => {\n if (item.type !== 'text' || !item.comments) return item\n const { comments: _, ...rest } = item\n return rest\n })\n\n/** What a block shows, without its children and comments. */\nconst own = (block: Block) =>\n JSON.stringify([\n block.type,\n block.props,\n withoutComments(block.content),\n block.rows?.map((row) =>\n row.cells.map((cell) => [withoutComments(cell.content), cell.colspan, cell.rowspan]),\n ),\n ])\n\n/** The blocks added, removed and changed from `before` to `after`. */\nexport function diffBlocks(before: readonly Block[], after: readonly Block[]): BlocksDiff {\n const old = flatten(before)\n const now = flatten(after)\n const oldById = new Map(old.map((block) => [block.id, block]))\n const nowIds = new Set(now.map((block) => block.id))\n const changes: BlockVersionChange[] = []\n const added: string[] = []\n const removed: string[] = []\n const updated: string[] = []\n // removed blocks go before the first block that followed them and is still there\n const removedBefore = new Map<string | null, Block[]>()\n let pending: Block[] = []\n for (const block of old) {\n if (nowIds.has(block.id)) {\n if (pending.length) removedBefore.set(block.id, pending)\n pending = []\n } else pending.push(block)\n }\n if (pending.length) removedBefore.set(null, pending)\n const putRemoved = (next: string | null) => {\n for (const block of removedBefore.get(next) ?? []) {\n changes.push({ type: 'removed', id: block.id, before: block, next })\n removed.push(block.id)\n }\n }\n for (const block of now) {\n putRemoved(block.id)\n const was = oldById.get(block.id)\n if (!was) {\n changes.push({ type: 'added', id: block.id, after: block })\n added.push(block.id)\n } else if (own(was) !== own(block)) {\n changes.push({ type: 'updated', id: block.id, before: was, after: block })\n updated.push(block.id)\n }\n }\n putRemoved(null)\n return { changes, added, removed, updated }\n}\n\n/**\n * A block's inline content as text, one character for each position it\n * takes in the editor: a line break and an inline node (a mention) are\n * one each.\n */\nexport function inlineText(content: readonly InlineContent[]): string {\n let text = ''\n for (const item of content)\n text += item.type === 'text' ? item.text : item.type === 'break' ? '\\n' : ''\n return text\n}\n\n/** A piece of a text diff: kept, only in the newer text, or only in the older one. */\nexport interface TextPart {\n type: 'same' | 'added' | 'removed'\n text: string\n}\n\n/** Above this many cells the texts are compared as one replaced middle, not character by character. */\nconst MAX_CELLS = 4_000_000\n\n/**\n * The characters `after` added to `before` and those it removed: their\n * longest common subsequence kept, the rest added or removed. Shared\n * starts and ends are taken off first, so a small edit in a long text is\n * cheap.\n */\nexport function diffText(before: string, after: string): TextPart[] {\n let start = 0\n while (start < before.length && start < after.length && before[start] === after[start]) start++\n let end = 0\n while (\n end < before.length - start &&\n end < after.length - start &&\n before[before.length - 1 - end] === after[after.length - 1 - end]\n )\n end++\n const a = before.slice(start, before.length - end)\n const b = after.slice(start, after.length - end)\n const parts: TextPart[] = []\n const push = (type: TextPart['type'], text: string) => {\n if (!text) return\n const last = parts[parts.length - 1]\n if (last?.type === type) last.text += text\n else parts.push({ type, text })\n }\n push('same', before.slice(0, start))\n if (!a.length || !b.length || a.length * b.length > MAX_CELLS) {\n push('removed', a)\n push('added', b)\n } else {\n // lengths of the longest common subsequences of the suffixes\n const width = b.length + 1\n const table = new Uint32Array((a.length + 1) * width)\n for (let i = a.length - 1; i >= 0; i--)\n for (let j = b.length - 1; j >= 0; j--)\n table[i * width + j] =\n a[i] === b[j]\n ? table[(i + 1) * width + j + 1]! + 1\n : Math.max(table[(i + 1) * width + j]!, table[i * width + j + 1]!)\n let i = 0\n let j = 0\n while (i < a.length && j < b.length) {\n if (a[i] === b[j]) {\n push('same', a[i]!)\n i++\n j++\n } else if (table[(i + 1) * width + j]! >= table[i * width + j + 1]!) push('removed', a[i++]!)\n else push('added', b[j++]!)\n }\n push('removed', a.slice(i))\n push('added', b.slice(j))\n }\n push('same', before.slice(before.length - end))\n return parts\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { blocksToDoc, docToBlocks } from "./model.js";
|
|
2
|
+
import { blockSelectionPlugin } from "./block-selection.js";
|
|
3
|
+
import { editorInputRules } from "./input-rules.js";
|
|
4
|
+
import { editorKeymap } from "./keymap.js";
|
|
5
|
+
import { idPlugin, placeholderPlugin } from "./plugins.js";
|
|
6
|
+
import { preparePastePlugin } from "./prepare.js";
|
|
7
|
+
import { keymap } from "prosemirror-keymap";
|
|
8
|
+
import { EditorState, Plugin, PluginKey } from "prosemirror-state";
|
|
9
|
+
import { EditorView } from "prosemirror-view";
|
|
10
|
+
import { baseKeymap } from "prosemirror-commands";
|
|
11
|
+
import { history, redo, undo } from "prosemirror-history";
|
|
12
|
+
// src/core/editor.ts
|
|
13
|
+
/** The transaction meta that names who made a change, for the `transaction` event. */
|
|
14
|
+
var originKey = "douOrigin";
|
|
15
|
+
var historyKey = new PluginKey("douHistory");
|
|
16
|
+
/**
|
|
17
|
+
* The undo and redo the editor runs: the built-in history's, or those of
|
|
18
|
+
* the extension or props that replace it. For node views that edit their
|
|
19
|
+
* text themselves, so their undo is the document's.
|
|
20
|
+
*/
|
|
21
|
+
function historyCommands(state) {
|
|
22
|
+
return historyKey.getState(state) ?? {
|
|
23
|
+
undo,
|
|
24
|
+
redo
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function createPlugins(options) {
|
|
28
|
+
const { kit } = options;
|
|
29
|
+
const provider = kit.history;
|
|
30
|
+
const builtInHistory = options.history !== false && !provider;
|
|
31
|
+
const commands = {
|
|
32
|
+
undo: options.undo ?? provider?.undo ?? undo,
|
|
33
|
+
redo: options.redo ?? provider?.redo ?? redo
|
|
34
|
+
};
|
|
35
|
+
return [
|
|
36
|
+
new Plugin({
|
|
37
|
+
key: historyKey,
|
|
38
|
+
state: {
|
|
39
|
+
init: () => commands,
|
|
40
|
+
apply: (_tr, value) => value
|
|
41
|
+
}
|
|
42
|
+
}),
|
|
43
|
+
...options.plugins ?? [],
|
|
44
|
+
...kit.plugins,
|
|
45
|
+
editorInputRules(kit.inputRules),
|
|
46
|
+
blockSelectionPlugin(),
|
|
47
|
+
...kit.keymaps.map((bindings) => keymap(bindings)),
|
|
48
|
+
...options.keymaps ?? [],
|
|
49
|
+
editorKeymap(commands),
|
|
50
|
+
keymap(baseKeymap),
|
|
51
|
+
...provider?.plugins ?? [],
|
|
52
|
+
...builtInHistory ? [history()] : [],
|
|
53
|
+
preparePastePlugin(kit),
|
|
54
|
+
idPlugin(),
|
|
55
|
+
placeholderPlugin(options.placeholder)
|
|
56
|
+
];
|
|
57
|
+
}
|
|
58
|
+
function createState(kit, blocks, plugins) {
|
|
59
|
+
return EditorState.create({
|
|
60
|
+
schema: kit.schema,
|
|
61
|
+
doc: blocksToDoc(kit.schema, blocks ?? []),
|
|
62
|
+
plugins
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
var viewNodeViews = /* @__PURE__ */ new WeakMap();
|
|
66
|
+
/** Fresh functions, so the view redraws every node view (e.g. after `editable` changes). */
|
|
67
|
+
var fresh = (nodeViews) => Object.fromEntries(Object.entries(nodeViews).map(([name, create]) => [name, ((...args) => create(...args))]));
|
|
68
|
+
function createEditorView(options) {
|
|
69
|
+
const editable = options.editable ?? true;
|
|
70
|
+
const plugins = createPlugins(options);
|
|
71
|
+
const nodeViews = {
|
|
72
|
+
...options.kit.nodeViews,
|
|
73
|
+
...options.nodeViews
|
|
74
|
+
};
|
|
75
|
+
const view = new EditorView(options.element, {
|
|
76
|
+
state: createState(options.kit, options.blocks, plugins),
|
|
77
|
+
editable: () => editable,
|
|
78
|
+
nodeViews: fresh(nodeViews),
|
|
79
|
+
attributes: () => ({
|
|
80
|
+
class: "dou-editor__surface",
|
|
81
|
+
role: "textbox",
|
|
82
|
+
"aria-multiline": "true",
|
|
83
|
+
...options.attributes?.()
|
|
84
|
+
}),
|
|
85
|
+
dispatchTransaction(tr) {
|
|
86
|
+
const before = this.state;
|
|
87
|
+
const next = before.apply(tr);
|
|
88
|
+
this.updateState(next);
|
|
89
|
+
if (next.doc !== before.doc) options.onChange?.(this, tr, before);
|
|
90
|
+
options.onUpdate?.(this, before);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
viewNodeViews.set(view, nodeViews);
|
|
94
|
+
return view;
|
|
95
|
+
}
|
|
96
|
+
function setEditable(view, value) {
|
|
97
|
+
view.setProps({
|
|
98
|
+
editable: () => value,
|
|
99
|
+
nodeViews: fresh(viewNodeViews.get(view) ?? {})
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
/** Replace the whole document, e.g. when `v-model` changes from outside. */
|
|
103
|
+
function replaceBlocks(view, blocks) {
|
|
104
|
+
const state = view.state;
|
|
105
|
+
const doc = blocksToDoc(state.schema, blocks);
|
|
106
|
+
const tr = state.tr.replaceWith(0, state.doc.content.size, doc.content);
|
|
107
|
+
tr.setMeta("addToHistory", false);
|
|
108
|
+
tr.setMeta(originKey, "external");
|
|
109
|
+
view.dispatch(tr);
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Replace the document with `blocks` as one change undo takes back, e.g.
|
|
113
|
+
* an older version. Only what differs is replaced, so the blocks that stay
|
|
114
|
+
* keep their place, and with collaboration only they are written.
|
|
115
|
+
*/
|
|
116
|
+
function restoreBlocks(view, blocks) {
|
|
117
|
+
const state = view.state;
|
|
118
|
+
const next = blocksToDoc(state.schema, blocks);
|
|
119
|
+
const start = state.doc.content.findDiffStart(next.content);
|
|
120
|
+
if (start === null) return false;
|
|
121
|
+
const end = state.doc.content.findDiffEnd(next.content);
|
|
122
|
+
const overlap = start - Math.min(end.a, end.b);
|
|
123
|
+
if (overlap > 0) {
|
|
124
|
+
end.a += overlap;
|
|
125
|
+
end.b += overlap;
|
|
126
|
+
}
|
|
127
|
+
view.dispatch(state.tr.replace(start, end.a, next.slice(start, end.b)).setMeta(originKey, "restore").scrollIntoView());
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
function getBlocks(view) {
|
|
131
|
+
return docToBlocks(view.state.doc);
|
|
132
|
+
}
|
|
133
|
+
function runCommand(view, command) {
|
|
134
|
+
return command(view.state, view.dispatch, view);
|
|
135
|
+
}
|
|
136
|
+
export { createEditorView, createPlugins, createState, getBlocks, historyCommands, originKey, replaceBlocks, restoreBlocks, runCommand, setEditable };
|
|
137
|
+
|
|
138
|
+
//# sourceMappingURL=editor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"editor.js","names":[],"sources":["../../src/core/editor.ts"],"sourcesContent":["import { baseKeymap } from 'prosemirror-commands'\nimport { history, redo, undo } from 'prosemirror-history'\nimport { keymap } from 'prosemirror-keymap'\nimport type { Node as PMNode } from 'prosemirror-model'\nimport { EditorState, Plugin, PluginKey, type Command, type Transaction } from 'prosemirror-state'\nimport { EditorView, type NodeViewConstructor } from 'prosemirror-view'\nimport { blockSelectionPlugin } from './block-selection'\nimport type { ResolvedEditorExtensions } from './extension'\nimport { editorInputRules } from './input-rules'\nimport { editorKeymap, type KeymapOptions } from './keymap'\nimport { blocksToDoc, docToBlocks, type Block, type PartialBlock } from './model'\nimport { idPlugin, placeholderPlugin, type PlaceholderOptions } from './plugins'\nimport { preparePastePlugin } from './prepare'\n\nexport interface CreateEditorOptions {\n /** Where the editor goes. */\n element: HTMLElement\n /** The resolved extensions: schema, plugins, node views. */\n kit: ResolvedEditorExtensions\n /** More node views, e.g. ones that render Vue components. */\n nodeViews?: Record<string, NodeViewConstructor>\n blocks?: PartialBlock[]\n editable?: boolean\n placeholder: PlaceholderOptions\n /** Called after each change to the document, with the transaction and the state before it. */\n onChange?: (view: EditorView, tr: Transaction, before: EditorState) => void\n /** Called after every state change, including the selection. */\n onUpdate?: (view: EditorView, before: EditorState) => void\n /**\n * Extra plugins, placed before the built-in ones so their keys win.\n * The collaboration extension adds its sync and cursor plugins here.\n */\n plugins?: Plugin[]\n /**\n * Key plugins placed after the extensions' keys, so an extension can\n * take one of their keys, and before the built-in ones.\n */\n keymaps?: Plugin[]\n /** Leave out the built-in undo history (collaboration brings its own). */\n history?: boolean\n undo?: KeymapOptions['undo']\n redo?: KeymapOptions['redo']\n /** Attributes for the editable element, e.g. an accessible name. */\n attributes?: () => Record<string, string>\n}\n\n/** The transaction meta that names who made a change, for the `transaction` event. */\nexport const originKey = 'douOrigin'\n\nconst historyKey = new PluginKey<{ undo: Command; redo: Command }>('douHistory')\n\n/**\n * The undo and redo the editor runs: the built-in history's, or those of\n * the extension or props that replace it. For node views that edit their\n * text themselves, so their undo is the document's.\n */\nexport function historyCommands(state: EditorState): {\n undo: Command\n redo: Command\n} {\n return historyKey.getState(state) ?? { undo, redo }\n}\n\nexport function createPlugins(options: Omit<CreateEditorOptions, 'element'>): Plugin[] {\n const { kit } = options\n const provider = kit.history\n const builtInHistory = options.history !== false && !provider\n const commands = {\n undo: options.undo ?? provider?.undo ?? undo,\n redo: options.redo ?? provider?.redo ?? redo,\n }\n return [\n new Plugin({\n key: historyKey,\n state: { init: () => commands, apply: (_tr, value) => value },\n }),\n ...(options.plugins ?? []),\n ...kit.plugins,\n editorInputRules(kit.inputRules),\n blockSelectionPlugin(),\n ...kit.keymaps.map((bindings) => keymap(bindings)),\n ...(options.keymaps ?? []),\n editorKeymap(commands),\n keymap(baseKeymap),\n ...(provider?.plugins ?? []),\n ...(builtInHistory ? [history()] : []),\n preparePastePlugin(kit),\n idPlugin(),\n placeholderPlugin(options.placeholder),\n ]\n}\n\nexport function createState(\n kit: ResolvedEditorExtensions,\n blocks: PartialBlock[] | undefined,\n plugins: Plugin[],\n): EditorState {\n return EditorState.create({\n schema: kit.schema,\n doc: blocksToDoc(kit.schema, blocks ?? []),\n plugins,\n })\n}\n\nconst viewNodeViews = new WeakMap<EditorView, Record<string, NodeViewConstructor>>()\n\n/** Fresh functions, so the view redraws every node view (e.g. after `editable` changes). */\nconst fresh = (nodeViews: Record<string, NodeViewConstructor>) =>\n Object.fromEntries(\n Object.entries(nodeViews).map(([name, create]) => [\n name,\n ((...args) => create(...args)) as NodeViewConstructor,\n ]),\n )\n\nexport function createEditorView(options: CreateEditorOptions): EditorView {\n const editable = options.editable ?? true\n const plugins = createPlugins(options)\n const nodeViews = { ...options.kit.nodeViews, ...options.nodeViews }\n const view: EditorView = new EditorView(options.element, {\n state: createState(options.kit, options.blocks, plugins),\n editable: () => editable,\n nodeViews: fresh(nodeViews),\n attributes: () => ({\n class: 'dou-editor__surface',\n role: 'textbox',\n 'aria-multiline': 'true',\n ...options.attributes?.(),\n }),\n // `this`, not `view`: plugins (e.g. collaboration) dispatch while the view is being created\n dispatchTransaction(this: EditorView, tr) {\n const before = this.state\n const next = before.apply(tr)\n this.updateState(next)\n if (next.doc !== before.doc) options.onChange?.(this, tr, before)\n options.onUpdate?.(this, before)\n },\n })\n viewNodeViews.set(view, nodeViews)\n return view\n}\n\nexport function setEditable(view: EditorView, value: boolean) {\n // node views draw controls (checkboxes…) that are disabled while read-only\n view.setProps({ editable: () => value, nodeViews: fresh(viewNodeViews.get(view) ?? {}) })\n}\n\n/** Replace the whole document, e.g. when `v-model` changes from outside. */\nexport function replaceBlocks(view: EditorView, blocks: PartialBlock[]) {\n const state = view.state\n const doc = blocksToDoc(state.schema, blocks)\n const tr = state.tr.replaceWith(0, state.doc.content.size, doc.content)\n tr.setMeta('addToHistory', false)\n tr.setMeta(originKey, 'external')\n view.dispatch(tr)\n}\n\n/**\n * Replace the document with `blocks` as one change undo takes back, e.g.\n * an older version. Only what differs is replaced, so the blocks that stay\n * keep their place, and with collaboration only they are written.\n */\nexport function restoreBlocks(view: EditorView, blocks: PartialBlock[]) {\n const state = view.state\n const next = blocksToDoc(state.schema, blocks)\n const start = state.doc.content.findDiffStart(next.content)\n if (start === null) return false\n const end = state.doc.content.findDiffEnd(next.content)!\n // the two ends can cross where the same text repeats: widen them to agree\n const overlap = start - Math.min(end.a, end.b)\n if (overlap > 0) {\n end.a += overlap\n end.b += overlap\n }\n view.dispatch(\n state.tr\n .replace(start, end.a, next.slice(start, end.b))\n .setMeta(originKey, 'restore')\n .scrollIntoView(),\n )\n return true\n}\n\nexport function getBlocks(view: EditorView): Block[] {\n return docToBlocks(view.state.doc)\n}\n\nexport function runCommand(view: EditorView, command: Command): boolean {\n return command(view.state, view.dispatch, view)\n}\n\nexport type { PMNode }\n"],"mappings":"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"}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { createSchema } from "./schema.js";
|
|
2
|
+
import { parseHotkey } from "dou-ui";
|
|
3
|
+
// src/core/extension.ts
|
|
4
|
+
var labelOf = (label, messages) => typeof label === "function" ? label(messages) : label ?? "";
|
|
5
|
+
/** Declare an extension; only gives it a type. */
|
|
6
|
+
function defineEditorExtension(extension) {
|
|
7
|
+
return extension;
|
|
8
|
+
}
|
|
9
|
+
var PM_KEYS = {
|
|
10
|
+
" ": "Space",
|
|
11
|
+
arrowup: "ArrowUp",
|
|
12
|
+
arrowdown: "ArrowDown",
|
|
13
|
+
arrowleft: "ArrowLeft",
|
|
14
|
+
arrowright: "ArrowRight",
|
|
15
|
+
pageup: "PageUp",
|
|
16
|
+
pagedown: "PageDown"
|
|
17
|
+
};
|
|
18
|
+
var PM_MODIFIERS = {
|
|
19
|
+
mod: "Mod",
|
|
20
|
+
ctrl: "Ctrl",
|
|
21
|
+
alt: "Alt",
|
|
22
|
+
meta: "Meta",
|
|
23
|
+
shift: "Shift"
|
|
24
|
+
};
|
|
25
|
+
/** `mod+shift+h` → `Mod-Shift-h`, the way the ProseMirror keymap names keys. */
|
|
26
|
+
function toEditorKey(hotkey) {
|
|
27
|
+
const steps = parseHotkey(hotkey);
|
|
28
|
+
if (steps.length !== 1) throw new Error(`[@dou-ui/editor] key sequences aren't supported in a keymap: "${hotkey}"`);
|
|
29
|
+
const keys = steps[0];
|
|
30
|
+
const modifiers = Object.keys(PM_MODIFIERS).filter((name) => keys.includes(name)).map((name) => PM_MODIFIERS[name]);
|
|
31
|
+
const key = keys.find((name) => !(name in PM_MODIFIERS)) ?? "";
|
|
32
|
+
const name = PM_KEYS[key] ?? (key.length > 1 ? key[0].toUpperCase() + key.slice(1) : key);
|
|
33
|
+
return [...modifiers, name].join("-");
|
|
34
|
+
}
|
|
35
|
+
var toEditorKeymap = (bindings) => Object.fromEntries(Object.entries(bindings).map(([key, command]) => [toEditorKey(key), command]));
|
|
36
|
+
function flatten(list) {
|
|
37
|
+
const result = [];
|
|
38
|
+
for (const item of list) if (Array.isArray(item)) result.push(...flatten(item));
|
|
39
|
+
else result.push(item);
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
function merge(target, source, what, owner) {
|
|
43
|
+
for (const [name, value] of Object.entries(source ?? {})) {
|
|
44
|
+
if (name in target) throw new Error(`[@dou-ui/editor] ${what} "${name}" from "${owner}" is already defined`);
|
|
45
|
+
target[name] = value;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/** Sort the extensions by priority, build the schema and collect what they add. */
|
|
49
|
+
function resolveEditorExtensions(list) {
|
|
50
|
+
const extensions = flatten(list).map((extension, index) => ({
|
|
51
|
+
extension,
|
|
52
|
+
index
|
|
53
|
+
})).sort((a, b) => (b.extension.priority ?? 100) - (a.extension.priority ?? 100) || a.index - b.index).map((entry) => entry.extension);
|
|
54
|
+
const names = /* @__PURE__ */ new Set();
|
|
55
|
+
const blocks = {};
|
|
56
|
+
const nodes = {};
|
|
57
|
+
const inlines = {};
|
|
58
|
+
const marks = {};
|
|
59
|
+
for (const extension of extensions) {
|
|
60
|
+
if (names.has(extension.name)) throw new Error(`[@dou-ui/editor] the extension "${extension.name}" is added twice`);
|
|
61
|
+
names.add(extension.name);
|
|
62
|
+
merge(blocks, extension.blocks, "block type", extension.name);
|
|
63
|
+
merge(nodes, extension.nodes, "node", extension.name);
|
|
64
|
+
merge(inlines, extension.inlines, "inline node", extension.name);
|
|
65
|
+
merge(marks, extension.marks, "mark", extension.name);
|
|
66
|
+
}
|
|
67
|
+
const schema = createSchema({
|
|
68
|
+
blocks,
|
|
69
|
+
nodes,
|
|
70
|
+
inlines,
|
|
71
|
+
marks
|
|
72
|
+
});
|
|
73
|
+
const context = { schema };
|
|
74
|
+
const providers = extensions.filter((extension) => extension.history);
|
|
75
|
+
if (providers.length > 1) throw new Error("[@dou-ui/editor] only one extension may replace the undo history");
|
|
76
|
+
const keepers = extensions.filter((extension) => extension.comments);
|
|
77
|
+
if (keepers.length > 1) throw new Error("[@dou-ui/editor] only one extension may keep the comment threads");
|
|
78
|
+
return {
|
|
79
|
+
extensions,
|
|
80
|
+
schema,
|
|
81
|
+
context,
|
|
82
|
+
nodeViews: Object.assign({}, ...extensions.map((extension) => extension.nodeViews ?? {})),
|
|
83
|
+
componentViews: Object.assign({}, ...extensions.map((extension) => extension.componentViews ?? {})),
|
|
84
|
+
blockOptions: extensions.flatMap((extension) => extension.blockOptions ?? []),
|
|
85
|
+
formats: extensions.flatMap((extension) => extension.formats ?? []),
|
|
86
|
+
overlays: extensions.flatMap((extension) => extension.overlays ?? []),
|
|
87
|
+
toolbar: extensions.flatMap((extension) => extension.toolbar ?? []),
|
|
88
|
+
blockMenu: extensions.flatMap((extension) => extension.blockMenu ?? []),
|
|
89
|
+
inputRules: extensions.flatMap((extension) => extension.inputRules?.(context) ?? []),
|
|
90
|
+
keymaps: extensions.flatMap((extension) => extension.keymap ? [toEditorKeymap(extension.keymap(context))] : []),
|
|
91
|
+
plugins: extensions.flatMap((extension) => extension.plugins?.(context) ?? []),
|
|
92
|
+
history: providers[0]?.history?.(context) ?? null,
|
|
93
|
+
comments: keepers[0]?.comments?.(context) ?? null,
|
|
94
|
+
externalDocument: extensions.some((extension) => extension.externalDocument)
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
export { defineEditorExtension, labelOf, resolveEditorExtensions, toEditorKey };
|
|
98
|
+
|
|
99
|
+
//# sourceMappingURL=extension.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"extension.js","names":[],"sources":["../../src/core/extension.ts"],"sourcesContent":["import type { InputRule } from 'prosemirror-inputrules'\nimport type { MarkSpec, NodeSpec, Schema } from 'prosemirror-model'\nimport type { Command, Plugin } from 'prosemirror-state'\nimport type { NodeViewConstructor } from 'prosemirror-view'\nimport { parseHotkey, type CommentAction, type CommentThread } from 'dou-ui'\nimport type { Component, Ref } from 'vue'\nimport type { EditorMessages } from '../locale'\nimport type { BlockProps } from './model'\nimport type { EditorExtensionOutput } from './output'\nimport { createSchema } from './schema'\n\n/** Text shown in a menu: fixed, or read from the editor's messages so it follows the locale. */\nexport type Label = string | ((messages: EditorMessages) => string)\n\nexport const labelOf = (label: Label | undefined, messages: EditorMessages) =>\n typeof label === 'function' ? label(messages) : (label ?? '')\n\n/** An entry of the slash menu and of the “turn into” menus. */\nexport interface BlockOption {\n /** Unique among the options. */\n key: string\n /** The block type it inserts or turns blocks into. */\n type: string\n props?: BlockProps\n label: Label\n /** A short description under the label in the slash menu. */\n hint?: Label\n icon: Component\n /** Extra words the slash menu matches, besides the label (pinyin works well). */\n keywords?: string[]\n /** The Markdown shortcut, shown as a hint. */\n shortcut?: string\n}\n\n/** A toggle in the formatting toolbar for a mark. */\nexport interface FormatOption {\n mark: string\n label: Label\n icon: Component\n /** The shortcut shown in its tooltip, written as in `useHotkey`, e.g. `mod+b`. */\n hotkey?: string\n}\n\n/** Where the comment threads live, when not in `v-model:comments`. */\nexport interface CommentsProvider {\n /** The threads, kept up to date. */\n threads: Readonly<Ref<readonly CommentThread[]>>\n /** Make a change this user asked for. */\n apply: (action: CommentAction) => void\n}\n\nexport interface EditorExtensionContext {\n schema: Schema\n}\n\n/** What the extension replacing the undo history provides (e.g. collaboration). */\nexport interface HistoryProvider {\n plugins: Plugin[]\n undo: Command\n redo: Command\n}\n\n/**\n * A piece of the editor: block types, marks, behaviour and menu entries.\n * The built-in blocks are extensions too (see `editorKit`).\n */\nexport interface EditorExtension {\n name: string\n /** Higher runs first: its keys, input rules and plugins win. Defaults to 100. */\n priority?: number\n /**\n * Content node specs for new block types, by type name. They join the\n * `blockContent` group. Extra spec fields the editor reads:\n * `continues` (Enter continues the same type, like a list) and\n * `component` (a Vue component that renders the block).\n */\n blocks?: Record<string, NodeSpec>\n /**\n * Nodes that only live inside the extension's blocks, e.g. a table's\n * rows and cells. They join no group.\n */\n nodes?: Record<string, NodeSpec>\n /** Inline nodes, e.g. a mention. */\n inlines?: Record<string, NodeSpec>\n marks?: Record<string, MarkSpec>\n nodeViews?: Record<string, NodeViewConstructor>\n /**\n * Node views that draw Vue components, by node type. Each is made with\n * `mount`, which renders a component into an element inside the editor's\n * app, so it can call `useEditor()` and follows the ConfigProvider.\n */\n componentViews?: Record<string, (mount: MountComponent) => NodeViewConstructor>\n inputRules?: (context: EditorExtensionContext) => InputRule[]\n /**\n * Keys, written as in `useHotkey` (e.g. `mod+shift+h`, `shift+tab`,\n * `enter`), while the editor has focus. A command returning `false` lets\n * the key through to the next extension.\n */\n keymap?: (context: EditorExtensionContext) => Record<string, Command>\n plugins?: (context: EditorExtensionContext) => Plugin[]\n /**\n * The document lives outside the editor (e.g. in a Yjs document): the\n * initial `modelValue` and later outside changes to it are ignored, and\n * `v-model` only mirrors the document.\n */\n externalDocument?: boolean\n /** Replace the built-in undo history. Only one extension may. */\n history?: (context: EditorExtensionContext) => HistoryProvider\n /**\n * Keep the comment threads somewhere else than `v-model:comments`, e.g.\n * in a shared document. `v-model:comments` then only mirrors them.\n * Only one extension may.\n */\n comments?: (context: EditorExtensionContext) => CommentsProvider\n /** Entries for the slash menu and the “turn into” menus. */\n blockOptions?: BlockOption[]\n /** Toggles for the formatting toolbar. */\n formats?: FormatOption[]\n /**\n * Components rendered next to the editing area while it is editable,\n * e.g. a popup of suggestions. They can call `useEditor()`.\n */\n overlays?: Component[]\n /**\n * Controls added to the formatting toolbar, after the marks (e.g. a\n * color menu). They can call `useEditor()`.\n */\n toolbar?: Component[]\n /**\n * Entries added to the block menu of the handle, usually a\n * `DouDropdownSub` or `DouDropdownItem`s. Each gets the block the menu is\n * for as the `block` prop (a `Block`) and can call `useEditor()`.\n */\n blockMenu?: Component[]\n /**\n * How the extension's blocks, inline nodes and marks are written as HTML\n * and Markdown (`blocksToHTML`, `blocksToMarkdown`).\n */\n output?: EditorExtensionOutput\n /**\n * Rewrite HTML before it is read as blocks (when pasting, and in\n * `htmlToBlocks` and `markdownToBlocks`), e.g. turn a `<details>`\n * element into a toggle with its children. `blockElement` and\n * `splitContainer` help build the editor's markup.\n */\n prepareHTML?: (root: HTMLElement) => void\n}\n\n/**\n * Renders `component` into `element` with `props` (a reactive object stays\n * live), inside the editor's app. Returns a function that removes it.\n */\nexport type MountComponent = (\n component: Component,\n props: Record<string, unknown>,\n element: HTMLElement,\n) => () => void\n\n/** Declare an extension; only gives it a type. */\nexport function defineEditorExtension(extension: EditorExtension): EditorExtension {\n return extension\n}\n\n/** Everything the extensions contribute, gathered once per list of extensions. */\nexport interface ResolvedEditorExtensions {\n extensions: EditorExtension[]\n schema: Schema\n context: EditorExtensionContext\n nodeViews: Record<string, NodeViewConstructor>\n componentViews: Record<string, (mount: MountComponent) => NodeViewConstructor>\n blockOptions: BlockOption[]\n formats: FormatOption[]\n overlays: Component[]\n toolbar: Component[]\n blockMenu: Component[]\n inputRules: InputRule[]\n keymaps: Record<string, Command>[]\n plugins: Plugin[]\n history: HistoryProvider | null\n comments: CommentsProvider | null\n externalDocument: boolean\n}\n\nconst PM_KEYS: Record<string, string> = {\n ' ': 'Space',\n arrowup: 'ArrowUp',\n arrowdown: 'ArrowDown',\n arrowleft: 'ArrowLeft',\n arrowright: 'ArrowRight',\n pageup: 'PageUp',\n pagedown: 'PageDown',\n}\nconst PM_MODIFIERS: Record<string, string> = {\n mod: 'Mod',\n ctrl: 'Ctrl',\n alt: 'Alt',\n meta: 'Meta',\n shift: 'Shift',\n}\n\n/** `mod+shift+h` → `Mod-Shift-h`, the way the ProseMirror keymap names keys. */\nexport function toEditorKey(hotkey: string): string {\n const steps = parseHotkey(hotkey)\n if (steps.length !== 1)\n throw new Error(`[@dou-ui/editor] key sequences aren't supported in a keymap: \"${hotkey}\"`)\n const keys = steps[0]!\n const modifiers = Object.keys(PM_MODIFIERS)\n .filter((name) => keys.includes(name))\n .map((name) => PM_MODIFIERS[name])\n const key = keys.find((name) => !(name in PM_MODIFIERS)) ?? ''\n const name = PM_KEYS[key] ?? (key.length > 1 ? key[0]!.toUpperCase() + key.slice(1) : key)\n return [...modifiers, name].join('-')\n}\n\nconst toEditorKeymap = (bindings: Record<string, Command>) =>\n Object.fromEntries(Object.entries(bindings).map(([key, command]) => [toEditorKey(key), command]))\n\nfunction flatten(list: readonly (EditorExtension | EditorExtension[])[]): EditorExtension[] {\n const result: EditorExtension[] = []\n for (const item of list) {\n if (Array.isArray(item)) result.push(...flatten(item))\n else result.push(item)\n }\n return result\n}\n\nfunction merge<T>(\n target: Record<string, T>,\n source: Record<string, T> | undefined,\n what: string,\n owner: string,\n) {\n for (const [name, value] of Object.entries(source ?? {})) {\n if (name in target)\n throw new Error(`[@dou-ui/editor] ${what} \"${name}\" from \"${owner}\" is already defined`)\n target[name] = value\n }\n}\n\n/** Sort the extensions by priority, build the schema and collect what they add. */\nexport function resolveEditorExtensions(\n list: readonly (EditorExtension | EditorExtension[])[],\n): ResolvedEditorExtensions {\n const extensions = flatten(list)\n .map((extension, index) => ({ extension, index }))\n // oxlint-disable-next-line unicorn/no-array-sort -- sorts a fresh array\n .sort(\n (a, b) => (b.extension.priority ?? 100) - (a.extension.priority ?? 100) || a.index - b.index,\n )\n .map((entry) => entry.extension)\n\n const names = new Set<string>()\n const blocks: Record<string, NodeSpec> = {}\n const nodes: Record<string, NodeSpec> = {}\n const inlines: Record<string, NodeSpec> = {}\n const marks: Record<string, MarkSpec> = {}\n for (const extension of extensions) {\n if (names.has(extension.name))\n throw new Error(`[@dou-ui/editor] the extension \"${extension.name}\" is added twice`)\n names.add(extension.name)\n merge(blocks, extension.blocks, 'block type', extension.name)\n merge(nodes, extension.nodes, 'node', extension.name)\n merge(inlines, extension.inlines, 'inline node', extension.name)\n merge(marks, extension.marks, 'mark', extension.name)\n }\n const schema = createSchema({ blocks, nodes, inlines, marks })\n const context: EditorExtensionContext = { schema }\n\n const providers = extensions.filter((extension) => extension.history)\n if (providers.length > 1)\n throw new Error('[@dou-ui/editor] only one extension may replace the undo history')\n\n const keepers = extensions.filter((extension) => extension.comments)\n if (keepers.length > 1)\n throw new Error('[@dou-ui/editor] only one extension may keep the comment threads')\n\n return {\n extensions,\n schema,\n context,\n nodeViews: Object.assign({}, ...extensions.map((extension) => extension.nodeViews ?? {})),\n componentViews: Object.assign(\n {},\n ...extensions.map((extension) => extension.componentViews ?? {}),\n ),\n blockOptions: extensions.flatMap((extension) => extension.blockOptions ?? []),\n formats: extensions.flatMap((extension) => extension.formats ?? []),\n overlays: extensions.flatMap((extension) => extension.overlays ?? []),\n toolbar: extensions.flatMap((extension) => extension.toolbar ?? []),\n blockMenu: extensions.flatMap((extension) => extension.blockMenu ?? []),\n inputRules: extensions.flatMap((extension) => extension.inputRules?.(context) ?? []),\n keymaps: extensions.flatMap((extension) =>\n extension.keymap ? [toEditorKeymap(extension.keymap(context))] : [],\n ),\n plugins: extensions.flatMap((extension) => extension.plugins?.(context) ?? []),\n history: providers[0]?.history?.(context) ?? null,\n comments: keepers[0]?.comments?.(context) ?? null,\n externalDocument: extensions.some((extension) => extension.externalDocument),\n }\n}\n"],"mappings":"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"}
|