// 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; 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 { 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]; } }