towel-txt 0.1.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 (94) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/LICENSE +21 -0
  3. package/README.md +334 -0
  4. package/dist/cli/args.d.ts +45 -0
  5. package/dist/cli/args.d.ts.map +1 -0
  6. package/dist/cli/args.js +208 -0
  7. package/dist/cli/assets.d.ts +15 -0
  8. package/dist/cli/assets.d.ts.map +1 -0
  9. package/dist/cli/assets.js +50 -0
  10. package/dist/cli/config.d.ts +29 -0
  11. package/dist/cli/config.d.ts.map +1 -0
  12. package/dist/cli/config.js +200 -0
  13. package/dist/cli/exit-codes.d.ts +8 -0
  14. package/dist/cli/exit-codes.d.ts.map +1 -0
  15. package/dist/cli/exit-codes.js +6 -0
  16. package/dist/cli/pdf.d.ts +9 -0
  17. package/dist/cli/pdf.d.ts.map +1 -0
  18. package/dist/cli/pdf.js +164 -0
  19. package/dist/cli/run.d.ts +16 -0
  20. package/dist/cli/run.d.ts.map +1 -0
  21. package/dist/cli/run.js +414 -0
  22. package/dist/cli/summary.d.ts +17 -0
  23. package/dist/cli/summary.d.ts.map +1 -0
  24. package/dist/cli/summary.js +20 -0
  25. package/dist/cli/watch.d.ts +8 -0
  26. package/dist/cli/watch.d.ts.map +1 -0
  27. package/dist/cli/watch.js +59 -0
  28. package/dist/cli.d.ts +3 -0
  29. package/dist/cli.d.ts.map +1 -0
  30. package/dist/cli.js +3 -0
  31. package/dist/index.d.ts +17 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +16 -0
  34. package/dist/meta.d.ts +3 -0
  35. package/dist/meta.d.ts.map +1 -0
  36. package/dist/meta.js +2 -0
  37. package/dist/parser/footnotes.d.ts +24 -0
  38. package/dist/parser/footnotes.d.ts.map +1 -0
  39. package/dist/parser/footnotes.js +159 -0
  40. package/dist/parser/headings.d.ts +8 -0
  41. package/dist/parser/headings.d.ts.map +1 -0
  42. package/dist/parser/headings.js +95 -0
  43. package/dist/parser/highlight.d.ts +2 -0
  44. package/dist/parser/highlight.d.ts.map +1 -0
  45. package/dist/parser/highlight.js +81 -0
  46. package/dist/parser/images.d.ts +8 -0
  47. package/dist/parser/images.d.ts.map +1 -0
  48. package/dist/parser/images.js +62 -0
  49. package/dist/parser/markdown.d.ts +12 -0
  50. package/dist/parser/markdown.d.ts.map +1 -0
  51. package/dist/parser/markdown.js +60 -0
  52. package/dist/parser/metadata.d.ts +18 -0
  53. package/dist/parser/metadata.d.ts.map +1 -0
  54. package/dist/parser/metadata.js +95 -0
  55. package/dist/parser/page-breaks.d.ts +3 -0
  56. package/dist/parser/page-breaks.d.ts.map +1 -0
  57. package/dist/parser/page-breaks.js +21 -0
  58. package/dist/render/document.d.ts +13 -0
  59. package/dist/render/document.d.ts.map +1 -0
  60. package/dist/render/document.js +103 -0
  61. package/dist/render/minify.d.ts +2 -0
  62. package/dist/render/minify.d.ts.map +1 -0
  63. package/dist/render/minify.js +21 -0
  64. package/dist/render/print-options.d.ts +6 -0
  65. package/dist/render/print-options.d.ts.map +1 -0
  66. package/dist/render/print-options.js +24 -0
  67. package/dist/render/toc.d.ts +8 -0
  68. package/dist/render/toc.d.ts.map +1 -0
  69. package/dist/render/toc.js +58 -0
  70. package/dist/theme/default.d.ts +2 -0
  71. package/dist/theme/default.d.ts.map +1 -0
  72. package/dist/theme/default.js +327 -0
  73. package/dist/theme/themes.d.ts +5 -0
  74. package/dist/theme/themes.d.ts.map +1 -0
  75. package/dist/theme/themes.js +97 -0
  76. package/dist/utils/ids.d.ts +3 -0
  77. package/dist/utils/ids.d.ts.map +1 -0
  78. package/dist/utils/ids.js +21 -0
  79. package/docs/cli-reference.md +222 -0
  80. package/docs/examples.md +63 -0
  81. package/docs/print-notes.md +11 -0
  82. package/docs/production-roadmap.md +159 -0
  83. package/docs/release.md +44 -0
  84. package/docs/security-and-limits.md +121 -0
  85. package/examples/brief.md +30 -0
  86. package/examples/image-workflow.md +26 -0
  87. package/examples/images/workflow.svg +43 -0
  88. package/examples/print.css +7 -0
  89. package/examples/report.css +22 -0
  90. package/examples/report.md +44 -0
  91. package/examples/sample.md +39 -0
  92. package/examples/technical-note.md +46 -0
  93. package/examples/towel-txt.config.yaml +7 -0
  94. package/package.json +85 -0
