lkb-fields-document 1.0.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/component-blocks/dist/lkb-fields-document-component-blocks.cjs.d.ts +2 -0
- package/component-blocks/dist/lkb-fields-document-component-blocks.cjs.js +306 -0
- package/component-blocks/dist/lkb-fields-document-component-blocks.esm.js +300 -0
- package/component-blocks/dist/lkb-fields-document-component-blocks.node.cjs.js +306 -0
- package/component-blocks/dist/lkb-fields-document-component-blocks.node.esm.js +300 -0
- package/component-blocks/package.json +4 -0
- package/dist/Cell-0ac0ac66.node.cjs.js +21 -0
- package/dist/Cell-242f7404.esm.js +17 -0
- package/dist/Cell-3103f73d.node.esm.js +17 -0
- package/dist/Cell-bfb56d74.cjs.js +21 -0
- package/dist/Field-0e0f75ed.node.cjs.js +1628 -0
- package/dist/Field-28177061.cjs.js +1628 -0
- package/dist/Field-35b79e6b.node.esm.js +1619 -0
- package/dist/Field-92d13205.esm.js +1619 -0
- package/dist/api-2f524611.esm.js +502 -0
- package/dist/api-73636987.cjs.js +506 -0
- package/dist/api-8e2b20b8.node.cjs.js +506 -0
- package/dist/api-c32e360e.node.esm.js +502 -0
- package/dist/callout-ui-2aded278.cjs.js +131 -0
- package/dist/callout-ui-3e5ca544.node.esm.js +126 -0
- package/dist/callout-ui-8b5f2376.esm.js +126 -0
- package/dist/callout-ui-ad50f301.node.cjs.js +131 -0
- package/dist/declarations/src/component-blocks.d.ts +4 -0
- package/dist/declarations/src/component-blocks.d.ts.map +1 -0
- package/dist/declarations/src/document-editor/component-blocks/api.d.ts +120 -0
- package/dist/declarations/src/document-editor/component-blocks/api.d.ts.map +1 -0
- package/dist/declarations/src/document-editor/component-blocks/types.d.ts +241 -0
- package/dist/declarations/src/document-editor/component-blocks/types.d.ts.map +1 -0
- package/dist/declarations/src/document-editor/toolset/relationship/relationship-shared.d.ts +10 -0
- package/dist/declarations/src/document-editor/toolset/relationship/relationship-shared.d.ts.map +1 -0
- package/dist/declarations/src/index.d.ts +7 -0
- package/dist/declarations/src/index.d.ts.map +1 -0
- package/dist/declarations/src/my-component-blocks/index.d.ts +46 -0
- package/dist/declarations/src/my-component-blocks/index.d.ts.map +1 -0
- package/dist/declarations/src/structure/Cell.d.ts +5 -0
- package/dist/declarations/src/structure/Cell.d.ts.map +1 -0
- package/dist/declarations/src/structure/Field.d.ts +5 -0
- package/dist/declarations/src/structure/Field.d.ts.map +1 -0
- package/dist/declarations/src/structure/controller.d.ts +10 -0
- package/dist/declarations/src/structure/controller.d.ts.map +1 -0
- package/dist/declarations/src/structure/structure.d.ts +4 -0
- package/dist/declarations/src/structure/structure.d.ts.map +1 -0
- package/dist/declarations/src/structure-views.d.ts +5 -0
- package/dist/declarations/src/structure-views.d.ts.map +1 -0
- package/dist/declarations/src/types/DocumentFeatures.d.ts +33 -0
- package/dist/declarations/src/types/DocumentFeatures.d.ts.map +1 -0
- package/dist/declarations/src/types/DocumentFieldConfig.d.ts +18 -0
- package/dist/declarations/src/types/DocumentFieldConfig.d.ts.map +1 -0
- package/dist/declarations/src/types/FormattingConfig.d.ts +28 -0
- package/dist/declarations/src/types/FormattingConfig.d.ts.map +1 -0
- package/dist/declarations/src/types/RelationshipsConfig.d.ts +9 -0
- package/dist/declarations/src/types/RelationshipsConfig.d.ts.map +1 -0
- package/dist/declarations/src/types/StructureFieldConfig.d.ts +10 -0
- package/dist/declarations/src/types/StructureFieldConfig.d.ts.map +1 -0
- package/dist/declarations/src/validation/structure-validation.d.ts +218 -0
- package/dist/declarations/src/validation/structure-validation.d.ts.map +1 -0
- package/dist/declarations/src/views/Cell.d.ts +5 -0
- package/dist/declarations/src/views/Cell.d.ts.map +1 -0
- package/dist/declarations/src/views/Field.d.ts +5 -0
- package/dist/declarations/src/views/Field.d.ts.map +1 -0
- package/dist/declarations/src/views/controller.d.ts +15 -0
- package/dist/declarations/src/views/controller.d.ts.map +1 -0
- package/dist/declarations/src/views/document.d.ts +4 -0
- package/dist/declarations/src/views/document.d.ts.map +1 -0
- package/dist/declarations/src/views.d.ts +7 -0
- package/dist/declarations/src/views.d.ts.map +1 -0
- package/dist/editor-shared-a6e340e6.node.esm.js +1993 -0
- package/dist/editor-shared-a997ae98.node.cjs.js +2007 -0
- package/dist/editor-shared-cc1293ed.cjs.js +2007 -0
- package/dist/editor-shared-da518ba3.esm.js +1993 -0
- package/dist/form-from-preview-2042b9ef.cjs.js +512 -0
- package/dist/form-from-preview-5df6e492.node.esm.js +508 -0
- package/dist/form-from-preview-9e501058.node.cjs.js +512 -0
- package/dist/form-from-preview-b3a66f37.esm.js +508 -0
- package/dist/index-06c36775.cjs.js +14 -0
- package/dist/index-586adb8f.node.esm.js +11 -0
- package/dist/index-67d52357.esm.js +11 -0
- package/dist/index-c3223fdc.node.cjs.js +14 -0
- package/dist/layouts-6412fa2a.esm.js +189 -0
- package/dist/layouts-a4a3cf0b.node.cjs.js +196 -0
- package/dist/layouts-ba9a558b.cjs.js +196 -0
- package/dist/layouts-e653b908.node.esm.js +189 -0
- package/dist/lkb-fields-document.cjs.d.ts +2 -0
- package/dist/lkb-fields-document.cjs.js +1167 -0
- package/dist/lkb-fields-document.esm.js +1162 -0
- package/dist/lkb-fields-document.node.cjs.js +1167 -0
- package/dist/lkb-fields-document.node.esm.js +1162 -0
- package/dist/shared-0533009e.cjs.js +594 -0
- package/dist/shared-4684cc24.node.cjs.js +594 -0
- package/dist/shared-5e864055.node.esm.js +579 -0
- package/dist/shared-aaba5901.esm.js +579 -0
- package/dist/toolbar-state-3359e2f3.cjs.js +994 -0
- package/dist/toolbar-state-945823b8.node.esm.js +971 -0
- package/dist/toolbar-state-9611743f.node.cjs.js +994 -0
- package/dist/toolbar-state-bc8fe661.esm.js +971 -0
- package/dist/utils-06bcddc4.node.cjs.js +747 -0
- package/dist/utils-200ff260.node.esm.js +722 -0
- package/dist/utils-6409f730.cjs.js +747 -0
- package/dist/utils-bc6a0b82.esm.js +722 -0
- package/package.json +118 -0
- package/structure-views/dist/lkb-fields-document-structure-views.cjs.d.ts +2 -0
- package/structure-views/dist/lkb-fields-document-structure-views.cjs.js +138 -0
- package/structure-views/dist/lkb-fields-document-structure-views.esm.js +131 -0
- package/structure-views/dist/lkb-fields-document-structure-views.node.cjs.js +138 -0
- package/structure-views/dist/lkb-fields-document-structure-views.node.esm.js +131 -0
- package/structure-views/package.json +4 -0
- package/views/dist/lkb-fields-document-views.cjs.d.ts +2 -0
- package/views/dist/lkb-fields-document-views.cjs.js +114 -0
- package/views/dist/lkb-fields-document-views.esm.js +95 -0
- package/views/dist/lkb-fields-document-views.node.cjs.js +114 -0
- package/views/dist/lkb-fields-document-views.node.esm.js +95 -0
- package/views/package.json +4 -0
|
@@ -0,0 +1,1993 @@
|
|
|
1
|
+
import { Range, Editor, Transforms, Node, Path, Text, Element, Point, createEditor } from 'slate';
|
|
2
|
+
import { withHistory } from 'slate-history';
|
|
3
|
+
import weakMemoize from '@emotion/weak-memoize';
|
|
4
|
+
import { b as getDocumentFeaturesForChildField, c as getAncestorSchemas, d as assert, e as getValueAtPropPath, m as moveChildren, f as getKeysForArrayValue, g as getInitialPropsValue, s as setKeysForArrayValue, r as replaceValueAtPropPath, h as findChildPropPaths, a as assertNever, t as traverseProps, i as getNewArrayElementKey, E as EditorAfterButIgnoringingPointsWithNoContent, j as isElementActive, k as insertNodesButReplaceIfSelectionIsAtEmptyParagraphOrHeading } from './utils-200ff260.node.esm.js';
|
|
5
|
+
import { a as areArraysEqual, n as normalizeTextBasedOnInlineMarksAndSoftBreaks, b as normalizeElementBasedOnDocumentFeatures, c as normalizeInlineBasedOnLinksAndRelationships, g as getAncestorComponentChildFieldDocumentFeatures, w as withList, d as withParagraphs, e as withLayouts, f as withDocumentFeaturesNormalization } from './shared-5e864055.node.esm.js';
|
|
6
|
+
import { i as isValidURL } from './index-586adb8f.node.esm.js';
|
|
7
|
+
import mdASTUtilFromMarkdown from 'mdast-util-from-markdown';
|
|
8
|
+
import autoLinkLiteralFromMarkdownExtension from 'mdast-util-gfm-autolink-literal/from-markdown';
|
|
9
|
+
import autoLinkLiteralMarkdownSyntax from 'micromark-extension-gfm-autolink-literal';
|
|
10
|
+
import gfmStrikethroughFromMarkdownExtension from 'mdast-util-gfm-strikethrough/from-markdown';
|
|
11
|
+
import gfmStrikethroughMarkdownSyntax from 'micromark-extension-gfm-strikethrough';
|
|
12
|
+
|
|
13
|
+
function getAncestorComponentBlock(editor) {
|
|
14
|
+
if (editor.selection) {
|
|
15
|
+
const ancestorEntry = Editor.above(editor, {
|
|
16
|
+
match: node => Element.isElement(node) && Editor.isBlock(editor, node) && node.type !== 'paragraph'
|
|
17
|
+
});
|
|
18
|
+
if (ancestorEntry && (ancestorEntry[0].type === 'component-block-prop' || ancestorEntry[0].type === 'component-inline-prop')) {
|
|
19
|
+
return {
|
|
20
|
+
isInside: true,
|
|
21
|
+
componentBlock: Editor.parent(editor, ancestorEntry[1]),
|
|
22
|
+
prop: ancestorEntry
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
isInside: false
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
const alreadyNormalizedThings = new WeakMap();
|
|
31
|
+
function normalizeNodeWithinComponentProp([node, path], editor, fieldOptions, relationships) {
|
|
32
|
+
let alreadyNormalizedNodes = alreadyNormalizedThings.get(fieldOptions);
|
|
33
|
+
if (!alreadyNormalizedNodes) {
|
|
34
|
+
alreadyNormalizedNodes = new WeakSet();
|
|
35
|
+
alreadyNormalizedThings.set(fieldOptions, alreadyNormalizedNodes);
|
|
36
|
+
}
|
|
37
|
+
if (alreadyNormalizedNodes.has(node)) return false;
|
|
38
|
+
let didNormalization = false;
|
|
39
|
+
if (fieldOptions.inlineMarks !== 'inherit' && Text.isText(node)) {
|
|
40
|
+
didNormalization = normalizeTextBasedOnInlineMarksAndSoftBreaks([node, path], editor, fieldOptions.inlineMarks, fieldOptions.softBreaks);
|
|
41
|
+
}
|
|
42
|
+
if (Element.isElement(node)) {
|
|
43
|
+
for (const [i, child] of node.children.entries()) {
|
|
44
|
+
if (normalizeNodeWithinComponentProp([child, [...path, i]], editor, fieldOptions, relationships)) {
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (fieldOptions.kind === 'block') {
|
|
49
|
+
if (node.type === 'component-block') {
|
|
50
|
+
if (!fieldOptions.componentBlocks) {
|
|
51
|
+
Transforms.unwrapNodes(editor, {
|
|
52
|
+
at: path
|
|
53
|
+
});
|
|
54
|
+
didNormalization = true;
|
|
55
|
+
}
|
|
56
|
+
} else {
|
|
57
|
+
didNormalization = normalizeElementBasedOnDocumentFeatures([node, path], editor, fieldOptions.documentFeatures, relationships);
|
|
58
|
+
}
|
|
59
|
+
} else {
|
|
60
|
+
didNormalization = normalizeInlineBasedOnLinksAndRelationships([node, path], editor, fieldOptions.documentFeatures.links, fieldOptions.documentFeatures.relationships, relationships);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (didNormalization === false) {
|
|
64
|
+
alreadyNormalizedNodes.add(node);
|
|
65
|
+
}
|
|
66
|
+
return didNormalization;
|
|
67
|
+
}
|
|
68
|
+
function canSchemaContainChildField(rootSchema) {
|
|
69
|
+
const queue = new Set([rootSchema]);
|
|
70
|
+
for (const schema of queue) {
|
|
71
|
+
if (schema.kind === 'form' || schema.kind === 'relationship') continue;
|
|
72
|
+
if (schema.kind === 'child') return true;
|
|
73
|
+
if (schema.kind === 'array') {
|
|
74
|
+
queue.add(schema.element);
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (schema.kind === 'object') {
|
|
78
|
+
for (const innerProp of Object.values(schema.fields)) {
|
|
79
|
+
queue.add(innerProp);
|
|
80
|
+
}
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (schema.kind === 'conditional') {
|
|
84
|
+
for (const innerProp of Object.values(schema.values)) {
|
|
85
|
+
queue.add(innerProp);
|
|
86
|
+
}
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
assertNever(schema);
|
|
90
|
+
}
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
function doesSchemaOnlyEverContainASingleChildField(rootSchema) {
|
|
94
|
+
const queue = new Set([rootSchema]);
|
|
95
|
+
let hasFoundChildField = false;
|
|
96
|
+
for (const schema of queue) {
|
|
97
|
+
if (schema.kind === 'form' || schema.kind === 'relationship') continue;
|
|
98
|
+
if (schema.kind === 'child') {
|
|
99
|
+
if (hasFoundChildField) return false;
|
|
100
|
+
hasFoundChildField = true;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (schema.kind === 'array') {
|
|
104
|
+
if (canSchemaContainChildField(schema.element)) return false;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (schema.kind === 'object') {
|
|
108
|
+
for (const innerProp of Object.values(schema.fields)) {
|
|
109
|
+
queue.add(innerProp);
|
|
110
|
+
}
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (schema.kind === 'conditional') {
|
|
114
|
+
for (const innerProp of Object.values(schema.values)) {
|
|
115
|
+
queue.add(innerProp);
|
|
116
|
+
}
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
assertNever(schema);
|
|
120
|
+
}
|
|
121
|
+
return hasFoundChildField;
|
|
122
|
+
}
|
|
123
|
+
function findArrayFieldsWithSingleChildField(schema, value) {
|
|
124
|
+
const propPaths = [];
|
|
125
|
+
traverseProps(schema, value, (schema, value, path) => {
|
|
126
|
+
if (schema.kind === 'array' && doesSchemaOnlyEverContainASingleChildField(schema.element)) {
|
|
127
|
+
propPaths.push([path, schema]);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
return propPaths;
|
|
131
|
+
}
|
|
132
|
+
function isEmptyChildFieldNode(element) {
|
|
133
|
+
const firstChild = element.children[0];
|
|
134
|
+
return element.children.length === 1 && (element.type === 'component-inline-prop' && firstChild.type === undefined && firstChild.text === '' || element.type === 'component-block-prop' && firstChild.type === 'paragraph' && firstChild.children.length === 1 && firstChild.children[0].type === undefined && firstChild.children[0].text === '');
|
|
135
|
+
}
|
|
136
|
+
function withComponentBlocks(blockComponents, editorDocumentFeatures, relationships, editor) {
|
|
137
|
+
// note that conflicts between the editor document features
|
|
138
|
+
// and the child field document features are dealt with elsewhere
|
|
139
|
+
const memoizedGetDocumentFeaturesForChildField = weakMemoize(options => {
|
|
140
|
+
return getDocumentFeaturesForChildField(editorDocumentFeatures, options);
|
|
141
|
+
});
|
|
142
|
+
const {
|
|
143
|
+
normalizeNode,
|
|
144
|
+
deleteBackward,
|
|
145
|
+
insertBreak
|
|
146
|
+
} = editor;
|
|
147
|
+
editor.deleteBackward = unit => {
|
|
148
|
+
if (editor.selection) {
|
|
149
|
+
const ancestorComponentBlock = getAncestorComponentBlock(editor);
|
|
150
|
+
if (ancestorComponentBlock.isInside && Range.isCollapsed(editor.selection) && Editor.isStart(editor, editor.selection.anchor, ancestorComponentBlock.prop[1]) && ancestorComponentBlock.prop[1][ancestorComponentBlock.prop[1].length - 1] === 0) {
|
|
151
|
+
Transforms.unwrapNodes(editor, {
|
|
152
|
+
at: ancestorComponentBlock.componentBlock[1]
|
|
153
|
+
});
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
deleteBackward(unit);
|
|
158
|
+
};
|
|
159
|
+
editor.insertBreak = () => {
|
|
160
|
+
const ancestorComponentBlock = getAncestorComponentBlock(editor);
|
|
161
|
+
if (editor.selection && ancestorComponentBlock.isInside) {
|
|
162
|
+
const {
|
|
163
|
+
prop: [componentPropNode, componentPropPath],
|
|
164
|
+
componentBlock: [componentBlockNode, componentBlockPath]
|
|
165
|
+
} = ancestorComponentBlock;
|
|
166
|
+
const isLastProp = componentPropPath[componentPropPath.length - 1] === componentBlockNode.children.length - 1;
|
|
167
|
+
if (componentPropNode.type === 'component-block-prop') {
|
|
168
|
+
const [[paragraphNode, paragraphPath]] = Editor.nodes(editor, {
|
|
169
|
+
match: node => node.type === 'paragraph'
|
|
170
|
+
});
|
|
171
|
+
const isLastParagraph = paragraphPath[paragraphPath.length - 1] === componentPropNode.children.length - 1;
|
|
172
|
+
if (Node.string(paragraphNode) === '' && isLastParagraph) {
|
|
173
|
+
if (isLastProp) {
|
|
174
|
+
Transforms.moveNodes(editor, {
|
|
175
|
+
at: paragraphPath,
|
|
176
|
+
to: Path.next(ancestorComponentBlock.componentBlock[1])
|
|
177
|
+
});
|
|
178
|
+
} else {
|
|
179
|
+
Transforms.move(editor, {
|
|
180
|
+
distance: 1,
|
|
181
|
+
unit: 'line'
|
|
182
|
+
});
|
|
183
|
+
Transforms.removeNodes(editor, {
|
|
184
|
+
at: paragraphPath
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (componentPropNode.type === 'component-inline-prop') {
|
|
191
|
+
Editor.withoutNormalizing(editor, () => {
|
|
192
|
+
const componentBlock = blockComponents[componentBlockNode.component];
|
|
193
|
+
if (componentPropNode.propPath !== undefined && componentBlock !== undefined) {
|
|
194
|
+
const rootSchema = {
|
|
195
|
+
kind: 'object',
|
|
196
|
+
fields: componentBlock.schema
|
|
197
|
+
};
|
|
198
|
+
const ancestorFields = getAncestorSchemas(rootSchema, componentPropNode.propPath, componentBlockNode.props);
|
|
199
|
+
const idx = [...ancestorFields].reverse().findIndex(item => item.kind === 'array');
|
|
200
|
+
if (idx !== -1) {
|
|
201
|
+
const arrayFieldIdx = ancestorFields.length - 1 - idx;
|
|
202
|
+
const arrayField = ancestorFields[arrayFieldIdx];
|
|
203
|
+
assert(arrayField.kind === 'array');
|
|
204
|
+
const val = getValueAtPropPath(componentBlockNode.props, componentPropNode.propPath.slice(0, arrayFieldIdx));
|
|
205
|
+
if (doesSchemaOnlyEverContainASingleChildField(arrayField.element)) {
|
|
206
|
+
if (Node.string(componentPropNode) === '' && val.length - 1 === componentPropNode.propPath[arrayFieldIdx]) {
|
|
207
|
+
Transforms.removeNodes(editor, {
|
|
208
|
+
at: componentPropPath
|
|
209
|
+
});
|
|
210
|
+
if (isLastProp) {
|
|
211
|
+
Transforms.insertNodes(editor, {
|
|
212
|
+
type: 'paragraph',
|
|
213
|
+
children: [{
|
|
214
|
+
text: ''
|
|
215
|
+
}]
|
|
216
|
+
}, {
|
|
217
|
+
at: Path.next(componentBlockPath)
|
|
218
|
+
});
|
|
219
|
+
Transforms.select(editor, Path.next(componentBlockPath));
|
|
220
|
+
} else {
|
|
221
|
+
Transforms.move(editor, {
|
|
222
|
+
distance: 1,
|
|
223
|
+
unit: 'line'
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
} else {
|
|
227
|
+
insertBreak();
|
|
228
|
+
}
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
Transforms.splitNodes(editor, {
|
|
234
|
+
always: true
|
|
235
|
+
});
|
|
236
|
+
const splitNodePath = Path.next(componentPropPath);
|
|
237
|
+
if (isLastProp) {
|
|
238
|
+
Transforms.moveNodes(editor, {
|
|
239
|
+
at: splitNodePath,
|
|
240
|
+
to: Path.next(componentBlockPath)
|
|
241
|
+
});
|
|
242
|
+
} else {
|
|
243
|
+
moveChildren(editor, splitNodePath, [...Path.next(splitNodePath), 0]);
|
|
244
|
+
Transforms.removeNodes(editor, {
|
|
245
|
+
at: splitNodePath
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
insertBreak();
|
|
253
|
+
};
|
|
254
|
+
editor.normalizeNode = entry => {
|
|
255
|
+
const [node, path] = entry;
|
|
256
|
+
if (node.type === 'component-inline-prop' && !node.propPath && (node.children.length !== 1 || !Text.isText(node.children[0]) || node.children[0].text !== '')) {
|
|
257
|
+
Transforms.removeNodes(editor, {
|
|
258
|
+
at: path
|
|
259
|
+
});
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
if (node.type === 'component-block') {
|
|
263
|
+
const componentBlock = blockComponents[node.component];
|
|
264
|
+
if (componentBlock) {
|
|
265
|
+
const rootSchema = {
|
|
266
|
+
kind: 'object',
|
|
267
|
+
fields: componentBlock.schema
|
|
268
|
+
};
|
|
269
|
+
const updatedProps = addMissingFields(node.props, rootSchema);
|
|
270
|
+
if (updatedProps !== node.props) {
|
|
271
|
+
Transforms.setNodes(editor, {
|
|
272
|
+
props: updatedProps
|
|
273
|
+
}, {
|
|
274
|
+
at: path
|
|
275
|
+
});
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
for (const [propPath, arrayField] of findArrayFieldsWithSingleChildField(rootSchema, node.props)) {
|
|
279
|
+
if (node.children.length === 1 && node.children[0].type === 'component-inline-prop' && node.children[0].propPath === undefined) {
|
|
280
|
+
break;
|
|
281
|
+
}
|
|
282
|
+
const nodesWithin = [];
|
|
283
|
+
for (const [idx, childNode] of node.children.entries()) {
|
|
284
|
+
if ((childNode.type === 'component-block-prop' || childNode.type === 'component-inline-prop') && childNode.propPath !== undefined) {
|
|
285
|
+
const subPath = childNode.propPath.concat();
|
|
286
|
+
while (subPath.length) {
|
|
287
|
+
if (typeof subPath.pop() === 'number') break;
|
|
288
|
+
}
|
|
289
|
+
if (areArraysEqual(propPath, subPath)) {
|
|
290
|
+
nodesWithin.push([idx, childNode]);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
const arrVal = getValueAtPropPath(node.props, propPath);
|
|
295
|
+
const prevKeys = getKeysForArrayValue(arrVal);
|
|
296
|
+
const prevKeysSet = new Set(prevKeys);
|
|
297
|
+
const alreadyUsedIndicies = new Set();
|
|
298
|
+
const newVal = [];
|
|
299
|
+
const newKeys = [];
|
|
300
|
+
const getNewKey = () => {
|
|
301
|
+
let key = getNewArrayElementKey();
|
|
302
|
+
while (prevKeysSet.has(key)) {
|
|
303
|
+
key = getNewArrayElementKey();
|
|
304
|
+
}
|
|
305
|
+
return key;
|
|
306
|
+
};
|
|
307
|
+
for (const [, node] of nodesWithin) {
|
|
308
|
+
const idxFromValue = node.propPath[propPath.length];
|
|
309
|
+
assert(typeof idxFromValue === 'number');
|
|
310
|
+
if (arrVal.length <= idxFromValue || alreadyUsedIndicies.has(idxFromValue) && isEmptyChildFieldNode(node)) {
|
|
311
|
+
newVal.push(getInitialPropsValue(arrayField.element));
|
|
312
|
+
newKeys.push(getNewKey());
|
|
313
|
+
} else {
|
|
314
|
+
alreadyUsedIndicies.add(idxFromValue);
|
|
315
|
+
newVal.push(arrVal[idxFromValue]);
|
|
316
|
+
newKeys.push(alreadyUsedIndicies.has(idxFromValue) ? getNewKey() : prevKeys[idxFromValue]);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
setKeysForArrayValue(newVal, newKeys);
|
|
320
|
+
if (!areArraysEqual(arrVal, newVal)) {
|
|
321
|
+
const transformedProps = replaceValueAtPropPath(rootSchema, node.props, newVal, propPath);
|
|
322
|
+
Transforms.setNodes(editor, {
|
|
323
|
+
props: transformedProps
|
|
324
|
+
}, {
|
|
325
|
+
at: path
|
|
326
|
+
});
|
|
327
|
+
for (const [idx, [idxInChildrenOfBlock, nodeWithin]] of nodesWithin.entries()) {
|
|
328
|
+
const newPropPath = [...nodeWithin.propPath];
|
|
329
|
+
newPropPath[propPath.length] = idx;
|
|
330
|
+
Transforms.setNodes(editor, {
|
|
331
|
+
propPath: newPropPath
|
|
332
|
+
}, {
|
|
333
|
+
at: [...path, idxInChildrenOfBlock]
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
const missingKeys = new Map(findChildPropPaths(node.props, componentBlock.schema).map(x => [JSON.stringify(x.path), x.options.kind]));
|
|
340
|
+
node.children.forEach(node => {
|
|
341
|
+
assert(node.type === 'component-block-prop' || node.type === 'component-inline-prop');
|
|
342
|
+
missingKeys.delete(JSON.stringify(node.propPath));
|
|
343
|
+
});
|
|
344
|
+
if (missingKeys.size) {
|
|
345
|
+
Transforms.insertNodes(editor, [...missingKeys].map(([prop, kind]) => ({
|
|
346
|
+
type: `component-${kind}-prop`,
|
|
347
|
+
propPath: prop ? JSON.parse(prop) : prop,
|
|
348
|
+
children: [{
|
|
349
|
+
text: ''
|
|
350
|
+
}]
|
|
351
|
+
})), {
|
|
352
|
+
at: [...path, node.children.length]
|
|
353
|
+
});
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
const foundProps = new Set();
|
|
357
|
+
const stringifiedInlinePropPaths = {};
|
|
358
|
+
findChildPropPaths(node.props, blockComponents[node.component].schema).forEach((x, index) => {
|
|
359
|
+
stringifiedInlinePropPaths[JSON.stringify(x.path)] = {
|
|
360
|
+
options: x.options,
|
|
361
|
+
index
|
|
362
|
+
};
|
|
363
|
+
});
|
|
364
|
+
for (const [index, childNode] of node.children.entries()) {
|
|
365
|
+
if (
|
|
366
|
+
// children that are not these will be handled by
|
|
367
|
+
// the generic allowedChildren normalization
|
|
368
|
+
childNode.type !== 'component-inline-prop' && childNode.type !== 'component-block-prop') {
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
const childPath = [...path, index];
|
|
372
|
+
const stringifiedPropPath = JSON.stringify(childNode.propPath);
|
|
373
|
+
if (stringifiedInlinePropPaths[stringifiedPropPath] === undefined) {
|
|
374
|
+
Transforms.removeNodes(editor, {
|
|
375
|
+
at: childPath
|
|
376
|
+
});
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
if (foundProps.has(stringifiedPropPath)) {
|
|
380
|
+
Transforms.removeNodes(editor, {
|
|
381
|
+
at: childPath
|
|
382
|
+
});
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
foundProps.add(stringifiedPropPath);
|
|
386
|
+
const propInfo = stringifiedInlinePropPaths[stringifiedPropPath];
|
|
387
|
+
const expectedIndex = propInfo.index;
|
|
388
|
+
if (index !== expectedIndex) {
|
|
389
|
+
Transforms.moveNodes(editor, {
|
|
390
|
+
at: childPath,
|
|
391
|
+
to: [...path, expectedIndex]
|
|
392
|
+
});
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
const expectedChildNodeType = `component-${propInfo.options.kind}-prop`;
|
|
396
|
+
if (childNode.type !== expectedChildNodeType) {
|
|
397
|
+
Transforms.setNodes(editor, {
|
|
398
|
+
type: expectedChildNodeType
|
|
399
|
+
}, {
|
|
400
|
+
at: childPath
|
|
401
|
+
});
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
const documentFeatures = memoizedGetDocumentFeaturesForChildField(propInfo.options);
|
|
405
|
+
if (normalizeNodeWithinComponentProp([childNode, childPath], editor, documentFeatures, relationships)) {
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
normalizeNode(entry);
|
|
412
|
+
};
|
|
413
|
+
return editor;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// the only thing that this will fix is a new field being added to an object field, nothing else.
|
|
417
|
+
function addMissingFields(value, schema) {
|
|
418
|
+
if (schema.kind === 'child' || schema.kind === 'form' || schema.kind === 'relationship') {
|
|
419
|
+
return value;
|
|
420
|
+
}
|
|
421
|
+
if (schema.kind === 'conditional') {
|
|
422
|
+
const conditionalValue = value;
|
|
423
|
+
const updatedInnerValue = addMissingFields(conditionalValue.value, schema.values[conditionalValue.discriminant.toString()]);
|
|
424
|
+
if (updatedInnerValue === conditionalValue.value) {
|
|
425
|
+
return value;
|
|
426
|
+
}
|
|
427
|
+
return {
|
|
428
|
+
discriminant: conditionalValue.discriminant,
|
|
429
|
+
value: updatedInnerValue
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
if (schema.kind === 'array') {
|
|
433
|
+
const arrValue = value;
|
|
434
|
+
const newArrValue = arrValue.map(x => addMissingFields(x, schema.element));
|
|
435
|
+
if (areArraysEqual(arrValue, newArrValue)) {
|
|
436
|
+
return value;
|
|
437
|
+
}
|
|
438
|
+
return newArrValue;
|
|
439
|
+
}
|
|
440
|
+
if (schema.kind === 'object') {
|
|
441
|
+
const objectValue = value;
|
|
442
|
+
let hasChanged = false;
|
|
443
|
+
const newObjectValue = {};
|
|
444
|
+
for (const [key, innerSchema] of Object.entries(schema.fields)) {
|
|
445
|
+
const innerValue = objectValue[key];
|
|
446
|
+
if (innerValue === undefined) {
|
|
447
|
+
hasChanged = true;
|
|
448
|
+
newObjectValue[key] = getInitialPropsValue(innerSchema);
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
const newInnerValue = addMissingFields(innerValue, innerSchema);
|
|
452
|
+
if (newInnerValue !== innerValue) {
|
|
453
|
+
hasChanged = true;
|
|
454
|
+
}
|
|
455
|
+
newObjectValue[key] = newInnerValue;
|
|
456
|
+
}
|
|
457
|
+
if (hasChanged) {
|
|
458
|
+
return newObjectValue;
|
|
459
|
+
}
|
|
460
|
+
return value;
|
|
461
|
+
}
|
|
462
|
+
assertNever(schema);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const isLinkActive = editor => {
|
|
466
|
+
return isElementActive(editor, 'link');
|
|
467
|
+
};
|
|
468
|
+
function wrapLink(editor, url) {
|
|
469
|
+
if (isLinkActive(editor)) {
|
|
470
|
+
Transforms.unwrapNodes(editor, {
|
|
471
|
+
match: n => n.type === 'link'
|
|
472
|
+
});
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
const {
|
|
476
|
+
selection
|
|
477
|
+
} = editor;
|
|
478
|
+
const isCollapsed = selection && Range.isCollapsed(selection);
|
|
479
|
+
if (isCollapsed) {
|
|
480
|
+
Transforms.insertNodes(editor, {
|
|
481
|
+
type: 'link',
|
|
482
|
+
href: url,
|
|
483
|
+
children: [{
|
|
484
|
+
text: url
|
|
485
|
+
}]
|
|
486
|
+
});
|
|
487
|
+
} else {
|
|
488
|
+
Transforms.wrapNodes(editor, {
|
|
489
|
+
type: 'link',
|
|
490
|
+
href: url,
|
|
491
|
+
children: [{
|
|
492
|
+
text: ''
|
|
493
|
+
}]
|
|
494
|
+
}, {
|
|
495
|
+
split: true
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
const markdownLinkPattern = /(^|\s)\[(.+?)\]\((\S+)\)$/;
|
|
500
|
+
function withLink(editorDocumentFeatures, componentBlocks, editor) {
|
|
501
|
+
const {
|
|
502
|
+
insertText,
|
|
503
|
+
isInline,
|
|
504
|
+
normalizeNode
|
|
505
|
+
} = editor;
|
|
506
|
+
editor.isInline = element => {
|
|
507
|
+
return element.type === 'link' ? true : isInline(element);
|
|
508
|
+
};
|
|
509
|
+
if (editorDocumentFeatures.links) {
|
|
510
|
+
editor.insertText = text => {
|
|
511
|
+
insertText(text);
|
|
512
|
+
if (text !== ')' || !editor.selection) return;
|
|
513
|
+
const startOfBlock = Editor.start(editor, Editor.above(editor, {
|
|
514
|
+
match: node => Element.isElement(node) && Editor.isBlock(editor, node)
|
|
515
|
+
})[1]);
|
|
516
|
+
const startOfBlockToEndOfShortcutString = Editor.string(editor, {
|
|
517
|
+
anchor: editor.selection.anchor,
|
|
518
|
+
focus: startOfBlock
|
|
519
|
+
});
|
|
520
|
+
const match = markdownLinkPattern.exec(startOfBlockToEndOfShortcutString);
|
|
521
|
+
if (!match) return;
|
|
522
|
+
const ancestorComponentChildFieldDocumentFeatures = getAncestorComponentChildFieldDocumentFeatures(editor, editorDocumentFeatures, componentBlocks);
|
|
523
|
+
if ((ancestorComponentChildFieldDocumentFeatures === null || ancestorComponentChildFieldDocumentFeatures === void 0 ? void 0 : ancestorComponentChildFieldDocumentFeatures.documentFeatures.links) === false) {
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
const [, maybeWhitespace, linkText, href] = match;
|
|
527
|
+
// by doing this, the insertText(')') above will happen in a different undo than the link replacement
|
|
528
|
+
// so that means that when someone does an undo after this
|
|
529
|
+
// it will undo to the state of "[content](link)" rather than "[content](link" (note the missing closing bracket)
|
|
530
|
+
editor.writeHistory('undos', {
|
|
531
|
+
operations: [],
|
|
532
|
+
selectionBefore: null
|
|
533
|
+
});
|
|
534
|
+
const startOfShortcut = match.index === 0 ? startOfBlock : EditorAfterButIgnoringingPointsWithNoContent(editor, startOfBlock, {
|
|
535
|
+
distance: match.index
|
|
536
|
+
});
|
|
537
|
+
const startOfLinkText = EditorAfterButIgnoringingPointsWithNoContent(editor, startOfShortcut, {
|
|
538
|
+
distance: maybeWhitespace === '' ? 1 : 2
|
|
539
|
+
});
|
|
540
|
+
const endOfLinkText = EditorAfterButIgnoringingPointsWithNoContent(editor, startOfLinkText, {
|
|
541
|
+
distance: linkText.length
|
|
542
|
+
});
|
|
543
|
+
Transforms.delete(editor, {
|
|
544
|
+
at: {
|
|
545
|
+
anchor: endOfLinkText,
|
|
546
|
+
focus: editor.selection.anchor
|
|
547
|
+
}
|
|
548
|
+
});
|
|
549
|
+
Transforms.delete(editor, {
|
|
550
|
+
at: {
|
|
551
|
+
anchor: startOfShortcut,
|
|
552
|
+
focus: startOfLinkText
|
|
553
|
+
}
|
|
554
|
+
});
|
|
555
|
+
Transforms.wrapNodes(editor, {
|
|
556
|
+
type: 'link',
|
|
557
|
+
href,
|
|
558
|
+
children: []
|
|
559
|
+
}, {
|
|
560
|
+
at: {
|
|
561
|
+
anchor: editor.selection.anchor,
|
|
562
|
+
focus: startOfShortcut
|
|
563
|
+
},
|
|
564
|
+
split: true
|
|
565
|
+
});
|
|
566
|
+
const nextNode = Editor.next(editor);
|
|
567
|
+
if (nextNode) {
|
|
568
|
+
Transforms.select(editor, nextNode[1]);
|
|
569
|
+
}
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
editor.normalizeNode = ([node, path]) => {
|
|
573
|
+
if (node.type === 'link') {
|
|
574
|
+
if (Node.string(node) === '') {
|
|
575
|
+
Transforms.unwrapNodes(editor, {
|
|
576
|
+
at: path
|
|
577
|
+
});
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
for (const [idx, child] of node.children.entries()) {
|
|
581
|
+
if (child.type === 'link') {
|
|
582
|
+
// links cannot contain links
|
|
583
|
+
Transforms.unwrapNodes(editor, {
|
|
584
|
+
at: [...path, idx]
|
|
585
|
+
});
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
if (isInlineContainer(node)) {
|
|
591
|
+
let lastMergableLink = null;
|
|
592
|
+
for (const [idx, child] of node.children.entries()) {
|
|
593
|
+
var _lastMergableLink;
|
|
594
|
+
if (child.type === 'link' && child.href === ((_lastMergableLink = lastMergableLink) === null || _lastMergableLink === void 0 ? void 0 : _lastMergableLink.node.href)) {
|
|
595
|
+
const firstLinkPath = [...path, lastMergableLink.index];
|
|
596
|
+
const secondLinkPath = [...path, idx];
|
|
597
|
+
const to = [...firstLinkPath, lastMergableLink.node.children.length];
|
|
598
|
+
// note this is going in reverse, js doesn't have double-ended iterators so it's a for(;;)
|
|
599
|
+
for (let i = child.children.length - 1; i >= 0; i--) {
|
|
600
|
+
const childPath = [...secondLinkPath, i];
|
|
601
|
+
Transforms.moveNodes(editor, {
|
|
602
|
+
at: childPath,
|
|
603
|
+
to
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
Transforms.removeNodes(editor, {
|
|
607
|
+
at: secondLinkPath
|
|
608
|
+
});
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
if (!Text.isText(child) || child.text !== '') {
|
|
612
|
+
lastMergableLink = null;
|
|
613
|
+
}
|
|
614
|
+
if (child.type === 'link') {
|
|
615
|
+
lastMergableLink = {
|
|
616
|
+
index: idx,
|
|
617
|
+
node: child
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
normalizeNode([node, path]);
|
|
623
|
+
};
|
|
624
|
+
return editor;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
function withHeading(editor) {
|
|
628
|
+
const {
|
|
629
|
+
insertBreak
|
|
630
|
+
} = editor;
|
|
631
|
+
editor.insertBreak = () => {
|
|
632
|
+
insertBreak();
|
|
633
|
+
const entry = Editor.above(editor, {
|
|
634
|
+
match: n => n.type === 'heading'
|
|
635
|
+
});
|
|
636
|
+
if (!entry || !editor.selection || !Range.isCollapsed(editor.selection)) return;
|
|
637
|
+
const path = entry[1];
|
|
638
|
+
if (
|
|
639
|
+
// we want to unwrap the heading when the user inserted a break at the end of the heading
|
|
640
|
+
// when the user inserts a break at the end of a heading, the new heading
|
|
641
|
+
// that we want to unwrap will be empty so the end will be equal to the selection
|
|
642
|
+
Point.equals(Editor.end(editor, path), editor.selection.anchor)) {
|
|
643
|
+
Transforms.unwrapNodes(editor, {
|
|
644
|
+
at: path
|
|
645
|
+
});
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
// we also want to unwrap the _previous_ heading when the user inserted a break
|
|
649
|
+
// at the start of the heading, essentially just inserting an empty paragraph above the heading
|
|
650
|
+
if (!Path.hasPrevious(path)) return;
|
|
651
|
+
const previousPath = Path.previous(path);
|
|
652
|
+
const previousNode = Node.get(editor, previousPath);
|
|
653
|
+
if (previousNode.type === 'heading' && previousNode.children.length === 1 && Text.isText(previousNode.children[0]) && previousNode.children[0].text === '') {
|
|
654
|
+
Transforms.unwrapNodes(editor, {
|
|
655
|
+
at: previousPath
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
};
|
|
659
|
+
return editor;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function insertBlockquote(editor) {
|
|
663
|
+
const isActive = isElementActive(editor, 'blockquote');
|
|
664
|
+
if (isActive) {
|
|
665
|
+
Transforms.unwrapNodes(editor, {
|
|
666
|
+
match: node => node.type === 'blockquote'
|
|
667
|
+
});
|
|
668
|
+
} else {
|
|
669
|
+
Transforms.wrapNodes(editor, {
|
|
670
|
+
type: 'blockquote',
|
|
671
|
+
children: []
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
function getDirectBlockquoteParentFromSelection(editor) {
|
|
676
|
+
if (!editor.selection) return {
|
|
677
|
+
isInside: false
|
|
678
|
+
};
|
|
679
|
+
const [, parentPath] = Editor.parent(editor, editor.selection);
|
|
680
|
+
if (!parentPath.length) {
|
|
681
|
+
return {
|
|
682
|
+
isInside: false
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
const [maybeBlockquoteParent, maybeBlockquoteParentPath] = Editor.parent(editor, parentPath);
|
|
686
|
+
const isBlockquote = maybeBlockquoteParent.type === 'blockquote';
|
|
687
|
+
return isBlockquote ? {
|
|
688
|
+
isInside: true,
|
|
689
|
+
path: maybeBlockquoteParentPath
|
|
690
|
+
} : {
|
|
691
|
+
isInside: false
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
function withBlockquote(editor) {
|
|
695
|
+
const {
|
|
696
|
+
insertBreak,
|
|
697
|
+
deleteBackward
|
|
698
|
+
} = editor;
|
|
699
|
+
editor.deleteBackward = unit => {
|
|
700
|
+
if (editor.selection) {
|
|
701
|
+
const parentBlockquote = getDirectBlockquoteParentFromSelection(editor);
|
|
702
|
+
if (parentBlockquote.isInside && Range.isCollapsed(editor.selection) &&
|
|
703
|
+
// the selection is at the start of the paragraph
|
|
704
|
+
editor.selection.anchor.offset === 0 &&
|
|
705
|
+
// it's the first paragraph in the panel
|
|
706
|
+
editor.selection.anchor.path[editor.selection.anchor.path.length - 2] === 0) {
|
|
707
|
+
Transforms.unwrapNodes(editor, {
|
|
708
|
+
match: node => node.type === 'blockquote',
|
|
709
|
+
split: true
|
|
710
|
+
});
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
deleteBackward(unit);
|
|
715
|
+
};
|
|
716
|
+
editor.insertBreak = () => {
|
|
717
|
+
const panel = getDirectBlockquoteParentFromSelection(editor);
|
|
718
|
+
if (editor.selection && panel.isInside) {
|
|
719
|
+
const [node, nodePath] = Editor.node(editor, editor.selection);
|
|
720
|
+
if (Path.isDescendant(nodePath, panel.path) && Node.string(node) === '') {
|
|
721
|
+
Transforms.unwrapNodes(editor, {
|
|
722
|
+
match: node => node.type === 'blockquote',
|
|
723
|
+
split: true
|
|
724
|
+
});
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
insertBreak();
|
|
729
|
+
};
|
|
730
|
+
return editor;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
function withRelationship(editor) {
|
|
734
|
+
const {
|
|
735
|
+
isVoid,
|
|
736
|
+
isInline
|
|
737
|
+
} = editor;
|
|
738
|
+
editor.isVoid = element => element.type === 'relationship' || isVoid(element);
|
|
739
|
+
editor.isInline = element => element.type === 'relationship' || isInline(element);
|
|
740
|
+
return editor;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
function insertDivider(editor) {
|
|
744
|
+
insertNodesButReplaceIfSelectionIsAtEmptyParagraphOrHeading(editor, {
|
|
745
|
+
type: 'divider',
|
|
746
|
+
children: [{
|
|
747
|
+
text: ''
|
|
748
|
+
}]
|
|
749
|
+
});
|
|
750
|
+
Editor.insertNode(editor, {
|
|
751
|
+
type: 'paragraph',
|
|
752
|
+
children: [{
|
|
753
|
+
text: ''
|
|
754
|
+
}]
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
function withDivider(editor) {
|
|
758
|
+
const {
|
|
759
|
+
isVoid
|
|
760
|
+
} = editor;
|
|
761
|
+
editor.isVoid = node => {
|
|
762
|
+
return node.type === 'divider' || isVoid(node);
|
|
763
|
+
};
|
|
764
|
+
return editor;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
function withCodeBlock(editor) {
|
|
768
|
+
const {
|
|
769
|
+
insertBreak,
|
|
770
|
+
normalizeNode
|
|
771
|
+
} = editor;
|
|
772
|
+
editor.insertBreak = () => {
|
|
773
|
+
const [node, path] = Editor.above(editor, {
|
|
774
|
+
match: n => Element.isElement(n) && Editor.isBlock(editor, n)
|
|
775
|
+
}) || [editor, []];
|
|
776
|
+
if (node.type === 'code' && Text.isText(node.children[0])) {
|
|
777
|
+
const text = node.children[0].text;
|
|
778
|
+
if (text[text.length - 1] === '\n' && editor.selection && Range.isCollapsed(editor.selection) && Point.equals(Editor.end(editor, path), editor.selection.anchor)) {
|
|
779
|
+
insertBreak();
|
|
780
|
+
Transforms.setNodes(editor, {
|
|
781
|
+
type: 'paragraph',
|
|
782
|
+
children: []
|
|
783
|
+
});
|
|
784
|
+
Transforms.delete(editor, {
|
|
785
|
+
distance: 1,
|
|
786
|
+
at: {
|
|
787
|
+
path: [...path, 0],
|
|
788
|
+
offset: text.length - 1
|
|
789
|
+
}
|
|
790
|
+
});
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
editor.insertText('\n');
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
insertBreak();
|
|
797
|
+
};
|
|
798
|
+
editor.normalizeNode = ([node, path]) => {
|
|
799
|
+
if (node.type === 'code' && Element.isElement(node)) {
|
|
800
|
+
for (const [index, childNode] of node.children.entries()) {
|
|
801
|
+
if (!Text.isText(childNode)) {
|
|
802
|
+
if (editor.isVoid(childNode)) {
|
|
803
|
+
Transforms.removeNodes(editor, {
|
|
804
|
+
at: [...path, index]
|
|
805
|
+
});
|
|
806
|
+
} else {
|
|
807
|
+
Transforms.unwrapNodes(editor, {
|
|
808
|
+
at: [...path, index]
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
813
|
+
const marks = Object.keys(childNode).filter(x => x !== 'text');
|
|
814
|
+
if (marks.length) {
|
|
815
|
+
Transforms.unsetNodes(editor, marks, {
|
|
816
|
+
at: [...path, index]
|
|
817
|
+
});
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
normalizeNode([node, path]);
|
|
823
|
+
};
|
|
824
|
+
return editor;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
const allMarkdownShortcuts = {
|
|
828
|
+
bold: ['**', '__'],
|
|
829
|
+
italic: ['*', '_'],
|
|
830
|
+
strikethrough: ['~~'],
|
|
831
|
+
code: ['`']
|
|
832
|
+
};
|
|
833
|
+
function applyMark(editor, mark, shortcutText, startOfStartPoint) {
|
|
834
|
+
// so that this starts a new undo group
|
|
835
|
+
editor.writeHistory('undos', {
|
|
836
|
+
operations: [],
|
|
837
|
+
selectionBefore: null
|
|
838
|
+
});
|
|
839
|
+
const startPointRef = Editor.pointRef(editor, startOfStartPoint);
|
|
840
|
+
Transforms.delete(editor, {
|
|
841
|
+
at: editor.selection.anchor,
|
|
842
|
+
distance: shortcutText.length,
|
|
843
|
+
reverse: true
|
|
844
|
+
});
|
|
845
|
+
Transforms.delete(editor, {
|
|
846
|
+
at: startOfStartPoint,
|
|
847
|
+
distance: shortcutText.length
|
|
848
|
+
});
|
|
849
|
+
Transforms.setNodes(editor, {
|
|
850
|
+
[mark]: true
|
|
851
|
+
}, {
|
|
852
|
+
match: Text.isText,
|
|
853
|
+
split: true,
|
|
854
|
+
at: {
|
|
855
|
+
anchor: startPointRef.unref(),
|
|
856
|
+
focus: editor.selection.anchor
|
|
857
|
+
}
|
|
858
|
+
});
|
|
859
|
+
// once you've ended the shortcut, you're done with the mark
|
|
860
|
+
// so we need to remove it so the text you insert after doesn't have it
|
|
861
|
+
editor.removeMark(mark);
|
|
862
|
+
}
|
|
863
|
+
function withMarks(editorDocumentFeatures, componentBlocks, editor) {
|
|
864
|
+
const {
|
|
865
|
+
insertText,
|
|
866
|
+
insertBreak
|
|
867
|
+
} = editor;
|
|
868
|
+
editor.insertBreak = () => {
|
|
869
|
+
insertBreak();
|
|
870
|
+
const marksAfterInsertBreak = Editor.marks(editor);
|
|
871
|
+
if (!marksAfterInsertBreak || !editor.selection) return;
|
|
872
|
+
const parentBlock = Editor.above(editor, {
|
|
873
|
+
match: node => Element.isElement(node) && Editor.isBlock(editor, node)
|
|
874
|
+
});
|
|
875
|
+
if (!parentBlock) return;
|
|
876
|
+
const point = EditorAfterButIgnoringingPointsWithNoContent(editor, editor.selection.anchor);
|
|
877
|
+
const marksAfterInsertBreakArr = Object.keys(marksAfterInsertBreak);
|
|
878
|
+
if (!point || !Path.isDescendant(point.path, parentBlock[1])) {
|
|
879
|
+
for (const mark of marksAfterInsertBreakArr) {
|
|
880
|
+
editor.removeMark(mark);
|
|
881
|
+
}
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
884
|
+
const textNode = Node.get(editor, point.path);
|
|
885
|
+
for (const mark of marksAfterInsertBreakArr) {
|
|
886
|
+
if (!textNode[mark]) {
|
|
887
|
+
editor.removeMark(mark);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
};
|
|
891
|
+
const selectedMarkdownShortcuts = {};
|
|
892
|
+
const enabledMarks = editorDocumentFeatures.formatting.inlineMarks;
|
|
893
|
+
Object.keys(allMarkdownShortcuts).forEach(mark => {
|
|
894
|
+
if (enabledMarks[mark]) {
|
|
895
|
+
selectedMarkdownShortcuts[mark] = allMarkdownShortcuts[mark];
|
|
896
|
+
}
|
|
897
|
+
});
|
|
898
|
+
if (Object.keys(selectedMarkdownShortcuts).length === 0) return editor;
|
|
899
|
+
editor.insertText = text => {
|
|
900
|
+
insertText(text);
|
|
901
|
+
if (editor.selection && Range.isCollapsed(editor.selection)) {
|
|
902
|
+
for (const [mark, shortcuts] of Object.entries(selectedMarkdownShortcuts)) {
|
|
903
|
+
for (const shortcutText of shortcuts) {
|
|
904
|
+
if (text === shortcutText[shortcutText.length - 1]) {
|
|
905
|
+
// this function is not inlined because
|
|
906
|
+
// https://github.com/swc-project/swc/issues/2622
|
|
907
|
+
const startOfBlock = getStartOfBlock(editor);
|
|
908
|
+
const startOfBlockToEndOfShortcutString = Editor.string(editor, {
|
|
909
|
+
anchor: editor.selection.anchor,
|
|
910
|
+
focus: startOfBlock
|
|
911
|
+
});
|
|
912
|
+
const hasWhitespaceBeforeEndOfShortcut = /\s/.test(startOfBlockToEndOfShortcutString.slice(-shortcutText.length - 1, -shortcutText.length));
|
|
913
|
+
const endOfShortcutContainsExpectedContent = shortcutText === startOfBlockToEndOfShortcutString.slice(-shortcutText.length);
|
|
914
|
+
if (hasWhitespaceBeforeEndOfShortcut || !endOfShortcutContainsExpectedContent) {
|
|
915
|
+
continue;
|
|
916
|
+
}
|
|
917
|
+
const strToMatchOn = startOfBlockToEndOfShortcutString.slice(0, -shortcutText.length - 1);
|
|
918
|
+
// TODO: use regex probs
|
|
919
|
+
for (const [offsetFromStartOfBlock] of [...strToMatchOn].reverse().entries()) {
|
|
920
|
+
const expectedShortcutText = strToMatchOn.slice(offsetFromStartOfBlock, offsetFromStartOfBlock + shortcutText.length);
|
|
921
|
+
if (expectedShortcutText !== shortcutText) {
|
|
922
|
+
continue;
|
|
923
|
+
}
|
|
924
|
+
const startOfStartOfShortcut = offsetFromStartOfBlock === 0 ? startOfBlock : EditorAfterButIgnoringingPointsWithNoContent(editor, startOfBlock, {
|
|
925
|
+
distance: offsetFromStartOfBlock
|
|
926
|
+
});
|
|
927
|
+
const endOfStartOfShortcut = Editor.after(editor, startOfStartOfShortcut, {
|
|
928
|
+
distance: shortcutText.length
|
|
929
|
+
});
|
|
930
|
+
if (offsetFromStartOfBlock !== 0 && !/\s/.test(Editor.string(editor, {
|
|
931
|
+
anchor: Editor.before(editor, startOfStartOfShortcut, {
|
|
932
|
+
unit: 'character'
|
|
933
|
+
}),
|
|
934
|
+
focus: startOfStartOfShortcut
|
|
935
|
+
}))) {
|
|
936
|
+
continue;
|
|
937
|
+
}
|
|
938
|
+
const contentBetweenShortcuts = Editor.string(editor, {
|
|
939
|
+
anchor: endOfStartOfShortcut,
|
|
940
|
+
focus: editor.selection.anchor
|
|
941
|
+
}).slice(0, -shortcutText.length);
|
|
942
|
+
if (contentBetweenShortcuts === '' || /\s/.test(contentBetweenShortcuts[0])) {
|
|
943
|
+
continue;
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
// this is a bit of a weird one
|
|
947
|
+
// let's say you had <text>__thing _<cursor /></text> and you insert `_`.
|
|
948
|
+
// without the below, that would turn into <text italic>_thing _<cursor /></text>
|
|
949
|
+
// but it's probably meant to be bold but it's not because of the space before the ending _
|
|
950
|
+
// there's probably a better way to do this but meh, this works
|
|
951
|
+
if (mark === 'italic' && (contentBetweenShortcuts[0] === '_' || contentBetweenShortcuts[0] === '*')) {
|
|
952
|
+
continue;
|
|
953
|
+
}
|
|
954
|
+
const ancestorComponentChildFieldDocumentFeatures = getAncestorComponentChildFieldDocumentFeatures(editor, editorDocumentFeatures, componentBlocks);
|
|
955
|
+
if (ancestorComponentChildFieldDocumentFeatures && ancestorComponentChildFieldDocumentFeatures.inlineMarks !== 'inherit' && ancestorComponentChildFieldDocumentFeatures.inlineMarks[mark] === false) {
|
|
956
|
+
continue;
|
|
957
|
+
}
|
|
958
|
+
applyMark(editor, mark, shortcutText, startOfStartOfShortcut);
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
};
|
|
966
|
+
return editor;
|
|
967
|
+
}
|
|
968
|
+
function getStartOfBlock(editor) {
|
|
969
|
+
return Editor.start(editor, Editor.above(editor, {
|
|
970
|
+
match: node => Element.isElement(node) && Editor.isBlock(editor, node)
|
|
971
|
+
})[1]);
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
function withSoftBreaks(editor) {
|
|
975
|
+
// TODO: should soft breaks only work in particular places
|
|
976
|
+
editor.insertSoftBreak = () => {
|
|
977
|
+
Transforms.insertText(editor, '\n');
|
|
978
|
+
};
|
|
979
|
+
return editor;
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
const shortcuts = {
|
|
983
|
+
'...': '…',
|
|
984
|
+
'-->': '→',
|
|
985
|
+
'->': '→',
|
|
986
|
+
'<-': '←',
|
|
987
|
+
'<--': '←',
|
|
988
|
+
'--': '–'
|
|
989
|
+
};
|
|
990
|
+
function withShortcuts(editor) {
|
|
991
|
+
const {
|
|
992
|
+
insertText
|
|
993
|
+
} = editor;
|
|
994
|
+
editor.insertText = text => {
|
|
995
|
+
insertText(text);
|
|
996
|
+
if (text === ' ' && editor.selection && Range.isCollapsed(editor.selection)) {
|
|
997
|
+
const selectionPoint = editor.selection.anchor;
|
|
998
|
+
const ancestorBlock = Editor.above(editor, {
|
|
999
|
+
match: node => Element.isElement(node) && Editor.isBlock(editor, node)
|
|
1000
|
+
});
|
|
1001
|
+
if (ancestorBlock) {
|
|
1002
|
+
Object.keys(shortcuts).forEach(shortcut => {
|
|
1003
|
+
const pointBefore = Editor.before(editor, selectionPoint, {
|
|
1004
|
+
unit: 'character',
|
|
1005
|
+
distance: shortcut.length + 1
|
|
1006
|
+
});
|
|
1007
|
+
if (pointBefore && Path.isDescendant(pointBefore.path, ancestorBlock[1])) {
|
|
1008
|
+
const range = {
|
|
1009
|
+
anchor: selectionPoint,
|
|
1010
|
+
focus: pointBefore
|
|
1011
|
+
};
|
|
1012
|
+
const str = Editor.string(editor, range);
|
|
1013
|
+
if (str.slice(0, shortcut.length) === shortcut) {
|
|
1014
|
+
editor.writeHistory('undos', {
|
|
1015
|
+
operations: [],
|
|
1016
|
+
selectionBefore: null
|
|
1017
|
+
});
|
|
1018
|
+
Transforms.select(editor, range);
|
|
1019
|
+
editor.insertText(shortcuts[shortcut] + ' ');
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
});
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
};
|
|
1026
|
+
return editor;
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
const nodeListsWithoutInsertMenu = new WeakSet();
|
|
1030
|
+
const nodesWithoutInsertMenu = new WeakSet();
|
|
1031
|
+
function findPathWithInsertMenu(node, path) {
|
|
1032
|
+
if (Text.isText(node)) return node.insertMenu ? path : undefined;
|
|
1033
|
+
if (nodeListsWithoutInsertMenu.has(node.children)) return;
|
|
1034
|
+
for (const [index, child] of node.children.entries()) {
|
|
1035
|
+
if (nodesWithoutInsertMenu.has(child)) continue;
|
|
1036
|
+
const maybePath = findPathWithInsertMenu(child, [...path, index]);
|
|
1037
|
+
if (maybePath) {
|
|
1038
|
+
return maybePath;
|
|
1039
|
+
}
|
|
1040
|
+
nodesWithoutInsertMenu.add(child);
|
|
1041
|
+
}
|
|
1042
|
+
nodeListsWithoutInsertMenu.add(node.children);
|
|
1043
|
+
}
|
|
1044
|
+
function removeInsertMenuMarkWhenOutsideOfSelection(editor) {
|
|
1045
|
+
var _Editor$marks;
|
|
1046
|
+
const path = findPathWithInsertMenu(editor, []);
|
|
1047
|
+
if (path && !((_Editor$marks = Editor.marks(editor)) !== null && _Editor$marks !== void 0 && _Editor$marks.insertMenu) && (!editor.selection || !Path.equals(editor.selection.anchor.path, path) || !Path.equals(editor.selection.focus.path, path))) {
|
|
1048
|
+
Transforms.unsetNodes(editor, 'insertMenu', {
|
|
1049
|
+
at: path
|
|
1050
|
+
});
|
|
1051
|
+
return true;
|
|
1052
|
+
}
|
|
1053
|
+
return false;
|
|
1054
|
+
}
|
|
1055
|
+
function withInsertMenu(editor) {
|
|
1056
|
+
const {
|
|
1057
|
+
normalizeNode,
|
|
1058
|
+
apply,
|
|
1059
|
+
insertText
|
|
1060
|
+
} = editor;
|
|
1061
|
+
editor.normalizeNode = ([node, path]) => {
|
|
1062
|
+
if (Text.isText(node) && node.insertMenu) {
|
|
1063
|
+
if (node.text[0] !== '/') {
|
|
1064
|
+
Transforms.unsetNodes(editor, 'insertMenu', {
|
|
1065
|
+
at: path
|
|
1066
|
+
});
|
|
1067
|
+
return;
|
|
1068
|
+
}
|
|
1069
|
+
const whitespaceMatch = /\s/.exec(node.text);
|
|
1070
|
+
if (whitespaceMatch) {
|
|
1071
|
+
Transforms.unsetNodes(editor, 'insertMenu', {
|
|
1072
|
+
at: {
|
|
1073
|
+
anchor: {
|
|
1074
|
+
path,
|
|
1075
|
+
offset: whitespaceMatch.index
|
|
1076
|
+
},
|
|
1077
|
+
focus: Editor.end(editor, path)
|
|
1078
|
+
},
|
|
1079
|
+
match: Text.isText,
|
|
1080
|
+
split: true
|
|
1081
|
+
});
|
|
1082
|
+
return;
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
if (Editor.isEditor(editor) && removeInsertMenuMarkWhenOutsideOfSelection(editor)) {
|
|
1086
|
+
return;
|
|
1087
|
+
}
|
|
1088
|
+
normalizeNode([node, path]);
|
|
1089
|
+
};
|
|
1090
|
+
editor.apply = op => {
|
|
1091
|
+
apply(op);
|
|
1092
|
+
// we're calling this here AND in normalizeNode
|
|
1093
|
+
// because normalizeNode won't be called on selection changes
|
|
1094
|
+
// but apply will
|
|
1095
|
+
// we're still calling this from normalizeNode though because we want it to happen
|
|
1096
|
+
// when normalization happens
|
|
1097
|
+
if (op.type === 'set_selection') {
|
|
1098
|
+
removeInsertMenuMarkWhenOutsideOfSelection(editor);
|
|
1099
|
+
}
|
|
1100
|
+
};
|
|
1101
|
+
editor.insertText = text => {
|
|
1102
|
+
insertText(text);
|
|
1103
|
+
if (editor.selection && text === '/') {
|
|
1104
|
+
const startOfBlock = Editor.start(editor, Editor.above(editor, {
|
|
1105
|
+
match: node => Element.isElement(node) && Editor.isBlock(editor, node)
|
|
1106
|
+
})[1]);
|
|
1107
|
+
const before = Editor.before(editor, editor.selection.anchor, {
|
|
1108
|
+
unit: 'character'
|
|
1109
|
+
});
|
|
1110
|
+
if (before && (Point.equals(startOfBlock, before) || before.offset !== 0 && /\s/.test(Node.get(editor, before.path).text[before.offset - 1]))) {
|
|
1111
|
+
Transforms.setNodes(editor, {
|
|
1112
|
+
insertMenu: true
|
|
1113
|
+
}, {
|
|
1114
|
+
at: {
|
|
1115
|
+
anchor: before,
|
|
1116
|
+
focus: editor.selection.anchor
|
|
1117
|
+
},
|
|
1118
|
+
match: Text.isText,
|
|
1119
|
+
split: true
|
|
1120
|
+
});
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
};
|
|
1124
|
+
return editor;
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
function withBlockMarkdownShortcuts(documentFeatures, componentBlocks, editor) {
|
|
1128
|
+
const {
|
|
1129
|
+
insertText
|
|
1130
|
+
} = editor;
|
|
1131
|
+
const shortcuts = Object.create(null);
|
|
1132
|
+
const editorDocumentFeaturesForNormalizationToCheck = {
|
|
1133
|
+
...documentFeatures,
|
|
1134
|
+
relationships: true
|
|
1135
|
+
};
|
|
1136
|
+
const addShortcut = (text, insert, shouldBeEnabledInComponentBlock, type = 'paragraph') => {
|
|
1137
|
+
if (!shouldBeEnabledInComponentBlock(editorDocumentFeaturesForNormalizationToCheck)) return;
|
|
1138
|
+
const trigger = text[text.length - 1];
|
|
1139
|
+
if (!shortcuts[trigger]) {
|
|
1140
|
+
shortcuts[trigger] = Object.create(null);
|
|
1141
|
+
}
|
|
1142
|
+
shortcuts[trigger][text] = {
|
|
1143
|
+
insert,
|
|
1144
|
+
type,
|
|
1145
|
+
shouldBeEnabledInComponentBlock
|
|
1146
|
+
};
|
|
1147
|
+
};
|
|
1148
|
+
addShortcut('1. ', () => {
|
|
1149
|
+
Transforms.wrapNodes(editor, {
|
|
1150
|
+
type: 'ordered-list',
|
|
1151
|
+
children: []
|
|
1152
|
+
}, {
|
|
1153
|
+
match: n => Element.isElement(n) && Editor.isBlock(editor, n)
|
|
1154
|
+
});
|
|
1155
|
+
}, features => features.formatting.listTypes.ordered);
|
|
1156
|
+
addShortcut('- ', () => {
|
|
1157
|
+
Transforms.wrapNodes(editor, {
|
|
1158
|
+
type: 'unordered-list',
|
|
1159
|
+
children: []
|
|
1160
|
+
}, {
|
|
1161
|
+
match: n => Element.isElement(n) && Editor.isBlock(editor, n)
|
|
1162
|
+
});
|
|
1163
|
+
}, features => features.formatting.listTypes.unordered);
|
|
1164
|
+
addShortcut('* ', () => {
|
|
1165
|
+
Transforms.wrapNodes(editor, {
|
|
1166
|
+
type: 'unordered-list',
|
|
1167
|
+
children: []
|
|
1168
|
+
}, {
|
|
1169
|
+
match: n => Element.isElement(n) && Editor.isBlock(editor, n)
|
|
1170
|
+
});
|
|
1171
|
+
}, features => features.formatting.listTypes.unordered);
|
|
1172
|
+
documentFeatures.formatting.headingLevels.forEach(level => {
|
|
1173
|
+
addShortcut('#'.repeat(level) + ' ', () => {
|
|
1174
|
+
Transforms.setNodes(editor, {
|
|
1175
|
+
type: 'heading',
|
|
1176
|
+
level
|
|
1177
|
+
}, {
|
|
1178
|
+
match: node => node.type === 'paragraph' || node.type === 'heading'
|
|
1179
|
+
});
|
|
1180
|
+
}, features => features.formatting.headingLevels.includes(level), 'heading-or-paragraph');
|
|
1181
|
+
});
|
|
1182
|
+
addShortcut('> ', () => {
|
|
1183
|
+
Transforms.wrapNodes(editor, {
|
|
1184
|
+
type: 'blockquote',
|
|
1185
|
+
children: []
|
|
1186
|
+
}, {
|
|
1187
|
+
match: node => node.type === 'paragraph'
|
|
1188
|
+
});
|
|
1189
|
+
}, features => features.formatting.blockTypes.blockquote);
|
|
1190
|
+
addShortcut('```', () => {
|
|
1191
|
+
Transforms.wrapNodes(editor, {
|
|
1192
|
+
type: 'code',
|
|
1193
|
+
children: []
|
|
1194
|
+
}, {
|
|
1195
|
+
match: node => node.type === 'paragraph'
|
|
1196
|
+
});
|
|
1197
|
+
}, features => features.formatting.blockTypes.code);
|
|
1198
|
+
addShortcut('---', () => {
|
|
1199
|
+
insertDivider(editor);
|
|
1200
|
+
}, features => features.dividers);
|
|
1201
|
+
editor.insertText = text => {
|
|
1202
|
+
insertText(text);
|
|
1203
|
+
const shortcutsForTrigger = shortcuts[text];
|
|
1204
|
+
if (shortcutsForTrigger && editor.selection && Range.isCollapsed(editor.selection)) {
|
|
1205
|
+
const {
|
|
1206
|
+
anchor
|
|
1207
|
+
} = editor.selection;
|
|
1208
|
+
const block = Editor.above(editor, {
|
|
1209
|
+
match: node => Element.isElement(node) && Editor.isBlock(editor, node)
|
|
1210
|
+
});
|
|
1211
|
+
if (!block || block[0].type !== 'paragraph' && block[0].type !== 'heading') return;
|
|
1212
|
+
const start = Editor.start(editor, block[1]);
|
|
1213
|
+
const range = {
|
|
1214
|
+
anchor,
|
|
1215
|
+
focus: start
|
|
1216
|
+
};
|
|
1217
|
+
const shortcutText = Editor.string(editor, range);
|
|
1218
|
+
const shortcut = shortcutsForTrigger[shortcutText];
|
|
1219
|
+
if (!shortcut || shortcut.type === 'paragraph' && block[0].type !== 'paragraph') {
|
|
1220
|
+
return;
|
|
1221
|
+
}
|
|
1222
|
+
const locationDocumentFeatures = getAncestorComponentChildFieldDocumentFeatures(editor, documentFeatures, componentBlocks);
|
|
1223
|
+
if (locationDocumentFeatures && (locationDocumentFeatures.kind === 'inline' || !shortcut.shouldBeEnabledInComponentBlock(locationDocumentFeatures.documentFeatures))) {
|
|
1224
|
+
return;
|
|
1225
|
+
}
|
|
1226
|
+
// so that this starts a new undo group
|
|
1227
|
+
editor.writeHistory('undos', {
|
|
1228
|
+
operations: [],
|
|
1229
|
+
selectionBefore: null
|
|
1230
|
+
});
|
|
1231
|
+
Transforms.select(editor, range);
|
|
1232
|
+
Transforms.delete(editor);
|
|
1233
|
+
shortcut.insert();
|
|
1234
|
+
}
|
|
1235
|
+
};
|
|
1236
|
+
return editor;
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
// a v important note
|
|
1240
|
+
// marks in the markdown ast/html are represented quite differently to how they are in slate
|
|
1241
|
+
// if you had the markdown **something https://site.com something**
|
|
1242
|
+
// the bold node is the parent of the link node
|
|
1243
|
+
// but in slate, marks are only represented on text nodes
|
|
1244
|
+
|
|
1245
|
+
const currentlyActiveMarks = new Set();
|
|
1246
|
+
const currentlyDisabledMarks = new Set();
|
|
1247
|
+
let currentLink = null;
|
|
1248
|
+
function addMarkToChildren(mark, cb) {
|
|
1249
|
+
const wasPreviouslyActive = currentlyActiveMarks.has(mark);
|
|
1250
|
+
currentlyActiveMarks.add(mark);
|
|
1251
|
+
try {
|
|
1252
|
+
return cb();
|
|
1253
|
+
} finally {
|
|
1254
|
+
if (!wasPreviouslyActive) {
|
|
1255
|
+
currentlyActiveMarks.delete(mark);
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
function setLinkForChildren(href, cb) {
|
|
1260
|
+
// we'll only use the outer link
|
|
1261
|
+
if (currentLink !== null) {
|
|
1262
|
+
return cb();
|
|
1263
|
+
}
|
|
1264
|
+
currentLink = href;
|
|
1265
|
+
try {
|
|
1266
|
+
return cb();
|
|
1267
|
+
} finally {
|
|
1268
|
+
currentLink = null;
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
function addMarksToChildren(marks, cb) {
|
|
1272
|
+
const marksToRemove = new Set();
|
|
1273
|
+
for (const mark of marks) {
|
|
1274
|
+
if (!currentlyActiveMarks.has(mark)) {
|
|
1275
|
+
marksToRemove.add(mark);
|
|
1276
|
+
}
|
|
1277
|
+
currentlyActiveMarks.add(mark);
|
|
1278
|
+
}
|
|
1279
|
+
try {
|
|
1280
|
+
return cb();
|
|
1281
|
+
} finally {
|
|
1282
|
+
for (const mark of marksToRemove) {
|
|
1283
|
+
currentlyActiveMarks.delete(mark);
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
function forceDisableMarkForChildren(mark, cb) {
|
|
1288
|
+
const wasPreviouslyDisabled = currentlyDisabledMarks.has(mark);
|
|
1289
|
+
currentlyDisabledMarks.add(mark);
|
|
1290
|
+
try {
|
|
1291
|
+
return cb();
|
|
1292
|
+
} finally {
|
|
1293
|
+
if (!wasPreviouslyDisabled) {
|
|
1294
|
+
currentlyDisabledMarks.delete(mark);
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
/**
|
|
1300
|
+
* This type is more strict than `Element & { type: 'link'; }` because `children`
|
|
1301
|
+
* is constrained to only contain Text nodes. This can't be assumed generally around the editor
|
|
1302
|
+
* (because of inline relationships or nested links(which are normalized away but the editor needs to not break if it happens))
|
|
1303
|
+
* but where this type is used, we're only going to allow links to contain Text and that's important
|
|
1304
|
+
* so that we know a block will never be inside an inline because Slate gets unhappy when that happens
|
|
1305
|
+
* (really the link inline should probably be a mark rather than an inline,
|
|
1306
|
+
* non-void inlines are probably always bad but that would imply changing the document
|
|
1307
|
+
* structure which would be such unnecessary breakage)
|
|
1308
|
+
*/
|
|
1309
|
+
|
|
1310
|
+
// inline relationships are not here because we never create them from handling a paste from html or markdown
|
|
1311
|
+
|
|
1312
|
+
function getInlineNodes(text) {
|
|
1313
|
+
const node = {
|
|
1314
|
+
text
|
|
1315
|
+
};
|
|
1316
|
+
for (const mark of currentlyActiveMarks) {
|
|
1317
|
+
if (!currentlyDisabledMarks.has(mark)) {
|
|
1318
|
+
node[mark] = true;
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
if (currentLink !== null) {
|
|
1322
|
+
return [{
|
|
1323
|
+
text: ''
|
|
1324
|
+
}, {
|
|
1325
|
+
type: 'link',
|
|
1326
|
+
href: currentLink,
|
|
1327
|
+
children: [node]
|
|
1328
|
+
}, {
|
|
1329
|
+
text: ''
|
|
1330
|
+
}];
|
|
1331
|
+
}
|
|
1332
|
+
return [node];
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
// very loosely based on https://github.com/ianstormtaylor/slate/blob/d22c76ae1313fe82111317417912a2670e73f5c9/site/examples/paste-html.tsx
|
|
1336
|
+
function getAlignmentFromElement(element) {
|
|
1337
|
+
const parent = element.parentElement;
|
|
1338
|
+
// confluence
|
|
1339
|
+
const attribute = parent === null || parent === void 0 ? void 0 : parent.getAttribute('data-align');
|
|
1340
|
+
// note: we don't show html that confluence would parse as alignment
|
|
1341
|
+
// we could change that but meh
|
|
1342
|
+
// (they match on div.fabric-editor-block-mark with data-align)
|
|
1343
|
+
if (attribute === 'center' || attribute === 'end') {
|
|
1344
|
+
return attribute;
|
|
1345
|
+
}
|
|
1346
|
+
if (element instanceof HTMLElement) {
|
|
1347
|
+
// Google docs
|
|
1348
|
+
const textAlign = element.style.textAlign;
|
|
1349
|
+
if (textAlign === 'center') {
|
|
1350
|
+
return 'center';
|
|
1351
|
+
}
|
|
1352
|
+
// TODO: RTL things?
|
|
1353
|
+
if (textAlign === 'right' || textAlign === 'end') {
|
|
1354
|
+
return 'end';
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
const headings = {
|
|
1359
|
+
H1: 1,
|
|
1360
|
+
H2: 2,
|
|
1361
|
+
H3: 3,
|
|
1362
|
+
H4: 4,
|
|
1363
|
+
H5: 5,
|
|
1364
|
+
H6: 6
|
|
1365
|
+
};
|
|
1366
|
+
const TEXT_TAGS = {
|
|
1367
|
+
CODE: 'code',
|
|
1368
|
+
DEL: 'strikethrough',
|
|
1369
|
+
S: 'strikethrough',
|
|
1370
|
+
STRIKE: 'strikethrough',
|
|
1371
|
+
EM: 'italic',
|
|
1372
|
+
I: 'italic',
|
|
1373
|
+
STRONG: 'bold',
|
|
1374
|
+
U: 'underline',
|
|
1375
|
+
SUP: 'superscript',
|
|
1376
|
+
SUB: 'subscript',
|
|
1377
|
+
KBD: 'keyboard'
|
|
1378
|
+
};
|
|
1379
|
+
function marksFromElementAttributes(element) {
|
|
1380
|
+
const marks = new Set();
|
|
1381
|
+
const style = element.style;
|
|
1382
|
+
const {
|
|
1383
|
+
nodeName
|
|
1384
|
+
} = element;
|
|
1385
|
+
const markFromNodeName = TEXT_TAGS[nodeName];
|
|
1386
|
+
if (markFromNodeName) {
|
|
1387
|
+
marks.add(markFromNodeName);
|
|
1388
|
+
}
|
|
1389
|
+
const {
|
|
1390
|
+
fontWeight,
|
|
1391
|
+
textDecoration,
|
|
1392
|
+
verticalAlign
|
|
1393
|
+
} = style;
|
|
1394
|
+
if (textDecoration === 'underline') {
|
|
1395
|
+
marks.add('underline');
|
|
1396
|
+
} else if (textDecoration === 'line-through') {
|
|
1397
|
+
marks.add('strikethrough');
|
|
1398
|
+
}
|
|
1399
|
+
// confluence
|
|
1400
|
+
if (nodeName === 'SPAN' && element.classList.contains('code')) {
|
|
1401
|
+
marks.add('code');
|
|
1402
|
+
}
|
|
1403
|
+
// Google Docs does weird things with <b>
|
|
1404
|
+
if (nodeName === 'B' && fontWeight !== 'normal') {
|
|
1405
|
+
marks.add('bold');
|
|
1406
|
+
} else if (typeof fontWeight === 'string' && (fontWeight === 'bold' || fontWeight === 'bolder' || fontWeight === '1000' || /^[5-9]\d{2}$/.test(fontWeight))) {
|
|
1407
|
+
marks.add('bold');
|
|
1408
|
+
}
|
|
1409
|
+
if (style.fontStyle === 'italic') {
|
|
1410
|
+
marks.add('italic');
|
|
1411
|
+
}
|
|
1412
|
+
// Google Docs uses vertical align for subscript and superscript instead of <sup> and <sub>
|
|
1413
|
+
if (verticalAlign === 'super') {
|
|
1414
|
+
marks.add('superscript');
|
|
1415
|
+
} else if (verticalAlign === 'sub') {
|
|
1416
|
+
marks.add('subscript');
|
|
1417
|
+
}
|
|
1418
|
+
return marks;
|
|
1419
|
+
}
|
|
1420
|
+
function deserializeHTML(html) {
|
|
1421
|
+
const parsed = new DOMParser().parseFromString(html, 'text/html');
|
|
1422
|
+
return fixNodesForBlockChildren(deserializeNodes(parsed.body.childNodes));
|
|
1423
|
+
}
|
|
1424
|
+
function deserializeHTMLNode(el) {
|
|
1425
|
+
if (!(el instanceof globalThis.HTMLElement)) {
|
|
1426
|
+
const text = el.textContent;
|
|
1427
|
+
if (!text) {
|
|
1428
|
+
return [];
|
|
1429
|
+
}
|
|
1430
|
+
return getInlineNodes(text);
|
|
1431
|
+
}
|
|
1432
|
+
if (el.nodeName === 'BR') {
|
|
1433
|
+
return getInlineNodes('\n');
|
|
1434
|
+
}
|
|
1435
|
+
if (el.nodeName === 'IMG') {
|
|
1436
|
+
const alt = el.getAttribute('alt');
|
|
1437
|
+
return getInlineNodes(alt !== null && alt !== void 0 ? alt : '');
|
|
1438
|
+
}
|
|
1439
|
+
if (el.nodeName === 'HR') {
|
|
1440
|
+
return [{
|
|
1441
|
+
type: 'divider',
|
|
1442
|
+
children: [{
|
|
1443
|
+
text: ''
|
|
1444
|
+
}]
|
|
1445
|
+
}];
|
|
1446
|
+
}
|
|
1447
|
+
const marks = marksFromElementAttributes(el);
|
|
1448
|
+
|
|
1449
|
+
// Dropbox Paper displays blockquotes as lists for some reason
|
|
1450
|
+
if (el.classList.contains('listtype-quote')) {
|
|
1451
|
+
marks.delete('italic');
|
|
1452
|
+
return addMarksToChildren(marks, () => [{
|
|
1453
|
+
type: 'blockquote',
|
|
1454
|
+
children: fixNodesForBlockChildren(deserializeNodes(el.childNodes))
|
|
1455
|
+
}]);
|
|
1456
|
+
}
|
|
1457
|
+
return addMarksToChildren(marks, () => {
|
|
1458
|
+
const {
|
|
1459
|
+
nodeName
|
|
1460
|
+
} = el;
|
|
1461
|
+
if (nodeName === 'A') {
|
|
1462
|
+
const href = el.getAttribute('href');
|
|
1463
|
+
if (href) {
|
|
1464
|
+
return setLinkForChildren(href, () => forceDisableMarkForChildren('underline', () => deserializeNodes(el.childNodes)));
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
if (nodeName === 'PRE' && el.textContent) {
|
|
1468
|
+
return [{
|
|
1469
|
+
type: 'code',
|
|
1470
|
+
children: [{
|
|
1471
|
+
text: el.textContent || ''
|
|
1472
|
+
}]
|
|
1473
|
+
}];
|
|
1474
|
+
}
|
|
1475
|
+
const deserialized = deserializeNodes(el.childNodes);
|
|
1476
|
+
const children = fixNodesForBlockChildren(deserialized);
|
|
1477
|
+
if (nodeName === 'LI') {
|
|
1478
|
+
let nestedList;
|
|
1479
|
+
const listItemContent = {
|
|
1480
|
+
type: 'list-item-content',
|
|
1481
|
+
children: children.filter(node => {
|
|
1482
|
+
if (nestedList === undefined && (node.type === 'ordered-list' || node.type === 'unordered-list')) {
|
|
1483
|
+
nestedList = node;
|
|
1484
|
+
return false;
|
|
1485
|
+
}
|
|
1486
|
+
return true;
|
|
1487
|
+
})
|
|
1488
|
+
};
|
|
1489
|
+
const listItemChildren = nestedList ? [listItemContent, nestedList] : [listItemContent];
|
|
1490
|
+
return [{
|
|
1491
|
+
type: 'list-item',
|
|
1492
|
+
children: listItemChildren
|
|
1493
|
+
}];
|
|
1494
|
+
}
|
|
1495
|
+
if (nodeName === 'P') {
|
|
1496
|
+
return [{
|
|
1497
|
+
type: 'paragraph',
|
|
1498
|
+
textAlign: getAlignmentFromElement(el),
|
|
1499
|
+
children
|
|
1500
|
+
}];
|
|
1501
|
+
}
|
|
1502
|
+
const headingLevel = headings[nodeName];
|
|
1503
|
+
if (typeof headingLevel === 'number') {
|
|
1504
|
+
return [{
|
|
1505
|
+
type: 'heading',
|
|
1506
|
+
level: headingLevel,
|
|
1507
|
+
textAlign: getAlignmentFromElement(el),
|
|
1508
|
+
children
|
|
1509
|
+
}];
|
|
1510
|
+
}
|
|
1511
|
+
if (nodeName === 'BLOCKQUOTE') {
|
|
1512
|
+
return [{
|
|
1513
|
+
type: 'blockquote',
|
|
1514
|
+
children
|
|
1515
|
+
}];
|
|
1516
|
+
}
|
|
1517
|
+
if (nodeName === 'OL') {
|
|
1518
|
+
return [{
|
|
1519
|
+
type: 'ordered-list',
|
|
1520
|
+
children
|
|
1521
|
+
}];
|
|
1522
|
+
}
|
|
1523
|
+
if (nodeName === 'UL') {
|
|
1524
|
+
return [{
|
|
1525
|
+
type: 'unordered-list',
|
|
1526
|
+
children
|
|
1527
|
+
}];
|
|
1528
|
+
}
|
|
1529
|
+
if (nodeName === 'DIV' && !isBlock(children[0])) {
|
|
1530
|
+
return [{
|
|
1531
|
+
type: 'paragraph',
|
|
1532
|
+
children
|
|
1533
|
+
}];
|
|
1534
|
+
}
|
|
1535
|
+
return deserialized;
|
|
1536
|
+
});
|
|
1537
|
+
}
|
|
1538
|
+
function deserializeNodes(nodes) {
|
|
1539
|
+
const outputNodes = [];
|
|
1540
|
+
for (const node of nodes) {
|
|
1541
|
+
outputNodes.push(...deserializeHTMLNode(node));
|
|
1542
|
+
}
|
|
1543
|
+
return outputNodes;
|
|
1544
|
+
}
|
|
1545
|
+
function fixNodesForBlockChildren(deserializedNodes) {
|
|
1546
|
+
if (!deserializedNodes.length) {
|
|
1547
|
+
// Slate also gets unhappy if an element has no children
|
|
1548
|
+
// the empty text nodes will get normalized away if they're not needed
|
|
1549
|
+
return [{
|
|
1550
|
+
text: ''
|
|
1551
|
+
}];
|
|
1552
|
+
}
|
|
1553
|
+
if (deserializedNodes.some(isBlock)) {
|
|
1554
|
+
const result = [];
|
|
1555
|
+
let queuedInlines = [];
|
|
1556
|
+
const flushInlines = () => {
|
|
1557
|
+
if (queuedInlines.length) {
|
|
1558
|
+
result.push({
|
|
1559
|
+
type: 'paragraph',
|
|
1560
|
+
children: queuedInlines
|
|
1561
|
+
});
|
|
1562
|
+
queuedInlines = [];
|
|
1563
|
+
}
|
|
1564
|
+
};
|
|
1565
|
+
for (const node of deserializedNodes) {
|
|
1566
|
+
if (isBlock(node)) {
|
|
1567
|
+
flushInlines();
|
|
1568
|
+
result.push(node);
|
|
1569
|
+
continue;
|
|
1570
|
+
}
|
|
1571
|
+
// we want to ignore whitespace between block level elements
|
|
1572
|
+
// useful info about whitespace in html:
|
|
1573
|
+
// https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model/Whitespace
|
|
1574
|
+
if (Node.string(node).trim() !== '') {
|
|
1575
|
+
queuedInlines.push(node);
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
flushInlines();
|
|
1579
|
+
return result;
|
|
1580
|
+
}
|
|
1581
|
+
return deserializedNodes;
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
const markdownConfig = {
|
|
1585
|
+
mdastExtensions: [autoLinkLiteralFromMarkdownExtension, gfmStrikethroughFromMarkdownExtension],
|
|
1586
|
+
extensions: [autoLinkLiteralMarkdownSyntax, gfmStrikethroughMarkdownSyntax()]
|
|
1587
|
+
};
|
|
1588
|
+
function deserializeMarkdown(markdown) {
|
|
1589
|
+
const root = mdASTUtilFromMarkdown(markdown, markdownConfig);
|
|
1590
|
+
let nodes = root.children;
|
|
1591
|
+
if (nodes.length === 1 && nodes[0].type === 'paragraph') {
|
|
1592
|
+
nodes = nodes[0].children;
|
|
1593
|
+
}
|
|
1594
|
+
return deserializeChildren(nodes, markdown);
|
|
1595
|
+
}
|
|
1596
|
+
function deserializeChildren(nodes, input) {
|
|
1597
|
+
const outputNodes = [];
|
|
1598
|
+
for (const node of nodes) {
|
|
1599
|
+
const result = deserializeMarkdownNode(node, input);
|
|
1600
|
+
if (result.length) {
|
|
1601
|
+
outputNodes.push(...result);
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
if (!outputNodes.length) {
|
|
1605
|
+
outputNodes.push({
|
|
1606
|
+
text: ''
|
|
1607
|
+
});
|
|
1608
|
+
}
|
|
1609
|
+
return outputNodes;
|
|
1610
|
+
}
|
|
1611
|
+
function deserializeMarkdownNode(node, input) {
|
|
1612
|
+
switch (node.type) {
|
|
1613
|
+
case 'blockquote':
|
|
1614
|
+
return [{
|
|
1615
|
+
type: 'blockquote',
|
|
1616
|
+
children: deserializeChildren(node.children, input)
|
|
1617
|
+
}];
|
|
1618
|
+
case 'link':
|
|
1619
|
+
{
|
|
1620
|
+
// arguably this could just return a link node rather than use setLinkForChildren since the children _should_ only be inlines
|
|
1621
|
+
// but rather than relying on the markdown parser we use being correct in this way since it isn't nicely codified in types
|
|
1622
|
+
// let's be safe since we already have the code to do it the safer way because of html pasting
|
|
1623
|
+
return setLinkForChildren(node.url, () => deserializeChildren(node.children, input));
|
|
1624
|
+
}
|
|
1625
|
+
case 'code':
|
|
1626
|
+
return [{
|
|
1627
|
+
type: 'code',
|
|
1628
|
+
children: [{
|
|
1629
|
+
text: node.value
|
|
1630
|
+
}]
|
|
1631
|
+
}];
|
|
1632
|
+
case 'paragraph':
|
|
1633
|
+
return [{
|
|
1634
|
+
type: 'paragraph',
|
|
1635
|
+
children: deserializeChildren(node.children, input)
|
|
1636
|
+
}];
|
|
1637
|
+
case 'heading':
|
|
1638
|
+
{
|
|
1639
|
+
return [{
|
|
1640
|
+
type: 'heading',
|
|
1641
|
+
level: node.depth,
|
|
1642
|
+
children: deserializeChildren(node.children, input)
|
|
1643
|
+
}];
|
|
1644
|
+
}
|
|
1645
|
+
case 'list':
|
|
1646
|
+
{
|
|
1647
|
+
return [{
|
|
1648
|
+
type: node.ordered ? 'ordered-list' : 'unordered-list',
|
|
1649
|
+
children: deserializeChildren(node.children, input)
|
|
1650
|
+
}];
|
|
1651
|
+
}
|
|
1652
|
+
case 'listItem':
|
|
1653
|
+
return [{
|
|
1654
|
+
type: 'list-item',
|
|
1655
|
+
children: deserializeChildren(node.children, input)
|
|
1656
|
+
}];
|
|
1657
|
+
case 'thematicBreak':
|
|
1658
|
+
return [{
|
|
1659
|
+
type: 'divider',
|
|
1660
|
+
children: [{
|
|
1661
|
+
text: ''
|
|
1662
|
+
}]
|
|
1663
|
+
}];
|
|
1664
|
+
case 'break':
|
|
1665
|
+
return getInlineNodes('\n');
|
|
1666
|
+
case 'delete':
|
|
1667
|
+
return addMarkToChildren('strikethrough', () => deserializeChildren(node.children, input));
|
|
1668
|
+
case 'strong':
|
|
1669
|
+
return addMarkToChildren('bold', () => deserializeChildren(node.children, input));
|
|
1670
|
+
case 'emphasis':
|
|
1671
|
+
return addMarkToChildren('italic', () => deserializeChildren(node.children, input));
|
|
1672
|
+
case 'inlineCode':
|
|
1673
|
+
return addMarkToChildren('code', () => getInlineNodes(node.value));
|
|
1674
|
+
case 'text':
|
|
1675
|
+
return getInlineNodes(node.value);
|
|
1676
|
+
}
|
|
1677
|
+
return getInlineNodes(input.slice(node.position.start.offset, node.position.end.offset));
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
const urlPattern = /https?:\/\//;
|
|
1681
|
+
function insertFragmentButDifferent(editor, nodes) {
|
|
1682
|
+
const firstNode = nodes[0];
|
|
1683
|
+
if (Element.isElement(firstNode) && Editor.isBlock(editor, firstNode)) {
|
|
1684
|
+
insertNodesButReplaceIfSelectionIsAtEmptyParagraphOrHeading(editor, nodes);
|
|
1685
|
+
} else {
|
|
1686
|
+
Transforms.insertFragment(editor, nodes);
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
function withPasting(editor) {
|
|
1690
|
+
const {
|
|
1691
|
+
insertData,
|
|
1692
|
+
setFragmentData
|
|
1693
|
+
} = editor;
|
|
1694
|
+
editor.setFragmentData = data => {
|
|
1695
|
+
if (editor.selection) {
|
|
1696
|
+
data.setData('application/x-keystone-document-editor', 'true');
|
|
1697
|
+
}
|
|
1698
|
+
setFragmentData(data);
|
|
1699
|
+
};
|
|
1700
|
+
editor.insertData = data => {
|
|
1701
|
+
// this exists because behind the scenes, Slate sets the slate document
|
|
1702
|
+
// on the data transfer, this is great because it means when you copy and paste
|
|
1703
|
+
// something in the editor or between editors, it'll use the actual Slate data
|
|
1704
|
+
// rather than the serialized html so component blocks and etc. will work fine
|
|
1705
|
+
// we're setting application/x-keystone-document-editor
|
|
1706
|
+
// though so that we only accept slate data from Keystone's editor
|
|
1707
|
+
// because other editors will likely have a different structure
|
|
1708
|
+
// so we'll rely on the html deserialization instead
|
|
1709
|
+
// (note that yes, we do call insertData at the end of this function
|
|
1710
|
+
// which is where Slate's logic will run, it'll never do anything there though
|
|
1711
|
+
// since anything that will have slate data will also have text/html which we handle
|
|
1712
|
+
// before we call insertData)
|
|
1713
|
+
// TODO: handle the case of copying between editors with different components blocks
|
|
1714
|
+
// (right now, things will blow up in most cases)
|
|
1715
|
+
if (data.getData('application/x-keystone-document-editor') === 'true') {
|
|
1716
|
+
insertData(data);
|
|
1717
|
+
return;
|
|
1718
|
+
}
|
|
1719
|
+
const blockAbove = Editor.above(editor, {
|
|
1720
|
+
match: node => Element.isElement(node) && Editor.isBlock(editor, node)
|
|
1721
|
+
});
|
|
1722
|
+
if ((blockAbove === null || blockAbove === void 0 ? void 0 : blockAbove[0].type) === 'code') {
|
|
1723
|
+
const plain = data.getData('text/plain');
|
|
1724
|
+
editor.insertText(plain);
|
|
1725
|
+
return;
|
|
1726
|
+
}
|
|
1727
|
+
const vsCodeEditorData = data.getData('vscode-editor-data');
|
|
1728
|
+
if (vsCodeEditorData) {
|
|
1729
|
+
try {
|
|
1730
|
+
const vsCodeData = JSON.parse(vsCodeEditorData);
|
|
1731
|
+
if ((vsCodeData === null || vsCodeData === void 0 ? void 0 : vsCodeData.mode) === 'markdown' || (vsCodeData === null || vsCodeData === void 0 ? void 0 : vsCodeData.mode) === 'mdx') {
|
|
1732
|
+
const plain = data.getData('text/plain');
|
|
1733
|
+
if (plain) {
|
|
1734
|
+
const fragment = deserializeMarkdown(plain);
|
|
1735
|
+
insertFragmentButDifferent(editor, fragment);
|
|
1736
|
+
return;
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
} catch (err) {
|
|
1740
|
+
console.log(err);
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
const plain = data.getData('text/plain');
|
|
1744
|
+
if (
|
|
1745
|
+
// isValidURL is a bit more permissive than a user might expect
|
|
1746
|
+
// so for pasting, we'll constrain it to starting with https:// or http://
|
|
1747
|
+
urlPattern.test(plain) && isValidURL(plain) && editor.selection && !Range.isCollapsed(editor.selection) &&
|
|
1748
|
+
// we only want to turn the selected text into a link if the selection is within the same block
|
|
1749
|
+
Editor.above(editor, {
|
|
1750
|
+
match: node => Element.isElement(node) && Editor.isBlock(editor, node) && !(Element.isElement(node.children[0]) && Editor.isBlock(editor, node.children[0]))
|
|
1751
|
+
}) &&
|
|
1752
|
+
// and there is only text(potentially with marks) in the selection
|
|
1753
|
+
// no other links or inline relationships
|
|
1754
|
+
Editor.nodes(editor, {
|
|
1755
|
+
match: node => Element.isElement(node) && Editor.isInline(editor, node)
|
|
1756
|
+
}).next().done) {
|
|
1757
|
+
Transforms.wrapNodes(editor, {
|
|
1758
|
+
type: 'link',
|
|
1759
|
+
href: plain,
|
|
1760
|
+
children: []
|
|
1761
|
+
}, {
|
|
1762
|
+
split: true
|
|
1763
|
+
});
|
|
1764
|
+
return;
|
|
1765
|
+
}
|
|
1766
|
+
const html = data.getData('text/html');
|
|
1767
|
+
if (html) {
|
|
1768
|
+
const fragment = deserializeHTML(html);
|
|
1769
|
+
insertFragmentButDifferent(editor, fragment);
|
|
1770
|
+
return;
|
|
1771
|
+
}
|
|
1772
|
+
if (plain) {
|
|
1773
|
+
const fragment = deserializeMarkdown(plain);
|
|
1774
|
+
insertFragmentButDifferent(editor, fragment);
|
|
1775
|
+
return;
|
|
1776
|
+
}
|
|
1777
|
+
insertData(data);
|
|
1778
|
+
};
|
|
1779
|
+
return editor;
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1782
|
+
const blockquoteChildren = ['paragraph', 'code', 'heading', 'ordered-list', 'unordered-list', 'divider'];
|
|
1783
|
+
const paragraphLike = [...blockquoteChildren, 'blockquote'];
|
|
1784
|
+
const insideOfLayouts = [...paragraphLike, 'component-block'];
|
|
1785
|
+
const editorSchema = {
|
|
1786
|
+
editor: blockContainer({
|
|
1787
|
+
allowedChildren: [...insideOfLayouts, 'layout'],
|
|
1788
|
+
invalidPositionHandleMode: 'move'
|
|
1789
|
+
}),
|
|
1790
|
+
layout: blockContainer({
|
|
1791
|
+
allowedChildren: ['layout-area'],
|
|
1792
|
+
invalidPositionHandleMode: 'move'
|
|
1793
|
+
}),
|
|
1794
|
+
'layout-area': blockContainer({
|
|
1795
|
+
allowedChildren: insideOfLayouts,
|
|
1796
|
+
invalidPositionHandleMode: 'unwrap'
|
|
1797
|
+
}),
|
|
1798
|
+
blockquote: blockContainer({
|
|
1799
|
+
allowedChildren: blockquoteChildren,
|
|
1800
|
+
invalidPositionHandleMode: 'move'
|
|
1801
|
+
}),
|
|
1802
|
+
paragraph: inlineContainer({
|
|
1803
|
+
invalidPositionHandleMode: 'unwrap'
|
|
1804
|
+
}),
|
|
1805
|
+
code: inlineContainer({
|
|
1806
|
+
invalidPositionHandleMode: 'move'
|
|
1807
|
+
}),
|
|
1808
|
+
divider: inlineContainer({
|
|
1809
|
+
invalidPositionHandleMode: 'move'
|
|
1810
|
+
}),
|
|
1811
|
+
heading: inlineContainer({
|
|
1812
|
+
invalidPositionHandleMode: 'unwrap'
|
|
1813
|
+
}),
|
|
1814
|
+
'component-block': blockContainer({
|
|
1815
|
+
allowedChildren: ['component-block-prop', 'component-inline-prop'],
|
|
1816
|
+
invalidPositionHandleMode: 'move'
|
|
1817
|
+
}),
|
|
1818
|
+
'component-inline-prop': inlineContainer({
|
|
1819
|
+
invalidPositionHandleMode: 'unwrap'
|
|
1820
|
+
}),
|
|
1821
|
+
'component-block-prop': blockContainer({
|
|
1822
|
+
allowedChildren: insideOfLayouts,
|
|
1823
|
+
invalidPositionHandleMode: 'unwrap'
|
|
1824
|
+
}),
|
|
1825
|
+
'ordered-list': blockContainer({
|
|
1826
|
+
allowedChildren: ['list-item'],
|
|
1827
|
+
invalidPositionHandleMode: 'move'
|
|
1828
|
+
}),
|
|
1829
|
+
'unordered-list': blockContainer({
|
|
1830
|
+
allowedChildren: ['list-item'],
|
|
1831
|
+
invalidPositionHandleMode: 'move'
|
|
1832
|
+
}),
|
|
1833
|
+
'list-item': blockContainer({
|
|
1834
|
+
allowedChildren: ['list-item-content', 'ordered-list', 'unordered-list'],
|
|
1835
|
+
invalidPositionHandleMode: 'unwrap'
|
|
1836
|
+
}),
|
|
1837
|
+
'list-item-content': inlineContainer({
|
|
1838
|
+
invalidPositionHandleMode: 'unwrap'
|
|
1839
|
+
})
|
|
1840
|
+
};
|
|
1841
|
+
function inlineContainer(args) {
|
|
1842
|
+
return {
|
|
1843
|
+
kind: 'inlines',
|
|
1844
|
+
invalidPositionHandleMode: args.invalidPositionHandleMode
|
|
1845
|
+
};
|
|
1846
|
+
}
|
|
1847
|
+
const inlineContainerTypes = new Set(Object.entries(editorSchema).filter(([, value]) => value.kind === 'inlines').map(([type]) => type));
|
|
1848
|
+
function isInlineContainer(node) {
|
|
1849
|
+
return node.type !== undefined && inlineContainerTypes.has(node.type);
|
|
1850
|
+
}
|
|
1851
|
+
function createDocumentEditor(documentFeatures, componentBlocks, relationships, slate) {
|
|
1852
|
+
var _slate$withReact;
|
|
1853
|
+
return withPasting(withSoftBreaks(withBlocksSchema(withLink(documentFeatures, componentBlocks, withList(withHeading(withRelationship(withInsertMenu(withComponentBlocks(componentBlocks, documentFeatures, relationships, withParagraphs(withShortcuts(withDivider(withLayouts(withMarks(documentFeatures, componentBlocks, withCodeBlock(withBlockMarkdownShortcuts(documentFeatures, componentBlocks, withBlockquote(withDocumentFeaturesNormalization(documentFeatures, relationships, withHistory((_slate$withReact = slate === null || slate === void 0 ? void 0 : slate.withReact(createEditor())) !== null && _slate$withReact !== void 0 ? _slate$withReact : createEditor())))))))))))))))))));
|
|
1854
|
+
}
|
|
1855
|
+
function blockContainer(args) {
|
|
1856
|
+
return {
|
|
1857
|
+
kind: 'blocks',
|
|
1858
|
+
allowedChildren: new Set(args.allowedChildren),
|
|
1859
|
+
blockToWrapInlinesIn: args.allowedChildren[0],
|
|
1860
|
+
invalidPositionHandleMode: args.invalidPositionHandleMode
|
|
1861
|
+
};
|
|
1862
|
+
}
|
|
1863
|
+
const blockTypes = new Set(Object.keys(editorSchema).filter(x => x !== 'editor'));
|
|
1864
|
+
function isBlock(node) {
|
|
1865
|
+
return blockTypes.has(node.type);
|
|
1866
|
+
}
|
|
1867
|
+
function withBlocksSchema(editor) {
|
|
1868
|
+
const {
|
|
1869
|
+
normalizeNode
|
|
1870
|
+
} = editor;
|
|
1871
|
+
editor.normalizeNode = ([node, path]) => {
|
|
1872
|
+
if (!Text.isText(node) && node.type !== 'link' && node.type !== 'relationship') {
|
|
1873
|
+
const nodeType = Editor.isEditor(node) ? 'editor' : node.type;
|
|
1874
|
+
if (typeof nodeType !== 'string' || editorSchema[nodeType] === undefined) {
|
|
1875
|
+
Transforms.unwrapNodes(editor, {
|
|
1876
|
+
at: path
|
|
1877
|
+
});
|
|
1878
|
+
return;
|
|
1879
|
+
}
|
|
1880
|
+
const info = editorSchema[nodeType];
|
|
1881
|
+
if (info.kind === 'blocks' && node.children.length !== 0 && node.children.every(child => !(Element.isElement(child) && Editor.isBlock(editor, child)))) {
|
|
1882
|
+
Transforms.wrapNodes(editor, {
|
|
1883
|
+
type: info.blockToWrapInlinesIn,
|
|
1884
|
+
children: []
|
|
1885
|
+
}, {
|
|
1886
|
+
at: path,
|
|
1887
|
+
match: node => !(Element.isElement(node) && Editor.isBlock(editor, node))
|
|
1888
|
+
});
|
|
1889
|
+
return;
|
|
1890
|
+
}
|
|
1891
|
+
for (const [index, childNode] of node.children.entries()) {
|
|
1892
|
+
const childPath = [...path, index];
|
|
1893
|
+
if (info.kind === 'inlines') {
|
|
1894
|
+
if (!Text.isText(childNode) && !Editor.isInline(editor, childNode) &&
|
|
1895
|
+
// these checks are implicit in Editor.isBlock
|
|
1896
|
+
// but that isn't encoded in types so these will make TS happy
|
|
1897
|
+
childNode.type !== 'link' && childNode.type !== 'relationship') {
|
|
1898
|
+
handleNodeInInvalidPosition(editor, [childNode, childPath], path);
|
|
1899
|
+
return;
|
|
1900
|
+
}
|
|
1901
|
+
} else {
|
|
1902
|
+
if (!(Element.isElement(childNode) && Editor.isBlock(editor, childNode)) ||
|
|
1903
|
+
// these checks are implicit in Editor.isBlock
|
|
1904
|
+
// but that isn't encoded in types so these will make TS happy
|
|
1905
|
+
childNode.type === 'link' || childNode.type === 'relationship') {
|
|
1906
|
+
Transforms.wrapNodes(editor, {
|
|
1907
|
+
type: info.blockToWrapInlinesIn,
|
|
1908
|
+
children: []
|
|
1909
|
+
}, {
|
|
1910
|
+
at: childPath
|
|
1911
|
+
});
|
|
1912
|
+
return;
|
|
1913
|
+
}
|
|
1914
|
+
if (Element.isElement(childNode) && Editor.isBlock(editor, childNode) && !info.allowedChildren.has(childNode.type)) {
|
|
1915
|
+
handleNodeInInvalidPosition(editor, [childNode, childPath], path);
|
|
1916
|
+
return;
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
normalizeNode([node, path]);
|
|
1922
|
+
};
|
|
1923
|
+
return editor;
|
|
1924
|
+
}
|
|
1925
|
+
function handleNodeInInvalidPosition(editor, [node, path], parentPath) {
|
|
1926
|
+
const nodeType = node.type;
|
|
1927
|
+
const childNodeInfo = editorSchema[nodeType];
|
|
1928
|
+
// the parent of a block will never be an inline so this casting is okay
|
|
1929
|
+
const parentNode = Node.get(editor, parentPath);
|
|
1930
|
+
const parentNodeType = Editor.isEditor(parentNode) ? 'editor' : parentNode.type;
|
|
1931
|
+
const parentNodeInfo = editorSchema[parentNodeType];
|
|
1932
|
+
if (!childNodeInfo || childNodeInfo.invalidPositionHandleMode === 'unwrap') {
|
|
1933
|
+
if (parentNodeInfo.kind === 'blocks' && parentNodeInfo.blockToWrapInlinesIn) {
|
|
1934
|
+
Transforms.setNodes(editor, {
|
|
1935
|
+
type: parentNodeInfo.blockToWrapInlinesIn,
|
|
1936
|
+
...Object.fromEntries(Object.keys(node).filter(key => key !== 'type' && key !== 'children').map(key => [key, null])) // the Slate types don't understand that null is allowed and it will unset properties with setNodes
|
|
1937
|
+
}, {
|
|
1938
|
+
at: path
|
|
1939
|
+
});
|
|
1940
|
+
return;
|
|
1941
|
+
}
|
|
1942
|
+
Transforms.unwrapNodes(editor, {
|
|
1943
|
+
at: path
|
|
1944
|
+
});
|
|
1945
|
+
return;
|
|
1946
|
+
}
|
|
1947
|
+
const info = editorSchema[parentNode.type || 'editor'];
|
|
1948
|
+
if ((info === null || info === void 0 ? void 0 : info.kind) === 'blocks' && info.allowedChildren.has(nodeType)) {
|
|
1949
|
+
if (parentPath.length === 0) {
|
|
1950
|
+
Transforms.moveNodes(editor, {
|
|
1951
|
+
at: path,
|
|
1952
|
+
to: [path[0] + 1]
|
|
1953
|
+
});
|
|
1954
|
+
} else {
|
|
1955
|
+
Transforms.moveNodes(editor, {
|
|
1956
|
+
at: path,
|
|
1957
|
+
to: Path.next(parentPath)
|
|
1958
|
+
});
|
|
1959
|
+
}
|
|
1960
|
+
return;
|
|
1961
|
+
}
|
|
1962
|
+
if (Editor.isEditor(parentNode)) {
|
|
1963
|
+
Transforms.moveNodes(editor, {
|
|
1964
|
+
at: path,
|
|
1965
|
+
to: [path[0] + 1]
|
|
1966
|
+
});
|
|
1967
|
+
Transforms.unwrapNodes(editor, {
|
|
1968
|
+
at: [path[0] + 1]
|
|
1969
|
+
});
|
|
1970
|
+
return;
|
|
1971
|
+
}
|
|
1972
|
+
handleNodeInInvalidPosition(editor, [node, path], parentPath.slice(0, -1));
|
|
1973
|
+
}
|
|
1974
|
+
|
|
1975
|
+
// to print the editor schema in Graphviz if you want to visualize it
|
|
1976
|
+
// function printEditorSchema(editorSchema: EditorSchema) {
|
|
1977
|
+
// return `digraph G {
|
|
1978
|
+
// concentrate=true;
|
|
1979
|
+
// ${Object.keys(editorSchema)
|
|
1980
|
+
// .map(key => {
|
|
1981
|
+
// let val = editorSchema[key];
|
|
1982
|
+
// if (val.kind === 'inlines') {
|
|
1983
|
+
// return `"${key}" -> inlines`;
|
|
1984
|
+
// }
|
|
1985
|
+
// if (val.kind === 'blocks') {
|
|
1986
|
+
// return `"${key}" -> {${[...val.allowedChildren].map(x => JSON.stringify(x)).join(' ')}}`;
|
|
1987
|
+
// }
|
|
1988
|
+
// })
|
|
1989
|
+
// .join('\n ')}
|
|
1990
|
+
// }`;
|
|
1991
|
+
// }
|
|
1992
|
+
|
|
1993
|
+
export { insertDivider as a, createDocumentEditor as c, insertBlockquote as i, wrapLink as w };
|