@designcrowd/library.brand-page 6.0.15-json-prereq-9 → 6.0.15

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 (94) hide show
  1. package/lib/src/{CreatedOnBrandCrowd-de-de-DPhfV5Hs.js → CreatedOnBrandCrowd-de-de-NigWLJ0X.js} +1 -1
  2. package/lib/src/{CreatedOnBrandCrowd-en-us-C9H7Jefm.js → CreatedOnBrandCrowd-en-us-XH20r8-U.js} +1 -1
  3. package/lib/src/{CreatedOnBrandCrowd-es-es-Dk9xGanz.js → CreatedOnBrandCrowd-es-es-C3gWj2WK.js} +1 -1
  4. package/lib/src/{CreatedOnBrandCrowd-fr-fr-DHdlhPfU.js → CreatedOnBrandCrowd-fr-ca-dIQB4uNK.js} +1 -1
  5. package/lib/src/{CreatedOnBrandCrowd-fr-ca-DHdlhPfU.js → CreatedOnBrandCrowd-fr-fr-dIQB4uNK.js} +1 -1
  6. package/lib/src/{CreatedOnBrandCrowd-pt-pt-BKbUxeTQ.js → CreatedOnBrandCrowd-pt-br-BT_T8yh8.js} +1 -1
  7. package/lib/src/{CreatedOnBrandCrowd-pt-br-BKbUxeTQ.js → CreatedOnBrandCrowd-pt-pt-BT_T8yh8.js} +1 -1
  8. package/lib/src/{CreatedOnDcom-de-de-DTkGzo6N.js → CreatedOnDcom-de-de-De__zZDv.js} +1 -1
  9. package/lib/src/{CreatedOnDcom-en-us-C2B1VW8J.js → CreatedOnDcom-en-us-hdp83p6i.js} +1 -1
  10. package/lib/src/{CreatedOnDcom-es-es-D5Bk5-L3.js → CreatedOnDcom-es-es-CqYvZBCH.js} +1 -1
  11. package/lib/src/{CreatedOnDcom-fr-ca-CFxhpG72.js → CreatedOnDcom-fr-ca-Dv7SmQJr.js} +1 -1
  12. package/lib/src/{CreatedOnDcom-fr-fr-C2--bwHl.js → CreatedOnDcom-fr-fr-CBpV3x-_.js} +1 -1
  13. package/lib/src/{CreatedOnDcom-pt-br-DB47OioZ.js → CreatedOnDcom-pt-br-BDBFpQ1l.js} +1 -1
  14. package/lib/src/{CreatedOnDcom-pt-pt-BruOq51P.js → CreatedOnDcom-pt-pt-CagUAArG.js} +1 -1
  15. package/lib/src/brand-page/models/index.d.ts +26 -34
  16. package/lib/src/brand-page/utils/migration.util.js +0 -11
  17. package/lib/src/brand-page/utils/text.util.d.ts +1 -1
  18. package/lib/src/brand-page/utils/text.util.js +0 -32
  19. package/lib/src/brand-page/utils/vcard.util.js +4 -4
  20. package/lib/src/brand-page/utils/view-model.util.d.ts +0 -1
  21. package/lib/src/brand-page/utils/view-model.util.js +20 -68
  22. package/lib/src/brand-page/view-models.d.ts +3 -3
  23. package/lib/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.js +100 -21
  24. package/lib/src/brand-page-maker/utils/index.d.ts +0 -2
  25. package/lib/src/brand-page-maker/utils/index.js +0 -2
  26. package/lib/src/brand-page-maker/utils/rich-text-editor.util.d.ts +5 -19
  27. package/lib/src/brand-page-maker/utils/rich-text-editor.util.js +5 -35
  28. package/lib/src/brand-page-maker/validators/field.validator.js +2 -10
  29. package/lib/src/css/library.brand-page-brandCrowd.min.css +1 -1
  30. package/lib/src/css/library.brand-page-brandPage.min.css +1 -1
  31. package/lib/src/css/library.brand-page-designCom.min.css +1 -1
  32. package/lib/src/css/library.brand-page-preview.min.css +1 -1
  33. package/lib/src/index.d.ts +0 -1
  34. package/lib/src/index.mjs +138 -143
  35. package/lib/src/{lib-DNUmDjp2.js → lib-01eP2NjY.js} +55009 -55274
  36. package/lib/src/lib.js +0 -2
  37. package/lib/src/metadata-builder/builders/update-content-text-builder.js +15 -20
  38. package/lib/src/metadata-builder/builders/update-content-text-style-builder.js +3 -5
  39. package/lib/src/metadata-builder/helpers/rte.helpers.d.ts +1 -1
  40. package/lib/src/metadata-builder/helpers/rte.helpers.js +3 -6
  41. package/lib/src/shared/content-settings/components/GenericTextInput.mixin.js +1 -14
  42. package/lib/src/shared/content-settings/components/content-settings-item/ContentSettingsItemField.mixin.js +3 -12
  43. package/lib/src/utils/minify-brand-page-metadata.js +1 -4
  44. package/package.json +1 -1
  45. package/src/brand-page/components/text-wrapper/TextWrapper.vue +6 -17
  46. package/src/brand-page/models/index.ts +26 -35
  47. package/src/brand-page/utils/__snapshots__/migration.util.test.ts.snap +3 -3
  48. package/src/brand-page/utils/migration.util.ts +0 -12
  49. package/src/brand-page/utils/style.util.test.ts +0 -51
  50. package/src/brand-page/utils/text.util.ts +4 -43
  51. package/src/brand-page/utils/vcard.util.ts +4 -6
  52. package/src/brand-page/utils/view-model.util.ts +26 -94
  53. package/src/brand-page/view-models.ts +2 -3
  54. package/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.ts +107 -23
  55. package/src/brand-page-maker/utils/index.ts +0 -2
  56. package/src/brand-page-maker/utils/rich-text-editor.util.test.ts +3 -4
  57. package/src/brand-page-maker/utils/rich-text-editor.util.ts +11 -50
  58. package/src/brand-page-maker/validators/field.validator.ts +2 -10
  59. package/src/lib.ts +0 -9
  60. package/src/metadata-builder/builders/update-content-text-builder.ts +18 -32
  61. package/src/metadata-builder/builders/update-content-text-style-builder.ts +4 -6
  62. package/src/metadata-builder/helpers/rte.helpers.ts +7 -11
  63. package/src/shared/content-settings/components/GenericTextInput.mixin.ts +1 -17
  64. package/src/shared/content-settings/components/content-settings-item/ContentSettingsItemField.mixin.ts +3 -16
  65. package/src/utils/minify-brand-page-metadata.ts +1 -4
  66. package/lib/src/brand-page/utils/migration/migration-19.d.ts +0 -2
  67. package/lib/src/brand-page/utils/migration/migration-19.js +0 -71
  68. package/lib/src/brand-page-maker/__fixtures__/complex-rte.fixture.d.ts +0 -22
  69. package/lib/src/brand-page-maker/__fixtures__/complex-rte.fixture.js +0 -22
  70. package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/FontFamily.d.ts +0 -14
  71. package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/FontFamily.js +0 -39
  72. package/lib/src/brand-page-maker/composables/rich-text-editor/getRteExtensions.d.ts +0 -28
  73. package/lib/src/brand-page-maker/composables/rich-text-editor/getRteExtensions.js +0 -66
  74. package/lib/src/brand-page-maker/utils/rte-render.util.d.ts +0 -33
  75. package/lib/src/brand-page-maker/utils/rte-render.util.js +0 -50
  76. package/lib/src/brand-page-maker/utils/rte-sanitize.util.d.ts +0 -6
  77. package/lib/src/brand-page-maker/utils/rte-sanitize.util.js +0 -158
  78. package/lib/src/brand-page-maker/utils/rte-serializer.d.ts +0 -5
  79. package/lib/src/brand-page-maker/utils/rte-serializer.js +0 -112
  80. package/src/brand-page/utils/migration/migration-19.test.ts +0 -72
  81. package/src/brand-page/utils/migration/migration-19.ts +0 -83
  82. package/src/brand-page/utils/text.util.test.ts +0 -47
  83. package/src/brand-page/utils/view-model.pageid-rte.test.ts +0 -104
  84. package/src/brand-page-maker/__fixtures__/complex-rte.fixture.ts +0 -23
  85. package/src/brand-page-maker/composables/rich-text-editor/extensions/FontFamily.test.ts +0 -52
  86. package/src/brand-page-maker/composables/rich-text-editor/extensions/FontFamily.ts +0 -42
  87. package/src/brand-page-maker/composables/rich-text-editor/getRteExtensions.ts +0 -113
  88. package/src/brand-page-maker/utils/__snapshots__/rte-render-parity.test.ts.snap +0 -200
  89. package/src/brand-page-maker/utils/rte-render-parity.test.ts +0 -156
  90. package/src/brand-page-maker/utils/rte-render.util.ts +0 -77
  91. package/src/brand-page-maker/utils/rte-sanitize-json.test.ts +0 -158
  92. package/src/brand-page-maker/utils/rte-sanitize.util.ts +0 -195
  93. package/src/brand-page-maker/utils/rte-serializer.ts +0 -172
  94. package/src/brand-page-maker/validators/field.validator.test.ts +0 -55
