@haklex/rich-static-renderer 0.0.44 → 0.0.46

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
@@ -43,7 +43,15 @@ interface RichRendererProps {
43
43
  as?: keyof React.JSX.IntrinsicElements
44
44
  rendererConfig?: RendererConfig
45
45
  extraNodes?: Array<Klass<LexicalNode>>
46
+ builtinNodeOverrides?: Record<string, BuiltinNodeRenderer>
46
47
  }
48
+
49
+ type BuiltinNodeRenderer = (
50
+ node: any,
51
+ key: string,
52
+ children: ReactNode[] | null,
53
+ defaultRenderer: () => ReactNode,
54
+ ) => ReactNode
47
55
  ```
48
56
 
49
57
  字段说明:
@@ -52,6 +60,7 @@ interface RichRendererProps {
52
60
  - `as`:渲染容器标签,默认 `div`
53
61
  - `rendererConfig`:覆写某些节点的渲染组件(Image/CodeBlock/...)
54
62
  - `extraNodes`:注册扩展节点(如 Tldraw、Embed、Gallery、CodeSnippet)
63
+ - `builtinNodeOverrides`:覆写内置节点的渲染(paragraph/heading/link/...),按 `node.type` 匹配
55
64
 
56
65
  ## 与增强渲染器一起用
57
66
 
@@ -86,6 +95,32 @@ const extraNodes = [
86
95
  />
87
96
  ```
88
97
 
98
+ ## 覆写内置节点渲染
99
+
100
+ 通过 `builtinNodeOverrides` 可按 `node.type` 覆写 paragraph、heading、link 等内置节点的渲染逻辑。回调提供 `defaultRenderer` 用于回退默认渲染。
101
+
102
+ ```tsx
103
+ <RichRenderer
104
+ value={value}
105
+ builtinNodeOverrides={{
106
+ link: (node, key, children) => (
107
+ <a key={key} href={node.url} className="custom-link" target="_blank">
108
+ {children}
109
+ </a>
110
+ ),
111
+ heading: (node, key, children, defaultRenderer) => {
112
+ // 仅覆写 h1,其余回退默认
113
+ if (node.tag === 'h1') {
114
+ return <h1 key={key} className="custom-h1">{children}</h1>
115
+ }
116
+ return defaultRenderer()
117
+ },
118
+ }}
119
+ />
120
+ ```
121
+
122
+ 支持的 type:`root`、`paragraph`、`heading`、`quote`、`list`、`listitem`、`link`、`autolink`、`horizontalrule`、`table`、`tablerow`、`tablecell`、`details`、`spoiler`、`ruby`、`code`、`code-highlight`、`linebreak`、`tab`。
123
+
89
124
  ## 渲染行为说明
90
125
 
91
126
  - 内部使用 `@lexical/headless` 构建只读 editor,再将节点树转为 React
@@ -1,3 +1,3 @@
1
1
  import { RichRendererProps } from './types';
2
- export declare function RichRenderer({ value, variant, theme, className, style, as: Component, rendererConfig, extraNodes, }: RichRendererProps): import("react/jsx-runtime").JSX.Element;
2
+ export declare function RichRenderer({ value, variant, theme, className, style, as: Component, rendererConfig, extraNodes, builtinNodeOverrides, }: RichRendererProps): import("react/jsx-runtime").JSX.Element;
3
3
  //# sourceMappingURL=RichRenderer.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"RichRenderer.d.ts","sourceRoot":"","sources":["../src/RichRenderer.tsx"],"names":[],"mappings":"AAuBA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAA;AAyMhD,wBAAgB,YAAY,CAAC,EAC3B,KAAK,EACL,OAAmB,EACnB,KAAe,EACf,SAAS,EACT,KAAK,EACL,EAAE,EAAE,SAAiB,EACrB,cAAc,EACd,UAAU,GACX,EAAE,iBAAiB,2CAwCnB"}
1
+ {"version":3,"file":"RichRenderer.d.ts","sourceRoot":"","sources":["../src/RichRenderer.tsx"],"names":[],"mappings":"AAwBA,OAAO,KAAK,EAAuB,iBAAiB,EAAE,MAAM,SAAS,CAAA;AAkOrE,wBAAgB,YAAY,CAAC,EAC3B,KAAK,EACL,OAAmB,EACnB,KAAe,EACf,SAAS,EACT,KAAK,EACL,EAAE,EAAE,SAAiB,EACrB,cAAc,EACd,UAAU,EACV,oBAAoB,GACrB,EAAE,iBAAiB,2CAuCnB"}
@@ -1,3 +1,3 @@
1
1
  import { ReactNode } from 'react';
