@incremark/vue 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 +22 -0
- package/README.md +120 -0
- package/dist/index.css +348 -0
- package/dist/index.css.map +1 -0
- package/dist/index.js +1342 -0
- package/dist/index.js.map +1 -0
- package/package.json +59 -0
- package/src/components/Incremark.vue +99 -0
- package/src/components/IncremarkBlockquote.vue +40 -0
- package/src/components/IncremarkCode.vue +394 -0
- package/src/components/IncremarkDefault.vue +42 -0
- package/src/components/IncremarkHeading.vue +33 -0
- package/src/components/IncremarkInline.vue +75 -0
- package/src/components/IncremarkList.vue +83 -0
- package/src/components/IncremarkMath.vue +150 -0
- package/src/components/IncremarkParagraph.vue +21 -0
- package/src/components/IncremarkRenderer.vue +38 -0
- package/src/components/IncremarkTable.vue +74 -0
- package/src/components/IncremarkThematicBreak.vue +16 -0
- package/src/components/index.ts +18 -0
- package/src/composables/index.ts +8 -0
- package/src/composables/useDevTools.ts +54 -0
- package/src/composables/useIncremark.ts +147 -0
- package/src/composables/useStreamRenderer.ts +55 -0
- package/src/index.ts +30 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, ref, watch, shallowRef, onUnmounted } from 'vue'
|
|
3
|
+
|
|
4
|
+
// Math 节点类型(来自 mdast-util-math)
|
|
5
|
+
interface MathNode {
|
|
6
|
+
type: 'math' | 'inlineMath'
|
|
7
|
+
value: string
|
|
8
|
+
data?: {
|
|
9
|
+
hName?: string
|
|
10
|
+
hProperties?: Record<string, any>
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const props = withDefaults(
|
|
15
|
+
defineProps<{
|
|
16
|
+
node: MathNode
|
|
17
|
+
/** 渲染延迟(毫秒),用于流式输入时防抖 */
|
|
18
|
+
renderDelay?: number
|
|
19
|
+
}>(),
|
|
20
|
+
{
|
|
21
|
+
renderDelay: 300
|
|
22
|
+
}
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
const renderedHtml = ref('')
|
|
26
|
+
const renderError = ref('')
|
|
27
|
+
const isLoading = ref(false)
|
|
28
|
+
const katexRef = shallowRef<any>(null)
|
|
29
|
+
let renderTimer: ReturnType<typeof setTimeout> | null = null
|
|
30
|
+
|
|
31
|
+
const isInline = computed(() => props.node.type === 'inlineMath')
|
|
32
|
+
const formula = computed(() => props.node.value)
|
|
33
|
+
|
|
34
|
+
// 带防抖动的渲染
|
|
35
|
+
function scheduleRender() {
|
|
36
|
+
if (!formula.value) {
|
|
37
|
+
renderedHtml.value = ''
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// 清除之前的定时器
|
|
42
|
+
if (renderTimer) {
|
|
43
|
+
clearTimeout(renderTimer)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
isLoading.value = true
|
|
47
|
+
|
|
48
|
+
// 防抖动延迟渲染
|
|
49
|
+
renderTimer = setTimeout(() => {
|
|
50
|
+
doRender()
|
|
51
|
+
}, props.renderDelay)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function doRender() {
|
|
55
|
+
if (!formula.value) return
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
// 动态导入 KaTeX
|
|
59
|
+
if (!katexRef.value) {
|
|
60
|
+
// @ts-ignore - katex 是可选依赖
|
|
61
|
+
const katexModule = await import('katex')
|
|
62
|
+
katexRef.value = katexModule.default
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const katex = katexRef.value
|
|
66
|
+
renderedHtml.value = katex.renderToString(formula.value, {
|
|
67
|
+
displayMode: !isInline.value,
|
|
68
|
+
throwOnError: false,
|
|
69
|
+
strict: false
|
|
70
|
+
})
|
|
71
|
+
renderError.value = ''
|
|
72
|
+
} catch (e: any) {
|
|
73
|
+
// 静默失败,可能是公式不完整
|
|
74
|
+
renderError.value = ''
|
|
75
|
+
renderedHtml.value = ''
|
|
76
|
+
} finally {
|
|
77
|
+
isLoading.value = false
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
onUnmounted(() => {
|
|
82
|
+
if (renderTimer) {
|
|
83
|
+
clearTimeout(renderTimer)
|
|
84
|
+
}
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
watch(formula, scheduleRender, { immediate: true })
|
|
88
|
+
</script>
|
|
89
|
+
|
|
90
|
+
<template>
|
|
91
|
+
<!-- 行内公式 -->
|
|
92
|
+
<span v-if="isInline" class="incremark-math-inline">
|
|
93
|
+
<!-- 渲染成功 -->
|
|
94
|
+
<span v-if="renderedHtml && !isLoading" v-html="renderedHtml" />
|
|
95
|
+
<!-- 加载中或未渲染:显示源码 -->
|
|
96
|
+
<code v-else class="math-source">{{ formula }}</code>
|
|
97
|
+
</span>
|
|
98
|
+
<!-- 块级公式 -->
|
|
99
|
+
<div v-else class="incremark-math-block">
|
|
100
|
+
<!-- 渲染成功 -->
|
|
101
|
+
<div v-if="renderedHtml && !isLoading" v-html="renderedHtml" class="math-rendered" />
|
|
102
|
+
<!-- 加载中或未渲染:显示源码 -->
|
|
103
|
+
<pre v-else class="math-source-block"><code>{{ formula }}</code></pre>
|
|
104
|
+
</div>
|
|
105
|
+
</template>
|
|
106
|
+
|
|
107
|
+
<style scoped>
|
|
108
|
+
.incremark-math-inline {
|
|
109
|
+
display: inline;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.incremark-math-block {
|
|
113
|
+
margin: 1em 0;
|
|
114
|
+
padding: 1em;
|
|
115
|
+
overflow-x: auto;
|
|
116
|
+
text-align: center;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.math-source {
|
|
120
|
+
background: #f3f4f6;
|
|
121
|
+
padding: 0.1em 0.3em;
|
|
122
|
+
border-radius: 3px;
|
|
123
|
+
font-size: 0.9em;
|
|
124
|
+
color: #6b7280;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.math-source-block {
|
|
128
|
+
margin: 0;
|
|
129
|
+
padding: 1em;
|
|
130
|
+
background: #f3f4f6;
|
|
131
|
+
border-radius: 6px;
|
|
132
|
+
text-align: left;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.math-source-block code {
|
|
136
|
+
font-family: 'Fira Code', monospace;
|
|
137
|
+
font-size: 0.9em;
|
|
138
|
+
color: #374151;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.math-rendered :deep(.katex) {
|
|
142
|
+
font-size: 1.1em;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.math-rendered :deep(.katex-display) {
|
|
146
|
+
margin: 0;
|
|
147
|
+
overflow-x: auto;
|
|
148
|
+
overflow-y: hidden;
|
|
149
|
+
}
|
|
150
|
+
</style>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { Paragraph } from 'mdast'
|
|
3
|
+
import IncremarkInline from './IncremarkInline.vue'
|
|
4
|
+
|
|
5
|
+
defineProps<{
|
|
6
|
+
node: Paragraph
|
|
7
|
+
}>()
|
|
8
|
+
</script>
|
|
9
|
+
|
|
10
|
+
<template>
|
|
11
|
+
<p class="incremark-paragraph">
|
|
12
|
+
<IncremarkInline :nodes="node.children" />
|
|
13
|
+
</p>
|
|
14
|
+
</template>
|
|
15
|
+
|
|
16
|
+
<style scoped>
|
|
17
|
+
.incremark-paragraph {
|
|
18
|
+
margin: 0.75em 0;
|
|
19
|
+
line-height: 1.6;
|
|
20
|
+
}
|
|
21
|
+
</style>
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { RootContent } from 'mdast'
|
|
3
|
+
import type { Component } from 'vue'
|
|
4
|
+
import IncremarkHeading from './IncremarkHeading.vue'
|
|
5
|
+
import IncremarkParagraph from './IncremarkParagraph.vue'
|
|
6
|
+
import IncremarkCode from './IncremarkCode.vue'
|
|
7
|
+
import IncremarkList from './IncremarkList.vue'
|
|
8
|
+
import IncremarkTable from './IncremarkTable.vue'
|
|
9
|
+
import IncremarkBlockquote from './IncremarkBlockquote.vue'
|
|
10
|
+
import IncremarkThematicBreak from './IncremarkThematicBreak.vue'
|
|
11
|
+
import IncremarkMath from './IncremarkMath.vue'
|
|
12
|
+
import IncremarkDefault from './IncremarkDefault.vue'
|
|
13
|
+
|
|
14
|
+
defineProps<{
|
|
15
|
+
node: RootContent
|
|
16
|
+
}>()
|
|
17
|
+
|
|
18
|
+
const componentMap: Record<string, Component> = {
|
|
19
|
+
heading: IncremarkHeading,
|
|
20
|
+
paragraph: IncremarkParagraph,
|
|
21
|
+
code: IncremarkCode,
|
|
22
|
+
list: IncremarkList,
|
|
23
|
+
table: IncremarkTable,
|
|
24
|
+
blockquote: IncremarkBlockquote,
|
|
25
|
+
thematicBreak: IncremarkThematicBreak,
|
|
26
|
+
math: IncremarkMath,
|
|
27
|
+
inlineMath: IncremarkMath,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function getComponent(type: string): Component {
|
|
31
|
+
return componentMap[type] || IncremarkDefault
|
|
32
|
+
}
|
|
33
|
+
</script>
|
|
34
|
+
|
|
35
|
+
<template>
|
|
36
|
+
<component :is="getComponent(node.type)" :node="node" />
|
|
37
|
+
</template>
|
|
38
|
+
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { Table, TableCell, PhrasingContent } from 'mdast'
|
|
3
|
+
import IncremarkInline from './IncremarkInline.vue'
|
|
4
|
+
|
|
5
|
+
defineProps<{
|
|
6
|
+
node: Table
|
|
7
|
+
}>()
|
|
8
|
+
|
|
9
|
+
function getCellContent(cell: TableCell): PhrasingContent[] {
|
|
10
|
+
return cell.children as PhrasingContent[]
|
|
11
|
+
}
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<template>
|
|
15
|
+
<div class="incremark-table-wrapper">
|
|
16
|
+
<table class="incremark-table">
|
|
17
|
+
<thead>
|
|
18
|
+
<tr v-if="node.children[0]">
|
|
19
|
+
<th
|
|
20
|
+
v-for="(cell, cellIndex) in node.children[0].children"
|
|
21
|
+
:key="cellIndex"
|
|
22
|
+
:style="{ textAlign: node.align?.[cellIndex] || 'left' }"
|
|
23
|
+
>
|
|
24
|
+
<IncremarkInline :nodes="getCellContent(cell)" />
|
|
25
|
+
</th>
|
|
26
|
+
</tr>
|
|
27
|
+
</thead>
|
|
28
|
+
<tbody>
|
|
29
|
+
<tr v-for="(row, rowIndex) in node.children.slice(1)" :key="rowIndex">
|
|
30
|
+
<td
|
|
31
|
+
v-for="(cell, cellIndex) in row.children"
|
|
32
|
+
:key="cellIndex"
|
|
33
|
+
:style="{ textAlign: node.align?.[cellIndex] || 'left' }"
|
|
34
|
+
>
|
|
35
|
+
<IncremarkInline :nodes="getCellContent(cell)" />
|
|
36
|
+
</td>
|
|
37
|
+
</tr>
|
|
38
|
+
</tbody>
|
|
39
|
+
</table>
|
|
40
|
+
</div>
|
|
41
|
+
</template>
|
|
42
|
+
|
|
43
|
+
<style scoped>
|
|
44
|
+
.incremark-table-wrapper {
|
|
45
|
+
overflow-x: auto;
|
|
46
|
+
margin: 1em 0;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.incremark-table {
|
|
50
|
+
width: 100%;
|
|
51
|
+
border-collapse: collapse;
|
|
52
|
+
font-size: 14px;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.incremark-table th,
|
|
56
|
+
.incremark-table td {
|
|
57
|
+
border: 1px solid #ddd;
|
|
58
|
+
padding: 10px 14px;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.incremark-table th {
|
|
62
|
+
background: #f8f9fa;
|
|
63
|
+
font-weight: 600;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.incremark-table tr:nth-child(even) {
|
|
67
|
+
background: #fafafa;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.incremark-table tr:hover {
|
|
71
|
+
background: #f0f0f0;
|
|
72
|
+
}
|
|
73
|
+
</style>
|
|
74
|
+
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// ThematicBreak 不需要 props
|
|
3
|
+
</script>
|
|
4
|
+
|
|
5
|
+
<template>
|
|
6
|
+
<hr class="incremark-hr" />
|
|
7
|
+
</template>
|
|
8
|
+
|
|
9
|
+
<style scoped>
|
|
10
|
+
.incremark-hr {
|
|
11
|
+
margin: 2em 0;
|
|
12
|
+
border: none;
|
|
13
|
+
border-top: 2px solid #e5e5e5;
|
|
14
|
+
}
|
|
15
|
+
</style>
|
|
16
|
+
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// 主组件
|
|
2
|
+
export { default as Incremark } from './Incremark.vue'
|
|
3
|
+
export type { ComponentMap, BlockWithStableId } from './Incremark.vue'
|
|
4
|
+
|
|
5
|
+
// 渲染器组件 - 用于自定义渲染
|
|
6
|
+
export { default as IncremarkRenderer } from './IncremarkRenderer.vue'
|
|
7
|
+
|
|
8
|
+
// 各节点类型组件 - 用于自定义或扩展
|
|
9
|
+
export { default as IncremarkHeading } from './IncremarkHeading.vue'
|
|
10
|
+
export { default as IncremarkParagraph } from './IncremarkParagraph.vue'
|
|
11
|
+
export { default as IncremarkCode } from './IncremarkCode.vue'
|
|
12
|
+
export { default as IncremarkList } from './IncremarkList.vue'
|
|
13
|
+
export { default as IncremarkTable } from './IncremarkTable.vue'
|
|
14
|
+
export { default as IncremarkBlockquote } from './IncremarkBlockquote.vue'
|
|
15
|
+
export { default as IncremarkThematicBreak } from './IncremarkThematicBreak.vue'
|
|
16
|
+
export { default as IncremarkInline } from './IncremarkInline.vue'
|
|
17
|
+
export { default as IncremarkMath } from './IncremarkMath.vue'
|
|
18
|
+
export { default as IncremarkDefault } from './IncremarkDefault.vue'
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { useIncremark } from './useIncremark'
|
|
2
|
+
export type { UseIncremarkOptions } from './useIncremark'
|
|
3
|
+
|
|
4
|
+
export { useStreamRenderer } from './useStreamRenderer'
|
|
5
|
+
export type { UseStreamRendererOptions } from './useStreamRenderer'
|
|
6
|
+
|
|
7
|
+
export { useDevTools } from './useDevTools'
|
|
8
|
+
export type { UseDevToolsOptions } from './useDevTools'
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { onMounted, onUnmounted } from 'vue'
|
|
2
|
+
import { createDevTools, type DevToolsOptions } from '@incremark/devtools'
|
|
3
|
+
import type { UseIncremarkReturn } from './useIncremark'
|
|
4
|
+
|
|
5
|
+
export interface UseDevToolsOptions extends DevToolsOptions {}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Vue 3 DevTools 一行接入
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```vue
|
|
12
|
+
* <script setup>
|
|
13
|
+
* import { useIncremark, useDevTools } from '@incremark/vue'
|
|
14
|
+
*
|
|
15
|
+
* const incremark = useIncremark()
|
|
16
|
+
* useDevTools(incremark) // 就这一行!
|
|
17
|
+
* </script>
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export function useDevTools(
|
|
21
|
+
incremark: UseIncremarkReturn,
|
|
22
|
+
options: UseDevToolsOptions = {}
|
|
23
|
+
) {
|
|
24
|
+
const devtools = createDevTools(options)
|
|
25
|
+
|
|
26
|
+
// 设置 parser 的 onChange 回调
|
|
27
|
+
incremark.parser.setOnChange((state) => {
|
|
28
|
+
const blocks = [
|
|
29
|
+
...state.completedBlocks.map((b) => ({ ...b, stableId: b.id })),
|
|
30
|
+
...state.pendingBlocks.map((b, i) => ({ ...b, stableId: `pending-${i}` }))
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
devtools.update({
|
|
34
|
+
blocks,
|
|
35
|
+
completedBlocks: state.completedBlocks,
|
|
36
|
+
pendingBlocks: state.pendingBlocks,
|
|
37
|
+
markdown: state.markdown,
|
|
38
|
+
ast: state.ast,
|
|
39
|
+
isLoading: state.pendingBlocks.length > 0
|
|
40
|
+
})
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
onMounted(() => {
|
|
44
|
+
devtools.mount()
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
onUnmounted(() => {
|
|
48
|
+
devtools.unmount()
|
|
49
|
+
// 清理回调
|
|
50
|
+
incremark.parser.setOnChange(undefined)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
return devtools
|
|
54
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { ref, shallowRef, computed, markRaw } from 'vue'
|
|
2
|
+
import {
|
|
3
|
+
IncremarkParser,
|
|
4
|
+
createIncremarkParser,
|
|
5
|
+
type ParserOptions,
|
|
6
|
+
type ParsedBlock,
|
|
7
|
+
type IncrementalUpdate,
|
|
8
|
+
type Root
|
|
9
|
+
} from '@incremark/core'
|
|
10
|
+
|
|
11
|
+
export interface UseIncremarkOptions extends ParserOptions {}
|
|
12
|
+
|
|
13
|
+
/** useIncremark 的返回类型 */
|
|
14
|
+
export type UseIncremarkReturn = ReturnType<typeof useIncremark>
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Vue 3 Composable: Incremark 流式 Markdown 解析器
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```vue
|
|
21
|
+
* <script setup>
|
|
22
|
+
* import { useIncremark } from '@incremark/vue'
|
|
23
|
+
*
|
|
24
|
+
* const { markdown, blocks, append, finalize } = useIncremark()
|
|
25
|
+
*
|
|
26
|
+
* // 处理 AI 流式输出
|
|
27
|
+
* async function handleStream(stream) {
|
|
28
|
+
* for await (const chunk of stream) {
|
|
29
|
+
* append(chunk)
|
|
30
|
+
* }
|
|
31
|
+
* finalize()
|
|
32
|
+
* }
|
|
33
|
+
* </script>
|
|
34
|
+
*
|
|
35
|
+
* <template>
|
|
36
|
+
* <div>已接收: {{ markdown.length }} 字符</div>
|
|
37
|
+
* </template>
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export function useIncremark(options: UseIncremarkOptions = {}) {
|
|
41
|
+
const parser = createIncremarkParser(options)
|
|
42
|
+
const completedBlocks = shallowRef<ParsedBlock[]>([])
|
|
43
|
+
const pendingBlocks = shallowRef<ParsedBlock[]>([])
|
|
44
|
+
const isLoading = ref(false)
|
|
45
|
+
// 使用 ref 存储 markdown,确保响应式
|
|
46
|
+
const markdown = ref('')
|
|
47
|
+
|
|
48
|
+
const ast = computed<Root>(() => ({
|
|
49
|
+
type: 'root',
|
|
50
|
+
children: [
|
|
51
|
+
...completedBlocks.value.map((b) => b.node),
|
|
52
|
+
...pendingBlocks.value.map((b) => b.node)
|
|
53
|
+
]
|
|
54
|
+
}))
|
|
55
|
+
|
|
56
|
+
// 所有块,带稳定 ID(已完成块用真实 ID,待处理块用索引)
|
|
57
|
+
const blocks = computed(() => {
|
|
58
|
+
const result: Array<ParsedBlock & { stableId: string }> = []
|
|
59
|
+
|
|
60
|
+
for (const block of completedBlocks.value) {
|
|
61
|
+
result.push({ ...block, stableId: block.id })
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
for (let i = 0; i < pendingBlocks.value.length; i++) {
|
|
65
|
+
result.push({
|
|
66
|
+
...pendingBlocks.value[i],
|
|
67
|
+
stableId: `pending-${i}`
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return result
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
function append(chunk: string): IncrementalUpdate {
|
|
75
|
+
isLoading.value = true
|
|
76
|
+
const update = parser.append(chunk)
|
|
77
|
+
|
|
78
|
+
// 更新 markdown ref
|
|
79
|
+
markdown.value = parser.getBuffer()
|
|
80
|
+
|
|
81
|
+
// 使用 markRaw 避免深层响应式
|
|
82
|
+
if (update.completed.length > 0) {
|
|
83
|
+
completedBlocks.value = [
|
|
84
|
+
...completedBlocks.value,
|
|
85
|
+
...update.completed.map((b) => markRaw(b))
|
|
86
|
+
]
|
|
87
|
+
}
|
|
88
|
+
pendingBlocks.value = update.pending.map((b) => markRaw(b))
|
|
89
|
+
|
|
90
|
+
return update
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function finalize(): IncrementalUpdate {
|
|
94
|
+
const update = parser.finalize()
|
|
95
|
+
|
|
96
|
+
// 更新 markdown ref
|
|
97
|
+
markdown.value = parser.getBuffer()
|
|
98
|
+
|
|
99
|
+
if (update.completed.length > 0) {
|
|
100
|
+
completedBlocks.value = [
|
|
101
|
+
...completedBlocks.value,
|
|
102
|
+
...update.completed.map((b) => markRaw(b))
|
|
103
|
+
]
|
|
104
|
+
}
|
|
105
|
+
pendingBlocks.value = []
|
|
106
|
+
isLoading.value = false
|
|
107
|
+
|
|
108
|
+
return update
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function abort(): IncrementalUpdate {
|
|
112
|
+
return finalize()
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function reset(): void {
|
|
116
|
+
parser.reset()
|
|
117
|
+
completedBlocks.value = []
|
|
118
|
+
pendingBlocks.value = []
|
|
119
|
+
markdown.value = ''
|
|
120
|
+
isLoading.value = false
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
/** 已收集的完整 Markdown 字符串 */
|
|
125
|
+
markdown,
|
|
126
|
+
/** 已完成的块列表 */
|
|
127
|
+
completedBlocks,
|
|
128
|
+
/** 待处理的块列表 */
|
|
129
|
+
pendingBlocks,
|
|
130
|
+
/** 当前完整的 AST */
|
|
131
|
+
ast,
|
|
132
|
+
/** 所有块(完成 + 待处理),带稳定 ID */
|
|
133
|
+
blocks,
|
|
134
|
+
/** 是否正在加载 */
|
|
135
|
+
isLoading,
|
|
136
|
+
/** 追加内容 */
|
|
137
|
+
append,
|
|
138
|
+
/** 完成解析 */
|
|
139
|
+
finalize,
|
|
140
|
+
/** 强制中断 */
|
|
141
|
+
abort,
|
|
142
|
+
/** 重置解析器 */
|
|
143
|
+
reset,
|
|
144
|
+
/** 解析器实例 */
|
|
145
|
+
parser
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { ref, computed, type Ref, type ComputedRef } from 'vue'
|
|
2
|
+
import type { ParsedBlock } from '@incremark/core'
|
|
3
|
+
|
|
4
|
+
export interface BlockWithStableId extends ParsedBlock {
|
|
5
|
+
/** 稳定的渲染 ID(用于 Vue key) */
|
|
6
|
+
stableId: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface UseStreamRendererOptions {
|
|
10
|
+
/** 已完成的块 */
|
|
11
|
+
completedBlocks: Ref<ParsedBlock[]>
|
|
12
|
+
/** 待处理的块 */
|
|
13
|
+
pendingBlocks: Ref<ParsedBlock[]>
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface UseStreamRendererReturn {
|
|
17
|
+
/** 带稳定 ID 的已完成块 */
|
|
18
|
+
stableCompletedBlocks: ComputedRef<BlockWithStableId[]>
|
|
19
|
+
/** 带稳定 ID 的待处理块 */
|
|
20
|
+
stablePendingBlocks: ComputedRef<BlockWithStableId[]>
|
|
21
|
+
/** 所有带稳定 ID 的块 */
|
|
22
|
+
allStableBlocks: ComputedRef<BlockWithStableId[]>
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Vue 3 Composable: 流式渲染辅助
|
|
27
|
+
*
|
|
28
|
+
* 为块分配稳定的渲染 ID,确保 Vue 的虚拟 DOM 复用
|
|
29
|
+
*/
|
|
30
|
+
export function useStreamRenderer(options: UseStreamRendererOptions): UseStreamRendererReturn {
|
|
31
|
+
const { completedBlocks, pendingBlocks } = options
|
|
32
|
+
|
|
33
|
+
const stableCompletedBlocks = computed<BlockWithStableId[]>(() =>
|
|
34
|
+
completedBlocks.value.map((block) => ({
|
|
35
|
+
...block,
|
|
36
|
+
stableId: block.id
|
|
37
|
+
}))
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
const stablePendingBlocks = computed<BlockWithStableId[]>(() =>
|
|
41
|
+
pendingBlocks.value.map((block, index) => ({
|
|
42
|
+
...block,
|
|
43
|
+
stableId: `pending-${index}`
|
|
44
|
+
}))
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
const allStableBlocks = computed(() => [...stableCompletedBlocks.value, ...stablePendingBlocks.value])
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
stableCompletedBlocks,
|
|
51
|
+
stablePendingBlocks,
|
|
52
|
+
allStableBlocks
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Composables
|
|
2
|
+
export { useIncremark, useStreamRenderer, useDevTools } from './composables'
|
|
3
|
+
export type { UseIncremarkOptions, UseStreamRendererOptions, UseDevToolsOptions } from './composables'
|
|
4
|
+
|
|
5
|
+
// Components
|
|
6
|
+
export {
|
|
7
|
+
Incremark,
|
|
8
|
+
IncremarkRenderer,
|
|
9
|
+
IncremarkHeading,
|
|
10
|
+
IncremarkParagraph,
|
|
11
|
+
IncremarkCode,
|
|
12
|
+
IncremarkList,
|
|
13
|
+
IncremarkTable,
|
|
14
|
+
IncremarkBlockquote,
|
|
15
|
+
IncremarkThematicBreak,
|
|
16
|
+
IncremarkInline,
|
|
17
|
+
IncremarkMath,
|
|
18
|
+
IncremarkDefault
|
|
19
|
+
} from './components'
|
|
20
|
+
export type { ComponentMap, BlockWithStableId } from './components'
|
|
21
|
+
|
|
22
|
+
// Re-export core types
|
|
23
|
+
export type {
|
|
24
|
+
ParsedBlock,
|
|
25
|
+
IncrementalUpdate,
|
|
26
|
+
ParserOptions,
|
|
27
|
+
BlockStatus,
|
|
28
|
+
Root,
|
|
29
|
+
RootContent
|
|
30
|
+
} from '@incremark/core'
|