slige/web/public/src/index.ts
2024-12-16 22:07:16 +01:00

186 lines
5.6 KiB
TypeScript

import { CodeCovRender, loadCodeCoverage } from "./code_coverage.ts";
import * as data from "./data.ts";
import { loadFlameGraph } from "./flamegraph.ts";
function countLines(code: string) {
let lines = 0;
for (const char of code) {
if (char === "\n") lines += 1;
}
return lines;
}
function createLineElement(code: string): HTMLPreElement {
const lines = countLines(code) + 1;
const maxLineWidth = lines.toString().length;
let text = "";
for (let i = 1; i < lines; ++i) {
const node = i.toString().padStart(maxLineWidth);
text += node;
text += "\n";
}
const lineElement = document.createElement("pre");
lineElement.classList.add("code-lines");
lineElement.innerText = text;
return lineElement;
}
async function checkStatus(): Promise<"running" | "done"> {
const status = await data.status();
if (status.running) {
return "running";
}
const statusHtml = document.querySelector<HTMLSpanElement>(
"#status",
)!;
statusHtml.innerText = "Done";
document.body.classList.remove("status-waiting");
document.body.classList.add("status-done");
return "done";
}
function sourceCode(view: Element, codeData: string) {
const outerContainer = document.createElement("div");
outerContainer.classList.add("code-container");
const innerContainer = document.createElement("div");
innerContainer.classList.add("code-container-inner");
const lines = createLineElement(codeData);
const code = document.createElement("pre");
code.classList.add("code-source");
code.innerText = codeData;
innerContainer.append(lines, code);
outerContainer.append(innerContainer);
view.replaceChildren(outerContainer);
}
function createRadio(
id: string,
content: string,
checked: boolean,
): [HTMLDivElement, HTMLInputElement] {
const label = document.createElement("label");
label.htmlFor = id;
label.innerText = content;
const input = document.createElement("input");
input.id = id;
input.name = "coverage-radio";
input.type = "radio";
input.hidden = true;
input.checked = checked;
const container = document.createElement("div");
container.classList.add("coverage-radio-group");
container.append(input, label);
return [container, input];
}
async function codeCoverage(view: Element, codeData: string) {
const codeCoverageData = await data.codeCoverageData();
const outerContainer = document.createElement("div");
outerContainer.classList.add("code-container");
outerContainer.classList.add("code-coverage");
const innerContainer = document.createElement("div");
innerContainer.classList.add("code-container-inner");
const [perfGroup, perfInput] = createRadio(
"performance-coverage",
"Performance view",
true,
);
const [testGroup, testInput] = createRadio(
"test-coverage",
"Test view",
false,
);
function load(mode: CodeCovRender, tooltip: HTMLDivElement) {
const lines = createLineElement(codeData);
const code = loadCodeCoverage(
codeData,
codeCoverageData,
tooltip,
mode,
);
innerContainer.replaceChildren(lines, code);
}
const radios = document.createElement("div");
radios.append(perfGroup, testGroup);
radios.classList.add("coverage-radio");
const tooltip = document.createElement("div");
tooltip.id = "covers-tooltip";
tooltip.hidden = true;
outerContainer.append(innerContainer);
view.replaceChildren(outerContainer, tooltip, radios);
if (perfInput.checked) {
load("performance", tooltip);
} else if (testInput.checked) {
load("test-coverage", tooltip);
}
perfInput.addEventListener("input", () => load("performance", tooltip));
testInput.addEventListener("input", () => load("test-coverage", tooltip));
}
async function main() {
type RenderFns = {
"source-code": () => void;
"code-coverage": () => void;
"flame-graph": () => void;
};
const codeData = await data.codeData();
const view = document.querySelector("#view")!;
const renderFunctions: RenderFns = {
"source-code": () => sourceCode(view, codeData),
"code-coverage": async () => await codeCoverage(view, codeData),
"flame-graph": async () => {
const flameGraphData = await data.flameGraphData();
const flameGraphFnNames = await data.flameGraphFnNames();
const container = document.createElement("div");
container.classList.add("flame-graph");
container.id = "flame-graph";
const view = document.querySelector("#view")!;
view.replaceChildren(container);
loadFlameGraph(flameGraphData, flameGraphFnNames, container);
},
};
const viewRadios: NodeListOf<HTMLInputElement> = document.querySelectorAll(
'input[name="views"]',
);
for (const input of viewRadios) {
input.addEventListener("input", (ev) => {
const target = ev.target as HTMLInputElement;
const value = target.value as keyof RenderFns;
renderFunctions[value]();
});
if (input.checked) {
const value = input.value as keyof RenderFns;
renderFunctions[value]();
}
}
checkStatus().then((status) => {
if (status == "done") {
return;
}
const interval = setInterval(async () => {
const status = await checkStatus();
if (status == "done") {
clearInterval(interval);
}
}, 500);
});
}
main();