@designcrowd/library.brand-page 6.0.6-json-prereq-7 → 6.0.6-tracker-1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/lib/src/{CreatedOnBrandCrowd-de-de-SPVLPi8n.js → CreatedOnBrandCrowd-de-de-DD0OBnij.js} +1 -1
  2. package/lib/src/{CreatedOnBrandCrowd-en-us-CvtrCPHd.js → CreatedOnBrandCrowd-en-us-DysrpsmM.js} +1 -1
  3. package/lib/src/{CreatedOnBrandCrowd-es-es-BSsac7jB.js → CreatedOnBrandCrowd-es-es-B6twGr9h.js} +1 -1
  4. package/lib/src/{CreatedOnBrandCrowd-fr-fr-BauENV2a.js → CreatedOnBrandCrowd-fr-ca-yxl0ih_4.js} +1 -1
  5. package/lib/src/{CreatedOnBrandCrowd-fr-ca-BauENV2a.js → CreatedOnBrandCrowd-fr-fr-yxl0ih_4.js} +1 -1
  6. package/lib/src/{CreatedOnBrandCrowd-pt-pt-Be0hawmr.js → CreatedOnBrandCrowd-pt-br-BtXDrIcx.js} +1 -1
  7. package/lib/src/{CreatedOnBrandCrowd-pt-br-Be0hawmr.js → CreatedOnBrandCrowd-pt-pt-BtXDrIcx.js} +1 -1
  8. package/lib/src/{CreatedOnDcom-de-de-CTjE2Tmn.js → CreatedOnDcom-de-de-ho_ROy1L.js} +1 -1
  9. package/lib/src/{CreatedOnDcom-en-us-DnZRp5Pg.js → CreatedOnDcom-en-us-C91p9kls.js} +1 -1
  10. package/lib/src/{CreatedOnDcom-es-es-J-9qvjy8.js → CreatedOnDcom-es-es-B3FYmObE.js} +1 -1
  11. package/lib/src/{CreatedOnDcom-fr-ca-BDNHqm4G.js → CreatedOnDcom-fr-ca-D40m7fPx.js} +1 -1
  12. package/lib/src/{CreatedOnDcom-fr-fr-B_2-ceuO.js → CreatedOnDcom-fr-fr-Cd57oYyc.js} +1 -1
  13. package/lib/src/{CreatedOnDcom-pt-br-76lJVrAY.js → CreatedOnDcom-pt-br-Du57mWxG.js} +1 -1
  14. package/lib/src/{CreatedOnDcom-pt-pt-TQp3CNcu.js → CreatedOnDcom-pt-pt-fYoPZWwr.js} +1 -1
  15. package/lib/src/brand-page/models/index.d.ts +26 -34
  16. package/lib/src/brand-page/utils/migration.util.js +0 -11
  17. package/lib/src/brand-page/utils/text.util.d.ts +1 -1
  18. package/lib/src/brand-page/utils/text.util.js +0 -32
  19. package/lib/src/brand-page/utils/vcard.util.js +4 -4
  20. package/lib/src/brand-page/utils/view-model.util.d.ts +0 -1
  21. package/lib/src/brand-page/utils/view-model.util.js +20 -68
  22. package/lib/src/brand-page/view-models.d.ts +3 -3
  23. package/lib/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.js +99 -20
  24. package/lib/src/brand-page-maker/utils/index.d.ts +0 -2
  25. package/lib/src/brand-page-maker/utils/index.js +0 -2
  26. package/lib/src/brand-page-maker/utils/rich-text-editor.util.d.ts +5 -19
  27. package/lib/src/brand-page-maker/utils/rich-text-editor.util.js +5 -35
  28. package/lib/src/brand-page-maker/validators/field.validator.js +2 -10
  29. package/lib/src/css/library.brand-page-brandCrowd.min.css +1 -1
  30. package/lib/src/css/library.brand-page-brandPage.min.css +1 -1
  31. package/lib/src/css/library.brand-page-designCom.min.css +1 -1
  32. package/lib/src/css/library.brand-page-preview.min.css +1 -1
  33. package/lib/src/index.d.ts +0 -1
  34. package/lib/src/index.mjs +137 -141
  35. package/lib/src/{lib-Bh9Y5_-x.js → lib-lMNy66QO.js} +43580 -44184
  36. package/lib/src/lib.js +0 -2
  37. package/lib/src/metadata-builder/builders/update-content-text-builder.js +15 -20
  38. package/lib/src/metadata-builder/builders/update-content-text-style-builder.js +3 -5
  39. package/lib/src/metadata-builder/helpers/rte.helpers.d.ts +1 -1
  40. package/lib/src/metadata-builder/helpers/rte.helpers.js +3 -6
  41. package/lib/src/shared/content-settings/components/GenericTextInput.mixin.js +1 -14
  42. package/lib/src/shared/content-settings/components/content-settings-item/ContentSettingsItemField.mixin.js +3 -12
  43. package/lib/src/tracker.d.ts +2 -0
  44. package/lib/src/tracker.js +12 -0
  45. package/lib/src/tracker.mjs +12 -0
  46. package/lib/src/useEventTracker-DmQ6AkNn.js +356 -0
  47. package/lib/src/utils/minify-brand-page-metadata.js +1 -4
  48. package/package.json +16 -1
  49. package/src/brand-page/components/text-wrapper/TextWrapper.vue +6 -17
  50. package/src/brand-page/models/index.ts +26 -35
  51. package/src/brand-page/utils/__snapshots__/migration.util.test.ts.snap +3 -3
  52. package/src/brand-page/utils/migration.util.ts +0 -12
  53. package/src/brand-page/utils/style.util.test.ts +0 -51
  54. package/src/brand-page/utils/text.util.ts +4 -43
  55. package/src/brand-page/utils/vcard.util.ts +4 -6
  56. package/src/brand-page/utils/view-model.util.ts +26 -94
  57. package/src/brand-page/view-models.ts +2 -3
  58. package/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.ts +106 -22
  59. package/src/brand-page-maker/utils/index.ts +0 -2
  60. package/src/brand-page-maker/utils/rich-text-editor.util.test.ts +3 -4
  61. package/src/brand-page-maker/utils/rich-text-editor.util.ts +11 -50
  62. package/src/brand-page-maker/validators/field.validator.ts +2 -10
  63. package/src/lib.ts +0 -9
  64. package/src/metadata-builder/builders/update-content-text-builder.ts +18 -32
  65. package/src/metadata-builder/builders/update-content-text-style-builder.ts +4 -6
  66. package/src/metadata-builder/helpers/rte.helpers.ts +7 -11
  67. package/src/shared/content-settings/components/GenericTextInput.mixin.ts +1 -17
  68. package/src/shared/content-settings/components/content-settings-item/ContentSettingsItemField.mixin.ts +3 -16
  69. package/src/tracker.ts +28 -0
  70. package/src/utils/minify-brand-page-metadata.ts +1 -4
  71. package/lib/src/brand-page/utils/migration/migration-18.d.ts +0 -2
  72. package/lib/src/brand-page/utils/migration/migration-18.js +0 -71
  73. package/lib/src/brand-page-maker/__fixtures__/complex-rte.fixture.d.ts +0 -22
  74. package/lib/src/brand-page-maker/__fixtures__/complex-rte.fixture.js +0 -22
  75. package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/FontFamily.d.ts +0 -14
  76. package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/FontFamily.js +0 -39
  77. package/lib/src/brand-page-maker/composables/rich-text-editor/getRteExtensions.d.ts +0 -28
  78. package/lib/src/brand-page-maker/composables/rich-text-editor/getRteExtensions.js +0 -66
  79. package/lib/src/brand-page-maker/utils/rte-render.util.d.ts +0 -33
  80. package/lib/src/brand-page-maker/utils/rte-render.util.js +0 -50
  81. package/lib/src/brand-page-maker/utils/rte-sanitize.util.d.ts +0 -6
  82. package/lib/src/brand-page-maker/utils/rte-sanitize.util.js +0 -158
  83. package/lib/src/brand-page-maker/utils/rte-serializer.d.ts +0 -5
  84. package/lib/src/brand-page-maker/utils/rte-serializer.js +0 -112
  85. package/src/brand-page/utils/migration/migration-18.test.ts +0 -72
  86. package/src/brand-page/utils/migration/migration-18.ts +0 -83
  87. package/src/brand-page/utils/text.util.test.ts +0 -47
  88. package/src/brand-page/utils/view-model.pageid-rte.test.ts +0 -104
  89. package/src/brand-page-maker/__fixtures__/complex-rte.fixture.ts +0 -23
  90. package/src/brand-page-maker/composables/rich-text-editor/extensions/FontFamily.test.ts +0 -52
  91. package/src/brand-page-maker/composables/rich-text-editor/extensions/FontFamily.ts +0 -42
  92. package/src/brand-page-maker/composables/rich-text-editor/getRteExtensions.ts +0 -113
  93. package/src/brand-page-maker/utils/__snapshots__/rte-render-parity.test.ts.snap +0 -200
  94. package/src/brand-page-maker/utils/rte-render-parity.test.ts +0 -156
  95. package/src/brand-page-maker/utils/rte-render.util.ts +0 -77
  96. package/src/brand-page-maker/utils/rte-sanitize-json.test.ts +0 -158
  97. package/src/brand-page-maker/utils/rte-sanitize.util.ts +0 -195
  98. package/src/brand-page-maker/utils/rte-serializer.ts +0 -172
  99. package/src/brand-page-maker/validators/field.validator.test.ts +0 -55
