@designcrowd/library.brand-page 6.0.6-json-prereq-7 → 6.0.6-undo-2

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 (134) hide show
  1. package/lib/src/{CreatedOnBrandCrowd-de-de-SPVLPi8n.js → CreatedOnBrandCrowd-de-de-BSFTp6yl.js} +1 -1
  2. package/lib/src/{CreatedOnBrandCrowd-en-us-CvtrCPHd.js → CreatedOnBrandCrowd-en-us-rPQrOY1L.js} +1 -1
  3. package/lib/src/{CreatedOnBrandCrowd-es-es-BSsac7jB.js → CreatedOnBrandCrowd-es-es-awd8-QFa.js} +1 -1
  4. package/lib/src/{CreatedOnBrandCrowd-fr-fr-BauENV2a.js → CreatedOnBrandCrowd-fr-ca-CwJ4J7Ow.js} +1 -1
  5. package/lib/src/{CreatedOnBrandCrowd-fr-ca-BauENV2a.js → CreatedOnBrandCrowd-fr-fr-CwJ4J7Ow.js} +1 -1
  6. package/lib/src/{CreatedOnBrandCrowd-pt-pt-Be0hawmr.js → CreatedOnBrandCrowd-pt-br-BXYfTLlD.js} +1 -1
  7. package/lib/src/{CreatedOnBrandCrowd-pt-br-Be0hawmr.js → CreatedOnBrandCrowd-pt-pt-BXYfTLlD.js} +1 -1
  8. package/lib/src/{CreatedOnDcom-de-de-CTjE2Tmn.js → CreatedOnDcom-de-de-kX6u1lNK.js} +1 -1
  9. package/lib/src/{CreatedOnDcom-en-us-DnZRp5Pg.js → CreatedOnDcom-en-us-Bkq5sgFs.js} +1 -1
  10. package/lib/src/{CreatedOnDcom-es-es-J-9qvjy8.js → CreatedOnDcom-es-es-zCAC9ujj.js} +1 -1
  11. package/lib/src/{CreatedOnDcom-fr-ca-BDNHqm4G.js → CreatedOnDcom-fr-ca-BYz6Wzg5.js} +1 -1
  12. package/lib/src/{CreatedOnDcom-fr-fr-B_2-ceuO.js → CreatedOnDcom-fr-fr-CN-wAaSH.js} +1 -1
  13. package/lib/src/{CreatedOnDcom-pt-br-76lJVrAY.js → CreatedOnDcom-pt-br-BrK4Ngg-.js} +1 -1
  14. package/lib/src/{CreatedOnDcom-pt-pt-TQp3CNcu.js → CreatedOnDcom-pt-pt-AfI_g7xJ.js} +1 -1
  15. package/lib/src/brand-page/models/index.d.ts +26 -34
  16. package/lib/src/brand-page/utils/logo.util.d.ts +14 -0
  17. package/lib/src/brand-page/utils/logo.util.js +19 -0
  18. package/lib/src/brand-page/utils/migration.util.js +0 -11
  19. package/lib/src/brand-page/utils/text.util.d.ts +1 -1
  20. package/lib/src/brand-page/utils/text.util.js +0 -32
  21. package/lib/src/brand-page/utils/vcard.util.js +4 -4
  22. package/lib/src/brand-page/utils/view-model.util.d.ts +0 -1
  23. package/lib/src/brand-page/utils/view-model.util.js +20 -68
  24. package/lib/src/brand-page/view-models.d.ts +3 -3
  25. package/lib/src/brand-page-maker/components/action-bar/ActionBar.mixin.d.ts +1 -1
  26. package/lib/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.mixin.d.ts +1 -1
  27. package/lib/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.mixin.js +1 -1
  28. package/lib/src/brand-page-maker/components/brand-page-maker/BrandPageMakerTemplateSettings.mixin.d.ts +1 -1
  29. package/lib/src/brand-page-maker/components/brandpage-maker-page/BrandPageMakerPage.mixin.d.ts +1 -1
  30. package/lib/src/brand-page-maker/components/brandpage-settings/ReplaceImageSettingsForBrandPageSettings.mixin.d.ts +1 -1
  31. package/lib/src/brand-page-maker/components/seo-settings/SeoPageSettings.mixin.d.ts +1 -1
  32. package/lib/src/brand-page-maker/components/seo-settings/UrlSettings.mixin.d.ts +1 -1
  33. package/lib/src/brand-page-maker/components/social-share/SocialShare.mixin.d.ts +1 -1
  34. package/lib/src/brand-page-maker/components/template-preview/TemplatePreview.mixin.d.ts +1 -1
  35. package/lib/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.js +115 -20
  36. package/lib/src/brand-page-maker/composables/useUndoRedoFlag.d.ts +6 -7
  37. package/lib/src/brand-page-maker/composables/useUndoRedoFlag.js +7 -12
  38. package/lib/src/brand-page-maker/stores/brandpage-maker/brandpage-maker-store.d.ts +1 -1
  39. package/lib/src/brand-page-maker/stores/brandpage-maker/brandpage-maker-store.js +12 -7
  40. package/lib/src/brand-page-maker/stores/history/history-store.d.ts +6 -0
  41. package/lib/src/brand-page-maker/stores/history/history-store.js +11 -1
  42. package/lib/src/brand-page-maker/utils/index.d.ts +0 -2
  43. package/lib/src/brand-page-maker/utils/index.js +0 -2
  44. package/lib/src/brand-page-maker/utils/rich-text-editor.util.d.ts +5 -19
  45. package/lib/src/brand-page-maker/utils/rich-text-editor.util.js +5 -35
  46. package/lib/src/brand-page-maker/validators/field.validator.js +2 -10
  47. package/lib/src/css/library.brand-page-brandCrowd.min.css +1 -1
  48. package/lib/src/css/library.brand-page-brandPage.min.css +1 -1
  49. package/lib/src/css/library.brand-page-designCom.min.css +1 -1
  50. package/lib/src/css/library.brand-page-preview.min.css +1 -1
  51. package/lib/src/index.d.ts +0 -1
  52. package/lib/src/index.mjs +145 -150
  53. package/lib/src/{lib-Bh9Y5_-x.js → lib-BYFWAvIq.js} +55864 -56119
  54. package/lib/src/lib.js +0 -2
  55. package/lib/src/metadata-builder/builders/reorder-pages-builder.d.ts +13 -0
  56. package/lib/src/metadata-builder/builders/reorder-pages-builder.js +49 -0
  57. package/lib/src/metadata-builder/builders/update-content-text-builder.js +15 -20
  58. package/lib/src/metadata-builder/builders/update-content-text-style-builder.js +3 -5
  59. package/lib/src/metadata-builder/helpers/rte.helpers.d.ts +1 -1
  60. package/lib/src/metadata-builder/helpers/rte.helpers.js +3 -6
  61. package/lib/src/shared/content-settings/components/GenericTextInput.mixin.js +1 -14
  62. package/lib/src/shared/content-settings/components/content-settings/ContentSettings.mixin.d.ts +1 -1
  63. package/lib/src/shared/content-settings/components/content-settings/ReplaceImageSettingsForContent.mixin.d.ts +1 -1
  64. package/lib/src/shared/content-settings/components/content-settings-item/ContentSettingsItemField.mixin.d.ts +1 -1
  65. package/lib/src/shared/content-settings/components/content-settings-item/ContentSettingsItemField.mixin.js +3 -12
  66. package/lib/src/shared/content-settings/components/text-style-field/TextStyleField.mixin.d.ts +1 -0
  67. package/lib/src/shared/content-settings/components/text-style-field/TextStyleField.mixin.js +18 -7
  68. package/lib/src/shared/logo-settings/components/LogoSettings.mixin.d.ts +1 -1
  69. package/lib/src/shared/page-settings/components/page-settings/PageSettings.mixin.d.ts +1 -1
  70. package/lib/src/shared/page-settings/components/page-settings-section-styles/PageSettingsSectionStyles.mixin.d.ts +1 -1
  71. package/lib/src/shared/publish-brand-page-with-state/PublishBrandPageModalWithState.mixin.d.ts +1 -1
  72. package/lib/src/shared/style-settings/components/StyleSettings.mixin.d.ts +1 -1
  73. package/lib/src/utils/minify-brand-page-metadata.js +1 -4
  74. package/package.json +1 -1
  75. package/src/brand-page/components/text-wrapper/TextWrapper.vue +6 -17
  76. package/src/brand-page/models/index.ts +26 -35
  77. package/src/brand-page/utils/__snapshots__/migration.util.test.ts.snap +3 -3
  78. package/src/brand-page/utils/logo.util.test.ts +54 -1
  79. package/src/brand-page/utils/logo.util.ts +23 -0
  80. package/src/brand-page/utils/migration.util.ts +0 -12
  81. package/src/brand-page/utils/style.util.test.ts +0 -51
  82. package/src/brand-page/utils/text.util.ts +4 -43
  83. package/src/brand-page/utils/vcard.util.ts +4 -6
  84. package/src/brand-page/utils/view-model.util.ts +26 -94
  85. package/src/brand-page/view-models.ts +2 -3
  86. package/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.mixin.ts +1 -1
  87. package/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.ts +123 -22
  88. package/src/brand-page-maker/composables/useUndoRedoFlag.ts +7 -13
  89. package/src/brand-page-maker/stores/brandpage-maker/brandpage-maker-store.test.ts +53 -0
  90. package/src/brand-page-maker/stores/brandpage-maker/brandpage-maker-store.ts +13 -16
  91. package/src/brand-page-maker/stores/history/history-store.test.ts +51 -0
  92. package/src/brand-page-maker/stores/history/history-store.ts +19 -0
  93. package/src/brand-page-maker/utils/index.ts +0 -2
  94. package/src/brand-page-maker/utils/rich-text-editor.util.test.ts +3 -4
  95. package/src/brand-page-maker/utils/rich-text-editor.util.ts +11 -50
  96. package/src/brand-page-maker/validators/field.validator.ts +2 -10
  97. package/src/lib.ts +0 -9
  98. package/src/metadata-builder/builders/reorder-pages-builder.ts +80 -0
  99. package/src/metadata-builder/builders/update-content-text-builder.ts +18 -32
  100. package/src/metadata-builder/builders/update-content-text-style-builder.ts +4 -6
  101. package/src/metadata-builder/helpers/rte.helpers.ts +7 -11
  102. package/src/shared/content-settings/components/GenericTextInput.mixin.ts +1 -17
  103. package/src/shared/content-settings/components/content-settings-item/ContentSettingsItemField.mixin.ts +3 -16
  104. package/src/shared/content-settings/components/text-style-field/TextStyleField.mixin.ts +19 -11
  105. package/src/utils/minify-brand-page-metadata.ts +1 -4
  106. package/lib/src/brand-page/utils/migration/migration-18.d.ts +0 -2
  107. package/lib/src/brand-page/utils/migration/migration-18.js +0 -71
  108. package/lib/src/brand-page-maker/__fixtures__/complex-rte.fixture.d.ts +0 -22
  109. package/lib/src/brand-page-maker/__fixtures__/complex-rte.fixture.js +0 -22
  110. package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/FontFamily.d.ts +0 -14
  111. package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/FontFamily.js +0 -39
  112. package/lib/src/brand-page-maker/composables/rich-text-editor/getRteExtensions.d.ts +0 -28
  113. package/lib/src/brand-page-maker/composables/rich-text-editor/getRteExtensions.js +0 -66
  114. package/lib/src/brand-page-maker/utils/rte-render.util.d.ts +0 -33
  115. package/lib/src/brand-page-maker/utils/rte-render.util.js +0 -50
  116. package/lib/src/brand-page-maker/utils/rte-sanitize.util.d.ts +0 -6
  117. package/lib/src/brand-page-maker/utils/rte-sanitize.util.js +0 -158
  118. package/lib/src/brand-page-maker/utils/rte-serializer.d.ts +0 -5
  119. package/lib/src/brand-page-maker/utils/rte-serializer.js +0 -112
  120. package/src/brand-page/utils/migration/migration-18.test.ts +0 -72
  121. package/src/brand-page/utils/migration/migration-18.ts +0 -83
  122. package/src/brand-page/utils/text.util.test.ts +0 -47
  123. package/src/brand-page/utils/view-model.pageid-rte.test.ts +0 -104
  124. package/src/brand-page-maker/__fixtures__/complex-rte.fixture.ts +0 -23
  125. package/src/brand-page-maker/composables/rich-text-editor/extensions/FontFamily.test.ts +0 -52
  126. package/src/brand-page-maker/composables/rich-text-editor/extensions/FontFamily.ts +0 -42
  127. package/src/brand-page-maker/composables/rich-text-editor/getRteExtensions.ts +0 -113
  128. package/src/brand-page-maker/utils/__snapshots__/rte-render-parity.test.ts.snap +0 -200
  129. package/src/brand-page-maker/utils/rte-render-parity.test.ts +0 -156
  130. package/src/brand-page-maker/utils/rte-render.util.ts +0 -77
  131. package/src/brand-page-maker/utils/rte-sanitize-json.test.ts +0 -158
  132. package/src/brand-page-maker/utils/rte-sanitize.util.ts +0 -195
  133. package/src/brand-page-maker/utils/rte-serializer.ts +0 -172
  134. package/src/brand-page-maker/validators/field.validator.test.ts +0 -55
