@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.
@@ -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'