@designcrowd/library.brand-page 6.0.26-json-prereq-2 → 6.0.26

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 (155) hide show
  1. package/lib/src/{CreatedOnBrandCrowd-de-de-CKBqjDH3.js → CreatedOnBrandCrowd-de-de-DU6YEVcW.js} +1 -1
  2. package/lib/src/{CreatedOnBrandCrowd-en-us-CRXiimvT.js → CreatedOnBrandCrowd-en-us-DNWWK6Tj.js} +1 -1
  3. package/lib/src/{CreatedOnBrandCrowd-es-es-DCYdY6sj.js → CreatedOnBrandCrowd-es-es-DVFvV9Qp.js} +1 -1
  4. package/lib/src/{CreatedOnBrandCrowd-fr-fr-Dk7LOhxE.js → CreatedOnBrandCrowd-fr-ca-B4ZkjNdw.js} +1 -1
  5. package/lib/src/{CreatedOnBrandCrowd-fr-ca-Dk7LOhxE.js → CreatedOnBrandCrowd-fr-fr-B4ZkjNdw.js} +1 -1
  6. package/lib/src/{CreatedOnBrandCrowd-pt-pt-Cp4ufHJP.js → CreatedOnBrandCrowd-pt-br-DP7AQGgW.js} +1 -1
  7. package/lib/src/{CreatedOnBrandCrowd-pt-br-Cp4ufHJP.js → CreatedOnBrandCrowd-pt-pt-DP7AQGgW.js} +1 -1
  8. package/lib/src/{CreatedOnDcom-de-de-ZSLkWMwM.js → CreatedOnDcom-de-de-CaC3GtG9.js} +1 -1
  9. package/lib/src/{CreatedOnDcom-en-us-BgdIGfT3.js → CreatedOnDcom-en-us-D16yT0-P.js} +1 -1
  10. package/lib/src/{CreatedOnDcom-es-es-D0KemkCZ.js → CreatedOnDcom-es-es-DA5sHgD_.js} +1 -1
  11. package/lib/src/{CreatedOnDcom-fr-ca-BuwNjNbC.js → CreatedOnDcom-fr-ca-CDv2SG1-.js} +1 -1
  12. package/lib/src/{CreatedOnDcom-fr-fr-DCQcsFPf.js → CreatedOnDcom-fr-fr-Cygc7c5d.js} +1 -1
  13. package/lib/src/{CreatedOnDcom-pt-br-DD-oLAS1.js → CreatedOnDcom-pt-br-B_3RIzgF.js} +1 -1
  14. package/lib/src/{CreatedOnDcom-pt-pt-D8qLq2wW.js → CreatedOnDcom-pt-pt-Bz5w-rrD.js} +1 -1
  15. package/lib/src/brand-page/components/contact-form/ContactForm.mixin.js +2 -5
  16. package/lib/src/brand-page/models/index.d.ts +26 -34
  17. package/lib/src/brand-page/utils/migration.util.js +0 -11
  18. package/lib/src/brand-page/utils/text.util.d.ts +1 -1
  19. package/lib/src/brand-page/utils/text.util.js +0 -32
  20. package/lib/src/brand-page/utils/vcard.util.js +4 -4
  21. package/lib/src/brand-page/utils/view-model.util.d.ts +0 -1
  22. package/lib/src/brand-page/utils/view-model.util.js +20 -68
  23. package/lib/src/brand-page/view-models.d.ts +3 -3
  24. package/lib/src/brand-page-maker/composables/ai-chat/useAiMetadataBuilder.d.ts +1 -0
  25. package/lib/src/brand-page-maker/composables/rich-text-editor/bubbleMenu.d.ts +9 -2
  26. package/lib/src/brand-page-maker/composables/rich-text-editor/bubbleMenu.js +10 -6
  27. package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/FontSize.d.ts +20 -0
  28. package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/FontSize.js +44 -0
  29. package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/LineHeight.d.ts +27 -0
  30. package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/LineHeight.js +44 -0
  31. package/lib/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.d.ts +1 -1
  32. package/lib/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.js +103 -24
  33. package/lib/src/brand-page-maker/utils/index.d.ts +0 -2
  34. package/lib/src/brand-page-maker/utils/index.js +0 -2
  35. package/lib/src/brand-page-maker/utils/rich-text-editor.util.d.ts +5 -28
  36. package/lib/src/brand-page-maker/utils/rich-text-editor.util.js +5 -48
  37. package/lib/src/brand-page-maker/validators/field.validator.js +2 -10
  38. package/lib/src/css/library.brand-page-brandCrowd.min.css +39 -39
  39. package/lib/src/css/library.brand-page-brandPage.min.css +39 -39
  40. package/lib/src/css/library.brand-page-designCom.min.css +39 -39
  41. package/lib/src/css/library.brand-page-preview.min.css +39 -39
  42. package/lib/src/index.d.ts +0 -1
  43. package/lib/src/index.mjs +138 -143
  44. package/lib/src/{lib-Db6j9fQZ.js → lib-6_P6kXPu.js} +51709 -55194
  45. package/lib/src/lib.js +0 -2
  46. package/lib/src/metadata-builder/ai-metadata-executor.d.ts +1 -0
  47. package/lib/src/metadata-builder/ai-metadata-executor.js +19 -0
  48. package/lib/src/metadata-builder/builders/section-builders/about-section-builder.js +3 -2
  49. package/lib/src/metadata-builder/builders/section-builders/banner-section-builder.js +3 -2
  50. package/lib/src/metadata-builder/builders/section-builders/booking-section-builder.js +3 -2
  51. package/lib/src/metadata-builder/builders/section-builders/contact-form-builder.js +4 -3
  52. package/lib/src/metadata-builder/builders/section-builders/images-section-builder.js +3 -2
  53. package/lib/src/metadata-builder/builders/section-builders/mailing-list-builder.js +2 -1
  54. package/lib/src/metadata-builder/builders/section-builders/mailing-list-section-builder.js +2 -1
  55. package/lib/src/metadata-builder/builders/section-builders/profile-section-builder.js +4 -3
  56. package/lib/src/metadata-builder/builders/section-builders/shapes-section-builder.js +3 -2
  57. package/lib/src/metadata-builder/builders/section-builders/testimonial-section-builder.js +2 -1
  58. package/lib/src/metadata-builder/builders/section-builders/text-section-builder.js +11 -15
  59. package/lib/src/metadata-builder/builders/update-content-text-builder.d.ts +17 -0
  60. package/lib/src/metadata-builder/builders/update-content-text-builder.js +103 -0
  61. package/lib/src/metadata-builder/builders/update-content-text-style-builder.d.ts +16 -0
  62. package/lib/src/metadata-builder/builders/update-content-text-style-builder.js +216 -0
  63. package/lib/src/metadata-builder/helpers/rte.helpers.d.ts +13 -11
  64. package/lib/src/metadata-builder/helpers/rte.helpers.js +73 -14
  65. package/lib/src/metadata-builder/metadata-executor.js +4 -0
  66. package/lib/src/metadata-builder/models.d.ts +2 -0
  67. package/lib/src/metadata-builder/models.js +2 -0
  68. package/lib/src/metadata-builder/parsers/ai-toolcall-parser.d.ts +19 -0
  69. package/lib/src/metadata-builder/parsers/ai-toolcall-parser.js +4 -0
  70. package/lib/src/metadata-builder/sanitizers/rte-schema-check.d.ts +8 -7
  71. package/lib/src/metadata-builder/sanitizers/rte-schema-check.js +52 -21
  72. package/lib/src/shared/content-settings/components/GenericTextInput.mixin.js +1 -14
  73. package/lib/src/shared/content-settings/components/content-settings-item/ContentSettingsItemField.mixin.js +3 -12
  74. package/lib/src/shared/content-settings/components/testimonial-items-field/TestimonialItemsField.mixin.d.ts +2 -3
  75. package/lib/src/shared/content-settings/components/testimonial-items-field/TestimonialItemsField.mixin.js +3 -10
  76. package/lib/src/shared/content-settings/components/text-items-field/TextItemsField.mixin.d.ts +1 -2
  77. package/lib/src/shared/content-settings/components/text-items-field/TextItemsField.mixin.js +1 -4
  78. package/lib/src/utils/minify-brand-page-metadata.js +1 -4
  79. package/package.json +15 -17
  80. package/src/brand-page/components/contact-form/ContactForm.mixin.ts +2 -5
  81. package/src/brand-page/components/text-wrapper/TextWrapper.vue +6 -17
  82. package/src/brand-page/models/index.ts +26 -35
  83. package/src/brand-page/utils/__snapshots__/migration.util.test.ts.snap +3 -3
  84. package/src/brand-page/utils/migration.util.ts +0 -12
  85. package/src/brand-page/utils/style.util.test.ts +0 -51
  86. package/src/brand-page/utils/text.util.ts +4 -43
  87. package/src/brand-page/utils/vcard.util.ts +4 -6
  88. package/src/brand-page/utils/view-model.util.ts +26 -94
  89. package/src/brand-page/view-models.ts +2 -3
  90. package/src/brand-page-maker/components/rich-text-editor/RichTextEditor.vue +3 -4
  91. package/src/brand-page-maker/composables/ai-chat/useAiMetadataBuilder.ts +1 -0
  92. package/src/brand-page-maker/composables/rich-text-editor/bubbleMenu.ts +10 -6
  93. package/src/brand-page-maker/composables/rich-text-editor/extensions/FontSize.ts +73 -0
  94. package/src/brand-page-maker/composables/rich-text-editor/extensions/LineHeight.ts +85 -0
  95. package/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.ts +111 -27
  96. package/src/brand-page-maker/utils/index.ts +0 -2
  97. package/src/brand-page-maker/utils/rich-text-editor.util.test.ts +3 -37
  98. package/src/brand-page-maker/utils/rich-text-editor.util.ts +11 -66
  99. package/src/brand-page-maker/validators/field.validator.ts +2 -10
  100. package/src/lib.ts +0 -9
  101. package/src/metadata-builder/ai-metadata-executor.ts +26 -0
  102. package/src/metadata-builder/builders/section-builders/about-section-builder.ts +3 -2
  103. package/src/metadata-builder/builders/section-builders/banner-section-builder.ts +3 -2
  104. package/src/metadata-builder/builders/section-builders/booking-section-builder.ts +5 -2
  105. package/src/metadata-builder/builders/section-builders/contact-form-builder.ts +4 -3
  106. package/src/metadata-builder/builders/section-builders/images-section-builder.ts +3 -2
  107. package/src/metadata-builder/builders/section-builders/mailing-list-builder.ts +2 -1
  108. package/src/metadata-builder/builders/section-builders/mailing-list-section-builder.ts +2 -1
  109. package/src/metadata-builder/builders/section-builders/profile-section-builder.ts +6 -3
  110. package/src/metadata-builder/builders/section-builders/shapes-section-builder.ts +3 -2
  111. package/src/metadata-builder/builders/section-builders/testimonial-section-builder.ts +2 -1
  112. package/src/metadata-builder/builders/section-builders/text-section-builder.ts +13 -19
  113. package/src/metadata-builder/builders/update-content-text-builder.ts +148 -0
  114. package/src/metadata-builder/builders/update-content-text-style-builder.ts +352 -0
  115. package/src/metadata-builder/helpers/rte.helpers.ts +101 -15
  116. package/src/metadata-builder/metadata-executor.ts +5 -0
  117. package/src/metadata-builder/models.ts +3 -0
  118. package/src/metadata-builder/parsers/ai-toolcall-parser.ts +27 -0
  119. package/src/metadata-builder/sanitizers/rte-schema-check.ts +53 -24
  120. package/src/shared/content-settings/components/GenericTextInput.mixin.ts +1 -17
  121. package/src/shared/content-settings/components/content-settings-item/ContentSettingsItemField.mixin.ts +3 -16
  122. package/src/shared/content-settings/components/testimonial-items-field/TestimonialItemsField.mixin.ts +4 -13
  123. package/src/shared/content-settings/components/testimonial-items-field/TestimonialItemsField.vue +4 -6
  124. package/src/shared/content-settings/components/text-image-field/TextImageField.vue +3 -6
  125. package/src/shared/content-settings/components/text-items-field/TextItemsField.mixin.ts +1 -6
  126. package/src/shared/content-settings/components/text-items-field/TextItemsField.vue +1 -1
  127. package/src/utils/minify-brand-page-metadata.ts +1 -4
  128. package/lib/src/brand-page/utils/migration/migration-20.d.ts +0 -2
  129. package/lib/src/brand-page/utils/migration/migration-20.js +0 -66
  130. package/lib/src/brand-page-maker/__fixtures__/complex-rte.fixture.d.ts +0 -22
  131. package/lib/src/brand-page-maker/__fixtures__/complex-rte.fixture.js +0 -22
  132. package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/FontFamily.d.ts +0 -14
  133. package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/FontFamily.js +0 -39
  134. package/lib/src/brand-page-maker/composables/rich-text-editor/getRteExtensions.d.ts +0 -28
  135. package/lib/src/brand-page-maker/composables/rich-text-editor/getRteExtensions.js +0 -90
  136. package/lib/src/brand-page-maker/utils/rte-render.util.d.ts +0 -63
  137. package/lib/src/brand-page-maker/utils/rte-render.util.js +0 -95
  138. package/lib/src/brand-page-maker/utils/rte-sanitize.util.d.ts +0 -6
  139. package/lib/src/brand-page-maker/utils/rte-sanitize.util.js +0 -158
  140. package/src/brand-page/utils/migration/migration-20.server.test.ts +0 -125
  141. package/src/brand-page/utils/migration/migration-20.test.ts +0 -72
  142. package/src/brand-page/utils/migration/migration-20.ts +0 -87
  143. package/src/brand-page/utils/text.util.test.ts +0 -47
  144. package/src/brand-page/utils/view-model.pageid-rte.test.ts +0 -104
  145. package/src/brand-page-maker/__fixtures__/complex-rte.fixture.ts +0 -23
  146. package/src/brand-page-maker/composables/rich-text-editor/extensions/FontFamily.test.ts +0 -52
  147. package/src/brand-page-maker/composables/rich-text-editor/extensions/FontFamily.ts +0 -42
  148. package/src/brand-page-maker/composables/rich-text-editor/getRteExtensions.ts +0 -137
  149. package/src/brand-page-maker/utils/__snapshots__/rte-render-parity.test.ts.snap +0 -202
  150. package/src/brand-page-maker/utils/rte-features.test.ts +0 -351
  151. package/src/brand-page-maker/utils/rte-render-parity.test.ts +0 -157
  152. package/src/brand-page-maker/utils/rte-render.util.ts +0 -165
  153. package/src/brand-page-maker/utils/rte-sanitize-json.test.ts +0 -158
  154. package/src/brand-page-maker/utils/rte-sanitize.util.ts +0 -195
  155. package/src/brand-page-maker/validators/field.validator.test.ts +0 -55
