@kerebron/extension-basic-editor 0.4.28 → 0.4.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/esm/ExtensionBaseKeymap.js +1 -0
  2. package/esm/ExtensionBaseKeymap.js.map +1 -0
  3. package/esm/ExtensionBasicCodeEditor.js +1 -0
  4. package/esm/ExtensionBasicCodeEditor.js.map +1 -0
  5. package/esm/ExtensionBasicEditor.js +1 -0
  6. package/esm/ExtensionBasicEditor.js.map +1 -0
  7. package/esm/ExtensionDropcursor.js +1 -0
  8. package/esm/ExtensionDropcursor.js.map +1 -0
  9. package/esm/ExtensionGapcursor.js +1 -0
  10. package/esm/ExtensionGapcursor.js.map +1 -0
  11. package/esm/ExtensionHistory.js +1 -0
  12. package/esm/ExtensionHistory.js.map +1 -0
  13. package/esm/ExtensionHtml.js +1 -0
  14. package/esm/ExtensionHtml.js.map +1 -0
  15. package/esm/ExtensionMediaUpload.js +1 -0
  16. package/esm/ExtensionMediaUpload.js.map +1 -0
  17. package/esm/ExtensionSelection.js +1 -0
  18. package/esm/ExtensionSelection.js.map +1 -0
  19. package/esm/ExtensionTextAlign.js +1 -0
  20. package/esm/ExtensionTextAlign.js.map +1 -0
  21. package/esm/MarkBookmark.js +1 -0
  22. package/esm/MarkBookmark.js.map +1 -0
  23. package/esm/MarkChange.js +1 -0
  24. package/esm/MarkChange.js.map +1 -0
  25. package/esm/MarkCode.js +1 -0
  26. package/esm/MarkCode.js.map +1 -0
  27. package/esm/MarkHighlight.js +1 -0
  28. package/esm/MarkHighlight.js.map +1 -0
  29. package/esm/MarkItalic.js +1 -0
  30. package/esm/MarkItalic.js.map +1 -0
  31. package/esm/MarkLink.js +1 -0
  32. package/esm/MarkLink.js.map +1 -0
  33. package/esm/MarkStrike.js +1 -0
  34. package/esm/MarkStrike.js.map +1 -0
  35. package/esm/MarkStrong.js +1 -0
  36. package/esm/MarkStrong.js.map +1 -0
  37. package/esm/MarkSubscript.js +1 -0
  38. package/esm/MarkSubscript.js.map +1 -0
  39. package/esm/MarkSuperscript.js +1 -0
  40. package/esm/MarkSuperscript.js.map +1 -0
  41. package/esm/MarkTextColor.js +1 -0
  42. package/esm/MarkTextColor.js.map +1 -0
  43. package/esm/MarkUnderline.js +1 -0
  44. package/esm/MarkUnderline.js.map +1 -0
  45. package/esm/NodeAside.js +1 -0
  46. package/esm/NodeAside.js.map +1 -0
  47. package/esm/NodeBlockquote.js +1 -0
  48. package/esm/NodeBlockquote.js.map +1 -0
  49. package/esm/NodeBookmark.js +1 -0
  50. package/esm/NodeBookmark.js.map +1 -0
  51. package/esm/NodeBulletList.js +1 -0
  52. package/esm/NodeBulletList.js.map +1 -0
  53. package/esm/NodeCodeBlock.js +1 -0
  54. package/esm/NodeCodeBlock.js.map +1 -0
  55. package/esm/NodeDefinitionDesc.js +1 -0
  56. package/esm/NodeDefinitionDesc.js.map +1 -0
  57. package/esm/NodeDefinitionList.js +1 -0
  58. package/esm/NodeDefinitionList.js.map +1 -0
  59. package/esm/NodeDefinitionTerm.js +1 -0
  60. package/esm/NodeDefinitionTerm.js.map +1 -0
  61. package/esm/NodeDocument.js +1 -0
  62. package/esm/NodeDocument.js.map +1 -0
  63. package/esm/NodeDocumentCode.js +1 -0
  64. package/esm/NodeDocumentCode.js.map +1 -0
  65. package/esm/NodeFrontmatter.js +1 -0
  66. package/esm/NodeFrontmatter.js.map +1 -0
  67. package/esm/NodeHardBreak.js +1 -0
  68. package/esm/NodeHardBreak.js.map +1 -0
  69. package/esm/NodeHeading.js +1 -0
  70. package/esm/NodeHeading.js.map +1 -0
  71. package/esm/NodeHorizontalRule.js +1 -0
  72. package/esm/NodeHorizontalRule.js.map +1 -0
  73. package/esm/NodeImage.js +1 -0
  74. package/esm/NodeImage.js.map +1 -0
  75. package/esm/NodeInlineShortCode.js +1 -0
  76. package/esm/NodeInlineShortCode.js.map +1 -0
  77. package/esm/NodeListItem.js +1 -0
  78. package/esm/NodeListItem.js.map +1 -0
  79. package/esm/NodeMath.js +1 -0
  80. package/esm/NodeMath.js.map +1 -0
  81. package/esm/NodeOrderedList.js +1 -0
  82. package/esm/NodeOrderedList.js.map +1 -0
  83. package/esm/NodeParagraph.js +1 -0
  84. package/esm/NodeParagraph.js.map +1 -0
  85. package/esm/NodeTaskItem.js +1 -0
  86. package/esm/NodeTaskItem.js.map +1 -0
  87. package/esm/NodeTaskList.js +1 -0
  88. package/esm/NodeTaskList.js.map +1 -0
  89. package/esm/NodeText.js +1 -0
  90. package/esm/NodeText.js.map +1 -0
  91. package/esm/NodeVideo.js +1 -0
  92. package/esm/NodeVideo.js.map +1 -0
  93. package/esm/remote-selection/ExtensionRemoteSelection.js +1 -0
  94. package/esm/remote-selection/ExtensionRemoteSelection.js.map +1 -0
  95. package/esm/remote-selection/remoteSelectionPlugin.js +1 -0
  96. package/esm/remote-selection/remoteSelectionPlugin.js.map +1 -0
  97. package/package.json +6 -2
  98. package/src/ExtensionBaseKeymap.ts +64 -0
  99. package/src/ExtensionBasicCodeEditor.ts +82 -0
  100. package/src/ExtensionBasicEditor.ts +97 -0
  101. package/src/ExtensionDropcursor.ts +221 -0
  102. package/src/ExtensionGapcursor.ts +278 -0
  103. package/src/ExtensionHistory.ts +48 -0
  104. package/src/ExtensionHtml.ts +158 -0
  105. package/src/ExtensionMediaUpload.ts +258 -0
  106. package/src/ExtensionSelection.ts +379 -0
  107. package/src/ExtensionTextAlign.ts +50 -0
  108. package/src/MarkBookmark.ts +20 -0
  109. package/src/MarkChange.ts +17 -0
  110. package/src/MarkCode.ts +35 -0
  111. package/src/MarkHighlight.ts +38 -0
  112. package/src/MarkItalic.ts +41 -0
  113. package/src/MarkLink.ts +32 -0
  114. package/src/MarkStrike.ts +38 -0
  115. package/src/MarkStrong.ts +52 -0
  116. package/src/MarkSubscript.ts +42 -0
  117. package/src/MarkSuperscript.ts +42 -0
  118. package/src/MarkTextColor.ts +29 -0
  119. package/src/MarkUnderline.ts +47 -0
  120. package/src/NodeAside.ts +19 -0
  121. package/src/NodeBlockquote.ts +51 -0
  122. package/src/NodeBookmark.ts +23 -0
  123. package/src/NodeBulletList.ts +51 -0
  124. package/src/NodeCodeBlock.ts +60 -0
  125. package/src/NodeDefinitionDesc.ts +19 -0
  126. package/src/NodeDefinitionList.ts +46 -0
  127. package/src/NodeDefinitionTerm.ts +19 -0
  128. package/src/NodeDocument.ts +22 -0
  129. package/src/NodeDocumentCode.ts +33 -0
  130. package/src/NodeFrontmatter.ts +19 -0
  131. package/src/NodeHardBreak.ts +92 -0
  132. package/src/NodeHeading.ts +76 -0
  133. package/src/NodeHorizontalRule.ts +43 -0
  134. package/src/NodeImage.ts +36 -0
  135. package/src/NodeInlineShortCode.ts +55 -0
  136. package/src/NodeListItem.ts +320 -0
  137. package/src/NodeMath.ts +109 -0
  138. package/src/NodeOrderedList.ts +79 -0
  139. package/src/NodeParagraph.ts +60 -0
  140. package/src/NodeTaskItem.ts +190 -0
  141. package/src/NodeTaskList.ts +38 -0
  142. package/src/NodeText.ts +12 -0
  143. package/src/NodeVideo.ts +44 -0
  144. package/src/remote-selection/ExtensionRemoteSelection.ts +45 -0
  145. package/src/remote-selection/remoteSelectionPlugin.ts +157 -0
