UNPKG

@bobril/highlighter

Version:

Syntax highlighter component for Bobril

190 lines (160 loc) 5.46 kB
import high from "highlight.js/lib/core"; import type { Emitter, LanguageFn } from "highlight.js"; export type StyledText = { text: string; style: undefined | string | string[] }; export type StyledLinesAndSpans = StyledText[][]; export interface HighlightResult { language: string | undefined; relevance: number; lines: StyledLinesAndSpans; } export function highlight(value: string, language?: string): HighlightResult { if (language != undefined && !high.getLanguage(language)) { language = undefined; } high.configure({ __emitter: TreeEmitter, classPrefix: "" }); if (language == undefined) { let list = high.listLanguages(); let result: TreeRoot = { type: "root", data: { language: undefined, relevance: 0 }, children: [], }; for (var i = 0; i < list.length; i++) { let root = highlightCore(value, list[i]); if (root.data.relevance > result.data.relevance) result = root; } return flattenToLines(result); } else { return flattenToLines(highlightCore(value, language)); } } function highlightCore(value: string, language: string) { let result = high.highlight(value, { language, ignoreIllegals: true }); let root = (result._emitter as TreeEmitter).root; root.data.language = result.language; root.data.relevance = result.relevance; return root; } function flattenToLines(root: TreeRoot): HighlightResult { var res = [[]]; flattenTree(root, [], res); return { language: root.data.language, relevance: root.data.relevance, lines: res }; } function convertToSimpleRepresentation(classStack: string[]): undefined | string | string[] { if (classStack.length == 0) return undefined; if (classStack.length == 1) return classStack[0]; return classStack.slice(0); } function flattenTree(tree: TreeRoot | TreeNode, classStack: string[], res: StyledLinesAndSpans) { if (tree.type === "text") { var lines = tree.value.split("\n"); for (var i = 0; i < lines.length; i++) { if (i > 0) { res.push([]); } if (lines[i].length > 0) res[res.length - 1].push({ text: lines[i], style: convertToSimpleRepresentation(classStack) }); } } else if (tree.type === "span") { let backupLen = classStack.length; let cn = tree.className; for (var i = 0; i < cn.length; i++) { var c = cn[i]; if (classStack.indexOf(c) < 0) classStack.push(c); } let ch = tree.children; for (var i = 0; i < ch.length; i++) { flattenTree(ch[i], classStack, res); } classStack.length = backupLen; } else { let ch = tree.children; for (var i = 0; i < ch.length; i++) { flattenTree(ch[i], classStack, res); } } } export function registerLanguage(language: string, syntax: LanguageFn): void { high.registerLanguage(language, syntax); } export function listLanguages(): string[] { return high.listLanguages(); } export function registerAliases(language: string, aliases: string | string[]) { high.registerAliases(aliases, { languageName: language }); } type TreeNode = TreeSpan | TreeText; type TreeText = { type: "text"; value: string; }; type TreeSpan = { type: "span"; className: string[]; children: TreeNode[]; }; type TreeRoot = { type: "root"; data: { language: string | undefined; relevance: number }; children: TreeNode[]; }; class TreeEmitter implements Emitter { root: TreeRoot; stack: [TreeRoot, ...TreeSpan[]]; constructor() { this.root = { type: "root", data: { language: undefined, relevance: 0 }, children: [], }; this.stack = [this.root]; } addText(value: string) { if (value === "") return; let current = this.stack[this.stack.length - 1]; let tail = current.children[current.children.length - 1]; if (tail && tail.type === "text") { tail.value += value; } else { current.children.push({ type: "text", value }); } } __addSublanguage(other: Emitter, name: string) { var current = this.stack[this.stack.length - 1]; var results = (other as unknown as TreeEmitter).root.children; if (name) { current.children.push({ type: "span", className: [name], children: results, }); } else { current.children.push(...results); } } openNode(name: string) { var className = name.split("."); var current = this.stack[this.stack.length - 1]; var child: TreeSpan = { type: "span", className, children: [], }; current.children.push(child); this.stack.push(child); } closeNode() { this.stack.pop(); } closeAllNodes() {} startScope(name: string): void { this.openNode(name); } endScope(): void { this.closeNode(); } finalize() {} toHTML() { return ""; } }