@@ -1,200 +0,0 @@
1
- // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
-
3
- exports[`BP-5787 RTE render parity (golden master) > T1 — editor renders the complex fixture to the frozen HTML baseline > editor-normalized-html 1`] = `"<h2 style="text-align: center;"><span style="color: var(--logo-text-color);">Themed heading</span></h2><p><strong>bold</strong> <em>italic</em> <u>underline</u> <s>strike</s> <span style="font-family: Montserrat; font-size: 1.5em; letter-spacing: 0.05em; line-height: 1.4;">styled span</span> <mark data-color="#ffe08a" style="background-color: rgb(255, 224, 138); color: inherit;">highlight</mark> <span style="color: rgb(255, 0, 0);">literal hex</span> <a href="/about" target="_self">internal link</a> <a href="https://example.com" target="_blank" rel="noopener noreferrer nofollow">external link</a></p><p style="text-align: right;">second paragraph, right-aligned</p>"`;
4
-
5
- exports[`BP-5787 RTE render parity (golden master) > T1 — render-from-JSON (DOM-less serialiser) produces equivalent HTML > serializer-html 1`] = `"<h2 style="text-align: center"><span style="color: var(--logo-text-color)">Themed heading</span></h2><p><strong>bold</strong> <em>italic</em> <u>underline</u> <s>strike</s> <span style="font-family: Montserrat; font-size: 1.5em; letter-spacing: 0.05em; line-height: 1.4">styled span</span> <mark data-color="#ffe08a" style="background-color: #ffe08a; color: inherit;">highlight</mark> <span style="color: rgb(255, 0, 0)">literal hex</span> <a href="/about" target="_self">internal link</a> <a href="https://example.com" target="_blank" rel="noopener noreferrer nofollow">external link</a></p><p style="text-align: right">second paragraph, right-aligned</p>"`;
6
-
7
- exports[`BP-5787 RTE render parity (golden master) > T6 — freezes the JSON shape that becomes the stored form > editor-json 1`] = `
8
- {
9
- "content": [
10
- {
11
- "attrs": {
12
- "level": 2,
13
- "textAlign": "center",
14
- },
15
- "content": [
16
- {
17
- "marks": [
18
- {
19
- "attrs": {
20
- "color": "var(--logo-text-color)",
21
- "fontFamily": "",
22
- "fontSize": "",
23
- "letterSpacing": "",
24
- "lineHeight": "",
25
- },
26
- "type": "textStyle",
27
- },
28
- ],
29
- "text": "Themed heading",
30
- "type": "text",
31
- },
32
- ],
33
- "type": "heading",
34
- },
35
- {
36
- "attrs": {
37
- "textAlign": null,
38
- },
39
- "content": [
40
- {
41
- "marks": [
42
- {
43
- "type": "bold",
44
- },
45
- ],
46
- "text": "bold",
47
- "type": "text",
48
- },
49
- {
50
- "text": " ",
51
- "type": "text",
52
- },
53
- {
54
- "marks": [
55
- {
56
- "type": "italic",
57
- },
58
- ],
59
- "text": "italic",
60
- "type": "text",
61
- },
62
- {
63
- "text": " ",
64
- "type": "text",
65
- },
66
- {
67
- "marks": [
68
- {
69
- "type": "underline",
70
- },
71
- ],
72
- "text": "underline",
73
- "type": "text",
74
- },
75
- {
76
- "text": " ",
77
- "type": "text",
78
- },
79
- {
80
- "marks": [
81
- {
82
- "type": "strike",
83
- },
84
- ],
85
- "text": "strike",
86
- "type": "text",
87
- },
88
- {
89
- "text": " ",
90
- "type": "text",
91
- },
92
- {
93
- "marks": [
94
- {
95
- "attrs": {
96
- "color": "",
97
- "fontFamily": "Montserrat",
98
- "fontSize": "1.5em",
99
- "letterSpacing": "0.05em",
100
- "lineHeight": "1.4",
101
- },
102
- "type": "textStyle",
103
- },
104
- ],
105
- "text": "styled span",
106
- "type": "text",
107
- },
108
- {
109
- "text": " ",
110
- "type": "text",
111
- },
112
- {
113
- "marks": [
114
- {
115
- "attrs": {
116
- "color": "#ffe08a",
117
- },
118
- "type": "highlight",
119
- },
120
- ],
121
- "text": "highlight",
122
- "type": "text",
123
- },
124
- {
125
- "text": " ",
126
- "type": "text",
127
- },
128
- {
129
- "marks": [
130
- {
131
- "attrs": {
132
- "color": "rgb(255, 0, 0)",
133
- "fontFamily": "",
134
- "fontSize": "",
135
- "letterSpacing": "",
136
- "lineHeight": "",
137
- },
138
- "type": "textStyle",
139
- },
140
- ],
141
- "text": "literal hex",
142
- "type": "text",
143
- },
144
- {
145
- "text": " ",
146
- "type": "text",
147
- },
148
- {
149
- "marks": [
150
- {
151
- "attrs": {
152
- "class": undefined,
153
- "href": "/about",
154
- "rel": undefined,
155
- "target": "_self",
156
- },
157
- "type": "link",
158
- },
159
- ],
160
- "text": "internal link",
161
- "type": "text",
162
- },
163
- {
164
- "text": " ",
165
- "type": "text",
166
- },
167
- {
168
- "marks": [
169
- {
170
- "attrs": {
171
- "class": undefined,
172
- "href": "https://example.com",
173
- "rel": "noopener noreferrer nofollow",
174
- "target": "_blank",
175
- },
176
- "type": "link",
177
- },
178
- ],
179
- "text": "external link",
180
- "type": "text",
181
- },
182
- ],
183
- "type": "paragraph",
184
- },
185
- {
186
- "attrs": {
187
- "textAlign": "right",
188
- },
189
- "content": [
190
- {
191
- "text": "second paragraph, right-aligned",
192
- "type": "text",
193
- },
194
- ],
195
- "type": "paragraph",
196
- },
197
- ],
198
- "type": "doc",
199
- }
200
- `;
@@ -1,156 +0,0 @@
1
- import { describe, it, expect, afterEach } from 'vitest';
2
- import type { Editor } from '@tiptap/vue-3';
3
- import { useExternalRichTextEditor } from '../composables/rich-text-editor/useRichTextEditor';
4
- import {
5
- renderRteToHtml,
6
- toRteJson,
7
- getRteJsonPlainText,
8
- } from './rte-render.util';
9
- import {
10
- COMPLEX_RTE_HTML,
11
- THEME_TOKEN_REF,
12
- } from '../__fixtures__/complex-rte.fixture';
13
-
14
- /**
15
- * BP-5787 — golden-master render-parity baseline.
16
- *
17
- * Captured on the CURRENT (HTML) pipeline. These committed snapshots are the
18
- * frozen reference for the HTML→JSON migration:
19
- *
20
- * - `editor-normalized-html` is what the editor renders the fixture to today.
21
- * After the migration, `htmlFromJson(getJSON())` MUST equal this snapshot —
22
- * i.e. the JSON pipeline reproduces identical rendered output (no drift).
23
- *
24
- * - `editor-json` freezes the JSON shape that BECOMES the stored form. It is
25
- * expected to change exactly once, intentionally, when storage flips to JSON
26
- * (see "expected vs forbidden drift" in docs/plans/BP-5787-...html).
27
- *
28
- * The headless editor uses the SAME extension set as the live editor
29
- * (useExternalRichTextEditor), so it is a faithful stand-in for the renderer.
30
- */
31
- describe('BP-5787 RTE render parity (golden master)', () => {
32
- let editor: Editor;
33
-
34
- const makeEditor = (): Editor => {
35
- editor = useExternalRichTextEditor().externalEditor;
36
-
37
- return editor;
38
- };
39
-
40
- afterEach(() => {
41
- editor?.destroy();
42
- });
43
-
44
- it('T1 — editor renders the complex fixture to the frozen HTML baseline', () => {
45
- const ed = makeEditor();
46
-
47
- ed.commands.setContent(COMPLEX_RTE_HTML);
48
-
49
- // The golden master. The JSON pipeline must reproduce this exactly.
50
- expect(ed.getHTML()).toMatchSnapshot('editor-normalized-html');
51
- });
52
-
53
- it('T1 — round-trip is idempotent (html → json → html === json → html)', () => {
54
- const ed = makeEditor();
55
-
56
- ed.commands.setContent(COMPLEX_RTE_HTML);
57
- const pass1 = ed.getHTML();
58
- const json = ed.getJSON();
59
-
60
- // Re-seed from the JSON the migration will store, render again.
61
- ed.commands.setContent(json);
62
- const pass2 = ed.getHTML();
63
-
64
- expect(pass2).toEqual(pass1);
65
- });
66
-
67
- it('T6 — freezes the JSON shape that becomes the stored form', () => {
68
- const ed = makeEditor();
69
-
70
- ed.commands.setContent(COMPLEX_RTE_HTML);
71
-
72
- expect(ed.getJSON()).toMatchSnapshot('editor-json');
73
- });
74
-
75
- it('T2 — theme token ref survives the round-trip (NOT flattened to rgb)', () => {
76
- const ed = makeEditor();
77
-
78
- ed.commands.setContent(COMPLEX_RTE_HTML);
79
-
80
- // Today the editor preserves var(--…) in marks; the migration must keep it.
81
- expect(ed.getHTML()).toContain(THEME_TOKEN_REF);
82
- expect(JSON.stringify(ed.getJSON())).toContain(THEME_TOKEN_REF);
83
- });
84
-
85
- it('T1 — render-from-JSON (DOM-less serialiser) produces equivalent HTML', () => {
86
- // renderRteToHtml now serialises JSON → HTML with a pure function (no
87
- // editor/DOM, so it works on the server). Output is semantically identical
88
- // to the editor's — the Playwright visual golden-master is the authority on
89
- // pixel parity; here we freeze the serialiser output and assert structure.
90
- const viaJson = renderRteToHtml(toRteJson(COMPLEX_RTE_HTML));
91
-
92
- expect(viaJson).toMatchSnapshot('serializer-html');
93
-
94
- // marks/nodes preserved
95
- expect(viaJson).toContain('<strong>bold</strong>');
96
- expect(viaJson).toContain('<em>italic</em>');
97
- expect(viaJson).toContain('<u>underline</u>');
98
- expect(viaJson).toContain('<s>strike</s>');
99
- expect(viaJson).toContain('font-family: Montserrat');
100
- expect(viaJson).toContain('font-size: 1.5em');
101
- // theme token ref preserved (the crux of BP-5787)
102
- expect(viaJson).toContain('color: var(--logo-text-color)');
103
- // links + alignment
104
- expect(viaJson).toContain('href="/about"');
105
- expect(viaJson).toContain('href="https://example.com"');
106
- expect(viaJson).toContain('text-align: center');
107
- expect(viaJson).toContain('text-align: right');
108
- });
109
-
110
- it('T4 — legacy HTML strings render unchanged (back-compat passthrough)', () => {
111
- expect(renderRteToHtml(COMPLEX_RTE_HTML)).toEqual(COMPLEX_RTE_HTML);
112
- });
113
-
114
- it('T8 — getRteJsonPlainText extracts visible text only (for fraud/translate)', () => {
115
- const json = toRteJson(COMPLEX_RTE_HTML)!;
116
- const text = getRteJsonPlainText(json);
117
-
118
- expect(text).toContain('Themed heading');
119
- expect(text).toContain('bold');
120
- expect(text).toContain('external link');
121
- // no markup or token refs leak into the extracted text
122
- expect(text).not.toContain('<');
123
- expect(text).not.toContain('var(--');
124
- expect(text).not.toContain('font-size');
125
- });
126
-
127
- it('T1 — preserves every mark/node in the fixture', () => {
128
- const ed = makeEditor();
129
-
130
- ed.commands.setContent(COMPLEX_RTE_HTML);
131
- const html = ed.getHTML();
132
-
133
- // bold / italic / underline / strike
134
- expect(html).toContain('<strong>');
135
- expect(html).toContain('<em>');
136
- expect(html).toContain('<u>');
137
- expect(html).toContain('<s>');
138
- // textStyle attrs
139
- expect(html).toContain('font-family: Montserrat');
140
- expect(html).toContain('font-size: 1.5em');
141
- expect(html).toContain('letter-spacing: 0.05em');
142
- expect(html).toContain('line-height: 1.4');
143
- // highlight: data-color keeps the hex; the rendered style is normalised to rgb()
144
- expect(html).toContain('data-color="#ffe08a"');
145
- expect(html).toContain('background-color: rgb(255, 224, 138)');
146
- // literal hex colour is normalised to rgb() by the editor...
147
- expect(html).toContain('color: rgb(255, 0, 0)');
148
- // ...but the THEME TOKEN ref is preserved verbatim (the crux of BP-5787)
149
- expect(html).toContain('color: var(--logo-text-color)');
150
- expect(html).toContain('text-align: center');
151
- expect(html).toContain('text-align: right');
152
- // links
153
- expect(html).toContain('href="/about"');
154
- expect(html).toContain('href="https://example.com"');
155
- });
156
- });
@@ -1,77 +0,0 @@
1
- import { Editor, type JSONContent } from '@tiptap/vue-3';
2
- import { getRteExtensions } from '../composables/rich-text-editor/getRteExtensions';
3
- import { isTiptapDocument, getRteJsonPlainText } from './rich-text-editor.util';
4
- import { serializeRteJsonToHtml } from './rte-serializer';
5
-
6
- export { isTiptapDocument, getRteJsonPlainText };
7
-
8
- /**
9
- * BP-5787 — the boundary plumbing for storing RTE as Tiptap JSON.
10
- *
11
- * An RTE field is either a legacy inline-HTML string (pre-migration, or a
12
- * not-yet-migrated published site) or a Tiptap JSON document (post-migration).
13
- * These helpers let every consumer treat the two uniformly:
14
- *
15
- * - isTiptapDocument — the canonical discriminator (object with type 'doc').
16
- * - renderRteToHtml — render either form to final HTML for display.
17
- * - toRteJson — normalise either form to JSON for storage.
18
- *
19
- * - renderRteToHtml uses a DOM-less serialiser (works on the server).
20
- * - toRteJson uses a headless editor to PARSE HTML → JSON, so it only runs
21
- * where a DOM exists (browser, or jsdom in tests); the server never parses
22
- * HTML (migration-18 is DOM-guarded, translation/fraud walk JSON directly).
23
- */
24
- export type RichTextValue = string | JSONContent;
25
-
26
- /**
27
- * Run a function with a short-lived headless editor (created then destroyed).
28
- * Used only by toRteJson (HTML → JSON parsing), which needs a real DOM. Not
29
- * used by the render path, which is DOM-less.
30
- */
31
- const withHeadlessEditor = <T>(fn: (editor: Editor) => T): T => {
32
- const editor = new Editor({
33
- editable: false,
34
- extensions: getRteExtensions({ includeLinkRel: true }),
35
- });
36
-
37
- try {
38
- return fn(editor);
39
- } finally {
40
- editor.destroy();
41
- }
42
- };
43
-
44
- /**
45
- * Render an RTE value to final HTML. Legacy HTML strings pass through untouched
46
- * (they are already HTML); Tiptap JSON is serialised by a DOM-less pure
47
- * function (rte-serializer) — no editor, no DOM — so it works on the server
48
- * (SSR/publish), not just the browser, and is cheap enough to run per render.
49
- */
50
- export const renderRteToHtml = (
51
- value: RichTextValue | null | undefined,
52
- ): string => {
53
- if (value == null || value === '') return '';
54
-
55
- if (typeof value === 'string') return value;
56
-
57
- return serializeRteJsonToHtml(value);
58
- };
59
-
60
- /**
61
- * Normalise an RTE value to a Tiptap JSON document. HTML strings are parsed
62
- * via the headless editor; documents pass through unchanged. Returns
63
- * undefined for empty/blank input so callers can omit empty fields.
64
- */
65
- export const toRteJson = (
66
- value: RichTextValue | null | undefined,
67
- ): JSONContent | undefined => {
68
- if (value == null || value === '') return undefined;
69
-
70
- if (isTiptapDocument(value)) return value;
71
-
72
- return withHeadlessEditor((editor) => {
73
- editor.commands.setContent(value);
74
-
75
- return editor.getJSON();
76
- });
77
- };
@@ -1,158 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { sanitizeRteJson } from './rte-sanitize.util';
3
- import { toRteJson } from './rte-render.util';
4
- import { COMPLEX_RTE_HTML } from '../__fixtures__/complex-rte.fixture';
5
-
6
- /**
7
- * BP-5787 — native JSON sanitiser (T5). Operates on the Tiptap tree directly
8
- * (no editor, no HTML, no DOMPurify). Mirrors the HTML allowlist's guarantees.
9
- */
10
- const doc = (content: unknown[]): unknown => ({ type: 'doc', content });
11
- const para = (content: unknown[]) => ({ type: 'paragraph', content });
12
- const text = (t: string, marks?: unknown[]) => ({
13
- type: 'text',
14
- text: t,
15
- marks,
16
- });
17
- const json = (v: unknown) => JSON.stringify(v);
18
-
19
- describe('sanitizeRteJson (native)', () => {
20
- it('drops unknown node types but keeps their valid text siblings', () => {
21
- const out = sanitizeRteJson(
22
- doc([
23
- para([text('keep me')]),
24
- { type: 'image', attrs: { src: 'x', onerror: 'alert(1)' } },
25
- { type: 'script', content: [text('alert(1)')] },
26
- ]),
27
- );
28
-
29
- expect(json(out)).toContain('keep me');
30
- expect(json(out)).not.toContain('image');
31
- expect(json(out)).not.toContain('script');
32
- expect(json(out)).not.toContain('alert(1)');
33
- });
34
-
35
- it('drops unknown mark types, keeps allow-listed ones', () => {
36
- const out = sanitizeRteJson(
37
- doc([
38
- para([
39
- text('bold', [{ type: 'bold' }]),
40
- text('evil', [{ type: 'onclick', attrs: { js: 'alert(1)' } }]),
41
- ]),
42
- ]),
43
- );
44
-
45
- expect(json(out)).toContain('"type":"bold"');
46
- expect(json(out)).not.toContain('onclick');
47
- expect(json(out)).not.toContain('alert(1)');
48
- });
49
-
50
- it('strips javascript: / data: hrefs from link marks (drops the mark)', () => {
51
- const out = sanitizeRteJson(
52
- doc([
53
- para([
54
- // eslint-disable-next-line no-script-url
55
- text('click', [
56
- { type: 'link', attrs: { href: 'javascript:alert(1)' } },
57
- ]),
58
- ]),
59
- ]),
60
- );
61
-
62
- expect(json(out)).toContain('click'); // text kept
63
- expect(json(out)).not.toContain('javascript:');
64
- expect(json(out)).not.toContain('"type":"link"');
65
- });
66
-
67
- it('keeps safe links', () => {
68
- const out = sanitizeRteJson(
69
- doc([
70
- para([
71
- text('site', [
72
- {
73
- type: 'link',
74
- attrs: { href: 'https://example.com', target: '_blank' },
75
- },
76
- ]),
77
- ]),
78
- ]),
79
- );
80
-
81
- expect(json(out)).toContain('https://example.com');
82
- });
83
-
84
- it('keeps safe textStyle attrs incl. theme token, drops injection values', () => {
85
- const out = sanitizeRteJson(
86
- doc([
87
- para([
88
- text('ok', [
89
- {
90
- type: 'textStyle',
91
- attrs: {
92
- color: 'var(--logo-text-color)',
93
- fontFamily: 'Open Sans',
94
- fontSize: '1.5em',
95
- },
96
- },
97
- ]),
98
- text('bad', [
99
- {
100
- type: 'textStyle',
101
- attrs: {
102
- color: 'red; position: fixed; top: 0',
103
- fontSize: '1em" onload="x',
104
- },
105
- },
106
- ]),
107
- ]),
108
- ]),
109
- );
110
-
111
- const s = json(out);
112
-
113
- expect(s).toContain('var(--logo-text-color)');
114
- expect(s).toContain('Open Sans');
115
- expect(s).toContain('1.5em');
116
- // injection values dropped
117
- expect(s).not.toContain('position: fixed');
118
- expect(s).not.toContain('onload');
119
- });
120
-
121
- it('clamps heading level and keeps valid textAlign', () => {
122
- const out: any = sanitizeRteJson(
123
- doc([
124
- {
125
- type: 'heading',
126
- attrs: { level: 99, textAlign: 'center' },
127
- content: [text('H')],
128
- },
129
- {
130
- type: 'paragraph',
131
- attrs: { textAlign: 'evil' },
132
- content: [text('P')],
133
- },
134
- ]),
135
- );
136
-
137
- expect(out.content[0].attrs.level).toBe(1);
138
- expect(out.content[0].attrs.textAlign).toBe('center');
139
- expect(out.content[1].attrs?.textAlign).toBeUndefined();
140
- });
141
-
142
- it('passes the clean complex fixture through unchanged (idempotent)', () => {
143
- const clean = toRteJson(COMPLEX_RTE_HTML);
144
- const once = sanitizeRteJson(clean);
145
- const twice = sanitizeRteJson(once);
146
-
147
- expect(twice).toEqual(once);
148
- // legitimate content preserved
149
- expect(json(once)).toContain('var(--logo-text-color)');
150
- expect(json(once)).toContain('Montserrat');
151
- expect(json(once)).toContain('https://example.com');
152
- });
153
-
154
- it('returns an empty doc for non-document input', () => {
155
- expect(sanitizeRteJson(null)).toEqual({ type: 'doc', content: [] });
156
- expect(sanitizeRteJson('<p>x</p>')).toEqual({ type: 'doc', content: [] });
157
- });
158
- });