130 lines
4.3 KiB
JavaScript
130 lines
4.3 KiB
JavaScript
import { createHash } from "node:crypto";
|
|
import katex from "katex";
|
|
|
|
export const pluginId = "tex.math";
|
|
export const operations = ["describe", "validate", "project", "lower", "measure", "apply"];
|
|
|
|
const renderCache = new Map();
|
|
|
|
export function dispatch(request) {
|
|
if (request?.schema !== "gamecult.eve.plugin_abi.request.v1") throw new Error("Unsupported request schema.");
|
|
if (request.pluginId !== pluginId) throw new Error(`Expected plugin ${pluginId}.`);
|
|
if (!operations.includes(request.operation)) throw new Error(`Unsupported operation ${request.operation}.`);
|
|
|
|
switch (request.operation) {
|
|
case "describe": return describe();
|
|
case "validate": return validate(request.input || {});
|
|
case "project": return project(request.input || {});
|
|
case "lower": return lower(request.input || {});
|
|
case "measure": return measure(request.input || {});
|
|
case "apply": return apply(request.input || {});
|
|
}
|
|
}
|
|
|
|
export function describe() {
|
|
return {
|
|
pluginId,
|
|
componentKinds: ["embed.tex"],
|
|
commands: [],
|
|
capabilities: ["embed.tex", "tex.inline", "tex.block"],
|
|
operations,
|
|
semanticEngine: "katex",
|
|
stateAuthority: "provider-owns-source-state",
|
|
};
|
|
}
|
|
|
|
export function validate(input) {
|
|
const requested = Array.isArray(input.componentKinds) ? input.componentKinds : [];
|
|
let validSource = true;
|
|
let diagnostic = null;
|
|
if (typeof input.source === "string") {
|
|
try {
|
|
render(input);
|
|
} catch (error) {
|
|
validSource = false;
|
|
diagnostic = error.message;
|
|
}
|
|
}
|
|
return {
|
|
acceptedComponentKinds: requested.filter((kind) => kind === "embed.tex"),
|
|
rejectedComponentKinds: requested.filter((kind) => kind !== "embed.tex"),
|
|
validSource,
|
|
...(diagnostic ? { diagnostic } : {}),
|
|
requiredCapabilities: ["embed.tex"],
|
|
};
|
|
}
|
|
|
|
export function project(input) {
|
|
return {
|
|
schema: "tex.math.render_request.v1",
|
|
projectionKind: "render-request",
|
|
ownedComponentKinds: ["embed.tex"],
|
|
outputSchemas: ["tex.math.render_request.v1", "tex.math.render_result.v1"],
|
|
documentId: requiredString(input.documentId, "documentId"),
|
|
source: requiredString(input.source, "source"),
|
|
displayMode: Boolean(input.displayMode),
|
|
macros: normalizeMacros(input.macros),
|
|
};
|
|
}
|
|
|
|
export function lower(input) {
|
|
const rendered = render(input);
|
|
return {
|
|
schema: "tex.math.render_result.v1",
|
|
loweringKind: "typeset-fragment",
|
|
preservedComponentKinds: ["embed.tex"],
|
|
requiredSchemas: ["tex.math.render_request.v1", "tex.math.render_result.v1"],
|
|
fallbackKind: "source-text",
|
|
documentId: requiredString(input.documentId, "documentId"),
|
|
source: input.source,
|
|
html: rendered.html,
|
|
contentHash: rendered.contentHash,
|
|
engine: "katex",
|
|
};
|
|
}
|
|
|
|
export function measure(input) {
|
|
render(input);
|
|
return {
|
|
measurementKind: "typeset-baseline-metrics",
|
|
measurementOutputs: ["baseline", "advance", "inkBounds", "fallbackBounds"],
|
|
measurementAuthority: "runtime-layout-required",
|
|
preservesProviderAuthority: true,
|
|
};
|
|
}
|
|
|
|
export function apply(input) {
|
|
const rendered = render(input);
|
|
renderCache.set(rendered.contentHash, rendered);
|
|
return {
|
|
stateEffects: ["cache-render-result", "emit-fallback-on-render-failure"],
|
|
receiptSchema: "gamecult.eve.plugin_receipt.v1",
|
|
contentHash: rendered.contentHash,
|
|
cacheStatus: "stored",
|
|
providerStateMutated: false,
|
|
};
|
|
}
|
|
|
|
function render(input) {
|
|
const source = requiredString(input.source, "source");
|
|
const macros = normalizeMacros(input.macros);
|
|
const contentHash = createHash("sha256").update(JSON.stringify({ source, macros, displayMode: Boolean(input.displayMode) })).digest("hex");
|
|
const cached = renderCache.get(contentHash);
|
|
if (cached) return cached;
|
|
return {
|
|
contentHash,
|
|
html: katex.renderToString(source, { displayMode: Boolean(input.displayMode), macros, throwOnError: true, output: "htmlAndMathml" }),
|
|
};
|
|
}
|
|
|
|
function normalizeMacros(value) {
|
|
if (value == null) return {};
|
|
if (typeof value !== "object" || Array.isArray(value)) throw new Error("macros must be an object.");
|
|
return Object.fromEntries(Object.entries(value).map(([key, expansion]) => [key, requiredString(expansion, `macro ${key}`)]));
|
|
}
|
|
|
|
function requiredString(value, field) {
|
|
if (typeof value !== "string" || !value.trim()) throw new Error(`${field} is required.`);
|
|
return value;
|
|
}
|
|
|