@humanspeak/svelte-markdown 1.0.4 → 1.1.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.md +42 -0
- package/dist/Parser.svelte +11 -6
- package/dist/Parser.svelte.d.ts +3 -2
- package/dist/SvelteMarkdown.svelte +62 -7
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/renderers/TableCell.svelte +2 -2
- package/dist/renderers/TableCell.svelte.d.ts +2 -2
- package/dist/types.d.ts +24 -2
- package/dist/utils/incremental-parser.d.ts +65 -0
- package/dist/utils/incremental-parser.js +75 -0
- package/dist/utils/parse-and-cache.d.ts +20 -2
- package/dist/utils/parse-and-cache.js +21 -7
- package/dist/utils/token-cache.d.ts +1 -1
- package/dist/utils/token-cache.js +4 -4
- package/dist/utils/token-cleanup.d.ts +0 -45
- package/dist/utils/token-cleanup.js +26 -18
- package/package.json +16 -16
package/README.md
CHANGED
|
@@ -27,6 +27,7 @@ A powerful, customizable markdown renderer for Svelte with TypeScript support. B
|
|
|
27
27
|
- 🧪 Comprehensive test coverage (vitest and playwright)
|
|
28
28
|
- 🧩 First-class marked extensions support via `extensions` prop (e.g., KaTeX math, alerts)
|
|
29
29
|
- ⚡ Intelligent token caching (50-200x faster re-renders)
|
|
30
|
+
- 📡 LLM streaming mode with incremental rendering (~1.6ms avg per update)
|
|
30
31
|
- 🖼️ Smart image lazy loading with fade-in animation
|
|
31
32
|
|
|
32
33
|
## Installation
|
|
@@ -678,6 +679,46 @@ Images automatically lazy load using native `loading="lazy"` and IntersectionObs
|
|
|
678
679
|
<SvelteMarkdown source={markdown} {renderers} />
|
|
679
680
|
```
|
|
680
681
|
|
|
682
|
+
### LLM Streaming
|
|
683
|
+
|
|
684
|
+
For real-time rendering of AI responses from ChatGPT, Claude, Gemini, and other LLMs, enable the `streaming` prop. This uses a smart diff algorithm that re-parses the full source for correctness but only updates changed DOM nodes, keeping render times constant regardless of document size.
|
|
685
|
+
|
|
686
|
+
```svelte
|
|
687
|
+
<script lang="ts">
|
|
688
|
+
import SvelteMarkdown from '@humanspeak/svelte-markdown'
|
|
689
|
+
|
|
690
|
+
let source = $state('')
|
|
691
|
+
|
|
692
|
+
async function streamResponse() {
|
|
693
|
+
const response = await fetch('/api/chat', { method: 'POST', body: '...' })
|
|
694
|
+
const reader = response.body.getReader()
|
|
695
|
+
const decoder = new TextDecoder()
|
|
696
|
+
|
|
697
|
+
while (true) {
|
|
698
|
+
const { done, value } = await reader.read()
|
|
699
|
+
if (done) break
|
|
700
|
+
source += decoder.decode(value, { stream: true })
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
</script>
|
|
704
|
+
|
|
705
|
+
<SvelteMarkdown {source} streaming={true} />
|
|
706
|
+
```
|
|
707
|
+
|
|
708
|
+
**Performance** (measured at 100 characters/sec, character mode):
|
|
709
|
+
|
|
710
|
+
| Metric | Standard Mode | Streaming Mode |
|
|
711
|
+
| -------------- | :-----------: | :------------: |
|
|
712
|
+
| Average render | ~3.6ms | ~1.6ms |
|
|
713
|
+
| Peak render | ~21ms | ~10ms |
|
|
714
|
+
| Dropped frames | 0 | 0 |
|
|
715
|
+
|
|
716
|
+
When `streaming` is `false` (default), existing behavior is unchanged. The `streaming` prop skips cache lookups (always a miss during streaming) and uses in-place token array mutation so Svelte only re-renders components for tokens that actually changed.
|
|
717
|
+
|
|
718
|
+
**Note:** `streaming` is automatically disabled when async extensions (e.g., `markedMermaid`) are used. A console warning is logged in this case.
|
|
719
|
+
|
|
720
|
+
See the [full streaming documentation](https://markdown.svelte.page/docs/advanced/llm-streaming) and [interactive demo](https://markdown.svelte.page/examples/llm-streaming).
|
|
721
|
+
|
|
681
722
|
## Available Renderers
|
|
682
723
|
|
|
683
724
|
- `text` - Text within other elements
|
|
@@ -764,6 +805,7 @@ The component emits a `parsed` event when tokens are calculated:
|
|
|
764
805
|
| Prop | Type | Description |
|
|
765
806
|
| ---------- | ----------------------- | ------------------------------------------------ |
|
|
766
807
|
| source | `string \| Token[]` | Markdown content or pre-parsed tokens |
|
|
808
|
+
| streaming | `boolean` | Enable incremental rendering for LLM streaming |
|
|
767
809
|
| renderers | `Partial<Renderers>` | Custom component overrides |
|
|
768
810
|
| options | `SvelteMarkdownOptions` | Marked parser configuration |
|
|
769
811
|
| isInline | `boolean` | Toggle inline parsing mode |
|
package/dist/Parser.svelte
CHANGED
|
@@ -56,6 +56,9 @@
|
|
|
56
56
|
RendererComponent
|
|
57
57
|
} from './utils/markdown-parser.js'
|
|
58
58
|
|
|
59
|
+
// trunk-ignore(eslint/@typescript-eslint/no-explicit-any)
|
|
60
|
+
type AnySnippet = (..._args: any[]) => any
|
|
61
|
+
|
|
59
62
|
interface Props<T extends Renderers = Renderers> {
|
|
60
63
|
type?: string
|
|
61
64
|
tokens?: Token[] | TokensList
|
|
@@ -63,8 +66,8 @@
|
|
|
63
66
|
rows?: Tokens.TableCell[][]
|
|
64
67
|
ordered?: boolean
|
|
65
68
|
renderers: T
|
|
66
|
-
snippetOverrides?: Record<string,
|
|
67
|
-
htmlSnippetOverrides?: Record<string,
|
|
69
|
+
snippetOverrides?: Record<string, AnySnippet>
|
|
70
|
+
htmlSnippetOverrides?: Record<string, AnySnippet>
|
|
68
71
|
}
|
|
69
72
|
|
|
70
73
|
const {
|
|
@@ -121,14 +124,14 @@
|
|
|
121
124
|
{#if cellSnippet}
|
|
122
125
|
{@render cellSnippet({
|
|
123
126
|
header: true,
|
|
124
|
-
align: (rest.align as string[])[i],
|
|
127
|
+
align: (rest.align as string[] | undefined)?.[i] ?? null,
|
|
125
128
|
...cellRest,
|
|
126
129
|
children: headerCellContent
|
|
127
130
|
})}
|
|
128
131
|
{:else}
|
|
129
132
|
<renderers.tablecell
|
|
130
133
|
header={true}
|
|
131
|
-
align={(rest.align as string[])[i]}
|
|
134
|
+
align={(rest.align as string[] | undefined)?.[i] ?? null}
|
|
132
135
|
{...cellRest}
|
|
133
136
|
>
|
|
134
137
|
{@render headerCellContent()}
|
|
@@ -172,7 +175,8 @@
|
|
|
172
175
|
{#if cellSnippet}
|
|
173
176
|
{@render cellSnippet({
|
|
174
177
|
header: false,
|
|
175
|
-
align:
|
|
178
|
+
align:
|
|
179
|
+
(rest.align as string[] | undefined)?.[j] ?? null,
|
|
176
180
|
...cellRest,
|
|
177
181
|
children: bodyCellContent
|
|
178
182
|
})}
|
|
@@ -180,7 +184,8 @@
|
|
|
180
184
|
<renderers.tablecell
|
|
181
185
|
{...cellRest}
|
|
182
186
|
header={false}
|
|
183
|
-
align={(rest.align as string[])[j]
|
|
187
|
+
align={(rest.align as string[] | undefined)?.[j] ??
|
|
188
|
+
null}
|
|
184
189
|
>
|
|
185
190
|
{@render bodyCellContent()}
|
|
186
191
|
</renderers.tablecell>
|
package/dist/Parser.svelte.d.ts
CHANGED
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
*/
|
|
47
47
|
import Parser from './Parser.svelte';
|
|
48
48
|
import type { Renderers, Token, TokensList, Tokens } from './utils/markdown-parser.js';
|
|
49
|
+
type AnySnippet = (..._args: any[]) => any;
|
|
49
50
|
interface Props<T extends Renderers = Renderers> {
|
|
50
51
|
type?: string;
|
|
51
52
|
tokens?: Token[] | TokensList;
|
|
@@ -53,8 +54,8 @@ interface Props<T extends Renderers = Renderers> {
|
|
|
53
54
|
rows?: Tokens.TableCell[][];
|
|
54
55
|
ordered?: boolean;
|
|
55
56
|
renderers: T;
|
|
56
|
-
snippetOverrides?: Record<string,
|
|
57
|
-
htmlSnippetOverrides?: Record<string,
|
|
57
|
+
snippetOverrides?: Record<string, AnySnippet>;
|
|
58
|
+
htmlSnippetOverrides?: Record<string, AnySnippet>;
|
|
58
59
|
}
|
|
59
60
|
type $$ComponentProps = Props & {
|
|
60
61
|
[key: string]: unknown;
|
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
|
|
52
52
|
import Parser from './Parser.svelte'
|
|
53
53
|
import { type SvelteMarkdownProps } from './types.js'
|
|
54
|
+
import { IncrementalParser } from './utils/incremental-parser.js'
|
|
54
55
|
import {
|
|
55
56
|
defaultOptions,
|
|
56
57
|
defaultRenderers,
|
|
@@ -62,8 +63,12 @@
|
|
|
62
63
|
import { rendererKeysInternal } from './utils/rendererKeys.js'
|
|
63
64
|
import { Marked } from 'marked'
|
|
64
65
|
|
|
66
|
+
// trunk-ignore(eslint/@typescript-eslint/no-explicit-any)
|
|
67
|
+
type AnySnippet = (..._args: any[]) => any
|
|
68
|
+
|
|
65
69
|
const {
|
|
66
70
|
source = [],
|
|
71
|
+
streaming = false,
|
|
67
72
|
renderers = {},
|
|
68
73
|
options = {},
|
|
69
74
|
isInline = false,
|
|
@@ -90,9 +95,59 @@
|
|
|
90
95
|
// Detect if any extension requires async processing
|
|
91
96
|
const hasAsyncExtension = $derived(extensions.some((ext) => ext.async === true))
|
|
92
97
|
|
|
93
|
-
//
|
|
98
|
+
// Streaming mode: full re-parse + smart in-place diff
|
|
99
|
+
let incrementalParser: IncrementalParser | undefined
|
|
100
|
+
let lastOptionsKey = ''
|
|
101
|
+
let streamTokens = $state<Token[]>([])
|
|
102
|
+
|
|
103
|
+
$effect(() => {
|
|
104
|
+
if (!streaming || hasAsyncExtension) {
|
|
105
|
+
if (incrementalParser) {
|
|
106
|
+
incrementalParser = undefined
|
|
107
|
+
lastOptionsKey = ''
|
|
108
|
+
}
|
|
109
|
+
if (streaming && hasAsyncExtension) {
|
|
110
|
+
console.warn(
|
|
111
|
+
'[svelte-markdown] streaming prop is ignored when async extensions are used. ' +
|
|
112
|
+
'Remove async extensions or set streaming={false} to silence this warning.'
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
return
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Read combinedOptions unconditionally so Svelte tracks it as a dependency
|
|
119
|
+
const currentOptions = combinedOptions
|
|
120
|
+
const optionsKey = JSON.stringify(currentOptions)
|
|
121
|
+
|
|
122
|
+
if (Array.isArray(source)) {
|
|
123
|
+
streamTokens = source as Token[]
|
|
124
|
+
return
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (source === '') {
|
|
128
|
+
if (incrementalParser) incrementalParser.reset()
|
|
129
|
+
streamTokens.length = 0
|
|
130
|
+
return
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Recreate parser only when options actually change
|
|
134
|
+
if (!incrementalParser || lastOptionsKey !== optionsKey) {
|
|
135
|
+
incrementalParser = new IncrementalParser(currentOptions)
|
|
136
|
+
lastOptionsKey = optionsKey
|
|
137
|
+
}
|
|
138
|
+
const { tokens: newTokens, divergeAt } = incrementalParser.update(source as string)
|
|
139
|
+
|
|
140
|
+
// In-place update: only touch changed/appended indices
|
|
141
|
+
for (let i = divergeAt; i < newTokens.length; i++) {
|
|
142
|
+
streamTokens[i] = newTokens[i]
|
|
143
|
+
}
|
|
144
|
+
streamTokens.length = newTokens.length
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
// Synchronous token derivation (default fast path — non-streaming)
|
|
94
148
|
const syncTokens = $derived.by(() => {
|
|
95
149
|
if (hasAsyncExtension) return undefined
|
|
150
|
+
if (streaming) return undefined
|
|
96
151
|
|
|
97
152
|
// Pre-parsed tokens - skip caching and parsing
|
|
98
153
|
if (Array.isArray(source)) {
|
|
@@ -104,7 +159,7 @@
|
|
|
104
159
|
return []
|
|
105
160
|
}
|
|
106
161
|
|
|
107
|
-
//
|
|
162
|
+
// Standard mode - full parse with caching
|
|
108
163
|
return parseAndCacheTokens(source as string, combinedOptions, isInline)
|
|
109
164
|
}) satisfies Token[] | TokensList | undefined
|
|
110
165
|
|
|
@@ -146,8 +201,8 @@
|
|
|
146
201
|
})
|
|
147
202
|
})
|
|
148
203
|
|
|
149
|
-
// Unified tokens:
|
|
150
|
-
const tokens = $derived(hasAsyncExtension ? asyncTokens : syncTokens)
|
|
204
|
+
// Unified tokens: streaming > sync > async
|
|
205
|
+
const tokens = $derived(streaming ? streamTokens : hasAsyncExtension ? asyncTokens : syncTokens)
|
|
151
206
|
|
|
152
207
|
$effect(() => {
|
|
153
208
|
if (!tokens) return
|
|
@@ -174,7 +229,7 @@
|
|
|
174
229
|
allRendererKeys
|
|
175
230
|
.filter((key) => key in rest && rest[key] != null)
|
|
176
231
|
.map((key) => [key, rest[key]])
|
|
177
|
-
)
|
|
232
|
+
) as Record<string, AnySnippet>
|
|
178
233
|
)
|
|
179
234
|
|
|
180
235
|
// Collect HTML snippet overrides (keys matching html_<tag>)
|
|
@@ -183,7 +238,7 @@
|
|
|
183
238
|
Object.entries(rest)
|
|
184
239
|
.filter(([key, val]) => key.startsWith('html_') && val != null)
|
|
185
240
|
.map(([key, val]) => [key.slice(5), val])
|
|
186
|
-
)
|
|
241
|
+
) as Record<string, AnySnippet>
|
|
187
242
|
)
|
|
188
243
|
|
|
189
244
|
// Passthrough: everything that isn't a known snippet override
|
|
@@ -199,7 +254,7 @@
|
|
|
199
254
|
{tokens}
|
|
200
255
|
{...passThroughProps}
|
|
201
256
|
options={combinedOptions}
|
|
202
|
-
slug={(val: string): string =>
|
|
257
|
+
slug={(val: string): string => slugger.slug(val)}
|
|
203
258
|
renderers={combinedRenderers}
|
|
204
259
|
{snippetOverrides}
|
|
205
260
|
{htmlSnippetOverrides}
|
package/dist/index.d.ts
CHANGED
|
@@ -58,6 +58,7 @@ export { htmlRendererKeysInternal as htmlRendererKeys, rendererKeysInternal as r
|
|
|
58
58
|
* - `tokenCache` — shared singleton `TokenCache` instance
|
|
59
59
|
*/
|
|
60
60
|
export { MemoryCache } from './utils/cache.js';
|
|
61
|
+
export { IncrementalParser, type IncrementalUpdateResult } from './utils/incremental-parser.js';
|
|
61
62
|
export { TokenCache, tokenCache } from './utils/token-cache.js';
|
|
62
63
|
/** Re-exported `MarkedExtension` type for the `extensions` prop. */
|
|
63
64
|
export type { MarkedExtension } from 'marked';
|
package/dist/index.js
CHANGED
|
@@ -57,4 +57,5 @@ export { htmlRendererKeysInternal as htmlRendererKeys, rendererKeysInternal as r
|
|
|
57
57
|
* - `tokenCache` — shared singleton `TokenCache` instance
|
|
58
58
|
*/
|
|
59
59
|
export { MemoryCache } from './utils/cache.js';
|
|
60
|
+
export { IncrementalParser } from './utils/incremental-parser.js';
|
|
60
61
|
export { TokenCache, tokenCache } from './utils/token-cache.js';
|
|
@@ -4,7 +4,7 @@ Renders a table cell as either `<th>` (header) or `<td>` (data), with optional
|
|
|
4
4
|
text alignment applied as an inline `text-align` style.
|
|
5
5
|
|
|
6
6
|
@prop {boolean} header - When `true`, renders a `<th>`; otherwise renders a `<td>`.
|
|
7
|
-
@prop {'left'|'center'|'right'|
|
|
7
|
+
@prop {'left'|'center'|'right'|null} align - Column alignment.
|
|
8
8
|
@prop {Snippet} [children] - Cell content.
|
|
9
9
|
-->
|
|
10
10
|
<script lang="ts">
|
|
@@ -12,7 +12,7 @@ text alignment applied as an inline `text-align` style.
|
|
|
12
12
|
|
|
13
13
|
interface Props {
|
|
14
14
|
header: boolean
|
|
15
|
-
align: 'left' | 'center' | 'right' |
|
|
15
|
+
align: 'left' | 'center' | 'right' | null
|
|
16
16
|
children?: Snippet
|
|
17
17
|
}
|
|
18
18
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
2
|
interface Props {
|
|
3
3
|
header: boolean;
|
|
4
|
-
align: 'left' | 'center' | 'right' |
|
|
4
|
+
align: 'left' | 'center' | 'right' | null;
|
|
5
5
|
children?: Snippet;
|
|
6
6
|
}
|
|
7
7
|
/**
|
|
@@ -9,7 +9,7 @@ interface Props {
|
|
|
9
9
|
* text alignment applied as an inline `text-align` style.
|
|
10
10
|
*
|
|
11
11
|
* @prop {boolean} header - When `true`, renders a `<th>`; otherwise renders a `<td>`.
|
|
12
|
-
* @prop {'left'|'center'|'right'|
|
|
12
|
+
* @prop {'left'|'center'|'right'|null} align - Column alignment.
|
|
13
13
|
* @prop {Snippet} [children] - Cell content.
|
|
14
14
|
*/
|
|
15
15
|
declare const TableCell: import("svelte").Component<Props, {}, "">;
|
package/dist/types.d.ts
CHANGED
|
@@ -75,7 +75,7 @@ export interface TableRowSnippetProps {
|
|
|
75
75
|
}
|
|
76
76
|
export interface TableCellSnippetProps {
|
|
77
77
|
header: boolean;
|
|
78
|
-
align: 'left' | 'center' | 'right' |
|
|
78
|
+
align: 'left' | 'center' | 'right' | null;
|
|
79
79
|
children?: Snippet;
|
|
80
80
|
}
|
|
81
81
|
export interface EmSnippetProps {
|
|
@@ -124,8 +124,16 @@ export type SnippetOverrides = {
|
|
|
124
124
|
rawtext?: Snippet<[RawTextSnippetProps]>;
|
|
125
125
|
escape?: Snippet<[EscapeSnippetProps]>;
|
|
126
126
|
};
|
|
127
|
+
/**
|
|
128
|
+
* Props passed to HTML snippet overrides.
|
|
129
|
+
*
|
|
130
|
+
* **Security note:** `attributes` are spread directly onto the rendered HTML element.
|
|
131
|
+
* This includes any attribute from the source markdown, such as `onclick` or `onerror`.
|
|
132
|
+
* If rendering untrusted markdown, use `allowHtmlOnly`/`excludeHtmlOnly` to restrict
|
|
133
|
+
* allowed tags, or integrate your own sanitizer to strip dangerous attributes.
|
|
134
|
+
*/
|
|
127
135
|
export interface HtmlSnippetProps {
|
|
128
|
-
attributes?: Record<string,
|
|
136
|
+
attributes?: Record<string, string | number | boolean | undefined>;
|
|
129
137
|
children?: Snippet;
|
|
130
138
|
}
|
|
131
139
|
export type HtmlSnippetOverrides = {
|
|
@@ -182,6 +190,20 @@ export type SvelteMarkdownProps<T extends Renderers = Renderers> = {
|
|
|
182
190
|
* @defaultValue `false`
|
|
183
191
|
*/
|
|
184
192
|
isInline?: boolean;
|
|
193
|
+
/**
|
|
194
|
+
* Enables optimized rendering for LLM streaming scenarios.
|
|
195
|
+
*
|
|
196
|
+
* When `true`, the component performs a full re-parse on each source
|
|
197
|
+
* update but diffs the resulting tokens against the previous parse.
|
|
198
|
+
* Only changed or appended tokens trigger DOM updates, keeping render
|
|
199
|
+
* cost proportional to the change rather than the full document size.
|
|
200
|
+
*
|
|
201
|
+
* Use this when appending tokens to `source` in a streaming fashion
|
|
202
|
+
* (e.g., ChatGPT/Claude SSE responses).
|
|
203
|
+
*
|
|
204
|
+
* @defaultValue `false`
|
|
205
|
+
*/
|
|
206
|
+
streaming?: boolean;
|
|
185
207
|
/**
|
|
186
208
|
* Callback invoked after the source has been parsed into tokens.
|
|
187
209
|
*
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Incremental Markdown Parser for Streaming
|
|
3
|
+
*
|
|
4
|
+
* Optimizes streaming scenarios (LLM token-by-token updates) by performing
|
|
5
|
+
* a full re-parse but diffing the result against the previous token array.
|
|
6
|
+
* Only changed/appended tokens are returned as updates, allowing Svelte to
|
|
7
|
+
* skip re-rendering unchanged components.
|
|
8
|
+
*
|
|
9
|
+
* @module incremental-parser
|
|
10
|
+
*/
|
|
11
|
+
import type { SvelteMarkdownOptions } from '../types.js';
|
|
12
|
+
import type { Token } from './markdown-parser.js';
|
|
13
|
+
/**
|
|
14
|
+
* Result of an incremental parse update.
|
|
15
|
+
*/
|
|
16
|
+
export interface IncrementalUpdateResult {
|
|
17
|
+
/** The full new token array */
|
|
18
|
+
tokens: Token[];
|
|
19
|
+
/** Index of the first token that differs from the previous parse */
|
|
20
|
+
divergeAt: number;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Streaming-optimized parser that performs full re-parses but diffs results
|
|
24
|
+
* against the previous token array to minimize DOM updates.
|
|
25
|
+
*
|
|
26
|
+
* For append-only streaming (typical LLM use case), most tokens are identical
|
|
27
|
+
* between updates. By comparing `raw` strings, we identify which tokens changed
|
|
28
|
+
* so Svelte can skip re-rendering unchanged components.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```typescript
|
|
32
|
+
* const parser = new IncrementalParser({ gfm: true })
|
|
33
|
+
*
|
|
34
|
+
* // First update — all tokens are "new"
|
|
35
|
+
* const r1 = parser.update('# Hello')
|
|
36
|
+
* // r1.divergeAt === 0
|
|
37
|
+
*
|
|
38
|
+
* // Second update — heading unchanged, paragraph appended
|
|
39
|
+
* const r2 = parser.update('# Hello\n\nWorld')
|
|
40
|
+
* // r2.divergeAt === 1 (heading at index 0 unchanged)
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
export declare class IncrementalParser {
|
|
44
|
+
/** Previous parse result for diffing */
|
|
45
|
+
private prevTokens;
|
|
46
|
+
/** Parser options passed to the Marked lexer */
|
|
47
|
+
private options;
|
|
48
|
+
/**
|
|
49
|
+
* Creates a new incremental parser instance.
|
|
50
|
+
*
|
|
51
|
+
* @param options - Svelte markdown parser options forwarded to Marked's Lexer
|
|
52
|
+
*/
|
|
53
|
+
constructor(options: SvelteMarkdownOptions);
|
|
54
|
+
/**
|
|
55
|
+
* Parses the full source and diffs against the previous result.
|
|
56
|
+
*
|
|
57
|
+
* @param source - The full accumulated markdown source string
|
|
58
|
+
* @returns The new tokens and the index where they diverge from the previous parse
|
|
59
|
+
*/
|
|
60
|
+
update: (source: string) => IncrementalUpdateResult;
|
|
61
|
+
/**
|
|
62
|
+
* Resets the parser state. Call this when starting a new stream.
|
|
63
|
+
*/
|
|
64
|
+
reset: () => void;
|
|
65
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Incremental Markdown Parser for Streaming
|
|
3
|
+
*
|
|
4
|
+
* Optimizes streaming scenarios (LLM token-by-token updates) by performing
|
|
5
|
+
* a full re-parse but diffing the result against the previous token array.
|
|
6
|
+
* Only changed/appended tokens are returned as updates, allowing Svelte to
|
|
7
|
+
* skip re-rendering unchanged components.
|
|
8
|
+
*
|
|
9
|
+
* @module incremental-parser
|
|
10
|
+
*/
|
|
11
|
+
import { lexAndClean } from './parse-and-cache.js';
|
|
12
|
+
/**
|
|
13
|
+
* Streaming-optimized parser that performs full re-parses but diffs results
|
|
14
|
+
* against the previous token array to minimize DOM updates.
|
|
15
|
+
*
|
|
16
|
+
* For append-only streaming (typical LLM use case), most tokens are identical
|
|
17
|
+
* between updates. By comparing `raw` strings, we identify which tokens changed
|
|
18
|
+
* so Svelte can skip re-rendering unchanged components.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```typescript
|
|
22
|
+
* const parser = new IncrementalParser({ gfm: true })
|
|
23
|
+
*
|
|
24
|
+
* // First update — all tokens are "new"
|
|
25
|
+
* const r1 = parser.update('# Hello')
|
|
26
|
+
* // r1.divergeAt === 0
|
|
27
|
+
*
|
|
28
|
+
* // Second update — heading unchanged, paragraph appended
|
|
29
|
+
* const r2 = parser.update('# Hello\n\nWorld')
|
|
30
|
+
* // r2.divergeAt === 1 (heading at index 0 unchanged)
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export class IncrementalParser {
|
|
34
|
+
/** Previous parse result for diffing */
|
|
35
|
+
prevTokens = [];
|
|
36
|
+
/** Parser options passed to the Marked lexer */
|
|
37
|
+
options;
|
|
38
|
+
/**
|
|
39
|
+
* Creates a new incremental parser instance.
|
|
40
|
+
*
|
|
41
|
+
* @param options - Svelte markdown parser options forwarded to Marked's Lexer
|
|
42
|
+
*/
|
|
43
|
+
constructor(options) {
|
|
44
|
+
this.options = options;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Parses the full source and diffs against the previous result.
|
|
48
|
+
*
|
|
49
|
+
* @param source - The full accumulated markdown source string
|
|
50
|
+
* @returns The new tokens and the index where they diverge from the previous parse
|
|
51
|
+
*/
|
|
52
|
+
update = (source) => {
|
|
53
|
+
const newTokens = lexAndClean(source, this.options, false);
|
|
54
|
+
// Apply walkTokens if configured
|
|
55
|
+
if (typeof this.options.walkTokens === 'function') {
|
|
56
|
+
newTokens.forEach(this.options.walkTokens);
|
|
57
|
+
}
|
|
58
|
+
// Find first divergence point by comparing raw strings
|
|
59
|
+
let divergeAt = 0;
|
|
60
|
+
const minLen = Math.min(this.prevTokens.length, newTokens.length);
|
|
61
|
+
while (divergeAt < minLen) {
|
|
62
|
+
if (this.prevTokens[divergeAt].raw !== newTokens[divergeAt].raw)
|
|
63
|
+
break;
|
|
64
|
+
divergeAt++;
|
|
65
|
+
}
|
|
66
|
+
this.prevTokens = newTokens;
|
|
67
|
+
return { tokens: newTokens, divergeAt };
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* Resets the parser state. Call this when starting a new stream.
|
|
71
|
+
*/
|
|
72
|
+
reset = () => {
|
|
73
|
+
this.prevTokens = [];
|
|
74
|
+
};
|
|
75
|
+
}
|
|
@@ -8,6 +8,24 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import type { SvelteMarkdownOptions } from '../types.js';
|
|
10
10
|
import type { Token, TokensList } from './markdown-parser.js';
|
|
11
|
+
/**
|
|
12
|
+
* Lexes markdown source and cleans the resulting tokens. Shared by sync and async paths.
|
|
13
|
+
*
|
|
14
|
+
* @param source - Raw markdown string to lex
|
|
15
|
+
* @param options - Parser options forwarded to the Marked lexer
|
|
16
|
+
* @param isInline - When true, uses inline tokenization (no block elements)
|
|
17
|
+
* @returns Cleaned token array with HTML tokens properly nested
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```typescript
|
|
21
|
+
* import { lexAndClean } from './parse-and-cache.js'
|
|
22
|
+
*
|
|
23
|
+
* const tokens = lexAndClean('# Hello **world**', { gfm: true }, false)
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* @internal
|
|
27
|
+
*/
|
|
28
|
+
export declare const lexAndClean: (source: string, options: SvelteMarkdownOptions, isInline: boolean) => Token[];
|
|
11
29
|
/**
|
|
12
30
|
* Parses markdown source with caching (synchronous path).
|
|
13
31
|
* Checks cache first, parses on miss, stores result, and returns tokens.
|
|
@@ -28,7 +46,7 @@ import type { Token, TokensList } from './markdown-parser.js';
|
|
|
28
46
|
* const cachedTokens = parseAndCacheTokens('# Hello World', { gfm: true }, false)
|
|
29
47
|
* ```
|
|
30
48
|
*/
|
|
31
|
-
export declare
|
|
49
|
+
export declare const parseAndCacheTokens: (source: string, options: SvelteMarkdownOptions, isInline: boolean) => Token[] | TokensList;
|
|
32
50
|
/**
|
|
33
51
|
* Parses markdown source with caching (async path).
|
|
34
52
|
* Uses Marked's recursive walkTokens with Promise.all to properly
|
|
@@ -46,4 +64,4 @@ export declare function parseAndCacheTokens(source: string, options: SvelteMarkd
|
|
|
46
64
|
* const tokens = await parseAndCacheTokensAsync('# Hello', opts, false)
|
|
47
65
|
* ```
|
|
48
66
|
*/
|
|
49
|
-
export declare
|
|
67
|
+
export declare const parseAndCacheTokensAsync: (source: string, options: SvelteMarkdownOptions, isInline: boolean) => Promise<Token[] | TokensList>;
|
|
@@ -10,13 +10,27 @@ import { tokenCache } from './token-cache.js';
|
|
|
10
10
|
import { shrinkHtmlTokens } from './token-cleanup.js';
|
|
11
11
|
import { Lexer, Marked } from 'marked';
|
|
12
12
|
/**
|
|
13
|
-
*
|
|
13
|
+
* Lexes markdown source and cleans the resulting tokens. Shared by sync and async paths.
|
|
14
|
+
*
|
|
15
|
+
* @param source - Raw markdown string to lex
|
|
16
|
+
* @param options - Parser options forwarded to the Marked lexer
|
|
17
|
+
* @param isInline - When true, uses inline tokenization (no block elements)
|
|
18
|
+
* @returns Cleaned token array with HTML tokens properly nested
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```typescript
|
|
22
|
+
* import { lexAndClean } from './parse-and-cache.js'
|
|
23
|
+
*
|
|
24
|
+
* const tokens = lexAndClean('# Hello **world**', { gfm: true }, false)
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* @internal
|
|
14
28
|
*/
|
|
15
|
-
|
|
29
|
+
export const lexAndClean = (source, options, isInline) => {
|
|
16
30
|
const lexer = new Lexer(options);
|
|
17
31
|
const parsedTokens = isInline ? lexer.inlineTokens(source) : lexer.lex(source);
|
|
18
32
|
return shrinkHtmlTokens(parsedTokens);
|
|
19
|
-
}
|
|
33
|
+
};
|
|
20
34
|
/**
|
|
21
35
|
* Parses markdown source with caching (synchronous path).
|
|
22
36
|
* Checks cache first, parses on miss, stores result, and returns tokens.
|
|
@@ -37,7 +51,7 @@ function lexAndClean(source, options, isInline) {
|
|
|
37
51
|
* const cachedTokens = parseAndCacheTokens('# Hello World', { gfm: true }, false)
|
|
38
52
|
* ```
|
|
39
53
|
*/
|
|
40
|
-
export
|
|
54
|
+
export const parseAndCacheTokens = (source, options, isInline) => {
|
|
41
55
|
// Check cache first - avoids expensive parsing
|
|
42
56
|
const cached = tokenCache.getTokens(source, options);
|
|
43
57
|
if (cached) {
|
|
@@ -51,7 +65,7 @@ export function parseAndCacheTokens(source, options, isInline) {
|
|
|
51
65
|
// Cache the cleaned tokens for next time
|
|
52
66
|
tokenCache.setTokens(source, options, cleanedTokens);
|
|
53
67
|
return cleanedTokens;
|
|
54
|
-
}
|
|
68
|
+
};
|
|
55
69
|
/**
|
|
56
70
|
* Parses markdown source with caching (async path).
|
|
57
71
|
* Uses Marked's recursive walkTokens with Promise.all to properly
|
|
@@ -69,7 +83,7 @@ export function parseAndCacheTokens(source, options, isInline) {
|
|
|
69
83
|
* const tokens = await parseAndCacheTokensAsync('# Hello', opts, false)
|
|
70
84
|
* ```
|
|
71
85
|
*/
|
|
72
|
-
export async
|
|
86
|
+
export const parseAndCacheTokensAsync = async (source, options, isInline) => {
|
|
73
87
|
// Check cache first - avoids expensive parsing
|
|
74
88
|
const cached = tokenCache.getTokens(source, options);
|
|
75
89
|
if (cached) {
|
|
@@ -89,4 +103,4 @@ export async function parseAndCacheTokensAsync(source, options, isInline) {
|
|
|
89
103
|
// Cache the cleaned tokens for next time
|
|
90
104
|
tokenCache.setTokens(source, options, cleanedTokens);
|
|
91
105
|
return cleanedTokens;
|
|
92
|
-
}
|
|
106
|
+
};
|
|
@@ -35,7 +35,7 @@ import type { Token, TokensList } from './markdown-parser.js';
|
|
|
35
35
|
* console.log(hash1 !== hash2) // true - different content = different hash
|
|
36
36
|
* ```
|
|
37
37
|
*/
|
|
38
|
-
declare
|
|
38
|
+
declare const hashString: (str: string) => string;
|
|
39
39
|
/**
|
|
40
40
|
* Specialized cache for markdown token storage.
|
|
41
41
|
* Extends MemoryCache with markdown-specific convenience methods.
|
|
@@ -33,7 +33,7 @@ import { MemoryCache } from './cache.js';
|
|
|
33
33
|
* console.log(hash1 !== hash2) // true - different content = different hash
|
|
34
34
|
* ```
|
|
35
35
|
*/
|
|
36
|
-
|
|
36
|
+
const hashString = (str) => {
|
|
37
37
|
let hash = 2166136261; // FNV offset basis (32-bit)
|
|
38
38
|
for (let i = 0; i < str.length; i++) {
|
|
39
39
|
hash ^= str.charCodeAt(i);
|
|
@@ -42,7 +42,7 @@ function hashString(str) {
|
|
|
42
42
|
}
|
|
43
43
|
// Convert to unsigned 32-bit integer and base36 string
|
|
44
44
|
return (hash >>> 0).toString(36);
|
|
45
|
-
}
|
|
45
|
+
};
|
|
46
46
|
/**
|
|
47
47
|
* Generates a cache key from markdown source and parser options.
|
|
48
48
|
* Combines hashes of both source content and options to ensure
|
|
@@ -71,7 +71,7 @@ function hashString(str) {
|
|
|
71
71
|
// reactivity system creates new objects on prop changes ($derived), so
|
|
72
72
|
// this is safe for all internal usage paths.
|
|
73
73
|
const optionsHashCache = new WeakMap();
|
|
74
|
-
|
|
74
|
+
const getCacheKey = (source, options) => {
|
|
75
75
|
const sourceHash = hashString(source);
|
|
76
76
|
let optionsHash = optionsHashCache.get(options);
|
|
77
77
|
if (!optionsHash) {
|
|
@@ -90,7 +90,7 @@ function getCacheKey(source, options) {
|
|
|
90
90
|
optionsHashCache.set(options, optionsHash);
|
|
91
91
|
}
|
|
92
92
|
return `${sourceHash}:${optionsHash}`;
|
|
93
|
-
}
|
|
93
|
+
};
|
|
94
94
|
/**
|
|
95
95
|
* Specialized cache for markdown token storage.
|
|
96
96
|
* Extends MemoryCache with markdown-specific convenience methods.
|
|
@@ -32,51 +32,6 @@ export declare const isHtmlOpenTag: (raw: string) => {
|
|
|
32
32
|
* @internal
|
|
33
33
|
*/
|
|
34
34
|
export declare const extractAttributes: (raw: string) => Record<string, string>;
|
|
35
|
-
/**
|
|
36
|
-
* Converts an HTML string into a sequence of tokens using htmlparser2.
|
|
37
|
-
* Handles complex nested structures while maintaining proper order and relationships.
|
|
38
|
-
*
|
|
39
|
-
* Key features:
|
|
40
|
-
* - Preserves original HTML structure without automatic tag closing
|
|
41
|
-
* - Handles self-closing tags with proper XML syntax (e.g., <br/> instead of <br>)
|
|
42
|
-
* - Gracefully handles malformed HTML by preserving the original structure
|
|
43
|
-
* - Maintains attribute information in opening tags
|
|
44
|
-
* - Processes text content between tags
|
|
45
|
-
*
|
|
46
|
-
* @param {string} html - HTML string to be parsed
|
|
47
|
-
* @returns {Token[]} Array of tokens representing the HTML structure
|
|
48
|
-
*
|
|
49
|
-
* @example
|
|
50
|
-
* // Well-formed HTML
|
|
51
|
-
* parseHtmlBlock('<div>Hello <span>world</span></div>')
|
|
52
|
-
* // Returns [
|
|
53
|
-
* // { type: 'html', raw: '<div>', ... },
|
|
54
|
-
* // { type: 'text', raw: 'Hello ', ... },
|
|
55
|
-
* // { type: 'html', raw: '<span>', ... },
|
|
56
|
-
* // { type: 'text', raw: 'world', ... },
|
|
57
|
-
* // { type: 'html', raw: '</span>', ... },
|
|
58
|
-
* // { type: 'html', raw: '</div>', ... }
|
|
59
|
-
* // ]
|
|
60
|
-
*
|
|
61
|
-
* // Self-closing tags
|
|
62
|
-
* parseHtmlBlock('<div>Before<br/>After</div>')
|
|
63
|
-
* // Returns [
|
|
64
|
-
* // { type: 'html', raw: '<div>', ... },
|
|
65
|
-
* // { type: 'text', raw: 'Before', ... },
|
|
66
|
-
* // { type: 'html', raw: '<br/>', ... },
|
|
67
|
-
* // { type: 'text', raw: 'After', ... },
|
|
68
|
-
* // { type: 'html', raw: '</div>', ... }
|
|
69
|
-
* // ]
|
|
70
|
-
*
|
|
71
|
-
* // Malformed HTML
|
|
72
|
-
* parseHtmlBlock('<div>Unclosed')
|
|
73
|
-
* // Returns [
|
|
74
|
-
* // { type: 'html', raw: '<div>', ... },
|
|
75
|
-
* // { type: 'text', raw: 'Unclosed', ... }
|
|
76
|
-
* // ]
|
|
77
|
-
*
|
|
78
|
-
* @internal
|
|
79
|
-
*/
|
|
80
35
|
export declare const parseHtmlBlock: (html: string) => Token[];
|
|
81
36
|
export declare const containsMultipleTags: (html: string) => boolean;
|
|
82
37
|
/**
|
|
@@ -141,6 +141,22 @@ export const extractAttributes = (raw) => {
|
|
|
141
141
|
*
|
|
142
142
|
* @internal
|
|
143
143
|
*/
|
|
144
|
+
/**
|
|
145
|
+
* Serializes an HTML attribute map into a string for tag construction.
|
|
146
|
+
* Escapes double quotes in values to prevent attribute injection.
|
|
147
|
+
*
|
|
148
|
+
* @param {Record<string, string>} attributes - Map of attribute names to values
|
|
149
|
+
* @returns {string} Serialized attributes string with leading spaces
|
|
150
|
+
*
|
|
151
|
+
* @example
|
|
152
|
+
* serializeAttributes({ class: 'foo', id: 'bar' })
|
|
153
|
+
* // Returns ' class="foo" id="bar"'
|
|
154
|
+
*
|
|
155
|
+
* @internal
|
|
156
|
+
*/
|
|
157
|
+
const serializeAttributes = (attributes) => Object.entries(attributes)
|
|
158
|
+
.map(([key, value]) => ` ${key}="${value.replace(/"/g, '"')}"`)
|
|
159
|
+
.join('');
|
|
144
160
|
export const parseHtmlBlock = (html) => {
|
|
145
161
|
const tokens = [];
|
|
146
162
|
let currentText = '';
|
|
@@ -158,9 +174,7 @@ export const parseHtmlBlock = (html) => {
|
|
|
158
174
|
if (SELF_CLOSING_TAGS.test(name)) {
|
|
159
175
|
tokens.push({
|
|
160
176
|
type: 'html',
|
|
161
|
-
raw: `<${name}${
|
|
162
|
-
.map(([key, value]) => ` ${key}="${value}"`)
|
|
163
|
-
.join('')}/>`,
|
|
177
|
+
raw: `<${name}${serializeAttributes(attributes)}/>`,
|
|
164
178
|
tag: name,
|
|
165
179
|
attributes
|
|
166
180
|
});
|
|
@@ -169,9 +183,7 @@ export const parseHtmlBlock = (html) => {
|
|
|
169
183
|
openTags.push(name);
|
|
170
184
|
tokens.push({
|
|
171
185
|
type: 'html',
|
|
172
|
-
raw: `<${name}${
|
|
173
|
-
.map(([key, value]) => ` ${key}="${value}"`)
|
|
174
|
-
.join('')}>`,
|
|
186
|
+
raw: `<${name}${serializeAttributes(attributes)}>`,
|
|
175
187
|
tag: name,
|
|
176
188
|
attributes
|
|
177
189
|
});
|
|
@@ -203,8 +215,7 @@ export const parseHtmlBlock = (html) => {
|
|
|
203
215
|
}
|
|
204
216
|
}
|
|
205
217
|
}, {
|
|
206
|
-
xmlMode:
|
|
207
|
-
// Add this to prevent automatic tag closing
|
|
218
|
+
xmlMode: false,
|
|
208
219
|
recognizeSelfClosing: true
|
|
209
220
|
});
|
|
210
221
|
parser.write(html);
|
|
@@ -290,19 +301,16 @@ export const shrinkHtmlTokens = (tokens) => {
|
|
|
290
301
|
}
|
|
291
302
|
else if (token.type === 'table') {
|
|
292
303
|
// Process header cells
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
304
|
+
const tableToken = token;
|
|
305
|
+
if (tableToken.header) {
|
|
306
|
+
tableToken.header = tableToken.header.map((cell) => ({
|
|
296
307
|
...cell,
|
|
297
308
|
tokens: cell.tokens ? shrinkHtmlTokens(cell.tokens) : []
|
|
298
309
|
}));
|
|
299
310
|
}
|
|
300
311
|
// Process row cells
|
|
301
|
-
if (
|
|
302
|
-
|
|
303
|
-
token.rows = token.rows.map((row) =>
|
|
304
|
-
// @ts-expect-error: expected any
|
|
305
|
-
row.map((cell) => ({
|
|
312
|
+
if (tableToken.rows) {
|
|
313
|
+
tableToken.rows = tableToken.rows.map((row) => row.map((cell) => ({
|
|
306
314
|
...cell,
|
|
307
315
|
tokens: cell.tokens ? shrinkHtmlTokens(cell.tokens) : []
|
|
308
316
|
})));
|
|
@@ -394,9 +402,9 @@ export const processHtmlTokens = (tokens) => {
|
|
|
394
402
|
result.push(token);
|
|
395
403
|
}
|
|
396
404
|
}
|
|
397
|
-
// If we have unclosed tags, return
|
|
405
|
+
// If we have unclosed tags, return partial result (better than discarding all work)
|
|
398
406
|
if (stack.length > 0) {
|
|
399
|
-
return
|
|
407
|
+
return result;
|
|
400
408
|
}
|
|
401
409
|
return result;
|
|
402
410
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@humanspeak/svelte-markdown",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Fast, customizable markdown renderer for Svelte with built-in caching, TypeScript support, and Svelte 5 runes",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"svelte",
|
|
@@ -69,8 +69,8 @@
|
|
|
69
69
|
"dependencies": {
|
|
70
70
|
"@humanspeak/memory-cache": "^1.0.6",
|
|
71
71
|
"github-slugger": "^2.0.0",
|
|
72
|
-
"htmlparser2": "^
|
|
73
|
-
"marked": "^17.0.
|
|
72
|
+
"htmlparser2": "^12.0.0",
|
|
73
|
+
"marked": "^17.0.5"
|
|
74
74
|
},
|
|
75
75
|
"devDependencies": {
|
|
76
76
|
"@eslint/compat": "^2.0.3",
|
|
@@ -86,32 +86,32 @@
|
|
|
86
86
|
"@testing-library/user-event": "^14.6.1",
|
|
87
87
|
"@types/katex": "^0.16.8",
|
|
88
88
|
"@types/node": "^25.5.0",
|
|
89
|
-
"@typescript-eslint/eslint-plugin": "^8.57.
|
|
90
|
-
"@typescript-eslint/parser": "^8.57.
|
|
91
|
-
"@vitest/coverage-v8": "^4.1.
|
|
92
|
-
"eslint": "^10.0
|
|
89
|
+
"@typescript-eslint/eslint-plugin": "^8.57.2",
|
|
90
|
+
"@typescript-eslint/parser": "^8.57.2",
|
|
91
|
+
"@vitest/coverage-v8": "^4.1.1",
|
|
92
|
+
"eslint": "^10.1.0",
|
|
93
93
|
"eslint-config-prettier": "^10.1.8",
|
|
94
94
|
"eslint-plugin-import": "^2.32.0",
|
|
95
|
-
"eslint-plugin-svelte": "^3.
|
|
95
|
+
"eslint-plugin-svelte": "^3.16.0",
|
|
96
96
|
"eslint-plugin-unused-imports": "^4.4.1",
|
|
97
97
|
"globals": "^17.4.0",
|
|
98
98
|
"husky": "^9.1.7",
|
|
99
|
-
"jsdom": "^
|
|
100
|
-
"katex": "^0.16.
|
|
99
|
+
"jsdom": "^29.0.1",
|
|
100
|
+
"katex": "^0.16.41",
|
|
101
101
|
"marked-katex-extension": "^5.1.7",
|
|
102
102
|
"mermaid": "^11.13.0",
|
|
103
|
-
"mprocs": "^0.
|
|
103
|
+
"mprocs": "^0.9.2",
|
|
104
104
|
"prettier": "^3.8.1",
|
|
105
105
|
"prettier-plugin-organize-imports": "^4.3.0",
|
|
106
106
|
"prettier-plugin-svelte": "^3.5.1",
|
|
107
107
|
"prettier-plugin-tailwindcss": "^0.7.2",
|
|
108
108
|
"publint": "^0.3.18",
|
|
109
|
-
"svelte": "^5.
|
|
109
|
+
"svelte": "^5.55.0",
|
|
110
110
|
"svelte-check": "^4.4.5",
|
|
111
|
-
"typescript": "^
|
|
112
|
-
"typescript-eslint": "^8.57.
|
|
113
|
-
"vite": "^8.0.
|
|
114
|
-
"vitest": "^4.1.
|
|
111
|
+
"typescript": "^6.0.2",
|
|
112
|
+
"typescript-eslint": "^8.57.2",
|
|
113
|
+
"vite": "^8.0.2",
|
|
114
|
+
"vitest": "^4.1.1"
|
|
115
115
|
},
|
|
116
116
|
"peerDependencies": {
|
|
117
117
|
"mermaid": ">=10.0.0",
|