@@ -1,6 +1,16 @@
1
- import { getSchema } from '@tiptap/core';
2
- import type { Schema } from '@tiptap/pm/model';
3
- import { getRteExtensions } from '../../brand-page-maker/composables/rich-text-editor/getRteExtensions';
1
+ import { Editor } from '@tiptap/vue-3';
2
+ import StarterKit from '@tiptap/starter-kit';
3
+ import Underline from '@tiptap/extension-underline';
4
+ import TextStyle from '@tiptap/extension-text-style';
5
+ import TextAlign from '@tiptap/extension-text-align';
6
+ import FontFamily from '@tiptap/extension-font-family';
7
+ import ColorExtension from '@tiptap/extension-color';
8
+ import Highlight from '@tiptap/extension-highlight';
9
+ import Link from '@tiptap/extension-link';
10
+ import { FontSize } from '../../brand-page-maker/composables/rich-text-editor/extensions/FontSize';
11
+ import { LetterSpacing } from '../../brand-page-maker/composables/rich-text-editor/extensions/LetterSpacing';
12
+ import { LineHeight } from '../../brand-page-maker/composables/rich-text-editor/extensions/LineHeight';
13
+ import { AutoLink } from '../../brand-page-maker/composables/rich-text-editor/extensions/AutoLink';
4
14
  import { isTiptapDocument } from '../parsers/section-tool-arguments';
