@designcrowd/library.brand-page 6.0.25 → 6.0.26-json-prereq-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 (176) hide show
  1. package/lib/src/{CreatedOnBrandCrowd-de-de-CS3ct0wa.js → CreatedOnBrandCrowd-de-de-CKBqjDH3.js} +1 -1
  2. package/lib/src/{CreatedOnBrandCrowd-en-us-CmBcnDu6.js → CreatedOnBrandCrowd-en-us-CRXiimvT.js} +1 -1
  3. package/lib/src/{CreatedOnBrandCrowd-es-es-BdTuJtqV.js → CreatedOnBrandCrowd-es-es-DCYdY6sj.js} +1 -1
  4. package/lib/src/{CreatedOnBrandCrowd-fr-fr-Cx_H9X6l.js → CreatedOnBrandCrowd-fr-ca-Dk7LOhxE.js} +1 -1
  5. package/lib/src/{CreatedOnBrandCrowd-fr-ca-Cx_H9X6l.js → CreatedOnBrandCrowd-fr-fr-Dk7LOhxE.js} +1 -1
  6. package/lib/src/{CreatedOnBrandCrowd-pt-pt-BZZ9hjUi.js → CreatedOnBrandCrowd-pt-br-Cp4ufHJP.js} +1 -1
  7. package/lib/src/{CreatedOnBrandCrowd-pt-br-BZZ9hjUi.js → CreatedOnBrandCrowd-pt-pt-Cp4ufHJP.js} +1 -1
  8. package/lib/src/{CreatedOnDcom-de-de-SbONDQN1.js → CreatedOnDcom-de-de-ZSLkWMwM.js} +1 -1
  9. package/lib/src/{CreatedOnDcom-en-us-Beno1_gw.js → CreatedOnDcom-en-us-BgdIGfT3.js} +1 -1
  10. package/lib/src/{CreatedOnDcom-es-es-BBE_zdRu.js → CreatedOnDcom-es-es-D0KemkCZ.js} +1 -1
  11. package/lib/src/{CreatedOnDcom-fr-ca-C6pgDB5x.js → CreatedOnDcom-fr-ca-BuwNjNbC.js} +1 -1
  12. package/lib/src/{CreatedOnDcom-fr-fr-FD1eqtg3.js → CreatedOnDcom-fr-fr-DCQcsFPf.js} +1 -1
  13. package/lib/src/{CreatedOnDcom-pt-br-BiSx3OwM.js → CreatedOnDcom-pt-br-DD-oLAS1.js} +1 -1
  14. package/lib/src/{CreatedOnDcom-pt-pt-Dn4DmKGA.js → CreatedOnDcom-pt-pt-D8qLq2wW.js} +1 -1
  15. package/lib/src/brand-page/components/contact-form/ContactForm.mixin.js +5 -2
  16. package/lib/src/brand-page/models/index.d.ts +34 -26
  17. package/lib/src/brand-page/utils/migration/migration-20.d.ts +2 -0
  18. package/lib/src/brand-page/utils/migration/migration-20.js +66 -0
  19. package/lib/src/brand-page/utils/migration.util.js +11 -0
  20. package/lib/src/brand-page/utils/text.util.d.ts +1 -1
  21. package/lib/src/brand-page/utils/text.util.js +32 -0
  22. package/lib/src/brand-page/utils/vcard.util.js +4 -4
  23. package/lib/src/brand-page/utils/view-model.util.d.ts +1 -0
  24. package/lib/src/brand-page/utils/view-model.util.js +68 -20
  25. package/lib/src/brand-page/view-models.d.ts +3 -3
  26. package/lib/src/brand-page-maker/__fixtures__/complex-rte.fixture.d.ts +22 -0
  27. package/lib/src/brand-page-maker/__fixtures__/complex-rte.fixture.js +22 -0
  28. package/lib/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.mixin.d.ts +1 -1
  29. package/lib/src/brand-page-maker/components/seo-settings/SeoPageSettings.mixin.d.ts +1 -1
  30. package/lib/src/brand-page-maker/composables/ai-chat/useAiMetadataBuilder.d.ts +0 -1
  31. package/lib/src/brand-page-maker/composables/ai-chat/useAiWebsiteBuilderChat.js +21 -9
  32. package/lib/src/brand-page-maker/composables/rich-text-editor/bubbleMenu.d.ts +2 -9
  33. package/lib/src/brand-page-maker/composables/rich-text-editor/bubbleMenu.js +6 -10
  34. package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/FontFamily.d.ts +14 -0
  35. package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/FontFamily.js +39 -0
  36. package/lib/src/brand-page-maker/composables/rich-text-editor/getRteExtensions.d.ts +28 -0
  37. package/lib/src/brand-page-maker/composables/rich-text-editor/getRteExtensions.js +90 -0
  38. package/lib/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.d.ts +1 -1
  39. package/lib/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.js +24 -103
  40. package/lib/src/brand-page-maker/stores/ai-thread/ai-website-builder-thread-store.d.ts +8 -2
  41. package/lib/src/brand-page-maker/stores/ai-thread/ai-website-builder-thread-store.js +20 -0
  42. package/lib/src/brand-page-maker/stores/brandpage-maker/brandpage-maker-store.js +7 -0
  43. package/lib/src/brand-page-maker/utils/index.d.ts +2 -0
  44. package/lib/src/brand-page-maker/utils/index.js +2 -0
  45. package/lib/src/brand-page-maker/utils/rich-text-editor.util.d.ts +28 -5
  46. package/lib/src/brand-page-maker/utils/rich-text-editor.util.js +48 -5
  47. package/lib/src/brand-page-maker/utils/rte-render.util.d.ts +63 -0
  48. package/lib/src/brand-page-maker/utils/rte-render.util.js +95 -0
  49. package/lib/src/brand-page-maker/utils/rte-sanitize.util.d.ts +6 -0
  50. package/lib/src/brand-page-maker/utils/rte-sanitize.util.js +158 -0
  51. package/lib/src/brand-page-maker/validators/field.validator.js +10 -2
  52. package/lib/src/css/library.brand-page-brandCrowd.min.css +39 -39
  53. package/lib/src/css/library.brand-page-brandPage.min.css +39 -39
  54. package/lib/src/css/library.brand-page-designCom.min.css +39 -39
  55. package/lib/src/css/library.brand-page-preview.min.css +39 -39
  56. package/lib/src/index.d.ts +1 -0
  57. package/lib/src/index.mjs +143 -138
  58. package/lib/src/{lib-BVHFYSLL.js → lib-Db6j9fQZ.js} +56262 -52768
  59. package/lib/src/lib.js +2 -0
  60. package/lib/src/metadata-builder/ai-metadata-executor.d.ts +0 -1
  61. package/lib/src/metadata-builder/ai-metadata-executor.js +0 -19
  62. package/lib/src/metadata-builder/builders/section-builders/about-section-builder.js +2 -3
  63. package/lib/src/metadata-builder/builders/section-builders/banner-section-builder.js +2 -3
  64. package/lib/src/metadata-builder/builders/section-builders/booking-section-builder.js +2 -3
  65. package/lib/src/metadata-builder/builders/section-builders/contact-form-builder.js +3 -4
  66. package/lib/src/metadata-builder/builders/section-builders/images-section-builder.js +2 -3
  67. package/lib/src/metadata-builder/builders/section-builders/mailing-list-builder.js +1 -2
  68. package/lib/src/metadata-builder/builders/section-builders/mailing-list-section-builder.js +1 -2
  69. package/lib/src/metadata-builder/builders/section-builders/profile-section-builder.js +3 -4
  70. package/lib/src/metadata-builder/builders/section-builders/shapes-section-builder.js +2 -3
  71. package/lib/src/metadata-builder/builders/section-builders/testimonial-section-builder.js +1 -2
  72. package/lib/src/metadata-builder/builders/section-builders/text-section-builder.js +15 -11
  73. package/lib/src/metadata-builder/helpers/rte.helpers.d.ts +11 -13
  74. package/lib/src/metadata-builder/helpers/rte.helpers.js +14 -73
  75. package/lib/src/metadata-builder/metadata-executor.js +0 -4
  76. package/lib/src/metadata-builder/models.d.ts +0 -2
  77. package/lib/src/metadata-builder/models.js +0 -2
  78. package/lib/src/metadata-builder/parsers/ai-toolcall-parser.d.ts +0 -19
  79. package/lib/src/metadata-builder/parsers/ai-toolcall-parser.js +0 -4
  80. package/lib/src/metadata-builder/sanitizers/rte-schema-check.d.ts +7 -8
  81. package/lib/src/metadata-builder/sanitizers/rte-schema-check.js +21 -52
  82. package/lib/src/shared/content-settings/components/GenericTextInput.mixin.d.ts +2 -2
  83. package/lib/src/shared/content-settings/components/GenericTextInput.mixin.js +14 -1
  84. package/lib/src/shared/content-settings/components/content-settings/ContentSettings.mixin.d.ts +1 -1
  85. package/lib/src/shared/content-settings/components/content-settings/ReplaceImageSettingsForContent.mixin.d.ts +1 -1
  86. package/lib/src/shared/content-settings/components/content-settings-item/ContentSettingsItemField.mixin.js +12 -3
  87. package/lib/src/shared/content-settings/components/email-field/EmailField.mixin.d.ts +2 -2
  88. package/lib/src/shared/content-settings/components/radio-field/RadioField.mixin.d.ts +1 -1
  89. package/lib/src/shared/content-settings/components/slider-field/SliderField.mixin.d.ts +1 -1
  90. package/lib/src/shared/content-settings/components/tel-field/TelField.mixin.d.ts +2 -2
  91. package/lib/src/shared/content-settings/components/testimonial-items-field/TestimonialItemsField.mixin.d.ts +3 -2
  92. package/lib/src/shared/content-settings/components/testimonial-items-field/TestimonialItemsField.mixin.js +10 -3
  93. package/lib/src/shared/content-settings/components/text-field/TextField.mixin.d.ts +2 -2
  94. package/lib/src/shared/content-settings/components/text-items-field/TextItemsField.mixin.d.ts +2 -1
  95. package/lib/src/shared/content-settings/components/text-items-field/TextItemsField.mixin.js +4 -1
  96. package/lib/src/shared/content-settings/components/textarea-field/TextareaField.mixin.d.ts +2 -2
  97. package/lib/src/shared/content-settings/components/url-field/UrlField.mixin.d.ts +2 -2
  98. package/lib/src/shared/content-settings/components/youtube-video-field/YoutubeVideoField.mixin.d.ts +2 -2
  99. package/lib/src/shared/side-bar/components/SideBar.mixin.d.ts +1 -1
  100. package/lib/src/utils/minify-brand-page-metadata.js +4 -1
  101. package/package.json +17 -15
  102. package/src/brand-page/components/contact-form/ContactForm.mixin.ts +5 -2
  103. package/src/brand-page/components/text-wrapper/TextWrapper.vue +17 -6
  104. package/src/brand-page/models/index.ts +35 -26
  105. package/src/brand-page/utils/__snapshots__/migration.util.test.ts.snap +3 -3
  106. package/src/brand-page/utils/migration/migration-20.server.test.ts +125 -0
  107. package/src/brand-page/utils/migration/migration-20.test.ts +72 -0
  108. package/src/brand-page/utils/migration/migration-20.ts +87 -0
  109. package/src/brand-page/utils/migration.util.ts +12 -0
  110. package/src/brand-page/utils/style.util.test.ts +51 -0
  111. package/src/brand-page/utils/text.util.test.ts +47 -0
  112. package/src/brand-page/utils/text.util.ts +43 -4
  113. package/src/brand-page/utils/vcard.util.ts +6 -4
  114. package/src/brand-page/utils/view-model.pageid-rte.test.ts +104 -0
  115. package/src/brand-page/utils/view-model.util.ts +94 -26
  116. package/src/brand-page/view-models.ts +3 -2
  117. package/src/brand-page-maker/__fixtures__/complex-rte.fixture.ts +23 -0
  118. package/src/brand-page-maker/components/rich-text-editor/RichTextEditor.vue +4 -3
  119. package/src/brand-page-maker/composables/ai-chat/useAiMetadataBuilder.ts +0 -1
  120. package/src/brand-page-maker/composables/ai-chat/useAiWebsiteBuilderChat.ts +28 -9
  121. package/src/brand-page-maker/composables/rich-text-editor/bubbleMenu.ts +6 -10
  122. package/src/brand-page-maker/composables/rich-text-editor/extensions/FontFamily.test.ts +52 -0
  123. package/src/brand-page-maker/composables/rich-text-editor/extensions/FontFamily.ts +42 -0
  124. package/src/brand-page-maker/composables/rich-text-editor/getRteExtensions.ts +137 -0
  125. package/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.ts +27 -111
  126. package/src/brand-page-maker/stores/ai-thread/ai-website-builder-thread-store.ts +23 -0
  127. package/src/brand-page-maker/stores/brandpage-maker/brandpage-maker-store.ts +8 -0
  128. package/src/brand-page-maker/utils/__snapshots__/rte-render-parity.test.ts.snap +202 -0
  129. package/src/brand-page-maker/utils/index.ts +2 -0
  130. package/src/brand-page-maker/utils/rich-text-editor.util.test.ts +37 -3
  131. package/src/brand-page-maker/utils/rich-text-editor.util.ts +66 -11
  132. package/src/brand-page-maker/utils/rte-features.test.ts +351 -0
  133. package/src/brand-page-maker/utils/rte-render-parity.test.ts +157 -0
  134. package/src/brand-page-maker/utils/rte-render.util.ts +165 -0
  135. package/src/brand-page-maker/utils/rte-sanitize-json.test.ts +158 -0
  136. package/src/brand-page-maker/utils/rte-sanitize.util.ts +195 -0
  137. package/src/brand-page-maker/validators/field.validator.test.ts +55 -0
  138. package/src/brand-page-maker/validators/field.validator.ts +10 -2
  139. package/src/lib.ts +9 -0
  140. package/src/metadata-builder/ai-metadata-executor.ts +0 -26
  141. package/src/metadata-builder/builders/section-builders/about-section-builder.ts +2 -3
  142. package/src/metadata-builder/builders/section-builders/banner-section-builder.ts +2 -3
  143. package/src/metadata-builder/builders/section-builders/booking-section-builder.ts +2 -5
  144. package/src/metadata-builder/builders/section-builders/contact-form-builder.ts +3 -4
  145. package/src/metadata-builder/builders/section-builders/images-section-builder.ts +2 -3
  146. package/src/metadata-builder/builders/section-builders/mailing-list-builder.ts +1 -2
  147. package/src/metadata-builder/builders/section-builders/mailing-list-section-builder.ts +1 -2
  148. package/src/metadata-builder/builders/section-builders/profile-section-builder.ts +3 -6
  149. package/src/metadata-builder/builders/section-builders/shapes-section-builder.ts +2 -3
  150. package/src/metadata-builder/builders/section-builders/testimonial-section-builder.ts +1 -2
  151. package/src/metadata-builder/builders/section-builders/text-section-builder.ts +19 -13
  152. package/src/metadata-builder/helpers/rte.helpers.ts +15 -101
  153. package/src/metadata-builder/metadata-executor.ts +0 -5
  154. package/src/metadata-builder/models.ts +0 -3
  155. package/src/metadata-builder/parsers/ai-toolcall-parser.ts +0 -27
  156. package/src/metadata-builder/sanitizers/rte-schema-check.ts +24 -53
  157. package/src/shared/content-settings/components/GenericTextInput.mixin.ts +17 -1
  158. package/src/shared/content-settings/components/content-settings-item/ContentSettingsItemField.mixin.ts +16 -3
  159. package/src/shared/content-settings/components/testimonial-items-field/TestimonialItemsField.mixin.ts +13 -4
  160. package/src/shared/content-settings/components/testimonial-items-field/TestimonialItemsField.vue +6 -4
  161. package/src/shared/content-settings/components/text-image-field/TextImageField.vue +6 -3
  162. package/src/shared/content-settings/components/text-items-field/TextItemsField.mixin.ts +6 -1
  163. package/src/shared/content-settings/components/text-items-field/TextItemsField.vue +1 -1
  164. package/src/utils/minify-brand-page-metadata.ts +4 -1
  165. package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/FontSize.d.ts +0 -20
  166. package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/FontSize.js +0 -44
  167. package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/LineHeight.d.ts +0 -27
  168. package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/LineHeight.js +0 -44
  169. package/lib/src/metadata-builder/builders/update-content-text-builder.d.ts +0 -17
  170. package/lib/src/metadata-builder/builders/update-content-text-builder.js +0 -103
  171. package/lib/src/metadata-builder/builders/update-content-text-style-builder.d.ts +0 -16
  172. package/lib/src/metadata-builder/builders/update-content-text-style-builder.js +0 -216
  173. package/src/brand-page-maker/composables/rich-text-editor/extensions/FontSize.ts +0 -73
  174. package/src/brand-page-maker/composables/rich-text-editor/extensions/LineHeight.ts +0 -85
  175. package/src/metadata-builder/builders/update-content-text-builder.ts +0 -148
  176. package/src/metadata-builder/builders/update-content-text-style-builder.ts +0 -352
