@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
@@ -30,6 +30,15 @@ import type { LogoImageModel, LogoTemplateImageModel } from './logo.model';
30
30
  import cloneDeep from 'lodash.clonedeep';
31
31
  import * as uuid from 'uuid';
32
32
  import type { BrandPagePaymentConfig } from './payment.model';
33
+ import type { JSONContent } from '@tiptap/vue-3';
34
+
35
+ /**
36
+ * BP-5787 — a rich-text field is either a legacy inline-HTML string or a
37
+ * Tiptap JSON document. Typing RTE content fields as RichTextValue (instead of
38
+ * `string`) forces every consumer to handle both forms — surfacing at compile
39
+ * time any code that assumes a plain string (e.g. `.replace`, `.length`).
40
+ */
41
+ export type RichTextValue = string | JSONContent;
33
42
 
34
43
  export * from './brand-page-context.model';
35
44
  export * from './payment.model';
@@ -280,7 +289,7 @@ export interface TextContent extends Content {
280
289
  /**
281
290
  * For BrandPage/BrandContact only
282
291
  */
283
- text: string;
292
+ text: RichTextValue;
284
293
  /**
285
294
  * For BrandSite
286
295
  */
@@ -304,9 +313,9 @@ export interface TextContent extends Content {
304
313
 
305
314
  export interface ProfileContent extends Content {
306
315
  avatarImageUrl: string;
307
- name: string;
308
- title: string;
309
- description: string;
316
+ name: RichTextValue;
317
+ title: RichTextValue;
318
+ description: RichTextValue;
310
319
  backgroundColor: BrandPageCssStyle;
311
320
  colorReplacements: ColorReplacement[];
312
321
  mediaType?: string;
@@ -327,7 +336,7 @@ export interface MapContent extends Content {
327
336
 
328
337
  export interface MailingListContent extends Content {
329
338
  acceptedFields?: 'emailOnly' | 'nameAndEmail';
330
- title: string;
339
+ title: RichTextValue;
331
340
  buttonText: string;
332
341
  titleStyle: TextStyleDeclaration;
333
342
  buttonTextStyle: TextStyleDeclaration;
@@ -387,10 +396,10 @@ export interface FormField {
387
396
  export interface ContactFormContent extends Content {
388
397
  type: 'contact-form';
389
398
  displayStyle: string;
390
- title: string;
391
- description: string;
399
+ title: RichTextValue;
400
+ description: RichTextValue;
392
401
  buttonText: string;
393
- successMessage: string;
402
+ successMessage: RichTextValue;
394
403
  fields: FormField[];
395
404
  responseEmail: string;
396
405
  imageUrl?: string;
@@ -410,8 +419,8 @@ export interface ContactFormContent extends Content {
410
419
  export interface BannerSectionContent extends Content {
411
420
  type: 'banner-section';
412
421
  imageUrl: string;
413
- title: string;
414
- description: string;
422
+ title: RichTextValue;
423
+ description: RichTextValue;
415
424
  buttonText: string | false;
416
425
  url: string;
417
426
  email?: string;
@@ -441,8 +450,8 @@ export const MEDIA_TYPES = {
441
450
  export type MediaType = keyof typeof MEDIA_TYPES;
442
451
 
443
452
  export interface TextMediaItem extends ImageItem {
444
- title: string;
445
- paragraph: string;
453
+ title: RichTextValue;
454
+ paragraph: RichTextValue;
446
455
  itemsId: number;
447
456
 
448
457
  buttonText?: string | false;
@@ -466,8 +475,8 @@ export interface TextShapeItem extends TextMediaItem {
466
475
  export interface ShapeItem extends ImageItem {
467
476
  colorReplacements: ColorReplacement[];
468
477
  mediaType: MediaType;
469
- title?: string;
470
- paragraph?: string;
478
+ title?: RichTextValue;
479
+ paragraph?: RichTextValue;
471
480
 
472
481
  // overrides for personalisation
473
482
  titleOverride?: string;
@@ -478,9 +487,9 @@ export interface TestimonialItems {
478
487
  /** Stable per-item identifier. See ImageItem.id. */
479
488
  id: string;
480
489
  imageUrl: string;
481
- name: string;
482
- position: string;
483
- testimonials: string;
490
+ name: RichTextValue;
491
+ position: RichTextValue;
492
+ testimonials: RichTextValue;
484
493
  colorReplacements: ColorReplacement[];
485
494
  mediaType: string;
486
495
  size?: string;
@@ -496,7 +505,7 @@ export interface TestimonialItems {
496
505
  export interface TextItem {
497
506
  /** Stable per-item identifier. See ImageItem.id. */
498
507
  id: string;
499
- text: string;
508
+ text: RichTextValue;
500
509
  textStyle: TextStyleDeclaration;
501
510
 
502
511
  // overrides for personalisation
@@ -518,8 +527,8 @@ export interface ImagesSectionContent extends Content, MediaSection {
518
527
  }
519
528
 
520
529
  export interface MediaSection extends Content {
521
- title: string;
522
- description: string;
530
+ title: RichTextValue;
531
+ description: RichTextValue;
523
532
  displayStyle: ImagesSectionContentDisplayStyle;
524
533
  items: TextShapeItem[] | TextMediaItem[];
525
534
  titleStyle: TextStyleDeclaration;
@@ -538,7 +547,7 @@ export interface ColorReplacement {
538
547
 
539
548
  export interface TestimonialSectionContent extends Content {
540
549
  type: 'testimonial-section';
541
- title: string;
550
+ title: RichTextValue;
542
551
  displayStyle: string;
543
552
  items: TestimonialItems[];
544
553
  titleStyle: TextStyleDeclaration;
@@ -553,8 +562,8 @@ export interface TestimonialSectionContent extends Content {
553
562
  export interface AboutSectionContent extends Content {
554
563
  type: 'about-section';
555
564
  imageUrl: string;
556
- title: string;
557
- paragraph: string;
565
+ title: RichTextValue;
566
+ paragraph: RichTextValue;
558
567
  displayStyle: string;
559
568
  titleStyle: TextStyleDeclaration;
560
569
  paragraphStyle: TextStyleDeclaration;
@@ -583,7 +592,7 @@ export type SocialLinksIconDisplayStyle =
583
592
 
584
593
  export interface SocialLinksContent extends Content {
585
594
  type: 'social-links';
586
- title: string;
595
+ title: RichTextValue;
587
596
  links: LinksItem[];
588
597
  iconDisplayStyle: SocialLinksIconDisplayStyle;
589
598
  iconFillStyles: BrandPageCssStyle;
@@ -610,8 +619,8 @@ export interface DividerSectionContent extends Content {
610
619
 
611
620
  export interface BookingSectionContent extends Content {
612
621
  type: 'booking-section';
613
- title: string;
614
- description: string;
622
+ title: RichTextValue;
623
+ description: RichTextValue;
615
624
  buttonText: string;
616
625
  url: string;
617
626
  email?: string;
@@ -462,7 +462,7 @@ exports[`migration.util > Banner Contents - text Align and Font Family should no
462
462
  },
463
463
  },
464
464
  },
465
- "v": 19,
465
+ "v": 20,
466
466
  },
467
467
  "useOldLogoRenderSize": true,
468
468
  }
@@ -914,7 +914,7 @@ exports[`migration.util > migrateBrandPageModel should return brand page model w
914
914
  },
915
915
  },
916
916
  },
917
- "v": 19,
917
+ "v": 20,
918
918
  },
919
919
  "useOldLogoRenderSize": true,
920
920
  }
@@ -1373,7 +1373,7 @@ exports[`migration.util > migrateBrandPageModel should return brand page model w
1373
1373
  },
1374
1374
  },
1375
1375
  },
1376
- "v": 19,
1376
+ "v": 20,
1377
1377
  },
1378
1378
  "useOldLogoRenderSize": true,
1379
1379
  }
@@ -0,0 +1,125 @@
1
+ /**
2
+ * BP-5787 — migration-20 must convert HTML RTE → Tiptap JSON on the SERVER, not
3
+ * just the browser. The HTML→JSON parse needs a DOM parser, which pure-node SSR
4
+ * lacks. migration-20 resolves one via `resolveHtmlParser` — a jsdom `DOMParser`
5
+ * INSTANCE on the server (no global mutation), threaded explicitly into the pass.
6
+ *
7
+ * These tests run under the default jsdom environment but DELETE the global
8
+ * `window`/`document`/`DOMParser` around each case, so `resolveHtmlParser` takes
9
+ * its real pure-node branch (dynamic jsdom import → parser instance). The sibling
10
+ * migration-20.test.ts guards the browser path (native global DOMParser).
11
+ */
12
+ import { describe, test, expect, beforeEach, afterEach } from 'vitest';
13
+ import { migration20 } from './migration-20';
14
+ import type { MigrationData } from '../migration.util';
15
+ import { isTiptapDocument, getRteJsonPlainText } from '@brand-page-maker/utils';
16
+
17
+ const createMigrationData = (brandPageData: any): MigrationData =>
18
+ ({ brandPageModel: brandPageData }) as MigrationData;
19
+
20
+ const model = (contents: any[]) => ({
21
+ pages: [{ id: 'p1', title: 'Home', slug: 'home', contents }],
22
+ });
23
+
24
+ const g = globalThis as any;
25
+ let savedWindow: unknown;
26
+ let savedDocument: unknown;
27
+ let savedDOMParser: unknown;
28
+
29
+ describe('migration20 (server / no DOM) — HTML RTE → Tiptap JSON', () => {
30
+ beforeEach(() => {
31
+ // Force the pure-node path: no global window/document AND no global
32
+ // DOMParser, so resolveHtmlParser must load jsdom itself.
33
+ savedWindow = g.window;
34
+ savedDocument = g.document;
35
+ savedDOMParser = g.DOMParser;
36
+ delete g.window;
37
+ delete g.document;
38
+ delete g.DOMParser;
39
+
40
+ expect(typeof window).toBe('undefined');
41
+ expect(typeof document).toBe('undefined');
42
+ expect(typeof DOMParser).toBe('undefined');
43
+ });
44
+
45
+ afterEach(() => {
46
+ g.window = savedWindow;
47
+ g.document = savedDocument;
48
+ g.DOMParser = savedDOMParser;
49
+ });
50
+
51
+ test('converts a top-level HTML RTE field with no global DOM', async () => {
52
+ const data = model([
53
+ {
54
+ id: '1',
55
+ type: 'banner-section',
56
+ title:
57
+ '<p><span style="color: var(--logo-text-color)">Hello</span></p>',
58
+ },
59
+ ]);
60
+
61
+ const result = await migration20(createMigrationData(data));
62
+ const title = (result.brandPageModel.pages[0].contents[0] as any).title;
63
+
64
+ expect(isTiptapDocument(title)).toBe(true);
65
+ expect(getRteJsonPlainText(title)).toContain('Hello');
66
+ // theme token ref preserved through the server-side parse
67
+ expect(JSON.stringify(title)).toContain('var(--logo-text-color)');
68
+ });
69
+
70
+ test('converts RTE inside item arrays server-side (marks preserved)', async () => {
71
+ const data = model([
72
+ {
73
+ id: '2',
74
+ type: 'text',
75
+ textItems: [{ id: 'ti1', text: '<p><strong>bold</strong> item</p>' }],
76
+ },
77
+ ]);
78
+
79
+ const result = await migration20(createMigrationData(data));
80
+ const item = (result.brandPageModel.pages[0].contents[0] as any)
81
+ .textItems[0];
82
+
83
+ expect(isTiptapDocument(item.text)).toBe(true);
84
+ expect(getRteJsonPlainText(item.text)).toContain('bold');
85
+ // the <strong> mark survived the jsdom parse
86
+ expect(JSON.stringify(item.text)).toContain('bold');
87
+ });
88
+
89
+ test('leaves plain strings and already-JSON content untouched (idempotent)', async () => {
90
+ const jsonDoc = {
91
+ type: 'doc',
92
+ content: [{ type: 'paragraph', content: [{ type: 'text', text: 'X' }] }],
93
+ };
94
+ const data = model([
95
+ {
96
+ id: '3',
97
+ type: 'banner-section',
98
+ title: jsonDoc, // already JSON
99
+ url: 'https://example.com', // plain string, not RTE
100
+ titleStyle: { color: '#fff', fontSize: '1em' }, // style object, untouched
101
+ },
102
+ ]);
103
+
104
+ const result = await migration20(createMigrationData(data));
105
+ const c = result.brandPageModel.pages[0].contents[0] as any;
106
+
107
+ expect(c.title).toEqual(jsonDoc);
108
+ expect(c.url).toBe('https://example.com');
109
+ expect(c.titleStyle).toEqual({ color: '#fff', fontSize: '1em' });
110
+ });
111
+
112
+ test('does not mutate globalThis (no window/document/DOMParser leak)', async () => {
113
+ const data = model([
114
+ { id: '4', type: 'banner-section', title: '<p>Leak check</p>' },
115
+ ]);
116
+
117
+ await migration20(createMigrationData(data));
118
+
119
+ // The pass resolves a parser INSTANCE and never assigns to globalThis, so the
120
+ // deleted globals must stay absent — no transient install, nothing to leak.
121
+ expect(typeof window).toBe('undefined');
122
+ expect(typeof document).toBe('undefined');
123
+ expect(typeof DOMParser).toBe('undefined');
124
+ });
125
+ });
@@ -0,0 +1,72 @@
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
+ });
@@ -0,0 +1,87 @@
1
+ import type { BrandPageModel } from '../../models';
2
+ import type { MigrationData } from '../migration.util';
3
+ import {
4
+ hasRTEContent,
5
+ toRteJson,
6
+ resolveHtmlParser,
7
+ type HtmlParser,
8
+ } from '../../../brand-page-maker/utils';
9
+
10
+ /**
11
+ * BP-5787 — migrate existing inline-HTML RTE content to Tiptap JSON.
12
+ *
13
+ * The editor emits, and the renderer reads, Tiptap JSON. Existing
14
+ * published/saved sites still hold RTE fields as HTML strings. This converts
15
+ * them on load, in EVERY environment:
16
+ * - maker (browser): the loaded model is JSON, so the next save persists JSON.
17
+ * - web-renderer (SSR / server API routes): converted on read, so old sites
18
+ * are normalised without a re-save and downstream code can treat RTE as JSON.
19
+ *
20
+ * This is a one-time, versioned migration (runAlways is NOT set): legacy sites
21
+ * are at model version < 20 and convert on first load in any environment; once
22
+ * `v` is stamped to 20 the conversion is skipped. This is only safe because the
23
+ * AI section builders now emit Tiptap JSON directly (they no longer write HTML),
24
+ * so there is no recurring post-load source of HTML to normalise.
25
+ *
26
+ * HTML→JSON parsing needs a DOM parser. Rather than mutate a global `window`,
27
+ * the parser is resolved once (native `DOMParser` in the browser, a jsdom
28
+ * `DOMParser` instance on the server) and threaded explicitly through the
29
+ * conversion — see resolveHtmlParser / toRteJson. No global state is touched, so
30
+ * the pass is safe on any server runtime and under concurrent requests.
31
+ *
32
+ * Scope: only HTML-RTE strings (hasRTEContent) are converted. Plain strings,
33
+ * numbers, and already-JSON docs are left untouched, so the migration is
34
+ * idempotent (re-running on JSON content is a no-op — non-string values are
35
+ * skipped). Nested RTE inside item arrays (textItems, testimonials, images…)
36
+ * is handled by recursing into arrays; style objects are intentionally not
37
+ * descended into (they never hold RTE content).
38
+ */
39
+ const convertRteStringsToJson = (value: unknown, parser: HtmlParser): void => {
40
+ if (Array.isArray(value)) {
41
+ value.forEach((item) => convertRteStringsToJson(item, parser));
42
+
43
+ return;
44
+ }
45
+
46
+ if (!value || typeof value !== 'object') return;
47
+
48
+ const record = value as Record<string, unknown>;
49
+
50
+ Object.keys(record).forEach((key) => {
51
+ const field = record[key];
52
+
53
+ if (typeof field === 'string' && hasRTEContent(field)) {
54
+ const json = toRteJson(field, parser);
55
+
56
+ if (json) record[key] = json;
57
+ } else if (Array.isArray(field)) {
58
+ convertRteStringsToJson(field, parser);
59
+ }
60
+ });
61
+ };
62
+
63
+ const convertModel = (
64
+ brandPageModel: BrandPageModel,
65
+ parser: HtmlParser,
66
+ ): void => {
67
+ for (const page of brandPageModel.pages) {
68
+ for (const content of page.contents) {
69
+ convertRteStringsToJson(content, parser);
70
+ }
71
+ }
72
+ };
73
+
74
+ export const migration20 = async (
75
+ migrationResult: MigrationData,
76
+ ): Promise<MigrationData> => {
77
+ const { brandPageModel } = migrationResult;
78
+
79
+ // Resolve a DOM parser for this environment (native in the browser, a jsdom
80
+ // instance on the server — never touching globals), then convert every
81
+ // HTML-RTE field to Tiptap JSON with it.
82
+ const parser = await resolveHtmlParser();
83
+
84
+ convertModel(brandPageModel, parser);
85
+
86
+ return migrationResult;
87
+ };
@@ -22,6 +22,7 @@ import { migration16 } from './migration/migration-16';
22
22
  import { migration17 } from './migration/migration-17';
23
23
  import { migration18 } from './migration/migration-18';
24
24
  import { migration19 } from './migration/migration-19';
25
+ import { migration20 } from './migration/migration-20';
25
26
 
26
27
  export interface MigrationData {
27
28
  brandPageModel: BrandPageModel;
@@ -179,6 +180,17 @@ const MIGRATION_TABLE = [
179
180
  description:
180
181
  'BP-5800: seed the button fill colour as each block’s own container colour so it is independent of the site button (existing sites render unchanged) — backgroundStyle for mailing-list, request-payment and support-me (all types); cardStyle for the BP/BC contact form (skipped on brand-site)',
181
182
  },
183
+ {
184
+ version: 20,
185
+ migrateFn: migration20,
186
+ // One-time versioned migration (NOT runAlways): legacy sites (v < 20)
187
+ // convert on their first load in any environment — the maker (browser DOM)
188
+ // and web-renderer SSR (migration-20 installs a jsdom window). It no longer
189
+ // needs to re-run every load because the AI section builders now emit Tiptap
190
+ // JSON directly, so there is no recurring post-load source of HTML to catch.
191
+ description:
192
+ 'BP-5787: convert inline-HTML RTE content to Tiptap JSON on first load (client or server); AI builders emit JSON directly, so this runs once per site',
193
+ },
182
194
  ] as MigrationTableItem[];
183
195
 
184
196
  export const GetLatestModelVersion = () =>
@@ -87,6 +87,57 @@ describe('style.util', () => {
87
87
  );
88
88
  });
89
89
 
90
+ test('applyLogoThemeToBrandPageModel preserves var() token refs in JSON RTE content (BP-5787)', () => {
91
+ // Arrange — title stored as Tiptap JSON with a theme-token colour mark.
92
+ const logoTheme = logoWithFlatColorsTheme;
93
+ const titleDoc = {
94
+ type: 'doc',
95
+ content: [
96
+ {
97
+ type: 'heading',
98
+ attrs: { level: 2, textAlign: 'center' },
99
+ content: [
100
+ {
101
+ type: 'text',
102
+ marks: [
103
+ {
104
+ type: 'textStyle',
105
+ attrs: { color: `var(--${STYLE_THEME_KEYS.LogoTextColor})` },
106
+ },
107
+ ],
108
+ text: 'Themed',
109
+ },
110
+ ],
111
+ },
112
+ ],
113
+ };
114
+ const brandPageModel = new BrandPageModel({
115
+ ...BRAND_SITE,
116
+ pages: [
117
+ {
118
+ title: '1',
119
+ slug: '1',
120
+ contents: [
121
+ {
122
+ ...createInitialBannerSectionContent(),
123
+ title: titleDoc,
124
+ } as any,
125
+ ],
126
+ },
127
+ ],
128
+ });
129
+
130
+ // Act
131
+ const { model } = applyLogoThemeToBrandPageModel(logoTheme, brandPageModel);
132
+
133
+ // Assert — JSON RTE content is an object, so the var() token is NOT
134
+ // flattened to rgb(); it survives so a new theme can still propagate.
135
+ const title = JSON.stringify((model.pages[0].contents[0] as any).title);
136
+
137
+ expect(title).toContain(`var(--${STYLE_THEME_KEYS.LogoTextColor})`);
138
+ expect(title).not.toContain(logoTheme.logoTextColor.rgb);
139
+ });
140
+
90
141
  test('applyLogoThemeToBrandPageModel should apply logo theme color to content color when content color has a color matched theme key', () => {
91
142
  // Arrange
92
143
  const logoTheme = logoWithFlatColorsTheme;
@@ -0,0 +1,47 @@
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,17 +1,56 @@
1
1
  import { ESCAPED_NEWLINE_CHARACTER } from '../../constants/ui';
2
2
 
3
+ /**
4
+ * BP-5787 — a Tiptap JSON document is an object with `type: 'doc'`.
5
+ * Inlined here (rather than importing from brand-page-maker) to avoid a
6
+ * circular import: rich-text-editor.util imports getRenderText from this file.
7
+ */
8
+ const isTiptapDoc = (value: unknown): value is { type: 'doc' } =>
9
+ typeof value === 'object' &&
10
+ value !== null &&
11
+ (value as { type?: string }).type === 'doc';
12
+
13
+ const jsonDocPlainText = (doc: unknown): string => {
14
+ const parts: string[] = [];
15
+
16
+ const walk = (node: any): void => {
17
+ if (typeof node?.text === 'string') parts.push(node.text);
18
+
19
+ node?.content?.forEach(walk);
20
+ };
21
+
22
+ walk(doc);
23
+
24
+ return parts.join(' ').replace(/\s+/g, ' ').trim();
25
+ };
26
+
3
27
  export const getRenderText = (text: string): string => {
28
+ // BP-5787: RTE fields may now be Tiptap JSON objects. Non-strings pass
29
+ // through untouched so the renderer (TextWrapper) handles them via its
30
+ // JSON-aware path, instead of throwing on `.replace`.
31
+ if (typeof text !== 'string') {
32
+ return text as unknown as string;
33
+ }
34
+
4
35
  return text.replace(
5
36
  new RegExp(`(${ESCAPED_NEWLINE_CHARACTER}|\\n)`, 'g'),
6
37
  '<br/>',
7
38
  );
8
39
  };
9
40
 
10
- export const escapeContentText = (
11
- text: string | undefined,
12
- ): string | undefined => {
41
+ // Accepts a legacy inline-HTML string or a Tiptap JSON document (BP-5787):
42
+ // for JSON it returns the concatenated visible text; for HTML it strips tags.
43
+ export const escapeContentText = (text: unknown): string | undefined => {
13
44
  if (!text) {
14
- return text;
45
+ return text as undefined;
46
+ }
47
+
48
+ if (isTiptapDoc(text)) {
49
+ return jsonDocPlainText(text);
50
+ }
51
+
52
+ if (typeof text !== 'string') {
53
+ return undefined;
15
54
  }
16
55
 
17
56
  return text