Initial Code
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { execFile } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
export default class Typst {
|
||||
messages: Map<string, { replyId: string; ownerId?: string }>;
|
||||
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;
|
||||
}
|
||||
|
||||
async compile(code: string, options?: { cleanup?: boolean }): Promise<{ buffers?: Buffer[]; error?: string; files?: string[]; typPath?: string }> {
|
||||
const storageDir = path.resolve('./storage/typst');
|
||||
await fs.promises.mkdir(storageDir, { recursive: true }).catch(() => {});
|
||||
|
||||
const uid = `${Date.now()}_${Math.random().toString(16).slice(2, 8)}`;
|
||||
const baseName = `typ_${uid}`;
|
||||
const typPath = path.join(storageDir, `${baseName}.typ`);
|
||||
const pngTemplate = path.join(storageDir, `${baseName}{p}.png`);
|
||||
const pngSingle = path.join(storageDir, `${baseName}.png`);
|
||||
const cleanup = options?.cleanup !== undefined ? options!.cleanup : true;
|
||||
|
||||
try {
|
||||
await fs.promises.writeFile(typPath, code, 'utf8');
|
||||
} catch (writeErr: any) {
|
||||
return { error: `Failed to write typ file: ${String(writeErr)}` };
|
||||
}
|
||||
|
||||
let compiledToTemplate = false;
|
||||
try {
|
||||
await execFileAsync('typst', ['compile', '--format', 'png', '--ppi', '300', typPath, pngTemplate], { timeout: 30000 });
|
||||
compiledToTemplate = true;
|
||||
} catch (multiErr: any) {
|
||||
try {
|
||||
await execFileAsync('typst', ['compile', '--format', 'png', '--ppi', '300', typPath, pngSingle], { timeout: 30000 });
|
||||
compiledToTemplate = false;
|
||||
} catch (singleErr: any) {
|
||||
if (cleanup) await fs.promises.unlink(typPath).catch(() => {});
|
||||
const msg = (multiErr && (multiErr.stderr || multiErr.message)) || (singleErr && (singleErr.stderr || singleErr.message)) || 'Unknown typst error';
|
||||
return { error: `Typst compilation failed: ${String(msg)}` };
|
||||
}
|
||||
}
|
||||
|
||||
let pngFiles: string[] = [];
|
||||
try {
|
||||
const all = await fs.promises.readdir(storageDir);
|
||||
if (compiledToTemplate) pngFiles = all.filter(f => f.startsWith(baseName) && f.endsWith('.png')).sort();
|
||||
else if (all.includes(`${baseName}.png`)) pngFiles = [`${baseName}.png`];
|
||||
} catch (readErr: any) {
|
||||
if (cleanup) await fs.promises.unlink(typPath).catch(() => {});
|
||||
return { error: `Failed to read output PNG files: ${String(readErr)}` };
|
||||
}
|
||||
|
||||
if (pngFiles.length === 0) {
|
||||
await fs.promises.unlink(typPath).catch(() => {});
|
||||
return { error: 'Compilation finished but no PNG files were produced' };
|
||||
}
|
||||
|
||||
const fullPaths = pngFiles.map(f => path.join(storageDir, f));
|
||||
const buffers: Buffer[] = await Promise.all(fullPaths.map(async p => {
|
||||
try { return await fs.promises.readFile(p); } catch (e) { return null as any; }
|
||||
}));
|
||||
|
||||
const validBuffers = buffers.filter(Boolean) as Buffer[];
|
||||
|
||||
if (cleanup) {
|
||||
await fs.promises.unlink(typPath).catch(() => {});
|
||||
await Promise.all(fullPaths.map(f => fs.promises.unlink(f).catch(() => {})));
|
||||
}
|
||||
|
||||
if (validBuffers.length === 0) return { error: 'No PNG buffers could be read from generated output' };
|
||||
|
||||
const result: { buffers?: Buffer[]; files?: string[]; typPath?: string } = { buffers: validBuffers };
|
||||
if (!cleanup) {
|
||||
result.files = fullPaths;
|
||||
result.typPath = typPath;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async renderToImage(typstCode: string): Promise<string | null> {
|
||||
const styledCode = `
|
||||
#set page(width: 210mm, height: auto, margin: (x: 20pt, y: 25pt), fill: white)
|
||||
#set text(size: 16pt, font: "New Computer Modern")
|
||||
#set par(justify: true, leading: 0.65em)
|
||||
#set block(spacing: 1.2em)
|
||||
|
||||
${typstCode}
|
||||
`;
|
||||
|
||||
const result = await this.compile(styledCode, { cleanup: false });
|
||||
|
||||
if (result.error || !result.files || result.files.length === 0) {
|
||||
console.error("Typst render error:", result.error);
|
||||
return null;
|
||||
}
|
||||
|
||||
return result.files[0];
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user