@@ -1,6 +1,6 @@
1
- import type { ChainedCommands, Editor } from '@tiptap/vue-3';
1
+ import type { ChainedCommands, Editor, JSONContent } from '@tiptap/vue-3';
2
2
  import type { EditorState, Transaction, Selection } from 'prosemirror-state';
3
- import { type Content, type TextStyleDeclaration, type TextStyleType } from '@brand-page';
3
+ import { type Content, type TextStyleDeclaration, type TextStyleType, type RichTextValue } from '@brand-page';
4
4
  import type { PageSettingsState } from '@shared/page-settings/store';
5
5
  import type { StyleSettingsState } from '@shared/style-settings/store';
6
6
  import type { Mark } from 'prosemirror-model';
@@ -9,12 +9,31 @@ export declare const RTE_ALLOWED_SECTION_TYPES: string[];
9
9
  export declare const isRTEManagedStyleField: (sectionType: string, fieldName: string) => boolean;
10
10
  export declare const isStyleChanged: (tr: Transaction) => boolean;
11
11
  export declare const skipStyleDetection: (chain: ChainedCommands, marks?: Mark[]) => ChainedCommands;
12
- export declare const hasRTEContent: (content: string) => boolean;
13
- export declare const getPlainTextFromRTE: (content: string) => string;
12
+ /**
13
+ * BP-5787 — canonical discriminator for the JSON RTE model: a ProseMirror
14
+ * document is an object with `type: 'doc'`.
15
+ */
16
+ export declare const isTiptapDocument: (value: unknown) => value is JSONContent;
17
+ /**
18
+ * Extract visible plain text from a Tiptap JSON document (concatenated text
19
+ * nodes). Dependency-free — no editor instantiation.
20
+ */
21
+ export declare const getRteJsonPlainText: (doc: JSONContent) => string;
22
+ export declare const hasRTEContent: (content: unknown) => boolean;
23
+ export declare const getPlainTextFromRTE: (content: unknown) => string;
24
+ /**
25
+ * BP-5787 — visible-text length of an RTE value, for the maker item-field
26
+ * max-length / emptiness checks. A plain string keeps its raw `.length`, so the
27
+ * pre-JSON behaviour is byte-for-byte identical; a Tiptap JSON doc measures its
28
+ * concatenated visible text. Without this, `doc.length` is `undefined` (silently
29
+ * disabling the check) and `doc.trim()` throws — the class of bug that survived
30
+ * the `RichTextValue` widening because Vue Options-API `this`/item access is `any`.
31
+ */
32
+ export declare const getRteTextLength: (value: RichTextValue | null | undefined) => number;
14
33
  export declare const replaceTextInRTE: (existingHtml: string, newText: string) => string;
