@designcrowd/library.brand-page 6.1.1-json-prereq-3 → 6.1.1

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 (216) hide show
  1. package/lib/src/{CreatedOnBrandCrowd-de-de-hj_ievMc.js → CreatedOnBrandCrowd-de-de-B5B5ThJ6.js} +1 -1
  2. package/lib/src/{CreatedOnBrandCrowd-en-us-jAOSpfCw.js → CreatedOnBrandCrowd-en-us-a48CaORy.js} +1 -1
  3. package/lib/src/{CreatedOnBrandCrowd-es-es-DZMVc-y3.js → CreatedOnBrandCrowd-es-es-xpuZ317-.js} +1 -1
  4. package/lib/src/{CreatedOnBrandCrowd-fr-fr-DvB54H38.js → CreatedOnBrandCrowd-fr-ca-V-iUMArW.js} +1 -1
  5. package/lib/src/{CreatedOnBrandCrowd-fr-ca-DvB54H38.js → CreatedOnBrandCrowd-fr-fr-V-iUMArW.js} +1 -1
  6. package/lib/src/{CreatedOnBrandCrowd-pt-pt-bf-x1xhi.js → CreatedOnBrandCrowd-pt-br-DZ0NKSAy.js} +1 -1
  7. package/lib/src/{CreatedOnBrandCrowd-pt-br-bf-x1xhi.js → CreatedOnBrandCrowd-pt-pt-DZ0NKSAy.js} +1 -1
  8. package/lib/src/{CreatedOnDcom-de-de-B-NY1B44.js → CreatedOnDcom-de-de-_950cgGp.js} +1 -1
  9. package/lib/src/{CreatedOnDcom-en-us-Cd4C6KQf.js → CreatedOnDcom-en-us-DogKzH-g.js} +1 -1
  10. package/lib/src/{CreatedOnDcom-es-es-DONlsQsm.js → CreatedOnDcom-es-es-CmGM3IPX.js} +1 -1
  11. package/lib/src/{CreatedOnDcom-fr-ca-Dsv3FRpZ.js → CreatedOnDcom-fr-ca-BonEDkWZ.js} +1 -1
  12. package/lib/src/{CreatedOnDcom-fr-fr-DOgyvY5C.js → CreatedOnDcom-fr-fr-DQHtKrd_.js} +1 -1
  13. package/lib/src/{CreatedOnDcom-pt-br-DX66MhJS.js → CreatedOnDcom-pt-br-CtYJSBjs.js} +1 -1
  14. package/lib/src/{CreatedOnDcom-pt-pt-COe2G0bU.js → CreatedOnDcom-pt-pt-DoO9r4lA.js} +1 -1
  15. package/lib/src/brand-page/components/contact-form/ContactForm.mixin.js +7 -8
  16. package/lib/src/brand-page/components/mailing-list/MailingList.mixin.js +6 -4
  17. package/lib/src/brand-page/components/page-footer/PageFooter.mixin.d.ts +1 -0
  18. package/lib/src/brand-page/components/page-footer/PageFooter.mixin.js +4 -1
  19. package/lib/src/brand-page/components/payment/Payment.mixin.js +4 -3
  20. package/lib/src/brand-page/components/payment/__fixtures__/request-payment.d.ts +1 -0
  21. package/lib/src/brand-page/components/payment/__fixtures__/support-me.d.ts +1 -0
  22. package/lib/src/brand-page/components/payment/request-payment/RequestPayment.mixin.d.ts +1 -0
  23. package/lib/src/brand-page/components/payment/request-payment/RequestPayment.mixin.js +4 -1
  24. package/lib/src/brand-page/composables/consent.composable.d.ts +2 -0
  25. package/lib/src/brand-page/composables/consent.composable.js +9 -3
  26. package/lib/src/brand-page/models/brand-page-context.model.d.ts +7 -0
  27. package/lib/src/brand-page/models/index.d.ts +26 -34
  28. package/lib/src/brand-page/utils/context.util.d.ts +8 -0
  29. package/lib/src/brand-page/utils/context.util.js +8 -0
  30. package/lib/src/brand-page/utils/migration.util.js +0 -11
  31. package/lib/src/brand-page/utils/text.util.d.ts +1 -13
  32. package/lib/src/brand-page/utils/text.util.js +0 -64
  33. package/lib/src/brand-page/utils/vcard.util.js +4 -4
  34. package/lib/src/brand-page/utils/view-model.util.d.ts +0 -1
  35. package/lib/src/brand-page/utils/view-model.util.js +20 -68
  36. package/lib/src/brand-page/view-models.d.ts +3 -3
  37. package/lib/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.mixin.d.ts +1 -1
  38. package/lib/src/brand-page-maker/components/template-preview/TemplatePreview.mixin.js +51 -18
  39. package/lib/src/brand-page-maker/composables/ai-chat/useAiMetadataBuilder.d.ts +1 -0
  40. package/lib/src/brand-page-maker/composables/rich-text-editor/bubbleMenu.d.ts +9 -2
  41. package/lib/src/brand-page-maker/composables/rich-text-editor/bubbleMenu.js +10 -6
  42. package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/FontSize.d.ts +20 -0
  43. package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/FontSize.js +44 -0
  44. package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/LineHeight.d.ts +27 -0
  45. package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/LineHeight.js +44 -0
  46. package/lib/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.d.ts +1 -1
  47. package/lib/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.js +103 -24
  48. package/lib/src/brand-page-maker/stores/brandpage-container/brand-page-container-store.js +8 -0
  49. package/lib/src/brand-page-maker/stores/brandpage-maker/brandpage-maker-store.js +9 -3
  50. package/lib/src/brand-page-maker/stores/ui/ui-store.d.ts +1 -1
  51. package/lib/src/brand-page-maker/utils/index.d.ts +0 -2
  52. package/lib/src/brand-page-maker/utils/index.js +0 -2
  53. package/lib/src/brand-page-maker/utils/rich-text-editor.util.d.ts +5 -28
  54. package/lib/src/brand-page-maker/utils/rich-text-editor.util.js +5 -48
  55. package/lib/src/brand-page-maker/validators/field.validator.js +2 -10
  56. package/lib/src/clients/models.d.ts +2 -0
  57. package/lib/src/css/library.brand-page-brandCrowd.min.css +99 -102
  58. package/lib/src/css/library.brand-page-brandPage.min.css +99 -102
  59. package/lib/src/css/library.brand-page-designCom.min.css +99 -102
  60. package/lib/src/css/library.brand-page-preview.min.css +99 -102
  61. package/lib/src/index.d.ts +1 -1
  62. package/lib/src/index.mjs +219 -221
  63. package/lib/src/{lib-scDtk-Pi.js → lib-DtalJKI3.js} +72445 -75920
  64. package/lib/src/lib.js +1 -2
  65. package/lib/src/metadata-builder/ai-metadata-executor.d.ts +1 -0
  66. package/lib/src/metadata-builder/ai-metadata-executor.js +19 -0
  67. package/lib/src/metadata-builder/builders/section-builders/about-section-builder.js +3 -2
  68. package/lib/src/metadata-builder/builders/section-builders/banner-section-builder.js +3 -2
  69. package/lib/src/metadata-builder/builders/section-builders/booking-section-builder.js +3 -2
  70. package/lib/src/metadata-builder/builders/section-builders/contact-form-builder.js +4 -3
  71. package/lib/src/metadata-builder/builders/section-builders/images-section-builder.js +3 -2
  72. package/lib/src/metadata-builder/builders/section-builders/mailing-list-builder.js +2 -1
  73. package/lib/src/metadata-builder/builders/section-builders/mailing-list-section-builder.js +2 -1
  74. package/lib/src/metadata-builder/builders/section-builders/profile-section-builder.js +4 -3
  75. package/lib/src/metadata-builder/builders/section-builders/shapes-section-builder.js +3 -2
  76. package/lib/src/metadata-builder/builders/section-builders/testimonial-section-builder.js +2 -1
  77. package/lib/src/metadata-builder/builders/section-builders/text-section-builder.js +11 -15
  78. package/lib/src/metadata-builder/builders/update-content-text-builder.d.ts +17 -0
  79. package/lib/src/metadata-builder/builders/update-content-text-builder.js +103 -0
  80. package/lib/src/metadata-builder/builders/update-content-text-style-builder.d.ts +16 -0
  81. package/lib/src/metadata-builder/builders/update-content-text-style-builder.js +216 -0
  82. package/lib/src/metadata-builder/helpers/rte.helpers.d.ts +13 -11
  83. package/lib/src/metadata-builder/helpers/rte.helpers.js +73 -14
  84. package/lib/src/metadata-builder/metadata-executor.js +4 -0
  85. package/lib/src/metadata-builder/models.d.ts +2 -0
  86. package/lib/src/metadata-builder/models.js +2 -0
  87. package/lib/src/metadata-builder/parsers/ai-toolcall-parser.d.ts +19 -0
  88. package/lib/src/metadata-builder/parsers/ai-toolcall-parser.js +4 -0
  89. package/lib/src/metadata-builder/sanitizers/rte-schema-check.d.ts +8 -7
  90. package/lib/src/metadata-builder/sanitizers/rte-schema-check.js +52 -21
  91. package/lib/src/shared/content-settings/components/GenericTextInput.mixin.d.ts +1 -1
  92. package/lib/src/shared/content-settings/components/GenericTextInput.mixin.js +10 -35
  93. package/lib/src/shared/content-settings/components/content-settings-item/ContentSettingsItemField.mixin.d.ts +1 -1
  94. package/lib/src/shared/content-settings/components/content-settings-item/ContentSettingsItemField.mixin.js +3 -12
  95. package/lib/src/shared/content-settings/components/email-field/EmailField.mixin.d.ts +1 -1
  96. package/lib/src/shared/content-settings/components/tel-field/TelField.mixin.d.ts +1 -1
  97. package/lib/src/shared/content-settings/components/testimonial-items-field/TestimonialItemsField.mixin.d.ts +2 -3
  98. package/lib/src/shared/content-settings/components/testimonial-items-field/TestimonialItemsField.mixin.js +3 -10
  99. package/lib/src/shared/content-settings/components/text-field/TextField.mixin.d.ts +1 -1
  100. package/lib/src/shared/content-settings/components/text-items-field/TextItemsField.mixin.d.ts +1 -2
  101. package/lib/src/shared/content-settings/components/text-items-field/TextItemsField.mixin.js +1 -4
  102. package/lib/src/shared/content-settings/components/textarea-field/TextareaField.mixin.d.ts +1 -1
  103. package/lib/src/shared/content-settings/components/url-field/UrlField.mixin.d.ts +1 -1
  104. package/lib/src/shared/content-settings/components/youtube-video-field/YoutubeVideoField.mixin.d.ts +1 -1
  105. package/lib/src/shared/logo-settings/components/EditLogoSettings.mixin.d.ts +1 -1
  106. package/lib/src/shared/logo-versions/LogoVersions.mixin.d.ts +1 -1
  107. package/lib/src/shared/my-designs/MyDesigns.mixin.d.ts +1 -1
  108. package/lib/src/shared/my-uploads/MyMedia.mixin.d.ts +1 -1
  109. package/lib/src/shared/my-uploads/MyUploads.mixin.d.ts +1 -1
  110. package/lib/src/shared/page-settings/components/page-settings/PageSettings.mixin.d.ts +1 -1
  111. package/lib/src/shared/page-settings/components/page-settings-section-styles/PageSettingsSectionStyles.mixin.d.ts +1 -1
  112. package/lib/src/shared/style-settings/components/StyleSettings.mixin.d.ts +1 -1
  113. package/lib/src/shared/template-settings/components/TemplatesSettings.mixin.d.ts +1 -1
  114. package/lib/src/shared/template-settings/store/template-settings-store.d.ts +2 -0
  115. package/lib/src/shared/template-settings/store/template-settings-store.js +72 -24
  116. package/lib/src/shared/unsplash-library/UnsplashLibrary.mixin.d.ts +1 -1
  117. package/lib/src/utils/minify-brand-page-metadata.js +1 -4
  118. package/package.json +15 -17
  119. package/src/brand-page/components/contact-form/ContactForm.mixin.ts +9 -7
  120. package/src/brand-page/components/cookie-consent/CookieConsent.vue +1 -0
  121. package/src/brand-page/components/mailing-list/MailingList.mixin.ts +8 -3
  122. package/src/brand-page/components/navigation/navigation-carousel/NavigationCarousel.vue +5 -1
  123. package/src/brand-page/components/page-footer/PageFooter.mixin.ts +4 -1
  124. package/src/brand-page/components/page-footer/PageFooter.vue +1 -1
  125. package/src/brand-page/components/payment/Payment.mixin.ts +4 -3
  126. package/src/brand-page/components/payment/request-payment/RequestPayment.mixin.ts +8 -1
  127. package/src/brand-page/components/payment/request-payment/RequestPayment.vue +1 -1
  128. package/src/brand-page/components/save-contact/SaveContactWidget.vue +2 -1
  129. package/src/brand-page/components/shop/Shop.vue +6 -3
  130. package/src/brand-page/components/social-links/SocialLinks.vue +5 -2
  131. package/src/brand-page/components/text-wrapper/TextWrapper.vue +6 -17
  132. package/src/brand-page/composables/consent.composable.ts +18 -4
  133. package/src/brand-page/models/brand-page-context.model.ts +7 -0
  134. package/src/brand-page/models/index.ts +26 -35
  135. package/src/brand-page/utils/__snapshots__/migration.util.test.ts.snap +3 -3
  136. package/src/brand-page/utils/context.util.test.ts +43 -0
  137. package/src/brand-page/utils/context.util.ts +10 -0
  138. package/src/brand-page/utils/migration.util.ts +0 -12
  139. package/src/brand-page/utils/style.util.test.ts +0 -51
  140. package/src/brand-page/utils/text.util.ts +4 -83
  141. package/src/brand-page/utils/vcard.util.ts +4 -6
  142. package/src/brand-page/utils/view-model.util.ts +26 -94
  143. package/src/brand-page/view-models.ts +2 -3
  144. package/src/brand-page-maker/components/rich-text-editor/RichTextEditor.vue +4 -5
  145. package/src/brand-page-maker/components/template-preview/TemplatePreview.mixin.test.ts +135 -0
  146. package/src/brand-page-maker/components/template-preview/TemplatePreview.mixin.ts +62 -21
  147. package/src/brand-page-maker/composables/ai-chat/useAiMetadataBuilder.ts +1 -0
  148. package/src/brand-page-maker/composables/rich-text-editor/bubbleMenu.ts +10 -6
  149. package/src/brand-page-maker/composables/rich-text-editor/extensions/FontSize.ts +73 -0
  150. package/src/brand-page-maker/composables/rich-text-editor/extensions/LineHeight.ts +85 -0
  151. package/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.ts +111 -27
  152. package/src/brand-page-maker/stores/brandpage-container/brand-page-container-store.ts +9 -0
  153. package/src/brand-page-maker/stores/brandpage-maker/brandpage-maker-store.ts +11 -3
  154. package/src/brand-page-maker/stores/ui/ui-store.ts +1 -1
  155. package/src/brand-page-maker/utils/index.ts +0 -2
  156. package/src/brand-page-maker/utils/rich-text-editor.util.test.ts +3 -37
  157. package/src/brand-page-maker/utils/rich-text-editor.util.ts +11 -66
  158. package/src/brand-page-maker/validators/field.validator.ts +2 -10
  159. package/src/clients/models.ts +3 -0
  160. package/src/lib.ts +1 -9
  161. package/src/metadata-builder/ai-metadata-executor.ts +26 -0
  162. package/src/metadata-builder/builders/section-builders/about-section-builder.ts +3 -2
  163. package/src/metadata-builder/builders/section-builders/banner-section-builder.ts +3 -2
  164. package/src/metadata-builder/builders/section-builders/booking-section-builder.ts +5 -2
  165. package/src/metadata-builder/builders/section-builders/contact-form-builder.ts +4 -3
  166. package/src/metadata-builder/builders/section-builders/images-section-builder.ts +3 -2
  167. package/src/metadata-builder/builders/section-builders/mailing-list-builder.ts +2 -1
  168. package/src/metadata-builder/builders/section-builders/mailing-list-section-builder.ts +2 -1
  169. package/src/metadata-builder/builders/section-builders/profile-section-builder.ts +6 -3
  170. package/src/metadata-builder/builders/section-builders/shapes-section-builder.ts +3 -2
  171. package/src/metadata-builder/builders/section-builders/testimonial-section-builder.ts +2 -1
  172. package/src/metadata-builder/builders/section-builders/text-section-builder.ts +13 -19
  173. package/src/metadata-builder/builders/update-content-text-builder.ts +148 -0
  174. package/src/metadata-builder/builders/update-content-text-style-builder.ts +352 -0
  175. package/src/metadata-builder/helpers/rte.helpers.ts +101 -15
  176. package/src/metadata-builder/metadata-executor.ts +5 -0
  177. package/src/metadata-builder/models.ts +3 -0
  178. package/src/metadata-builder/parsers/ai-toolcall-parser.ts +27 -0
  179. package/src/metadata-builder/sanitizers/rte-schema-check.ts +53 -24
  180. package/src/shared/content-settings/components/GenericTextInput.mixin.ts +11 -42
  181. package/src/shared/content-settings/components/content-settings-item/ContentSettingsItemField.mixin.ts +3 -16
  182. package/src/shared/content-settings/components/testimonial-items-field/TestimonialItemsField.mixin.ts +4 -13
  183. package/src/shared/content-settings/components/testimonial-items-field/TestimonialItemsField.vue +4 -6
  184. package/src/shared/content-settings/components/text-image-field/TextImageField.vue +3 -6
  185. package/src/shared/content-settings/components/text-items-field/TextItemsField.mixin.ts +1 -6
  186. package/src/shared/content-settings/components/text-items-field/TextItemsField.vue +1 -1
  187. package/src/shared/template-settings/store/template-settings-store.ts +93 -27
  188. package/src/utils/minify-brand-page-metadata.ts +1 -4
  189. package/lib/src/brand-page/utils/migration/migration-20.d.ts +0 -2
  190. package/lib/src/brand-page/utils/migration/migration-20.js +0 -66
  191. package/lib/src/brand-page-maker/__fixtures__/complex-rte.fixture.d.ts +0 -22
  192. package/lib/src/brand-page-maker/__fixtures__/complex-rte.fixture.js +0 -22
  193. package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/FontFamily.d.ts +0 -14
  194. package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/FontFamily.js +0 -39
  195. package/lib/src/brand-page-maker/composables/rich-text-editor/getRteExtensions.d.ts +0 -28
  196. package/lib/src/brand-page-maker/composables/rich-text-editor/getRteExtensions.js +0 -117
  197. package/lib/src/brand-page-maker/utils/rte-render.util.d.ts +0 -63
  198. package/lib/src/brand-page-maker/utils/rte-render.util.js +0 -95
  199. package/lib/src/brand-page-maker/utils/rte-sanitize.util.d.ts +0 -6
  200. package/lib/src/brand-page-maker/utils/rte-sanitize.util.js +0 -156
  201. package/src/brand-page/utils/migration/migration-20.server.test.ts +0 -125
  202. package/src/brand-page/utils/migration/migration-20.test.ts +0 -72
  203. package/src/brand-page/utils/migration/migration-20.ts +0 -87
  204. package/src/brand-page/utils/text.util.test.ts +0 -106
  205. package/src/brand-page/utils/view-model.pageid-rte.test.ts +0 -104
  206. package/src/brand-page-maker/__fixtures__/complex-rte.fixture.ts +0 -23
  207. package/src/brand-page-maker/composables/rich-text-editor/extensions/FontFamily.test.ts +0 -52
  208. package/src/brand-page-maker/composables/rich-text-editor/extensions/FontFamily.ts +0 -42
  209. package/src/brand-page-maker/composables/rich-text-editor/getRteExtensions.ts +0 -162
  210. package/src/brand-page-maker/utils/__snapshots__/rte-render-parity.test.ts.snap +0 -202
  211. package/src/brand-page-maker/utils/rte-features.test.ts +0 -378
  212. package/src/brand-page-maker/utils/rte-render-parity.test.ts +0 -164
  213. package/src/brand-page-maker/utils/rte-render.util.ts +0 -165
  214. package/src/brand-page-maker/utils/rte-sanitize-json.test.ts +0 -172
  215. package/src/brand-page-maker/utils/rte-sanitize.util.ts +0 -192
  216. package/src/brand-page-maker/validators/field.validator.test.ts +0 -55