@@ -0,0 +1,278 @@
1
+ import {
2
+ EditorState,
3
+ NodeSelection,
4
+ Plugin,
5
+ Selection,
6
+ TextSelection,
7
+ } from 'prosemirror-state';
8
+ import { Fragment, Node, ResolvedPos, Slice } from 'prosemirror-model';
9
+ import { Mappable } from 'prosemirror-transform';
10
+ import { Decoration, DecorationSet, EditorView } from 'prosemirror-view';
11
+
12
+ import type { Command } from '@kerebron/editor/commands';
13
+ import { Extension } from '@kerebron/editor';
14
+ import { keydownHandler } from '@kerebron/editor/plugins/keymap';
15
+
16
+ /// Gap cursor selections are represented using this class. Its
17
+ /// `$anchor` and `$head` properties both point at the cursor position.
18
+ export class GapCursor extends Selection {
19
+ /// Create a gap cursor.
20
+ constructor($pos: ResolvedPos) {
21
+ super($pos, $pos);
22
+ this.visible = false;
23
+ }
24
+
25
+ map(doc: Node, mapping: Mappable): Selection {
26
+ let $pos = doc.resolve(mapping.map(this.head));
27
+ return GapCursor.valid($pos) ? new GapCursor($pos) : Selection.near($pos);
28
+ }
29
+
30
+ override content() {
31
+ return Slice.empty;
32
+ }
33
+
34
+ eq(other: Selection): boolean {
35
+ return other instanceof GapCursor && other.head == this.head;
36
+ }
37
+
38
+ toJSON(): any {
39
+ return { type: 'gapcursor', pos: this.head };
40
+ }
41
+
42
+ /// @internal
43
+ static fromJSONToGapCursor(doc: Node, json: any): GapCursor {
44
+ if (typeof json.pos != 'number') {
45
+ throw new RangeError('Invalid input for GapCursor.fromJSON');
46
+ }
47
+ return new GapCursor(doc.resolve(json.pos));
48
+ }
49
+
50
+ /// @internal
51
+ override getBookmark(): GapBookmark {
52
+ return new GapBookmark(this.anchor);
53
+ }
54
+
55
+ /// @internal
56
+ static valid($pos: ResolvedPos) {
57
+ let parent = $pos.parent;
58
+ if (parent.isTextblock || !closedBefore($pos) || !closedAfter($pos)) {
59
+ return false;
60
+ }
61
+ let override = parent.type.spec.allowGapCursor;
62
+ if (override != null) return override;
63
+ let deflt = parent.contentMatchAt($pos.index()).defaultType;
64
+ return deflt && deflt.isTextblock;
65
+ }
66
+
67
+ /// @internal
68
+ static findGapCursorFrom($pos: ResolvedPos, dir: number, mustMove = false) {
69
+ search: for (;;) {
70
+ if (!mustMove && GapCursor.valid($pos)) return $pos;
71
+ let pos = $pos.pos, next = null;
72
+ // Scan up from this position
73
+ for (let d = $pos.depth;; d--) {
74
+ let parent = $pos.node(d);
75
+ if (
76
+ dir > 0 ? $pos.indexAfter(d) < parent.childCount : $pos.index(d) > 0
77
+ ) {
78
+ next = parent.child(dir > 0 ? $pos.indexAfter(d) : $pos.index(d) - 1);
79
+ break;
80
+ } else if (d == 0) {
81
+ return null;
82
+ }
83
+ pos += dir;
84
+ let $cur = $pos.doc.resolve(pos);
85
+ if (GapCursor.valid($cur)) return $cur;
86
+ }
87
+
88
+ // And then down into the next node
89
+ for (;;) {
90
+ let inside: Node | null = dir > 0 ? next.firstChild : next.lastChild;
91
+ if (!inside) {
92
+ if (
93
+ next.isAtom && !next.isText && !NodeSelection.isSelectable(next)
94
+ ) {
95
+ $pos = $pos.doc.resolve(pos + next.nodeSize * dir);
96
+ mustMove = false;
97
+ continue search;
98
+ }
99
+ break;
100
+ }
101
+ next = inside;
102
+ pos += dir;
103
+ let $cur = $pos.doc.resolve(pos);
104
+ if (GapCursor.valid($cur)) return $cur;
105
+ }
106
+
107
+ return null;
108
+ }
109
+ }
110
+ }
111
+
112
+ Selection.jsonID('gapcursor', GapCursor);
113
+
114
+ class GapBookmark {
115
+ constructor(readonly pos: number) {}
116
+
117
+ map(mapping: Mappable) {
118
+ return new GapBookmark(mapping.map(this.pos));
119
+ }
120
+ resolve(doc: Node): Selection {
121
+ let $pos = doc.resolve(this.pos);
122
+ return GapCursor.valid($pos) ? new GapCursor($pos) : Selection.near($pos);
123
+ }
124
+ }
125
+
126
+ function closedBefore($pos: ResolvedPos) {
127
+ for (let d = $pos.depth; d >= 0; d--) {
128
+ let index = $pos.index(d), parent = $pos.node(d);
129
+ // At the start of this parent, look at next one
130
+ if (index == 0) {
131
+ if (parent.type.spec.isolating) return true;
132
+ continue;
133
+ }
134
+ // See if the node before (or its first ancestor) is closed
135
+ for (let before = parent.child(index - 1);; before = before.lastChild!) {
136
+ if (
137
+ (before.childCount == 0 && !before.inlineContent) || before.isAtom ||
138
+ before.type.spec.isolating
139
+ ) return true;
140
+ if (before.inlineContent) return false;
141
+ }
142
+ }
143
+ // Hit start of document
144
+ return true;
145
+ }
146
+
147
+ function closedAfter($pos: ResolvedPos) {
148
+ for (let d = $pos.depth; d >= 0; d--) {
149
+ let index = $pos.indexAfter(d), parent = $pos.node(d);
150
+ if (index == parent.childCount) {
151
+ if (parent.type.spec.isolating) return true;
152
+ continue;
153
+ }
154
+ for (let after = parent.child(index);; after = after.firstChild!) {
155
+ if (
156
+ (after.childCount == 0 && !after.inlineContent) || after.isAtom ||
157
+ after.type.spec.isolating
158
+ ) return true;
159
+ if (after.inlineContent) return false;
160
+ }
161
+ }
162
+ return true;
163
+ }
164
+
165
+ /// Create a gap cursor plugin. When enabled, this will capture clicks
166
+ /// near and arrow-key-motion past places that don't have a normally
167
+ /// selectable position nearby, and create a gap cursor selection for
168
+ /// them. The cursor is drawn as an element with class
169
+ /// `kb-gapcursor`. You can either include
170
+ /// `style/gapcursor.css` from the package's directory or add your own
171
+ /// styles to make it visible.
172
+ function gapCursor(): Plugin {
173
+ return new Plugin({
174
+ props: {
175
+ decorations: drawGapCursor,
176
+
177
+ createSelectionBetween(_view, $anchor, $head) {
178
+ return $anchor.pos == $head.pos && GapCursor.valid($head)
179
+ ? new GapCursor($head)
180
+ : null;
181
+ },
182
+
183
+ handleClick,
184
+ handleKeyDown,
185
+ handleDOMEvents: { beforeinput: beforeinput as any },
186
+ },
187
+ });
188
+ }
189
+
190
+ const handleKeyDown = keydownHandler({
191
+ 'ArrowLeft': arrow('horiz', -1),
192
+ 'ArrowRight': arrow('horiz', 1),
193
+ 'ArrowUp': arrow('vert', -1),
194
+ 'ArrowDown': arrow('vert', 1),
195
+ });
196
+
197
+ function arrow(axis: 'vert' | 'horiz', dir: number): Command {
198
+ const dirStr = axis == 'vert'
199
+ ? (dir > 0 ? 'down' : 'up')
200
+ : (dir > 0 ? 'right' : 'left');
201
+ return function (state, dispatch, view) {
202
+ let sel = state.selection;
203
+ let $start = dir > 0 ? sel.$to : sel.$from, mustMove = sel.empty;
204
+ if (sel instanceof TextSelection) {
205
+ if (!view!.endOfTextblock(dirStr) || $start.depth == 0) return false;
206
+ mustMove = false;
207
+ $start = state.doc.resolve(dir > 0 ? $start.after() : $start.before());
208
+ }
209
+ let $found = GapCursor.findGapCursorFrom($start, dir, mustMove);
210
+ if (!$found) return false;
211
+ if (dispatch) dispatch(state.tr.setSelection(new GapCursor($found)));
212
+ return true;
213
+ };
214
+ }
215
+
216
+ function handleClick(view: EditorView, pos: number, event: MouseEvent) {
217
+ if (!view || !view.editable) return false;
218
+ let $pos = view.state.doc.resolve(pos);
219
+ if (!GapCursor.valid($pos)) return false;
220
+ let clickPos = view.posAtCoords({ left: event.clientX, top: event.clientY });
221
+ if (
222
+ clickPos && clickPos.inside > -1 &&
223
+ NodeSelection.isSelectable(view.state.doc.nodeAt(clickPos.inside)!)
224
+ ) return false;
225
+ view.dispatch(view.state.tr.setSelection(new GapCursor($pos)));
226
+ return true;
227
+ }
228
+
229
+ // This is a hack that, when a composition starts while a gap cursor
230
+ // is active, quickly creates an inline context for the composition to
231
+ // happen in, to avoid it being aborted by the DOM selection being
232
+ // moved into a valid position.
233
+ function beforeinput(view: EditorView, event: InputEvent) {
234
+ if (
235
+ event.inputType != 'insertCompositionText' ||
236
+ !(view.state.selection instanceof GapCursor)
237
+ ) return false;
238
+
239
+ let { $from } = view.state.selection;
240
+ let insert = $from.parent.contentMatchAt($from.index()).findWrapping(
241
+ view.state.schema.nodes.text,
242
+ );
243
+ if (!insert) return false;
244
+
245
+ let frag = Fragment.empty;
246
+ for (let i = insert.length - 1; i >= 0; i--) {
247
+ frag = Fragment.from(insert[i].createAndFill(null, frag));
248
+ }
249
+ let tr = view.state.tr.replace($from.pos, $from.pos, new Slice(frag, 0, 0));
250
+ tr.setSelection(TextSelection.near(tr.doc.resolve($from.pos + 1)));
251
+ view.dispatch(tr);
252
+ return false;
253
+ }
254
+
255
+ function drawGapCursor(state: EditorState) {
256
+ if (!(state.selection instanceof GapCursor)) return null;
257
+ let node = document.createElement('div');
258
+ node.className = 'kb-gapcursor';
259
+ return DecorationSet.create(state.doc, [
260
+ Decoration.widget(state.selection.head, node, { key: 'gapcursor' }),
261
+ ]);
262
+ }
263
+
264
+ export class ExtensionGapcursor extends Extension {
265
+ name = 'gapcursor';
266
+
267
+ options = {
268
+ color: 'currentColor',
269
+ width: 1,
270
+ class: undefined,
271
+ };
272
+
273
+ override getProseMirrorPlugins(): Plugin[] {
274
+ return [
275
+ gapCursor(),
276
+ ];
277
+ }
278
+ }
@@ -0,0 +1,48 @@
1
+ import { history, redo, undo } from 'prosemirror-history';
2
+ import { Plugin } from 'prosemirror-state';
3
+
4
+ import { type CoreEditor, Extension } from '@kerebron/editor';
5
+ import {
6
+ type Command,
7
+ type CommandFactories,
8
+ type CommandShortcuts,
9
+ } from '@kerebron/editor/commands';
10
+
11
+ export class ExtensionHistory extends Extension {
12
+ name = 'history';
13
+
14
+ options = {
15
+ depth: 100,
16
+ newGroupDelay: 500,
17
+ };
18
+
19
+ override getCommandFactories(editor: CoreEditor): Partial<CommandFactories> {
20
+ return {
21
+ 'undo': () => undo,
22
+ 'redo': () => redo,
23
+ };
24
+ }
25
+
26
+ override getKeyboardShortcuts(): Partial<CommandShortcuts> {
27
+ // https://stackoverflow.com/a/73619128
28
+ const mac = typeof navigator != 'undefined'
29
+ ? /Mac|iP(hone|[oa]d)/.test(navigator?.platform)
30
+ : false;
31
+
32
+ const shortcuts = {
33
+ 'Mod-z': 'undo',
34
+ 'Mod-y': 'redo',
35
+ };
36
+ if (!mac) {
37
+ shortcuts['Mod-y'] = 'redo';
38
+ }
39
+
40
+ return shortcuts;
41
+ }
42
+
43
+ override getProseMirrorPlugins(): Plugin[] {
44
+ return [
45
+ history(this.options),
46
+ ];
47
+ }
48
+ }
@@ -0,0 +1,158 @@
1
+ import {
2
+ DOMParser,
3
+ DOMSerializer,
4
+ Fragment,
5
+ Node,
6
+ type ParseOptions,
7
+ Schema,
8
+ } from 'prosemirror-model';
9
+
10
+ import { type Converter, type CoreEditor, Extension } from '@kerebron/editor';
11
+
12
+ export type CreateNodeFromContentOptions = {
13
+ parseOptions?: ParseOptions;
14
+ errorOnInvalidContent?: boolean;
15
+ };
16
+
17
+ export function getHTMLFromFragment(
18
+ fragment: Fragment,
19
+ schema: Schema,
20
+ ): string {
21
+ const document = globalThis.document;
22
+ const documentFragment = DOMSerializer.fromSchema(schema).serializeFragment(
23
+ fragment,
24
+ { document },
25
+ );
26
+
27
+ const temporaryDocument = document.implementation.createHTMLDocument();
28
+ const container = temporaryDocument.createElement('div');
29
+
30
+ container.appendChild(documentFragment);
31
+
32
+ return container.innerHTML;
33
+ }
34
+
35
+ const removeWhitespaces = (node: HTMLElement) => {
36
+ const children = node.childNodes;
37
+
38
+ for (let i = children.length - 1; i >= 0; i -= 1) {
39
+ const child = children[i];
40
+
41
+ if (
42
+ child.nodeType === 3 && child.nodeValue &&
43
+ /^(\n\s\s|\n)$/.test(child.nodeValue)
44
+ ) {
45
+ node.removeChild(child);
46
+ } else if (child.nodeType === 1) {
47
+ removeWhitespaces(child as HTMLElement);
48
+ }
49
+ }
50
+
51
+ return node;
52
+ };
53
+
54
+ export function elementFromString(value: string): HTMLElement {
55
+ // add a wrapper to preserve leading and trailing whitespace
56
+ const wrappedValue = `<html lang="en"><body>${value}</body></html>`;
57
+
58
+ const body =
59
+ new globalThis.DOMParser().parseFromString(wrappedValue, 'text/html').body;
60
+
61
+ return removeWhitespaces(body);
62
+ }
63
+
64
+ function prepareContentCheckSchema(schema: Schema): Schema {
65
+ const contentCheckSchema = new Schema({
66
+ topNode: schema.spec.topNode,
67
+ marks: schema.spec.marks,
68
+ // Prosemirror's schemas are executed such that: the last to execute, matches last
69
+ // This means that we can add a catch-all node at the end of the schema to catch any content that we don't know how to handle
70
+ nodes: schema.spec.nodes.append({
71
+ __unknown__catch__all__node: {
72
+ content: 'inline*',
73
+ group: 'block',
74
+ parseDOM: [
75
+ {
76
+ tag: '*',
77
+ getAttrs: (e) => {
78
+ // Try to stringify the element for a more helpful error message
79
+ const invalidContent = typeof e === 'string' ? e : e.outerHTML;
80
+ throw new Error('Invalid HTML content', {
81
+ cause: new Error(`Invalid element found: ${invalidContent}`),
82
+ });
83
+ },
84
+ },
85
+ ],
86
+ },
87
+ }),
88
+ });
89
+
90
+ return contentCheckSchema;
91
+ }
92
+
93
+ export function createNodeFromHTML(
94
+ content: string,
95
+ schema: Schema,
96
+ options?: CreateNodeFromContentOptions,
97
+ ): Node {
98
+ options = {
99
+ parseOptions: {},
100
+ ...options,
101
+ };
102
+
103
+ if (options.errorOnInvalidContent) {
104
+ const contentCheckSchema = prepareContentCheckSchema(schema);
105
+ DOMParser.fromSchema(contentCheckSchema).parse(
106
+ elementFromString(content),
107
+ options.parseOptions,
108
+ );
109
+ }
110
+
111
+ const parser = DOMParser.fromSchema(schema);
112
+ return parser.parse(elementFromString(content), options.parseOptions);
113
+ }
114
+
115
+ export function createFragmentFromHTML(
116
+ content: string,
117
+ schema: Schema,
118
+ options?: CreateNodeFromContentOptions,
119
+ ): Fragment {
120
+ options = {
121
+ parseOptions: {},
122
+ ...options,
123
+ };
124
+
125
+ if (options.errorOnInvalidContent) {
126
+ const contentCheckSchema = prepareContentCheckSchema(schema);
127
+ DOMParser.fromSchema(contentCheckSchema).parseSlice(
128
+ elementFromString(content),
129
+ options.parseOptions,
130
+ );
131
+ }
132
+
133
+ const parser = DOMParser.fromSchema(schema);
134
+ return parser.parseSlice(elementFromString(content), options.parseOptions)
135
+ .content;
136
+ }
137
+
138
+ export class ExtensionHtml extends Extension {
139
+ name = 'html';
140
+
141
+ override getConverters(
142
+ editor: CoreEditor,
143
+ schema: Schema,
144
+ ): Record<string, Converter> {
145
+ return {
146
+ 'text/html': {
147
+ fromDoc: async (document: Node): Promise<Uint8Array> => {
148
+ const html = getHTMLFromFragment(document.content, editor.schema);
149
+ return new TextEncoder().encode(html);
150
+ },
151
+ toDoc: async (buffer: Uint8Array): Promise<Node> => {
152
+ const html = new TextDecoder().decode(buffer);
153
+ return createNodeFromHTML(html, editor.schema);
154
+ },
155
+ },
156
+ };
157
+ }
158
+ }