@incremark/svelte 0.2.6 → 0.3.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.en.md +274 -0
- package/dist/ThemeProvider.svelte +4 -1
- package/dist/ThemeProvider.svelte.d.ts.map +1 -1
- package/dist/components/AutoScrollContainer.svelte +18 -18
- package/dist/components/ConfigProvider.svelte +18 -0
- package/dist/components/ConfigProvider.svelte.d.ts +9 -0
- package/dist/components/ConfigProvider.svelte.d.ts.map +1 -0
- package/dist/components/Incremark.svelte +63 -72
- package/dist/components/Incremark.svelte.d.ts +5 -7
- package/dist/components/Incremark.svelte.d.ts.map +1 -1
- package/dist/components/IncremarkCode.svelte +28 -251
- package/dist/components/IncremarkCode.svelte.d.ts +4 -1
- package/dist/components/IncremarkCode.svelte.d.ts.map +1 -1
- package/dist/components/IncremarkCodeDefault.svelte +115 -0
- package/dist/components/IncremarkCodeDefault.svelte.d.ts +18 -0
- package/dist/components/IncremarkCodeDefault.svelte.d.ts.map +1 -0
- package/dist/components/IncremarkCodeMermaid.svelte +148 -0
- package/dist/components/IncremarkCodeMermaid.svelte.d.ts +14 -0
- package/dist/components/IncremarkCodeMermaid.svelte.d.ts.map +1 -0
- package/dist/components/IncremarkContent.svelte +115 -0
- package/dist/components/IncremarkContent.svelte.d.ts +5 -0
- package/dist/components/IncremarkContent.svelte.d.ts.map +1 -0
- package/dist/components/IncremarkFootnotes.svelte +4 -8
- package/dist/components/IncremarkFootnotes.svelte.d.ts.map +1 -1
- package/dist/components/IncremarkInline.svelte +3 -3
- package/dist/components/IncremarkList.svelte +1 -0
- package/dist/components/IncremarkRenderer.svelte +4 -0
- package/dist/components/IncremarkRenderer.svelte.d.ts +2 -0
- package/dist/components/IncremarkRenderer.svelte.d.ts.map +1 -1
- package/dist/components/IncremarkTable.svelte +6 -9
- package/dist/components/IncremarkTable.svelte.d.ts.map +1 -1
- package/dist/components/SvgIcon.svelte +25 -0
- package/dist/components/SvgIcon.svelte.d.ts +12 -0
- package/dist/components/SvgIcon.svelte.d.ts.map +1 -0
- package/dist/components/index.d.ts +2 -1
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +1 -0
- package/dist/components/types.d.ts +29 -3
- package/dist/components/types.d.ts.map +1 -1
- package/dist/index.d.ts +8 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -2
- package/dist/stores/{useDevTools.d.ts → useDevTools.svelte.d.ts} +1 -1
- package/dist/stores/useDevTools.svelte.d.ts.map +1 -0
- package/dist/stores/{useDevTools.js → useDevTools.svelte.js} +10 -9
- package/dist/stores/useIncremark.d.ts +4 -4
- package/dist/stores/useIncremark.d.ts.map +1 -1
- package/dist/stores/useLocale.svelte.d.ts +29 -0
- package/dist/stores/useLocale.svelte.d.ts.map +1 -0
- package/dist/stores/useLocale.svelte.js +32 -0
- package/dist/stores/useShiki.svelte.d.ts +9 -0
- package/dist/stores/useShiki.svelte.d.ts.map +1 -0
- package/dist/stores/useShiki.svelte.js +110 -0
- package/dist/stores/useTypewriter.d.ts +0 -1
- package/dist/stores/useTypewriter.d.ts.map +1 -1
- package/dist/stores/useTypewriter.js +1 -3
- package/package.json +13 -7
- package/dist/stores/useDevTools.d.ts.map +0 -1
|
@@ -1,11 +1,8 @@
|
|
|
1
|
-
<!--
|
|
2
|
-
@file IncremarkCode.svelte - 代码块组件
|
|
3
|
-
@description 渲染 Markdown 代码块,支持 Shiki 代码高亮和 Mermaid 图表渲染
|
|
4
|
-
-->
|
|
5
|
-
|
|
6
1
|
<script lang="ts">
|
|
7
2
|
import type { Code } from 'mdast'
|
|
8
|
-
import {
|
|
3
|
+
import type { Component } from 'svelte'
|
|
4
|
+
import IncremarkCodeMermaid from './IncremarkCodeMermaid.svelte'
|
|
5
|
+
import IncremarkCodeDefault from './IncremarkCodeDefault.svelte'
|
|
9
6
|
|
|
10
7
|
/**
|
|
11
8
|
* 组件 Props
|
|
@@ -27,6 +24,8 @@
|
|
|
27
24
|
codeBlockConfigs?: Record<string, { takeOver?: boolean }>
|
|
28
25
|
/** 块状态,用于判断是否使用自定义组件 */
|
|
29
26
|
blockStatus?: 'pending' | 'stable' | 'completed'
|
|
27
|
+
/** 默认代码块渲染组件(当不是 mermaid 且没有自定义组件时使用) */
|
|
28
|
+
defaultCodeComponent?: Component<any>
|
|
30
29
|
}
|
|
31
30
|
|
|
32
31
|
let {
|
|
@@ -37,29 +36,10 @@
|
|
|
37
36
|
mermaidDelay = 500,
|
|
38
37
|
customCodeBlocks,
|
|
39
38
|
codeBlockConfigs,
|
|
40
|
-
blockStatus = 'completed'
|
|
39
|
+
blockStatus = 'completed',
|
|
40
|
+
defaultCodeComponent
|
|
41
41
|
}: Props = $props()
|
|
42
42
|
|
|
43
|
-
// 状态
|
|
44
|
-
let copied = $state(false)
|
|
45
|
-
let highlightedHtml = $state('')
|
|
46
|
-
let isHighlighting = $state(false)
|
|
47
|
-
let highlightError = $state(false)
|
|
48
|
-
|
|
49
|
-
// Mermaid 支持
|
|
50
|
-
let mermaidSvg = $state('')
|
|
51
|
-
let mermaidError = $state('')
|
|
52
|
-
let mermaidLoading = $state(false)
|
|
53
|
-
let mermaidRef: any = null
|
|
54
|
-
let mermaidTimer: ReturnType<typeof setTimeout> | null = null
|
|
55
|
-
// 视图模式:'preview' | 'source'
|
|
56
|
-
let mermaidViewMode = $state<'preview' | 'source'>('preview')
|
|
57
|
-
|
|
58
|
-
// 缓存 highlighter
|
|
59
|
-
let highlighterRef: any = null
|
|
60
|
-
const loadedLanguages = new Set<string>()
|
|
61
|
-
const loadedThemes = new Set<string>()
|
|
62
|
-
|
|
63
43
|
/**
|
|
64
44
|
* 计算属性
|
|
65
45
|
*/
|
|
@@ -72,240 +52,37 @@
|
|
|
72
52
|
const component = customCodeBlocks?.[language]
|
|
73
53
|
if (!component) return null
|
|
74
54
|
|
|
75
|
-
// 检查该语言的配置
|
|
76
55
|
const config = codeBlockConfigs?.[language]
|
|
77
56
|
|
|
78
|
-
|
|
79
|
-
if (
|
|
80
|
-
return component
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// 否则,默认行为:只在 completed 状态使用
|
|
84
|
-
if (blockStatus !== 'completed') {
|
|
85
|
-
return null
|
|
86
|
-
}
|
|
57
|
+
if (config?.takeOver) return component
|
|
58
|
+
if (blockStatus !== 'completed') return null
|
|
87
59
|
|
|
88
60
|
return component
|
|
89
61
|
})
|
|
90
62
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
*/
|
|
94
|
-
function toggleMermaidView() {
|
|
95
|
-
mermaidViewMode = mermaidViewMode === 'preview' ? 'source' : 'preview'
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Mermaid 渲染(带防抖动)
|
|
100
|
-
*/
|
|
101
|
-
function scheduleRenderMermaid() {
|
|
102
|
-
if (!isMermaid || !code) return
|
|
103
|
-
|
|
104
|
-
// 清除之前的定时器
|
|
105
|
-
if (mermaidTimer) {
|
|
106
|
-
clearTimeout(mermaidTimer)
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// 显示加载状态
|
|
110
|
-
mermaidLoading = true
|
|
111
|
-
|
|
112
|
-
// 防抖动延迟渲染
|
|
113
|
-
mermaidTimer = setTimeout(() => {
|
|
114
|
-
doRenderMermaid()
|
|
115
|
-
}, mermaidDelay)
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* 执行 Mermaid 渲染
|
|
120
|
-
*/
|
|
121
|
-
async function doRenderMermaid() {
|
|
122
|
-
if (!code) return
|
|
123
|
-
|
|
124
|
-
mermaidError = ''
|
|
125
|
-
|
|
126
|
-
try {
|
|
127
|
-
// 动态导入 mermaid
|
|
128
|
-
if (!mermaidRef) {
|
|
129
|
-
// @ts-ignore - mermaid 是可选依赖
|
|
130
|
-
const mermaidModule = await import('mermaid')
|
|
131
|
-
mermaidRef = mermaidModule.default
|
|
132
|
-
mermaidRef.initialize({
|
|
133
|
-
startOnLoad: false,
|
|
134
|
-
theme: 'dark',
|
|
135
|
-
securityLevel: 'loose'
|
|
136
|
-
})
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
const mermaid = mermaidRef
|
|
140
|
-
const id = `mermaid-${Date.now()}-${Math.random().toString(36).slice(2)}`
|
|
141
|
-
|
|
142
|
-
const { svg } = await mermaid.render(id, code)
|
|
143
|
-
mermaidSvg = svg
|
|
144
|
-
} catch (e: any) {
|
|
145
|
-
// 不显示错误,可能是代码还不完整
|
|
146
|
-
mermaidError = ''
|
|
147
|
-
mermaidSvg = ''
|
|
148
|
-
} finally {
|
|
149
|
-
mermaidLoading = false
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* 动态加载 shiki 并高亮
|
|
155
|
-
*/
|
|
156
|
-
async function highlight() {
|
|
157
|
-
if (isMermaid) {
|
|
158
|
-
scheduleRenderMermaid()
|
|
159
|
-
return
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
if (!code || disableHighlight) {
|
|
163
|
-
highlightedHtml = ''
|
|
164
|
-
return
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
isHighlighting = true
|
|
168
|
-
highlightError = false
|
|
169
|
-
|
|
170
|
-
try {
|
|
171
|
-
// 动态导入 shiki
|
|
172
|
-
if (!highlighterRef) {
|
|
173
|
-
const { createHighlighter } = await import('shiki')
|
|
174
|
-
highlighterRef = await createHighlighter({
|
|
175
|
-
themes: [theme as any],
|
|
176
|
-
langs: []
|
|
177
|
-
})
|
|
178
|
-
loadedThemes.add(theme)
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
const highlighter = highlighterRef
|
|
182
|
-
const lang = language
|
|
183
|
-
|
|
184
|
-
// 按需加载语言
|
|
185
|
-
if (!loadedLanguages.has(lang) && lang !== 'text') {
|
|
186
|
-
try {
|
|
187
|
-
await highlighter.loadLanguage(lang)
|
|
188
|
-
loadedLanguages.add(lang)
|
|
189
|
-
} catch {
|
|
190
|
-
// 语言不支持,标记但不阻止
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// 按需加载主题
|
|
195
|
-
if (!loadedThemes.has(theme)) {
|
|
196
|
-
try {
|
|
197
|
-
await highlighter.loadTheme(theme)
|
|
198
|
-
loadedThemes.add(theme)
|
|
199
|
-
} catch {
|
|
200
|
-
// 主题不支持
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
const html = highlighter.codeToHtml(code, {
|
|
205
|
-
lang: loadedLanguages.has(lang) ? lang : 'text',
|
|
206
|
-
theme: loadedThemes.has(theme) ? theme : fallbackTheme
|
|
207
|
-
})
|
|
208
|
-
highlightedHtml = html
|
|
209
|
-
} catch (e) {
|
|
210
|
-
// Shiki 不可用或加载失败
|
|
211
|
-
highlightError = true
|
|
212
|
-
highlightedHtml = ''
|
|
213
|
-
} finally {
|
|
214
|
-
isHighlighting = false
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
* 复制代码
|
|
220
|
-
*/
|
|
221
|
-
async function copyCode() {
|
|
222
|
-
try {
|
|
223
|
-
await navigator.clipboard.writeText(code)
|
|
224
|
-
copied = true
|
|
225
|
-
setTimeout(() => {
|
|
226
|
-
copied = false
|
|
227
|
-
}, 2000)
|
|
228
|
-
} catch {
|
|
229
|
-
// 复制失败静默处理
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// 监听代码变化,重新高亮/渲染
|
|
234
|
-
$effect(() => {
|
|
235
|
-
highlight()
|
|
236
|
-
})
|
|
237
|
-
|
|
238
|
-
// 清理
|
|
239
|
-
onDestroy(() => {
|
|
240
|
-
if (mermaidTimer) {
|
|
241
|
-
clearTimeout(mermaidTimer)
|
|
242
|
-
}
|
|
243
|
-
})
|
|
63
|
+
// 默认代码块组件
|
|
64
|
+
const DefaultCodeBlock = $derived(defaultCodeComponent || IncremarkCodeDefault)
|
|
244
65
|
</script>
|
|
245
66
|
|
|
246
|
-
<!-- 自定义代码块组件 -->
|
|
247
67
|
{#if CustomCodeBlock}
|
|
248
68
|
{@const Component = CustomCodeBlock}
|
|
249
|
-
<Component
|
|
69
|
+
<Component
|
|
70
|
+
codeStr={code}
|
|
71
|
+
lang={language}
|
|
72
|
+
completed={blockStatus === 'completed'}
|
|
73
|
+
takeOver={codeBlockConfigs?.[language]?.takeOver}
|
|
74
|
+
/>
|
|
250
75
|
{:else if isMermaid}
|
|
251
|
-
<
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
<button
|
|
256
|
-
class="code-btn"
|
|
257
|
-
onclick={toggleMermaidView}
|
|
258
|
-
type="button"
|
|
259
|
-
disabled={!mermaidSvg}
|
|
260
|
-
>
|
|
261
|
-
{mermaidViewMode === 'preview' ? '源码' : '预览'}
|
|
262
|
-
</button>
|
|
263
|
-
<button class="code-btn" onclick={copyCode} type="button">
|
|
264
|
-
{copied ? '✓ 已复制' : '复制'}
|
|
265
|
-
</button>
|
|
266
|
-
</div>
|
|
267
|
-
</div>
|
|
268
|
-
<div class="mermaid-content">
|
|
269
|
-
<!-- 加载中 -->
|
|
270
|
-
{#if mermaidLoading && !mermaidSvg}
|
|
271
|
-
<pre class="mermaid-source-code">{code}</pre>
|
|
272
|
-
<!-- 源码模式 -->
|
|
273
|
-
{:else if mermaidViewMode === 'source'}
|
|
274
|
-
<pre class="mermaid-source-code">{code}</pre>
|
|
275
|
-
<!-- 预览模式 -->
|
|
276
|
-
{:else if mermaidSvg}
|
|
277
|
-
{@html mermaidSvg}
|
|
278
|
-
<!-- 无法渲染时显示源码 -->
|
|
279
|
-
{:else}
|
|
280
|
-
<pre class="mermaid-source-code">{code}</pre>
|
|
281
|
-
{/if}
|
|
282
|
-
</div>
|
|
283
|
-
</div>
|
|
76
|
+
<IncremarkCodeMermaid
|
|
77
|
+
{node}
|
|
78
|
+
{mermaidDelay}
|
|
79
|
+
/>
|
|
284
80
|
{:else}
|
|
285
|
-
|
|
286
|
-
<
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
</div>
|
|
293
|
-
<div class="code-content">
|
|
294
|
-
<!-- 正在加载高亮 -->
|
|
295
|
-
{#if isHighlighting && !highlightedHtml}
|
|
296
|
-
<div class="code-loading">
|
|
297
|
-
<pre><code>{code}</code></pre>
|
|
298
|
-
</div>
|
|
299
|
-
<!-- 高亮后的代码 -->
|
|
300
|
-
{:else if highlightedHtml}
|
|
301
|
-
<div class="shiki-wrapper">
|
|
302
|
-
{@html highlightedHtml}
|
|
303
|
-
</div>
|
|
304
|
-
<!-- 回退:无高亮 -->
|
|
305
|
-
{:else}
|
|
306
|
-
<pre class="code-fallback"><code>{code}</code></pre>
|
|
307
|
-
{/if}
|
|
308
|
-
</div>
|
|
309
|
-
</div>
|
|
81
|
+
{@const Component = DefaultCodeBlock}
|
|
82
|
+
<Component
|
|
83
|
+
{node}
|
|
84
|
+
{theme}
|
|
85
|
+
{fallbackTheme}
|
|
86
|
+
{disableHighlight}
|
|
87
|
+
/>
|
|
310
88
|
{/if}
|
|
311
|
-
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Code } from 'mdast';
|
|
2
|
+
import type { Component } from 'svelte';
|
|
2
3
|
/**
|
|
3
4
|
* 组件 Props
|
|
4
5
|
*/
|
|
@@ -21,8 +22,10 @@ interface Props {
|
|
|
21
22
|
}>;
|
|
22
23
|
/** 块状态,用于判断是否使用自定义组件 */
|
|
23
24
|
blockStatus?: 'pending' | 'stable' | 'completed';
|
|
25
|
+
/** 默认代码块渲染组件(当不是 mermaid 且没有自定义组件时使用) */
|
|
26
|
+
defaultCodeComponent?: Component<any>;
|
|
24
27
|
}
|
|
25
|
-
declare const IncremarkCode:
|
|
28
|
+
declare const IncremarkCode: Component<Props, {}, "">;
|
|
26
29
|
type IncremarkCode = ReturnType<typeof IncremarkCode>;
|
|
27
30
|
export default IncremarkCode;
|
|
28
31
|
//# sourceMappingURL=IncremarkCode.svelte.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"IncremarkCode.svelte.d.ts","sourceRoot":"","sources":["../../src/components/IncremarkCode.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"IncremarkCode.svelte.d.ts","sourceRoot":"","sources":["../../src/components/IncremarkCode.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,OAAO,CAAA;AACjC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAA;AAKrC;;GAEG;AACH,UAAU,KAAK;IACb,WAAW;IACX,IAAI,EAAE,IAAI,CAAA;IACV,8BAA8B;IAC9B,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,0CAA0C;IAC1C,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,eAAe;IACf,gBAAgB,CAAC,EAAE,OAAO,CAAA;IAC1B,iCAAiC;IACjC,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,6BAA6B;IAC7B,gBAAgB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IACtC,0BAA0B;IAC1B,gBAAgB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,QAAQ,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC,CAAA;IACzD,wBAAwB;IACxB,WAAW,CAAC,EAAE,SAAS,GAAG,QAAQ,GAAG,WAAW,CAAA;IAChD,yCAAyC;IACzC,oBAAoB,CAAC,EAAE,SAAS,CAAC,GAAG,CAAC,CAAA;CACtC;AAwDH,QAAA,MAAM,aAAa,0BAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Code } from 'mdast'
|
|
3
|
+
import { onDestroy } from 'svelte'
|
|
4
|
+
import { LucideCopy, LucideCopyCheck } from '@incremark/icons'
|
|
5
|
+
import { isClipboardAvailable } from '@incremark/shared'
|
|
6
|
+
import SvgIcon from './SvgIcon.svelte'
|
|
7
|
+
import { useShiki } from '../stores/useShiki.svelte'
|
|
8
|
+
import { useLocale } from '../stores/useLocale.svelte'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 组件 Props
|
|
12
|
+
*/
|
|
13
|
+
interface Props {
|
|
14
|
+
/** 代码节点 */
|
|
15
|
+
node: Code
|
|
16
|
+
/** Shiki 主题,默认 github-dark */
|
|
17
|
+
theme?: string
|
|
18
|
+
/** 默认回退主题(当指定主题加载失败时使用),默认 github-dark */
|
|
19
|
+
fallbackTheme?: string
|
|
20
|
+
/** 是否禁用代码高亮 */
|
|
21
|
+
disableHighlight?: boolean
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let {
|
|
25
|
+
node,
|
|
26
|
+
theme = 'github-dark',
|
|
27
|
+
fallbackTheme = 'github-dark',
|
|
28
|
+
disableHighlight = false
|
|
29
|
+
}: Props = $props()
|
|
30
|
+
|
|
31
|
+
// 状态
|
|
32
|
+
let copied = $state(false)
|
|
33
|
+
let highlightedHtml = $state('')
|
|
34
|
+
|
|
35
|
+
// 使用 Shiki 单例管理器
|
|
36
|
+
const shiki = useShiki(() => theme)
|
|
37
|
+
|
|
38
|
+
// 使用 i18n
|
|
39
|
+
const { t } = useLocale()
|
|
40
|
+
|
|
41
|
+
// 计算属性
|
|
42
|
+
const language = $derived(node.lang || 'text')
|
|
43
|
+
const code = $derived(node.value)
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 动态加载 shiki 并高亮
|
|
47
|
+
*/
|
|
48
|
+
async function doHighlight() {
|
|
49
|
+
if (!code || disableHighlight) {
|
|
50
|
+
highlightedHtml = ''
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
const html = await shiki.highlight(code, language, fallbackTheme)
|
|
56
|
+
highlightedHtml = html
|
|
57
|
+
} catch (e) {
|
|
58
|
+
highlightedHtml = ''
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
let copyTimeoutId: ReturnType<typeof setTimeout> | null = null
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* 复制代码
|
|
66
|
+
*/
|
|
67
|
+
async function copyCode() {
|
|
68
|
+
if (!isClipboardAvailable()) return
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
await navigator.clipboard.writeText(code)
|
|
72
|
+
copied = true
|
|
73
|
+
|
|
74
|
+
if (copyTimeoutId) clearTimeout(copyTimeoutId)
|
|
75
|
+
copyTimeoutId = setTimeout(() => { copied = false }, 2000)
|
|
76
|
+
} catch { /* 静默处理 */ }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
onDestroy(() => {
|
|
80
|
+
if (copyTimeoutId) clearTimeout(copyTimeoutId)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
// 监听代码、主题、语言变化并重新渲染
|
|
84
|
+
$effect(() => {
|
|
85
|
+
doHighlight()
|
|
86
|
+
})
|
|
87
|
+
</script>
|
|
88
|
+
|
|
89
|
+
<div class="incremark-code">
|
|
90
|
+
<div class="code-header">
|
|
91
|
+
<span class="language">{language}</span>
|
|
92
|
+
<button
|
|
93
|
+
class="code-btn"
|
|
94
|
+
onclick={copyCode}
|
|
95
|
+
type="button"
|
|
96
|
+
aria-label={copied ? t('code.copied') : t('code.copy')}
|
|
97
|
+
title={copied ? 'Copied!' : 'Copy'}
|
|
98
|
+
>
|
|
99
|
+
<SvgIcon svg={copied ? LucideCopyCheck : LucideCopy} />
|
|
100
|
+
</button>
|
|
101
|
+
</div>
|
|
102
|
+
<div class="code-content">
|
|
103
|
+
{#if shiki.isHighlighting && !highlightedHtml}
|
|
104
|
+
<div class="code-loading">
|
|
105
|
+
<pre><code>{code}</code></pre>
|
|
106
|
+
</div>
|
|
107
|
+
{:else if highlightedHtml}
|
|
108
|
+
<div class="shiki-wrapper">
|
|
109
|
+
{@html highlightedHtml}
|
|
110
|
+
</div>
|
|
111
|
+
{:else}
|
|
112
|
+
<pre class="code-fallback"><code>{code}</code></pre>
|
|
113
|
+
{/if}
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Code } from 'mdast';
|
|
2
|
+
/**
|
|
3
|
+
* 组件 Props
|
|
4
|
+
*/
|
|
5
|
+
interface Props {
|
|
6
|
+
/** 代码节点 */
|
|
7
|
+
node: Code;
|
|
8
|
+
/** Shiki 主题,默认 github-dark */
|
|
9
|
+
theme?: string;
|
|
10
|
+
/** 默认回退主题(当指定主题加载失败时使用),默认 github-dark */
|
|
11
|
+
fallbackTheme?: string;
|
|
12
|
+
/** 是否禁用代码高亮 */
|
|
13
|
+
disableHighlight?: boolean;
|
|
14
|
+
}
|
|
15
|
+
declare const IncremarkCodeDefault: import("svelte").Component<Props, {}, "">;
|
|
16
|
+
type IncremarkCodeDefault = ReturnType<typeof IncremarkCodeDefault>;
|
|
17
|
+
export default IncremarkCodeDefault;
|
|
18
|
+
//# sourceMappingURL=IncremarkCodeDefault.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"IncremarkCodeDefault.svelte.d.ts","sourceRoot":"","sources":["../../src/components/IncremarkCodeDefault.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,OAAO,CAAA;AAS/B;;GAEG;AACH,UAAU,KAAK;IACb,WAAW;IACX,IAAI,EAAE,IAAI,CAAA;IACV,8BAA8B;IAC9B,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,0CAA0C;IAC1C,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,eAAe;IACf,gBAAgB,CAAC,EAAE,OAAO,CAAA;CAC3B;AAoGH,QAAA,MAAM,oBAAoB,2CAAwC,CAAC;AACnE,KAAK,oBAAoB,GAAG,UAAU,CAAC,OAAO,oBAAoB,CAAC,CAAC;AACpE,eAAe,oBAAoB,CAAC"}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Code } from 'mdast'
|
|
3
|
+
import { onDestroy } from 'svelte'
|
|
4
|
+
import { GravityMermaid, LucideCode, LucideEye, LucideCopy, LucideCopyCheck } from '@incremark/icons'
|
|
5
|
+
import { isClipboardAvailable } from '@incremark/shared'
|
|
6
|
+
import SvgIcon from './SvgIcon.svelte'
|
|
7
|
+
import { useLocale } from '../stores/useLocale.svelte'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 组件 Props
|
|
11
|
+
*/
|
|
12
|
+
interface Props {
|
|
13
|
+
/** 代码节点 */
|
|
14
|
+
node: Code
|
|
15
|
+
/** Mermaid 渲染延迟(毫秒),用于流式输入时防抖 */
|
|
16
|
+
mermaidDelay?: number
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let {
|
|
20
|
+
node,
|
|
21
|
+
mermaidDelay = 500
|
|
22
|
+
}: Props = $props()
|
|
23
|
+
|
|
24
|
+
// 状态
|
|
25
|
+
let copied = $state(false)
|
|
26
|
+
let mermaidSvg = $state('')
|
|
27
|
+
let mermaidLoading = $state(false)
|
|
28
|
+
let mermaidRef: any = null
|
|
29
|
+
let mermaidTimer: ReturnType<typeof setTimeout> | null = null
|
|
30
|
+
let mermaidViewMode = $state<'preview' | 'source'>('preview')
|
|
31
|
+
|
|
32
|
+
// 计算属性
|
|
33
|
+
const code = $derived(node.value)
|
|
34
|
+
|
|
35
|
+
// 使用 i18n
|
|
36
|
+
const { t } = useLocale()
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 切换 Mermaid 视图模式
|
|
40
|
+
*/
|
|
41
|
+
function toggleMermaidView() {
|
|
42
|
+
mermaidViewMode = mermaidViewMode === 'preview' ? 'source' : 'preview'
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 执行 Mermaid 渲染
|
|
47
|
+
*/
|
|
48
|
+
async function doRenderMermaid() {
|
|
49
|
+
if (!code) return
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
if (!mermaidRef) {
|
|
53
|
+
// @ts-ignore
|
|
54
|
+
const mermaidModule = await import('mermaid')
|
|
55
|
+
mermaidRef = mermaidModule.default
|
|
56
|
+
mermaidRef.initialize({
|
|
57
|
+
startOnLoad: false,
|
|
58
|
+
theme: 'dark',
|
|
59
|
+
securityLevel: 'loose',
|
|
60
|
+
suppressErrorRendering: true
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const id = `mermaid-${Date.now()}-${Math.random().toString(36).slice(2)}`
|
|
65
|
+
const { svg } = await mermaidRef.render(id, code)
|
|
66
|
+
mermaidSvg = svg
|
|
67
|
+
} catch (e: any) {
|
|
68
|
+
mermaidSvg = ''
|
|
69
|
+
} finally {
|
|
70
|
+
mermaidLoading = false
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
let copyTimeoutId: ReturnType<typeof setTimeout> | null = null
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* 复制代码
|
|
78
|
+
*/
|
|
79
|
+
async function copyCode() {
|
|
80
|
+
if (!isClipboardAvailable()) return
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
await navigator.clipboard.writeText(code)
|
|
84
|
+
copied = true
|
|
85
|
+
|
|
86
|
+
if (copyTimeoutId) clearTimeout(copyTimeoutId)
|
|
87
|
+
copyTimeoutId = setTimeout(() => { copied = false }, 2000)
|
|
88
|
+
} catch { /* 静默处理 */ }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 监听代码变化并重新渲染
|
|
92
|
+
$effect(() => {
|
|
93
|
+
// Mermaid 防抖逻辑
|
|
94
|
+
if (mermaidTimer) clearTimeout(mermaidTimer)
|
|
95
|
+
mermaidLoading = true
|
|
96
|
+
mermaidTimer = setTimeout(doRenderMermaid, mermaidDelay)
|
|
97
|
+
|
|
98
|
+
// 返回清理函数
|
|
99
|
+
return () => {
|
|
100
|
+
if (mermaidTimer) clearTimeout(mermaidTimer)
|
|
101
|
+
}
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
onDestroy(() => {
|
|
105
|
+
if (copyTimeoutId) clearTimeout(copyTimeoutId)
|
|
106
|
+
})
|
|
107
|
+
</script>
|
|
108
|
+
|
|
109
|
+
<div class="incremark-mermaid">
|
|
110
|
+
<div class="mermaid-header">
|
|
111
|
+
<span class="language">
|
|
112
|
+
<SvgIcon svg={GravityMermaid} class="language-icon" />
|
|
113
|
+
MERMAID
|
|
114
|
+
</span>
|
|
115
|
+
<div class="mermaid-actions">
|
|
116
|
+
<button
|
|
117
|
+
class="code-btn"
|
|
118
|
+
onclick={toggleMermaidView}
|
|
119
|
+
type="button"
|
|
120
|
+
disabled={!mermaidSvg}
|
|
121
|
+
aria-label={mermaidViewMode === 'preview' ? t('mermaid.viewSource') : t('mermaid.preview')}
|
|
122
|
+
title={mermaidViewMode === 'preview' ? 'View Source' : 'Preview'}
|
|
123
|
+
>
|
|
124
|
+
<SvgIcon svg={mermaidViewMode === 'preview' ? LucideCode : LucideEye} />
|
|
125
|
+
</button>
|
|
126
|
+
<button
|
|
127
|
+
class="code-btn"
|
|
128
|
+
onclick={copyCode}
|
|
129
|
+
type="button"
|
|
130
|
+
aria-label={copied ? t('mermaid.copied') : t('mermaid.copy')}
|
|
131
|
+
title={copied ? 'Copied!' : 'Copy'}
|
|
132
|
+
>
|
|
133
|
+
<SvgIcon svg={copied ? LucideCopyCheck : LucideCopy} />
|
|
134
|
+
</button>
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
<div class="mermaid-content">
|
|
138
|
+
{#if mermaidLoading && !mermaidSvg}
|
|
139
|
+
<pre class="mermaid-source-code">{code}</pre>
|
|
140
|
+
{:else if mermaidViewMode === 'source'}
|
|
141
|
+
<pre class="mermaid-source-code">{code}</pre>
|
|
142
|
+
{:else if mermaidSvg}
|
|
143
|
+
{@html mermaidSvg}
|
|
144
|
+
{:else}
|
|
145
|
+
<pre class="mermaid-source-code">{code}</pre>
|
|
146
|
+
{/if}
|
|
147
|
+
</div>
|
|
148
|
+
</div>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Code } from 'mdast';
|
|
2
|
+
/**
|
|
3
|
+
* 组件 Props
|
|
4
|
+
*/
|
|
5
|
+
interface Props {
|
|
6
|
+
/** 代码节点 */
|
|
7
|
+
node: Code;
|
|
8
|
+
/** Mermaid 渲染延迟(毫秒),用于流式输入时防抖 */
|
|
9
|
+
mermaidDelay?: number;
|
|
10
|
+
}
|
|
11
|
+
declare const IncremarkCodeMermaid: import("svelte").Component<Props, {}, "">;
|
|
12
|
+
type IncremarkCodeMermaid = ReturnType<typeof IncremarkCodeMermaid>;
|
|
13
|
+
export default IncremarkCodeMermaid;
|
|
14
|
+
//# sourceMappingURL=IncremarkCodeMermaid.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"IncremarkCodeMermaid.svelte.d.ts","sourceRoot":"","sources":["../../src/components/IncremarkCodeMermaid.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,OAAO,CAAA;AAQ/B;;GAEG;AACH,UAAU,KAAK;IACb,WAAW;IACX,IAAI,EAAE,IAAI,CAAA;IACV,iCAAiC;IACjC,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB;AAkIH,QAAA,MAAM,oBAAoB,2CAAwC,CAAC;AACnE,KAAK,oBAAoB,GAAG,UAAU,CAAC,OAAO,oBAAoB,CAAC,CAAC;AACpE,eAAe,oBAAoB,CAAC"}
|