@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.
Files changed (58) hide show
  1. package/README.en.md +274 -0
  2. package/dist/ThemeProvider.svelte +4 -1
  3. package/dist/ThemeProvider.svelte.d.ts.map +1 -1
  4. package/dist/components/AutoScrollContainer.svelte +18 -18
  5. package/dist/components/ConfigProvider.svelte +18 -0
  6. package/dist/components/ConfigProvider.svelte.d.ts +9 -0
  7. package/dist/components/ConfigProvider.svelte.d.ts.map +1 -0
  8. package/dist/components/Incremark.svelte +63 -72
  9. package/dist/components/Incremark.svelte.d.ts +5 -7
  10. package/dist/components/Incremark.svelte.d.ts.map +1 -1
  11. package/dist/components/IncremarkCode.svelte +28 -251
  12. package/dist/components/IncremarkCode.svelte.d.ts +4 -1
  13. package/dist/components/IncremarkCode.svelte.d.ts.map +1 -1
  14. package/dist/components/IncremarkCodeDefault.svelte +115 -0
  15. package/dist/components/IncremarkCodeDefault.svelte.d.ts +18 -0
  16. package/dist/components/IncremarkCodeDefault.svelte.d.ts.map +1 -0
  17. package/dist/components/IncremarkCodeMermaid.svelte +148 -0
  18. package/dist/components/IncremarkCodeMermaid.svelte.d.ts +14 -0
  19. package/dist/components/IncremarkCodeMermaid.svelte.d.ts.map +1 -0
  20. package/dist/components/IncremarkContent.svelte +115 -0
  21. package/dist/components/IncremarkContent.svelte.d.ts +5 -0
  22. package/dist/components/IncremarkContent.svelte.d.ts.map +1 -0
  23. package/dist/components/IncremarkFootnotes.svelte +4 -8
  24. package/dist/components/IncremarkFootnotes.svelte.d.ts.map +1 -1
  25. package/dist/components/IncremarkInline.svelte +3 -3
  26. package/dist/components/IncremarkList.svelte +1 -0
  27. package/dist/components/IncremarkRenderer.svelte +4 -0
  28. package/dist/components/IncremarkRenderer.svelte.d.ts +2 -0
  29. package/dist/components/IncremarkRenderer.svelte.d.ts.map +1 -1
  30. package/dist/components/IncremarkTable.svelte +6 -9
  31. package/dist/components/IncremarkTable.svelte.d.ts.map +1 -1
  32. package/dist/components/SvgIcon.svelte +25 -0
  33. package/dist/components/SvgIcon.svelte.d.ts +12 -0
  34. package/dist/components/SvgIcon.svelte.d.ts.map +1 -0
  35. package/dist/components/index.d.ts +2 -1
  36. package/dist/components/index.d.ts.map +1 -1
  37. package/dist/components/index.js +1 -0
  38. package/dist/components/types.d.ts +29 -3
  39. package/dist/components/types.d.ts.map +1 -1
  40. package/dist/index.d.ts +8 -2
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +7 -2
  43. package/dist/stores/{useDevTools.d.ts → useDevTools.svelte.d.ts} +1 -1
  44. package/dist/stores/useDevTools.svelte.d.ts.map +1 -0
  45. package/dist/stores/{useDevTools.js → useDevTools.svelte.js} +10 -9
  46. package/dist/stores/useIncremark.d.ts +4 -4
  47. package/dist/stores/useIncremark.d.ts.map +1 -1
  48. package/dist/stores/useLocale.svelte.d.ts +29 -0
  49. package/dist/stores/useLocale.svelte.d.ts.map +1 -0
  50. package/dist/stores/useLocale.svelte.js +32 -0
  51. package/dist/stores/useShiki.svelte.d.ts +9 -0
  52. package/dist/stores/useShiki.svelte.d.ts.map +1 -0
  53. package/dist/stores/useShiki.svelte.js +110 -0
  54. package/dist/stores/useTypewriter.d.ts +0 -1
  55. package/dist/stores/useTypewriter.d.ts.map +1 -1
  56. package/dist/stores/useTypewriter.js +1 -3
  57. package/package.json +13 -7
  58. 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 { onMount, onDestroy } from 'svelte'
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
- // 如果配置了 takeOver true,则从一开始就使用
79
- if (config?.takeOver) {
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
- * 切换 Mermaid 视图模式
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 codeStr={code} lang={language} completed={blockStatus === 'completed'} takeOver={codeBlockConfigs?.[language]?.takeOver} />
69
+ <Component
70
+ codeStr={code}
71
+ lang={language}
72
+ completed={blockStatus === 'completed'}
73
+ takeOver={codeBlockConfigs?.[language]?.takeOver}
74
+ />
250
75
  {:else if isMermaid}
251
- <div class="incremark-mermaid">
252
- <div class="mermaid-header">
253
- <span class="language">MERMAID</span>
254
- <div class="mermaid-actions">
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
- <div class="incremark-code">
287
- <div class="code-header">
288
- <span class="language">{language}</span>
289
- <button class="code-btn" onclick={copyCode} type="button">
290
- {copied ? '✓ 已复制' : '复制'}
291
- </button>
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: import("svelte").Component<Props, {}, "">;
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;AAI/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;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;CACjD;AA8RH,QAAA,MAAM,aAAa,2CAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
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"}