Forked from bakit/ai-to-pdf
src / toolsProvider.ts
import { tool } from "@lmstudio/sdk";
import { z } from "zod";
import fs from "fs/promises";
import path from "path";
import puppeteer from "puppeteer";
import { marked } from "marked";
import hljs from "highlight.js";
import { markedHighlight } from "marked-highlight";
marked.use(
markedHighlight({
langPrefix: "hljs language-",
highlight(code, lang) {
const language = hljs.getLanguage(lang) ? lang : "plaintext";
return hljs.highlight(code, { language }).value;
},
})
);
function sanitizeFilename(name: string) {
return name
.replace(/[<>:"/\\|?*]/g, "")
.replace(/\s+/g, "-")
.slice(0, 100);
}
function getThemeCss(theme: string) {
if (theme === "dark") {
return `
body {
background: #111827;
color: #f3f4f6;
}
pre {
background: #1f2937;
}
code {
color: #93c5fd;
}
table {
border-color: #374151;
}
`;
}
return `
body {
background: white;
color: #111827;
}
pre {
background: #f3f4f6;
}
code {
color: #2563eb;
}
`;
}
function createHtml(markdown: string, theme: string) {
const html = marked.parse(markdown);
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>AI Generated PDF</title>
<style>
body {
font-family: Inter, Segoe UI, Arial, sans-serif;
padding: 40px;
line-height: 1.7;
font-size: 14px;
}
h1,h2,h3,h4 {
margin-top: 28px;
}
img {
max-width: 100%;
}
table {
width: 100%;
border-collapse: collapse;
margin: 20px 0;
word-break: break-word;
overflow-wrap: anywhere;
}
th, td {
border: 1px solid #d1d5db;
padding: 10px;
text-align: left;
}
pre {
padding: 16px;
overflow-x: auto;
border-radius: 10px;
}
code {
font-family: Consolas, monospace;
}
blockquote {
border-left: 4px solid #9ca3af;
padding-left: 16px;
color: #6b7280;
}
${getThemeCss(theme)}
</style>
</head>
<body>
${html}
</body>
</html>
`;
}
export async function toolsProvider(ctl: any) {
return [
tool({
name: "generate_pdf",
description:
"Convert Markdown into a professional PDF document with syntax highlighting and themes.",
parameters: {
markdown: z.string().describe("Markdown content"),
filename: z
.string()
.optional()
.describe("Optional PDF filename"),
theme: z
.enum(["light", "dark"])
.optional()
.describe("PDF color theme"),
pageSize: z
.enum(["A4", "Letter"])
.optional()
.describe("PDF page size"),
},
implementation: async (
{ markdown, filename, theme = "light", pageSize = "A4" },
{ status, warn }
) => {
let browser;
try {
status("Preparing PDF...");
const workingDir = ctl.getWorkingDirectory();
await fs.mkdir(workingDir, { recursive: true });
const safeName = sanitizeFilename(
filename || `document-${Date.now()}`
);
const finalName = safeName.endsWith(".pdf")
? safeName
: `${safeName}.pdf`;
const outputPath = path.join(workingDir, finalName);
const html = createHtml(markdown, theme);
browser = await puppeteer.launch({
headless: true,
args: [
"--no-sandbox",
"--disable-setuid-sandbox",
"--disable-dev-shm-usage",
],
});
const page = await browser.newPage();
await page.setViewport({
width: 1400,
height: 1800,
});
await page.setContent(html, {
waitUntil: "networkidle0",
});
await page.pdf({
path: outputPath,
format: pageSize,
printBackground: true,
preferCSSPageSize: true,
margin: {
top: "40px",
right: "40px",
bottom: "40px",
left: "40px",
},
});
status(`PDF generated: ${finalName}`);
return {
success: true,
filename: finalName,
path: outputPath,
};
} catch (error: any) {
console.error(error);
warn(error?.message || "Unknown PDF generation error");
return {
success: false,
error: error?.message || String(error),
};
} finally {
if (browser) {
await browser.close();
}
}
},
}),
];
}