5
15
  import {
6
16
  rteStyleCheck,
@@ -9,30 +19,45 @@ import {
9
19
  } from './section-args-validator';
10
20
 
11
21
  /**
12
- * The ProseMirror schema built from the SAME extension set the editor and
13
- * renderer use (`getRteExtensions`), so validation matches real editor output.
14
- * `getSchema` is DOM-less (no Editor instance) and the schema is pure, so it is
15
- * built once and cached.
16
- *
17
- * We import `getRteExtensions` (a leaf module — @tiptap + prosemirror only), not
18
- * the maker composable (`useExternalRichTextEditor`), which would pull in the
19
- * AI executor → section-builders → section-rules graph and re-enter this file
20
- * before its exports are bound.
22
+ * Constructs a headless Tiptap Editor with the same extension set as
23
+ * `useExternalRichTextEditor` in the maker. We import the Tiptap extension
24
+ * files directly (they are leaf modules) instead of going through the maker
25
+ * composable, which would pull in the AI executor → section-builders →
26
+ * section-rules graph and re-enter this file before its exports are bound.
21
27
  */
22
- let cachedSchema: Schema | undefined;
23
-
24
- const getRteSchema = (): Schema =>
25
- (cachedSchema ??= getSchema(getRteExtensions({ includeLinkRel: true })));
28
+ const buildHeadlessEditor = (): Editor =>
29
+ new Editor({
30
+ editable: false,
31
+ extensions: [
32
+ StarterKit,
33
+ Underline,
34
+ TextStyle.configure({ mergeNestedSpanStyles: true }),
35
+ TextAlign.configure({ types: ['heading', 'paragraph'] }),
36
+ FontFamily.configure({ types: ['textStyle'] }),
37
+ FontSize,
38
+ LetterSpacing,
39
+ LineHeight,
40
+ ColorExtension.configure({ types: ['textStyle'] }),
41
+ Highlight.configure({ multicolor: true }),
42
+ Link.configure({
43
+ openOnClick: false,
44
+ defaultProtocol: 'https',
45
+ autolink: true,
46
+ }),
47
+ AutoLink,
48
+ ],
49
+ });
26
50
 
27
51
  /**
28
- * Validates a Tiptap doc against the real ProseMirror schema
29
- * (`schema.nodeFromJSON`). Catches malformed AI input — stray non-node children
30
- * in `content`, unknown node types, malformed attrs — before the builder runs
31
- * and the renderer's silent `setContent` drops the doc to `<p></p>` (BP-5669).
52
+ * Validates a Tiptap doc against the headless editor's real ProseMirror
53
+ * schema (`schema.nodeFromJSON`). Catches malformed AI input — stray
54
+ * non-node children in `content`, unknown node types, malformed attrs —
55
+ * before the builder runs and the renderer's silent `setContent` drops
56
+ * the doc to `<p></p>` (BP-5669).
32
57
  *
33
- * Returns the field-prefixed error string so the AI's tool-call retry loop reads
34
- * it via `BuilderResult.errors` → `tool_result.errorMessage` and re-emits a
35
- * corrected payload.
58
+ * Returns the field-prefixed error string so the AI's tool-call retry
59
+ * loop reads it via `BuilderResult.errors` → `tool_result.errorMessage`
60
+ * and re-emits a corrected payload.
36
61
  */
37
62
  export const rteSchemaCheck =
38
63
  (field: string): SectionCheck =>
@@ -41,14 +66,18 @@ export const rteSchemaCheck =
41
66
 
42
67
  if (!isTiptapDocument(value)) return null;
43
68
 
69
+ const editor = buildHeadlessEditor();
70
+
44
71
  try {
45
- getRteSchema().nodeFromJSON(value);
72
+ editor.schema.nodeFromJSON(value);
46
73
 
47
74
  return null;
48
75
  } catch (err) {
49
76
  const detail = err instanceof Error ? err.message : String(err);
50
77
 
51
78
  return `"${field}" is not valid Tiptap JSON: ${detail}`;
79
+ } finally {
80
+ editor.destroy();
52
81
  }
53
82
  };
