gutterpress 0.11.18-alpha.4 → 0.11.18-alpha.6

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/api/index.js CHANGED
@@ -136,7 +136,7 @@ import {
136
136
  testRemoteAccess,
137
137
  updateExtensions,
138
138
  validateProjectExtensions
139
- } from "../index-1ergtg60.js";
139
+ } from "../index-bf051pv9.js";
140
140
  import"../index-ht1bcqdd.js";
141
141
  import {
142
142
  BuildError
@@ -1,19 +1,19 @@
1
1
  import {
2
2
  runBuild,
3
3
  splitOutPath
4
- } from "./cli-dmcy8kay.js";
4
+ } from "./cli-mwg13jsy.js";
5
5
  import {
6
6
  log
7
- } from "./cli-36v78t6w.js";
7
+ } from "./cli-8hr9cs2d.js";
8
8
  import {
9
9
  UsageError,
10
10
  parseFormat,
11
11
  parsePdfxFlavor,
12
12
  rejectExtraPositionals,
13
13
  rejectUnknownFlags
14
- } from "./cli-88hddvbz.js";
14
+ } from "./cli-tgfkz5p1.js";
15
15
  import"./cli-ht1bcqdd.js";
16
- import"./cli-5x7s8bap.js";
16
+ import"./cli-ptez7zcc.js";
17
17
  import {
18
18
  BuildError
19
19
  } from "./cli-46ycxe6r.js";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  package_default
3
- } from "./cli-88hddvbz.js";
3
+ } from "./cli-tgfkz5p1.js";
4
4
  import {
5
5
  execCapture,
6
6
  findTool,
@@ -13,12 +13,12 @@ import {
13
13
  stampCreator,
14
14
  stripAnnotations,
15
15
  warn
16
- } from "./cli-36v78t6w.js";
16
+ } from "./cli-8hr9cs2d.js";
17
17
  import {
18
18
  DEBOUNCE,
19
19
  UsageError,
20
20
  resolvePort
21
- } from "./cli-88hddvbz.js";
21
+ } from "./cli-tgfkz5p1.js";
22
22
  import {
23
23
  blackTextToGray
24
24
  } from "./cli-ht1bcqdd.js";