@@ -0,0 +1,50 @@
1
+ import { copyFile, mkdir } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { extractImageReferences } from "../parser/images.js";
4
+ export async function copyLocalImageAssets(options) {
5
+ const inputDirectory = path.dirname(options.inputPath);
6
+ const outputDirectory = path.dirname(options.outputPath);
7
+ const imageReferences = extractImageReferences(options.markdown);
8
+ return Promise.all(imageReferences.map(async (reference) => {
9
+ if (reference.status === "skipped") {
10
+ return {
11
+ reason: reference.reason,
12
+ source: reference.source,
13
+ status: "skipped"
14
+ };
15
+ }
16
+ const { source } = reference;
17
+ const normalizedSource = source.replace(/\\/g, "/");
18
+ const targetSource = getTargetSource(normalizedSource, options.assetDirectory);
19
+ const sourcePath = path.resolve(inputDirectory, normalizedSource);
20
+ const targetPath = path.resolve(outputDirectory, targetSource);
21
+ if (sourcePath === targetPath) {
22
+ return {
23
+ reason: "already in output directory",
24
+ source,
25
+ status: "skipped",
26
+ targetSource
27
+ };
28
+ }
29
+ try {
30
+ await mkdir(path.dirname(targetPath), { recursive: true });
31
+ await copyFile(sourcePath, targetPath);
32
+ return { source, status: "copied", targetSource };
33
+ }
34
+ catch (error) {
35
+ return {
36
+ error: error instanceof Error ? error.message : "Unable to copy image asset.",
37
+ source,
38
+ status: "missing",
39
+ targetSource
40
+ };
41
+ }
42
+ }));
43
+ }
44
+ function getTargetSource(source, assetDirectory) {
45
+ const normalizedSource = source.replace(/\\/g, "/");
46
+ if (!assetDirectory) {
47
+ return normalizedSource;
48
+ }
49
+ return `${assetDirectory}/${normalizedSource}`;
50
+ }
@@ -0,0 +1,29 @@
1
+ import { type OutputFormat } from "./args.js";
2
+ import { type ThemeName } from "../theme/themes.js";
3
+ export interface CliConfigDefaults {
4
+ assetDirectory?: string;
5
+ browserPath?: string;
6
+ cover?: boolean;
7
+ cssPath?: string;
8
+ format?: OutputFormat;
9
+ margin?: string;
10
+ minify?: boolean;
11
+ outputPath?: string;
12
+ pageSize?: string;
13
+ strict?: boolean;
14
+ subtitle?: string;
15
+ summaryJsonPath?: string;
16
+ tableOfContents?: boolean;
17
+ theme?: ThemeName;
18
+ title?: string;
19
+ }
20
+ export interface LoadedCliConfig {
21
+ defaults: CliConfigDefaults;
22
+ path: string;
23
+ }
24
+ export declare function loadCliConfig({ configPath, cwd, noConfig }: {
25
+ configPath: string | undefined;
26
+ cwd: string;
27
+ noConfig: boolean;
28
+ }): Promise<LoadedCliConfig | undefined>;
29
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/cli/config.ts"],"names":[],"mappings":"AAKA,OAAO,EAAiB,KAAK,YAAY,EAAE,MAAM,WAAW,CAAC;AAC7D,OAAO,EAAe,KAAK,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAEjE,MAAM,WAAW,iBAAiB;IAChC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,iBAAiB,CAAC;IAC5B,IAAI,EAAE,MAAM,CAAC;CACd;AAyBD,wBAAsB,aAAa,CAAC,EAClC,UAAU,EACV,GAAG,EACH,QAAQ,EACT,EAAE;IACD,UAAU,EAAE,MAAM,GAAG,SAAS,CAAC;IAC/B,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,OAAO,CAAC;CACnB,GAAG,OAAO,CAAC,eAAe,GAAG,SAAS,CAAC,CAuBvC"}
@@ -0,0 +1,200 @@
1
+ import { constants } from "node:fs";
2
+ import { access, readFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { parse as parseYaml } from "yaml";
5
+ import { CliUsageError } from "./args.js";
6
+ import { isThemeName } from "../theme/themes.js";
7
+ const defaultConfigFilenames = [
8
+ "towel-txt.config.yaml",
9
+ "towel-txt.config.yml",
10
+ "towel-txt.config.json"
11
+ ];
12
+ const supportedFields = new Set([
13
+ "assetDir",
14
+ "browser",
15
+ "cover",
16
+ "css",
17
+ "format",
18
+ "margin",
19
+ "minify",
20
+ "output",
21
+ "pageSize",
22
+ "strict",
23
+ "subtitle",
24
+ "summaryJson",
25
+ "tableOfContents",
26
+ "theme",
27
+ "title"
28
+ ]);
29
+ export async function loadCliConfig({ configPath, cwd, noConfig }) {
30
+ if (configPath && noConfig) {
31
+ throw new CliUsageError("Do not pass --config with --no-config.");
32
+ }
33
+ if (noConfig) {
34
+ return undefined;
35
+ }
36
+ const resolvedConfigPath = configPath
37
+ ? path.resolve(cwd, configPath)
38
+ : await findDefaultConfig(cwd);
39
+ if (!resolvedConfigPath) {
40
+ return undefined;
41
+ }
42
+ const rawConfig = await readConfigFile(resolvedConfigPath);
43
+ return {
44
+ defaults: normalizeConfig(rawConfig, path.dirname(resolvedConfigPath)),
45
+ path: resolvedConfigPath
46
+ };
47
+ }
48
+ function normalizeConfig(value, configDirectory) {
49
+ if (!isPlainRecord(value)) {
50
+ throw new CliUsageError("Config file must contain a mapping.");
51
+ }
52
+ const unknownFields = Object.keys(value).filter((field) => !supportedFields.has(field));
53
+ if (unknownFields.length > 0) {
54
+ throw new CliUsageError(`Unsupported config field: ${unknownFields.join(", ")}.`);
55
+ }
56
+ return {
57
+ assetDirectory: getOptionalAssetDirectory(value.assetDir, "assetDir"),
58
+ browserPath: getOptionalPath(value.browser, configDirectory, {
59
+ allowBareCommand: true,
60
+ fieldName: "browser"
61
+ }),
62
+ cover: getOptionalBoolean(value.cover, "cover"),
63
+ cssPath: getOptionalPath(value.css, configDirectory, {
64
+ allowBareCommand: false,
65
+ fieldName: "css"
66
+ }),
67
+ format: getOptionalFormat(value.format),
68
+ margin: getOptionalString(value.margin, "margin"),
69
+ minify: getOptionalBoolean(value.minify, "minify"),
70
+ outputPath: getOptionalPath(value.output, configDirectory, {
71
+ allowBareCommand: false,
72
+ fieldName: "output"
73
+ }),
74
+ pageSize: getOptionalString(value.pageSize, "pageSize"),
75
+ strict: getOptionalBoolean(value.strict, "strict"),
76
+ subtitle: getOptionalString(value.subtitle, "subtitle"),
77
+ summaryJsonPath: getOptionalPath(value.summaryJson, configDirectory, {
78
+ allowBareCommand: false,
79
+ fieldName: "summaryJson"
80
+ }),
81
+ tableOfContents: getOptionalBoolean(value.tableOfContents, "tableOfContents"),
82
+ theme: getOptionalTheme(value.theme),
83
+ title: getOptionalString(value.title, "title")
84
+ };
85
+ }
86
+ function getOptionalString(value, fieldName) {
87
+ if (value === undefined || value === null) {
88
+ return undefined;
89
+ }
90
+ if (typeof value === "string") {
91
+ return value;
92
+ }
93
+ throw new CliUsageError(`Config field "${fieldName}" must be a string.`);
94
+ }
95
+ function getOptionalAssetDirectory(value, fieldName) {
96
+ const directory = getOptionalString(value, fieldName)?.trim();
97
+ if (!directory) {
98
+ return undefined;
99
+ }
100
+ if (directory.includes("?") ||
101
+ directory.includes("#") ||
102
+ /^[a-z][a-z0-9+.-]*:/iu.test(directory) ||
103
+ directory.startsWith("/") ||
104
+ directory.startsWith("\\")) {
105
+ throw new CliUsageError(`Config field "${fieldName}" must be a safe relative directory.`);
106
+ }
107
+ const normalizedDirectory = directory.replace(/\\/g, "/");
108
+ const parts = normalizedDirectory.split("/");
109
+ if (parts.includes("..") || parts.includes("")) {
110
+ throw new CliUsageError(`Config field "${fieldName}" must be a safe relative directory.`);
111
+ }
112
+ return normalizedDirectory;
113
+ }
114
+ function getOptionalPath(value, configDirectory, { allowBareCommand, fieldName }) {
115
+ const configValue = getOptionalString(value, fieldName);
116
+ if (!configValue) {
117
+ return undefined;
118
+ }
119
+ if (path.isAbsolute(configValue) || (allowBareCommand && isBareCommand(configValue))) {
120
+ return configValue;
121
+ }
122
+ return path.resolve(configDirectory, configValue);
123
+ }
124
+ function getOptionalBoolean(value, fieldName) {
125
+ if (value === undefined || value === null) {
126
+ return undefined;
127
+ }
128
+ if (typeof value === "boolean") {
129
+ return value;
130
+ }
131
+ throw new CliUsageError(`Config field "${fieldName}" must be a boolean.`);
132
+ }
133
+ function getOptionalFormat(value) {
134
+ if (value === undefined || value === null) {
135
+ return undefined;
136
+ }
137
+ if (value === "html" || value === "pdf") {
138
+ return value;
139
+ }
140
+ throw new CliUsageError('Config field "format" must be "html" or "pdf".');
141
+ }
142
+ function getOptionalTheme(value) {
143
+ if (value === undefined || value === null) {
144
+ return undefined;
145
+ }
146
+ if (isThemeName(value)) {
147
+ return value;
148
+ }
149
+ throw new CliUsageError('Config field "theme" must be "default", "compact", or "report".');
150
+ }
151
+ async function readConfigFile(configPath) {
152
+ let content;
153
+ try {
154
+ content = await readFile(configPath, "utf8");
155
+ }
156
+ catch (error) {
157
+ if (isFileNotFound(error)) {
158
+ throw new CliUsageError(`Config file not found: ${configPath}.`);
159
+ }
160
+ throw error;
161
+ }
162
+ try {
163
+ return parseYaml(content) ?? {};
164
+ }
165
+ catch (error) {
166
+ const message = error instanceof Error ? error.message : "Invalid config file.";
167
+ throw new CliUsageError(`Invalid config file: ${message}`);
168
+ }
169
+ }
170
+ async function findDefaultConfig(cwd) {
171
+ for (const filename of defaultConfigFilenames) {
172
+ const candidate = path.resolve(cwd, filename);
173
+ if (await fileExists(candidate)) {
174
+ return candidate;
175
+ }
176
+ }
177
+ return undefined;
178
+ }
179
+ async function fileExists(filePath) {
180
+ try {
181
+ await access(filePath, constants.F_OK);
182
+ return true;
183
+ }
184
+ catch (error) {
185
+ if (isFileNotFound(error)) {
186
+ return false;
187
+ }
188
+ throw error;
189
+ }
190
+ }
191
+ function isFileNotFound(error) {
192
+ const code = error instanceof Error && "code" in error ? error.code : undefined;
193
+ return code === "ENOENT" || code === "ENOTDIR";
194
+ }
195
+ function isBareCommand(value) {
196
+ return !value.includes("/") && !value.includes("\\");
197
+ }
198
+ function isPlainRecord(value) {
199
+ return typeof value === "object" && value !== null && !Array.isArray(value);
200
+ }
@@ -0,0 +1,8 @@
1
+ export declare const cliExitCodes: {
2
+ readonly renderError: 1;
3
+ readonly strictWarnings: 3;
4
+ readonly success: 0;
5
+ readonly usageError: 2;
6
+ };
7
+ export type CliExitCode = (typeof cliExitCodes)[keyof typeof cliExitCodes];
8
+ //# sourceMappingURL=exit-codes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"exit-codes.d.ts","sourceRoot":"","sources":["../../src/cli/exit-codes.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,YAAY;;;;;CAKf,CAAC;AAEX,MAAM,MAAM,WAAW,GAAG,CAAC,OAAO,YAAY,CAAC,CAAC,MAAM,OAAO,YAAY,CAAC,CAAC"}
@@ -0,0 +1,6 @@
1
+ export const cliExitCodes = {
2
+ renderError: 1,
3
+ strictWarnings: 3,
4
+ success: 0,
5
+ usageError: 2
6
+ };
@@ -0,0 +1,9 @@
1
+ export interface PdfPrintOptions {
2
+ basePath?: string;
3
+ browserPath?: string;
4
+ html: string;
5
+ outputPath: string;
6
+ }
7
+ export declare function printHtmlToPdf({ basePath, browserPath, html, outputPath }: PdfPrintOptions): Promise<void>;
8
+ export declare function addBaseHref(html: string, basePath: string | undefined): string;
9
+ //# sourceMappingURL=pdf.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pdf.d.ts","sourceRoot":"","sources":["../../src/cli/pdf.ts"],"names":[],"mappings":"AASA,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,wBAAsB,cAAc,CAAC,EACnC,QAAQ,EACR,WAAW,EACX,IAAI,EACJ,UAAU,EACX,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAmBjC;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAS9E"}
@@ -0,0 +1,164 @@
1
+ import { spawn } from "node:child_process";
2
+ import { constants } from "node:fs";
3
+ import { access, mkdtemp, rm, writeFile } from "node:fs/promises";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import { pathToFileURL } from "node:url";
7
+ import { CliUsageError } from "./args.js";
8
+ export async function printHtmlToPdf({ basePath, browserPath, html, outputPath }) {
9
+ const browserExecutable = browserPath ?? (await findBrowserExecutable());
10
+ if (!browserExecutable) {
11
+ throw new CliUsageError("No supported browser found. Install Chrome, Edge, or Chromium, or pass --browser <path>.");
12
+ }
13
+ const temporaryDirectory = await mkdtemp(path.join(os.tmpdir(), "towel-txt-pdf-"));
14
+ const temporaryHtmlPath = path.join(temporaryDirectory, "document.html");
15
+ try {
16
+ await writeFile(temporaryHtmlPath, addBaseHref(html, basePath), "utf8");
17
+ await runBrowserPrint(browserExecutable, temporaryHtmlPath, outputPath);
18
+ await assertPdfWasCreated(outputPath);
19
+ }
20
+ finally {
21
+ await rm(temporaryDirectory, { force: true, recursive: true });
22
+ }
23
+ }
24
+ export function addBaseHref(html, basePath) {
25
+ if (!basePath) {
26
+ return html;
27
+ }
28
+ const baseHref = pathToFileURL(`${path.resolve(basePath)}${path.sep}`).href;
29
+ const baseElement = `<base href="${escapeHtmlAttribute(baseHref)}">`;
30
+ return html.replace(/<head>/i, `<head>\n ${baseElement}`);
31
+ }
32
+ async function findBrowserExecutable() {
33
+ const configuredBrowser = process.env.TOWEL_TXT_BROWSER?.trim();
34
+ if (configuredBrowser) {
35
+ return configuredBrowser;
36
+ }
37
+ for (const candidate of getBrowserCandidates()) {
38
+ if (await fileExists(candidate)) {
39
+ return candidate;
40
+ }
41
+ }
42
+ for (const commandName of getBrowserCommandNames()) {
43
+ const executable = await findOnPath(commandName);
44
+ if (executable) {
45
+ return executable;
46
+ }
47
+ }
48
+ return undefined;
49
+ }
50
+ function getBrowserCandidates() {
51
+ if (process.platform === "win32") {
52
+ return [
53
+ getOptionalPath(process.env.ProgramFiles, "Microsoft", "Edge", "Application", "msedge.exe"),
54
+ getOptionalPath(process.env["ProgramFiles(x86)"], "Microsoft", "Edge", "Application", "msedge.exe"),
55
+ getOptionalPath(process.env.ProgramFiles, "Google", "Chrome", "Application", "chrome.exe"),
56
+ getOptionalPath(process.env["ProgramFiles(x86)"], "Google", "Chrome", "Application", "chrome.exe"),
57
+ getOptionalPath(process.env.LOCALAPPDATA, "Google", "Chrome", "Application", "chrome.exe")
58
+ ].filter((candidate) => Boolean(candidate));
59
+ }
60
+ if (process.platform === "darwin") {
61
+ return [
62
+ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
63
+ "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
64
+ "/Applications/Chromium.app/Contents/MacOS/Chromium"
65
+ ];
66
+ }
67
+ return [];
68
+ }
69
+ function getBrowserCommandNames() {
70
+ if (process.platform === "win32") {
71
+ return ["msedge", "chrome", "chromium"];
72
+ }
73
+ if (process.platform === "darwin") {
74
+ return ["google-chrome", "microsoft-edge", "chromium"];
75
+ }
76
+ return [
77
+ "google-chrome",
78
+ "google-chrome-stable",
79
+ "microsoft-edge",
80
+ "chromium",
81
+ "chromium-browser"
82
+ ];
83
+ }
84
+ function getOptionalPath(...parts) {
85
+ if (parts.some((part) => !part)) {
86
+ return undefined;
87
+ }
88
+ return path.join(...parts);
89
+ }
90
+ async function findOnPath(commandName) {
91
+ const pathEntries = process.env.PATH?.split(path.delimiter).filter(Boolean) ?? [];
92
+ const executableNames = getExecutableNames(commandName);
93
+ for (const pathEntry of pathEntries) {
94
+ for (const executableName of executableNames) {
95
+ const candidate = path.join(pathEntry, executableName);
96
+ if (await fileExists(candidate)) {
97
+ return candidate;
98
+ }
99
+ }
100
+ }
101
+ return undefined;
102
+ }
103
+ function getExecutableNames(commandName) {
104
+ if (process.platform !== "win32" || path.extname(commandName)) {
105
+ return [commandName];
106
+ }
107
+ const pathExtensions = process.env.PATHEXT?.split(";").filter(Boolean) ?? [
108
+ ".COM",
109
+ ".EXE",
110
+ ".BAT",
111
+ ".CMD"
112
+ ];
113
+ return pathExtensions.map((extension) => `${commandName}${extension.toLowerCase()}`);
114
+ }
115
+ async function fileExists(filePath) {
116
+ try {
117
+ await access(filePath, constants.F_OK);
118
+ return true;
119
+ }
120
+ catch (error) {
121
+ const code = error instanceof Error && "code" in error ? error.code : undefined;
122
+ if (code === "ENOENT" || code === "ENOTDIR") {
123
+ return false;
124
+ }
125
+ throw error;
126
+ }
127
+ }
128
+ async function assertPdfWasCreated(outputPath) {
129
+ if (await fileExists(outputPath)) {
130
+ return;
131
+ }
132
+ throw new Error("Browser PDF export completed without creating the output file.");
133
+ }
134
+ function runBrowserPrint(browserExecutable, htmlPath, outputPath) {
135
+ return new Promise((resolve, reject) => {
136
+ const browser = spawn(browserExecutable, [
137
+ "--headless=new",
138
+ "--disable-gpu",
139
+ "--allow-file-access-from-files",
140
+ `--print-to-pdf=${outputPath}`,
141
+ pathToFileURL(htmlPath).href
142
+ ], {
143
+ stdio: ["ignore", "ignore", "pipe"],
144
+ windowsHide: true
145
+ });
146
+ const stderrChunks = [];
147
+ browser.stderr.on("data", (chunk) => {
148
+ stderrChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
149
+ });
150
+ browser.on("error", reject);
151
+ browser.on("close", (exitCode) => {
152
+ if (exitCode === 0) {
153
+ resolve();
154
+ return;
155
+ }
156
+ const stderr = Buffer.concat(stderrChunks).toString("utf8").trim();
157
+ const details = stderr ? ` ${stderr}` : "";
158
+ reject(new Error(`Browser PDF export failed with exit code ${exitCode}.${details}`));
159
+ });
160
+ });
161
+ }
162
+ function escapeHtmlAttribute(value) {
163
+ return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;");
164
+ }
@@ -0,0 +1,16 @@
1
+ import { type OutputFormat } from "./args.js";
2
+ import { type PdfPrintOptions } from "./pdf.js";
3
+ import { type WatchFilesOptions } from "./watch.js";
4
+ export interface CliIo {
5
+ cwd: string;
6
+ pdfPrinter?: (options: PdfPrintOptions) => Promise<void>;
7
+ signal?: AbortSignal;
8
+ stderr: Pick<NodeJS.WriteStream, "write">;
9
+ stdin?: NodeJS.ReadableStream;
10
+ stdout: Pick<NodeJS.WriteStream, "write">;
11
+ watcher?: (options: WatchFilesOptions) => Promise<void>;
12
+ }
13
+ export declare function runCli(argv: string[], io?: CliIo): Promise<number>;
14
+ export declare function getHelpText(): string;
15
+ export declare function getDefaultOutputPath(inputPath: string, format?: OutputFormat): string;
16
+ //# sourceMappingURL=run.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/cli/run.ts"],"names":[],"mappings":"AAUA,OAAO,EAIL,KAAK,YAAY,EAElB,MAAM,WAAW,CAAC;AAInB,OAAO,EAAE,KAAK,eAAe,EAAkB,MAAM,UAAU,CAAC;AAEhE,OAAO,EAAE,KAAK,iBAAiB,EAAc,MAAM,YAAY,CAAC;AAEhE,MAAM,WAAW,KAAK;IACpB,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,eAAe,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACzD,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;IAC1C,KAAK,CAAC,EAAE,MAAM,CAAC,cAAc,CAAC;IAC9B,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;IAC1C,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,iBAAiB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACzD;AAED,wBAAsB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,GAAE,KAAsB,GAAG,OAAO,CAAC,MAAM,CAAC,CA8FxF;AA2JD,wBAAgB,WAAW,IAAI,MAAM,CAsCpC;AAED,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,GAAE,YAAqB,GAAG,MAAM,CAG7F"}