@humanspeak/svelte-markdown 0.8.11 → 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.
@@ -0,0 +1,263 @@
1
+ /**
2
+ * Special sentinel values to represent cached undefined/null values.
3
+ * This allows us to distinguish between "not cached" and "cached undefined/null".
4
+ */
5
+ const CACHED_UNDEFINED = Symbol('CACHED_UNDEFINED');
6
+ const CACHED_NULL = Symbol('CACHED_NULL');
7
+ /**
8
+ * Generic in-memory cache implementation with TTL and size-based eviction.
9
+ * Provides efficient caching for any type of data with automatic cleanup
10
+ * of expired or excess entries.
11
+ *
12
+ * @class MemoryCache
13
+ * @template T - The type of values being cached
14
+ *
15
+ * @example
16
+ * ```typescript
17
+ * // Create a cache for string values
18
+ * const cache = new MemoryCache<string>({
19
+ * maxSize: 100,
20
+ * ttl: 5 * 60 * 1000 // 5 minutes
21
+ * });
22
+ *
23
+ * // Store and retrieve values
24
+ * cache.set('key', 'value');
25
+ * const value = cache.get('key');
26
+ * ```
27
+ */
28
+ export class MemoryCache {
29
+ cache = new Map();
30
+ maxSize;
31
+ ttl;
32
+ /**
33
+ * Creates a new MemoryCache instance.
34
+ *
35
+ * @param {CacheOptions} options - Configuration options for the cache
36
+ * @param {number} [options.maxSize=100] - Maximum number of entries (default: 100)
37
+ * @param {number} [options.ttl=300000] - Time-to-live in milliseconds (default: 5 minutes)
38
+ */
39
+ constructor(options = {}) {
40
+ this.maxSize = options.maxSize ?? 100;
41
+ this.ttl = options.ttl ?? 5 * 60 * 1000; // 5 minutes default
42
+ }
43
+ /**
44
+ * Retrieves a value from the cache if it exists and hasn't expired.
45
+ *
46
+ * @param {string} key - The key to look up
47
+ * @returns {T | undefined} The cached value if found and valid, undefined otherwise
48
+ *
49
+ * @example
50
+ * ```typescript
51
+ * const cache = new MemoryCache<number>();
52
+ * cache.set('counter', 42);
53
+ * const value = cache.get('counter'); // Returns 42
54
+ * const missing = cache.get('nonexistent'); // Returns undefined
55
+ * ```
56
+ */
57
+ get(key) {
58
+ const entry = this.cache.get(key);
59
+ if (!entry)
60
+ return undefined;
61
+ // Check if entry has expired (skip check if TTL is 0 or negative)
62
+ if (this.ttl > 0 && Date.now() - entry.timestamp > this.ttl) {
63
+ this.cache.delete(key);
64
+ return undefined;
65
+ }
66
+ // Handle the special sentinel values for cached undefined/null
67
+ if (entry.value === CACHED_UNDEFINED) {
68
+ return undefined;
69
+ }
70
+ if (entry.value === CACHED_NULL) {
71
+ return null;
72
+ }
73
+ return entry.value;
74
+ }
75
+ /**
76
+ * Checks if a key exists in the cache (regardless of its value).
77
+ * This is useful for distinguishing between cache misses and cached undefined values.
78
+ *
79
+ * @param {string} key - The key to check
80
+ * @returns {boolean} True if the key exists in cache and hasn't expired, false otherwise
81
+ */
82
+ has(key) {
83
+ const entry = this.cache.get(key);
84
+ if (!entry)
85
+ return false;
86
+ // Check if entry has expired (skip check if TTL is 0 or negative)
87
+ if (this.ttl > 0 && Date.now() - entry.timestamp > this.ttl) {
88
+ this.cache.delete(key);
89
+ return false;
90
+ }
91
+ return true;
92
+ }
93
+ /**
94
+ * Stores a value in the cache. If the cache is full, the oldest entry is removed.
95
+ *
96
+ * @param {string} key - The key under which to store the value
97
+ * @param {T} value - The value to cache
98
+ *
99
+ * @example
100
+ * ```typescript
101
+ * const cache = new MemoryCache<string>();
102
+ * cache.set('greeting', 'Hello, World!');
103
+ * ```
104
+ */
105
+ set(key, value) {
106
+ // Remove oldest entry if cache is full (skip if maxSize is 0 or negative)
107
+ if (this.maxSize > 0 && this.cache.size >= this.maxSize) {
108
+ const oldestKey = this.cache.keys().next().value;
109
+ if (oldestKey) {
110
+ this.cache.delete(oldestKey);
111
+ }
112
+ }
113
+ // Store undefined/null values as sentinels to distinguish from cache misses
114
+ let valueToStore;
115
+ if (value === undefined) {
116
+ valueToStore = CACHED_UNDEFINED;
117
+ }
118
+ else if (value === null) {
119
+ valueToStore = CACHED_NULL;
120
+ }
121
+ else {
122
+ valueToStore = value;
123
+ }
124
+ this.cache.set(key, {
125
+ value: valueToStore,
126
+ timestamp: Date.now()
127
+ });
128
+ }
129
+ /**
130
+ * Removes a specific entry from the cache.
131
+ *
132
+ * @param {string} key - The key of the entry to remove
133
+ * @returns {boolean} True if an element was removed, false if the key wasn't found
134
+ *
135
+ * @example
136
+ * ```typescript
137
+ * const cache = new MemoryCache<string>();
138
+ * cache.set('key', 'value');
139
+ * cache.delete('key'); // Returns true
140
+ * cache.delete('nonexistent'); // Returns false
141
+ * ```
142
+ */
143
+ delete(key) {
144
+ return this.cache.delete(key);
145
+ }
146
+ async deleteAsync(key) {
147
+ return Promise.resolve(this.cache.delete(key));
148
+ }
149
+ /**
150
+ * Removes all entries from the cache.
151
+ *
152
+ * @example
153
+ * ```typescript
154
+ * const cache = new MemoryCache<string>();
155
+ * cache.set('key1', 'value1');
156
+ * cache.set('key2', 'value2');
157
+ * cache.clear(); // Removes all entries
158
+ * ```
159
+ */
160
+ clear() {
161
+ this.cache.clear();
162
+ }
163
+ /**
164
+ * Removes all entries from the cache whose keys start with the given prefix.
165
+ *
166
+ * @param {string} prefix - The prefix to match against cache keys
167
+ * @returns {number} Number of entries removed
168
+ *
169
+ * @example
170
+ * ```typescript
171
+ * const cache = new MemoryCache<string>();
172
+ * cache.set('user:123:name', 'John');
173
+ * cache.set('user:123:email', 'john@example.com');
174
+ * cache.set('post:456', 'Hello World');
175
+ *
176
+ * const removed = cache.deleteByPrefix('user:123:'); // Returns 2
177
+ * ```
178
+ */
179
+ deleteByPrefix(prefix) {
180
+ let count = 0;
181
+ for (const key of this.cache.keys()) {
182
+ if (key.startsWith(prefix)) {
183
+ this.cache.delete(key);
184
+ count++;
185
+ }
186
+ }
187
+ return count;
188
+ }
189
+ /**
190
+ * Removes all entries from the cache whose keys match the given wildcard pattern.
191
+ * Supports asterisk (*) wildcards for flexible pattern matching.
192
+ *
193
+ * @param {string} magicString - The wildcard pattern to match against cache keys (use * for wildcards)
194
+ * @returns {number} Number of entries removed
195
+ *
196
+ * @example
197
+ * ```typescript
198
+ * const cache = new MemoryCache<string>();
199
+ * cache.set('user:123:name', 'John');
200
+ * cache.set('user:123:email', 'john@example.com');
201
+ * cache.set('user:456:name', 'Jane');
202
+ * cache.set('post:789', 'Hello World');
203
+ *
204
+ * const removed1 = cache.deleteByMagicString('user:123:*'); // Returns 2 (matches name and email)
205
+ * const removed2 = cache.deleteByMagicString('user:*:name'); // Returns 1 (matches Jane's name)
206
+ * const removed3 = cache.deleteByMagicString('post:*'); // Returns 1 (matches the post)
207
+ * ```
208
+ */
209
+ deleteByMagicString(magicString) {
210
+ let count = 0;
211
+ // Convert wildcard pattern to regex
212
+ const escapedPattern = magicString
213
+ .replace(/[.+?^${}()|[\]\\]/g, '\\$&') // Escape regex special chars except *
214
+ .replace(/\*/g, '.*'); // Convert * to .*
215
+ const regex = new RegExp(`^${escapedPattern}$`);
216
+ for (const key of this.cache.keys()) {
217
+ if (regex.test(key)) {
218
+ this.cache.delete(key);
219
+ count++;
220
+ }
221
+ }
222
+ return count;
223
+ }
224
+ }
225
+ /**
226
+ * Cache decorator factory for method-level caching.
227
+ * Provides a way to cache method results based on their arguments.
228
+ *
229
+ * @template T - The return type of the decorated method
230
+ * @param {CacheOptions} options - Configuration options for the cache
231
+ * @returns A method decorator that caches the results
232
+ *
233
+ * @example
234
+ * ```typescript
235
+ * class UserService {
236
+ * @cached<User>({ ttl: 60000 })
237
+ * async getUser(id: string): Promise<User> {
238
+ * // Expensive operation
239
+ * return await fetchUser(id);
240
+ * }
241
+ * }
242
+ * ```
243
+ */
244
+ export function cached(options = {}) {
245
+ const cache = new MemoryCache(options);
246
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
247
+ return function (target, propertyKey, descriptor) {
248
+ const originalMethod = descriptor.value;
249
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
250
+ descriptor.value = function (...args) {
251
+ const key = `${propertyKey}:${JSON.stringify(args)}`;
252
+ // Use has() to check if key exists, then get() to retrieve value
253
+ // This allows us to distinguish between cache miss and cached undefined
254
+ if (cache.has(key)) {
255
+ return cache.get(key);
256
+ }
257
+ const result = originalMethod.apply(this, args);
258
+ cache.set(key, result);
259
+ return result;
260
+ };
261
+ return descriptor;
262
+ };
263
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Parse and Cache Utility
3
+ *
4
+ * Handles markdown parsing with intelligent caching.
5
+ * Separates parsing logic from component code for better testability.
6
+ *
7
+ * @module parse-and-cache
8
+ */
9
+ import type { SvelteMarkdownOptions } from '../types.js';
10
+ import type { Token, TokensList } from './markdown-parser.js';
11
+ /**
12
+ * Parses markdown source with caching.
13
+ * Checks cache first, parses on miss, stores result, and returns tokens.
14
+ *
15
+ * @param source - Raw markdown string to parse
16
+ * @param options - Svelte markdown parser options
17
+ * @param isInline - Whether to parse as inline markdown (no block elements)
18
+ * @returns Cleaned and cached token array
19
+ *
20
+ * @example
21
+ * ```typescript
22
+ * import { parseAndCacheTokens } from './parse-and-cache.js'
23
+ *
24
+ * // Parse markdown with caching
25
+ * const tokens = parseAndCacheTokens('# Hello World', { gfm: true }, false)
26
+ *
27
+ * // Second call with same input returns cached result (<1ms)
28
+ * const cachedTokens = parseAndCacheTokens('# Hello World', { gfm: true }, false)
29
+ * ```
30
+ */
31
+ export declare function parseAndCacheTokens(source: string, options: SvelteMarkdownOptions, isInline: boolean): Token[] | TokensList;
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Parse and Cache Utility
3
+ *
4
+ * Handles markdown parsing with intelligent caching.
5
+ * Separates parsing logic from component code for better testability.
6
+ *
7
+ * @module parse-and-cache
8
+ */
9
+ import { tokenCache } from './token-cache.js';
10
+ import { shrinkHtmlTokens } from './token-cleanup.js';
11
+ import { Lexer } from 'marked';
12
+ /**
13
+ * Parses markdown source with caching.
14
+ * Checks cache first, parses on miss, stores result, and returns tokens.
15
+ *
16
+ * @param source - Raw markdown string to parse
17
+ * @param options - Svelte markdown parser options
18
+ * @param isInline - Whether to parse as inline markdown (no block elements)
19
+ * @returns Cleaned and cached token array
20
+ *
21
+ * @example
22
+ * ```typescript
23
+ * import { parseAndCacheTokens } from './parse-and-cache.js'
24
+ *
25
+ * // Parse markdown with caching
26
+ * const tokens = parseAndCacheTokens('# Hello World', { gfm: true }, false)
27
+ *
28
+ * // Second call with same input returns cached result (<1ms)
29
+ * const cachedTokens = parseAndCacheTokens('# Hello World', { gfm: true }, false)
30
+ * ```
31
+ */
32
+ export function parseAndCacheTokens(source, options, isInline) {
33
+ // Check cache first - avoids expensive parsing
34
+ const cached = tokenCache.getTokens(source, options);
35
+ if (cached) {
36
+ return cached;
37
+ }
38
+ // Cache miss - parse and store
39
+ const lexer = new Lexer(options);
40
+ const parsedTokens = isInline ? lexer.inlineTokens(source) : lexer.lex(source);
41
+ const cleanedTokens = shrinkHtmlTokens(parsedTokens);
42
+ // Cache the cleaned tokens for next time
43
+ tokenCache.setTokens(source, options, cleanedTokens);
44
+ return cleanedTokens;
45
+ }
@@ -0,0 +1,178 @@
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 type { SvelteMarkdownOptions } from '../types.js';
17
+ import { MemoryCache } from './cache.js';
18
+ import type { Token, TokensList } from './markdown-parser.js';
19
+ /**
20
+ * Fast non-cryptographic hash function using FNV-1a algorithm.
21
+ * Optimized for speed over cryptographic security.
22
+ *
23
+ * FNV-1a (Fowler-Noll-Vo) provides:
24
+ * - Fast computation (single pass through string)
25
+ * - Good distribution (minimal collisions)
26
+ * - Small output size (base36 string)
27
+ *
28
+ * @param str - String to hash
29
+ * @returns Base36 hash string
30
+ *
31
+ * @example
32
+ * ```typescript
33
+ * const hash1 = hashString('# Hello World')
34
+ * const hash2 = hashString('# Hello World!')
35
+ * console.log(hash1 !== hash2) // true - different content = different hash
36
+ * ```
37
+ */
38
+ declare function hashString(str: string): string;
39
+ /**
40
+ * Specialized cache for markdown token storage.
41
+ * Extends MemoryCache with markdown-specific convenience methods.
42
+ *
43
+ * Inherits from MemoryCache:
44
+ * - Automatic LRU eviction when maxSize is reached
45
+ * - TTL-based expiration for time-sensitive content
46
+ * - Advanced deletion (deleteByPrefix, deleteByMagicString)
47
+ *
48
+ * Performance characteristics:
49
+ * - Cache hit: <1ms (vs 50-200ms parsing)
50
+ * - Memory: ~5MB for 50 cached documents (default maxSize)
51
+ * - Hash computation: ~0.05ms for 10KB, ~0.5ms for 100KB
52
+ *
53
+ * @class TokenCache
54
+ * @extends MemoryCache<Token[] | TokensList>
55
+ *
56
+ * @example
57
+ * ```typescript
58
+ * // Create cache with custom settings
59
+ * const cache = new TokenCache({
60
+ * maxSize: 100, // Cache up to 100 documents
61
+ * ttl: 5 * 60 * 1000 // Expire after 5 minutes
62
+ * })
63
+ *
64
+ * // Check cache before parsing
65
+ * const cached = cache.getTokens(markdown, options)
66
+ * if (cached) {
67
+ * return cached // Fast path - no parsing needed
68
+ * }
69
+ *
70
+ * // Parse and cache
71
+ * const tokens = lexer.lex(markdown)
72
+ * cache.setTokens(markdown, options, tokens)
73
+ * ```
74
+ */
75
+ export declare class TokenCache extends MemoryCache<Token[] | TokensList> {
76
+ /**
77
+ * Creates a new TokenCache instance.
78
+ *
79
+ * @param options - Cache configuration
80
+ * @param options.maxSize - Maximum number of documents to cache (default: 50)
81
+ * @param options.ttl - Time-to-live in milliseconds (default: 5 minutes)
82
+ */
83
+ constructor(options?: {
84
+ maxSize?: number;
85
+ ttl?: number;
86
+ });
87
+ /**
88
+ * Retrieves cached tokens for given markdown source and options.
89
+ * Returns undefined if not cached or expired.
90
+ *
91
+ * @param source - Raw markdown string
92
+ * @param options - Svelte markdown parser options
93
+ * @returns Cached tokens or undefined
94
+ *
95
+ * @example
96
+ * ```typescript
97
+ * const tokens = cache.getTokens('# Hello', { gfm: true })
98
+ * if (tokens) {
99
+ * console.log('Cache hit!')
100
+ * }
101
+ * ```
102
+ */
103
+ getTokens(source: string, options: SvelteMarkdownOptions): Token[] | TokensList | undefined;
104
+ /**
105
+ * Stores parsed tokens in cache for given markdown source and options.
106
+ * If cache is full, oldest entry is evicted (LRU).
107
+ *
108
+ * @param source - Raw markdown string
109
+ * @param options - Svelte markdown parser options
110
+ * @param tokens - Parsed token array or token list
111
+ *
112
+ * @example
113
+ * ```typescript
114
+ * const tokens = lexer.lex(markdown)
115
+ * cache.setTokens(markdown, options, tokens)
116
+ * ```
117
+ */
118
+ setTokens(source: string, options: SvelteMarkdownOptions, tokens: Token[] | TokensList): void;
119
+ /**
120
+ * Checks if tokens are cached without retrieving them.
121
+ * Useful for cache statistics or conditional logic.
122
+ *
123
+ * @param source - Raw markdown string
124
+ * @param options - Svelte markdown parser options
125
+ * @returns True if cached and not expired
126
+ *
127
+ * @example
128
+ * ```typescript
129
+ * if (cache.hasTokens(markdown, options)) {
130
+ * console.log('Cache hit - no parsing needed')
131
+ * }
132
+ * ```
133
+ */
134
+ hasTokens(source: string, options: SvelteMarkdownOptions): boolean;
135
+ /**
136
+ * Removes cached tokens for specific source and options.
137
+ *
138
+ * @param source - Raw markdown string
139
+ * @param options - Svelte markdown parser options
140
+ * @returns True if entry was removed, false if not found
141
+ *
142
+ * @example
143
+ * ```typescript
144
+ * cache.deleteTokens(markdown, options) // Remove specific cached entry
145
+ * ```
146
+ */
147
+ deleteTokens(source: string, options: SvelteMarkdownOptions): boolean;
148
+ /**
149
+ * Removes all cached tokens from the cache.
150
+ *
151
+ * @example
152
+ * ```typescript
153
+ * cache.clearAllTokens() // Clear entire cache
154
+ * ```
155
+ */
156
+ clearAllTokens(): void;
157
+ }
158
+ /**
159
+ * Global singleton instance for shared token caching.
160
+ * Use this instance across your application for maximum cache efficiency.
161
+ *
162
+ * @example
163
+ * ```typescript
164
+ * import { tokenCache } from './token-cache.js'
165
+ *
166
+ * const cached = tokenCache.getTokens(markdown, options)
167
+ * if (!cached) {
168
+ * const tokens = lexer.lex(markdown)
169
+ * tokenCache.setTokens(markdown, options, tokens)
170
+ * }
171
+ * ```
172
+ */
173
+ export declare const tokenCache: TokenCache;
174
+ /**
175
+ * Export hash function for testing purposes.
176
+ * @internal
177
+ */
178
+ export { hashString };