@squiz/formatted-text-editor 1.21.1-alpha.9 → 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.
Files changed (123) hide show
  1. package/demo/App.tsx +38 -10
  2. package/demo/index.scss +2 -7
  3. package/jest.config.ts +0 -2
  4. package/lib/Editor/Editor.js +45 -7
  5. package/lib/Editor/EditorContext.d.ts +15 -0
  6. package/lib/Editor/EditorContext.js +15 -0
  7. package/lib/EditorToolbar/FloatingToolbar.js +11 -5
  8. package/lib/EditorToolbar/Tools/Image/Form/ImageForm.d.ts +9 -8
  9. package/lib/EditorToolbar/Tools/Image/Form/ImageForm.js +91 -23
  10. package/lib/EditorToolbar/Tools/Image/ImageButton.d.ts +4 -1
  11. package/lib/EditorToolbar/Tools/Image/ImageButton.js +22 -14
  12. package/lib/EditorToolbar/Tools/Image/ImageModal.js +9 -5
  13. package/lib/EditorToolbar/Tools/Link/Form/LinkForm.d.ts +14 -5
  14. package/lib/EditorToolbar/Tools/Link/Form/LinkForm.js +66 -14
  15. package/lib/EditorToolbar/Tools/Link/LinkButton.js +21 -13
  16. package/lib/EditorToolbar/Tools/Link/LinkModal.js +12 -5
  17. package/lib/EditorToolbar/Tools/Link/RemoveLinkButton.js +1 -8
  18. package/lib/Extensions/CommandsExtension/CommandsExtension.d.ts +20 -0
  19. package/lib/Extensions/CommandsExtension/CommandsExtension.js +52 -0
  20. package/lib/Extensions/Extensions.d.ts +11 -1
  21. package/lib/Extensions/Extensions.js +42 -20
  22. package/lib/Extensions/ImageExtension/AssetImageExtension.d.ts +17 -0
  23. package/lib/Extensions/ImageExtension/AssetImageExtension.js +92 -0
  24. package/lib/Extensions/ImageExtension/ImageExtension.d.ts +4 -0
  25. package/lib/Extensions/ImageExtension/ImageExtension.js +11 -0
  26. package/lib/Extensions/LinkExtension/AssetLinkExtension.d.ts +26 -0
  27. package/lib/Extensions/LinkExtension/AssetLinkExtension.js +102 -0
  28. package/lib/Extensions/LinkExtension/LinkExtension.d.ts +19 -12
  29. package/lib/Extensions/LinkExtension/LinkExtension.js +56 -66
  30. package/lib/Extensions/LinkExtension/common.d.ts +7 -0
  31. package/lib/Extensions/LinkExtension/common.js +14 -0
  32. package/lib/Extensions/PreformattedExtension/PreformattedExtension.js +6 -2
  33. package/lib/hooks/index.d.ts +1 -0
  34. package/lib/hooks/index.js +1 -0
  35. package/lib/hooks/useExpandedSelection.d.ts +23 -0
  36. package/lib/hooks/useExpandedSelection.js +37 -0
  37. package/lib/index.css +58 -26
  38. package/lib/index.d.ts +3 -2
  39. package/lib/index.js +5 -3
  40. package/lib/types.d.ts +3 -0
  41. package/lib/types.js +2 -0
  42. package/lib/ui/Button/Button.d.ts +2 -1
  43. package/lib/ui/Button/Button.js +4 -5
  44. package/lib/ui/Fields/Input/Input.d.ts +1 -0
  45. package/lib/ui/Fields/Input/Input.js +9 -3
  46. package/lib/ui/Modal/Modal.js +5 -3
  47. package/lib/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.d.ts +1 -2
  48. package/lib/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.js +118 -104
  49. package/lib/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.js +102 -69
  50. package/lib/utils/resolveMatrixAssetUrl.d.ts +1 -0
  51. package/lib/utils/resolveMatrixAssetUrl.js +10 -0
  52. package/lib/utils/undefinedIfEmpty.d.ts +1 -0
  53. package/lib/utils/undefinedIfEmpty.js +7 -0
  54. package/package.json +8 -4
  55. package/src/Editor/Editor.spec.tsx +78 -18
  56. package/src/Editor/Editor.tsx +28 -9
  57. package/src/Editor/EditorContext.spec.tsx +26 -0
  58. package/src/Editor/EditorContext.ts +26 -0
  59. package/src/Editor/_editor.scss +20 -4
  60. package/src/EditorToolbar/FloatingToolbar.spec.tsx +26 -7
  61. package/src/EditorToolbar/FloatingToolbar.tsx +15 -6
  62. package/src/EditorToolbar/Tools/Image/Form/ImageForm.spec.tsx +81 -6
  63. package/src/EditorToolbar/Tools/Image/Form/ImageForm.tsx +167 -47
  64. package/src/EditorToolbar/Tools/Image/ImageButton.spec.tsx +250 -2
  65. package/src/EditorToolbar/Tools/Image/ImageButton.tsx +29 -16
  66. package/src/EditorToolbar/Tools/Image/ImageModal.spec.tsx +59 -20
  67. package/src/EditorToolbar/Tools/Image/ImageModal.tsx +12 -10
  68. package/src/EditorToolbar/Tools/Link/Form/LinkForm.spec.tsx +37 -9
  69. package/src/EditorToolbar/Tools/Link/Form/LinkForm.tsx +96 -26
  70. package/src/EditorToolbar/Tools/Link/LinkButton.spec.tsx +137 -26
  71. package/src/EditorToolbar/Tools/Link/LinkButton.tsx +28 -19
  72. package/src/EditorToolbar/Tools/Link/LinkModal.tsx +13 -6
  73. package/src/EditorToolbar/Tools/Link/RemoveLinkButton.spec.tsx +27 -26
  74. package/src/EditorToolbar/Tools/Link/RemoveLinkButton.tsx +2 -10
  75. package/src/EditorToolbar/Tools/Undo/UndoButton.spec.tsx +22 -1
  76. package/src/EditorToolbar/_floating-toolbar.scss +4 -5
  77. package/src/EditorToolbar/_toolbar.scss +1 -1
  78. package/src/Extensions/CommandsExtension/CommandsExtension.ts +54 -0
  79. package/src/Extensions/Extensions.ts +42 -19
  80. package/src/Extensions/ImageExtension/AssetImageExtension.spec.ts +76 -0
  81. package/src/Extensions/ImageExtension/AssetImageExtension.ts +111 -0
  82. package/src/Extensions/ImageExtension/ImageExtension.ts +17 -1
  83. package/src/Extensions/LinkExtension/AssetLinkExtension.spec.ts +104 -0
  84. package/src/Extensions/LinkExtension/AssetLinkExtension.ts +128 -0
  85. package/src/Extensions/LinkExtension/LinkExtension.spec.ts +68 -0
  86. package/src/Extensions/LinkExtension/LinkExtension.ts +71 -85
  87. package/src/Extensions/LinkExtension/common.ts +10 -0
  88. package/src/Extensions/PreformattedExtension/PreformattedExtension.spec.ts +41 -0
  89. package/src/Extensions/PreformattedExtension/PreformattedExtension.ts +6 -2
  90. package/src/hooks/index.ts +1 -0
  91. package/src/hooks/useExpandedSelection.ts +44 -0
  92. package/src/index.ts +3 -2
  93. package/src/types.ts +5 -0
  94. package/src/ui/Button/Button.tsx +10 -6
  95. package/src/ui/Button/_button.scss +1 -1
  96. package/src/ui/Fields/Input/Input.spec.tsx +7 -1
  97. package/src/ui/Fields/Input/Input.tsx +23 -4
  98. package/src/ui/Modal/Modal.spec.tsx +15 -0
  99. package/src/ui/Modal/Modal.tsx +8 -4
  100. package/src/ui/ToolbarDropdown/_toolbar-dropdown.scss +1 -1
  101. package/src/ui/_forms.scss +14 -0
  102. package/src/utils/converters/mocks/squizNodeJson.mock.ts +196 -0
  103. package/src/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.spec.ts +41 -6
  104. package/src/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.ts +132 -111
  105. package/src/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.spec.ts +68 -34
  106. package/src/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.ts +115 -79
  107. package/src/utils/resolveMatrixAssetUrl.spec.ts +26 -0
  108. package/src/utils/resolveMatrixAssetUrl.ts +7 -0
  109. package/src/utils/undefinedIfEmpty.spec.ts +12 -0
  110. package/src/utils/undefinedIfEmpty.ts +3 -0
  111. package/tailwind.config.cjs +3 -0
  112. package/tests/renderWithEditor.tsx +26 -13
  113. package/tsconfig.json +1 -1
  114. package/lib/FormattedTextEditor.d.ts +0 -2
  115. package/lib/FormattedTextEditor.js +0 -7
  116. package/lib/utils/converters/validNodeTypes.d.ts +0 -2
  117. package/lib/utils/converters/validNodeTypes.js +0 -21
  118. package/src/Editor/Editor.mock.tsx +0 -43
  119. package/src/FormattedTextEditor.spec.tsx +0 -10
  120. package/src/FormattedTextEditor.tsx +0 -3
  121. package/src/utils/converters/validNodeTypes.spec.ts +0 -33
  122. package/src/utils/converters/validNodeTypes.ts +0 -21
  123. /package/tests/{select.tsx → select.ts} +0 -0
