@fuzdev/fuz_code 0.46.0 → 0.46.2
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/dist/CodeHighlight.svelte +14 -50
- package/dist/CodeHighlight.svelte.d.ts +1 -1
- package/dist/CodeHighlight.svelte.d.ts.map +1 -1
- package/dist/CodeTextarea.svelte +156 -0
- package/dist/CodeTextarea.svelte.d.ts +43 -0
- package/dist/CodeTextarea.svelte.d.ts.map +1 -0
- package/dist/highlight_manager.d.ts +20 -6
- package/dist/highlight_manager.d.ts.map +1 -1
- package/dist/highlight_manager.js +128 -72
- package/dist/range_highlighting.svelte.d.ts +39 -0
- package/dist/range_highlighting.svelte.d.ts.map +1 -0
- package/dist/range_highlighting.svelte.js +57 -0
- package/dist/syntax_styler.d.ts +12 -0
- package/dist/syntax_styler.d.ts.map +1 -1
- package/dist/syntax_styler.js +49 -13
- package/package.json +6 -5
- package/src/lib/highlight_manager.ts +152 -82
- package/src/lib/range_highlighting.svelte.ts +100 -0
- package/src/lib/syntax_styler.ts +63 -14
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import {onDestroy} from 'svelte';
|
|
2
|
+
import {DEV} from 'esm-env';
|
|
3
|
+
|
|
4
|
+
import type {SyntaxStyler, SyntaxGrammar} from './syntax_styler.js';
|
|
5
|
+
import {HighlightManager, supports_css_highlight_api} from './highlight_manager.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Reactive inputs for `create_range_highlighting`. All values are getters so the
|
|
9
|
+
* helper can track the consuming component's reactive state across the call
|
|
10
|
+
* boundary (the Svelte 5 getter-injection pattern).
|
|
11
|
+
*/
|
|
12
|
+
export interface RangeHighlightingOptions {
|
|
13
|
+
/** The element whose first text node receives the highlight ranges. */
|
|
14
|
+
element: () => Element | undefined;
|
|
15
|
+
/**
|
|
16
|
+
* The text to tokenize. Must match the element's text node exactly (e.g. a
|
|
17
|
+
* textarea backdrop includes its trailing newline here too).
|
|
18
|
+
*/
|
|
19
|
+
text: () => string;
|
|
20
|
+
/** Language id; `null` disables highlighting. */
|
|
21
|
+
lang: () => string | null;
|
|
22
|
+
/** Optional custom grammar; takes precedence over `lang` for tokenization. */
|
|
23
|
+
grammar: () => SyntaxGrammar | undefined;
|
|
24
|
+
/** The syntax styler whose registered grammars back `lang` lookups. */
|
|
25
|
+
syntax_styler: () => SyntaxStyler;
|
|
26
|
+
/** Extra gate — ranges are only applied when this returns true. Defaults to always-on. */
|
|
27
|
+
enabled?: () => boolean;
|
|
28
|
+
/** Component name used in DEV warnings. */
|
|
29
|
+
dev_label: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Reactive outputs from `create_range_highlighting`. */
|
|
33
|
+
export interface RangeHighlighting {
|
|
34
|
+
readonly highlighting_disabled: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Wires up CSS Custom Highlight API range highlighting for a single element's
|
|
39
|
+
* text node, shared by `CodeHighlight` and `CodeTextarea`. Creates a
|
|
40
|
+
* `HighlightManager`, memoizes tokenization, applies/clears ranges in an effect,
|
|
41
|
+
* emits DEV warnings for unsupported languages, and tears down on destroy.
|
|
42
|
+
*
|
|
43
|
+
* Must be called during component initialization (it uses `$effect`/`onDestroy`).
|
|
44
|
+
*/
|
|
45
|
+
export const create_range_highlighting = (options: RangeHighlightingOptions): RangeHighlighting => {
|
|
46
|
+
const manager = supports_css_highlight_api() ? new HighlightManager() : null;
|
|
47
|
+
const is_enabled = options.enabled ?? (() => true);
|
|
48
|
+
|
|
49
|
+
const language_supported = $derived(
|
|
50
|
+
options.lang() !== null && !!options.syntax_styler().langs[options.lang()!],
|
|
51
|
+
);
|
|
52
|
+
const highlighting_disabled = $derived(
|
|
53
|
+
options.lang() === null || (!language_supported && !options.grammar()),
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
// tokenize once per (text, grammar, lang) change -- memoized so unrelated
|
|
57
|
+
// reactivity doesn't trigger a full re-tokenization
|
|
58
|
+
const range_tokens = $derived.by(() => {
|
|
59
|
+
if (!manager || !is_enabled() || highlighting_disabled) return null;
|
|
60
|
+
const text = options.text();
|
|
61
|
+
if (!text) return null;
|
|
62
|
+
// route through the styler so `before_tokenize`/`after_tokenize` hooks run,
|
|
63
|
+
// matching `stylize`'s HTML path; `grammar` undefined falls back to the
|
|
64
|
+
// registered lang grammar (`! safe bc of `highlighting_disabled`)
|
|
65
|
+
return options.syntax_styler().tokenize(text, options.lang()!, options.grammar());
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
if (manager) {
|
|
69
|
+
$effect(() => {
|
|
70
|
+
const element = options.element();
|
|
71
|
+
if (!element || !range_tokens) {
|
|
72
|
+
manager.clear_element_ranges();
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
manager.highlight_from_syntax_tokens(element, range_tokens);
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (DEV) {
|
|
80
|
+
$effect(() => {
|
|
81
|
+
// a lang was requested but we can't highlight it (unknown id, no grammar)
|
|
82
|
+
if (options.lang() && highlighting_disabled) {
|
|
83
|
+
const langs = Object.keys(options.syntax_styler().langs).join(', ');
|
|
84
|
+
// eslint-disable-next-line no-console
|
|
85
|
+
console.error(
|
|
86
|
+
`[${options.dev_label}] Language "${options.lang()}" is not supported and no custom grammar provided. ` +
|
|
87
|
+
`Highlighting disabled. Supported: ${langs}`,
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
onDestroy(() => manager?.destroy());
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
get highlighting_disabled() {
|
|
97
|
+
return highlighting_disabled;
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
};
|
package/src/lib/syntax_styler.ts
CHANGED
|
@@ -3,6 +3,13 @@ import {tokenize_syntax} from './tokenize_syntax.js';
|
|
|
3
3
|
|
|
4
4
|
export type AddSyntaxGrammar = (syntax_styler: SyntaxStyler) => void;
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Maps a matched `&`, `<`, or non-breaking space in text content to its
|
|
8
|
+
* HTML-safe form. Used as the replacer in `stringify_token` for leaf strings
|
|
9
|
+
* (non-breaking spaces are normalized to a regular space).
|
|
10
|
+
*/
|
|
11
|
+
const escape_text_char = (ch: string): string => (ch === '&' ? '&' : ch === '<' ? '<' : ' ');
|
|
12
|
+
|
|
6
13
|
/**
|
|
7
14
|
* Based on Prism (https://github.com/PrismJS/prism)
|
|
8
15
|
* by Lea Verou (https://lea.verou.me/)
|
|
@@ -127,7 +134,42 @@ export class SyntaxStyler {
|
|
|
127
134
|
lang: string,
|
|
128
135
|
grammar: SyntaxGrammar | undefined = this.get_lang(lang),
|
|
129
136
|
): string {
|
|
130
|
-
|
|
137
|
+
// stringify with the post-hook `lang`, which a `before_tokenize` hook may
|
|
138
|
+
// have rewritten (it flows into each token's `wrap` hook context)
|
|
139
|
+
const c = this.#tokenize_hooked(text, lang, grammar);
|
|
140
|
+
return this.stringify_token(c.tokens, c.lang);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Tokenizes `text` into a `SyntaxTokenStream`, running the `before_tokenize`
|
|
145
|
+
* and `after_tokenize` hooks. This is the tokenization half of `stylize` — use
|
|
146
|
+
* it when you need the token stream itself (e.g. CSS Custom Highlight API range
|
|
147
|
+
* highlighting) rather than HTML.
|
|
148
|
+
*
|
|
149
|
+
* @param text - source to tokenize
|
|
150
|
+
* @param lang - language identifier; passed to the tokenize hooks
|
|
151
|
+
* @param grammar - grammar to tokenize with; defaults to `this.get_lang(lang)`
|
|
152
|
+
* @returns the resulting token stream
|
|
153
|
+
*/
|
|
154
|
+
tokenize(
|
|
155
|
+
text: string,
|
|
156
|
+
lang: string,
|
|
157
|
+
grammar: SyntaxGrammar | undefined = this.get_lang(lang),
|
|
158
|
+
): SyntaxTokenStream {
|
|
159
|
+
return this.#tokenize_hooked(text, lang, grammar).tokens;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Runs `before_tokenize` → `tokenize_syntax` → `after_tokenize`, returning the
|
|
164
|
+
* resolved context. Shared by `stylize` (which also needs the post-hook `lang`)
|
|
165
|
+
* and `tokenize` (which only needs `tokens`).
|
|
166
|
+
*/
|
|
167
|
+
#tokenize_hooked(
|
|
168
|
+
text: string,
|
|
169
|
+
lang: string,
|
|
170
|
+
grammar: SyntaxGrammar,
|
|
171
|
+
): HookAfterTokenizeCallbackContext {
|
|
172
|
+
const ctx: HookBeforeTokenizeCallbackContext = {
|
|
131
173
|
code: text,
|
|
132
174
|
grammar,
|
|
133
175
|
lang,
|
|
@@ -137,7 +179,7 @@ export class SyntaxStyler {
|
|
|
137
179
|
const c = ctx as any as HookAfterTokenizeCallbackContext;
|
|
138
180
|
c.tokens = tokenize_syntax(c.code, c.grammar);
|
|
139
181
|
this.run_hook_after_tokenize(c);
|
|
140
|
-
return
|
|
182
|
+
return c;
|
|
141
183
|
}
|
|
142
184
|
|
|
143
185
|
/**
|
|
@@ -263,10 +305,9 @@ export class SyntaxStyler {
|
|
|
263
305
|
*/
|
|
264
306
|
stringify_token(o: string | SyntaxToken | SyntaxTokenStream, lang: string): string {
|
|
265
307
|
if (typeof o === 'string') {
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
.replace(/\u00a0/g, ' ');
|
|
308
|
+
// single pass over the leaf text (only `&` and `<` need escaping in text
|
|
309
|
+
// content; `\u00a0` is normalized to a regular space)
|
|
310
|
+
return o.replace(/[&<\u00a0]/g, escape_text_char);
|
|
270
311
|
}
|
|
271
312
|
if (Array.isArray(o)) {
|
|
272
313
|
var s = '';
|
|
@@ -276,21 +317,29 @@ export class SyntaxStyler {
|
|
|
276
317
|
return s;
|
|
277
318
|
}
|
|
278
319
|
|
|
320
|
+
var content = this.stringify_token(o.content, lang);
|
|
321
|
+
|
|
322
|
+
// build the class list once; aliases are always an array after normalization
|
|
323
|
+
var classes = `token_${o.type}`;
|
|
324
|
+
for (const a of o.alias) {
|
|
325
|
+
classes += ` token_${a}`;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// fast path: with no `wrap` hooks the tag is always a plain <span> with no
|
|
329
|
+
// attributes, so skip the per-token context object and hook dispatch
|
|
330
|
+
if (this.hooks_wrap.length === 0) {
|
|
331
|
+
return '<span class="' + classes + '">' + content + '</span>';
|
|
332
|
+
}
|
|
333
|
+
|
|
279
334
|
var ctx: HookWrapCallbackContext = {
|
|
280
335
|
type: o.type,
|
|
281
|
-
content
|
|
336
|
+
content,
|
|
282
337
|
tag: 'span',
|
|
283
|
-
classes:
|
|
338
|
+
classes: classes.split(' '),
|
|
284
339
|
attributes: {},
|
|
285
340
|
lang,
|
|
286
341
|
};
|
|
287
342
|
|
|
288
|
-
var aliases = o.alias;
|
|
289
|
-
// alias is always an array after normalization
|
|
290
|
-
for (const a of aliases) {
|
|
291
|
-
ctx.classes.push(`token_${a}`);
|
|
292
|
-
}
|
|
293
|
-
|
|
294
343
|
this.run_hook_wrap(ctx);
|
|
295
344
|
|
|
296
345
|
var attributes = '';
|