HighlightEngine

Core engine that manages the hidden WebView and executes Highlight.js highlighting.

Thread safety: WebView is always accessed on the Main thread. Concurrent highlight calls are serialized via mutex.

How highlighting works

Highlight.js runs inside a hidden off-screen WebView. When you call highlight, the engine:

  1. Tokenizes - calls highlightCode(code, lang) via evaluateJavascript(), which returns an HTML string where each token is wrapped in a <span class="hljs-keyword"> (or similar). highlight.js only assigns class names - it does not apply any colors itself.

  2. Resolves colors - HighlightTheme lazily parses its CSS file via ThemeParser, which translates CSS rules like .hljs-keyword { color: #7928a1 } into a map of "hljs-keyword" -> SpanStyle(color=Color(0xFF7928a1)). This is the bridge between CSS-based theming and Compose's styling model.

  3. Builds AnnotatedString - HtmlToAnnotatedString walks the HTML token tree with jsoup, looks up each span's class name in the theme's color map, and applies the matching SpanStyle. The result is a fully colored AnnotatedString ready for Compose Text.

Lifecycle

The engine holds a hidden WebView resource and implements Closeable for safe resource management. Always call close (or destroy) when the engine is no longer needed.

When used inside a Composable, use rememberHighlightEngine() which calls destroy automatically via DisposableEffect. For manual usage (e.g. in a ViewModel), call close or destroy in onCleared(). Since highlighting APIs are suspend, prefer coroutine-friendly try/finally cleanup for scoped usage:

val engine = HighlightEngine(context.applicationContext)
try {
val result = engine.highlight(code, "kotlin", theme)
} finally {
engine.close()
}

WebView requirement

This library requires WebView to be installed and enabled on the device. When WebView is unavailable (e.g. Android Go devices, during a system WebView update, or when disabled by MDM policy), all highlight methods return Result.failure wrapping HighlightException.WebViewInitFailed. Check for this specific exception type to distinguish WebView availability issues from JavaScript errors.

Composable usage (lower-level)

For most cases, prefer SyntaxHighlightedCode inside a HighlightThemeProvider - it handles the engine lifecycle automatically. Use rememberHighlightEngine directly only when you need lower-level control, such as calling highlightBothThemes or building a custom UI.

@Composable
fun MyCodeBlock(code: String) {
val engine = rememberHighlightEngine()
val theme = rememberTomorrowTheme()
var highlighted by remember(code) { mutableStateOf<AnnotatedString?>(null) }
LaunchedEffect(code) {
engine.highlight(code, "kotlin", theme).onSuccess { highlighted = it.annotated }
}
Text(text = highlighted ?: AnnotatedString(code))
}

Manual usage (e.g. ViewModel or background work)

val engine = HighlightEngine(context.applicationContext)

// Suspend calls must run inside a coroutine (e.g. viewModelScope.launch).
viewModelScope.launch {
// Optional: warm up before first use to reduce first-call latency.
engine.initialize().onFailure { /* handle WebViewInitFailed if needed */}

val result = engine.highlight(
code = "val x = 42",
language = "kotlin",
theme = HighlightTheme.atomOneDark(),
)
result.onSuccess { highlighted ->
display(highlighted.annotated) // AnnotatedString
log("spans: ${highlighted.spanCount}") // 0 = unsupported language
log("time: ${highlighted.durationMs} ms")
}
}

// Release resources when done (e.g. in ViewModel.onCleared())
engine.destroy() // or engine.close()

Highlight once, render in two themes

// Inside a coroutine (e.g. viewModelScope.launch or LaunchedEffect):
engine.highlightBothThemes(
code = sourceCode,
language = "typescript",
lightTheme = HighlightTheme.tomorrow(),
darkTheme = HighlightTheme.tomorrowNight(),
).onSuccess { result ->
val display = if (isDark) result.dark else result.light
}

Constructors

Link copied to clipboard
constructor(context: Context)

Properties

Link copied to clipboard
val isInitialized: StateFlow<Boolean>

true once initialize has completed successfully (or the first highlight / highlightToHtml call has finished warming up the WebView).

Functions

Link copied to clipboard
open override fun close()

Releases the WebView resources by delegating to destroy. Implements Closeable so this engine participates in IDE resource-leak inspections and supports explicit cleanup through close. Safe to call multiple times.

Link copied to clipboard
fun destroy()

Releases the WebView resources and clears all internal caches (languages, version).

Link copied to clipboard
suspend fun getLanguage(nameOrAlias: String): Result<HighlightLanguageInfo?>

Returns metadata for a Highlight.js language name or alias.

Link copied to clipboard
suspend fun highlight(code: String, language: String, theme: HighlightTheme): Result<HighlightResult>

Full pipeline: tokenise → apply theme → convert to HighlightResult.

Link copied to clipboard

Highlights code with Highlight.js automatic language detection.

Link copied to clipboard
suspend fun highlightBothThemes(code: String, language: String, lightTheme: HighlightTheme, darkTheme: HighlightTheme): Result<ThemedHighlightResult>

Highlights code once and produces a ThemedHighlightResult with both a light and a dark androidx.compose.ui.text.AnnotatedString.

Link copied to clipboard

Returns the version string of the bundled Highlight.js library (e.g. "11.11.1").

Link copied to clipboard
suspend fun highlightToHtml(code: String, language: String): Result<HtmlHighlightResult>

Highlights code and returns raw HTML with <span class="hljs-*"> tokens, together with the time taken for the JavaScript round-trip.

Link copied to clipboard
suspend fun initialize(): Result<Unit>

Warms up the hidden WebView and loads bridge.html.

Link copied to clipboard

Returns the list of language identifiers supported by the bundled Highlight.js.