@@ -1,71 +0,0 @@
1
- import { hasRTEContent } from '../../../brand-page-maker/utils';
2
- import { useExternalRichTextEditor } from '../../../brand-page-maker/composables/rich-text-editor/useRichTextEditor';
3
- /**
4
- * BP-5787 — migrate existing inline-HTML RTE content to Tiptap JSON.
5
- *
6
- * The editor now emits, and the renderer now reads, Tiptap JSON. Existing
7
- * published/saved sites still hold RTE fields as HTML strings. This converts
8
- * them on load:
9
- * - maker: the loaded model is JSON, so the next save persists JSON.
10
- * - web-renderer: lazy migrate-on-read — old sites render from JSON without
11
- * needing a re-save.
12
- *
13
- * Runs on EVERY load (runAlways) so it also normalises content the AI section
14
- * builders still emit as HTML — there is no need to change those builders or
15
- * widen the content model types.
16
- *
17
- * Scope: only HTML-RTE strings (hasRTEContent) are converted. Plain strings,
18
- * numbers, and already-JSON docs are left untouched, so the migration is
19
- * idempotent (re-running on JSON content is a no-op — non-string values are
20
- * skipped). Nested RTE inside item arrays (textItems, testimonials, images…)
21
- * is handled by recursing into arrays; style objects are intentionally not
22
- * descended into (they never hold RTE content).
23
- */
24
- const convertRteStringsToJson = (value, editor) => {
25
- if (Array.isArray(value)) {
26
- value.forEach((item) => convertRteStringsToJson(item, editor));
27
- return;
28
- }
29
- if (!value || typeof value !== 'object')
30
- return;
31
- const record = value;
32
- Object.keys(record).forEach((key) => {
33
- const field = record[key];
34
- if (typeof field === 'string' && hasRTEContent(field)) {
35
- editor.commands.setContent(field);
36
- record[key] = editor.getJSON();
37
- }
38
- else if (Array.isArray(field)) {
39
- convertRteStringsToJson(field, editor);
40
- }
41
- });
42
- };
43
- export const migration18 = async (migrationResult) => {
44
- const { brandPageModel } = migrationResult;
45
- // HTML→JSON parsing needs a DOM (Tiptap's editor). In a DOM-less context
46
- // (pure-node SSR or unit tests) we can't parse HTML, so we leave the RTE
47
- // fields as HTML strings — the renderer and the fraud/translation extractors
48
- // all dual-read HTML, so this is safe. Conversion happens wherever a DOM is
49
- // available (the browser editor, jsdom).
50
- if (typeof window === 'undefined' || typeof document === 'undefined') {
51
- return migrationResult;
52
- }
53
- let externalEditor;
54
- try {
55
- externalEditor = useExternalRichTextEditor().externalEditor;
56
- }
57
- catch {
58
- return migrationResult;
59
- }
60
- try {
61
- for (const page of brandPageModel.pages) {
62
- for (const content of page.contents) {
63
- convertRteStringsToJson(content, externalEditor);
64
- }
65
- }
66
- }
67
- finally {
68
- externalEditor.destroy();
69
- }
70
- return migrationResult;
71
- };
@@ -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-font-family").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
- * - `includeLinkRel` — the headless/render path emits rel="noopener noreferrer";
13
- * the live editing surface does not.
14
- * - `editing` — only the live editor carries the Placeholder + MaxLength plugins
15
- * (neither affects serialised output; they are editing-time only).
16
- */
17
- export interface RteExtensionOptions {
18
- /** Disable StarterKit nodes the editor doesn't use (lists, hr, code). */
19
- restrictStarterKit?: boolean;
20
- /** Emit rel="noopener noreferrer" on link HTMLAttributes (render path). */
21
- includeLinkRel?: 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,66 +0,0 @@
1
- import StarterKit from '@tiptap/starter-kit';
2
- import Underline from '@tiptap/extension-underline';
3
- import TextStyle 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 { FontSize } from './extensions/FontSize';
11
- import { LetterSpacing } from './extensions/LetterSpacing';
12
- import { LineHeight } from './extensions/LineHeight';
13
- import { MaxLength } from './extensions/MaxLength';
14
- import { AutoLink } from './extensions/AutoLink';
15
- import { isHrefPageId } from './hyperlink';
16
- export const getRteExtensions = (options = {}) => {
17
- const { restrictStarterKit = false, includeLinkRel = false, editing, } = options;
18
- const extensions = [
19
- restrictStarterKit
20
- ? StarterKit.configure({
21
- bulletList: false,
22
- orderedList: false,
23
- listItem: false,
24
- horizontalRule: false,
25
- codeBlock: false,
26
- code: false,
27
- })
28
- : StarterKit,
29
- Underline,
30
- TextStyle.configure({ mergeNestedSpanStyles: true }),
31
- TextAlign.configure({
32
- types: ['heading', 'paragraph'],
33
- }),
34
- FontFamily.configure({
35
- types: ['textStyle'],
36
- }),
37
- FontSize,
38
- LetterSpacing,
39
- LineHeight,
40
- ColorExtension.configure({
41
- types: ['textStyle'],
42
- }),
43
- Highlight.configure({
44
- multicolor: true,
45
- }),
46
- ];
47
- // Live-editing-only plugins sit between Highlight and Link (original order).
48
- if (editing) {
49
- extensions.push(Placeholder.configure({ placeholder: editing.placeholder }), MaxLength.configure({ maxLength: editing.maxLength }));
50
- }
51
- extensions.push(Link.configure({
52
- openOnClick: false,
53
- defaultProtocol: 'https',
54
- autolink: true,
55
- HTMLAttributes: (attrs) => {
56
- const href = attrs.href;
57
- const isExternal = href && (href.startsWith('http://') || href.startsWith('https://'));
58
- return {
59
- ...(includeLinkRel && { rel: 'noopener noreferrer' }),
60
- target: isExternal ? '_blank' : null,
61
- };
62
- },
63
- isAllowedUri: (url, ctx) => ctx.defaultValidate(url) || isHrefPageId(url),
64
- }), AutoLink);
65
- return extensions;
66
- };
@@ -1,33 +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
- * - renderRteToHtml uses a DOM-less serialiser (works on the server).
16
- * - toRteJson uses a headless editor to PARSE HTML → JSON, so it only runs
17
- * where a DOM exists (browser, or jsdom in tests); the server never parses
18
- * HTML (migration-18 is DOM-guarded, translation/fraud walk JSON directly).
19
- */
20
- export type RichTextValue = string | JSONContent;
21
- /**
22
- * Render an RTE value to final HTML. Legacy HTML strings pass through untouched
23
- * (they are already HTML); Tiptap JSON is serialised by a DOM-less pure
24
- * function (rte-serializer) — no editor, no DOM — so it works on the server
25
- * (SSR/publish), not just the browser, and is cheap enough to run per render.
26
- */
27
- export declare const renderRteToHtml: (value: RichTextValue | null | undefined) => string;
28
- /**
29
- * Normalise an RTE value to a Tiptap JSON document. HTML strings are parsed
30
- * via the headless editor; documents pass through unchanged. Returns
31
- * undefined for empty/blank input so callers can omit empty fields.
32
- */
33
- export declare const toRteJson: (value: RichTextValue | null | undefined) => JSONContent | undefined;
@@ -1,50 +0,0 @@
1
- import { Editor } from '@tiptap/vue-3';
2
- import { getRteExtensions } from '../composables/rich-text-editor/getRteExtensions';
3
- import { isTiptapDocument, getRteJsonPlainText } from './rich-text-editor.util';
4
- import { serializeRteJsonToHtml } from './rte-serializer';
5
- export { isTiptapDocument, getRteJsonPlainText };
6
- /**
7
- * Run a function with a short-lived headless editor (created then destroyed).
8
- * Used only by toRteJson (HTML → JSON parsing), which needs a real DOM. Not
9
- * used by the render path, which is DOM-less.
10
- */
11
- const withHeadlessEditor = (fn) => {
12
- const editor = new Editor({
13
- editable: false,
14
- extensions: getRteExtensions({ includeLinkRel: true }),
15
- });
16
- try {
17
- return fn(editor);
18
- }
19
- finally {
20
- editor.destroy();
21
- }
22
- };
23
- /**
24
- * Render an RTE value to final HTML. Legacy HTML strings pass through untouched
25
- * (they are already HTML); Tiptap JSON is serialised by a DOM-less pure
26
- * function (rte-serializer) — no editor, no DOM — so it works on the server
27
- * (SSR/publish), not just the browser, and is cheap enough to run per render.
28
- */
29
- export const renderRteToHtml = (value) => {
30
- if (value == null || value === '')
31
- return '';
32
- if (typeof value === 'string')
33
- return value;
34
- return serializeRteJsonToHtml(value);
35
- };
36
- /**
37
- * Normalise an RTE value to a Tiptap JSON document. HTML strings are parsed
38
- * via the headless editor; documents pass through unchanged. Returns
39
- * undefined for empty/blank input so callers can omit empty fields.
40
- */
41
- export const toRteJson = (value) => {
42
- if (value == null || value === '')
43
- return undefined;
44
- if (isTiptapDocument(value))
45
- return value;
46
- return withHeadlessEditor((editor) => {
47
- editor.commands.setContent(value);
48
- return editor.getJSON();
49
- });
50
- };
@@ -1,6 +0,0 @@
1
- import type { JSONContent } from '@tiptap/vue-3';
2
- /**
3
- * Sanitise a Tiptap JSON document (returns a new clean doc). Returns a minimal
4
- * empty doc if the input isn't a usable document.
5
- */
6
- export declare const sanitizeRteJson: (doc: unknown) => JSONContent;
@@ -1,158 +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
- const cleanAttrs = { href };
96
- if (attrs?.target != null)
97
- cleanAttrs.target = attrs.target;
98
- if (attrs?.rel != null)
99
- cleanAttrs.rel = attrs.rel;
100
- return { type, attrs: cleanAttrs };
101
- }
102
- if (type === 'textStyle') {
103
- const clean = attrs ? sanitizeTextStyleAttrs(attrs) : null;
104
- return clean ? { type, attrs: clean } : null;
105
- }
106
- if (type === 'highlight') {
107
- return isSafeCssValue(attrs?.color)
108
- ? { type, attrs: { color: attrs.color } }
109
- : { type };
110
- }
111
- // Plain marks (bold/italic/underline/strike/code) carry no attrs.
112
- return { type };
113
- };
114
- const ALIGN_VALUES = new Set(['left', 'center', 'right', 'justify']);
115
- const sanitizeNode = (node) => {
116
- const type = node?.type;
117
- if (!type || !ALLOWED_NODE_TYPES.has(type))
118
- return null;
119
- const clean = { type };
120
- const attrs = {};
121
- if (type === 'heading') {
122
- const level = Number(node.attrs?.level);
123
- attrs.level = level >= 1 && level <= 6 ? level : 1;
124
- }
125
- if (type === 'heading' || type === 'paragraph') {
126
- const align = node.attrs?.textAlign;
127
- if (typeof align === 'string' && ALIGN_VALUES.has(align)) {
128
- attrs.textAlign = align;
129
- }
130
- }
131
- if (Object.keys(attrs).length)
132
- clean.attrs = attrs;
133
- if (typeof node.text === 'string')
134
- clean.text = node.text;
135
- if (Array.isArray(node.marks)) {
136
- const marks = node.marks
137
- .map((m) => sanitizeMark(m))
138
- .filter(Boolean);
139
- if (marks && marks.length)
140
- clean.marks = marks;
141
- }
142
- if (Array.isArray(node.content)) {
143
- const content = node.content
144
- .map(sanitizeNode)
145
- .filter(Boolean);
146
- if (content.length)
147
- clean.content = content;
148
- }
149
- return clean;
150
- };
151
- /**
152
- * Sanitise a Tiptap JSON document (returns a new clean doc). Returns a minimal
153
- * empty doc if the input isn't a usable document.
154
- */
155
- export const sanitizeRteJson = (doc) => {
156
- const clean = doc && typeof doc === 'object' ? sanitizeNode(doc) : null;
157
- return clean && clean.type === 'doc' ? clean : { type: 'doc', content: [] };
158
- };
@@ -1,5 +0,0 @@
1
- import type { JSONContent } from '@tiptap/vue-3';
2
- /**
3
- * Serialise a Tiptap JSON document to HTML without an editor/DOM.
4
- */
5
- export declare const serializeRteJsonToHtml: (doc: JSONContent) => string;
@@ -1,112 +0,0 @@
1
- const escapeHtml = (value) => value.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
2
- const escapeAttr = (value) => escapeHtml(value).replace(/"/g, '&quot;');
3
- // textStyle attr → CSS property, in a stable order.
4
- const TEXT_STYLE_PROPS = [
5
- ['fontFamily', 'font-family'],
6
- ['fontSize', 'font-size'],
7
- ['letterSpacing', 'letter-spacing'],
8
- ['lineHeight', 'line-height'],
9
- ['color', 'color'],
10
- ];
11
- const textStyleCss = (attrs) => TEXT_STYLE_PROPS.filter(([key]) => typeof attrs[key] === 'string' && attrs[key])
12
- .map(([key, css]) => `${css}: ${attrs[key]}`)
13
- .join('; ');
14
- // Outermost → innermost. Reversed at wrap time so links wrap outside spans,
15
- // which wrap outside the plain formatting marks.
16
- const MARK_ORDER = [
17
- 'link',
18
- 'textStyle',
19
- 'highlight',
20
- 'strike',
21
- 'underline',
22
- 'italic',
23
- 'bold',
24
- 'code',
25
- ];
26
- const wrapMark = (type, attrs, inner) => {
27
- switch (type) {
28
- case 'bold':
29
- return `<strong>${inner}</strong>`;
30
- case 'italic':
31
- return `<em>${inner}</em>`;
32
- case 'underline':
33
- return `<u>${inner}</u>`;
34
- case 'strike':
35
- return `<s>${inner}</s>`;
36
- case 'code':
37
- return `<code>${inner}</code>`;
38
- case 'textStyle': {
39
- const css = textStyleCss(attrs);
40
- return css ? `<span style="${escapeAttr(css)}">${inner}</span>` : inner;
41
- }
42
- case 'highlight': {
43
- const color = typeof attrs.color === 'string' ? attrs.color : '';
44
- return color
45
- ? `<mark data-color="${escapeAttr(color)}" style="background-color: ${escapeAttr(color)}; color: inherit;">${inner}</mark>`
46
- : `<mark>${inner}</mark>`;
47
- }
48
- case 'link': {
49
- const href = typeof attrs.href === 'string' ? attrs.href : '';
50
- let open = `<a href="${escapeAttr(href)}"`;
51
- if (attrs.target)
52
- open += ` target="${escapeAttr(String(attrs.target))}"`;
53
- if (attrs.rel)
54
- open += ` rel="${escapeAttr(String(attrs.rel))}"`;
55
- return `${open}>${inner}</a>`;
56
- }
57
- default:
58
- return inner;
59
- }
60
- };
61
- const renderText = (node) => {
62
- let html = escapeHtml(node.text ?? '');
63
- const marks = Array.isArray(node.marks) ? node.marks : [];
64
- for (const type of [...MARK_ORDER].reverse()) {
65
- const mark = marks.find((m) => m?.type === type);
66
- if (mark)
67
- html = wrapMark(type, mark.attrs ?? {}, html);
68
- }
69
- return html;
70
- };
71
- const alignAttr = (node) => {
72
- const align = node.attrs?.textAlign;
73
- return typeof align === 'string' && align
74
- ? ` style="text-align: ${escapeAttr(align)}"`
75
- : '';
76
- };
77
- const renderChildren = (node) => (node.content ?? []).map(renderNode).join('');
78
- const renderNode = (node) => {
79
- if (!node || typeof node !== 'object')
80
- return '';
81
- switch (node.type) {
82
- case 'text':
83
- return renderText(node);
84
- case 'hardBreak':
85
- return '<br>';
86
- case 'horizontalRule':
87
- return '<hr>';
88
- case 'paragraph':
89
- return `<p${alignAttr(node)}>${renderChildren(node)}</p>`;
90
- case 'heading': {
91
- const level = Math.min(6, Math.max(1, Number(node.attrs?.level) || 1));
92
- return `<h${level}${alignAttr(node)}>${renderChildren(node)}</h${level}>`;
93
- }
94
- case 'bulletList':
95
- return `<ul>${renderChildren(node)}</ul>`;
96
- case 'orderedList':
97
- return `<ol>${renderChildren(node)}</ol>`;
98
- case 'listItem':
99
- return `<li>${renderChildren(node)}</li>`;
100
- case 'blockquote':
101
- return `<blockquote>${renderChildren(node)}</blockquote>`;
102
- case 'codeBlock':
103
- return `<pre><code>${renderChildren(node)}</code></pre>`;
104
- // doc and any unknown container: render children.
105
- default:
106
- return renderChildren(node);
107
- }
108
- };
109
- /**
110
- * Serialise a Tiptap JSON document to HTML without an editor/DOM.
111
- */
112
- export const serializeRteJsonToHtml = (doc) => renderChildren(doc);