@squiz/formatted-text-editor 1.16.0 → 1.21.1-alpha.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/.eslintrc.json +7 -0
  2. package/README.md +10 -0
  3. package/demo/App.tsx +18 -4
  4. package/demo/index.scss +16 -10
  5. package/jest.config.ts +8 -9
  6. package/lib/Editor/Editor.js +18 -13
  7. package/lib/EditorToolbar/FloatingToolbar.js +50 -20
  8. package/lib/EditorToolbar/Toolbar.js +33 -24
  9. package/lib/EditorToolbar/Tools/Bold/BoldButton.js +14 -9
  10. package/lib/EditorToolbar/Tools/Image/Form/ImageForm.d.ts +17 -0
  11. package/lib/EditorToolbar/Tools/Image/Form/ImageForm.js +84 -0
  12. package/lib/EditorToolbar/Tools/Image/ImageButton.d.ts +2 -0
  13. package/lib/EditorToolbar/Tools/Image/ImageButton.js +67 -0
  14. package/lib/EditorToolbar/Tools/Image/ImageModal.d.ts +8 -0
  15. package/lib/EditorToolbar/Tools/Image/ImageModal.js +19 -0
  16. package/lib/EditorToolbar/Tools/Italic/ItalicButton.js +14 -9
  17. package/lib/EditorToolbar/Tools/Link/Form/LinkForm.js +20 -15
  18. package/lib/EditorToolbar/Tools/Link/LinkButton.js +42 -14
  19. package/lib/EditorToolbar/Tools/Link/LinkModal.js +16 -11
  20. package/lib/EditorToolbar/Tools/Link/RemoveLinkButton.js +13 -8
  21. package/lib/EditorToolbar/Tools/Redo/RedoButton.js +13 -8
  22. package/lib/EditorToolbar/Tools/TextAlign/CenterAlign/CenterAlignButton.js +13 -8
  23. package/lib/EditorToolbar/Tools/TextAlign/JustifyAlign/JustifyAlignButton.js +13 -8
  24. package/lib/EditorToolbar/Tools/TextAlign/LeftAlign/LeftAlignButton.js +13 -8
  25. package/lib/EditorToolbar/Tools/TextAlign/RightAlign/RightAlignButton.js +13 -8
  26. package/lib/EditorToolbar/Tools/TextAlign/TextAlignButtons.js +19 -14
  27. package/lib/EditorToolbar/Tools/TextType/Heading/HeadingButton.js +19 -14
  28. package/lib/EditorToolbar/Tools/TextType/Paragraph/ParagraphButton.js +14 -9
  29. package/lib/EditorToolbar/Tools/TextType/Preformatted/PreformattedButton.js +13 -8
  30. package/lib/EditorToolbar/Tools/TextType/TextTypeDropdown.js +24 -19
  31. package/lib/EditorToolbar/Tools/Underline/UnderlineButton.js +14 -9
  32. package/lib/EditorToolbar/Tools/Undo/UndoButton.js +13 -8
  33. package/lib/EditorToolbar/index.js +18 -2
  34. package/lib/Extensions/Extensions.d.ts +2 -4
  35. package/lib/Extensions/Extensions.js +19 -13
  36. package/lib/Extensions/ImageExtension/ImageExtension.d.ts +3 -0
  37. package/lib/Extensions/ImageExtension/ImageExtension.js +7 -0
  38. package/lib/Extensions/LinkExtension/LinkExtension.js +17 -11
  39. package/lib/Extensions/PreformattedExtension/PreformattedExtension.d.ts +1 -1
  40. package/lib/Extensions/PreformattedExtension/PreformattedExtension.js +10 -7
  41. package/lib/FormattedTextEditor.js +7 -2
  42. package/lib/hooks/index.js +17 -1
  43. package/lib/hooks/useExtensionNames.js +9 -5
  44. package/lib/index.css +133 -76
  45. package/lib/index.d.ts +3 -1
  46. package/lib/index.js +12 -2
  47. package/lib/ui/Button/Button.d.ts +11 -0
  48. package/lib/ui/Button/Button.js +14 -0
  49. package/lib/ui/Fields/Input/Input.d.ts +4 -0
  50. package/lib/ui/Fields/Input/Input.js +33 -0
  51. package/lib/ui/Fields/Select/Select.js +53 -0
  52. package/lib/ui/Modal/FormModal.js +33 -5
  53. package/lib/ui/Modal/Modal.js +50 -22
  54. package/lib/ui/ToolbarDropdown/ToolbarDropdown.js +38 -10
  55. package/lib/ui/ToolbarDropdownButton/ToolbarDropdownButton.js +11 -6
  56. package/lib/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.d.ts +10 -0
  57. package/lib/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.js +160 -0
  58. package/lib/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.d.ts +9 -0
  59. package/lib/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.js +105 -0
  60. package/lib/utils/converters/validNodeTypes.d.ts +2 -0
  61. package/lib/utils/converters/validNodeTypes.js +21 -0
  62. package/lib/utils/createToolbarPositioner.js +16 -12
  63. package/lib/utils/getCursorRect.js +5 -1
  64. package/package.json +7 -3
  65. package/src/Editor/_editor.scss +2 -49
  66. package/src/EditorToolbar/FloatingToolbar.tsx +1 -1
  67. package/src/EditorToolbar/Toolbar.tsx +2 -0
  68. package/src/EditorToolbar/Tools/Bold/BoldButton.spec.tsx +1 -1
  69. package/src/EditorToolbar/Tools/Bold/BoldButton.tsx +2 -2
  70. package/src/EditorToolbar/Tools/Image/Form/ImageForm.spec.tsx +23 -0
  71. package/src/EditorToolbar/Tools/Image/Form/ImageForm.tsx +92 -0
  72. package/src/EditorToolbar/Tools/Image/ImageButton.spec.tsx +79 -0
  73. package/src/EditorToolbar/Tools/Image/ImageButton.tsx +57 -0
  74. package/src/EditorToolbar/Tools/Image/ImageModal.spec.tsx +83 -0
  75. package/src/EditorToolbar/Tools/Image/ImageModal.tsx +29 -0
  76. package/src/EditorToolbar/Tools/Italic/ItalicButton.spec.tsx +1 -1
  77. package/src/EditorToolbar/Tools/Italic/ItalicButton.tsx +2 -2
  78. package/src/EditorToolbar/Tools/Link/Form/LinkForm.tsx +5 -5
  79. package/src/EditorToolbar/Tools/Link/LinkButton.tsx +2 -2
  80. package/src/EditorToolbar/Tools/Link/RemoveLinkButton.tsx +2 -2
  81. package/src/EditorToolbar/Tools/Redo/RedoButton.tsx +2 -2
  82. package/src/EditorToolbar/Tools/TextAlign/CenterAlign/CenterAlignButton.tsx +2 -2
  83. package/src/EditorToolbar/Tools/TextAlign/JustifyAlign/JustifyAlignButton.tsx +2 -2
  84. package/src/EditorToolbar/Tools/TextAlign/LeftAlign/LeftAlignButton.tsx +2 -2
  85. package/src/EditorToolbar/Tools/TextAlign/RightAlign/RightAlignButton.tsx +2 -2
  86. package/src/EditorToolbar/Tools/Underline/Underline.spec.tsx +1 -1
  87. package/src/EditorToolbar/Tools/Underline/UnderlineButton.tsx +2 -2
  88. package/src/EditorToolbar/Tools/Undo/UndoButton.tsx +2 -2
  89. package/src/EditorToolbar/_floating-toolbar.scss +6 -0
  90. package/src/EditorToolbar/_toolbar.scss +8 -2
  91. package/src/Extensions/Extensions.ts +5 -2
  92. package/src/Extensions/ImageExtension/ImageExtension.ts +3 -0
  93. package/src/Extensions/LinkExtension/LinkExtension.ts +8 -5
  94. package/src/index.scss +2 -2
  95. package/src/index.ts +3 -1
  96. package/src/ui/Button/Button.spec.tsx +44 -0
  97. package/src/ui/Button/Button.tsx +31 -0
  98. package/src/ui/{_buttons.scss → Button/_button.scss} +19 -1
  99. package/src/ui/{Inputs/Text/TextInput.spec.tsx → Fields/Input/Input.spec.tsx} +8 -8
  100. package/src/ui/{Inputs/Text/TextInput.tsx → Fields/Input/Input.tsx} +4 -4
  101. package/src/ui/Modal/Modal.tsx +1 -0
  102. package/src/ui/ToolbarDropdown/ToolbarDropdown.spec.tsx +4 -2
  103. package/src/ui/ToolbarDropdown/ToolbarDropdown.tsx +1 -1
  104. package/src/ui/_typography.scss +46 -0
  105. package/src/utils/converters/mocks/squizNodeJson.mock.ts +75 -0
  106. package/src/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.spec.ts +445 -0
  107. package/src/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.ts +191 -0
  108. package/src/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.spec.ts +307 -0
  109. package/src/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.ts +123 -0
  110. package/src/utils/converters/validNodeTypes.spec.ts +33 -0
  111. package/src/utils/converters/validNodeTypes.ts +21 -0
  112. package/tests/renderWithEditor.tsx +2 -2
  113. package/tsconfig.json +1 -1
  114. package/lib/ui/Inputs/Select/Select.js +0 -23
  115. package/lib/ui/Inputs/Text/TextInput.d.ts +0 -4
  116. package/lib/ui/Inputs/Text/TextInput.js +0 -7
  117. package/lib/ui/ToolbarButton/ToolbarButton.d.ts +0 -10
  118. package/lib/ui/ToolbarButton/ToolbarButton.js +0 -5
  119. package/src/ui/ToolbarButton/ToolbarButton.tsx +0 -26
  120. package/src/ui/ToolbarButton/_toolbar-button.scss +0 -17
  121. /package/lib/ui/{Inputs → Fields}/Select/Select.d.ts +0 -0
  122. /package/src/ui/{Inputs → Fields}/Select/Select.spec.tsx +0 -0
  123. /package/src/ui/{Inputs → Fields}/Select/Select.tsx +0 -0
