@designcrowd/library.brand-page 6.0.25-json-prereq-2 → 6.0.25

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 (155) hide show
  1. package/lib/src/{CreatedOnBrandCrowd-de-de-MkdpvvLq.js → CreatedOnBrandCrowd-de-de-CS3ct0wa.js} +1 -1
  2. package/lib/src/{CreatedOnBrandCrowd-en-us-0K8Va8jv.js → CreatedOnBrandCrowd-en-us-CmBcnDu6.js} +1 -1
  3. package/lib/src/{CreatedOnBrandCrowd-es-es-BXBXK85u.js → CreatedOnBrandCrowd-es-es-BdTuJtqV.js} +1 -1
  4. package/lib/src/{CreatedOnBrandCrowd-fr-fr-wbrMlisb.js → CreatedOnBrandCrowd-fr-ca-Cx_H9X6l.js} +1 -1
  5. package/lib/src/{CreatedOnBrandCrowd-fr-ca-wbrMlisb.js → CreatedOnBrandCrowd-fr-fr-Cx_H9X6l.js} +1 -1
  6. package/lib/src/{CreatedOnBrandCrowd-pt-pt-BHBKnYHd.js → CreatedOnBrandCrowd-pt-br-BZZ9hjUi.js} +1 -1
  7. package/lib/src/{CreatedOnBrandCrowd-pt-br-BHBKnYHd.js → CreatedOnBrandCrowd-pt-pt-BZZ9hjUi.js} +1 -1
  8. package/lib/src/{CreatedOnDcom-de-de-BeG5A3Bb.js → CreatedOnDcom-de-de-SbONDQN1.js} +1 -1
  9. package/lib/src/{CreatedOnDcom-en-us-DVCNhdLS.js → CreatedOnDcom-en-us-Beno1_gw.js} +1 -1
  10. package/lib/src/{CreatedOnDcom-es-es-Ba8bq852.js → CreatedOnDcom-es-es-BBE_zdRu.js} +1 -1
  11. package/lib/src/{CreatedOnDcom-fr-ca-DuNhf0PH.js → CreatedOnDcom-fr-ca-C6pgDB5x.js} +1 -1
  12. package/lib/src/{CreatedOnDcom-fr-fr-CfofYZSL.js → CreatedOnDcom-fr-fr-FD1eqtg3.js} +1 -1
  13. package/lib/src/{CreatedOnDcom-pt-br-BMCBFLhc.js → CreatedOnDcom-pt-br-BiSx3OwM.js} +1 -1
  14. package/lib/src/{CreatedOnDcom-pt-pt-CQS6Ft_9.js → CreatedOnDcom-pt-pt-Dn4DmKGA.js} +1 -1
  15. package/lib/src/brand-page/components/contact-form/ContactForm.mixin.js +2 -5
  16. package/lib/src/brand-page/models/index.d.ts +26 -34
  17. package/lib/src/brand-page/utils/migration.util.js +0 -11
  18. package/lib/src/brand-page/utils/text.util.d.ts +1 -1
  19. package/lib/src/brand-page/utils/text.util.js +0 -32
  20. package/lib/src/brand-page/utils/vcard.util.js +4 -4
  21. package/lib/src/brand-page/utils/view-model.util.d.ts +0 -1
  22. package/lib/src/brand-page/utils/view-model.util.js +20 -68
  23. package/lib/src/brand-page/view-models.d.ts +3 -3
  24. package/lib/src/brand-page-maker/composables/ai-chat/useAiMetadataBuilder.d.ts +1 -0
  25. package/lib/src/brand-page-maker/composables/rich-text-editor/bubbleMenu.d.ts +9 -2
  26. package/lib/src/brand-page-maker/composables/rich-text-editor/bubbleMenu.js +10 -6
  27. package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/FontSize.d.ts +20 -0
  28. package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/FontSize.js +44 -0
  29. package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/LineHeight.d.ts +27 -0
  30. package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/LineHeight.js +44 -0
  31. package/lib/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.d.ts +1 -1
  32. package/lib/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.js +103 -24
  33. package/lib/src/brand-page-maker/utils/index.d.ts +0 -2
  34. package/lib/src/brand-page-maker/utils/index.js +0 -2
  35. package/lib/src/brand-page-maker/utils/rich-text-editor.util.d.ts +5 -28
  36. package/lib/src/brand-page-maker/utils/rich-text-editor.util.js +5 -48
  37. package/lib/src/brand-page-maker/validators/field.validator.js +2 -10
  38. package/lib/src/css/library.brand-page-brandCrowd.min.css +39 -39
  39. package/lib/src/css/library.brand-page-brandPage.min.css +39 -39
  40. package/lib/src/css/library.brand-page-designCom.min.css +39 -39
  41. package/lib/src/css/library.brand-page-preview.min.css +39 -39
  42. package/lib/src/index.d.ts +0 -1
  43. package/lib/src/index.mjs +138 -143
  44. package/lib/src/{lib-QPQAbiCG.js → lib-BVHFYSLL.js} +60149 -63629
  45. package/lib/src/lib.js +0 -2
  46. package/lib/src/metadata-builder/ai-metadata-executor.d.ts +1 -0
  47. package/lib/src/metadata-builder/ai-metadata-executor.js +19 -0
  48. package/lib/src/metadata-builder/builders/section-builders/about-section-builder.js +3 -2
  49. package/lib/src/metadata-builder/builders/section-builders/banner-section-builder.js +3 -2
  50. package/lib/src/metadata-builder/builders/section-builders/booking-section-builder.js +3 -2
  51. package/lib/src/metadata-builder/builders/section-builders/contact-form-builder.js +4 -3
  52. package/lib/src/metadata-builder/builders/section-builders/images-section-builder.js +3 -2
  53. package/lib/src/metadata-builder/builders/section-builders/mailing-list-builder.js +2 -1
  54. package/lib/src/metadata-builder/builders/section-builders/mailing-list-section-builder.js +2 -1
  55. package/lib/src/metadata-builder/builders/section-builders/profile-section-builder.js +4 -3
  56. package/lib/src/metadata-builder/builders/section-builders/shapes-section-builder.js +3 -2
  57. package/lib/src/metadata-builder/builders/section-builders/testimonial-section-builder.js +2 -1
  58. package/lib/src/metadata-builder/builders/section-builders/text-section-builder.js +11 -15
  59. package/lib/src/metadata-builder/builders/update-content-text-builder.d.ts +17 -0
  60. package/lib/src/metadata-builder/builders/update-content-text-builder.js +103 -0
  61. package/lib/src/metadata-builder/builders/update-content-text-style-builder.d.ts +16 -0
  62. package/lib/src/metadata-builder/builders/update-content-text-style-builder.js +216 -0
  63. package/lib/src/metadata-builder/helpers/rte.helpers.d.ts +13 -11
  64. package/lib/src/metadata-builder/helpers/rte.helpers.js +73 -14
  65. package/lib/src/metadata-builder/metadata-executor.js +4 -0
  66. package/lib/src/metadata-builder/models.d.ts +2 -0
  67. package/lib/src/metadata-builder/models.js +2 -0
  68. package/lib/src/metadata-builder/parsers/ai-toolcall-parser.d.ts +19 -0
  69. package/lib/src/metadata-builder/parsers/ai-toolcall-parser.js +4 -0
  70. package/lib/src/metadata-builder/sanitizers/rte-schema-check.d.ts +8 -7
  71. package/lib/src/metadata-builder/sanitizers/rte-schema-check.js +52 -21
  72. package/lib/src/shared/content-settings/components/GenericTextInput.mixin.js +1 -14
  73. package/lib/src/shared/content-settings/components/content-settings-item/ContentSettingsItemField.mixin.js +3 -12
  74. package/lib/src/shared/content-settings/components/testimonial-items-field/TestimonialItemsField.mixin.d.ts +2 -3
  75. package/lib/src/shared/content-settings/components/testimonial-items-field/TestimonialItemsField.mixin.js +3 -10
  76. package/lib/src/shared/content-settings/components/text-items-field/TextItemsField.mixin.d.ts +1 -2
  77. package/lib/src/shared/content-settings/components/text-items-field/TextItemsField.mixin.js +1 -4
  78. package/lib/src/utils/minify-brand-page-metadata.js +1 -4
  79. package/package.json +15 -17
  80. package/src/brand-page/components/contact-form/ContactForm.mixin.ts +2 -5
  81. package/src/brand-page/components/text-wrapper/TextWrapper.vue +6 -17
  82. package/src/brand-page/models/index.ts +26 -35
  83. package/src/brand-page/utils/__snapshots__/migration.util.test.ts.snap +3 -3
  84. package/src/brand-page/utils/migration.util.ts +0 -12
  85. package/src/brand-page/utils/style.util.test.ts +0 -51
  86. package/src/brand-page/utils/text.util.ts +4 -43
  87. package/src/brand-page/utils/vcard.util.ts +4 -6
  88. package/src/brand-page/utils/view-model.util.ts +26 -94
  89. package/src/brand-page/view-models.ts +2 -3
  90. package/src/brand-page-maker/components/rich-text-editor/RichTextEditor.vue +3 -4
  91. package/src/brand-page-maker/composables/ai-chat/useAiMetadataBuilder.ts +1 -0
  92. package/src/brand-page-maker/composables/rich-text-editor/bubbleMenu.ts +10 -6
  93. package/src/brand-page-maker/composables/rich-text-editor/extensions/FontSize.ts +73 -0
  94. package/src/brand-page-maker/composables/rich-text-editor/extensions/LineHeight.ts +85 -0
  95. package/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.ts +111 -27
  96. package/src/brand-page-maker/utils/index.ts +0 -2
  97. package/src/brand-page-maker/utils/rich-text-editor.util.test.ts +3 -37
  98. package/src/brand-page-maker/utils/rich-text-editor.util.ts +11 -66
  99. package/src/brand-page-maker/validators/field.validator.ts +2 -10
  100. package/src/lib.ts +0 -9
  101. package/src/metadata-builder/ai-metadata-executor.ts +26 -0
  102. package/src/metadata-builder/builders/section-builders/about-section-builder.ts +3 -2
  103. package/src/metadata-builder/builders/section-builders/banner-section-builder.ts +3 -2
  104. package/src/metadata-builder/builders/section-builders/booking-section-builder.ts +5 -2
  105. package/src/metadata-builder/builders/section-builders/contact-form-builder.ts +4 -3
  106. package/src/metadata-builder/builders/section-builders/images-section-builder.ts +3 -2
  107. package/src/metadata-builder/builders/section-builders/mailing-list-builder.ts +2 -1
  108. package/src/metadata-builder/builders/section-builders/mailing-list-section-builder.ts +2 -1
  109. package/src/metadata-builder/builders/section-builders/profile-section-builder.ts +6 -3
  110. package/src/metadata-builder/builders/section-builders/shapes-section-builder.ts +3 -2
  111. package/src/metadata-builder/builders/section-builders/testimonial-section-builder.ts +2 -1
  112. package/src/metadata-builder/builders/section-builders/text-section-builder.ts +13 -19
  113. package/src/metadata-builder/builders/update-content-text-builder.ts +148 -0
  114. package/src/metadata-builder/builders/update-content-text-style-builder.ts +352 -0
  115. package/src/metadata-builder/helpers/rte.helpers.ts +101 -15
  116. package/src/metadata-builder/metadata-executor.ts +5 -0
  117. package/src/metadata-builder/models.ts +3 -0
  118. package/src/metadata-builder/parsers/ai-toolcall-parser.ts +27 -0
  119. package/src/metadata-builder/sanitizers/rte-schema-check.ts +53 -24
  120. package/src/shared/content-settings/components/GenericTextInput.mixin.ts +1 -17
  121. package/src/shared/content-settings/components/content-settings-item/ContentSettingsItemField.mixin.ts +3 -16
  122. package/src/shared/content-settings/components/testimonial-items-field/TestimonialItemsField.mixin.ts +4 -13
  123. package/src/shared/content-settings/components/testimonial-items-field/TestimonialItemsField.vue +4 -6
  124. package/src/shared/content-settings/components/text-image-field/TextImageField.vue +3 -6
  125. package/src/shared/content-settings/components/text-items-field/TextItemsField.mixin.ts +1 -6
  126. package/src/shared/content-settings/components/text-items-field/TextItemsField.vue +1 -1
  127. package/src/utils/minify-brand-page-metadata.ts +1 -4
  128. package/lib/src/brand-page/utils/migration/migration-20.d.ts +0 -2
  129. package/lib/src/brand-page/utils/migration/migration-20.js +0 -111
  130. package/lib/src/brand-page-maker/__fixtures__/complex-rte.fixture.d.ts +0 -22
  131. package/lib/src/brand-page-maker/__fixtures__/complex-rte.fixture.js +0 -22
  132. package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/FontFamily.d.ts +0 -14
  133. package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/FontFamily.js +0 -39
  134. package/lib/src/brand-page-maker/composables/rich-text-editor/getRteExtensions.d.ts +0 -28
  135. package/lib/src/brand-page-maker/composables/rich-text-editor/getRteExtensions.js +0 -90
  136. package/lib/src/brand-page-maker/utils/rte-render.util.d.ts +0 -41
  137. package/lib/src/brand-page-maker/utils/rte-render.util.js +0 -33
  138. package/lib/src/brand-page-maker/utils/rte-sanitize.util.d.ts +0 -6
  139. package/lib/src/brand-page-maker/utils/rte-sanitize.util.js +0 -158
  140. package/src/brand-page/utils/migration/migration-20.server.test.ts +0 -121
  141. package/src/brand-page/utils/migration/migration-20.test.ts +0 -72
  142. package/src/brand-page/utils/migration/migration-20.ts +0 -152
  143. package/src/brand-page/utils/text.util.test.ts +0 -47
  144. package/src/brand-page/utils/view-model.pageid-rte.test.ts +0 -104
  145. package/src/brand-page-maker/__fixtures__/complex-rte.fixture.ts +0 -23
  146. package/src/brand-page-maker/composables/rich-text-editor/extensions/FontFamily.test.ts +0 -52
  147. package/src/brand-page-maker/composables/rich-text-editor/extensions/FontFamily.ts +0 -42
  148. package/src/brand-page-maker/composables/rich-text-editor/getRteExtensions.ts +0 -137
  149. package/src/brand-page-maker/utils/__snapshots__/rte-render-parity.test.ts.snap +0 -202
  150. package/src/brand-page-maker/utils/rte-features.test.ts +0 -351
  151. package/src/brand-page-maker/utils/rte-render-parity.test.ts +0 -157
  152. package/src/brand-page-maker/utils/rte-render.util.ts +0 -68
  153. package/src/brand-page-maker/utils/rte-sanitize-json.test.ts +0 -158
  154. package/src/brand-page-maker/utils/rte-sanitize.util.ts +0 -195
  155. package/src/brand-page-maker/validators/field.validator.test.ts +0 -55