@@ -27,7 +27,7 @@ import {
27
27
  getAssetPath,
28
28
  getAssetsDir,
29
29
  lastPageMode
30
- } from "./cli-5x7s8bap.js";
30
+ } from "./cli-ptez7zcc.js";
31
31
  import {
32
32
  BuildError,
33
33
  EXIT_CODES
@@ -10082,7 +10082,7 @@ class PdfOutput {
10082
10082
  const rawPdf = pdfxMode ? path13.join(stage, "raw.pdf") : path13.resolve(pdfFile);
10083
10083
  await fsp2.mkdir(path13.dirname(path13.resolve(pdfFile)), { recursive: true });
10084
10084
  log.info("Rendering HTML to PDF via the Gutterpress engine (native Chromium pagination)");
10085
- const { buildNativePdf } = await import("./engine-6zfj0jte.js");
10085
+ const { buildNativePdf } = await import("./engine-wtnakejg.js");
10086
10086
  const engineDiagnostics = await buildNativePdf(htmlFile, rawPdf, {
10087
10087
  title: config.title,
10088
10088
  author: config.authors.length > 0 ? config.authors.join(", ") : undefined,
@@ -2230,6 +2230,54 @@ ${mapCss}`;
2230
2230
  if (multicol.length)
2231
2231
  log(`audit: ${multicol.length} fragmenting multicol warning(s)`);
2232
2232
  }
2233
+ {
2234
+ const missing = JSON.parse(await page.evaluate(`(() => {
2235
+ const words = new Map();
2236
+ const styled = new Map();
2237
+ const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
2238
+ for (let n = walker.nextNode(); n; n = walker.nextNode()) {
2239
+ const found = n.data.match(/\\p{L}{7,}/gu);
2240
+ const el = n.parentElement;
2241
+ if (!found || !el) continue;
2242
+ if (!styled.has(el)) styled.set(el, getComputedStyle(el).hyphens === "auto");
2243
+ if (!styled.get(el)) continue;
2244
+ const host = el.closest("[lang]");
2245
+ const lang = host ? host.getAttribute("lang") : null;
2246
+ if (!words.has(lang)) words.set(lang, new Set());
2247
+ const set = words.get(lang);
2248
+ for (const w of found) if (set.size < 200) set.add(w);
2249
+ }
2250
+ if (!words.size) return "[]";
2251
+ const box = document.createElement("div");
2252
+ box.style.cssText = "position:absolute;left:-10000px;top:0;visibility:hidden";
2253
+ const probes = [];
2254
+ for (const [lang, set] of words) {
2255
+ for (const w of [...set].sort((a, b) => b.length - a.length).slice(0, 8)) {
2256
+ const pair = ["auto", "manual"].map((h) => {
2257
+ const d = document.createElement("div");
2258
+ if (lang !== null) d.setAttribute("lang", lang);
2259
+ d.style.cssText = "width:1px;font:16px/16px serif;word-break:normal;overflow-wrap:normal;hyphens:" + h;
2260
+ d.textContent = w;
2261
+ box.appendChild(d);
2262
+ return d;
2263
+ });
2264
+ probes.push({ lang, pair });
2265
+ }
2266
+ }
2267
+ document.body.appendChild(box);
2268
+ const hyphenates = new Set();
2269
+ for (const { lang, pair } of probes)
2270
+ if (pair[0].offsetHeight > pair[1].offsetHeight) hyphenates.add(lang);
2271
+ box.remove();
2272
+ const ua = navigator.userAgent;
2273
+ const browser = (/Electron\\/[\\d.]+/.exec(ua) || /(?:HeadlessChrome|Chrome)\\/[\\d.]+/.exec(ua) || [ua])[0];
2274
+ return JSON.stringify([...words.keys()].filter((l) => !hyphenates.has(l)).map((lang) => ({ lang, browser })));
2275
+ })()`));
2276
+ for (const { lang, browser: browser2 } of missing)
2277
+ diagnose("engine.hyphenation.unsupported", `Your stylesheets set hyphens: auto, but the browser making this PDF (${browser2}) has no hyphenation dictionary for ${lang ? `"${lang}"` : "the document's language"}, so no word is hyphenated automatically and lines (and pages) break differently than in a browser that has one. Build in a browser that hyphenates, such as Chrome (which the gutterpress CLI uses), or add soft hyphens (&shy;) where a word may break.`);
2278
+ if (missing.length)
2279
+ log(`audit: hyphenation unavailable for ${missing.length} language(s)`);
2280
+ }
2233
2281
  if (bytes === undefined) {
2234
2282
  prints++;
2235
2283
  bytes = await printPdf(page);
@@ -4,7 +4,7 @@ import {
4
4
  // package.json
5
5
  var package_default = {
6
6
  name: "gutterpress",
7
- version: "0.11.18-alpha.4",
7
+ version: "0.11.18-alpha.6",
8
8
  description: "Markdown-to-PDF converter for professional print layout using a native Chromium print engine and Ghostscript.",
9
9
  author: "itlackey",
10
10
  license: "MPL-2.0",
package/dist/cli.js CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  UsageError,
7
7
  package_default,
8
8
  rejectUnknownFlags
9
- } from "./cli-88hddvbz.js";
9
+ } from "./cli-tgfkz5p1.js";
10
10
  import {
11
11
  EXIT_CODES
12
12
  } from "./cli-46ycxe6r.js";
@@ -18,14 +18,14 @@ import {
18
18
  import { defineCommand, parseArgs, runMain } from "citty";
19
19
  import { statSync } from "node:fs";
20
20
  var SUBCOMMANDS = {
21
- new: () => import("./new-ede0f1nm.js").then((m) => m.default),
22
- preview: () => import("./preview-y2dxxbrk.js").then((m) => m.default),
23
- build: () => import("./build-63nwjhzf.js").then((m) => m.default),
24
- publish: () => import("./publish-n60n0hm1.js").then((m) => m.default),
25
- validate: () => import("./validate-2c7sfyxn.js").then((m) => m.default),
26
- preflight: () => import("./preflight-b6c3k3qt.js").then((m) => m.default),
27
- doctor: () => import("./doctor-n1cwrwx2.js").then((m) => m.default),
28
- ext: () => import("./ext-ddfjfcg4.js").then((m) => m.default)
21
+ new: () => import("./new-x1p4agff.js").then((m) => m.default),
22
+ preview: () => import("./preview-46pvad9t.js").then((m) => m.default),
23
+ build: () => import("./build-0ggjr8b2.js").then((m) => m.default),
24
+ publish: () => import("./publish-zm4tp6ms.js").then((m) => m.default),
25
+ validate: () => import("./validate-vrqdx7xb.js").then((m) => m.default),
26
+ preflight: () => import("./preflight-q75rr1pz.js").then((m) => m.default),
27
+ doctor: () => import("./doctor-7eeh2933.js").then((m) => m.default),
28
+ ext: () => import("./ext-rnvgxe91.js").then((m) => m.default)
29
29
  };
30
30
  var VERSION = package_default.version;
31
31
  var main = defineCommand({
@@ -50,7 +50,7 @@ async function preflightRequiredInvocations(rawArgs) {
50
50
  const [command, ...commandArgs] = rawArgs;
51
51
  try {
52
52
  if (command === "new") {
53
- const { newArgs } = await import("./new-ede0f1nm.js");
53
+ const { newArgs } = await import("./new-x1p4agff.js");
54
54
  rejectUnknownFlags(commandArgs, newArgs, "new");
55
55
  const parsed = parseArgs(commandArgs, {
56
56
  ...newArgs,
@@ -62,7 +62,7 @@ async function preflightRequiredInvocations(rawArgs) {
62
62
  return;
63
63
  }
64
64
  if (command === "preflight") {
65
- const { preflightArgs } = await import("./preflight-b6c3k3qt.js");
65
+ const { preflightArgs } = await import("./preflight-q75rr1pz.js");
66
66
  rejectUnknownFlags(commandArgs, preflightArgs, "preflight");
67
67
  const parsed = parseArgs(commandArgs, {
68
68
  ...preflightArgs,
@@ -82,7 +82,7 @@ async function preflightRequiredInvocations(rawArgs) {
82
82
  }
83
83
  if (subcommand.startsWith("-"))
84
84
  return;
85
- const { EXT_SUBCOMMANDS, extAddArgs, extRemoveArgs, extEnableArgs, extDisableArgs } = await import("./ext-ddfjfcg4.js");
85
+ const { EXT_SUBCOMMANDS, extAddArgs, extRemoveArgs, extEnableArgs, extDisableArgs } = await import("./ext-rnvgxe91.js");
86
86
  if (!EXT_SUBCOMMANDS.includes(subcommand)) {
87
87
  throw new UsageError(`gutterpress ext: unknown command "${subcommand}"`);
88
88
  }
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  getSystemDiagnostics,
3
3
  log
4
- } from "./cli-36v78t6w.js";
4
+ } from "./cli-8hr9cs2d.js";
5
5
  import {
6
6
  UsageError,
7
7
  rejectExtraPositionals,
8
8
  rejectUnknownFlags
9
- } from "./cli-88hddvbz.js";
9
+ } from "./cli-tgfkz5p1.js";
10
10
  import"./cli-46ycxe6r.js";
11
11
  import"./cli-7fkhgtfn.js";
12
12
  import"./cli-37x76zdn.js";
@@ -8,7 +8,7 @@ import { type PostprocessResult } from "./postprocess.ts";
8
8
  * set is closed here so a surface's label table can be asserted complete
9
9
  * against it rather than drifting silently as checks are added.
10
10
  */
11
- export type BuildDiagnosticCode = "engine.width.overflow" | "engine.width.intrinsic" | "engine.xref.broken" | "engine.abspos.leak" | "engine.layer.trapped" | "engine.multicol.dead-column" | "engine.content.overheight" | "engine.image.low-dpi" | "engine.flush.margin-box" | "engine.page-background.unreferenced";
11
+ export type BuildDiagnosticCode = "engine.width.overflow" | "engine.width.intrinsic" | "engine.xref.broken" | "engine.abspos.leak" | "engine.layer.trapped" | "engine.multicol.dead-column" | "engine.content.overheight" | "engine.image.low-dpi" | "engine.flush.margin-box" | "engine.page-background.unreferenced" | "engine.hyphenation.unsupported";
12
12
  export declare const BUILD_DIAGNOSTIC_CODES: readonly BuildDiagnosticCode[];
13
13
  export interface BuildDiagnostic {
14
14
  code: BuildDiagnosticCode;
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  build
3
- } from "./cli-5x7s8bap.js";
3
+ } from "./cli-ptez7zcc.js";
4
4
  import {
5
5
  BuildError
6
6
  } from "./cli-46ycxe6r.js";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  build
3
- } from "./index-qbd874dh.js";
3
+ } from "./index-n8vs6pss.js";
4
4
  import {
5
5
  BuildError
6
6
  } from "./index-jt552wtf.js";
@@ -12,17 +12,17 @@ import {
12
12
  searchNpmExtensions,
13
13
  setExtensionEnabled,
14
14
  updateExtensions
15
- } from "./cli-dmcy8kay.js";
16
- import"./cli-36v78t6w.js";
15
+ } from "./cli-mwg13jsy.js";
16
+ import"./cli-8hr9cs2d.js";
17
17
  import {
18
18
  UsageError,
19
19
  exitForUsage,
20
20
  rejectExtraPositionals,
21
21
  rejectUnknownFlags,
22
22
  resolveProjectDir
23
- } from "./cli-88hddvbz.js";
23
+ } from "./cli-tgfkz5p1.js";
24
24
  import"./cli-ht1bcqdd.js";
25
- import"./cli-5x7s8bap.js";
25
+ import"./cli-ptez7zcc.js";
26
26
  import {
27
27
  EXIT_CODES
28
28
  } from "./cli-46ycxe6r.js";
@@ -5916,7 +5916,7 @@ import git from "isomorphic-git";
5916
5916
  // package.json
5917
5917
  var package_default = {
5918
5918
  name: "gutterpress",
5919
- version: "0.11.18-alpha.4",
5919
+ version: "0.11.18-alpha.6",
5920
5920
  description: "Markdown-to-PDF converter for professional print layout using a native Chromium print engine and Ghostscript.",
5921
5921
  author: "itlackey",
5922
5922
  license: "MPL-2.0",
@@ -10662,7 +10662,7 @@ class PdfOutput {
10662
10662
  const rawPdf = pdfxMode ? path13.join(stage, "raw.pdf") : path13.resolve(pdfFile);
10663
10663
  await fsp2.mkdir(path13.dirname(path13.resolve(pdfFile)), { recursive: true });
10664
10664
  log.info("Rendering HTML to PDF via the Gutterpress engine (native Chromium pagination)");
10665
- const { buildNativePdf } = await import("./engine-zw8626be.js");
10665
+ const { buildNativePdf } = await import("./engine-zy6fzs5a.js");
10666
10666
  const engineDiagnostics = await buildNativePdf(htmlFile, rawPdf, {
10667
10667
  title: config.title,
10668
10668
  author: config.authors.length > 0 ? config.authors.join(", ") : undefined,
@@ -724,7 +724,8 @@ var BUILD_DIAGNOSTIC_CODES = [
724
724
  "engine.content.overheight",
725
725
  "engine.image.low-dpi",
726
726
  "engine.flush.margin-box",
727
- "engine.page-background.unreferenced"
727
+ "engine.page-background.unreferenced",
728
+ "engine.hyphenation.unsupported"
728
729
  ];
729
730
  async function build(opts) {
730
731
  const log = opts.onProgress ?? (() => {});
@@ -1333,6 +1334,54 @@ ${mapCss}`;
1333
1334
  if (multicol.length)
1334
1335
  log(`audit: ${multicol.length} fragmenting multicol warning(s)`);
1335
1336
  }
1337
+ {
1338
+ const missing = JSON.parse(await page.evaluate(`(() => {
1339
+ const words = new Map();
1340
+ const styled = new Map();
1341
+ const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
1342
+ for (let n = walker.nextNode(); n; n = walker.nextNode()) {
1343
+ const found = n.data.match(/\\p{L}{7,}/gu);
1344
+ const el = n.parentElement;
1345
+ if (!found || !el) continue;
1346
+ if (!styled.has(el)) styled.set(el, getComputedStyle(el).hyphens === "auto");
1347
+ if (!styled.get(el)) continue;
1348
+ const host = el.closest("[lang]");
1349
+ const lang = host ? host.getAttribute("lang") : null;
1350
+ if (!words.has(lang)) words.set(lang, new Set());
1351
+ const set = words.get(lang);
1352
+ for (const w of found) if (set.size < 200) set.add(w);
1353
+ }
1354
+ if (!words.size) return "[]";
1355
+ const box = document.createElement("div");
1356
+ box.style.cssText = "position:absolute;left:-10000px;top:0;visibility:hidden";
1357
+ const probes = [];
1358
+ for (const [lang, set] of words) {
1359
+ for (const w of [...set].sort((a, b) => b.length - a.length).slice(0, 8)) {
1360
+ const pair = ["auto", "manual"].map((h) => {
1361
+ const d = document.createElement("div");
1362
+ if (lang !== null) d.setAttribute("lang", lang);
1363
+ d.style.cssText = "width:1px;font:16px/16px serif;word-break:normal;overflow-wrap:normal;hyphens:" + h;
1364
+ d.textContent = w;
1365
+ box.appendChild(d);
1366
+ return d;
1367
+ });
1368
+ probes.push({ lang, pair });
1369
+ }
1370
+ }
1371
+ document.body.appendChild(box);
1372
+ const hyphenates = new Set();
1373
+ for (const { lang, pair } of probes)
1374
+ if (pair[0].offsetHeight > pair[1].offsetHeight) hyphenates.add(lang);
1375
+ box.remove();
1376
+ const ua = navigator.userAgent;
1377
+ const browser = (/Electron\\/[\\d.]+/.exec(ua) || /(?:HeadlessChrome|Chrome)\\/[\\d.]+/.exec(ua) || [ua])[0];
1378
+ return JSON.stringify([...words.keys()].filter((l) => !hyphenates.has(l)).map((lang) => ({ lang, browser })));
1379
+ })()`));
1380
+ for (const { lang, browser: browser2 } of missing)
1381
+ diagnose("engine.hyphenation.unsupported", `Your stylesheets set hyphens: auto, but the browser making this PDF (${browser2}) has no hyphenation dictionary for ${lang ? `"${lang}"` : "the document's language"}, so no word is hyphenated automatically and lines (and pages) break differently than in a browser that has one. Build in a browser that hyphenates, such as Chrome (which the gutterpress CLI uses), or add soft hyphens (&shy;) where a word may break.`);
1382
+ if (missing.length)
1383
+ log(`audit: hyphenation unavailable for ${missing.length} language(s)`);
1384
+ }
1336
1385
  if (bytes === undefined) {
1337
1386
  prints++;
1338
1387
  bytes = await printPdf(page);
package/dist/index.js CHANGED
@@ -153,11 +153,11 @@ import {
153
153
  testRemoteAccess,
154
154
  updateExtensions,
155
155
  validateProjectExtensions
156
- } from "./index-1ergtg60.js";
156
+ } from "./index-bf051pv9.js";
157
157
  import"./index-ht1bcqdd.js";
158
158
  import {
159
159
  BUILD_DIAGNOSTIC_CODES
160
- } from "./index-qbd874dh.js";
160
+ } from "./index-n8vs6pss.js";
161
161
  import {
162
162
  BuildError
163
163
  } from "./index-jt552wtf.js";
@@ -7,17 +7,17 @@ import {
7
7
  TARGET_IDS,
8
8
  scaffoldExtension,
9
9
  scaffoldProject
10
- } from "./cli-dmcy8kay.js";
10
+ } from "./cli-mwg13jsy.js";
11
11
  import {
12
12
  resolveGhostscript
13
- } from "./cli-36v78t6w.js";
13
+ } from "./cli-8hr9cs2d.js";
14
14
  import {
15
15
  UsageError,
16
16
  rejectExtraPositionals,
17
17
  rejectUnknownFlags
18
- } from "./cli-88hddvbz.js";
18
+ } from "./cli-tgfkz5p1.js";
19
19
  import"./cli-ht1bcqdd.js";
