@incremark/vue 0.1.2 → 0.2.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/dist/index.css +5 -356
- package/dist/index.css.map +1 -1
- package/dist/index.js +1181 -489
- package/dist/index.js.map +1 -1
- package/package.json +6 -4
- package/src/ThemeProvider.vue +41 -0
- package/src/components/Incremark.vue +53 -23
- package/src/components/IncremarkBlockquote.vue +0 -22
- package/src/components/IncremarkCode.vue +3 -161
- package/src/components/IncremarkDefault.vue +0 -27
- package/src/components/IncremarkFootnotes.vue +78 -0
- package/src/components/IncremarkHeading.vue +1 -17
- package/src/components/IncremarkHtmlElement.vue +138 -0
- package/src/components/IncremarkInline.vue +95 -56
- package/src/components/IncremarkList.vue +0 -37
- package/src/components/IncremarkMath.vue +0 -45
- package/src/components/IncremarkParagraph.vue +0 -7
- package/src/components/IncremarkRenderer.vue +15 -3
- package/src/components/IncremarkTable.vue +0 -32
- package/src/components/IncremarkThematicBreak.vue +0 -8
- package/src/components/index.ts +2 -0
- package/src/composables/useDefinationsContext.ts +16 -0
- package/src/composables/useIncremark.ts +47 -187
- package/src/composables/useProvideDefinations.ts +61 -0
- package/src/composables/useTypewriter.ts +205 -0
- package/src/index.ts +15 -0
- package/src/utils/cursor.ts +46 -0
|
@@ -1,19 +1,15 @@
|
|
|
1
|
-
import { ref, shallowRef, computed, markRaw,
|
|
1
|
+
import { ref, shallowRef, computed, markRaw, type ComputedRef } from 'vue'
|
|
2
2
|
import {
|
|
3
|
-
IncremarkParser,
|
|
4
3
|
createIncremarkParser,
|
|
5
|
-
createBlockTransformer,
|
|
6
|
-
defaultPlugins,
|
|
7
4
|
type ParserOptions,
|
|
8
5
|
type ParsedBlock,
|
|
9
6
|
type IncrementalUpdate,
|
|
10
7
|
type Root,
|
|
11
|
-
type RootContent,
|
|
12
|
-
type DisplayBlock,
|
|
13
8
|
type TransformerPlugin,
|
|
14
|
-
type AnimationEffect
|
|
15
|
-
type BlockTransformer
|
|
9
|
+
type AnimationEffect
|
|
16
10
|
} from '@incremark/core'
|
|
11
|
+
import { useProvideDefinations } from './useProvideDefinations'
|
|
12
|
+
import { useTypewriter } from './useTypewriter'
|
|
17
13
|
|
|
18
14
|
/** 打字机效果配置 */
|
|
19
15
|
export interface TypewriterOptions {
|
|
@@ -40,8 +36,10 @@ export interface UseIncremarkOptions extends ParserOptions {
|
|
|
40
36
|
|
|
41
37
|
/** 打字机控制对象 */
|
|
42
38
|
export interface TypewriterControls {
|
|
43
|
-
/**
|
|
44
|
-
enabled:
|
|
39
|
+
/** 是否启用(只读) */
|
|
40
|
+
enabled: ComputedRef<boolean>
|
|
41
|
+
/** 设置是否启用 */
|
|
42
|
+
setEnabled: (enabled: boolean) => void
|
|
45
43
|
/** 是否正在处理中 */
|
|
46
44
|
isProcessing: ComputedRef<boolean>
|
|
47
45
|
/** 是否已暂停 */
|
|
@@ -94,39 +92,33 @@ export type UseIncremarkReturn = ReturnType<typeof useIncremark>
|
|
|
94
92
|
* ```
|
|
95
93
|
*/
|
|
96
94
|
export function useIncremark(options: UseIncremarkOptions = {}) {
|
|
95
|
+
// 内部自动提供 definitions context
|
|
96
|
+
const { setDefinations, setFootnoteDefinitions, setFootnoteReferenceOrder } = useProvideDefinations()
|
|
97
|
+
|
|
97
98
|
// 解析器
|
|
98
|
-
const parser = createIncremarkParser(
|
|
99
|
+
const parser = createIncremarkParser({
|
|
100
|
+
...options,
|
|
101
|
+
onChange: (state) => {
|
|
102
|
+
setDefinations(state.definitions)
|
|
103
|
+
setFootnoteDefinitions(state.footnoteDefinitions)
|
|
104
|
+
// 调用用户提供的 onChange
|
|
105
|
+
options.onChange?.(state)
|
|
106
|
+
}
|
|
107
|
+
})
|
|
108
|
+
|
|
99
109
|
const completedBlocks = shallowRef<ParsedBlock[]>([])
|
|
100
110
|
const pendingBlocks = shallowRef<ParsedBlock[]>([])
|
|
101
111
|
const isLoading = ref(false)
|
|
102
112
|
const markdown = ref('')
|
|
113
|
+
const isFinalized = ref(false)
|
|
114
|
+
const footnoteReferenceOrder = ref<string[]>([])
|
|
103
115
|
|
|
104
|
-
//
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
}
|
|
116
|
+
// 使用 useTypewriter composable 管理打字机效果
|
|
117
|
+
const { blocks, typewriter, transformer } = useTypewriter({
|
|
118
|
+
typewriter: options.typewriter,
|
|
119
|
+
completedBlocks,
|
|
120
|
+
pendingBlocks
|
|
121
|
+
})
|
|
130
122
|
|
|
131
123
|
// AST
|
|
132
124
|
const ast = computed<Root>(() => ({
|
|
@@ -137,115 +129,6 @@ export function useIncremark(options: UseIncremarkOptions = {}) {
|
|
|
137
129
|
]
|
|
138
130
|
}))
|
|
139
131
|
|
|
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(() => {
|
|
202
|
-
const result: Array<ParsedBlock & { stableId: string }> = []
|
|
203
|
-
|
|
204
|
-
for (const block of completedBlocks.value) {
|
|
205
|
-
result.push({ ...block, stableId: block.id })
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
for (let i = 0; i < pendingBlocks.value.length; i++) {
|
|
209
|
-
result.push({
|
|
210
|
-
...pendingBlocks.value[i],
|
|
211
|
-
stableId: `pending-${i}`
|
|
212
|
-
})
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
return result
|
|
216
|
-
})
|
|
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
|
-
|
|
249
132
|
function append(chunk: string): IncrementalUpdate {
|
|
250
133
|
isLoading.value = true
|
|
251
134
|
const update = parser.append(chunk)
|
|
@@ -260,6 +143,10 @@ export function useIncremark(options: UseIncremarkOptions = {}) {
|
|
|
260
143
|
}
|
|
261
144
|
pendingBlocks.value = update.pending.map((b) => markRaw(b))
|
|
262
145
|
|
|
146
|
+
// 更新脚注引用顺序
|
|
147
|
+
footnoteReferenceOrder.value = update.footnoteReferenceOrder
|
|
148
|
+
setFootnoteReferenceOrder(update.footnoteReferenceOrder)
|
|
149
|
+
|
|
263
150
|
return update
|
|
264
151
|
}
|
|
265
152
|
|
|
@@ -276,6 +163,11 @@ export function useIncremark(options: UseIncremarkOptions = {}) {
|
|
|
276
163
|
}
|
|
277
164
|
pendingBlocks.value = []
|
|
278
165
|
isLoading.value = false
|
|
166
|
+
isFinalized.value = true
|
|
167
|
+
|
|
168
|
+
// 更新脚注引用顺序
|
|
169
|
+
footnoteReferenceOrder.value = update.footnoteReferenceOrder
|
|
170
|
+
setFootnoteReferenceOrder(update.footnoteReferenceOrder)
|
|
279
171
|
|
|
280
172
|
return update
|
|
281
173
|
}
|
|
@@ -290,6 +182,8 @@ export function useIncremark(options: UseIncremarkOptions = {}) {
|
|
|
290
182
|
pendingBlocks.value = []
|
|
291
183
|
markdown.value = ''
|
|
292
184
|
isLoading.value = false
|
|
185
|
+
isFinalized.value = false
|
|
186
|
+
footnoteReferenceOrder.value = []
|
|
293
187
|
|
|
294
188
|
// 重置 transformer
|
|
295
189
|
transformer?.reset()
|
|
@@ -302,51 +196,13 @@ export function useIncremark(options: UseIncremarkOptions = {}) {
|
|
|
302
196
|
completedBlocks.value = parser.getCompletedBlocks().map(b => markRaw(b))
|
|
303
197
|
pendingBlocks.value = []
|
|
304
198
|
isLoading.value = false
|
|
199
|
+
isFinalized.value = true
|
|
200
|
+
footnoteReferenceOrder.value = update.footnoteReferenceOrder
|
|
201
|
+
setFootnoteReferenceOrder(update.footnoteReferenceOrder)
|
|
305
202
|
|
|
306
203
|
return update
|
|
307
204
|
}
|
|
308
205
|
|
|
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
|
-
|
|
350
206
|
return {
|
|
351
207
|
/** 已收集的完整 Markdown 字符串 */
|
|
352
208
|
markdown,
|
|
@@ -360,6 +216,10 @@ export function useIncremark(options: UseIncremarkOptions = {}) {
|
|
|
360
216
|
blocks,
|
|
361
217
|
/** 是否正在加载 */
|
|
362
218
|
isLoading,
|
|
219
|
+
/** 是否已完成(finalize) */
|
|
220
|
+
isFinalized,
|
|
221
|
+
/** 脚注引用的出现顺序 */
|
|
222
|
+
footnoteReferenceOrder,
|
|
363
223
|
/** 追加内容 */
|
|
364
224
|
append,
|
|
365
225
|
/** 完成解析 */
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { ref, provide, type InjectionKey, type Ref } from 'vue';
|
|
2
|
+
import type { Definition, FootnoteDefinition } from 'mdast';
|
|
3
|
+
|
|
4
|
+
export const definationsInjectionKey: InjectionKey<{
|
|
5
|
+
definations: Ref<Record<string, Definition>>
|
|
6
|
+
footnoteDefinitions: Ref<Record<string, FootnoteDefinition>>
|
|
7
|
+
footnoteReferenceOrder: Ref<string[]>
|
|
8
|
+
}> = Symbol('provideDefinations');
|
|
9
|
+
|
|
10
|
+
export function useProvideDefinations() {
|
|
11
|
+
const definations = ref<Record<string, Definition>>({});
|
|
12
|
+
const footnoteDefinitions = ref<Record<string, FootnoteDefinition>>({});
|
|
13
|
+
const footnoteReferenceOrder = ref<string[]>([]);
|
|
14
|
+
|
|
15
|
+
provide(definationsInjectionKey, {
|
|
16
|
+
definations,
|
|
17
|
+
footnoteDefinitions,
|
|
18
|
+
footnoteReferenceOrder
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
function setDefinations(definitions: Record<string, Definition>) {
|
|
22
|
+
definations.value = definitions;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function setFootnoteDefinitions(definitions: Record<string, FootnoteDefinition>) {
|
|
26
|
+
footnoteDefinitions.value = definitions;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function setFootnoteReferenceOrder(order: string[]) {
|
|
30
|
+
footnoteReferenceOrder.value = order;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function clearDefinations() {
|
|
34
|
+
definations.value = {};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function clearFootnoteDefinitions() {
|
|
38
|
+
footnoteDefinitions.value = {};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function clearFootnoteReferenceOrder() {
|
|
42
|
+
footnoteReferenceOrder.value = [];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function clearAllDefinations() {
|
|
46
|
+
clearDefinations();
|
|
47
|
+
clearFootnoteDefinitions();
|
|
48
|
+
clearFootnoteReferenceOrder();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
setDefinations,
|
|
53
|
+
setFootnoteDefinitions,
|
|
54
|
+
setFootnoteReferenceOrder,
|
|
55
|
+
clearDefinations,
|
|
56
|
+
clearFootnoteDefinitions,
|
|
57
|
+
clearFootnoteReferenceOrder,
|
|
58
|
+
clearAllDefinations
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file useTypewriter Composable - 打字机效果管理
|
|
3
|
+
*
|
|
4
|
+
* @description
|
|
5
|
+
* 管理打字机效果的状态和控制逻辑,从 useIncremark 中拆分出来以简化代码。
|
|
6
|
+
*
|
|
7
|
+
* @author Incremark Team
|
|
8
|
+
* @license MIT
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { ref, shallowRef, computed, watch, onUnmounted, type Ref, type ComputedRef } from 'vue'
|
|
12
|
+
import {
|
|
13
|
+
createBlockTransformer,
|
|
14
|
+
defaultPlugins,
|
|
15
|
+
type RootContent,
|
|
16
|
+
type ParsedBlock,
|
|
17
|
+
type DisplayBlock,
|
|
18
|
+
type AnimationEffect,
|
|
19
|
+
type BlockTransformer
|
|
20
|
+
} from '@incremark/core'
|
|
21
|
+
import type { TypewriterOptions, TypewriterControls } from './useIncremark'
|
|
22
|
+
import { addCursorToNode } from '../utils/cursor'
|
|
23
|
+
|
|
24
|
+
export interface UseTypewriterOptions {
|
|
25
|
+
typewriter?: TypewriterOptions
|
|
26
|
+
completedBlocks: Ref<ParsedBlock[]>
|
|
27
|
+
pendingBlocks: Ref<ParsedBlock[]>
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface UseTypewriterReturn {
|
|
31
|
+
/** 用于渲染的 blocks(经过打字机处理或原始blocks) */
|
|
32
|
+
blocks: ComputedRef<Array<ParsedBlock & { stableId: string }>>
|
|
33
|
+
/** 打字机控制对象 */
|
|
34
|
+
typewriter: TypewriterControls
|
|
35
|
+
/** transformer 实例 */
|
|
36
|
+
transformer: BlockTransformer<RootContent> | null
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* useTypewriter Composable
|
|
41
|
+
*
|
|
42
|
+
* @description
|
|
43
|
+
* 管理打字机效果的所有状态和逻辑。
|
|
44
|
+
*
|
|
45
|
+
* @param options - 打字机配置和数据
|
|
46
|
+
* @returns 打字机状态和控制对象
|
|
47
|
+
*/
|
|
48
|
+
export function useTypewriter(options: UseTypewriterOptions): UseTypewriterReturn {
|
|
49
|
+
const { typewriter: typewriterConfig, completedBlocks, pendingBlocks } = options
|
|
50
|
+
|
|
51
|
+
// 打字机状态
|
|
52
|
+
const typewriterEnabled = ref(typewriterConfig?.enabled ?? !!typewriterConfig)
|
|
53
|
+
const displayBlocksRef = shallowRef<DisplayBlock<RootContent>[]>([])
|
|
54
|
+
const isTypewriterProcessing = ref(false)
|
|
55
|
+
const isTypewriterPaused = ref(false)
|
|
56
|
+
const typewriterEffect = ref<AnimationEffect>(typewriterConfig?.effect ?? 'none')
|
|
57
|
+
const typewriterCursor = ref(typewriterConfig?.cursor ?? '|')
|
|
58
|
+
|
|
59
|
+
// 创建 transformer(如果有 typewriter 配置)
|
|
60
|
+
let transformer: BlockTransformer<RootContent> | null = null
|
|
61
|
+
|
|
62
|
+
if (typewriterConfig) {
|
|
63
|
+
const twOptions = typewriterConfig
|
|
64
|
+
transformer = createBlockTransformer<RootContent>({
|
|
65
|
+
charsPerTick: twOptions.charsPerTick ?? [1, 3],
|
|
66
|
+
tickInterval: twOptions.tickInterval ?? 30,
|
|
67
|
+
effect: twOptions.effect ?? 'none',
|
|
68
|
+
pauseOnHidden: twOptions.pauseOnHidden ?? true,
|
|
69
|
+
plugins: twOptions.plugins ?? defaultPlugins,
|
|
70
|
+
onChange: (blocks) => {
|
|
71
|
+
displayBlocksRef.value = blocks as DisplayBlock<RootContent>[]
|
|
72
|
+
isTypewriterProcessing.value = transformer?.isProcessing() ?? false
|
|
73
|
+
isTypewriterPaused.value = transformer?.isPausedState() ?? false
|
|
74
|
+
}
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// 将 completedBlocks 转换为 SourceBlock 格式
|
|
79
|
+
const sourceBlocks = computed(() => {
|
|
80
|
+
return completedBlocks.value.map(block => ({
|
|
81
|
+
id: block.id,
|
|
82
|
+
node: block.node,
|
|
83
|
+
status: block.status as 'pending' | 'stable' | 'completed'
|
|
84
|
+
}))
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
// 监听 sourceBlocks 变化,推送给 transformer
|
|
88
|
+
if (transformer) {
|
|
89
|
+
watch(
|
|
90
|
+
sourceBlocks,
|
|
91
|
+
(blocks) => {
|
|
92
|
+
transformer!.push(blocks)
|
|
93
|
+
|
|
94
|
+
// 更新正在显示的 block
|
|
95
|
+
const currentDisplaying = displayBlocksRef.value.find((b) => !b.isDisplayComplete)
|
|
96
|
+
if (currentDisplaying) {
|
|
97
|
+
const updated = blocks.find((b) => b.id === currentDisplaying.id)
|
|
98
|
+
if (updated) {
|
|
99
|
+
transformer!.update(updated)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
{ immediate: true, deep: true }
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// 原始 blocks(不经过打字机)
|
|
108
|
+
const rawBlocks = computed(() => {
|
|
109
|
+
const result: Array<ParsedBlock & { stableId: string }> = []
|
|
110
|
+
|
|
111
|
+
for (const block of completedBlocks.value) {
|
|
112
|
+
result.push({ ...block, stableId: block.id })
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
for (let i = 0; i < pendingBlocks.value.length; i++) {
|
|
116
|
+
result.push({
|
|
117
|
+
...pendingBlocks.value[i],
|
|
118
|
+
stableId: `pending-${i}`
|
|
119
|
+
})
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return result
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
// 最终用于渲染的 blocks
|
|
126
|
+
const blocks = computed(() => {
|
|
127
|
+
// 未启用打字机或没有 transformer:返回原始 blocks
|
|
128
|
+
if (!typewriterEnabled.value || !transformer) {
|
|
129
|
+
return rawBlocks.value
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// 启用打字机:使用 displayBlocks
|
|
133
|
+
return displayBlocksRef.value.map((db, index) => {
|
|
134
|
+
const isPending = !db.isDisplayComplete
|
|
135
|
+
const isLastPending = isPending && index === displayBlocksRef.value.length - 1
|
|
136
|
+
|
|
137
|
+
// typing 效果时添加光标
|
|
138
|
+
let node = db.displayNode
|
|
139
|
+
if (typewriterEffect.value === 'typing' && isLastPending) {
|
|
140
|
+
node = addCursorToNode(db.displayNode, typewriterCursor.value)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
id: db.id,
|
|
145
|
+
stableId: db.id,
|
|
146
|
+
status: (db.isDisplayComplete ? 'completed' : 'pending') as 'pending' | 'stable' | 'completed',
|
|
147
|
+
isLastPending,
|
|
148
|
+
node,
|
|
149
|
+
startOffset: 0,
|
|
150
|
+
endOffset: 0,
|
|
151
|
+
rawText: ''
|
|
152
|
+
}
|
|
153
|
+
})
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
// 打字机控制对象
|
|
157
|
+
const typewriterControls: TypewriterControls = {
|
|
158
|
+
enabled: computed(() => typewriterEnabled.value),
|
|
159
|
+
setEnabled: (value: boolean) => {
|
|
160
|
+
typewriterEnabled.value = value
|
|
161
|
+
},
|
|
162
|
+
isProcessing: computed(() => isTypewriterProcessing.value),
|
|
163
|
+
isPaused: computed(() => isTypewriterPaused.value),
|
|
164
|
+
effect: computed(() => typewriterEffect.value),
|
|
165
|
+
skip: () => transformer?.skip(),
|
|
166
|
+
pause: () => {
|
|
167
|
+
transformer?.pause()
|
|
168
|
+
isTypewriterPaused.value = true
|
|
169
|
+
},
|
|
170
|
+
resume: () => {
|
|
171
|
+
transformer?.resume()
|
|
172
|
+
isTypewriterPaused.value = false
|
|
173
|
+
},
|
|
174
|
+
setOptions: (opts) => {
|
|
175
|
+
if (opts.enabled !== undefined) {
|
|
176
|
+
typewriterEnabled.value = opts.enabled
|
|
177
|
+
}
|
|
178
|
+
if (opts.charsPerTick !== undefined || opts.tickInterval !== undefined || opts.effect !== undefined || opts.pauseOnHidden !== undefined) {
|
|
179
|
+
transformer?.setOptions({
|
|
180
|
+
charsPerTick: opts.charsPerTick,
|
|
181
|
+
tickInterval: opts.tickInterval,
|
|
182
|
+
effect: opts.effect,
|
|
183
|
+
pauseOnHidden: opts.pauseOnHidden
|
|
184
|
+
})
|
|
185
|
+
}
|
|
186
|
+
if (opts.effect !== undefined) {
|
|
187
|
+
typewriterEffect.value = opts.effect
|
|
188
|
+
}
|
|
189
|
+
if (opts.cursor !== undefined) {
|
|
190
|
+
typewriterCursor.value = opts.cursor
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// 清理
|
|
196
|
+
onUnmounted(() => {
|
|
197
|
+
transformer?.destroy()
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
blocks,
|
|
202
|
+
typewriter: typewriterControls,
|
|
203
|
+
transformer
|
|
204
|
+
}
|
|
205
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// Composables
|
|
2
2
|
export { useIncremark, useStreamRenderer, useDevTools, useBlockTransformer } from './composables'
|
|
3
|
+
export { useProvideDefinations } from './composables/useProvideDefinations'
|
|
4
|
+
export { useDefinationsContext } from './composables/useDefinationsContext'
|
|
3
5
|
export type {
|
|
4
6
|
UseIncremarkOptions,
|
|
5
7
|
TypewriterOptions,
|
|
@@ -23,10 +25,13 @@ export {
|
|
|
23
25
|
IncremarkThematicBreak,
|
|
24
26
|
IncremarkInline,
|
|
25
27
|
IncremarkMath,
|
|
28
|
+
IncremarkHtmlElement,
|
|
26
29
|
IncremarkDefault,
|
|
30
|
+
IncremarkFootnotes,
|
|
27
31
|
AutoScrollContainer
|
|
28
32
|
} from './components'
|
|
29
33
|
export type { ComponentMap, BlockWithStableId } from './components'
|
|
34
|
+
export { default as ThemeProvider } from './ThemeProvider.vue'
|
|
30
35
|
|
|
31
36
|
// Re-export core types
|
|
32
37
|
export type {
|
|
@@ -61,3 +66,13 @@ export {
|
|
|
61
66
|
allPlugins,
|
|
62
67
|
createPlugin
|
|
63
68
|
} from '@incremark/core'
|
|
69
|
+
|
|
70
|
+
// Re-export theme utilities
|
|
71
|
+
export {
|
|
72
|
+
type DesignTokens,
|
|
73
|
+
defaultTheme,
|
|
74
|
+
darkTheme,
|
|
75
|
+
generateCSSVars,
|
|
76
|
+
mergeTheme,
|
|
77
|
+
applyTheme
|
|
78
|
+
} from '@incremark/theme'
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Cursor Utils - 光标工具函数
|
|
3
|
+
*
|
|
4
|
+
* @description
|
|
5
|
+
* 用于在 AST 节点末尾添加光标的工具函数。
|
|
6
|
+
*
|
|
7
|
+
* @author Incremark Team
|
|
8
|
+
* @license MIT
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { RootContent } from '@incremark/core'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 在节点末尾添加光标
|
|
15
|
+
*
|
|
16
|
+
* @param node - 要添加光标的节点
|
|
17
|
+
* @param cursor - 光标字符
|
|
18
|
+
* @returns 添加了光标的新节点
|
|
19
|
+
*/
|
|
20
|
+
export function addCursorToNode(node: RootContent, cursor: string): RootContent {
|
|
21
|
+
const cloned = JSON.parse(JSON.stringify(node))
|
|
22
|
+
|
|
23
|
+
function addToLast(n: { children?: unknown[]; type?: string; value?: string }): boolean {
|
|
24
|
+
if (n.children && n.children.length > 0) {
|
|
25
|
+
for (let i = n.children.length - 1; i >= 0; i--) {
|
|
26
|
+
if (addToLast(n.children[i] as { children?: unknown[]; type?: string; value?: string })) {
|
|
27
|
+
return true
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
n.children.push({ type: 'text', value: cursor })
|
|
31
|
+
return true
|
|
32
|
+
}
|
|
33
|
+
if (n.type === 'text' && typeof n.value === 'string') {
|
|
34
|
+
n.value += cursor
|
|
35
|
+
return true
|
|
36
|
+
}
|
|
37
|
+
if (typeof n.value === 'string') {
|
|
38
|
+
n.value += cursor
|
|
39
|
+
return true
|
|
40
|
+
}
|
|
41
|
+
return false
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
addToLast(cloned)
|
|
45
|
+
return cloned
|
|
46
|
+
}
|