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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (25) hide show
  1. package/lib/src/{CreatedOnBrandCrowd-de-de-Dx3e-vQ6.js → CreatedOnBrandCrowd-de-de-F1Ldbx6c.js} +1 -1
  2. package/lib/src/{CreatedOnBrandCrowd-en-us-CbchLDa2.js → CreatedOnBrandCrowd-en-us-D8z2m0Et.js} +1 -1
  3. package/lib/src/{CreatedOnBrandCrowd-es-es-BXbpN7li.js → CreatedOnBrandCrowd-es-es-CLSkbkYu.js} +1 -1
  4. package/lib/src/{CreatedOnBrandCrowd-fr-fr-hIk7h9bV.js → CreatedOnBrandCrowd-fr-ca-Cyc4YWYR.js} +1 -1
  5. package/lib/src/{CreatedOnBrandCrowd-fr-ca-hIk7h9bV.js → CreatedOnBrandCrowd-fr-fr-Cyc4YWYR.js} +1 -1
  6. package/lib/src/{CreatedOnBrandCrowd-pt-pt-CqsCRIqc.js → CreatedOnBrandCrowd-pt-br-DSSUcSmV.js} +1 -1
  7. package/lib/src/{CreatedOnBrandCrowd-pt-br-CqsCRIqc.js → CreatedOnBrandCrowd-pt-pt-DSSUcSmV.js} +1 -1
  8. package/lib/src/{CreatedOnDcom-de-de-NEOtgsvz.js → CreatedOnDcom-de-de-DF1JaJPE.js} +1 -1
  9. package/lib/src/{CreatedOnDcom-en-us-CKtEdcoG.js → CreatedOnDcom-en-us-Dek2GCu0.js} +1 -1
  10. package/lib/src/{CreatedOnDcom-es-es-DZR9o6DK.js → CreatedOnDcom-es-es-C3JD7_bP.js} +1 -1
  11. package/lib/src/{CreatedOnDcom-fr-ca-Bo_oTp-T.js → CreatedOnDcom-fr-ca-Bllxm84y.js} +1 -1
  12. package/lib/src/{CreatedOnDcom-fr-fr-DE6Fa3ie.js → CreatedOnDcom-fr-fr-B3cFZgRB.js} +1 -1
  13. package/lib/src/{CreatedOnDcom-pt-br-Cws2QJ7J.js → CreatedOnDcom-pt-br-DHCnZdZk.js} +1 -1
  14. package/lib/src/{CreatedOnDcom-pt-pt-DYGQo6q3.js → CreatedOnDcom-pt-pt-BLooAZOd.js} +1 -1
  15. package/lib/src/brand-page-maker/utils/rte-render.util.d.ts +8 -7
  16. package/lib/src/brand-page-maker/utils/rte-render.util.js +8 -36
  17. package/lib/src/brand-page-maker/utils/rte-serializer.d.ts +5 -0
  18. package/lib/src/brand-page-maker/utils/rte-serializer.js +112 -0
  19. package/lib/src/index.mjs +1 -1
  20. package/lib/src/{lib-C5H_bvMR.js → lib-BWEaceMZ.js} +16212 -16135
  21. package/package.json +1 -1
  22. package/src/brand-page-maker/utils/__snapshots__/rte-render-parity.test.ts.snap +2 -0
  23. package/src/brand-page-maker/utils/rte-render-parity.test.ts +21 -9
  24. package/src/brand-page-maker/utils/rte-render.util.ts +12 -47
  25. package/src/brand-page-maker/utils/rte-serializer.ts +172 -0
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@designcrowd/library.brand-page",
3
3
  "description": "A collection of Maker/Designer applications",
4
- "version": "6.0.6-json-prereq-1",
4
+ "version": "6.0.6-json-prereq-2",
5
5
  "author": "Design.com Eng",
6
6
  "license": "UNLICENSED",
7
7
  "type": "module",
@@ -2,6 +2,8 @@
2
2
 
3
3
  exports[`BP-5787 RTE render parity (golden master) > T1 — editor renders the complex fixture to the frozen HTML baseline > editor-normalized-html 1`] = `"<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: rgb(255, 224, 138); color: inherit;">highlight</mark> <span style="color: rgb(255, 0, 0);">literal hex</span> <a href="/about" target="_self">internal link</a> <a href="https://example.com" target="_blank" rel="noopener noreferrer nofollow">external link</a></p><p style="text-align: right;">second paragraph, right-aligned</p>"`;
4
4
 