@@ -1,4 +1,5 @@
1
1
  import { FORMATTED_TEXT_MODELS as FormattedTextModels } from '@squiz/dx-json-schema-lib';
2
+ import { RemirrorJSON } from '@remirror/core';
2
3
 
3
4
  export const mockSquizNodeJson: FormattedTextModels.v1.FormattedText = [
4
5
  {
@@ -73,3 +74,198 @@ export const mockSquizNodeTextJson: FormattedTextModels.v1.FormattedText = [
73
74
  type: 'text',
74
75
  },
75
76
  ];
77
+
78
+ type NodeExample = {
79
+ description: string;
80
+ remirrorNode: RemirrorJSON;
81
+ squizNode: FormattedTextModels.v1.FormattedText;
82
+ };
83
+
84
+ export const sharedNodeExamples: NodeExample[] = [
85
+ {
86
+ description: 'Asset link',
87
+ remirrorNode: {
88
+ type: 'text',
89
+ text: 'Hello',
90
+ marks: [
91
+ {
92
+ type: 'assetLink',
93
+ attrs: {
94
+ target: '_blank',
95
+ matrixAssetId: '123',
96
+ matrixDomain: 'https://my-matrix.squiz.net',
97
+ matrixIdentifier: 'matrix-api-identifier',
98
+ },
99
+ },
100
+ ],
101
+ },
102
+ squizNode: [
103
+ {
104
+ type: 'link-to-matrix-asset',
105
+ target: '_blank',
106
+ matrixAssetId: '123',
107
+ matrixDomain: 'https://my-matrix.squiz.net',
108
+ matrixIdentifier: 'matrix-api-identifier',
109
+ children: [{ type: 'text', value: 'Hello' }],
110
+ },
111
+ ],
112
+ },
113
+ {
114
+ description: 'Asset link with formatting applied inside of the link',
115
+ remirrorNode: {
116
+ type: 'text',
117
+ text: 'Hello',
118
+ marks: [
119
+ { type: 'bold' },
120
+ {
121
+ type: 'assetLink',
122
+ attrs: {
123
+ target: '_blank',
124
+ matrixAssetId: '123',
125
+ matrixDomain: 'https://my-matrix.squiz.net',
126
+ matrixIdentifier: 'matrix-api-identifier',
127
+ },
128
+ },
129
+ ],
130
+ },
131
+ squizNode: [
132
+ {
133
+ type: 'link-to-matrix-asset',
134
+ target: '_blank',
135
+ matrixAssetId: '123',
136
+ matrixDomain: 'https://my-matrix.squiz.net',
137
+ matrixIdentifier: 'matrix-api-identifier',
138
+ children: [
139
+ {
140
+ type: 'tag',
141
+ tag: 'span',
142
+ font: { bold: true },
143
+ children: [{ type: 'text', value: 'Hello' }],
144
+ },
145
+ ],
146
+ },
147
+ ],
148
+ },
149
+ {
150
+ description: 'Asset image',
151
+ remirrorNode: {
152
+ type: 'assetImage',
153
+ attrs: {
154
+ matrixAssetId: '123',
155
+ matrixDomain: 'https://my-matrix.squiz.net',
156
+ matrixIdentifier: 'matrix-api-identifier',
157
+ },
158
+ },
159
+ squizNode: [
160
+ {
161
+ type: 'matrix-image',
162
+ matrixAssetId: '123',
163
+ matrixDomain: 'https://my-matrix.squiz.net',
164
+ matrixIdentifier: 'matrix-api-identifier',
165
+ },
166
+ ],
167
+ },
168
+ ];
169
+
170
+ export const squizOnlyNodeExamples: NodeExample[] = [
171
+ {
172
+ description: 'Asset link with formatting applied inside, outside and with multiple levels of nesting',
173
+ squizNode: [
174
+ {
175
+ type: 'tag',
176
+ tag: 'span',
177
+ font: { bold: true },
178
+ children: [
179
+ {
180
+ type: 'link-to-matrix-asset',
181
+ target: '_blank',
182
+ matrixAssetId: '123',
183
+ matrixDomain: 'https://my-matrix.squiz.net',
184
+ matrixIdentifier: 'matrix-api-identifier',
185
+ children: [
186
+ {
187
+ type: 'tag',
188
+ tag: 'span',
189
+ font: { italics: true },
190
+ children: [
191
+ {
192
+ type: 'tag',
193
+ tag: 'span',
194
+ font: { underline: true },
195
+ children: [{ type: 'text', value: 'Hello' }],
196
+ },
197
+ ],
198
+ },
199
+ ],
200
+ },
201
+ ],
202
+ },
203
+ ],
204
+ remirrorNode: {
205
+ // reverse operation covered by "Asset link with formatting applied inside of the link".
206
+ type: 'text',
207
+ text: 'Hello',
208
+ marks: [
209
+ { type: 'underline' },
210
+ { type: 'italic' },
211
+ {
212
+ type: 'assetLink',
213
+ attrs: {
214
+ target: '_blank',
215
+ matrixAssetId: '123',
216
+ matrixDomain: 'https://my-matrix.squiz.net',
217
+ matrixIdentifier: 'matrix-api-identifier',
218
+ },
219
+ },
220
+ { type: 'bold' },
221
+ ],
222
+ },
223
+ },
224
+ {
225
+ description: 'Asset link with multiple levels of un-necessary nesting',
226
+ squizNode: [
227
+ {
228
+ type: 'tag',
229
+ tag: 'span',
230
+ children: [
231
+ {
232
+ type: 'link-to-matrix-asset',
233
+ target: '_blank',
234
+ matrixAssetId: '123',
235
+ matrixDomain: 'https://my-matrix.squiz.net',
236
+ matrixIdentifier: 'matrix-api-identifier',
237
+ children: [
238
+ {
239
+ type: 'tag',
240
+ tag: 'span',
241
+ children: [
242
+ {
243
+ type: 'tag',
244
+ tag: 'span',
245
+ children: [{ type: 'text', value: 'Hello' }],
246
+ },
247
+ ],
248
+ },
249
+ ],
250
+ },
251
+ ],
252
+ },
253
+ ],
254
+ remirrorNode: {
255
+ // reverse operation covered by "Asset link".
256
+ type: 'text',
257
+ text: 'Hello',
258
+ marks: [
259
+ {
260
+ type: 'assetLink',
261
+ attrs: {
262
+ target: '_blank',
263
+ matrixAssetId: '123',
264
+ matrixDomain: 'https://my-matrix.squiz.net',
265
+ matrixIdentifier: 'matrix-api-identifier',
266
+ },
267
+ },
268
+ ],
269
+ },
270
+ },
271
+ ];
@@ -2,6 +2,8 @@ import { FORMATTED_TEXT_MODELS as FormattedTextModels } from '@squiz/dx-json-sch
2
2
  import { remirrorNodeToSquizNode, resolveNodeTag } from './remirrorNodeToSquizNode';
3
3
  import { renderWithEditor } from '../../../../tests';
4
4
  import { RemirrorJSON } from '@remirror/core';
5
+ import { sharedNodeExamples } from '../mocks/squizNodeJson.mock';
6
+ import { ParagraphExtension, SupExtension } from 'remirror/extensions';
5
7
 
6
8
  type FormattedText = FormattedTextModels.v1.FormattedText;
7
9
 
@@ -375,8 +377,9 @@ describe('remirrorNodeToSquizNode', () => {
375
377
  });
376
378
 
377
379
  it('should handle invalid Remirror node provided', () => {
378
- const result = remirrorNodeToSquizNode(false as any);
379
- expect(result).toEqual([]);
380
+ expect(() => remirrorNodeToSquizNode(false as any)).toThrow(
381
+ 'Unable to convert from Remirror to Node data structure, unexpected node provided.',
382
+ );
380
383
  });
381
384
 
382
385
  it('should handle no content provided by Remirror', async () => {
@@ -389,6 +392,38 @@ describe('remirrorNodeToSquizNode', () => {
389
392
  const result = remirrorNodeToSquizNode(editor.doc);
390
393
  expect(result).toEqual([]);
391
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
+ });
392
427
  });
393
428
 
394
429
  describe('resolveNodeTag', () => {
@@ -425,7 +460,7 @@ describe('resolveNodeTag', () => {
425
460
  expect(resolveNodeTag(node)).toBe('ul');
426
461
  });
427
462
 
428
- it('should return null for a node with a toDOM method that returns undefined', () => {
463
+ it('should throw for a node with a toDOM method that returns undefined', () => {
429
464
  const node: any = {
430
465
  type: {
431
466
  spec: {
@@ -433,13 +468,13 @@ describe('resolveNodeTag', () => {
433
468
  },
434
469
  },
435
470
  };
436
- expect(resolveNodeTag(node)).toBeNull();
471
+ expect(() => resolveNodeTag(node)).toThrow('Unexpected Remirror node encountered, cannot resolve tag.');
437
472
  });
438
473
 
439
- it('should return null for a node without a toDOM method', () => {
474
+ it('should throw for a node without a toDOM method', () => {
440
475
  const node: any = {
441
476
  type: {},
442
477
  };
443
- expect(resolveNodeTag(node)).toBeNull();
478
+ expect(() => resolveNodeTag(node)).toThrow('Unexpected Remirror node encountered, cannot resolve tag.');
444
479
  });
445
480
  });
@@ -1,6 +1,8 @@
1
- import { ProsemirrorNode, Fragment as ProsemirrorFragment } from 'remirror';
1
+ import { ProsemirrorNode, Fragment as ProsemirrorFragment, Mark } from 'remirror';
2
+ import { Attrs } from 'prosemirror-model';
2
3
  import { FORMATTED_TEXT_MODELS as FormattedTextModels } from '@squiz/dx-json-schema-lib';
3
- import { validRemirrorNode } from '../validNodeTypes';
4
+ import { undefinedIfEmpty } from '../../undefinedIfEmpty';
5
+ import { NodeName } from '../../../Extensions/Extensions';
4
6
 
5
7
  type Fragment = ProsemirrorFragment & {
6
8
  content?: Fragment[];
@@ -9,16 +11,23 @@ type Fragment = ProsemirrorFragment & {
9
11
  type FormattingOptions = FormattedTextModels.v1.FormattingOptions;
10
12
  type FontOptions = FormattedTextModels.v1.FormattedNodeFontProperties;
11
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
+ }
12
22
 
13
- export const resolveNodeTag = (node: ProsemirrorNode): string | null => {
14
23
  if (node.type.spec?.toDOM) {
15
- const domNode = node.type.spec.toDOM(node) as any;
24
+ const domNode = node.type.spec.toDOM(node);
16
25
 
17
26
  if (domNode instanceof window.Node) {
18
27
  return domNode.nodeName.toLowerCase();
19
28
  }
20
29
 
21
- if (domNode?.dom instanceof window.Node) {
30
+ if (typeof domNode === 'object' && 'dom' in domNode && domNode.dom instanceof window.Node) {
22
31
  return domNode.dom.nodeName.toLowerCase();
23
32
  }
24
33
 
@@ -28,7 +37,7 @@ export const resolveNodeTag = (node: ProsemirrorNode): string | null => {
28
37
  }
29
38
  }
30
39
 
31
- return null;
40
+ throw new Error('Unexpected Remirror node encountered, cannot resolve tag.');
32
41
  };
33
42
 
34
43
  const resolveFormattingOptions = (node: ProsemirrorNode): FormattingOptions => {
@@ -41,7 +50,7 @@ const resolveFormattingOptions = (node: ProsemirrorNode): FormattingOptions => {
41
50
  return formattingOptions;
42
51
  };
43
52
 
44
- const resolveFontOptions = (node: ProsemirrorNode): FontOptions => {
53
+ const resolveFontOptions = (node: ProsemirrorNode): FormattedNodeFontProperties => {
45
54
  const fontOptions: FontOptions = {};
46
55
 
47
56
  node.marks.forEach((mark) => {
@@ -55,137 +64,149 @@ const resolveFontOptions = (node: ProsemirrorNode): FontOptions => {
55
64
  case 'underline':
56
65
  fontOptions.underline = true;
57
66
  break;
58
- default:
59
- // Currently unsupported mark type
60
- break;
61
67
  }
62
68
  });
63
69
 
64
70
  return fontOptions;
65
71
  };
66
72
 
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
- }
73
+ const transformAttributes = (attributes: Attrs): Record<string, string> => {
74
+ const transformed: Record<string, string> = {};
84
75
 
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]);
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]);
92
80
  }
93
81
  });
94
82
 
95
- return attributeOptions;
83
+ return transformed;
96
84
  };
97
85
 
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
- };
86
+ const transformFragment = (fragment: Fragment): FormattedText => {
87
+ const transformed: FormattedText = [];
119
88
 
120
- if (nodeType === 'doc') {
121
- return transformed.children;
122
- }
89
+ fragment.forEach((child) => transformed.push(transformNode(child)));
123
90
 
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
- }
91
+ return transformed;
92
+ };
137
93
 
138
- if (nodeTag) {
139
- transformed = {
140
- ...transformed,
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 = {
141
107
  type: 'tag',
142
- tag: nodeTag,
108
+ tag: resolveNodeTag(node),
109
+ children: node.type.name === NodeName.Text ? [transformedNode] : transformFragment(node.content),
110
+ attributes,
111
+ formattingOptions,
112
+ font,
143
113
  };
144
114
  }
145
115
 
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' };
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
+ };
152
123
  }
153
124
 
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
- };
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);
172
133
  }
173
- }
134
+ });
174
135
 
175
- // Remove empty formatting options from transformed object.
176
- if (transformed.formattingOptions && Object.keys(transformed.formattingOptions).length === 0) {
177
- delete transformed.formattingOptions;
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
+ };
178
169
  }
179
170
 
180
- // Remove empty font options from transformed object.
181
- if (transformed.font && Object.keys(transformed.font).length === 0) {
182
- delete transformed.font;
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
+ });
183
196
  }
184
197
 
185
- // Remove empty attributes options from transformed object.
186
- if (transformed.attributes && Object.keys(transformed.attributes).length === 0) {
187
- delete transformed.attributes;
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.');
188
209
  }
189
210
 
190
- return transformed;
211
+ return transformFragment(node.content);
191
212
  };