@@ -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,87 +0,0 @@
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
- };
@@ -1,106 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import {
3
- escapeContentText,
4
- getContentTextLength,
5
- getRenderText,
6
- } from './text.util';
7
-
8
- /**
9
- * BP-5787 — escapeContentText / getRenderText must accept either a legacy
10
- * inline-HTML string or a Tiptap JSON document without throwing. Section
11
- * components call these on RTE fields (e.g. banner description), which are now
12
- * JSON; the previous string-only versions threw `.replace is not a function`
13
- * and broke the field's reactivity (strike/format edits stopped rendering).
14
- */
15
- const doc = {
16
- type: 'doc',
17
- content: [
18
- {
19
- type: 'paragraph',
20
- content: [
21
- { type: 'text', text: 'WARNING', marks: [{ type: 'strike' }] },
22
- { type: 'text', text: ': take flight' },
23
- ],
24
- },
25
- ],
26
- };
27
-
28
- describe('escapeContentText (BP-5787 dual-read)', () => {
29
- it('extracts plain text from a Tiptap JSON document', () => {
30
- expect(escapeContentText(doc)).toBe('WARNING : take flight');
31
- });
32
-
33
- it('still strips tags from a legacy HTML string', () => {
34
- expect(escapeContentText('<p><s>WARNING</s></p>')).toBe('WARNING');
35
- });
36
-
37
- it('returns falsy input unchanged', () => {
38
- expect(escapeContentText('')).toBe('');
39
- expect(escapeContentText(undefined)).toBeUndefined();
40
- });
41
- });
42
-
43
- describe('getRenderText (BP-5787 JSON-safe)', () => {
44
- it('passes a Tiptap JSON document through untouched (no throw)', () => {
45
- expect(getRenderText(doc as unknown as string)).toBe(doc);
46
- });
47
-
48
- it('still converts newlines to <br/> for plain strings', () => {
49
- expect(getRenderText('a\nb')).toBe('a<br/>b');
50
- });
51
- });
52
-
53
- /**
54
- * BP-5787 — length limiting must count the editor's own char count (ProseMirror
55
- * textContent), NOT escapeContentText's space-joined plain text. A mark boundary
56
- * (e.g. hyperlinking a word) splits one text node into two; escapeContentText
57
- * inserts a phantom space at the split and reports length+1, which tripped the
58
- * "max reached" indicator at exactly-the-cap content. getContentTextLength
59
- * concatenates node text with no separator, matching the MaxLength gate.
60
- */
61
- describe('getContentTextLength (BP-5787 gate-matching length)', () => {
62
- const linkedDoc = {
63
- type: 'doc',
64
- content: [
65
- {
66
- type: 'paragraph',
67
- content: [
68
- { type: 'text', text: 'hello' },
69
- {
70
- type: 'text',
71
- text: 'world',
72
- marks: [{ type: 'link', attrs: { href: 'https://x.com' } }],
73
- },
74
- ],
75
- },
76
- ],
77
- };
78
-
79
- it('counts split (hyperlinked) nodes without a phantom space', () => {
80
- // escapeContentText over-counts this as 11 ("hello world"); the editor's
81
- // textContent is "helloworld" = 10.
82
- expect(getContentTextLength(linkedDoc)).toBe(10);
83
- expect(escapeContentText(linkedDoc)).toHaveLength(11);
84
- });
85
-
86
- it('matches plain single-node text', () => {
87
- const plain = {
88
- type: 'doc',
89
- content: [
90
- { type: 'paragraph', content: [{ type: 'text', text: 'helloworld' }] },
91
- ],
92
- };
93
-
94
- expect(getContentTextLength(plain)).toBe(10);
95
- });
96
-
97
- it('counts a legacy HTML string with tags stripped', () => {
98
- expect(getContentTextLength('<p>hello<a href="#">world</a></p>')).toBe(10);
99
- });
100
-
101
- it('returns 0 for empty / non-content values', () => {
102
- expect(getContentTextLength('')).toBe(0);
103
- expect(getContentTextLength(undefined)).toBe(0);
104
- expect(getContentTextLength(false)).toBe(0);
105
- });
106
- });
@@ -1,104 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { resolvePageIdsInRteJson } from './view-model.util';
3
- import type { NavViewModel } from '../view-models';
4
-
5
- /**
6
- * BP-5847 — internal RTE hyperlinks are stored as `href="pageid:<id>"`. On the
7
- * published site these must resolve to the page's relative path. The string
8
- * (legacy HTML) path already did this; once RTE moved to Tiptap JSON the link
9
- * marks were skipped, so links rendered as raw `pageid:` and didn't navigate.
10
- */
11
- describe('resolvePageIdsInRteJson (BP-5847)', () => {
12
- const nav: NavViewModel = {
13
- navItems: [
14
- {
15
- pageId: 'home-id',
16
- slug: 'home',
17
- displayName: 'Home',
18
- routeName: 'home',
19
- routeParams: { slug: 'mysite' },
20
- isRoot: true,
21
- isHidden: false,
22
- relativePath: '',
23
- nestedNavItems: [],
24
- },
25
- {
26
- pageId: 'about-id',
27
- slug: 'about',
28
- displayName: 'About',
29
- routeName: 'about',
30
- routeParams: { slug: 'mysite' },
31
- isRoot: false,
32
- isHidden: false,
33
- relativePath: 'about',
34
- nestedNavItems: [],
35
- },
36
- ],
37
- };
38
-
39
- const options = { brandPageSlug: 'mysite' } as any;
40
-
41
- const docWithLink = (href: string) => ({
42
- type: 'doc',
43
- content: [
44
- {
45
- type: 'paragraph',
46
- content: [
47
- {
48
- type: 'text',
49
- text: 'link',
50
- marks: [{ type: 'link', attrs: { href, target: '_blank' } }],
51
- },
52
- ],
53
- },
54
- ],
55
- });
56
-
57
- const linkHref = (doc: any) => doc.content[0].content[0].marks[0].attrs.href;
58
-
59
- it('resolves a pageid link to a sub-page relative path', () => {
60
- const out = resolvePageIdsInRteJson(
61
- docWithLink('pageid:about-id'),
62
- nav,
63
- options,
64
- );
65
-
66
- expect(linkHref(out)).toBe('/mysite/about');
67
- });
68
-
69
- it('resolves a root pageid link to the site root', () => {
70
- const out = resolvePageIdsInRteJson(
71
- docWithLink('pageid:home-id'),
72
- nav,
73
- options,
74
- );
75
-
76
- expect(linkHref(out)).toBe('/mysite');
77
- });
78
-
79
- it('falls back to root when the pageid cannot be matched', () => {
80
- const out = resolvePageIdsInRteJson(
81
- docWithLink('pageid:missing-id'),
82
- nav,
83
- options,
84
- );
85
-
86
- expect(linkHref(out)).toBe('/mysite');
87
- });
88
-
89
- it('leaves external links untouched and returns the same reference', () => {
90
- const doc = docWithLink('https://example.com');
91
- const out = resolvePageIdsInRteJson(doc, nav, options);
92
-
93
- expect(linkHref(out)).toBe('https://example.com');
94
- expect(out).toBe(doc);
95
- });
96
-
97
- it('does not mutate the source doc (returns a new object) when resolving', () => {
98
- const doc = docWithLink('pageid:about-id');
99
- const out = resolvePageIdsInRteJson(doc, nav, options);
100
-
101
- expect(out).not.toBe(doc);
102
- expect(linkHref(doc)).toBe('pageid:about-id');
103
- });
104
- });
@@ -1,23 +0,0 @@
1
- /**
2
- * BP-5787 — golden-master fixture for the RTE HTML→JSON migration.
3
- *
4
- * A single rich-text value that exercises every mark/node the editor can emit,
5
- * so one snapshot guards the whole serialisation surface. Captured as a baseline
6
- * on the current HTML pipeline; after the migration the JSON pipeline must
7
- * reproduce byte-identical rendered output (see rte-render-parity.test.ts).
8
- *
9
- * Coverage:
10
- * - block nodes: heading (h2), paragraph, text-align (center / right)
11
- * - inline marks: bold, italic, underline, strike
12
- * - textStyle attrs: fontFamily, fontSize (em), letterSpacing (em), lineHeight (unitless)
13
- * - colour: theme TOKEN ref (var(--logo-text-color)) AND a literal hex (#FF0000)
14
- * - highlight (background colour via <mark>)
15
- * - links: internal (target=_self) and external (target=_blank rel=noopener)
16
- */
17
- export const COMPLEX_RTE_HTML = `<h2 style="text-align: center"><span style="color: var(--logo-text-color)">Themed heading</span></h2><p><strong>bold</strong> <em>italic</em> <u>underline</u> <s>strike</s> <span style="font-family: Montserrat; font-size: 1.5em; letter-spacing: 0.05em; line-height: 1.4">styled span</span> <mark data-color="#ffe08a" style="background-color: #ffe08a">highlight</mark> <span style="color: #FF0000">literal hex</span> <a target="_self" href="/about">internal link</a> <a target="_blank" rel="noopener noreferrer nofollow" href="https://example.com">external link</a></p><p style="text-align: right">second paragraph, right-aligned</p>`;
18
-
19
- /**
20
- * The theme-token CSS variable that MUST survive the HTML→JSON round-trip
21
- * (BP-5787 stores token refs instead of flattening them to rgb()).
22
- */
23
- export const THEME_TOKEN_REF = 'var(--logo-text-color)';
@@ -1,52 +0,0 @@
1
- import { describe, it, expect, afterEach } from 'vitest';
2
- import { Editor } from '@tiptap/vue-3';
3
- import { getRteExtensions } from '../getRteExtensions';
4
- import { sanitizeRteJson } from '@brand-page-maker/utils/rte-sanitize.util';
5
-
6
- /**
7
- * BP-5787 — multi-word font families ("Open Sans") are quoted in CSS, so
8
- * re-parsing HTML (pasting RTE-styled text between editors) previously yielded
9
- * a value WITH quote chars, which the native sanitiser then dropped — losing
10
- * the font. The custom FontFamily extension strips the quotes on parse.
11
- */
12
- describe('FontFamily — quote-stripping parse (BP-5787)', () => {
13
- let editors: Editor[] = [];
14
- const mk = () => {
15
- const e = new Editor({
16
- extensions: getRteExtensions({ restrictStarterKit: true }),
17
- });
18
-
19
- editors.push(e);
20
-
21
- return e;
22
- };
23
-
24
- afterEach(() => {
25
- editors.forEach((e) => e.destroy());
26
- editors = [];
27
- });
28
-
29
- const fontFamilyOf = (json: any) =>
30
- json.content?.[0]?.content?.[0]?.marks?.find(
31
- (m: any) => m.type === 'textStyle',
32
- )?.attrs?.fontFamily;
33
-
34
- for (const font of ['Montserrat', 'Open Sans', 'Playwrite AU']) {
35
- it(`"${font}" survives an HTML re-parse + sanitise unquoted`, () => {
36
- const src = mk();
37
-
38
- src.commands.setContent('<p>Hello world</p>');
39
- src.commands.selectAll();
40
- src.commands.setFontFamily(font);
41
-
42
- // Re-parse the serialised HTML — mirrors an RTE→RTE paste.
43
- const tgt = mk();
44
-
45
- tgt.commands.setContent(src.getHTML());
46
-
47
- expect(fontFamilyOf(tgt.getJSON())).toBe(font);
48
- // and the sanitiser must keep it (isSafeCssValue rejects quote chars).
49
- expect(fontFamilyOf(sanitizeRteJson(tgt.getJSON()))).toBe(font);
50
- });
51
- }
52
- });
@@ -1,42 +0,0 @@
1
- import TiptapFontFamily from '@tiptap/extension-font-family';
2
-
3
- /**
4
- * BP-5787 — font-family with quote-stripping parse.
5
- *
6
- * The stock extension stores whatever `element.style.fontFamily` returns. For
7
- * multi-word families (e.g. "Open Sans", "Playwrite AU") the browser quotes the
8
- * value, so parsing HTML — pasting RTE-styled text between editors, or the
9
- * HTML→JSON migration — yields a value that still contains the quote chars
10
- * (`"Open Sans"`). That value then (a) no longer matches the font list and,
11
- * worse, (b) is dropped by the native RTE sanitiser (`sanitizeRteJson`), whose
12
- * `isSafeCssValue` rejects any value containing `"` `'` `;` — so the font is
13
- * silently lost. Mirror the FontSize/LetterSpacing extensions and strip the
14
- * quotes on parse so the stored value is always the bare family name.
15
- */
16
- export const FontFamily = TiptapFontFamily.extend({
17
- addGlobalAttributes() {
18
- return [
19
- {
20
- types: this.options.types,
21
- attributes: {
22
- fontFamily: {
23
- default: null,
24
- // Strip quotes only — keep '' for an absent value so the stored
25
- // JSON shape matches the other textStyle attrs (FontSize etc.).
26
- parseHTML: (element) =>
27
- element.style.fontFamily?.replace(/['"]+/g, ''),
28
- renderHTML: (attributes) => {
29
- if (!attributes.fontFamily) {
30
- return {};
31
- }
32
-
33
- return {
34
- style: `font-family: ${attributes.fontFamily}`,
35
- };
36
- },
37
- },
38
- },
39
- },
40
- ];
41
- },
42
- });
@@ -1,162 +0,0 @@
1
- import type { Extensions } from '@tiptap/vue-3';
2
- import StarterKit from '@tiptap/starter-kit';
3
- import Underline from '@tiptap/extension-underline';
4
- import { TextStyle, FontSize, LineHeight } from '@tiptap/extension-text-style';
5
- import TextAlign from '@tiptap/extension-text-align';
6
- import ColorExtension from '@tiptap/extension-color';
7
- import Highlight from '@tiptap/extension-highlight';
8
- import Placeholder from '@tiptap/extension-placeholder';
9
- import Link from '@tiptap/extension-link';
10
- import { FontFamily } from './extensions/FontFamily';
11
- import { LetterSpacing } from './extensions/LetterSpacing';
12
- import { MaxLength } from './extensions/MaxLength';
13
- import { AutoLink } from './extensions/AutoLink';
14
- import { isHrefPageId } from './hyperlink';
15
-
16
- /**
17
- * Single source of truth for the Tiptap extension set (BP-5787).
18
- *
19
- * The live editor, the headless serialiser (useExternalRichTextEditor), and
20
- * the upcoming render-from-JSON path must all use the SAME extensions, or
21
- * rendered output drifts from what the user authored. This factory removes the
22
- * previous duplication while preserving each consumer's exact configuration.
23
- *
24
- * Known, intentional differences are parameterised:
25
- * - `restrictStarterKit` — the live editor disables list/hr/code nodes.
26
- * - `editing` — only the live editor carries the Placeholder + MaxLength plugins
27
- * (neither affects serialised output; they are editing-time only).
28
- *
29
- * Link target/rel are NOT parameterised: they are a pure function of the href
30
- * applied in the Link mark's renderHTML (below), so every consumer — live
31
- * editor, static renderer, schema check — emits identical link markup.
32
- */
33
- export interface RteExtensionOptions {
34
- /** Disable StarterKit nodes the editor doesn't use (lists, hr, code). */
35
- restrictStarterKit?: boolean;
36
- /** When present, add the live-editing-only plugins (Placeholder, MaxLength). */
37
- editing?: {
38
- placeholder?: string;
39
- maxLength?: number;
40
- };
41
- }
42
-
43
- export const getRteExtensions = (
44
- options: RteExtensionOptions = {},
45
- ): Extensions => {
46
- const { restrictStarterKit = false, editing } = options;
47
-
48
- const extensions: Extensions = [
49
- // v3 StarterKit bundles Link + Underline; we register both separately (with
50
- // custom Link config), so disable the bundled copies to avoid duplicate
51
- // extension errors.
52
- // trailingNode: false — v3 StarterKit adds a TrailingNode plugin (not in v2)
53
- // that appends an empty paragraph after any non-paragraph trailing block.
54
- // Disabled to keep editor/stored-content behaviour identical to v2 (BP-5787
55
- // is backward-compat-first); revisit as a standalone UX change if wanted.
56
- restrictStarterKit
57
- ? StarterKit.configure({
58
- link: false,
59
- underline: false,
60
- trailingNode: false,
61
- bulletList: false,
62
- orderedList: false,
63
- listItem: false,
64
- horizontalRule: false,
65
- codeBlock: false,
66
- code: false,
67
- })
68
- : StarterKit.configure({
69
- link: false,
70
- underline: false,
71
- trailingNode: false,
72
- }),
73
- Underline,
74
- // priority: 99 — render the textStyle <span> (which carries font-size) as the
75
- // OUTERMOST inline mark, so decoration marks (<u>/<s>) nest INSIDE it and
76
- // inherit the applied font-size. v3's extension-text-style defaults to
77
- // priority 101, which flips this: <u>/<s> end up outside the font-size span
78
- // and inherit only the base paragraph size. Since text-decoration thickness
79
- // AND vertical position are computed from the decorating element's own font,
80
- // that produced thin, mis-centred underline/strike on large text — a
81
- // regression vs v2's serializer, which emitted the font-size span outermost
82
- // (BP-5787, backward-compat-first).
83
- TextStyle.extend({ priority: 99 }).configure({
84
- mergeNestedSpanStyles: true,
85
- }),
86
- TextAlign.configure({
87
- types: ['heading', 'paragraph'],
88
- }),
89
- FontFamily.configure({
90
- types: ['textStyle'],
91
- }),
92
- // FontSize + LineHeight are now shipped natively by @tiptap/extension-text-style.
93
- FontSize.configure({ types: ['textStyle'] }),
94
- LetterSpacing,
95
- LineHeight.configure({ types: ['textStyle'] }),
96
- ColorExtension.configure({
97
- types: ['textStyle'],
98
- }),
99
- Highlight.configure({
100
- multicolor: true,
101
- }),
102
- ];
103
-
104
- // Live-editing-only plugins sit between Highlight and Link (original order).
105
- if (editing) {
106
- extensions.push(
107
- Placeholder.configure({ placeholder: editing.placeholder }),
108
- MaxLength.configure({ maxLength: editing.maxLength }),
109
- );
110
- }
111
-
112
- extensions.push(
113
- Link.extend({
114
- // External-link policy lives at the serialisation boundary: target/rel are
115
- // a pure function of the href, applied whenever a link mark renders to <a>
116
- // (live editor DOM + static renderer alike). They are neither stored on the
117
- // mark nor the sanitiser's concern — the sanitiser validates href only
118
- // (see rte-sanitize.util.ts). This is what the old, unsupported
119
- // `HTMLAttributes: (attrs) => …` function was trying (and failing) to do:
120
- // Tiptap types HTMLAttributes as a static object, so a function silently
121
- // no-ops, which is why fresh links lost target/rel after the JSON migration.
122
- renderHTML(props) {
123
- const href = props.HTMLAttributes.href;
124
- const isExternal =
125
- typeof href === 'string' &&
126
- (href.startsWith('http://') || href.startsWith('https://'));
127
- const HTMLAttributes = {
128
- ...props.HTMLAttributes,
129
- target: isExternal ? '_blank' : null,
130
- rel: isExternal ? 'noopener noreferrer' : null,
131
- };
132
-
133
- // Delegate to the base renderHTML so its XSS href-strip guard still runs.
134
- // base Link always defines renderHTML, so the ?? branch is a defensive
135
- // fallback only (never taken in practice); href is pre-validated by the
136
- // sanitiser and isAllowedUri regardless.
137
- return (
138
- this.parent?.({ ...props, HTMLAttributes }) ?? [
139
- 'a',
140
- HTMLAttributes,
141
- 0,
142
- ]
143
- );
144
- },
145
- }).configure({
146
- openOnClick: false,
147
- defaultProtocol: 'https',
148
- autolink: true,
149
- // Override Tiptap's built-in Link defaults ({ target: '_blank', rel:
150
- // 'noopener noreferrer nofollow' }) with nulls: target/rel are NOT stored on
151
- // the mark, they are derived from the href at render time (see renderHTML
152
- // above). configure() deep-merges HTMLAttributes, so an empty {} would leave
153
- // those built-in defaults in place and stamp them onto every link — hence
154
- // the explicit nulls.
155
- HTMLAttributes: { target: null, rel: null },
156
- isAllowedUri: (url, ctx) => ctx.defaultValidate(url) || isHrefPageId(url),
157
- }),
158
- AutoLink,
159
- );
160
-
161
- return extensions;
162
- };