@localess/richtext 4.0.2-dev.20260929183905 → 4.0.2-dev.20260930092045
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/README.md +4 -2
- package/SKILL.md +15 -2
- package/dist/attrs.d.ts +9 -0
- package/dist/index.js +18 -2
- package/dist/index.mjs +18 -3
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -49,13 +49,15 @@ const html = renderRichTextToHtml(content.data.content);
|
|
|
49
49
|
|
|
50
50
|
### Overriding a node type
|
|
51
51
|
|
|
52
|
-
Pass `renderers` to replace how one type is rendered — node types and mark types alike. A renderer receives the node's own `attrs`, `text` and `marks` plus its already-rendered `children`, so you only describe the wrapper:
|
|
52
|
+
Pass `renderers` to replace how one type is rendered — node types and mark types alike. A renderer receives the node's own `attrs`, `text` and `marks` plus its already-rendered `children`, so you only describe the wrapper. A custom `link` renderer receives an already-sanitized `href`; the returned string is trusted HTML, so escape what you interpolate (`escapeAttr` / `escapeHtml`):
|
|
53
53
|
|
|
54
54
|
```ts
|
|
55
|
+
import { escapeAttr, renderRichTextToHtml } from '@localess/richtext';
|
|
56
|
+
|
|
55
57
|
const html = renderRichTextToHtml(content.data.content, {
|
|
56
58
|
renderers: {
|
|
57
59
|
heading: ({ attrs, children }) => `<h${attrs?.level} class="font-bold">${children}</h${attrs?.level}>`,
|
|
58
|
-
link: ({ attrs, children }) => `<a href="${attrs?.href}" rel="noopener">${children}</a>`,
|
|
60
|
+
link: ({ attrs, children }) => `<a href="${escapeAttr(attrs?.href ?? '')}" rel="noopener">${children}</a>`,
|
|
59
61
|
},
|
|
60
62
|
});
|
|
61
63
|
```
|
package/SKILL.md
CHANGED
|
@@ -69,6 +69,9 @@ renderRichTextToHtml(data.body, {
|
|
|
69
69
|
`children` arrives pre-rendered. `props.context.renderers` has the current
|
|
70
70
|
type unset — pass it to a nested `renderRichTextToHtml` call to re-render your
|
|
71
71
|
own node without infinite recursion.
|
|
72
|
+
A custom `link` renderer receives `attrs.href` already passed through
|
|
73
|
+
`sanitizeUrl`. The returned string is trusted HTML, so escape what you
|
|
74
|
+
interpolate: `escapeAttr` for attribute values, `escapeHtml` for text.
|
|
72
75
|
|
|
73
76
|
Renderer props (`LocalessRichTextRendererProps<TOut>`): `type`, `attrs?`,
|
|
74
77
|
`text?`, `marks?`, `content?`, `children`, `context: { renderers? }`, `_key?`.
|
|
@@ -88,7 +91,9 @@ The helpers encode the algorithms once so walkers are mechanical translations:
|
|
|
88
91
|
{ attrMap })` (attribute normalization; React passes `{ class: 'className' }`),
|
|
89
92
|
`NODE_RENDER_MAP` / `MARK_RENDER_MAP` / `resolveHeadingTag` (default table;
|
|
90
93
|
`null` entry = transparent, missing key = unknown), `escapeHtml` /
|
|
91
|
-
`escapeAttr` / `sanitizeUrl` (escaping and URL allowlist)
|
|
94
|
+
`escapeAttr` / `sanitizeUrl` (escaping and URL allowlist), `sanitizeElement(element)`
|
|
95
|
+
(pass every node/mark through it before invoking a custom renderer, so overrides
|
|
96
|
+
never receive an unsanitized link `href`).
|
|
92
97
|
See `@localess/react`'s `src/core/richtext.ts` for the reference walker.
|
|
93
98
|
|
|
94
99
|
## Test fixtures
|
|
@@ -107,9 +112,10 @@ never weaken an assertion to `toContain`.
|
|
|
107
112
|
```typescript
|
|
108
113
|
// @localess/richtext
|
|
109
114
|
export { renderRichTextToHtml } // HTML string renderer
|
|
110
|
-
export { normalizeInput, buildMarkTree, marksEqual, processAttrs }
|
|
115
|
+
export { normalizeInput, buildMarkTree, marksEqual, processAttrs, sanitizeElement } // walker helpers
|
|
111
116
|
export { escapeHtml, escapeAttr, sanitizeUrl } // escaping / URL policy
|
|
112
117
|
export { NODE_RENDER_MAP, MARK_RENDER_MAP, resolveHeadingTag } // default render table
|
|
118
|
+
export { RichTextParseError } // also on the parser subpaths
|
|
113
119
|
export type {
|
|
114
120
|
LocalessRichTextDocument, LocalessRichTextNode, LocalessRichTextNodeWithKey,
|
|
115
121
|
LocalessRichTextMark, LocalessRichTextLinkAttrs, LocalessRichTextElement,
|
|
@@ -117,8 +123,15 @@ export type {
|
|
|
117
123
|
LocalessRichTextRenderer, LocalessRichTextRenderers, LocalessRichTextRendererProps,
|
|
118
124
|
LocalessRichTextHtmlOptions, NormalizeInputOptions, ProcessAttrsOptions,
|
|
119
125
|
RichTextRenderSpec, MarkTreeSegment, MarkTreeText, MarkTreeMark,
|
|
126
|
+
RichTextParseOptions, RichTextParseResult, RichTextUnsupportedPolicy, RichTextUnsupportedReport,
|
|
120
127
|
}
|
|
121
128
|
|
|
129
|
+
// @localess/richtext/html-parser, @localess/richtext/markdown-parser
|
|
130
|
+
export { parseHtmlToRichText } // html-parser
|
|
131
|
+
export { parseMarkdownToRichText } // markdown-parser
|
|
132
|
+
export { RichTextParseError }
|
|
133
|
+
export type { RichTextParseOptions, RichTextParseResult, RichTextUnsupportedPolicy, RichTextUnsupportedReport }
|
|
134
|
+
|
|
122
135
|
// @localess/richtext/test-utils
|
|
123
136
|
export { richTextFixtures }
|
|
124
137
|
export type { RichTextFixture }
|
package/dist/attrs.d.ts
CHANGED
|
@@ -8,3 +8,12 @@ export interface ProcessAttrsOptions {
|
|
|
8
8
|
* and in the fixtures together if the parity test disagrees).
|
|
9
9
|
*/
|
|
10
10
|
export declare function processAttrs(type: string, attrs: Record<string, any> | undefined, options?: ProcessAttrsOptions): Record<string, any>;
|
|
11
|
+
/**
|
|
12
|
+
* Returns the node/mark a custom renderer receives: for `link`, a copy whose `href` has passed
|
|
13
|
+
* {@link sanitizeUrl}; every other element is returned unchanged. Applied by every Localess
|
|
14
|
+
* renderer before invoking an override, so custom `link` renderers never see a raw href.
|
|
15
|
+
*/
|
|
16
|
+
export declare function sanitizeElement<T extends {
|
|
17
|
+
type: string;
|
|
18
|
+
attrs?: Record<string, any> | null;
|
|
19
|
+
}>(element: T): T;
|
package/dist/index.js
CHANGED
|
@@ -29,6 +29,21 @@ function processAttrs(type, attrs, options = {}) {
|
|
|
29
29
|
}
|
|
30
30
|
return out;
|
|
31
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* Returns the node/mark a custom renderer receives: for `link`, a copy whose `href` has passed
|
|
34
|
+
* {@link sanitizeUrl}; every other element is returned unchanged. Applied by every Localess
|
|
35
|
+
* renderer before invoking an override, so custom `link` renderers never see a raw href.
|
|
36
|
+
*/
|
|
37
|
+
function sanitizeElement(element) {
|
|
38
|
+
if (element.type !== "link" || !element.attrs || element.attrs.href === null || element.attrs.href === void 0) return element;
|
|
39
|
+
return {
|
|
40
|
+
...element,
|
|
41
|
+
attrs: {
|
|
42
|
+
...element.attrs,
|
|
43
|
+
href: require_parse_common.sanitizeUrl(String(element.attrs.href))
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
}
|
|
32
47
|
//#endregion
|
|
33
48
|
//#region src/marks.ts
|
|
34
49
|
/** Deep equality of two marks (type + attrs). Attr key order must match, which holds for editor-produced documents. */
|
|
@@ -211,7 +226,7 @@ function renderNode(node, ctx) {
|
|
|
211
226
|
};
|
|
212
227
|
const children = node.type === "text" ? require_parse_common.escapeHtml(node.text ?? "") : renderNodes(node.content ?? [], childCtx);
|
|
213
228
|
return custom({
|
|
214
|
-
...node,
|
|
229
|
+
...sanitizeElement(node),
|
|
215
230
|
children,
|
|
216
231
|
context: { renderers: childRenderers }
|
|
217
232
|
});
|
|
@@ -246,7 +261,7 @@ function renderSegments(segments, ctx) {
|
|
|
246
261
|
const custom = ctx.renderers?.[segment.mark.type];
|
|
247
262
|
if (custom) {
|
|
248
263
|
out += custom({
|
|
249
|
-
...segment.mark,
|
|
264
|
+
...sanitizeElement(segment.mark),
|
|
250
265
|
children,
|
|
251
266
|
context: { renderers: ctx.renderers }
|
|
252
267
|
});
|
|
@@ -287,4 +302,5 @@ exports.normalizeInput = normalizeInput;
|
|
|
287
302
|
exports.processAttrs = processAttrs;
|
|
288
303
|
exports.renderRichTextToHtml = renderRichTextToHtml;
|
|
289
304
|
exports.resolveHeadingTag = resolveHeadingTag;
|
|
305
|
+
exports.sanitizeElement = sanitizeElement;
|
|
290
306
|
exports.sanitizeUrl = require_parse_common.sanitizeUrl;
|
package/dist/index.mjs
CHANGED
|
@@ -28,6 +28,21 @@ function processAttrs(type, attrs, options = {}) {
|
|
|
28
28
|
}
|
|
29
29
|
return out;
|
|
30
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* Returns the node/mark a custom renderer receives: for `link`, a copy whose `href` has passed
|
|
33
|
+
* {@link sanitizeUrl}; every other element is returned unchanged. Applied by every Localess
|
|
34
|
+
* renderer before invoking an override, so custom `link` renderers never see a raw href.
|
|
35
|
+
*/
|
|
36
|
+
function sanitizeElement(element) {
|
|
37
|
+
if (element.type !== "link" || !element.attrs || element.attrs.href === null || element.attrs.href === void 0) return element;
|
|
38
|
+
return {
|
|
39
|
+
...element,
|
|
40
|
+
attrs: {
|
|
41
|
+
...element.attrs,
|
|
42
|
+
href: sanitizeUrl(String(element.attrs.href))
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
}
|
|
31
46
|
//#endregion
|
|
32
47
|
//#region src/marks.ts
|
|
33
48
|
/** Deep equality of two marks (type + attrs). Attr key order must match, which holds for editor-produced documents. */
|
|
@@ -210,7 +225,7 @@ function renderNode(node, ctx) {
|
|
|
210
225
|
};
|
|
211
226
|
const children = node.type === "text" ? escapeHtml(node.text ?? "") : renderNodes(node.content ?? [], childCtx);
|
|
212
227
|
return custom({
|
|
213
|
-
...node,
|
|
228
|
+
...sanitizeElement(node),
|
|
214
229
|
children,
|
|
215
230
|
context: { renderers: childRenderers }
|
|
216
231
|
});
|
|
@@ -245,7 +260,7 @@ function renderSegments(segments, ctx) {
|
|
|
245
260
|
const custom = ctx.renderers?.[segment.mark.type];
|
|
246
261
|
if (custom) {
|
|
247
262
|
out += custom({
|
|
248
|
-
...segment.mark,
|
|
263
|
+
...sanitizeElement(segment.mark),
|
|
249
264
|
children,
|
|
250
265
|
context: { renderers: ctx.renderers }
|
|
251
266
|
});
|
|
@@ -273,4 +288,4 @@ function warnUnknown(ctx, type) {
|
|
|
273
288
|
console.warn(`[@localess/richtext] Unknown rich text element "${type}" was skipped. Provide a custom renderer to handle it.`);
|
|
274
289
|
}
|
|
275
290
|
//#endregion
|
|
276
|
-
export { MARK_RENDER_MAP, NODE_RENDER_MAP, RichTextParseError, UnsupportedTracker, buildMarkTree, emptyDocument, escapeAttr, escapeHtml, marksEqual, normalizeInput, processAttrs, renderRichTextToHtml, resolveHeadingTag, sanitizeUrl };
|
|
291
|
+
export { MARK_RENDER_MAP, NODE_RENDER_MAP, RichTextParseError, UnsupportedTracker, buildMarkTree, emptyDocument, escapeAttr, escapeHtml, marksEqual, normalizeInput, processAttrs, renderRichTextToHtml, resolveHeadingTag, sanitizeElement, sanitizeUrl };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@localess/richtext",
|
|
3
|
-
"version": "4.0.2-dev.
|
|
3
|
+
"version": "4.0.2-dev.20260930092045",
|
|
4
4
|
"description": "Framework-neutral rich text model and renderer for Localess's TipTap JSON content.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"localess",
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
},
|
|
59
59
|
"license": "MIT",
|
|
60
60
|
"dependencies": {
|
|
61
|
-
"@localess/model": "4.0.2-dev.
|
|
61
|
+
"@localess/model": "4.0.2-dev.20260930092045"
|
|
62
62
|
},
|
|
63
63
|
"devDependencies": {
|
|
64
64
|
"@tiptap/extension-bold": "^3.22.5",
|