@aexol/spectral 0.9.103 → 0.9.104

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.
@@ -1 +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,CAkH/E;AAED,wBAAgB,oBAAoB,CAAC,YAAY,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAO7E"}
1
+ {"version":3,"file":"generate.d.ts","sourceRoot":"","sources":["../../../../src/extensions/pdf/tools/generate.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,YAAY,EAGb,MAAM,oCAAoC,CAAC;AAC5C,OAAO,KAAK,EACV,SAAS,EAET,WAAW,EAGZ,MAAM,aAAa,CAAC;AAyBrB,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,YAAY,EACjB,MAAM,EAAE,SAAS,GAChB,IAAI,CA2KN;AAED,wBAAgB,oBAAoB,CAAC,YAAY,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAO7E"}
@@ -1,5 +1,5 @@
1
1
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
- import { createDocumentPaths, ensureDir, pathToFileHref, toDisplayPath } from "../utils/paths.js";
2
+ import { createDocumentPaths, ensureDir, pathToFileHref, toDisplayPath, } from "../utils/paths.js";
3
3
  import { printCssWarning, validateHtmlDocument } from "../utils/html.js";
4
4
  import { renderPdf } from "../utils/renderer.js";
5
5
  import { errorResult, textResult } from "./helpers.js";
@@ -29,15 +29,45 @@ export function registerGenerateTool(ext, config) {
29
29
  type: "object",
30
30
  properties: {
31
31
  title: { type: "string", description: "Document title." },
32
- html: { type: "string", description: "Complete HTML document. Must include doctype, html, head, and body." },
33
- outputName: { type: "string", description: "Optional stable filename/folder slug." },
34
- format: { type: "string", enum: [...formatEnum], description: "Page format. Default from config: A4." },
35
- orientation: { type: "string", enum: [...orientationEnum], description: "Page orientation. Default: portrait." },
36
- printBackground: { type: "boolean", description: "Render CSS backgrounds. Default: true." },
37
- allowRemoteAssets: { type: "boolean", description: "Allow http/https assets during validation/render. Default: false." },
38
- viewport: { type: "object", properties: { width: { type: "number" }, height: { type: "number" } }, description: "Override the browser viewport before rendering (useful for slide decks). Default: auto." },
39
- width: { type: "number", description: "Override PDF page width in pixels. Default: auto from format." },
40
- height: { type: "number", description: "Override PDF page height in pixels. Default: auto from format." },
32
+ html: {
33
+ type: "string",
34
+ description: "Complete HTML document. Must include doctype, html, head, and body.",
35
+ },
36
+ outputName: {
37
+ type: "string",
38
+ description: "Optional stable filename/folder slug.",
39
+ },
40
+ format: {
41
+ type: "string",
42
+ enum: [...formatEnum],
43
+ description: "Page format. Default from config: A4.",
44
+ },
45
+ orientation: {
46
+ type: "string",
47
+ enum: [...orientationEnum],
48
+ description: "Page orientation. Default: portrait.",
49
+ },
50
+ printBackground: {
51
+ type: "boolean",
52
+ description: "Render CSS backgrounds. Default: true.",
53
+ },
54
+ allowRemoteAssets: {
55
+ type: "boolean",
56
+ description: "Allow http/https assets during validation/render. Default: false.",
57
+ },
58
+ viewport: {
59
+ type: "object",
60
+ properties: { width: { type: "number" }, height: { type: "number" } },
61
+ description: "Override the browser viewport before rendering (useful for slide decks). Default: auto.",
62
+ },
63
+ width: {
64
+ type: "number",
65
+ description: "Custom PDF page width in millimetres. Omit for format-based sizing.",
66
+ },
67
+ height: {
68
+ type: "number",
69
+ description: "Custom PDF page height in millimetres. Omit for format-based sizing.",
70
+ },
41
71
  margin: {
42
72
  type: "object",
43
73
  properties: {
@@ -69,7 +99,13 @@ export function registerGenerateTool(ext, config) {
69
99
  pdfPath: toDisplayPath(ctx.cwd, paths.pdfPath),
70
100
  createdAt: now,
71
101
  updatedAt: now,
72
- page: { format, orientation, margin },
102
+ page: {
103
+ format,
104
+ orientation,
105
+ width: p.width,
106
+ height: p.height,
107
+ margin,
108
+ },
73
109
  source: "generated",
74
110
  };
75
111
  writeManifest(paths.manifestPath, manifest);
@@ -86,7 +122,10 @@ export function registerGenerateTool(ext, config) {
86
122
  height: p.height,
87
123
  waitUntil: allowRemoteAssets ? "networkidle" : "load",
88
124
  });
89
- writeManifest(paths.manifestPath, { ...manifest, updatedAt: new Date().toISOString() });
125
+ writeManifest(paths.manifestPath, {
126
+ ...manifest,
127
+ updatedAt: new Date().toISOString(),
128
+ });
90
129
  const warning = printCssWarning(p.html);
91
130
  const lines = [
92
131
  "✓ PDF generated from editable HTML",
@@ -1 +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"}
1
+ {"version":3,"file":"preview.d.ts","sourceRoot":"","sources":["../../../../src/extensions/pdf/tools/preview.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,YAAY,EAGb,MAAM,oCAAoC,CAAC;AAC5C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAW7C,wBAAgB,mBAAmB,CACjC,GAAG,EAAE,YAAY,EACjB,MAAM,EAAE,SAAS,GAChB,IAAI,CAuFN"}
@@ -1,6 +1,6 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { dirname, join } from "node:path";
3
- import { assertInsideWorkspace, ensureParent, resolveWorkspacePath, toDisplayPath } from "../utils/paths.js";
3
+ import { assertInsideWorkspace, ensureParent, resolveWorkspacePath, toDisplayPath, } from "../utils/paths.js";
4
4
  import { validateHtmlDocument } from "../utils/html.js";
5
5
  import { renderPreview } from "../utils/renderer.js";
6
6
  import { errorResult, textResult } from "./helpers.js";
@@ -13,9 +13,23 @@ export function registerPreviewTool(ext, config) {
13
13
  parameters: {
14
14
  type: "object",
15
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." },
16
+ htmlPath: {
17
+ type: "string",
18
+ description: "Path to an HTML file inside the workspace.",
19
+ },
20
+ outputPath: {
21
+ type: "string",
22
+ description: "Optional PNG output path inside the workspace.",
23
+ },
24
+ orientation: {
25
+ type: "string",
26
+ enum: ["portrait", "landscape"],
27
+ description: "Preview orientation. Default from PDF config.",
28
+ },
29
+ allowRemoteAssets: {
30
+ type: "boolean",
31
+ description: "Allow http/https assets during preview. Default: false.",
32
+ },
19
33
  },
20
34
  required: ["htmlPath"],
21
35
  },
@@ -26,12 +40,16 @@ export function registerPreviewTool(ext, config) {
26
40
  assertInsideWorkspace(ctx.cwd, htmlPath);
27
41
  if (!existsSync(htmlPath))
28
42
  throw new Error(`HTML file not found: ${p.htmlPath}`);
29
- const previewPath = p.outputPath ? resolveWorkspacePath(ctx.cwd, p.outputPath) : join(dirname(htmlPath), "preview.png");
43
+ const previewPath = p.outputPath
44
+ ? resolveWorkspacePath(ctx.cwd, p.outputPath)
45
+ : join(dirname(htmlPath), "preview.png");
30
46
  assertInsideWorkspace(ctx.cwd, previewPath);
31
47
  ensureParent(previewPath);
32
48
  const allowRemoteAssets = p.allowRemoteAssets ?? config.allowRemoteAssets;
33
- validateHtmlDocument(readFileSync(htmlPath, "utf-8"), { allowRemoteAssets });
34
- const preview = await renderPreview(config, htmlPath, previewPath, allowRemoteAssets);
49
+ validateHtmlDocument(readFileSync(htmlPath, "utf-8"), {
50
+ allowRemoteAssets,
51
+ });
52
+ const preview = await renderPreview(config, htmlPath, previewPath, allowRemoteAssets, p.orientation);
35
53
  return textResult([
36
54
  "✓ HTML preview rendered",
37
55
  `HTML: ${toDisplayPath(ctx.cwd, htmlPath)}`,
@@ -1 +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,CAgH7E"}
1
+ {"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../../../../src/extensions/pdf/tools/render.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,YAAY,EAGb,MAAM,oCAAoC,CAAC;AAC5C,OAAO,KAAK,EACV,SAAS,EAKV,MAAM,aAAa,CAAC;AAyBrB,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,YAAY,EAAE,MAAM,EAAE,SAAS,GAAG,IAAI,CAqK7E"}
@@ -1,6 +1,6 @@
1
1
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { basename, dirname, extname, join } from "node:path";
3
- import { assertInsideWorkspace, ensureParent, pathToFileHref, resolveWorkspacePath, toDisplayPath } from "../utils/paths.js";
3
+ import { assertInsideWorkspace, ensureParent, pathToFileHref, resolveWorkspacePath, toDisplayPath, } from "../utils/paths.js";
4
4
  import { printCssWarning, validateHtmlDocument } from "../utils/html.js";
5
5
  import { renderPdf } from "../utils/renderer.js";
6
6
  import { errorResult, textResult } from "./helpers.js";
@@ -25,15 +25,45 @@ export function registerRenderTool(ext, config) {
25
25
  parameters: {
26
26
  type: "object",
27
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
- viewport: { type: "object", properties: { width: { type: "number" }, height: { type: "number" } }, description: "Override the browser viewport before rendering. Default: auto." },
35
- width: { type: "number", description: "Override PDF page width in pixels. Default: auto from format." },
36
- height: { type: "number", description: "Override PDF page height in pixels. Default: auto from format." },
28
+ htmlPath: {
29
+ type: "string",
30
+ description: "Path to the existing HTML file, relative to workspace or absolute inside workspace.",
31
+ },
32
+ outputPath: {
33
+ type: "string",
34
+ description: "Optional PDF output path, relative to workspace or absolute inside workspace.",
35
+ },
36
+ format: {
37
+ type: "string",
38
+ enum: ["A4", "Letter"],
39
+ description: "Page format. Default: A4.",
40
+ },
41
+ orientation: {
42
+ type: "string",
43
+ enum: ["portrait", "landscape"],
44
+ description: "Page orientation. Default: portrait.",
45
+ },
46
+ printBackground: {
47
+ type: "boolean",
48
+ description: "Render CSS backgrounds. Default: true.",
49
+ },
50
+ allowRemoteAssets: {
51
+ type: "boolean",
52
+ description: "Allow http/https assets during validation/render. Default: false.",
53
+ },
54
+ viewport: {
55
+ type: "object",
56
+ properties: { width: { type: "number" }, height: { type: "number" } },
57
+ description: "Override the browser viewport before rendering. Default: auto.",
58
+ },
59
+ width: {
60
+ type: "number",
61
+ description: "Custom PDF page width in millimetres. Omit for format-based sizing.",
62
+ },
63
+ height: {
64
+ type: "number",
65
+ description: "Custom PDF page height in millimetres. Omit for format-based sizing.",
66
+ },
37
67
  margin: {
38
68
  type: "object",
39
69
  properties: {
@@ -53,7 +83,9 @@ export function registerRenderTool(ext, config) {
53
83
  assertInsideWorkspace(ctx.cwd, htmlPath);
54
84
  if (!existsSync(htmlPath))
55
85
  throw new Error(`HTML file not found: ${p.htmlPath}`);
56
- const outputPath = p.outputPath ? resolveWorkspacePath(ctx.cwd, p.outputPath) : defaultPdfPath(htmlPath);
86
+ const outputPath = p.outputPath
87
+ ? resolveWorkspacePath(ctx.cwd, p.outputPath)
88
+ : defaultPdfPath(htmlPath);
57
89
  assertInsideWorkspace(ctx.cwd, outputPath);
58
90
  ensureParent(outputPath);
59
91
  const html = readFileSync(htmlPath, "utf-8");
@@ -84,7 +116,13 @@ export function registerRenderTool(ext, config) {
84
116
  pdfPath: toDisplayPath(ctx.cwd, outputPath),
85
117
  createdAt: now,
86
118
  updatedAt: now,
87
- page: { format, orientation, margin },
119
+ page: {
120
+ format,
121
+ orientation,
122
+ width: p.width,
123
+ height: p.height,
124
+ margin,
125
+ },
88
126
  source: "rendered",
89
127
  };
90
128
  const manifestPath = join(dirname(htmlPath), "manifest.json");
@@ -20,7 +20,7 @@ export interface RenderPdfResult {
20
20
  bytes: number;
21
21
  }
22
22
  export declare function renderPdf(config: PdfConfig, opts: RenderPdfOptions): Promise<RenderPdfResult>;
23
- export declare function renderPreview(config: PdfConfig, htmlPath: string, previewPath: string, allowRemoteAssets: boolean): Promise<{
23
+ export declare function renderPreview(config: PdfConfig, htmlPath: string, previewPath: string, allowRemoteAssets: boolean, orientation?: PdfOrientation): Promise<{
24
24
  previewPath: string;
25
25
  bytes: number;
26
26
  }>;
@@ -1 +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,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IAC7C,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,CAmCnG;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"}
1
+ {"version":3,"file":"renderer.d.ts","sourceRoot":"","sources":["../../../../src/extensions/pdf/utils/renderer.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,SAAS,EACT,SAAS,EACT,SAAS,EACT,cAAc,EACf,MAAM,aAAa,CAAC;AAGrB,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,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IAC7C,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,CAC7B,MAAM,EAAE,SAAS,EACjB,IAAI,EAAE,gBAAgB,GACrB,OAAO,CAAC,eAAe,CAAC,CAuD1B;AAED,wBAAsB,aAAa,CACjC,MAAM,EAAE,SAAS,EACjB,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,EACnB,iBAAiB,EAAE,OAAO,EAC1B,WAAW,GAAE,cAA0C,GACtD,OAAO,CAAC;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CAgCjD"}
@@ -2,6 +2,17 @@ import { existsSync, statSync } from "node:fs";
2
2
  import { chromium } from "playwright";
3
3
  import { pathToFileHref } from "./paths.js";
4
4
  export async function renderPdf(config, opts) {
5
+ for (const [name, value] of [
6
+ ["width", opts.width],
7
+ ["height", opts.height],
8
+ ]) {
9
+ if (value !== undefined && (!Number.isFinite(value) || value <= 0)) {
10
+ throw new Error(`Custom PDF ${name} must be a positive number of millimetres.`);
11
+ }
12
+ }
13
+ if ((opts.width === undefined) !== (opts.height === undefined)) {
14
+ throw new Error("Custom PDF width and height must be provided together.");
15
+ }
5
16
  const browser = await chromium.launch({ headless: true });
6
17
  try {
7
18
  const page = await browser.newPage();
@@ -12,7 +23,9 @@ export async function renderPdf(config, opts) {
12
23
  if (!opts.allowRemoteAssets) {
13
24
  await page.route("**/*", (route) => {
14
25
  const url = route.request().url();
15
- if (url.startsWith("file://") || url.startsWith("data:") || url === "about:blank") {
26
+ if (url.startsWith("file://") ||
27
+ url.startsWith("data:") ||
28
+ url === "about:blank") {
16
29
  return route.continue();
17
30
  }
18
31
  return route.abort();
@@ -27,8 +40,10 @@ export async function renderPdf(config, opts) {
27
40
  format: opts.format,
28
41
  landscape: opts.orientation === "landscape",
29
42
  printBackground: opts.printBackground,
30
- ...(opts.width ? { width: opts.width } : {}),
31
- ...(opts.height ? { height: opts.height } : {}),
43
+ // Chromium expects PDF dimensions as strings with units. Numeric values
44
+ // are CSS pixels and can accidentally make a paper-sized page tiny.
45
+ ...(opts.width !== undefined ? { width: `${opts.width}mm` } : {}),
46
+ ...(opts.height !== undefined ? { height: `${opts.height}mm` } : {}),
32
47
  margin: opts.margin,
33
48
  });
34
49
  if (!existsSync(opts.pdfPath))
@@ -39,20 +54,30 @@ export async function renderPdf(config, opts) {
39
54
  await browser.close();
40
55
  }
41
56
  }
42
- export async function renderPreview(config, htmlPath, previewPath, allowRemoteAssets) {
57
+ export async function renderPreview(config, htmlPath, previewPath, allowRemoteAssets, orientation = config.defaultOrientation) {
43
58
  const browser = await chromium.launch({ headless: true });
44
59
  try {
45
- const page = await browser.newPage({ viewport: { width: 1240, height: 1754 }, deviceScaleFactor: 1 });
60
+ // Keep the preview in the same aspect ratio as the configured paper. This is
61
+ // intentionally a CSS viewport, not a page.pdf width/height override.
62
+ const viewport = orientation === "landscape"
63
+ ? { width: 1754, height: 1240 }
64
+ : { width: 1240, height: 1754 };
65
+ const page = await browser.newPage({ viewport, deviceScaleFactor: 1 });
46
66
  page.setDefaultTimeout(config.renderTimeoutMs);
47
67
  if (!allowRemoteAssets) {
48
68
  await page.route("**/*", (route) => {
49
69
  const url = route.request().url();
50
- if (url.startsWith("file://") || url.startsWith("data:") || url === "about:blank")
70
+ if (url.startsWith("file://") ||
71
+ url.startsWith("data:") ||
72
+ url === "about:blank")
51
73
  return route.continue();
52
74
  return route.abort();
53
75
  });
54
76
  }
55
- await page.goto(pathToFileHref(htmlPath), { waitUntil: "load", timeout: config.renderTimeoutMs });
77
+ await page.goto(pathToFileHref(htmlPath), {
78
+ waitUntil: "load",
79
+ timeout: config.renderTimeoutMs,
80
+ });
56
81
  await page.screenshot({ path: previewPath, fullPage: true, type: "png" });
57
82
  return { previewPath, bytes: statSync(previewPath).size };
58
83
  }
@@ -1 +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"}
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;AA2CD,wBAAgB,cAAc,CAC5B,IAAI,EAAE,MAAM,EACZ,IAAI,CAAC,EAAE;IAAE,iBAAiB,CAAC,EAAE,OAAO,CAAA;CAAE,GACrC,kBAAkB,EAAE,CAkMtB;AAED,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,kBAAkB,EAAE,GAC3B,OAAO,CAET"}
@@ -15,6 +15,23 @@ const WIDE_WIDTH_RE = /(?:width|min-width)\s*:\s*(?:[1-9]\d{3,}px|(?:8[5-9]|9\d|
15
15
  const IMAGE_RE = /<img\b[^>]*>/gi;
16
16
  const WIDTH_ATTR_RE = /\bwidth\s*=\s*["'][^"']+["']/i;
17
17
  const HEIGHT_ATTR_RE = /\bheight\s*=\s*["'][^"']+["']/i;
18
+ const TRANSFORM_SCALE_RE = /transform\s*:[^;{}]*scale\s*\(/i;
19
+ const ZOOM_RE = /zoom\s*:\s*(?!1(?:\.0+)?\b)(?:0?\.)?\d+/i;
20
+ const FIXED_VIEWPORT_RE = /(?:width|height)\s*:\s*100vw|(?:width|height)\s*:\s*100vh|(?:width|min-width|height|min-height)\s*:\s*\d{4,}px/i;
21
+ const PAGE_SIZE_RE = /@page\s*{[^}]*size\s*:\s*([^;}]*)/is;
22
+ function normalizePageSize(size) {
23
+ const value = size.trim().toLowerCase();
24
+ const orientation = value.includes("landscape")
25
+ ? "landscape"
26
+ : value.includes("portrait")
27
+ ? "portrait"
28
+ : undefined;
29
+ if (value.includes("a4"))
30
+ return { format: "A4", orientation };
31
+ if (value.includes("letter"))
32
+ return { format: "Letter", orientation };
33
+ return { orientation };
34
+ }
18
35
  export function analyzePdfHtml(html, opts) {
19
36
  const issues = [];
20
37
  if (html.length > 5_000_000) {
@@ -34,13 +51,25 @@ export function analyzePdfHtml(html, opts) {
34
51
  });
35
52
  }
36
53
  if (!HTML_RE.test(html)) {
37
- issues.push({ severity: "error", code: "missing_html", message: "HTML must include <html>." });
54
+ issues.push({
55
+ severity: "error",
56
+ code: "missing_html",
57
+ message: "HTML must include <html>.",
58
+ });
38
59
  }
39
60
  if (!HEAD_RE.test(html)) {
40
- issues.push({ severity: "error", code: "missing_head", message: "HTML must include <head>." });
61
+ issues.push({
62
+ severity: "error",
63
+ code: "missing_head",
64
+ message: "HTML must include <head>.",
65
+ });
41
66
  }
42
67
  if (!BODY_RE.test(html)) {
43
- issues.push({ severity: "error", code: "missing_body", message: "HTML must include <body>." });
68
+ issues.push({
69
+ severity: "error",
70
+ code: "missing_body",
71
+ message: "HTML must include <body>.",
72
+ });
44
73
  }
45
74
  if (!TITLE_RE.test(html)) {
46
75
  issues.push({
@@ -55,7 +84,7 @@ export function analyzePdfHtml(html, opts) {
55
84
  severity: "info",
56
85
  code: "missing_viewport",
57
86
  message: "HTML has no viewport meta tag.",
58
- suggestion: "Add <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">.",
87
+ suggestion: 'Add <meta name="viewport" content="width=device-width, initial-scale=1">.',
59
88
  });
60
89
  }
61
90
  if (!PAGE_RE.test(html)) {
@@ -123,6 +152,42 @@ export function analyzePdfHtml(html, opts) {
123
152
  suggestion: "Check pagination carefully; absolute elements can overlap page breaks.",
124
153
  });
125
154
  }
155
+ if (TRANSFORM_SCALE_RE.test(html)) {
156
+ issues.push({
157
+ severity: "warning",
158
+ code: "transform_scale",
159
+ message: "CSS uses transform: scale(), which can shrink or clip PDF content.",
160
+ suggestion: "Use print-native dimensions and layout instead of scaling the whole document.",
161
+ });
162
+ }
163
+ if (ZOOM_RE.test(html)) {
164
+ issues.push({
165
+ severity: "warning",
166
+ code: "css_zoom",
167
+ message: "CSS uses zoom other than 1, which can change PDF pagination and scale.",
168
+ suggestion: "Remove zoom for print output or scope it to an intentional print layout.",
169
+ });
170
+ }
171
+ if (FIXED_VIEWPORT_RE.test(html)) {
172
+ issues.push({
173
+ severity: "warning",
174
+ code: "fixed_or_viewport_layout",
175
+ message: "A viewport-sized or fixed oversized layout may not fit the selected paper.",
176
+ suggestion: "Use responsive width: 100% with print margins and avoid fixed pixel canvases.",
177
+ });
178
+ }
179
+ const pageSizeMatch = html.match(PAGE_SIZE_RE);
180
+ if (pageSizeMatch) {
181
+ const pageSize = normalizePageSize(pageSizeMatch[1]);
182
+ if (pageSize.orientation && pageSize.format) {
183
+ issues.push({
184
+ severity: "info",
185
+ code: "page_size_orientation",
186
+ message: `@page explicitly requests ${pageSize.format} ${pageSize.orientation}.`,
187
+ suggestion: "Keep @page size aligned with the PDF format and orientation options.",
188
+ });
189
+ }
190
+ }
126
191
  if (WIDE_WIDTH_RE.test(html)) {
127
192
  issues.push({
128
193
  severity: "warning",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aexol/spectral",
3
- "version": "0.9.103",
3
+ "version": "0.9.104",
4
4
  "description": "AI coding agent for Aexol with relay-based browser access.",
5
5
  "type": "module",
6
6
  "private": false,