2
- export declare function renderBuiltinNode(node: any, key: string, children: ReactNode[] | null, headingSlugs: Map<string, number>): ReactNode;
2
+ export declare function renderBuiltinNode(node: any, key: string, children: ReactNode[] | null, headingSlugs: Map<string, number>, textContent?: string): ReactNode;
3
3
  //# sourceMappingURL=renderBuiltinNode.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"renderBuiltinNode.d.ts","sourceRoot":"","sources":["../../src/engine/renderBuiltinNode.tsx"],"names":[],"mappings":"AACA,OAAO,EAAiB,KAAK,SAAS,EAAE,MAAM,OAAO,CAAA;AAmBrD,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,GAAG,EACT,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,SAAS,EAAE,GAAG,IAAI,EAC5B,YAAY,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAChC,SAAS,CAkMX"}
1
+ {"version":3,"file":"renderBuiltinNode.d.ts","sourceRoot":"","sources":["../../src/engine/renderBuiltinNode.tsx"],"names":[],"mappings":"AAKA,OAAO,EAAiB,KAAK,SAAS,EAAE,MAAM,OAAO,CAAA;AAmBrD,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,GAAG,EACT,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,SAAS,EAAE,GAAG,IAAI,EAC5B,YAAY,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,EACjC,WAAW,CAAC,EAAE,MAAM,GACnB,SAAS,CA+LX"}
package/dist/index.d.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  export { RichRenderer } from './RichRenderer';
2
- export type { RichRendererProps } from './types';
2
+ export type { BuiltinNodeRenderer, RichRendererProps } from './types';
3
3
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAC7C,YAAY,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAC7C,YAAY,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAA"}
package/dist/index.mjs CHANGED
@@ -1,22 +1,22 @@
1
1
  import { jsx, jsxs, Fragment } from "react/jsx-runtime";
2
- import { RendererWrapper, RubyRenderer, getVariantClass, allNodes, ColorSchemeProvider, RendererConfigProvider, FootnoteDefinitionsProvider, NestedContentRendererProvider, editorTheme } from "@haklex/rich-editor/static";
2
+ import { RendererWrapper, RubyRenderer, LinkFavicon, getVariantClass, allNodes, ColorSchemeProvider, RendererConfigProvider, FootnoteDefinitionsProvider, NestedContentRendererProvider, editorTheme } from "@haklex/rich-editor/static";
3
3
  import { PortalThemeProvider } from "@haklex/rich-style-token";
4
4
  import { createHeadlessEditor } from "@lexical/headless";
5
5
  import { $getRoot } from "lexical";
6
- import { createElement, isValidElement, cloneElement } from "react";
6
+ import { createElement, useMemo, isValidElement, cloneElement } from "react";
7
7
  var tableWrapper = "_1v9yxw30";
8
8
  var table = "_1v9yxw31";
9
9
  var tableHead = "_1v9yxw32";
10
10
  var tableCell = "_1v9yxw33";
11
11
  function textToSlug(text) {
12
- return text.toLowerCase().trim().replaceAll(/[^\w\s\u3001-\u9fff\uac00-\ud7af\uff00-\uffef-]/g, "").replaceAll(/[\s_]+/g, "-").replaceAll(/^-+|-+$/g, "");
12
+ return text.toLowerCase().trim().replaceAll(/[^\w\s\u3000-\u9fff\uac00-\ud7af\uff00-\uffef-]/g, "").replaceAll(/[\s_]+/g, "-").replaceAll(/^-+|-+$/g, "");
13
13
  }
14
14
  function extractText(node) {
15
15
  if (node.text) return node.text;
16
16
  if (node.children) return node.children.map(extractText).join("");
17
17
  return "";
18
18
  }
