import hljs from "highlight.js/lib/core.js"; import bash from "highlight.js/lib/languages/bash.js"; import c from "highlight.js/lib/languages/c.js"; import cpp from "highlight.js/lib/languages/cpp.js"; import csharp from "highlight.js/lib/languages/csharp.js"; import dart from "highlight.js/lib/languages/dart.js"; import go from "highlight.js/lib/languages/go.js"; import groovy from "highlight.js/lib/languages/groovy.js"; import java from "highlight.js/lib/languages/java.js"; import javascript from "highlight.js/lib/languages/javascript.js"; import kotlin from "highlight.js/lib/languages/kotlin.js"; import lua from "highlight.js/lib/languages/lua.js"; import nix from "highlight.js/lib/languages/nix.js"; import perl from "highlight.js/lib/languages/perl.js"; import php from "highlight.js/lib/languages/php.js"; import python from "highlight.js/lib/languages/python.js"; import ruby from "highlight.js/lib/languages/ruby.js"; import rust from "highlight.js/lib/languages/rust.js"; import scala from "highlight.js/lib/languages/scala.js"; import swift from "highlight.js/lib/languages/swift.js"; import typescript from "highlight.js/lib/languages/typescript.js"; import { decodeHtmlEntityAt } from "./html.ts"; const eagerLanguages = { python, java, go, javascript, cpp, typescript, php, ruby, c, csharp, nix, bash, rust, scala, kotlin, swift, dart, groovy, perl, lua, }; for (const [name, language] of Object.entries(eagerLanguages)) { hljs.registerLanguage(name, language); } let allLanguagesPromise: Promise | undefined; export function loadAllHighlightLanguages(): Promise { if (!allLanguagesPromise) { allLanguagesPromise = new Promise((resolve) => { setImmediate(() => { void import("highlight.js/lib/index.js").then( () => resolve(), () => { // Eager languages and plaintext fallback remain available. resolve(); }, ); }); }); } return allLanguagesPromise; } export type HighlightFormatter = (text: string) => string; export type HighlightTheme = Partial>; export interface HighlightOptions { language?: string; ignoreIllegals?: boolean; languageSubset?: string[]; theme?: HighlightTheme; } const SPAN_CLOSE = ""; const HIGHLIGHT_CLASS_PREFIX = "hljs-"; function getScopeFromSpanTag(tag: string): string | undefined { const match = /\sclass\s*=\s*(?:"([^"]*)"|'([^']*)')/.exec(tag); const classValue = match?.[1] ?? match?.[2]; if (!classValue) { return undefined; } for (const className of classValue.split(/\s+/)) { if (className.startsWith(HIGHLIGHT_CLASS_PREFIX)) { return className.slice(HIGHLIGHT_CLASS_PREFIX.length); } } return undefined; } function getScopeFormatter(scope: string, theme: HighlightTheme): HighlightFormatter | undefined { const exact = theme[scope]; if (exact) { return exact; } const dotIndex = scope.indexOf("."); if (dotIndex !== -1) { const prefixFormatter = theme[scope.slice(0, dotIndex)]; if (prefixFormatter) { return prefixFormatter; } } const dashIndex = scope.indexOf("-"); if (dashIndex !== -1) { const prefixFormatter = theme[scope.slice(0, dashIndex)]; if (prefixFormatter) { return prefixFormatter; } } return undefined; } function getActiveFormatter(scopes: Array, theme: HighlightTheme): HighlightFormatter | undefined { for (let i = scopes.length - 1; i >= 0; i--) { const scope = scopes[i]; if (!scope) { continue; } const formatter = getScopeFormatter(scope, theme); if (formatter) { return formatter; } } return theme.default; } function isSpanOpenTagStart(html: string, index: number): boolean { if (!html.startsWith("" || nextChar === " " || nextChar === "\t" || nextChar === "\n" || nextChar === "\r"; } export function renderHighlightedHtml(html: string, theme: HighlightTheme = {}): string { let output = ""; let textBuffer = ""; const scopes: Array = []; const flushText = () => { if (!textBuffer) { return; } const formatter = getActiveFormatter(scopes, theme); output += formatter ? formatter(textBuffer) : textBuffer; textBuffer = ""; }; let index = 0; while (index < html.length) { if (isSpanOpenTagStart(html, index)) { const tagEndIndex = html.indexOf(">", index + 5); if (tagEndIndex !== -1) { flushText(); const tag = html.slice(index, tagEndIndex + 1); const scope = getScopeFromSpanTag(tag); scopes.push(scope); index = tagEndIndex + 1; continue; } } if (html.startsWith(SPAN_CLOSE, index)) { flushText(); if (scopes.length > 0) { scopes.pop(); } index += SPAN_CLOSE.length; continue; } if (html[index] === "&") { const decoded = decodeHtmlEntityAt(html, index); if (decoded) { textBuffer += decoded.text; index += decoded.length; continue; } } textBuffer += html[index]; index++; } flushText(); return output; } export function highlight(code: string, options: HighlightOptions = {}): string { const html = options.language ? hljs.highlight(code, { language: options.language, ignoreIllegals: options.ignoreIllegals, }).value : hljs.highlightAuto(code, options.languageSubset).value; return renderHighlightedHtml(html, options.theme); } export function supportsLanguage(name: string): boolean { return hljs.getLanguage(name) !== undefined; }