As it so happens I was working on this exact thing today.
With some pointers from Static highlighting using CM v6 - /next - discuss.CodeMirror, I implemented the following. Hope it helps as a starting point
import {Language} from "@codemirror/language";
import {Decoration} from "@codemirror/view";
import {RangeSetBuilder} from "@codemirror/rangeset";
import {defaultHighlightStyle, highlightTree} from "@codemirror/highlight";
import {Text} from "@codemirror/text";
export function getHighlights(textContent: string, language: Language): string {
const tree = language.parser.parse(textContent);
let markCache: { [cls: string]: Decoration } = Object.create(null)
let builder = new RangeSetBuilder<Decoration>()
highlightTree(tree, defaultHighlightStyle.match, (from, to, style) => {
builder.add(from, to, markCache[style] || (markCache[style] = Decoration.mark({class: style})))
});
let decorationRangeSet = builder.finish();
let html = '';
let text = Text.of(textContent.split("\n"));
for (let i = 1; i <= text.lines; i++) {
let line = text.line(i), pos = line.from, cursor = decorationRangeSet.iter(line.from), lineInnerHtml = '';
while (cursor.value && cursor.from < line.to) {
if (cursor.from > pos) {
lineInnerHtml += `${text.sliceString(pos, cursor.from)}`;
}
lineInnerHtml += `<span class="${cursor.value.spec.class}">${text.sliceString(cursor.from, Math.min(line.to, cursor.to))}</span>`
pos = cursor.to;
cursor.next();
}
if (pos < line.to) {
lineInnerHtml += `${text.sliceString(pos, line.to)}`;
}
html += `<div class="cm-line">${lineInnerHtml || '<br/>'}</div>`;
}
return `<pre>${html}</pre>`;
}