19
- function renderBuiltinNode(node, key, children, headingSlugs) {
19
+ function renderBuiltinNode(node, key, children, headingSlugs, textContent) {
20
20
  switch (node.type) {
21
21
  case "root":
22
22
  return /* @__PURE__ */ jsx(Fragment, { children });
@@ -26,7 +26,7 @@ function renderBuiltinNode(node, key, children, headingSlugs) {
26
26
  }
27
27
  case "heading": {
28
28
  const Tag = node.tag;
29
- const text = extractText(node);
29
+ const text = textContent || extractText(node);
30
30
  const baseSlug = textToSlug(text);
31
31
  let slug = baseSlug;
32
32
  if (baseSlug) {
@@ -39,15 +39,7 @@ function renderBuiltinNode(node, key, children, headingSlugs) {
39
39
  }
40
40
  }
41
41
  return /* @__PURE__ */ jsxs(Tag, { id: slug || void 0, className: `rich-heading-${Tag}`, children: [
42
- slug && /* @__PURE__ */ jsx(
43
- "a",
44
- {
45
- className: "rich-heading-anchor",
46
- "aria-hidden": "true",
47
- tabIndex: -1,
48
- href: `#${slug}`
49
- }
50
- ),
42
+ slug && /* @__PURE__ */ jsx("a", { className: "rich-heading-anchor", tabIndex: 0, href: `#${slug}` }),
51
43
  children
52
44
  ] }, key);
53
45
  }
@@ -80,26 +72,32 @@ function renderBuiltinNode(node, key, children, headingSlugs) {
80
72
  return /* @__PURE__ */ jsx("li", { className: cls, value: node.value, children }, key);
81
73
  }
82
74
  case "link":
83
- return /* @__PURE__ */ jsx(
75
+ return /* @__PURE__ */ jsxs(
84
76
  "a",
85
77
  {
86
78
  className: "rich-link",
87
79
  href: node.url,
88
- target: node.target,
89
- rel: node.rel,
90
- children
80
+ target: node.target || "_blank",
81
+ rel: node.rel || "noopener",
82
+ children: [
83
+ /* @__PURE__ */ jsx(LinkFavicon, { href: node.url }),
84
+ children
85
+ ]
91
86
  },
92
87
  key
93
88
  );
94
89
  case "autolink":
95
- return /* @__PURE__ */ jsx(
90
+ return /* @__PURE__ */ jsxs(
96
91
  "a",
97
92
  {
98
93
  className: "rich-link",
99
94
  href: node.url,
100
95
  target: "_blank",
101
- rel: "noopener noreferrer",
102
- children
96
+ rel: "noopener",
97
+ children: [
98
+ /* @__PURE__ */ jsx(LinkFavicon, { href: node.url }),
99
+ children
100
+ ]
103
101
  },
104
102
  key
105
103
  );
@@ -295,7 +293,7 @@ function applyBlockId(element, blockId, nodeKey) {
295
293
  `${nodeKey}-block-anchor`
296
294
  );
297
295
  }
298
- function renderTree(node, editor, editorConfig, headingSlugs, key, blockId) {
296
+ function renderTree(node, editor, editorConfig, headingSlugs, key, blockId, builtinNodeOverrides) {
299
297
  const nodeKey = node.getKey ? node.getKey() : key;
300
298
  if (typeof node.decorate === "function") {
301
299
  try {
@@ -325,18 +323,36 @@ function renderTree(node, editor, editorConfig, headingSlugs, key, blockId) {
325
323
  editor,
326
324
  editorConfig,
327
325
  headingSlugs,
328
- `${nodeKey}-${i}`
326
+ `${nodeKey}-${i}`,
327
+ void 0,
328
+ builtinNodeOverrides
329
329
  )
330
330
  );
331
331
  }
332
332
  }
333
+ const textContent = node.getTextContent ? node.getTextContent() : void 0;
334
+ const override = builtinNodeOverrides?.[serialized.type];
335
+ if (override) {
336
+ const defaultRenderer = () => renderBuiltinNode(
337
+ serialized,
338
+ nodeKey,
339
+ children,
340
+ headingSlugs,
341
+ textContent
342
+ );
343
+ return applyBlockId(
344
+ override(serialized, nodeKey, children, defaultRenderer),
345
+ blockId,
346
+ nodeKey
347
+ );
348
+ }
333
349
  return applyBlockId(
334
- renderBuiltinNode(serialized, nodeKey, children, headingSlugs),
350
+ renderBuiltinNode(serialized, nodeKey, children, headingSlugs, textContent),
335
351
  blockId,
336
352
  nodeKey
337
353
  );
338
354
  }
339
- function renderEditorToReact(value, nodes) {
355
+ function renderEditorToReact(value, nodes, builtinNodeOverrides) {
340
356
  const editor = createHeadlessEditor({
341
357
  nodes,
342
358
  theme: editorTheme,
@@ -361,7 +377,8 @@ function renderEditorToReact(value, nodes) {
361
377
  editorConfig,
362
378
  headingSlugs,
363
379
  `ssr-${i}`,
364
- rawRootChildren?.[i]?.$?.blockId
380
+ rawRootChildren?.[i]?.$?.blockId,
381
+ builtinNodeOverrides
365
382
  )
366
383
  );
367
384
  content = /* @__PURE__ */ jsx(Fragment, { children });
@@ -393,7 +410,8 @@ function renderEditorToReact(value, nodes) {
393
410
  nestedEditorConfig,
394
411
  headingSlugs,
395
412
  `nested-${i}`,
396
- nestedRawChildren?.[i]?.$?.blockId
413
+ nestedRawChildren?.[i]?.$?.blockId,
414
+ builtinNodeOverrides
397
415
  )
398
416
  );
399
417
  nested = /* @__PURE__ */ jsx(Fragment, { children: ch });
@@ -410,14 +428,14 @@ function RichRenderer({
410
428
  style,
411
429
  as: Component = "div",
412
430
  rendererConfig,
413
- extraNodes
431
+ extraNodes,
432
+ builtinNodeOverrides
414
433
  }) {
415
434
  const variantClass = getVariantClass(variant);
416
- const nodes = extraNodes ? [...allNodes, ...extraNodes] : allNodes;
417
- const { content, footnoteData, renderNestedContent } = renderEditorToReact(
418
- value,
419
- nodes
420
- );
435
+ const { content, footnoteData, renderNestedContent } = useMemo(() => {
436
+ const nodes = extraNodes ? [...allNodes, ...extraNodes] : allNodes;
437
+ return renderEditorToReact(value, nodes, builtinNodeOverrides);
438
+ }, [builtinNodeOverrides, extraNodes, value]);
421
439
  const classes = ["rich-content", variantClass, className].filter(Boolean).join(" ");
422
440
  return /* @__PURE__ */ jsx(PortalThemeProvider, { className: variantClass, theme, children: /* @__PURE__ */ jsx(ColorSchemeProvider, { colorScheme: theme, children: /* @__PURE__ */ jsx(
423
441
  RendererConfigProvider,
package/dist/types.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { ColorScheme, RendererConfig, RichEditorVariant } from '@haklex/rich-editor/static';
2
2
  import { Klass, LexicalNode, SerializedEditorState } from 'lexical';
3
- import { CSSProperties } from 'react';
3
+ import { CSSProperties, ReactNode } from 'react';
4
+ export type BuiltinNodeRenderer = (node: any, key: string, children: ReactNode[] | null, defaultRenderer: () => ReactNode) => ReactNode;
4
5
  export interface RichRendererProps {
5
6
  value: SerializedEditorState;
6
7
  variant?: RichEditorVariant;
@@ -10,5 +11,6 @@ export interface RichRendererProps {
10
11
  as?: keyof React.JSX.IntrinsicElements;
11
12
  rendererConfig?: RendererConfig;
12
13
  extraNodes?: Array<Klass<LexicalNode>>;
14
+ builtinNodeOverrides?: Record<string, BuiltinNodeRenderer>;
13
15
  }
14
16
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,WAAW,EACX,cAAc,EACd,iBAAiB,EAClB,MAAM,4BAA4B,CAAA;AACnC,OAAO,KAAK,EAAE,KAAK,EAAE,WAAW,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAA;AACxE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,OAAO,CAAA;AAE1C,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,qBAAqB,CAAA;IAC5B,OAAO,CAAC,EAAE,iBAAiB,CAAA;IAC3B,KAAK,CAAC,EAAE,WAAW,CAAA;IACnB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,KAAK,CAAC,EAAE,aAAa,CAAA;IACrB,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC,GAAG,CAAC,iBAAiB,CAAA;IACtC,cAAc,CAAC,EAAE,cAAc,CAAA;IAC/B,UAAU,CAAC,EAAE,KAAK,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,CAAA;CACvC"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,WAAW,EACX,cAAc,EACd,iBAAiB,EAClB,MAAM,4BAA4B,CAAA;AACnC,OAAO,KAAK,EAAE,KAAK,EAAE,WAAW,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAA;AACxE,OAAO,KAAK,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAErD,MAAM,MAAM,mBAAmB,GAAG,CAChC,IAAI,EAAE,GAAG,EACT,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,SAAS,EAAE,GAAG,IAAI,EAC5B,eAAe,EAAE,MAAM,SAAS,KAC7B,SAAS,CAAA;AAEd,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,qBAAqB,CAAA;IAC5B,OAAO,CAAC,EAAE,iBAAiB,CAAA;IAC3B,KAAK,CAAC,EAAE,WAAW,CAAA;IACnB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,KAAK,CAAC,EAAE,aAAa,CAAA;IACrB,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC,GAAG,CAAC,iBAAiB,CAAA;IACtC,cAAc,CAAC,EAAE,cAAc,CAAA;IAC/B,UAAU,CAAC,EAAE,KAAK,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,CAAA;IACtC,oBAAoB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAA;CAC3D"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@haklex/rich-static-renderer",
3
- "version": "0.0.44",
3
+ "version": "0.0.46",
4
4
  "description": "Headless SSR engine for Lexical rich content",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -16,8 +16,8 @@
16
16
  ],
17
17
  "dependencies": {
18
18
  "@lexical/headless": "^0.41.0",
19
- "@haklex/rich-editor": "0.0.44",
20
- "@haklex/rich-style-token": "0.0.44"
19
+ "@haklex/rich-editor": "0.0.46",
20
+ "@haklex/rich-style-token": "0.0.46"
21
21
  },
22
22
  "devDependencies": {
23
23
  "@lexical/code": "^0.41.0",