@imontene/mdrender 1.0.0

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.
Files changed (82) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/LICENSE +21 -0
  3. package/README.md +243 -0
  4. package/dist/assets/theme-dark.css +40 -0
  5. package/dist/assets/theme.css +545 -0
  6. package/dist/assets.d.ts +45 -0
  7. package/dist/assets.js +109 -0
  8. package/dist/bin.d.ts +2 -0
  9. package/dist/bin.js +16 -0
  10. package/dist/browser/cache.d.ts +15 -0
  11. package/dist/browser/cache.js +30 -0
  12. package/dist/browser/detect.d.ts +17 -0
  13. package/dist/browser/detect.js +90 -0
  14. package/dist/browser/setup.d.ts +13 -0
  15. package/dist/browser/setup.js +69 -0
  16. package/dist/cli.d.ts +8 -0
  17. package/dist/cli.js +194 -0
  18. package/dist/config.d.ts +6 -0
  19. package/dist/config.js +32 -0
  20. package/dist/convert.d.ts +64 -0
  21. package/dist/convert.js +167 -0
  22. package/dist/diagnostics.d.ts +8 -0
  23. package/dist/diagnostics.js +6 -0
  24. package/dist/errors.d.ts +12 -0
  25. package/dist/errors.js +21 -0
  26. package/dist/exit-codes.d.ts +8 -0
  27. package/dist/exit-codes.js +8 -0
  28. package/dist/frontmatter.d.ts +10 -0
  29. package/dist/frontmatter.js +26 -0
  30. package/dist/html.d.ts +2 -0
  31. package/dist/html.js +12 -0
  32. package/dist/i18n.d.ts +9 -0
  33. package/dist/i18n.js +59 -0
  34. package/dist/index.d.ts +17 -0
  35. package/dist/index.js +18 -0
  36. package/dist/inputs.d.ts +26 -0
  37. package/dist/inputs.js +118 -0
  38. package/dist/markdown/alerts.d.ts +6 -0
  39. package/dist/markdown/alerts.js +35 -0
  40. package/dist/markdown/env.d.ts +29 -0
  41. package/dist/markdown/env.js +16 -0
  42. package/dist/markdown/highlight.d.ts +6 -0
  43. package/dist/markdown/highlight.js +17 -0
  44. package/dist/markdown/images.d.ts +9 -0
  45. package/dist/markdown/images.js +93 -0
  46. package/dist/markdown/index.d.ts +27 -0
  47. package/dist/markdown/index.js +69 -0
  48. package/dist/markdown/math.d.ts +9 -0
  49. package/dist/markdown/math.js +147 -0
  50. package/dist/markdown/mermaid.d.ts +8 -0
  51. package/dist/markdown/mermaid.js +22 -0
  52. package/dist/markdown/task-lists.d.ts +6 -0
  53. package/dist/markdown/task-lists.js +40 -0
  54. package/dist/markdown/toc.d.ts +22 -0
  55. package/dist/markdown/toc.js +101 -0
  56. package/dist/options.d.ts +55 -0
  57. package/dist/options.js +131 -0
  58. package/dist/paths.d.ts +2 -0
  59. package/dist/paths.js +4 -0
  60. package/dist/preview.d.ts +22 -0
  61. package/dist/preview.js +126 -0
  62. package/dist/render/browser.d.ts +48 -0
  63. package/dist/render/browser.js +134 -0
  64. package/dist/render/fonts.d.ts +10 -0
  65. package/dist/render/fonts.js +47 -0
  66. package/dist/render/header-footer.d.ts +22 -0
  67. package/dist/render/header-footer.js +39 -0
  68. package/dist/render/katex.d.ts +2 -0
  69. package/dist/render/katex.js +19 -0
  70. package/dist/render/mermaid.d.ts +25 -0
  71. package/dist/render/mermaid.js +91 -0
  72. package/dist/render/page.d.ts +7 -0
  73. package/dist/render/page.js +23 -0
  74. package/dist/render/template.d.ts +22 -0
  75. package/dist/render/template.js +54 -0
  76. package/dist/run.d.ts +26 -0
  77. package/dist/run.js +71 -0
  78. package/dist/version.d.ts +1 -0
  79. package/dist/version.js +4 -0
  80. package/dist/watch.d.ts +25 -0
  81. package/dist/watch.js +128 -0
  82. package/package.json +97 -0