20
- import"./cli-5x7s8bap.js";
20
+ import"./cli-ptez7zcc.js";
21
21
  import {
22
22
  EXIT_CODES
23
23
  } from "./cli-46ycxe6r.js";
@@ -2,17 +2,17 @@ import {
2
2
  executeValidation,
3
3
  publishTargetFor,
4
4
  reportMissingTools
5
- } from "./cli-dmcy8kay.js";
5
+ } from "./cli-mwg13jsy.js";
6
6
  import {
7
7
  log
8
- } from "./cli-36v78t6w.js";
8
+ } from "./cli-8hr9cs2d.js";
9
9
  import {
10
10
  UsageError,
11
11
  rejectExtraPositionals,
12
12
  rejectUnknownFlags
13
- } from "./cli-88hddvbz.js";
13
+ } from "./cli-tgfkz5p1.js";
14
14
  import"./cli-ht1bcqdd.js";
15
- import"./cli-5x7s8bap.js";
15
+ import"./cli-ptez7zcc.js";
16
16
  import {
17
17
  BuildError,
18
18
  EXIT_CODES
@@ -7,10 +7,10 @@ import {
7
7
  runBuild,
8
8
  splitOutPath,
9
9
  startPreviewServer
10
- } from "./cli-dmcy8kay.js";
10
+ } from "./cli-mwg13jsy.js";
11
11
  import {
12
12
  log
13
- } from "./cli-36v78t6w.js";
13
+ } from "./cli-8hr9cs2d.js";
14
14
  import {
15
15
  UsageError,
16
16
  parseFormat,
@@ -18,9 +18,9 @@ import {
18
18
  rejectExtraPositionals,
19
19
  rejectUnknownFlags,
20
20
  resolvePort
21
- } from "./cli-88hddvbz.js";
21
+ } from "./cli-tgfkz5p1.js";
22
22
  import"./cli-ht1bcqdd.js";