@@ -1,90 +0,0 @@
1
- import StarterKit from '@tiptap/starter-kit';
2
- import Underline from '@tiptap/extension-underline';
3
- import { TextStyle, FontSize, LineHeight } from '@tiptap/extension-text-style';
4
- import TextAlign from '@tiptap/extension-text-align';
5
- import ColorExtension from '@tiptap/extension-color';
6
- import Highlight from '@tiptap/extension-highlight';
7
- import Placeholder from '@tiptap/extension-placeholder';
8
- import Link from '@tiptap/extension-link';
9
- import { FontFamily } from './extensions/FontFamily';
10
- import { LetterSpacing } from './extensions/LetterSpacing';
11
- import { MaxLength } from './extensions/MaxLength';
12
- import { AutoLink } from './extensions/AutoLink';
13
- import { isHrefPageId } from './hyperlink';
14
- export const getRteExtensions = (options = {}) => {
15
- const { restrictStarterKit = false, includeLinkRel = false, editing, } = options;
16
- const extensions = [
17
- // v3 StarterKit bundles Link + Underline; we register both separately (with
18
- // custom Link config), so disable the bundled copies to avoid duplicate
19
- // extension errors.
20
- // trailingNode: false — v3 StarterKit adds a TrailingNode plugin (not in v2)
21
- // that appends an empty paragraph after any non-paragraph trailing block.
22
- // Disabled to keep editor/stored-content behaviour identical to v2 (BP-5787
23
- // is backward-compat-first); revisit as a standalone UX change if wanted.
24
- restrictStarterKit
25
- ? StarterKit.configure({
26
- link: false,
27
- underline: false,
28
- trailingNode: false,
29
- bulletList: false,
30
- orderedList: false,
31
- listItem: false,
32
- horizontalRule: false,
33
- codeBlock: false,
34
- code: false,
35
- })
36
- : StarterKit.configure({
37
- link: false,
38
- underline: false,
39
- trailingNode: false,
40
- }),
41
- Underline,
42
- // priority: 99 — render the textStyle <span> (which carries font-size) as the
43
- // OUTERMOST inline mark, so decoration marks (<u>/<s>) nest INSIDE it and
44
- // inherit the applied font-size. v3's extension-text-style defaults to
45
- // priority 101, which flips this: <u>/<s> end up outside the font-size span
46
- // and inherit only the base paragraph size. Since text-decoration thickness
47
- // AND vertical position are computed from the decorating element's own font,
48
- // that produced thin, mis-centred underline/strike on large text — a
49
- // regression vs v2's serializer, which emitted the font-size span outermost
50
- // (BP-5787, backward-compat-first).
51
- TextStyle.extend({ priority: 99 }).configure({
52
- mergeNestedSpanStyles: true,
53
- }),
54
- TextAlign.configure({
55
- types: ['heading', 'paragraph'],
56
- }),
57
- FontFamily.configure({
58
- types: ['textStyle'],
59
- }),
60
- // FontSize + LineHeight are now shipped natively by @tiptap/extension-text-style.
61
- FontSize.configure({ types: ['textStyle'] }),
62
- LetterSpacing,
63
- LineHeight.configure({ types: ['textStyle'] }),
64
- ColorExtension.configure({
65
- types: ['textStyle'],
66
- }),
67
- Highlight.configure({
68
- multicolor: true,
69
- }),
70
- ];
71
- // Live-editing-only plugins sit between Highlight and Link (original order).
72
- if (editing) {
73
- extensions.push(Placeholder.configure({ placeholder: editing.placeholder }), MaxLength.configure({ maxLength: editing.maxLength }));
74
- }
75
- extensions.push(Link.configure({
76
- openOnClick: false,
77
- defaultProtocol: 'https',
78
- autolink: true,
79
- HTMLAttributes: (attrs) => {
80
- const href = attrs.href;
81
- const isExternal = href && (href.startsWith('http://') || href.startsWith('https://'));
82
- return {
83
- ...(includeLinkRel && { rel: 'noopener noreferrer' }),
84
- target: isExternal ? '_blank' : null,
85
- };
86
- },
87
- isAllowedUri: (url, ctx) => ctx.defaultValidate(url) || isHrefPageId(url),
88
- }), AutoLink);
89
- return extensions;
90
- };
@@ -1,41 +0,0 @@
1
- import type { JSONContent } from '@tiptap/vue-3';
2
- import { isTiptapDocument, getRteJsonPlainText } from './rich-text-editor.util';
3
- export { isTiptapDocument, getRteJsonPlainText };
4
- /**
5
- * BP-5787 — the boundary plumbing for storing RTE as Tiptap JSON.
6
- *
7
- * An RTE field is either a legacy inline-HTML string (pre-migration, or a
8
- * not-yet-migrated published site) or a Tiptap JSON document (post-migration).
9
- * These helpers let every consumer treat the two uniformly:
10
- *
11
- * - isTiptapDocument — the canonical discriminator (object with type 'doc').
12
- * - renderRteToHtml — render either form to final HTML for display.
13
- * - toRteJson — normalise either form to JSON for storage.
14
- *
15
- * Both directions delegate to Tiptap's own schema-driven converters, fed by the
16
- * single-source-of-truth extension set (getRteExtensions) — so the mapping lives
17
- * in the extensions' `renderHTML`, not a hand-maintained copy that drifts when
18
- * Tiptap changes:
19
- *
20
- * - renderRteToHtml uses @tiptap/static-renderer (renderToHTMLString). It is
21
- * DOM-less, so it runs on the server (SSR/publish/preview), not just the
22
- * browser, and needs no Editor instance.
23
- * - toRteJson uses generateJSON, which PARSES HTML and so needs a DOM
24
- * (window.DOMParser). In the browser and in jsdom tests that global exists;
25
- * on the server the only caller is migration-20, which installs a jsdom
26
- * window around its conversion pass (see migration/migration-20.ts).
27
- */
28
- export type RichTextValue = string | JSONContent;
29
- /**
30
- * Render an RTE value to final HTML. Legacy HTML strings pass through untouched
31
- * (they are already HTML); Tiptap JSON is serialised by @tiptap/static-renderer
32
- * from the extension schema — no editor, no DOM — so it works on the server
33
- * (SSR/publish), not just the browser.
34
- */
35
- export declare const renderRteToHtml: (value: RichTextValue | null | undefined) => string;
36
- /**
37
- * Normalise an RTE value to a Tiptap JSON document. HTML strings are parsed via
38
- * generateJSON (needs a DOM — browser/jsdom); documents pass through unchanged.
39
- * Returns undefined for empty/blank input so callers can omit empty fields.
40
- */
41
- export declare const toRteJson: (value: RichTextValue | null | undefined) => JSONContent | undefined;
@@ -1,33 +0,0 @@
1
- import { generateJSON } from '@tiptap/core';
2
- import { renderToHTMLString } from '@tiptap/static-renderer/pm/html-string';
3
- import { getRteExtensions } from '../composables/rich-text-editor/getRteExtensions';
4
- import { isTiptapDocument, getRteJsonPlainText } from './rich-text-editor.util';
5
- export { isTiptapDocument, getRteJsonPlainText };
6
- // One extension set shared by both directions (render + parse). Schema knowledge
7
- // lives here, once — Tiptap/extension upgrades propagate to both automatically.
8
- const RTE_EXTENSIONS = getRteExtensions({ includeLinkRel: true });
9
- /**
10
- * Render an RTE value to final HTML. Legacy HTML strings pass through untouched
11
- * (they are already HTML); Tiptap JSON is serialised by @tiptap/static-renderer
12
- * from the extension schema — no editor, no DOM — so it works on the server
13
- * (SSR/publish), not just the browser.
14
- */
15
- export const renderRteToHtml = (value) => {
16
- if (value == null || value === '')
17
- return '';
18
- if (typeof value === 'string')
19
- return value;
20
- return renderToHTMLString({ content: value, extensions: RTE_EXTENSIONS });
21
- };
22
- /**
23
- * Normalise an RTE value to a Tiptap JSON document. HTML strings are parsed via
24
- * generateJSON (needs a DOM — browser/jsdom); documents pass through unchanged.
25
- * Returns undefined for empty/blank input so callers can omit empty fields.
26
- */
27
- export const toRteJson = (value) => {
28
- if (value == null || value === '')
29
- return undefined;
30
- if (isTiptapDocument(value))
31
- return value;
32
- return generateJSON(value, RTE_EXTENSIONS);
33
- };
@@ -1,6 +0,0 @@
1
- import type { JSONContent } from '@tiptap/vue-3';
2
- /**
3
- * Sanitise a Tiptap JSON document (returns a new clean doc). Returns a minimal
4
- * empty doc if the input isn't a usable document.
5
- */
6
- export declare const sanitizeRteJson: (doc: unknown) => JSONContent;
@@ -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,121 +0,0 @@
1
- /**
2
- * BP-5787 — migration-20 must convert HTML RTE → Tiptap JSON on the SERVER, not
3
- * just the browser. `generateJSON` (via toRteJson) needs `window.DOMParser`,
4
- * which pure-node SSR lacks; migration-20 provides a jsdom window for its pass.
5
- *
6
- * These tests run under the default jsdom environment but DELETE the global
7
- * `window`/`document` around each case, so `migration20` takes its DOM-less
8
- * server branch for real (dynamic jsdom import via a runtime-assembled specifier
9
- * + transient global install + restore). The sibling migration-20.test.ts
10
- * guards the browser path.
11
- *
12
- * (We simulate DOM-less rather than switch this file to a node environment,
13
- * because the shared test/vitest.setup.js assumes a jsdom `global.window`.)
14
- */
15
- import { describe, test, expect, beforeEach, afterEach } from 'vitest';
16
- import { migration20 } from './migration-20';
17
- import type { MigrationData } from '../migration.util';
18
- import { isTiptapDocument, getRteJsonPlainText } from '@brand-page-maker/utils';
19
-
20
- const createMigrationData = (brandPageData: any): MigrationData =>
21
- ({ brandPageModel: brandPageData }) as MigrationData;
22
-
23
- const model = (contents: any[]) => ({
24
- pages: [{ id: 'p1', title: 'Home', slug: 'home', contents }],
25
- });
26
-
27
- const g = globalThis as any;
28
- let savedWindow: unknown;
29
- let savedDocument: unknown;
30
-
31
- describe('migration20 (server / no DOM) — HTML RTE → Tiptap JSON', () => {
32
- beforeEach(() => {
33
- // Force migration-20's DOM-less server branch.
34
- savedWindow = g.window;
35
- savedDocument = g.document;
36
- delete g.window;
37
- delete g.document;
38
-
39
- expect(typeof window).toBe('undefined');
40
- expect(typeof document).toBe('undefined');
41
- });
42
-
43
- afterEach(() => {
44
- g.window = savedWindow;
45
- g.document = savedDocument;
46
- });
47
-
48
- test('converts a top-level HTML RTE field with no global window', async () => {
49
- const data = model([
50
- {
51
- id: '1',
52
- type: 'banner-section',
53
- title:
54
- '<p><span style="color: var(--logo-text-color)">Hello</span></p>',
55
- },
56
- ]);
57
-
58
- const result = await migration20(createMigrationData(data));
59
- const title = (result.brandPageModel.pages[0].contents[0] as any).title;
60
-
61
- expect(isTiptapDocument(title)).toBe(true);
62
- expect(getRteJsonPlainText(title)).toContain('Hello');
63
- // theme token ref preserved through the server-side parse
64
- expect(JSON.stringify(title)).toContain('var(--logo-text-color)');
65
- });
66
-
67
- test('converts RTE inside item arrays server-side (marks preserved)', async () => {
68
- const data = model([
69
- {
70
- id: '2',
71
- type: 'text',
72
- textItems: [{ id: 'ti1', text: '<p><strong>bold</strong> item</p>' }],
73
- },
74
- ]);
75
-
76
- const result = await migration20(createMigrationData(data));
77
- const item = (result.brandPageModel.pages[0].contents[0] as any)
78
- .textItems[0];
79
-
80
- expect(isTiptapDocument(item.text)).toBe(true);
81
- expect(getRteJsonPlainText(item.text)).toContain('bold');
82
- // the <strong> mark survived the jsdom parse
83
- expect(JSON.stringify(item.text)).toContain('bold');
84
- });
85
-
86
- test('leaves plain strings and already-JSON content untouched (idempotent)', async () => {
87
- const jsonDoc = {
88
- type: 'doc',
89
- content: [{ type: 'paragraph', content: [{ type: 'text', text: 'X' }] }],
90
- };
91
- const data = model([
92
- {
93
- id: '3',
94
- type: 'banner-section',
95
- title: jsonDoc, // already JSON
96
- url: 'https://example.com', // plain string, not RTE
97
- titleStyle: { color: '#fff', fontSize: '1em' }, // style object, untouched
98
- },
99
- ]);
100
-
101
- const result = await migration20(createMigrationData(data));
102
- const c = result.brandPageModel.pages[0].contents[0] as any;
103
-
104
- expect(c.title).toEqual(jsonDoc);
105
- expect(c.url).toBe('https://example.com');
106
- expect(c.titleStyle).toEqual({ color: '#fff', fontSize: '1em' });
107
- });
108
-
109
- test('does not leak the jsdom window into globalThis after the pass', async () => {
110
- const data = model([
111
- { id: '4', type: 'banner-section', title: '<p>Leak check</p>' },
112
- ]);
113
-
114
- await migration20(createMigrationData(data));
115
-
116
- // The transient jsdom globals installed during the pass must be restored
117
- // to their pre-pass state (deleted here), not leaked.
118
- expect(typeof window).toBe('undefined');
119
- expect(typeof document).toBe('undefined');
120
- });
121
- });
@@ -1,72 +0,0 @@
1
- import { describe, test, expect } from 'vitest';
2
- import { migration20 } from './migration-20';
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('migration20 — 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 migration20(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 migration20(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 migration20(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,152 +0,0 @@
1
- import type { BrandPageModel } from '../../models';
2
- import type { MigrationData } from '../migration.util';
3
- import { hasRTEContent, toRteJson } from '../../../brand-page-maker/utils';
4
-
5
- /**
6
- * BP-5787 — migrate existing inline-HTML RTE content to Tiptap JSON.
7
- *
8
- * The editor emits, and the renderer reads, Tiptap JSON. Existing
9
- * published/saved sites still hold RTE fields as HTML strings. This converts
10
- * them on load, in EVERY environment:
11
- * - maker (browser): the loaded model is JSON, so the next save persists JSON.
12
- * - web-renderer (SSR / server API routes): converted on read using a jsdom
13
- * window (see below), so old sites are normalised without a re-save and
14
- * downstream code can treat RTE as JSON.
15
- *
16
- * This is a one-time, versioned migration (runAlways is NOT set): legacy sites
17
- * are at model version < 20 and convert on first load in any environment; once
18
- * `v` is stamped to 20 the conversion is skipped. This is only safe because the
19
- * AI section builders now emit Tiptap JSON directly (they no longer write HTML),
20
- * so there is no recurring post-load source of HTML to normalise.
21
- *
22
- * Scope: only HTML-RTE strings (hasRTEContent) are converted. Plain strings,
23
- * numbers, and already-JSON docs are left untouched, so the migration is
24
- * idempotent (re-running on JSON content is a no-op — non-string values are
25
- * skipped). Nested RTE inside item arrays (textItems, testimonials, images…)
26
- * is handled by recursing into arrays; style objects are intentionally not
27
- * descended into (they never hold RTE content).
28
- */
29
- const convertRteStringsToJson = (value: unknown): void => {
30
- if (Array.isArray(value)) {
31
- value.forEach((item) => convertRteStringsToJson(item));
32
-
33
- return;
34
- }
35
-
36
- if (!value || typeof value !== 'object') return;
37
-
38
- const record = value as Record<string, unknown>;
39
-
40
- Object.keys(record).forEach((key) => {
41
- const field = record[key];
42
-
43
- if (typeof field === 'string' && hasRTEContent(field)) {
44
- const json = toRteJson(field);
45
-
46
- if (json) record[key] = json;
47
- } else if (Array.isArray(field)) {
48
- convertRteStringsToJson(field);
49
- }
50
- });
51
- };
52
-
53
- const convertModel = (brandPageModel: BrandPageModel): void => {
54
- for (const page of brandPageModel.pages) {
55
- for (const content of page.contents) {
56
- convertRteStringsToJson(content);
57
- }
58
- }
59
- };
60
-
61
- /**
62
- * HTML→JSON parsing (Tiptap's generateJSON, via toRteJson) needs `window.DOMParser`.
63
- * In pure-node SSR there is no global DOM, so we provide a jsdom-backed window.
64
- *
65
- * Memoised: JSDOM construction is relatively expensive, so one window is created
66
- * per process and reused. The parser is stateless (each parseFromString builds a
67
- * fresh document), so sharing the window across requests is safe.
68
- *
69
- * `jsdom` is dynamically imported via a runtime-assembled specifier (see
70
- * getServerWindow), so no bundler can follow it into a browser bundle — this
71
- * branch only runs when there is no global `window`.
72
- */
73
- type ServerWindow = { DOMParser: unknown; document: unknown };
74
-
75
- // Minimal shape of the jsdom module, declared locally rather than via an
76
- // `import()` type, so the specifier "jsdom" never appears anywhere a bundler
77
- // could follow it (value or type position). Only the members used below.
78
- type JsdomModule = { JSDOM: new (html?: string) => { window: ServerWindow } };
79
-
80
- let serverWindowPromise: Promise<ServerWindow> | null = null;
81
-
82
- const getServerWindow = (): Promise<ServerWindow> => {
83
- if (!serverWindowPromise) {
84
- // The specifier is assembled at runtime rather than written as the string
85
- // literal "jsdom", so no bundler (Rollup/Vite in the library build, and any
86
- // downstream browser consumer — BrandCrowd.Nuxt, web-renderer, …) can
87
- // statically follow it and try to bundle jsdom → cssstyle, whose generated
88
- // code breaks Rollup's CommonJS parser. The library stays self-contained
89
- // (it still loads its own jsdom at runtime on the server) while never
90
- // leaking jsdom into a browser bundle. `@vite-ignore` silences the
91
- // "cannot be analyzed" warning the non-literal specifier would otherwise
92
- // trigger. At Node runtime this resolves the real jsdom from node_modules.
93
- const jsdomModuleId = ['js', 'dom'].join('');
94
-
95
- serverWindowPromise = (
96
- import(/* @vite-ignore */ jsdomModuleId) as Promise<JsdomModule>
97
- )
98
- .then(({ JSDOM }) => new JSDOM('').window)
99
- .catch((error) => {
100
- // Don't cache the failure — let a later migration retry the import.
101
- serverWindowPromise = null;
102
-
103
- throw error;
104
- });
105
- }
106
-
107
- return serverWindowPromise;
108
- };
109
-
110
- export const migration20 = async (
111
- migrationResult: MigrationData,
112
- ): Promise<MigrationData> => {
113
- const { brandPageModel } = migrationResult;
114
-
115
- // Browser / jsdom test: a DOM is already present — convert directly.
116
- if (typeof window !== 'undefined' && typeof document !== 'undefined') {
117
- convertModel(brandPageModel);
118
-
119
- return migrationResult;
120
- }
121
-
122
- // Pure-node SSR: install a memoised jsdom window as the global for the duration
123
- // of the conversion pass, then restore. The pass is fully synchronous (no await
124
- // between install and restore), so in Node's single thread no concurrent request
125
- // can observe the temporary globals.
126
- const serverWindow = await getServerWindow();
127
- const globals = globalThis as unknown as {
128
- window?: unknown;
129
- document?: unknown;
130
- };
131
- // Track whether the globals existed so we restore exactly — deleting keys we
132
- // added rather than leaving `window: undefined` own-properties behind.
133
- const hadWindow = 'window' in globals;
134
- const hadDocument = 'document' in globals;
135
- const prevWindow = globals.window;
136
- const prevDocument = globals.document;
137
-
138
- globals.window = serverWindow;
139
- globals.document = serverWindow.document;
140
-
141
- try {
142
- convertModel(brandPageModel);
143
- } finally {
144
- if (hadWindow) globals.window = prevWindow;
145
- else delete globals.window;
146
-
147
- if (hadDocument) globals.document = prevDocument;
148
- else delete globals.document;
149
- }
150
-
151
- return migrationResult;
152
- };