@@ -0,0 +1,445 @@
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
+
6
+ type FormattedText = FormattedTextModels.v1.FormattedText;
7
+
8
+ describe('remirrorNodeToSquizNode', () => {
9
+ it('should convert a simple Remirror node to a Squiz component JSON object', async () => {
10
+ const content: RemirrorJSON = {
11
+ type: 'doc',
12
+ content: [
13
+ {
14
+ type: 'paragraph',
15
+ content: [
16
+ {
17
+ type: 'text',
18
+ text: 'Hello world!',
19
+ },
20
+ ],
21
+ },
22
+ ],
23
+ };
24
+
25
+ const { editor } = await renderWithEditor(null, { content });
26
+
27
+ const expected: FormattedText = [
28
+ {
29
+ type: 'tag',
30
+ tag: 'p',
31
+ children: [{ type: 'text', value: 'Hello world!' }],
32
+ },
33
+ ];
34
+
35
+ const result = remirrorNodeToSquizNode(editor.doc);
36
+ expect(result).toEqual(expected);
37
+ });
38
+
39
+ it('should handle preformatted formatting', async () => {
40
+ const content: RemirrorJSON = {
41
+ type: 'doc',
42
+ content: [
43
+ {
44
+ type: 'preformatted',
45
+ attrs: {
46
+ nodeIndent: null,
47
+ nodeTextAlignment: null,
48
+ nodeLineHeight: null,
49
+ style: '',
50
+ },
51
+ content: [
52
+ {
53
+ type: 'text',
54
+ text: 'Hello world',
55
+ },
56
+ ],
57
+ },
58
+ ],
59
+ };
60
+
61
+ const { editor } = await renderWithEditor(null, { content });
62
+
63
+ const expected: FormattedText = [
64
+ {
65
+ type: 'tag',
66
+ tag: 'pre',
67
+ children: [{ type: 'text', value: 'Hello world' }],
68
+ },
69
+ ];
70
+
71
+ const result = remirrorNodeToSquizNode(editor.doc);
72
+ expect(result).toEqual(expected);
73
+ });
74
+
75
+ it('should handle images', async () => {
76
+ const content: RemirrorJSON = {
77
+ type: 'doc',
78
+ content: [
79
+ {
80
+ type: 'paragraph',
81
+ attrs: {
82
+ nodeIndent: null,
83
+ nodeTextAlignment: null,
84
+ nodeLineHeight: null,
85
+ style: '',
86
+ },
87
+ content: [
88
+ {
89
+ type: 'image',
90
+ attrs: {
91
+ alt: '',
92
+ crop: null,
93
+ height: 150,
94
+ width: 200,
95
+ rotate: null,
96
+ src: 'https://media2.giphy.com/media/3o6ozsIxg5legYvggo/giphy.gif',
97
+ title: '',
98
+ fileName: null,
99
+ resizable: false,
100
+ },
101
+ },
102
+ ],
103
+ },
104
+ ],
105
+ };
106
+
107
+ const { editor } = await renderWithEditor(null, { content });
108
+
109
+ const expected: FormattedText = [
110
+ {
111
+ type: 'tag',
112
+ tag: 'p',
113
+ children: [
114
+ {
115
+ children: [],
116
+ attributes: {
117
+ alt: '',
118
+ height: '150',
119
+ width: '200',
120
+ src: 'https://media2.giphy.com/media/3o6ozsIxg5legYvggo/giphy.gif',
121
+ title: '',
122
+ },
123
+ type: 'tag',
124
+ tag: 'img',
125
+ },
126
+ ],
127
+ },
128
+ ];
129
+
130
+ const result = remirrorNodeToSquizNode(editor.doc);
131
+ expect(result).toEqual(expected);
132
+ });
133
+
134
+ it.each([1, 2, 3, 4, 5, 6])('should handle heading %s formatting', async (level) => {
135
+ const content: RemirrorJSON = {
136
+ type: 'doc',
137
+ content: [
138
+ {
139
+ type: 'heading',
140
+ attrs: {
141
+ nodeIndent: null,
142
+ nodeTextAlignment: null,
143
+ nodeLineHeight: null,
144
+ style: '',
145
+ level: level,
146
+ },
147
+ content: [
148
+ {
149
+ type: 'text',
150
+ text: 'Hello world',
151
+ },
152
+ ],
153
+ },
154
+ ],
155
+ };
156
+
157
+ const { editor } = await renderWithEditor(null, { content });
158
+
159
+ const expected: FormattedText = [
160
+ {
161
+ type: 'tag',
162
+ tag: `h${level}`,
163
+ children: [{ type: 'text', value: 'Hello world' }],
164
+ },
165
+ ];
166
+
167
+ const result = remirrorNodeToSquizNode(editor.doc);
168
+ expect(result).toEqual(expected);
169
+ });
170
+
171
+ it('should preserve font and attribute options', async () => {
172
+ const content: RemirrorJSON = {
173
+ type: 'doc',
174
+ content: [
175
+ {
176
+ type: 'paragraph',
177
+ content: [
178
+ {
179
+ type: 'text',
180
+ text: 'You can',
181
+ },
182
+ {
183
+ type: 'text',
184
+ marks: [
185
+ {
186
+ type: 'bold',
187
+ },
188
+ ],
189
+ text: ' click',
190
+ },
191
+ {
192
+ type: 'text',
193
+ text: ' ',
194
+ },
195
+ {
196
+ type: 'text',
197
+ marks: [
198
+ {
199
+ type: 'link',
200
+ attrs: {
201
+ href: 'https://www.google.com/',
202
+ target: null,
203
+ auto: false,
204
+ title: '',
205
+ },
206
+ },
207
+ {
208
+ type: 'italic',
209
+ },
210
+ ],
211
+ text: 'here',
212
+ },
213
+ {
214
+ type: 'text',
215
+ marks: [
216
+ {
217
+ type: 'italic',
218
+ },
219
+ ],
220
+ text: ' ',
221
+ },
222
+ {
223
+ type: 'text',
224
+ text: 'to go to',
225
+ },
226
+ {
227
+ type: 'text',
228
+ marks: [
229
+ {
230
+ type: 'bold',
231
+ },
232
+ {
233
+ type: 'italic',
234
+ },
235
+ {
236
+ type: 'underline',
237
+ },
238
+ ],
239
+ text: ' GOOGLE!',
240
+ },
241
+ ],
242
+ },
243
+ ],
244
+ };
245
+
246
+ const { editor } = await renderWithEditor(null, { content });
247
+
248
+ const expected: FormattedText = [
249
+ {
250
+ children: [
251
+ {
252
+ type: 'text',
253
+ value: 'You can',
254
+ },
255
+ {
256
+ children: [
257
+ {
258
+ type: 'text',
259
+ value: ' click',
260
+ },
261
+ ],
262
+ font: {
263
+ bold: true,
264
+ },
265
+ tag: 'span',
266
+ type: 'tag',
267
+ },
268
+ {
269
+ type: 'text',
270
+ value: ' ',
271
+ },
272
+ {
273
+ children: [
274
+ {
275
+ type: 'text',
276
+ value: 'here',
277
+ },
278
+ ],
279
+ attributes: {
280
+ href: 'https://www.google.com/',
281
+ title: '',
282
+ },
283
+ font: {
284
+ italics: true,
285
+ },
286
+ tag: 'a',
287
+ type: 'tag',
288
+ },
289
+ {
290
+ children: [
291
+ {
292
+ type: 'text',
293
+ value: ' ',
294
+ },
295
+ ],
296
+ font: {
297
+ italics: true,
298
+ },
299
+ tag: 'span',
300
+ type: 'tag',
301
+ },
302
+ {
303
+ type: 'text',
304
+ value: 'to go to',
305
+ },
306
+ {
307
+ children: [
308
+ {
309
+ type: 'text',
310
+ value: ' GOOGLE!',
311
+ },
312
+ ],
313
+ font: {
314
+ bold: true,
315
+ italics: true,
316
+ underline: true,
317
+ },
318
+ tag: 'span',
319
+ type: 'tag',
320
+ },
321
+ ],
322
+ type: 'tag',
323
+ tag: 'p',
324
+ },
325
+ ];
326
+
327
+ const result = remirrorNodeToSquizNode(editor.doc);
328
+ expect(result).toEqual(expected);
329
+ });
330
+
331
+ describe('preserve text formatting', () => {
332
+ it('should preserve text alignment', async () => {
333
+ const content: RemirrorJSON = {
334
+ type: 'doc',
335
+ content: [
336
+ {
337
+ type: 'paragraph',
338
+ attrs: {
339
+ nodeIndent: null,
340
+ nodeTextAlignment: 'center',
341
+ nodeLineHeight: null,
342
+ style: '',
343
+ },
344
+ content: [
345
+ {
346
+ type: 'text',
347
+ text: 'Hello world!',
348
+ },
349
+ ],
350
+ },
351
+ ],
352
+ };
353
+
354
+ const { editor } = await renderWithEditor(null, { content });
355
+
356
+ const expected: FormattedText = [
357
+ {
358
+ children: [
359
+ {
360
+ type: 'text',
361
+ value: 'Hello world!',
362
+ },
363
+ ],
364
+ formattingOptions: {
365
+ alignment: 'center',
366
+ },
367
+ type: 'tag',
368
+ tag: 'p',
369
+ },
370
+ ];
371
+
372
+ const result = remirrorNodeToSquizNode(editor.doc);
373
+ expect(result).toEqual(expected);
374
+ });
375
+ });
376
+
377
+ it('should handle invalid Remirror node provided', () => {
378
+ const result = remirrorNodeToSquizNode(false as any);
379
+ expect(result).toEqual([]);
380
+ });
381
+
382
+ it('should handle no content provided by Remirror', async () => {
383
+ const content: RemirrorJSON = {
384
+ type: 'doc',
385
+ };
386
+
387
+ const { editor } = await renderWithEditor(null, { content });
388
+
389
+ const result = remirrorNodeToSquizNode(editor.doc);
390
+ expect(result).toEqual([]);
391
+ });
392
+ });
393
+
394
+ describe('resolveNodeTag', () => {
395
+ it('should return the correct tag name for a node with a toDOM method that returns a Node', () => {
396
+ const node: any = {
397
+ type: {
398
+ spec: {
399
+ toDOM: () => document.createElement('div'),
400
+ },
401
+ },
402
+ };
403
+ expect(resolveNodeTag(node)).toBe('div');
404
+ });
405
+
406
+ it('should return the correct tag name for a node with a toDOM method that returns an object with a dom property', () => {
407
+ const node: any = {
408
+ type: {
409
+ spec: {
410
+ toDOM: () => ({ dom: document.createElement('span') }),
411
+ },
412
+ },
413
+ };
414
+ expect(resolveNodeTag(node)).toBe('span');
415
+ });
416
+
417
+ it('should return the correct tag name for a node with a toDOM method that returns an array', () => {
418
+ const node: any = {
419
+ type: {
420
+ spec: {
421
+ toDOM: () => ['ul', { class: 'my-list' }],
422
+ },
423
+ },
424
+ };
425
+ expect(resolveNodeTag(node)).toBe('ul');
426
+ });
427
+
428
+ it('should return null for a node with a toDOM method that returns undefined', () => {
429
+ const node: any = {
430
+ type: {
431
+ spec: {
432
+ toDOM: () => undefined,
433
+ },
434
+ },
435
+ };
436
+ expect(resolveNodeTag(node)).toBeNull();
437
+ });
438
+
439
+ it('should return null for a node without a toDOM method', () => {
440
+ const node: any = {
441
+ type: {},
442
+ };
443
+ expect(resolveNodeTag(node)).toBeNull();
444
+ });
445
+ });
@@ -0,0 +1,191 @@
1
+ import { ProsemirrorNode, Fragment as ProsemirrorFragment } from 'remirror';
2
+ import { FORMATTED_TEXT_MODELS as FormattedTextModels } from '@squiz/dx-json-schema-lib';
3
+ import { validRemirrorNode } from '../validNodeTypes';
4
+
5
+ type Fragment = ProsemirrorFragment & {
6
+ content?: Fragment[];
7
+ };
8
+
9
+ type FormattingOptions = FormattedTextModels.v1.FormattingOptions;
10
+ type FontOptions = FormattedTextModels.v1.FormattedNodeFontProperties;
11
+ type FormattedText = FormattedTextModels.v1.FormattedText;
12
+
13
+ export const resolveNodeTag = (node: ProsemirrorNode): string | null => {
14
+ if (node.type.spec?.toDOM) {
15
+ const domNode = node.type.spec.toDOM(node) as any;
16
+
17
+ if (domNode instanceof window.Node) {
18
+ return domNode.nodeName.toLowerCase();
19
+ }
20
+
21
+ if (domNode?.dom instanceof window.Node) {
22
+ return domNode.dom.nodeName.toLowerCase();
23
+ }
24
+
25
+ if (domNode instanceof Array) {
26
+ // [ tag, attributes, ...children ]
27
+ return domNode[0].toLowerCase();
28
+ }
29
+ }
30
+
31
+ return null;
32
+ };
33
+
34
+ const resolveFormattingOptions = (node: ProsemirrorNode): FormattingOptions => {
35
+ const formattingOptions: FormattingOptions = {};
36
+
37
+ if (node.attrs.nodeTextAlignment) {
38
+ formattingOptions.alignment = node.attrs.nodeTextAlignment;
39
+ }
40
+
41
+ return formattingOptions;
42
+ };
43
+
44
+ const resolveFontOptions = (node: ProsemirrorNode): FontOptions => {
45
+ const fontOptions: FontOptions = {};
46
+
47
+ node.marks.forEach((mark) => {
48
+ switch (mark.type.name) {
49
+ case 'bold':
50
+ fontOptions.bold = true;
51
+ break;
52
+ case 'italic':
53
+ fontOptions.italics = true;
54
+ break;
55
+ case 'underline':
56
+ fontOptions.underline = true;
57
+ break;
58
+ default:
59
+ // Currently unsupported mark type
60
+ break;
61
+ }
62
+ });
63
+
64
+ return fontOptions;
65
+ };
66
+
67
+ const resolveAttributeOptions = (node: ProsemirrorNode, nodeType: string | null): Record<string, string> => {
68
+ let attributeOptions: any = {};
69
+
70
+ if (nodeType === 'image') {
71
+ attributeOptions = { ...node.attrs };
72
+ } else {
73
+ node.marks.forEach((mark) => {
74
+ switch (mark.type.name) {
75
+ case 'link':
76
+ attributeOptions = { ...mark.attrs };
77
+ break;
78
+ default:
79
+ // Currently unsupported mark type
80
+ break;
81
+ }
82
+ });
83
+ }
84
+
85
+ // Remove any non string elements from attributes, squiz component only accepts strings.
86
+ Object.keys(attributeOptions).forEach((key) => {
87
+ if (typeof attributeOptions[key] !== 'string' && typeof attributeOptions[key] !== 'number') {
88
+ delete attributeOptions[key];
89
+ // If it's a number we make it a string so its accepted by component service
90
+ } else {
91
+ attributeOptions[key] = String(attributeOptions[key]);
92
+ }
93
+ });
94
+
95
+ return attributeOptions;
96
+ };
97
+
98
+ /**
99
+ * Converts Remirror node JSON structure to Squiz component JSON structure.
100
+ * @param {ProsemirrorNode} node Remirror node to convert to component.
101
+ * @export
102
+ * @returns {FormattedText} The converted Squiz component JSON.
103
+ */
104
+ export const remirrorNodeToSquizNode = (node: ProsemirrorNode): FormattedText => {
105
+ if (!validRemirrorNode(node)) return [];
106
+
107
+ const nodeType = node.type.name;
108
+ let nodeTag = resolveNodeTag(node);
109
+
110
+ // Filter out any children nodes that aren't currently supported.
111
+ const children = ((node.content as Fragment).content || []).map((child: any) => remirrorNodeToSquizNode(child));
112
+
113
+ let transformed: any = {
114
+ children,
115
+ formattingOptions: resolveFormattingOptions(node),
116
+ attributes: resolveAttributeOptions(node, nodeType),
117
+ font: resolveFontOptions(node),
118
+ };
119
+
120
+ if (nodeType === 'doc') {
121
+ return transformed.children;
122
+ }
123
+
124
+ // If we don't have a node tag yet, check if there is one needed
125
+ if (!nodeTag) {
126
+ node.marks.forEach((mark) => {
127
+ switch (mark.type.name) {
128
+ case 'link':
129
+ nodeTag = 'a';
130
+ break;
131
+ default:
132
+ // Currently unsupported mark type
133
+ break;
134
+ }
135
+ });
136
+ }
137
+
138
+ if (nodeTag) {
139
+ transformed = {
140
+ ...transformed,
141
+ type: 'tag',
142
+ tag: nodeTag,
143
+ };
144
+ }
145
+
146
+ if (
147
+ (Object.keys(transformed.font).length > 0 || Object.keys(transformed.attributes).length > 0) &&
148
+ !transformed.type
149
+ ) {
150
+ // Wrap in span so we can apply formatting to it
151
+ transformed = { ...transformed, tag: 'span', type: 'tag' };
152
+ }
153
+
154
+ if (nodeType === 'text') {
155
+ if (transformed.type) {
156
+ // If we have a tag already nest the text beneath it so we can preserve formatting options, etc.
157
+ transformed = {
158
+ ...transformed,
159
+ children: [
160
+ {
161
+ type: 'text',
162
+ value: node.text,
163
+ },
164
+ ],
165
+ };
166
+ } else {
167
+ // If we don't have a tag just rewrite the transformed value to be the text.
168
+ transformed = {
169
+ type: 'text',
170
+ value: node.text,
171
+ };
172
+ }
173
+ }
174
+
175
+ // Remove empty formatting options from transformed object.
176
+ if (transformed.formattingOptions && Object.keys(transformed.formattingOptions).length === 0) {
177
+ delete transformed.formattingOptions;
178
+ }
179
+
180
+ // Remove empty font options from transformed object.
181
+ if (transformed.font && Object.keys(transformed.font).length === 0) {
182
+ delete transformed.font;
183
+ }
184
+
185
+ // Remove empty attributes options from transformed object.
186
+ if (transformed.attributes && Object.keys(transformed.attributes).length === 0) {
187
+ delete transformed.attributes;
188
+ }
189
+
190
+ return transformed;
191
+ };