@@ -0,0 +1,131 @@
1
+ import path from "node:path";
2
+ import { ExitCode } from "./exit-codes.js";
3
+ import { MdRenderError } from "./errors.js";
4
+ import { DEFAULT_LANG } from "./i18n.js";
5
+ export const PAGE_SIZES = ["A3", "A4", "A5", "Letter", "Legal", "Tabloid"];
6
+ export const MERMAID_THEMES = ["default", "neutral", "dark", "forest", "base"];
7
+ export const BUILTIN_THEMES = ["light", "dark"];
8
+ export const DEFAULT_OPTIONS = {
9
+ pageSize: "A4",
10
+ margin: "20mm",
11
+ landscape: false,
12
+ toc: false,
13
+ tocDepth: 3,
14
+ pageNumbers: true,
15
+ theme: "light",
16
+ css: [],
17
+ lang: DEFAULT_LANG,
18
+ };
19
+ export class OptionsError extends MdRenderError {
20
+ constructor(message) {
21
+ super(message, ExitCode.Usage);
22
+ }
23
+ }
24
+ const LENGTH = /^\d+(\.\d+)?(mm|cm|in|px)$/;
25
+ /** Parse a CSS margin shorthand into the four sides, or throw. */
26
+ export function parseMargin(margin) {
27
+ const parts = margin.trim().split(/\s+/);
28
+ if (parts.length < 1 || parts.length > 4 || !parts.every((p) => LENGTH.test(p))) {
29
+ throw new OptionsError(`invalid margin "${margin}": use 1 to 4 lengths in mm, cm, in or px, e.g. "20mm" or "15mm 20mm"`);
30
+ }
31
+ const [top, right = top, bottom = top, left = right] = parts;
32
+ return { top, right, bottom, left };
33
+ }
34
+ const toCamel = (key) => key.replace(/[-_]([a-z])/g, (_m, c) => c.toUpperCase());
35
+ const isString = (v) => typeof v === "string";
36
+ const text = (value, describe) => {
37
+ if (value instanceof Date)
38
+ return value.toISOString().slice(0, 10);
39
+ if (isString(value) || typeof value === "number")
40
+ return String(value);
41
+ throw new OptionsError(`${describe} must be text`);
42
+ };
43
+ const bool = (value, describe) => {
44
+ if (typeof value === "boolean")
45
+ return value;
46
+ throw new OptionsError(`${describe} must be true or false`);
47
+ };
48
+ const oneOf = (choices) => (value, describe) => {
49
+ const match = choices.find((c) => isString(value) && c.toLowerCase() === value.toLowerCase());
50
+ if (match)
51
+ return match;
52
+ throw new OptionsError(`${describe} must be one of: ${choices.join(", ")}`);
53
+ };
54
+ const VALIDATORS = {
55
+ pageSize: oneOf(PAGE_SIZES),
56
+ margin: (value, describe) => {
57
+ const margin = text(value, describe, "");
58
+ try {
59
+ parseMargin(margin);
60
+ }
61
+ catch (error) {
62
+ throw new OptionsError(`${describe}: ${error.message}`);
63
+ }
64
+ return margin;
65
+ },
66
+ landscape: bool,
67
+ toc: bool,
68
+ tocDepth: (value, describe) => {
69
+ const depth = Number(value);
70
+ if (Number.isInteger(depth) && depth >= 1 && depth <= 6)
71
+ return depth;
72
+ throw new OptionsError(`${describe} must be a whole number from 1 to 6`);
73
+ },
74
+ pageNumbers: bool,
75
+ header: text,
76
+ footer: text,
77
+ mermaidTheme: oneOf(MERMAID_THEMES),
78
+ theme: (value, describe, baseDir) => {
79
+ const theme = text(value, describe, baseDir);
80
+ const builtin = BUILTIN_THEMES.find((t) => t === theme.toLowerCase());
81
+ if (builtin)
82
+ return builtin;
83
+ if (/\.css$/i.test(theme))
84
+ return path.resolve(baseDir, theme);
85
+ throw new OptionsError(`${describe} must be light, dark or a .css file`);
86
+ },
87
+ css: (value, describe, baseDir) => {
88
+ const list = Array.isArray(value) ? value : [value];
89
+ if (!list.every(isString))
90
+ throw new OptionsError(`${describe} must be a file path or a list`);
91
+ return list.map((file) => path.resolve(baseDir, file));
92
+ },
93
+ lang: text,
94
+ title: text,
95
+ subtitle: text,
96
+ author: text,
97
+ date: text,
98
+ };
99
+ /**
100
+ * Validate options from a config file, front matter or the command line.
101
+ *
102
+ * Keys may be camelCase or kebab-case (`page-size`). Unknown keys are ignored so front matter can
103
+ * hold other metadata. Relative CSS paths resolve against `baseDir`.
104
+ */
105
+ export function normalizeOptions(raw, source, baseDir,
106
+ /** How to name a key in error messages (e.g. as a command-line flag). */
107
+ label = (key) => `"${key}"`) {
108
+ const result = {};
109
+ for (const [key, value] of Object.entries(raw)) {
110
+ if (value === undefined || value === null)
111
+ continue;
112
+ const name = toCamel(key);
113
+ const validate = VALIDATORS[name];
114
+ if (!validate)
115
+ continue;
116
+ result[name] = validate(value, `${source}: ${label(key)}`, baseDir);
117
+ }
118
+ return result;
119
+ }
120
+ /** Merge option layers, later ones winning. CSS lists accumulate instead of replacing. */
121
+ export function mergeOptions(...layers) {
122
+ const merged = { ...DEFAULT_OPTIONS, css: [] };
123
+ for (const layer of layers) {
124
+ const { css, ...rest } = layer;
125
+ Object.assign(merged, rest);
126
+ if (css)
127
+ merged.css.push(...css);
128
+ }
129
+ return merged;
130
+ }
131
+ //# sourceMappingURL=options.js.map
@@ -0,0 +1,2 @@
1
+ export type OutputFormat = "pdf" | "html";
2
+ export declare function isMarkdownFile(file: string): boolean;
package/dist/paths.js ADDED
@@ -0,0 +1,4 @@
1
+ export function isMarkdownFile(file) {
2
+ return /\.(md|markdown)$/i.test(file);
3
+ }
4
+ //# sourceMappingURL=paths.js.map
@@ -0,0 +1,22 @@
1
+ import { type RenderSettings } from "./convert.js";
2
+ import { ExitCode } from "./exit-codes.js";
3
+ import { type Io } from "./run.js";
4
+ export interface PreviewOptions {
5
+ input: string;
6
+ settings: RenderSettings;
7
+ io: Io;
8
+ /** The preview stops when this signal aborts (Ctrl+C in the CLI). */
9
+ signal: AbortSignal;
10
+ /** Port to listen on; 0 picks a free one. */
11
+ port?: number;
12
+ /** Open the default browser on the preview URL. */
13
+ open?: boolean;
14
+ debounceMs?: number;
15
+ }
16
+ /** Open a URL with the system's default browser. */
17
+ export declare function openInBrowser(url: string): void;
18
+ /**
19
+ * Serve a live HTML preview of a Markdown file on localhost and reload it on every change to
20
+ * the file, its config file or its stylesheets.
21
+ */
22
+ export declare function preview(options: PreviewOptions): Promise<ExitCode>;
@@ -0,0 +1,126 @@
1
+ import { spawn } from "node:child_process";
2
+ import { createServer } from "node:http";
3
+ import path from "node:path";
4
+ import { watch } from "chokidar";
5
+ import { BrowserPool, renderDocument } from "./convert.js";
6
+ import { formatDiagnostic } from "./diagnostics.js";
7
+ import { ExitCode } from "./exit-codes.js";
8
+ import { escapeHtml } from "./html.js";
9
+ import { displayPath, info } from "./run.js";
10
+ /** Reloads the page when the server announces a new render (Server-Sent Events). */
11
+ const LIVE_RELOAD = `<script>
12
+ new EventSource("/events").addEventListener("reload", () => location.reload());
13
+ </script>`;
14
+ function errorPage(message) {
15
+ return `<!doctype html><html><head><meta charset="utf-8"><title>mdrender</title></head>
16
+ <body style="font-family: system-ui, sans-serif; padding: 2rem; color: #b42318;">
17
+ <h1>mdrender</h1><pre style="white-space: pre-wrap;">${escapeHtml(message)}</pre>
18
+ </body></html>`;
19
+ }
20
+ /** Open a URL with the system's default browser. */
21
+ export function openInBrowser(url) {
22
+ const [command, args] = process.platform === "win32"
23
+ ? ["cmd", ["/c", "start", "", url]]
24
+ : process.platform === "darwin"
25
+ ? ["open", [url]]
26
+ : ["xdg-open", [url]];
27
+ const child = spawn(command, args, { detached: true, stdio: "ignore", windowsHide: true });
28
+ child.on("error", () => undefined);
29
+ child.unref();
30
+ }
31
+ /**
32
+ * Serve a live HTML preview of a Markdown file on localhost and reload it on every change to
33
+ * the file, its config file or its stylesheets.
34
+ */
35
+ export async function preview(options) {
36
+ const { settings, io, signal } = options;
37
+ const input = path.resolve(options.input);
38
+ const browsers = new BrowserPool(settings.browser);
39
+ const clients = new Set();
40
+ let page = errorPage("Rendering…");
41
+ let dependencies = [];
42
+ const render = async () => {
43
+ try {
44
+ const doc = await renderDocument(input, settings, browsers);
45
+ for (const diagnostic of doc.diagnostics) {
46
+ io.err(`${formatDiagnostic(diagnostic, displayPath(input))}\n`);
47
+ }
48
+ page = doc.html.replace(/<\/body>\s*<\/html>\s*$/, `${LIVE_RELOAD}\n</body>\n</html>\n`);
49
+ dependencies = doc.dependencies;
50
+ }
51
+ catch (error) {
52
+ // Keep serving: show the problem in the page and retry on the next change.
53
+ const message = error instanceof Error ? error.message : String(error);
54
+ io.err(`error: ${message}\n`);
55
+ page = errorPage(message).replace("</body>", `${LIVE_RELOAD}</body>`);
56
+ }
57
+ for (const client of clients)
58
+ client.write("event: reload\ndata: \n\n");
59
+ };
60
+ await render();
61
+ const server = createServer((request, response) => {
62
+ if (request.url === "/events") {
63
+ response.writeHead(200, {
64
+ "Content-Type": "text/event-stream",
65
+ "Cache-Control": "no-cache",
66
+ Connection: "keep-alive",
67
+ });
68
+ response.write(": connected\n\n");
69
+ clients.add(response);
70
+ request.on("close", () => clients.delete(response));
71
+ return;
72
+ }
73
+ if (request.url === "/" || request.url?.startsWith("/?")) {
74
+ response.writeHead(200, {
75
+ "Content-Type": "text/html; charset=utf-8",
76
+ "Cache-Control": "no-store",
77
+ });
78
+ response.end(page);
79
+ return;
80
+ }
81
+ response.writeHead(404).end();
82
+ });
83
+ // Only reachable from this machine.
84
+ await new Promise((resolve, reject) => {
85
+ server.once("error", reject);
86
+ server.listen(options.port ?? 0, "127.0.0.1", () => resolve());
87
+ });
88
+ const { port } = server.address();
89
+ const url = `http://127.0.0.1:${port}/`;
90
+ io.out(`${url}\n`);
91
+ info(io, `Preview of ${displayPath(input)} at ${url} (Ctrl+C to stop)\n`);
92
+ if (options.open)
93
+ openInBrowser(url);
94
+ const watcher = watch([input, ...dependencies], {
95
+ ignoreInitial: true,
96
+ awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 },
97
+ });
98
+ let timer;
99
+ let running = Promise.resolve();
100
+ watcher.on("all", () => {
101
+ clearTimeout(timer);
102
+ timer = setTimeout(() => {
103
+ running = running.then(async () => {
104
+ const before = new Set(dependencies);
105
+ await render();
106
+ watcher.add(dependencies.filter((dep) => !before.has(dep)));
107
+ });
108
+ }, options.debounceMs ?? 150);
109
+ });
110
+ await new Promise((resolve) => {
111
+ if (signal.aborted)
112
+ resolve();
113
+ else
114
+ signal.addEventListener("abort", () => resolve(), { once: true });
115
+ });
116
+ clearTimeout(timer);
117
+ await running.catch(() => undefined);
118
+ await watcher.close();
119
+ for (const client of clients)
120
+ client.end();
121
+ server.closeAllConnections();
122
+ await new Promise((resolve) => server.close(() => resolve()));
123
+ await browsers.close();
124
+ return ExitCode.Ok;
125
+ }
126
+ //# sourceMappingURL=preview.js.map
@@ -0,0 +1,48 @@
1
+ import { type Page, type PDFOptions } from "puppeteer-core";
2
+ /**
3
+ * Whether a failed launch should be retried without Chrome's sandbox. Ubuntu 23.10+ blocks the
4
+ * sandbox (AppArmor user-namespace restrictions) for browsers not installed from a package,
5
+ * such as the one `mdrender setup` downloads. The pages mdrender opens run no document
6
+ * JavaScript and have no network access, so running unsandboxed there is an acceptable
7
+ * fallback.
8
+ */
9
+ export declare function shouldRetryWithoutSandbox(error: unknown, args: string[], platform?: NodeJS.Platform): boolean;
10
+ export interface PrintOptions {
11
+ output: string;
12
+ format?: PDFOptions["format"];
13
+ landscape?: boolean;
14
+ margin?: {
15
+ top: string;
16
+ right: string;
17
+ bottom: string;
18
+ left: string;
19
+ };
20
+ /** Chrome header/footer templates; both omitted means no header or footer. */
21
+ headerTemplate?: string;
22
+ footerTemplate?: string;
23
+ /** Let the page load http(s) resources (remote images, stylesheets). Off by default. */
24
+ allowRemote?: boolean;
25
+ }
26
+ /** One headless browser shared by every step of a conversion (Mermaid, then PDF). */
27
+ export declare class BrowserSession {
28
+ private readonly browser;
29
+ readonly timeout: number;
30
+ private constructor();
31
+ static open(executablePath: string, timeout?: number,
32
+ /** Told when the browser had to start without its sandbox. */
33
+ onUnsandboxed?: () => void): Promise<BrowserSession>;
34
+ /**
35
+ * A page for trusted code only (our own scripts), with no network access; see `printPdf` for
36
+ * user content.
37
+ */
38
+ newPage(): Promise<Page>;
39
+ /**
40
+ * Print a complete HTML document to PDF. Page scripts are disabled: the document is fully
41
+ * rendered by then (math by KaTeX, diagrams by Mermaid), and any <script> in the Markdown
42
+ * must not run.
43
+ */
44
+ printPdf(html: string, options: PrintOptions): Promise<{
45
+ blocked: string[];
46
+ }>;
47
+ close(): Promise<void>;
48
+ }
@@ -0,0 +1,134 @@
1
+ import { launch } from "puppeteer-core";
2
+ import { RenderError } from "../errors.js";
3
+ const DEFAULT_TIMEOUT = 60_000;
4
+ function launchArgs() {
5
+ const args = ["--disable-gpu", "--font-render-hinting=none"];
6
+ // Chromium refuses to start its sandbox as root (common in containers and CI).
7
+ const isRoot = process.platform !== "win32" && process.getuid?.() === 0;
8
+ if (isRoot || process.env.MDRENDER_NO_SANDBOX === "1") {
9
+ args.push("--no-sandbox");
10
+ }
11
+ return args;
12
+ }
13
+ /**
14
+ * Whether a failed launch should be retried without Chrome's sandbox. Ubuntu 23.10+ blocks the
15
+ * sandbox (AppArmor user-namespace restrictions) for browsers not installed from a package,
16
+ * such as the one `mdrender setup` downloads. The pages mdrender opens run no document
17
+ * JavaScript and have no network access, so running unsandboxed there is an acceptable
18
+ * fallback.
19
+ */
20
+ export function shouldRetryWithoutSandbox(error, args, platform = process.platform) {
21
+ return (platform === "linux" &&
22
+ !args.includes("--no-sandbox") &&
23
+ /no usable sandbox/i.test(String(error?.message ?? error)));
24
+ }
25
+ /**
26
+ * Only allow requests the document needs: inline data, plus http(s) when `allowRemote`.
27
+ * file: URLs are always refused, so HTML in a Markdown file cannot pull local files into
28
+ * the PDF (local images are embedded beforehand). Returns the URLs that were blocked.
29
+ */
30
+ async function restrictNetwork(page, allowRemote) {
31
+ const blocked = [];
32
+ await page.setRequestInterception(true);
33
+ page.on("request", (request) => {
34
+ if (request.isInterceptResolutionHandled())
35
+ return;
36
+ const url = request.url();
37
+ const scheme = url.slice(0, url.indexOf(":")).toLowerCase();
38
+ if (scheme === "data" || scheme === "about" || scheme === "blob") {
39
+ void request.continue();
40
+ }
41
+ else if (allowRemote && (scheme === "http" || scheme === "https")) {
42
+ void request.continue();
43
+ }
44
+ else {
45
+ blocked.push(url);
46
+ void request.abort("blockedbyclient");
47
+ }
48
+ });
49
+ return blocked;
50
+ }
51
+ /** One headless browser shared by every step of a conversion (Mermaid, then PDF). */
52
+ export class BrowserSession {
53
+ browser;
54
+ timeout;
55
+ constructor(browser, timeout) {
56
+ this.browser = browser;
57
+ this.timeout = timeout;
58
+ }
59
+ static async open(executablePath, timeout = DEFAULT_TIMEOUT,
60
+ /** Told when the browser had to start without its sandbox. */
61
+ onUnsandboxed) {
62
+ const args = launchArgs();
63
+ const start = (launchWith) => launch({ executablePath, headless: true, args: launchWith, timeout });
64
+ try {
65
+ let browser;
66
+ try {
67
+ browser = await start(args);
68
+ }
69
+ catch (error) {
70
+ if (!shouldRetryWithoutSandbox(error, args))
71
+ throw error;
72
+ browser = await start([...args, "--no-sandbox"]);
73
+ onUnsandboxed?.();
74
+ }
75
+ return new BrowserSession(browser, timeout);
76
+ }
77
+ catch (error) {
78
+ throw new RenderError(`could not start the browser at ${executablePath}: ${error.message}`);
79
+ }
80
+ }
81
+ /**
82
+ * A page for trusted code only (our own scripts), with no network access; see `printPdf` for
83
+ * user content.
84
+ */
85
+ async newPage() {
86
+ const page = await this.browser.newPage();
87
+ page.setDefaultTimeout(this.timeout);
88
+ await restrictNetwork(page, false);
89
+ return page;
90
+ }
91
+ /**
92
+ * Print a complete HTML document to PDF. Page scripts are disabled: the document is fully
93
+ * rendered by then (math by KaTeX, diagrams by Mermaid), and any <script> in the Markdown
94
+ * must not run.
95
+ */
96
+ async printPdf(html, options) {
97
+ const margin = options.margin ?? { top: "20mm", right: "20mm", bottom: "20mm", left: "20mm" };
98
+ const headerFooter = options.headerTemplate !== undefined || options.footerTemplate !== undefined;
99
+ const page = await this.browser.newPage();
100
+ page.setDefaultTimeout(this.timeout);
101
+ const blocked = await restrictNetwork(page, options.allowRemote ?? false);
102
+ try {
103
+ await page.setJavaScriptEnabled(false);
104
+ await page.setContent(html, { waitUntil: "load" });
105
+ // Make sure every embedded font has loaded before printing.
106
+ await page.evaluate("document.fonts.ready.then(() => undefined)");
107
+ await page.pdf({
108
+ path: options.output,
109
+ format: options.format ?? "A4",
110
+ landscape: options.landscape ?? false,
111
+ margin,
112
+ printBackground: true,
113
+ displayHeaderFooter: headerFooter,
114
+ headerTemplate: options.headerTemplate ?? "<span></span>",
115
+ footerTemplate: options.footerTemplate ?? "<span></span>",
116
+ // PDF bookmarks from the headings, and a tagged (accessible) PDF.
117
+ outline: true,
118
+ tagged: true,
119
+ timeout: this.timeout,
120
+ });
121
+ return { blocked: [...new Set(blocked)] };
122
+ }
123
+ catch (error) {
124
+ throw new RenderError(`failed to render PDF: ${error.message}`);
125
+ }
126
+ finally {
127
+ await page.close();
128
+ }
129
+ }
130
+ async close() {
131
+ await this.browser.close();
132
+ }
133
+ }
134
+ //# sourceMappingURL=browser.js.map
@@ -0,0 +1,10 @@
1
+ /** Inter Regular alone, for page headers/footers (Chrome renders them apart from the page). */
2
+ export declare function interRegularFontFace(): string;
3
+ /**
4
+ * Inline a Fontsource stylesheet: the woff2 source becomes a data URI (the legacy woff fallback is
5
+ * dropped) so the document needs no network or file access. `font-display: block` makes the
6
+ * browser wait for the real font instead of printing with a fallback.
7
+ */
8
+ export declare function inlineFontStylesheet(cssKey: string): string;
9
+ /** @font-face rules for every bundled font, with the font files embedded. */
10
+ export declare function fontFaceCss(): string;
@@ -0,0 +1,47 @@
1
+ import { INTER_DIR, INTER_FACES, MONO_STYLESHEETS, readAsset, readTextAsset, woff2References, } from "../assets.js";
2
+ /**
3
+ * Static (non-variable) font files are used on purpose: Chromium embeds variable fonts in PDFs
4
+ * as Type 3 fonts, while static fonts are embedded as proper TrueType fonts.
5
+ *
6
+ * Text: Inter 4 (the typeface served by Google Fonts) from the upstream release, full glyph set.
7
+ * Code: JetBrains Mono from Fontsource (split into unicode-range subsets).
8
+ */
9
+ function woff2DataUri(key) {
10
+ return `data:font/woff2;base64,${readAsset(key).toString("base64")}`;
11
+ }
12
+ function interFace({ weight, style, file }) {
13
+ return `@font-face {
14
+ font-family: "Inter";
15
+ font-style: ${style};
16
+ font-weight: ${weight};
17
+ font-display: block;
18
+ src: url(${woff2DataUri(`${INTER_DIR}/${file}`)}) format("woff2");
19
+ }`;
20
+ }
21
+ let regularFace;
22
+ /** Inter Regular alone, for page headers/footers (Chrome renders them apart from the page). */
23
+ export function interRegularFontFace() {
24
+ regularFace ??= interFace(INTER_FACES[0]);
25
+ return regularFace;
26
+ }
27
+ /**
28
+ * Inline a Fontsource stylesheet: the woff2 source becomes a data URI (the legacy woff fallback is
29
+ * dropped) so the document needs no network or file access. `font-display: block` makes the
30
+ * browser wait for the real font instead of printing with a fallback.
31
+ */
32
+ export function inlineFontStylesheet(cssKey) {
33
+ const css = readTextAsset(cssKey);
34
+ const fonts = woff2References(cssKey, css);
35
+ let index = 0;
36
+ return css
37
+ .replace(/font-display:\s*swap/g, "font-display: block")
38
+ .replace(/,\s*url\([^)]+\.woff\)\s*format\(['"]woff['"]\)/g, "")
39
+ .replace(/url\((['"]?)([^)'"]+\.woff2)\1\)/g, () => `url(${woff2DataUri(fonts[index++])})`);
40
+ }
41
+ let cached;
42
+ /** @font-face rules for every bundled font, with the font files embedded. */
43
+ export function fontFaceCss() {
44
+ cached ??= [...INTER_FACES.map(interFace), ...MONO_STYLESHEETS.map(inlineFontStylesheet)].join("\n");
45
+ return cached;
46
+ }
47
+ //# sourceMappingURL=fonts.js.map
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Header/footer text with placeholders, in up to three columns separated by `|`:
3
+ *
4
+ * "{title} | | {page} / {pages}" → title on the left, page numbers on the right
5
+ * "Confidencial" → centered
6
+ *
7
+ * Placeholders: {page}, {pages}, {title}, {author}, {date}. Use `\|` for a literal bar.
8
+ */
9
+ export interface HeaderFooterValues {
10
+ title: string;
11
+ author?: string;
12
+ date?: string;
13
+ }
14
+ /**
15
+ * Chrome header/footer template. Chrome renders it apart from the page, so it carries its own
16
+ * styles and its own copy of Inter.
17
+ */
18
+ export declare function headerFooterTemplate(spec: string | undefined, values: HeaderFooterValues, horizontalMargin: {
19
+ left: string;
20
+ right: string;
21
+ }): string;
22
+ export declare const DEFAULT_FOOTER = "{page} / {pages}";
@@ -0,0 +1,39 @@
1
+ import { escapeHtml } from "../html.js";
2
+ import { interRegularFontFace } from "./fonts.js";
3
+ const CHROME_CLASSES = { page: "pageNumber", pages: "totalPages" };
4
+ function renderText(text, values) {
5
+ return escapeHtml(text.trim()).replace(/\{(\w+)\}/g, (match, name) => {
6
+ const chromeClass = CHROME_CLASSES[name];
7
+ if (chromeClass)
8
+ return `<span class="${chromeClass}"></span>`;
9
+ const value = values[name];
10
+ return value !== undefined ? escapeHtml(value) : match;
11
+ });
12
+ }
13
+ /** Split on `|` (but not `\|`) into left, center and right columns. */
14
+ function columns(spec) {
15
+ const parts = spec.split(/(?<!\\)\|/).map((p) => p.replace(/\\\|/g, "|"));
16
+ if (parts.length === 1)
17
+ return ["", parts[0], ""];
18
+ if (parts.length === 2)
19
+ return [parts[0], "", parts[1]];
20
+ return [parts[0], parts[1], parts.slice(2).join("|")];
21
+ }
22
+ /**
23
+ * Chrome header/footer template. Chrome renders it apart from the page, so it carries its own
24
+ * styles and its own copy of Inter.
25
+ */
26
+ export function headerFooterTemplate(spec, values, horizontalMargin) {
27
+ if (!spec)
28
+ return "<span></span>";
29
+ const [left, center, right] = columns(spec).map((c) => renderText(c, values));
30
+ return `<style>${interRegularFontFace()}</style>
31
+ <div style="box-sizing: border-box; width: 100%; display: flex; padding: 0 ${horizontalMargin.right} 0 ${horizontalMargin.left};
32
+ font-family: Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; font-size: 8pt; color: #59636e;">
33
+ <span style="flex: 1; text-align: left;">${left}</span>
34
+ <span style="flex: 1; text-align: center;">${center}</span>
35
+ <span style="flex: 1; text-align: right;">${right}</span>
36
+ </div>`;
37
+ }
38
+ export const DEFAULT_FOOTER = "{page} / {pages}";
39
+ //# sourceMappingURL=header-footer.js.map
@@ -0,0 +1,2 @@
1
+ /** KaTeX's stylesheet with its woff2 fonts embedded (woff/ttf fallbacks dropped). */
2
+ export declare function katexCss(): string;
@@ -0,0 +1,19 @@
1
+ import { KATEX_CSS, readAsset, readTextAsset, woff2References } from "../assets.js";
2
+ let cached;
3
+ /** KaTeX's stylesheet with its woff2 fonts embedded (woff/ttf fallbacks dropped). */
4
+ export function katexCss() {
5
+ if (cached)
6
+ return cached;
7
+ const css = readTextAsset(KATEX_CSS);
8
+ const fonts = woff2References(KATEX_CSS, css);
9
+ let index = 0;
10
+ cached = css
11
+ .replace(/,url\([^)]+\.woff\) format\("woff"\)/g, "")
12
+ .replace(/,url\([^)]+\.ttf\) format\("truetype"\)/g, "")
13
+ .replace(/url\(([^)]+\.woff2)\)/g, () => {
14
+ const data = readAsset(fonts[index++]).toString("base64");
15
+ return `url(data:font/woff2;base64,${data})`;
16
+ });
17
+ return cached;
18
+ }
19
+ //# sourceMappingURL=katex.js.map
@@ -0,0 +1,25 @@
1
+ import type { Diagnostic } from "../diagnostics.js";
2
+ import type { MermaidBlock } from "../markdown/env.js";
3
+ import type { MermaidTheme } from "../options.js";
4
+ import type { BrowserSession } from "./browser.js";
5
+ export { MERMAID_THEMES, type MermaidTheme } from "../options.js";
6
+ type DiagramResult = {
7
+ svg: string;
8
+ } | {
9
+ error: string;
10
+ };
11
+ /**
12
+ * Render Mermaid diagrams to SVG in a blank page that holds no user content: only the diagram
13
+ * sources are passed in, as data. Mermaid runs with `securityLevel: "strict"`.
14
+ */
15
+ export declare function renderMermaid(session: BrowserSession, blocks: MermaidBlock[], theme?: MermaidTheme,
16
+ /** Width of the text column, so diagrams are laid out at their printed size. */
17
+ widthPx?: number): Promise<DiagramResult[]>;
18
+ /**
19
+ * Replace the Mermaid placeholders in `html` with rendered SVGs, or with a visible error box.
20
+ * Returns the new HTML and one error diagnostic per failed diagram.
21
+ */
22
+ export declare function fillMermaid(html: string, nonce: string, blocks: MermaidBlock[], results: DiagramResult[]): {
23
+ html: string;
24
+ diagnostics: Diagnostic[];
25
+ };