54
83
 
@@ -175,23 +175,7 @@ export default defineComponent({
175
175
  },
176
176
  },
177
177
  watch: {
178
- value(newVal: string | undefined, oldVal: string | undefined) {
179
- // BP-5787: RTE values are Tiptap JSON objects. The parent (TextItemsField)
180
- // re-clones its items array (cloneDeep) on ANY change — including a sibling
181
- // field's style sync — yielding a NEW object reference with identical text
182
- // content. For objects Vue fires this watcher on reference change, so that
183
- // spurious re-clone would overwrite `_value` and clobber an in-flight edit
184
- // (a mark applied but not yet debounced to the store), silently dropping
185
- // the user's formatting. Strings never hit this (same content = same
186
- // reference = no fire), so mirror that: ignore re-clones whose content is
187
- // unchanged from the previous prop value.
188
- if (
189
- this.showRichTextEditor &&
190
- JSON.stringify(newVal) === JSON.stringify(oldVal)
191
- ) {
192
- return;
193
- }
194
-
178
+ value(newVal: string | undefined) {
195
179
  this._value = newVal;
196
180
 
197
181
  if (this.showRichTextEditor) {
@@ -21,9 +21,6 @@ import {
21
21
  replaceTextInRTE,
22
22
  RTE_ALLOWED_SECTION_TYPES,
23
23
  isRTEManagedStyleField,
24
- isTiptapDocument,
25
- renderRteToHtml,
26
- toRteJson,
27
24
  } from '@brand-page-maker/utils';
28
25
  import { isDefaultContentText } from '@shared/content-settings/utils';
29
26
  import { useUiLogoSettingsStore } from '../../../logo-settings/store';
@@ -330,20 +327,10 @@ export default defineComponent({
330
327
  this.isToggled = isToggled;
331
328
  },
332
329
  onUseGeneratedText(generatedText: string) {
333
- let formattedText: unknown = generatedText;
330
+ let formattedText = generatedText;
334
331
 
335
- // BP-5787: preserve the field's formatting when dropping in AI text.
336
- // replaceTextInRTE is HTML-based, so JSON is rendered/parsed around it
337
- // (cold AI path, shared editor); string fields use it directly.
338
- if (this.showRichTextEditor) {
339
- if (isTiptapDocument(this.value)) {
340
- formattedText =
341
- toRteJson(
342
- replaceTextInRTE(renderRteToHtml(this.value), generatedText),
343
- ) ?? generatedText;
344
- } else if (typeof this.value === 'string') {
345
- formattedText = replaceTextInRTE(this.value, generatedText);
346
- }
332
+ if (this.showRichTextEditor && typeof this.value === 'string') {
333
+ formattedText = replaceTextInRTE(this.value, generatedText);
347
334
  }
348
335
 
349
336
  this.contentSettingsStore.updateItemValue({
@@ -4,7 +4,6 @@ import type {
4
4
  ColorReplacement,
5
5
  Content,
6
6
  LogoTheme,
7
- RichTextValue,
8
7
  TestimonialItems,
9
8
  } from '@brand-page';
10
9
  import {
@@ -18,7 +17,7 @@ import debounce from 'lodash.debounce';
18
17
  import type { UpdateItemValuePayload } from '../../models';
19
18
  import cloneDeep from 'lodash.clonedeep';
20
19
  import { useSnapshotStore } from '@brand-page-maker/stores';
21
- import { getPlainTextFromRTE, getRteTextLength } from '@brand-page-maker/utils';
20
+ import { getPlainTextFromRTE } from '@brand-page-maker/utils';
22
21
  import { isDefaultContentText } from '../../utils';
23
22
  import { decodeHtmlEntities } from '@brand-page/utils';
24
23
  import { ESCAPED_NEWLINE_CHARACTER } from '../../../../constants/ui';
@@ -92,10 +91,8 @@ export default defineComponent({
92
91
  created() {
93
92
  this.options = cloneDeep(this.value);
94
93
 
95
- // BP-5787: testimonials may be Tiptap JSON (migrated legacy content), so
96
- // read visible text via getPlainTextFromRTE rather than `.trim()` on a doc.
97
94
  this.isEmpty = this.options.map(
98
- (option) => getPlainTextFromRTE(option.testimonials).length === 0,
95
+ (option) => option.testimonials.trim().length === 0,
99
96
  );
100
97
 
101
98
  if (!this.changeDebounce) {
@@ -209,17 +206,11 @@ export default defineComponent({
209
206
  this.options[index][payload.name] = payload.value;
210
207
  this.emitChange();
211
208
  },
212
- getItemTextPreview(text: RichTextValue): string {
213
- // BP-5787: a JSON doc has no `.replace` — take its visible text first.
214
- const value = typeof text === 'string' ? text : getPlainTextFromRTE(text);
215
-
209
+ getItemTextPreview(text: string): string {
216
210
  return decodeHtmlEntities(
217
- value.replace(new RegExp(ESCAPED_NEWLINE_CHARACTER, 'g'), ' '),
211
+ text.replace(new RegExp(ESCAPED_NEWLINE_CHARACTER, 'g'), ' '),
218
212
  );
219
213
  },
220
- rteTextLength(value: RichTextValue): number {
221
- return getRteTextLength(value);
222
- },
223
214
  getPlainTextValue(index: number): string {
224
215
  const text = this.options[index]?.testimonials || '';
225
216
  const plainText = getPlainTextFromRTE(text);
@@ -26,8 +26,7 @@
26
26
  :error="
27
27
  isEmpty[option.index] ||
28
28
  (options[option.index].testimonials &&
29
- rteTextLength(options[option.index].testimonials) >=
30
- maxTestimonialLength)
29
+ options[option.index].testimonials.length >= maxTestimonialLength)
31
30
  "
32
31
  :resize="false"
33
32
  class="tw-w-full"
@@ -43,8 +42,7 @@
43
42
  <p
44
43
  v-else-if="
45
44
  options[option.index].testimonials &&
46
- rteTextLength(options[option.index].testimonials) >=
47
- maxTestimonialLength
45
+ options[option.index].testimonials.length >= maxTestimonialLength
48
46
  "
49
47
  class="tw-text-error-500 tw-text-sm"
50
48
  >
@@ -129,7 +127,7 @@
129
127
  v-if="
130
128
  options[option.index] &&
131
129
  options[option.index].name &&
132
- rteTextLength(options[option.index].name) >= maxTitleLength
130
+ options[option.index].name.length >= maxTitleLength
133
131
  "
134
132
  class="tw-text-error-500 tw-text-sm"
135
133
  >
@@ -160,7 +158,7 @@
160
158
  v-if="
161
159
  options[option.index] &&
162
160
  options[option.index].position &&
163
- rteTextLength(options[option.index].position) >= maxTitleLength
161
+ options[option.index].position.length >= maxTitleLength
164
162
  "
165
163
  class="tw-text-error-500 tw-text-sm"
166
164
  >
@@ -97,10 +97,10 @@
97
97
  :attrs="{
98
98
  'data-test-tag': 'image-title-text-input',
99
99
  }"
100
- :error="rteTextLength(options[option.index].title) >= titleLength"
100
+ :error="options[option.index].title.length >= titleLength"
101
101
  />
102
102
  <p
103
- v-if="rteTextLength(options[option.index].title) >= titleLength"
103
+ v-if="options[option.index].title.length >= titleLength"
104
104
  class="tw-text-error-500 tw-text-sm tw-mt-2"
105
105
  >
106
106
  {{ maxLengthError }}
@@ -134,9 +134,7 @@
134
134
  :attrs="{
135
135
  'data-test-tag': 'image-description-text-input',
136
136
  }"
137
- :error="
138
- rteTextLength(options[option.index].paragraph) >= descriptionLength
139
- "
137
+ :error="options[option.index].paragraph.length >= descriptionLength"
140
138
  @update:model-value="onTextAreaTextChange($event, option.index)"
141
139
  />
142
140
  <PersonalisationOverrideField
@@ -313,7 +311,6 @@ import debounce from 'lodash.debounce';
313
311
  import type { UpdateItemValuePayload } from '../../models';
314
312
  import cloneDeep from 'lodash.clonedeep';
315
313
  import { isURL } from '../../../../utils/validator.util';
316
- import { getRteTextLength as rteTextLength } from '@brand-page-maker/utils';
317
314
 
318
315
  const DEBOUNCE_IN_MS = 250;
319
316
 
@@ -3,7 +3,6 @@ import type { PropType } from 'vue';
3
3
  import type {
4
4
  Content,
5
5
  LogoTheme,
6
- RichTextValue,
7
6
  TextItem,
8
7
  TextStyleDeclaration,
9
8
  } from '@brand-page/models';
@@ -20,7 +19,7 @@ import type { UpdateItemValuePayload } from '../../models';
20
19
  import cloneDeep from 'lodash.clonedeep';
21
20
  import { decodeHtmlEntities, escapeContentText } from '@brand-page/utils';
22
21
  import { useSnapshotStore } from '@brand-page-maker/stores';
23
- import { getPlainTextFromRTE, getRteTextLength } from '@brand-page-maker/utils';
22
+ import { getPlainTextFromRTE } from '@brand-page-maker/utils';
24
23
  import { isDefaultContentText } from '../../utils';
25
24
 
26
25
  const DEBOUNCE_IN_MS = 250;
@@ -235,10 +234,6 @@ export default defineComponent({
235
234
  this.emitChange();
236
235
  },
237
236
 
238
- rteTextLength(value: RichTextValue): number {
239
- return getRteTextLength(value);
240
- },
241
-
242
237
  getPlainTextValue(index: number): string {
243
238
  const text = this.options[index]?.text || '';
244
239
  const plainText = getPlainTextFromRTE(text);
@@ -32,7 +32,7 @@
32
32
  size="sm"
33
33
  :error="
34
34
  isEmpty[option.index] ||
35
- rteTextLength(options[option.index].text) >= maxTextLength
35
+ options[option.index].text.length >= maxTextLength
36
36
  "
37
37
  :resize="false"
38
38
  class="tw-w-full"
@@ -10,7 +10,7 @@ import {
10
10
  } from '../brand-page/models';
11
11
  import type { Content, ContentType } from '../brand-page/models/content.model';
12
12
  import type { TextStyleDeclaration } from '../brand-page/models/styles.model';
13
- import { hasRTEContent, isTiptapDocument } from '../brand-page-maker/utils';
13
+ import { hasRTEContent } from '../brand-page-maker/utils';
14
14
  import { useExternalRichTextEditor } from '../brand-page-maker/composables/rich-text-editor/useRichTextEditor';
15
15
  import type { Editor, JSONContent } from '@tiptap/vue-3';
16
16
  import {
@@ -106,9 +106,6 @@ const convertText = (
106
106
  value: unknown,
107
107
  editor: Editor,
108
108
  ): RichTextValue | undefined => {
109
- // Already Tiptap JSON (BP-5787 storage) — keep as-is.
110
- if (isTiptapDocument(value)) return value;
111
-
112
109
  if (typeof value !== 'string' || !value) return undefined;
113
110
 
114
111
  if (hasRTEContent(value)) {
@@ -1,2 +0,0 @@
1
- import type { MigrationData } from '../migration.util';
2
- export declare const migration20: (migrationResult: MigrationData) => Promise<MigrationData>;
@@ -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
- * - `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,90 +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, includeLinkRel = 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.configure({
76
- openOnClick: false,
77
- defaultProtocol: 'https',
78
- autolink: true,
79
- HTMLAttributes: (attrs) => {
80
- const href = attrs.href;
81
- const isExternal = href && (href.startsWith('http://') || href.startsWith('https://'));
82
- return {
83
- ...(includeLinkRel && { rel: 'noopener noreferrer' }),
84
- target: isExternal ? '_blank' : null,
85
- };
86
- },
87
- isAllowedUri: (url, ctx) => ctx.defaultValidate(url) || isHrefPageId(url),
88
- }), AutoLink);
89
- return extensions;
90
- };