@incremark/vue 0.0.4 → 0.1.0
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 +180 -1
- package/dist/index.css +17 -0
- package/dist/index.css.map +1 -1
- package/dist/index.js +460 -48
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/components/AutoScrollContainer.vue +164 -0
- package/src/components/Incremark.vue +3 -1
- package/src/components/IncremarkInline.vue +83 -10
- package/src/components/index.ts +3 -0
- package/src/composables/index.ts +4 -1
- package/src/composables/useBlockTransformer.ts +141 -0
- package/src/composables/useIncremark.ts +239 -27
- package/src/index.ts +37 -4
|
@@ -1,14 +1,62 @@
|
|
|
1
|
-
import { ref, shallowRef, computed, markRaw } from 'vue'
|
|
1
|
+
import { ref, shallowRef, computed, markRaw, watch, onUnmounted, type Ref, type ComputedRef } from 'vue'
|
|
2
2
|
import {
|
|
3
3
|
IncremarkParser,
|
|
4
4
|
createIncremarkParser,
|
|
5
|
+
createBlockTransformer,
|
|
6
|
+
defaultPlugins,
|
|
5
7
|
type ParserOptions,
|
|
6
8
|
type ParsedBlock,
|
|
7
9
|
type IncrementalUpdate,
|
|
8
|
-
type Root
|
|
10
|
+
type Root,
|
|
11
|
+
type RootContent,
|
|
12
|
+
type DisplayBlock,
|
|
13
|
+
type TransformerPlugin,
|
|
14
|
+
type AnimationEffect,
|
|
15
|
+
type BlockTransformer
|
|
9
16
|
} from '@incremark/core'
|
|
10
17
|
|
|
11
|
-
|
|
18
|
+
/** 打字机效果配置 */
|
|
19
|
+
export interface TypewriterOptions {
|
|
20
|
+
/** 是否启用打字机效果(可响应式切换) */
|
|
21
|
+
enabled?: boolean
|
|
22
|
+
/** 每次显示的字符数,可以是固定值或范围 [min, max] */
|
|
23
|
+
charsPerTick?: number | [number, number]
|
|
24
|
+
/** 更新间隔 (ms) */
|
|
25
|
+
tickInterval?: number
|
|
26
|
+
/** 动画效果: 'none' | 'fade-in' | 'typing' */
|
|
27
|
+
effect?: AnimationEffect
|
|
28
|
+
/** 光标字符(仅 typing 效果使用) */
|
|
29
|
+
cursor?: string
|
|
30
|
+
/** 页面不可见时暂停 */
|
|
31
|
+
pauseOnHidden?: boolean
|
|
32
|
+
/** 自定义插件 */
|
|
33
|
+
plugins?: TransformerPlugin[]
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface UseIncremarkOptions extends ParserOptions {
|
|
37
|
+
/** 打字机配置,传入即创建 transformer(可通过 enabled 控制是否启用) */
|
|
38
|
+
typewriter?: TypewriterOptions
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** 打字机控制对象 */
|
|
42
|
+
export interface TypewriterControls {
|
|
43
|
+
/** 是否启用 */
|
|
44
|
+
enabled: Ref<boolean>
|
|
45
|
+
/** 是否正在处理中 */
|
|
46
|
+
isProcessing: ComputedRef<boolean>
|
|
47
|
+
/** 是否已暂停 */
|
|
48
|
+
isPaused: ComputedRef<boolean>
|
|
49
|
+
/** 当前动画效果 */
|
|
50
|
+
effect: ComputedRef<AnimationEffect>
|
|
51
|
+
/** 跳过动画,直接显示全部 */
|
|
52
|
+
skip: () => void
|
|
53
|
+
/** 暂停动画 */
|
|
54
|
+
pause: () => void
|
|
55
|
+
/** 恢复动画 */
|
|
56
|
+
resume: () => void
|
|
57
|
+
/** 动态更新配置 */
|
|
58
|
+
setOptions: (options: Partial<TypewriterOptions>) => void
|
|
59
|
+
}
|
|
12
60
|
|
|
13
61
|
/** useIncremark 的返回类型 */
|
|
14
62
|
export type UseIncremarkReturn = ReturnType<typeof useIncremark>
|
|
@@ -19,32 +67,68 @@ export type UseIncremarkReturn = ReturnType<typeof useIncremark>
|
|
|
19
67
|
* @example
|
|
20
68
|
* ```vue
|
|
21
69
|
* <script setup>
|
|
22
|
-
* import { useIncremark } from '@incremark/vue'
|
|
70
|
+
* import { useIncremark, Incremark } from '@incremark/vue'
|
|
23
71
|
*
|
|
24
|
-
*
|
|
72
|
+
* // 基础用法
|
|
73
|
+
* const { blocks, append, finalize } = useIncremark()
|
|
25
74
|
*
|
|
26
|
-
* //
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
75
|
+
* // 启用打字机效果
|
|
76
|
+
* const { blocks, append, finalize, typewriter } = useIncremark({
|
|
77
|
+
* typewriter: {
|
|
78
|
+
* enabled: true, // 可响应式切换
|
|
79
|
+
* charsPerTick: [1, 3],
|
|
80
|
+
* tickInterval: 30,
|
|
81
|
+
* effect: 'typing',
|
|
82
|
+
* cursor: '|'
|
|
30
83
|
* }
|
|
31
|
-
*
|
|
32
|
-
*
|
|
84
|
+
* })
|
|
85
|
+
*
|
|
86
|
+
* // 动态切换打字机效果
|
|
87
|
+
* typewriter.enabled.value = false
|
|
33
88
|
* </script>
|
|
34
89
|
*
|
|
35
90
|
* <template>
|
|
36
|
-
* <
|
|
91
|
+
* <Incremark :blocks="blocks" />
|
|
92
|
+
* <button v-if="typewriter.isProcessing.value" @click="typewriter.skip">跳过</button>
|
|
37
93
|
* </template>
|
|
38
94
|
* ```
|
|
39
95
|
*/
|
|
40
96
|
export function useIncremark(options: UseIncremarkOptions = {}) {
|
|
97
|
+
// 解析器
|
|
41
98
|
const parser = createIncremarkParser(options)
|
|
42
99
|
const completedBlocks = shallowRef<ParsedBlock[]>([])
|
|
43
100
|
const pendingBlocks = shallowRef<ParsedBlock[]>([])
|
|
44
101
|
const isLoading = ref(false)
|
|
45
|
-
// 使用 ref 存储 markdown,确保响应式
|
|
46
102
|
const markdown = ref('')
|
|
47
103
|
|
|
104
|
+
// 打字机相关状态
|
|
105
|
+
const typewriterEnabled = ref(options.typewriter?.enabled ?? !!options.typewriter)
|
|
106
|
+
const displayBlocksRef = shallowRef<DisplayBlock<RootContent>[]>([])
|
|
107
|
+
const isTypewriterProcessing = ref(false)
|
|
108
|
+
const isTypewriterPaused = ref(false)
|
|
109
|
+
const typewriterEffect = ref<AnimationEffect>(options.typewriter?.effect ?? 'none')
|
|
110
|
+
const typewriterCursor = ref(options.typewriter?.cursor ?? '|')
|
|
111
|
+
|
|
112
|
+
// 创建 transformer(如果有 typewriter 配置)
|
|
113
|
+
let transformer: BlockTransformer<RootContent> | null = null
|
|
114
|
+
|
|
115
|
+
if (options.typewriter) {
|
|
116
|
+
const twOptions = options.typewriter
|
|
117
|
+
transformer = createBlockTransformer<RootContent>({
|
|
118
|
+
charsPerTick: twOptions.charsPerTick ?? [1, 3],
|
|
119
|
+
tickInterval: twOptions.tickInterval ?? 30,
|
|
120
|
+
effect: twOptions.effect ?? 'none',
|
|
121
|
+
pauseOnHidden: twOptions.pauseOnHidden ?? true,
|
|
122
|
+
plugins: twOptions.plugins ?? defaultPlugins,
|
|
123
|
+
onChange: (blocks) => {
|
|
124
|
+
displayBlocksRef.value = blocks
|
|
125
|
+
isTypewriterProcessing.value = transformer?.isProcessing() ?? false
|
|
126
|
+
isTypewriterPaused.value = transformer?.isPausedState() ?? false
|
|
127
|
+
}
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// AST
|
|
48
132
|
const ast = computed<Root>(() => ({
|
|
49
133
|
type: 'root',
|
|
50
134
|
children: [
|
|
@@ -53,8 +137,68 @@ export function useIncremark(options: UseIncremarkOptions = {}) {
|
|
|
53
137
|
]
|
|
54
138
|
}))
|
|
55
139
|
|
|
56
|
-
//
|
|
57
|
-
const
|
|
140
|
+
// 将 completedBlocks 转换为 SourceBlock 格式
|
|
141
|
+
const sourceBlocks = computed(() => {
|
|
142
|
+
return completedBlocks.value.map(block => ({
|
|
143
|
+
id: block.id,
|
|
144
|
+
node: block.node,
|
|
145
|
+
status: block.status as 'pending' | 'stable' | 'completed'
|
|
146
|
+
}))
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
// 监听 sourceBlocks 变化,推送给 transformer
|
|
150
|
+
if (transformer) {
|
|
151
|
+
watch(
|
|
152
|
+
sourceBlocks,
|
|
153
|
+
(blocks) => {
|
|
154
|
+
transformer!.push(blocks)
|
|
155
|
+
|
|
156
|
+
// 更新正在显示的 block
|
|
157
|
+
const currentDisplaying = displayBlocksRef.value.find((b) => !b.isDisplayComplete)
|
|
158
|
+
if (currentDisplaying) {
|
|
159
|
+
const updated = blocks.find((b) => b.id === currentDisplaying.id)
|
|
160
|
+
if (updated) {
|
|
161
|
+
transformer!.update(updated)
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
{ immediate: true, deep: true }
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* 在节点末尾添加光标
|
|
171
|
+
*/
|
|
172
|
+
function addCursorToNode(node: RootContent, cursor: string): RootContent {
|
|
173
|
+
const cloned = JSON.parse(JSON.stringify(node))
|
|
174
|
+
|
|
175
|
+
function addToLast(n: { children?: unknown[]; type?: string; value?: string }): boolean {
|
|
176
|
+
if (n.children && n.children.length > 0) {
|
|
177
|
+
for (let i = n.children.length - 1; i >= 0; i--) {
|
|
178
|
+
if (addToLast(n.children[i] as { children?: unknown[]; type?: string; value?: string })) {
|
|
179
|
+
return true
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
n.children.push({ type: 'text', value: cursor })
|
|
183
|
+
return true
|
|
184
|
+
}
|
|
185
|
+
if (n.type === 'text' && typeof n.value === 'string') {
|
|
186
|
+
n.value += cursor
|
|
187
|
+
return true
|
|
188
|
+
}
|
|
189
|
+
if (typeof n.value === 'string') {
|
|
190
|
+
n.value += cursor
|
|
191
|
+
return true
|
|
192
|
+
}
|
|
193
|
+
return false
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
addToLast(cloned)
|
|
197
|
+
return cloned
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// 原始 blocks(不经过打字机)
|
|
201
|
+
const rawBlocks = computed(() => {
|
|
58
202
|
const result: Array<ParsedBlock & { stableId: string }> = []
|
|
59
203
|
|
|
60
204
|
for (const block of completedBlocks.value) {
|
|
@@ -71,14 +215,43 @@ export function useIncremark(options: UseIncremarkOptions = {}) {
|
|
|
71
215
|
return result
|
|
72
216
|
})
|
|
73
217
|
|
|
218
|
+
// 最终用于渲染的 blocks
|
|
219
|
+
const blocks = computed(() => {
|
|
220
|
+
// 未启用打字机或没有 transformer:返回原始 blocks
|
|
221
|
+
if (!typewriterEnabled.value || !transformer) {
|
|
222
|
+
return rawBlocks.value
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// 启用打字机:使用 displayBlocks
|
|
226
|
+
return displayBlocksRef.value.map((db, index) => {
|
|
227
|
+
const isPending = !db.isDisplayComplete
|
|
228
|
+
const isLastPending = isPending && index === displayBlocksRef.value.length - 1
|
|
229
|
+
|
|
230
|
+
// typing 效果时添加光标
|
|
231
|
+
let node = db.displayNode
|
|
232
|
+
if (typewriterEffect.value === 'typing' && isLastPending) {
|
|
233
|
+
node = addCursorToNode(db.displayNode, typewriterCursor.value)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
id: db.id,
|
|
238
|
+
stableId: db.id,
|
|
239
|
+
status: (db.isDisplayComplete ? 'completed' : 'pending') as 'pending' | 'stable' | 'completed',
|
|
240
|
+
isLastPending,
|
|
241
|
+
node,
|
|
242
|
+
startOffset: 0,
|
|
243
|
+
endOffset: 0,
|
|
244
|
+
rawText: ''
|
|
245
|
+
}
|
|
246
|
+
})
|
|
247
|
+
})
|
|
248
|
+
|
|
74
249
|
function append(chunk: string): IncrementalUpdate {
|
|
75
250
|
isLoading.value = true
|
|
76
251
|
const update = parser.append(chunk)
|
|
77
252
|
|
|
78
|
-
// 更新 markdown ref
|
|
79
253
|
markdown.value = parser.getBuffer()
|
|
80
254
|
|
|
81
|
-
// 使用 markRaw 避免深层响应式
|
|
82
255
|
if (update.completed.length > 0) {
|
|
83
256
|
completedBlocks.value = [
|
|
84
257
|
...completedBlocks.value,
|
|
@@ -93,7 +266,6 @@ export function useIncremark(options: UseIncremarkOptions = {}) {
|
|
|
93
266
|
function finalize(): IncrementalUpdate {
|
|
94
267
|
const update = parser.finalize()
|
|
95
268
|
|
|
96
|
-
// 更新 markdown ref
|
|
97
269
|
markdown.value = parser.getBuffer()
|
|
98
270
|
|
|
99
271
|
if (update.completed.length > 0) {
|
|
@@ -118,17 +290,14 @@ export function useIncremark(options: UseIncremarkOptions = {}) {
|
|
|
118
290
|
pendingBlocks.value = []
|
|
119
291
|
markdown.value = ''
|
|
120
292
|
isLoading.value = false
|
|
293
|
+
|
|
294
|
+
// 重置 transformer
|
|
295
|
+
transformer?.reset()
|
|
121
296
|
}
|
|
122
297
|
|
|
123
|
-
/**
|
|
124
|
-
* 一次性渲染完整 Markdown(reset + append + finalize)
|
|
125
|
-
* @param content 完整的 Markdown 内容
|
|
126
|
-
*/
|
|
127
298
|
function render(content: string): IncrementalUpdate {
|
|
128
|
-
// 调用 parser 的 render 方法
|
|
129
299
|
const update = parser.render(content)
|
|
130
300
|
|
|
131
|
-
// 同步状态
|
|
132
301
|
markdown.value = parser.getBuffer()
|
|
133
302
|
completedBlocks.value = parser.getCompletedBlocks().map(b => markRaw(b))
|
|
134
303
|
pendingBlocks.value = []
|
|
@@ -137,6 +306,47 @@ export function useIncremark(options: UseIncremarkOptions = {}) {
|
|
|
137
306
|
return update
|
|
138
307
|
}
|
|
139
308
|
|
|
309
|
+
// 打字机控制对象
|
|
310
|
+
const typewriter: TypewriterControls = {
|
|
311
|
+
enabled: typewriterEnabled,
|
|
312
|
+
isProcessing: computed(() => isTypewriterProcessing.value),
|
|
313
|
+
isPaused: computed(() => isTypewriterPaused.value),
|
|
314
|
+
effect: computed(() => typewriterEffect.value),
|
|
315
|
+
skip: () => transformer?.skip(),
|
|
316
|
+
pause: () => {
|
|
317
|
+
transformer?.pause()
|
|
318
|
+
isTypewriterPaused.value = true
|
|
319
|
+
},
|
|
320
|
+
resume: () => {
|
|
321
|
+
transformer?.resume()
|
|
322
|
+
isTypewriterPaused.value = false
|
|
323
|
+
},
|
|
324
|
+
setOptions: (opts) => {
|
|
325
|
+
if (opts.enabled !== undefined) {
|
|
326
|
+
typewriterEnabled.value = opts.enabled
|
|
327
|
+
}
|
|
328
|
+
if (opts.charsPerTick !== undefined || opts.tickInterval !== undefined || opts.effect !== undefined || opts.pauseOnHidden !== undefined) {
|
|
329
|
+
transformer?.setOptions({
|
|
330
|
+
charsPerTick: opts.charsPerTick,
|
|
331
|
+
tickInterval: opts.tickInterval,
|
|
332
|
+
effect: opts.effect,
|
|
333
|
+
pauseOnHidden: opts.pauseOnHidden
|
|
334
|
+
})
|
|
335
|
+
}
|
|
336
|
+
if (opts.effect !== undefined) {
|
|
337
|
+
typewriterEffect.value = opts.effect
|
|
338
|
+
}
|
|
339
|
+
if (opts.cursor !== undefined) {
|
|
340
|
+
typewriterCursor.value = opts.cursor
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// 清理
|
|
346
|
+
onUnmounted(() => {
|
|
347
|
+
transformer?.destroy()
|
|
348
|
+
})
|
|
349
|
+
|
|
140
350
|
return {
|
|
141
351
|
/** 已收集的完整 Markdown 字符串 */
|
|
142
352
|
markdown,
|
|
@@ -146,7 +356,7 @@ export function useIncremark(options: UseIncremarkOptions = {}) {
|
|
|
146
356
|
pendingBlocks,
|
|
147
357
|
/** 当前完整的 AST */
|
|
148
358
|
ast,
|
|
149
|
-
/**
|
|
359
|
+
/** 用于渲染的 blocks(根据打字机设置自动处理) */
|
|
150
360
|
blocks,
|
|
151
361
|
/** 是否正在加载 */
|
|
152
362
|
isLoading,
|
|
@@ -156,11 +366,13 @@ export function useIncremark(options: UseIncremarkOptions = {}) {
|
|
|
156
366
|
finalize,
|
|
157
367
|
/** 强制中断 */
|
|
158
368
|
abort,
|
|
159
|
-
/**
|
|
369
|
+
/** 重置解析器和打字机 */
|
|
160
370
|
reset,
|
|
161
371
|
/** 一次性渲染(reset + append + finalize) */
|
|
162
372
|
render,
|
|
163
373
|
/** 解析器实例 */
|
|
164
|
-
parser
|
|
374
|
+
parser,
|
|
375
|
+
/** 打字机控制 */
|
|
376
|
+
typewriter
|
|
165
377
|
}
|
|
166
378
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
// Composables
|
|
2
|
-
export { useIncremark, useStreamRenderer, useDevTools } from './composables'
|
|
3
|
-
export type {
|
|
2
|
+
export { useIncremark, useStreamRenderer, useDevTools, useBlockTransformer } from './composables'
|
|
3
|
+
export type {
|
|
4
|
+
UseIncremarkOptions,
|
|
5
|
+
TypewriterOptions,
|
|
6
|
+
TypewriterControls,
|
|
7
|
+
UseStreamRendererOptions,
|
|
8
|
+
UseDevToolsOptions,
|
|
9
|
+
UseBlockTransformerOptions,
|
|
10
|
+
UseBlockTransformerReturn
|
|
11
|
+
} from './composables'
|
|
4
12
|
|
|
5
13
|
// Components
|
|
6
14
|
export {
|
|
@@ -15,7 +23,8 @@ export {
|
|
|
15
23
|
IncremarkThematicBreak,
|
|
16
24
|
IncremarkInline,
|
|
17
25
|
IncremarkMath,
|
|
18
|
-
IncremarkDefault
|
|
26
|
+
IncremarkDefault,
|
|
27
|
+
AutoScrollContainer
|
|
19
28
|
} from './components'
|
|
20
29
|
export type { ComponentMap, BlockWithStableId } from './components'
|
|
21
30
|
|
|
@@ -26,5 +35,29 @@ export type {
|
|
|
26
35
|
ParserOptions,
|
|
27
36
|
BlockStatus,
|
|
28
37
|
Root,
|
|
29
|
-
RootContent
|
|
38
|
+
RootContent,
|
|
39
|
+
// Transformer types
|
|
40
|
+
SourceBlock,
|
|
41
|
+
DisplayBlock,
|
|
42
|
+
TransformerPlugin,
|
|
43
|
+
TransformerOptions,
|
|
44
|
+
TransformerState,
|
|
45
|
+
AnimationEffect
|
|
46
|
+
} from '@incremark/core'
|
|
47
|
+
|
|
48
|
+
// Re-export transformer utilities and plugins
|
|
49
|
+
export {
|
|
50
|
+
BlockTransformer,
|
|
51
|
+
createBlockTransformer,
|
|
52
|
+
countChars,
|
|
53
|
+
sliceAst,
|
|
54
|
+
cloneNode,
|
|
55
|
+
codeBlockPlugin,
|
|
56
|
+
mermaidPlugin,
|
|
57
|
+
imagePlugin,
|
|
58
|
+
mathPlugin,
|
|
59
|
+
thematicBreakPlugin,
|
|
60
|
+
defaultPlugins,
|
|
61
|
+
allPlugins,
|
|
62
|
+
createPlugin
|
|
30
63
|
} from '@incremark/core'
|