@squiz/formatted-text-editor 1.21.1-alpha.7 → 1.22.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/demo/App.tsx +52 -10
- package/demo/index.scss +11 -10
- package/jest.config.ts +0 -2
- package/lib/Editor/Editor.js +45 -7
- package/lib/Editor/EditorContext.d.ts +15 -0
- package/lib/Editor/EditorContext.js +15 -0
- package/lib/EditorToolbar/FloatingToolbar.js +11 -5
- package/lib/EditorToolbar/Tools/Image/Form/ImageForm.d.ts +9 -8
- package/lib/EditorToolbar/Tools/Image/Form/ImageForm.js +91 -23
- package/lib/EditorToolbar/Tools/Image/ImageButton.d.ts +4 -1
- package/lib/EditorToolbar/Tools/Image/ImageButton.js +22 -14
- package/lib/EditorToolbar/Tools/Image/ImageModal.js +9 -5
- package/lib/EditorToolbar/Tools/Link/Form/LinkForm.d.ts +14 -5
- package/lib/EditorToolbar/Tools/Link/Form/LinkForm.js +66 -14
- package/lib/EditorToolbar/Tools/Link/LinkButton.js +21 -13
- package/lib/EditorToolbar/Tools/Link/LinkModal.js +12 -5
- package/lib/EditorToolbar/Tools/Link/RemoveLinkButton.js +1 -8
- package/lib/Extensions/CommandsExtension/CommandsExtension.d.ts +20 -0
- package/lib/Extensions/CommandsExtension/CommandsExtension.js +52 -0
- package/lib/Extensions/Extensions.d.ts +12 -5
- package/lib/Extensions/Extensions.js +42 -20
- package/lib/Extensions/ImageExtension/AssetImageExtension.d.ts +17 -0
- package/lib/Extensions/ImageExtension/AssetImageExtension.js +92 -0
- package/lib/Extensions/ImageExtension/ImageExtension.d.ts +4 -0
- package/lib/Extensions/ImageExtension/ImageExtension.js +11 -0
- package/lib/Extensions/LinkExtension/AssetLinkExtension.d.ts +26 -0
- package/lib/Extensions/LinkExtension/AssetLinkExtension.js +102 -0
- package/lib/Extensions/LinkExtension/LinkExtension.d.ts +19 -12
- package/lib/Extensions/LinkExtension/LinkExtension.js +56 -66
- package/lib/Extensions/LinkExtension/common.d.ts +7 -0
- package/lib/Extensions/LinkExtension/common.js +14 -0
- package/lib/Extensions/PreformattedExtension/PreformattedExtension.d.ts +1 -1
- package/lib/Extensions/PreformattedExtension/PreformattedExtension.js +6 -2
- package/lib/hooks/index.d.ts +1 -0
- package/lib/hooks/index.js +1 -0
- package/lib/hooks/useExpandedSelection.d.ts +23 -0
- package/lib/hooks/useExpandedSelection.js +37 -0
- package/lib/index.css +58 -23
- package/lib/index.d.ts +5 -2
- package/lib/index.js +9 -3
- package/lib/types.d.ts +3 -0
- package/lib/types.js +2 -0
- package/lib/ui/Button/Button.d.ts +2 -1
- package/lib/ui/Button/Button.js +4 -5
- package/lib/ui/Fields/Input/Input.d.ts +1 -0
- package/lib/ui/Fields/Input/Input.js +9 -3
- package/lib/ui/Modal/Modal.js +5 -3
- package/lib/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.d.ts +9 -0
- package/lib/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.js +174 -0
- package/lib/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.d.ts +9 -0
- package/lib/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.js +138 -0
- package/lib/utils/resolveMatrixAssetUrl.d.ts +1 -0
- package/lib/utils/resolveMatrixAssetUrl.js +10 -0
- package/lib/utils/undefinedIfEmpty.d.ts +1 -0
- package/lib/utils/undefinedIfEmpty.js +7 -0
- package/package.json +10 -4
- package/src/Editor/Editor.spec.tsx +78 -18
- package/src/Editor/Editor.tsx +28 -9
- package/src/Editor/EditorContext.spec.tsx +26 -0
- package/src/Editor/EditorContext.ts +26 -0
- package/src/Editor/_editor.scss +20 -4
- package/src/EditorToolbar/FloatingToolbar.spec.tsx +26 -7
- package/src/EditorToolbar/FloatingToolbar.tsx +15 -6
- package/src/EditorToolbar/Tools/Image/Form/ImageForm.spec.tsx +81 -6
- package/src/EditorToolbar/Tools/Image/Form/ImageForm.tsx +167 -47
- package/src/EditorToolbar/Tools/Image/ImageButton.spec.tsx +250 -2
- package/src/EditorToolbar/Tools/Image/ImageButton.tsx +29 -16
- package/src/EditorToolbar/Tools/Image/ImageModal.spec.tsx +59 -20
- package/src/EditorToolbar/Tools/Image/ImageModal.tsx +12 -10
- package/src/EditorToolbar/Tools/Link/Form/LinkForm.spec.tsx +37 -9
- package/src/EditorToolbar/Tools/Link/Form/LinkForm.tsx +96 -26
- package/src/EditorToolbar/Tools/Link/LinkButton.spec.tsx +137 -26
- package/src/EditorToolbar/Tools/Link/LinkButton.tsx +28 -19
- package/src/EditorToolbar/Tools/Link/LinkModal.tsx +13 -6
- package/src/EditorToolbar/Tools/Link/RemoveLinkButton.spec.tsx +27 -26
- package/src/EditorToolbar/Tools/Link/RemoveLinkButton.tsx +2 -10
- package/src/EditorToolbar/Tools/Undo/UndoButton.spec.tsx +22 -1
- package/src/EditorToolbar/_floating-toolbar.scss +4 -5
- package/src/EditorToolbar/_toolbar.scss +1 -1
- package/src/Extensions/CommandsExtension/CommandsExtension.ts +54 -0
- package/src/Extensions/Extensions.ts +42 -18
- package/src/Extensions/ImageExtension/AssetImageExtension.spec.ts +76 -0
- package/src/Extensions/ImageExtension/AssetImageExtension.ts +111 -0
- package/src/Extensions/ImageExtension/ImageExtension.ts +17 -1
- package/src/Extensions/LinkExtension/AssetLinkExtension.spec.ts +104 -0
- package/src/Extensions/LinkExtension/AssetLinkExtension.ts +128 -0
- package/src/Extensions/LinkExtension/LinkExtension.spec.ts +68 -0
- package/src/Extensions/LinkExtension/LinkExtension.ts +71 -85
- package/src/Extensions/LinkExtension/common.ts +10 -0
- package/src/Extensions/PreformattedExtension/PreformattedExtension.spec.ts +41 -0
- package/src/Extensions/PreformattedExtension/PreformattedExtension.ts +6 -2
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useExpandedSelection.ts +44 -0
- package/src/index.ts +5 -2
- package/src/types.ts +5 -0
- package/src/ui/Button/Button.tsx +10 -6
- package/src/ui/Button/_button.scss +1 -1
- package/src/ui/Fields/Input/Input.spec.tsx +7 -1
- package/src/ui/Fields/Input/Input.tsx +23 -4
- package/src/ui/Modal/Modal.spec.tsx +15 -0
- package/src/ui/Modal/Modal.tsx +8 -4
- package/src/ui/ToolbarDropdown/_toolbar-dropdown.scss +1 -1
- package/src/ui/_forms.scss +14 -0
- package/src/utils/converters/mocks/squizNodeJson.mock.ts +271 -0
- package/src/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.spec.ts +480 -0
- package/src/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.ts +212 -0
- package/src/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.spec.ts +341 -0
- package/src/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.ts +159 -0
- package/src/utils/resolveMatrixAssetUrl.spec.ts +26 -0
- package/src/utils/resolveMatrixAssetUrl.ts +7 -0
- package/src/utils/undefinedIfEmpty.spec.ts +12 -0
- package/src/utils/undefinedIfEmpty.ts +3 -0
- package/tailwind.config.cjs +3 -0
- package/tests/renderWithEditor.tsx +28 -15
- package/tsconfig.json +1 -1
- package/lib/FormattedTextEditor.d.ts +0 -2
- package/lib/FormattedTextEditor.js +0 -7
- package/src/Editor/Editor.mock.tsx +0 -43
- package/src/FormattedTextEditor.spec.tsx +0 -10
- package/src/FormattedTextEditor.tsx +0 -3
- /package/tests/{select.tsx → select.ts} +0 -0
@@ -0,0 +1,480 @@
|
|
1
|
+
import { FORMATTED_TEXT_MODELS as FormattedTextModels } from '@squiz/dx-json-schema-lib';
|
2
|
+
import { remirrorNodeToSquizNode, resolveNodeTag } from './remirrorNodeToSquizNode';
|
3
|
+
import { renderWithEditor } from '../../../../tests';
|
4
|
+
import { RemirrorJSON } from '@remirror/core';
|
5
|
+
import { sharedNodeExamples } from '../mocks/squizNodeJson.mock';
|
6
|
+
import { ParagraphExtension, SupExtension } from 'remirror/extensions';
|
7
|
+
|
8
|
+
type FormattedText = FormattedTextModels.v1.FormattedText;
|
9
|
+
|
10
|
+
describe('remirrorNodeToSquizNode', () => {
|
11
|
+
it('should convert a simple Remirror node to a Squiz component JSON object', async () => {
|
12
|
+
const content: RemirrorJSON = {
|
13
|
+
type: 'doc',
|
14
|
+
content: [
|
15
|
+
{
|
16
|
+
type: 'paragraph',
|
17
|
+
content: [
|
18
|
+
{
|
19
|
+
type: 'text',
|
20
|
+
text: 'Hello world!',
|
21
|
+
},
|
22
|
+
],
|
23
|
+
},
|
24
|
+
],
|
25
|
+
};
|
26
|
+
|
27
|
+
const { editor } = await renderWithEditor(null, { content });
|
28
|
+
|
29
|
+
const expected: FormattedText = [
|
30
|
+
{
|
31
|
+
type: 'tag',
|
32
|
+
tag: 'p',
|
33
|
+
children: [{ type: 'text', value: 'Hello world!' }],
|
34
|
+
},
|
35
|
+
];
|
36
|
+
|
37
|
+
const result = remirrorNodeToSquizNode(editor.doc);
|
38
|
+
expect(result).toEqual(expected);
|
39
|
+
});
|
40
|
+
|
41
|
+
it('should handle preformatted formatting', async () => {
|
42
|
+
const content: RemirrorJSON = {
|
43
|
+
type: 'doc',
|
44
|
+
content: [
|
45
|
+
{
|
46
|
+
type: 'preformatted',
|
47
|
+
attrs: {
|
48
|
+
nodeIndent: null,
|
49
|
+
nodeTextAlignment: null,
|
50
|
+
nodeLineHeight: null,
|
51
|
+
style: '',
|
52
|
+
},
|
53
|
+
content: [
|
54
|
+
{
|
55
|
+
type: 'text',
|
56
|
+
text: 'Hello world',
|
57
|
+
},
|
58
|
+
],
|
59
|
+
},
|
60
|
+
],
|
61
|
+
};
|
62
|
+
|
63
|
+
const { editor } = await renderWithEditor(null, { content });
|
64
|
+
|
65
|
+
const expected: FormattedText = [
|
66
|
+
{
|
67
|
+
type: 'tag',
|
68
|
+
tag: 'pre',
|
69
|
+
children: [{ type: 'text', value: 'Hello world' }],
|
70
|
+
},
|
71
|
+
];
|
72
|
+
|
73
|
+
const result = remirrorNodeToSquizNode(editor.doc);
|
74
|
+
expect(result).toEqual(expected);
|
75
|
+
});
|
76
|
+
|
77
|
+
it('should handle images', async () => {
|
78
|
+
const content: RemirrorJSON = {
|
79
|
+
type: 'doc',
|
80
|
+
content: [
|
81
|
+
{
|
82
|
+
type: 'paragraph',
|
83
|
+
attrs: {
|
84
|
+
nodeIndent: null,
|
85
|
+
nodeTextAlignment: null,
|
86
|
+
nodeLineHeight: null,
|
87
|
+
style: '',
|
88
|
+
},
|
89
|
+
content: [
|
90
|
+
{
|
91
|
+
type: 'image',
|
92
|
+
attrs: {
|
93
|
+
alt: '',
|
94
|
+
crop: null,
|
95
|
+
height: 150,
|
96
|
+
width: 200,
|
97
|
+
rotate: null,
|
98
|
+
src: 'https://media2.giphy.com/media/3o6ozsIxg5legYvggo/giphy.gif',
|
99
|
+
title: '',
|
100
|
+
fileName: null,
|
101
|
+
resizable: false,
|
102
|
+
},
|
103
|
+
},
|
104
|
+
],
|
105
|
+
},
|
106
|
+
],
|
107
|
+
};
|
108
|
+
|
109
|
+
const { editor } = await renderWithEditor(null, { content });
|
110
|
+
|
111
|
+
const expected: FormattedText = [
|
112
|
+
{
|
113
|
+
type: 'tag',
|
114
|
+
tag: 'p',
|
115
|
+
children: [
|
116
|
+
{
|
117
|
+
children: [],
|
118
|
+
attributes: {
|
119
|
+
alt: '',
|
120
|
+
height: '150',
|
121
|
+
width: '200',
|
122
|
+
src: 'https://media2.giphy.com/media/3o6ozsIxg5legYvggo/giphy.gif',
|
123
|
+
title: '',
|
124
|
+
},
|
125
|
+
type: 'tag',
|
126
|
+
tag: 'img',
|
127
|
+
},
|
128
|
+
],
|
129
|
+
},
|
130
|
+
];
|
131
|
+
|
132
|
+
const result = remirrorNodeToSquizNode(editor.doc);
|
133
|
+
expect(result).toEqual(expected);
|
134
|
+
});
|
135
|
+
|
136
|
+
it.each([1, 2, 3, 4, 5, 6])('should handle heading %s formatting', async (level) => {
|
137
|
+
const content: RemirrorJSON = {
|
138
|
+
type: 'doc',
|
139
|
+
content: [
|
140
|
+
{
|
141
|
+
type: 'heading',
|
142
|
+
attrs: {
|
143
|
+
nodeIndent: null,
|
144
|
+
nodeTextAlignment: null,
|
145
|
+
nodeLineHeight: null,
|
146
|
+
style: '',
|
147
|
+
level: level,
|
148
|
+
},
|
149
|
+
content: [
|
150
|
+
{
|
151
|
+
type: 'text',
|
152
|
+
text: 'Hello world',
|
153
|
+
},
|
154
|
+
],
|
155
|
+
},
|
156
|
+
],
|
157
|
+
};
|
158
|
+
|
159
|
+
const { editor } = await renderWithEditor(null, { content });
|
160
|
+
|
161
|
+
const expected: FormattedText = [
|
162
|
+
{
|
163
|
+
type: 'tag',
|
164
|
+
tag: `h${level}`,
|
165
|
+
children: [{ type: 'text', value: 'Hello world' }],
|
166
|
+
},
|
167
|
+
];
|
168
|
+
|
169
|
+
const result = remirrorNodeToSquizNode(editor.doc);
|
170
|
+
expect(result).toEqual(expected);
|
171
|
+
});
|
172
|
+
|
173
|
+
it('should preserve font and attribute options', async () => {
|
174
|
+
const content: RemirrorJSON = {
|
175
|
+
type: 'doc',
|
176
|
+
content: [
|
177
|
+
{
|
178
|
+
type: 'paragraph',
|
179
|
+
content: [
|
180
|
+
{
|
181
|
+
type: 'text',
|
182
|
+
text: 'You can',
|
183
|
+
},
|
184
|
+
{
|
185
|
+
type: 'text',
|
186
|
+
marks: [
|
187
|
+
{
|
188
|
+
type: 'bold',
|
189
|
+
},
|
190
|
+
],
|
191
|
+
text: ' click',
|
192
|
+
},
|
193
|
+
{
|
194
|
+
type: 'text',
|
195
|
+
text: ' ',
|
196
|
+
},
|
197
|
+
{
|
198
|
+
type: 'text',
|
199
|
+
marks: [
|
200
|
+
{
|
201
|
+
type: 'link',
|
202
|
+
attrs: {
|
203
|
+
href: 'https://www.google.com/',
|
204
|
+
target: null,
|
205
|
+
auto: false,
|
206
|
+
title: '',
|
207
|
+
},
|
208
|
+
},
|
209
|
+
{
|
210
|
+
type: 'italic',
|
211
|
+
},
|
212
|
+
],
|
213
|
+
text: 'here',
|
214
|
+
},
|
215
|
+
{
|
216
|
+
type: 'text',
|
217
|
+
marks: [
|
218
|
+
{
|
219
|
+
type: 'italic',
|
220
|
+
},
|
221
|
+
],
|
222
|
+
text: ' ',
|
223
|
+
},
|
224
|
+
{
|
225
|
+
type: 'text',
|
226
|
+
text: 'to go to',
|
227
|
+
},
|
228
|
+
{
|
229
|
+
type: 'text',
|
230
|
+
marks: [
|
231
|
+
{
|
232
|
+
type: 'bold',
|
233
|
+
},
|
234
|
+
{
|
235
|
+
type: 'italic',
|
236
|
+
},
|
237
|
+
{
|
238
|
+
type: 'underline',
|
239
|
+
},
|
240
|
+
],
|
241
|
+
text: ' GOOGLE!',
|
242
|
+
},
|
243
|
+
],
|
244
|
+
},
|
245
|
+
],
|
246
|
+
};
|
247
|
+
|
248
|
+
const { editor } = await renderWithEditor(null, { content });
|
249
|
+
|
250
|
+
const expected: FormattedText = [
|
251
|
+
{
|
252
|
+
children: [
|
253
|
+
{
|
254
|
+
type: 'text',
|
255
|
+
value: 'You can',
|
256
|
+
},
|
257
|
+
{
|
258
|
+
children: [
|
259
|
+
{
|
260
|
+
type: 'text',
|
261
|
+
value: ' click',
|
262
|
+
},
|
263
|
+
],
|
264
|
+
font: {
|
265
|
+
bold: true,
|
266
|
+
},
|
267
|
+
tag: 'span',
|
268
|
+
type: 'tag',
|
269
|
+
},
|
270
|
+
{
|
271
|
+
type: 'text',
|
272
|
+
value: ' ',
|
273
|
+
},
|
274
|
+
{
|
275
|
+
children: [
|
276
|
+
{
|
277
|
+
type: 'text',
|
278
|
+
value: 'here',
|
279
|
+
},
|
280
|
+
],
|
281
|
+
attributes: {
|
282
|
+
href: 'https://www.google.com/',
|
283
|
+
title: '',
|
284
|
+
},
|
285
|
+
font: {
|
286
|
+
italics: true,
|
287
|
+
},
|
288
|
+
tag: 'a',
|
289
|
+
type: 'tag',
|
290
|
+
},
|
291
|
+
{
|
292
|
+
children: [
|
293
|
+
{
|
294
|
+
type: 'text',
|
295
|
+
value: ' ',
|
296
|
+
},
|
297
|
+
],
|
298
|
+
font: {
|
299
|
+
italics: true,
|
300
|
+
},
|
301
|
+
tag: 'span',
|
302
|
+
type: 'tag',
|
303
|
+
},
|
304
|
+
{
|
305
|
+
type: 'text',
|
306
|
+
value: 'to go to',
|
307
|
+
},
|
308
|
+
{
|
309
|
+
children: [
|
310
|
+
{
|
311
|
+
type: 'text',
|
312
|
+
value: ' GOOGLE!',
|
313
|
+
},
|
314
|
+
],
|
315
|
+
font: {
|
316
|
+
bold: true,
|
317
|
+
italics: true,
|
318
|
+
underline: true,
|
319
|
+
},
|
320
|
+
tag: 'span',
|
321
|
+
type: 'tag',
|
322
|
+
},
|
323
|
+
],
|
324
|
+
type: 'tag',
|
325
|
+
tag: 'p',
|
326
|
+
},
|
327
|
+
];
|
328
|
+
|
329
|
+
const result = remirrorNodeToSquizNode(editor.doc);
|
330
|
+
expect(result).toEqual(expected);
|
331
|
+
});
|
332
|
+
|
333
|
+
describe('preserve text formatting', () => {
|
334
|
+
it('should preserve text alignment', async () => {
|
335
|
+
const content: RemirrorJSON = {
|
336
|
+
type: 'doc',
|
337
|
+
content: [
|
338
|
+
{
|
339
|
+
type: 'paragraph',
|
340
|
+
attrs: {
|
341
|
+
nodeIndent: null,
|
342
|
+
nodeTextAlignment: 'center',
|
343
|
+
nodeLineHeight: null,
|
344
|
+
style: '',
|
345
|
+
},
|
346
|
+
content: [
|
347
|
+
{
|
348
|
+
type: 'text',
|
349
|
+
text: 'Hello world!',
|
350
|
+
},
|
351
|
+
],
|
352
|
+
},
|
353
|
+
],
|
354
|
+
};
|
355
|
+
|
356
|
+
const { editor } = await renderWithEditor(null, { content });
|
357
|
+
|
358
|
+
const expected: FormattedText = [
|
359
|
+
{
|
360
|
+
children: [
|
361
|
+
{
|
362
|
+
type: 'text',
|
363
|
+
value: 'Hello world!',
|
364
|
+
},
|
365
|
+
],
|
366
|
+
formattingOptions: {
|
367
|
+
alignment: 'center',
|
368
|
+
},
|
369
|
+
type: 'tag',
|
370
|
+
tag: 'p',
|
371
|
+
},
|
372
|
+
];
|
373
|
+
|
374
|
+
const result = remirrorNodeToSquizNode(editor.doc);
|
375
|
+
expect(result).toEqual(expected);
|
376
|
+
});
|
377
|
+
});
|
378
|
+
|
379
|
+
it('should handle invalid Remirror node provided', () => {
|
380
|
+
expect(() => remirrorNodeToSquizNode(false as any)).toThrow(
|
381
|
+
'Unable to convert from Remirror to Node data structure, unexpected node provided.',
|
382
|
+
);
|
383
|
+
});
|
384
|
+
|
385
|
+
it('should handle no content provided by Remirror', async () => {
|
386
|
+
const content: RemirrorJSON = {
|
387
|
+
type: 'doc',
|
388
|
+
};
|
389
|
+
|
390
|
+
const { editor } = await renderWithEditor(null, { content });
|
391
|
+
|
392
|
+
const result = remirrorNodeToSquizNode(editor.doc);
|
393
|
+
expect(result).toEqual([]);
|
394
|
+
});
|
395
|
+
|
396
|
+
it.each(sharedNodeExamples)(
|
397
|
+
'should convert a Remirror node to the expected Squiz representation - $description',
|
398
|
+
async ({ remirrorNode, squizNode }: any) => {
|
399
|
+
const { editor } = await renderWithEditor(null, {
|
400
|
+
content: {
|
401
|
+
type: 'doc',
|
402
|
+
content: [{ type: 'paragraph', content: [remirrorNode] }],
|
403
|
+
},
|
404
|
+
});
|
405
|
+
|
406
|
+
expect(remirrorNodeToSquizNode(editor.doc)).toEqual([
|
407
|
+
{
|
408
|
+
type: 'tag',
|
409
|
+
tag: 'p',
|
410
|
+
children: squizNode,
|
411
|
+
},
|
412
|
+
]);
|
413
|
+
},
|
414
|
+
);
|
415
|
+
|
416
|
+
it('should throw if the remirror node has an unsupported mark applied', async () => {
|
417
|
+
const { editor } = await renderWithEditor(null, {
|
418
|
+
extensions: [new ParagraphExtension(), new SupExtension()],
|
419
|
+
content: {
|
420
|
+
type: 'doc',
|
421
|
+
content: [{ type: 'paragraph', marks: [{ type: 'sup' }], content: [] }],
|
422
|
+
},
|
423
|
+
});
|
424
|
+
|
425
|
+
expect(() => remirrorNodeToSquizNode(editor.doc)).toThrow('Unsupported mark "sup" was applied to node.');
|
426
|
+
});
|
427
|
+
});
|
428
|
+
|
429
|
+
describe('resolveNodeTag', () => {
|
430
|
+
it('should return the correct tag name for a node with a toDOM method that returns a Node', () => {
|
431
|
+
const node: any = {
|
432
|
+
type: {
|
433
|
+
spec: {
|
434
|
+
toDOM: () => document.createElement('div'),
|
435
|
+
},
|
436
|
+
},
|
437
|
+
};
|
438
|
+
expect(resolveNodeTag(node)).toBe('div');
|
439
|
+
});
|
440
|
+
|
441
|
+
it('should return the correct tag name for a node with a toDOM method that returns an object with a dom property', () => {
|
442
|
+
const node: any = {
|
443
|
+
type: {
|
444
|
+
spec: {
|
445
|
+
toDOM: () => ({ dom: document.createElement('span') }),
|
446
|
+
},
|
447
|
+
},
|
448
|
+
};
|
449
|
+
expect(resolveNodeTag(node)).toBe('span');
|
450
|
+
});
|
451
|
+
|
452
|
+
it('should return the correct tag name for a node with a toDOM method that returns an array', () => {
|
453
|
+
const node: any = {
|
454
|
+
type: {
|
455
|
+
spec: {
|
456
|
+
toDOM: () => ['ul', { class: 'my-list' }],
|
457
|
+
},
|
458
|
+
},
|
459
|
+
};
|
460
|
+
expect(resolveNodeTag(node)).toBe('ul');
|
461
|
+
});
|
462
|
+
|
463
|
+
it('should throw for a node with a toDOM method that returns undefined', () => {
|
464
|
+
const node: any = {
|
465
|
+
type: {
|
466
|
+
spec: {
|
467
|
+
toDOM: () => undefined,
|
468
|
+
},
|
469
|
+
},
|
470
|
+
};
|
471
|
+
expect(() => resolveNodeTag(node)).toThrow('Unexpected Remirror node encountered, cannot resolve tag.');
|
472
|
+
});
|
473
|
+
|
474
|
+
it('should throw for a node without a toDOM method', () => {
|
475
|
+
const node: any = {
|
476
|
+
type: {},
|
477
|
+
};
|
478
|
+
expect(() => resolveNodeTag(node)).toThrow('Unexpected Remirror node encountered, cannot resolve tag.');
|
479
|
+
});
|
480
|
+
});
|
@@ -0,0 +1,212 @@
|
|
1
|
+
import { ProsemirrorNode, Fragment as ProsemirrorFragment, Mark } from 'remirror';
|
2
|
+
import { Attrs } from 'prosemirror-model';
|
3
|
+
import { FORMATTED_TEXT_MODELS as FormattedTextModels } from '@squiz/dx-json-schema-lib';
|
4
|
+
import { undefinedIfEmpty } from '../../undefinedIfEmpty';
|
5
|
+
import { NodeName } from '../../../Extensions/Extensions';
|
6
|
+
|
7
|
+
type Fragment = ProsemirrorFragment & {
|
8
|
+
content?: Fragment[];
|
9
|
+
};
|
10
|
+
|
11
|
+
type FormattingOptions = FormattedTextModels.v1.FormattingOptions;
|
12
|
+
type FontOptions = FormattedTextModels.v1.FormattedNodeFontProperties;
|
13
|
+
type FormattedText = FormattedTextModels.v1.FormattedText;
|
14
|
+
type FormattedNode = FormattedTextModels.v1.FormattedNodes;
|
15
|
+
type FormattedNodeFontProperties = FormattedTextModels.v1.FormattedNodeFontProperties;
|
16
|
+
type FormattedNodeWithChildren = Extract<FormattedNode, { children: FormattedNode[] }>;
|
17
|
+
|
18
|
+
export const resolveNodeTag = (node: ProsemirrorNode): string => {
|
19
|
+
if (node.type.name === 'text') {
|
20
|
+
return 'span';
|
21
|
+
}
|
22
|
+
|
23
|
+
if (node.type.spec?.toDOM) {
|
24
|
+
const domNode = node.type.spec.toDOM(node);
|
25
|
+
|
26
|
+
if (domNode instanceof window.Node) {
|
27
|
+
return domNode.nodeName.toLowerCase();
|
28
|
+
}
|
29
|
+
|
30
|
+
if (typeof domNode === 'object' && 'dom' in domNode && domNode.dom instanceof window.Node) {
|
31
|
+
return domNode.dom.nodeName.toLowerCase();
|
32
|
+
}
|
33
|
+
|
34
|
+
if (domNode instanceof Array) {
|
35
|
+
// [ tag, attributes, ...children ]
|
36
|
+
return domNode[0].toLowerCase();
|
37
|
+
}
|
38
|
+
}
|
39
|
+
|
40
|
+
throw new Error('Unexpected Remirror node encountered, cannot resolve tag.');
|
41
|
+
};
|
42
|
+
|
43
|
+
const resolveFormattingOptions = (node: ProsemirrorNode): FormattingOptions => {
|
44
|
+
const formattingOptions: FormattingOptions = {};
|
45
|
+
|
46
|
+
if (node.attrs.nodeTextAlignment) {
|
47
|
+
formattingOptions.alignment = node.attrs.nodeTextAlignment;
|
48
|
+
}
|
49
|
+
|
50
|
+
return formattingOptions;
|
51
|
+
};
|
52
|
+
|
53
|
+
const resolveFontOptions = (node: ProsemirrorNode): FormattedNodeFontProperties => {
|
54
|
+
const fontOptions: FontOptions = {};
|
55
|
+
|
56
|
+
node.marks.forEach((mark) => {
|
57
|
+
switch (mark.type.name) {
|
58
|
+
case 'bold':
|
59
|
+
fontOptions.bold = true;
|
60
|
+
break;
|
61
|
+
case 'italic':
|
62
|
+
fontOptions.italics = true;
|
63
|
+
break;
|
64
|
+
case 'underline':
|
65
|
+
fontOptions.underline = true;
|
66
|
+
break;
|
67
|
+
}
|
68
|
+
});
|
69
|
+
|
70
|
+
return fontOptions;
|
71
|
+
};
|
72
|
+
|
73
|
+
const transformAttributes = (attributes: Attrs): Record<string, string> => {
|
74
|
+
const transformed: Record<string, string> = {};
|
75
|
+
|
76
|
+
Object.keys(attributes).forEach((key) => {
|
77
|
+
// Component service requires attributes to be a string, cast as needed.
|
78
|
+
if (typeof attributes[key] === 'string' || typeof attributes[key] === 'number') {
|
79
|
+
transformed[key] = String(attributes[key]);
|
80
|
+
}
|
81
|
+
});
|
82
|
+
|
83
|
+
return transformed;
|
84
|
+
};
|
85
|
+
|
86
|
+
const transformFragment = (fragment: Fragment): FormattedText => {
|
87
|
+
const transformed: FormattedText = [];
|
88
|
+
|
89
|
+
fragment.forEach((child) => transformed.push(transformNode(child)));
|
90
|
+
|
91
|
+
return transformed;
|
92
|
+
};
|
93
|
+
|
94
|
+
const transformNode = (node: ProsemirrorNode): FormattedNode => {
|
95
|
+
const attributes = node.type.name === NodeName.Image ? transformAttributes(node.attrs) : undefined;
|
96
|
+
const formattingOptions = undefinedIfEmpty(resolveFormattingOptions(node));
|
97
|
+
const font = undefinedIfEmpty(resolveFontOptions(node));
|
98
|
+
let transformedNode: FormattedNode = { type: 'text', value: node.text || '' };
|
99
|
+
|
100
|
+
// Squiz "text" nodes can't have formatting/font options but Remirror "text" nodes can.
|
101
|
+
// If the node has formatting options wrap in a tag.
|
102
|
+
// If the node isn't a text type assume it is a tag type and wrap in a tag.
|
103
|
+
// If we pick the wrong tag here it will be corrected later as part of looping through the
|
104
|
+
// non-font marks.
|
105
|
+
if (node.type.name !== NodeName.Text || attributes || formattingOptions || font) {
|
106
|
+
transformedNode = {
|
107
|
+
type: 'tag',
|
108
|
+
tag: resolveNodeTag(node),
|
109
|
+
children: node.type.name === NodeName.Text ? [transformedNode] : transformFragment(node.content),
|
110
|
+
attributes,
|
111
|
+
formattingOptions,
|
112
|
+
font,
|
113
|
+
};
|
114
|
+
}
|
115
|
+
|
116
|
+
if (node.type.name === NodeName.AssetImage) {
|
117
|
+
transformedNode = {
|
118
|
+
type: 'matrix-image',
|
119
|
+
matrixAssetId: node.attrs.matrixAssetId,
|
120
|
+
matrixIdentifier: node.attrs.matrixIdentifier,
|
121
|
+
matrixDomain: node.attrs.matrixDomain,
|
122
|
+
};
|
123
|
+
}
|
124
|
+
|
125
|
+
node.marks.forEach((mark) => {
|
126
|
+
switch (mark.type.name) {
|
127
|
+
case 'bold':
|
128
|
+
case 'italic':
|
129
|
+
case 'underline':
|
130
|
+
break;
|
131
|
+
default:
|
132
|
+
transformedNode = transformMark(mark, transformedNode);
|
133
|
+
}
|
134
|
+
});
|
135
|
+
|
136
|
+
return transformedNode;
|
137
|
+
};
|
138
|
+
|
139
|
+
/**
|
140
|
+
* Merges 2 nodes together if they are compatible without losing any important details.
|
141
|
+
* Otherwise will wrap the node.
|
142
|
+
*
|
143
|
+
* @param {FormattedNode} node
|
144
|
+
* @param {FormattedNodeWithChildren} wrappingNode
|
145
|
+
*
|
146
|
+
* @return {FormattedNode}
|
147
|
+
*/
|
148
|
+
const wrapNodeIfNeeded = (node: FormattedNode, wrappingNode: FormattedNodeWithChildren): FormattedNode => {
|
149
|
+
if (node.type === 'tag' && wrappingNode.type === 'tag' && (node.tag === 'span' || node.tag === wrappingNode.tag)) {
|
150
|
+
// if the node we are wrapping with is a DOM node, and the node being wrapped is
|
151
|
+
// a plain looking DOM node merge the 2 nodes.
|
152
|
+
return {
|
153
|
+
...node,
|
154
|
+
...wrappingNode,
|
155
|
+
formattingOptions: undefinedIfEmpty({
|
156
|
+
...node.formattingOptions,
|
157
|
+
...wrappingNode.formattingOptions,
|
158
|
+
}),
|
159
|
+
attributes: undefinedIfEmpty({
|
160
|
+
...node.attributes,
|
161
|
+
...wrappingNode.attributes,
|
162
|
+
}),
|
163
|
+
font: undefinedIfEmpty({
|
164
|
+
...node.font,
|
165
|
+
...wrappingNode.font,
|
166
|
+
}),
|
167
|
+
children: [...node.children, ...wrappingNode.children],
|
168
|
+
};
|
169
|
+
}
|
170
|
+
|
171
|
+
// if the node we are wrapping or the wrapping nodes are not compatible merge them.
|
172
|
+
return {
|
173
|
+
...wrappingNode,
|
174
|
+
children: [node, ...wrappingNode.children],
|
175
|
+
};
|
176
|
+
};
|
177
|
+
|
178
|
+
const transformMark = (mark: Mark, node: FormattedNode): FormattedNode => {
|
179
|
+
switch (mark.type.name) {
|
180
|
+
case 'link':
|
181
|
+
return wrapNodeIfNeeded(node, {
|
182
|
+
type: 'tag',
|
183
|
+
tag: 'a',
|
184
|
+
attributes: transformAttributes(mark.attrs),
|
185
|
+
children: [],
|
186
|
+
});
|
187
|
+
case 'assetLink':
|
188
|
+
return wrapNodeIfNeeded(node, {
|
189
|
+
type: 'link-to-matrix-asset',
|
190
|
+
target: mark.attrs.target,
|
191
|
+
matrixIdentifier: mark.attrs.matrixIdentifier,
|
192
|
+
matrixDomain: mark.attrs.matrixDomain,
|
193
|
+
matrixAssetId: mark.attrs.matrixAssetId,
|
194
|
+
children: [],
|
195
|
+
});
|
196
|
+
}
|
197
|
+
|
198
|
+
throw new Error(`Unsupported mark "${mark.type.name}" was applied to node.`);
|
199
|
+
};
|
200
|
+
|
201
|
+
/**
|
202
|
+
* Converts Remirror node JSON structure to Squiz component JSON structure.
|
203
|
+
* @param {ProsemirrorNode} node Remirror node to convert to component.
|
204
|
+
* @returns {FormattedText} The converted Squiz component JSON.
|
205
|
+
*/
|
206
|
+
export const remirrorNodeToSquizNode = (node: ProsemirrorNode): FormattedText => {
|
207
|
+
if (node?.type?.name !== 'doc') {
|
208
|
+
throw new Error('Unable to convert from Remirror to Node data structure, unexpected node provided.');
|
209
|
+
}
|
210
|
+
|
211
|
+
return transformFragment(node.content);
|
212
|
+
};
|