@incremark/svelte 0.2.7 → 0.3.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/README.en.md +274 -0
- package/dist/ThemeProvider.svelte +4 -1
- package/dist/ThemeProvider.svelte.d.ts.map +1 -1
- 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 +62 -71
- package/dist/components/Incremark.svelte.d.ts +4 -6
- package/dist/components/Incremark.svelte.d.ts.map +1 -1
- package/dist/components/IncremarkCode.svelte +20 -152
- 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 +41 -10
- package/dist/components/IncremarkContent.svelte.d.ts +1 -2
- package/dist/components/IncremarkContent.svelte.d.ts.map +1 -1
- 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/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/index.d.ts +7 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -1
- 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} +8 -7
- package/dist/stores/useIncremark.d.ts +4 -2
- package/dist/stores/useIncremark.d.ts.map +1 -1
- package/dist/stores/useIncremark.js +19 -0
- 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.map +1 -1
- package/dist/stores/useShiki.svelte.js +17 -5
- package/package.json +13 -7
- package/dist/stores/useDevTools.d.ts.map +0 -1
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Code } from 'mdast'
|
|
3
|
-
import {
|
|
3
|
+
import type { Component } from 'svelte'
|
|
4
|
+
import IncremarkCodeMermaid from './IncremarkCodeMermaid.svelte'
|
|
5
|
+
import IncremarkCodeDefault from './IncremarkCodeDefault.svelte'
|
|
4
6
|
|
|
5
7
|
/**
|
|
6
8
|
* 组件 Props
|
|
@@ -22,6 +24,8 @@
|
|
|
22
24
|
codeBlockConfigs?: Record<string, { takeOver?: boolean }>
|
|
23
25
|
/** 块状态,用于判断是否使用自定义组件 */
|
|
24
26
|
blockStatus?: 'pending' | 'stable' | 'completed'
|
|
27
|
+
/** 默认代码块渲染组件(当不是 mermaid 且没有自定义组件时使用) */
|
|
28
|
+
defaultCodeComponent?: Component<any>
|
|
25
29
|
}
|
|
26
30
|
|
|
27
31
|
let {
|
|
@@ -32,25 +36,10 @@
|
|
|
32
36
|
mermaidDelay = 500,
|
|
33
37
|
customCodeBlocks,
|
|
34
38
|
codeBlockConfigs,
|
|
35
|
-
blockStatus = 'completed'
|
|
39
|
+
blockStatus = 'completed',
|
|
40
|
+
defaultCodeComponent
|
|
36
41
|
}: Props = $props()
|
|
37
42
|
|
|
38
|
-
// 状态
|
|
39
|
-
let copied = $state(false)
|
|
40
|
-
let highlightedHtml = $state('')
|
|
41
|
-
|
|
42
|
-
// Mermaid 支持
|
|
43
|
-
let mermaidSvg = $state('')
|
|
44
|
-
let mermaidError = $state('')
|
|
45
|
-
let mermaidLoading = $state(false)
|
|
46
|
-
let mermaidRef: any = null
|
|
47
|
-
let mermaidTimer: ReturnType<typeof setTimeout> | null = null
|
|
48
|
-
// 视图模式:'preview' | 'source'
|
|
49
|
-
let mermaidViewMode = $state<'preview' | 'source'>('preview')
|
|
50
|
-
|
|
51
|
-
// 使用 Shiki 单例管理器 - 修复:传入 getter 闭包以保持响应式
|
|
52
|
-
const shiki = useShiki(() => theme)
|
|
53
|
-
|
|
54
43
|
/**
|
|
55
44
|
* 计算属性
|
|
56
45
|
*/
|
|
@@ -71,90 +60,8 @@
|
|
|
71
60
|
return component
|
|
72
61
|
})
|
|
73
62
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
*/
|
|
77
|
-
function toggleMermaidView() {
|
|
78
|
-
mermaidViewMode = mermaidViewMode === 'preview' ? 'source' : 'preview'
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* 执行 Mermaid 渲染
|
|
83
|
-
*/
|
|
84
|
-
async function doRenderMermaid() {
|
|
85
|
-
if (!code) return
|
|
86
|
-
mermaidError = ''
|
|
87
|
-
|
|
88
|
-
try {
|
|
89
|
-
if (!mermaidRef) {
|
|
90
|
-
// @ts-ignore
|
|
91
|
-
const mermaidModule = await import('mermaid')
|
|
92
|
-
mermaidRef = mermaidModule.default
|
|
93
|
-
mermaidRef.initialize({
|
|
94
|
-
startOnLoad: false,
|
|
95
|
-
theme: 'dark',
|
|
96
|
-
securityLevel: 'loose',
|
|
97
|
-
suppressErrorRendering: true
|
|
98
|
-
})
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const id = `mermaid-${Date.now()}-${Math.random().toString(36).slice(2)}`
|
|
102
|
-
const { svg } = await mermaidRef.render(id, code)
|
|
103
|
-
mermaidSvg = svg
|
|
104
|
-
} catch (e: any) {
|
|
105
|
-
mermaidError = ''
|
|
106
|
-
mermaidSvg = ''
|
|
107
|
-
} finally {
|
|
108
|
-
mermaidLoading = false
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* 动态加载 shiki 并高亮
|
|
114
|
-
*/
|
|
115
|
-
async function doHighlight() {
|
|
116
|
-
if (isMermaid) {
|
|
117
|
-
// Mermaid 防抖逻辑
|
|
118
|
-
if (mermaidTimer) clearTimeout(mermaidTimer)
|
|
119
|
-
mermaidLoading = true
|
|
120
|
-
mermaidTimer = setTimeout(doRenderMermaid, mermaidDelay)
|
|
121
|
-
return
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
if (!code || disableHighlight) {
|
|
125
|
-
highlightedHtml = ''
|
|
126
|
-
return
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
try {
|
|
130
|
-
// 调用经过修复的 shiki.highlight
|
|
131
|
-
const html = await shiki.highlight(code, language, fallbackTheme)
|
|
132
|
-
highlightedHtml = html
|
|
133
|
-
} catch (e) {
|
|
134
|
-
highlightedHtml = ''
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* 复制代码
|
|
140
|
-
*/
|
|
141
|
-
async function copyCode() {
|
|
142
|
-
try {
|
|
143
|
-
await navigator.clipboard.writeText(code)
|
|
144
|
-
copied = true
|
|
145
|
-
setTimeout(() => { copied = false }, 2000)
|
|
146
|
-
} catch { /* 静默处理 */ }
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// 监听代码、主题、语言变化并重新渲染
|
|
150
|
-
$effect(() => {
|
|
151
|
-
doHighlight()
|
|
152
|
-
|
|
153
|
-
// 返回清理函数取代 onDestroy
|
|
154
|
-
return () => {
|
|
155
|
-
if (mermaidTimer) clearTimeout(mermaidTimer)
|
|
156
|
-
}
|
|
157
|
-
})
|
|
63
|
+
// 默认代码块组件
|
|
64
|
+
const DefaultCodeBlock = $derived(defaultCodeComponent || IncremarkCodeDefault)
|
|
158
65
|
</script>
|
|
159
66
|
|
|
160
67
|
{#if CustomCodeBlock}
|
|
@@ -166,55 +73,16 @@
|
|
|
166
73
|
takeOver={codeBlockConfigs?.[language]?.takeOver}
|
|
167
74
|
/>
|
|
168
75
|
{:else if isMermaid}
|
|
169
|
-
<
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
<button
|
|
174
|
-
class="code-btn"
|
|
175
|
-
onclick={toggleMermaidView}
|
|
176
|
-
type="button"
|
|
177
|
-
disabled={!mermaidSvg}
|
|
178
|
-
>
|
|
179
|
-
{mermaidViewMode === 'preview' ? '源码' : '预览'}
|
|
180
|
-
</button>
|
|
181
|
-
<button class="code-btn" onclick={copyCode} type="button">
|
|
182
|
-
{copied ? '✓ 已复制' : '复制'}
|
|
183
|
-
</button>
|
|
184
|
-
</div>
|
|
185
|
-
</div>
|
|
186
|
-
<div class="mermaid-content">
|
|
187
|
-
{#if mermaidLoading && !mermaidSvg}
|
|
188
|
-
<pre class="mermaid-source-code">{code}</pre>
|
|
189
|
-
{:else if mermaidViewMode === 'source'}
|
|
190
|
-
<pre class="mermaid-source-code">{code}</pre>
|
|
191
|
-
{:else if mermaidSvg}
|
|
192
|
-
{@html mermaidSvg}
|
|
193
|
-
{:else}
|
|
194
|
-
<pre class="mermaid-source-code">{code}</pre>
|
|
195
|
-
{/if}
|
|
196
|
-
</div>
|
|
197
|
-
</div>
|
|
76
|
+
<IncremarkCodeMermaid
|
|
77
|
+
{node}
|
|
78
|
+
{mermaidDelay}
|
|
79
|
+
/>
|
|
198
80
|
{:else}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
<div class="code-content">
|
|
207
|
-
{#if shiki.isHighlighting && !highlightedHtml}
|
|
208
|
-
<div class="code-loading">
|
|
209
|
-
<pre><code>{code}</code></pre>
|
|
210
|
-
</div>
|
|
211
|
-
{:else if highlightedHtml}
|
|
212
|
-
<div class="shiki-wrapper">
|
|
213
|
-
{@html highlightedHtml}
|
|
214
|
-
</div>
|
|
215
|
-
{:else}
|
|
216
|
-
<pre class="code-fallback"><code>{code}</code></pre>
|
|
217
|
-
{/if}
|
|
218
|
-
</div>
|
|
219
|
-
</div>
|
|
81
|
+
{@const Component = DefaultCodeBlock}
|
|
82
|
+
<Component
|
|
83
|
+
{node}
|
|
84
|
+
{theme}
|
|
85
|
+
{fallbackTheme}
|
|
86
|
+
{disableHighlight}
|
|
87
|
+
/>
|
|
220
88
|
{/if}
|
|
@@ -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"}
|
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
-->
|
|
5
5
|
|
|
6
6
|
<script lang="ts">
|
|
7
|
-
import type { Component } from 'svelte'
|
|
8
7
|
import { useIncremark, type UseIncremarkOptions } from '../stores/useIncremark'
|
|
9
|
-
import type {
|
|
8
|
+
import type { IncremarkContentProps } from './types'
|
|
10
9
|
import Incremark from './Incremark.svelte'
|
|
11
10
|
import { get } from 'svelte/store'
|
|
11
|
+
import { untrack } from 'svelte'
|
|
12
12
|
|
|
13
13
|
let {
|
|
14
14
|
stream,
|
|
@@ -23,16 +23,47 @@
|
|
|
23
23
|
showBlockStatus = false
|
|
24
24
|
}: IncremarkContentProps = $props()
|
|
25
25
|
|
|
26
|
-
//
|
|
27
|
-
const
|
|
28
|
-
gfm: true,
|
|
29
|
-
htmlTree: true,
|
|
30
|
-
containers: true,
|
|
31
|
-
math: true,
|
|
32
|
-
|
|
26
|
+
// 创建 incremark 实例(只创建一次)
|
|
27
|
+
const incremarkInstance = untrack(() => useIncremark({
|
|
28
|
+
gfm: incremarkOptions?.gfm ?? true,
|
|
29
|
+
htmlTree: incremarkOptions?.htmlTree ?? true,
|
|
30
|
+
containers: incremarkOptions?.containers ?? true,
|
|
31
|
+
math: incremarkOptions?.math ?? true,
|
|
32
|
+
astBuilder: incremarkOptions?.astBuilder,
|
|
33
|
+
typewriter: incremarkOptions?.typewriter
|
|
34
|
+
}))
|
|
35
|
+
|
|
36
|
+
// 解构出需要的方法和状态
|
|
37
|
+
const { blocks, append, finalize, render, reset, updateOptions, isDisplayComplete, markdown, typewriter } = incremarkInstance
|
|
38
|
+
|
|
39
|
+
// 计算 parser 配置的稳定 key(用于检测变化)
|
|
40
|
+
// astBuilder 用名称标识,因为它是类不能 JSON.stringify
|
|
41
|
+
function getConfigKey() {
|
|
42
|
+
const { typewriter: _, astBuilder, ...parserOptions } = incremarkOptions ?? {}
|
|
43
|
+
return JSON.stringify(parserOptions) + '|' + (astBuilder?.name ?? 'default')
|
|
33
44
|
}
|
|
34
45
|
|
|
35
|
-
|
|
46
|
+
// 监听 parser 配置变化,使用 updateOptions 动态更新(不重建实例)
|
|
47
|
+
let prevConfigKey = getConfigKey()
|
|
48
|
+
let isFirstRender = true
|
|
49
|
+
$effect(() => {
|
|
50
|
+
const currentConfigKey = getConfigKey()
|
|
51
|
+
if (isFirstRender) {
|
|
52
|
+
isFirstRender = false
|
|
53
|
+
return
|
|
54
|
+
}
|
|
55
|
+
if (prevConfigKey !== currentConfigKey) {
|
|
56
|
+
prevConfigKey = currentConfigKey
|
|
57
|
+
// 使用 updateOptions 动态更新配置(包括引擎切换)
|
|
58
|
+
updateOptions({
|
|
59
|
+
gfm: incremarkOptions?.gfm ?? true,
|
|
60
|
+
htmlTree: incremarkOptions?.htmlTree ?? true,
|
|
61
|
+
containers: incremarkOptions?.containers ?? true,
|
|
62
|
+
math: incremarkOptions?.math ?? true,
|
|
63
|
+
astBuilder: incremarkOptions?.astBuilder
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
})
|
|
36
67
|
|
|
37
68
|
// 监听 incremarkOptions 的变化,更新 typewriter 配置
|
|
38
69
|
$effect(() => {
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import type { Component } from 'svelte';
|
|
2
1
|
import type { IncremarkContentProps } from './types';
|
|
3
|
-
declare const IncremarkContent: Component<IncremarkContentProps, {}, "">;
|
|
2
|
+
declare const IncremarkContent: import("svelte").Component<IncremarkContentProps, {}, "">;
|
|
4
3
|
type IncremarkContent = ReturnType<typeof IncremarkContent>;
|
|
5
4
|
export default IncremarkContent;
|
|
6
5
|
//# sourceMappingURL=IncremarkContent.svelte.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"IncremarkContent.svelte.d.ts","sourceRoot":"","sources":["../../src/components/IncremarkContent.svelte.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"IncremarkContent.svelte.d.ts","sourceRoot":"","sources":["../../src/components/IncremarkContent.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAA;AA6IpD,QAAA,MAAM,gBAAgB,2DAAwC,CAAC;AAC/D,KAAK,gBAAgB,GAAG,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAC5D,eAAe,gBAAgB,CAAC"}
|
|
@@ -4,26 +4,22 @@
|
|
|
4
4
|
-->
|
|
5
5
|
|
|
6
6
|
<script lang="ts">
|
|
7
|
-
import type {
|
|
8
|
-
import type { FootnoteDefinition, RootContent } from 'mdast'
|
|
7
|
+
import type { RootContent } from 'mdast'
|
|
9
8
|
import { getDefinitionsContext } from '../context/definitionsContext'
|
|
10
9
|
import IncremarkRenderer from './IncremarkRenderer.svelte'
|
|
11
10
|
|
|
12
|
-
|
|
13
11
|
const context = getDefinitionsContext()
|
|
14
|
-
|
|
15
|
-
const footnoteReferenceOrder =
|
|
12
|
+
// 解构 store 以便使用 $ 语法订阅
|
|
13
|
+
const { footnoteDefinitions, footnoteReferenceOrder } = context
|
|
16
14
|
|
|
17
15
|
/**
|
|
18
16
|
* 按引用顺序排列的脚注列表
|
|
19
17
|
* 只显示已有定义的脚注
|
|
20
18
|
*/
|
|
21
19
|
const orderedFootnotes = $derived.by(() => {
|
|
22
|
-
if (!footnoteReferenceOrder || !footnoteDefinitions) {
|
|
23
|
-
return []
|
|
24
|
-
}
|
|
25
20
|
const order = $footnoteReferenceOrder
|
|
26
21
|
const definitions = $footnoteDefinitions
|
|
22
|
+
|
|
27
23
|
return order
|
|
28
24
|
.map(identifier => ({
|
|
29
25
|
identifier,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"IncremarkFootnotes.svelte.d.ts","sourceRoot":"","sources":["../../src/components/IncremarkFootnotes.svelte.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"IncremarkFootnotes.svelte.d.ts","sourceRoot":"","sources":["../../src/components/IncremarkFootnotes.svelte.ts"],"names":[],"mappings":"AAuEA,QAAA,MAAM,kBAAkB,2DAAwC,CAAC;AACjE,KAAK,kBAAkB,GAAG,UAAU,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAChE,eAAe,kBAAkB,CAAC"}
|
|
@@ -69,10 +69,10 @@
|
|
|
69
69
|
return (node as unknown as MathNode).type === 'inlineMath'
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
// 获取 definitions context
|
|
73
|
-
// 使用 $derived 来确保响应式
|
|
72
|
+
// 获取 definitions context
|
|
74
73
|
const context = getDefinitionsContext()
|
|
75
|
-
|
|
74
|
+
// 解构 store 以便使用 $ 语法订阅
|
|
75
|
+
const { definations } = context
|
|
76
76
|
|
|
77
77
|
|
|
78
78
|
/**
|