@@ -1,158 +0,0 @@
1
- import { validateAndSanitizeHref } from './rich-text-editor.util';
2
- /**
3
- * BP-5787 — native JSON sanitiser for RTE content.
4
- *
5
- * Operates directly on the Tiptap document tree: no editor instantiation, no
6
- * HTML round-trip, no DOMPurify. This is the hot path (runs on every editor
7
- * update and on the server sanitise gate), so it is a single, allocation-light
8
- * recursive walk.
9
- *
10
- * Security model (mirrors the HTML allowlist it replaces):
11
- * - only allow-listed node and mark types survive; everything else is dropped;
12
- * - textStyle / highlight attrs are restricted to known keys whose values must
13
- * be "safe CSS values" (no quotes/angle-brackets/extra declarations/url()/
14
- * expression()), preventing CSS-injection when the value is later rendered
15
- * into a `style="…"` attribute;
16
- * - link hrefs go through the same validateAndSanitizeHref used by the HTML
17
- * sanitiser; unsafe links lose their href (and the mark);
18
- * - heading levels are clamped to 1–6.
19
- */
20
- // ProseMirror node types the editor schema can legitimately produce.
21
- const ALLOWED_NODE_TYPES = new Set([
22
- 'doc',
23
- 'paragraph',
24
- 'heading',
25
- 'text',
26
- 'hardBreak',
27
- 'bulletList',
28
- 'orderedList',
29
- 'listItem',
30
- 'blockquote',
31
- 'codeBlock',
32
- 'horizontalRule',
33
- ]);
34
- // Inline mark types the editor produces (strong/em/u/s/code/span/mark/a).
35
- const ALLOWED_MARK_TYPES = new Set([
36
- 'bold',
37
- 'italic',
38
- 'underline',
39
- 'strike',
40
- 'code',
41
- 'textStyle',
42
- 'highlight',
43
- 'link',
44
- ]);
45
- // textStyle attrs map to the allow-listed CSS properties (font-weight →
46
- // bold mark, text-decoration → underline/strike, text-align → node attr,
47
- // background-color → highlight, so they are intentionally not here).
48
- const ALLOWED_TEXT_STYLE_ATTRS = new Set([
49
- 'color',
50
- 'fontFamily',
51
- 'fontSize',
52
- 'letterSpacing',
53
- 'lineHeight',
54
- ]);
55
- /**
56
- * A safe CSS value is a short string free of characters that could inject an
57
- * extra declaration or escape the attribute, and free of the url()/expression()
58
- * vectors. Legitimate values — `rgb(255, 0, 0)`, `#FF0000`,
59
- * `var(--logo-text-color)`, `Open Sans`, `1.5em` — all pass.
60
- */
61
- const isSafeCssValue = (value) => typeof value === 'string' &&
62
- value.length > 0 &&
63
- value.length <= 200 &&
64
- !/[<>"';]/.test(value) &&
65
- !/url\s*\(/i.test(value) &&
66
- !/expression\s*\(/i.test(value);
67
- const sanitizeTextStyleAttrs = (attrs) => {
68
- const clean = {};
69
- for (const key of Object.keys(attrs)) {
70
- if (!ALLOWED_TEXT_STYLE_ATTRS.has(key))
71
- continue;
72
- const value = attrs[key];
73
- // Tiptap emits '' for unset textStyle slots — preserve them.
74
- if (value === '' || value == null) {
75
- clean[key] = '';
76
- }
77
- else if (isSafeCssValue(value)) {
78
- clean[key] = value;
79
- }
80
- // else: drop the unsafe value
81
- }
82
- return Object.keys(clean).length ? clean : null;
83
- };
84
- const sanitizeMark = (mark) => {
85
- const type = mark?.type;
86
- if (!type || !ALLOWED_MARK_TYPES.has(type))
87
- return null;
88
- const attrs = mark.attrs;
89
- if (type === 'link') {
90
- const href = typeof attrs?.href === 'string'
91
- ? validateAndSanitizeHref(attrs.href)
92
- : null;
93
- if (!href)
94
- return null; // drop links with unsafe/empty href
95
- const cleanAttrs = { href };
96
- if (attrs?.target != null)
97
- cleanAttrs.target = attrs.target;
98
- if (attrs?.rel != null)
99
- cleanAttrs.rel = attrs.rel;
100
- return { type, attrs: cleanAttrs };
101
- }
102
- if (type === 'textStyle') {
103
- const clean = attrs ? sanitizeTextStyleAttrs(attrs) : null;
104
- return clean ? { type, attrs: clean } : null;
105
- }
106
- if (type === 'highlight') {
107
- return isSafeCssValue(attrs?.color)
108
- ? { type, attrs: { color: attrs.color } }
109
- : { type };
110
- }
111
- // Plain marks (bold/italic/underline/strike/code) carry no attrs.
112
- return { type };
113
- };
114
- const ALIGN_VALUES = new Set(['left', 'center', 'right', 'justify']);
115
- const sanitizeNode = (node) => {
116
- const type = node?.type;
117
- if (!type || !ALLOWED_NODE_TYPES.has(type))
118
- return null;
119
- const clean = { type };
120
- const attrs = {};
121
- if (type === 'heading') {
122
- const level = Number(node.attrs?.level);
123
- attrs.level = level >= 1 && level <= 6 ? level : 1;
124
- }
125
- if (type === 'heading' || type === 'paragraph') {
126
- const align = node.attrs?.textAlign;
127
- if (typeof align === 'string' && ALIGN_VALUES.has(align)) {
128
- attrs.textAlign = align;
129
- }
130
- }
131
- if (Object.keys(attrs).length)
132
- clean.attrs = attrs;
133
- if (typeof node.text === 'string')
134
- clean.text = node.text;
135
- if (Array.isArray(node.marks)) {
136
- const marks = node.marks
137
- .map((m) => sanitizeMark(m))
138
- .filter(Boolean);
139
- if (marks && marks.length)
140
- clean.marks = marks;
141
- }
142
- if (Array.isArray(node.content)) {
143
- const content = node.content
144
- .map(sanitizeNode)
145
- .filter(Boolean);
146
- if (content.length)
147
- clean.content = content;
148
- }
149
- return clean;
150
- };
151
- /**
152
- * Sanitise a Tiptap JSON document (returns a new clean doc). Returns a minimal
153
- * empty doc if the input isn't a usable document.
154
- */
155
- export const sanitizeRteJson = (doc) => {
156
- const clean = doc && typeof doc === 'object' ? sanitizeNode(doc) : null;
157
- return clean && clean.type === 'doc' ? clean : { type: 'doc', content: [] };
158
- };
@@ -1,5 +0,0 @@
1
- import type { JSONContent } from '@tiptap/vue-3';
2
- /**
3
- * Serialise a Tiptap JSON document to HTML without an editor/DOM.
4
- */
5
- export declare const serializeRteJsonToHtml: (doc: JSONContent) => string;
@@ -1,112 +0,0 @@
1
- const escapeHtml = (value) => value.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
2
- const escapeAttr = (value) => escapeHtml(value).replace(/"/g, '&quot;');
3
- // textStyle attr → CSS property, in a stable order.
4
- const TEXT_STYLE_PROPS = [
5
- ['fontFamily', 'font-family'],
6
- ['fontSize', 'font-size'],
7
- ['letterSpacing', 'letter-spacing'],
8
- ['lineHeight', 'line-height'],
9
- ['color', 'color'],
10
- ];
11
- const textStyleCss = (attrs) => TEXT_STYLE_PROPS.filter(([key]) => typeof attrs[key] === 'string' && attrs[key])
12
- .map(([key, css]) => `${css}: ${attrs[key]}`)
13
- .join('; ');
14
- // Outermost → innermost. Reversed at wrap time so links wrap outside spans,
15
- // which wrap outside the plain formatting marks.
16
- const MARK_ORDER = [
17
- 'link',
18
- 'textStyle',
19
- 'highlight',
20
- 'strike',
21
- 'underline',
22
- 'italic',
23
- 'bold',
24
- 'code',
25
- ];
26
- const wrapMark = (type, attrs, inner) => {
27
- switch (type) {
28
- case 'bold':
29
- return `<strong>${inner}</strong>`;
30
- case 'italic':
31
- return `<em>${inner}</em>`;
32
- case 'underline':
33
- return `<u>${inner}</u>`;
34
- case 'strike':
35
- return `<s>${inner}</s>`;
36
- case 'code':
37
- return `<code>${inner}</code>`;
38
- case 'textStyle': {
39
- const css = textStyleCss(attrs);
40
- return css ? `<span style="${escapeAttr(css)}">${inner}</span>` : inner;
41
- }
42
- case 'highlight': {
43
- const color = typeof attrs.color === 'string' ? attrs.color : '';
44
- return color
45
- ? `<mark data-color="${escapeAttr(color)}" style="background-color: ${escapeAttr(color)}; color: inherit;">${inner}</mark>`
46
- : `<mark>${inner}</mark>`;
47
- }
48
- case 'link': {
49
- const href = typeof attrs.href === 'string' ? attrs.href : '';
50
- let open = `<a href="${escapeAttr(href)}"`;
51
- if (attrs.target)
52
- open += ` target="${escapeAttr(String(attrs.target))}"`;
53
- if (attrs.rel)
54
- open += ` rel="${escapeAttr(String(attrs.rel))}"`;
55
- return `${open}>${inner}</a>`;
56
- }
57
- default:
58
- return inner;
59
- }
60
- };
61
- const renderText = (node) => {
62
- let html = escapeHtml(node.text ?? '');
63
- const marks = Array.isArray(node.marks) ? node.marks : [];
64
- for (const type of [...MARK_ORDER].reverse()) {
65
- const mark = marks.find((m) => m?.type === type);
66
- if (mark)
67
- html = wrapMark(type, mark.attrs ?? {}, html);
68
- }
69
- return html;
70
- };
71
- const alignAttr = (node) => {
72
- const align = node.attrs?.textAlign;
73
- return typeof align === 'string' && align
74
- ? ` style="text-align: ${escapeAttr(align)}"`
75
- : '';
76
- };
77
- const renderChildren = (node) => (node.content ?? []).map(renderNode).join('');
78
- const renderNode = (node) => {
79
- if (!node || typeof node !== 'object')
80
- return '';
81
- switch (node.type) {
82
- case 'text':
83
- return renderText(node);
84
- case 'hardBreak':
85
- return '<br>';
86
- case 'horizontalRule':
87
- return '<hr>';
88
- case 'paragraph':
89
- return `<p${alignAttr(node)}>${renderChildren(node)}</p>`;
90
- case 'heading': {
91
- const level = Math.min(6, Math.max(1, Number(node.attrs?.level) || 1));
92
- return `<h${level}${alignAttr(node)}>${renderChildren(node)}</h${level}>`;
93
- }
94
- case 'bulletList':
95
- return `<ul>${renderChildren(node)}</ul>`;
96
- case 'orderedList':
97
- return `<ol>${renderChildren(node)}</ol>`;
98
- case 'listItem':
99
- return `<li>${renderChildren(node)}</li>`;
100
- case 'blockquote':
101
- return `<blockquote>${renderChildren(node)}</blockquote>`;
102
- case 'codeBlock':
103
- return `<pre><code>${renderChildren(node)}</code></pre>`;
104
- // doc and any unknown container: render children.
105
- default:
106
- return renderChildren(node);
107
- }
108
- };
109
- /**
110
- * Serialise a Tiptap JSON document to HTML without an editor/DOM.
111
- */
112
- export const serializeRteJsonToHtml = (doc) => renderChildren(doc);
@@ -1,72 +0,0 @@
1
- import { describe, test, expect } from 'vitest';
2
- import { migration18 } from './migration-18';
3
- import type { MigrationData } from '../migration.util';
4
- import { isTiptapDocument, getRteJsonPlainText } from '@brand-page-maker/utils';
5
-
6
- const createMigrationData = (brandPageData: any): MigrationData =>
7
- ({ brandPageModel: brandPageData }) as MigrationData;
8
-
9
- const model = (contents: any[]) => ({
10
- pages: [{ id: 'p1', title: 'Home', slug: 'home', contents }],
11
- });
12
-
13
- describe('migration18 — HTML RTE → Tiptap JSON', () => {
14
- test('converts a top-level HTML RTE field to a JSON document', async () => {
15
- const data = model([
16
- {
17
- id: '1',
18
- type: 'banner-section',
19
- title:
20
- '<p><span style="color: var(--logo-text-color)">Hello</span></p>',
21
- },
22
- ]);
23
-
24
- const result = await migration18(createMigrationData(data));
25
- const title = (result.brandPageModel.pages[0].contents[0] as any).title;
26
-
27
- expect(isTiptapDocument(title)).toBe(true);
28
- expect(getRteJsonPlainText(title)).toContain('Hello');
29
- // theme token ref preserved through migration
30
- expect(JSON.stringify(title)).toContain('var(--logo-text-color)');
31
- });
32
-
33
- test('converts RTE inside item arrays (text items / testimonials)', async () => {
34
- const data = model([
35
- {
36
- id: '2',
37
- type: 'text',
38
- textItems: [{ id: 'ti1', text: '<p><strong>bold</strong> item</p>' }],
39
- },
40
- ]);
41
-
42
- const result = await migration18(createMigrationData(data));
43
- const item = (result.brandPageModel.pages[0].contents[0] as any)
44
- .textItems[0];
45
-
46
- expect(isTiptapDocument(item.text)).toBe(true);
47
- expect(getRteJsonPlainText(item.text)).toContain('bold');
48
- });
49
-
50
- test('leaves plain strings and already-JSON content untouched (idempotent)', async () => {
51
- const jsonDoc = {
52
- type: 'doc',
53
- content: [{ type: 'paragraph', content: [{ type: 'text', text: 'X' }] }],
54
- };
55
- const data = model([
56
- {
57
- id: '3',
58
- type: 'banner-section',
59
- title: jsonDoc, // already JSON
60
- url: 'https://example.com', // plain string, not RTE
61
- titleStyle: { color: '#fff', fontSize: '1em' }, // style object, untouched
62
- },
63
- ]);
64
-
65
- const result = await migration18(createMigrationData(data));
66
- const c = result.brandPageModel.pages[0].contents[0] as any;
67
-
68
- expect(c.title).toEqual(jsonDoc);
69
- expect(c.url).toBe('https://example.com');
70
- expect(c.titleStyle).toEqual({ color: '#fff', fontSize: '1em' });
71
- });
72
- });
@@ -1,83 +0,0 @@
1
- import type { Editor } from '@tiptap/vue-3';
2
- import type { MigrationData } from '../migration.util';
3
- import { hasRTEContent } from '../../../brand-page-maker/utils';
4
- import { useExternalRichTextEditor } from '../../../brand-page-maker/composables/rich-text-editor/useRichTextEditor';
5
-
6
- /**
7
- * BP-5787 — migrate existing inline-HTML RTE content to Tiptap JSON.
8
- *
9
- * The editor now emits, and the renderer now reads, Tiptap JSON. Existing
10
- * published/saved sites still hold RTE fields as HTML strings. This converts
11
- * them on load:
12
- * - maker: the loaded model is JSON, so the next save persists JSON.
13
- * - web-renderer: lazy migrate-on-read — old sites render from JSON without
14
- * needing a re-save.
15
- *
16
- * Runs on EVERY load (runAlways) so it also normalises content the AI section
17
- * builders still emit as HTML — there is no need to change those builders or
18
- * widen the content model types.
19
- *
20
- * Scope: only HTML-RTE strings (hasRTEContent) are converted. Plain strings,
21
- * numbers, and already-JSON docs are left untouched, so the migration is
22
- * idempotent (re-running on JSON content is a no-op — non-string values are
23
- * skipped). Nested RTE inside item arrays (textItems, testimonials, images…)
24
- * is handled by recursing into arrays; style objects are intentionally not
25
- * descended into (they never hold RTE content).
26
- */
27
- const convertRteStringsToJson = (value: unknown, editor: Editor): void => {
28
- if (Array.isArray(value)) {
29
- value.forEach((item) => convertRteStringsToJson(item, editor));
30
-
31
- return;
32
- }
33
-
34
- if (!value || typeof value !== 'object') return;
35
-
36
- const record = value as Record<string, unknown>;
37
-
38
- Object.keys(record).forEach((key) => {
39
- const field = record[key];
40
-
41
- if (typeof field === 'string' && hasRTEContent(field)) {
42
- editor.commands.setContent(field);
43
- record[key] = editor.getJSON();
44
- } else if (Array.isArray(field)) {
45
- convertRteStringsToJson(field, editor);
46
- }
47
- });
48
- };
49
-
50
- export const migration18 = async (
51
- migrationResult: MigrationData,
52
- ): Promise<MigrationData> => {
53
- const { brandPageModel } = migrationResult;
54
-
55
- // HTML→JSON parsing needs a DOM (Tiptap's editor). In a DOM-less context
56
- // (pure-node SSR or unit tests) we can't parse HTML, so we leave the RTE
57
- // fields as HTML strings — the renderer and the fraud/translation extractors
58
- // all dual-read HTML, so this is safe. Conversion happens wherever a DOM is
59
- // available (the browser editor, jsdom).
60
- if (typeof window === 'undefined' || typeof document === 'undefined') {
61
- return migrationResult;
62
- }
63
-
64
- let externalEditor;
65
-
66
- try {
67
- externalEditor = useExternalRichTextEditor().externalEditor;
68
- } catch {
69
- return migrationResult;
70
- }
71
-
72
- try {
73
- for (const page of brandPageModel.pages) {
74
- for (const content of page.contents) {
75
- convertRteStringsToJson(content, externalEditor);
76
- }
77
- }
78
- } finally {
79
- externalEditor.destroy();
80
- }
81
-
82
- return migrationResult;
83
- };
@@ -1,47 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { escapeContentText, getRenderText } from './text.util';
3
-
4
- /**
5
- * BP-5787 — escapeContentText / getRenderText must accept either a legacy
6
- * inline-HTML string or a Tiptap JSON document without throwing. Section
7
- * components call these on RTE fields (e.g. banner description), which are now
8
- * JSON; the previous string-only versions threw `.replace is not a function`
9
- * and broke the field's reactivity (strike/format edits stopped rendering).
10
- */
11
- const doc = {
12
- type: 'doc',
13
- content: [
14
- {
15
- type: 'paragraph',
16
- content: [
17
- { type: 'text', text: 'WARNING', marks: [{ type: 'strike' }] },
18
- { type: 'text', text: ': take flight' },
19
- ],
20
- },
21
- ],
22
- };
23
-
24
- describe('escapeContentText (BP-5787 dual-read)', () => {
25
- it('extracts plain text from a Tiptap JSON document', () => {
26
- expect(escapeContentText(doc)).toBe('WARNING : take flight');
27
- });
28
-
29
- it('still strips tags from a legacy HTML string', () => {
30
- expect(escapeContentText('<p><s>WARNING</s></p>')).toBe('WARNING');
31
- });
32
-
33
- it('returns falsy input unchanged', () => {
34
- expect(escapeContentText('')).toBe('');
35
- expect(escapeContentText(undefined)).toBeUndefined();
36
- });
37
- });
38
-
39
- describe('getRenderText (BP-5787 JSON-safe)', () => {
40
- it('passes a Tiptap JSON document through untouched (no throw)', () => {
41
- expect(getRenderText(doc as unknown as string)).toBe(doc);
42
- });
43
-
44
- it('still converts newlines to <br/> for plain strings', () => {
45
- expect(getRenderText('a\nb')).toBe('a<br/>b');
46
- });
47
- });
@@ -1,104 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { resolvePageIdsInRteJson } from './view-model.util';
3
- import type { NavViewModel } from '../view-models';
4
-
5
- /**
6
- * BP-5847 — internal RTE hyperlinks are stored as `href="pageid:<id>"`. On the
7
- * published site these must resolve to the page's relative path. The string
8
- * (legacy HTML) path already did this; once RTE moved to Tiptap JSON the link
9
- * marks were skipped, so links rendered as raw `pageid:` and didn't navigate.
10
- */
11
- describe('resolvePageIdsInRteJson (BP-5847)', () => {
12
- const nav: NavViewModel = {
13
- navItems: [
14
- {
15
- pageId: 'home-id',
16
- slug: 'home',
17
- displayName: 'Home',
18
- routeName: 'home',
19
- routeParams: { slug: 'mysite' },
20
- isRoot: true,
21
- isHidden: false,
22
- relativePath: '',
23
- nestedNavItems: [],
24
- },
25
- {
26
- pageId: 'about-id',
27
- slug: 'about',
28
- displayName: 'About',
29
- routeName: 'about',
30
- routeParams: { slug: 'mysite' },
31
- isRoot: false,
32
- isHidden: false,
33
- relativePath: 'about',
34
- nestedNavItems: [],
35
- },
36
- ],
37
- };
38
-
39
- const options = { brandPageSlug: 'mysite' } as any;
40
-
41
- const docWithLink = (href: string) => ({
42
- type: 'doc',
43
- content: [
44
- {
45
- type: 'paragraph',
46
- content: [
47
- {
48
- type: 'text',
49
- text: 'link',
50
- marks: [{ type: 'link', attrs: { href, target: '_blank' } }],
51
- },
52
- ],
53
- },
54
- ],
55
- });
56
-
57
- const linkHref = (doc: any) => doc.content[0].content[0].marks[0].attrs.href;
58
-
59
- it('resolves a pageid link to a sub-page relative path', () => {
60
- const out = resolvePageIdsInRteJson(
61
- docWithLink('pageid:about-id'),
62
- nav,
63
- options,
64
- );
65
-
66
- expect(linkHref(out)).toBe('/mysite/about');
67
- });
68
-
69
- it('resolves a root pageid link to the site root', () => {
70
- const out = resolvePageIdsInRteJson(
71
- docWithLink('pageid:home-id'),
72
- nav,
73
- options,
74
- );
75
-
76
- expect(linkHref(out)).toBe('/mysite');
77
- });
78
-
79
- it('falls back to root when the pageid cannot be matched', () => {
80
- const out = resolvePageIdsInRteJson(
81
- docWithLink('pageid:missing-id'),
82
- nav,
83
- options,
84
- );
85
-
86
- expect(linkHref(out)).toBe('/mysite');
87
- });
88
-
89
- it('leaves external links untouched and returns the same reference', () => {
90
- const doc = docWithLink('https://example.com');
91
- const out = resolvePageIdsInRteJson(doc, nav, options);
92
-
93
- expect(linkHref(out)).toBe('https://example.com');
94
- expect(out).toBe(doc);
95
- });
96
-
97
- it('does not mutate the source doc (returns a new object) when resolving', () => {
98
- const doc = docWithLink('pageid:about-id');
99
- const out = resolvePageIdsInRteJson(doc, nav, options);
100
-
101
- expect(out).not.toBe(doc);
102
- expect(linkHref(doc)).toBe('pageid:about-id');
103
- });
104
- });
@@ -1,23 +0,0 @@
1
- /**
2
- * BP-5787 — golden-master fixture for the RTE HTML→JSON migration.
3
- *
4
- * A single rich-text value that exercises every mark/node the editor can emit,
5
- * so one snapshot guards the whole serialisation surface. Captured as a baseline
6
- * on the current HTML pipeline; after the migration the JSON pipeline must
7
- * reproduce byte-identical rendered output (see rte-render-parity.test.ts).
8
- *
9
- * Coverage:
10
- * - block nodes: heading (h2), paragraph, text-align (center / right)
11
- * - inline marks: bold, italic, underline, strike
12
- * - textStyle attrs: fontFamily, fontSize (em), letterSpacing (em), lineHeight (unitless)
13
- * - colour: theme TOKEN ref (var(--logo-text-color)) AND a literal hex (#FF0000)
14
- * - highlight (background colour via <mark>)
15
- * - links: internal (target=_self) and external (target=_blank rel=noopener)
16
- */
17
- export const COMPLEX_RTE_HTML = `<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">highlight</mark> <span style="color: #FF0000">literal hex</span> <a target="_self" href="/about">internal link</a> <a target="_blank" rel="noopener noreferrer nofollow" href="https://example.com">external link</a></p><p style="text-align: right">second paragraph, right-aligned</p>`;
18
-
19
- /**
20
- * The theme-token CSS variable that MUST survive the HTML→JSON round-trip
21
- * (BP-5787 stores token refs instead of flattening them to rgb()).
22
- */
23
- export const THEME_TOKEN_REF = 'var(--logo-text-color)';
@@ -1,52 +0,0 @@
1
- import { describe, it, expect, afterEach } from 'vitest';
2
- import { Editor } from '@tiptap/vue-3';
3
- import { getRteExtensions } from '../getRteExtensions';
4
- import { sanitizeRteJson } from '@brand-page-maker/utils/rte-sanitize.util';
5
-
6
- /**
7
- * BP-5787 — multi-word font families ("Open Sans") are quoted in CSS, so
8
- * re-parsing HTML (pasting RTE-styled text between editors) previously yielded
9
- * a value WITH quote chars, which the native sanitiser then dropped — losing
10
- * the font. The custom FontFamily extension strips the quotes on parse.
11
- */
12
- describe('FontFamily — quote-stripping parse (BP-5787)', () => {
13
- let editors: Editor[] = [];
14
- const mk = () => {
15
- const e = new Editor({
16
- extensions: getRteExtensions({ restrictStarterKit: true }),
17
- });
18
-
19
- editors.push(e);
20
-
21
- return e;
22
- };
23
-
24
- afterEach(() => {
25
- editors.forEach((e) => e.destroy());
26
- editors = [];
27
- });
28
-
29
- const fontFamilyOf = (json: any) =>
30
- json.content?.[0]?.content?.[0]?.marks?.find(
31
- (m: any) => m.type === 'textStyle',
32
- )?.attrs?.fontFamily;
33
-
34
- for (const font of ['Montserrat', 'Open Sans', 'Playwrite AU']) {
35
- it(`"${font}" survives an HTML re-parse + sanitise unquoted`, () => {
36
- const src = mk();
37
-
38
- src.commands.setContent('<p>Hello world</p>');
39
- src.commands.selectAll();
40
- src.commands.setFontFamily(font);
41
-
42
- // Re-parse the serialised HTML — mirrors an RTE→RTE paste.
43
- const tgt = mk();
44
-
45
- tgt.commands.setContent(src.getHTML());
46
-
47
- expect(fontFamilyOf(tgt.getJSON())).toBe(font);
48
- // and the sanitiser must keep it (isSafeCssValue rejects quote chars).
49
- expect(fontFamilyOf(sanitizeRteJson(tgt.getJSON()))).toBe(font);
50
- });
51
- }
52
- });