91 lines
3.5 KiB
TypeScript
91 lines
3.5 KiB
TypeScript
// Default page settings prepended to all compile() calls so the API never falls back to A4.
|
|
// Users can override with their own #set page() — Typst's last rule wins.
|
|
const DEFAULT_PAGE = `#set page(width: auto, height: auto, margin: 8pt, fill: white)\n`;
|
|
|
|
export default class Typst {
|
|
messages: Map<string, { replyId: string; ownerId?: string }>;
|
|
private readonly apiUrl = "https://typ.sirblob.co/v1/render";
|
|
|
|
constructor() {
|
|
this.messages = new Map();
|
|
}
|
|
|
|
addMessage(userMessage: string, responseMessage: string, ownerId?: string) { this.messages.set(userMessage, { replyId: responseMessage, ownerId }); }
|
|
removeMessage(userMessage: string) { this.messages.delete(userMessage); }
|
|
hasMessage(userMessage: string) { return this.messages.has(userMessage); }
|
|
getResponse(userMessage: string) { return this.messages.get(userMessage)?.replyId; }
|
|
getOwner(userMessage: string) { return this.messages.get(userMessage)?.ownerId; }
|
|
findByReply(replyId: string): { userMessage?: string; ownerId?: string } | null {
|
|
for (const [userMessage, data] of this.messages.entries()) {
|
|
if (data.replyId === replyId) return { userMessage, ownerId: data.ownerId };
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private get apiKey(): string {
|
|
const key = process.env.TYPSTDRIVE_API_KEY;
|
|
if (!key) throw new Error("TYPSTDRIVE_API_KEY environment variable is missing.");
|
|
return key;
|
|
}
|
|
|
|
async compile(code: string, _options?: { cleanup?: boolean }): Promise<{ buffers?: Buffer[]; error?: string }> {
|
|
try {
|
|
const response = await fetch(this.apiUrl, {
|
|
method: "POST",
|
|
headers: {
|
|
"Authorization": `Bearer ${this.apiKey}`,
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({ code: DEFAULT_PAGE + code, format: "png" }),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const text = await response.text().catch(() => response.statusText);
|
|
return { error: `TypstDrive API error ${response.status}: ${text}` };
|
|
}
|
|
|
|
const arrayBuffer = await response.arrayBuffer();
|
|
return { buffers: [Buffer.from(arrayBuffer)] };
|
|
} catch (error: any) {
|
|
return { error: `TypstDrive request failed: ${error?.message ?? String(error)}` };
|
|
}
|
|
}
|
|
|
|
async renderToImage(typstCode: string, metadata?: { topic?: string; difficulty?: string }): Promise<Buffer | null> {
|
|
const metadataHeader = metadata ? `
|
|
#align(center)[
|
|
#block(
|
|
fill: rgb("#e0f2fe"),
|
|
inset: 10pt,
|
|
radius: 4pt,
|
|
[
|
|
#text(weight: "bold", size: 14pt)[Topic: ${metadata.topic || "N/A"}] \\
|
|
#text(size: 12pt, fill: rgb("#64748b"))[Difficulty: ${metadata.difficulty || "N/A"}]
|
|
]
|
|
)
|
|
]
|
|
|
|
#v(0.8em)
|
|
` : '';
|
|
|
|
// 500pt wide keeps equations readable; height: auto sizes to content.
|
|
// This #set page overrides the DEFAULT_PAGE prepended in compile().
|
|
const styledCode = `
|
|
#set page(width: 500pt, height: auto, margin: 12pt, fill: white)
|
|
#set text(size: 16pt, font: "New Computer Modern")
|
|
#set par(justify: true, leading: 0.65em)
|
|
#set block(spacing: 1.2em)
|
|
|
|
${metadataHeader}${typstCode}
|
|
`;
|
|
|
|
const result = await this.compile(styledCode);
|
|
if (result.error || !result.buffers || result.buffers.length === 0) {
|
|
console.error("Typst render error:", result.error);
|
|
return null;
|
|
}
|
|
|
|
return result.buffers[0];
|
|
}
|
|
}
|