@aexol/spectral 0.9.69 → 0.9.71
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/extensions/pdf/config.d.ts +3 -0
- package/dist/extensions/pdf/config.d.ts.map +1 -0
- package/dist/extensions/pdf/config.js +37 -0
- package/dist/extensions/pdf/index.d.ts +3 -0
- package/dist/extensions/pdf/index.d.ts.map +1 -0
- package/dist/extensions/pdf/index.js +19 -0
- package/dist/extensions/pdf/tools/generate.d.ts +5 -0
- package/dist/extensions/pdf/tools/generate.d.ts.map +1 -0
- package/dist/extensions/pdf/tools/generate.js +119 -0
- package/dist/extensions/pdf/tools/helpers.d.ts +4 -0
- package/dist/extensions/pdf/tools/helpers.d.ts.map +1 -0
- package/dist/extensions/pdf/tools/helpers.js +6 -0
- package/dist/extensions/pdf/tools/preview.d.ts +4 -0
- package/dist/extensions/pdf/tools/preview.d.ts.map +1 -0
- package/dist/extensions/pdf/tools/preview.js +53 -0
- package/dist/extensions/pdf/tools/render.d.ts +4 -0
- package/dist/extensions/pdf/tools/render.d.ts.map +1 -0
- package/dist/extensions/pdf/tools/render.js +111 -0
- package/dist/extensions/pdf/tools/validate.d.ts +4 -0
- package/dist/extensions/pdf/tools/validate.d.ts.map +1 -0
- package/dist/extensions/pdf/tools/validate.js +76 -0
- package/dist/extensions/pdf/types.d.ts +32 -0
- package/dist/extensions/pdf/types.d.ts.map +1 -0
- package/dist/extensions/pdf/types.js +1 -0
- package/dist/extensions/pdf/utils/html.d.ts +6 -0
- package/dist/extensions/pdf/utils/html.d.ts.map +1 -0
- package/dist/extensions/pdf/utils/html.js +19 -0
- package/dist/extensions/pdf/utils/paths.d.ts +19 -0
- package/dist/extensions/pdf/utils/paths.d.ts.map +1 -0
- package/dist/extensions/pdf/utils/paths.js +60 -0
- package/dist/extensions/pdf/utils/renderer.d.ts +21 -0
- package/dist/extensions/pdf/utils/renderer.d.ts.map +1 -0
- package/dist/extensions/pdf/utils/renderer.js +57 -0
- package/dist/extensions/pdf/utils/validate.d.ts +12 -0
- package/dist/extensions/pdf/utils/validate.d.ts.map +1 -0
- package/dist/extensions/pdf/utils/validate.js +148 -0
- package/dist/sdk/coding-agent/core/extensions/native-extensions.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/extensions/native-extensions.js +10 -0
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../../src/extensions/pdf/config.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAA6B,MAAM,YAAY,CAAC;AAyBvE,wBAAgB,aAAa,IAAI,SAAS,CAczC"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
function envBool(key, def) {
|
|
3
|
+
const v = process.env[key];
|
|
4
|
+
if (v === undefined)
|
|
5
|
+
return def;
|
|
6
|
+
return v === "1" || v.toLowerCase() === "true" || v.toLowerCase() === "yes";
|
|
7
|
+
}
|
|
8
|
+
function envInt(key, def) {
|
|
9
|
+
const v = process.env[key];
|
|
10
|
+
if (v === undefined)
|
|
11
|
+
return def;
|
|
12
|
+
const n = Number.parseInt(v, 10);
|
|
13
|
+
return Number.isNaN(n) ? def : n;
|
|
14
|
+
}
|
|
15
|
+
function envFormat(key, def) {
|
|
16
|
+
const v = process.env[key];
|
|
17
|
+
return v === "Letter" || v === "A4" ? v : def;
|
|
18
|
+
}
|
|
19
|
+
function envOrientation(key, def) {
|
|
20
|
+
const v = process.env[key];
|
|
21
|
+
return v === "landscape" || v === "portrait" ? v : def;
|
|
22
|
+
}
|
|
23
|
+
export function loadPdfConfig() {
|
|
24
|
+
return {
|
|
25
|
+
outputDir: process.env.SPECTRAL_PDF_OUTPUT_DIR ?? join(".spectral", "pdf"),
|
|
26
|
+
renderTimeoutMs: envInt("SPECTRAL_PDF_RENDER_TIMEOUT_MS", 30_000),
|
|
27
|
+
allowRemoteAssets: envBool("SPECTRAL_PDF_ALLOW_REMOTE_ASSETS", false),
|
|
28
|
+
defaultFormat: envFormat("SPECTRAL_PDF_DEFAULT_FORMAT", "A4"),
|
|
29
|
+
defaultOrientation: envOrientation("SPECTRAL_PDF_DEFAULT_ORIENTATION", "portrait"),
|
|
30
|
+
defaultMargin: {
|
|
31
|
+
top: process.env.SPECTRAL_PDF_MARGIN_TOP ?? "18mm",
|
|
32
|
+
right: process.env.SPECTRAL_PDF_MARGIN_RIGHT ?? "16mm",
|
|
33
|
+
bottom: process.env.SPECTRAL_PDF_MARGIN_BOTTOM ?? "18mm",
|
|
34
|
+
left: process.env.SPECTRAL_PDF_MARGIN_LEFT ?? "16mm",
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/extensions/pdf/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iCAAiC,CAAC;AAOpE,wBAA8B,YAAY,CAAC,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAY3E"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { loadPdfConfig } from "./config.js";
|
|
2
|
+
import { registerGenerateTool } from "./tools/generate.js";
|
|
3
|
+
import { registerPreviewTool } from "./tools/preview.js";
|
|
4
|
+
import { registerRenderTool } from "./tools/render.js";
|
|
5
|
+
import { registerValidateTool } from "./tools/validate.js";
|
|
6
|
+
export default async function pdfExtension(ext) {
|
|
7
|
+
const config = loadPdfConfig();
|
|
8
|
+
try {
|
|
9
|
+
registerValidateTool(ext, config);
|
|
10
|
+
registerGenerateTool(ext, config);
|
|
11
|
+
registerRenderTool(ext, config);
|
|
12
|
+
registerPreviewTool(ext, config);
|
|
13
|
+
process.stderr.write(`[pdf] Registered PDF tools. Output dir: ${config.outputDir}.\n`);
|
|
14
|
+
}
|
|
15
|
+
catch (err) {
|
|
16
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
17
|
+
process.stderr.write(`[pdf] Failed to register PDF tools: ${msg}\n`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "../../../sdk/coding-agent/index.js";
|
|
2
|
+
import type { PdfConfig, PdfManifest } from "../types.js";
|
|
3
|
+
export declare function registerGenerateTool(ext: ExtensionAPI, config: PdfConfig): void;
|
|
4
|
+
export declare function readExistingManifest(manifestPath: string): PdfManifest | null;
|
|
5
|
+
//# sourceMappingURL=generate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"generate.d.ts","sourceRoot":"","sources":["../../../../src/extensions/pdf/tools/generate.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAoC,MAAM,oCAAoC,CAAC;AACzG,OAAO,KAAK,EAAE,SAAS,EAAa,WAAW,EAA6B,MAAM,aAAa,CAAC;AAiBhG,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,YAAY,EAAE,MAAM,EAAE,SAAS,GAAG,IAAI,CAuG/E;AAED,wBAAgB,oBAAoB,CAAC,YAAY,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAO7E"}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { createDocumentPaths, ensureDir, toDisplayPath } from "../utils/paths.js";
|
|
3
|
+
import { printCssWarning, validateHtmlDocument } from "../utils/html.js";
|
|
4
|
+
import { renderPdf } from "../utils/renderer.js";
|
|
5
|
+
import { errorResult, textResult } from "./helpers.js";
|
|
6
|
+
const formatEnum = ["A4", "Letter"];
|
|
7
|
+
const orientationEnum = ["portrait", "landscape"];
|
|
8
|
+
function mergeMargin(config, margin) {
|
|
9
|
+
return { ...config.defaultMargin, ...(margin ?? {}) };
|
|
10
|
+
}
|
|
11
|
+
function writeManifest(path, manifest) {
|
|
12
|
+
writeFileSync(path, JSON.stringify(manifest, null, 2), "utf-8");
|
|
13
|
+
}
|
|
14
|
+
export function registerGenerateTool(ext, config) {
|
|
15
|
+
const tool = {
|
|
16
|
+
name: "pdf_generate",
|
|
17
|
+
label: "Generate PDF from HTML",
|
|
18
|
+
description: "Generate a PDF by first saving an editable HTML source file, then rendering it with Chromium. " +
|
|
19
|
+
"For polished user-facing documents, use designer tools to create/refine the HTML before calling this tool.",
|
|
20
|
+
promptSnippet: "`pdf_generate { title, html, outputName? }` — save editable HTML and render it to PDF",
|
|
21
|
+
promptGuidelines: [
|
|
22
|
+
"Never generate PDFs directly from plain text. First create a complete HTML document, then render it.",
|
|
23
|
+
"For polished PDFs, use designer_list_systems and designer_get_system before writing the HTML.",
|
|
24
|
+
"Keep the saved HTML source as the editable artifact; refine it and call pdf_render again for updates.",
|
|
25
|
+
"Use print CSS with @page, fixed margins, break-inside avoidance, and print-color-adjust: exact.",
|
|
26
|
+
],
|
|
27
|
+
parameters: {
|
|
28
|
+
type: "object",
|
|
29
|
+
properties: {
|
|
30
|
+
title: { type: "string", description: "Document title." },
|
|
31
|
+
html: { type: "string", description: "Complete HTML document. Must include doctype, html, head, and body." },
|
|
32
|
+
outputName: { type: "string", description: "Optional stable filename/folder slug." },
|
|
33
|
+
format: { type: "string", enum: [...formatEnum], description: "Page format. Default from config: A4." },
|
|
34
|
+
orientation: { type: "string", enum: [...orientationEnum], description: "Page orientation. Default: portrait." },
|
|
35
|
+
printBackground: { type: "boolean", description: "Render CSS backgrounds. Default: true." },
|
|
36
|
+
allowRemoteAssets: { type: "boolean", description: "Allow http/https assets during validation/render. Default: false." },
|
|
37
|
+
margin: {
|
|
38
|
+
type: "object",
|
|
39
|
+
properties: {
|
|
40
|
+
top: { type: "string" },
|
|
41
|
+
right: { type: "string" },
|
|
42
|
+
bottom: { type: "string" },
|
|
43
|
+
left: { type: "string" },
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
required: ["title", "html"],
|
|
48
|
+
},
|
|
49
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
50
|
+
try {
|
|
51
|
+
const p = params;
|
|
52
|
+
const allowRemoteAssets = p.allowRemoteAssets ?? config.allowRemoteAssets;
|
|
53
|
+
validateHtmlDocument(p.html, { allowRemoteAssets });
|
|
54
|
+
const paths = createDocumentPaths(ctx.cwd, config, p.title, p.outputName);
|
|
55
|
+
ensureDir(paths.dir);
|
|
56
|
+
writeFileSync(paths.htmlPath, p.html, "utf-8");
|
|
57
|
+
const now = new Date().toISOString();
|
|
58
|
+
const format = p.format ?? config.defaultFormat;
|
|
59
|
+
const orientation = p.orientation ?? config.defaultOrientation;
|
|
60
|
+
const margin = mergeMargin(config, p.margin);
|
|
61
|
+
const manifest = {
|
|
62
|
+
id: paths.id,
|
|
63
|
+
title: p.title,
|
|
64
|
+
htmlPath: toDisplayPath(ctx.cwd, paths.htmlPath),
|
|
65
|
+
pdfPath: toDisplayPath(ctx.cwd, paths.pdfPath),
|
|
66
|
+
createdAt: now,
|
|
67
|
+
updatedAt: now,
|
|
68
|
+
page: { format, orientation, margin },
|
|
69
|
+
source: "generated",
|
|
70
|
+
};
|
|
71
|
+
writeManifest(paths.manifestPath, manifest);
|
|
72
|
+
const rendered = await renderPdf(config, {
|
|
73
|
+
htmlPath: paths.htmlPath,
|
|
74
|
+
pdfPath: paths.pdfPath,
|
|
75
|
+
format,
|
|
76
|
+
orientation,
|
|
77
|
+
margin,
|
|
78
|
+
printBackground: p.printBackground ?? true,
|
|
79
|
+
allowRemoteAssets,
|
|
80
|
+
waitUntil: allowRemoteAssets ? "networkidle" : "load",
|
|
81
|
+
});
|
|
82
|
+
writeManifest(paths.manifestPath, { ...manifest, updatedAt: new Date().toISOString() });
|
|
83
|
+
const warning = printCssWarning(p.html);
|
|
84
|
+
const lines = [
|
|
85
|
+
"✓ PDF generated from editable HTML",
|
|
86
|
+
`HTML: ${toDisplayPath(ctx.cwd, paths.htmlPath)}`,
|
|
87
|
+
`PDF: ${toDisplayPath(ctx.cwd, paths.pdfPath)}`,
|
|
88
|
+
`Manifest: ${toDisplayPath(ctx.cwd, paths.manifestPath)}`,
|
|
89
|
+
`Size: ${rendered.bytes} bytes`,
|
|
90
|
+
];
|
|
91
|
+
if (warning)
|
|
92
|
+
lines.push(`Warning: ${warning}`);
|
|
93
|
+
return textResult(lines.join("\n"), {
|
|
94
|
+
isError: false,
|
|
95
|
+
id: paths.id,
|
|
96
|
+
htmlPath: toDisplayPath(ctx.cwd, paths.htmlPath),
|
|
97
|
+
pdfPath: toDisplayPath(ctx.cwd, paths.pdfPath),
|
|
98
|
+
manifestPath: toDisplayPath(ctx.cwd, paths.manifestPath),
|
|
99
|
+
bytes: rendered.bytes,
|
|
100
|
+
warning,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
return errorResult(err instanceof Error ? err.message : String(err));
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
ext.registerTool(tool);
|
|
109
|
+
}
|
|
110
|
+
export function readExistingManifest(manifestPath) {
|
|
111
|
+
if (!existsSync(manifestPath))
|
|
112
|
+
return null;
|
|
113
|
+
try {
|
|
114
|
+
return JSON.parse(readFileSync(manifestPath, "utf-8"));
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { AgentToolResult } from "../../../sdk/coding-agent/index.js";
|
|
2
|
+
export declare function textResult(text: string, details?: Record<string, unknown>): AgentToolResult<Record<string, unknown>>;
|
|
3
|
+
export declare function errorResult(message: string, details?: Record<string, unknown>): AgentToolResult<Record<string, unknown>>;
|
|
4
|
+
//# sourceMappingURL=helpers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../../../src/extensions/pdf/tools/helpers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,oCAAoC,CAAC;AAE1E,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GAAG,eAAe,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAExH;AAED,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GAAG,eAAe,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAE5H"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export function textResult(text, details = {}) {
|
|
2
|
+
return { content: [{ type: "text", text }], details };
|
|
3
|
+
}
|
|
4
|
+
export function errorResult(message, details = {}) {
|
|
5
|
+
return { content: [{ type: "text", text: `❌ ${message}` }], details: { isError: true, ...details } };
|
|
6
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"preview.d.ts","sourceRoot":"","sources":["../../../../src/extensions/pdf/tools/preview.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAoC,MAAM,oCAAoC,CAAC;AACzG,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAM7C,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,YAAY,EAAE,MAAM,EAAE,SAAS,GAAG,IAAI,CA+C9E"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { assertInsideWorkspace, ensureParent, resolveWorkspacePath, toDisplayPath } from "../utils/paths.js";
|
|
4
|
+
import { validateHtmlDocument } from "../utils/html.js";
|
|
5
|
+
import { renderPreview } from "../utils/renderer.js";
|
|
6
|
+
import { errorResult, textResult } from "./helpers.js";
|
|
7
|
+
export function registerPreviewTool(ext, config) {
|
|
8
|
+
const tool = {
|
|
9
|
+
name: "pdf_preview_html",
|
|
10
|
+
label: "Preview HTML for PDF",
|
|
11
|
+
description: "Render a PNG preview screenshot for an existing HTML document before PDF export.",
|
|
12
|
+
promptSnippet: "`pdf_preview_html { htmlPath }` — render PNG preview for PDF HTML",
|
|
13
|
+
parameters: {
|
|
14
|
+
type: "object",
|
|
15
|
+
properties: {
|
|
16
|
+
htmlPath: { type: "string", description: "Path to an HTML file inside the workspace." },
|
|
17
|
+
outputPath: { type: "string", description: "Optional PNG output path inside the workspace." },
|
|
18
|
+
allowRemoteAssets: { type: "boolean", description: "Allow http/https assets during preview. Default: false." },
|
|
19
|
+
},
|
|
20
|
+
required: ["htmlPath"],
|
|
21
|
+
},
|
|
22
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
23
|
+
try {
|
|
24
|
+
const p = params;
|
|
25
|
+
const htmlPath = resolveWorkspacePath(ctx.cwd, p.htmlPath);
|
|
26
|
+
assertInsideWorkspace(ctx.cwd, htmlPath);
|
|
27
|
+
if (!existsSync(htmlPath))
|
|
28
|
+
throw new Error(`HTML file not found: ${p.htmlPath}`);
|
|
29
|
+
const previewPath = p.outputPath ? resolveWorkspacePath(ctx.cwd, p.outputPath) : join(dirname(htmlPath), "preview.png");
|
|
30
|
+
assertInsideWorkspace(ctx.cwd, previewPath);
|
|
31
|
+
ensureParent(previewPath);
|
|
32
|
+
const allowRemoteAssets = p.allowRemoteAssets ?? config.allowRemoteAssets;
|
|
33
|
+
validateHtmlDocument(readFileSync(htmlPath, "utf-8"), { allowRemoteAssets });
|
|
34
|
+
const preview = await renderPreview(config, htmlPath, previewPath, allowRemoteAssets);
|
|
35
|
+
return textResult([
|
|
36
|
+
"✓ HTML preview rendered",
|
|
37
|
+
`HTML: ${toDisplayPath(ctx.cwd, htmlPath)}`,
|
|
38
|
+
`Preview: ${toDisplayPath(ctx.cwd, previewPath)}`,
|
|
39
|
+
`Size: ${preview.bytes} bytes`,
|
|
40
|
+
].join("\n"), {
|
|
41
|
+
isError: false,
|
|
42
|
+
htmlPath: toDisplayPath(ctx.cwd, htmlPath),
|
|
43
|
+
previewPath: toDisplayPath(ctx.cwd, previewPath),
|
|
44
|
+
bytes: preview.bytes,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
return errorResult(err instanceof Error ? err.message : String(err));
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
ext.registerTool(tool);
|
|
53
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../../../../src/extensions/pdf/tools/render.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAoC,MAAM,oCAAoC,CAAC;AACzG,OAAO,KAAK,EAAE,SAAS,EAAqD,MAAM,aAAa,CAAC;AAgBhG,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,YAAY,EAAE,MAAM,EAAE,SAAS,GAAG,IAAI,CAsG7E"}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { basename, dirname, extname, join } from "node:path";
|
|
3
|
+
import { assertInsideWorkspace, ensureParent, resolveWorkspacePath, toDisplayPath } from "../utils/paths.js";
|
|
4
|
+
import { printCssWarning, validateHtmlDocument } from "../utils/html.js";
|
|
5
|
+
import { renderPdf } from "../utils/renderer.js";
|
|
6
|
+
import { errorResult, textResult } from "./helpers.js";
|
|
7
|
+
function mergeMargin(config, margin) {
|
|
8
|
+
return { ...config.defaultMargin, ...(margin ?? {}) };
|
|
9
|
+
}
|
|
10
|
+
function defaultPdfPath(htmlPath) {
|
|
11
|
+
const ext = extname(htmlPath);
|
|
12
|
+
const name = basename(htmlPath, ext || undefined);
|
|
13
|
+
return join(dirname(htmlPath), `${name}.pdf`);
|
|
14
|
+
}
|
|
15
|
+
export function registerRenderTool(ext, config) {
|
|
16
|
+
const tool = {
|
|
17
|
+
name: "pdf_render",
|
|
18
|
+
label: "Render HTML to PDF",
|
|
19
|
+
description: "Render an existing HTML file to PDF. Use this after editing the saved HTML source with designer refinements.",
|
|
20
|
+
promptSnippet: "`pdf_render { htmlPath, outputPath? }` — render an existing HTML file to PDF",
|
|
21
|
+
promptGuidelines: [
|
|
22
|
+
"Use pdf_render after editing an existing HTML artifact. Do not overwrite the HTML unless the user asked for refinements.",
|
|
23
|
+
"If the PDF needs visual improvement, refine the HTML with designer guidance, then render again.",
|
|
24
|
+
],
|
|
25
|
+
parameters: {
|
|
26
|
+
type: "object",
|
|
27
|
+
properties: {
|
|
28
|
+
htmlPath: { type: "string", description: "Path to the existing HTML file, relative to workspace or absolute inside workspace." },
|
|
29
|
+
outputPath: { type: "string", description: "Optional PDF output path, relative to workspace or absolute inside workspace." },
|
|
30
|
+
format: { type: "string", enum: ["A4", "Letter"], description: "Page format. Default: A4." },
|
|
31
|
+
orientation: { type: "string", enum: ["portrait", "landscape"], description: "Page orientation. Default: portrait." },
|
|
32
|
+
printBackground: { type: "boolean", description: "Render CSS backgrounds. Default: true." },
|
|
33
|
+
allowRemoteAssets: { type: "boolean", description: "Allow http/https assets during validation/render. Default: false." },
|
|
34
|
+
margin: {
|
|
35
|
+
type: "object",
|
|
36
|
+
properties: {
|
|
37
|
+
top: { type: "string" },
|
|
38
|
+
right: { type: "string" },
|
|
39
|
+
bottom: { type: "string" },
|
|
40
|
+
left: { type: "string" },
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
required: ["htmlPath"],
|
|
45
|
+
},
|
|
46
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
47
|
+
try {
|
|
48
|
+
const p = params;
|
|
49
|
+
const htmlPath = resolveWorkspacePath(ctx.cwd, p.htmlPath);
|
|
50
|
+
assertInsideWorkspace(ctx.cwd, htmlPath);
|
|
51
|
+
if (!existsSync(htmlPath))
|
|
52
|
+
throw new Error(`HTML file not found: ${p.htmlPath}`);
|
|
53
|
+
const outputPath = p.outputPath ? resolveWorkspacePath(ctx.cwd, p.outputPath) : defaultPdfPath(htmlPath);
|
|
54
|
+
assertInsideWorkspace(ctx.cwd, outputPath);
|
|
55
|
+
ensureParent(outputPath);
|
|
56
|
+
const html = readFileSync(htmlPath, "utf-8");
|
|
57
|
+
const allowRemoteAssets = p.allowRemoteAssets ?? config.allowRemoteAssets;
|
|
58
|
+
validateHtmlDocument(html, { allowRemoteAssets });
|
|
59
|
+
const format = p.format ?? config.defaultFormat;
|
|
60
|
+
const orientation = p.orientation ?? config.defaultOrientation;
|
|
61
|
+
const margin = mergeMargin(config, p.margin);
|
|
62
|
+
const rendered = await renderPdf(config, {
|
|
63
|
+
htmlPath,
|
|
64
|
+
pdfPath: outputPath,
|
|
65
|
+
format,
|
|
66
|
+
orientation,
|
|
67
|
+
margin,
|
|
68
|
+
printBackground: p.printBackground ?? true,
|
|
69
|
+
allowRemoteAssets,
|
|
70
|
+
waitUntil: allowRemoteAssets ? "networkidle" : "load",
|
|
71
|
+
});
|
|
72
|
+
const now = new Date().toISOString();
|
|
73
|
+
const titleMatch = html.match(/<title[^>]*>([^<]+)<\/title>/i);
|
|
74
|
+
const manifest = {
|
|
75
|
+
id: basename(dirname(htmlPath)),
|
|
76
|
+
title: titleMatch?.[1]?.trim() || basename(htmlPath),
|
|
77
|
+
htmlPath: toDisplayPath(ctx.cwd, htmlPath),
|
|
78
|
+
pdfPath: toDisplayPath(ctx.cwd, outputPath),
|
|
79
|
+
createdAt: now,
|
|
80
|
+
updatedAt: now,
|
|
81
|
+
page: { format, orientation, margin },
|
|
82
|
+
source: "rendered",
|
|
83
|
+
};
|
|
84
|
+
const manifestPath = join(dirname(htmlPath), "manifest.json");
|
|
85
|
+
writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), "utf-8");
|
|
86
|
+
const warning = printCssWarning(html);
|
|
87
|
+
const lines = [
|
|
88
|
+
"✓ HTML rendered to PDF",
|
|
89
|
+
`HTML: ${toDisplayPath(ctx.cwd, htmlPath)}`,
|
|
90
|
+
`PDF: ${toDisplayPath(ctx.cwd, outputPath)}`,
|
|
91
|
+
`Manifest: ${toDisplayPath(ctx.cwd, manifestPath)}`,
|
|
92
|
+
`Size: ${rendered.bytes} bytes`,
|
|
93
|
+
];
|
|
94
|
+
if (warning)
|
|
95
|
+
lines.push(`Warning: ${warning}`);
|
|
96
|
+
return textResult(lines.join("\n"), {
|
|
97
|
+
isError: false,
|
|
98
|
+
htmlPath: toDisplayPath(ctx.cwd, htmlPath),
|
|
99
|
+
pdfPath: toDisplayPath(ctx.cwd, outputPath),
|
|
100
|
+
manifestPath: toDisplayPath(ctx.cwd, manifestPath),
|
|
101
|
+
bytes: rendered.bytes,
|
|
102
|
+
warning,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
return errorResult(err instanceof Error ? err.message : String(err));
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
ext.registerTool(tool);
|
|
111
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../../../src/extensions/pdf/tools/validate.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAoC,MAAM,oCAAoC,CAAC;AACzG,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAwB7C,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,YAAY,EAAE,MAAM,EAAE,SAAS,GAAG,IAAI,CAmD/E"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { analyzePdfHtml } from "../utils/validate.js";
|
|
3
|
+
import { assertInsideWorkspace, resolveWorkspacePath, toDisplayPath } from "../utils/paths.js";
|
|
4
|
+
import { errorResult, textResult } from "./helpers.js";
|
|
5
|
+
function issueCounts(issues) {
|
|
6
|
+
return {
|
|
7
|
+
errors: issues.filter((issue) => issue.severity === "error").length,
|
|
8
|
+
warnings: issues.filter((issue) => issue.severity === "warning").length,
|
|
9
|
+
info: issues.filter((issue) => issue.severity === "info").length,
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
function formatIssues(issues) {
|
|
13
|
+
if (issues.length === 0)
|
|
14
|
+
return "No issues found.";
|
|
15
|
+
return issues
|
|
16
|
+
.map((issue) => {
|
|
17
|
+
const icon = issue.severity === "error" ? "✗" : issue.severity === "warning" ? "⚠" : "ℹ";
|
|
18
|
+
const suggestion = issue.suggestion ? `\n Suggestion: ${issue.suggestion}` : "";
|
|
19
|
+
return `${icon} [${issue.severity}] ${issue.code}: ${issue.message}${suggestion}`;
|
|
20
|
+
})
|
|
21
|
+
.join("\n");
|
|
22
|
+
}
|
|
23
|
+
export function registerValidateTool(ext, config) {
|
|
24
|
+
const tool = {
|
|
25
|
+
name: "pdf_validate_html",
|
|
26
|
+
label: "Validate PDF HTML",
|
|
27
|
+
description: "Analyze PDF HTML before rendering. Reports blocking errors and layout-quality warnings for print CSS, remote assets, pagination, images, and metadata.",
|
|
28
|
+
promptSnippet: "`pdf_validate_html { html | htmlPath }` — validate PDF HTML before render",
|
|
29
|
+
promptGuidelines: [
|
|
30
|
+
"Use pdf_validate_html before rendering complex or customer-facing PDFs.",
|
|
31
|
+
"Fix error-level issues before pdf_generate or pdf_render.",
|
|
32
|
+
"Treat warnings as design polish tasks; use designer tools to refine the HTML.",
|
|
33
|
+
],
|
|
34
|
+
parameters: {
|
|
35
|
+
type: "object",
|
|
36
|
+
properties: {
|
|
37
|
+
html: { type: "string", description: "Complete HTML document to validate." },
|
|
38
|
+
htmlPath: { type: "string", description: "Path to an existing HTML file inside the workspace." },
|
|
39
|
+
allowRemoteAssets: { type: "boolean", description: "Allow http/https assets during validation. Default: false." },
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
43
|
+
try {
|
|
44
|
+
const p = params;
|
|
45
|
+
if (!p.html && !p.htmlPath)
|
|
46
|
+
throw new Error("Provide either html or htmlPath.");
|
|
47
|
+
let html = p.html;
|
|
48
|
+
let source = "inline HTML";
|
|
49
|
+
if (!html && p.htmlPath) {
|
|
50
|
+
const htmlPath = resolveWorkspacePath(ctx.cwd, p.htmlPath);
|
|
51
|
+
assertInsideWorkspace(ctx.cwd, htmlPath);
|
|
52
|
+
if (!existsSync(htmlPath))
|
|
53
|
+
throw new Error(`HTML file not found: ${p.htmlPath}`);
|
|
54
|
+
html = readFileSync(htmlPath, "utf-8");
|
|
55
|
+
source = toDisplayPath(ctx.cwd, htmlPath);
|
|
56
|
+
}
|
|
57
|
+
const allowRemoteAssets = p.allowRemoteAssets ?? config.allowRemoteAssets;
|
|
58
|
+
const issues = analyzePdfHtml(html ?? "", { allowRemoteAssets });
|
|
59
|
+
const counts = issueCounts(issues);
|
|
60
|
+
const ok = counts.errors === 0;
|
|
61
|
+
const text = [
|
|
62
|
+
ok ? "✓ PDF HTML validation passed" : "✗ PDF HTML validation failed",
|
|
63
|
+
`Source: ${source}`,
|
|
64
|
+
`Errors: ${counts.errors} · Warnings: ${counts.warnings} · Info: ${counts.info}`,
|
|
65
|
+
"",
|
|
66
|
+
formatIssues(issues),
|
|
67
|
+
].join("\n");
|
|
68
|
+
return textResult(text, { isError: !ok, ok, issues, counts, source });
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
return errorResult(err instanceof Error ? err.message : String(err));
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
ext.registerTool(tool);
|
|
76
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export type PdfFormat = "A4" | "Letter";
|
|
2
|
+
export type PdfOrientation = "portrait" | "landscape";
|
|
3
|
+
export interface PdfMargin {
|
|
4
|
+
top?: string;
|
|
5
|
+
right?: string;
|
|
6
|
+
bottom?: string;
|
|
7
|
+
left?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface PdfConfig {
|
|
10
|
+
outputDir: string;
|
|
11
|
+
renderTimeoutMs: number;
|
|
12
|
+
allowRemoteAssets: boolean;
|
|
13
|
+
defaultFormat: PdfFormat;
|
|
14
|
+
defaultOrientation: PdfOrientation;
|
|
15
|
+
defaultMargin: Required<PdfMargin>;
|
|
16
|
+
}
|
|
17
|
+
export interface PdfManifest {
|
|
18
|
+
id: string;
|
|
19
|
+
title: string;
|
|
20
|
+
htmlPath: string;
|
|
21
|
+
pdfPath?: string;
|
|
22
|
+
previewPath?: string;
|
|
23
|
+
createdAt: string;
|
|
24
|
+
updatedAt: string;
|
|
25
|
+
page: {
|
|
26
|
+
format: PdfFormat;
|
|
27
|
+
orientation: PdfOrientation;
|
|
28
|
+
margin: Required<PdfMargin>;
|
|
29
|
+
};
|
|
30
|
+
source: "generated" | "rendered";
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/extensions/pdf/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,SAAS,GAAG,IAAI,GAAG,QAAQ,CAAC;AACxC,MAAM,MAAM,cAAc,GAAG,UAAU,GAAG,WAAW,CAAC;AAEtD,MAAM,WAAW,SAAS;IACxB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,SAAS;IACxB,SAAS,EAAE,MAAM,CAAC;IAClB,eAAe,EAAE,MAAM,CAAC;IACxB,iBAAiB,EAAE,OAAO,CAAC;IAC3B,aAAa,EAAE,SAAS,CAAC;IACzB,kBAAkB,EAAE,cAAc,CAAC;IACnC,aAAa,EAAE,QAAQ,CAAC,SAAS,CAAC,CAAC;CACpC;AAED,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE;QACJ,MAAM,EAAE,SAAS,CAAC;QAClB,WAAW,EAAE,cAAc,CAAC;QAC5B,MAAM,EAAE,QAAQ,CAAC,SAAS,CAAC,CAAC;KAC7B,CAAC;IACF,MAAM,EAAE,WAAW,GAAG,UAAU,CAAC;CAClC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare function validateHtmlDocument(html: string, opts?: {
|
|
2
|
+
allowRemoteAssets?: boolean;
|
|
3
|
+
}): void;
|
|
4
|
+
export declare function hasPrintCss(html: string): boolean;
|
|
5
|
+
export declare function printCssWarning(html: string): string | null;
|
|
6
|
+
//# sourceMappingURL=html.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"html.d.ts","sourceRoot":"","sources":["../../../../src/extensions/pdf/utils/html.ts"],"names":[],"mappings":"AAEA,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE;IAAE,iBAAiB,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,IAAI,CAS/F;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEjD;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAG3D"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { analyzePdfHtml, hasBlockingPdfHtmlIssues } from "./validate.js";
|
|
2
|
+
export function validateHtmlDocument(html, opts) {
|
|
3
|
+
const issues = analyzePdfHtml(html, opts);
|
|
4
|
+
const firstError = issues.find((issue) => issue.severity === "error");
|
|
5
|
+
if (firstError) {
|
|
6
|
+
throw new Error(firstError.message + (firstError.suggestion ? ` ${firstError.suggestion}` : ""));
|
|
7
|
+
}
|
|
8
|
+
if (hasBlockingPdfHtmlIssues(issues)) {
|
|
9
|
+
throw new Error("HTML has blocking PDF validation issues.");
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export function hasPrintCss(html) {
|
|
13
|
+
return /@page\s*{/i.test(html) || /@media\s+print/i.test(html);
|
|
14
|
+
}
|
|
15
|
+
export function printCssWarning(html) {
|
|
16
|
+
if (hasPrintCss(html))
|
|
17
|
+
return null;
|
|
18
|
+
return "HTML has no @page or @media print CSS. PDF still rendered, but page layout may need refinement.";
|
|
19
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { PdfConfig } from "../types.js";
|
|
2
|
+
export declare function slugify(input: string): string;
|
|
3
|
+
export declare function uniqueSlug(title: string): string;
|
|
4
|
+
export declare function ensureDir(path: string): void;
|
|
5
|
+
export declare function ensureParent(path: string): void;
|
|
6
|
+
export declare function resolveWorkspacePath(cwd: string, input: string): string;
|
|
7
|
+
export declare function assertInsideWorkspace(cwd: string, targetPath: string): void;
|
|
8
|
+
export declare function resolveOutputDir(cwd: string, config: PdfConfig): string;
|
|
9
|
+
export declare function createDocumentPaths(cwd: string, config: PdfConfig, title: string, outputName?: string): {
|
|
10
|
+
id: string;
|
|
11
|
+
dir: string;
|
|
12
|
+
htmlPath: string;
|
|
13
|
+
pdfPath: string;
|
|
14
|
+
manifestPath: string;
|
|
15
|
+
previewPath: string;
|
|
16
|
+
};
|
|
17
|
+
export declare function pathToFileHref(path: string): string;
|
|
18
|
+
export declare function toDisplayPath(cwd: string, path: string): string;
|
|
19
|
+
//# sourceMappingURL=paths.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"paths.d.ts","sourceRoot":"","sources":["../../../../src/extensions/pdf/utils/paths.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAE7C,wBAAgB,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAS7C;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAIhD;AAED,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAE5C;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAE/C;AAED,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAEvE;AAED,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,CAO3E;AAED,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,GAAG,MAAM,CAIvE;AAED,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG;IACvG,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;CACrB,CAYA;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEnD;AAED,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAG/D"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { mkdirSync } from "node:fs";
|
|
2
|
+
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
export function slugify(input) {
|
|
5
|
+
const slug = input
|
|
6
|
+
.normalize("NFKD")
|
|
7
|
+
.replace(/[\u0300-\u036f]/g, "")
|
|
8
|
+
.toLowerCase()
|
|
9
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
10
|
+
.replace(/^-+|-+$/g, "")
|
|
11
|
+
.slice(0, 80);
|
|
12
|
+
return slug || "document";
|
|
13
|
+
}
|
|
14
|
+
export function uniqueSlug(title) {
|
|
15
|
+
const base = slugify(title);
|
|
16
|
+
const suffix = new Date().toISOString().replace(/[-:.TZ]/g, "").slice(0, 14);
|
|
17
|
+
return `${base}-${suffix}`;
|
|
18
|
+
}
|
|
19
|
+
export function ensureDir(path) {
|
|
20
|
+
mkdirSync(path, { recursive: true });
|
|
21
|
+
}
|
|
22
|
+
export function ensureParent(path) {
|
|
23
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
24
|
+
}
|
|
25
|
+
export function resolveWorkspacePath(cwd, input) {
|
|
26
|
+
return isAbsolute(input) ? resolve(input) : resolve(cwd, input);
|
|
27
|
+
}
|
|
28
|
+
export function assertInsideWorkspace(cwd, targetPath) {
|
|
29
|
+
const root = resolve(cwd);
|
|
30
|
+
const target = resolve(targetPath);
|
|
31
|
+
const rel = relative(root, target);
|
|
32
|
+
if (rel.startsWith("..") || isAbsolute(rel)) {
|
|
33
|
+
throw new Error(`Path escapes workspace: ${targetPath}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export function resolveOutputDir(cwd, config) {
|
|
37
|
+
const dir = resolveWorkspacePath(cwd, config.outputDir);
|
|
38
|
+
assertInsideWorkspace(cwd, dir);
|
|
39
|
+
return dir;
|
|
40
|
+
}
|
|
41
|
+
export function createDocumentPaths(cwd, config, title, outputName) {
|
|
42
|
+
const id = outputName ? slugify(outputName) : uniqueSlug(title);
|
|
43
|
+
const baseDir = resolveOutputDir(cwd, config);
|
|
44
|
+
const dir = join(baseDir, id);
|
|
45
|
+
return {
|
|
46
|
+
id,
|
|
47
|
+
dir,
|
|
48
|
+
htmlPath: join(dir, `${id}.html`),
|
|
49
|
+
pdfPath: join(dir, `${id}.pdf`),
|
|
50
|
+
manifestPath: join(dir, "manifest.json"),
|
|
51
|
+
previewPath: join(dir, "preview.png"),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
export function pathToFileHref(path) {
|
|
55
|
+
return pathToFileURL(path).href;
|
|
56
|
+
}
|
|
57
|
+
export function toDisplayPath(cwd, path) {
|
|
58
|
+
const rel = relative(resolve(cwd), resolve(path));
|
|
59
|
+
return rel.startsWith("..") || isAbsolute(rel) ? path : rel;
|
|
60
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { PdfConfig, PdfFormat, PdfMargin, PdfOrientation } from "../types.js";
|
|
2
|
+
export interface RenderPdfOptions {
|
|
3
|
+
htmlPath: string;
|
|
4
|
+
pdfPath: string;
|
|
5
|
+
format: PdfFormat;
|
|
6
|
+
orientation: PdfOrientation;
|
|
7
|
+
margin: Required<PdfMargin>;
|
|
8
|
+
printBackground: boolean;
|
|
9
|
+
allowRemoteAssets: boolean;
|
|
10
|
+
waitUntil: "load" | "networkidle";
|
|
11
|
+
}
|
|
12
|
+
export interface RenderPdfResult {
|
|
13
|
+
pdfPath: string;
|
|
14
|
+
bytes: number;
|
|
15
|
+
}
|
|
16
|
+
export declare function renderPdf(config: PdfConfig, opts: RenderPdfOptions): Promise<RenderPdfResult>;
|
|
17
|
+
export declare function renderPreview(config: PdfConfig, htmlPath: string, previewPath: string, allowRemoteAssets: boolean): Promise<{
|
|
18
|
+
previewPath: string;
|
|
19
|
+
bytes: number;
|
|
20
|
+
}>;
|
|
21
|
+
//# sourceMappingURL=renderer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"renderer.d.ts","sourceRoot":"","sources":["../../../../src/extensions/pdf/utils/renderer.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAGnF,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,SAAS,CAAC;IAClB,WAAW,EAAE,cAAc,CAAC;IAC5B,MAAM,EAAE,QAAQ,CAAC,SAAS,CAAC,CAAC;IAC5B,eAAe,EAAE,OAAO,CAAC;IACzB,iBAAiB,EAAE,OAAO,CAAC;IAC3B,SAAS,EAAE,MAAM,GAAG,aAAa,CAAC;CACnC;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,wBAAsB,SAAS,CAAC,MAAM,EAAE,SAAS,EAAE,IAAI,EAAE,gBAAgB,GAAG,OAAO,CAAC,eAAe,CAAC,CA8BnG;AAED,wBAAsB,aAAa,CAAC,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,iBAAiB,EAAE,OAAO,GAAG,OAAO,CAAC;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CAkBzK"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { existsSync, statSync } from "node:fs";
|
|
2
|
+
import { chromium } from "playwright";
|
|
3
|
+
import { pathToFileHref } from "./paths.js";
|
|
4
|
+
export async function renderPdf(config, opts) {
|
|
5
|
+
const browser = await chromium.launch({ headless: true });
|
|
6
|
+
try {
|
|
7
|
+
const page = await browser.newPage();
|
|
8
|
+
page.setDefaultTimeout(config.renderTimeoutMs);
|
|
9
|
+
if (!opts.allowRemoteAssets) {
|
|
10
|
+
await page.route("**/*", (route) => {
|
|
11
|
+
const url = route.request().url();
|
|
12
|
+
if (url.startsWith("file://") || url.startsWith("data:") || url === "about:blank") {
|
|
13
|
+
return route.continue();
|
|
14
|
+
}
|
|
15
|
+
return route.abort();
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
await page.goto(pathToFileHref(opts.htmlPath), {
|
|
19
|
+
waitUntil: opts.waitUntil,
|
|
20
|
+
timeout: config.renderTimeoutMs,
|
|
21
|
+
});
|
|
22
|
+
await page.pdf({
|
|
23
|
+
path: opts.pdfPath,
|
|
24
|
+
format: opts.format,
|
|
25
|
+
landscape: opts.orientation === "landscape",
|
|
26
|
+
printBackground: opts.printBackground,
|
|
27
|
+
margin: opts.margin,
|
|
28
|
+
});
|
|
29
|
+
if (!existsSync(opts.pdfPath))
|
|
30
|
+
throw new Error("PDF render produced no output file.");
|
|
31
|
+
return { pdfPath: opts.pdfPath, bytes: statSync(opts.pdfPath).size };
|
|
32
|
+
}
|
|
33
|
+
finally {
|
|
34
|
+
await browser.close();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export async function renderPreview(config, htmlPath, previewPath, allowRemoteAssets) {
|
|
38
|
+
const browser = await chromium.launch({ headless: true });
|
|
39
|
+
try {
|
|
40
|
+
const page = await browser.newPage({ viewport: { width: 1240, height: 1754 }, deviceScaleFactor: 1 });
|
|
41
|
+
page.setDefaultTimeout(config.renderTimeoutMs);
|
|
42
|
+
if (!allowRemoteAssets) {
|
|
43
|
+
await page.route("**/*", (route) => {
|
|
44
|
+
const url = route.request().url();
|
|
45
|
+
if (url.startsWith("file://") || url.startsWith("data:") || url === "about:blank")
|
|
46
|
+
return route.continue();
|
|
47
|
+
return route.abort();
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
await page.goto(pathToFileHref(htmlPath), { waitUntil: "load", timeout: config.renderTimeoutMs });
|
|
51
|
+
await page.screenshot({ path: previewPath, fullPage: true, type: "png" });
|
|
52
|
+
return { previewPath, bytes: statSync(previewPath).size };
|
|
53
|
+
}
|
|
54
|
+
finally {
|
|
55
|
+
await browser.close();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type PdfValidationSeverity = "error" | "warning" | "info";
|
|
2
|
+
export interface PdfValidationIssue {
|
|
3
|
+
severity: PdfValidationSeverity;
|
|
4
|
+
code: string;
|
|
5
|
+
message: string;
|
|
6
|
+
suggestion?: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function analyzePdfHtml(html: string, opts?: {
|
|
9
|
+
allowRemoteAssets?: boolean;
|
|
10
|
+
}): PdfValidationIssue[];
|
|
11
|
+
export declare function hasBlockingPdfHtmlIssues(issues: PdfValidationIssue[]): boolean;
|
|
12
|
+
//# sourceMappingURL=validate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../../../src/extensions/pdf/utils/validate.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,qBAAqB,GAAG,OAAO,GAAG,SAAS,GAAG,MAAM,CAAC;AAEjE,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,qBAAqB,CAAC;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAoBD,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE;IAAE,iBAAiB,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,kBAAkB,EAAE,CA8HzG;AAED,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,kBAAkB,EAAE,GAAG,OAAO,CAE9E"}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
const DOCTYPE_RE = /^\s*<!doctype\s+html/i;
|
|
2
|
+
const HTML_RE = /<html[\s>]/i;
|
|
3
|
+
const HEAD_RE = /<head[\s>]/i;
|
|
4
|
+
const BODY_RE = /<body[\s>]/i;
|
|
5
|
+
const REMOTE_SRC_RE = /\b(?:src|href)\s*=\s*["']https?:\/\//gi;
|
|
6
|
+
const PAGE_RE = /@page\s*{/i;
|
|
7
|
+
const PRINT_MEDIA_RE = /@media\s+print/i;
|
|
8
|
+
const PRINT_COLOR_ADJUST_RE = /print-color-adjust\s*:\s*exact|-webkit-print-color-adjust\s*:\s*exact/i;
|
|
9
|
+
const VIEWPORT_RE = /<meta\s+[^>]*name\s*=\s*["']viewport["'][^>]*>/i;
|
|
10
|
+
const TITLE_RE = /<title[^>]*>\s*([^<]+?)\s*<\/title>/i;
|
|
11
|
+
const FIXED_RE = /position\s*:\s*fixed/i;
|
|
12
|
+
const ABSOLUTE_RE = /position\s*:\s*absolute/i;
|
|
13
|
+
const BREAK_INSIDE_RE = /break-inside\s*:\s*avoid|page-break-inside\s*:\s*avoid/i;
|
|
14
|
+
const WIDE_WIDTH_RE = /(?:width|min-width)\s*:\s*(?:[1-9]\d{3,}px|(?:8[5-9]|9\d|\d{3,})vw)/i;
|
|
15
|
+
const IMAGE_RE = /<img\b[^>]*>/gi;
|
|
16
|
+
const WIDTH_ATTR_RE = /\bwidth\s*=\s*["'][^"']+["']/i;
|
|
17
|
+
const HEIGHT_ATTR_RE = /\bheight\s*=\s*["'][^"']+["']/i;
|
|
18
|
+
export function analyzePdfHtml(html, opts) {
|
|
19
|
+
const issues = [];
|
|
20
|
+
if (html.length > 5_000_000) {
|
|
21
|
+
issues.push({
|
|
22
|
+
severity: "error",
|
|
23
|
+
code: "html_too_large",
|
|
24
|
+
message: "HTML is too large. Limit is 5 MB.",
|
|
25
|
+
suggestion: "Split content or remove embedded assets.",
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
if (!DOCTYPE_RE.test(html)) {
|
|
29
|
+
issues.push({
|
|
30
|
+
severity: "error",
|
|
31
|
+
code: "missing_doctype",
|
|
32
|
+
message: "HTML must start with <!doctype html>.",
|
|
33
|
+
suggestion: "Add <!doctype html> as the first line.",
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
if (!HTML_RE.test(html)) {
|
|
37
|
+
issues.push({ severity: "error", code: "missing_html", message: "HTML must include <html>." });
|
|
38
|
+
}
|
|
39
|
+
if (!HEAD_RE.test(html)) {
|
|
40
|
+
issues.push({ severity: "error", code: "missing_head", message: "HTML must include <head>." });
|
|
41
|
+
}
|
|
42
|
+
if (!BODY_RE.test(html)) {
|
|
43
|
+
issues.push({ severity: "error", code: "missing_body", message: "HTML must include <body>." });
|
|
44
|
+
}
|
|
45
|
+
if (!TITLE_RE.test(html)) {
|
|
46
|
+
issues.push({
|
|
47
|
+
severity: "warning",
|
|
48
|
+
code: "missing_title",
|
|
49
|
+
message: "HTML has no <title>.",
|
|
50
|
+
suggestion: "Add a descriptive document title for previews and manifests.",
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
if (!VIEWPORT_RE.test(html)) {
|
|
54
|
+
issues.push({
|
|
55
|
+
severity: "info",
|
|
56
|
+
code: "missing_viewport",
|
|
57
|
+
message: "HTML has no viewport meta tag.",
|
|
58
|
+
suggestion: "Add <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">.",
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
if (!PAGE_RE.test(html)) {
|
|
62
|
+
issues.push({
|
|
63
|
+
severity: "warning",
|
|
64
|
+
code: "missing_page_css",
|
|
65
|
+
message: "HTML has no @page CSS rule.",
|
|
66
|
+
suggestion: "Add @page { size: A4; margin: 18mm 16mm; } for predictable PDF pagination.",
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
if (!PRINT_MEDIA_RE.test(html)) {
|
|
70
|
+
issues.push({
|
|
71
|
+
severity: "info",
|
|
72
|
+
code: "missing_print_media",
|
|
73
|
+
message: "HTML has no @media print block.",
|
|
74
|
+
suggestion: "Add @media print for PDF-only layout tweaks.",
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
if (!PRINT_COLOR_ADJUST_RE.test(html)) {
|
|
78
|
+
issues.push({
|
|
79
|
+
severity: "warning",
|
|
80
|
+
code: "missing_print_color_adjust",
|
|
81
|
+
message: "CSS does not force exact print colors.",
|
|
82
|
+
suggestion: "Add print-color-adjust: exact and -webkit-print-color-adjust: exact on html/body.",
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
if (!BREAK_INSIDE_RE.test(html)) {
|
|
86
|
+
issues.push({
|
|
87
|
+
severity: "info",
|
|
88
|
+
code: "missing_break_avoidance",
|
|
89
|
+
message: "No break-inside avoidance rule detected.",
|
|
90
|
+
suggestion: "Use break-inside: avoid on cards, tables, signatures, and key sections.",
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
const remoteMatches = [...html.matchAll(REMOTE_SRC_RE)];
|
|
94
|
+
if (!opts?.allowRemoteAssets && remoteMatches.length > 0) {
|
|
95
|
+
issues.push({
|
|
96
|
+
severity: "error",
|
|
97
|
+
code: "remote_assets_disabled",
|
|
98
|
+
message: `Remote assets are disabled, but ${remoteMatches.length} remote src/href reference(s) were found.`,
|
|
99
|
+
suggestion: "Use local ./assets files, data URLs, or pass allowRemoteAssets=true.",
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
else if (remoteMatches.length > 0) {
|
|
103
|
+
issues.push({
|
|
104
|
+
severity: "warning",
|
|
105
|
+
code: "remote_assets",
|
|
106
|
+
message: `${remoteMatches.length} remote src/href reference(s) found.`,
|
|
107
|
+
suggestion: "Prefer local assets for reproducible PDF renders.",
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
if (FIXED_RE.test(html)) {
|
|
111
|
+
issues.push({
|
|
112
|
+
severity: "warning",
|
|
113
|
+
code: "position_fixed",
|
|
114
|
+
message: "CSS uses position: fixed, which can repeat or overlap across PDF pages.",
|
|
115
|
+
suggestion: "Use normal flow layout unless fixed headers/footers were intentional and tested.",
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
if (ABSOLUTE_RE.test(html)) {
|
|
119
|
+
issues.push({
|
|
120
|
+
severity: "info",
|
|
121
|
+
code: "position_absolute",
|
|
122
|
+
message: "CSS uses position: absolute.",
|
|
123
|
+
suggestion: "Check pagination carefully; absolute elements can overlap page breaks.",
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
if (WIDE_WIDTH_RE.test(html)) {
|
|
127
|
+
issues.push({
|
|
128
|
+
severity: "warning",
|
|
129
|
+
code: "wide_layout",
|
|
130
|
+
message: "A very wide width/min-width rule was detected.",
|
|
131
|
+
suggestion: "Keep printable content within the page width, usually max-width: 180mm for A4 portrait.",
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
const images = html.match(IMAGE_RE) ?? [];
|
|
135
|
+
const imagesWithoutDimensions = images.filter((img) => !WIDTH_ATTR_RE.test(img) || !HEIGHT_ATTR_RE.test(img)).length;
|
|
136
|
+
if (imagesWithoutDimensions > 0) {
|
|
137
|
+
issues.push({
|
|
138
|
+
severity: "info",
|
|
139
|
+
code: "images_without_dimensions",
|
|
140
|
+
message: `${imagesWithoutDimensions} image(s) lack width or height attributes.`,
|
|
141
|
+
suggestion: "Set image dimensions to reduce layout shifts and improve render predictability.",
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
return issues;
|
|
145
|
+
}
|
|
146
|
+
export function hasBlockingPdfHtmlIssues(issues) {
|
|
147
|
+
return issues.some((issue) => issue.severity === "error");
|
|
148
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"native-extensions.d.ts","sourceRoot":"","sources":["../../../../../src/sdk/coding-agent/core/extensions/native-extensions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,MAAM,WAAW,uBAAuB;IACtC,qEAAqE;IACrE,EAAE,EAAE,MAAM,CAAC;IACX,4DAA4D;IAC5D,KAAK,EAAE,MAAM,CAAC;IACd,yCAAyC;IACzC,WAAW,EAAE,MAAM,CAAC;IACpB,mDAAmD;IACnD,QAAQ,EAAE,MAAM,GAAG,YAAY,GAAG,aAAa,CAAC;IAChD;;;OAGG;IACH,SAAS,EAAE,MAAM,CAAC;IAClB,oEAAoE;IACpE,cAAc,EAAE,OAAO,CAAC;IACxB,mCAAmC;IACnC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;CACjB;AAED;;;;;;GAMG;AACH,eAAO,MAAM,iBAAiB,EAAE,uBAAuB,
|
|
1
|
+
{"version":3,"file":"native-extensions.d.ts","sourceRoot":"","sources":["../../../../../src/sdk/coding-agent/core/extensions/native-extensions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,MAAM,WAAW,uBAAuB;IACtC,qEAAqE;IACrE,EAAE,EAAE,MAAM,CAAC;IACX,4DAA4D;IAC5D,KAAK,EAAE,MAAM,CAAC;IACd,yCAAyC;IACzC,WAAW,EAAE,MAAM,CAAC;IACpB,mDAAmD;IACnD,QAAQ,EAAE,MAAM,GAAG,YAAY,GAAG,aAAa,CAAC;IAChD;;;OAGG;IACH,SAAS,EAAE,MAAM,CAAC;IAClB,oEAAoE;IACpE,cAAc,EAAE,OAAO,CAAC;IACxB,mCAAmC;IACnC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;CACjB;AAED;;;;;;GAMG;AACH,eAAO,MAAM,iBAAiB,EAAE,uBAAuB,EA4GtD,CAAC;AAEF;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,MAAM,GAAG,uBAAuB,GAAG,SAAS,CAElF;AAED;;;GAGG;AACH,wBAAgB,8BAA8B,CAAC,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,EAAE,UAAU,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,MAAM,EAAE,CAI1G;AAED;;GAEG;AACH,wBAAgB,yBAAyB,CAAC,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,EAAE,UAAU,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,KAAK,CAAC;IAClG,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,EAAE,OAAO,CAAC;IACxB,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;CACjB,CAAC,CAUD"}
|
|
@@ -107,6 +107,16 @@ export const NATIVE_EXTENSIONS = [
|
|
|
107
107
|
defaultEnabled: true,
|
|
108
108
|
tags: ["web", "fetch", "reading"],
|
|
109
109
|
},
|
|
110
|
+
{
|
|
111
|
+
id: "pdf",
|
|
112
|
+
label: "PDF Generator",
|
|
113
|
+
description: "Generates polished PDF documents from editable HTML sources. " +
|
|
114
|
+
"HTML can be refined with designer tools before rendering or re-rendered after edits.",
|
|
115
|
+
category: "automation",
|
|
116
|
+
entryPath: "src/extensions/pdf/index.ts",
|
|
117
|
+
defaultEnabled: true,
|
|
118
|
+
tags: ["pdf", "html", "documents", "playwright", "design"],
|
|
119
|
+
},
|
|
110
120
|
];
|
|
111
121
|
/**
|
|
112
122
|
* Look up a native extension by id. Returns undefined for unknown ids.
|