15
34
  export declare const transformToRTE: (uiPageSettingStore: PageSettingsState, externalEditor: Editor, uiStyleSettingsStore?: StyleSettingsState, newContent?: Content, type?: TextStyleType, newStyle?: TextStyleDeclaration) => void;
16
35
  export declare const switchAllContentToRTE: (uiPageSettingStore: PageSettingsState, externalEditor: Editor, uiStyleSettingsStore?: StyleSettingsState) => void;
17
- export declare const getUpdatedHTMLFromEditor: (externalEditor: Editor, contentText: string, newContentStyle: TextStyleDeclaration) => string;
36
+ export declare const getUpdatedHTMLFromEditor: (externalEditor: Editor, contentText: RichTextValue, newContentStyle: TextStyleDeclaration) => string | import("@tiptap/core").DocumentType<Record<string, any> | undefined, import("@tiptap/core").NodeType<string, Record<string, any> | undefined, any, (import("@tiptap/core").NodeType<any, any, any, any> | import("@tiptap/core").TextType<import("@tiptap/core").MarkType<any, any>>)[]>[]>;
18
37
  export declare const buildInitialMarks: (state: EditorState, style: TextStyleDeclaration) => any[];
19
38
  export declare const retainStyleOnEmptyLine: (editor: Editor, selection: Selection) => void;