5
+ exports[`BP-5787 RTE render parity (golden master) > T1 — render-from-JSON (DOM-less serialiser) produces equivalent HTML > serializer-html 1`] = `"<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; color: inherit;">highlight</mark> <span style="color: rgb(255, 0, 0)">literal hex</span> <a href="/about" target="_self">internal link</a> <a href="https://example.com" target="_blank" rel="noopener noreferrer nofollow">external link</a></p><p style="text-align: right">second paragraph, right-aligned</p>"`;
6
+
5
7
  exports[`BP-5787 RTE render parity (golden master) > T6 — freezes the JSON shape that becomes the stored form > editor-json 1`] = `
6
8
  {
7
9
  "content": [
@@ -82,17 +82,29 @@ describe('BP-5787 RTE render parity (golden master)', () => {
82
82
  expect(JSON.stringify(ed.getJSON())).toContain(THEME_TOKEN_REF);
83
83
  });
84
84
 
85
- it('T1 — render-from-JSON reproduces the editor-normalised HTML', () => {
86
- // The migration's core guarantee: store JSON, render to the SAME HTML
87
- // the editor produces directly (which the baseline snapshot freezes).
88
- const ed = makeEditor();
89
-
90
- ed.commands.setContent(COMPLEX_RTE_HTML);
91
- const reference = ed.getHTML();
92
-
85
+ it('T1 — render-from-JSON (DOM-less serialiser) produces equivalent HTML', () => {
86
+ // renderRteToHtml now serialises JSON → HTML with a pure function (no
87
+ // editor/DOM, so it works on the server). Output is semantically identical
88
+ // to the editor's — the Playwright visual golden-master is the authority on
89
+ // pixel parity; here we freeze the serialiser output and assert structure.
93
90
  const viaJson = renderRteToHtml(toRteJson(COMPLEX_RTE_HTML));
94
91
 
95
- expect(viaJson).toEqual(reference);
92
+ expect(viaJson).toMatchSnapshot('serializer-html');
93
+
94
+ // marks/nodes preserved
95
+ expect(viaJson).toContain('<strong>bold</strong>');
96
+ expect(viaJson).toContain('<em>italic</em>');
97
+ expect(viaJson).toContain('<u>underline</u>');
98
+ expect(viaJson).toContain('<s>strike</s>');
99
+ expect(viaJson).toContain('font-family: Montserrat');
100
+ expect(viaJson).toContain('font-size: 1.5em');
101
+ // theme token ref preserved (the crux of BP-5787)
102
+ expect(viaJson).toContain('color: var(--logo-text-color)');
103
+ // links + alignment
104
+ expect(viaJson).toContain('href="/about"');
105
+ expect(viaJson).toContain('href="https://example.com"');
106
+ expect(viaJson).toContain('text-align: center');
107
+ expect(viaJson).toContain('text-align: right');
96
108
  });
97
109
 
98
110
  it('T4 — legacy HTML strings render unchanged (back-compat passthrough)', () => {
@@ -1,6 +1,7 @@
1
1
  import { Editor, type JSONContent } from '@tiptap/vue-3';
2
2
  import { getRteExtensions } from '../composables/rich-text-editor/getRteExtensions';
3
3
  import { isTiptapDocument, getRteJsonPlainText } from './rich-text-editor.util';
4
+ import { serializeRteJsonToHtml } from './rte-serializer';
4
5
 
5
6
  export { isTiptapDocument, getRteJsonPlainText };
6
7
 
@@ -15,20 +16,17 @@ export { isTiptapDocument, getRteJsonPlainText };
15
16
  * - renderRteToHtml — render either form to final HTML for display.
16
17
  * - toRteJson — normalise either form to JSON for storage.
17
18
  *
18
- * Rendering uses the SAME extension set as the headless serialiser
19
- * (getRteExtensions, includeLinkRel) so JSON-rendered output is byte-identical
20
- * to today's stored HTML — proven by the render-parity golden master.
19
+ * - renderRteToHtml uses a DOM-less serialiser (works on the server).
20
+ * - toRteJson uses a headless editor to PARSE HTML → JSON, so it only runs
21
+ * where a DOM exists (browser, or jsdom in tests); the server never parses
22
+ * HTML (migration-18 is DOM-guarded, translation/fraud walk JSON directly).
21
23
  */
22
24
  export type RichTextValue = string | JSONContent;
23
25
 
24
26
  /**
25
27
  * Run a function with a short-lived headless editor (created then destroyed).
26
- *
27
- * We deliberately do NOT keep a long-lived editor instance: a lingering Tiptap
28
- * editor throws "document is not defined" when a test DOM (jsdom) is torn down.
29
- * The expensive repeat-render case (re-rendering unchanged content on a
30
- * display-style/layout change) is handled by the render cache below, so this
31
- * only runs on a cache miss — i.e. genuinely new/changed content.
28
+ * Used only by toRteJson (HTML → JSON parsing), which needs a real DOM. Not
29
+ * used by the render path, which is DOM-less.
32
30
  */
33
31
  const withHeadlessEditor = <T>(fn: (editor: Editor) => T): T => {
34
32
  const editor = new Editor({
@@ -44,20 +42,10 @@ const withHeadlessEditor = <T>(fn: (editor: Editor) => T): T => {
44
42
  };
45
43
 
46
44
  /**
47
- * JSON→HTML is pure: the same document always renders to the same HTML. A
48
- * display-style/layout change re-renders every TextWrapper but does NOT change
49
- * the text content, so without a cache we would re-run the (expensive)
50
- * setContent+getHTML for unchanged content on every re-render. A small LRU
51
- * keyed by the document makes those repeat renders a near-free map lookup.
52
- */
53
- const RENDER_CACHE_LIMIT = 200;
54
- const renderCache = new Map<string, string>();
55
-
56
- /**
57
- * Render an RTE value to final HTML. Legacy HTML strings pass through
58
- * untouched (they are already HTML); Tiptap JSON is rendered via the
59
- * headless editor's renderHTML, matching the stored-HTML output exactly,
60
- * and memoised so repeat renders of unchanged content don't re-convert.
45
+ * Render an RTE value to final HTML. Legacy HTML strings pass through untouched
46
+ * (they are already HTML); Tiptap JSON is serialised by a DOM-less pure
47
+ * function (rte-serializer) — no editor, no DOM — so it works on the server
48
+ * (SSR/publish), not just the browser, and is cheap enough to run per render.
61
49
  */
62
50
  export const renderRteToHtml = (
63
51
  value: RichTextValue | null | undefined,
@@ -66,30 +54,7 @@ export const renderRteToHtml = (
66
54
 
67
55
  if (typeof value === 'string') return value;
68
56
 
69
- const key = JSON.stringify(value);
70
- const cached = renderCache.get(key);
71
-
72
- if (cached !== undefined) {
73
- // LRU bump: re-insert so the most-recently-used key is evicted last.
74
- renderCache.delete(key);
75
- renderCache.set(key, cached);
76
-
77
- return cached;
78
- }
79
-
80
- const html = withHeadlessEditor((editor) => {
81
- editor.commands.setContent(value);
82
-
83
- return editor.getHTML();
84
- });
85
-
86
- renderCache.set(key, html);
87
-
88
- if (renderCache.size > RENDER_CACHE_LIMIT) {
89
- renderCache.delete(renderCache.keys().next().value as string);
90
- }
91
-
92
- return html;
57
+ return serializeRteJsonToHtml(value);
93
58
  };
94
59
 
95
60
  /**
@@ -0,0 +1,172 @@
1
+ import type { JSONContent } from '@tiptap/vue-3';
2
+
3
+ /**
4
+ * BP-5787 — DOM-less Tiptap-JSON → HTML serialiser.
5
+ *
6
+ * The editor-based render (`new Editor().getHTML()`) needs a browser DOM and
7
+ * throws "document is not defined" on the server (web-renderer SSR / publish),
8
+ * which is where published pages are rendered. This pure-function serialiser
9
+ * walks the JSON and emits the equivalent HTML with no editor and no DOM, so it
10
+ * runs anywhere (SSR, node, browser) and is far cheaper than instantiating an
11
+ * editor per field.
12
+ *
13
+ * The output is semantically identical to the editor's (same tags, same inline
14
+ * styles); the Playwright visual golden-master is the authority on pixel parity.
15
+ */
16
+
17
+ type Mark = { type?: string; attrs?: Record<string, unknown> };
18
+
19
+ type Node = {
20
+ type?: string;
21
+ text?: string;
22
+ marks?: Mark[];
23
+ attrs?: Record<string, unknown>;
24
+ content?: Node[];
25
+ };
26
+
27
+ const escapeHtml = (value: string): string =>
28
+ value.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
29
+
30
+ const escapeAttr = (value: string): string =>
31
+ escapeHtml(value).replace(/"/g, '&quot;');
32
+
33
+ // textStyle attr → CSS property, in a stable order.
34
+ const TEXT_STYLE_PROPS: [string, string][] = [
35
+ ['fontFamily', 'font-family'],
36
+ ['fontSize', 'font-size'],
37
+ ['letterSpacing', 'letter-spacing'],
38
+ ['lineHeight', 'line-height'],
39
+ ['color', 'color'],
40
+ ];
41
+
42
+ const textStyleCss = (attrs: Record<string, unknown>): string =>
43
+ TEXT_STYLE_PROPS.filter(
44
+ ([key]) => typeof attrs[key] === 'string' && attrs[key],
45
+ )
46
+ .map(([key, css]) => `${css}: ${attrs[key] as string}`)
47
+ .join('; ');
48
+
49
+ // Outermost → innermost. Reversed at wrap time so links wrap outside spans,
50
+ // which wrap outside the plain formatting marks.
51
+ const MARK_ORDER = [
52
+ 'link',
53
+ 'textStyle',
54
+ 'highlight',
55
+ 'strike',
56
+ 'underline',
57
+ 'italic',
58
+ 'bold',
59
+ 'code',
60
+ ];
61
+
62
+ const wrapMark = (
63
+ type: string,
64
+ attrs: Record<string, unknown>,
65
+ inner: string,
66
+ ): string => {
67
+ switch (type) {
68
+ case 'bold':
69
+ return `<strong>${inner}</strong>`;
70
+ case 'italic':
71
+ return `<em>${inner}</em>`;
72
+ case 'underline':
73
+ return `<u>${inner}</u>`;
74
+ case 'strike':
75
+ return `<s>${inner}</s>`;
76
+ case 'code':
77
+ return `<code>${inner}</code>`;
78
+
79
+ case 'textStyle': {
80
+ const css = textStyleCss(attrs);
81
+
82
+ return css ? `<span style="${escapeAttr(css)}">${inner}</span>` : inner;
83
+ }
84
+
85
+ case 'highlight': {
86
+ const color = typeof attrs.color === 'string' ? attrs.color : '';
87
+
88
+ return color
89
+ ? `<mark data-color="${escapeAttr(color)}" style="background-color: ${escapeAttr(color)}; color: inherit;">${inner}</mark>`
90
+ : `<mark>${inner}</mark>`;
91
+ }
92
+
93
+ case 'link': {
94
+ const href = typeof attrs.href === 'string' ? attrs.href : '';
95
+ let open = `<a href="${escapeAttr(href)}"`;
96
+
97
+ if (attrs.target) open += ` target="${escapeAttr(String(attrs.target))}"`;
98
+
99
+ if (attrs.rel) open += ` rel="${escapeAttr(String(attrs.rel))}"`;
100
+
101
+ return `${open}>${inner}</a>`;
102
+ }
103
+
104
+ default:
105
+ return inner;
106
+ }
107
+ };
108
+
109
+ const renderText = (node: Node): string => {
110
+ let html = escapeHtml(node.text ?? '');
111
+ const marks = Array.isArray(node.marks) ? node.marks : [];
112
+
113
+ for (const type of [...MARK_ORDER].reverse()) {
114
+ const mark = marks.find((m) => m?.type === type);
115
+
116
+ if (mark) html = wrapMark(type, mark.attrs ?? {}, html);
117
+ }
118
+
119
+ return html;
120
+ };
121
+
122
+ const alignAttr = (node: Node): string => {
123
+ const align = node.attrs?.textAlign;
124
+
125
+ return typeof align === 'string' && align
126
+ ? ` style="text-align: ${escapeAttr(align)}"`
127
+ : '';
128
+ };
129
+
130
+ const renderChildren = (node: Node): string =>
131
+ (node.content ?? []).map(renderNode).join('');
132
+
133
+ const renderNode = (node: Node): string => {
134
+ if (!node || typeof node !== 'object') return '';
135
+
136
+ switch (node.type) {
137
+ case 'text':
138
+ return renderText(node);
139
+ case 'hardBreak':
140
+ return '<br>';
141
+ case 'horizontalRule':
142
+ return '<hr>';
143
+ case 'paragraph':
144
+ return `<p${alignAttr(node)}>${renderChildren(node)}</p>`;
145
+
146
+ case 'heading': {
147
+ const level = Math.min(6, Math.max(1, Number(node.attrs?.level) || 1));
148
+
149
+ return `<h${level}${alignAttr(node)}>${renderChildren(node)}</h${level}>`;
150
+ }
151
+
152
+ case 'bulletList':
153
+ return `<ul>${renderChildren(node)}</ul>`;
154
+ case 'orderedList':
155
+ return `<ol>${renderChildren(node)}</ol>`;
156
+ case 'listItem':
157
+ return `<li>${renderChildren(node)}</li>`;
158
+ case 'blockquote':
159
+ return `<blockquote>${renderChildren(node)}</blockquote>`;
160
+ case 'codeBlock':
161
+ return `<pre><code>${renderChildren(node)}</code></pre>`;
162
+ // doc and any unknown container: render children.
163
+ default:
164
+ return renderChildren(node);
165
+ }
166
+ };
167
+
168
+ /**
169
+ * Serialise a Tiptap JSON document to HTML without an editor/DOM.
170
+ */
171
+ export const serializeRteJsonToHtml = (doc: JSONContent): string =>
172
+ renderChildren(doc as Node);