syntastica 0.6.1

Modern and easy syntax highlighting using tree-sitter
Documentation
<script lang="ts">
    import syntastica, { BUILTIN_THEMES, Theme, type BuiltinTheme } from '@syntastica/core'
    import wasmUrl from '@syntastica/core/wasm?url'
    const langUrls = import.meta.glob('/node_modules/@syntastica/lang-*/*.wasm', {
        query: 'url',
        import: 'default',
    })
    const LANGUAGES = Object.keys(langUrls).map(path => path.split('/').at(-1).split('.wasm')[0])

    let loading = true
    const initPromise = syntastica.init({
        // this allows us to override the url to the Wasm file,
        // which is required for the vite dev server to load this correctly
        locateFile: () => wasmUrl,
    })
    initPromise.then(() => (loading = false))

    let code = 'fn main() {\n    println!("Hello, World!");\n}'
    let language: string = 'rust'
    let theme: BuiltinTheme = 'one::dark'

    let highlightedCode = ''
    let themeBg = 'transparent'
    const loadedLanguages = []

    $: loadLanguage(language)
    $: code, theme, updateHighlights()
    $: theme, updateThemeBg()

    async function updateHighlights() {
        await initPromise
        if (!loadedLanguages.includes(language)) return
        // in a real application, this should probably be run in a web worker to not freeze
        // the entire UI while highlighting
        highlightedCode = syntastica.highlight(code, language, theme)
    }

    async function updateThemeBg() {
        await initPromise
        const bg = Theme.fromBuiltin(theme).bg
        themeBg = bg === null ? 'transparent' : `rgb(${bg.red}, ${bg.green}, ${bg.blue})`
    }

    async function loadLanguage(lang: string) {
        if (!loadedLanguages.includes(lang)) {
            await initPromise
            loading = true
            await syntastica.loadLanguage(
                (await langUrls[`/node_modules/@syntastica/lang-${lang}/${lang}.wasm`]()) as string,
            )
            loading = false
            loadedLanguages.push(lang)
        }
        updateHighlights()
    }

    // event callback to slightly improve the default textarea editing experience
    function editorKeybinds(
        event: KeyboardEvent & { currentTarget: EventTarget & HTMLTextAreaElement },
    ) {
        const start = event.currentTarget.selectionStart
        const end = event.currentTarget.selectionEnd

        function insert(str: string, caretOffset: number = str.length) {
            // prevent default event handling
            event.preventDefault()

            // insert text
            event.currentTarget.value =
                event.currentTarget.value.substring(0, start) +
                str +
                event.currentTarget.value.substring(end)

            // put caret at the correct position
            event.currentTarget.selectionStart = event.currentTarget.selectionEnd =
                start + caretOffset

            // trigger svelte update
            code = event.currentTarget.value
        }

        if (event.key === 'Tab') {
            insert('    ') // tab inserts four spaces
        } else if (event.key === '(') {
            insert('()', 1) // bracket pairs get auto-closed
        } else if (event.key === '[') {
            insert('[]', 1)
        } else if (event.key === '{') {
            insert('{}', 1)
        } else if (
            // closing a bracket pair only moves the caret if it's already closed
            [')', ']', '}'].includes(event.key) &&
            start === end &&
            event.currentTarget.value[start] === event.key
        ) {
            event.preventDefault()
            event.currentTarget.selectionStart = event.currentTarget.selectionEnd = start + 1
        }
    }
</script>

<main>
    <div id="lang-select-container">
        <label for="language">Language:</label>
        <select name="language" id="lang-select" bind:value={language}>
            {#each LANGUAGES as lang}
                <option value={lang}>{lang}</option>
            {/each}
        </select>
    </div>
    <div id="theme-select-container">
        <label for="theme">Theme:</label>
        <select name="theme" id="theme-select" bind:value={theme}>
            {#each BUILTIN_THEMES as theme}
                <option value={theme}>{theme}</option>
            {/each}
        </select>
    </div>

    <textarea id="editor" bind:value={code} on:keydown={editorKeybinds} />
    <div id="preview" style:background-color={themeBg}>
        {#if !loading}
            {@html highlightedCode}
        {:else}
            loading...
        {/if}
    </div>
</main>

<style>
    main {
        display: grid;
        grid-template-columns: 1fr 1fr;
        grid-template-rows: auto 1fr;
        min-height: min(40rem, calc(100vh - 2rem));
        gap: 1rem;
    }

    @media screen and (max-width: 40rem) {
        main {
            grid-template-columns: 1fr;
            grid-template-rows: auto auto 1fr;
        }
    }

    #editor {
        resize: none;
        white-space: nowrap;
    }

    #preview {
        font-family: monospace;
        text-align: left;
        overflow-x: auto;
        white-space: nowrap;
        border-radius: 4px;
        padding: 4px;
    }
</style>