Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/range-highlighting-class.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@fuzdev/fuz_code': minor
---

refactor: replace the `create_range_highlighting` getter factory with a `RangeHighlighting` class (the interface of that name is replaced by the class; construct during component init)
4 changes: 2 additions & 2 deletions src/lib/CodeHighlight.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
import { syntax_styler_global } from './syntax_styler_global.ts';
import type { SyntaxStyler } from './syntax_styler.ts';
import { supports_css_highlight_api, type HighlightMode } from './highlight_manager.ts';
import { create_range_highlighting } from './range_highlighting.svelte.ts';
import { RangeHighlighting } from './range_highlighting.svelte.ts';

const {
content,
Expand Down Expand Up @@ -80,7 +80,7 @@

const use_ranges = $derived(supports_ranges && (mode === 'ranges' || mode === 'auto'));

const rh = create_range_highlighting({
const rh = new RangeHighlighting({
element: () => code_element,
text: () => content,
enabled: () => use_ranges,
Expand Down
5 changes: 3 additions & 2 deletions src/lib/CodeTextarea.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

import { syntax_styler_global } from './syntax_styler_global.ts';
import type { SyntaxStyler } from './syntax_styler.ts';
import { create_range_highlighting } from './range_highlighting.svelte.ts';
import { RangeHighlighting } from './range_highlighting.svelte.ts';

let {
value = $bindable(''),
Expand Down Expand Up @@ -59,7 +59,8 @@
// and tokenized as-is so range positions match the text node exactly.
const display_text = $derived(value + '\n');

create_range_highlighting({
// eslint-disable-next-line no-new
new RangeHighlighting({
element: () => backdrop,
text: () => display_text,
lang: () => lang,
Expand Down
96 changes: 48 additions & 48 deletions src/lib/range_highlighting.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import type { SyntaxStyler } from './syntax_styler.ts';
import { HighlightManager, supports_css_highlight_api } from './highlight_manager.ts';

/**
* Reactive inputs for `create_range_highlighting`. All values are getters so the
* helper can track the consuming component's reactive state across the call
* Reactive inputs for `RangeHighlighting`. All values are getters so the
* class can track the consuming component's reactive state across the call
* boundary (the Svelte 5 getter-injection pattern).
*/
export interface RangeHighlightingOptions {
Expand All @@ -27,67 +27,67 @@ export interface RangeHighlightingOptions {
dev_label: string;
}

/** Reactive outputs from `create_range_highlighting`. */
export interface RangeHighlighting {
readonly highlighting_disabled: boolean;
}

/**
* Wires up CSS Custom Highlight API range highlighting for a single element's
* text node, shared by `CodeHighlight` and `CodeTextarea`. Creates a
* `HighlightManager`, memoizes tokenization, applies/clears ranges in an effect,
* emits DEV warnings for unsupported languages, and tears down on destroy.
*
* Must be called during component initialization (it uses `$effect`/`onDestroy`).
* Must be constructed during component initialization (it uses `$effect`/`onDestroy`).
*/
export const create_range_highlighting = (options: RangeHighlightingOptions): RangeHighlighting => {
const manager = supports_css_highlight_api() ? new HighlightManager() : null;
const is_enabled = options.enabled ?? (() => true);
export class RangeHighlighting {
readonly #options: RangeHighlightingOptions;
readonly #manager: HighlightManager | null;
readonly #is_enabled: () => boolean;

readonly #language_supported: boolean = $derived.by(() => {
const lang = this.#options.lang();
return lang !== null && this.#options.syntax_styler().has_lang(lang);
});

const language_supported = $derived(
options.lang() !== null && options.syntax_styler().has_lang(options.lang()!)
readonly highlighting_disabled: boolean = $derived.by(
() => this.#options.lang() === null || !this.#language_supported
);
const highlighting_disabled = $derived(options.lang() === null || !language_supported);

// lex once per (text, lang) change -- memoized so unrelated reactivity doesn't
// trigger a full re-lex (`! safe bc of `highlighting_disabled`)
const range_lexed = $derived.by(() => {
if (!manager || !is_enabled() || highlighting_disabled) return null;
const text = options.text();
readonly #range_lexed = $derived.by(() => {
if (!this.#manager || !this.#is_enabled() || this.highlighting_disabled) return null;
const text = this.#options.text();
if (!text) return null;
return options.syntax_styler().lex(text, options.lang()!);
return this.#options.syntax_styler().lex(text, this.#options.lang()!);
});

if (manager) {
$effect(() => {
const element = options.element();
if (!element || !range_lexed) {
manager.clear_element_ranges();
return;
}
manager.highlight_from_lexed(element, range_lexed);
});
}
constructor(options: RangeHighlightingOptions) {
this.#options = options;
this.#is_enabled = options.enabled ?? (() => true);
const manager = (this.#manager = supports_css_highlight_api() ? new HighlightManager() : null);

if (DEV) {
$effect(() => {
// a lang was requested but we can't highlight it (unknown id)
if (options.lang() && highlighting_disabled) {
const langs = [...options.syntax_styler().langs.keys()].join(', ');
// eslint-disable-next-line no-console
console.error(
`[${options.dev_label}] Language "${options.lang()}" is not supported. ` +
`Highlighting disabled. Supported: ${langs}`
);
}
});
}

onDestroy(() => manager?.destroy());
if (manager) {
$effect(() => {
const element = options.element();
if (!element || !this.#range_lexed) {
manager.clear_element_ranges();
return;
}
manager.highlight_from_lexed(element, this.#range_lexed);
});
}

return {
get highlighting_disabled() {
return highlighting_disabled;
if (DEV) {
$effect(() => {
// a lang was requested but we can't highlight it (unknown id)
if (options.lang() && this.highlighting_disabled) {
const langs = [...options.syntax_styler().langs.keys()].join(', ');
// eslint-disable-next-line no-console
console.error(
`[${options.dev_label}] Language "${options.lang()}" is not supported. ` +
`Highlighting disabled. Supported: ${langs}`
);
}
});
}
};
};

onDestroy(() => manager?.destroy());
}
}