@localess/richtext 3.4.1-dev.20260831165456 → 3.4.1-dev.20260901200805

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1 +1,323 @@
1
- Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});var e={"&":`&amp;`,"<":`&lt;`,">":`&gt;`},t={...e,'"':`&quot;`};function n(t){return t.replace(/[&<>]/g,t=>e[t])}function r(e){return e.replace(/[&"<>]/g,e=>t[e])}var i=/^(?:https?:|mailto:|tel:)/i,a=/^[a-z][a-z0-9+.-]*:/i;function o(e){let t=e.trim();return t===``?``:i.test(t)||!a.test(t)?t:``}function s(e,t,n={}){let r={},i=e=>n.attrMap?.[e]??e,a=(e,t)=>{t!=null&&t!==``&&(r[i(e)]=t)};if(!t)return r;switch(e){case`orderedList`:t.start!==null&&t.start!==void 0&&t.start!==1&&a(`start`,t.start);break;case`codeBlock`:t.language&&a(`class`,`language-${t.language}`);break;case`link`:a(`target`,t.target),a(`rel`,t.rel),r[i(`href`)]=o(String(t.href??``)),a(`class`,t.class)}return r}function c(e,t){return e.type===t.type&&JSON.stringify(e.attrs??{})===JSON.stringify(t.attrs??{})}function l(e){let t=[],n=[];for(let r of e){let e=r.marks??[],i=0;for(;i<n.length&&i<e.length&&c(n[i].mark,e[i]);)i++;n.length=i;for(let r=i;r<e.length;r++){let i={kind:`mark`,mark:e[r],children:[]};(n.length>0?n[n.length-1].children:t).push(i),n.push(i)}(n.length>0?n[n.length-1].children:t).push({kind:`text`,text:r.text})}return t}function u(e,t={}){let n;return n=e?Array.isArray(e)?e:e.type===`doc`?e.content??[]:typeof e.type==`string`?[e]:[]:[],t.withKeys?d(n,{}):n}function d(e,t){return e.map(e=>{t[e.type]=(t[e.type]??0)+1;let n={...e,_key:`${e.type}-${t[e.type]}`};return Array.isArray(n.content)&&(n.content=d(n.content,t)),n})}var f=[1,2,3,4,5,6];function p(e){let t=e?.level;return`h${f.includes(t)?t:1}`}var m={doc:null,text:null,paragraph:{tag:`p`,content:!0},heading:{resolve:p,content:!0},bulletList:{tag:`ul`,content:!0},orderedList:{tag:`ol`,content:!0},listItem:{tag:`li`,content:!0},codeBlock:{tag:`pre`,children:[{tag:`code`,content:!0}]}},h={bold:{tag:`strong`,content:!0},italic:{tag:`em`,content:!0},strike:{tag:`s`,content:!0},underline:{tag:`u`,content:!0},code:{tag:`code`,content:!0},link:{tag:`a`,content:!0}};function g(e,t={}){return _(u(e),{renderers:t.renderers,warned:new Set})}function _(e,t){let n=``,r=0;for(;r<e.length;){let i=e[r];if(i.type===`text`&&!t.renderers?.text){let i=[];for(;r<e.length&&e[r].type===`text`;)i.push(e[r]),r++;n+=y(l(i),t)}else n+=v(i,t),r++}return n}function v(e,t){let r=t.renderers?.[e.type];if(r){let i={...t.renderers,[e.type]:void 0},a={renderers:i,warned:t.warned},o=e.type===`text`?n(e.text??``):_(e.content??[],a);return r({...e,children:o,context:{renderers:i}})}if(e.type===`text`)return y(l([e]),t);let i=m[e.type];if(i===void 0)return x(t,e.type),``;if(i===null)return _(e.content??[],t);let a=s(e.type,e.attrs),o=_(e.content??[],t);if(i.children){let e=o;for(let t=i.children.length-1;t>=0;t--){let n=i.children[t];e=b(n.tag,n.content?a:{},e)}return b(i.tag,{},e)}return b(i.resolve?i.resolve(e.attrs):i.tag,a,o)}function y(e,t){let r=``;for(let i of e){if(i.kind===`text`){r+=n(i.text);continue}let e=y(i.children,t),a=t.renderers?.[i.mark.type];if(a){r+=a({...i.mark,children:e,context:{renderers:t.renderers}});continue}let o=h[i.mark.type];if(!o){x(t,i.mark.type),r+=e;continue}r+=b(o.tag,s(i.mark.type,i.mark.attrs),e)}return r}function b(e,t,n){let i=`<${e}`;for(let[e,n]of Object.entries(t))i+=` ${e}="${r(String(n))}"`;return`${i}>${n}</${e}>`}function x(e,t){typeof process<`u`&&process.env&&process.env.NODE_ENV===`production`||e.warned.has(t)||(e.warned.add(t),console.warn(`[@localess/richtext] Unknown rich text element "${t}" was skipped. Provide a custom renderer to handle it.`))}exports.MARK_RENDER_MAP=h,exports.NODE_RENDER_MAP=m,exports.buildMarkTree=l,exports.escapeAttr=r,exports.escapeHtml=n,exports.marksEqual=c,exports.normalizeInput=u,exports.processAttrs=s,exports.renderRichTextToHtml=g,exports.resolveHeadingTag=p,exports.sanitizeUrl=o;
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region src/escape.ts
3
+ var TEXT_ESCAPES = {
4
+ "&": "&amp;",
5
+ "<": "&lt;",
6
+ ">": "&gt;"
7
+ };
8
+ var ATTR_ESCAPES = {
9
+ ...TEXT_ESCAPES,
10
+ "\"": "&quot;"
11
+ };
12
+ /**
13
+ * Escapes text content for safe HTML output. The escape set (`& < >`) matches
14
+ * TipTap's `generateHTML` DOM serialization — parity-tested; do not widen it
15
+ * without updating the parity fixtures.
16
+ */
17
+ function escapeHtml(text) {
18
+ return text.replace(/[&<>]/g, (ch) => TEXT_ESCAPES[ch]);
19
+ }
20
+ /** Escapes an attribute value for safe double-quoted HTML output (`& " < >`). */
21
+ function escapeAttr(value) {
22
+ return value.replace(/[&"<>]/g, (ch) => ATTR_ESCAPES[ch]);
23
+ }
24
+ var SAFE_SCHEME = /^(?:https?:|mailto:|tel:)/i;
25
+ var HAS_SCHEME = /^[a-z][a-z0-9+.-]*:/i;
26
+ /**
27
+ * Allowlist URL sanitizer for link hrefs: `http:`, `https:`, `mailto:`, `tel:`
28
+ * and scheme-less (relative/protocol-relative/fragment/query) URLs pass;
29
+ * everything else (e.g. `javascript:`, `data:`) becomes `''`.
30
+ */
31
+ function sanitizeUrl(url) {
32
+ const trimmed = url.trim();
33
+ if (trimmed === "") return "";
34
+ if (SAFE_SCHEME.test(trimmed)) return trimmed;
35
+ if (!HAS_SCHEME.test(trimmed)) return trimmed;
36
+ return "";
37
+ }
38
+ //#endregion
39
+ //#region src/attrs.ts
40
+ /**
41
+ * Normalizes a node/mark's stored attrs into the attributes to emit, in the
42
+ * order TipTap's `generateHTML` emits them (parity-tested — adjust order here
43
+ * and in the fixtures together if the parity test disagrees).
44
+ */
45
+ function processAttrs(type, attrs, options = {}) {
46
+ const out = {};
47
+ const name = (key) => options.attrMap?.[key] ?? key;
48
+ const put = (key, value) => {
49
+ if (value === null || value === void 0 || value === "") return;
50
+ out[name(key)] = value;
51
+ };
52
+ if (!attrs) return out;
53
+ switch (type) {
54
+ case "orderedList":
55
+ if (attrs.start !== null && attrs.start !== void 0 && attrs.start !== 1) put("start", attrs.start);
56
+ break;
57
+ case "codeBlock":
58
+ if (attrs.language) put("class", `language-${attrs.language}`);
59
+ break;
60
+ case "link":
61
+ put("target", attrs.target);
62
+ put("rel", attrs.rel);
63
+ out[name("href")] = sanitizeUrl(String(attrs.href ?? ""));
64
+ put("class", attrs.class);
65
+ }
66
+ return out;
67
+ }
68
+ //#endregion
69
+ //#region src/marks.ts
70
+ /** Deep equality of two marks (type + attrs). Attr key order must match, which holds for editor-produced documents. */
71
+ function marksEqual(a, b) {
72
+ return a.type === b.type && JSON.stringify(a.attrs ?? {}) === JSON.stringify(b.attrs ?? {});
73
+ }
74
+ /**
75
+ * Folds a run of consecutive text nodes into a tree in which adjacent nodes
76
+ * sharing the same outer marks share one wrapper — the same merging
77
+ * ProseMirror's DOM serializer performs, so output matches TipTap's
78
+ * `generateHTML` (one `<a>` per link span, `<strong>a<em>b</em></strong>`
79
+ * instead of sibling `<strong>` wrappers).
80
+ */
81
+ function buildMarkTree(nodes) {
82
+ const root = [];
83
+ const stack = [];
84
+ for (const node of nodes) {
85
+ const marks = node.marks ?? [];
86
+ let depth = 0;
87
+ while (depth < stack.length && depth < marks.length && marksEqual(stack[depth].mark, marks[depth])) depth++;
88
+ stack.length = depth;
89
+ for (let i = depth; i < marks.length; i++) {
90
+ const segment = {
91
+ kind: "mark",
92
+ mark: marks[i],
93
+ children: []
94
+ };
95
+ (stack.length > 0 ? stack[stack.length - 1].children : root).push(segment);
96
+ stack.push(segment);
97
+ }
98
+ (stack.length > 0 ? stack[stack.length - 1].children : root).push({
99
+ kind: "text",
100
+ text: node.text
101
+ });
102
+ }
103
+ return root;
104
+ }
105
+ //#endregion
106
+ //#region src/normalize.ts
107
+ /**
108
+ * Flattens any accepted rich text input (document, node, node array, or the
109
+ * loose `ContentRichText` shape from `@localess/client`) into a node list.
110
+ * Never throws; malformed input yields `[]`.
111
+ */
112
+ function normalizeInput(input, options = {}) {
113
+ let nodes;
114
+ if (!input) nodes = [];
115
+ else if (Array.isArray(input)) nodes = input;
116
+ else if (input.type === "doc") nodes = input.content ?? [];
117
+ else if (typeof input.type === "string") nodes = [input];
118
+ else nodes = [];
119
+ return options.withKeys ? addKeys(nodes, {}) : nodes;
120
+ }
121
+ function addKeys(nodes, counters) {
122
+ return nodes.map((node) => {
123
+ counters[node.type] = (counters[node.type] ?? 0) + 1;
124
+ const keyed = {
125
+ ...node,
126
+ _key: `${node.type}-${counters[node.type]}`
127
+ };
128
+ if (Array.isArray(keyed.content)) keyed.content = addKeys(keyed.content, counters);
129
+ return keyed;
130
+ });
131
+ }
132
+ //#endregion
133
+ //#region src/render-map.ts
134
+ var HEADING_LEVELS = [
135
+ 1,
136
+ 2,
137
+ 3,
138
+ 4,
139
+ 5,
140
+ 6
141
+ ];
142
+ /** Invalid levels fall back to h1, matching TipTap's first-configured-level behavior. */
143
+ function resolveHeadingTag(attrs) {
144
+ const level = attrs?.level;
145
+ return `h${HEADING_LEVELS.includes(level) ? level : 1}`;
146
+ }
147
+ /** `null` = transparent (render children only, no element). Missing key = unknown type. */
148
+ var NODE_RENDER_MAP = {
149
+ doc: null,
150
+ text: null,
151
+ paragraph: {
152
+ tag: "p",
153
+ content: true
154
+ },
155
+ heading: {
156
+ resolve: resolveHeadingTag,
157
+ content: true
158
+ },
159
+ bulletList: {
160
+ tag: "ul",
161
+ content: true
162
+ },
163
+ orderedList: {
164
+ tag: "ol",
165
+ content: true
166
+ },
167
+ listItem: {
168
+ tag: "li",
169
+ content: true
170
+ },
171
+ codeBlock: {
172
+ tag: "pre",
173
+ children: [{
174
+ tag: "code",
175
+ content: true
176
+ }]
177
+ }
178
+ };
179
+ var MARK_RENDER_MAP = {
180
+ bold: {
181
+ tag: "strong",
182
+ content: true
183
+ },
184
+ italic: {
185
+ tag: "em",
186
+ content: true
187
+ },
188
+ strike: {
189
+ tag: "s",
190
+ content: true
191
+ },
192
+ underline: {
193
+ tag: "u",
194
+ content: true
195
+ },
196
+ code: {
197
+ tag: "code",
198
+ content: true
199
+ },
200
+ link: {
201
+ tag: "a",
202
+ content: true
203
+ }
204
+ };
205
+ //#endregion
206
+ //#region src/render-html.ts
207
+ /**
208
+ * Renders Localess rich text JSON to an HTML string. Framework-neutral,
209
+ * dependency-free, and byte-compatible with TipTap's `generateHTML` for the
210
+ * node set the Localess Studio editor produces.
211
+ */
212
+ function renderRichTextToHtml(input, options = {}) {
213
+ return renderNodes(normalizeInput(input), {
214
+ renderers: options.renderers,
215
+ warned: /* @__PURE__ */ new Set()
216
+ });
217
+ }
218
+ function renderNodes(nodes, ctx) {
219
+ let result = "";
220
+ let i = 0;
221
+ while (i < nodes.length) {
222
+ const node = nodes[i];
223
+ if (node.type === "text" && !ctx.renderers?.text) {
224
+ const run = [];
225
+ while (i < nodes.length && nodes[i].type === "text") {
226
+ run.push(nodes[i]);
227
+ i++;
228
+ }
229
+ result += renderSegments(buildMarkTree(run), ctx);
230
+ } else {
231
+ result += renderNode(node, ctx);
232
+ i++;
233
+ }
234
+ }
235
+ return result;
236
+ }
237
+ function renderNode(node, ctx) {
238
+ const custom = ctx.renderers?.[node.type];
239
+ if (custom) {
240
+ const childRenderers = {
241
+ ...ctx.renderers,
242
+ [node.type]: void 0
243
+ };
244
+ const childCtx = {
245
+ renderers: childRenderers,
246
+ warned: ctx.warned
247
+ };
248
+ const children = node.type === "text" ? escapeHtml(node.text ?? "") : renderNodes(node.content ?? [], childCtx);
249
+ return custom({
250
+ ...node,
251
+ children,
252
+ context: { renderers: childRenderers }
253
+ });
254
+ }
255
+ if (node.type === "text") return renderSegments(buildMarkTree([node]), ctx);
256
+ const spec = NODE_RENDER_MAP[node.type];
257
+ if (spec === void 0) {
258
+ warnUnknown(ctx, node.type);
259
+ return "";
260
+ }
261
+ if (spec === null) return renderNodes(node.content ?? [], ctx);
262
+ const attrs = processAttrs(node.type, node.attrs);
263
+ const children = renderNodes(node.content ?? [], ctx);
264
+ if (spec.children) {
265
+ let inner = children;
266
+ for (let i = spec.children.length - 1; i >= 0; i--) {
267
+ const child = spec.children[i];
268
+ inner = wrapTag(child.tag, child.content ? attrs : {}, inner);
269
+ }
270
+ return wrapTag(spec.tag, {}, inner);
271
+ }
272
+ return wrapTag(spec.resolve ? spec.resolve(node.attrs) : spec.tag, attrs, children);
273
+ }
274
+ function renderSegments(segments, ctx) {
275
+ let out = "";
276
+ for (const segment of segments) {
277
+ if (segment.kind === "text") {
278
+ out += escapeHtml(segment.text);
279
+ continue;
280
+ }
281
+ const children = renderSegments(segment.children, ctx);
282
+ const custom = ctx.renderers?.[segment.mark.type];
283
+ if (custom) {
284
+ out += custom({
285
+ ...segment.mark,
286
+ children,
287
+ context: { renderers: ctx.renderers }
288
+ });
289
+ continue;
290
+ }
291
+ const spec = MARK_RENDER_MAP[segment.mark.type];
292
+ if (!spec) {
293
+ warnUnknown(ctx, segment.mark.type);
294
+ out += children;
295
+ continue;
296
+ }
297
+ out += wrapTag(spec.tag, processAttrs(segment.mark.type, segment.mark.attrs), children);
298
+ }
299
+ return out;
300
+ }
301
+ function wrapTag(tag, attrs, children) {
302
+ let open = `<${tag}`;
303
+ for (const [name, value] of Object.entries(attrs)) open += ` ${name}="${escapeAttr(String(value))}"`;
304
+ return `${open}>${children}</${tag}>`;
305
+ }
306
+ function warnUnknown(ctx, type) {
307
+ if (typeof process !== "undefined" && process.env && process.env.NODE_ENV === "production") return;
308
+ if (ctx.warned.has(type)) return;
309
+ ctx.warned.add(type);
310
+ console.warn(`[@localess/richtext] Unknown rich text element "${type}" was skipped. Provide a custom renderer to handle it.`);
311
+ }
312
+ //#endregion
313
+ exports.MARK_RENDER_MAP = MARK_RENDER_MAP;
314
+ exports.NODE_RENDER_MAP = NODE_RENDER_MAP;
315
+ exports.buildMarkTree = buildMarkTree;
316
+ exports.escapeAttr = escapeAttr;
317
+ exports.escapeHtml = escapeHtml;
318
+ exports.marksEqual = marksEqual;
319
+ exports.normalizeInput = normalizeInput;
320
+ exports.processAttrs = processAttrs;
321
+ exports.renderRichTextToHtml = renderRichTextToHtml;
322
+ exports.resolveHeadingTag = resolveHeadingTag;
323
+ exports.sanitizeUrl = sanitizeUrl;
package/dist/index.mjs CHANGED
@@ -1,86 +1,136 @@
1
1
  //#region src/escape.ts
2
- var e = {
2
+ var TEXT_ESCAPES = {
3
3
  "&": "&amp;",
4
4
  "<": "&lt;",
5
5
  ">": "&gt;"
6
- }, t = {
7
- ...e,
6
+ };
7
+ var ATTR_ESCAPES = {
8
+ ...TEXT_ESCAPES,
8
9
  "\"": "&quot;"
9
10
  };
10
- function n(t) {
11
- return t.replace(/[&<>]/g, (t) => e[t]);
11
+ /**
12
+ * Escapes text content for safe HTML output. The escape set (`& < >`) matches
13
+ * TipTap's `generateHTML` DOM serialization — parity-tested; do not widen it
14
+ * without updating the parity fixtures.
15
+ */
16
+ function escapeHtml(text) {
17
+ return text.replace(/[&<>]/g, (ch) => TEXT_ESCAPES[ch]);
12
18
  }
13
- function r(e) {
14
- return e.replace(/[&"<>]/g, (e) => t[e]);
19
+ /** Escapes an attribute value for safe double-quoted HTML output (`& " < >`). */
20
+ function escapeAttr(value) {
21
+ return value.replace(/[&"<>]/g, (ch) => ATTR_ESCAPES[ch]);
15
22
  }
16
- var i = /^(?:https?:|mailto:|tel:)/i, a = /^[a-z][a-z0-9+.-]*:/i;
17
- function o(e) {
18
- let t = e.trim();
19
- return t === "" ? "" : i.test(t) || !a.test(t) ? t : "";
23
+ var SAFE_SCHEME = /^(?:https?:|mailto:|tel:)/i;
24
+ var HAS_SCHEME = /^[a-z][a-z0-9+.-]*:/i;
25
+ /**
26
+ * Allowlist URL sanitizer for link hrefs: `http:`, `https:`, `mailto:`, `tel:`
27
+ * and scheme-less (relative/protocol-relative/fragment/query) URLs pass;
28
+ * everything else (e.g. `javascript:`, `data:`) becomes `''`.
29
+ */
30
+ function sanitizeUrl(url) {
31
+ const trimmed = url.trim();
32
+ if (trimmed === "") return "";
33
+ if (SAFE_SCHEME.test(trimmed)) return trimmed;
34
+ if (!HAS_SCHEME.test(trimmed)) return trimmed;
35
+ return "";
20
36
  }
21
37
  //#endregion
22
38
  //#region src/attrs.ts
23
- function s(e, t, n = {}) {
24
- let r = {}, i = (e) => n.attrMap?.[e] ?? e, a = (e, t) => {
25
- t != null && t !== "" && (r[i(e)] = t);
39
+ /**
40
+ * Normalizes a node/mark's stored attrs into the attributes to emit, in the
41
+ * order TipTap's `generateHTML` emits them (parity-tested adjust order here
42
+ * and in the fixtures together if the parity test disagrees).
43
+ */
44
+ function processAttrs(type, attrs, options = {}) {
45
+ const out = {};
46
+ const name = (key) => options.attrMap?.[key] ?? key;
47
+ const put = (key, value) => {
48
+ if (value === null || value === void 0 || value === "") return;
49
+ out[name(key)] = value;
26
50
  };
27
- if (!t) return r;
28
- switch (e) {
51
+ if (!attrs) return out;
52
+ switch (type) {
29
53
  case "orderedList":
30
- t.start !== null && t.start !== void 0 && t.start !== 1 && a("start", t.start);
54
+ if (attrs.start !== null && attrs.start !== void 0 && attrs.start !== 1) put("start", attrs.start);
31
55
  break;
32
56
  case "codeBlock":
33
- t.language && a("class", `language-${t.language}`);
57
+ if (attrs.language) put("class", `language-${attrs.language}`);
34
58
  break;
35
- case "link": a("target", t.target), a("rel", t.rel), r[i("href")] = o(String(t.href ?? "")), a("class", t.class);
59
+ case "link":
60
+ put("target", attrs.target);
61
+ put("rel", attrs.rel);
62
+ out[name("href")] = sanitizeUrl(String(attrs.href ?? ""));
63
+ put("class", attrs.class);
36
64
  }
37
- return r;
65
+ return out;
38
66
  }
39
67
  //#endregion
40
68
  //#region src/marks.ts
41
- function c(e, t) {
42
- return e.type === t.type && JSON.stringify(e.attrs ?? {}) === JSON.stringify(t.attrs ?? {});
69
+ /** Deep equality of two marks (type + attrs). Attr key order must match, which holds for editor-produced documents. */
70
+ function marksEqual(a, b) {
71
+ return a.type === b.type && JSON.stringify(a.attrs ?? {}) === JSON.stringify(b.attrs ?? {});
43
72
  }
44
- function l(e) {
45
- let t = [], n = [];
46
- for (let r of e) {
47
- let e = r.marks ?? [], i = 0;
48
- for (; i < n.length && i < e.length && c(n[i].mark, e[i]);) i++;
49
- n.length = i;
50
- for (let r = i; r < e.length; r++) {
51
- let i = {
73
+ /**
74
+ * Folds a run of consecutive text nodes into a tree in which adjacent nodes
75
+ * sharing the same outer marks share one wrapper — the same merging
76
+ * ProseMirror's DOM serializer performs, so output matches TipTap's
77
+ * `generateHTML` (one `<a>` per link span, `<strong>a<em>b</em></strong>`
78
+ * instead of sibling `<strong>` wrappers).
79
+ */
80
+ function buildMarkTree(nodes) {
81
+ const root = [];
82
+ const stack = [];
83
+ for (const node of nodes) {
84
+ const marks = node.marks ?? [];
85
+ let depth = 0;
86
+ while (depth < stack.length && depth < marks.length && marksEqual(stack[depth].mark, marks[depth])) depth++;
87
+ stack.length = depth;
88
+ for (let i = depth; i < marks.length; i++) {
89
+ const segment = {
52
90
  kind: "mark",
53
- mark: e[r],
91
+ mark: marks[i],
54
92
  children: []
55
93
  };
56
- (n.length > 0 ? n[n.length - 1].children : t).push(i), n.push(i);
94
+ (stack.length > 0 ? stack[stack.length - 1].children : root).push(segment);
95
+ stack.push(segment);
57
96
  }
58
- (n.length > 0 ? n[n.length - 1].children : t).push({
97
+ (stack.length > 0 ? stack[stack.length - 1].children : root).push({
59
98
  kind: "text",
60
- text: r.text
99
+ text: node.text
61
100
  });
62
101
  }
63
- return t;
102
+ return root;
64
103
  }
65
104
  //#endregion
66
105
  //#region src/normalize.ts
67
- function u(e, t = {}) {
68
- let n;
69
- return n = e ? Array.isArray(e) ? e : e.type === "doc" ? e.content ?? [] : typeof e.type == "string" ? [e] : [] : [], t.withKeys ? d(n, {}) : n;
106
+ /**
107
+ * Flattens any accepted rich text input (document, node, node array, or the
108
+ * loose `ContentRichText` shape from `@localess/client`) into a node list.
109
+ * Never throws; malformed input yields `[]`.
110
+ */
111
+ function normalizeInput(input, options = {}) {
112
+ let nodes;
113
+ if (!input) nodes = [];
114
+ else if (Array.isArray(input)) nodes = input;
115
+ else if (input.type === "doc") nodes = input.content ?? [];
116
+ else if (typeof input.type === "string") nodes = [input];
117
+ else nodes = [];
118
+ return options.withKeys ? addKeys(nodes, {}) : nodes;
70
119
  }
71
- function d(e, t) {
72
- return e.map((e) => {
73
- t[e.type] = (t[e.type] ?? 0) + 1;
74
- let n = {
75
- ...e,
76
- _key: `${e.type}-${t[e.type]}`
120
+ function addKeys(nodes, counters) {
121
+ return nodes.map((node) => {
122
+ counters[node.type] = (counters[node.type] ?? 0) + 1;
123
+ const keyed = {
124
+ ...node,
125
+ _key: `${node.type}-${counters[node.type]}`
77
126
  };
78
- return Array.isArray(n.content) && (n.content = d(n.content, t)), n;
127
+ if (Array.isArray(keyed.content)) keyed.content = addKeys(keyed.content, counters);
128
+ return keyed;
79
129
  });
80
130
  }
81
131
  //#endregion
82
132
  //#region src/render-map.ts
83
- var f = [
133
+ var HEADING_LEVELS = [
84
134
  1,
85
135
  2,
86
136
  3,
@@ -88,149 +138,175 @@ var f = [
88
138
  5,
89
139
  6
90
140
  ];
91
- function p(e) {
92
- let t = e?.level;
93
- return `h${f.includes(t) ? t : 1}`;
141
+ /** Invalid levels fall back to h1, matching TipTap's first-configured-level behavior. */
142
+ function resolveHeadingTag(attrs) {
143
+ const level = attrs?.level;
144
+ return `h${HEADING_LEVELS.includes(level) ? level : 1}`;
94
145
  }
95
- var m = {
146
+ /** `null` = transparent (render children only, no element). Missing key = unknown type. */
147
+ var NODE_RENDER_MAP = {
96
148
  doc: null,
97
149
  text: null,
98
150
  paragraph: {
99
151
  tag: "p",
100
- content: !0
152
+ content: true
101
153
  },
102
154
  heading: {
103
- resolve: p,
104
- content: !0
155
+ resolve: resolveHeadingTag,
156
+ content: true
105
157
  },
106
158
  bulletList: {
107
159
  tag: "ul",
108
- content: !0
160
+ content: true
109
161
  },
110
162
  orderedList: {
111
163
  tag: "ol",
112
- content: !0
164
+ content: true
113
165
  },
114
166
  listItem: {
115
167
  tag: "li",
116
- content: !0
168
+ content: true
117
169
  },
118
170
  codeBlock: {
119
171
  tag: "pre",
120
172
  children: [{
121
173
  tag: "code",
122
- content: !0
174
+ content: true
123
175
  }]
124
176
  }
125
- }, h = {
177
+ };
178
+ var MARK_RENDER_MAP = {
126
179
  bold: {
127
180
  tag: "strong",
128
- content: !0
181
+ content: true
129
182
  },
130
183
  italic: {
131
184
  tag: "em",
132
- content: !0
185
+ content: true
133
186
  },
134
187
  strike: {
135
188
  tag: "s",
136
- content: !0
189
+ content: true
137
190
  },
138
191
  underline: {
139
192
  tag: "u",
140
- content: !0
193
+ content: true
141
194
  },
142
195
  code: {
143
196
  tag: "code",
144
- content: !0
197
+ content: true
145
198
  },
146
199
  link: {
147
200
  tag: "a",
148
- content: !0
201
+ content: true
149
202
  }
150
203
  };
151
204
  //#endregion
152
205
  //#region src/render-html.ts
153
- function g(e, t = {}) {
154
- return _(u(e), {
155
- renderers: t.renderers,
206
+ /**
207
+ * Renders Localess rich text JSON to an HTML string. Framework-neutral,
208
+ * dependency-free, and byte-compatible with TipTap's `generateHTML` for the
209
+ * node set the Localess Studio editor produces.
210
+ */
211
+ function renderRichTextToHtml(input, options = {}) {
212
+ return renderNodes(normalizeInput(input), {
213
+ renderers: options.renderers,
156
214
  warned: /* @__PURE__ */ new Set()
157
215
  });
158
216
  }
159
- function _(e, t) {
160
- let n = "", r = 0;
161
- for (; r < e.length;) {
162
- let i = e[r];
163
- if (i.type === "text" && !t.renderers?.text) {
164
- let i = [];
165
- for (; r < e.length && e[r].type === "text";) i.push(e[r]), r++;
166
- n += y(l(i), t);
167
- } else n += v(i, t), r++;
217
+ function renderNodes(nodes, ctx) {
218
+ let result = "";
219
+ let i = 0;
220
+ while (i < nodes.length) {
221
+ const node = nodes[i];
222
+ if (node.type === "text" && !ctx.renderers?.text) {
223
+ const run = [];
224
+ while (i < nodes.length && nodes[i].type === "text") {
225
+ run.push(nodes[i]);
226
+ i++;
227
+ }
228
+ result += renderSegments(buildMarkTree(run), ctx);
229
+ } else {
230
+ result += renderNode(node, ctx);
231
+ i++;
232
+ }
168
233
  }
169
- return n;
234
+ return result;
170
235
  }
171
- function v(e, t) {
172
- let r = t.renderers?.[e.type];
173
- if (r) {
174
- let i = {
175
- ...t.renderers,
176
- [e.type]: void 0
177
- }, a = {
178
- renderers: i,
179
- warned: t.warned
180
- }, o = e.type === "text" ? n(e.text ?? "") : _(e.content ?? [], a);
181
- return r({
182
- ...e,
183
- children: o,
184
- context: { renderers: i }
236
+ function renderNode(node, ctx) {
237
+ const custom = ctx.renderers?.[node.type];
238
+ if (custom) {
239
+ const childRenderers = {
240
+ ...ctx.renderers,
241
+ [node.type]: void 0
242
+ };
243
+ const childCtx = {
244
+ renderers: childRenderers,
245
+ warned: ctx.warned
246
+ };
247
+ const children = node.type === "text" ? escapeHtml(node.text ?? "") : renderNodes(node.content ?? [], childCtx);
248
+ return custom({
249
+ ...node,
250
+ children,
251
+ context: { renderers: childRenderers }
185
252
  });
186
253
  }
187
- if (e.type === "text") return y(l([e]), t);
188
- let i = m[e.type];
189
- if (i === void 0) return x(t, e.type), "";
190
- if (i === null) return _(e.content ?? [], t);
191
- let a = s(e.type, e.attrs), o = _(e.content ?? [], t);
192
- if (i.children) {
193
- let e = o;
194
- for (let t = i.children.length - 1; t >= 0; t--) {
195
- let n = i.children[t];
196
- e = b(n.tag, n.content ? a : {}, e);
254
+ if (node.type === "text") return renderSegments(buildMarkTree([node]), ctx);
255
+ const spec = NODE_RENDER_MAP[node.type];
256
+ if (spec === void 0) {
257
+ warnUnknown(ctx, node.type);
258
+ return "";
259
+ }
260
+ if (spec === null) return renderNodes(node.content ?? [], ctx);
261
+ const attrs = processAttrs(node.type, node.attrs);
262
+ const children = renderNodes(node.content ?? [], ctx);
263
+ if (spec.children) {
264
+ let inner = children;
265
+ for (let i = spec.children.length - 1; i >= 0; i--) {
266
+ const child = spec.children[i];
267
+ inner = wrapTag(child.tag, child.content ? attrs : {}, inner);
197
268
  }
198
- return b(i.tag, {}, e);
269
+ return wrapTag(spec.tag, {}, inner);
199
270
  }
200
- return b(i.resolve ? i.resolve(e.attrs) : i.tag, a, o);
271
+ return wrapTag(spec.resolve ? spec.resolve(node.attrs) : spec.tag, attrs, children);
201
272
  }
202
- function y(e, t) {
203
- let r = "";
204
- for (let i of e) {
205
- if (i.kind === "text") {
206
- r += n(i.text);
273
+ function renderSegments(segments, ctx) {
274
+ let out = "";
275
+ for (const segment of segments) {
276
+ if (segment.kind === "text") {
277
+ out += escapeHtml(segment.text);
207
278
  continue;
208
279
  }
209
- let e = y(i.children, t), a = t.renderers?.[i.mark.type];
210
- if (a) {
211
- r += a({
212
- ...i.mark,
213
- children: e,
214
- context: { renderers: t.renderers }
280
+ const children = renderSegments(segment.children, ctx);
281
+ const custom = ctx.renderers?.[segment.mark.type];
282
+ if (custom) {
283
+ out += custom({
284
+ ...segment.mark,
285
+ children,
286
+ context: { renderers: ctx.renderers }
215
287
  });
216
288
  continue;
217
289
  }
218
- let o = h[i.mark.type];
219
- if (!o) {
220
- x(t, i.mark.type), r += e;
290
+ const spec = MARK_RENDER_MAP[segment.mark.type];
291
+ if (!spec) {
292
+ warnUnknown(ctx, segment.mark.type);
293
+ out += children;
221
294
  continue;
222
295
  }
223
- r += b(o.tag, s(i.mark.type, i.mark.attrs), e);
296
+ out += wrapTag(spec.tag, processAttrs(segment.mark.type, segment.mark.attrs), children);
224
297
  }
225
- return r;
298
+ return out;
226
299
  }
227
- function b(e, t, n) {
228
- let i = `<${e}`;
229
- for (let [e, n] of Object.entries(t)) i += ` ${e}="${r(String(n))}"`;
230
- return `${i}>${n}</${e}>`;
300
+ function wrapTag(tag, attrs, children) {
301
+ let open = `<${tag}`;
302
+ for (const [name, value] of Object.entries(attrs)) open += ` ${name}="${escapeAttr(String(value))}"`;
303
+ return `${open}>${children}</${tag}>`;
231
304
  }
232
- function x(e, t) {
233
- typeof process < "u" && process.env && process.env.NODE_ENV === "production" || e.warned.has(t) || (e.warned.add(t), console.warn(`[@localess/richtext] Unknown rich text element "${t}" was skipped. Provide a custom renderer to handle it.`));
305
+ function warnUnknown(ctx, type) {
306
+ if (typeof process !== "undefined" && process.env && process.env.NODE_ENV === "production") return;
307
+ if (ctx.warned.has(type)) return;
308
+ ctx.warned.add(type);
309
+ console.warn(`[@localess/richtext] Unknown rich text element "${type}" was skipped. Provide a custom renderer to handle it.`);
234
310
  }
235
311
  //#endregion
236
- export { h as MARK_RENDER_MAP, m as NODE_RENDER_MAP, l as buildMarkTree, r as escapeAttr, n as escapeHtml, c as marksEqual, u as normalizeInput, s as processAttrs, g as renderRichTextToHtml, p as resolveHeadingTag, o as sanitizeUrl };
312
+ export { MARK_RENDER_MAP, NODE_RENDER_MAP, buildMarkTree, escapeAttr, escapeHtml, marksEqual, normalizeInput, processAttrs, renderRichTextToHtml, resolveHeadingTag, sanitizeUrl };
@@ -1 +1,204 @@
1
- Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});var e=(...e)=>({type:`doc`,content:e}),t=(...e)=>({type:`paragraph`,content:e}),n=(e,t)=>({type:`text`,text:e,...t?{marks:t}:{}}),r=(...e)=>({type:`listItem`,content:e}),i={type:`link`,attrs:{href:`https://example.com`,target:`_blank`,rel:`noopener noreferrer nofollow`,class:null}},a=[{title:`empty doc`,input:e(),expected:``,parity:!0},{title:`null input`,input:null,expected:``,parity:!1},{title:`plain paragraph`,input:e(t(n(`Hello world`))),expected:`<p>Hello world</p>`,parity:!0},{title:`two paragraphs`,input:e(t(n(`One`)),t(n(`Two`))),expected:`<p>One</p><p>Two</p>`,parity:!0},{title:`all six heading levels`,input:e(...[1,2,3,4,5,6].map(e=>({type:`heading`,attrs:{level:e},content:[n(`H${e}`)]}))),expected:`<h1>H1</h1><h2>H2</h2><h3>H3</h3><h4>H4</h4><h5>H5</h5><h6>H6</h6>`,parity:!0},{title:`invalid heading level falls back to h1`,input:e({type:`heading`,attrs:{level:9},content:[n(`Big`)]}),expected:`<h1>Big</h1>`,parity:!0},{title:`every simple mark`,input:e(t(n(`b`,[{type:`bold`}]),n(`i`,[{type:`italic`}]),n(`s`,[{type:`strike`}]),n(`u`,[{type:`underline`}]),n(`c`,[{type:`code`}]))),expected:`<p><strong>b</strong><em>i</em><s>s</s><u>u</u><code>c</code></p>`,parity:!0},{title:`nested marks fold with marks[0] outermost`,input:e(t(n(`x`,[{type:`bold`},{type:`italic`}]))),expected:`<p><strong><em>x</em></strong></p>`,parity:!0},{title:`adjacent nodes sharing an outer mark merge into one wrapper`,input:e(t(n(`a`,[{type:`bold`}]),n(`b`,[{type:`bold`},{type:`italic`}]))),expected:`<p><strong>a<em>b</em></strong></p>`,parity:!0},{title:`bullet list with paragraphs in items`,input:e({type:`bulletList`,content:[r(t(n(`One`))),r(t(n(`Two`)))]}),expected:`<ul><li><p>One</p></li><li><p>Two</p></li></ul>`,parity:!0},{title:`ordered list omits start=1`,input:e({type:`orderedList`,attrs:{start:1},content:[r(t(n(`One`)))]}),expected:`<ol><li><p>One</p></li></ol>`,parity:!0},{title:`ordered list emits start=3`,input:e({type:`orderedList`,attrs:{start:3},content:[r(t(n(`Three`)))]}),expected:`<ol start="3"><li><p>Three</p></li></ol>`,parity:!0},{title:`nested bullet list inside a list item`,input:e({type:`bulletList`,content:[r(t(n(`Outer`)),{type:`bulletList`,content:[r(t(n(`Inner`)))]})]}),expected:`<ul><li><p>Outer</p><ul><li><p>Inner</p></li></ul></li></ul>`,parity:!0},{title:`code block without language`,input:e({type:`codeBlock`,attrs:{language:null},content:[n(`const x = 1;`)]}),expected:`<pre><code>const x = 1;</code></pre>`,parity:!0},{title:`code block with language class`,input:e({type:`codeBlock`,attrs:{language:`js`},content:[n(`const x = 1;`)]}),expected:`<pre><code class="language-js">const x = 1;</code></pre>`,parity:!0},{title:`link with editor-default attrs`,input:e(t(n(`Visit`,[i]))),expected:`<p><a target="_blank" rel="noopener noreferrer nofollow" href="https://example.com">Visit</a></p>`,parity:!0},{title:`link spanning differently-marked text renders one anchor`,input:e(t(n(`go `,[i]),n(`bold`,[i,{type:`bold`}]),n(` now`,[i]))),expected:`<p><a target="_blank" rel="noopener noreferrer nofollow" href="https://example.com">go <strong>bold</strong> now</a></p>`,parity:!0},{title:`text escaping of angle brackets and ampersands`,input:e(t(n(`a < b & c > d`))),expected:`<p>a &lt; b &amp; c &gt; d</p>`,parity:!0},{title:`javascript: href is sanitized to empty (intentionally stricter than TipTap)`,input:e(t(n(`x`,[{type:`link`,attrs:{href:`javascript:alert(1)`}}]))),expected:`<p><a href="">x</a></p>`,parity:!1},{title:`unknown node types are skipped`,input:e({type:`schema`,attrs:{data:{}}},t(n(`kept`))),expected:`<p>kept</p>`,parity:!1}];exports.richTextFixtures=a;
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region src/test-utils/fixtures.ts
3
+ var doc = (...content) => ({
4
+ type: "doc",
5
+ content
6
+ });
7
+ var p = (...content) => ({
8
+ type: "paragraph",
9
+ content
10
+ });
11
+ var t = (text, marks) => ({
12
+ type: "text",
13
+ text,
14
+ ...marks ? { marks } : {}
15
+ });
16
+ var li = (...content) => ({
17
+ type: "listItem",
18
+ content
19
+ });
20
+ var link = {
21
+ type: "link",
22
+ attrs: {
23
+ href: "https://example.com",
24
+ target: "_blank",
25
+ rel: "noopener noreferrer nofollow",
26
+ class: null
27
+ }
28
+ };
29
+ /**
30
+ * Shared correctness corpus. Every renderer in every framework package must
31
+ * produce exactly these strings (DOM-roundtrip-normalized where the framework
32
+ * renders through a real DOM). `parity: true` fixtures are additionally
33
+ * asserted byte-identical to TipTap's `generateHTML`.
34
+ */
35
+ var richTextFixtures = [
36
+ {
37
+ title: "empty doc",
38
+ input: doc(),
39
+ expected: "",
40
+ parity: true
41
+ },
42
+ {
43
+ title: "null input",
44
+ input: null,
45
+ expected: "",
46
+ parity: false
47
+ },
48
+ {
49
+ title: "plain paragraph",
50
+ input: doc(p(t("Hello world"))),
51
+ expected: "<p>Hello world</p>",
52
+ parity: true
53
+ },
54
+ {
55
+ title: "two paragraphs",
56
+ input: doc(p(t("One")), p(t("Two"))),
57
+ expected: "<p>One</p><p>Two</p>",
58
+ parity: true
59
+ },
60
+ {
61
+ title: "all six heading levels",
62
+ input: doc(...[
63
+ 1,
64
+ 2,
65
+ 3,
66
+ 4,
67
+ 5,
68
+ 6
69
+ ].map((level) => ({
70
+ type: "heading",
71
+ attrs: { level },
72
+ content: [t(`H${level}`)]
73
+ }))),
74
+ expected: "<h1>H1</h1><h2>H2</h2><h3>H3</h3><h4>H4</h4><h5>H5</h5><h6>H6</h6>",
75
+ parity: true
76
+ },
77
+ {
78
+ title: "invalid heading level falls back to h1",
79
+ input: doc({
80
+ type: "heading",
81
+ attrs: { level: 9 },
82
+ content: [t("Big")]
83
+ }),
84
+ expected: "<h1>Big</h1>",
85
+ parity: true
86
+ },
87
+ {
88
+ title: "every simple mark",
89
+ input: doc(p(t("b", [{ type: "bold" }]), t("i", [{ type: "italic" }]), t("s", [{ type: "strike" }]), t("u", [{ type: "underline" }]), t("c", [{ type: "code" }]))),
90
+ expected: "<p><strong>b</strong><em>i</em><s>s</s><u>u</u><code>c</code></p>",
91
+ parity: true
92
+ },
93
+ {
94
+ title: "nested marks fold with marks[0] outermost",
95
+ input: doc(p(t("x", [{ type: "bold" }, { type: "italic" }]))),
96
+ expected: "<p><strong><em>x</em></strong></p>",
97
+ parity: true
98
+ },
99
+ {
100
+ title: "adjacent nodes sharing an outer mark merge into one wrapper",
101
+ input: doc(p(t("a", [{ type: "bold" }]), t("b", [{ type: "bold" }, { type: "italic" }]))),
102
+ expected: "<p><strong>a<em>b</em></strong></p>",
103
+ parity: true
104
+ },
105
+ {
106
+ title: "bullet list with paragraphs in items",
107
+ input: doc({
108
+ type: "bulletList",
109
+ content: [li(p(t("One"))), li(p(t("Two")))]
110
+ }),
111
+ expected: "<ul><li><p>One</p></li><li><p>Two</p></li></ul>",
112
+ parity: true
113
+ },
114
+ {
115
+ title: "ordered list omits start=1",
116
+ input: doc({
117
+ type: "orderedList",
118
+ attrs: { start: 1 },
119
+ content: [li(p(t("One")))]
120
+ }),
121
+ expected: "<ol><li><p>One</p></li></ol>",
122
+ parity: true
123
+ },
124
+ {
125
+ title: "ordered list emits start=3",
126
+ input: doc({
127
+ type: "orderedList",
128
+ attrs: { start: 3 },
129
+ content: [li(p(t("Three")))]
130
+ }),
131
+ expected: "<ol start=\"3\"><li><p>Three</p></li></ol>",
132
+ parity: true
133
+ },
134
+ {
135
+ title: "nested bullet list inside a list item",
136
+ input: doc({
137
+ type: "bulletList",
138
+ content: [li(p(t("Outer")), {
139
+ type: "bulletList",
140
+ content: [li(p(t("Inner")))]
141
+ })]
142
+ }),
143
+ expected: "<ul><li><p>Outer</p><ul><li><p>Inner</p></li></ul></li></ul>",
144
+ parity: true
145
+ },
146
+ {
147
+ title: "code block without language",
148
+ input: doc({
149
+ type: "codeBlock",
150
+ attrs: { language: null },
151
+ content: [t("const x = 1;")]
152
+ }),
153
+ expected: "<pre><code>const x = 1;</code></pre>",
154
+ parity: true
155
+ },
156
+ {
157
+ title: "code block with language class",
158
+ input: doc({
159
+ type: "codeBlock",
160
+ attrs: { language: "js" },
161
+ content: [t("const x = 1;")]
162
+ }),
163
+ expected: "<pre><code class=\"language-js\">const x = 1;</code></pre>",
164
+ parity: true
165
+ },
166
+ {
167
+ title: "link with editor-default attrs",
168
+ input: doc(p(t("Visit", [link]))),
169
+ expected: "<p><a target=\"_blank\" rel=\"noopener noreferrer nofollow\" href=\"https://example.com\">Visit</a></p>",
170
+ parity: true
171
+ },
172
+ {
173
+ title: "link spanning differently-marked text renders one anchor",
174
+ input: doc(p(t("go ", [link]), t("bold", [link, { type: "bold" }]), t(" now", [link]))),
175
+ expected: "<p><a target=\"_blank\" rel=\"noopener noreferrer nofollow\" href=\"https://example.com\">go <strong>bold</strong> now</a></p>",
176
+ parity: true
177
+ },
178
+ {
179
+ title: "text escaping of angle brackets and ampersands",
180
+ input: doc(p(t("a < b & c > d"))),
181
+ expected: "<p>a &lt; b &amp; c &gt; d</p>",
182
+ parity: true
183
+ },
184
+ {
185
+ title: "javascript: href is sanitized to empty (intentionally stricter than TipTap)",
186
+ input: doc(p(t("x", [{
187
+ type: "link",
188
+ attrs: { href: "javascript:alert(1)" }
189
+ }]))),
190
+ expected: "<p><a href=\"\">x</a></p>",
191
+ parity: false
192
+ },
193
+ {
194
+ title: "unknown node types are skipped",
195
+ input: doc({
196
+ type: "schema",
197
+ attrs: { data: {} }
198
+ }, p(t("kept"))),
199
+ expected: "<p>kept</p>",
200
+ parity: false
201
+ }
202
+ ];
203
+ //#endregion
204
+ exports.richTextFixtures = richTextFixtures;
@@ -1,18 +1,22 @@
1
1
  //#region src/test-utils/fixtures.ts
2
- var e = (...e) => ({
2
+ var doc = (...content) => ({
3
3
  type: "doc",
4
- content: e
5
- }), t = (...e) => ({
4
+ content
5
+ });
6
+ var p = (...content) => ({
6
7
  type: "paragraph",
7
- content: e
8
- }), n = (e, t) => ({
8
+ content
9
+ });
10
+ var t = (text, marks) => ({
9
11
  type: "text",
10
- text: e,
11
- ...t ? { marks: t } : {}
12
- }), r = (...e) => ({
12
+ text,
13
+ ...marks ? { marks } : {}
14
+ });
15
+ var li = (...content) => ({
13
16
  type: "listItem",
14
- content: e
15
- }), i = {
17
+ content
18
+ });
19
+ var link = {
16
20
  type: "link",
17
21
  attrs: {
18
22
  href: "https://example.com",
@@ -20,173 +24,180 @@ var e = (...e) => ({
20
24
  rel: "noopener noreferrer nofollow",
21
25
  class: null
22
26
  }
23
- }, a = [
27
+ };
28
+ /**
29
+ * Shared correctness corpus. Every renderer in every framework package must
30
+ * produce exactly these strings (DOM-roundtrip-normalized where the framework
31
+ * renders through a real DOM). `parity: true` fixtures are additionally
32
+ * asserted byte-identical to TipTap's `generateHTML`.
33
+ */
34
+ var richTextFixtures = [
24
35
  {
25
36
  title: "empty doc",
26
- input: e(),
37
+ input: doc(),
27
38
  expected: "",
28
- parity: !0
39
+ parity: true
29
40
  },
30
41
  {
31
42
  title: "null input",
32
43
  input: null,
33
44
  expected: "",
34
- parity: !1
45
+ parity: false
35
46
  },
36
47
  {
37
48
  title: "plain paragraph",
38
- input: e(t(n("Hello world"))),
49
+ input: doc(p(t("Hello world"))),
39
50
  expected: "<p>Hello world</p>",
40
- parity: !0
51
+ parity: true
41
52
  },
42
53
  {
43
54
  title: "two paragraphs",
44
- input: e(t(n("One")), t(n("Two"))),
55
+ input: doc(p(t("One")), p(t("Two"))),
45
56
  expected: "<p>One</p><p>Two</p>",
46
- parity: !0
57
+ parity: true
47
58
  },
48
59
  {
49
60
  title: "all six heading levels",
50
- input: e(...[
61
+ input: doc(...[
51
62
  1,
52
63
  2,
53
64
  3,
54
65
  4,
55
66
  5,
56
67
  6
57
- ].map((e) => ({
68
+ ].map((level) => ({
58
69
  type: "heading",
59
- attrs: { level: e },
60
- content: [n(`H${e}`)]
70
+ attrs: { level },
71
+ content: [t(`H${level}`)]
61
72
  }))),
62
73
  expected: "<h1>H1</h1><h2>H2</h2><h3>H3</h3><h4>H4</h4><h5>H5</h5><h6>H6</h6>",
63
- parity: !0
74
+ parity: true
64
75
  },
65
76
  {
66
77
  title: "invalid heading level falls back to h1",
67
- input: e({
78
+ input: doc({
68
79
  type: "heading",
69
80
  attrs: { level: 9 },
70
- content: [n("Big")]
81
+ content: [t("Big")]
71
82
  }),
72
83
  expected: "<h1>Big</h1>",
73
- parity: !0
84
+ parity: true
74
85
  },
75
86
  {
76
87
  title: "every simple mark",
77
- input: e(t(n("b", [{ type: "bold" }]), n("i", [{ type: "italic" }]), n("s", [{ type: "strike" }]), n("u", [{ type: "underline" }]), n("c", [{ type: "code" }]))),
88
+ input: doc(p(t("b", [{ type: "bold" }]), t("i", [{ type: "italic" }]), t("s", [{ type: "strike" }]), t("u", [{ type: "underline" }]), t("c", [{ type: "code" }]))),
78
89
  expected: "<p><strong>b</strong><em>i</em><s>s</s><u>u</u><code>c</code></p>",
79
- parity: !0
90
+ parity: true
80
91
  },
81
92
  {
82
93
  title: "nested marks fold with marks[0] outermost",
83
- input: e(t(n("x", [{ type: "bold" }, { type: "italic" }]))),
94
+ input: doc(p(t("x", [{ type: "bold" }, { type: "italic" }]))),
84
95
  expected: "<p><strong><em>x</em></strong></p>",
85
- parity: !0
96
+ parity: true
86
97
  },
87
98
  {
88
99
  title: "adjacent nodes sharing an outer mark merge into one wrapper",
89
- input: e(t(n("a", [{ type: "bold" }]), n("b", [{ type: "bold" }, { type: "italic" }]))),
100
+ input: doc(p(t("a", [{ type: "bold" }]), t("b", [{ type: "bold" }, { type: "italic" }]))),
90
101
  expected: "<p><strong>a<em>b</em></strong></p>",
91
- parity: !0
102
+ parity: true
92
103
  },
93
104
  {
94
105
  title: "bullet list with paragraphs in items",
95
- input: e({
106
+ input: doc({
96
107
  type: "bulletList",
97
- content: [r(t(n("One"))), r(t(n("Two")))]
108
+ content: [li(p(t("One"))), li(p(t("Two")))]
98
109
  }),
99
110
  expected: "<ul><li><p>One</p></li><li><p>Two</p></li></ul>",
100
- parity: !0
111
+ parity: true
101
112
  },
102
113
  {
103
114
  title: "ordered list omits start=1",
104
- input: e({
115
+ input: doc({
105
116
  type: "orderedList",
106
117
  attrs: { start: 1 },
107
- content: [r(t(n("One")))]
118
+ content: [li(p(t("One")))]
108
119
  }),
109
120
  expected: "<ol><li><p>One</p></li></ol>",
110
- parity: !0
121
+ parity: true
111
122
  },
112
123
  {
113
124
  title: "ordered list emits start=3",
114
- input: e({
125
+ input: doc({
115
126
  type: "orderedList",
116
127
  attrs: { start: 3 },
117
- content: [r(t(n("Three")))]
128
+ content: [li(p(t("Three")))]
118
129
  }),
119
130
  expected: "<ol start=\"3\"><li><p>Three</p></li></ol>",
120
- parity: !0
131
+ parity: true
121
132
  },
122
133
  {
123
134
  title: "nested bullet list inside a list item",
124
- input: e({
135
+ input: doc({
125
136
  type: "bulletList",
126
- content: [r(t(n("Outer")), {
137
+ content: [li(p(t("Outer")), {
127
138
  type: "bulletList",
128
- content: [r(t(n("Inner")))]
139
+ content: [li(p(t("Inner")))]
129
140
  })]
130
141
  }),
131
142
  expected: "<ul><li><p>Outer</p><ul><li><p>Inner</p></li></ul></li></ul>",
132
- parity: !0
143
+ parity: true
133
144
  },
134
145
  {
135
146
  title: "code block without language",
136
- input: e({
147
+ input: doc({
137
148
  type: "codeBlock",
138
149
  attrs: { language: null },
139
- content: [n("const x = 1;")]
150
+ content: [t("const x = 1;")]
140
151
  }),
141
152
  expected: "<pre><code>const x = 1;</code></pre>",
142
- parity: !0
153
+ parity: true
143
154
  },
144
155
  {
145
156
  title: "code block with language class",
146
- input: e({
157
+ input: doc({
147
158
  type: "codeBlock",
148
159
  attrs: { language: "js" },
149
- content: [n("const x = 1;")]
160
+ content: [t("const x = 1;")]
150
161
  }),
151
162
  expected: "<pre><code class=\"language-js\">const x = 1;</code></pre>",
152
- parity: !0
163
+ parity: true
153
164
  },
154
165
  {
155
166
  title: "link with editor-default attrs",
156
- input: e(t(n("Visit", [i]))),
167
+ input: doc(p(t("Visit", [link]))),
157
168
  expected: "<p><a target=\"_blank\" rel=\"noopener noreferrer nofollow\" href=\"https://example.com\">Visit</a></p>",
158
- parity: !0
169
+ parity: true
159
170
  },
160
171
  {
161
172
  title: "link spanning differently-marked text renders one anchor",
162
- input: e(t(n("go ", [i]), n("bold", [i, { type: "bold" }]), n(" now", [i]))),
173
+ input: doc(p(t("go ", [link]), t("bold", [link, { type: "bold" }]), t(" now", [link]))),
163
174
  expected: "<p><a target=\"_blank\" rel=\"noopener noreferrer nofollow\" href=\"https://example.com\">go <strong>bold</strong> now</a></p>",
164
- parity: !0
175
+ parity: true
165
176
  },
166
177
  {
167
178
  title: "text escaping of angle brackets and ampersands",
168
- input: e(t(n("a < b & c > d"))),
179
+ input: doc(p(t("a < b & c > d"))),
169
180
  expected: "<p>a &lt; b &amp; c &gt; d</p>",
170
- parity: !0
181
+ parity: true
171
182
  },
172
183
  {
173
184
  title: "javascript: href is sanitized to empty (intentionally stricter than TipTap)",
174
- input: e(t(n("x", [{
185
+ input: doc(p(t("x", [{
175
186
  type: "link",
176
187
  attrs: { href: "javascript:alert(1)" }
177
188
  }]))),
178
189
  expected: "<p><a href=\"\">x</a></p>",
179
- parity: !1
190
+ parity: false
180
191
  },
181
192
  {
182
193
  title: "unknown node types are skipped",
183
- input: e({
194
+ input: doc({
184
195
  type: "schema",
185
196
  attrs: { data: {} }
186
- }, t(n("kept"))),
197
+ }, p(t("kept"))),
187
198
  expected: "<p>kept</p>",
188
- parity: !1
199
+ parity: false
189
200
  }
190
201
  ];
191
202
  //#endregion
192
- export { a as richTextFixtures };
203
+ export { richTextFixtures };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@localess/richtext",
3
- "version": "3.4.1-dev.20260831165456",
3
+ "version": "3.4.1-dev.20260901200805",
4
4
  "description": "Framework-neutral rich text model and renderer for Localess's TipTap JSON content.",
5
5
  "keywords": [
6
6
  "localess",