Skip to content

HighlightEngine

HighlightEngine is the low-level API that owns the hidden WebView and runs the highlight.js pipeline.

For most UI use cases, prefer SyntaxHighlightedCode inside HighlightThemeProvider. Use HighlightEngine directly when you need highlighting outside composables, such as in a ViewModel or service layer.

Full API in Dokka:

When to use it

  • You need highlighting in non-UI layers.
  • You want direct control over engine initialization and lifetime.
  • You need APIs like language lookup, auto-detection, or HTML-only output.

Lifecycle and cleanup

The engine holds a hidden WebView and implements Closeable. Always call close() (or destroy()) when done.

// highlight() is a suspend function - run this inside a coroutine, e.g. lifecycleScope.launch { }
val engine = HighlightEngine(context.applicationContext)
try {
    engine.highlight(code, "kotlin", theme).onSuccess { result -> /* use result.annotated */ }
} finally {
    engine.close()
}

close() is not permanent: calling the engine again afterwards re-creates the WebView (which then needs another close()). Treat a closed engine as finished and create a new one when needed.

In Compose, use rememberHighlightEngine() so cleanup is handled automatically.

Core operations

  • initialize() - optional warm-up to reduce first highlight latency.
  • highlight() - highlight one language and return Result<HighlightResult> containing the AnnotatedString and timings.
  • highlightBothThemes() - tokenize once and generate light and dark variants.
  • highlightToHtml() - get highlighted HTML directly.
  • supportedLanguages() and highlightJsVersion() - runtime capability introspection.
  • getLanguage() and highlightAuto() - language metadata and auto-detection workflows.

All suspend APIs return Result<T> and report failures through typed HighlightException subtypes:

  • WebViewInitFailed - WebView is disabled, unavailable, or failed to construct on the device.
  • JsExecutionFailed - Highlight.js execution returned null or raised a JavaScript error.
  • HtmlParseFailed - HTML token stream parser failed to convert Highlight.js output.
  • ThemeNotFound - CSS theme asset could not be loaded or produced no valid color rules.
  • RendererGone - Sandboxed WebView renderer process crashed or was terminated by the OS Low Memory Killer (LMK). The host application does not crash, and subsequent calls automatically recover.
  • EngineDestroyed - Engine was closed or destroyed while an operation was in flight.
  • Timeout - JavaScript call did not complete within the 5-second timeout window.

Usage in a ViewModel

import dev.hossain.highlight.engine.HighlightEngine
import dev.hossain.highlight.engine.HighlightTheme

class CodeViewModel(application: Application) : AndroidViewModel(application) {
    private val engine = HighlightEngine(application.applicationContext)

    init {
        viewModelScope.launch {
            engine.initialize()
        }
    }

    suspend fun highlight(code: String, language: String, theme: HighlightTheme): AnnotatedString? {
        return engine.highlight(code, language, theme)
            .getOrNull()
            ?.annotated
    }

    override fun onCleared() {
        engine.close()
    }
}

Usage in Compose (lower-level)

import dev.hossain.highlight.ui.rememberHighlightEngine
import dev.hossain.highlight.ui.rememberTomorrowLightTheme

@Composable
fun MyCodeBlock(code: String) {
    val engine = rememberHighlightEngine()
    val theme  = rememberTomorrowLightTheme()
    var highlighted by remember(code) { mutableStateOf<AnnotatedString?>(null) }

    LaunchedEffect(code) {
        engine.highlight(code, "kotlin", theme).onSuccess { highlighted = it.annotated }
    }
    Text(text = highlighted ?: AnnotatedString(code))
}

Highlight both themes for instant switching

highlightBothThemes() tokenizes once and applies both color maps, so theme switching does not trigger another JS tokenization pass.

import dev.hossain.highlight.engine.HighlightTheme

// Outside Compose: use the non-@Composable factories on HighlightTheme.
// highlightBothThemes is a suspend fun, so call it from a coroutine.
viewModelScope.launch {
    engine.highlightBothThemes(
        code       = sourceCode,
        language   = "typescript",
        lightTheme = HighlightTheme.tomorrow(),
        darkTheme  = HighlightTheme.tomorrowNight(),
    ).onSuccess { result ->
        val display = if (isDark) result.dark else result.light
    }
}

Note

Inside @Composable functions, prefer rememberTomorrowLightTheme() / rememberTomorrowNightTheme() so the theme is remembered across recompositions. The HighlightTheme.tomorrow() and HighlightTheme.tomorrowNight() factories above are the non-composable equivalents - safe to call from a ViewModel or repository.

Language lookup and auto-detection

Use getLanguage() to validate names and inspect aliases:

engine.getLanguage("ts").onSuccess { info ->
    if (info != null) {
        // info.name    = "TypeScript"
        // info.aliases = [ts, tsx, mts, cts]
    }
}

Use highlightAuto() when language is unknown:

var detected  by remember { mutableStateOf("") }
var annotated by remember(code) { mutableStateOf(AnnotatedString(code)) }

LaunchedEffect(code, theme) {
    engine.highlightAuto(code, theme).onSuccess { result ->
        detected  = result.detectedLanguage // e.g. "python", or "" if undetected
        annotated = result.annotated
    }
}
Text(text = annotated)

Common pitfalls

  • Forgetting applicationContext can leak short-lived contexts.
  • Forgetting close() in non-Compose code can keep WebView resources alive.
  • Treating Result.failure as exceptional flow from the API point of view; this is expected error signaling for this library.