@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 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 } // walker helpers
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.20260929183905",
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.20260929183905"
61
+ "@localess/model": "4.0.2-dev.20260930092045"
62
62
  },
63
63
  "devDependencies": {
64
64
  "@tiptap/extension-bold": "^3.22.5",