@localess/richtext 3.4.1-dev.20260831165456
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/SKILL.md +75 -0
- package/dist/attrs.d.ts +10 -0
- package/dist/escape.d.ts +14 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +1 -0
- package/dist/index.mjs +236 -0
- package/dist/marks.d.ts +24 -0
- package/dist/model.d.ts +109 -0
- package/dist/normalize.d.ts +11 -0
- package/dist/render-html.d.ts +11 -0
- package/dist/render-map.d.ts +18 -0
- package/dist/test-utils/fixtures.d.ts +14 -0
- package/dist/test-utils/index.d.ts +1 -0
- package/dist/test-utils/index.js +1 -0
- package/dist/test-utils/index.mjs +192 -0
- package/package.json +75 -0
package/SKILL.md
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: localess-richtext
|
|
3
|
+
description: Framework-neutral rich text model and renderer for Localess TipTap JSON content. Zero dependencies. Use when rendering Localess RICH_TEXT fields to HTML or building a framework-specific rich text walker.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# @localess/richtext
|
|
7
|
+
|
|
8
|
+
Renders Localess rich text field values (TipTap/ProseMirror JSON produced by
|
|
9
|
+
the Localess Studio editor) without TipTap at runtime. Zero production
|
|
10
|
+
dependencies; safe in browsers, SSR, and edge runtimes.
|
|
11
|
+
|
|
12
|
+
Framework packages (`@localess/react`, `@localess/vue`, `@localess/svelte`,
|
|
13
|
+
`@localess/astro`, `@localess/angular`) ship idiomatic wrappers — prefer those
|
|
14
|
+
in app code. Use this package directly for custom pipelines (emails, static
|
|
15
|
+
generation, other frameworks).
|
|
16
|
+
|
|
17
|
+
## Render to HTML
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { renderRichTextToHtml } from '@localess/richtext';
|
|
21
|
+
|
|
22
|
+
const html = renderRichTextToHtml(data.body);
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
- Input: `LocalessRichTextInput` — a `doc`, a node, a node array, `null`, or
|
|
26
|
+
`@localess/client`'s `ContentRichText` (structurally compatible, no cast).
|
|
27
|
+
- Output is byte-identical to TipTap's `generateHTML` for the Studio's
|
|
28
|
+
extension set, except link `href`s pass a protocol allowlist
|
|
29
|
+
(`http:`/`https:`/`mailto:`/`tel:`/scheme-less); `javascript:`/`data:`
|
|
30
|
+
hrefs become `""`.
|
|
31
|
+
- Never throws; malformed input renders `''`.
|
|
32
|
+
|
|
33
|
+
## Supported node set
|
|
34
|
+
|
|
35
|
+
Nodes: `doc`, `paragraph`, `heading` (1–6), `bulletList`, `orderedList`
|
|
36
|
+
(`start`), `listItem`, `codeBlock` (`language` → `class="language-x"` on
|
|
37
|
+
`<code>`), `text`. Marks: `bold` → `<strong>`, `italic` → `<em>`,
|
|
38
|
+
`strike` → `<s>`, `underline` → `<u>`, `code` → `<code>`, `link` → `<a>`.
|
|
39
|
+
|
|
40
|
+
Unknown types are skipped with a dev-only warning unless a custom renderer is
|
|
41
|
+
provided for that type string.
|
|
42
|
+
|
|
43
|
+
## Custom renderers
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
renderRichTextToHtml(data.body, {
|
|
47
|
+
renderers: {
|
|
48
|
+
heading: ({ attrs, children }) => `<h${attrs.level} class="title">${children}</h${attrs.level}>`,
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
`children` arrives pre-rendered. `props.context.renderers` has the current
|
|
54
|
+
type unset — pass it to a nested `renderRichTextToHtml` call to re-render your
|
|
55
|
+
own node without infinite recursion.
|
|
56
|
+
|
|
57
|
+
## Building a native walker
|
|
58
|
+
|
|
59
|
+
The helpers encode the algorithms once so walkers are mechanical translations:
|
|
60
|
+
`normalizeInput(input, { withKeys: true })` (keyed node list),
|
|
61
|
+
`buildMarkTree(textRun)` (adjacent-mark merging), `processAttrs(type, attrs,
|
|
62
|
+
{ attrMap })` (attribute normalization; React passes `{ class: 'className' }`),
|
|
63
|
+
`NODE_RENDER_MAP` / `MARK_RENDER_MAP` / `resolveHeadingTag` (default table).
|
|
64
|
+
See `@localess/react`'s `src/core/richtext.ts` for the reference walker.
|
|
65
|
+
|
|
66
|
+
## Test fixtures
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
import { richTextFixtures } from '@localess/richtext/test-utils';
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`{ title, input, expected, parity }` corpus asserted by every Localess
|
|
73
|
+
renderer. `parity: true` fixtures are additionally byte-compared to TipTap's
|
|
74
|
+
`generateHTML` — parity is normative; never weaken an assertion to
|
|
75
|
+
`toContain`.
|
package/dist/attrs.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface ProcessAttrsOptions {
|
|
2
|
+
/** Per-framework attribute renames, e.g. `{ class: 'className' }` for React. */
|
|
3
|
+
attrMap?: Record<string, string>;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Normalizes a node/mark's stored attrs into the attributes to emit, in the
|
|
7
|
+
* order TipTap's `generateHTML` emits them (parity-tested — adjust order here
|
|
8
|
+
* and in the fixtures together if the parity test disagrees).
|
|
9
|
+
*/
|
|
10
|
+
export declare function processAttrs(type: string, attrs: Record<string, any> | undefined, options?: ProcessAttrsOptions): Record<string, any>;
|
package/dist/escape.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Escapes text content for safe HTML output. The escape set (`& < >`) matches
|
|
3
|
+
* TipTap's `generateHTML` DOM serialization — parity-tested; do not widen it
|
|
4
|
+
* without updating the parity fixtures.
|
|
5
|
+
*/
|
|
6
|
+
export declare function escapeHtml(text: string): string;
|
|
7
|
+
/** Escapes an attribute value for safe double-quoted HTML output (`& " < >`). */
|
|
8
|
+
export declare function escapeAttr(value: string): string;
|
|
9
|
+
/**
|
|
10
|
+
* Allowlist URL sanitizer for link hrefs: `http:`, `https:`, `mailto:`, `tel:`
|
|
11
|
+
* and scheme-less (relative/protocol-relative/fragment/query) URLs pass;
|
|
12
|
+
* everything else (e.g. `javascript:`, `data:`) becomes `''`.
|
|
13
|
+
*/
|
|
14
|
+
export declare function sanitizeUrl(url: string): string;
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});var e={"&":`&`,"<":`<`,">":`>`},t={...e,'"':`"`};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;
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
//#region src/escape.ts
|
|
2
|
+
var e = {
|
|
3
|
+
"&": "&",
|
|
4
|
+
"<": "<",
|
|
5
|
+
">": ">"
|
|
6
|
+
}, t = {
|
|
7
|
+
...e,
|
|
8
|
+
"\"": """
|
|
9
|
+
};
|
|
10
|
+
function n(t) {
|
|
11
|
+
return t.replace(/[&<>]/g, (t) => e[t]);
|
|
12
|
+
}
|
|
13
|
+
function r(e) {
|
|
14
|
+
return e.replace(/[&"<>]/g, (e) => t[e]);
|
|
15
|
+
}
|
|
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 : "";
|
|
20
|
+
}
|
|
21
|
+
//#endregion
|
|
22
|
+
//#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);
|
|
26
|
+
};
|
|
27
|
+
if (!t) return r;
|
|
28
|
+
switch (e) {
|
|
29
|
+
case "orderedList":
|
|
30
|
+
t.start !== null && t.start !== void 0 && t.start !== 1 && a("start", t.start);
|
|
31
|
+
break;
|
|
32
|
+
case "codeBlock":
|
|
33
|
+
t.language && a("class", `language-${t.language}`);
|
|
34
|
+
break;
|
|
35
|
+
case "link": a("target", t.target), a("rel", t.rel), r[i("href")] = o(String(t.href ?? "")), a("class", t.class);
|
|
36
|
+
}
|
|
37
|
+
return r;
|
|
38
|
+
}
|
|
39
|
+
//#endregion
|
|
40
|
+
//#region src/marks.ts
|
|
41
|
+
function c(e, t) {
|
|
42
|
+
return e.type === t.type && JSON.stringify(e.attrs ?? {}) === JSON.stringify(t.attrs ?? {});
|
|
43
|
+
}
|
|
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 = {
|
|
52
|
+
kind: "mark",
|
|
53
|
+
mark: e[r],
|
|
54
|
+
children: []
|
|
55
|
+
};
|
|
56
|
+
(n.length > 0 ? n[n.length - 1].children : t).push(i), n.push(i);
|
|
57
|
+
}
|
|
58
|
+
(n.length > 0 ? n[n.length - 1].children : t).push({
|
|
59
|
+
kind: "text",
|
|
60
|
+
text: r.text
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
return t;
|
|
64
|
+
}
|
|
65
|
+
//#endregion
|
|
66
|
+
//#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;
|
|
70
|
+
}
|
|
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]}`
|
|
77
|
+
};
|
|
78
|
+
return Array.isArray(n.content) && (n.content = d(n.content, t)), n;
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
//#endregion
|
|
82
|
+
//#region src/render-map.ts
|
|
83
|
+
var f = [
|
|
84
|
+
1,
|
|
85
|
+
2,
|
|
86
|
+
3,
|
|
87
|
+
4,
|
|
88
|
+
5,
|
|
89
|
+
6
|
|
90
|
+
];
|
|
91
|
+
function p(e) {
|
|
92
|
+
let t = e?.level;
|
|
93
|
+
return `h${f.includes(t) ? t : 1}`;
|
|
94
|
+
}
|
|
95
|
+
var m = {
|
|
96
|
+
doc: null,
|
|
97
|
+
text: null,
|
|
98
|
+
paragraph: {
|
|
99
|
+
tag: "p",
|
|
100
|
+
content: !0
|
|
101
|
+
},
|
|
102
|
+
heading: {
|
|
103
|
+
resolve: p,
|
|
104
|
+
content: !0
|
|
105
|
+
},
|
|
106
|
+
bulletList: {
|
|
107
|
+
tag: "ul",
|
|
108
|
+
content: !0
|
|
109
|
+
},
|
|
110
|
+
orderedList: {
|
|
111
|
+
tag: "ol",
|
|
112
|
+
content: !0
|
|
113
|
+
},
|
|
114
|
+
listItem: {
|
|
115
|
+
tag: "li",
|
|
116
|
+
content: !0
|
|
117
|
+
},
|
|
118
|
+
codeBlock: {
|
|
119
|
+
tag: "pre",
|
|
120
|
+
children: [{
|
|
121
|
+
tag: "code",
|
|
122
|
+
content: !0
|
|
123
|
+
}]
|
|
124
|
+
}
|
|
125
|
+
}, h = {
|
|
126
|
+
bold: {
|
|
127
|
+
tag: "strong",
|
|
128
|
+
content: !0
|
|
129
|
+
},
|
|
130
|
+
italic: {
|
|
131
|
+
tag: "em",
|
|
132
|
+
content: !0
|
|
133
|
+
},
|
|
134
|
+
strike: {
|
|
135
|
+
tag: "s",
|
|
136
|
+
content: !0
|
|
137
|
+
},
|
|
138
|
+
underline: {
|
|
139
|
+
tag: "u",
|
|
140
|
+
content: !0
|
|
141
|
+
},
|
|
142
|
+
code: {
|
|
143
|
+
tag: "code",
|
|
144
|
+
content: !0
|
|
145
|
+
},
|
|
146
|
+
link: {
|
|
147
|
+
tag: "a",
|
|
148
|
+
content: !0
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
//#endregion
|
|
152
|
+
//#region src/render-html.ts
|
|
153
|
+
function g(e, t = {}) {
|
|
154
|
+
return _(u(e), {
|
|
155
|
+
renderers: t.renderers,
|
|
156
|
+
warned: /* @__PURE__ */ new Set()
|
|
157
|
+
});
|
|
158
|
+
}
|
|
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++;
|
|
168
|
+
}
|
|
169
|
+
return n;
|
|
170
|
+
}
|
|
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 }
|
|
185
|
+
});
|
|
186
|
+
}
|
|
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);
|
|
197
|
+
}
|
|
198
|
+
return b(i.tag, {}, e);
|
|
199
|
+
}
|
|
200
|
+
return b(i.resolve ? i.resolve(e.attrs) : i.tag, a, o);
|
|
201
|
+
}
|
|
202
|
+
function y(e, t) {
|
|
203
|
+
let r = "";
|
|
204
|
+
for (let i of e) {
|
|
205
|
+
if (i.kind === "text") {
|
|
206
|
+
r += n(i.text);
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
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 }
|
|
215
|
+
});
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
let o = h[i.mark.type];
|
|
219
|
+
if (!o) {
|
|
220
|
+
x(t, i.mark.type), r += e;
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
r += b(o.tag, s(i.mark.type, i.mark.attrs), e);
|
|
224
|
+
}
|
|
225
|
+
return r;
|
|
226
|
+
}
|
|
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}>`;
|
|
231
|
+
}
|
|
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.`));
|
|
234
|
+
}
|
|
235
|
+
//#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 };
|
package/dist/marks.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { LocalessRichTextMark } from './model';
|
|
2
|
+
export interface MarkTreeText {
|
|
3
|
+
kind: 'text';
|
|
4
|
+
text: string;
|
|
5
|
+
}
|
|
6
|
+
export interface MarkTreeMark {
|
|
7
|
+
kind: 'mark';
|
|
8
|
+
mark: LocalessRichTextMark;
|
|
9
|
+
children: MarkTreeSegment[];
|
|
10
|
+
}
|
|
11
|
+
export type MarkTreeSegment = MarkTreeText | MarkTreeMark;
|
|
12
|
+
/** Deep equality of two marks (type + attrs). Attr key order must match, which holds for editor-produced documents. */
|
|
13
|
+
export declare function marksEqual(a: LocalessRichTextMark, b: LocalessRichTextMark): boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Folds a run of consecutive text nodes into a tree in which adjacent nodes
|
|
16
|
+
* sharing the same outer marks share one wrapper — the same merging
|
|
17
|
+
* ProseMirror's DOM serializer performs, so output matches TipTap's
|
|
18
|
+
* `generateHTML` (one `<a>` per link span, `<strong>a<em>b</em></strong>`
|
|
19
|
+
* instead of sibling `<strong>` wrappers).
|
|
20
|
+
*/
|
|
21
|
+
export declare function buildMarkTree(nodes: Array<{
|
|
22
|
+
text: string;
|
|
23
|
+
marks?: LocalessRichTextMark[];
|
|
24
|
+
}>): MarkTreeSegment[];
|
package/dist/model.d.ts
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Attributes of a `link` mark as stored by the Localess Studio editor
|
|
3
|
+
* (TipTap Link extension JSON).
|
|
4
|
+
*/
|
|
5
|
+
export interface LocalessRichTextLinkAttrs {
|
|
6
|
+
href: string;
|
|
7
|
+
target?: string | null;
|
|
8
|
+
rel?: string | null;
|
|
9
|
+
class?: string | null;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* A rich text mark — inline formatting applied to a `text` node.
|
|
13
|
+
* Exactly the mark set the Localess Studio editor can produce.
|
|
14
|
+
*/
|
|
15
|
+
export type LocalessRichTextMark = {
|
|
16
|
+
type: 'bold';
|
|
17
|
+
} | {
|
|
18
|
+
type: 'italic';
|
|
19
|
+
} | {
|
|
20
|
+
type: 'strike';
|
|
21
|
+
} | {
|
|
22
|
+
type: 'underline';
|
|
23
|
+
} | {
|
|
24
|
+
type: 'code';
|
|
25
|
+
} | {
|
|
26
|
+
type: 'link';
|
|
27
|
+
attrs: LocalessRichTextLinkAttrs;
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* A rich text node — exactly the node set the Localess Studio editor can produce.
|
|
31
|
+
*/
|
|
32
|
+
export type LocalessRichTextNode = {
|
|
33
|
+
type: 'paragraph';
|
|
34
|
+
content?: LocalessRichTextNode[];
|
|
35
|
+
} | {
|
|
36
|
+
type: 'heading';
|
|
37
|
+
attrs: {
|
|
38
|
+
level: 1 | 2 | 3 | 4 | 5 | 6;
|
|
39
|
+
};
|
|
40
|
+
content?: LocalessRichTextNode[];
|
|
41
|
+
} | {
|
|
42
|
+
type: 'bulletList';
|
|
43
|
+
content?: LocalessRichTextNode[];
|
|
44
|
+
} | {
|
|
45
|
+
type: 'orderedList';
|
|
46
|
+
attrs?: {
|
|
47
|
+
start?: number;
|
|
48
|
+
};
|
|
49
|
+
content?: LocalessRichTextNode[];
|
|
50
|
+
} | {
|
|
51
|
+
type: 'listItem';
|
|
52
|
+
content?: LocalessRichTextNode[];
|
|
53
|
+
} | {
|
|
54
|
+
type: 'codeBlock';
|
|
55
|
+
attrs?: {
|
|
56
|
+
language?: string | null;
|
|
57
|
+
};
|
|
58
|
+
content?: LocalessRichTextNode[];
|
|
59
|
+
} | {
|
|
60
|
+
type: 'text';
|
|
61
|
+
text: string;
|
|
62
|
+
marks?: LocalessRichTextMark[];
|
|
63
|
+
};
|
|
64
|
+
/** A node with an optional stable key injected by `normalizeInput(input, { withKeys: true })`. */
|
|
65
|
+
export type LocalessRichTextNodeWithKey = LocalessRichTextNode & {
|
|
66
|
+
_key?: string;
|
|
67
|
+
};
|
|
68
|
+
/** The root document node (`getJSON()` output of the Studio editor). */
|
|
69
|
+
export interface LocalessRichTextDocument {
|
|
70
|
+
type: 'doc';
|
|
71
|
+
content?: LocalessRichTextNode[];
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Structural stand-in for `@localess/client`'s `ContentRichText` so client
|
|
75
|
+
* values pass without casting. Deliberately not imported — this package has
|
|
76
|
+
* zero dependencies.
|
|
77
|
+
*/
|
|
78
|
+
export interface ContentRichTextLike {
|
|
79
|
+
type?: string;
|
|
80
|
+
content?: ContentRichTextLike[];
|
|
81
|
+
}
|
|
82
|
+
/** Anything a render function accepts. */
|
|
83
|
+
export type LocalessRichTextInput = LocalessRichTextDocument | LocalessRichTextNode | LocalessRichTextNode[] | ContentRichTextLike | null | undefined;
|
|
84
|
+
/** Union of every known node and mark type name. */
|
|
85
|
+
export type LocalessRichTextElement = LocalessRichTextNode['type'] | LocalessRichTextMark['type'] | 'doc';
|
|
86
|
+
/**
|
|
87
|
+
* Props passed to a custom renderer. `children` is the pre-rendered content of
|
|
88
|
+
* the node/mark; `context.renderers` is the override map with the current
|
|
89
|
+
* type unset (loop prevention) for safe re-entrant rendering.
|
|
90
|
+
*/
|
|
91
|
+
export interface LocalessRichTextRendererProps<TOut> {
|
|
92
|
+
type: string;
|
|
93
|
+
attrs?: Record<string, any>;
|
|
94
|
+
text?: string;
|
|
95
|
+
marks?: LocalessRichTextMark[];
|
|
96
|
+
content?: LocalessRichTextNode[];
|
|
97
|
+
children: TOut;
|
|
98
|
+
context: {
|
|
99
|
+
renderers?: LocalessRichTextRenderers<TOut>;
|
|
100
|
+
};
|
|
101
|
+
_key?: string;
|
|
102
|
+
}
|
|
103
|
+
/** A custom renderer for one node or mark type. */
|
|
104
|
+
export type LocalessRichTextRenderer<TOut> = (props: LocalessRichTextRendererProps<TOut>) => TOut;
|
|
105
|
+
/**
|
|
106
|
+
* Override map: node/mark type name → custom renderer. Unknown type names are
|
|
107
|
+
* allowed — that is the forward-compat seam for future node types.
|
|
108
|
+
*/
|
|
109
|
+
export type LocalessRichTextRenderers<TOut> = Record<string, LocalessRichTextRenderer<TOut> | undefined>;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { LocalessRichTextInput, LocalessRichTextNodeWithKey } from './model';
|
|
2
|
+
export interface NormalizeInputOptions {
|
|
3
|
+
/** Inject recursive `_key` values (`paragraph-1`, `text-3`, …) for keyed renderers. */
|
|
4
|
+
withKeys?: boolean;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Flattens any accepted rich text input (document, node, node array, or the
|
|
8
|
+
* loose `ContentRichText` shape from `@localess/client`) into a node list.
|
|
9
|
+
* Never throws; malformed input yields `[]`.
|
|
10
|
+
*/
|
|
11
|
+
export declare function normalizeInput(input: LocalessRichTextInput, options?: NormalizeInputOptions): LocalessRichTextNodeWithKey[];
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { LocalessRichTextInput, LocalessRichTextRenderers } from './model';
|
|
2
|
+
export interface LocalessRichTextHtmlOptions {
|
|
3
|
+
/** Per-type overrides. A custom renderer receives pre-rendered `children` and a loop-safe `context`. */
|
|
4
|
+
renderers?: LocalessRichTextRenderers<string>;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Renders Localess rich text JSON to an HTML string. Framework-neutral,
|
|
8
|
+
* dependency-free, and byte-compatible with TipTap's `generateHTML` for the
|
|
9
|
+
* node set the Localess Studio editor produces.
|
|
10
|
+
*/
|
|
11
|
+
export declare function renderRichTextToHtml(input: LocalessRichTextInput, options?: LocalessRichTextHtmlOptions): string;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Declarative default rendering for one node/mark type. */
|
|
2
|
+
export interface RichTextRenderSpec {
|
|
3
|
+
tag?: string;
|
|
4
|
+
/** Dynamic tag resolution (heading levels). Wins over `tag`. */
|
|
5
|
+
resolve?: (attrs: Record<string, any> | undefined) => string;
|
|
6
|
+
/** Render children inside this element. */
|
|
7
|
+
content?: boolean;
|
|
8
|
+
/** Static nested structure; node attrs attach to the child with `content: true`. */
|
|
9
|
+
children?: Array<{
|
|
10
|
+
tag: string;
|
|
11
|
+
content?: boolean;
|
|
12
|
+
}>;
|
|
13
|
+
}
|
|
14
|
+
/** Invalid levels fall back to h1, matching TipTap's first-configured-level behavior. */
|
|
15
|
+
export declare function resolveHeadingTag(attrs: Record<string, any> | undefined): string;
|
|
16
|
+
/** `null` = transparent (render children only, no element). Missing key = unknown type. */
|
|
17
|
+
export declare const NODE_RENDER_MAP: Record<string, RichTextRenderSpec | null>;
|
|
18
|
+
export declare const MARK_RENDER_MAP: Record<string, RichTextRenderSpec>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { LocalessRichTextInput } from '../model';
|
|
2
|
+
export interface RichTextFixture {
|
|
3
|
+
title: string;
|
|
4
|
+
input: LocalessRichTextInput;
|
|
5
|
+
expected: string;
|
|
6
|
+
parity: boolean;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Shared correctness corpus. Every renderer in every framework package must
|
|
10
|
+
* produce exactly these strings (DOM-roundtrip-normalized where the framework
|
|
11
|
+
* renders through a real DOM). `parity: true` fixtures are additionally
|
|
12
|
+
* asserted byte-identical to TipTap's `generateHTML`.
|
|
13
|
+
*/
|
|
14
|
+
export declare const richTextFixtures: RichTextFixture[];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './fixtures';
|
|
@@ -0,0 +1 @@
|
|
|
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 < b & c > 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;
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
//#region src/test-utils/fixtures.ts
|
|
2
|
+
var e = (...e) => ({
|
|
3
|
+
type: "doc",
|
|
4
|
+
content: e
|
|
5
|
+
}), t = (...e) => ({
|
|
6
|
+
type: "paragraph",
|
|
7
|
+
content: e
|
|
8
|
+
}), n = (e, t) => ({
|
|
9
|
+
type: "text",
|
|
10
|
+
text: e,
|
|
11
|
+
...t ? { marks: t } : {}
|
|
12
|
+
}), r = (...e) => ({
|
|
13
|
+
type: "listItem",
|
|
14
|
+
content: e
|
|
15
|
+
}), i = {
|
|
16
|
+
type: "link",
|
|
17
|
+
attrs: {
|
|
18
|
+
href: "https://example.com",
|
|
19
|
+
target: "_blank",
|
|
20
|
+
rel: "noopener noreferrer nofollow",
|
|
21
|
+
class: null
|
|
22
|
+
}
|
|
23
|
+
}, a = [
|
|
24
|
+
{
|
|
25
|
+
title: "empty doc",
|
|
26
|
+
input: e(),
|
|
27
|
+
expected: "",
|
|
28
|
+
parity: !0
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
title: "null input",
|
|
32
|
+
input: null,
|
|
33
|
+
expected: "",
|
|
34
|
+
parity: !1
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
title: "plain paragraph",
|
|
38
|
+
input: e(t(n("Hello world"))),
|
|
39
|
+
expected: "<p>Hello world</p>",
|
|
40
|
+
parity: !0
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
title: "two paragraphs",
|
|
44
|
+
input: e(t(n("One")), t(n("Two"))),
|
|
45
|
+
expected: "<p>One</p><p>Two</p>",
|
|
46
|
+
parity: !0
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
title: "all six heading levels",
|
|
50
|
+
input: e(...[
|
|
51
|
+
1,
|
|
52
|
+
2,
|
|
53
|
+
3,
|
|
54
|
+
4,
|
|
55
|
+
5,
|
|
56
|
+
6
|
|
57
|
+
].map((e) => ({
|
|
58
|
+
type: "heading",
|
|
59
|
+
attrs: { level: e },
|
|
60
|
+
content: [n(`H${e}`)]
|
|
61
|
+
}))),
|
|
62
|
+
expected: "<h1>H1</h1><h2>H2</h2><h3>H3</h3><h4>H4</h4><h5>H5</h5><h6>H6</h6>",
|
|
63
|
+
parity: !0
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
title: "invalid heading level falls back to h1",
|
|
67
|
+
input: e({
|
|
68
|
+
type: "heading",
|
|
69
|
+
attrs: { level: 9 },
|
|
70
|
+
content: [n("Big")]
|
|
71
|
+
}),
|
|
72
|
+
expected: "<h1>Big</h1>",
|
|
73
|
+
parity: !0
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
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" }]))),
|
|
78
|
+
expected: "<p><strong>b</strong><em>i</em><s>s</s><u>u</u><code>c</code></p>",
|
|
79
|
+
parity: !0
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
title: "nested marks fold with marks[0] outermost",
|
|
83
|
+
input: e(t(n("x", [{ type: "bold" }, { type: "italic" }]))),
|
|
84
|
+
expected: "<p><strong><em>x</em></strong></p>",
|
|
85
|
+
parity: !0
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
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" }]))),
|
|
90
|
+
expected: "<p><strong>a<em>b</em></strong></p>",
|
|
91
|
+
parity: !0
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
title: "bullet list with paragraphs in items",
|
|
95
|
+
input: e({
|
|
96
|
+
type: "bulletList",
|
|
97
|
+
content: [r(t(n("One"))), r(t(n("Two")))]
|
|
98
|
+
}),
|
|
99
|
+
expected: "<ul><li><p>One</p></li><li><p>Two</p></li></ul>",
|
|
100
|
+
parity: !0
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
title: "ordered list omits start=1",
|
|
104
|
+
input: e({
|
|
105
|
+
type: "orderedList",
|
|
106
|
+
attrs: { start: 1 },
|
|
107
|
+
content: [r(t(n("One")))]
|
|
108
|
+
}),
|
|
109
|
+
expected: "<ol><li><p>One</p></li></ol>",
|
|
110
|
+
parity: !0
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
title: "ordered list emits start=3",
|
|
114
|
+
input: e({
|
|
115
|
+
type: "orderedList",
|
|
116
|
+
attrs: { start: 3 },
|
|
117
|
+
content: [r(t(n("Three")))]
|
|
118
|
+
}),
|
|
119
|
+
expected: "<ol start=\"3\"><li><p>Three</p></li></ol>",
|
|
120
|
+
parity: !0
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
title: "nested bullet list inside a list item",
|
|
124
|
+
input: e({
|
|
125
|
+
type: "bulletList",
|
|
126
|
+
content: [r(t(n("Outer")), {
|
|
127
|
+
type: "bulletList",
|
|
128
|
+
content: [r(t(n("Inner")))]
|
|
129
|
+
})]
|
|
130
|
+
}),
|
|
131
|
+
expected: "<ul><li><p>Outer</p><ul><li><p>Inner</p></li></ul></li></ul>",
|
|
132
|
+
parity: !0
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
title: "code block without language",
|
|
136
|
+
input: e({
|
|
137
|
+
type: "codeBlock",
|
|
138
|
+
attrs: { language: null },
|
|
139
|
+
content: [n("const x = 1;")]
|
|
140
|
+
}),
|
|
141
|
+
expected: "<pre><code>const x = 1;</code></pre>",
|
|
142
|
+
parity: !0
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
title: "code block with language class",
|
|
146
|
+
input: e({
|
|
147
|
+
type: "codeBlock",
|
|
148
|
+
attrs: { language: "js" },
|
|
149
|
+
content: [n("const x = 1;")]
|
|
150
|
+
}),
|
|
151
|
+
expected: "<pre><code class=\"language-js\">const x = 1;</code></pre>",
|
|
152
|
+
parity: !0
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
title: "link with editor-default attrs",
|
|
156
|
+
input: e(t(n("Visit", [i]))),
|
|
157
|
+
expected: "<p><a target=\"_blank\" rel=\"noopener noreferrer nofollow\" href=\"https://example.com\">Visit</a></p>",
|
|
158
|
+
parity: !0
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
title: "link spanning differently-marked text renders one anchor",
|
|
162
|
+
input: e(t(n("go ", [i]), n("bold", [i, { type: "bold" }]), n(" now", [i]))),
|
|
163
|
+
expected: "<p><a target=\"_blank\" rel=\"noopener noreferrer nofollow\" href=\"https://example.com\">go <strong>bold</strong> now</a></p>",
|
|
164
|
+
parity: !0
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
title: "text escaping of angle brackets and ampersands",
|
|
168
|
+
input: e(t(n("a < b & c > d"))),
|
|
169
|
+
expected: "<p>a < b & c > d</p>",
|
|
170
|
+
parity: !0
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
title: "javascript: href is sanitized to empty (intentionally stricter than TipTap)",
|
|
174
|
+
input: e(t(n("x", [{
|
|
175
|
+
type: "link",
|
|
176
|
+
attrs: { href: "javascript:alert(1)" }
|
|
177
|
+
}]))),
|
|
178
|
+
expected: "<p><a href=\"\">x</a></p>",
|
|
179
|
+
parity: !1
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
title: "unknown node types are skipped",
|
|
183
|
+
input: e({
|
|
184
|
+
type: "schema",
|
|
185
|
+
attrs: { data: {} }
|
|
186
|
+
}, t(n("kept"))),
|
|
187
|
+
expected: "<p>kept</p>",
|
|
188
|
+
parity: !1
|
|
189
|
+
}
|
|
190
|
+
];
|
|
191
|
+
//#endregion
|
|
192
|
+
export { a as richTextFixtures };
|
package/package.json
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@localess/richtext",
|
|
3
|
+
"version": "3.4.1-dev.20260831165456",
|
|
4
|
+
"description": "Framework-neutral rich text model and renderer for Localess's TipTap JSON content.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"localess",
|
|
7
|
+
"sdk",
|
|
8
|
+
"richtext",
|
|
9
|
+
"tiptap",
|
|
10
|
+
"javascript",
|
|
11
|
+
"typescript"
|
|
12
|
+
],
|
|
13
|
+
"author": "Lessify",
|
|
14
|
+
"homepage": "https://github.com/Lessify/localess-js",
|
|
15
|
+
"sideEffects": false,
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"SKILL.md"
|
|
19
|
+
],
|
|
20
|
+
"main": "dist/index.js",
|
|
21
|
+
"module": "dist/index.mjs",
|
|
22
|
+
"types": "dist/index.d.ts",
|
|
23
|
+
"exports": {
|
|
24
|
+
".": {
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"import": "./dist/index.mjs",
|
|
27
|
+
"require": "./dist/index.js"
|
|
28
|
+
},
|
|
29
|
+
"./test-utils": {
|
|
30
|
+
"types": "./dist/test-utils/index.d.ts",
|
|
31
|
+
"import": "./dist/test-utils/index.mjs",
|
|
32
|
+
"require": "./dist/test-utils/index.js"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "git+https://github.com/Lessify/localess-js.git",
|
|
38
|
+
"directory": "packages/richtext"
|
|
39
|
+
},
|
|
40
|
+
"bugs": {
|
|
41
|
+
"url": "https://github.com/Lessify/localess-js/issues"
|
|
42
|
+
},
|
|
43
|
+
"scripts": {
|
|
44
|
+
"build": "vite build",
|
|
45
|
+
"test": "vitest run",
|
|
46
|
+
"test:watch": "vitest",
|
|
47
|
+
"test:coverage": "vitest run --coverage"
|
|
48
|
+
},
|
|
49
|
+
"license": "MIT",
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@tiptap/extension-bold": "^3.22.5",
|
|
52
|
+
"@tiptap/extension-bullet-list": "^3.22.5",
|
|
53
|
+
"@tiptap/extension-code": "^3.22.5",
|
|
54
|
+
"@tiptap/extension-code-block-lowlight": "^3.22.5",
|
|
55
|
+
"@tiptap/extension-document": "^3.22.5",
|
|
56
|
+
"@tiptap/extension-heading": "^3.22.5",
|
|
57
|
+
"@tiptap/extension-history": "^3.22.5",
|
|
58
|
+
"@tiptap/extension-italic": "^3.22.5",
|
|
59
|
+
"@tiptap/extension-link": "^3.22.5",
|
|
60
|
+
"@tiptap/extension-list-item": "^3.22.5",
|
|
61
|
+
"@tiptap/extension-ordered-list": "^3.22.5",
|
|
62
|
+
"@tiptap/extension-paragraph": "^3.22.5",
|
|
63
|
+
"@tiptap/extension-strike": "^3.22.5",
|
|
64
|
+
"@tiptap/extension-text": "^3.22.5",
|
|
65
|
+
"@tiptap/extension-underline": "^3.22.5",
|
|
66
|
+
"@tiptap/html": "^3.22.5",
|
|
67
|
+
"@types/node": "^24",
|
|
68
|
+
"typescript": "^5.9.3",
|
|
69
|
+
"vite": "^8.0.16",
|
|
70
|
+
"vite-plugin-dts": "^5.0.0"
|
|
71
|
+
},
|
|
72
|
+
"engines": {
|
|
73
|
+
"node": ">= 24.0.0"
|
|
74
|
+
}
|
|
75
|
+
}
|