20
39
  export interface SanitizationResult {
@@ -22,6 +41,10 @@ export interface SanitizationResult {
22
41
  removedElements: string[];
23
42
  }
24
43
  export declare const sanitiseClientSideRTE: (html: string) => SanitizationResult;
44
+ /**
45
+ * Validates if an href value is safe and returns a sanitized version
46
+ */
47
+ export declare const validateAndSanitizeHref: (href: string) => string | null;
25
48
  /**
26
49
  * Sanitize HTML string using DOMPurify.
27
50
  * Only allow tags and attributes that correspond to the enabled Tiptap extensions.
@@ -175,14 +175,41 @@ export const skipStyleDetection = (chain, marks) => chain.command(({ tr }) => {
175
175
  }
176
176
  return true;
177
177
  });
178
+ /**
179
+ * BP-5787 — canonical discriminator for the JSON RTE model: a ProseMirror
180
+ * document is an object with `type: 'doc'`.
181
+ */
182
+ export const isTiptapDocument = (value) => typeof value === 'object' &&
183
+ value !== null &&
184
+ value.type === 'doc';
185
+ /**
186
+ * Extract visible plain text from a Tiptap JSON document (concatenated text
187
+ * nodes). Dependency-free — no editor instantiation.
188
+ */
189
+ export const getRteJsonPlainText = (doc) => {
190
+ const parts = [];
191
+ const walk = (node) => {
192
+ if (typeof node.text === 'string')
193
+ parts.push(node.text);
194
+ node.content?.forEach(walk);
195
+ };
196
+ walk(doc);
197
+ return parts.join(' ').replace(/\s+/g, ' ').trim();
198
+ };
199
+ // Accepts a legacy inline-HTML string or a Tiptap JSON document (BP-5787).
178
200
  export const hasRTEContent = (content) => {
179
- if (!content || content.length === 0)
201
+ if (isTiptapDocument(content))
202
+ return true;
203
+ if (typeof content !== 'string' || content.length === 0)
180
204
  return false;
181
205
  const trimmed = content.trim();
182
206
  const tagWrapRegex = /^<([A-Za-z][A-Za-z0-9-]*)\b[^>]*>[\s\S]*<\/\1>$/;
183
207
  return tagWrapRegex.test(trimmed);
184
208
  };
185
209
  export const getPlainTextFromRTE = (content) => {
210
+ if (isTiptapDocument(content)) {
211
+ return getRteJsonPlainText(content);
212
+ }
186
213
  if (typeof content !== 'string') {
187
214
  return '';
188
215
  }
@@ -200,6 +227,19 @@ export const getPlainTextFromRTE = (content) => {
200
227
  }
201
228
  return plainText.trim();
202
229
  };
230
+ /**
231
+ * BP-5787 — visible-text length of an RTE value, for the maker item-field
232
+ * max-length / emptiness checks. A plain string keeps its raw `.length`, so the
233
+ * pre-JSON behaviour is byte-for-byte identical; a Tiptap JSON doc measures its
234
+ * concatenated visible text. Without this, `doc.length` is `undefined` (silently
235
+ * disabling the check) and `doc.trim()` throws — the class of bug that survived
236
+ * the `RichTextValue` widening because Vue Options-API `this`/item access is `any`.
237
+ */
238
+ export const getRteTextLength = (value) => {
239
+ if (typeof value === 'string')
240
+ return value.length;
241
+ return value ? getRteJsonPlainText(value).length : 0;
242
+ };
203
243
  export const replaceTextInRTE = (existingHtml, newText) => {
204
244
  if (!hasRTEContent(existingHtml)) {
205
245
  return `<p>${newText}</p>`;
@@ -474,7 +514,10 @@ export const getUpdatedHTMLFromEditor = (externalEditor, contentText, newContent
474
514
  if (!contentText) {
475
515
  return contentText;
476
516
  }
477
- const content = contentText.includes(ESCAPED_NEWLINE_CHARACTER)
517
+ // BP-5787: accept a string (legacy HTML) or a Tiptap JSON doc. setContent
518
+ // handles both; only the newline-escaping shim applies to strings.
519
+ const content = typeof contentText === 'string' &&
520
+ contentText.includes(ESCAPED_NEWLINE_CHARACTER)
478
521
  ? getRenderText(contentText)
479
522
  : contentText;
480
523
  externalEditor.commands.setContent(content);
@@ -518,8 +561,8 @@ export const getUpdatedHTMLFromEditor = (externalEditor, contentText, newContent
518
561
  : chain.unsetBold();
519
562
  chain = newContentStyle?.fontStyle ? chain.setItalic() : chain.unsetItalic();
520
563
  chain.run();
521
- const html = externalEditor.getHTML();
522
- return html;
564
+ // BP-5787: return the JSON form, consistent with how RTE is now stored.
565
+ return externalEditor.getJSON();
523
566
  };
524
567
  // Helper to build stored marks from a style declaration
525
568
  export const buildInitialMarks = (state, style) => {
@@ -598,7 +641,7 @@ export const sanitiseClientSideRTE = (html) => {
598
641
  /**
599
642
  * Validates if an href value is safe and returns a sanitized version
600
643
  */
601
- const validateAndSanitizeHref = (href) => {
644
+ export const validateAndSanitizeHref = (href) => {
602
645
  if (!href || typeof href !== 'string')
603
646
  return null;
604
647
  // Trim whitespace and decode entities
@@ -0,0 +1,63 @@
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
+ * The schema-driven core of both directions is Tiptap/ProseMirror, fed by the
16
+ * single-source-of-truth extension set (getRteExtensions) — so the mapping lives
17
+ * in the extensions' schema, not a hand-maintained copy that drifts:
18
+ *
19
+ * - renderRteToHtml uses @tiptap/static-renderer (renderToHTMLString). It is
20
+ * DOM-less, so it runs on the server (SSR/publish/preview), not just the
21
+ * browser, and needs no Editor instance.
22
+ * - toRteJson turns an HTML string into a DOM node with an INJECTED parser and
23
+ * feeds it to ProseMirror's schema-driven DOMParser. The HTML→DOM step needs
24
+ * a DOM parser; rather than read the global `window.DOMParser` (as Tiptap's
25
+ * `generateJSON` does — unusable on a pure-node server and fragile across SSR
26
+ * execution contexts), the parser is supplied by the caller: the native
27
+ * `DOMParser` in the browser, a jsdom `DOMParser` *instance* on the server
28
+ * (see resolveHtmlParser). No global is ever mutated, so the conversion is
29
+ * fully isomorphic regardless of how the SSR host bundles/executes this code.
30
+ */
31
+ export type RichTextValue = string | JSONContent;
32
+ /**
33
+ * A `DOMParser`-compatible object: turns an HTML string into a Document whose
34
+ * `body` is a DOM node ProseMirror can parse. The browser's native `DOMParser`
35
+ * and jsdom's `DOMParser` both satisfy this, so the DOM dependency is injected
36
+ * rather than read from a global.
37
+ */
38
+ export interface HtmlParser {
39
+ parseFromString(html: string, type: 'text/html'): Document;
40
+ }
41
+ /**
42
+ * Render an RTE value to final HTML. Legacy HTML strings pass through untouched
43
+ * (they are already HTML); Tiptap JSON is serialised by @tiptap/static-renderer
44
+ * from the extension schema — no editor, no DOM — so it works on the server
45
+ * (SSR/publish), not just the browser.
46
+ */
47
+ export declare const renderRteToHtml: (value: RichTextValue | null | undefined) => string;
48
+ /**
49
+ * Resolve an HtmlParser for the current runtime: the native `DOMParser` in the
50
+ * browser (and jsdom-based test envs), or a jsdom `DOMParser` INSTANCE on a
51
+ * pure-node server — created and returned directly, never assigned to a global,
52
+ * so it is immune to SSR execution-context differences. The jsdom specifier is
53
+ * assembled at runtime so no bundler follows it into a browser build. Memoised
54
+ * on the server (JSDOM construction is relatively expensive).
55
+ */
56
+ export declare const resolveHtmlParser: () => Promise<HtmlParser>;
57
+ /**
58
+ * Normalise an RTE value to a Tiptap JSON document. HTML strings are parsed with
59
+ * `parser` (defaults to the browser's native DOMParser); JSON documents pass
60
+ * through unchanged. Returns undefined for empty/blank input so callers can omit
61
+ * empty fields.
62
+ */
63
+ export declare const toRteJson: (value: RichTextValue | null | undefined, parser?: HtmlParser) => JSONContent | undefined;
@@ -0,0 +1,95 @@
1
+ import { getSchema } from '@tiptap/core';
2
+ import { DOMParser as ProseMirrorDOMParser } from '@tiptap/pm/model';
3
+ import { renderToHTMLString } from '@tiptap/static-renderer/pm/html-string';
4
+ import { getRteExtensions } from '../composables/rich-text-editor/getRteExtensions';
5
+ import { isTiptapDocument, getRteJsonPlainText } from './rich-text-editor.util';
6
+ export { isTiptapDocument, getRteJsonPlainText };
7
+ // One extension set shared by both directions (render + parse). Schema knowledge
8
+ // lives here, once — Tiptap/extension upgrades propagate to both automatically.
9
+ const RTE_EXTENSIONS = getRteExtensions({ includeLinkRel: true });
10
+ // ProseMirror schema derived once from the shared extension set. This is the
11
+ // schema-driven heart of HTML→JSON (no drift); only the HTML→DOM step is injected.
12
+ const RTE_SCHEMA = getSchema(RTE_EXTENSIONS);
13
+ /**
14
+ * Render an RTE value to final HTML. Legacy HTML strings pass through untouched
15
+ * (they are already HTML); Tiptap JSON is serialised by @tiptap/static-renderer
16
+ * from the extension schema — no editor, no DOM — so it works on the server
17
+ * (SSR/publish), not just the browser.
18
+ */
19
+ export const renderRteToHtml = (value) => {
20
+ if (value == null || value === '')
21
+ return '';
22
+ if (typeof value === 'string')
23
+ return value;
24
+ return renderToHTMLString({ content: value, extensions: RTE_EXTENSIONS });
25
+ };
26
+ // Tiptap's elementFromString strips whitespace-only text nodes (pretty-print
27
+ // indentation between tags) before parsing; replicate it so injected-parser
28
+ // output stays identical to the previous generateJSON-based conversion.
29
+ const stripIndentationTextNodes = (node) => {
30
+ const children = node.childNodes;
31
+ for (let i = children.length - 1; i >= 0; i -= 1) {
32
+ const child = children[i];
33
+ if (child.nodeType === 3 &&
34
+ child.nodeValue &&
35
+ /^(\n\s\s|\n)$/.test(child.nodeValue)) {
36
+ node.removeChild(child);
37
+ }
38
+ else if (child.nodeType === 1) {
39
+ stripIndentationTextNodes(child);
40
+ }
41
+ }
42
+ return node;
43
+ };
44
+ const htmlToRteJson = (html, parser) => {
45
+ const body = parser.parseFromString(`<body>${html}</body>`, 'text/html').body;
46
+ const dom = stripIndentationTextNodes(body);
47
+ return ProseMirrorDOMParser.fromSchema(RTE_SCHEMA).parse(dom).toJSON();
48
+ };
49
+ let serverParserPromise = null;
50
+ /**
51
+ * Resolve an HtmlParser for the current runtime: the native `DOMParser` in the
52
+ * browser (and jsdom-based test envs), or a jsdom `DOMParser` INSTANCE on a
53
+ * pure-node server — created and returned directly, never assigned to a global,
54
+ * so it is immune to SSR execution-context differences. The jsdom specifier is
55
+ * assembled at runtime so no bundler follows it into a browser build. Memoised
56
+ * on the server (JSDOM construction is relatively expensive).
57
+ */
58
+ export const resolveHtmlParser = async () => {
59
+ if (typeof DOMParser !== 'undefined')
60
+ return new DOMParser();
61
+ if (!serverParserPromise) {
62
+ const jsdomModuleId = ['js', 'dom'].join('');
63
+ serverParserPromise = import(/* @vite-ignore */ jsdomModuleId)
64
+ .then((mod) => {
65
+ const { JSDOM } = mod;
66
+ const { DOMParser: JsdomDOMParser } = new JSDOM('').window;
67
+ return new JsdomDOMParser();
68
+ })
69
+ .catch((error) => {
70
+ // Don't cache the failure — let a later call retry the import.
71
+ serverParserPromise = null;
72
+ throw error;
73
+ });
74
+ }
75
+ return serverParserPromise;
76
+ };
77
+ const getDefaultParser = () => {
78
+ if (typeof DOMParser !== 'undefined')
79
+ return new DOMParser();
80
+ throw new Error('toRteJson: no global DOMParser in this environment. On the server, pass an ' +
81
+ 'explicit parser as the second argument (see resolveHtmlParser).');
82
+ };
83
+ /**
84
+ * Normalise an RTE value to a Tiptap JSON document. HTML strings are parsed with
85
+ * `parser` (defaults to the browser's native DOMParser); JSON documents pass
86
+ * through unchanged. Returns undefined for empty/blank input so callers can omit
87
+ * empty fields.
88
+ */
89
+ export const toRteJson = (value, parser) => {
90
+ if (value == null || value === '')
91
+ return undefined;
92
+ if (isTiptapDocument(value))
93
+ return value;
94
+ return htmlToRteJson(value, parser ?? getDefaultParser());
95
+ };
@@ -0,0 +1,6 @@
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;
@@ -0,0 +1,158 @@
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 +1,5 @@
1
1
  import { isURL, isEmail, isTel } from '@utils/validator.util';
2
- import { YOUTUBE_URL_RE } from '@brand-page-maker/utils';
2
+ import { YOUTUBE_URL_RE, isTiptapDocument } from '@brand-page-maker/utils';
3
3
  import { validatorTr } from '@src/composables/useTranslate';
4
4
  const addressValueValidator = {
5
5
  validate: (name, value) => {
@@ -62,7 +62,15 @@ export const dummyValidator = {
62
62
  };
63
63
  const nonEmptyStringValidator = {
64
64
  validate: (name, value) => {
65
- const isValid = typeof value === 'string' && value.length > 0;
65
+ // BP-5787/BP-5857: match legacy (pre-JSON) validation semantics.
66
+ // An RTE field's stored value used to be inline HTML — an empty editor was
67
+ // "<p></p>", a NON-empty string, so the required check always passed for RTE
68
+ // fields (it only ever failed for genuinely empty PLAIN inputs). Now RTE
69
+ // fields are Tiptap JSON docs; treat any doc as "present" so behaviour
70
+ // stays identical. A plain string is still required to be non-empty.
71
+ const isValid = isTiptapDocument(value)
72
+ ? true
73
+ : typeof value === 'string' && value.length > 0;
66
74
  return {
67
75
  isValid,
68
76
  message: isValid ? '' : validatorTr(`emptyStringMessage`),