@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,394 @@
|
|
|
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="view-toggle"
|
|
192
|
+
@click="toggleMermaidView"
|
|
193
|
+
type="button"
|
|
194
|
+
:disabled="!mermaidSvg"
|
|
195
|
+
>
|
|
196
|
+
{{ mermaidViewMode === 'preview' ? '源码' : '预览' }}
|
|
197
|
+
</button>
|
|
198
|
+
<button class="copy-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="copy-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>
|
|
237
|
+
|
|
238
|
+
<style scoped>
|
|
239
|
+
/* Mermaid 样式 */
|
|
240
|
+
.incremark-mermaid {
|
|
241
|
+
margin: 1em 0;
|
|
242
|
+
border-radius: 8px;
|
|
243
|
+
overflow: hidden;
|
|
244
|
+
background: #1a1a2e;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
.mermaid-header {
|
|
248
|
+
display: flex;
|
|
249
|
+
justify-content: space-between;
|
|
250
|
+
align-items: center;
|
|
251
|
+
padding: 8px 16px;
|
|
252
|
+
background: #16213e;
|
|
253
|
+
border-bottom: 1px solid #0f3460;
|
|
254
|
+
font-size: 12px;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
.mermaid-actions {
|
|
258
|
+
display: flex;
|
|
259
|
+
gap: 8px;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
.view-toggle {
|
|
263
|
+
padding: 4px 10px;
|
|
264
|
+
border: 1px solid #0f3460;
|
|
265
|
+
border-radius: 6px;
|
|
266
|
+
background: transparent;
|
|
267
|
+
color: #8b949e;
|
|
268
|
+
font-size: 12px;
|
|
269
|
+
cursor: pointer;
|
|
270
|
+
transition: all 0.2s;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
.view-toggle:hover:not(:disabled) {
|
|
274
|
+
background: #0f3460;
|
|
275
|
+
color: #e0e0e0;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
.view-toggle:disabled {
|
|
279
|
+
opacity: 0.5;
|
|
280
|
+
cursor: not-allowed;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
.mermaid-content {
|
|
284
|
+
padding: 16px;
|
|
285
|
+
min-height: 100px;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
.mermaid-loading {
|
|
289
|
+
color: #8b949e;
|
|
290
|
+
font-size: 14px;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
.mermaid-source-code {
|
|
294
|
+
margin: 0;
|
|
295
|
+
padding: 12px;
|
|
296
|
+
background: #0d1117;
|
|
297
|
+
border-radius: 6px;
|
|
298
|
+
color: #c9d1d9;
|
|
299
|
+
font-family: 'Fira Code', 'SF Mono', monospace;
|
|
300
|
+
font-size: 13px;
|
|
301
|
+
line-height: 1.5;
|
|
302
|
+
white-space: pre-wrap;
|
|
303
|
+
overflow-x: auto;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
.mermaid-svg {
|
|
307
|
+
overflow-x: auto;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
.mermaid-svg :deep(svg) {
|
|
311
|
+
max-width: 100%;
|
|
312
|
+
height: auto;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/* 代码块样式 */
|
|
316
|
+
.incremark-code {
|
|
317
|
+
margin: 1em 0;
|
|
318
|
+
border-radius: 8px;
|
|
319
|
+
overflow: hidden;
|
|
320
|
+
background: #24292e;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
.code-header {
|
|
324
|
+
display: flex;
|
|
325
|
+
justify-content: space-between;
|
|
326
|
+
align-items: center;
|
|
327
|
+
padding: 8px 16px;
|
|
328
|
+
background: #1f2428;
|
|
329
|
+
border-bottom: 1px solid #30363d;
|
|
330
|
+
font-size: 12px;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
.language {
|
|
334
|
+
color: #8b949e;
|
|
335
|
+
text-transform: uppercase;
|
|
336
|
+
font-weight: 500;
|
|
337
|
+
letter-spacing: 0.5px;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
.copy-btn {
|
|
341
|
+
padding: 4px 12px;
|
|
342
|
+
border: 1px solid #30363d;
|
|
343
|
+
border-radius: 6px;
|
|
344
|
+
background: transparent;
|
|
345
|
+
color: #8b949e;
|
|
346
|
+
font-size: 12px;
|
|
347
|
+
cursor: pointer;
|
|
348
|
+
transition: all 0.2s;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
.copy-btn:hover {
|
|
352
|
+
background: #30363d;
|
|
353
|
+
color: #c9d1d9;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
.code-content {
|
|
357
|
+
overflow-x: auto;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
.code-loading {
|
|
361
|
+
opacity: 0.7;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/* Shiki 生成的代码样式 */
|
|
365
|
+
.shiki-wrapper :deep(pre) {
|
|
366
|
+
margin: 0;
|
|
367
|
+
padding: 16px;
|
|
368
|
+
background: transparent !important;
|
|
369
|
+
overflow-x: auto;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
.shiki-wrapper :deep(code) {
|
|
373
|
+
font-family: 'Fira Code', 'SF Mono', 'Monaco', 'Consolas', monospace;
|
|
374
|
+
font-size: 14px;
|
|
375
|
+
line-height: 1.6;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/* 回退样式 */
|
|
379
|
+
.code-fallback,
|
|
380
|
+
.code-loading pre {
|
|
381
|
+
margin: 0;
|
|
382
|
+
padding: 16px;
|
|
383
|
+
overflow-x: auto;
|
|
384
|
+
background: transparent;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
.code-fallback code,
|
|
388
|
+
.code-loading code {
|
|
389
|
+
font-family: 'Fira Code', 'SF Mono', 'Monaco', 'Consolas', monospace;
|
|
390
|
+
font-size: 14px;
|
|
391
|
+
line-height: 1.6;
|
|
392
|
+
color: #c9d1d9;
|
|
393
|
+
}
|
|
394
|
+
</style>
|
|
@@ -0,0 +1,42 @@
|
|
|
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
|
+
|
|
16
|
+
<style scoped>
|
|
17
|
+
.incremark-default {
|
|
18
|
+
margin: 0.5em 0;
|
|
19
|
+
padding: 10px;
|
|
20
|
+
background: #fff3cd;
|
|
21
|
+
border: 1px solid #ffc107;
|
|
22
|
+
border-radius: 4px;
|
|
23
|
+
font-size: 12px;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.type-badge {
|
|
27
|
+
display: inline-block;
|
|
28
|
+
padding: 2px 8px;
|
|
29
|
+
background: #ffc107;
|
|
30
|
+
border-radius: 4px;
|
|
31
|
+
font-weight: 600;
|
|
32
|
+
margin-bottom: 8px;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
pre {
|
|
36
|
+
margin: 0;
|
|
37
|
+
white-space: pre-wrap;
|
|
38
|
+
word-break: break-all;
|
|
39
|
+
font-size: 11px;
|
|
40
|
+
}
|
|
41
|
+
</style>
|
|
42
|
+
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { Heading } from 'mdast'
|
|
3
|
+
import { computed } from 'vue'
|
|
4
|
+
import IncremarkInline from './IncremarkInline.vue'
|
|
5
|
+
|
|
6
|
+
const props = defineProps<{
|
|
7
|
+
node: Heading
|
|
8
|
+
}>()
|
|
9
|
+
|
|
10
|
+
const tag = computed(() => `h${props.node.depth}`)
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<template>
|
|
14
|
+
<component :is="tag" class="incremark-heading">
|
|
15
|
+
<IncremarkInline :nodes="node.children" />
|
|
16
|
+
</component>
|
|
17
|
+
</template>
|
|
18
|
+
|
|
19
|
+
<style scoped>
|
|
20
|
+
.incremark-heading {
|
|
21
|
+
margin: 0.5em 0;
|
|
22
|
+
font-weight: 600;
|
|
23
|
+
line-height: 1.3;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
h1 { font-size: 2em; border-bottom: 1px solid #eee; padding-bottom: 0.3em; }
|
|
27
|
+
h2 { font-size: 1.5em; }
|
|
28
|
+
h3 { font-size: 1.25em; }
|
|
29
|
+
h4 { font-size: 1em; }
|
|
30
|
+
h5 { font-size: 0.875em; }
|
|
31
|
+
h6 { font-size: 0.85em; color: #666; }
|
|
32
|
+
</style>
|
|
33
|
+
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { PhrasingContent } from 'mdast'
|
|
3
|
+
import IncremarkMath from './IncremarkMath.vue'
|
|
4
|
+
|
|
5
|
+
defineProps<{
|
|
6
|
+
nodes: PhrasingContent[]
|
|
7
|
+
}>()
|
|
8
|
+
|
|
9
|
+
function escapeHtml(str: string): string {
|
|
10
|
+
return str
|
|
11
|
+
.replace(/&/g, '&')
|
|
12
|
+
.replace(/</g, '<')
|
|
13
|
+
.replace(/>/g, '>')
|
|
14
|
+
.replace(/"/g, '"')
|
|
15
|
+
}
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
<template>
|
|
19
|
+
<template v-for="(node, idx) in nodes" :key="idx">
|
|
20
|
+
<!-- 文本 -->
|
|
21
|
+
<template v-if="node.type === 'text'">{{ node.value }}</template>
|
|
22
|
+
|
|
23
|
+
<!-- 行内公式 -->
|
|
24
|
+
<IncremarkMath v-else-if="(node as any).type === 'inlineMath'" :node="node as any" />
|
|
25
|
+
|
|
26
|
+
<!-- 加粗 -->
|
|
27
|
+
<strong v-else-if="node.type === 'strong'">
|
|
28
|
+
<IncremarkInline :nodes="(node.children as PhrasingContent[])" />
|
|
29
|
+
</strong>
|
|
30
|
+
|
|
31
|
+
<!-- 斜体 -->
|
|
32
|
+
<em v-else-if="node.type === 'emphasis'">
|
|
33
|
+
<IncremarkInline :nodes="(node.children as PhrasingContent[])" />
|
|
34
|
+
</em>
|
|
35
|
+
|
|
36
|
+
<!-- 行内代码 -->
|
|
37
|
+
<code v-else-if="node.type === 'inlineCode'" class="incremark-inline-code">{{ node.value }}</code>
|
|
38
|
+
|
|
39
|
+
<!-- 链接 -->
|
|
40
|
+
<a
|
|
41
|
+
v-else-if="node.type === 'link'"
|
|
42
|
+
:href="node.url"
|
|
43
|
+
target="_blank"
|
|
44
|
+
rel="noopener"
|
|
45
|
+
>
|
|
46
|
+
<IncremarkInline :nodes="(node.children as PhrasingContent[])" />
|
|
47
|
+
</a>
|
|
48
|
+
|
|
49
|
+
<!-- 图片 -->
|
|
50
|
+
<img
|
|
51
|
+
v-else-if="node.type === 'image'"
|
|
52
|
+
:src="node.url"
|
|
53
|
+
:alt="node.alt || ''"
|
|
54
|
+
loading="lazy"
|
|
55
|
+
/>
|
|
56
|
+
|
|
57
|
+
<!-- 换行 -->
|
|
58
|
+
<br v-else-if="node.type === 'break'" />
|
|
59
|
+
|
|
60
|
+
<!-- 删除线 -->
|
|
61
|
+
<del v-else-if="node.type === 'delete'">
|
|
62
|
+
<IncremarkInline :nodes="(node.children as PhrasingContent[])" />
|
|
63
|
+
</del>
|
|
64
|
+
</template>
|
|
65
|
+
</template>
|
|
66
|
+
|
|
67
|
+
<style>
|
|
68
|
+
.incremark-inline-code {
|
|
69
|
+
padding: 0.2em 0.4em;
|
|
70
|
+
background: rgba(0, 0, 0, 0.06);
|
|
71
|
+
border-radius: 4px;
|
|
72
|
+
font-family: 'Fira Code', 'SF Mono', Consolas, monospace;
|
|
73
|
+
font-size: 0.9em;
|
|
74
|
+
}
|
|
75
|
+
</style>
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { List, ListItem, PhrasingContent } from 'mdast'
|
|
3
|
+
import { computed } from 'vue'
|
|
4
|
+
import IncremarkInline from './IncremarkInline.vue'
|
|
5
|
+
|
|
6
|
+
const props = defineProps<{
|
|
7
|
+
node: List
|
|
8
|
+
}>()
|
|
9
|
+
|
|
10
|
+
const tag = computed(() => props.node.ordered ? 'ol' : 'ul')
|
|
11
|
+
|
|
12
|
+
function getItemContent(item: ListItem): PhrasingContent[] {
|
|
13
|
+
const firstChild = item.children[0]
|
|
14
|
+
if (firstChild?.type === 'paragraph') {
|
|
15
|
+
return firstChild.children as PhrasingContent[]
|
|
16
|
+
}
|
|
17
|
+
return []
|
|
18
|
+
}
|
|
19
|
+
</script>
|
|
20
|
+
|
|
21
|
+
<template>
|
|
22
|
+
<component :is="tag" class="incremark-list" :class="{ 'task-list': node.children.some(item => item.checked !== null && item.checked !== undefined) }">
|
|
23
|
+
<li
|
|
24
|
+
v-for="(item, index) in node.children"
|
|
25
|
+
:key="index"
|
|
26
|
+
class="incremark-list-item"
|
|
27
|
+
:class="{ 'task-item': item.checked !== null && item.checked !== undefined }"
|
|
28
|
+
>
|
|
29
|
+
<label v-if="item.checked !== null && item.checked !== undefined" class="task-label">
|
|
30
|
+
<input
|
|
31
|
+
type="checkbox"
|
|
32
|
+
:checked="item.checked"
|
|
33
|
+
disabled
|
|
34
|
+
class="checkbox"
|
|
35
|
+
/>
|
|
36
|
+
<span class="task-content">
|
|
37
|
+
<IncremarkInline :nodes="getItemContent(item)" />
|
|
38
|
+
</span>
|
|
39
|
+
</label>
|
|
40
|
+
<template v-else>
|
|
41
|
+
<IncremarkInline :nodes="getItemContent(item)" />
|
|
42
|
+
</template>
|
|
43
|
+
</li>
|
|
44
|
+
</component>
|
|
45
|
+
</template>
|
|
46
|
+
|
|
47
|
+
<style scoped>
|
|
48
|
+
.incremark-list {
|
|
49
|
+
margin: 0.75em 0;
|
|
50
|
+
padding-left: 2em;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.incremark-list.task-list {
|
|
54
|
+
list-style: none;
|
|
55
|
+
padding-left: 0;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.incremark-list-item {
|
|
59
|
+
margin: 0.25em 0;
|
|
60
|
+
line-height: 1.6;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.task-item {
|
|
64
|
+
list-style: none;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.task-label {
|
|
68
|
+
display: flex;
|
|
69
|
+
align-items: flex-start;
|
|
70
|
+
gap: 0.5em;
|
|
71
|
+
cursor: default;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.checkbox {
|
|
75
|
+
margin-top: 0.3em;
|
|
76
|
+
flex-shrink: 0;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.task-content {
|
|
80
|
+
flex: 1;
|
|
81
|
+
}
|
|
82
|
+
</style>
|
|
83
|
+
|