@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.
- package/lib/src/{CreatedOnBrandCrowd-de-de-DPhfV5Hs.js → CreatedOnBrandCrowd-de-de-NigWLJ0X.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-en-us-C9H7Jefm.js → CreatedOnBrandCrowd-en-us-XH20r8-U.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-es-es-Dk9xGanz.js → CreatedOnBrandCrowd-es-es-C3gWj2WK.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-fr-fr-DHdlhPfU.js → CreatedOnBrandCrowd-fr-ca-dIQB4uNK.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-fr-ca-DHdlhPfU.js → CreatedOnBrandCrowd-fr-fr-dIQB4uNK.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-pt-pt-BKbUxeTQ.js → CreatedOnBrandCrowd-pt-br-BT_T8yh8.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-pt-br-BKbUxeTQ.js → CreatedOnBrandCrowd-pt-pt-BT_T8yh8.js} +1 -1
- package/lib/src/{CreatedOnDcom-de-de-DTkGzo6N.js → CreatedOnDcom-de-de-De__zZDv.js} +1 -1
- package/lib/src/{CreatedOnDcom-en-us-C2B1VW8J.js → CreatedOnDcom-en-us-hdp83p6i.js} +1 -1
- package/lib/src/{CreatedOnDcom-es-es-D5Bk5-L3.js → CreatedOnDcom-es-es-CqYvZBCH.js} +1 -1
- package/lib/src/{CreatedOnDcom-fr-ca-CFxhpG72.js → CreatedOnDcom-fr-ca-Dv7SmQJr.js} +1 -1
- package/lib/src/{CreatedOnDcom-fr-fr-C2--bwHl.js → CreatedOnDcom-fr-fr-CBpV3x-_.js} +1 -1
- package/lib/src/{CreatedOnDcom-pt-br-DB47OioZ.js → CreatedOnDcom-pt-br-BDBFpQ1l.js} +1 -1
- package/lib/src/{CreatedOnDcom-pt-pt-BruOq51P.js → CreatedOnDcom-pt-pt-CagUAArG.js} +1 -1
- package/lib/src/brand-page/models/index.d.ts +26 -34
- package/lib/src/brand-page/utils/migration.util.js +0 -11
- package/lib/src/brand-page/utils/text.util.d.ts +1 -1
- package/lib/src/brand-page/utils/text.util.js +0 -32
- package/lib/src/brand-page/utils/vcard.util.js +4 -4
- package/lib/src/brand-page/utils/view-model.util.d.ts +0 -1
- package/lib/src/brand-page/utils/view-model.util.js +20 -68
- package/lib/src/brand-page/view-models.d.ts +3 -3
- package/lib/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.js +100 -21
- package/lib/src/brand-page-maker/utils/index.d.ts +0 -2
- package/lib/src/brand-page-maker/utils/index.js +0 -2
- package/lib/src/brand-page-maker/utils/rich-text-editor.util.d.ts +5 -19
- package/lib/src/brand-page-maker/utils/rich-text-editor.util.js +5 -35
- package/lib/src/brand-page-maker/validators/field.validator.js +2 -10
- package/lib/src/css/library.brand-page-brandCrowd.min.css +1 -1
- package/lib/src/css/library.brand-page-brandPage.min.css +1 -1
- package/lib/src/css/library.brand-page-designCom.min.css +1 -1
- package/lib/src/css/library.brand-page-preview.min.css +1 -1
- package/lib/src/index.d.ts +0 -1
- package/lib/src/index.mjs +138 -143
- package/lib/src/{lib-DNUmDjp2.js → lib-01eP2NjY.js} +55009 -55274
- package/lib/src/lib.js +0 -2
- package/lib/src/metadata-builder/builders/update-content-text-builder.js +15 -20
- package/lib/src/metadata-builder/builders/update-content-text-style-builder.js +3 -5
- package/lib/src/metadata-builder/helpers/rte.helpers.d.ts +1 -1
- package/lib/src/metadata-builder/helpers/rte.helpers.js +3 -6
- package/lib/src/shared/content-settings/components/GenericTextInput.mixin.js +1 -14
- package/lib/src/shared/content-settings/components/content-settings-item/ContentSettingsItemField.mixin.js +3 -12
- package/lib/src/utils/minify-brand-page-metadata.js +1 -4
- package/package.json +1 -1
- package/src/brand-page/components/text-wrapper/TextWrapper.vue +6 -17
- package/src/brand-page/models/index.ts +26 -35
- package/src/brand-page/utils/__snapshots__/migration.util.test.ts.snap +3 -3
- package/src/brand-page/utils/migration.util.ts +0 -12
- package/src/brand-page/utils/style.util.test.ts +0 -51
- package/src/brand-page/utils/text.util.ts +4 -43
- package/src/brand-page/utils/vcard.util.ts +4 -6
- package/src/brand-page/utils/view-model.util.ts +26 -94
- package/src/brand-page/view-models.ts +2 -3
- package/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.ts +107 -23
- package/src/brand-page-maker/utils/index.ts +0 -2
- package/src/brand-page-maker/utils/rich-text-editor.util.test.ts +3 -4
- package/src/brand-page-maker/utils/rich-text-editor.util.ts +11 -50
- package/src/brand-page-maker/validators/field.validator.ts +2 -10
- package/src/lib.ts +0 -9
- package/src/metadata-builder/builders/update-content-text-builder.ts +18 -32
- package/src/metadata-builder/builders/update-content-text-style-builder.ts +4 -6
- package/src/metadata-builder/helpers/rte.helpers.ts +7 -11
- package/src/shared/content-settings/components/GenericTextInput.mixin.ts +1 -17
- package/src/shared/content-settings/components/content-settings-item/ContentSettingsItemField.mixin.ts +3 -16
- package/src/utils/minify-brand-page-metadata.ts +1 -4
- package/lib/src/brand-page/utils/migration/migration-19.d.ts +0 -2
- package/lib/src/brand-page/utils/migration/migration-19.js +0 -71
- package/lib/src/brand-page-maker/__fixtures__/complex-rte.fixture.d.ts +0 -22
- package/lib/src/brand-page-maker/__fixtures__/complex-rte.fixture.js +0 -22
- package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/FontFamily.d.ts +0 -14
- package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/FontFamily.js +0 -39
- package/lib/src/brand-page-maker/composables/rich-text-editor/getRteExtensions.d.ts +0 -28
- package/lib/src/brand-page-maker/composables/rich-text-editor/getRteExtensions.js +0 -66
- package/lib/src/brand-page-maker/utils/rte-render.util.d.ts +0 -33
- package/lib/src/brand-page-maker/utils/rte-render.util.js +0 -50
- package/lib/src/brand-page-maker/utils/rte-sanitize.util.d.ts +0 -6
- package/lib/src/brand-page-maker/utils/rte-sanitize.util.js +0 -158
- package/lib/src/brand-page-maker/utils/rte-serializer.d.ts +0 -5
- package/lib/src/brand-page-maker/utils/rte-serializer.js +0 -112
- package/src/brand-page/utils/migration/migration-19.test.ts +0 -72
- package/src/brand-page/utils/migration/migration-19.ts +0 -83
- package/src/brand-page/utils/text.util.test.ts +0 -47
- package/src/brand-page/utils/view-model.pageid-rte.test.ts +0 -104
- package/src/brand-page-maker/__fixtures__/complex-rte.fixture.ts +0 -23
- package/src/brand-page-maker/composables/rich-text-editor/extensions/FontFamily.test.ts +0 -52
- package/src/brand-page-maker/composables/rich-text-editor/extensions/FontFamily.ts +0 -42
- package/src/brand-page-maker/composables/rich-text-editor/getRteExtensions.ts +0 -113
- package/src/brand-page-maker/utils/__snapshots__/rte-render-parity.test.ts.snap +0 -200
- package/src/brand-page-maker/utils/rte-render-parity.test.ts +0 -156
- package/src/brand-page-maker/utils/rte-render.util.ts +0 -77
- package/src/brand-page-maker/utils/rte-sanitize-json.test.ts +0 -158
- package/src/brand-page-maker/utils/rte-sanitize.util.ts +0 -195
- package/src/brand-page-maker/utils/rte-serializer.ts +0 -172
- 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
|
-
});
|