@incremark/react 0.0.1

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/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 wangyishuai
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
package/README.md ADDED
@@ -0,0 +1,137 @@
1
+ # @incremark/react
2
+
3
+ Incremark 的 React 18+ 集成库。
4
+
5
+ ## 特性
6
+
7
+ - 📦 **开箱即用** - 提供 `useIncremark` hook 和 `<Incremark>` 组件
8
+ - 🎨 **可定制** - 支持自定义渲染组件
9
+ - ⚡ **高性能** - 利用 React 的 reconciliation 机制
10
+ - 🔧 **DevTools** - 内置开发者工具
11
+
12
+ ## 安装
13
+
14
+ ```bash
15
+ pnpm add @incremark/core @incremark/react
16
+ ```
17
+
18
+ ## 快速开始
19
+
20
+ ```tsx
21
+ import { useIncremark, Incremark } from '@incremark/react'
22
+
23
+ function App() {
24
+ const { blocks, append, finalize, reset } = useIncremark({ gfm: true })
25
+
26
+ async function handleStream(stream: ReadableStream) {
27
+ reset()
28
+ const reader = stream.getReader()
29
+ const decoder = new TextDecoder()
30
+
31
+ while (true) {
32
+ const { done, value } = await reader.read()
33
+ if (done) break
34
+ append(decoder.decode(value))
35
+ }
36
+
37
+ finalize()
38
+ }
39
+
40
+ return (
41
+ <>
42
+ <button onClick={() => handleStream(stream)}>开始</button>
43
+ <Incremark blocks={blocks} />
44
+ </>
45
+ )
46
+ }
47
+ ```
48
+
49
+ ## API
50
+
51
+ ### useIncremark(options)
52
+
53
+ 核心 hook。
54
+
55
+ **返回值:**
56
+
57
+ | 属性 | 类型 | 说明 |
58
+ |------|------|------|
59
+ | `markdown` | `string` | 完整 Markdown |
60
+ | `blocks` | `Block[]` | 所有块 |
61
+ | `completedBlocks` | `Block[]` | 已完成块 |
62
+ | `pendingBlocks` | `Block[]` | 待处理块 |
63
+ | `append` | `Function` | 追加内容 |
64
+ | `finalize` | `Function` | 完成解析 |
65
+ | `reset` | `Function` | 重置状态 |
66
+
67
+ ### useDevTools(incremark)
68
+
69
+ 启用 DevTools。
70
+
71
+ ```tsx
72
+ const incremark = useIncremark()
73
+ useDevTools(incremark)
74
+ ```
75
+
76
+ ### \<Incremark\>
77
+
78
+ 渲染组件。
79
+
80
+ ```tsx
81
+ <Incremark
82
+ blocks={blocks}
83
+ components={{ heading: MyHeading }}
84
+ />
85
+ ```
86
+
87
+ ## 自定义组件
88
+
89
+ ```tsx
90
+ import { useIncremark, Incremark } from '@incremark/react'
91
+ import MyCode from './MyCode'
92
+
93
+ function App() {
94
+ const { blocks } = useIncremark()
95
+
96
+ return (
97
+ <Incremark
98
+ blocks={blocks}
99
+ components={{ code: MyCode }}
100
+ />
101
+ )
102
+ }
103
+ ```
104
+
105
+ ## 与 React Query 集成
106
+
107
+ ```tsx
108
+ import { useQuery } from '@tanstack/react-query'
109
+ import { useIncremark, Incremark } from '@incremark/react'
110
+
111
+ function StreamingContent() {
112
+ const { blocks, append, finalize, reset } = useIncremark()
113
+
114
+ const { refetch } = useQuery({
115
+ queryKey: ['chat'],
116
+ queryFn: async () => {
117
+ reset()
118
+ // ... 流式处理
119
+ finalize()
120
+ return null
121
+ },
122
+ enabled: false
123
+ })
124
+
125
+ return (
126
+ <>
127
+ <button onClick={() => refetch()}>开始</button>
128
+ <Incremark blocks={blocks} />
129
+ </>
130
+ )
131
+ }
132
+ ```
133
+
134
+ ## License
135
+
136
+ MIT
137
+
@@ -0,0 +1,119 @@
1
+ import { ParserOptions, ParsedBlock, Root, IncrementalUpdate, IncremarkParser, RootContent } from '@incremark/core';
2
+ export { IncrementalUpdate, ParsedBlock, ParserOptions, Root, RootContent } from '@incremark/core';
3
+ import * as _incremark_devtools from '@incremark/devtools';
4
+ import { DevToolsOptions } from '@incremark/devtools';
5
+ import React from 'react';
6
+
7
+ interface UseIncremarkOptions extends ParserOptions {
8
+ }
9
+ interface BlockWithStableId$1 extends ParsedBlock {
10
+ stableId: string;
11
+ }
12
+ /**
13
+ * React Hook: Incremark 流式 Markdown 解析器
14
+ *
15
+ * @example
16
+ * ```tsx
17
+ * import { useIncremark } from '@incremark/react'
18
+ *
19
+ * function App() {
20
+ * const { markdown, blocks, append, finalize, reset } = useIncremark()
21
+ *
22
+ * async function handleStream(stream) {
23
+ * for await (const chunk of stream) {
24
+ * append(chunk)
25
+ * }
26
+ * finalize()
27
+ * }
28
+ *
29
+ * return <div>{markdown.length} 字符</div>
30
+ * }
31
+ * ```
32
+ */
33
+ declare function useIncremark(options?: UseIncremarkOptions): {
34
+ /** 已收集的完整 Markdown 字符串 */
35
+ markdown: string;
36
+ /** 已完成的块列表 */
37
+ completedBlocks: ParsedBlock[];
38
+ /** 待处理的块列表 */
39
+ pendingBlocks: ParsedBlock[];
40
+ /** 当前完整的 AST */
41
+ ast: Root;
42
+ /** 所有块(完成 + 待处理),带稳定 ID */
43
+ blocks: BlockWithStableId$1[];
44
+ /** 是否正在加载 */
45
+ isLoading: boolean;
46
+ /** 追加内容 */
47
+ append: (chunk: string) => IncrementalUpdate;
48
+ /** 完成解析 */
49
+ finalize: () => IncrementalUpdate;
50
+ /** 强制中断 */
51
+ abort: () => IncrementalUpdate;
52
+ /** 重置解析器 */
53
+ reset: () => void;
54
+ /** 解析器实例 */
55
+ parser: IncremarkParser;
56
+ };
57
+ type UseIncremarkReturn = ReturnType<typeof useIncremark>;
58
+
59
+ interface UseDevToolsOptions extends DevToolsOptions {
60
+ }
61
+ /**
62
+ * React DevTools 一行接入
63
+ *
64
+ * @example
65
+ * ```tsx
66
+ * import { useIncremark, useDevTools } from '@incremark/react'
67
+ *
68
+ * function App() {
69
+ * const incremark = useIncremark()
70
+ * useDevTools(incremark) // 就这一行!
71
+ *
72
+ * return <div>...</div>
73
+ * }
74
+ * ```
75
+ */
76
+ declare function useDevTools(incremark: UseIncremarkReturn, options?: UseDevToolsOptions): _incremark_devtools.IncremarkDevTools | null;
77
+
78
+ interface BlockWithStableId extends ParsedBlock {
79
+ stableId: string;
80
+ }
81
+ interface IncremarkProps {
82
+ /** 要渲染的块列表 */
83
+ blocks: BlockWithStableId[];
84
+ /** 自定义组件映射 */
85
+ components?: Partial<Record<string, React.ComponentType<{
86
+ node: any;
87
+ }>>>;
88
+ /** 是否显示块状态(待处理块边框) */
89
+ showBlockStatus?: boolean;
90
+ /** 自定义类名 */
91
+ className?: string;
92
+ }
93
+ /**
94
+ * Incremark 主渲染组件
95
+ *
96
+ * @example
97
+ * ```tsx
98
+ * import { useIncremark, Incremark } from '@incremark/react'
99
+ *
100
+ * function App() {
101
+ * const { blocks } = useIncremark()
102
+ * return <Incremark blocks={blocks} />
103
+ * }
104
+ * ```
105
+ */
106
+ declare const Incremark: React.FC<IncremarkProps>;
107
+
108
+ interface IncremarkRendererProps {
109
+ node: RootContent;
110
+ components?: Partial<Record<string, React.ComponentType<{
111
+ node: any;
112
+ }>>>;
113
+ }
114
+ /**
115
+ * 渲染单个 AST 节点
116
+ */
117
+ declare const IncremarkRenderer: React.FC<IncremarkRendererProps>;
118
+
119
+ export { Incremark, type IncremarkProps, IncremarkRenderer, type IncremarkRendererProps, type UseDevToolsOptions, type UseIncremarkOptions, type UseIncremarkReturn, useDevTools, useIncremark };
package/dist/index.js ADDED
@@ -0,0 +1,214 @@
1
+ // src/hooks/useIncremark.ts
2
+ import { useState, useCallback, useMemo, useRef } from "react";
3
+ import {
4
+ createIncremarkParser
5
+ } from "@incremark/core";
6
+ function useIncremark(options = {}) {
7
+ const parserRef = useRef(null);
8
+ if (!parserRef.current) {
9
+ parserRef.current = createIncremarkParser(options);
10
+ }
11
+ const parser = parserRef.current;
12
+ const [markdown, setMarkdown] = useState("");
13
+ const [completedBlocks, setCompletedBlocks] = useState([]);
14
+ const [pendingBlocks, setPendingBlocks] = useState([]);
15
+ const [isLoading, setIsLoading] = useState(false);
16
+ const blocks = useMemo(() => {
17
+ const result = [];
18
+ for (const block of completedBlocks) {
19
+ result.push({ ...block, stableId: block.id });
20
+ }
21
+ for (let i = 0; i < pendingBlocks.length; i++) {
22
+ result.push({
23
+ ...pendingBlocks[i],
24
+ stableId: `pending-${i}`
25
+ });
26
+ }
27
+ return result;
28
+ }, [completedBlocks, pendingBlocks]);
29
+ const ast = useMemo(
30
+ () => ({
31
+ type: "root",
32
+ children: [...completedBlocks.map((b) => b.node), ...pendingBlocks.map((b) => b.node)]
33
+ }),
34
+ [completedBlocks, pendingBlocks]
35
+ );
36
+ const append = useCallback(
37
+ (chunk) => {
38
+ setIsLoading(true);
39
+ const update = parser.append(chunk);
40
+ setMarkdown(parser.getBuffer());
41
+ if (update.completed.length > 0) {
42
+ setCompletedBlocks((prev) => [...prev, ...update.completed]);
43
+ }
44
+ setPendingBlocks(update.pending);
45
+ return update;
46
+ },
47
+ [parser]
48
+ );
49
+ const finalize = useCallback(() => {
50
+ const update = parser.finalize();
51
+ setMarkdown(parser.getBuffer());
52
+ if (update.completed.length > 0) {
53
+ setCompletedBlocks((prev) => [...prev, ...update.completed]);
54
+ }
55
+ setPendingBlocks([]);
56
+ setIsLoading(false);
57
+ return update;
58
+ }, [parser]);
59
+ const abort = useCallback(() => {
60
+ return finalize();
61
+ }, [finalize]);
62
+ const reset = useCallback(() => {
63
+ parser.reset();
64
+ setCompletedBlocks([]);
65
+ setPendingBlocks([]);
66
+ setMarkdown("");
67
+ setIsLoading(false);
68
+ }, [parser]);
69
+ return {
70
+ /** 已收集的完整 Markdown 字符串 */
71
+ markdown,
72
+ /** 已完成的块列表 */
73
+ completedBlocks,
74
+ /** 待处理的块列表 */
75
+ pendingBlocks,
76
+ /** 当前完整的 AST */
77
+ ast,
78
+ /** 所有块(完成 + 待处理),带稳定 ID */
79
+ blocks,
80
+ /** 是否正在加载 */
81
+ isLoading,
82
+ /** 追加内容 */
83
+ append,
84
+ /** 完成解析 */
85
+ finalize,
86
+ /** 强制中断 */
87
+ abort,
88
+ /** 重置解析器 */
89
+ reset,
90
+ /** 解析器实例 */
91
+ parser
92
+ };
93
+ }
94
+
95
+ // src/hooks/useDevTools.ts
96
+ import { useEffect, useRef as useRef2 } from "react";
97
+ import { createDevTools } from "@incremark/devtools";
98
+ function useDevTools(incremark, options = {}) {
99
+ const devtoolsRef = useRef2(null);
100
+ const optionsRef = useRef2(options);
101
+ useEffect(() => {
102
+ const devtools = createDevTools(optionsRef.current);
103
+ devtoolsRef.current = devtools;
104
+ incremark.parser.setOnChange((state) => {
105
+ const blocks = [
106
+ ...state.completedBlocks.map((b) => ({ ...b, stableId: b.id })),
107
+ ...state.pendingBlocks.map((b, i) => ({ ...b, stableId: `pending-${i}` }))
108
+ ];
109
+ devtools.update({
110
+ blocks,
111
+ completedBlocks: state.completedBlocks,
112
+ pendingBlocks: state.pendingBlocks,
113
+ markdown: state.markdown,
114
+ ast: state.ast,
115
+ isLoading: state.pendingBlocks.length > 0
116
+ });
117
+ });
118
+ devtools.mount();
119
+ return () => {
120
+ devtools.unmount();
121
+ incremark.parser.setOnChange(void 0);
122
+ };
123
+ }, [incremark.parser]);
124
+ return devtoolsRef.current;
125
+ }
126
+
127
+ // src/components/IncremarkRenderer.tsx
128
+ import React from "react";
129
+ import { jsx, jsxs } from "react/jsx-runtime";
130
+ function renderInlineChildren(children) {
131
+ if (!children) return null;
132
+ return children.map((child, i) => {
133
+ switch (child.type) {
134
+ case "text":
135
+ return /* @__PURE__ */ jsx(React.Fragment, { children: child.value }, i);
136
+ case "strong":
137
+ return /* @__PURE__ */ jsx("strong", { children: renderInlineChildren(child.children) }, i);
138
+ case "emphasis":
139
+ return /* @__PURE__ */ jsx("em", { children: renderInlineChildren(child.children) }, i);
140
+ case "inlineCode":
141
+ return /* @__PURE__ */ jsx("code", { className: "incremark-inline-code", children: child.value }, i);
142
+ case "link":
143
+ return /* @__PURE__ */ jsx("a", { href: child.url, target: "_blank", rel: "noopener noreferrer", children: renderInlineChildren(child.children) }, i);
144
+ case "image":
145
+ return /* @__PURE__ */ jsx("img", { src: child.url, alt: child.alt || "", loading: "lazy" }, i);
146
+ case "break":
147
+ return /* @__PURE__ */ jsx("br", {}, i);
148
+ case "delete":
149
+ return /* @__PURE__ */ jsx("del", { children: renderInlineChildren(child.children) }, i);
150
+ case "paragraph":
151
+ return /* @__PURE__ */ jsx(React.Fragment, { children: renderInlineChildren(child.children) }, i);
152
+ default:
153
+ return /* @__PURE__ */ jsx("span", { children: child.value || "" }, i);
154
+ }
155
+ });
156
+ }
157
+ var DefaultHeading = ({ node }) => {
158
+ const Tag = `h${node.depth}`;
159
+ return /* @__PURE__ */ jsx(Tag, { className: "incremark-heading", children: renderInlineChildren(node.children) });
160
+ };
161
+ var DefaultParagraph = ({ node }) => /* @__PURE__ */ jsx("p", { className: "incremark-paragraph", children: renderInlineChildren(node.children) });
162
+ var DefaultCode = ({ node }) => /* @__PURE__ */ jsxs("div", { className: "incremark-code", children: [
163
+ /* @__PURE__ */ jsx("div", { className: "code-header", children: /* @__PURE__ */ jsx("span", { className: "language", children: node.lang || "text" }) }),
164
+ /* @__PURE__ */ jsx("pre", { children: /* @__PURE__ */ jsx("code", { children: node.value }) })
165
+ ] });
166
+ var DefaultList = ({ node }) => {
167
+ const Tag = node.ordered ? "ol" : "ul";
168
+ return /* @__PURE__ */ jsx(Tag, { className: "incremark-list", children: node.children?.map((item, i) => /* @__PURE__ */ jsx("li", { children: renderInlineChildren(item.children) }, i)) });
169
+ };
170
+ var DefaultBlockquote = ({ node }) => /* @__PURE__ */ jsx("blockquote", { className: "incremark-blockquote", children: node.children?.map((child, i) => /* @__PURE__ */ jsx(React.Fragment, { children: child.type === "paragraph" ? /* @__PURE__ */ jsx("p", { children: renderInlineChildren(child.children) }) : renderInlineChildren(child.children || []) }, i)) });
171
+ var DefaultTable = ({ node }) => /* @__PURE__ */ jsx("div", { className: "incremark-table-wrapper", children: /* @__PURE__ */ jsxs("table", { className: "incremark-table", children: [
172
+ /* @__PURE__ */ jsx("thead", { children: node.children?.[0] && /* @__PURE__ */ jsx("tr", { children: node.children[0].children?.map((cell, i) => /* @__PURE__ */ jsx("th", { children: renderInlineChildren(cell.children) }, i)) }) }),
173
+ /* @__PURE__ */ jsx("tbody", { children: node.children?.slice(1).map((row, i) => /* @__PURE__ */ jsx("tr", { children: row.children?.map((cell, j) => /* @__PURE__ */ jsx("td", { children: renderInlineChildren(cell.children) }, j)) }, i)) })
174
+ ] }) });
175
+ var DefaultThematicBreak = () => /* @__PURE__ */ jsx("hr", { className: "incremark-hr" });
176
+ var DefaultDefault = ({ node }) => /* @__PURE__ */ jsx("div", { className: "incremark-unknown", "data-type": node.type, children: /* @__PURE__ */ jsx("pre", { children: JSON.stringify(node, null, 2) }) });
177
+ var defaultComponents = {
178
+ heading: DefaultHeading,
179
+ paragraph: DefaultParagraph,
180
+ code: DefaultCode,
181
+ list: DefaultList,
182
+ blockquote: DefaultBlockquote,
183
+ table: DefaultTable,
184
+ thematicBreak: DefaultThematicBreak
185
+ };
186
+ var IncremarkRenderer = ({ node, components = {} }) => {
187
+ const mergedComponents = { ...defaultComponents, ...components };
188
+ const Component = mergedComponents[node.type] || DefaultDefault;
189
+ return /* @__PURE__ */ jsx(Component, { node });
190
+ };
191
+
192
+ // src/components/Incremark.tsx
193
+ import { jsx as jsx2 } from "react/jsx-runtime";
194
+ var Incremark = ({
195
+ blocks,
196
+ components,
197
+ showBlockStatus = true,
198
+ className = ""
199
+ }) => {
200
+ return /* @__PURE__ */ jsx2("div", { className: `incremark ${className}`, children: blocks.map((block) => /* @__PURE__ */ jsx2(
201
+ "div",
202
+ {
203
+ className: `incremark-block ${showBlockStatus && block.status === "pending" ? "pending" : ""}`,
204
+ children: /* @__PURE__ */ jsx2(IncremarkRenderer, { node: block.node, components })
205
+ },
206
+ block.stableId
207
+ )) });
208
+ };
209
+ export {
210
+ Incremark,
211
+ IncremarkRenderer,
212
+ useDevTools,
213
+ useIncremark
214
+ };
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@incremark/react",
3
+ "version": "0.0.1",
4
+ "license": "MIT",
5
+ "description": "Incremark React integration - Incremental Markdown parser for AI streaming",
6
+ "type": "module",
7
+ "main": "./dist/index.js",
8
+ "module": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "import": "./dist/index.js",
13
+ "types": "./dist/index.d.ts"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "peerDependencies": {
20
+ "react": ">=18.0.0",
21
+ "@incremark/core": "0.0.1"
22
+ },
23
+ "dependencies": {
24
+ "@incremark/devtools": "0.0.1"
25
+ },
26
+ "devDependencies": {
27
+ "@types/react": "^18.2.0",
28
+ "react": "^18.2.0",
29
+ "tsup": "^8.0.0",
30
+ "typescript": "^5.3.0"
31
+ },
32
+ "scripts": {
33
+ "build": "tsup",
34
+ "dev": "tsup --watch"
35
+ }
36
+ }