23
- import"./cli-5x7s8bap.js";
23
+ import"./cli-ptez7zcc.js";
24
24
  import {
25
25
  BuildError
26
26
  } from "./cli-46ycxe6r.js";
@@ -11,18 +11,18 @@ import {
11
11
  publishProviderFor,
12
12
  resolvePublishFormat,
13
13
  runPublish
14
- } from "./cli-dmcy8kay.js";
14
+ } from "./cli-mwg13jsy.js";
15
15
  import {
16
16
  FileTokenStore,
17
17
  log
18
- } from "./cli-36v78t6w.js";
18
+ } from "./cli-8hr9cs2d.js";
19
19
  import {
20
20
  UsageError,
21
21
  rejectExtraPositionals,
22
22
  rejectUnknownFlags
23
- } from "./cli-88hddvbz.js";
23
+ } from "./cli-tgfkz5p1.js";
24
24
  import"./cli-ht1bcqdd.js";
25
- import"./cli-5x7s8bap.js";
25
+ import"./cli-ptez7zcc.js";
26
26
  import {
27
27
  EXIT_CODES
28
28
  } from "./cli-46ycxe6r.js";
@@ -1,16 +1,16 @@
1
1
  import {
2
2
  executeAndReport
3
- } from "./cli-dmcy8kay.js";
3
+ } from "./cli-mwg13jsy.js";
4
4
  import {
5
5
  log
6
- } from "./cli-36v78t6w.js";
6
+ } from "./cli-8hr9cs2d.js";
7
7
  import {
8
8
  UsageError,
9
9
  rejectExtraPositionals,
10
10
  rejectUnknownFlags
11
- } from "./cli-88hddvbz.js";
11
+ } from "./cli-tgfkz5p1.js";
12
12
  import"./cli-ht1bcqdd.js";
13
- import"./cli-5x7s8bap.js";
13
+ import"./cli-ptez7zcc.js";
14
14
  import {
15
15
  BuildError,
16
16
  EXIT_CODES
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gutterpress",
3
- "version": "0.11.18-alpha.4",
3
+ "version": "0.11.18-alpha.6",
4
4
  "description": "Markdown-to-PDF converter for professional print layout using a native Chromium print engine and Ghostscript.",
5
5
  "author": "itlackey",
6
6
  "license": "MPL-2.0",