@incremark/vue 0.2.2 → 0.2.3
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/ThemeProvider.vue.d.ts +25 -0
- package/dist/components/AutoScrollContainer.vue.d.ts +31 -0
- package/dist/components/Incremark.vue.d.ts +30 -0
- package/dist/components/IncremarkBlockquote.vue.d.ts +6 -0
- package/dist/components/IncremarkCode.vue.d.ts +16 -0
- package/dist/components/IncremarkDefault.vue.d.ts +6 -0
- package/dist/components/IncremarkFootnotes.vue.d.ts +2 -0
- package/dist/components/IncremarkHeading.vue.d.ts +6 -0
- package/dist/components/IncremarkHtmlElement.vue.d.ts +20 -0
- package/dist/components/IncremarkInline.vue.d.ts +6 -0
- package/dist/components/IncremarkList.vue.d.ts +6 -0
- package/dist/components/IncremarkMath.vue.d.ts +17 -0
- package/dist/components/IncremarkParagraph.vue.d.ts +6 -0
- package/dist/components/IncremarkRenderer.vue.d.ts +6 -0
- package/dist/components/IncremarkTable.vue.d.ts +6 -0
- package/dist/components/IncremarkThematicBreak.vue.d.ts +2 -0
- package/{src/components/index.ts → dist/components/index.d.ts} +16 -23
- package/dist/composables/index.d.ts +8 -0
- package/dist/composables/useBlockTransformer.d.ts +68 -0
- package/dist/composables/useDefinationsContext.d.ts +9 -0
- package/dist/composables/useDevTools.d.ts +18 -0
- package/dist/composables/useIncremark.d.ts +112 -0
- package/dist/composables/useProvideDefinations.d.ts +16 -0
- package/dist/composables/useStreamRenderer.d.ts +26 -0
- package/dist/composables/useTypewriter.d.ts +37 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +1 -2
- package/dist/index.js.map +1 -1
- package/dist/utils/cursor.d.ts +18 -0
- package/package.json +11 -13
- package/dist/index.css +0 -6
- package/dist/index.css.map +0 -1
- package/src/ThemeProvider.vue +0 -41
- package/src/components/AutoScrollContainer.vue +0 -164
- package/src/components/Incremark.vue +0 -131
- package/src/components/IncremarkBlockquote.vue +0 -18
- package/src/components/IncremarkCode.vue +0 -236
- package/src/components/IncremarkDefault.vue +0 -15
- package/src/components/IncremarkFootnotes.vue +0 -78
- package/src/components/IncremarkHeading.vue +0 -17
- package/src/components/IncremarkHtmlElement.vue +0 -127
- package/src/components/IncremarkInline.vue +0 -187
- package/src/components/IncremarkList.vue +0 -46
- package/src/components/IncremarkMath.vue +0 -105
- package/src/components/IncremarkParagraph.vue +0 -14
- package/src/components/IncremarkRenderer.vue +0 -50
- package/src/components/IncremarkTable.vue +0 -42
- package/src/components/IncremarkThematicBreak.vue +0 -8
- package/src/composables/index.ts +0 -11
- package/src/composables/useBlockTransformer.ts +0 -141
- package/src/composables/useDefinationsContext.ts +0 -16
- package/src/composables/useDevTools.ts +0 -54
- package/src/composables/useIncremark.ts +0 -238
- package/src/composables/useProvideDefinations.ts +0 -61
- package/src/composables/useStreamRenderer.ts +0 -55
- package/src/composables/useTypewriter.ts +0 -205
- package/src/index.ts +0 -78
- package/src/utils/cursor.ts +0 -46
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Cursor Utils - 光标工具函数
|
|
3
|
+
*
|
|
4
|
+
* @description
|
|
5
|
+
* 用于在 AST 节点末尾添加光标的工具函数。
|
|
6
|
+
*
|
|
7
|
+
* @author Incremark Team
|
|
8
|
+
* @license MIT
|
|
9
|
+
*/
|
|
10
|
+
import type { RootContent } from '@incremark/core';
|
|
11
|
+
/**
|
|
12
|
+
* 在节点末尾添加光标
|
|
13
|
+
*
|
|
14
|
+
* @param node - 要添加光标的节点
|
|
15
|
+
* @param cursor - 光标字符
|
|
16
|
+
* @returns 添加了光标的新节点
|
|
17
|
+
*/
|
|
18
|
+
export declare function addCursorToNode(node: RootContent, cursor: string): RootContent;
|
package/package.json
CHANGED
|
@@ -1,31 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@incremark/vue",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "Incremark Vue 3 集成",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
7
|
"module": "./dist/index.js",
|
|
8
|
-
"types": "./
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
9
|
"exports": {
|
|
10
10
|
".": {
|
|
11
|
-
"types": "./
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
12
|
"import": "./dist/index.js"
|
|
13
13
|
},
|
|
14
|
-
"./components/*": {
|
|
15
|
-
"import": "./src/components/*.vue"
|
|
16
|
-
},
|
|
17
14
|
"./style.css": "@incremark/theme/styles.css"
|
|
18
15
|
},
|
|
19
16
|
"files": [
|
|
20
|
-
"dist"
|
|
21
|
-
"src"
|
|
17
|
+
"dist"
|
|
22
18
|
],
|
|
23
19
|
"dependencies": {
|
|
24
20
|
"shiki": "^3.20.0",
|
|
25
|
-
"@incremark/
|
|
26
|
-
"@incremark/
|
|
27
|
-
"@incremark/
|
|
28
|
-
"@incremark/
|
|
21
|
+
"@incremark/devtools": "0.2.3",
|
|
22
|
+
"@incremark/shared": "0.2.3",
|
|
23
|
+
"@incremark/theme": "0.2.3",
|
|
24
|
+
"@incremark/core": "0.2.3"
|
|
29
25
|
},
|
|
30
26
|
"peerDependencies": {
|
|
31
27
|
"vue": "^3.3.0",
|
|
@@ -46,6 +42,7 @@
|
|
|
46
42
|
"esbuild-plugin-vue3": "^0.4.2",
|
|
47
43
|
"tsup": "^8.0.0",
|
|
48
44
|
"typescript": "^5.3.0",
|
|
45
|
+
"vue-tsc": "^2.2.12",
|
|
49
46
|
"vue": "^3.4.0"
|
|
50
47
|
},
|
|
51
48
|
"keywords": [
|
|
@@ -63,7 +60,8 @@
|
|
|
63
60
|
},
|
|
64
61
|
"homepage": "https://www.incremark.com/",
|
|
65
62
|
"scripts": {
|
|
66
|
-
"build": "tsup",
|
|
63
|
+
"build": "tsup && npm run build:types",
|
|
64
|
+
"build:types": "vue-tsc -p tsconfig.build.json",
|
|
67
65
|
"dev": "tsup --watch"
|
|
68
66
|
}
|
|
69
67
|
}
|
package/dist/index.css
DELETED
package/dist/index.css.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["sfc-style:/Users/yishuai/develop/ai/markdown/packages/vue/src/components/AutoScrollContainer.vue?type=style&index=0"],"sourcesContent":["\n.auto-scroll-container[data-v-e0d180b8] {\n overflow-y: auto;\n height: 100%;\n}\n"],"mappings":";AACA,CAAC,qBAAqB,CAAC;AACrB,cAAY;AACZ,UAAQ;AACV;","names":[]}
|
package/src/ThemeProvider.vue
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
import { ref, watch } from 'vue'
|
|
3
|
-
import type { DesignTokens } from '@incremark/theme'
|
|
4
|
-
import { applyTheme } from '@incremark/theme'
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* 主题配置,可以是:
|
|
8
|
-
* - 字符串:'default' | 'dark'
|
|
9
|
-
* - 完整主题对象:DesignTokens
|
|
10
|
-
* - 部分主题对象:Partial<DesignTokens>(会合并到默认主题)
|
|
11
|
-
*/
|
|
12
|
-
const props = withDefaults(
|
|
13
|
-
defineProps<{
|
|
14
|
-
theme: 'default' | 'dark' | DesignTokens | Partial<DesignTokens>
|
|
15
|
-
class?: string
|
|
16
|
-
}>(),
|
|
17
|
-
{
|
|
18
|
-
class: ''
|
|
19
|
-
}
|
|
20
|
-
)
|
|
21
|
-
|
|
22
|
-
const containerRef = ref<HTMLElement>()
|
|
23
|
-
|
|
24
|
-
watch(
|
|
25
|
-
() => props.theme,
|
|
26
|
-
(theme) => {
|
|
27
|
-
if (containerRef.value) {
|
|
28
|
-
// 应用主题到容器元素
|
|
29
|
-
applyTheme(containerRef.value, theme)
|
|
30
|
-
}
|
|
31
|
-
},
|
|
32
|
-
{ immediate: true }
|
|
33
|
-
)
|
|
34
|
-
</script>
|
|
35
|
-
|
|
36
|
-
<template>
|
|
37
|
-
<div ref="containerRef" :class="props.class" class="incremark-theme-provider">
|
|
38
|
-
<slot />
|
|
39
|
-
</div>
|
|
40
|
-
</template>
|
|
41
|
-
|
|
@@ -1,164 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
import { ref, watch, onMounted, onUnmounted, nextTick } from 'vue'
|
|
3
|
-
|
|
4
|
-
const props = withDefaults(defineProps<{
|
|
5
|
-
/** 是否启用自动滚动 */
|
|
6
|
-
enabled?: boolean
|
|
7
|
-
/** 触发自动滚动的底部阈值(像素) */
|
|
8
|
-
threshold?: number
|
|
9
|
-
/** 滚动行为 */
|
|
10
|
-
behavior?: ScrollBehavior
|
|
11
|
-
}>(), {
|
|
12
|
-
enabled: true,
|
|
13
|
-
threshold: 50,
|
|
14
|
-
behavior: 'instant'
|
|
15
|
-
})
|
|
16
|
-
|
|
17
|
-
const containerRef = ref<HTMLDivElement | null>(null)
|
|
18
|
-
const isUserScrolledUp = ref(false)
|
|
19
|
-
|
|
20
|
-
// 记录上一次滚动位置,用于判断滚动方向
|
|
21
|
-
let lastScrollTop = 0
|
|
22
|
-
let lastScrollHeight = 0
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* 检查是否在底部附近
|
|
26
|
-
*/
|
|
27
|
-
function isNearBottom(): boolean {
|
|
28
|
-
const container = containerRef.value
|
|
29
|
-
if (!container) return true
|
|
30
|
-
|
|
31
|
-
const { scrollTop, scrollHeight, clientHeight } = container
|
|
32
|
-
return scrollHeight - scrollTop - clientHeight <= props.threshold
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* 滚动到底部
|
|
37
|
-
*/
|
|
38
|
-
function scrollToBottom(force = false): void {
|
|
39
|
-
const container = containerRef.value
|
|
40
|
-
if (!container) return
|
|
41
|
-
|
|
42
|
-
// 如果用户手动向上滚动了,且不是强制滚动,则不自动滚动
|
|
43
|
-
if (isUserScrolledUp.value && !force) return
|
|
44
|
-
|
|
45
|
-
container.scrollTo({
|
|
46
|
-
top: container.scrollHeight,
|
|
47
|
-
behavior: props.behavior
|
|
48
|
-
})
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* 检查是否有滚动条
|
|
53
|
-
*/
|
|
54
|
-
function hasScrollbar(): boolean {
|
|
55
|
-
const container = containerRef.value
|
|
56
|
-
if (!container) return false
|
|
57
|
-
return container.scrollHeight > container.clientHeight
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* 处理滚动事件
|
|
62
|
-
*/
|
|
63
|
-
function handleScroll(): void {
|
|
64
|
-
const container = containerRef.value
|
|
65
|
-
if (!container) return
|
|
66
|
-
|
|
67
|
-
const { scrollTop, scrollHeight, clientHeight } = container
|
|
68
|
-
|
|
69
|
-
// 如果没有滚动条,恢复自动滚动
|
|
70
|
-
if (scrollHeight <= clientHeight) {
|
|
71
|
-
isUserScrolledUp.value = false
|
|
72
|
-
lastScrollTop = 0
|
|
73
|
-
lastScrollHeight = scrollHeight
|
|
74
|
-
return
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// 检查用户是否滚动到底部附近
|
|
78
|
-
if (isNearBottom()) {
|
|
79
|
-
// 用户滚动到底部,恢复自动滚动
|
|
80
|
-
isUserScrolledUp.value = false
|
|
81
|
-
} else {
|
|
82
|
-
// 判断是否是用户主动向上滚动
|
|
83
|
-
// 条件:scrollTop 减少(向上滚动)且 scrollHeight 没有变化(不是因为内容增加)
|
|
84
|
-
const isScrollingUp = scrollTop < lastScrollTop
|
|
85
|
-
const isContentUnchanged = scrollHeight === lastScrollHeight
|
|
86
|
-
|
|
87
|
-
if (isScrollingUp && isContentUnchanged) {
|
|
88
|
-
// 用户主动向上滚动,暂停自动滚动
|
|
89
|
-
isUserScrolledUp.value = true
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// 更新记录
|
|
94
|
-
lastScrollTop = scrollTop
|
|
95
|
-
lastScrollHeight = scrollHeight
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// 监听 slot 内容变化(使用 MutationObserver)
|
|
99
|
-
let observer: MutationObserver | null = null
|
|
100
|
-
|
|
101
|
-
onMounted(() => {
|
|
102
|
-
if (!containerRef.value) return
|
|
103
|
-
|
|
104
|
-
// 初始化滚动位置记录
|
|
105
|
-
lastScrollTop = containerRef.value.scrollTop
|
|
106
|
-
lastScrollHeight = containerRef.value.scrollHeight
|
|
107
|
-
|
|
108
|
-
observer = new MutationObserver(() => {
|
|
109
|
-
nextTick(() => {
|
|
110
|
-
if (!containerRef.value) return
|
|
111
|
-
|
|
112
|
-
// 如果没有滚动条,重置状态
|
|
113
|
-
if (!hasScrollbar()) {
|
|
114
|
-
isUserScrolledUp.value = false
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// 更新 scrollHeight 记录(内容变化后)
|
|
118
|
-
lastScrollHeight = containerRef.value.scrollHeight
|
|
119
|
-
|
|
120
|
-
// 自动滚动
|
|
121
|
-
if (props.enabled && !isUserScrolledUp.value) {
|
|
122
|
-
scrollToBottom()
|
|
123
|
-
}
|
|
124
|
-
})
|
|
125
|
-
})
|
|
126
|
-
|
|
127
|
-
observer.observe(containerRef.value, {
|
|
128
|
-
childList: true,
|
|
129
|
-
subtree: true,
|
|
130
|
-
characterData: true
|
|
131
|
-
})
|
|
132
|
-
})
|
|
133
|
-
|
|
134
|
-
onUnmounted(() => {
|
|
135
|
-
observer?.disconnect()
|
|
136
|
-
})
|
|
137
|
-
|
|
138
|
-
// 暴露方法给父组件
|
|
139
|
-
defineExpose({
|
|
140
|
-
/** 强制滚动到底部 */
|
|
141
|
-
scrollToBottom: () => scrollToBottom(true),
|
|
142
|
-
/** 是否用户手动向上滚动了 */
|
|
143
|
-
isUserScrolledUp: () => isUserScrolledUp.value,
|
|
144
|
-
/** 容器元素引用 */
|
|
145
|
-
container: containerRef
|
|
146
|
-
})
|
|
147
|
-
</script>
|
|
148
|
-
|
|
149
|
-
<template>
|
|
150
|
-
<div
|
|
151
|
-
ref="containerRef"
|
|
152
|
-
class="auto-scroll-container"
|
|
153
|
-
@scroll="handleScroll"
|
|
154
|
-
>
|
|
155
|
-
<slot />
|
|
156
|
-
</div>
|
|
157
|
-
</template>
|
|
158
|
-
|
|
159
|
-
<style scoped>
|
|
160
|
-
.auto-scroll-container {
|
|
161
|
-
overflow-y: auto;
|
|
162
|
-
height: 100%;
|
|
163
|
-
}
|
|
164
|
-
</style>
|
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
import { computed, type Component } from 'vue'
|
|
3
|
-
import type { ParsedBlock, RootContent } from '@incremark/core'
|
|
4
|
-
import type { HTML } from 'mdast'
|
|
5
|
-
import { useDefinationsContext } from '../composables/useDefinationsContext'
|
|
6
|
-
import type { UseIncremarkReturn } from '../composables/useIncremark'
|
|
7
|
-
import IncremarkHeading from './IncremarkHeading.vue'
|
|
8
|
-
import IncremarkParagraph from './IncremarkParagraph.vue'
|
|
9
|
-
import IncremarkCode from './IncremarkCode.vue'
|
|
10
|
-
import IncremarkList from './IncremarkList.vue'
|
|
11
|
-
import IncremarkTable from './IncremarkTable.vue'
|
|
12
|
-
import IncremarkBlockquote from './IncremarkBlockquote.vue'
|
|
13
|
-
import IncremarkThematicBreak from './IncremarkThematicBreak.vue'
|
|
14
|
-
import IncremarkMath from './IncremarkMath.vue'
|
|
15
|
-
import IncremarkHtmlElement from './IncremarkHtmlElement.vue'
|
|
16
|
-
import IncremarkDefault from './IncremarkDefault.vue'
|
|
17
|
-
import IncremarkFootnotes from './IncremarkFootnotes.vue'
|
|
18
|
-
|
|
19
|
-
// 组件映射类型
|
|
20
|
-
export type ComponentMap = Partial<Record<string, Component>>
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* 检查是否是 html 节点
|
|
24
|
-
*/
|
|
25
|
-
function isHtmlNode(node: RootContent): node is HTML {
|
|
26
|
-
return node.type === 'html'
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// 带稳定 ID 的块类型
|
|
30
|
-
export interface BlockWithStableId extends ParsedBlock {
|
|
31
|
-
stableId: string
|
|
32
|
-
isLastPending?: boolean // 是否是最后一个 pending 块
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const props = withDefaults(
|
|
36
|
-
defineProps<{
|
|
37
|
-
/** 要渲染的块列表(来自 useIncremark 的 blocks) */
|
|
38
|
-
blocks?: BlockWithStableId[]
|
|
39
|
-
/** 自定义组件映射,key 为节点类型 */
|
|
40
|
-
components?: ComponentMap
|
|
41
|
-
/** 待处理块的样式类名 */
|
|
42
|
-
pendingClass?: string
|
|
43
|
-
/** 已完成块的样式类名 */
|
|
44
|
-
completedClass?: string
|
|
45
|
-
/** 是否显示块状态边框 */
|
|
46
|
-
showBlockStatus?: boolean
|
|
47
|
-
/** 可选:useIncremark 返回的对象(用于自动注入数据) */
|
|
48
|
-
incremark?: UseIncremarkReturn
|
|
49
|
-
}>(),
|
|
50
|
-
{
|
|
51
|
-
blocks: () => [],
|
|
52
|
-
components: () => ({}),
|
|
53
|
-
pendingClass: 'incremark-pending',
|
|
54
|
-
completedClass: 'incremark-completed',
|
|
55
|
-
showBlockStatus: false
|
|
56
|
-
}
|
|
57
|
-
)
|
|
58
|
-
|
|
59
|
-
// 从 context 获取 footnoteReferenceOrder(如果有的话)
|
|
60
|
-
let footnoteReferenceOrder
|
|
61
|
-
try {
|
|
62
|
-
const context = useDefinationsContext()
|
|
63
|
-
footnoteReferenceOrder = context.footnoteReferenceOrder
|
|
64
|
-
} catch {
|
|
65
|
-
// 如果没有 context,使用空数组
|
|
66
|
-
footnoteReferenceOrder = computed(() => [])
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// 计算实际使用的 blocks 和 isFinalized
|
|
70
|
-
const actualBlocks = computed<BlockWithStableId[]>(() => props.incremark?.blocks.value || props.blocks || [])
|
|
71
|
-
const actualIsFinalized = computed(() => {
|
|
72
|
-
if (props.incremark) {
|
|
73
|
-
return props.incremark.isFinalized.value
|
|
74
|
-
}
|
|
75
|
-
// 如果手动传入 blocks,自动判断是否所有 block 都是 completed
|
|
76
|
-
const blocks = props.blocks || []
|
|
77
|
-
return blocks.length > 0 && blocks.every(b => b.status === 'completed')
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
// 默认组件映射
|
|
81
|
-
const defaultComponents: Record<string, Component> = {
|
|
82
|
-
heading: IncremarkHeading,
|
|
83
|
-
paragraph: IncremarkParagraph,
|
|
84
|
-
code: IncremarkCode,
|
|
85
|
-
list: IncremarkList,
|
|
86
|
-
table: IncremarkTable,
|
|
87
|
-
blockquote: IncremarkBlockquote,
|
|
88
|
-
thematicBreak: IncremarkThematicBreak,
|
|
89
|
-
math: IncremarkMath,
|
|
90
|
-
inlineMath: IncremarkMath,
|
|
91
|
-
htmlElement: IncremarkHtmlElement
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// 合并用户组件和默认组件
|
|
95
|
-
const mergedComponents = computed(() => ({
|
|
96
|
-
...defaultComponents,
|
|
97
|
-
...props.components
|
|
98
|
-
}))
|
|
99
|
-
|
|
100
|
-
function getComponent(type: string): Component {
|
|
101
|
-
return mergedComponents.value[type] || props.components?.default || IncremarkDefault
|
|
102
|
-
}
|
|
103
|
-
</script>
|
|
104
|
-
|
|
105
|
-
<template>
|
|
106
|
-
<div class="incremark">
|
|
107
|
-
<!-- 主要内容块 -->
|
|
108
|
-
<template v-for="block in actualBlocks">
|
|
109
|
-
<div
|
|
110
|
-
v-if="block.node.type !== 'definition' && block.node.type !== 'footnoteDefinition'"
|
|
111
|
-
:key="block.stableId"
|
|
112
|
-
:class="[
|
|
113
|
-
'incremark-block',
|
|
114
|
-
block.status === 'completed' ? completedClass : pendingClass,
|
|
115
|
-
{ 'incremark-show-status': showBlockStatus },
|
|
116
|
-
{ 'incremark-last-pending': block.isLastPending }
|
|
117
|
-
]"
|
|
118
|
-
>
|
|
119
|
-
<!-- HTML 节点:渲染为代码块显示源代码 -->
|
|
120
|
-
<pre v-if="isHtmlNode(block.node)" class="incremark-html-code"><code>{{ (block.node as HTML).value }}</code></pre>
|
|
121
|
-
<!-- 其他节点:使用对应组件 -->
|
|
122
|
-
<component v-else :is="getComponent(block.node.type)" :node="block.node" />
|
|
123
|
-
</div>
|
|
124
|
-
</template>
|
|
125
|
-
|
|
126
|
-
<!-- 脚注列表(仅在 finalize 后显示) -->
|
|
127
|
-
<IncremarkFootnotes
|
|
128
|
-
v-if="actualIsFinalized && (footnoteReferenceOrder as any).value?.length > 0"
|
|
129
|
-
/>
|
|
130
|
-
</div>
|
|
131
|
-
</template>
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
import type { Blockquote } from 'mdast'
|
|
3
|
-
import IncremarkParagraph from './IncremarkParagraph.vue'
|
|
4
|
-
|
|
5
|
-
defineProps<{
|
|
6
|
-
node: Blockquote
|
|
7
|
-
}>()
|
|
8
|
-
</script>
|
|
9
|
-
|
|
10
|
-
<template>
|
|
11
|
-
<blockquote class="incremark-blockquote">
|
|
12
|
-
<template v-for="(child, index) in node.children" :key="index">
|
|
13
|
-
<IncremarkParagraph v-if="child.type === 'paragraph'" :node="child" />
|
|
14
|
-
<div v-else class="unknown-child">{{ child.type }}</div>
|
|
15
|
-
</template>
|
|
16
|
-
</blockquote>
|
|
17
|
-
</template>
|
|
18
|
-
|
|
@@ -1,236 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
import type { Code } from 'mdast'
|
|
3
|
-
import { computed, ref, watch, shallowRef, onUnmounted } from 'vue'
|
|
4
|
-
|
|
5
|
-
const props = withDefaults(
|
|
6
|
-
defineProps<{
|
|
7
|
-
node: Code
|
|
8
|
-
/** Shiki 主题,默认 github-dark */
|
|
9
|
-
theme?: string
|
|
10
|
-
/** 是否禁用代码高亮 */
|
|
11
|
-
disableHighlight?: boolean
|
|
12
|
-
/** Mermaid 渲染延迟(毫秒),用于流式输入时防抖 */
|
|
13
|
-
mermaidDelay?: number
|
|
14
|
-
}>(),
|
|
15
|
-
{
|
|
16
|
-
theme: 'github-dark',
|
|
17
|
-
disableHighlight: false,
|
|
18
|
-
mermaidDelay: 500
|
|
19
|
-
}
|
|
20
|
-
)
|
|
21
|
-
|
|
22
|
-
const copied = ref(false)
|
|
23
|
-
const highlightedHtml = ref('')
|
|
24
|
-
const isHighlighting = ref(false)
|
|
25
|
-
const highlightError = ref(false)
|
|
26
|
-
|
|
27
|
-
// Mermaid 支持
|
|
28
|
-
const mermaidSvg = ref('')
|
|
29
|
-
const mermaidError = ref('')
|
|
30
|
-
const mermaidLoading = ref(false)
|
|
31
|
-
const mermaidRef = shallowRef<any>(null)
|
|
32
|
-
let mermaidTimer: ReturnType<typeof setTimeout> | null = null
|
|
33
|
-
// 视图模式:'preview' | 'source'
|
|
34
|
-
const mermaidViewMode = ref<'preview' | 'source'>('preview')
|
|
35
|
-
|
|
36
|
-
function toggleMermaidView() {
|
|
37
|
-
mermaidViewMode.value = mermaidViewMode.value === 'preview' ? 'source' : 'preview'
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const language = computed(() => props.node.lang || 'text')
|
|
41
|
-
const code = computed(() => props.node.value)
|
|
42
|
-
const isMermaid = computed(() => language.value === 'mermaid')
|
|
43
|
-
|
|
44
|
-
// 缓存 highlighter
|
|
45
|
-
const highlighterRef = shallowRef<any>(null)
|
|
46
|
-
const loadedLanguages = new Set<string>()
|
|
47
|
-
const loadedThemes = new Set<string>()
|
|
48
|
-
|
|
49
|
-
// Mermaid 渲染(带防抖动)
|
|
50
|
-
function scheduleRenderMermaid() {
|
|
51
|
-
if (!isMermaid.value || !code.value) return
|
|
52
|
-
|
|
53
|
-
// 清除之前的定时器
|
|
54
|
-
if (mermaidTimer) {
|
|
55
|
-
clearTimeout(mermaidTimer)
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// 显示加载状态
|
|
59
|
-
mermaidLoading.value = true
|
|
60
|
-
|
|
61
|
-
// 防抖动延迟渲染
|
|
62
|
-
mermaidTimer = setTimeout(() => {
|
|
63
|
-
doRenderMermaid()
|
|
64
|
-
}, props.mermaidDelay)
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
async function doRenderMermaid() {
|
|
68
|
-
if (!code.value) return
|
|
69
|
-
|
|
70
|
-
mermaidError.value = ''
|
|
71
|
-
|
|
72
|
-
try {
|
|
73
|
-
// 动态导入 mermaid
|
|
74
|
-
if (!mermaidRef.value) {
|
|
75
|
-
// @ts-ignore - mermaid 是可选依赖
|
|
76
|
-
const mermaidModule = await import('mermaid')
|
|
77
|
-
mermaidRef.value = mermaidModule.default
|
|
78
|
-
mermaidRef.value.initialize({
|
|
79
|
-
startOnLoad: false,
|
|
80
|
-
theme: 'dark',
|
|
81
|
-
securityLevel: 'loose'
|
|
82
|
-
})
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const mermaid = mermaidRef.value
|
|
86
|
-
const id = `mermaid-${Date.now()}-${Math.random().toString(36).slice(2)}`
|
|
87
|
-
|
|
88
|
-
const { svg } = await mermaid.render(id, code.value)
|
|
89
|
-
mermaidSvg.value = svg
|
|
90
|
-
} catch (e: any) {
|
|
91
|
-
// 不显示错误,可能是代码还不完整
|
|
92
|
-
mermaidError.value = ''
|
|
93
|
-
mermaidSvg.value = ''
|
|
94
|
-
} finally {
|
|
95
|
-
mermaidLoading.value = false
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
onUnmounted(() => {
|
|
100
|
-
if (mermaidTimer) {
|
|
101
|
-
clearTimeout(mermaidTimer)
|
|
102
|
-
}
|
|
103
|
-
})
|
|
104
|
-
|
|
105
|
-
// 动态加载 shiki 并高亮
|
|
106
|
-
async function highlight() {
|
|
107
|
-
if (isMermaid.value) {
|
|
108
|
-
scheduleRenderMermaid()
|
|
109
|
-
return
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
if (!code.value || props.disableHighlight) {
|
|
113
|
-
highlightedHtml.value = ''
|
|
114
|
-
return
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
isHighlighting.value = true
|
|
118
|
-
highlightError.value = false
|
|
119
|
-
|
|
120
|
-
try {
|
|
121
|
-
// 动态导入 shiki
|
|
122
|
-
if (!highlighterRef.value) {
|
|
123
|
-
const { createHighlighter } = await import('shiki')
|
|
124
|
-
highlighterRef.value = await createHighlighter({
|
|
125
|
-
themes: [props.theme as any],
|
|
126
|
-
langs: []
|
|
127
|
-
})
|
|
128
|
-
loadedThemes.add(props.theme)
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
const highlighter = highlighterRef.value
|
|
132
|
-
const lang = language.value
|
|
133
|
-
|
|
134
|
-
// 按需加载语言
|
|
135
|
-
if (!loadedLanguages.has(lang) && lang !== 'text') {
|
|
136
|
-
try {
|
|
137
|
-
await highlighter.loadLanguage(lang)
|
|
138
|
-
loadedLanguages.add(lang)
|
|
139
|
-
} catch {
|
|
140
|
-
// 语言不支持,标记但不阻止
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// 按需加载主题
|
|
145
|
-
if (!loadedThemes.has(props.theme)) {
|
|
146
|
-
try {
|
|
147
|
-
await highlighter.loadTheme(props.theme)
|
|
148
|
-
loadedThemes.add(props.theme)
|
|
149
|
-
} catch {
|
|
150
|
-
// 主题不支持
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
const html = highlighter.codeToHtml(code.value, {
|
|
155
|
-
lang: loadedLanguages.has(lang) ? lang : 'text',
|
|
156
|
-
theme: loadedThemes.has(props.theme) ? props.theme : 'github-dark'
|
|
157
|
-
})
|
|
158
|
-
highlightedHtml.value = html
|
|
159
|
-
} catch (e) {
|
|
160
|
-
// Shiki 不可用或加载失败
|
|
161
|
-
highlightError.value = true
|
|
162
|
-
highlightedHtml.value = ''
|
|
163
|
-
} finally {
|
|
164
|
-
isHighlighting.value = false
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// 监听代码变化,重新高亮/渲染
|
|
169
|
-
watch([code, () => props.theme, isMermaid], highlight, { immediate: true })
|
|
170
|
-
|
|
171
|
-
async function copyCode() {
|
|
172
|
-
try {
|
|
173
|
-
await navigator.clipboard.writeText(code.value)
|
|
174
|
-
copied.value = true
|
|
175
|
-
setTimeout(() => {
|
|
176
|
-
copied.value = false
|
|
177
|
-
}, 2000)
|
|
178
|
-
} catch {
|
|
179
|
-
// 复制失败静默处理
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
</script>
|
|
183
|
-
|
|
184
|
-
<template>
|
|
185
|
-
<!-- Mermaid 图表 -->
|
|
186
|
-
<div v-if="isMermaid" class="incremark-mermaid">
|
|
187
|
-
<div class="mermaid-header">
|
|
188
|
-
<span class="language">MERMAID</span>
|
|
189
|
-
<div class="mermaid-actions">
|
|
190
|
-
<button
|
|
191
|
-
class="code-btn"
|
|
192
|
-
@click="toggleMermaidView"
|
|
193
|
-
type="button"
|
|
194
|
-
:disabled="!mermaidSvg"
|
|
195
|
-
>
|
|
196
|
-
{{ mermaidViewMode === 'preview' ? '源码' : '预览' }}
|
|
197
|
-
</button>
|
|
198
|
-
<button class="code-btn" @click="copyCode" type="button">
|
|
199
|
-
{{ copied ? '✓ 已复制' : '复制' }}
|
|
200
|
-
</button>
|
|
201
|
-
</div>
|
|
202
|
-
</div>
|
|
203
|
-
<div class="mermaid-content">
|
|
204
|
-
<!-- 加载中 -->
|
|
205
|
-
<div v-if="mermaidLoading && !mermaidSvg" class="mermaid-loading">
|
|
206
|
-
<pre class="mermaid-source-code">{{ code }}</pre>
|
|
207
|
-
</div>
|
|
208
|
-
<!-- 源码模式 -->
|
|
209
|
-
<pre v-else-if="mermaidViewMode === 'source'" class="mermaid-source-code">{{ code }}</pre>
|
|
210
|
-
<!-- 预览模式 -->
|
|
211
|
-
<div v-else-if="mermaidSvg" v-html="mermaidSvg" class="mermaid-svg" />
|
|
212
|
-
<!-- 无法渲染时显示源码 -->
|
|
213
|
-
<pre v-else class="mermaid-source-code">{{ code }}</pre>
|
|
214
|
-
</div>
|
|
215
|
-
</div>
|
|
216
|
-
|
|
217
|
-
<!-- 普通代码块 -->
|
|
218
|
-
<div v-else class="incremark-code">
|
|
219
|
-
<div class="code-header">
|
|
220
|
-
<span class="language">{{ language }}</span>
|
|
221
|
-
<button class="code-btn" @click="copyCode" type="button">
|
|
222
|
-
{{ copied ? '✓ 已复制' : '复制' }}
|
|
223
|
-
</button>
|
|
224
|
-
</div>
|
|
225
|
-
<div class="code-content">
|
|
226
|
-
<!-- 正在加载高亮 -->
|
|
227
|
-
<div v-if="isHighlighting && !highlightedHtml" class="code-loading">
|
|
228
|
-
<pre><code>{{ code }}</code></pre>
|
|
229
|
-
</div>
|
|
230
|
-
<!-- 高亮后的代码 -->
|
|
231
|
-
<div v-else-if="highlightedHtml" v-html="highlightedHtml" class="shiki-wrapper" />
|
|
232
|
-
<!-- 回退:无高亮 -->
|
|
233
|
-
<pre v-else class="code-fallback"><code>{{ code }}</code></pre>
|
|
234
|
-
</div>
|
|
235
|
-
</div>
|
|
236
|
-
</template>
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
import type { RootContent } from 'mdast'
|
|
3
|
-
|
|
4
|
-
defineProps<{
|
|
5
|
-
node: RootContent
|
|
6
|
-
}>()
|
|
7
|
-
</script>
|
|
8
|
-
|
|
9
|
-
<template>
|
|
10
|
-
<div class="incremark-default">
|
|
11
|
-
<span class="type-badge">{{ node.type }}</span>
|
|
12
|
-
<pre>{{ JSON.stringify(node, null, 2) }}</pre>
|
|
13
|
-
</div>
|
|
14
|
-
</template>
|
|
15
|
-
|