boxpdf-html
Advanced tools
| // src/render-file.ts | ||
| import { existsSync, readFileSync } from "fs"; | ||
| import { isAbsolute, resolve } from "path"; | ||
| import { StandardFonts } from "pdf-lib"; | ||
| import { loadFont, loadImage } from "boxpdf"; | ||
| function injectCss(html, stylesheets) { | ||
| if (stylesheets.length === 0) return html; | ||
| const style = `<style> | ||
| ${stylesheets.join("\n")} | ||
| </style>`; | ||
| if (/<\/head>/i.test(html)) return html.replace(/<\/head>/i, `${style} | ||
| </head>`); | ||
| return `${style} | ||
| ${html}`; | ||
| } | ||
| async function loadFaces(pdf, spec, baseUrl) { | ||
| const normal = spec.font ? await loadFont(pdf, readFileSync(resolve(spec.font))) : await pdf.embedFont(StandardFonts.Helvetica); | ||
| const bold = spec.boldFont ? await loadFont(pdf, readFileSync(resolve(spec.boldFont))) : await pdf.embedFont(StandardFonts.HelveticaBold); | ||
| const italic = spec.italicFont ? await loadFont(pdf, readFileSync(resolve(spec.italicFont))) : await pdf.embedFont(StandardFonts.HelveticaOblique); | ||
| const boldItalic = spec.boldItalicFont ? await loadFont(pdf, readFileSync(resolve(spec.boldItalicFont))) : await pdf.embedFont(StandardFonts.HelveticaBoldOblique); | ||
| const faces = { normal, bold, italic, boldItalic }; | ||
| const families = { | ||
| Helvetica: faces, | ||
| Arial: faces, | ||
| "sans-serif": faces, | ||
| serif: faces, | ||
| monospace: faces | ||
| }; | ||
| for (const mapping of spec.families ?? []) { | ||
| const [name, familySpec] = splitOnce(mapping, "="); | ||
| if (!name || !familySpec) throw new Error(`invalid --font-family "${mapping}"`); | ||
| families[name.trim()] = await loadFamily(pdf, familySpec, baseUrl); | ||
| } | ||
| return { normal, bold, italic, boldItalic, families }; | ||
| } | ||
| async function loadFamily(pdf, spec, baseUrl) { | ||
| const out = {}; | ||
| for (const part of spec.split(",")) { | ||
| const [rawKey, rawPath] = splitOnce(part, ":"); | ||
| if (!rawKey || !rawPath) throw new Error(`invalid font family face "${part}"`); | ||
| const key = rawKey.trim(); | ||
| if (!["normal", "bold", "italic", "boldItalic"].includes(key) && !/^\d+$/.test(key)) { | ||
| throw new Error(`invalid font face key "${key}"`); | ||
| } | ||
| out[key] = await loadFont(pdf, readFileSync(resolveAssetUrl(rawPath.trim(), baseUrl))); | ||
| } | ||
| return out; | ||
| } | ||
| async function loadImages(pdf, html, baseUrl, options = {}) { | ||
| return loadImageUrls(pdf, imageUrls(html), baseUrl, options); | ||
| } | ||
| async function loadImageUrls(pdf, urls, baseUrl, options = {}) { | ||
| const images = /* @__PURE__ */ new Map(); | ||
| for (const url of urls) { | ||
| const resolved = resolveAssetUrl(url, baseUrl); | ||
| if (images.has(resolved)) continue; | ||
| try { | ||
| images.set(resolved, await loadImage(pdf, assetSource(resolved, options.allowRemote ?? false))); | ||
| } catch (error) { | ||
| const message = error instanceof Error ? error.message : String(error); | ||
| options.onWarn?.(`image "${url}" did not load: ${message}`); | ||
| } | ||
| } | ||
| return images; | ||
| } | ||
| function imageUrls(source) { | ||
| const urls = []; | ||
| for (const match of source.matchAll(/url\(\s*(?:"([^"]+)"|'([^']+)'|([^)]*?))\s*\)/gi)) { | ||
| const url = (match[1] ?? match[2] ?? match[3])?.trim(); | ||
| if (url) urls.push(url); | ||
| } | ||
| for (const match of source.matchAll(/<(?:img|source)\b[^>]*\bsrc\s*=\s*(?:"([^"]+)"|'([^']+)'|([^\s>]+))/gi)) { | ||
| const url = (match[1] ?? match[2] ?? match[3])?.trim(); | ||
| if (url) urls.push(url); | ||
| } | ||
| return urls; | ||
| } | ||
| function resolveAssetUrl(url, baseUrl) { | ||
| if (/^(https?:|data:)/i.test(url)) return url; | ||
| if (url.startsWith("file://")) return new URL(url).pathname; | ||
| if (/^[a-z]+:\/\//i.test(url)) return url; | ||
| return isAbsolute(url) ? url : resolve(baseUrl, url); | ||
| } | ||
| function assetSource(resolved, allowRemote) { | ||
| if (/^(https?:)/i.test(resolved)) { | ||
| if (!allowRemote) throw new Error(`remote fetch blocked (allowRemote is off): ${resolved}`); | ||
| return resolved; | ||
| } | ||
| if (/^data:/i.test(resolved)) return resolved; | ||
| if (!existsSync(resolved)) throw new Error(`file not found: ${resolved}`); | ||
| return readFileSync(resolved); | ||
| } | ||
| function splitOnce(value, separator) { | ||
| const index = value.indexOf(separator); | ||
| if (index === -1) return [value, void 0]; | ||
| return [value.slice(0, index), value.slice(index + separator.length)]; | ||
| } | ||
| export { | ||
| injectCss, | ||
| loadFaces, | ||
| loadImages, | ||
| loadImageUrls, | ||
| resolveAssetUrl | ||
| }; | ||
| //# sourceMappingURL=chunk-44K6D5SS.js.map |
| {"version":3,"sources":["../src/render-file.ts"],"sourcesContent":["import { existsSync, readFileSync } from \"node:fs\";\nimport { isAbsolute, resolve } from \"node:path\";\nimport { PDFDocument, StandardFonts, type PDFFont, type PDFImage } from \"pdf-lib\";\nimport { loadFont, loadImage } from \"boxpdf\";\nimport { fontFamily, type FontFamilyMap } from \"./font.js\";\n\n/**\n * Shared, filesystem-aware rendering helpers used by both the `boxpdf-html`\n * CLI and the MCP server. These throw `Error` on bad input (the CLI's\n * top-level handler turns that into a `boxpdf-html: <message>` exit; the MCP\n * server turns it into a tool error result).\n */\n\nexport interface FaceSpec {\n /** Path to a regular-weight TTF/OTF. Falls back to built-in Helvetica. */\n font?: string;\n /** Path to a bold TTF/OTF. Falls back to Helvetica-Bold. */\n boldFont?: string;\n /** Path to an italic TTF/OTF. Falls back to Helvetica-Oblique. */\n italicFont?: string;\n /** Path to a bold-italic TTF/OTF. Falls back to Helvetica-BoldOblique. */\n boldItalicFont?: string;\n /** Repeatable `Family=normal:a.ttf,bold:b.ttf` mappings (CLI form). */\n families?: string[];\n}\n\nexport interface LoadedFaces {\n normal: PDFFont;\n bold: PDFFont;\n italic: PDFFont;\n boldItalic: PDFFont;\n families: FontFamilyMap;\n}\n\nexport function injectCss(html: string, stylesheets: string[]): string {\n if (stylesheets.length === 0) return html;\n const style = `<style>\\n${stylesheets.join(\"\\n\")}\\n</style>`;\n if (/<\\/head>/i.test(html)) return html.replace(/<\\/head>/i, `${style}\\n</head>`);\n return `${style}\\n${html}`;\n}\n\nexport async function loadFaces(pdf: PDFDocument, spec: FaceSpec, baseUrl: string): Promise<LoadedFaces> {\n const normal = spec.font ? await loadFont(pdf, readFileSync(resolve(spec.font))) : await pdf.embedFont(StandardFonts.Helvetica);\n const bold = spec.boldFont ? await loadFont(pdf, readFileSync(resolve(spec.boldFont))) : await pdf.embedFont(StandardFonts.HelveticaBold);\n const italic = spec.italicFont ? await loadFont(pdf, readFileSync(resolve(spec.italicFont))) : await pdf.embedFont(StandardFonts.HelveticaOblique);\n const boldItalic = spec.boldItalicFont\n ? await loadFont(pdf, readFileSync(resolve(spec.boldItalicFont)))\n : await pdf.embedFont(StandardFonts.HelveticaBoldOblique);\n\n const faces = { normal, bold, italic, boldItalic };\n const families: FontFamilyMap = {\n Helvetica: faces,\n Arial: faces,\n \"sans-serif\": faces,\n serif: faces,\n monospace: faces\n };\n\n for (const mapping of spec.families ?? []) {\n const [name, familySpec] = splitOnce(mapping, \"=\");\n if (!name || !familySpec) throw new Error(`invalid --font-family \"${mapping}\"`);\n families[name.trim()] = await loadFamily(pdf, familySpec, baseUrl);\n }\n\n return { normal, bold, italic, boldItalic, families };\n}\n\nasync function loadFamily(pdf: PDFDocument, spec: string, baseUrl: string): Promise<FontFamilyMap[string]> {\n const out: Exclude<FontFamilyMap[string], PDFFont> = {};\n for (const part of spec.split(\",\")) {\n const [rawKey, rawPath] = splitOnce(part, \":\");\n if (!rawKey || !rawPath) throw new Error(`invalid font family face \"${part}\"`);\n const key = rawKey.trim();\n if (![\"normal\", \"bold\", \"italic\", \"boldItalic\"].includes(key) && !/^\\d+$/.test(key)) {\n throw new Error(`invalid font face key \"${key}\"`);\n }\n out[key as keyof typeof out] = await loadFont(pdf, readFileSync(resolveAssetUrl(rawPath.trim(), baseUrl)));\n }\n return out;\n}\n\nexport interface LoadImagesOptions {\n /** Allow fetching http(s) image URLs. Off by default to prevent SSRF. */\n allowRemote?: boolean;\n /** Called with a human-readable message when an image fails to load. */\n onWarn?: (message: string) => void;\n}\n\nexport async function loadImages(\n pdf: PDFDocument,\n html: string,\n baseUrl: string,\n options: LoadImagesOptions = {}\n): Promise<Map<string, PDFImage>> {\n return loadImageUrls(pdf, imageUrls(html), baseUrl, options);\n}\n\nexport async function loadImageUrls(\n pdf: PDFDocument,\n urls: Iterable<string>,\n baseUrl: string,\n options: LoadImagesOptions = {}\n): Promise<Map<string, PDFImage>> {\n const images = new Map<string, PDFImage>();\n for (const url of urls) {\n const resolved = resolveAssetUrl(url, baseUrl);\n if (images.has(resolved)) continue;\n try {\n images.set(resolved, await loadImage(pdf, assetSource(resolved, options.allowRemote ?? false)));\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n options.onWarn?.(`image \"${url}\" did not load: ${message}`);\n }\n }\n return images;\n}\n\nexport function imageUrls(source: string): string[] {\n const urls: string[] = [];\n for (const match of source.matchAll(/url\\(\\s*(?:\"([^\"]+)\"|'([^']+)'|([^)]*?))\\s*\\)/gi)) {\n const url = (match[1] ?? match[2] ?? match[3])?.trim();\n if (url) urls.push(url);\n }\n for (const match of source.matchAll(/<(?:img|source)\\b[^>]*\\bsrc\\s*=\\s*(?:\"([^\"]+)\"|'([^']+)'|([^\\s>]+))/gi)) {\n const url = (match[1] ?? match[2] ?? match[3])?.trim();\n if (url) urls.push(url);\n }\n return urls;\n}\n\nexport function resolveAssetUrl(url: string, baseUrl: string): string {\n if (/^(https?:|data:)/i.test(url)) return url;\n if (url.startsWith(\"file://\")) return new URL(url).pathname;\n if (/^[a-z]+:\\/\\//i.test(url)) return url;\n return isAbsolute(url) ? url : resolve(baseUrl, url);\n}\n\nfunction assetSource(resolved: string, allowRemote: boolean): string | Uint8Array {\n if (/^(https?:)/i.test(resolved)) {\n if (!allowRemote) throw new Error(`remote fetch blocked (allowRemote is off): ${resolved}`);\n return resolved;\n }\n if (/^data:/i.test(resolved)) return resolved;\n if (!existsSync(resolved)) throw new Error(`file not found: ${resolved}`);\n return readFileSync(resolved);\n}\n\nexport function splitOnce(value: string, separator: string): [string, string] | [string, undefined] {\n const index = value.indexOf(separator);\n if (index === -1) return [value, undefined];\n return [value.slice(0, index), value.slice(index + separator.length)];\n}\n"],"mappings":"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","names":[]} |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
| import { | ||
| injectCss, | ||
| loadFaces, | ||
| loadImages, | ||
| resolveAssetUrl | ||
| } from "./chunk-44K6D5SS.js"; | ||
| import { | ||
| fontFamily, | ||
| htmlToBoxpdf | ||
| } from "./chunk-MQ2YACWX.js"; | ||
| // src/mcp.ts | ||
| import { createInterface } from "readline"; | ||
| import { createRequire } from "module"; | ||
| import { existsSync, readFileSync, writeFileSync } from "fs"; | ||
| import { dirname, resolve } from "path"; | ||
| import { PDFDocument } from "pdf-lib"; | ||
| import { PageSizes, pageContent, renderFlow } from "boxpdf"; | ||
| var PROTOCOL_VERSION = "2025-11-25"; | ||
| var INLINE_BYTE_CAP = 1e6; | ||
| var CORE_TEMPLATES = ["receipt", "boarding-pass", "resume", "order-confirmation", "certificate"]; | ||
| var DOC_TOPICS = ["quickstart", "fonts", "themes", "tables", "pagination", "streaming", "html-api", "cloudflare"]; | ||
| function coreDir() { | ||
| try { | ||
| const require2 = createRequire(import.meta.url); | ||
| let dir = dirname(require2.resolve("boxpdf")); | ||
| for (let i = 0; i < 8; i += 1) { | ||
| const pkg = resolve(dir, "package.json"); | ||
| if (existsSync(pkg)) { | ||
| try { | ||
| if (JSON.parse(readFileSync(pkg, "utf8")).name === "boxpdf") return dir; | ||
| } catch { | ||
| } | ||
| } | ||
| const up = dirname(dir); | ||
| if (up === dir) break; | ||
| dir = up; | ||
| } | ||
| } catch { | ||
| return void 0; | ||
| } | ||
| return void 0; | ||
| } | ||
| function coreReadme() { | ||
| const dir = coreDir(); | ||
| if (!dir) return void 0; | ||
| const path = resolve(dir, "README.md"); | ||
| return existsSync(path) ? readFileSync(path, "utf8") : void 0; | ||
| } | ||
| function coreTemplate(name) { | ||
| const dir = coreDir(); | ||
| if (!dir) return void 0; | ||
| const path = resolve(dir, "templates", `${name}.ts`); | ||
| if (!existsSync(path)) return void 0; | ||
| return readFileSync(path, "utf8").replaceAll('from "../src/index.js"', 'from "boxpdf"').replaceAll(`new URL("../fixtures/${name}.pdf", import.meta.url)`, `new URL("./${name}.pdf", import.meta.url)`).replaceAll(`wrote fixtures/${name}.pdf`, `wrote ${name}.pdf`); | ||
| } | ||
| function htmlReadme() { | ||
| try { | ||
| const path = resolve(dirname(new URL(import.meta.url).pathname), "..", "README.md"); | ||
| return existsSync(path) ? readFileSync(path, "utf8") : void 0; | ||
| } catch { | ||
| return void 0; | ||
| } | ||
| } | ||
| function resources() { | ||
| const list = [ | ||
| { uri: "boxpdf-html://guide", name: "Agent guide", description: "How to turn HTML into a PDF with boxpdf-html, and when to drop to the boxpdf library.", mimeType: "text/markdown" }, | ||
| { uri: "boxpdf-html://readme", name: "boxpdf-html README", description: "Full boxpdf-html README: CLI, htmlToPdf, htmlToBoxpdf, fonts, Tailwind, supported CSS.", mimeType: "text/markdown" }, | ||
| { uri: "boxpdf://readme", name: "boxpdf README", description: "Full boxpdf README: layout DSL, themes, fonts, pagination, streaming.", mimeType: "text/markdown" } | ||
| ]; | ||
| for (const name of CORE_TEMPLATES) { | ||
| list.push({ uri: `boxpdf://templates/${name}`, name: `${name}.ts`, description: `Copy-paste boxpdf ${name} template source.`, mimeType: "text/typescript" }); | ||
| } | ||
| return list; | ||
| } | ||
| function readResource(uri) { | ||
| if (uri === "boxpdf-html://guide") return { uri, mimeType: "text/markdown", text: docText("quickstart") + "\n\n" + docText("html-api") }; | ||
| if (uri === "boxpdf-html://readme") { | ||
| const text = htmlReadme(); | ||
| return text ? { uri, mimeType: "text/markdown", text } : void 0; | ||
| } | ||
| if (uri === "boxpdf://readme") { | ||
| const text = coreReadme(); | ||
| return text ? { uri, mimeType: "text/markdown", text } : void 0; | ||
| } | ||
| const prefix = "boxpdf://templates/"; | ||
| if (uri.startsWith(prefix)) { | ||
| const text = coreTemplate(uri.slice(prefix.length)); | ||
| return text ? { uri, mimeType: "text/typescript", text } : void 0; | ||
| } | ||
| return void 0; | ||
| } | ||
| function tools() { | ||
| return [ | ||
| { | ||
| name: "html_to_pdf", | ||
| description: "Render an HTML string (optionally with extra CSS) to a PDF using boxpdf-html. Supports a practical subset of CSS \u2014 flex, grid, tables, borders, colors, typography, and compiled Tailwind. No browser or JS execution. Returns the PDF (written to outputPath, or inline as a base64 resource) plus any warnings and unsupported-CSS diagnostics so you can fix the input.", | ||
| inputSchema: { | ||
| type: "object", | ||
| required: ["html"], | ||
| properties: { | ||
| html: { type: "string", description: "HTML markup. May include <style> blocks and inline styles." }, | ||
| css: { type: "string", description: "Extra stylesheet injected before render (e.g. compiled Tailwind output)." }, | ||
| outputPath: { type: "string", description: "Where to write the PDF (cwd-relative ok). If omitted, the PDF is returned inline as a base64 resource." }, | ||
| size: { type: "string", enum: Object.keys(PageSizes), default: "Letter", description: "Page size." }, | ||
| margin: { type: "number", default: 40, description: "Page margin in PDF points." }, | ||
| baseUrl: { type: "string", description: "Directory or URL for resolving relative <img> and background-image URLs. Defaults to the working directory." }, | ||
| fonts: { | ||
| type: "object", | ||
| description: "Optional TTF/OTF file paths to embed instead of the built-in Helvetica family.", | ||
| properties: { | ||
| regular: { type: "string" }, | ||
| bold: { type: "string" }, | ||
| italic: { type: "string" }, | ||
| boldItalic: { type: "string" } | ||
| } | ||
| }, | ||
| allowRemote: { type: "boolean", default: false, description: "Allow fetching http(s) images. Off by default to prevent SSRF." }, | ||
| debug: { type: "boolean", default: false, description: "Draw boxpdf debug overlays." } | ||
| } | ||
| } | ||
| }, | ||
| { | ||
| name: "boxpdf_docs", | ||
| description: "Get focused guidance on using the boxpdf / boxpdf-html libraries directly when html_to_pdf is not enough \u2014 custom layout, pagination, tables, fonts, themes, streaming, the HTML API, or Cloudflare Workers.", | ||
| inputSchema: { | ||
| type: "object", | ||
| properties: { | ||
| topic: { type: "string", enum: [...DOC_TOPICS], default: "quickstart", description: "Documentation topic. Defaults to quickstart." } | ||
| } | ||
| } | ||
| } | ||
| ]; | ||
| } | ||
| async function callTool(name, args) { | ||
| if (name === "html_to_pdf") return htmlToPdfTool(args); | ||
| if (name === "boxpdf_docs") return docsTool(args); | ||
| return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true }; | ||
| } | ||
| function docsTool(args) { | ||
| const topic = typeof args.topic === "string" ? args.topic : "quickstart"; | ||
| if (!DOC_TOPICS.includes(topic)) { | ||
| return { content: [{ type: "text", text: `Unknown topic "${topic}". Available: ${DOC_TOPICS.join(", ")}.` }], isError: true }; | ||
| } | ||
| return { content: [{ type: "text", text: docText(topic) }] }; | ||
| } | ||
| async function htmlToPdfTool(args) { | ||
| if (typeof args.html !== "string" || args.html.length === 0) { | ||
| return { content: [{ type: "text", text: "html_to_pdf requires a non-empty `html` string." }], isError: true }; | ||
| } | ||
| const sizeKey = typeof args.size === "string" ? args.size : "Letter"; | ||
| const size = PageSizes[sizeKey]; | ||
| if (!size) { | ||
| return { content: [{ type: "text", text: `Unknown size "${sizeKey}". Available: ${Object.keys(PageSizes).join(", ")}.` }], isError: true }; | ||
| } | ||
| const margin = typeof args.margin === "number" ? args.margin : 40; | ||
| const baseUrl = typeof args.baseUrl === "string" ? resolve(args.baseUrl) : process.cwd(); | ||
| const fonts = args.fonts ?? {}; | ||
| const warnings = []; | ||
| const html = injectCss(args.html, typeof args.css === "string" ? [args.css] : []); | ||
| const pdf = await PDFDocument.create(); | ||
| const faces = await loadFaces( | ||
| pdf, | ||
| { font: fonts.regular, boldFont: fonts.bold, italicFont: fonts.italic, boldItalicFont: fonts.boldItalic }, | ||
| baseUrl | ||
| ); | ||
| const images = await loadImages(pdf, html, baseUrl, { | ||
| allowRemote: args.allowRemote === true, | ||
| onWarn: (message) => warnings.push(message) | ||
| }); | ||
| const result = htmlToBoxpdf(html, { | ||
| font: faces.normal, | ||
| boldFont: faces.bold, | ||
| italicFont: faces.italic, | ||
| resolveFont: fontFamily(faces.families), | ||
| resolveImage: ({ url }) => images.get(resolveAssetUrl(url, baseUrl)), | ||
| baseUrl, | ||
| width: pageContent(size, margin).width, | ||
| diagnostics: { unsupportedCss: true, sampleLimit: 5 } | ||
| }); | ||
| warnings.push(...result.warnings); | ||
| const { pages } = await renderFlow(pdf, result.nodes, { margin, size, debug: args.debug === true, warnings: false }); | ||
| const bytes = await pdf.save(); | ||
| const unsupported = result.diagnostics?.unsupportedCss ?? []; | ||
| const lines = []; | ||
| const structured = { | ||
| bytes: bytes.length, | ||
| pages: pages.length, | ||
| warnings, | ||
| unsupportedCss: unsupported.map(({ property, value, count }) => ({ property, value, count })) | ||
| }; | ||
| const content = []; | ||
| if (typeof args.outputPath === "string" && args.outputPath.length > 0) { | ||
| const out = resolve(args.outputPath); | ||
| writeFileSync(out, bytes); | ||
| structured.outputPath = out; | ||
| lines.push(`Wrote ${out} \u2014 ${bytes.length} bytes, ${pages.length} page${pages.length === 1 ? "" : "s"}.`); | ||
| } else if (bytes.length > INLINE_BYTE_CAP) { | ||
| lines.push( | ||
| `Rendered ${bytes.length} bytes across ${pages.length} page${pages.length === 1 ? "" : "s"}, which is too large to return inline. Re-run with an \`outputPath\` to write the file instead.` | ||
| ); | ||
| } else { | ||
| lines.push(`Rendered ${bytes.length} bytes, ${pages.length} page${pages.length === 1 ? "" : "s"}.`); | ||
| content.push({ | ||
| type: "resource", | ||
| resource: { uri: "boxpdf-html://render.pdf", mimeType: "application/pdf", blob: Buffer.from(bytes).toString("base64") } | ||
| }); | ||
| } | ||
| if (warnings.length > 0) lines.push("", "Warnings:", ...warnings.map((w) => `- ${w}`)); | ||
| if (unsupported.length > 0) { | ||
| lines.push("", "Unsupported CSS (rendered without these declarations):"); | ||
| for (const item of unsupported) lines.push(`- ${item.property}: ${item.value} (${item.count}\xD7)`); | ||
| } | ||
| content.unshift({ type: "text", text: lines.join("\n") }); | ||
| return { content, structuredContent: structured }; | ||
| } | ||
| function docText(topic) { | ||
| return DOCS[topic]; | ||
| } | ||
| var DOCS = { | ||
| quickstart: `# boxpdf quickstart (library) | ||
| Shortest path to bytes \u2014 no pdf-lib import, no manual save: | ||
| \`\`\`ts | ||
| import { cleanTheme, flowToPdf, hline, hstack, standardFonts, text, vstack } from "boxpdf"; | ||
| const bytes = await flowToPdf(async (pdf) => { | ||
| const { font, bold } = await standardFonts(pdf); // built-in Helvetica family | ||
| const t = cleanTheme({ font, bold }); | ||
| return [ | ||
| vstack({ gap: 8 }, text("Receipt #18472", t.type.h1), text("May 14, 2026", t.type.caption)), | ||
| hline(t.hr), | ||
| hstack({ gap: 16, justify: "between", width: 515 }, | ||
| text("Wool socks", t.type.body), | ||
| text("$28.00", { ...t.type.body, font: bold, align: "right", width: 80 })) | ||
| ]; | ||
| }); | ||
| \`\`\` | ||
| \`standardFonts(pdf, family?)\` returns \`{ font, bold, italic, boldItalic }\` (family: "helvetica" | "times" | "courier"). \`flowToPdf(build, options?)\` owns create + paginate + save. For multiple render passes use \`renderFlow(pdf, nodes, options)\` and call \`pdf.save()\` yourself.`, | ||
| fonts: `# Fonts | ||
| - Built-in (no bytes): \`const fonts = await standardFonts(pdf)\` \u2192 drop into any theme. | ||
| - Custom TTF/OTF: \`const font = await loadFont(pdf, source)\` where source is bytes, a URL, a data URL, or a base64 string. | ||
| - Bundled Inter: \`import { embedInter } from "boxpdf/inter"; const { font, bold } = await embedInter(pdf);\` | ||
| - Tabular figures for money columns: \`loadFont(pdf, bytes, { features: { tnum: true } })\` or \`embedInter(pdf, { tabularFigures: true })\`. | ||
| - Generate a bundled font module: \`npx boxpdf font add ./Acme-Regular.ttf=regular --out src/fonts/acme.ts\`.`, | ||
| themes: `# Themes | ||
| \`cleanTheme\`, \`stripeTheme\`, \`editorialTheme\`, \`brutalistTheme\`. Each accepts a \`{ font, bold, italic? }\` object (what \`standardFonts\`/\`embedInter\` return) or positional fonts: | ||
| \`\`\`ts | ||
| const t = cleanTheme(await standardFonts(pdf)); | ||
| const serif = editorialTheme(await standardFonts(pdf, "times")); | ||
| \`\`\` | ||
| Every theme exposes \`colors\`, \`spacing\`, \`radii\`, \`type\` (display/h1/h2/h3/body/bodySmall/caption/label), \`card\`, \`hr\`.`, | ||
| tables: `# Tables | ||
| \`table({ columns, rows, ... })\` with fixed / auto / fractional columns, header & footer rows, dividers, colSpan, styled cells, per-side borders, vertical alignment, and row-level page fragmentation under \`renderFlow\` (headers repeat on continuation pages). | ||
| \`\`\`ts | ||
| table({ | ||
| columns: [{ width: "auto" }, { width: "1fr" }, { width: 80 }], | ||
| header: [text("Qty", t.type.label), text("Item", t.type.label), text("Total", t.type.label)], | ||
| rows: items.map((i) => [text(String(i.qty)), text(i.name), text(formatCurrency(i.total), { align: "right" })]) | ||
| }); | ||
| \`\`\``, | ||
| pagination: `# Pagination | ||
| \`renderFlow(pdf, nodes[], options)\` paginates top-level children. Top-level \`vstack\` nodes fragment between children; \`table()\` fragments between rows. Use \`keepTogether(...)\` or \`breakInside: "avoid"\` to keep a block atomic. | ||
| Options: \`size\` (default Letter; \`PageSizes.A4\` etc.), \`margin\`, \`header\`/\`footer\` (receive \`{ pageNumber, totalPages }\`), \`reserveBottom\`, document metadata (\`title\`/\`author\`/...), \`debug\`. For one page, \`renderToPdf(node, options)\` returns bytes directly.`, | ||
| streaming: `# Streaming (memory-bounded) | ||
| For long documents use \`streamFlow(pdf, writable, asyncIterable, options)\` \u2014 it writes PDF bytes to a \`WritableStream<Uint8Array>\` as each page closes, keeping peak heap flat regardless of page count. | ||
| \`\`\`ts | ||
| const { readable, writable } = new TransformStream<Uint8Array, Uint8Array>(); | ||
| streamFlow(pdf, writable, generate(font, bold)).catch(console.error); | ||
| return new Response(readable, { headers: { "content-type": "application/pdf" } }); | ||
| \`\`\` | ||
| All \`embedFont\`/\`embedPng\`/\`embedJpg\` calls must finish before \`streamFlow\`. \`totalPages\` is unavailable in headers/footers when streaming \u2014 use \`renderFlow\` if you need "Page X of Y". For Node, wrap a \`stream.Writable\` with \`nodeAdapter\`.`, | ||
| "html-api": `# HTML \u2192 PDF (boxpdf-html, as a library) | ||
| One call to bytes (fonts default to Helvetica): | ||
| \`\`\`ts | ||
| import { htmlToPdf } from "boxpdf-html"; | ||
| const bytes = await htmlToPdf("<h1>Invoice</h1><p>Thanks!</p>"); | ||
| \`\`\` | ||
| For the nodes, warnings, and diagnostics (full control), use \`htmlToBoxpdf\` + \`renderFlow\`: | ||
| \`\`\`ts | ||
| import { fontFamily, htmlToBoxpdf } from "boxpdf-html"; | ||
| import { renderFlow } from "boxpdf"; | ||
| const result = htmlToBoxpdf(html, { font, boldFont, resolveFont: fontFamily({ Inter: { normal: font, bold: boldFont } }), width: 532 }); | ||
| await renderFlow(pdf, result.nodes, { margin: 40 }); | ||
| \`\`\` | ||
| \`width\` is the CSS containing-block width in points (Letter \u2212 2\xD7margin). Supported CSS is a practical subset (flex, grid, tables, borders, type, Tailwind output); pass \`diagnostics: { unsupportedCss: true }\` to see what was dropped.`, | ||
| cloudflare: `# Cloudflare Workers / edge | ||
| Both boxpdf and \`boxpdf/inter\` run on Workers without \`nodejs_compat\`. No headless browser, WASM, or native deps. | ||
| \`\`\`ts | ||
| import { cleanTheme, flowToPdf, standardFonts, text } from "boxpdf"; | ||
| export default { | ||
| async fetch() { | ||
| const bytes = await flowToPdf(async (pdf) => { | ||
| const t = cleanTheme(await standardFonts(pdf)); | ||
| return [text("Generated at the edge.", t.type.body)]; | ||
| }); | ||
| return new Response(bytes, { headers: { "content-type": "application/pdf" } }); | ||
| } | ||
| }; | ||
| \`\`\`` | ||
| }; | ||
| function ok(id, value) { | ||
| return { jsonrpc: "2.0", id, result: value }; | ||
| } | ||
| function err(id, code, message, data) { | ||
| return { jsonrpc: "2.0", id, error: { code, message, ...data === void 0 ? {} : { data } } }; | ||
| } | ||
| async function dispatch(message) { | ||
| if (!message.method || message.id === void 0) return void 0; | ||
| const id = message.id; | ||
| switch (message.method) { | ||
| case "initialize": | ||
| return ok(id, { | ||
| protocolVersion: PROTOCOL_VERSION, | ||
| capabilities: { resources: {}, tools: {} }, | ||
| serverInfo: { name: "boxpdf-html", title: "boxpdf-html", version: "1.0.0", description: "HTML-to-PDF tool plus boxpdf library docs and templates." }, | ||
| instructions: "Call html_to_pdf to render HTML (and optional CSS) to a PDF. Read its warnings and unsupportedCss to fix the input. Call boxpdf_docs (or read the resources) when you need to build PDFs with the boxpdf library directly." | ||
| }); | ||
| case "ping": | ||
| return ok(id, {}); | ||
| case "resources/list": | ||
| return ok(id, { resources: resources() }); | ||
| case "resources/read": { | ||
| const uri = readUri(message.params); | ||
| if (!uri) return err(id, -32602, "Missing resource URI"); | ||
| const resource = readResource(uri); | ||
| if (!resource) return err(id, -32002, "Resource not found", { uri }); | ||
| return ok(id, { contents: [resource] }); | ||
| } | ||
| case "resources/templates/list": | ||
| return ok(id, { resourceTemplates: [] }); | ||
| case "tools/list": | ||
| return ok(id, { tools: tools() }); | ||
| case "tools/call": { | ||
| const params = message.params ?? {}; | ||
| if (typeof params.name !== "string") return err(id, -32602, "Missing tool name"); | ||
| const args = params.arguments ?? {}; | ||
| try { | ||
| return ok(id, await callTool(params.name, args)); | ||
| } catch (error) { | ||
| const text = error instanceof Error ? error.message : String(error); | ||
| return ok(id, { content: [{ type: "text", text: `Error: ${text}` }], isError: true }); | ||
| } | ||
| } | ||
| case "prompts/list": | ||
| return ok(id, { prompts: [] }); | ||
| default: | ||
| return err(id, -32601, `Method not found: ${message.method}`); | ||
| } | ||
| } | ||
| function readUri(params) { | ||
| if (!params || typeof params !== "object" || !("uri" in params)) return void 0; | ||
| const uri = params.uri; | ||
| return typeof uri === "string" ? uri : void 0; | ||
| } | ||
| function startMcpServer() { | ||
| const rl = createInterface({ input: process.stdin, crlfDelay: Infinity }); | ||
| rl.on("line", (line) => { | ||
| if (!line.trim()) return; | ||
| let message; | ||
| try { | ||
| message = JSON.parse(line); | ||
| } catch (error) { | ||
| process.stdout.write(`${JSON.stringify(err(0, -32700, "Parse error", error instanceof Error ? error.message : String(error)))} | ||
| `); | ||
| return; | ||
| } | ||
| void dispatch(message).then((response) => { | ||
| if (response) process.stdout.write(`${JSON.stringify(response)} | ||
| `); | ||
| }); | ||
| }); | ||
| } | ||
| export { | ||
| dispatch, | ||
| startMcpServer | ||
| }; | ||
| //# sourceMappingURL=mcp-6LFHT65R.js.map |
| {"version":3,"sources":["../src/mcp.ts"],"sourcesContent":["import { createInterface } from \"node:readline\";\nimport { createRequire } from \"node:module\";\nimport { existsSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { dirname, resolve } from \"node:path\";\nimport { PDFDocument } from \"pdf-lib\";\nimport { PageSizes, pageContent, renderFlow, type PageSize } from \"boxpdf\";\nimport { fontFamily, htmlToBoxpdf } from \"./index.js\";\nimport { injectCss, loadFaces, loadImages, resolveAssetUrl } from \"./render-file.js\";\n\n/**\n * Hand-rolled JSON-RPC / stdio MCP server for boxpdf-html. No SDK dependency —\n * keeps the package lean and the transport identical to core's `boxpdf mcp`.\n *\n * It is the batteries-included agent server: the `html_to_pdf` tool for the\n * one-shot path, plus `boxpdf_docs` and resources that surface BOTH the\n * boxpdf-html and boxpdf library docs (read from the installed `boxpdf`\n * package), so an agent never has to wire up a second server.\n */\n\nconst PROTOCOL_VERSION = \"2025-11-25\";\nconst INLINE_BYTE_CAP = 1_000_000;\nconst CORE_TEMPLATES = [\"receipt\", \"boarding-pass\", \"resume\", \"order-confirmation\", \"certificate\"] as const;\nconst DOC_TOPICS = [\"quickstart\", \"fonts\", \"themes\", \"tables\", \"pagination\", \"streaming\", \"html-api\", \"cloudflare\"] as const;\n\ntype DocTopic = (typeof DOC_TOPICS)[number];\n\ninterface JsonRpcRequest {\n jsonrpc?: \"2.0\";\n id?: string | number;\n method?: string;\n params?: unknown;\n}\n\n// ---------------------------------------------------------------------------\n// boxpdf package docs (read from node_modules/boxpdf — it ships README + templates)\n// ---------------------------------------------------------------------------\n\nfunction coreDir(): string | undefined {\n // `boxpdf`'s `exports` map blocks `require.resolve(\"boxpdf/package.json\")`,\n // so resolve the entry point and climb to the package root.\n try {\n const require = createRequire(import.meta.url);\n let dir = dirname(require.resolve(\"boxpdf\"));\n for (let i = 0; i < 8; i += 1) {\n const pkg = resolve(dir, \"package.json\");\n if (existsSync(pkg)) {\n try {\n if ((JSON.parse(readFileSync(pkg, \"utf8\")) as { name?: string }).name === \"boxpdf\") return dir;\n } catch {\n // keep climbing\n }\n }\n const up = dirname(dir);\n if (up === dir) break;\n dir = up;\n }\n } catch {\n return undefined;\n }\n return undefined;\n}\n\nfunction coreReadme(): string | undefined {\n const dir = coreDir();\n if (!dir) return undefined;\n const path = resolve(dir, \"README.md\");\n return existsSync(path) ? readFileSync(path, \"utf8\") : undefined;\n}\n\nfunction coreTemplate(name: string): string | undefined {\n const dir = coreDir();\n if (!dir) return undefined;\n const path = resolve(dir, \"templates\", `${name}.ts`);\n if (!existsSync(path)) return undefined;\n return readFileSync(path, \"utf8\")\n .replaceAll('from \"../src/index.js\"', 'from \"boxpdf\"')\n .replaceAll(`new URL(\"../fixtures/${name}.pdf\", import.meta.url)`, `new URL(\"./${name}.pdf\", import.meta.url)`)\n .replaceAll(`wrote fixtures/${name}.pdf`, `wrote ${name}.pdf`);\n}\n\nfunction htmlReadme(): string | undefined {\n try {\n const path = resolve(dirname(new URL(import.meta.url).pathname), \"..\", \"README.md\");\n return existsSync(path) ? readFileSync(path, \"utf8\") : undefined;\n } catch {\n return undefined;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Resources\n// ---------------------------------------------------------------------------\n\ninterface Resource {\n uri: string;\n name: string;\n description: string;\n mimeType: string;\n}\n\nfunction resources(): Resource[] {\n const list: Resource[] = [\n { uri: \"boxpdf-html://guide\", name: \"Agent guide\", description: \"How to turn HTML into a PDF with boxpdf-html, and when to drop to the boxpdf library.\", mimeType: \"text/markdown\" },\n { uri: \"boxpdf-html://readme\", name: \"boxpdf-html README\", description: \"Full boxpdf-html README: CLI, htmlToPdf, htmlToBoxpdf, fonts, Tailwind, supported CSS.\", mimeType: \"text/markdown\" },\n { uri: \"boxpdf://readme\", name: \"boxpdf README\", description: \"Full boxpdf README: layout DSL, themes, fonts, pagination, streaming.\", mimeType: \"text/markdown\" }\n ];\n for (const name of CORE_TEMPLATES) {\n list.push({ uri: `boxpdf://templates/${name}`, name: `${name}.ts`, description: `Copy-paste boxpdf ${name} template source.`, mimeType: \"text/typescript\" });\n }\n return list;\n}\n\nfunction readResource(uri: string): { uri: string; mimeType: string; text: string } | undefined {\n if (uri === \"boxpdf-html://guide\") return { uri, mimeType: \"text/markdown\", text: docText(\"quickstart\") + \"\\n\\n\" + docText(\"html-api\") };\n if (uri === \"boxpdf-html://readme\") {\n const text = htmlReadme();\n return text ? { uri, mimeType: \"text/markdown\", text } : undefined;\n }\n if (uri === \"boxpdf://readme\") {\n const text = coreReadme();\n return text ? { uri, mimeType: \"text/markdown\", text } : undefined;\n }\n const prefix = \"boxpdf://templates/\";\n if (uri.startsWith(prefix)) {\n const text = coreTemplate(uri.slice(prefix.length));\n return text ? { uri, mimeType: \"text/typescript\", text } : undefined;\n }\n return undefined;\n}\n\n// ---------------------------------------------------------------------------\n// Tools\n// ---------------------------------------------------------------------------\n\ninterface ToolDef {\n name: string;\n description: string;\n inputSchema: Record<string, unknown>;\n}\n\nfunction tools(): ToolDef[] {\n return [\n {\n name: \"html_to_pdf\",\n description:\n \"Render an HTML string (optionally with extra CSS) to a PDF using boxpdf-html. Supports a practical subset of CSS — flex, grid, tables, borders, colors, typography, and compiled Tailwind. No browser or JS execution. Returns the PDF (written to outputPath, or inline as a base64 resource) plus any warnings and unsupported-CSS diagnostics so you can fix the input.\",\n inputSchema: {\n type: \"object\",\n required: [\"html\"],\n properties: {\n html: { type: \"string\", description: \"HTML markup. May include <style> blocks and inline styles.\" },\n css: { type: \"string\", description: \"Extra stylesheet injected before render (e.g. compiled Tailwind output).\" },\n outputPath: { type: \"string\", description: \"Where to write the PDF (cwd-relative ok). If omitted, the PDF is returned inline as a base64 resource.\" },\n size: { type: \"string\", enum: Object.keys(PageSizes), default: \"Letter\", description: \"Page size.\" },\n margin: { type: \"number\", default: 40, description: \"Page margin in PDF points.\" },\n baseUrl: { type: \"string\", description: \"Directory or URL for resolving relative <img> and background-image URLs. Defaults to the working directory.\" },\n fonts: {\n type: \"object\",\n description: \"Optional TTF/OTF file paths to embed instead of the built-in Helvetica family.\",\n properties: {\n regular: { type: \"string\" },\n bold: { type: \"string\" },\n italic: { type: \"string\" },\n boldItalic: { type: \"string\" }\n }\n },\n allowRemote: { type: \"boolean\", default: false, description: \"Allow fetching http(s) images. Off by default to prevent SSRF.\" },\n debug: { type: \"boolean\", default: false, description: \"Draw boxpdf debug overlays.\" }\n }\n }\n },\n {\n name: \"boxpdf_docs\",\n description:\n \"Get focused guidance on using the boxpdf / boxpdf-html libraries directly when html_to_pdf is not enough — custom layout, pagination, tables, fonts, themes, streaming, the HTML API, or Cloudflare Workers.\",\n inputSchema: {\n type: \"object\",\n properties: {\n topic: { type: \"string\", enum: [...DOC_TOPICS], default: \"quickstart\", description: \"Documentation topic. Defaults to quickstart.\" }\n }\n }\n }\n ];\n}\n\ninterface ToolResult {\n content: Array<Record<string, unknown>>;\n structuredContent?: Record<string, unknown>;\n isError?: boolean;\n}\n\nasync function callTool(name: string, args: Record<string, unknown>): Promise<ToolResult> {\n if (name === \"html_to_pdf\") return htmlToPdfTool(args);\n if (name === \"boxpdf_docs\") return docsTool(args);\n return { content: [{ type: \"text\", text: `Unknown tool: ${name}` }], isError: true };\n}\n\nfunction docsTool(args: Record<string, unknown>): ToolResult {\n const topic = (typeof args.topic === \"string\" ? args.topic : \"quickstart\") as DocTopic;\n if (!DOC_TOPICS.includes(topic)) {\n return { content: [{ type: \"text\", text: `Unknown topic \"${topic}\". Available: ${DOC_TOPICS.join(\", \")}.` }], isError: true };\n }\n return { content: [{ type: \"text\", text: docText(topic) }] };\n}\n\nasync function htmlToPdfTool(args: Record<string, unknown>): Promise<ToolResult> {\n if (typeof args.html !== \"string\" || args.html.length === 0) {\n return { content: [{ type: \"text\", text: \"html_to_pdf requires a non-empty `html` string.\" }], isError: true };\n }\n const sizeKey = typeof args.size === \"string\" ? args.size : \"Letter\";\n const size = (PageSizes as Record<string, PageSize>)[sizeKey];\n if (!size) {\n return { content: [{ type: \"text\", text: `Unknown size \"${sizeKey}\". Available: ${Object.keys(PageSizes).join(\", \")}.` }], isError: true };\n }\n const margin = typeof args.margin === \"number\" ? args.margin : 40;\n const baseUrl = typeof args.baseUrl === \"string\" ? resolve(args.baseUrl) : process.cwd();\n const fonts = (args.fonts ?? {}) as Record<string, string | undefined>;\n const warnings: string[] = [];\n\n const html = injectCss(args.html, typeof args.css === \"string\" ? [args.css] : []);\n const pdf = await PDFDocument.create();\n const faces = await loadFaces(\n pdf,\n { font: fonts.regular, boldFont: fonts.bold, italicFont: fonts.italic, boldItalicFont: fonts.boldItalic },\n baseUrl\n );\n const images = await loadImages(pdf, html, baseUrl, {\n allowRemote: args.allowRemote === true,\n onWarn: (message) => warnings.push(message)\n });\n\n const result = htmlToBoxpdf(html, {\n font: faces.normal,\n boldFont: faces.bold,\n italicFont: faces.italic,\n resolveFont: fontFamily(faces.families),\n resolveImage: ({ url }) => images.get(resolveAssetUrl(url, baseUrl)),\n baseUrl,\n width: pageContent(size, margin).width,\n diagnostics: { unsupportedCss: true, sampleLimit: 5 }\n });\n warnings.push(...result.warnings);\n\n const { pages } = await renderFlow(pdf, result.nodes, { margin, size, debug: args.debug === true, warnings: false });\n const bytes = await pdf.save();\n const unsupported = result.diagnostics?.unsupportedCss ?? [];\n\n const lines: string[] = [];\n const structured: Record<string, unknown> = {\n bytes: bytes.length,\n pages: pages.length,\n warnings,\n unsupportedCss: unsupported.map(({ property, value, count }) => ({ property, value, count }))\n };\n\n const content: Array<Record<string, unknown>> = [];\n if (typeof args.outputPath === \"string\" && args.outputPath.length > 0) {\n const out = resolve(args.outputPath);\n writeFileSync(out, bytes);\n structured.outputPath = out;\n lines.push(`Wrote ${out} — ${bytes.length} bytes, ${pages.length} page${pages.length === 1 ? \"\" : \"s\"}.`);\n } else if (bytes.length > INLINE_BYTE_CAP) {\n lines.push(\n `Rendered ${bytes.length} bytes across ${pages.length} page${pages.length === 1 ? \"\" : \"s\"}, which is too large to return inline. ` +\n \"Re-run with an `outputPath` to write the file instead.\"\n );\n } else {\n lines.push(`Rendered ${bytes.length} bytes, ${pages.length} page${pages.length === 1 ? \"\" : \"s\"}.`);\n content.push({\n type: \"resource\",\n resource: { uri: \"boxpdf-html://render.pdf\", mimeType: \"application/pdf\", blob: Buffer.from(bytes).toString(\"base64\") }\n });\n }\n\n if (warnings.length > 0) lines.push(\"\", \"Warnings:\", ...warnings.map((w) => `- ${w}`));\n if (unsupported.length > 0) {\n lines.push(\"\", \"Unsupported CSS (rendered without these declarations):\");\n for (const item of unsupported) lines.push(`- ${item.property}: ${item.value} (${item.count}×)`);\n }\n\n content.unshift({ type: \"text\", text: lines.join(\"\\n\") });\n return { content, structuredContent: structured };\n}\n\n// ---------------------------------------------------------------------------\n// Docs content\n// ---------------------------------------------------------------------------\n\nfunction docText(topic: DocTopic): string {\n return DOCS[topic];\n}\n\nconst DOCS: Record<DocTopic, string> = {\n quickstart: `# boxpdf quickstart (library)\n\nShortest path to bytes — no pdf-lib import, no manual save:\n\n\\`\\`\\`ts\nimport { cleanTheme, flowToPdf, hline, hstack, standardFonts, text, vstack } from \"boxpdf\";\n\nconst bytes = await flowToPdf(async (pdf) => {\n const { font, bold } = await standardFonts(pdf); // built-in Helvetica family\n const t = cleanTheme({ font, bold });\n return [\n vstack({ gap: 8 }, text(\"Receipt #18472\", t.type.h1), text(\"May 14, 2026\", t.type.caption)),\n hline(t.hr),\n hstack({ gap: 16, justify: \"between\", width: 515 },\n text(\"Wool socks\", t.type.body),\n text(\"$28.00\", { ...t.type.body, font: bold, align: \"right\", width: 80 }))\n ];\n});\n\\`\\`\\`\n\n\\`standardFonts(pdf, family?)\\` returns \\`{ font, bold, italic, boldItalic }\\` (family: \"helvetica\" | \"times\" | \"courier\"). \\`flowToPdf(build, options?)\\` owns create + paginate + save. For multiple render passes use \\`renderFlow(pdf, nodes, options)\\` and call \\`pdf.save()\\` yourself.`,\n\n fonts: `# Fonts\n\n- Built-in (no bytes): \\`const fonts = await standardFonts(pdf)\\` → drop into any theme.\n- Custom TTF/OTF: \\`const font = await loadFont(pdf, source)\\` where source is bytes, a URL, a data URL, or a base64 string.\n- Bundled Inter: \\`import { embedInter } from \"boxpdf/inter\"; const { font, bold } = await embedInter(pdf);\\`\n- Tabular figures for money columns: \\`loadFont(pdf, bytes, { features: { tnum: true } })\\` or \\`embedInter(pdf, { tabularFigures: true })\\`.\n- Generate a bundled font module: \\`npx boxpdf font add ./Acme-Regular.ttf=regular --out src/fonts/acme.ts\\`.`,\n\n themes: `# Themes\n\n\\`cleanTheme\\`, \\`stripeTheme\\`, \\`editorialTheme\\`, \\`brutalistTheme\\`. Each accepts a \\`{ font, bold, italic? }\\` object (what \\`standardFonts\\`/\\`embedInter\\` return) or positional fonts:\n\n\\`\\`\\`ts\nconst t = cleanTheme(await standardFonts(pdf));\nconst serif = editorialTheme(await standardFonts(pdf, \"times\"));\n\\`\\`\\`\n\nEvery theme exposes \\`colors\\`, \\`spacing\\`, \\`radii\\`, \\`type\\` (display/h1/h2/h3/body/bodySmall/caption/label), \\`card\\`, \\`hr\\`.`,\n\n tables: `# Tables\n\n\\`table({ columns, rows, ... })\\` with fixed / auto / fractional columns, header & footer rows, dividers, colSpan, styled cells, per-side borders, vertical alignment, and row-level page fragmentation under \\`renderFlow\\` (headers repeat on continuation pages).\n\n\\`\\`\\`ts\ntable({\n columns: [{ width: \"auto\" }, { width: \"1fr\" }, { width: 80 }],\n header: [text(\"Qty\", t.type.label), text(\"Item\", t.type.label), text(\"Total\", t.type.label)],\n rows: items.map((i) => [text(String(i.qty)), text(i.name), text(formatCurrency(i.total), { align: \"right\" })])\n});\n\\`\\`\\``,\n\n pagination: `# Pagination\n\n\\`renderFlow(pdf, nodes[], options)\\` paginates top-level children. Top-level \\`vstack\\` nodes fragment between children; \\`table()\\` fragments between rows. Use \\`keepTogether(...)\\` or \\`breakInside: \"avoid\"\\` to keep a block atomic.\n\nOptions: \\`size\\` (default Letter; \\`PageSizes.A4\\` etc.), \\`margin\\`, \\`header\\`/\\`footer\\` (receive \\`{ pageNumber, totalPages }\\`), \\`reserveBottom\\`, document metadata (\\`title\\`/\\`author\\`/...), \\`debug\\`. For one page, \\`renderToPdf(node, options)\\` returns bytes directly.`,\n\n streaming: `# Streaming (memory-bounded)\n\nFor long documents use \\`streamFlow(pdf, writable, asyncIterable, options)\\` — it writes PDF bytes to a \\`WritableStream<Uint8Array>\\` as each page closes, keeping peak heap flat regardless of page count.\n\n\\`\\`\\`ts\nconst { readable, writable } = new TransformStream<Uint8Array, Uint8Array>();\nstreamFlow(pdf, writable, generate(font, bold)).catch(console.error);\nreturn new Response(readable, { headers: { \"content-type\": \"application/pdf\" } });\n\\`\\`\\`\n\nAll \\`embedFont\\`/\\`embedPng\\`/\\`embedJpg\\` calls must finish before \\`streamFlow\\`. \\`totalPages\\` is unavailable in headers/footers when streaming — use \\`renderFlow\\` if you need \"Page X of Y\". For Node, wrap a \\`stream.Writable\\` with \\`nodeAdapter\\`.`,\n\n \"html-api\": `# HTML → PDF (boxpdf-html, as a library)\n\nOne call to bytes (fonts default to Helvetica):\n\n\\`\\`\\`ts\nimport { htmlToPdf } from \"boxpdf-html\";\nconst bytes = await htmlToPdf(\"<h1>Invoice</h1><p>Thanks!</p>\");\n\\`\\`\\`\n\nFor the nodes, warnings, and diagnostics (full control), use \\`htmlToBoxpdf\\` + \\`renderFlow\\`:\n\n\\`\\`\\`ts\nimport { fontFamily, htmlToBoxpdf } from \"boxpdf-html\";\nimport { renderFlow } from \"boxpdf\";\nconst result = htmlToBoxpdf(html, { font, boldFont, resolveFont: fontFamily({ Inter: { normal: font, bold: boldFont } }), width: 532 });\nawait renderFlow(pdf, result.nodes, { margin: 40 });\n\\`\\`\\`\n\n\\`width\\` is the CSS containing-block width in points (Letter − 2×margin). Supported CSS is a practical subset (flex, grid, tables, borders, type, Tailwind output); pass \\`diagnostics: { unsupportedCss: true }\\` to see what was dropped.`,\n\n cloudflare: `# Cloudflare Workers / edge\n\nBoth boxpdf and \\`boxpdf/inter\\` run on Workers without \\`nodejs_compat\\`. No headless browser, WASM, or native deps.\n\n\\`\\`\\`ts\nimport { cleanTheme, flowToPdf, standardFonts, text } from \"boxpdf\";\n\nexport default {\n async fetch() {\n const bytes = await flowToPdf(async (pdf) => {\n const t = cleanTheme(await standardFonts(pdf));\n return [text(\"Generated at the edge.\", t.type.body)];\n });\n return new Response(bytes, { headers: { \"content-type\": \"application/pdf\" } });\n }\n};\n\\`\\`\\``\n};\n\n// ---------------------------------------------------------------------------\n// JSON-RPC dispatch\n// ---------------------------------------------------------------------------\n\nfunction ok(id: string | number, value: unknown): Record<string, unknown> {\n return { jsonrpc: \"2.0\", id, result: value };\n}\n\nfunction err(id: string | number, code: number, message: string, data?: unknown): Record<string, unknown> {\n return { jsonrpc: \"2.0\", id, error: { code, message, ...(data === undefined ? {} : { data }) } };\n}\n\n/**\n * Handle one JSON-RPC request and return the response (or undefined for\n * notifications / messages without an id). Pure and side-effect-free except\n * for the `html_to_pdf` tool's file I/O — exported for tests.\n */\nexport async function dispatch(message: JsonRpcRequest): Promise<Record<string, unknown> | undefined> {\n if (!message.method || message.id === undefined) return undefined;\n const id = message.id;\n\n switch (message.method) {\n case \"initialize\":\n return ok(id, {\n protocolVersion: PROTOCOL_VERSION,\n capabilities: { resources: {}, tools: {} },\n serverInfo: { name: \"boxpdf-html\", title: \"boxpdf-html\", version: \"1.0.0\", description: \"HTML-to-PDF tool plus boxpdf library docs and templates.\" },\n instructions:\n \"Call html_to_pdf to render HTML (and optional CSS) to a PDF. Read its warnings and unsupportedCss to fix the input. Call boxpdf_docs (or read the resources) when you need to build PDFs with the boxpdf library directly.\"\n });\n case \"ping\":\n return ok(id, {});\n case \"resources/list\":\n return ok(id, { resources: resources() });\n case \"resources/read\": {\n const uri = readUri(message.params);\n if (!uri) return err(id, -32602, \"Missing resource URI\");\n const resource = readResource(uri);\n if (!resource) return err(id, -32002, \"Resource not found\", { uri });\n return ok(id, { contents: [resource] });\n }\n case \"resources/templates/list\":\n return ok(id, { resourceTemplates: [] });\n case \"tools/list\":\n return ok(id, { tools: tools() });\n case \"tools/call\": {\n const params = (message.params ?? {}) as { name?: unknown; arguments?: unknown };\n if (typeof params.name !== \"string\") return err(id, -32602, \"Missing tool name\");\n const args = (params.arguments ?? {}) as Record<string, unknown>;\n try {\n return ok(id, await callTool(params.name, args));\n } catch (error) {\n const text = error instanceof Error ? error.message : String(error);\n return ok(id, { content: [{ type: \"text\", text: `Error: ${text}` }], isError: true });\n }\n }\n case \"prompts/list\":\n return ok(id, { prompts: [] });\n default:\n return err(id, -32601, `Method not found: ${message.method}`);\n }\n}\n\nfunction readUri(params: unknown): string | undefined {\n if (!params || typeof params !== \"object\" || !(\"uri\" in params)) return undefined;\n const uri = (params as { uri?: unknown }).uri;\n return typeof uri === \"string\" ? uri : undefined;\n}\n\nexport function startMcpServer(): void {\n const rl = createInterface({ input: process.stdin, crlfDelay: Infinity });\n rl.on(\"line\", (line) => {\n if (!line.trim()) return;\n let message: JsonRpcRequest;\n try {\n message = JSON.parse(line) as JsonRpcRequest;\n } catch (error) {\n process.stdout.write(`${JSON.stringify(err(0, -32700, \"Parse error\", error instanceof Error ? error.message : String(error)))}\\n`);\n return;\n }\n void dispatch(message).then((response) => {\n if (response) process.stdout.write(`${JSON.stringify(response)}\\n`);\n });\n });\n}\n"],"mappings":"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","names":["require"]} |
+107
-8
@@ -5,15 +5,27 @@ #!/usr/bin/env node | ||
| loadFaces, | ||
| loadImageUrls, | ||
| loadImages, | ||
| resolveAssetUrl | ||
| } from "./chunk-2RDJ5N3O.js"; | ||
| } from "./chunk-44K6D5SS.js"; | ||
| import { | ||
| fontFamily, | ||
| htmlToBoxpdf | ||
| } from "./chunk-QSE4JV4S.js"; | ||
| htmlToBoxpdf, | ||
| streamHtmlToPdf | ||
| } from "./chunk-MQ2YACWX.js"; | ||
| // src/cli.ts | ||
| import { readFileSync, writeFileSync } from "fs"; | ||
| import { dirname, resolve } from "path"; | ||
| import { | ||
| createReadStream, | ||
| createWriteStream, | ||
| mkdtempSync, | ||
| readFileSync, | ||
| renameSync, | ||
| rmSync, | ||
| writeFileSync | ||
| } from "fs"; | ||
| import { pipeline } from "stream/promises"; | ||
| import { tmpdir } from "os"; | ||
| import { basename, dirname, join, resolve } from "path"; | ||
| import { PDFDocument } from "pdf-lib"; | ||
| import { renderFlow, savePdf } from "boxpdf"; | ||
| import { nodeAdapter, renderFlow, savePdf } from "boxpdf"; | ||
@@ -55,2 +67,3 @@ // src/password-env.ts | ||
| --profile Print render phase timings. | ||
| --stream Use bounded-memory two-pass HTML and PDF streaming. | ||
| --password-env <name> Encrypt with the password stored in this environment variable. | ||
@@ -62,2 +75,4 @@ -h, --help Show this help. | ||
| boxpdf-html invoice.html invoice.pdf --css dist/tailwind.css | ||
| boxpdf-html archive.html archive.pdf --stream | ||
| type archive.html | boxpdf-html - archive.pdf --stream | ||
| BOXPDF_PASSWORD='open me' boxpdf-html invoice.html invoice.pdf --password-env BOXPDF_PASSWORD | ||
@@ -76,3 +91,3 @@ boxpdf-html invoice.html invoice.pdf --font ./Inter.ttf --bold-font ./Inter-Bold.ttf | ||
| if (argv[0] === "mcp") { | ||
| const { startMcpServer } = await import("./mcp-TEV4EDKR.js"); | ||
| const { startMcpServer } = await import("./mcp-6LFHT65R.js"); | ||
| startMcpServer(); | ||
@@ -88,2 +103,11 @@ return; | ||
| const baseUrl = options.baseUrl ? resolve(options.baseUrl) : inputPath ? dirname(inputPath) : process.cwd(); | ||
| if (options.stream) { | ||
| await renderStreamed( | ||
| { ...options, input: options.input, output: options.output }, | ||
| inputPath, | ||
| baseUrl, | ||
| password | ||
| ); | ||
| return; | ||
| } | ||
| const html = injectCss(readInput(options.input), options.css.map((path) => readFileSync(resolve(path), "utf8"))); | ||
@@ -120,3 +144,4 @@ const pdf = await PDFDocument.create(); | ||
| unsupportedCss: false, | ||
| profile: false | ||
| profile: false, | ||
| stream: false | ||
| }; | ||
@@ -175,2 +200,5 @@ for (let i = 0; i < args.length; i += 1) { | ||
| break; | ||
| case "--stream": | ||
| options.stream = true; | ||
| break; | ||
| case "--password-env": | ||
@@ -185,2 +213,73 @@ options.passwordEnv = next(); | ||
| } | ||
| async function renderStreamed(options, inputPath, baseUrl, password) { | ||
| const startedAt = performance.now(); | ||
| const inputTemp = inputPath ? void 0 : mkdtempSync(join(tmpdir(), "boxpdf-html-input-")); | ||
| const sourcePath = inputPath ?? join(inputTemp, "stdin.htm"); | ||
| const outputPath = resolve(options.output); | ||
| const outputTemp = mkdtempSync(join(dirname(outputPath), `.${basename(outputPath)}-`)); | ||
| const partialPath = join(outputTemp, "output.pdf"); | ||
| try { | ||
| if (!inputPath) await pipeline(process.stdin, createWriteStream(sourcePath)); | ||
| const stylesheets = options.css.map((path) => readFileSync(resolve(path), "utf8")); | ||
| const openInput = fileSource(sourcePath, stylesheets); | ||
| const pdf = await PDFDocument.create(); | ||
| const faces = await loadFaces(pdf, options, baseUrl); | ||
| let images = /* @__PURE__ */ new Map(); | ||
| const result = await streamHtmlToPdf(openInput, nodeAdapter(createWriteStream(partialPath)), { | ||
| pdf, | ||
| font: faces.normal, | ||
| boldFont: faces.bold, | ||
| italicFont: faces.italic, | ||
| preloadFonts: faceFonts(faces), | ||
| resolveFont: fontFamily(faces.families), | ||
| resolveImage: ({ url }) => images.get(resolveAssetUrl(url, baseUrl)), | ||
| baseUrl, | ||
| width: options.width ?? Math.max(0, 612 - options.margin * 2), | ||
| margin: options.margin, | ||
| debug: options.debug, | ||
| warnings: true, | ||
| diagnostics: options.unsupportedCss ? { unsupportedCss: true, sampleLimit: 5 } : void 0, | ||
| encryption: password === void 0 ? void 0 : { password }, | ||
| prepare: async (preflight) => { | ||
| images = await loadImageUrls(pdf, preflight.assetUrls, baseUrl, { | ||
| allowRemote: true, | ||
| onWarn: (message) => console.warn(`boxpdf-html: ${message}`) | ||
| }); | ||
| } | ||
| }); | ||
| for (const warning of result.warnings) console.warn(`boxpdf-html: ${warning}`); | ||
| if (options.unsupportedCss) printUnsupportedCss(result.diagnostics?.unsupportedCss ?? []); | ||
| renameSync(partialPath, outputPath); | ||
| if (options.profile) { | ||
| console.error( | ||
| `[profile] stream ${(performance.now() - startedAt).toFixed(1)}ms, ${result.pageCount} pages, ${result.preflight.htmlBytes} HTML bytes, max ${result.dom.maxBufferedNodes} DOM nodes buffered` | ||
| ); | ||
| } | ||
| } finally { | ||
| rmSync(outputTemp, { recursive: true, force: true }); | ||
| if (inputTemp) rmSync(inputTemp, { recursive: true, force: true }); | ||
| } | ||
| } | ||
| function fileSource(path, stylesheets) { | ||
| const injected = stylesheets.length === 0 ? void 0 : new TextEncoder().encode(`<style> | ||
| ${stylesheets.join("\n")} | ||
| </style>`); | ||
| return async function* openInput() { | ||
| for await (const chunk of createReadStream(path)) yield chunk; | ||
| if (injected) yield injected; | ||
| }; | ||
| } | ||
| function faceFonts(faces) { | ||
| const fonts = /* @__PURE__ */ new Set([faces.normal, faces.bold, faces.italic, faces.boldItalic]); | ||
| for (const family of Object.values(faces.families)) { | ||
| if ("embedder" in family) { | ||
| fonts.add(family); | ||
| continue; | ||
| } | ||
| for (const font of Object.values(family)) { | ||
| if (font) fonts.add(font); | ||
| } | ||
| } | ||
| return [...fonts]; | ||
| } | ||
| function readInput(input) { | ||
@@ -187,0 +286,0 @@ if (input === "-") return readFileSync(0, "utf8"); |
+1
-1
@@ -1,1 +0,1 @@ | ||
| {"version":3,"sources":["../src/cli.ts","../src/password-env.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport { readFileSync, writeFileSync } from \"node:fs\";\nimport { dirname, resolve } from \"node:path\";\nimport { PDFDocument } from \"pdf-lib\";\nimport { renderFlow, savePdf } from \"boxpdf\";\nimport { fontFamily, htmlToBoxpdf } from \"./index.js\";\nimport { passwordFromEnvironment } from \"./password-env.js\";\nimport { injectCss, loadFaces, loadImages, resolveAssetUrl } from \"./render-file.js\";\n\ninterface CliOptions {\n input?: string;\n output?: string;\n css: string[];\n baseUrl?: string;\n font?: string;\n boldFont?: string;\n italicFont?: string;\n boldItalicFont?: string;\n families: string[];\n width?: number;\n margin: number;\n debug: boolean;\n unsupportedCss: boolean;\n profile: boolean;\n passwordEnv?: string;\n}\n\nconst help = `boxpdf-html\n\nUsage:\n boxpdf-html <input.html> <output.pdf> [options]\n boxpdf-html - <output.pdf> [options]\n boxpdf-html mcp # start the MCP server (stdio)\n\nOptions:\n --css <file> Inject an extra stylesheet before rendering. Repeatable.\n --base-url <dir-or-url> Base path for relative images and background URLs.\n --font <file> Default normal TTF/OTF font.\n --bold-font <file> Default bold TTF/OTF font.\n --italic-font <file> Default italic TTF/OTF font.\n --bold-italic-font <file> Default bold italic TTF/OTF font.\n --font-family <mapping> Map a CSS family to loaded font files. Repeatable.\n Example: Inter=normal:Inter.ttf,bold:Inter-Bold.ttf\n --width <pt> CSS containing block width in PDF points.\n --margin <pt> Page margin for renderFlow. Default: 40.\n --debug Draw boxpdf debug overlays.\n --unsupported-css Print aggregated unsupported CSS diagnostics.\n --profile Print render phase timings.\n --password-env <name> Encrypt with the password stored in this environment variable.\n -h, --help Show this help.\n\nExamples:\n boxpdf-html invoice.html invoice.pdf\n boxpdf-html invoice.html invoice.pdf --css dist/tailwind.css\n BOXPDF_PASSWORD='open me' boxpdf-html invoice.html invoice.pdf --password-env BOXPDF_PASSWORD\n boxpdf-html invoice.html invoice.pdf --font ./Inter.ttf --bold-font ./Inter-Bold.ttf\n boxpdf-html invoice.html invoice.pdf \\\\\n --font-family 'Inter=normal:Inter.ttf,bold:Inter-Bold.ttf,italic:Inter-Italic.ttf'\n`;\n\nmain().catch((error: unknown) => {\n const message = error instanceof Error ? error.message : String(error);\n console.error(`boxpdf-html: ${message}`);\n process.exit(1);\n});\n\nasync function main(): Promise<void> {\n const argv = process.argv.slice(2);\n if (argv[0] === \"mcp\") {\n const { startMcpServer } = await import(\"./mcp.js\");\n startMcpServer();\n return;\n }\n\n const options = parseArgs(argv);\n if (!options.input || !options.output) {\n printHelpAndExit(options.input || options.output ? 1 : 0);\n }\n const password = passwordFromEnvironment(options.passwordEnv);\n\n const inputPath = options.input === \"-\" ? undefined : resolve(options.input);\n const baseUrl = options.baseUrl ? resolve(options.baseUrl) : inputPath ? dirname(inputPath) : process.cwd();\n const html = injectCss(readInput(options.input), options.css.map((path) => readFileSync(resolve(path), \"utf8\")));\n const pdf = await PDFDocument.create();\n const faces = await loadFaces(pdf, options, baseUrl);\n const images = await loadImages(pdf, html, baseUrl, {\n allowRemote: true,\n onWarn: (message) => console.warn(`boxpdf-html: ${message}`)\n });\n\n const result = htmlToBoxpdf(html, {\n font: faces.normal,\n boldFont: faces.bold,\n italicFont: faces.italic,\n resolveFont: fontFamily(faces.families),\n resolveImage: ({ url }) => images.get(resolveAssetUrl(url, baseUrl)),\n baseUrl,\n width: options.width ?? Math.max(0, 612 - options.margin * 2),\n diagnostics: options.unsupportedCss ? { unsupportedCss: true, sampleLimit: 5 } : undefined,\n profile: options.profile ? (event) => console.error(`[profile] ${event.phase} ${event.elapsedMs.toFixed(1)}ms`) : undefined\n });\n\n for (const warning of result.warnings) console.warn(`boxpdf-html: ${warning}`);\n if (options.unsupportedCss) printUnsupportedCss(result.diagnostics?.unsupportedCss ?? []);\n\n await renderFlow(pdf, result.nodes, { margin: options.margin, debug: options.debug });\n const bytes = password === undefined\n ? await pdf.save()\n : await savePdf(pdf, { encryption: { password } });\n writeFileSync(resolve(options.output), bytes);\n}\n\nfunction parseArgs(args: string[]): CliOptions {\n const options: CliOptions = {\n css: [],\n families: [],\n margin: 40,\n debug: false,\n unsupportedCss: false,\n profile: false\n };\n\n for (let i = 0; i < args.length; i += 1) {\n const arg = args[i]!;\n if (arg === \"-h\" || arg === \"--help\" || arg === \"help\") printHelpAndExit(0);\n if (!arg.startsWith(\"-\") || arg === \"-\") {\n if (!options.input) options.input = arg;\n else if (!options.output) options.output = arg;\n else fail(`unexpected argument \"${arg}\"`);\n continue;\n }\n\n const next = (): string => {\n const value = args[i + 1];\n if (!value) fail(`${arg} requires a value`);\n i += 1;\n return value;\n };\n\n switch (arg) {\n case \"--css\":\n options.css.push(next());\n break;\n case \"--base-url\":\n options.baseUrl = next();\n break;\n case \"--font\":\n options.font = next();\n break;\n case \"--bold-font\":\n options.boldFont = next();\n break;\n case \"--italic-font\":\n options.italicFont = next();\n break;\n case \"--bold-italic-font\":\n options.boldItalicFont = next();\n break;\n case \"--font-family\":\n options.families.push(next());\n break;\n case \"--width\":\n options.width = parseNumber(next(), arg);\n break;\n case \"--margin\":\n options.margin = parseNumber(next(), arg);\n break;\n case \"--debug\":\n options.debug = true;\n break;\n case \"--unsupported-css\":\n options.unsupportedCss = true;\n break;\n case \"--profile\":\n options.profile = true;\n break;\n case \"--password-env\":\n options.passwordEnv = next();\n break;\n default:\n fail(`unknown option \"${arg}\"`);\n }\n }\n\n return options;\n}\n\nfunction readInput(input: string): string {\n if (input === \"-\") return readFileSync(0, \"utf8\");\n return readFileSync(resolve(input), \"utf8\");\n}\n\nfunction printUnsupportedCss(items: Array<{ property: string; value: string; count: number; samples?: string[] }>): void {\n if (items.length === 0) return;\n console.error(\"Unsupported CSS:\");\n for (const item of items) {\n console.error(`- ${item.property}: ${item.value} (${item.count})`);\n for (const sample of item.samples ?? []) console.error(` ${sample}`);\n }\n}\n\nfunction parseNumber(value: string, option: string): number {\n const parsed = Number(value);\n if (!Number.isFinite(parsed) || parsed < 0) fail(`${option} must be a non-negative number`);\n return parsed;\n}\n\nfunction printHelpAndExit(code: number): never {\n console.log(help);\n process.exit(code);\n}\n\nfunction fail(message: string): never {\n console.error(`boxpdf-html: ${message}`);\n process.exit(1);\n}\n","export function passwordFromEnvironment(\n name: string | undefined,\n environment: NodeJS.ProcessEnv = process.env\n): string | undefined {\n if (name === undefined) return undefined;\n const password = environment[name];\n if (password === undefined) {\n throw new Error(`environment variable \"${name}\" named by --password-env is not set`);\n }\n if (password.length === 0) {\n throw new Error(`environment variable \"${name}\" named by --password-env is empty`);\n }\n return password;\n}\n"],"mappings":"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","names":[]} | ||
| {"version":3,"sources":["../src/cli.ts","../src/password-env.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport {\n createReadStream,\n createWriteStream,\n mkdtempSync,\n readFileSync,\n renameSync,\n rmSync,\n writeFileSync\n} from \"node:fs\";\nimport { pipeline } from \"node:stream/promises\";\nimport { tmpdir } from \"node:os\";\nimport { basename, dirname, join, resolve } from \"node:path\";\nimport { PDFDocument, type PDFFont, type PDFImage } from \"pdf-lib\";\nimport { nodeAdapter, renderFlow, savePdf } from \"boxpdf\";\nimport { fontFamily, htmlToBoxpdf, streamHtmlToPdf, type HtmlStreamSource } from \"./index.js\";\nimport { passwordFromEnvironment } from \"./password-env.js\";\nimport {\n injectCss,\n loadFaces,\n loadImages,\n loadImageUrls,\n resolveAssetUrl,\n type LoadedFaces\n} from \"./render-file.js\";\n\ninterface CliOptions {\n input?: string;\n output?: string;\n css: string[];\n baseUrl?: string;\n font?: string;\n boldFont?: string;\n italicFont?: string;\n boldItalicFont?: string;\n families: string[];\n width?: number;\n margin: number;\n debug: boolean;\n unsupportedCss: boolean;\n profile: boolean;\n stream: boolean;\n passwordEnv?: string;\n}\n\nconst help = `boxpdf-html\n\nUsage:\n boxpdf-html <input.html> <output.pdf> [options]\n boxpdf-html - <output.pdf> [options]\n boxpdf-html mcp # start the MCP server (stdio)\n\nOptions:\n --css <file> Inject an extra stylesheet before rendering. Repeatable.\n --base-url <dir-or-url> Base path for relative images and background URLs.\n --font <file> Default normal TTF/OTF font.\n --bold-font <file> Default bold TTF/OTF font.\n --italic-font <file> Default italic TTF/OTF font.\n --bold-italic-font <file> Default bold italic TTF/OTF font.\n --font-family <mapping> Map a CSS family to loaded font files. Repeatable.\n Example: Inter=normal:Inter.ttf,bold:Inter-Bold.ttf\n --width <pt> CSS containing block width in PDF points.\n --margin <pt> Page margin for renderFlow. Default: 40.\n --debug Draw boxpdf debug overlays.\n --unsupported-css Print aggregated unsupported CSS diagnostics.\n --profile Print render phase timings.\n --stream Use bounded-memory two-pass HTML and PDF streaming.\n --password-env <name> Encrypt with the password stored in this environment variable.\n -h, --help Show this help.\n\nExamples:\n boxpdf-html invoice.html invoice.pdf\n boxpdf-html invoice.html invoice.pdf --css dist/tailwind.css\n boxpdf-html archive.html archive.pdf --stream\n type archive.html | boxpdf-html - archive.pdf --stream\n BOXPDF_PASSWORD='open me' boxpdf-html invoice.html invoice.pdf --password-env BOXPDF_PASSWORD\n boxpdf-html invoice.html invoice.pdf --font ./Inter.ttf --bold-font ./Inter-Bold.ttf\n boxpdf-html invoice.html invoice.pdf \\\\\n --font-family 'Inter=normal:Inter.ttf,bold:Inter-Bold.ttf,italic:Inter-Italic.ttf'\n`;\n\nmain().catch((error: unknown) => {\n const message = error instanceof Error ? error.message : String(error);\n console.error(`boxpdf-html: ${message}`);\n process.exit(1);\n});\n\nasync function main(): Promise<void> {\n const argv = process.argv.slice(2);\n if (argv[0] === \"mcp\") {\n const { startMcpServer } = await import(\"./mcp.js\");\n startMcpServer();\n return;\n }\n\n const options = parseArgs(argv);\n if (!options.input || !options.output) {\n printHelpAndExit(options.input || options.output ? 1 : 0);\n }\n const password = passwordFromEnvironment(options.passwordEnv);\n\n const inputPath = options.input === \"-\" ? undefined : resolve(options.input);\n const baseUrl = options.baseUrl ? resolve(options.baseUrl) : inputPath ? dirname(inputPath) : process.cwd();\n if (options.stream) {\n await renderStreamed(\n { ...options, input: options.input, output: options.output },\n inputPath,\n baseUrl,\n password\n );\n return;\n }\n const html = injectCss(readInput(options.input), options.css.map((path) => readFileSync(resolve(path), \"utf8\")));\n const pdf = await PDFDocument.create();\n const faces = await loadFaces(pdf, options, baseUrl);\n const images = await loadImages(pdf, html, baseUrl, {\n allowRemote: true,\n onWarn: (message) => console.warn(`boxpdf-html: ${message}`)\n });\n\n const result = htmlToBoxpdf(html, {\n font: faces.normal,\n boldFont: faces.bold,\n italicFont: faces.italic,\n resolveFont: fontFamily(faces.families),\n resolveImage: ({ url }) => images.get(resolveAssetUrl(url, baseUrl)),\n baseUrl,\n width: options.width ?? Math.max(0, 612 - options.margin * 2),\n diagnostics: options.unsupportedCss ? { unsupportedCss: true, sampleLimit: 5 } : undefined,\n profile: options.profile ? (event) => console.error(`[profile] ${event.phase} ${event.elapsedMs.toFixed(1)}ms`) : undefined\n });\n\n for (const warning of result.warnings) console.warn(`boxpdf-html: ${warning}`);\n if (options.unsupportedCss) printUnsupportedCss(result.diagnostics?.unsupportedCss ?? []);\n\n await renderFlow(pdf, result.nodes, { margin: options.margin, debug: options.debug });\n const bytes = password === undefined\n ? await pdf.save()\n : await savePdf(pdf, { encryption: { password } });\n writeFileSync(resolve(options.output), bytes);\n}\n\nfunction parseArgs(args: string[]): CliOptions {\n const options: CliOptions = {\n css: [],\n families: [],\n margin: 40,\n debug: false,\n unsupportedCss: false,\n profile: false,\n stream: false\n };\n\n for (let i = 0; i < args.length; i += 1) {\n const arg = args[i]!;\n if (arg === \"-h\" || arg === \"--help\" || arg === \"help\") printHelpAndExit(0);\n if (!arg.startsWith(\"-\") || arg === \"-\") {\n if (!options.input) options.input = arg;\n else if (!options.output) options.output = arg;\n else fail(`unexpected argument \"${arg}\"`);\n continue;\n }\n\n const next = (): string => {\n const value = args[i + 1];\n if (!value) fail(`${arg} requires a value`);\n i += 1;\n return value;\n };\n\n switch (arg) {\n case \"--css\":\n options.css.push(next());\n break;\n case \"--base-url\":\n options.baseUrl = next();\n break;\n case \"--font\":\n options.font = next();\n break;\n case \"--bold-font\":\n options.boldFont = next();\n break;\n case \"--italic-font\":\n options.italicFont = next();\n break;\n case \"--bold-italic-font\":\n options.boldItalicFont = next();\n break;\n case \"--font-family\":\n options.families.push(next());\n break;\n case \"--width\":\n options.width = parseNumber(next(), arg);\n break;\n case \"--margin\":\n options.margin = parseNumber(next(), arg);\n break;\n case \"--debug\":\n options.debug = true;\n break;\n case \"--unsupported-css\":\n options.unsupportedCss = true;\n break;\n case \"--profile\":\n options.profile = true;\n break;\n case \"--stream\":\n options.stream = true;\n break;\n case \"--password-env\":\n options.passwordEnv = next();\n break;\n default:\n fail(`unknown option \"${arg}\"`);\n }\n }\n\n return options;\n}\n\nasync function renderStreamed(\n options: CliOptions & { input: string; output: string },\n inputPath: string | undefined,\n baseUrl: string,\n password: string | undefined\n): Promise<void> {\n const startedAt = performance.now();\n const inputTemp = inputPath ? undefined : mkdtempSync(join(tmpdir(), \"boxpdf-html-input-\"));\n const sourcePath = inputPath ?? join(inputTemp!, \"stdin.htm\");\n const outputPath = resolve(options.output);\n const outputTemp = mkdtempSync(join(dirname(outputPath), `.${basename(outputPath)}-`));\n const partialPath = join(outputTemp, \"output.pdf\");\n\n try {\n if (!inputPath) await pipeline(process.stdin, createWriteStream(sourcePath));\n const stylesheets = options.css.map((path) => readFileSync(resolve(path), \"utf8\"));\n const openInput = fileSource(sourcePath, stylesheets);\n const pdf = await PDFDocument.create();\n const faces = await loadFaces(pdf, options, baseUrl);\n let images = new Map<string, PDFImage>();\n const result = await streamHtmlToPdf(openInput, nodeAdapter(createWriteStream(partialPath)), {\n pdf,\n font: faces.normal,\n boldFont: faces.bold,\n italicFont: faces.italic,\n preloadFonts: faceFonts(faces),\n resolveFont: fontFamily(faces.families),\n resolveImage: ({ url }) => images.get(resolveAssetUrl(url, baseUrl)),\n baseUrl,\n width: options.width ?? Math.max(0, 612 - options.margin * 2),\n margin: options.margin,\n debug: options.debug,\n warnings: true,\n diagnostics: options.unsupportedCss ? { unsupportedCss: true, sampleLimit: 5 } : undefined,\n encryption: password === undefined ? undefined : { password },\n prepare: async (preflight) => {\n images = await loadImageUrls(pdf, preflight.assetUrls, baseUrl, {\n allowRemote: true,\n onWarn: (message) => console.warn(`boxpdf-html: ${message}`)\n });\n }\n });\n for (const warning of result.warnings) console.warn(`boxpdf-html: ${warning}`);\n if (options.unsupportedCss) printUnsupportedCss(result.diagnostics?.unsupportedCss ?? []);\n renameSync(partialPath, outputPath);\n if (options.profile) {\n console.error(\n `[profile] stream ${(performance.now() - startedAt).toFixed(1)}ms, ` +\n `${result.pageCount} pages, ${result.preflight.htmlBytes} HTML bytes, ` +\n `max ${result.dom.maxBufferedNodes} DOM nodes buffered`\n );\n }\n } finally {\n rmSync(outputTemp, { recursive: true, force: true });\n if (inputTemp) rmSync(inputTemp, { recursive: true, force: true });\n }\n}\n\nfunction fileSource(path: string, stylesheets: string[]): HtmlStreamSource {\n const injected = stylesheets.length === 0\n ? undefined\n : new TextEncoder().encode(`<style>\\n${stylesheets.join(\"\\n\")}\\n</style>`);\n return async function* openInput() {\n for await (const chunk of createReadStream(path)) yield chunk;\n if (injected) yield injected;\n };\n}\n\nfunction faceFonts(faces: LoadedFaces): PDFFont[] {\n const fonts = new Set<PDFFont>([faces.normal, faces.bold, faces.italic, faces.boldItalic]);\n for (const family of Object.values(faces.families)) {\n if (\"embedder\" in family) {\n fonts.add(family);\n continue;\n }\n for (const font of Object.values(family)) {\n if (font) fonts.add(font);\n }\n }\n return [...fonts];\n}\n\nfunction readInput(input: string): string {\n if (input === \"-\") return readFileSync(0, \"utf8\");\n return readFileSync(resolve(input), \"utf8\");\n}\n\nfunction printUnsupportedCss(items: Array<{ property: string; value: string; count: number; samples?: string[] }>): void {\n if (items.length === 0) return;\n console.error(\"Unsupported CSS:\");\n for (const item of items) {\n console.error(`- ${item.property}: ${item.value} (${item.count})`);\n for (const sample of item.samples ?? []) console.error(` ${sample}`);\n }\n}\n\nfunction parseNumber(value: string, option: string): number {\n const parsed = Number(value);\n if (!Number.isFinite(parsed) || parsed < 0) fail(`${option} must be a non-negative number`);\n return parsed;\n}\n\nfunction printHelpAndExit(code: number): never {\n console.log(help);\n process.exit(code);\n}\n\nfunction fail(message: string): never {\n console.error(`boxpdf-html: ${message}`);\n process.exit(1);\n}\n","export function passwordFromEnvironment(\n name: string | undefined,\n environment: NodeJS.ProcessEnv = process.env\n): string | undefined {\n if (name === undefined) return undefined;\n const password = environment[name];\n if (password === undefined) {\n throw new Error(`environment variable \"${name}\" named by --password-env is not set`);\n }\n if (password.length === 0) {\n throw new Error(`environment variable \"${name}\" named by --password-env is empty`);\n }\n return password;\n}\n"],"mappings":"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","names":[]} |
+60
-2
@@ -1,2 +0,2 @@ | ||
| import { RGB, Node, EdgesInput, PageSize } from 'boxpdf'; | ||
| import { RGB, Node, EdgesInput, PageSize, StreamFlowOptions } from 'boxpdf'; | ||
| import { PDFFont, PDFImage, PDFDocument } from 'pdf-lib'; | ||
@@ -59,2 +59,8 @@ | ||
| parent?: HtmlElementNode; | ||
| /** Internal marker for bounded fragments of one logical streamed element. */ | ||
| streamContinuation?: { | ||
| id: string; | ||
| final: boolean; | ||
| first: boolean; | ||
| }; | ||
| } | ||
@@ -144,4 +150,56 @@ type HtmlNode = HtmlTextNode | HtmlElementNode; | ||
| interface StreamDomStats { | ||
| emittedRoots: number; | ||
| maxOpenDepth: number; | ||
| maxPendingRoots: number; | ||
| maxBufferedNodes: number; | ||
| } | ||
| interface HtmlPreflight { | ||
| stylesheets: string[]; | ||
| assetUrls: Set<string>; | ||
| glyphs: Set<string>; | ||
| htmlBytes: number; | ||
| } | ||
| type HtmlStreamSource = () => AsyncIterable<string | Uint8Array>; | ||
| interface StreamHtmlToPdfOptions extends HtmlToBoxpdfOptions { | ||
| pdf: PDFDocument; | ||
| /** | ||
| * Every custom font that may be selected by resolveFont. The preflight glyph | ||
| * set is encoded into these fonts before streamFlow freezes PDF resources. | ||
| */ | ||
| preloadFonts?: PDFFont[]; | ||
| margin?: StreamFlowOptions["margin"]; | ||
| size?: PageSize; | ||
| debug?: boolean; | ||
| warnings?: boolean; | ||
| /** Completed children retained per ordinary streamed wrapper. Default 64. */ | ||
| fragmentChildren?: number; | ||
| /** Hard cap for nodes retained inside atomic layout contexts. */ | ||
| maxBufferedNodes?: number; | ||
| /** Hard cap for one uninterrupted UTF-8 text node. */ | ||
| maxTextBytes?: number; | ||
| /** | ||
| * Runs after the resource preflight and before PDF output starts. Use this | ||
| * to embed images discovered by the first pass. | ||
| */ | ||
| prepare?: (preflight: HtmlPreflight) => void | Promise<void>; | ||
| encryption?: StreamFlowOptions["encryption"]; | ||
| } | ||
| interface StreamHtmlToPdfResult { | ||
| pageCount: number; | ||
| preflight: HtmlPreflight; | ||
| dom: StreamDomStats; | ||
| warnings: string[]; | ||
| diagnostics?: HtmlDiagnostics; | ||
| } | ||
| /** | ||
| * Convert a reopenable HTML byte source to PDF without retaining the complete | ||
| * source, DOM, box tree, or PDF page list. | ||
| */ | ||
| declare function streamHtmlToPdf(openInput: HtmlStreamSource, writable: WritableStream<Uint8Array>, options: StreamHtmlToPdfOptions): Promise<StreamHtmlToPdfResult>; | ||
| declare function htmlToBoxpdf(html: string, options: HtmlToBoxpdfOptions): RenderResult; | ||
| export { type FontFamilyFace, type FontFamilyMap, type FontStyle, type FontWeight, type HtmlDiagnostics, type HtmlDiagnosticsOptions, type HtmlFontRequest, type HtmlFontResolver, type HtmlProfileCallback, type HtmlProfileEvent, type HtmlToBoxpdfOptions, type HtmlToPdfOptions, type HtmlUnsupportedCss, type ParsedHtml, type RenderResult, fontFamily, htmlToBoxpdf, htmlToPdf, parseHtml }; | ||
| export { type FontFamilyFace, type FontFamilyMap, type FontStyle, type FontWeight, type HtmlDiagnostics, type HtmlDiagnosticsOptions, type HtmlFontRequest, type HtmlFontResolver, type HtmlProfileCallback, type HtmlProfileEvent, type HtmlStreamSource, type HtmlToBoxpdfOptions, type HtmlToPdfOptions, type HtmlUnsupportedCss, type ParsedHtml, type RenderResult, type StreamHtmlToPdfOptions, type StreamHtmlToPdfResult, fontFamily, htmlToBoxpdf, htmlToPdf, parseHtml, streamHtmlToPdf }; |
+60
-2
@@ -1,2 +0,2 @@ | ||
| import { RGB, Node, EdgesInput, PageSize } from 'boxpdf'; | ||
| import { RGB, Node, EdgesInput, PageSize, StreamFlowOptions } from 'boxpdf'; | ||
| import { PDFFont, PDFImage, PDFDocument } from 'pdf-lib'; | ||
@@ -59,2 +59,8 @@ | ||
| parent?: HtmlElementNode; | ||
| /** Internal marker for bounded fragments of one logical streamed element. */ | ||
| streamContinuation?: { | ||
| id: string; | ||
| final: boolean; | ||
| first: boolean; | ||
| }; | ||
| } | ||
@@ -144,4 +150,56 @@ type HtmlNode = HtmlTextNode | HtmlElementNode; | ||
| interface StreamDomStats { | ||
| emittedRoots: number; | ||
| maxOpenDepth: number; | ||
| maxPendingRoots: number; | ||
| maxBufferedNodes: number; | ||
| } | ||
| interface HtmlPreflight { | ||
| stylesheets: string[]; | ||
| assetUrls: Set<string>; | ||
| glyphs: Set<string>; | ||
| htmlBytes: number; | ||
| } | ||
| type HtmlStreamSource = () => AsyncIterable<string | Uint8Array>; | ||
| interface StreamHtmlToPdfOptions extends HtmlToBoxpdfOptions { | ||
| pdf: PDFDocument; | ||
| /** | ||
| * Every custom font that may be selected by resolveFont. The preflight glyph | ||
| * set is encoded into these fonts before streamFlow freezes PDF resources. | ||
| */ | ||
| preloadFonts?: PDFFont[]; | ||
| margin?: StreamFlowOptions["margin"]; | ||
| size?: PageSize; | ||
| debug?: boolean; | ||
| warnings?: boolean; | ||
| /** Completed children retained per ordinary streamed wrapper. Default 64. */ | ||
| fragmentChildren?: number; | ||
| /** Hard cap for nodes retained inside atomic layout contexts. */ | ||
| maxBufferedNodes?: number; | ||
| /** Hard cap for one uninterrupted UTF-8 text node. */ | ||
| maxTextBytes?: number; | ||
| /** | ||
| * Runs after the resource preflight and before PDF output starts. Use this | ||
| * to embed images discovered by the first pass. | ||
| */ | ||
| prepare?: (preflight: HtmlPreflight) => void | Promise<void>; | ||
| encryption?: StreamFlowOptions["encryption"]; | ||
| } | ||
| interface StreamHtmlToPdfResult { | ||
| pageCount: number; | ||
| preflight: HtmlPreflight; | ||
| dom: StreamDomStats; | ||
| warnings: string[]; | ||
| diagnostics?: HtmlDiagnostics; | ||
| } | ||
| /** | ||
| * Convert a reopenable HTML byte source to PDF without retaining the complete | ||
| * source, DOM, box tree, or PDF page list. | ||
| */ | ||
| declare function streamHtmlToPdf(openInput: HtmlStreamSource, writable: WritableStream<Uint8Array>, options: StreamHtmlToPdfOptions): Promise<StreamHtmlToPdfResult>; | ||
| declare function htmlToBoxpdf(html: string, options: HtmlToBoxpdfOptions): RenderResult; | ||
| export { type FontFamilyFace, type FontFamilyMap, type FontStyle, type FontWeight, type HtmlDiagnostics, type HtmlDiagnosticsOptions, type HtmlFontRequest, type HtmlFontResolver, type HtmlProfileCallback, type HtmlProfileEvent, type HtmlToBoxpdfOptions, type HtmlToPdfOptions, type HtmlUnsupportedCss, type ParsedHtml, type RenderResult, fontFamily, htmlToBoxpdf, htmlToPdf, parseHtml }; | ||
| export { type FontFamilyFace, type FontFamilyMap, type FontStyle, type FontWeight, type HtmlDiagnostics, type HtmlDiagnosticsOptions, type HtmlFontRequest, type HtmlFontResolver, type HtmlProfileCallback, type HtmlProfileEvent, type HtmlStreamSource, type HtmlToBoxpdfOptions, type HtmlToPdfOptions, type HtmlUnsupportedCss, type ParsedHtml, type RenderResult, type StreamHtmlToPdfOptions, type StreamHtmlToPdfResult, fontFamily, htmlToBoxpdf, htmlToPdf, parseHtml, streamHtmlToPdf }; |
+5
-3
@@ -5,4 +5,5 @@ import { | ||
| htmlToPdf, | ||
| parseHtml | ||
| } from "./chunk-QSE4JV4S.js"; | ||
| parseHtml, | ||
| streamHtmlToPdf | ||
| } from "./chunk-MQ2YACWX.js"; | ||
| export { | ||
@@ -12,4 +13,5 @@ fontFamily, | ||
| htmlToPdf, | ||
| parseHtml | ||
| parseHtml, | ||
| streamHtmlToPdf | ||
| }; | ||
| //# sourceMappingURL=index.js.map |
+3
-2
| { | ||
| "name": "boxpdf-html", | ||
| "version": "1.5.0", | ||
| "version": "1.6.0", | ||
| "mcpName": "io.github.earonesty/boxpdf-html", | ||
@@ -46,5 +46,6 @@ "description": "Readable HTML-to-PDF translator built on boxpdf.", | ||
| "dependencies": { | ||
| "boxpdf": "^1.11.0", | ||
| "boxpdf": "^1.12.0", | ||
| "css-tree": "^3.1.0", | ||
| "parse5": "^8.0.0", | ||
| "parse5-sax-parser": "^8.0.0", | ||
| "pdf-lib": "^1.17.1" | ||
@@ -51,0 +52,0 @@ }, |
+44
-0
@@ -24,2 +24,14 @@ # boxpdf-html | ||
| For very large inputs, add `--stream`: | ||
| ```sh | ||
| npx boxpdf-html archive.html archive.pdf --stream | ||
| type archive.html | npx boxpdf-html - archive.pdf --stream | ||
| ``` | ||
| Streaming makes two bounded passes over the HTML: one for CSS, fonts, and | ||
| images, then one for incremental layout and PDF output. Stdin is spooled to a | ||
| temporary file so it can be reopened. The output replaces its destination only | ||
| after a successful conversion. This path requires `boxpdf` 1.12.0 or newer. | ||
| With PDF 2.0 AES-256 password encryption: | ||
@@ -56,2 +68,3 @@ | ||
| boxpdf-html input.html output.pdf --profile | ||
| boxpdf-html input.html output.pdf --stream | ||
| ``` | ||
@@ -151,2 +164,33 @@ | ||
| ### `streamHtmlToPdf` — bounded large-document conversion | ||
| `streamHtmlToPdf` accepts a function that reopens the HTML for each of its two | ||
| passes and writes PDF bytes incrementally. Embed fonts before calling it; use | ||
| `prepare` to embed images found by the resource preflight before output begins. | ||
| ```ts | ||
| import { createReadStream, createWriteStream } from "node:fs"; | ||
| import { PDFDocument, StandardFonts } from "pdf-lib"; | ||
| import { nodeAdapter } from "boxpdf"; | ||
| import { streamHtmlToPdf } from "boxpdf-html"; | ||
| const pdf = await PDFDocument.create(); | ||
| const font = await pdf.embedFont(StandardFonts.Helvetica); | ||
| const result = await streamHtmlToPdf( | ||
| () => createReadStream("archive.html"), | ||
| nodeAdapter(createWriteStream("archive.pdf")), | ||
| { pdf, font, width: 532, margin: 40 } | ||
| ); | ||
| console.log(result.pageCount, result.dom.maxBufferedNodes); | ||
| ``` | ||
| Ordinary block wrappers and tables are released in bounded continuation | ||
| fragments. Atomic layouts such as flex/grid, positioned or transformed | ||
| containers, and single uninterrupted text nodes have explicit safety caps and | ||
| fail with a useful error when they cannot be streamed safely. Selectors whose | ||
| meaning depends on sibling position conservatively disable wrapper | ||
| fragmentation. | ||
| ## Fonts | ||
@@ -153,0 +197,0 @@ |
| // src/render-file.ts | ||
| import { existsSync, readFileSync } from "fs"; | ||
| import { isAbsolute, resolve } from "path"; | ||
| import { StandardFonts } from "pdf-lib"; | ||
| import { loadFont, loadImage } from "boxpdf"; | ||
| function injectCss(html, stylesheets) { | ||
| if (stylesheets.length === 0) return html; | ||
| const style = `<style> | ||
| ${stylesheets.join("\n")} | ||
| </style>`; | ||
| if (/<\/head>/i.test(html)) return html.replace(/<\/head>/i, `${style} | ||
| </head>`); | ||
| return `${style} | ||
| ${html}`; | ||
| } | ||
| async function loadFaces(pdf, spec, baseUrl) { | ||
| const normal = spec.font ? await loadFont(pdf, readFileSync(resolve(spec.font))) : await pdf.embedFont(StandardFonts.Helvetica); | ||
| const bold = spec.boldFont ? await loadFont(pdf, readFileSync(resolve(spec.boldFont))) : await pdf.embedFont(StandardFonts.HelveticaBold); | ||
| const italic = spec.italicFont ? await loadFont(pdf, readFileSync(resolve(spec.italicFont))) : await pdf.embedFont(StandardFonts.HelveticaOblique); | ||
| const boldItalic = spec.boldItalicFont ? await loadFont(pdf, readFileSync(resolve(spec.boldItalicFont))) : await pdf.embedFont(StandardFonts.HelveticaBoldOblique); | ||
| const faces = { normal, bold, italic, boldItalic }; | ||
| const families = { | ||
| Helvetica: faces, | ||
| Arial: faces, | ||
| "sans-serif": faces, | ||
| serif: faces, | ||
| monospace: faces | ||
| }; | ||
| for (const mapping of spec.families ?? []) { | ||
| const [name, familySpec] = splitOnce(mapping, "="); | ||
| if (!name || !familySpec) throw new Error(`invalid --font-family "${mapping}"`); | ||
| families[name.trim()] = await loadFamily(pdf, familySpec, baseUrl); | ||
| } | ||
| return { normal, bold, italic, families }; | ||
| } | ||
| async function loadFamily(pdf, spec, baseUrl) { | ||
| const out = {}; | ||
| for (const part of spec.split(",")) { | ||
| const [rawKey, rawPath] = splitOnce(part, ":"); | ||
| if (!rawKey || !rawPath) throw new Error(`invalid font family face "${part}"`); | ||
| const key = rawKey.trim(); | ||
| if (!["normal", "bold", "italic", "boldItalic"].includes(key) && !/^\d+$/.test(key)) { | ||
| throw new Error(`invalid font face key "${key}"`); | ||
| } | ||
| out[key] = await loadFont(pdf, readFileSync(resolveAssetUrl(rawPath.trim(), baseUrl))); | ||
| } | ||
| return out; | ||
| } | ||
| async function loadImages(pdf, html, baseUrl, options = {}) { | ||
| const images = /* @__PURE__ */ new Map(); | ||
| for (const url of imageUrls(html)) { | ||
| const resolved = resolveAssetUrl(url, baseUrl); | ||
| if (images.has(resolved)) continue; | ||
| try { | ||
| images.set(resolved, await loadImage(pdf, assetSource(resolved, options.allowRemote ?? false))); | ||
| } catch (error) { | ||
| const message = error instanceof Error ? error.message : String(error); | ||
| options.onWarn?.(`image "${url}" did not load: ${message}`); | ||
| } | ||
| } | ||
| return images; | ||
| } | ||
| function imageUrls(source) { | ||
| const urls = []; | ||
| for (const match of source.matchAll(/url\(\s*(?:"([^"]+)"|'([^']+)'|([^)]*?))\s*\)/gi)) { | ||
| const url = (match[1] ?? match[2] ?? match[3])?.trim(); | ||
| if (url) urls.push(url); | ||
| } | ||
| for (const match of source.matchAll(/<(?:img|source)\b[^>]*\bsrc\s*=\s*(?:"([^"]+)"|'([^']+)'|([^\s>]+))/gi)) { | ||
| const url = (match[1] ?? match[2] ?? match[3])?.trim(); | ||
| if (url) urls.push(url); | ||
| } | ||
| return urls; | ||
| } | ||
| function resolveAssetUrl(url, baseUrl) { | ||
| if (/^(https?:|data:)/i.test(url)) return url; | ||
| if (url.startsWith("file://")) return new URL(url).pathname; | ||
| if (/^[a-z]+:\/\//i.test(url)) return url; | ||
| return isAbsolute(url) ? url : resolve(baseUrl, url); | ||
| } | ||
| function assetSource(resolved, allowRemote) { | ||
| if (/^(https?:)/i.test(resolved)) { | ||
| if (!allowRemote) throw new Error(`remote fetch blocked (allowRemote is off): ${resolved}`); | ||
| return resolved; | ||
| } | ||
| if (/^data:/i.test(resolved)) return resolved; | ||
| if (!existsSync(resolved)) throw new Error(`file not found: ${resolved}`); | ||
| return readFileSync(resolved); | ||
| } | ||
| function splitOnce(value, separator) { | ||
| const index = value.indexOf(separator); | ||
| if (index === -1) return [value, void 0]; | ||
| return [value.slice(0, index), value.slice(index + separator.length)]; | ||
| } | ||
| export { | ||
| injectCss, | ||
| loadFaces, | ||
| loadImages, | ||
| resolveAssetUrl | ||
| }; | ||
| //# sourceMappingURL=chunk-2RDJ5N3O.js.map |
| {"version":3,"sources":["../src/render-file.ts"],"sourcesContent":["import { existsSync, readFileSync } from \"node:fs\";\nimport { isAbsolute, resolve } from \"node:path\";\nimport { PDFDocument, StandardFonts, type PDFFont, type PDFImage } from \"pdf-lib\";\nimport { loadFont, loadImage } from \"boxpdf\";\nimport { fontFamily, type FontFamilyMap } from \"./font.js\";\n\n/**\n * Shared, filesystem-aware rendering helpers used by both the `boxpdf-html`\n * CLI and the MCP server. These throw `Error` on bad input (the CLI's\n * top-level handler turns that into a `boxpdf-html: <message>` exit; the MCP\n * server turns it into a tool error result).\n */\n\nexport interface FaceSpec {\n /** Path to a regular-weight TTF/OTF. Falls back to built-in Helvetica. */\n font?: string;\n /** Path to a bold TTF/OTF. Falls back to Helvetica-Bold. */\n boldFont?: string;\n /** Path to an italic TTF/OTF. Falls back to Helvetica-Oblique. */\n italicFont?: string;\n /** Path to a bold-italic TTF/OTF. Falls back to Helvetica-BoldOblique. */\n boldItalicFont?: string;\n /** Repeatable `Family=normal:a.ttf,bold:b.ttf` mappings (CLI form). */\n families?: string[];\n}\n\nexport interface LoadedFaces {\n normal: PDFFont;\n bold: PDFFont;\n italic: PDFFont;\n families: FontFamilyMap;\n}\n\nexport function injectCss(html: string, stylesheets: string[]): string {\n if (stylesheets.length === 0) return html;\n const style = `<style>\\n${stylesheets.join(\"\\n\")}\\n</style>`;\n if (/<\\/head>/i.test(html)) return html.replace(/<\\/head>/i, `${style}\\n</head>`);\n return `${style}\\n${html}`;\n}\n\nexport async function loadFaces(pdf: PDFDocument, spec: FaceSpec, baseUrl: string): Promise<LoadedFaces> {\n const normal = spec.font ? await loadFont(pdf, readFileSync(resolve(spec.font))) : await pdf.embedFont(StandardFonts.Helvetica);\n const bold = spec.boldFont ? await loadFont(pdf, readFileSync(resolve(spec.boldFont))) : await pdf.embedFont(StandardFonts.HelveticaBold);\n const italic = spec.italicFont ? await loadFont(pdf, readFileSync(resolve(spec.italicFont))) : await pdf.embedFont(StandardFonts.HelveticaOblique);\n const boldItalic = spec.boldItalicFont\n ? await loadFont(pdf, readFileSync(resolve(spec.boldItalicFont)))\n : await pdf.embedFont(StandardFonts.HelveticaBoldOblique);\n\n const faces = { normal, bold, italic, boldItalic };\n const families: FontFamilyMap = {\n Helvetica: faces,\n Arial: faces,\n \"sans-serif\": faces,\n serif: faces,\n monospace: faces\n };\n\n for (const mapping of spec.families ?? []) {\n const [name, familySpec] = splitOnce(mapping, \"=\");\n if (!name || !familySpec) throw new Error(`invalid --font-family \"${mapping}\"`);\n families[name.trim()] = await loadFamily(pdf, familySpec, baseUrl);\n }\n\n return { normal, bold, italic, families };\n}\n\nasync function loadFamily(pdf: PDFDocument, spec: string, baseUrl: string): Promise<FontFamilyMap[string]> {\n const out: Exclude<FontFamilyMap[string], PDFFont> = {};\n for (const part of spec.split(\",\")) {\n const [rawKey, rawPath] = splitOnce(part, \":\");\n if (!rawKey || !rawPath) throw new Error(`invalid font family face \"${part}\"`);\n const key = rawKey.trim();\n if (![\"normal\", \"bold\", \"italic\", \"boldItalic\"].includes(key) && !/^\\d+$/.test(key)) {\n throw new Error(`invalid font face key \"${key}\"`);\n }\n out[key as keyof typeof out] = await loadFont(pdf, readFileSync(resolveAssetUrl(rawPath.trim(), baseUrl)));\n }\n return out;\n}\n\nexport interface LoadImagesOptions {\n /** Allow fetching http(s) image URLs. Off by default to prevent SSRF. */\n allowRemote?: boolean;\n /** Called with a human-readable message when an image fails to load. */\n onWarn?: (message: string) => void;\n}\n\nexport async function loadImages(\n pdf: PDFDocument,\n html: string,\n baseUrl: string,\n options: LoadImagesOptions = {}\n): Promise<Map<string, PDFImage>> {\n const images = new Map<string, PDFImage>();\n for (const url of imageUrls(html)) {\n const resolved = resolveAssetUrl(url, baseUrl);\n if (images.has(resolved)) continue;\n try {\n images.set(resolved, await loadImage(pdf, assetSource(resolved, options.allowRemote ?? false)));\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n options.onWarn?.(`image \"${url}\" did not load: ${message}`);\n }\n }\n return images;\n}\n\nexport function imageUrls(source: string): string[] {\n const urls: string[] = [];\n for (const match of source.matchAll(/url\\(\\s*(?:\"([^\"]+)\"|'([^']+)'|([^)]*?))\\s*\\)/gi)) {\n const url = (match[1] ?? match[2] ?? match[3])?.trim();\n if (url) urls.push(url);\n }\n for (const match of source.matchAll(/<(?:img|source)\\b[^>]*\\bsrc\\s*=\\s*(?:\"([^\"]+)\"|'([^']+)'|([^\\s>]+))/gi)) {\n const url = (match[1] ?? match[2] ?? match[3])?.trim();\n if (url) urls.push(url);\n }\n return urls;\n}\n\nexport function resolveAssetUrl(url: string, baseUrl: string): string {\n if (/^(https?:|data:)/i.test(url)) return url;\n if (url.startsWith(\"file://\")) return new URL(url).pathname;\n if (/^[a-z]+:\\/\\//i.test(url)) return url;\n return isAbsolute(url) ? url : resolve(baseUrl, url);\n}\n\nfunction assetSource(resolved: string, allowRemote: boolean): string | Uint8Array {\n if (/^(https?:)/i.test(resolved)) {\n if (!allowRemote) throw new Error(`remote fetch blocked (allowRemote is off): ${resolved}`);\n return resolved;\n }\n if (/^data:/i.test(resolved)) return resolved;\n if (!existsSync(resolved)) throw new Error(`file not found: ${resolved}`);\n return readFileSync(resolved);\n}\n\nexport function splitOnce(value: string, separator: string): [string, string] | [string, undefined] {\n const index = value.indexOf(separator);\n if (index === -1) return [value, undefined];\n return [value.slice(0, index), value.slice(index + separator.length)];\n}\n"],"mappings":";AAAA,SAAS,YAAY,oBAAoB;AACzC,SAAS,YAAY,eAAe;AACpC,SAAsB,qBAAkD;AACxE,SAAS,UAAU,iBAAiB;AA8B7B,SAAS,UAAU,MAAc,aAA+B;AACrE,MAAI,YAAY,WAAW,EAAG,QAAO;AACrC,QAAM,QAAQ;AAAA,EAAY,YAAY,KAAK,IAAI,CAAC;AAAA;AAChD,MAAI,YAAY,KAAK,IAAI,EAAG,QAAO,KAAK,QAAQ,aAAa,GAAG,KAAK;AAAA,QAAW;AAChF,SAAO,GAAG,KAAK;AAAA,EAAK,IAAI;AAC1B;AAEA,eAAsB,UAAU,KAAkB,MAAgB,SAAuC;AACvG,QAAM,SAAS,KAAK,OAAO,MAAM,SAAS,KAAK,aAAa,QAAQ,KAAK,IAAI,CAAC,CAAC,IAAI,MAAM,IAAI,UAAU,cAAc,SAAS;AAC9H,QAAM,OAAO,KAAK,WAAW,MAAM,SAAS,KAAK,aAAa,QAAQ,KAAK,QAAQ,CAAC,CAAC,IAAI,MAAM,IAAI,UAAU,cAAc,aAAa;AACxI,QAAM,SAAS,KAAK,aAAa,MAAM,SAAS,KAAK,aAAa,QAAQ,KAAK,UAAU,CAAC,CAAC,IAAI,MAAM,IAAI,UAAU,cAAc,gBAAgB;AACjJ,QAAM,aAAa,KAAK,iBACpB,MAAM,SAAS,KAAK,aAAa,QAAQ,KAAK,cAAc,CAAC,CAAC,IAC9D,MAAM,IAAI,UAAU,cAAc,oBAAoB;AAE1D,QAAM,QAAQ,EAAE,QAAQ,MAAM,QAAQ,WAAW;AACjD,QAAM,WAA0B;AAAA,IAC9B,WAAW;AAAA,IACX,OAAO;AAAA,IACP,cAAc;AAAA,IACd,OAAO;AAAA,IACP,WAAW;AAAA,EACb;AAEA,aAAW,WAAW,KAAK,YAAY,CAAC,GAAG;AACzC,UAAM,CAAC,MAAM,UAAU,IAAI,UAAU,SAAS,GAAG;AACjD,QAAI,CAAC,QAAQ,CAAC,WAAY,OAAM,IAAI,MAAM,0BAA0B,OAAO,GAAG;AAC9E,aAAS,KAAK,KAAK,CAAC,IAAI,MAAM,WAAW,KAAK,YAAY,OAAO;AAAA,EACnE;AAEA,SAAO,EAAE,QAAQ,MAAM,QAAQ,SAAS;AAC1C;AAEA,eAAe,WAAW,KAAkB,MAAc,SAAiD;AACzG,QAAM,MAA+C,CAAC;AACtD,aAAW,QAAQ,KAAK,MAAM,GAAG,GAAG;AAClC,UAAM,CAAC,QAAQ,OAAO,IAAI,UAAU,MAAM,GAAG;AAC7C,QAAI,CAAC,UAAU,CAAC,QAAS,OAAM,IAAI,MAAM,6BAA6B,IAAI,GAAG;AAC7E,UAAM,MAAM,OAAO,KAAK;AACxB,QAAI,CAAC,CAAC,UAAU,QAAQ,UAAU,YAAY,EAAE,SAAS,GAAG,KAAK,CAAC,QAAQ,KAAK,GAAG,GAAG;AACnF,YAAM,IAAI,MAAM,0BAA0B,GAAG,GAAG;AAAA,IAClD;AACA,QAAI,GAAuB,IAAI,MAAM,SAAS,KAAK,aAAa,gBAAgB,QAAQ,KAAK,GAAG,OAAO,CAAC,CAAC;AAAA,EAC3G;AACA,SAAO;AACT;AASA,eAAsB,WACpB,KACA,MACA,SACA,UAA6B,CAAC,GACE;AAChC,QAAM,SAAS,oBAAI,IAAsB;AACzC,aAAW,OAAO,UAAU,IAAI,GAAG;AACjC,UAAM,WAAW,gBAAgB,KAAK,OAAO;AAC7C,QAAI,OAAO,IAAI,QAAQ,EAAG;AAC1B,QAAI;AACF,aAAO,IAAI,UAAU,MAAM,UAAU,KAAK,YAAY,UAAU,QAAQ,eAAe,KAAK,CAAC,CAAC;AAAA,IAChG,SAAS,OAAO;AACd,YAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AACrE,cAAQ,SAAS,UAAU,GAAG,mBAAmB,OAAO,EAAE;AAAA,IAC5D;AAAA,EACF;AACA,SAAO;AACT;AAEO,SAAS,UAAU,QAA0B;AAClD,QAAM,OAAiB,CAAC;AACxB,aAAW,SAAS,OAAO,SAAS,iDAAiD,GAAG;AACtF,UAAM,OAAO,MAAM,CAAC,KAAK,MAAM,CAAC,KAAK,MAAM,CAAC,IAAI,KAAK;AACrD,QAAI,IAAK,MAAK,KAAK,GAAG;AAAA,EACxB;AACA,aAAW,SAAS,OAAO,SAAS,uEAAuE,GAAG;AAC5G,UAAM,OAAO,MAAM,CAAC,KAAK,MAAM,CAAC,KAAK,MAAM,CAAC,IAAI,KAAK;AACrD,QAAI,IAAK,MAAK,KAAK,GAAG;AAAA,EACxB;AACA,SAAO;AACT;AAEO,SAAS,gBAAgB,KAAa,SAAyB;AACpE,MAAI,oBAAoB,KAAK,GAAG,EAAG,QAAO;AAC1C,MAAI,IAAI,WAAW,SAAS,EAAG,QAAO,IAAI,IAAI,GAAG,EAAE;AACnD,MAAI,gBAAgB,KAAK,GAAG,EAAG,QAAO;AACtC,SAAO,WAAW,GAAG,IAAI,MAAM,QAAQ,SAAS,GAAG;AACrD;AAEA,SAAS,YAAY,UAAkB,aAA2C;AAChF,MAAI,cAAc,KAAK,QAAQ,GAAG;AAChC,QAAI,CAAC,YAAa,OAAM,IAAI,MAAM,8CAA8C,QAAQ,EAAE;AAC1F,WAAO;AAAA,EACT;AACA,MAAI,UAAU,KAAK,QAAQ,EAAG,QAAO;AACrC,MAAI,CAAC,WAAW,QAAQ,EAAG,OAAM,IAAI,MAAM,mBAAmB,QAAQ,EAAE;AACxE,SAAO,aAAa,QAAQ;AAC9B;AAEO,SAAS,UAAU,OAAe,WAA2D;AAClG,QAAM,QAAQ,MAAM,QAAQ,SAAS;AACrC,MAAI,UAAU,GAAI,QAAO,CAAC,OAAO,MAAS;AAC1C,SAAO,CAAC,MAAM,MAAM,GAAG,KAAK,GAAG,MAAM,MAAM,QAAQ,UAAU,MAAM,CAAC;AACtE;","names":[]} |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
| import { | ||
| injectCss, | ||
| loadFaces, | ||
| loadImages, | ||
| resolveAssetUrl | ||
| } from "./chunk-2RDJ5N3O.js"; | ||
| import { | ||
| fontFamily, | ||
| htmlToBoxpdf | ||
| } from "./chunk-QSE4JV4S.js"; | ||
| // src/mcp.ts | ||
| import { createInterface } from "readline"; | ||
| import { createRequire } from "module"; | ||
| import { existsSync, readFileSync, writeFileSync } from "fs"; | ||
| import { dirname, resolve } from "path"; | ||
| import { PDFDocument } from "pdf-lib"; | ||
| import { PageSizes, pageContent, renderFlow } from "boxpdf"; | ||
| var PROTOCOL_VERSION = "2025-11-25"; | ||
| var INLINE_BYTE_CAP = 1e6; | ||
| var CORE_TEMPLATES = ["receipt", "boarding-pass", "resume", "order-confirmation", "certificate"]; | ||
| var DOC_TOPICS = ["quickstart", "fonts", "themes", "tables", "pagination", "streaming", "html-api", "cloudflare"]; | ||
| function coreDir() { | ||
| try { | ||
| const require2 = createRequire(import.meta.url); | ||
| let dir = dirname(require2.resolve("boxpdf")); | ||
| for (let i = 0; i < 8; i += 1) { | ||
| const pkg = resolve(dir, "package.json"); | ||
| if (existsSync(pkg)) { | ||
| try { | ||
| if (JSON.parse(readFileSync(pkg, "utf8")).name === "boxpdf") return dir; | ||
| } catch { | ||
| } | ||
| } | ||
| const up = dirname(dir); | ||
| if (up === dir) break; | ||
| dir = up; | ||
| } | ||
| } catch { | ||
| return void 0; | ||
| } | ||
| return void 0; | ||
| } | ||
| function coreReadme() { | ||
| const dir = coreDir(); | ||
| if (!dir) return void 0; | ||
| const path = resolve(dir, "README.md"); | ||
| return existsSync(path) ? readFileSync(path, "utf8") : void 0; | ||
| } | ||
| function coreTemplate(name) { | ||
| const dir = coreDir(); | ||
| if (!dir) return void 0; | ||
| const path = resolve(dir, "templates", `${name}.ts`); | ||
| if (!existsSync(path)) return void 0; | ||
| return readFileSync(path, "utf8").replaceAll('from "../src/index.js"', 'from "boxpdf"').replaceAll(`new URL("../fixtures/${name}.pdf", import.meta.url)`, `new URL("./${name}.pdf", import.meta.url)`).replaceAll(`wrote fixtures/${name}.pdf`, `wrote ${name}.pdf`); | ||
| } | ||
| function htmlReadme() { | ||
| try { | ||
| const path = resolve(dirname(new URL(import.meta.url).pathname), "..", "README.md"); | ||
| return existsSync(path) ? readFileSync(path, "utf8") : void 0; | ||
| } catch { | ||
| return void 0; | ||
| } | ||
| } | ||
| function resources() { | ||
| const list = [ | ||
| { uri: "boxpdf-html://guide", name: "Agent guide", description: "How to turn HTML into a PDF with boxpdf-html, and when to drop to the boxpdf library.", mimeType: "text/markdown" }, | ||
| { uri: "boxpdf-html://readme", name: "boxpdf-html README", description: "Full boxpdf-html README: CLI, htmlToPdf, htmlToBoxpdf, fonts, Tailwind, supported CSS.", mimeType: "text/markdown" }, | ||
| { uri: "boxpdf://readme", name: "boxpdf README", description: "Full boxpdf README: layout DSL, themes, fonts, pagination, streaming.", mimeType: "text/markdown" } | ||
| ]; | ||
| for (const name of CORE_TEMPLATES) { | ||
| list.push({ uri: `boxpdf://templates/${name}`, name: `${name}.ts`, description: `Copy-paste boxpdf ${name} template source.`, mimeType: "text/typescript" }); | ||
| } | ||
| return list; | ||
| } | ||
| function readResource(uri) { | ||
| if (uri === "boxpdf-html://guide") return { uri, mimeType: "text/markdown", text: docText("quickstart") + "\n\n" + docText("html-api") }; | ||
| if (uri === "boxpdf-html://readme") { | ||
| const text = htmlReadme(); | ||
| return text ? { uri, mimeType: "text/markdown", text } : void 0; | ||
| } | ||
| if (uri === "boxpdf://readme") { | ||
| const text = coreReadme(); | ||
| return text ? { uri, mimeType: "text/markdown", text } : void 0; | ||
| } | ||
| const prefix = "boxpdf://templates/"; | ||
| if (uri.startsWith(prefix)) { | ||
| const text = coreTemplate(uri.slice(prefix.length)); | ||
| return text ? { uri, mimeType: "text/typescript", text } : void 0; | ||
| } | ||
| return void 0; | ||
| } | ||
| function tools() { | ||
| return [ | ||
| { | ||
| name: "html_to_pdf", | ||
| description: "Render an HTML string (optionally with extra CSS) to a PDF using boxpdf-html. Supports a practical subset of CSS \u2014 flex, grid, tables, borders, colors, typography, and compiled Tailwind. No browser or JS execution. Returns the PDF (written to outputPath, or inline as a base64 resource) plus any warnings and unsupported-CSS diagnostics so you can fix the input.", | ||
| inputSchema: { | ||
| type: "object", | ||
| required: ["html"], | ||
| properties: { | ||
| html: { type: "string", description: "HTML markup. May include <style> blocks and inline styles." }, | ||
| css: { type: "string", description: "Extra stylesheet injected before render (e.g. compiled Tailwind output)." }, | ||
| outputPath: { type: "string", description: "Where to write the PDF (cwd-relative ok). If omitted, the PDF is returned inline as a base64 resource." }, | ||
| size: { type: "string", enum: Object.keys(PageSizes), default: "Letter", description: "Page size." }, | ||
| margin: { type: "number", default: 40, description: "Page margin in PDF points." }, | ||
| baseUrl: { type: "string", description: "Directory or URL for resolving relative <img> and background-image URLs. Defaults to the working directory." }, | ||
| fonts: { | ||
| type: "object", | ||
| description: "Optional TTF/OTF file paths to embed instead of the built-in Helvetica family.", | ||
| properties: { | ||
| regular: { type: "string" }, | ||
| bold: { type: "string" }, | ||
| italic: { type: "string" }, | ||
| boldItalic: { type: "string" } | ||
| } | ||
| }, | ||
| allowRemote: { type: "boolean", default: false, description: "Allow fetching http(s) images. Off by default to prevent SSRF." }, | ||
| debug: { type: "boolean", default: false, description: "Draw boxpdf debug overlays." } | ||
| } | ||
| } | ||
| }, | ||
| { | ||
| name: "boxpdf_docs", | ||
| description: "Get focused guidance on using the boxpdf / boxpdf-html libraries directly when html_to_pdf is not enough \u2014 custom layout, pagination, tables, fonts, themes, streaming, the HTML API, or Cloudflare Workers.", | ||
| inputSchema: { | ||
| type: "object", | ||
| properties: { | ||
| topic: { type: "string", enum: [...DOC_TOPICS], default: "quickstart", description: "Documentation topic. Defaults to quickstart." } | ||
| } | ||
| } | ||
| } | ||
| ]; | ||
| } | ||
| async function callTool(name, args) { | ||
| if (name === "html_to_pdf") return htmlToPdfTool(args); | ||
| if (name === "boxpdf_docs") return docsTool(args); | ||
| return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true }; | ||
| } | ||
| function docsTool(args) { | ||
| const topic = typeof args.topic === "string" ? args.topic : "quickstart"; | ||
| if (!DOC_TOPICS.includes(topic)) { | ||
| return { content: [{ type: "text", text: `Unknown topic "${topic}". Available: ${DOC_TOPICS.join(", ")}.` }], isError: true }; | ||
| } | ||
| return { content: [{ type: "text", text: docText(topic) }] }; | ||
| } | ||
| async function htmlToPdfTool(args) { | ||
| if (typeof args.html !== "string" || args.html.length === 0) { | ||
| return { content: [{ type: "text", text: "html_to_pdf requires a non-empty `html` string." }], isError: true }; | ||
| } | ||
| const sizeKey = typeof args.size === "string" ? args.size : "Letter"; | ||
| const size = PageSizes[sizeKey]; | ||
| if (!size) { | ||
| return { content: [{ type: "text", text: `Unknown size "${sizeKey}". Available: ${Object.keys(PageSizes).join(", ")}.` }], isError: true }; | ||
| } | ||
| const margin = typeof args.margin === "number" ? args.margin : 40; | ||
| const baseUrl = typeof args.baseUrl === "string" ? resolve(args.baseUrl) : process.cwd(); | ||
| const fonts = args.fonts ?? {}; | ||
| const warnings = []; | ||
| const html = injectCss(args.html, typeof args.css === "string" ? [args.css] : []); | ||
| const pdf = await PDFDocument.create(); | ||
| const faces = await loadFaces( | ||
| pdf, | ||
| { font: fonts.regular, boldFont: fonts.bold, italicFont: fonts.italic, boldItalicFont: fonts.boldItalic }, | ||
| baseUrl | ||
| ); | ||
| const images = await loadImages(pdf, html, baseUrl, { | ||
| allowRemote: args.allowRemote === true, | ||
| onWarn: (message) => warnings.push(message) | ||
| }); | ||
| const result = htmlToBoxpdf(html, { | ||
| font: faces.normal, | ||
| boldFont: faces.bold, | ||
| italicFont: faces.italic, | ||
| resolveFont: fontFamily(faces.families), | ||
| resolveImage: ({ url }) => images.get(resolveAssetUrl(url, baseUrl)), | ||
| baseUrl, | ||
| width: pageContent(size, margin).width, | ||
| diagnostics: { unsupportedCss: true, sampleLimit: 5 } | ||
| }); | ||
| warnings.push(...result.warnings); | ||
| const { pages } = await renderFlow(pdf, result.nodes, { margin, size, debug: args.debug === true, warnings: false }); | ||
| const bytes = await pdf.save(); | ||
| const unsupported = result.diagnostics?.unsupportedCss ?? []; | ||
| const lines = []; | ||
| const structured = { | ||
| bytes: bytes.length, | ||
| pages: pages.length, | ||
| warnings, | ||
| unsupportedCss: unsupported.map(({ property, value, count }) => ({ property, value, count })) | ||
| }; | ||
| const content = []; | ||
| if (typeof args.outputPath === "string" && args.outputPath.length > 0) { | ||
| const out = resolve(args.outputPath); | ||
| writeFileSync(out, bytes); | ||
| structured.outputPath = out; | ||
| lines.push(`Wrote ${out} \u2014 ${bytes.length} bytes, ${pages.length} page${pages.length === 1 ? "" : "s"}.`); | ||
| } else if (bytes.length > INLINE_BYTE_CAP) { | ||
| lines.push( | ||
| `Rendered ${bytes.length} bytes across ${pages.length} page${pages.length === 1 ? "" : "s"}, which is too large to return inline. Re-run with an \`outputPath\` to write the file instead.` | ||
| ); | ||
| } else { | ||
| lines.push(`Rendered ${bytes.length} bytes, ${pages.length} page${pages.length === 1 ? "" : "s"}.`); | ||
| content.push({ | ||
| type: "resource", | ||
| resource: { uri: "boxpdf-html://render.pdf", mimeType: "application/pdf", blob: Buffer.from(bytes).toString("base64") } | ||
| }); | ||
| } | ||
| if (warnings.length > 0) lines.push("", "Warnings:", ...warnings.map((w) => `- ${w}`)); | ||
| if (unsupported.length > 0) { | ||
| lines.push("", "Unsupported CSS (rendered without these declarations):"); | ||
| for (const item of unsupported) lines.push(`- ${item.property}: ${item.value} (${item.count}\xD7)`); | ||
| } | ||
| content.unshift({ type: "text", text: lines.join("\n") }); | ||
| return { content, structuredContent: structured }; | ||
| } | ||
| function docText(topic) { | ||
| return DOCS[topic]; | ||
| } | ||
| var DOCS = { | ||
| quickstart: `# boxpdf quickstart (library) | ||
| Shortest path to bytes \u2014 no pdf-lib import, no manual save: | ||
| \`\`\`ts | ||
| import { cleanTheme, flowToPdf, hline, hstack, standardFonts, text, vstack } from "boxpdf"; | ||
| const bytes = await flowToPdf(async (pdf) => { | ||
| const { font, bold } = await standardFonts(pdf); // built-in Helvetica family | ||
| const t = cleanTheme({ font, bold }); | ||
| return [ | ||
| vstack({ gap: 8 }, text("Receipt #18472", t.type.h1), text("May 14, 2026", t.type.caption)), | ||
| hline(t.hr), | ||
| hstack({ gap: 16, justify: "between", width: 515 }, | ||
| text("Wool socks", t.type.body), | ||
| text("$28.00", { ...t.type.body, font: bold, align: "right", width: 80 })) | ||
| ]; | ||
| }); | ||
| \`\`\` | ||
| \`standardFonts(pdf, family?)\` returns \`{ font, bold, italic, boldItalic }\` (family: "helvetica" | "times" | "courier"). \`flowToPdf(build, options?)\` owns create + paginate + save. For multiple render passes use \`renderFlow(pdf, nodes, options)\` and call \`pdf.save()\` yourself.`, | ||
| fonts: `# Fonts | ||
| - Built-in (no bytes): \`const fonts = await standardFonts(pdf)\` \u2192 drop into any theme. | ||
| - Custom TTF/OTF: \`const font = await loadFont(pdf, source)\` where source is bytes, a URL, a data URL, or a base64 string. | ||
| - Bundled Inter: \`import { embedInter } from "boxpdf/inter"; const { font, bold } = await embedInter(pdf);\` | ||
| - Tabular figures for money columns: \`loadFont(pdf, bytes, { features: { tnum: true } })\` or \`embedInter(pdf, { tabularFigures: true })\`. | ||
| - Generate a bundled font module: \`npx boxpdf font add ./Acme-Regular.ttf=regular --out src/fonts/acme.ts\`.`, | ||
| themes: `# Themes | ||
| \`cleanTheme\`, \`stripeTheme\`, \`editorialTheme\`, \`brutalistTheme\`. Each accepts a \`{ font, bold, italic? }\` object (what \`standardFonts\`/\`embedInter\` return) or positional fonts: | ||
| \`\`\`ts | ||
| const t = cleanTheme(await standardFonts(pdf)); | ||
| const serif = editorialTheme(await standardFonts(pdf, "times")); | ||
| \`\`\` | ||
| Every theme exposes \`colors\`, \`spacing\`, \`radii\`, \`type\` (display/h1/h2/h3/body/bodySmall/caption/label), \`card\`, \`hr\`.`, | ||
| tables: `# Tables | ||
| \`table({ columns, rows, ... })\` with fixed / auto / fractional columns, header & footer rows, dividers, colSpan, styled cells, per-side borders, vertical alignment, and row-level page fragmentation under \`renderFlow\` (headers repeat on continuation pages). | ||
| \`\`\`ts | ||
| table({ | ||
| columns: [{ width: "auto" }, { width: "1fr" }, { width: 80 }], | ||
| header: [text("Qty", t.type.label), text("Item", t.type.label), text("Total", t.type.label)], | ||
| rows: items.map((i) => [text(String(i.qty)), text(i.name), text(formatCurrency(i.total), { align: "right" })]) | ||
| }); | ||
| \`\`\``, | ||
| pagination: `# Pagination | ||
| \`renderFlow(pdf, nodes[], options)\` paginates top-level children. Top-level \`vstack\` nodes fragment between children; \`table()\` fragments between rows. Use \`keepTogether(...)\` or \`breakInside: "avoid"\` to keep a block atomic. | ||
| Options: \`size\` (default Letter; \`PageSizes.A4\` etc.), \`margin\`, \`header\`/\`footer\` (receive \`{ pageNumber, totalPages }\`), \`reserveBottom\`, document metadata (\`title\`/\`author\`/...), \`debug\`. For one page, \`renderToPdf(node, options)\` returns bytes directly.`, | ||
| streaming: `# Streaming (memory-bounded) | ||
| For long documents use \`streamFlow(pdf, writable, asyncIterable, options)\` \u2014 it writes PDF bytes to a \`WritableStream<Uint8Array>\` as each page closes, keeping peak heap flat regardless of page count. | ||
| \`\`\`ts | ||
| const { readable, writable } = new TransformStream<Uint8Array, Uint8Array>(); | ||
| streamFlow(pdf, writable, generate(font, bold)).catch(console.error); | ||
| return new Response(readable, { headers: { "content-type": "application/pdf" } }); | ||
| \`\`\` | ||
| All \`embedFont\`/\`embedPng\`/\`embedJpg\` calls must finish before \`streamFlow\`. \`totalPages\` is unavailable in headers/footers when streaming \u2014 use \`renderFlow\` if you need "Page X of Y". For Node, wrap a \`stream.Writable\` with \`nodeAdapter\`.`, | ||
| "html-api": `# HTML \u2192 PDF (boxpdf-html, as a library) | ||
| One call to bytes (fonts default to Helvetica): | ||
| \`\`\`ts | ||
| import { htmlToPdf } from "boxpdf-html"; | ||
| const bytes = await htmlToPdf("<h1>Invoice</h1><p>Thanks!</p>"); | ||
| \`\`\` | ||
| For the nodes, warnings, and diagnostics (full control), use \`htmlToBoxpdf\` + \`renderFlow\`: | ||
| \`\`\`ts | ||
| import { fontFamily, htmlToBoxpdf } from "boxpdf-html"; | ||
| import { renderFlow } from "boxpdf"; | ||
| const result = htmlToBoxpdf(html, { font, boldFont, resolveFont: fontFamily({ Inter: { normal: font, bold: boldFont } }), width: 532 }); | ||
| await renderFlow(pdf, result.nodes, { margin: 40 }); | ||
| \`\`\` | ||
| \`width\` is the CSS containing-block width in points (Letter \u2212 2\xD7margin). Supported CSS is a practical subset (flex, grid, tables, borders, type, Tailwind output); pass \`diagnostics: { unsupportedCss: true }\` to see what was dropped.`, | ||
| cloudflare: `# Cloudflare Workers / edge | ||
| Both boxpdf and \`boxpdf/inter\` run on Workers without \`nodejs_compat\`. No headless browser, WASM, or native deps. | ||
| \`\`\`ts | ||
| import { cleanTheme, flowToPdf, standardFonts, text } from "boxpdf"; | ||
| export default { | ||
| async fetch() { | ||
| const bytes = await flowToPdf(async (pdf) => { | ||
| const t = cleanTheme(await standardFonts(pdf)); | ||
| return [text("Generated at the edge.", t.type.body)]; | ||
| }); | ||
| return new Response(bytes, { headers: { "content-type": "application/pdf" } }); | ||
| } | ||
| }; | ||
| \`\`\`` | ||
| }; | ||
| function ok(id, value) { | ||
| return { jsonrpc: "2.0", id, result: value }; | ||
| } | ||
| function err(id, code, message, data) { | ||
| return { jsonrpc: "2.0", id, error: { code, message, ...data === void 0 ? {} : { data } } }; | ||
| } | ||
| async function dispatch(message) { | ||
| if (!message.method || message.id === void 0) return void 0; | ||
| const id = message.id; | ||
| switch (message.method) { | ||
| case "initialize": | ||
| return ok(id, { | ||
| protocolVersion: PROTOCOL_VERSION, | ||
| capabilities: { resources: {}, tools: {} }, | ||
| serverInfo: { name: "boxpdf-html", title: "boxpdf-html", version: "1.0.0", description: "HTML-to-PDF tool plus boxpdf library docs and templates." }, | ||
| instructions: "Call html_to_pdf to render HTML (and optional CSS) to a PDF. Read its warnings and unsupportedCss to fix the input. Call boxpdf_docs (or read the resources) when you need to build PDFs with the boxpdf library directly." | ||
| }); | ||
| case "ping": | ||
| return ok(id, {}); | ||
| case "resources/list": | ||
| return ok(id, { resources: resources() }); | ||
| case "resources/read": { | ||
| const uri = readUri(message.params); | ||
| if (!uri) return err(id, -32602, "Missing resource URI"); | ||
| const resource = readResource(uri); | ||
| if (!resource) return err(id, -32002, "Resource not found", { uri }); | ||
| return ok(id, { contents: [resource] }); | ||
| } | ||
| case "resources/templates/list": | ||
| return ok(id, { resourceTemplates: [] }); | ||
| case "tools/list": | ||
| return ok(id, { tools: tools() }); | ||
| case "tools/call": { | ||
| const params = message.params ?? {}; | ||
| if (typeof params.name !== "string") return err(id, -32602, "Missing tool name"); | ||
| const args = params.arguments ?? {}; | ||
| try { | ||
| return ok(id, await callTool(params.name, args)); | ||
| } catch (error) { | ||
| const text = error instanceof Error ? error.message : String(error); | ||
| return ok(id, { content: [{ type: "text", text: `Error: ${text}` }], isError: true }); | ||
| } | ||
| } | ||
| case "prompts/list": | ||
| return ok(id, { prompts: [] }); | ||
| default: | ||
| return err(id, -32601, `Method not found: ${message.method}`); | ||
| } | ||
| } | ||
| function readUri(params) { | ||
| if (!params || typeof params !== "object" || !("uri" in params)) return void 0; | ||
| const uri = params.uri; | ||
| return typeof uri === "string" ? uri : void 0; | ||
| } | ||
| function startMcpServer() { | ||
| const rl = createInterface({ input: process.stdin, crlfDelay: Infinity }); | ||
| rl.on("line", (line) => { | ||
| if (!line.trim()) return; | ||
| let message; | ||
| try { | ||
| message = JSON.parse(line); | ||
| } catch (error) { | ||
| process.stdout.write(`${JSON.stringify(err(0, -32700, "Parse error", error instanceof Error ? error.message : String(error)))} | ||
| `); | ||
| return; | ||
| } | ||
| void dispatch(message).then((response) => { | ||
| if (response) process.stdout.write(`${JSON.stringify(response)} | ||
| `); | ||
| }); | ||
| }); | ||
| } | ||
| export { | ||
| dispatch, | ||
| startMcpServer | ||
| }; | ||
| //# sourceMappingURL=mcp-TEV4EDKR.js.map |
| {"version":3,"sources":["../src/mcp.ts"],"sourcesContent":["import { createInterface } from \"node:readline\";\nimport { createRequire } from \"node:module\";\nimport { existsSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { dirname, resolve } from \"node:path\";\nimport { PDFDocument } from \"pdf-lib\";\nimport { PageSizes, pageContent, renderFlow, type PageSize } from \"boxpdf\";\nimport { fontFamily, htmlToBoxpdf } from \"./index.js\";\nimport { injectCss, loadFaces, loadImages, resolveAssetUrl } from \"./render-file.js\";\n\n/**\n * Hand-rolled JSON-RPC / stdio MCP server for boxpdf-html. No SDK dependency —\n * keeps the package lean and the transport identical to core's `boxpdf mcp`.\n *\n * It is the batteries-included agent server: the `html_to_pdf` tool for the\n * one-shot path, plus `boxpdf_docs` and resources that surface BOTH the\n * boxpdf-html and boxpdf library docs (read from the installed `boxpdf`\n * package), so an agent never has to wire up a second server.\n */\n\nconst PROTOCOL_VERSION = \"2025-11-25\";\nconst INLINE_BYTE_CAP = 1_000_000;\nconst CORE_TEMPLATES = [\"receipt\", \"boarding-pass\", \"resume\", \"order-confirmation\", \"certificate\"] as const;\nconst DOC_TOPICS = [\"quickstart\", \"fonts\", \"themes\", \"tables\", \"pagination\", \"streaming\", \"html-api\", \"cloudflare\"] as const;\n\ntype DocTopic = (typeof DOC_TOPICS)[number];\n\ninterface JsonRpcRequest {\n jsonrpc?: \"2.0\";\n id?: string | number;\n method?: string;\n params?: unknown;\n}\n\n// ---------------------------------------------------------------------------\n// boxpdf package docs (read from node_modules/boxpdf — it ships README + templates)\n// ---------------------------------------------------------------------------\n\nfunction coreDir(): string | undefined {\n // `boxpdf`'s `exports` map blocks `require.resolve(\"boxpdf/package.json\")`,\n // so resolve the entry point and climb to the package root.\n try {\n const require = createRequire(import.meta.url);\n let dir = dirname(require.resolve(\"boxpdf\"));\n for (let i = 0; i < 8; i += 1) {\n const pkg = resolve(dir, \"package.json\");\n if (existsSync(pkg)) {\n try {\n if ((JSON.parse(readFileSync(pkg, \"utf8\")) as { name?: string }).name === \"boxpdf\") return dir;\n } catch {\n // keep climbing\n }\n }\n const up = dirname(dir);\n if (up === dir) break;\n dir = up;\n }\n } catch {\n return undefined;\n }\n return undefined;\n}\n\nfunction coreReadme(): string | undefined {\n const dir = coreDir();\n if (!dir) return undefined;\n const path = resolve(dir, \"README.md\");\n return existsSync(path) ? readFileSync(path, \"utf8\") : undefined;\n}\n\nfunction coreTemplate(name: string): string | undefined {\n const dir = coreDir();\n if (!dir) return undefined;\n const path = resolve(dir, \"templates\", `${name}.ts`);\n if (!existsSync(path)) return undefined;\n return readFileSync(path, \"utf8\")\n .replaceAll('from \"../src/index.js\"', 'from \"boxpdf\"')\n .replaceAll(`new URL(\"../fixtures/${name}.pdf\", import.meta.url)`, `new URL(\"./${name}.pdf\", import.meta.url)`)\n .replaceAll(`wrote fixtures/${name}.pdf`, `wrote ${name}.pdf`);\n}\n\nfunction htmlReadme(): string | undefined {\n try {\n const path = resolve(dirname(new URL(import.meta.url).pathname), \"..\", \"README.md\");\n return existsSync(path) ? readFileSync(path, \"utf8\") : undefined;\n } catch {\n return undefined;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Resources\n// ---------------------------------------------------------------------------\n\ninterface Resource {\n uri: string;\n name: string;\n description: string;\n mimeType: string;\n}\n\nfunction resources(): Resource[] {\n const list: Resource[] = [\n { uri: \"boxpdf-html://guide\", name: \"Agent guide\", description: \"How to turn HTML into a PDF with boxpdf-html, and when to drop to the boxpdf library.\", mimeType: \"text/markdown\" },\n { uri: \"boxpdf-html://readme\", name: \"boxpdf-html README\", description: \"Full boxpdf-html README: CLI, htmlToPdf, htmlToBoxpdf, fonts, Tailwind, supported CSS.\", mimeType: \"text/markdown\" },\n { uri: \"boxpdf://readme\", name: \"boxpdf README\", description: \"Full boxpdf README: layout DSL, themes, fonts, pagination, streaming.\", mimeType: \"text/markdown\" }\n ];\n for (const name of CORE_TEMPLATES) {\n list.push({ uri: `boxpdf://templates/${name}`, name: `${name}.ts`, description: `Copy-paste boxpdf ${name} template source.`, mimeType: \"text/typescript\" });\n }\n return list;\n}\n\nfunction readResource(uri: string): { uri: string; mimeType: string; text: string } | undefined {\n if (uri === \"boxpdf-html://guide\") return { uri, mimeType: \"text/markdown\", text: docText(\"quickstart\") + \"\\n\\n\" + docText(\"html-api\") };\n if (uri === \"boxpdf-html://readme\") {\n const text = htmlReadme();\n return text ? { uri, mimeType: \"text/markdown\", text } : undefined;\n }\n if (uri === \"boxpdf://readme\") {\n const text = coreReadme();\n return text ? { uri, mimeType: \"text/markdown\", text } : undefined;\n }\n const prefix = \"boxpdf://templates/\";\n if (uri.startsWith(prefix)) {\n const text = coreTemplate(uri.slice(prefix.length));\n return text ? { uri, mimeType: \"text/typescript\", text } : undefined;\n }\n return undefined;\n}\n\n// ---------------------------------------------------------------------------\n// Tools\n// ---------------------------------------------------------------------------\n\ninterface ToolDef {\n name: string;\n description: string;\n inputSchema: Record<string, unknown>;\n}\n\nfunction tools(): ToolDef[] {\n return [\n {\n name: \"html_to_pdf\",\n description:\n \"Render an HTML string (optionally with extra CSS) to a PDF using boxpdf-html. Supports a practical subset of CSS — flex, grid, tables, borders, colors, typography, and compiled Tailwind. No browser or JS execution. Returns the PDF (written to outputPath, or inline as a base64 resource) plus any warnings and unsupported-CSS diagnostics so you can fix the input.\",\n inputSchema: {\n type: \"object\",\n required: [\"html\"],\n properties: {\n html: { type: \"string\", description: \"HTML markup. May include <style> blocks and inline styles.\" },\n css: { type: \"string\", description: \"Extra stylesheet injected before render (e.g. compiled Tailwind output).\" },\n outputPath: { type: \"string\", description: \"Where to write the PDF (cwd-relative ok). If omitted, the PDF is returned inline as a base64 resource.\" },\n size: { type: \"string\", enum: Object.keys(PageSizes), default: \"Letter\", description: \"Page size.\" },\n margin: { type: \"number\", default: 40, description: \"Page margin in PDF points.\" },\n baseUrl: { type: \"string\", description: \"Directory or URL for resolving relative <img> and background-image URLs. Defaults to the working directory.\" },\n fonts: {\n type: \"object\",\n description: \"Optional TTF/OTF file paths to embed instead of the built-in Helvetica family.\",\n properties: {\n regular: { type: \"string\" },\n bold: { type: \"string\" },\n italic: { type: \"string\" },\n boldItalic: { type: \"string\" }\n }\n },\n allowRemote: { type: \"boolean\", default: false, description: \"Allow fetching http(s) images. Off by default to prevent SSRF.\" },\n debug: { type: \"boolean\", default: false, description: \"Draw boxpdf debug overlays.\" }\n }\n }\n },\n {\n name: \"boxpdf_docs\",\n description:\n \"Get focused guidance on using the boxpdf / boxpdf-html libraries directly when html_to_pdf is not enough — custom layout, pagination, tables, fonts, themes, streaming, the HTML API, or Cloudflare Workers.\",\n inputSchema: {\n type: \"object\",\n properties: {\n topic: { type: \"string\", enum: [...DOC_TOPICS], default: \"quickstart\", description: \"Documentation topic. Defaults to quickstart.\" }\n }\n }\n }\n ];\n}\n\ninterface ToolResult {\n content: Array<Record<string, unknown>>;\n structuredContent?: Record<string, unknown>;\n isError?: boolean;\n}\n\nasync function callTool(name: string, args: Record<string, unknown>): Promise<ToolResult> {\n if (name === \"html_to_pdf\") return htmlToPdfTool(args);\n if (name === \"boxpdf_docs\") return docsTool(args);\n return { content: [{ type: \"text\", text: `Unknown tool: ${name}` }], isError: true };\n}\n\nfunction docsTool(args: Record<string, unknown>): ToolResult {\n const topic = (typeof args.topic === \"string\" ? args.topic : \"quickstart\") as DocTopic;\n if (!DOC_TOPICS.includes(topic)) {\n return { content: [{ type: \"text\", text: `Unknown topic \"${topic}\". Available: ${DOC_TOPICS.join(\", \")}.` }], isError: true };\n }\n return { content: [{ type: \"text\", text: docText(topic) }] };\n}\n\nasync function htmlToPdfTool(args: Record<string, unknown>): Promise<ToolResult> {\n if (typeof args.html !== \"string\" || args.html.length === 0) {\n return { content: [{ type: \"text\", text: \"html_to_pdf requires a non-empty `html` string.\" }], isError: true };\n }\n const sizeKey = typeof args.size === \"string\" ? args.size : \"Letter\";\n const size = (PageSizes as Record<string, PageSize>)[sizeKey];\n if (!size) {\n return { content: [{ type: \"text\", text: `Unknown size \"${sizeKey}\". Available: ${Object.keys(PageSizes).join(\", \")}.` }], isError: true };\n }\n const margin = typeof args.margin === \"number\" ? args.margin : 40;\n const baseUrl = typeof args.baseUrl === \"string\" ? resolve(args.baseUrl) : process.cwd();\n const fonts = (args.fonts ?? {}) as Record<string, string | undefined>;\n const warnings: string[] = [];\n\n const html = injectCss(args.html, typeof args.css === \"string\" ? [args.css] : []);\n const pdf = await PDFDocument.create();\n const faces = await loadFaces(\n pdf,\n { font: fonts.regular, boldFont: fonts.bold, italicFont: fonts.italic, boldItalicFont: fonts.boldItalic },\n baseUrl\n );\n const images = await loadImages(pdf, html, baseUrl, {\n allowRemote: args.allowRemote === true,\n onWarn: (message) => warnings.push(message)\n });\n\n const result = htmlToBoxpdf(html, {\n font: faces.normal,\n boldFont: faces.bold,\n italicFont: faces.italic,\n resolveFont: fontFamily(faces.families),\n resolveImage: ({ url }) => images.get(resolveAssetUrl(url, baseUrl)),\n baseUrl,\n width: pageContent(size, margin).width,\n diagnostics: { unsupportedCss: true, sampleLimit: 5 }\n });\n warnings.push(...result.warnings);\n\n const { pages } = await renderFlow(pdf, result.nodes, { margin, size, debug: args.debug === true, warnings: false });\n const bytes = await pdf.save();\n const unsupported = result.diagnostics?.unsupportedCss ?? [];\n\n const lines: string[] = [];\n const structured: Record<string, unknown> = {\n bytes: bytes.length,\n pages: pages.length,\n warnings,\n unsupportedCss: unsupported.map(({ property, value, count }) => ({ property, value, count }))\n };\n\n const content: Array<Record<string, unknown>> = [];\n if (typeof args.outputPath === \"string\" && args.outputPath.length > 0) {\n const out = resolve(args.outputPath);\n writeFileSync(out, bytes);\n structured.outputPath = out;\n lines.push(`Wrote ${out} — ${bytes.length} bytes, ${pages.length} page${pages.length === 1 ? \"\" : \"s\"}.`);\n } else if (bytes.length > INLINE_BYTE_CAP) {\n lines.push(\n `Rendered ${bytes.length} bytes across ${pages.length} page${pages.length === 1 ? \"\" : \"s\"}, which is too large to return inline. ` +\n \"Re-run with an `outputPath` to write the file instead.\"\n );\n } else {\n lines.push(`Rendered ${bytes.length} bytes, ${pages.length} page${pages.length === 1 ? \"\" : \"s\"}.`);\n content.push({\n type: \"resource\",\n resource: { uri: \"boxpdf-html://render.pdf\", mimeType: \"application/pdf\", blob: Buffer.from(bytes).toString(\"base64\") }\n });\n }\n\n if (warnings.length > 0) lines.push(\"\", \"Warnings:\", ...warnings.map((w) => `- ${w}`));\n if (unsupported.length > 0) {\n lines.push(\"\", \"Unsupported CSS (rendered without these declarations):\");\n for (const item of unsupported) lines.push(`- ${item.property}: ${item.value} (${item.count}×)`);\n }\n\n content.unshift({ type: \"text\", text: lines.join(\"\\n\") });\n return { content, structuredContent: structured };\n}\n\n// ---------------------------------------------------------------------------\n// Docs content\n// ---------------------------------------------------------------------------\n\nfunction docText(topic: DocTopic): string {\n return DOCS[topic];\n}\n\nconst DOCS: Record<DocTopic, string> = {\n quickstart: `# boxpdf quickstart (library)\n\nShortest path to bytes — no pdf-lib import, no manual save:\n\n\\`\\`\\`ts\nimport { cleanTheme, flowToPdf, hline, hstack, standardFonts, text, vstack } from \"boxpdf\";\n\nconst bytes = await flowToPdf(async (pdf) => {\n const { font, bold } = await standardFonts(pdf); // built-in Helvetica family\n const t = cleanTheme({ font, bold });\n return [\n vstack({ gap: 8 }, text(\"Receipt #18472\", t.type.h1), text(\"May 14, 2026\", t.type.caption)),\n hline(t.hr),\n hstack({ gap: 16, justify: \"between\", width: 515 },\n text(\"Wool socks\", t.type.body),\n text(\"$28.00\", { ...t.type.body, font: bold, align: \"right\", width: 80 }))\n ];\n});\n\\`\\`\\`\n\n\\`standardFonts(pdf, family?)\\` returns \\`{ font, bold, italic, boldItalic }\\` (family: \"helvetica\" | \"times\" | \"courier\"). \\`flowToPdf(build, options?)\\` owns create + paginate + save. For multiple render passes use \\`renderFlow(pdf, nodes, options)\\` and call \\`pdf.save()\\` yourself.`,\n\n fonts: `# Fonts\n\n- Built-in (no bytes): \\`const fonts = await standardFonts(pdf)\\` → drop into any theme.\n- Custom TTF/OTF: \\`const font = await loadFont(pdf, source)\\` where source is bytes, a URL, a data URL, or a base64 string.\n- Bundled Inter: \\`import { embedInter } from \"boxpdf/inter\"; const { font, bold } = await embedInter(pdf);\\`\n- Tabular figures for money columns: \\`loadFont(pdf, bytes, { features: { tnum: true } })\\` or \\`embedInter(pdf, { tabularFigures: true })\\`.\n- Generate a bundled font module: \\`npx boxpdf font add ./Acme-Regular.ttf=regular --out src/fonts/acme.ts\\`.`,\n\n themes: `# Themes\n\n\\`cleanTheme\\`, \\`stripeTheme\\`, \\`editorialTheme\\`, \\`brutalistTheme\\`. Each accepts a \\`{ font, bold, italic? }\\` object (what \\`standardFonts\\`/\\`embedInter\\` return) or positional fonts:\n\n\\`\\`\\`ts\nconst t = cleanTheme(await standardFonts(pdf));\nconst serif = editorialTheme(await standardFonts(pdf, \"times\"));\n\\`\\`\\`\n\nEvery theme exposes \\`colors\\`, \\`spacing\\`, \\`radii\\`, \\`type\\` (display/h1/h2/h3/body/bodySmall/caption/label), \\`card\\`, \\`hr\\`.`,\n\n tables: `# Tables\n\n\\`table({ columns, rows, ... })\\` with fixed / auto / fractional columns, header & footer rows, dividers, colSpan, styled cells, per-side borders, vertical alignment, and row-level page fragmentation under \\`renderFlow\\` (headers repeat on continuation pages).\n\n\\`\\`\\`ts\ntable({\n columns: [{ width: \"auto\" }, { width: \"1fr\" }, { width: 80 }],\n header: [text(\"Qty\", t.type.label), text(\"Item\", t.type.label), text(\"Total\", t.type.label)],\n rows: items.map((i) => [text(String(i.qty)), text(i.name), text(formatCurrency(i.total), { align: \"right\" })])\n});\n\\`\\`\\``,\n\n pagination: `# Pagination\n\n\\`renderFlow(pdf, nodes[], options)\\` paginates top-level children. Top-level \\`vstack\\` nodes fragment between children; \\`table()\\` fragments between rows. Use \\`keepTogether(...)\\` or \\`breakInside: \"avoid\"\\` to keep a block atomic.\n\nOptions: \\`size\\` (default Letter; \\`PageSizes.A4\\` etc.), \\`margin\\`, \\`header\\`/\\`footer\\` (receive \\`{ pageNumber, totalPages }\\`), \\`reserveBottom\\`, document metadata (\\`title\\`/\\`author\\`/...), \\`debug\\`. For one page, \\`renderToPdf(node, options)\\` returns bytes directly.`,\n\n streaming: `# Streaming (memory-bounded)\n\nFor long documents use \\`streamFlow(pdf, writable, asyncIterable, options)\\` — it writes PDF bytes to a \\`WritableStream<Uint8Array>\\` as each page closes, keeping peak heap flat regardless of page count.\n\n\\`\\`\\`ts\nconst { readable, writable } = new TransformStream<Uint8Array, Uint8Array>();\nstreamFlow(pdf, writable, generate(font, bold)).catch(console.error);\nreturn new Response(readable, { headers: { \"content-type\": \"application/pdf\" } });\n\\`\\`\\`\n\nAll \\`embedFont\\`/\\`embedPng\\`/\\`embedJpg\\` calls must finish before \\`streamFlow\\`. \\`totalPages\\` is unavailable in headers/footers when streaming — use \\`renderFlow\\` if you need \"Page X of Y\". For Node, wrap a \\`stream.Writable\\` with \\`nodeAdapter\\`.`,\n\n \"html-api\": `# HTML → PDF (boxpdf-html, as a library)\n\nOne call to bytes (fonts default to Helvetica):\n\n\\`\\`\\`ts\nimport { htmlToPdf } from \"boxpdf-html\";\nconst bytes = await htmlToPdf(\"<h1>Invoice</h1><p>Thanks!</p>\");\n\\`\\`\\`\n\nFor the nodes, warnings, and diagnostics (full control), use \\`htmlToBoxpdf\\` + \\`renderFlow\\`:\n\n\\`\\`\\`ts\nimport { fontFamily, htmlToBoxpdf } from \"boxpdf-html\";\nimport { renderFlow } from \"boxpdf\";\nconst result = htmlToBoxpdf(html, { font, boldFont, resolveFont: fontFamily({ Inter: { normal: font, bold: boldFont } }), width: 532 });\nawait renderFlow(pdf, result.nodes, { margin: 40 });\n\\`\\`\\`\n\n\\`width\\` is the CSS containing-block width in points (Letter − 2×margin). Supported CSS is a practical subset (flex, grid, tables, borders, type, Tailwind output); pass \\`diagnostics: { unsupportedCss: true }\\` to see what was dropped.`,\n\n cloudflare: `# Cloudflare Workers / edge\n\nBoth boxpdf and \\`boxpdf/inter\\` run on Workers without \\`nodejs_compat\\`. No headless browser, WASM, or native deps.\n\n\\`\\`\\`ts\nimport { cleanTheme, flowToPdf, standardFonts, text } from \"boxpdf\";\n\nexport default {\n async fetch() {\n const bytes = await flowToPdf(async (pdf) => {\n const t = cleanTheme(await standardFonts(pdf));\n return [text(\"Generated at the edge.\", t.type.body)];\n });\n return new Response(bytes, { headers: { \"content-type\": \"application/pdf\" } });\n }\n};\n\\`\\`\\``\n};\n\n// ---------------------------------------------------------------------------\n// JSON-RPC dispatch\n// ---------------------------------------------------------------------------\n\nfunction ok(id: string | number, value: unknown): Record<string, unknown> {\n return { jsonrpc: \"2.0\", id, result: value };\n}\n\nfunction err(id: string | number, code: number, message: string, data?: unknown): Record<string, unknown> {\n return { jsonrpc: \"2.0\", id, error: { code, message, ...(data === undefined ? {} : { data }) } };\n}\n\n/**\n * Handle one JSON-RPC request and return the response (or undefined for\n * notifications / messages without an id). Pure and side-effect-free except\n * for the `html_to_pdf` tool's file I/O — exported for tests.\n */\nexport async function dispatch(message: JsonRpcRequest): Promise<Record<string, unknown> | undefined> {\n if (!message.method || message.id === undefined) return undefined;\n const id = message.id;\n\n switch (message.method) {\n case \"initialize\":\n return ok(id, {\n protocolVersion: PROTOCOL_VERSION,\n capabilities: { resources: {}, tools: {} },\n serverInfo: { name: \"boxpdf-html\", title: \"boxpdf-html\", version: \"1.0.0\", description: \"HTML-to-PDF tool plus boxpdf library docs and templates.\" },\n instructions:\n \"Call html_to_pdf to render HTML (and optional CSS) to a PDF. Read its warnings and unsupportedCss to fix the input. Call boxpdf_docs (or read the resources) when you need to build PDFs with the boxpdf library directly.\"\n });\n case \"ping\":\n return ok(id, {});\n case \"resources/list\":\n return ok(id, { resources: resources() });\n case \"resources/read\": {\n const uri = readUri(message.params);\n if (!uri) return err(id, -32602, \"Missing resource URI\");\n const resource = readResource(uri);\n if (!resource) return err(id, -32002, \"Resource not found\", { uri });\n return ok(id, { contents: [resource] });\n }\n case \"resources/templates/list\":\n return ok(id, { resourceTemplates: [] });\n case \"tools/list\":\n return ok(id, { tools: tools() });\n case \"tools/call\": {\n const params = (message.params ?? {}) as { name?: unknown; arguments?: unknown };\n if (typeof params.name !== \"string\") return err(id, -32602, \"Missing tool name\");\n const args = (params.arguments ?? {}) as Record<string, unknown>;\n try {\n return ok(id, await callTool(params.name, args));\n } catch (error) {\n const text = error instanceof Error ? error.message : String(error);\n return ok(id, { content: [{ type: \"text\", text: `Error: ${text}` }], isError: true });\n }\n }\n case \"prompts/list\":\n return ok(id, { prompts: [] });\n default:\n return err(id, -32601, `Method not found: ${message.method}`);\n }\n}\n\nfunction readUri(params: unknown): string | undefined {\n if (!params || typeof params !== \"object\" || !(\"uri\" in params)) return undefined;\n const uri = (params as { uri?: unknown }).uri;\n return typeof uri === \"string\" ? uri : undefined;\n}\n\nexport function startMcpServer(): void {\n const rl = createInterface({ input: process.stdin, crlfDelay: Infinity });\n rl.on(\"line\", (line) => {\n if (!line.trim()) return;\n let message: JsonRpcRequest;\n try {\n message = JSON.parse(line) as JsonRpcRequest;\n } catch (error) {\n process.stdout.write(`${JSON.stringify(err(0, -32700, \"Parse error\", error instanceof Error ? error.message : String(error)))}\\n`);\n return;\n }\n void dispatch(message).then((response) => {\n if (response) process.stdout.write(`${JSON.stringify(response)}\\n`);\n });\n });\n}\n"],"mappings":"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","names":["require"]} |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
1478289
17.11%13111
19.01%351
14.33%5
25%+ Added
+ Added
Updated