@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.
- package/lib/src/{CreatedOnBrandCrowd-de-de-CS3ct0wa.js → CreatedOnBrandCrowd-de-de-CKBqjDH3.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-en-us-CmBcnDu6.js → CreatedOnBrandCrowd-en-us-CRXiimvT.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-es-es-BdTuJtqV.js → CreatedOnBrandCrowd-es-es-DCYdY6sj.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-fr-fr-Cx_H9X6l.js → CreatedOnBrandCrowd-fr-ca-Dk7LOhxE.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-fr-ca-Cx_H9X6l.js → CreatedOnBrandCrowd-fr-fr-Dk7LOhxE.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-pt-pt-BZZ9hjUi.js → CreatedOnBrandCrowd-pt-br-Cp4ufHJP.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-pt-br-BZZ9hjUi.js → CreatedOnBrandCrowd-pt-pt-Cp4ufHJP.js} +1 -1
- package/lib/src/{CreatedOnDcom-de-de-SbONDQN1.js → CreatedOnDcom-de-de-ZSLkWMwM.js} +1 -1
- package/lib/src/{CreatedOnDcom-en-us-Beno1_gw.js → CreatedOnDcom-en-us-BgdIGfT3.js} +1 -1
- package/lib/src/{CreatedOnDcom-es-es-BBE_zdRu.js → CreatedOnDcom-es-es-D0KemkCZ.js} +1 -1
- package/lib/src/{CreatedOnDcom-fr-ca-C6pgDB5x.js → CreatedOnDcom-fr-ca-BuwNjNbC.js} +1 -1
- package/lib/src/{CreatedOnDcom-fr-fr-FD1eqtg3.js → CreatedOnDcom-fr-fr-DCQcsFPf.js} +1 -1
- package/lib/src/{CreatedOnDcom-pt-br-BiSx3OwM.js → CreatedOnDcom-pt-br-DD-oLAS1.js} +1 -1
- package/lib/src/{CreatedOnDcom-pt-pt-Dn4DmKGA.js → CreatedOnDcom-pt-pt-D8qLq2wW.js} +1 -1
- package/lib/src/brand-page/components/contact-form/ContactForm.mixin.js +5 -2
- package/lib/src/brand-page/models/index.d.ts +34 -26
- package/lib/src/brand-page/utils/migration/migration-20.d.ts +2 -0
- package/lib/src/brand-page/utils/migration/migration-20.js +66 -0
- package/lib/src/brand-page/utils/migration.util.js +11 -0
- package/lib/src/brand-page/utils/text.util.d.ts +1 -1
- package/lib/src/brand-page/utils/text.util.js +32 -0
- package/lib/src/brand-page/utils/vcard.util.js +4 -4
- package/lib/src/brand-page/utils/view-model.util.d.ts +1 -0
- package/lib/src/brand-page/utils/view-model.util.js +68 -20
- package/lib/src/brand-page/view-models.d.ts +3 -3
- package/lib/src/brand-page-maker/__fixtures__/complex-rte.fixture.d.ts +22 -0
- package/lib/src/brand-page-maker/__fixtures__/complex-rte.fixture.js +22 -0
- package/lib/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.mixin.d.ts +1 -1
- package/lib/src/brand-page-maker/components/seo-settings/SeoPageSettings.mixin.d.ts +1 -1
- package/lib/src/brand-page-maker/composables/ai-chat/useAiMetadataBuilder.d.ts +0 -1
- package/lib/src/brand-page-maker/composables/ai-chat/useAiWebsiteBuilderChat.js +21 -9
- package/lib/src/brand-page-maker/composables/rich-text-editor/bubbleMenu.d.ts +2 -9
- package/lib/src/brand-page-maker/composables/rich-text-editor/bubbleMenu.js +6 -10
- package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/FontFamily.d.ts +14 -0
- package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/FontFamily.js +39 -0
- package/lib/src/brand-page-maker/composables/rich-text-editor/getRteExtensions.d.ts +28 -0
- package/lib/src/brand-page-maker/composables/rich-text-editor/getRteExtensions.js +90 -0
- package/lib/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.d.ts +1 -1
- package/lib/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.js +24 -103
- package/lib/src/brand-page-maker/stores/ai-thread/ai-website-builder-thread-store.d.ts +8 -2
- package/lib/src/brand-page-maker/stores/ai-thread/ai-website-builder-thread-store.js +20 -0
- package/lib/src/brand-page-maker/stores/brandpage-maker/brandpage-maker-store.js +7 -0
- package/lib/src/brand-page-maker/utils/index.d.ts +2 -0
- package/lib/src/brand-page-maker/utils/index.js +2 -0
- package/lib/src/brand-page-maker/utils/rich-text-editor.util.d.ts +28 -5
- package/lib/src/brand-page-maker/utils/rich-text-editor.util.js +48 -5
- package/lib/src/brand-page-maker/utils/rte-render.util.d.ts +63 -0
- package/lib/src/brand-page-maker/utils/rte-render.util.js +95 -0
- package/lib/src/brand-page-maker/utils/rte-sanitize.util.d.ts +6 -0
- package/lib/src/brand-page-maker/utils/rte-sanitize.util.js +158 -0
- package/lib/src/brand-page-maker/validators/field.validator.js +10 -2
- package/lib/src/css/library.brand-page-brandCrowd.min.css +39 -39
- package/lib/src/css/library.brand-page-brandPage.min.css +39 -39
- package/lib/src/css/library.brand-page-designCom.min.css +39 -39
- package/lib/src/css/library.brand-page-preview.min.css +39 -39
- package/lib/src/index.d.ts +1 -0
- package/lib/src/index.mjs +143 -138
- package/lib/src/{lib-BVHFYSLL.js → lib-Db6j9fQZ.js} +56262 -52768
- package/lib/src/lib.js +2 -0
- package/lib/src/metadata-builder/ai-metadata-executor.d.ts +0 -1
- package/lib/src/metadata-builder/ai-metadata-executor.js +0 -19
- package/lib/src/metadata-builder/builders/section-builders/about-section-builder.js +2 -3
- package/lib/src/metadata-builder/builders/section-builders/banner-section-builder.js +2 -3
- package/lib/src/metadata-builder/builders/section-builders/booking-section-builder.js +2 -3
- package/lib/src/metadata-builder/builders/section-builders/contact-form-builder.js +3 -4
- package/lib/src/metadata-builder/builders/section-builders/images-section-builder.js +2 -3
- package/lib/src/metadata-builder/builders/section-builders/mailing-list-builder.js +1 -2
- package/lib/src/metadata-builder/builders/section-builders/mailing-list-section-builder.js +1 -2
- package/lib/src/metadata-builder/builders/section-builders/profile-section-builder.js +3 -4
- package/lib/src/metadata-builder/builders/section-builders/shapes-section-builder.js +2 -3
- package/lib/src/metadata-builder/builders/section-builders/testimonial-section-builder.js +1 -2
- package/lib/src/metadata-builder/builders/section-builders/text-section-builder.js +15 -11
- package/lib/src/metadata-builder/helpers/rte.helpers.d.ts +11 -13
- package/lib/src/metadata-builder/helpers/rte.helpers.js +14 -73
- package/lib/src/metadata-builder/metadata-executor.js +0 -4
- package/lib/src/metadata-builder/models.d.ts +0 -2
- package/lib/src/metadata-builder/models.js +0 -2
- package/lib/src/metadata-builder/parsers/ai-toolcall-parser.d.ts +0 -19
- package/lib/src/metadata-builder/parsers/ai-toolcall-parser.js +0 -4
- package/lib/src/metadata-builder/sanitizers/rte-schema-check.d.ts +7 -8
- package/lib/src/metadata-builder/sanitizers/rte-schema-check.js +21 -52
- package/lib/src/shared/content-settings/components/GenericTextInput.mixin.d.ts +2 -2
- package/lib/src/shared/content-settings/components/GenericTextInput.mixin.js +14 -1
- package/lib/src/shared/content-settings/components/content-settings/ContentSettings.mixin.d.ts +1 -1
- package/lib/src/shared/content-settings/components/content-settings/ReplaceImageSettingsForContent.mixin.d.ts +1 -1
- package/lib/src/shared/content-settings/components/content-settings-item/ContentSettingsItemField.mixin.js +12 -3
- package/lib/src/shared/content-settings/components/email-field/EmailField.mixin.d.ts +2 -2
- package/lib/src/shared/content-settings/components/radio-field/RadioField.mixin.d.ts +1 -1
- package/lib/src/shared/content-settings/components/slider-field/SliderField.mixin.d.ts +1 -1
- package/lib/src/shared/content-settings/components/tel-field/TelField.mixin.d.ts +2 -2
- package/lib/src/shared/content-settings/components/testimonial-items-field/TestimonialItemsField.mixin.d.ts +3 -2
- package/lib/src/shared/content-settings/components/testimonial-items-field/TestimonialItemsField.mixin.js +10 -3
- package/lib/src/shared/content-settings/components/text-field/TextField.mixin.d.ts +2 -2
- package/lib/src/shared/content-settings/components/text-items-field/TextItemsField.mixin.d.ts +2 -1
- package/lib/src/shared/content-settings/components/text-items-field/TextItemsField.mixin.js +4 -1
- package/lib/src/shared/content-settings/components/textarea-field/TextareaField.mixin.d.ts +2 -2
- package/lib/src/shared/content-settings/components/url-field/UrlField.mixin.d.ts +2 -2
- package/lib/src/shared/content-settings/components/youtube-video-field/YoutubeVideoField.mixin.d.ts +2 -2
- package/lib/src/shared/side-bar/components/SideBar.mixin.d.ts +1 -1
- package/lib/src/utils/minify-brand-page-metadata.js +4 -1
- package/package.json +17 -15
- package/src/brand-page/components/contact-form/ContactForm.mixin.ts +5 -2
- package/src/brand-page/components/text-wrapper/TextWrapper.vue +17 -6
- package/src/brand-page/models/index.ts +35 -26
- package/src/brand-page/utils/__snapshots__/migration.util.test.ts.snap +3 -3
- package/src/brand-page/utils/migration/migration-20.server.test.ts +125 -0
- package/src/brand-page/utils/migration/migration-20.test.ts +72 -0
- package/src/brand-page/utils/migration/migration-20.ts +87 -0
- package/src/brand-page/utils/migration.util.ts +12 -0
- package/src/brand-page/utils/style.util.test.ts +51 -0
- package/src/brand-page/utils/text.util.test.ts +47 -0
- package/src/brand-page/utils/text.util.ts +43 -4
- package/src/brand-page/utils/vcard.util.ts +6 -4
- package/src/brand-page/utils/view-model.pageid-rte.test.ts +104 -0
- package/src/brand-page/utils/view-model.util.ts +94 -26
- package/src/brand-page/view-models.ts +3 -2
- package/src/brand-page-maker/__fixtures__/complex-rte.fixture.ts +23 -0
- package/src/brand-page-maker/components/rich-text-editor/RichTextEditor.vue +4 -3
- package/src/brand-page-maker/composables/ai-chat/useAiMetadataBuilder.ts +0 -1
- package/src/brand-page-maker/composables/ai-chat/useAiWebsiteBuilderChat.ts +28 -9
- package/src/brand-page-maker/composables/rich-text-editor/bubbleMenu.ts +6 -10
- package/src/brand-page-maker/composables/rich-text-editor/extensions/FontFamily.test.ts +52 -0
- package/src/brand-page-maker/composables/rich-text-editor/extensions/FontFamily.ts +42 -0
- package/src/brand-page-maker/composables/rich-text-editor/getRteExtensions.ts +137 -0
- package/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.ts +27 -111
- package/src/brand-page-maker/stores/ai-thread/ai-website-builder-thread-store.ts +23 -0
- package/src/brand-page-maker/stores/brandpage-maker/brandpage-maker-store.ts +8 -0
- package/src/brand-page-maker/utils/__snapshots__/rte-render-parity.test.ts.snap +202 -0
- package/src/brand-page-maker/utils/index.ts +2 -0
- package/src/brand-page-maker/utils/rich-text-editor.util.test.ts +37 -3
- package/src/brand-page-maker/utils/rich-text-editor.util.ts +66 -11
- package/src/brand-page-maker/utils/rte-features.test.ts +351 -0
- package/src/brand-page-maker/utils/rte-render-parity.test.ts +157 -0
- package/src/brand-page-maker/utils/rte-render.util.ts +165 -0
- package/src/brand-page-maker/utils/rte-sanitize-json.test.ts +158 -0
- package/src/brand-page-maker/utils/rte-sanitize.util.ts +195 -0
- package/src/brand-page-maker/validators/field.validator.test.ts +55 -0
- package/src/brand-page-maker/validators/field.validator.ts +10 -2
- package/src/lib.ts +9 -0
- package/src/metadata-builder/ai-metadata-executor.ts +0 -26
- package/src/metadata-builder/builders/section-builders/about-section-builder.ts +2 -3
- package/src/metadata-builder/builders/section-builders/banner-section-builder.ts +2 -3
- package/src/metadata-builder/builders/section-builders/booking-section-builder.ts +2 -5
- package/src/metadata-builder/builders/section-builders/contact-form-builder.ts +3 -4
- package/src/metadata-builder/builders/section-builders/images-section-builder.ts +2 -3
- package/src/metadata-builder/builders/section-builders/mailing-list-builder.ts +1 -2
- package/src/metadata-builder/builders/section-builders/mailing-list-section-builder.ts +1 -2
- package/src/metadata-builder/builders/section-builders/profile-section-builder.ts +3 -6
- package/src/metadata-builder/builders/section-builders/shapes-section-builder.ts +2 -3
- package/src/metadata-builder/builders/section-builders/testimonial-section-builder.ts +1 -2
- package/src/metadata-builder/builders/section-builders/text-section-builder.ts +19 -13
- package/src/metadata-builder/helpers/rte.helpers.ts +15 -101
- package/src/metadata-builder/metadata-executor.ts +0 -5
- package/src/metadata-builder/models.ts +0 -3
- package/src/metadata-builder/parsers/ai-toolcall-parser.ts +0 -27
- package/src/metadata-builder/sanitizers/rte-schema-check.ts +24 -53
- package/src/shared/content-settings/components/GenericTextInput.mixin.ts +17 -1
- package/src/shared/content-settings/components/content-settings-item/ContentSettingsItemField.mixin.ts +16 -3
- package/src/shared/content-settings/components/testimonial-items-field/TestimonialItemsField.mixin.ts +13 -4
- package/src/shared/content-settings/components/testimonial-items-field/TestimonialItemsField.vue +6 -4
- package/src/shared/content-settings/components/text-image-field/TextImageField.vue +6 -3
- package/src/shared/content-settings/components/text-items-field/TextItemsField.mixin.ts +6 -1
- package/src/shared/content-settings/components/text-items-field/TextItemsField.vue +1 -1
- package/src/utils/minify-brand-page-metadata.ts +4 -1
- package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/FontSize.d.ts +0 -20
- package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/FontSize.js +0 -44
- package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/LineHeight.d.ts +0 -27
- package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/LineHeight.js +0 -44
- package/lib/src/metadata-builder/builders/update-content-text-builder.d.ts +0 -17
- package/lib/src/metadata-builder/builders/update-content-text-builder.js +0 -103
- package/lib/src/metadata-builder/builders/update-content-text-style-builder.d.ts +0 -16
- package/lib/src/metadata-builder/builders/update-content-text-style-builder.js +0 -216
- package/src/brand-page-maker/composables/rich-text-editor/extensions/FontSize.ts +0 -73
- package/src/brand-page-maker/composables/rich-text-editor/extensions/LineHeight.ts +0 -85
- package/src/metadata-builder/builders/update-content-text-builder.ts +0 -148
- 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:
|
|
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:
|
|
308
|
-
title:
|
|
309
|
-
description:
|
|
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:
|
|
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:
|
|
391
|
-
description:
|
|
399
|
+
title: RichTextValue;
|
|
400
|
+
description: RichTextValue;
|
|
392
401
|
buttonText: string;
|
|
393
|
-
successMessage:
|
|
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:
|
|
414
|
-
description:
|
|
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:
|
|
445
|
-
paragraph:
|
|
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?:
|
|
470
|
-
paragraph?:
|
|
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:
|
|
482
|
-
position:
|
|
483
|
-
testimonials:
|
|
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:
|
|
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:
|
|
522
|
-
description:
|
|
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:
|
|
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:
|
|
557
|
-
paragraph:
|
|
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:
|
|
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:
|
|
614
|
-
description:
|
|
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":
|
|
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":
|
|
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":
|
|
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
|
-
|
|
11
|
-
|
|
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
|