treehouse/static/js/components/literate-programming/highlight.js

106 lines
3.5 KiB
JavaScript
Raw Permalink Normal View History

2024-02-17 18:01:17 +01:00
// This tokenizer is highly inspired by the one found in rxi's lite.
// I highly recommend checking it out!
// https://github.com/rxi/lite/blob/master/data/core/tokenizer.lua
2024-03-10 23:23:50 +01:00
// There's also a mirror of it in the static generator, to enable highlighting of code blocks which
// are *not* JavaScript-powered.
2024-02-17 18:01:17 +01:00
export function compileSyntax(def) {
for (let pattern of def.patterns) {
let flags = "dy";
if (pattern.flags != null) {
if ("dotMatchesNewline" in pattern.flags) {
flags += "s";
}
}
pattern.regex = new RegExp(pattern.regex, flags);
2024-02-17 18:01:17 +01:00
}
def.keywords = new Map(Object.entries(def.keywords));
2024-02-17 18:01:17 +01:00
return def;
}
function pushToken(tokens, kind, string) {
let previousToken = tokens[tokens.length - 1];
if (previousToken != null && previousToken.kind == kind) {
previousToken.string += string;
} else {
tokens.push({ kind, string });
}
}
function tokenize(text, syntax) {
let tokens = [];
let i = 0;
while (i < text.length) {
let hadMatch = false;
for (let pattern of syntax.patterns) {
let match;
pattern.regex.lastIndex = i;
if ((match = pattern.regex.exec(text)) != null) {
if (typeof pattern.is == "object") {
let lastMatchEnd = i;
for (let i = 1; i < match.indices.length; ++i) {
let [start, end] = match.indices[i];
if (match.indices[i] != null) {
pushToken(
tokens,
pattern.is.default,
text.substring(lastMatchEnd, start),
);
pushToken(
tokens,
pattern.is.captures[i - 1],
text.substring(start, end),
);
}
2024-07-25 23:36:50 +02:00
lastMatchEnd = end;
}
2024-07-25 23:36:50 +02:00
pushToken(
tokens,
pattern.is.default,
text.substring(lastMatchEnd, match.indices[0][1]),
);
} else {
pushToken(tokens, pattern.is, match[0]);
}
2024-02-17 18:01:17 +01:00
i = pattern.regex.lastIndex;
hadMatch = true;
break;
}
}
// Base case: no pattern matched, just add the current character to the output.
if (!hadMatch) {
pushToken(tokens, "default", text.substring(i, i + 1));
++i;
}
}
for (let token of tokens) {
let replacement = syntax.keywords.get(token.string);
if (replacement != null) {
if (replacement.onlyReplaces == null || token.kind == replacement.onlyReplaces) {
token.kind = replacement.into;
}
}
}
return tokens;
}
2024-02-17 21:03:45 +01:00
export function highlight(element, syntax, customize = null) {
2024-02-17 18:01:17 +01:00
let tokens = tokenize(element.textContent, syntax);
element.textContent = "";
element.classList.add("th-syntax-highlighting");
for (let token of tokens) {
let span = document.createElement("span");
span.textContent = token.string;
span.classList.add(token.kind);
2024-02-17 21:03:45 +01:00
if (customize != null) {
customize(token, span);
}
2024-02-17 18:01:17 +01:00
element.appendChild(span);
}
}