@humanspeak/svelte-markdown 0.8.12 → 0.8.13
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 +135 -0
- package/dist/Parser.svelte +9 -6
- package/dist/SvelteMarkdown.svelte +14 -10
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -0
- package/dist/renderers/Image.svelte +91 -2
- package/dist/renderers/Image.svelte.d.ts +2 -0
- package/dist/utils/cache.d.ts +168 -0
- package/dist/utils/cache.js +263 -0
- package/dist/utils/parse-and-cache.d.ts +31 -0
- package/dist/utils/parse-and-cache.js +45 -0
- package/dist/utils/token-cache.d.ts +178 -0
- package/dist/utils/token-cache.js +238 -0
- package/dist/utils/token-cleanup.js +6 -1
- package/package.json +28 -20
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token Cache
|
|
3
|
+
*
|
|
4
|
+
* Efficient caching layer for parsed markdown tokens.
|
|
5
|
+
* Built on top of the existing MemoryCache infrastructure with
|
|
6
|
+
* specialized features for markdown parsing optimization.
|
|
7
|
+
*
|
|
8
|
+
* Key features:
|
|
9
|
+
* - Fast FNV-1a hashing for cache keys
|
|
10
|
+
* - LRU eviction (inherited from MemoryCache)
|
|
11
|
+
* - TTL support (inherited from MemoryCache)
|
|
12
|
+
* - Handles large markdown documents efficiently
|
|
13
|
+
*
|
|
14
|
+
* @module token-cache
|
|
15
|
+
*/
|
|
16
|
+
import { MemoryCache } from './cache.js';
|
|
17
|
+
/**
|
|
18
|
+
* Fast non-cryptographic hash function using FNV-1a algorithm.
|
|
19
|
+
* Optimized for speed over cryptographic security.
|
|
20
|
+
*
|
|
21
|
+
* FNV-1a (Fowler-Noll-Vo) provides:
|
|
22
|
+
* - Fast computation (single pass through string)
|
|
23
|
+
* - Good distribution (minimal collisions)
|
|
24
|
+
* - Small output size (base36 string)
|
|
25
|
+
*
|
|
26
|
+
* @param str - String to hash
|
|
27
|
+
* @returns Base36 hash string
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```typescript
|
|
31
|
+
* const hash1 = hashString('# Hello World')
|
|
32
|
+
* const hash2 = hashString('# Hello World!')
|
|
33
|
+
* console.log(hash1 !== hash2) // true - different content = different hash
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
function hashString(str) {
|
|
37
|
+
let hash = 2166136261; // FNV offset basis (32-bit)
|
|
38
|
+
for (let i = 0; i < str.length; i++) {
|
|
39
|
+
hash ^= str.charCodeAt(i);
|
|
40
|
+
// FNV prime multiply using bit shifts (faster than multiplication)
|
|
41
|
+
hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24);
|
|
42
|
+
}
|
|
43
|
+
// Convert to unsigned 32-bit integer and base36 string
|
|
44
|
+
return (hash >>> 0).toString(36);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Generates a cache key from markdown source and parser options.
|
|
48
|
+
* Combines hashes of both source content and options to ensure
|
|
49
|
+
* different parsing configurations are cached separately.
|
|
50
|
+
*
|
|
51
|
+
* Handles non-serializable options (extensions, tokenizer, hooks, etc.) by:
|
|
52
|
+
* - Serializing functions by name/toString for stable keys
|
|
53
|
+
* - Detecting and handling circular references
|
|
54
|
+
* - Including ALL options that affect parsing
|
|
55
|
+
*
|
|
56
|
+
* @param source - Raw markdown string
|
|
57
|
+
* @param options - Svelte markdown parser options
|
|
58
|
+
* @returns Composite cache key
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```typescript
|
|
62
|
+
* const key1 = getCacheKey('# Test', { gfm: true })
|
|
63
|
+
* const key2 = getCacheKey('# Test', { gfm: false })
|
|
64
|
+
* console.log(key1 !== key2) // true - different options = different key
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
function getCacheKey(source, options) {
|
|
68
|
+
const sourceHash = hashString(source);
|
|
69
|
+
// Safely serialize all options including functions and objects
|
|
70
|
+
const seen = new WeakSet();
|
|
71
|
+
const optionsHash = hashString(JSON.stringify(options, (_, value) => {
|
|
72
|
+
// Serialize functions by their name or source code
|
|
73
|
+
if (typeof value === 'function') {
|
|
74
|
+
return value.name || value.toString();
|
|
75
|
+
}
|
|
76
|
+
// Handle circular references
|
|
77
|
+
if (value && typeof value === 'object') {
|
|
78
|
+
if (seen.has(value))
|
|
79
|
+
return '[Circular]';
|
|
80
|
+
seen.add(value);
|
|
81
|
+
}
|
|
82
|
+
return value;
|
|
83
|
+
}));
|
|
84
|
+
return `${sourceHash}:${optionsHash}`;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Specialized cache for markdown token storage.
|
|
88
|
+
* Extends MemoryCache with markdown-specific convenience methods.
|
|
89
|
+
*
|
|
90
|
+
* Inherits from MemoryCache:
|
|
91
|
+
* - Automatic LRU eviction when maxSize is reached
|
|
92
|
+
* - TTL-based expiration for time-sensitive content
|
|
93
|
+
* - Advanced deletion (deleteByPrefix, deleteByMagicString)
|
|
94
|
+
*
|
|
95
|
+
* Performance characteristics:
|
|
96
|
+
* - Cache hit: <1ms (vs 50-200ms parsing)
|
|
97
|
+
* - Memory: ~5MB for 50 cached documents (default maxSize)
|
|
98
|
+
* - Hash computation: ~0.05ms for 10KB, ~0.5ms for 100KB
|
|
99
|
+
*
|
|
100
|
+
* @class TokenCache
|
|
101
|
+
* @extends MemoryCache<Token[] | TokensList>
|
|
102
|
+
*
|
|
103
|
+
* @example
|
|
104
|
+
* ```typescript
|
|
105
|
+
* // Create cache with custom settings
|
|
106
|
+
* const cache = new TokenCache({
|
|
107
|
+
* maxSize: 100, // Cache up to 100 documents
|
|
108
|
+
* ttl: 5 * 60 * 1000 // Expire after 5 minutes
|
|
109
|
+
* })
|
|
110
|
+
*
|
|
111
|
+
* // Check cache before parsing
|
|
112
|
+
* const cached = cache.getTokens(markdown, options)
|
|
113
|
+
* if (cached) {
|
|
114
|
+
* return cached // Fast path - no parsing needed
|
|
115
|
+
* }
|
|
116
|
+
*
|
|
117
|
+
* // Parse and cache
|
|
118
|
+
* const tokens = lexer.lex(markdown)
|
|
119
|
+
* cache.setTokens(markdown, options, tokens)
|
|
120
|
+
* ```
|
|
121
|
+
*/
|
|
122
|
+
export class TokenCache extends MemoryCache {
|
|
123
|
+
/**
|
|
124
|
+
* Creates a new TokenCache instance.
|
|
125
|
+
*
|
|
126
|
+
* @param options - Cache configuration
|
|
127
|
+
* @param options.maxSize - Maximum number of documents to cache (default: 50)
|
|
128
|
+
* @param options.ttl - Time-to-live in milliseconds (default: 5 minutes)
|
|
129
|
+
*/
|
|
130
|
+
constructor(options) {
|
|
131
|
+
super({ maxSize: 50, ttl: 5 * 60 * 1000, ...options });
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Retrieves cached tokens for given markdown source and options.
|
|
135
|
+
* Returns undefined if not cached or expired.
|
|
136
|
+
*
|
|
137
|
+
* @param source - Raw markdown string
|
|
138
|
+
* @param options - Svelte markdown parser options
|
|
139
|
+
* @returns Cached tokens or undefined
|
|
140
|
+
*
|
|
141
|
+
* @example
|
|
142
|
+
* ```typescript
|
|
143
|
+
* const tokens = cache.getTokens('# Hello', { gfm: true })
|
|
144
|
+
* if (tokens) {
|
|
145
|
+
* console.log('Cache hit!')
|
|
146
|
+
* }
|
|
147
|
+
* ```
|
|
148
|
+
*/
|
|
149
|
+
getTokens(source, options) {
|
|
150
|
+
const key = getCacheKey(source, options);
|
|
151
|
+
return this.get(key);
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Stores parsed tokens in cache for given markdown source and options.
|
|
155
|
+
* If cache is full, oldest entry is evicted (LRU).
|
|
156
|
+
*
|
|
157
|
+
* @param source - Raw markdown string
|
|
158
|
+
* @param options - Svelte markdown parser options
|
|
159
|
+
* @param tokens - Parsed token array or token list
|
|
160
|
+
*
|
|
161
|
+
* @example
|
|
162
|
+
* ```typescript
|
|
163
|
+
* const tokens = lexer.lex(markdown)
|
|
164
|
+
* cache.setTokens(markdown, options, tokens)
|
|
165
|
+
* ```
|
|
166
|
+
*/
|
|
167
|
+
setTokens(source, options, tokens) {
|
|
168
|
+
const key = getCacheKey(source, options);
|
|
169
|
+
this.set(key, tokens);
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Checks if tokens are cached without retrieving them.
|
|
173
|
+
* Useful for cache statistics or conditional logic.
|
|
174
|
+
*
|
|
175
|
+
* @param source - Raw markdown string
|
|
176
|
+
* @param options - Svelte markdown parser options
|
|
177
|
+
* @returns True if cached and not expired
|
|
178
|
+
*
|
|
179
|
+
* @example
|
|
180
|
+
* ```typescript
|
|
181
|
+
* if (cache.hasTokens(markdown, options)) {
|
|
182
|
+
* console.log('Cache hit - no parsing needed')
|
|
183
|
+
* }
|
|
184
|
+
* ```
|
|
185
|
+
*/
|
|
186
|
+
hasTokens(source, options) {
|
|
187
|
+
const key = getCacheKey(source, options);
|
|
188
|
+
return this.has(key);
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Removes cached tokens for specific source and options.
|
|
192
|
+
*
|
|
193
|
+
* @param source - Raw markdown string
|
|
194
|
+
* @param options - Svelte markdown parser options
|
|
195
|
+
* @returns True if entry was removed, false if not found
|
|
196
|
+
*
|
|
197
|
+
* @example
|
|
198
|
+
* ```typescript
|
|
199
|
+
* cache.deleteTokens(markdown, options) // Remove specific cached entry
|
|
200
|
+
* ```
|
|
201
|
+
*/
|
|
202
|
+
deleteTokens(source, options) {
|
|
203
|
+
const key = getCacheKey(source, options);
|
|
204
|
+
return this.delete(key);
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Removes all cached tokens from the cache.
|
|
208
|
+
*
|
|
209
|
+
* @example
|
|
210
|
+
* ```typescript
|
|
211
|
+
* cache.clearAllTokens() // Clear entire cache
|
|
212
|
+
* ```
|
|
213
|
+
*/
|
|
214
|
+
clearAllTokens() {
|
|
215
|
+
this.clear();
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Global singleton instance for shared token caching.
|
|
220
|
+
* Use this instance across your application for maximum cache efficiency.
|
|
221
|
+
*
|
|
222
|
+
* @example
|
|
223
|
+
* ```typescript
|
|
224
|
+
* import { tokenCache } from './token-cache.js'
|
|
225
|
+
*
|
|
226
|
+
* const cached = tokenCache.getTokens(markdown, options)
|
|
227
|
+
* if (!cached) {
|
|
228
|
+
* const tokens = lexer.lex(markdown)
|
|
229
|
+
* tokenCache.setTokens(markdown, options, tokens)
|
|
230
|
+
* }
|
|
231
|
+
* ```
|
|
232
|
+
*/
|
|
233
|
+
export const tokenCache = new TokenCache();
|
|
234
|
+
/**
|
|
235
|
+
* Export hash function for testing purposes.
|
|
236
|
+
* @internal
|
|
237
|
+
*/
|
|
238
|
+
export { hashString };
|
|
@@ -273,7 +273,12 @@ export const containsMultipleTags = (html) => {
|
|
|
273
273
|
export const shrinkHtmlTokens = (tokens) => {
|
|
274
274
|
const result = [];
|
|
275
275
|
for (const token of tokens) {
|
|
276
|
-
if (token
|
|
276
|
+
if ('tokens' in token && Array.isArray(token.tokens)) {
|
|
277
|
+
const t = token;
|
|
278
|
+
t.tokens = shrinkHtmlTokens(t.tokens);
|
|
279
|
+
result.push(token);
|
|
280
|
+
}
|
|
281
|
+
else if (token.type === 'list') {
|
|
277
282
|
token.items = token.items.map((item, index) => ({
|
|
278
283
|
...item,
|
|
279
284
|
listItemIndex: index,
|
package/package.json
CHANGED
|
@@ -1,21 +1,29 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@humanspeak/svelte-markdown",
|
|
3
|
-
"version": "0.8.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.8.13",
|
|
4
|
+
"description": "Fast, customizable markdown renderer for Svelte with built-in caching, TypeScript support, and Svelte 5 runes",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"svelte",
|
|
7
|
+
"svelte5",
|
|
8
|
+
"sveltekit",
|
|
7
9
|
"markdown",
|
|
8
10
|
"renderer",
|
|
9
11
|
"parser",
|
|
12
|
+
"fast",
|
|
13
|
+
"performance",
|
|
14
|
+
"cache",
|
|
15
|
+
"caching",
|
|
10
16
|
"marked",
|
|
11
17
|
"component",
|
|
12
|
-
"
|
|
13
|
-
"
|
|
18
|
+
"typescript",
|
|
19
|
+
"runes",
|
|
14
20
|
"md",
|
|
15
21
|
"documentation",
|
|
16
22
|
"html",
|
|
17
23
|
"converter",
|
|
18
|
-
"formatting"
|
|
24
|
+
"formatting",
|
|
25
|
+
"customizable",
|
|
26
|
+
"extensible"
|
|
19
27
|
],
|
|
20
28
|
"homepage": "https://markdown.svelte.page",
|
|
21
29
|
"bugs": {
|
|
@@ -57,25 +65,25 @@
|
|
|
57
65
|
"dependencies": {
|
|
58
66
|
"github-slugger": "^2.0.0",
|
|
59
67
|
"htmlparser2": "^10.0.0",
|
|
60
|
-
"marked": "^16.
|
|
68
|
+
"marked": "^16.4.0"
|
|
61
69
|
},
|
|
62
70
|
"devDependencies": {
|
|
63
71
|
"@eslint/compat": "^1.4.0",
|
|
64
|
-
"@eslint/js": "^9.
|
|
65
|
-
"@playwright/test": "^1.
|
|
66
|
-
"@sveltejs/adapter-auto": "^6.1.
|
|
67
|
-
"@sveltejs/kit": "^2.
|
|
72
|
+
"@eslint/js": "^9.37.0",
|
|
73
|
+
"@playwright/test": "^1.56.0",
|
|
74
|
+
"@sveltejs/adapter-auto": "^6.1.1",
|
|
75
|
+
"@sveltejs/kit": "^2.46.3",
|
|
68
76
|
"@sveltejs/package": "^2.5.4",
|
|
69
77
|
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
|
70
|
-
"@testing-library/jest-dom": "^6.
|
|
78
|
+
"@testing-library/jest-dom": "^6.9.1",
|
|
71
79
|
"@testing-library/svelte": "^5.2.8",
|
|
72
80
|
"@testing-library/user-event": "^14.6.1",
|
|
73
|
-
"@types/node": "^24.
|
|
74
|
-
"@typescript-eslint/eslint-plugin": "^8.
|
|
75
|
-
"@typescript-eslint/parser": "^8.
|
|
81
|
+
"@types/node": "^24.7.0",
|
|
82
|
+
"@typescript-eslint/eslint-plugin": "^8.46.0",
|
|
83
|
+
"@typescript-eslint/parser": "^8.46.0",
|
|
76
84
|
"@vitest/coverage-v8": "^3.2.4",
|
|
77
85
|
"concurrently": "^9.2.1",
|
|
78
|
-
"eslint": "^9.
|
|
86
|
+
"eslint": "^9.37.0",
|
|
79
87
|
"eslint-config-prettier": "^10.1.8",
|
|
80
88
|
"eslint-plugin-import": "^2.32.0",
|
|
81
89
|
"eslint-plugin-svelte": "^3.12.4",
|
|
@@ -87,12 +95,12 @@
|
|
|
87
95
|
"prettier-plugin-organize-imports": "^4.3.0",
|
|
88
96
|
"prettier-plugin-svelte": "^3.4.0",
|
|
89
97
|
"prettier-plugin-tailwindcss": "^0.6.14",
|
|
90
|
-
"publint": "^0.3.
|
|
91
|
-
"svelte": "^5.39.
|
|
98
|
+
"publint": "^0.3.14",
|
|
99
|
+
"svelte": "^5.39.11",
|
|
92
100
|
"svelte-check": "^4.3.2",
|
|
93
|
-
"typescript": "^5.9.
|
|
94
|
-
"typescript-eslint": "^8.
|
|
95
|
-
"vite": "^7.1.
|
|
101
|
+
"typescript": "^5.9.3",
|
|
102
|
+
"typescript-eslint": "^8.46.0",
|
|
103
|
+
"vite": "^7.1.9",
|
|
96
104
|
"vitest": "^3.2.4"
|
|
97
105
|
},
|
|
98
106
|
"peerDependencies": {
|