@designcrowd/library.brand-page 6.1.1-json-prereq-4 → 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.
- package/lib/src/{CreatedOnBrandCrowd-de-de-Dk-G3RbI.js → CreatedOnBrandCrowd-de-de-B5B5ThJ6.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-en-us-AVAWRjbj.js → CreatedOnBrandCrowd-en-us-a48CaORy.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-es-es-C5Xq4Jln.js → CreatedOnBrandCrowd-es-es-xpuZ317-.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-fr-fr-BQXjmcrr.js → CreatedOnBrandCrowd-fr-ca-V-iUMArW.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-fr-ca-BQXjmcrr.js → CreatedOnBrandCrowd-fr-fr-V-iUMArW.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-pt-pt-DRMRc1Zk.js → CreatedOnBrandCrowd-pt-br-DZ0NKSAy.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-pt-br-DRMRc1Zk.js → CreatedOnBrandCrowd-pt-pt-DZ0NKSAy.js} +1 -1
- package/lib/src/{CreatedOnDcom-de-de-Dd9-oQ34.js → CreatedOnDcom-de-de-_950cgGp.js} +1 -1
- package/lib/src/{CreatedOnDcom-en-us-CnDfLzF4.js → CreatedOnDcom-en-us-DogKzH-g.js} +1 -1
- package/lib/src/{CreatedOnDcom-es-es-bvqvI9gT.js → CreatedOnDcom-es-es-CmGM3IPX.js} +1 -1
- package/lib/src/{CreatedOnDcom-fr-ca-BG7UGFe8.js → CreatedOnDcom-fr-ca-BonEDkWZ.js} +1 -1
- package/lib/src/{CreatedOnDcom-fr-fr-xdz35O_B.js → CreatedOnDcom-fr-fr-DQHtKrd_.js} +1 -1
- package/lib/src/{CreatedOnDcom-pt-br-CLfHnCkk.js → CreatedOnDcom-pt-br-CtYJSBjs.js} +1 -1
- package/lib/src/{CreatedOnDcom-pt-pt-Z7FY0BXt.js → CreatedOnDcom-pt-pt-DoO9r4lA.js} +1 -1
- package/lib/src/brand-page/components/contact-form/ContactForm.mixin.js +7 -8
- package/lib/src/brand-page/components/mailing-list/MailingList.mixin.js +6 -4
- package/lib/src/brand-page/components/page-footer/PageFooter.mixin.d.ts +1 -0
- package/lib/src/brand-page/components/page-footer/PageFooter.mixin.js +4 -1
- package/lib/src/brand-page/components/payment/Payment.mixin.js +4 -3
- package/lib/src/brand-page/components/payment/__fixtures__/request-payment.d.ts +1 -0
- package/lib/src/brand-page/components/payment/__fixtures__/support-me.d.ts +1 -0
- package/lib/src/brand-page/components/payment/request-payment/RequestPayment.mixin.d.ts +1 -0
- package/lib/src/brand-page/components/payment/request-payment/RequestPayment.mixin.js +4 -1
- package/lib/src/brand-page/composables/consent.composable.d.ts +2 -0
- package/lib/src/brand-page/composables/consent.composable.js +9 -3
- package/lib/src/brand-page/models/brand-page-context.model.d.ts +7 -0
- package/lib/src/brand-page/models/index.d.ts +26 -34
- package/lib/src/brand-page/utils/context.util.d.ts +8 -0
- package/lib/src/brand-page/utils/context.util.js +8 -0
- package/lib/src/brand-page/utils/migration.util.js +0 -11
- package/lib/src/brand-page/utils/text.util.d.ts +1 -13
- package/lib/src/brand-page/utils/text.util.js +0 -64
- package/lib/src/brand-page/utils/vcard.util.js +4 -4
- package/lib/src/brand-page/utils/view-model.util.d.ts +0 -1
- package/lib/src/brand-page/utils/view-model.util.js +20 -68
- package/lib/src/brand-page/view-models.d.ts +3 -3
- package/lib/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.mixin.d.ts +1 -1
- package/lib/src/brand-page-maker/components/template-preview/TemplatePreview.mixin.js +51 -18
- package/lib/src/brand-page-maker/composables/ai-chat/useAiMetadataBuilder.d.ts +1 -0
- package/lib/src/brand-page-maker/composables/rich-text-editor/bubbleMenu.d.ts +9 -2
- package/lib/src/brand-page-maker/composables/rich-text-editor/bubbleMenu.js +10 -6
- package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/FontSize.d.ts +20 -0
- package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/FontSize.js +44 -0
- package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/LineHeight.d.ts +27 -0
- package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/LineHeight.js +44 -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 +103 -24
- package/lib/src/brand-page-maker/stores/brandpage-container/brand-page-container-store.js +8 -0
- package/lib/src/brand-page-maker/stores/brandpage-maker/brandpage-maker-store.js +9 -3
- package/lib/src/brand-page-maker/stores/ui/ui-store.d.ts +1 -1
- package/lib/src/brand-page-maker/utils/index.d.ts +0 -2
- package/lib/src/brand-page-maker/utils/index.js +0 -2
- package/lib/src/brand-page-maker/utils/rich-text-editor.util.d.ts +5 -36
- package/lib/src/brand-page-maker/utils/rich-text-editor.util.js +5 -65
- package/lib/src/brand-page-maker/validators/field.validator.js +2 -10
- package/lib/src/clients/models.d.ts +2 -0
- package/lib/src/css/library.brand-page-brandCrowd.min.css +99 -102
- package/lib/src/css/library.brand-page-brandPage.min.css +99 -102
- package/lib/src/css/library.brand-page-designCom.min.css +99 -102
- package/lib/src/css/library.brand-page-preview.min.css +99 -102
- package/lib/src/index.d.ts +1 -1
- package/lib/src/index.mjs +219 -221
- package/lib/src/{lib-uYwxz8qR.js → lib-DtalJKI3.js} +71861 -75344
- package/lib/src/lib.js +1 -2
- package/lib/src/metadata-builder/ai-metadata-executor.d.ts +1 -0
- package/lib/src/metadata-builder/ai-metadata-executor.js +19 -0
- package/lib/src/metadata-builder/builders/section-builders/about-section-builder.js +3 -2
- package/lib/src/metadata-builder/builders/section-builders/banner-section-builder.js +3 -2
- package/lib/src/metadata-builder/builders/section-builders/booking-section-builder.js +3 -2
- package/lib/src/metadata-builder/builders/section-builders/contact-form-builder.js +4 -3
- package/lib/src/metadata-builder/builders/section-builders/images-section-builder.js +3 -2
- package/lib/src/metadata-builder/builders/section-builders/mailing-list-builder.js +2 -1
- package/lib/src/metadata-builder/builders/section-builders/mailing-list-section-builder.js +2 -1
- package/lib/src/metadata-builder/builders/section-builders/profile-section-builder.js +4 -3
- package/lib/src/metadata-builder/builders/section-builders/shapes-section-builder.js +3 -2
- package/lib/src/metadata-builder/builders/section-builders/testimonial-section-builder.js +2 -1
- package/lib/src/metadata-builder/builders/section-builders/text-section-builder.js +11 -15
- package/lib/src/metadata-builder/builders/update-content-text-builder.d.ts +17 -0
- package/lib/src/metadata-builder/builders/update-content-text-builder.js +103 -0
- package/lib/src/metadata-builder/builders/update-content-text-style-builder.d.ts +16 -0
- package/lib/src/metadata-builder/builders/update-content-text-style-builder.js +216 -0
- package/lib/src/metadata-builder/helpers/rte.helpers.d.ts +13 -11
- package/lib/src/metadata-builder/helpers/rte.helpers.js +73 -14
- package/lib/src/metadata-builder/metadata-executor.js +4 -0
- package/lib/src/metadata-builder/models.d.ts +2 -0
- package/lib/src/metadata-builder/models.js +2 -0
- package/lib/src/metadata-builder/parsers/ai-toolcall-parser.d.ts +19 -0
- package/lib/src/metadata-builder/parsers/ai-toolcall-parser.js +4 -0
- package/lib/src/metadata-builder/sanitizers/rte-schema-check.d.ts +8 -7
- package/lib/src/metadata-builder/sanitizers/rte-schema-check.js +52 -21
- package/lib/src/shared/content-settings/components/GenericTextInput.mixin.d.ts +1 -1
- package/lib/src/shared/content-settings/components/GenericTextInput.mixin.js +10 -35
- package/lib/src/shared/content-settings/components/content-settings-item/ContentSettingsItemField.mixin.d.ts +1 -1
- package/lib/src/shared/content-settings/components/content-settings-item/ContentSettingsItemField.mixin.js +3 -12
- package/lib/src/shared/content-settings/components/email-field/EmailField.mixin.d.ts +1 -1
- package/lib/src/shared/content-settings/components/tel-field/TelField.mixin.d.ts +1 -1
- package/lib/src/shared/content-settings/components/testimonial-items-field/TestimonialItemsField.mixin.d.ts +2 -3
- package/lib/src/shared/content-settings/components/testimonial-items-field/TestimonialItemsField.mixin.js +3 -10
- package/lib/src/shared/content-settings/components/text-field/TextField.mixin.d.ts +1 -1
- package/lib/src/shared/content-settings/components/text-items-field/TextItemsField.mixin.d.ts +1 -2
- package/lib/src/shared/content-settings/components/text-items-field/TextItemsField.mixin.js +1 -4
- package/lib/src/shared/content-settings/components/textarea-field/TextareaField.mixin.d.ts +1 -1
- package/lib/src/shared/content-settings/components/url-field/UrlField.mixin.d.ts +1 -1
- package/lib/src/shared/content-settings/components/youtube-video-field/YoutubeVideoField.mixin.d.ts +1 -1
- package/lib/src/shared/logo-settings/components/EditLogoSettings.mixin.d.ts +1 -1
- package/lib/src/shared/logo-versions/LogoVersions.mixin.d.ts +1 -1
- package/lib/src/shared/my-designs/MyDesigns.mixin.d.ts +1 -1
- package/lib/src/shared/my-uploads/MyMedia.mixin.d.ts +1 -1
- package/lib/src/shared/my-uploads/MyUploads.mixin.d.ts +1 -1
- package/lib/src/shared/page-settings/components/page-settings/PageSettings.mixin.d.ts +1 -1
- package/lib/src/shared/page-settings/components/page-settings-section-styles/PageSettingsSectionStyles.mixin.d.ts +1 -1
- package/lib/src/shared/style-settings/components/StyleSettings.mixin.d.ts +1 -1
- package/lib/src/shared/template-settings/components/TemplatesSettings.mixin.d.ts +1 -1
- package/lib/src/shared/template-settings/store/template-settings-store.d.ts +2 -0
- package/lib/src/shared/template-settings/store/template-settings-store.js +72 -24
- package/lib/src/shared/unsplash-library/UnsplashLibrary.mixin.d.ts +1 -1
- package/lib/src/utils/minify-brand-page-metadata.js +1 -4
- package/package.json +15 -17
- package/src/brand-page/components/contact-form/ContactForm.mixin.ts +9 -7
- package/src/brand-page/components/cookie-consent/CookieConsent.vue +1 -0
- package/src/brand-page/components/mailing-list/MailingList.mixin.ts +8 -3
- package/src/brand-page/components/navigation/navigation-carousel/NavigationCarousel.vue +5 -1
- package/src/brand-page/components/page-footer/PageFooter.mixin.ts +4 -1
- package/src/brand-page/components/page-footer/PageFooter.vue +1 -1
- package/src/brand-page/components/payment/Payment.mixin.ts +4 -3
- package/src/brand-page/components/payment/request-payment/RequestPayment.mixin.ts +8 -1
- package/src/brand-page/components/payment/request-payment/RequestPayment.vue +1 -1
- package/src/brand-page/components/save-contact/SaveContactWidget.vue +2 -1
- package/src/brand-page/components/shop/Shop.vue +6 -3
- package/src/brand-page/components/social-links/SocialLinks.vue +5 -2
- package/src/brand-page/components/text-wrapper/TextWrapper.vue +6 -17
- package/src/brand-page/composables/consent.composable.ts +18 -4
- package/src/brand-page/models/brand-page-context.model.ts +7 -0
- package/src/brand-page/models/index.ts +26 -35
- package/src/brand-page/utils/__snapshots__/migration.util.test.ts.snap +3 -3
- package/src/brand-page/utils/context.util.test.ts +43 -0
- package/src/brand-page/utils/context.util.ts +10 -0
- package/src/brand-page/utils/migration.util.ts +0 -12
- package/src/brand-page/utils/style.util.test.ts +0 -51
- package/src/brand-page/utils/text.util.ts +4 -83
- package/src/brand-page/utils/vcard.util.ts +4 -6
- package/src/brand-page/utils/view-model.util.ts +26 -94
- package/src/brand-page/view-models.ts +2 -3
- package/src/brand-page-maker/components/rich-text-editor/RichTextEditor.vue +4 -5
- package/src/brand-page-maker/components/template-preview/TemplatePreview.mixin.test.ts +135 -0
- package/src/brand-page-maker/components/template-preview/TemplatePreview.mixin.ts +62 -21
- package/src/brand-page-maker/composables/ai-chat/useAiMetadataBuilder.ts +1 -0
- package/src/brand-page-maker/composables/rich-text-editor/bubbleMenu.ts +10 -6
- package/src/brand-page-maker/composables/rich-text-editor/extensions/FontSize.ts +73 -0
- package/src/brand-page-maker/composables/rich-text-editor/extensions/LineHeight.ts +85 -0
- package/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.ts +111 -27
- package/src/brand-page-maker/stores/brandpage-container/brand-page-container-store.ts +9 -0
- package/src/brand-page-maker/stores/brandpage-maker/brandpage-maker-store.ts +11 -3
- package/src/brand-page-maker/stores/ui/ui-store.ts +1 -1
- package/src/brand-page-maker/utils/index.ts +0 -2
- package/src/brand-page-maker/utils/rich-text-editor.util.test.ts +3 -62
- package/src/brand-page-maker/utils/rich-text-editor.util.ts +11 -86
- package/src/brand-page-maker/validators/field.validator.ts +2 -10
- package/src/clients/models.ts +3 -0
- package/src/lib.ts +1 -9
- package/src/metadata-builder/ai-metadata-executor.ts +26 -0
- package/src/metadata-builder/builders/section-builders/about-section-builder.ts +3 -2
- package/src/metadata-builder/builders/section-builders/banner-section-builder.ts +3 -2
- package/src/metadata-builder/builders/section-builders/booking-section-builder.ts +5 -2
- package/src/metadata-builder/builders/section-builders/contact-form-builder.ts +4 -3
- package/src/metadata-builder/builders/section-builders/images-section-builder.ts +3 -2
- package/src/metadata-builder/builders/section-builders/mailing-list-builder.ts +2 -1
- package/src/metadata-builder/builders/section-builders/mailing-list-section-builder.ts +2 -1
- package/src/metadata-builder/builders/section-builders/profile-section-builder.ts +6 -3
- package/src/metadata-builder/builders/section-builders/shapes-section-builder.ts +3 -2
- package/src/metadata-builder/builders/section-builders/testimonial-section-builder.ts +2 -1
- package/src/metadata-builder/builders/section-builders/text-section-builder.ts +13 -19
- package/src/metadata-builder/builders/update-content-text-builder.ts +148 -0
- package/src/metadata-builder/builders/update-content-text-style-builder.ts +352 -0
- package/src/metadata-builder/helpers/rte.helpers.ts +101 -15
- package/src/metadata-builder/metadata-executor.ts +5 -0
- package/src/metadata-builder/models.ts +3 -0
- package/src/metadata-builder/parsers/ai-toolcall-parser.ts +27 -0
- package/src/metadata-builder/sanitizers/rte-schema-check.ts +53 -24
- package/src/shared/content-settings/components/GenericTextInput.mixin.ts +11 -42
- package/src/shared/content-settings/components/content-settings-item/ContentSettingsItemField.mixin.ts +3 -16
- package/src/shared/content-settings/components/testimonial-items-field/TestimonialItemsField.mixin.ts +4 -13
- package/src/shared/content-settings/components/testimonial-items-field/TestimonialItemsField.vue +4 -6
- package/src/shared/content-settings/components/text-image-field/TextImageField.vue +3 -6
- package/src/shared/content-settings/components/text-items-field/TextItemsField.mixin.ts +1 -6
- package/src/shared/content-settings/components/text-items-field/TextItemsField.vue +1 -1
- package/src/shared/template-settings/store/template-settings-store.ts +93 -27
- package/src/utils/minify-brand-page-metadata.ts +1 -4
- package/lib/src/brand-page/utils/migration/migration-20.d.ts +0 -2
- package/lib/src/brand-page/utils/migration/migration-20.js +0 -66
- package/lib/src/brand-page-maker/__fixtures__/complex-rte.fixture.d.ts +0 -22
- package/lib/src/brand-page-maker/__fixtures__/complex-rte.fixture.js +0 -22
- package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/FontFamily.d.ts +0 -14
- package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/FontFamily.js +0 -39
- package/lib/src/brand-page-maker/composables/rich-text-editor/getRteExtensions.d.ts +0 -28
- package/lib/src/brand-page-maker/composables/rich-text-editor/getRteExtensions.js +0 -117
- package/lib/src/brand-page-maker/utils/rte-render.util.d.ts +0 -63
- package/lib/src/brand-page-maker/utils/rte-render.util.js +0 -95
- package/lib/src/brand-page-maker/utils/rte-sanitize.util.d.ts +0 -6
- package/lib/src/brand-page-maker/utils/rte-sanitize.util.js +0 -156
- package/src/brand-page/utils/migration/migration-20.server.test.ts +0 -125
- package/src/brand-page/utils/migration/migration-20.test.ts +0 -72
- package/src/brand-page/utils/migration/migration-20.ts +0 -87
- package/src/brand-page/utils/text.util.test.ts +0 -106
- package/src/brand-page/utils/view-model.pageid-rte.test.ts +0 -104
- package/src/brand-page-maker/__fixtures__/complex-rte.fixture.ts +0 -23
- package/src/brand-page-maker/composables/rich-text-editor/extensions/FontFamily.test.ts +0 -52
- package/src/brand-page-maker/composables/rich-text-editor/extensions/FontFamily.ts +0 -42
- package/src/brand-page-maker/composables/rich-text-editor/getRteExtensions.ts +0 -162
- package/src/brand-page-maker/utils/__snapshots__/rte-render-parity.test.ts.snap +0 -202
- package/src/brand-page-maker/utils/rte-features.test.ts +0 -378
- package/src/brand-page-maker/utils/rte-render-parity.test.ts +0 -164
- package/src/brand-page-maker/utils/rte-render.util.ts +0 -165
- package/src/brand-page-maker/utils/rte-sanitize-json.test.ts +0 -172
- package/src/brand-page-maker/utils/rte-sanitize.util.ts +0 -192
- package/src/brand-page-maker/validators/field.validator.test.ts +0 -55
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import { hasRTEContent, toRteJson, resolveHtmlParser, } from '../../../brand-page-maker/utils';
|
|
2
|
-
/**
|
|
3
|
-
* BP-5787 — migrate existing inline-HTML RTE content to Tiptap JSON.
|
|
4
|
-
*
|
|
5
|
-
* The editor emits, and the renderer reads, Tiptap JSON. Existing
|
|
6
|
-
* published/saved sites still hold RTE fields as HTML strings. This converts
|
|
7
|
-
* them on load, in EVERY environment:
|
|
8
|
-
* - maker (browser): the loaded model is JSON, so the next save persists JSON.
|
|
9
|
-
* - web-renderer (SSR / server API routes): converted on read, so old sites
|
|
10
|
-
* are normalised without a re-save and downstream code can treat RTE as JSON.
|
|
11
|
-
*
|
|
12
|
-
* This is a one-time, versioned migration (runAlways is NOT set): legacy sites
|
|
13
|
-
* are at model version < 20 and convert on first load in any environment; once
|
|
14
|
-
* `v` is stamped to 20 the conversion is skipped. This is only safe because the
|
|
15
|
-
* AI section builders now emit Tiptap JSON directly (they no longer write HTML),
|
|
16
|
-
* so there is no recurring post-load source of HTML to normalise.
|
|
17
|
-
*
|
|
18
|
-
* HTML→JSON parsing needs a DOM parser. Rather than mutate a global `window`,
|
|
19
|
-
* the parser is resolved once (native `DOMParser` in the browser, a jsdom
|
|
20
|
-
* `DOMParser` instance on the server) and threaded explicitly through the
|
|
21
|
-
* conversion — see resolveHtmlParser / toRteJson. No global state is touched, so
|
|
22
|
-
* the pass is safe on any server runtime and under concurrent requests.
|
|
23
|
-
*
|
|
24
|
-
* Scope: only HTML-RTE strings (hasRTEContent) are converted. Plain strings,
|
|
25
|
-
* numbers, and already-JSON docs are left untouched, so the migration is
|
|
26
|
-
* idempotent (re-running on JSON content is a no-op — non-string values are
|
|
27
|
-
* skipped). Nested RTE inside item arrays (textItems, testimonials, images…)
|
|
28
|
-
* is handled by recursing into arrays; style objects are intentionally not
|
|
29
|
-
* descended into (they never hold RTE content).
|
|
30
|
-
*/
|
|
31
|
-
const convertRteStringsToJson = (value, parser) => {
|
|
32
|
-
if (Array.isArray(value)) {
|
|
33
|
-
value.forEach((item) => convertRteStringsToJson(item, parser));
|
|
34
|
-
return;
|
|
35
|
-
}
|
|
36
|
-
if (!value || typeof value !== 'object')
|
|
37
|
-
return;
|
|
38
|
-
const record = value;
|
|
39
|
-
Object.keys(record).forEach((key) => {
|
|
40
|
-
const field = record[key];
|
|
41
|
-
if (typeof field === 'string' && hasRTEContent(field)) {
|
|
42
|
-
const json = toRteJson(field, parser);
|
|
43
|
-
if (json)
|
|
44
|
-
record[key] = json;
|
|
45
|
-
}
|
|
46
|
-
else if (Array.isArray(field)) {
|
|
47
|
-
convertRteStringsToJson(field, parser);
|
|
48
|
-
}
|
|
49
|
-
});
|
|
50
|
-
};
|
|
51
|
-
const convertModel = (brandPageModel, parser) => {
|
|
52
|
-
for (const page of brandPageModel.pages) {
|
|
53
|
-
for (const content of page.contents) {
|
|
54
|
-
convertRteStringsToJson(content, parser);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
};
|
|
58
|
-
export const migration20 = async (migrationResult) => {
|
|
59
|
-
const { brandPageModel } = migrationResult;
|
|
60
|
-
// Resolve a DOM parser for this environment (native in the browser, a jsdom
|
|
61
|
-
// instance on the server — never touching globals), then convert every
|
|
62
|
-
// HTML-RTE field to Tiptap JSON with it.
|
|
63
|
-
const parser = await resolveHtmlParser();
|
|
64
|
-
convertModel(brandPageModel, parser);
|
|
65
|
-
return migrationResult;
|
|
66
|
-
};
|
|
@@ -1,22 +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 declare 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
|
-
* The theme-token CSS variable that MUST survive the HTML→JSON round-trip
|
|
20
|
-
* (BP-5787 stores token refs instead of flattening them to rgb()).
|
|
21
|
-
*/
|
|
22
|
-
export declare const THEME_TOKEN_REF = "var(--logo-text-color)";
|
|
@@ -1,22 +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
|
-
* The theme-token CSS variable that MUST survive the HTML→JSON round-trip
|
|
20
|
-
* (BP-5787 stores token refs instead of flattening them to rgb()).
|
|
21
|
-
*/
|
|
22
|
-
export const THEME_TOKEN_REF = 'var(--logo-text-color)';
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* BP-5787 — font-family with quote-stripping parse.
|
|
3
|
-
*
|
|
4
|
-
* The stock extension stores whatever `element.style.fontFamily` returns. For
|
|
5
|
-
* multi-word families (e.g. "Open Sans", "Playwrite AU") the browser quotes the
|
|
6
|
-
* value, so parsing HTML — pasting RTE-styled text between editors, or the
|
|
7
|
-
* HTML→JSON migration — yields a value that still contains the quote chars
|
|
8
|
-
* (`"Open Sans"`). That value then (a) no longer matches the font list and,
|
|
9
|
-
* worse, (b) is dropped by the native RTE sanitiser (`sanitizeRteJson`), whose
|
|
10
|
-
* `isSafeCssValue` rejects any value containing `"` `'` `;` — so the font is
|
|
11
|
-
* silently lost. Mirror the FontSize/LetterSpacing extensions and strip the
|
|
12
|
-
* quotes on parse so the stored value is always the bare family name.
|
|
13
|
-
*/
|
|
14
|
-
export declare const FontFamily: import("@tiptap/core").Extension<import("@tiptap/extension-text-style").FontFamilyOptions, any>;
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import TiptapFontFamily from '@tiptap/extension-font-family';
|
|
2
|
-
/**
|
|
3
|
-
* BP-5787 — font-family with quote-stripping parse.
|
|
4
|
-
*
|
|
5
|
-
* The stock extension stores whatever `element.style.fontFamily` returns. For
|
|
6
|
-
* multi-word families (e.g. "Open Sans", "Playwrite AU") the browser quotes the
|
|
7
|
-
* value, so parsing HTML — pasting RTE-styled text between editors, or the
|
|
8
|
-
* HTML→JSON migration — yields a value that still contains the quote chars
|
|
9
|
-
* (`"Open Sans"`). That value then (a) no longer matches the font list and,
|
|
10
|
-
* worse, (b) is dropped by the native RTE sanitiser (`sanitizeRteJson`), whose
|
|
11
|
-
* `isSafeCssValue` rejects any value containing `"` `'` `;` — so the font is
|
|
12
|
-
* silently lost. Mirror the FontSize/LetterSpacing extensions and strip the
|
|
13
|
-
* quotes on parse so the stored value is always the bare family name.
|
|
14
|
-
*/
|
|
15
|
-
export const FontFamily = TiptapFontFamily.extend({
|
|
16
|
-
addGlobalAttributes() {
|
|
17
|
-
return [
|
|
18
|
-
{
|
|
19
|
-
types: this.options.types,
|
|
20
|
-
attributes: {
|
|
21
|
-
fontFamily: {
|
|
22
|
-
default: null,
|
|
23
|
-
// Strip quotes only — keep '' for an absent value so the stored
|
|
24
|
-
// JSON shape matches the other textStyle attrs (FontSize etc.).
|
|
25
|
-
parseHTML: (element) => element.style.fontFamily?.replace(/['"]+/g, ''),
|
|
26
|
-
renderHTML: (attributes) => {
|
|
27
|
-
if (!attributes.fontFamily) {
|
|
28
|
-
return {};
|
|
29
|
-
}
|
|
30
|
-
return {
|
|
31
|
-
style: `font-family: ${attributes.fontFamily}`,
|
|
32
|
-
};
|
|
33
|
-
},
|
|
34
|
-
},
|
|
35
|
-
},
|
|
36
|
-
},
|
|
37
|
-
];
|
|
38
|
-
},
|
|
39
|
-
});
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import type { Extensions } from '@tiptap/vue-3';
|
|
2
|
-
/**
|
|
3
|
-
* Single source of truth for the Tiptap extension set (BP-5787).
|
|
4
|
-
*
|
|
5
|
-
* The live editor, the headless serialiser (useExternalRichTextEditor), and
|
|
6
|
-
* the upcoming render-from-JSON path must all use the SAME extensions, or
|
|
7
|
-
* rendered output drifts from what the user authored. This factory removes the
|
|
8
|
-
* previous duplication while preserving each consumer's exact configuration.
|
|
9
|
-
*
|
|
10
|
-
* Known, intentional differences are parameterised:
|
|
11
|
-
* - `restrictStarterKit` — the live editor disables list/hr/code nodes.
|
|
12
|
-
* - `editing` — only the live editor carries the Placeholder + MaxLength plugins
|
|
13
|
-
* (neither affects serialised output; they are editing-time only).
|
|
14
|
-
*
|
|
15
|
-
* Link target/rel are NOT parameterised: they are a pure function of the href
|
|
16
|
-
* applied in the Link mark's renderHTML (below), so every consumer — live
|
|
17
|
-
* editor, static renderer, schema check — emits identical link markup.
|
|
18
|
-
*/
|
|
19
|
-
export interface RteExtensionOptions {
|
|
20
|
-
/** Disable StarterKit nodes the editor doesn't use (lists, hr, code). */
|
|
21
|
-
restrictStarterKit?: boolean;
|
|
22
|
-
/** When present, add the live-editing-only plugins (Placeholder, MaxLength). */
|
|
23
|
-
editing?: {
|
|
24
|
-
placeholder?: string;
|
|
25
|
-
maxLength?: number;
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
export declare const getRteExtensions: (options?: RteExtensionOptions) => Extensions;
|
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
import StarterKit from '@tiptap/starter-kit';
|
|
2
|
-
import Underline from '@tiptap/extension-underline';
|
|
3
|
-
import { TextStyle, FontSize, LineHeight } from '@tiptap/extension-text-style';
|
|
4
|
-
import TextAlign from '@tiptap/extension-text-align';
|
|
5
|
-
import ColorExtension from '@tiptap/extension-color';
|
|
6
|
-
import Highlight from '@tiptap/extension-highlight';
|
|
7
|
-
import Placeholder from '@tiptap/extension-placeholder';
|
|
8
|
-
import Link from '@tiptap/extension-link';
|
|
9
|
-
import { FontFamily } from './extensions/FontFamily';
|
|
10
|
-
import { LetterSpacing } from './extensions/LetterSpacing';
|
|
11
|
-
import { MaxLength } from './extensions/MaxLength';
|
|
12
|
-
import { AutoLink } from './extensions/AutoLink';
|
|
13
|
-
import { isHrefPageId } from './hyperlink';
|
|
14
|
-
export const getRteExtensions = (options = {}) => {
|
|
15
|
-
const { restrictStarterKit = false, editing } = options;
|
|
16
|
-
const extensions = [
|
|
17
|
-
// v3 StarterKit bundles Link + Underline; we register both separately (with
|
|
18
|
-
// custom Link config), so disable the bundled copies to avoid duplicate
|
|
19
|
-
// extension errors.
|
|
20
|
-
// trailingNode: false — v3 StarterKit adds a TrailingNode plugin (not in v2)
|
|
21
|
-
// that appends an empty paragraph after any non-paragraph trailing block.
|
|
22
|
-
// Disabled to keep editor/stored-content behaviour identical to v2 (BP-5787
|
|
23
|
-
// is backward-compat-first); revisit as a standalone UX change if wanted.
|
|
24
|
-
restrictStarterKit
|
|
25
|
-
? StarterKit.configure({
|
|
26
|
-
link: false,
|
|
27
|
-
underline: false,
|
|
28
|
-
trailingNode: false,
|
|
29
|
-
bulletList: false,
|
|
30
|
-
orderedList: false,
|
|
31
|
-
listItem: false,
|
|
32
|
-
horizontalRule: false,
|
|
33
|
-
codeBlock: false,
|
|
34
|
-
code: false,
|
|
35
|
-
})
|
|
36
|
-
: StarterKit.configure({
|
|
37
|
-
link: false,
|
|
38
|
-
underline: false,
|
|
39
|
-
trailingNode: false,
|
|
40
|
-
}),
|
|
41
|
-
Underline,
|
|
42
|
-
// priority: 99 — render the textStyle <span> (which carries font-size) as the
|
|
43
|
-
// OUTERMOST inline mark, so decoration marks (<u>/<s>) nest INSIDE it and
|
|
44
|
-
// inherit the applied font-size. v3's extension-text-style defaults to
|
|
45
|
-
// priority 101, which flips this: <u>/<s> end up outside the font-size span
|
|
46
|
-
// and inherit only the base paragraph size. Since text-decoration thickness
|
|
47
|
-
// AND vertical position are computed from the decorating element's own font,
|
|
48
|
-
// that produced thin, mis-centred underline/strike on large text — a
|
|
49
|
-
// regression vs v2's serializer, which emitted the font-size span outermost
|
|
50
|
-
// (BP-5787, backward-compat-first).
|
|
51
|
-
TextStyle.extend({ priority: 99 }).configure({
|
|
52
|
-
mergeNestedSpanStyles: true,
|
|
53
|
-
}),
|
|
54
|
-
TextAlign.configure({
|
|
55
|
-
types: ['heading', 'paragraph'],
|
|
56
|
-
}),
|
|
57
|
-
FontFamily.configure({
|
|
58
|
-
types: ['textStyle'],
|
|
59
|
-
}),
|
|
60
|
-
// FontSize + LineHeight are now shipped natively by @tiptap/extension-text-style.
|
|
61
|
-
FontSize.configure({ types: ['textStyle'] }),
|
|
62
|
-
LetterSpacing,
|
|
63
|
-
LineHeight.configure({ types: ['textStyle'] }),
|
|
64
|
-
ColorExtension.configure({
|
|
65
|
-
types: ['textStyle'],
|
|
66
|
-
}),
|
|
67
|
-
Highlight.configure({
|
|
68
|
-
multicolor: true,
|
|
69
|
-
}),
|
|
70
|
-
];
|
|
71
|
-
// Live-editing-only plugins sit between Highlight and Link (original order).
|
|
72
|
-
if (editing) {
|
|
73
|
-
extensions.push(Placeholder.configure({ placeholder: editing.placeholder }), MaxLength.configure({ maxLength: editing.maxLength }));
|
|
74
|
-
}
|
|
75
|
-
extensions.push(Link.extend({
|
|
76
|
-
// External-link policy lives at the serialisation boundary: target/rel are
|
|
77
|
-
// a pure function of the href, applied whenever a link mark renders to <a>
|
|
78
|
-
// (live editor DOM + static renderer alike). They are neither stored on the
|
|
79
|
-
// mark nor the sanitiser's concern — the sanitiser validates href only
|
|
80
|
-
// (see rte-sanitize.util.ts). This is what the old, unsupported
|
|
81
|
-
// `HTMLAttributes: (attrs) => …` function was trying (and failing) to do:
|
|
82
|
-
// Tiptap types HTMLAttributes as a static object, so a function silently
|
|
83
|
-
// no-ops, which is why fresh links lost target/rel after the JSON migration.
|
|
84
|
-
renderHTML(props) {
|
|
85
|
-
const href = props.HTMLAttributes.href;
|
|
86
|
-
const isExternal = typeof href === 'string' &&
|
|
87
|
-
(href.startsWith('http://') || href.startsWith('https://'));
|
|
88
|
-
const HTMLAttributes = {
|
|
89
|
-
...props.HTMLAttributes,
|
|
90
|
-
target: isExternal ? '_blank' : null,
|
|
91
|
-
rel: isExternal ? 'noopener noreferrer' : null,
|
|
92
|
-
};
|
|
93
|
-
// Delegate to the base renderHTML so its XSS href-strip guard still runs.
|
|
94
|
-
// base Link always defines renderHTML, so the ?? branch is a defensive
|
|
95
|
-
// fallback only (never taken in practice); href is pre-validated by the
|
|
96
|
-
// sanitiser and isAllowedUri regardless.
|
|
97
|
-
return (this.parent?.({ ...props, HTMLAttributes }) ?? [
|
|
98
|
-
'a',
|
|
99
|
-
HTMLAttributes,
|
|
100
|
-
0,
|
|
101
|
-
]);
|
|
102
|
-
},
|
|
103
|
-
}).configure({
|
|
104
|
-
openOnClick: false,
|
|
105
|
-
defaultProtocol: 'https',
|
|
106
|
-
autolink: true,
|
|
107
|
-
// Override Tiptap's built-in Link defaults ({ target: '_blank', rel:
|
|
108
|
-
// 'noopener noreferrer nofollow' }) with nulls: target/rel are NOT stored on
|
|
109
|
-
// the mark, they are derived from the href at render time (see renderHTML
|
|
110
|
-
// above). configure() deep-merges HTMLAttributes, so an empty {} would leave
|
|
111
|
-
// those built-in defaults in place and stamp them onto every link — hence
|
|
112
|
-
// the explicit nulls.
|
|
113
|
-
HTMLAttributes: { target: null, rel: null },
|
|
114
|
-
isAllowedUri: (url, ctx) => ctx.defaultValidate(url) || isHrefPageId(url),
|
|
115
|
-
}), AutoLink);
|
|
116
|
-
return extensions;
|
|
117
|
-
};
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import type { JSONContent } from '@tiptap/vue-3';
|
|
2
|
-
import { isTiptapDocument, getRteJsonPlainText } from './rich-text-editor.util';
|
|
3
|
-
export { isTiptapDocument, getRteJsonPlainText };
|
|
4
|
-
/**
|
|
5
|
-
* BP-5787 — the boundary plumbing for storing RTE as Tiptap JSON.
|
|
6
|
-
*
|
|
7
|
-
* An RTE field is either a legacy inline-HTML string (pre-migration, or a
|
|
8
|
-
* not-yet-migrated published site) or a Tiptap JSON document (post-migration).
|
|
9
|
-
* These helpers let every consumer treat the two uniformly:
|
|
10
|
-
*
|
|
11
|
-
* - isTiptapDocument — the canonical discriminator (object with type 'doc').
|
|
12
|
-
* - renderRteToHtml — render either form to final HTML for display.
|
|
13
|
-
* - toRteJson — normalise either form to JSON for storage.
|
|
14
|
-
*
|
|
15
|
-
* The schema-driven core of both directions is Tiptap/ProseMirror, fed by the
|
|
16
|
-
* single-source-of-truth extension set (getRteExtensions) — so the mapping lives
|
|
17
|
-
* in the extensions' schema, not a hand-maintained copy that drifts:
|
|
18
|
-
*
|
|
19
|
-
* - renderRteToHtml uses @tiptap/static-renderer (renderToHTMLString). It is
|
|
20
|
-
* DOM-less, so it runs on the server (SSR/publish/preview), not just the
|
|
21
|
-
* browser, and needs no Editor instance.
|
|
22
|
-
* - toRteJson turns an HTML string into a DOM node with an INJECTED parser and
|
|
23
|
-
* feeds it to ProseMirror's schema-driven DOMParser. The HTML→DOM step needs
|
|
24
|
-
* a DOM parser; rather than read the global `window.DOMParser` (as Tiptap's
|
|
25
|
-
* `generateJSON` does — unusable on a pure-node server and fragile across SSR
|
|
26
|
-
* execution contexts), the parser is supplied by the caller: the native
|
|
27
|
-
* `DOMParser` in the browser, a jsdom `DOMParser` *instance* on the server
|
|
28
|
-
* (see resolveHtmlParser). No global is ever mutated, so the conversion is
|
|
29
|
-
* fully isomorphic regardless of how the SSR host bundles/executes this code.
|
|
30
|
-
*/
|
|
31
|
-
export type RichTextValue = string | JSONContent;
|
|
32
|
-
/**
|
|
33
|
-
* A `DOMParser`-compatible object: turns an HTML string into a Document whose
|
|
34
|
-
* `body` is a DOM node ProseMirror can parse. The browser's native `DOMParser`
|
|
35
|
-
* and jsdom's `DOMParser` both satisfy this, so the DOM dependency is injected
|
|
36
|
-
* rather than read from a global.
|
|
37
|
-
*/
|
|
38
|
-
export interface HtmlParser {
|
|
39
|
-
parseFromString(html: string, type: 'text/html'): Document;
|
|
40
|
-
}
|
|
41
|
-
/**
|
|
42
|
-
* Render an RTE value to final HTML. Legacy HTML strings pass through untouched
|
|
43
|
-
* (they are already HTML); Tiptap JSON is serialised by @tiptap/static-renderer
|
|
44
|
-
* from the extension schema — no editor, no DOM — so it works on the server
|
|
45
|
-
* (SSR/publish), not just the browser.
|
|
46
|
-
*/
|
|
47
|
-
export declare const renderRteToHtml: (value: RichTextValue | null | undefined) => string;
|
|
48
|
-
/**
|
|
49
|
-
* Resolve an HtmlParser for the current runtime: the native `DOMParser` in the
|
|
50
|
-
* browser (and jsdom-based test envs), or a jsdom `DOMParser` INSTANCE on a
|
|
51
|
-
* pure-node server — created and returned directly, never assigned to a global,
|
|
52
|
-
* so it is immune to SSR execution-context differences. The jsdom specifier is
|
|
53
|
-
* assembled at runtime so no bundler follows it into a browser build. Memoised
|
|
54
|
-
* on the server (JSDOM construction is relatively expensive).
|
|
55
|
-
*/
|
|
56
|
-
export declare const resolveHtmlParser: () => Promise<HtmlParser>;
|
|
57
|
-
/**
|
|
58
|
-
* Normalise an RTE value to a Tiptap JSON document. HTML strings are parsed with
|
|
59
|
-
* `parser` (defaults to the browser's native DOMParser); JSON documents pass
|
|
60
|
-
* through unchanged. Returns undefined for empty/blank input so callers can omit
|
|
61
|
-
* empty fields.
|
|
62
|
-
*/
|
|
63
|
-
export declare const toRteJson: (value: RichTextValue | null | undefined, parser?: HtmlParser) => JSONContent | undefined;
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
import { getSchema } from '@tiptap/core';
|
|
2
|
-
import { DOMParser as ProseMirrorDOMParser } from '@tiptap/pm/model';
|
|
3
|
-
import { renderToHTMLString } from '@tiptap/static-renderer/pm/html-string';
|
|
4
|
-
import { getRteExtensions } from '../composables/rich-text-editor/getRteExtensions';
|
|
5
|
-
import { isTiptapDocument, getRteJsonPlainText } from './rich-text-editor.util';
|
|
6
|
-
export { isTiptapDocument, getRteJsonPlainText };
|
|
7
|
-
// One extension set shared by both directions (render + parse). Schema knowledge
|
|
8
|
-
// lives here, once — Tiptap/extension upgrades propagate to both automatically.
|
|
9
|
-
const RTE_EXTENSIONS = getRteExtensions();
|
|
10
|
-
// ProseMirror schema derived once from the shared extension set. This is the
|
|
11
|
-
// schema-driven heart of HTML→JSON (no drift); only the HTML→DOM step is injected.
|
|
12
|
-
const RTE_SCHEMA = getSchema(RTE_EXTENSIONS);
|
|
13
|
-
/**
|
|
14
|
-
* Render an RTE value to final HTML. Legacy HTML strings pass through untouched
|
|
15
|
-
* (they are already HTML); Tiptap JSON is serialised by @tiptap/static-renderer
|
|
16
|
-
* from the extension schema — no editor, no DOM — so it works on the server
|
|
17
|
-
* (SSR/publish), not just the browser.
|
|
18
|
-
*/
|
|
19
|
-
export const renderRteToHtml = (value) => {
|
|
20
|
-
if (value == null || value === '')
|
|
21
|
-
return '';
|
|
22
|
-
if (typeof value === 'string')
|
|
23
|
-
return value;
|
|
24
|
-
return renderToHTMLString({ content: value, extensions: RTE_EXTENSIONS });
|
|
25
|
-
};
|
|
26
|
-
// Tiptap's elementFromString strips whitespace-only text nodes (pretty-print
|
|
27
|
-
// indentation between tags) before parsing; replicate it so injected-parser
|
|
28
|
-
// output stays identical to the previous generateJSON-based conversion.
|
|
29
|
-
const stripIndentationTextNodes = (node) => {
|
|
30
|
-
const children = node.childNodes;
|
|
31
|
-
for (let i = children.length - 1; i >= 0; i -= 1) {
|
|
32
|
-
const child = children[i];
|
|
33
|
-
if (child.nodeType === 3 &&
|
|
34
|
-
child.nodeValue &&
|
|
35
|
-
/^(\n\s\s|\n)$/.test(child.nodeValue)) {
|
|
36
|
-
node.removeChild(child);
|
|
37
|
-
}
|
|
38
|
-
else if (child.nodeType === 1) {
|
|
39
|
-
stripIndentationTextNodes(child);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
return node;
|
|
43
|
-
};
|
|
44
|
-
const htmlToRteJson = (html, parser) => {
|
|
45
|
-
const body = parser.parseFromString(`<body>${html}</body>`, 'text/html').body;
|
|
46
|
-
const dom = stripIndentationTextNodes(body);
|
|
47
|
-
return ProseMirrorDOMParser.fromSchema(RTE_SCHEMA).parse(dom).toJSON();
|
|
48
|
-
};
|
|
49
|
-
let serverParserPromise = null;
|
|
50
|
-
/**
|
|
51
|
-
* Resolve an HtmlParser for the current runtime: the native `DOMParser` in the
|
|
52
|
-
* browser (and jsdom-based test envs), or a jsdom `DOMParser` INSTANCE on a
|
|
53
|
-
* pure-node server — created and returned directly, never assigned to a global,
|
|
54
|
-
* so it is immune to SSR execution-context differences. The jsdom specifier is
|
|
55
|
-
* assembled at runtime so no bundler follows it into a browser build. Memoised
|
|
56
|
-
* on the server (JSDOM construction is relatively expensive).
|
|
57
|
-
*/
|
|
58
|
-
export const resolveHtmlParser = async () => {
|
|
59
|
-
if (typeof DOMParser !== 'undefined')
|
|
60
|
-
return new DOMParser();
|
|
61
|
-
if (!serverParserPromise) {
|
|
62
|
-
const jsdomModuleId = ['js', 'dom'].join('');
|
|
63
|
-
serverParserPromise = import(/* @vite-ignore */ jsdomModuleId)
|
|
64
|
-
.then((mod) => {
|
|
65
|
-
const { JSDOM } = mod;
|
|
66
|
-
const { DOMParser: JsdomDOMParser } = new JSDOM('').window;
|
|
67
|
-
return new JsdomDOMParser();
|
|
68
|
-
})
|
|
69
|
-
.catch((error) => {
|
|
70
|
-
// Don't cache the failure — let a later call retry the import.
|
|
71
|
-
serverParserPromise = null;
|
|
72
|
-
throw error;
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
return serverParserPromise;
|
|
76
|
-
};
|
|
77
|
-
const getDefaultParser = () => {
|
|
78
|
-
if (typeof DOMParser !== 'undefined')
|
|
79
|
-
return new DOMParser();
|
|
80
|
-
throw new Error('toRteJson: no global DOMParser in this environment. On the server, pass an ' +
|
|
81
|
-
'explicit parser as the second argument (see resolveHtmlParser).');
|
|
82
|
-
};
|
|
83
|
-
/**
|
|
84
|
-
* Normalise an RTE value to a Tiptap JSON document. HTML strings are parsed with
|
|
85
|
-
* `parser` (defaults to the browser's native DOMParser); JSON documents pass
|
|
86
|
-
* through unchanged. Returns undefined for empty/blank input so callers can omit
|
|
87
|
-
* empty fields.
|
|
88
|
-
*/
|
|
89
|
-
export const toRteJson = (value, parser) => {
|
|
90
|
-
if (value == null || value === '')
|
|
91
|
-
return undefined;
|
|
92
|
-
if (isTiptapDocument(value))
|
|
93
|
-
return value;
|
|
94
|
-
return htmlToRteJson(value, parser ?? getDefaultParser());
|
|
95
|
-
};
|
|
@@ -1,156 +0,0 @@
|
|
|
1
|
-
import { validateAndSanitizeHref } from './rich-text-editor.util';
|
|
2
|
-
/**
|
|
3
|
-
* BP-5787 — native JSON sanitiser for RTE content.
|
|
4
|
-
*
|
|
5
|
-
* Operates directly on the Tiptap document tree: no editor instantiation, no
|
|
6
|
-
* HTML round-trip, no DOMPurify. This is the hot path (runs on every editor
|
|
7
|
-
* update and on the server sanitise gate), so it is a single, allocation-light
|
|
8
|
-
* recursive walk.
|
|
9
|
-
*
|
|
10
|
-
* Security model (mirrors the HTML allowlist it replaces):
|
|
11
|
-
* - only allow-listed node and mark types survive; everything else is dropped;
|
|
12
|
-
* - textStyle / highlight attrs are restricted to known keys whose values must
|
|
13
|
-
* be "safe CSS values" (no quotes/angle-brackets/extra declarations/url()/
|
|
14
|
-
* expression()), preventing CSS-injection when the value is later rendered
|
|
15
|
-
* into a `style="…"` attribute;
|
|
16
|
-
* - link hrefs go through the same validateAndSanitizeHref used by the HTML
|
|
17
|
-
* sanitiser; unsafe links lose their href (and the mark);
|
|
18
|
-
* - heading levels are clamped to 1–6.
|
|
19
|
-
*/
|
|
20
|
-
// ProseMirror node types the editor schema can legitimately produce.
|
|
21
|
-
const ALLOWED_NODE_TYPES = new Set([
|
|
22
|
-
'doc',
|
|
23
|
-
'paragraph',
|
|
24
|
-
'heading',
|
|
25
|
-
'text',
|
|
26
|
-
'hardBreak',
|
|
27
|
-
'bulletList',
|
|
28
|
-
'orderedList',
|
|
29
|
-
'listItem',
|
|
30
|
-
'blockquote',
|
|
31
|
-
'codeBlock',
|
|
32
|
-
'horizontalRule',
|
|
33
|
-
]);
|
|
34
|
-
// Inline mark types the editor produces (strong/em/u/s/code/span/mark/a).
|
|
35
|
-
const ALLOWED_MARK_TYPES = new Set([
|
|
36
|
-
'bold',
|
|
37
|
-
'italic',
|
|
38
|
-
'underline',
|
|
39
|
-
'strike',
|
|
40
|
-
'code',
|
|
41
|
-
'textStyle',
|
|
42
|
-
'highlight',
|
|
43
|
-
'link',
|
|
44
|
-
]);
|
|
45
|
-
// textStyle attrs map to the allow-listed CSS properties (font-weight →
|
|
46
|
-
// bold mark, text-decoration → underline/strike, text-align → node attr,
|
|
47
|
-
// background-color → highlight, so they are intentionally not here).
|
|
48
|
-
const ALLOWED_TEXT_STYLE_ATTRS = new Set([
|
|
49
|
-
'color',
|
|
50
|
-
'fontFamily',
|
|
51
|
-
'fontSize',
|
|
52
|
-
'letterSpacing',
|
|
53
|
-
'lineHeight',
|
|
54
|
-
]);
|
|
55
|
-
/**
|
|
56
|
-
* A safe CSS value is a short string free of characters that could inject an
|
|
57
|
-
* extra declaration or escape the attribute, and free of the url()/expression()
|
|
58
|
-
* vectors. Legitimate values — `rgb(255, 0, 0)`, `#FF0000`,
|
|
59
|
-
* `var(--logo-text-color)`, `Open Sans`, `1.5em` — all pass.
|
|
60
|
-
*/
|
|
61
|
-
const isSafeCssValue = (value) => typeof value === 'string' &&
|
|
62
|
-
value.length > 0 &&
|
|
63
|
-
value.length <= 200 &&
|
|
64
|
-
!/[<>"';]/.test(value) &&
|
|
65
|
-
!/url\s*\(/i.test(value) &&
|
|
66
|
-
!/expression\s*\(/i.test(value);
|
|
67
|
-
const sanitizeTextStyleAttrs = (attrs) => {
|
|
68
|
-
const clean = {};
|
|
69
|
-
for (const key of Object.keys(attrs)) {
|
|
70
|
-
if (!ALLOWED_TEXT_STYLE_ATTRS.has(key))
|
|
71
|
-
continue;
|
|
72
|
-
const value = attrs[key];
|
|
73
|
-
// Tiptap emits '' for unset textStyle slots — preserve them.
|
|
74
|
-
if (value === '' || value == null) {
|
|
75
|
-
clean[key] = '';
|
|
76
|
-
}
|
|
77
|
-
else if (isSafeCssValue(value)) {
|
|
78
|
-
clean[key] = value;
|
|
79
|
-
}
|
|
80
|
-
// else: drop the unsafe value
|
|
81
|
-
}
|
|
82
|
-
return Object.keys(clean).length ? clean : null;
|
|
83
|
-
};
|
|
84
|
-
const sanitizeMark = (mark) => {
|
|
85
|
-
const type = mark?.type;
|
|
86
|
-
if (!type || !ALLOWED_MARK_TYPES.has(type))
|
|
87
|
-
return null;
|
|
88
|
-
const attrs = mark.attrs;
|
|
89
|
-
if (type === 'link') {
|
|
90
|
-
const href = typeof attrs?.href === 'string'
|
|
91
|
-
? validateAndSanitizeHref(attrs.href)
|
|
92
|
-
: null;
|
|
93
|
-
if (!href)
|
|
94
|
-
return null; // drop links with unsafe/empty href
|
|
95
|
-
// href only — the sanitiser validates, it does not enrich. target/rel are
|
|
96
|
-
// a render-time policy derived from the href in the Link mark's renderHTML
|
|
97
|
-
// (see getRteExtensions.ts), so they are neither stored nor re-added here.
|
|
98
|
-
return { type, attrs: { href } };
|
|
99
|
-
}
|
|
100
|
-
if (type === 'textStyle') {
|
|
101
|
-
const clean = attrs ? sanitizeTextStyleAttrs(attrs) : null;
|
|
102
|
-
return clean ? { type, attrs: clean } : null;
|
|
103
|
-
}
|
|
104
|
-
if (type === 'highlight') {
|
|
105
|
-
return isSafeCssValue(attrs?.color)
|
|
106
|
-
? { type, attrs: { color: attrs.color } }
|
|
107
|
-
: { type };
|
|
108
|
-
}
|
|
109
|
-
// Plain marks (bold/italic/underline/strike/code) carry no attrs.
|
|
110
|
-
return { type };
|
|
111
|
-
};
|
|
112
|
-
const ALIGN_VALUES = new Set(['left', 'center', 'right', 'justify']);
|
|
113
|
-
const sanitizeNode = (node) => {
|
|
114
|
-
const type = node?.type;
|
|
115
|
-
if (!type || !ALLOWED_NODE_TYPES.has(type))
|
|
116
|
-
return null;
|
|
117
|
-
const clean = { type };
|
|
118
|
-
const attrs = {};
|
|
119
|
-
if (type === 'heading') {
|
|
120
|
-
const level = Number(node.attrs?.level);
|
|
121
|
-
attrs.level = level >= 1 && level <= 6 ? level : 1;
|
|
122
|
-
}
|
|
123
|
-
if (type === 'heading' || type === 'paragraph') {
|
|
124
|
-
const align = node.attrs?.textAlign;
|
|
125
|
-
if (typeof align === 'string' && ALIGN_VALUES.has(align)) {
|
|
126
|
-
attrs.textAlign = align;
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
if (Object.keys(attrs).length)
|
|
130
|
-
clean.attrs = attrs;
|
|
131
|
-
if (typeof node.text === 'string')
|
|
132
|
-
clean.text = node.text;
|
|
133
|
-
if (Array.isArray(node.marks)) {
|
|
134
|
-
const marks = node.marks
|
|
135
|
-
.map((m) => sanitizeMark(m))
|
|
136
|
-
.filter(Boolean);
|
|
137
|
-
if (marks && marks.length)
|
|
138
|
-
clean.marks = marks;
|
|
139
|
-
}
|
|
140
|
-
if (Array.isArray(node.content)) {
|
|
141
|
-
const content = node.content
|
|
142
|
-
.map(sanitizeNode)
|
|
143
|
-
.filter(Boolean);
|
|
144
|
-
if (content.length)
|
|
145
|
-
clean.content = content;
|
|
146
|
-
}
|
|
147
|
-
return clean;
|
|
148
|
-
};
|
|
149
|
-
/**
|
|
150
|
-
* Sanitise a Tiptap JSON document (returns a new clean doc). Returns a minimal
|
|
151
|
-
* empty doc if the input isn't a usable document.
|
|
152
|
-
*/
|
|
153
|
-
export const sanitizeRteJson = (doc) => {
|
|
154
|
-
const clean = doc && typeof doc === 'object' ? sanitizeNode(doc) : null;
|
|
155
|
-
return clean && clean.type === 'doc' ? clean : { type: 'doc', content: [] };
|
|
156
|
-
};
|