luckymeowth/themes/lucky/scripts/katexRender.ts

140 lines
3.1 KiB
TypeScript
Raw Permalink Normal View History

2024-11-21 20:01:52 -03:00
import {
DOMParser,
Element,
NodeType,
Text,
} from "https://deno.land/x/deno_dom@v0.1.38/deno-dom-wasm.ts";
import { assert } from "https://deno.land/std/testing/asserts.ts";
import katex from "https://cdn.jsdelivr.net/npm/katex@0.16.8/dist/katex.mjs";
// this is similar to delimiter object in auto-render.js
interface IKaTeXAutoRenderSurround {
display: boolean;
test: RegExp;
}
// options to be given to renderMathInElement
interface IKaTeXAutoRenderOption {
// delimiters
surronds: IKaTeXAutoRenderSurround[];
// tag names which are excluded
ignoreTags: string[];
}
// the default option is same with auto-render.js
const DefaultOptions: IKaTeXAutoRenderOption = {
surronds: [
{ test: /\$\$(.+?)\$\$/, display: true },
{ test: /\$(.+?)\$/, display: false },
],
ignoreTags: [
"script",
"noscript",
"style",
"textarea",
"pre",
"code",
],
};
function renderText(
element: Text,
options: IKaTeXAutoRenderOption,
): string | null {
let restText = element.textContent;
const partialResults = new Array<string>();
let anyMatch = false;
while (true) {
let match: RegExpExecArray | null = null;
let surround: IKaTeXAutoRenderSurround | null = null;
for (const sr of options.surronds) {
match = sr.test.exec(restText);
surround = sr;
if (match) break;
}
if (!match) {
break;
} else {
anyMatch = true;
}
// Insert text before the match
partialResults.push(restText.substr(0, match.index));
// Renders and inserts the KaTeX text
assert(surround !== null);
const renderOption = Object.assign({}, options, {
displayMode: surround.display,
});
partialResults.push(
katex.renderToString(match[1], renderOption),
);
restText = restText.substr(match.index + match[0].length);
}
if (!anyMatch) {
return null;
}
// Inserts the leftover text
partialResults.push(restText);
return partialResults.join("");
}
function renderElement(
element: Element,
options: IKaTeXAutoRenderOption,
): void {
for (const child of element.childNodes) {
switch (child.nodeType) {
case NodeType.ELEMENT_NODE:
{
if (
!options.ignoreTags.includes(
child.nodeName.toLowerCase(),
)
) {
renderElement(child as Element, options);
}
break;
}
case NodeType.TEXT_NODE:
{
const newText = renderText(child as Text, options);
if (newText != null) {
const document = child.ownerDocument;
assert(document !== null);
const newChild = document.createElement("");
newChild.innerHTML = newText;
child.replaceWith(...newChild.childNodes);
}
break;
}
default:
continue;
}
}
}
async function renderMathInFile(filename: string): Promise<void> {
const decoder = new TextDecoder();
const document = new DOMParser().parseFromString(
decoder.decode(await Deno.readFile(filename)),
"text/html",
)!;
renderElement(document.body, DefaultOptions);
assert(document.documentElement !== null);
await Deno.writeTextFile(filename, '<!DOCTYPE html>' + document.documentElement.outerHTML);
}
const filenames = Deno.args;
for (const filename of filenames) {
await renderMathInFile(filename);
}