8644db2c2a
Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com>
156 lines
5.2 KiB
TypeScript
156 lines
5.2 KiB
TypeScript
/// <reference lib="webworker" />
|
|
|
|
import { ShikiStreamTokenizer } from "@shikijs/stream"
|
|
import { createMarkdownParser } from "@opencode-ai/ui/context/marked-parser"
|
|
import { OpenCodeTheme } from "@opencode-ai/ui/context/marked-theme"
|
|
import {
|
|
bundledLanguages,
|
|
createHighlighter,
|
|
getTokenStyleObject,
|
|
stringifyTokenStyle,
|
|
type BundledLanguage,
|
|
type ThemedToken,
|
|
} from "shiki"
|
|
import type { MarkdownToken, MarkdownWorkerRequest, MarkdownWorkerResponse } from "./markdown-worker-protocol"
|
|
import { createLatestWorkerQueue } from "./markdown-worker-queue"
|
|
import { project, type Projection } from "./markdown-stream"
|
|
|
|
type Stream = {
|
|
language: string
|
|
source: string
|
|
tokenizer: ShikiStreamTokenizer
|
|
}
|
|
|
|
const streams = new Map<string, Stream>()
|
|
const projections = new Map<string, Projection>()
|
|
let highlighter: ReturnType<typeof createHighlighter> | undefined
|
|
const highlightQueue = createLatestWorkerQueue<Extract<MarkdownWorkerRequest, { type: "highlight" }>>({
|
|
run: highlight,
|
|
supersede: (request) => post({ type: "superseded", id: request.id, key: request.key }),
|
|
dispose: (key) => void streams.delete(key),
|
|
})
|
|
const projectQueue = createLatestWorkerQueue<Extract<MarkdownWorkerRequest, { type: "project" }>>({
|
|
run: runProject,
|
|
supersede: (request) => post({ type: "superseded", id: request.id, key: request.key }),
|
|
dispose: (key) => void projections.delete(key),
|
|
})
|
|
const parser = createMarkdownParser(async (code, language) => {
|
|
const instance = await getHighlighter()
|
|
const name = language in bundledLanguages ? language : "text"
|
|
if (!instance.getLoadedLanguages().includes(name))
|
|
await instance.loadLanguage(bundledLanguages[name as BundledLanguage])
|
|
return instance
|
|
.codeToHtml(code, { lang: name as BundledLanguage, theme: "OpenCode", tabindex: false })
|
|
.replace("<code>", `<code class="language-${name}">`)
|
|
})
|
|
|
|
self.onmessage = (event: MessageEvent<MarkdownWorkerRequest>) => {
|
|
if (event.data.type === "dispose") {
|
|
highlightQueue.dispose(event.data.key)
|
|
projectQueue.dispose(event.data.key)
|
|
return
|
|
}
|
|
if (event.data.type === "parse") {
|
|
void parse(event.data)
|
|
return
|
|
}
|
|
if (event.data.type === "project") {
|
|
projectQueue.highlight(event.data)
|
|
return
|
|
}
|
|
|
|
highlightQueue.highlight(event.data)
|
|
}
|
|
|
|
async function parse(request: Extract<MarkdownWorkerRequest, { type: "parse" }>) {
|
|
try {
|
|
post({ type: "parse", id: request.id, html: await parser.parse(request.text) })
|
|
} catch (error) {
|
|
post({ type: "error", id: request.id, message: error instanceof Error ? error.message : String(error) })
|
|
}
|
|
}
|
|
|
|
async function runProject(request: Extract<MarkdownWorkerRequest, { type: "project" }>) {
|
|
try {
|
|
const projection = project(projections.get(request.key), request.text, request.live)
|
|
projections.set(request.key, projection)
|
|
post({ type: "project", id: request.id, key: request.key, projection })
|
|
} catch (error) {
|
|
post({
|
|
type: "error",
|
|
id: request.id,
|
|
key: request.key,
|
|
message: error instanceof Error ? error.message : String(error),
|
|
})
|
|
}
|
|
}
|
|
|
|
async function highlight(request: Extract<MarkdownWorkerRequest, { type: "highlight" }>) {
|
|
try {
|
|
const instance = await getHighlighter()
|
|
const language = request.language in bundledLanguages ? request.language : "text"
|
|
if (!instance.getLoadedLanguages().includes(language))
|
|
await instance.loadLanguage(bundledLanguages[language as BundledLanguage])
|
|
|
|
if (request.complete) {
|
|
const result = instance.codeToTokens(request.text, { lang: language as BundledLanguage, theme: "OpenCode" })
|
|
streams.delete(request.key)
|
|
post({
|
|
type: "highlight",
|
|
id: request.id,
|
|
key: request.key,
|
|
language,
|
|
reset: true,
|
|
stable: result.tokens
|
|
.flatMap((line, index) =>
|
|
index === result.tokens.length - 1 ? line : [...line, { content: "\n", offset: 0 }],
|
|
)
|
|
.map(token),
|
|
unstable: [],
|
|
})
|
|
return
|
|
}
|
|
|
|
const previous = streams.get(request.key)
|
|
const reset = !previous || previous.language !== language || !request.text.startsWith(previous.source)
|
|
const stream = reset
|
|
? {
|
|
language,
|
|
source: "",
|
|
tokenizer: new ShikiStreamTokenizer({ highlighter: instance, lang: language, theme: "OpenCode" }),
|
|
}
|
|
: previous
|
|
const result = await stream.tokenizer.enqueue(request.text.slice(stream.source.length))
|
|
stream.source = request.text
|
|
streams.set(request.key, stream)
|
|
post({
|
|
type: "highlight",
|
|
id: request.id,
|
|
key: request.key,
|
|
language,
|
|
reset,
|
|
stable: result.stable.filter((token) => token.content.length > 0).map(token),
|
|
unstable: result.unstable.filter((token) => token.content.length > 0).map(token),
|
|
})
|
|
} catch (error) {
|
|
post({
|
|
type: "error",
|
|
id: request.id,
|
|
key: request.key,
|
|
message: error instanceof Error ? error.message : String(error),
|
|
})
|
|
}
|
|
}
|
|
|
|
function getHighlighter() {
|
|
return (highlighter ??= createHighlighter({ themes: [OpenCodeTheme], langs: [] }))
|
|
}
|
|
|
|
function post(response: MarkdownWorkerResponse) {
|
|
self.postMessage(response)
|
|
}
|
|
|
|
function token(value: ThemedToken): MarkdownToken {
|
|
return [value.content, stringifyTokenStyle(value.htmlStyle ?? getTokenStyleObject(value))]
|
|
}
|