@open-nav/invoicing 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 (53) hide show
  1. package/LICENSE +29 -0
  2. package/README.md +190 -0
  3. package/dist/color.d.ts +31 -0
  4. package/dist/color.d.ts.map +1 -0
  5. package/dist/color.js +150 -0
  6. package/dist/color.js.map +1 -0
  7. package/dist/export.d.ts +89 -0
  8. package/dist/export.d.ts.map +1 -0
  9. package/dist/export.js +115 -0
  10. package/dist/export.js.map +1 -0
  11. package/dist/format.d.ts +23 -0
  12. package/dist/format.d.ts.map +1 -0
  13. package/dist/format.js +52 -0
  14. package/dist/format.js.map +1 -0
  15. package/dist/html.d.ts +43 -0
  16. package/dist/html.d.ts.map +1 -0
  17. package/dist/html.js +355 -0
  18. package/dist/html.js.map +1 -0
  19. package/dist/index.d.ts +10 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +10 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/labels.d.ts +8 -0
  24. package/dist/labels.d.ts.map +1 -0
  25. package/dist/labels.js +199 -0
  26. package/dist/labels.js.map +1 -0
  27. package/dist/markings.d.ts +28 -0
  28. package/dist/markings.d.ts.map +1 -0
  29. package/dist/markings.js +69 -0
  30. package/dist/markings.js.map +1 -0
  31. package/dist/pdf-native.d.ts +32 -0
  32. package/dist/pdf-native.d.ts.map +1 -0
  33. package/dist/pdf-native.js +601 -0
  34. package/dist/pdf-native.js.map +1 -0
  35. package/dist/pdf.d.ts +92 -0
  36. package/dist/pdf.d.ts.map +1 -0
  37. package/dist/pdf.js +203 -0
  38. package/dist/pdf.js.map +1 -0
  39. package/dist/theme.d.ts +85 -0
  40. package/dist/theme.d.ts.map +1 -0
  41. package/dist/theme.js +210 -0
  42. package/dist/theme.js.map +1 -0
  43. package/package.json +54 -0
  44. package/src/color.ts +170 -0
  45. package/src/export.ts +204 -0
  46. package/src/format.ts +66 -0
  47. package/src/html.ts +446 -0
  48. package/src/index.ts +9 -0
  49. package/src/labels.ts +222 -0
  50. package/src/markings.ts +102 -0
  51. package/src/pdf-native.ts +754 -0
  52. package/src/pdf.ts +302 -0
  53. package/src/theme.ts +305 -0
package/dist/pdf.d.ts ADDED
@@ -0,0 +1,92 @@
1
+ import type { InvoiceData } from '@open-nav/core';
2
+ import { type NativePdfOptions } from './pdf-native.js';
3
+ /**
4
+ * PDF output, by either of two engines.
5
+ *
6
+ * `native` is the default and needs nothing installed: pdfmake lays the
7
+ * document out and pdfkit writes the PDF, using the Roboto files pdfmake
8
+ * bundles. Roboto covers Latin Extended-A, so `ő` and `ű` embed correctly —
9
+ * which was the whole difficulty, since the PDF core fonts cannot represent
10
+ * them and a font therefore has to be embedded.
11
+ *
12
+ * `browser` renders the HTML document and converts it with Chrome, Chromium
13
+ * or Edge. It follows the HTML exactly, at the cost of needing a browser.
14
+ *
15
+ * They are two layouts, not one: the native engine follows the same theme but
16
+ * builds the page from pdfmake's document model rather than CSS, so the two
17
+ * will not match pixel for pixel.
18
+ */
19
+ export declare class PdfConversionError extends Error {
20
+ }
21
+ export interface PdfOptions extends NativePdfOptions {
22
+ /**
23
+ * Which engine to use.
24
+ *
25
+ * `native` (the default) needs nothing installed. `browser` follows the
26
+ * HTML document exactly but requires Chrome, Chromium or Edge.
27
+ */
28
+ engine?: 'native' | 'browser';
29
+ /** Browser executable. Auto-detected when omitted. `browser` engine only. */
30
+ browserPath?: string;
31
+ /** Extra arguments passed to the browser. */
32
+ browserArgs?: string[];
33
+ /**
34
+ * Run the browser with its sandbox. On by default.
35
+ *
36
+ * The sandbox is what contains a malicious document, and invoice data is
37
+ * input. It is left on even though that means conversion fails when running
38
+ * as root — in a container or CI, pass `sandbox: false` deliberately. The
39
+ * error message says so, rather than quietly weakening the default for
40
+ * everybody.
41
+ */
42
+ sandbox?: boolean;
43
+ /** Give up after this long. Defaults to 30 000 ms. */
44
+ timeoutMs?: number;
45
+ /** Environment consulted when detecting a browser. Injectable for tests. */
46
+ env?: Record<string, string | undefined>;
47
+ /**
48
+ * Absolute locations probed when detecting a browser, in place of the usual
49
+ * install paths. Injectable for tests: those paths are the host's, so
50
+ * without this a test cannot describe a machine that has no browser.
51
+ */
52
+ searchPaths?: readonly string[];
53
+ /**
54
+ * Convert HTML to PDF yourself, instead of spawning a browser.
55
+ *
56
+ * ```ts
57
+ * import { chromium } from 'playwright';
58
+ * const convert = async (html: string) => {
59
+ * const browser = await chromium.launch();
60
+ * const page = await browser.newPage();
61
+ * await page.setContent(html, { waitUntil: 'load' });
62
+ * const pdf = await page.pdf({ printBackground: true });
63
+ * await browser.close();
64
+ * return pdf;
65
+ * };
66
+ * ```
67
+ */
68
+ convert?: (html: string) => Promise<Buffer>;
69
+ }
70
+ /**
71
+ * Render an invoice straight to PDF bytes.
72
+ *
73
+ * Uses the native engine unless `engine: 'browser'` is asked for, or a
74
+ * `convert` function is supplied — passing a converter is itself a request to
75
+ * go through HTML.
76
+ */
77
+ export declare function renderInvoicePdf(document: InvoiceData, options?: PdfOptions): Promise<Buffer>;
78
+ /** Convert a rendered document to PDF bytes. */
79
+ export declare function htmlToPdf(html: string, options?: PdfOptions): Promise<Buffer>;
80
+ /**
81
+ * Locate a browser able to print to PDF.
82
+ *
83
+ * Order: an explicit environment variable, a Playwright install, the usual
84
+ * install locations, then `PATH`.
85
+ *
86
+ * `searchPaths` replaces that third step. It exists because the usual
87
+ * locations are absolute paths on the host, so no value of `env` can describe
88
+ * a machine without a browser — which is exactly what the failure path has to
89
+ * be tested against.
90
+ */
91
+ export declare function findBrowser(env?: Record<string, string | undefined>, searchPaths?: readonly string[]): string | undefined;
92
+ //# sourceMappingURL=pdf.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pdf.d.ts","sourceRoot":"","sources":["../src/pdf.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAElD,OAAO,EAA0B,KAAK,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAEhF;;;;;;;;;;;;;;;GAeG;AAEH,qBAAa,kBAAmB,SAAQ,KAAK;CAAG;AAEhD,MAAM,WAAW,UAAW,SAAQ,gBAAgB;IAClD;;;;;OAKG;IACH,MAAM,CAAC,EAAE,QAAQ,GAAG,SAAS,CAAC;IAC9B,6EAA6E;IAC7E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,6CAA6C;IAC7C,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB;;;;;;;;OAQG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,sDAAsD;IACtD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,4EAA4E;IAC5E,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;IACzC;;;;OAIG;IACH,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAChC;;;;;;;;;;;;;;OAcG;IACH,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;CAC7C;AAED;;;;;;GAMG;AACH,wBAAsB,gBAAgB,CACpC,QAAQ,EAAE,WAAW,EACrB,OAAO,GAAE,UAAe,GACvB,OAAO,CAAC,MAAM,CAAC,CAIjB;AAED,gDAAgD;AAChD,wBAAsB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,UAAe,GAAG,OAAO,CAAC,MAAM,CAAC,CAavF;AA8HD;;;;;;;;;;GAUG;AACH,wBAAgB,WAAW,CACzB,GAAG,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAe,EACrD,WAAW,GAAE,SAAS,MAAM,EAAqB,GAChD,MAAM,GAAG,SAAS,CAuBpB"}
package/dist/pdf.js ADDED
@@ -0,0 +1,203 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { existsSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { renderInvoiceHtml } from './html.js';
6
+ import { renderInvoicePdfNative } from './pdf-native.js';
7
+ /**
8
+ * PDF output, by either of two engines.
9
+ *
10
+ * `native` is the default and needs nothing installed: pdfmake lays the
11
+ * document out and pdfkit writes the PDF, using the Roboto files pdfmake
12
+ * bundles. Roboto covers Latin Extended-A, so `ő` and `ű` embed correctly —
13
+ * which was the whole difficulty, since the PDF core fonts cannot represent
14
+ * them and a font therefore has to be embedded.
15
+ *
16
+ * `browser` renders the HTML document and converts it with Chrome, Chromium
17
+ * or Edge. It follows the HTML exactly, at the cost of needing a browser.
18
+ *
19
+ * They are two layouts, not one: the native engine follows the same theme but
20
+ * builds the page from pdfmake's document model rather than CSS, so the two
21
+ * will not match pixel for pixel.
22
+ */
23
+ export class PdfConversionError extends Error {
24
+ }
25
+ /**
26
+ * Render an invoice straight to PDF bytes.
27
+ *
28
+ * Uses the native engine unless `engine: 'browser'` is asked for, or a
29
+ * `convert` function is supplied — passing a converter is itself a request to
30
+ * go through HTML.
31
+ */
32
+ export async function renderInvoicePdf(document, options = {}) {
33
+ const engine = options.engine ?? (options.convert || options.browserPath ? 'browser' : 'native');
34
+ if (engine === 'native')
35
+ return renderInvoicePdfNative(document, options);
36
+ return htmlToPdf(renderInvoiceHtml(document, options), options);
37
+ }
38
+ /** Convert a rendered document to PDF bytes. */
39
+ export async function htmlToPdf(html, options = {}) {
40
+ if (options.convert)
41
+ return options.convert(html);
42
+ const browser = options.browserPath ?? findBrowser(options.env, options.searchPaths);
43
+ if (!browser) {
44
+ throw new PdfConversionError('No browser found to convert the document to PDF.\n' +
45
+ 'Install Chrome, Chromium or Edge, or set one of OPEN_NAV_BROWSER, ' +
46
+ 'CHROME_PATH or PUPPETEER_EXECUTABLE_PATH, or pass browserPath, ' +
47
+ 'or supply your own convert() — see PdfOptions.');
48
+ }
49
+ return spawnConversion(html, browser, options);
50
+ }
51
+ async function spawnConversion(html, browser, options) {
52
+ const workDir = mkdtempSync(join(tmpdir(), 'open-nav-pdf-'));
53
+ const inputPath = join(workDir, 'invoice.html');
54
+ const outputPath = join(workDir, 'invoice.pdf');
55
+ try {
56
+ writeFileSync(inputPath, html, 'utf8');
57
+ const args = [
58
+ '--headless',
59
+ '--disable-gpu',
60
+ '--no-pdf-header-footer',
61
+ // Its own profile, so concurrent conversions do not fight over one.
62
+ `--user-data-dir=${join(workDir, 'profile')}`,
63
+ ...(options.sandbox === false ? ['--no-sandbox'] : []),
64
+ ...(options.browserArgs ?? []),
65
+ `--print-to-pdf=${outputPath}`,
66
+ `file://${inputPath}`,
67
+ ];
68
+ const { code, stderr } = await run(browser, args, options.timeoutMs ?? 30_000);
69
+ if (!existsSync(outputPath)) {
70
+ throw new PdfConversionError(explainFailure(browser, code, stderr, options));
71
+ }
72
+ const pdf = readFileSync(outputPath);
73
+ if (!pdf.subarray(0, 5).equals(Buffer.from('%PDF-'))) {
74
+ throw new PdfConversionError(`${browser} produced ${pdf.length} bytes that are not a PDF.\n${stderr.trim()}`);
75
+ }
76
+ return pdf;
77
+ }
78
+ finally {
79
+ rmSync(workDir, { recursive: true, force: true });
80
+ }
81
+ }
82
+ /** Turn a browser failure into something the caller can act on. */
83
+ function explainFailure(browser, code, stderr, options) {
84
+ if (/without --no-sandbox is not supported/i.test(stderr)) {
85
+ return (`${browser} refuses to run as root with its sandbox enabled.\n` +
86
+ 'Pass sandbox: false (or --no-sandbox on the CLI) if you accept that, ' +
87
+ 'which is usual in a container, or run as a non-root user.');
88
+ }
89
+ if (options.sandbox === false && /sandbox/i.test(stderr)) {
90
+ return `${browser} could not start even with the sandbox disabled.\n${stderr.trim()}`;
91
+ }
92
+ return `${browser} exited with code ${code ?? 'unknown'} and produced no PDF.\n${stderr.trim()}`;
93
+ }
94
+ function run(command, args, timeoutMs) {
95
+ return new Promise((resolve, reject) => {
96
+ const child = spawn(command, args, { stdio: ['ignore', 'ignore', 'pipe'] });
97
+ let stderr = '';
98
+ child.stderr?.on('data', (chunk) => {
99
+ stderr += chunk.toString();
100
+ });
101
+ const timer = setTimeout(() => {
102
+ child.kill('SIGKILL');
103
+ reject(new PdfConversionError(`${command} did not finish within ${timeoutMs}ms. ` +
104
+ 'A logo fetched over the network is the usual cause; inline it instead.'));
105
+ }, timeoutMs);
106
+ child.on('error', (error) => {
107
+ clearTimeout(timer);
108
+ reject(new PdfConversionError(`Could not run ${command}: ${error.message}`));
109
+ });
110
+ child.on('close', (code) => {
111
+ clearTimeout(timer);
112
+ resolve({ code, stderr });
113
+ });
114
+ });
115
+ }
116
+ /** Environment variables that name a browser, in the order they are honoured. */
117
+ const BROWSER_ENV_VARS = ['OPEN_NAV_BROWSER', 'CHROME_PATH', 'PUPPETEER_EXECUTABLE_PATH'];
118
+ const EXECUTABLE_NAMES = [
119
+ 'google-chrome-stable',
120
+ 'google-chrome',
121
+ 'chromium-browser',
122
+ 'chromium',
123
+ 'microsoft-edge',
124
+ 'chrome',
125
+ ];
126
+ const WELL_KNOWN_PATHS = [
127
+ // macOS
128
+ '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
129
+ '/Applications/Chromium.app/Contents/MacOS/Chromium',
130
+ '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
131
+ // Linux
132
+ '/usr/bin/google-chrome-stable',
133
+ '/usr/bin/google-chrome',
134
+ '/usr/bin/chromium-browser',
135
+ '/usr/bin/chromium',
136
+ '/usr/bin/microsoft-edge',
137
+ '/snap/bin/chromium',
138
+ // Windows
139
+ 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
140
+ 'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',
141
+ 'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe',
142
+ ];
143
+ /**
144
+ * Locate a browser able to print to PDF.
145
+ *
146
+ * Order: an explicit environment variable, a Playwright install, the usual
147
+ * install locations, then `PATH`.
148
+ *
149
+ * `searchPaths` replaces that third step. It exists because the usual
150
+ * locations are absolute paths on the host, so no value of `env` can describe
151
+ * a machine without a browser — which is exactly what the failure path has to
152
+ * be tested against.
153
+ */
154
+ export function findBrowser(env = process.env, searchPaths = WELL_KNOWN_PATHS) {
155
+ for (const name of BROWSER_ENV_VARS) {
156
+ const candidate = env[name];
157
+ if (candidate && existsSync(candidate))
158
+ return candidate;
159
+ }
160
+ const fromPlaywright = findPlaywrightChromium(env['PLAYWRIGHT_BROWSERS_PATH']);
161
+ if (fromPlaywright)
162
+ return fromPlaywright;
163
+ for (const candidate of searchPaths) {
164
+ if (existsSync(candidate))
165
+ return candidate;
166
+ }
167
+ const pathEntries = (env['PATH'] ?? '').split(process.platform === 'win32' ? ';' : ':');
168
+ for (const directory of pathEntries) {
169
+ if (!directory)
170
+ continue;
171
+ for (const name of EXECUTABLE_NAMES) {
172
+ const candidate = join(directory, name);
173
+ if (existsSync(candidate))
174
+ return candidate;
175
+ }
176
+ }
177
+ return undefined;
178
+ }
179
+ /**
180
+ * Playwright keeps its browsers in a versioned directory, so the exact path
181
+ * is not predictable and has to be discovered.
182
+ */
183
+ function findPlaywrightChromium(browsersPath) {
184
+ if (!browsersPath || !existsSync(browsersPath))
185
+ return undefined;
186
+ const candidates = [];
187
+ let entries;
188
+ try {
189
+ entries = readdirSync(browsersPath);
190
+ }
191
+ catch {
192
+ return undefined;
193
+ }
194
+ for (const entry of entries) {
195
+ if (!entry.startsWith('chromium'))
196
+ continue;
197
+ candidates.push(join(browsersPath, entry, 'chrome-linux', 'chrome'), join(browsersPath, entry, 'chrome-mac', 'Chromium.app', 'Contents', 'MacOS', 'Chromium'), join(browsersPath, entry, 'chrome-win', 'chrome.exe'));
198
+ }
199
+ // Prefer a full build over the headless shell: the shell cannot print.
200
+ candidates.sort((left, right) => Number(left.includes('headless')) - Number(right.includes('headless')));
201
+ return candidates.find((candidate) => existsSync(candidate));
202
+ }
203
+ //# sourceMappingURL=pdf.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pdf.js","sourceRoot":"","sources":["../src/pdf.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Visual configuration for the invoice document.
3
+ *
4
+ * Every field is optional; what is left out falls back to a restrained
5
+ * default that prints well in black and white. Values that end up inside the
6
+ * stylesheet are validated rather than interpolated blindly — a theme is
7
+ * configuration, but configuration is still input.
8
+ */
9
+ export interface InvoiceTheme {
10
+ /** Accent for headings, rules and the total. */
11
+ accentColor?: string;
12
+ /** Body text colour. */
13
+ inkColor?: string;
14
+ /** Secondary text: labels, references, the footer. */
15
+ mutedColor?: string;
16
+ /** Background of the markings panel. */
17
+ panelColor?: string;
18
+ /** Rules and borders. */
19
+ borderColor?: string;
20
+ /** CSS font stack. Give a real fallback chain; a PDF converter needs it. */
21
+ fontFamily?: string;
22
+ /** Base size, as a CSS length. Everything else scales from it. */
23
+ baseFontSize?: string;
24
+ /** Page size for `@page`, e.g. `A4` or `Letter`. */
25
+ pageSize?: string;
26
+ /** Page margin for `@page`, e.g. `16mm 14mm`. */
27
+ pageMargin?: string;
28
+ /** Logo shown in the header. Use `embedImage` to inline a local file. */
29
+ logo?: InvoiceLogo;
30
+ /** Extra lines in the supplier block: phone, email, web, registration. */
31
+ issuerContact?: string[];
32
+ /** Lines printed in the footer, e.g. payment terms or company details. */
33
+ footerLines?: string[];
34
+ /** Tint alternate table rows. Off by default; it prints heavier. */
35
+ zebraRows?: boolean;
36
+ /** Show the "rendered from reported data" note. Defaults to true. */
37
+ provenanceNote?: boolean;
38
+ /** Appended verbatim after the generated stylesheet. The escape hatch. */
39
+ customCss?: string;
40
+ }
41
+ export interface InvoiceLogo {
42
+ /**
43
+ * Image source: a `data:` URI, or an absolute URL.
44
+ *
45
+ * Prefer a data URI. An external URL leaves the document dependent on the
46
+ * network, which defeats archiving it as a single file and can quietly
47
+ * produce a logo-less PDF.
48
+ */
49
+ src: string;
50
+ /** Rendered width as a CSS length. Height follows the aspect ratio. */
51
+ width?: string;
52
+ /** Alternative text, for accessibility and for a failed image. */
53
+ alt?: string;
54
+ }
55
+ export interface ResolvedTheme extends Required<Omit<InvoiceTheme, 'logo' | 'customCss'>> {
56
+ logo?: InvoiceLogo;
57
+ customCss?: string;
58
+ }
59
+ export declare const DEFAULT_THEME: ResolvedTheme;
60
+ export declare class ThemeError extends Error {
61
+ }
62
+ /** Fill in the defaults, validating everything that lands in the CSS. */
63
+ export declare function resolveTheme(theme?: InvoiceTheme): ResolvedTheme;
64
+ /** Largest logo we inline, before base64 expansion. */
65
+ export declare const MAX_LOGO_BYTES: number;
66
+ /**
67
+ * Read an image and return it as a `data:` URI.
68
+ *
69
+ * Inlining keeps the document self-contained, which is what makes it archive
70
+ * as one file and print the same offline.
71
+ */
72
+ export declare function embedImage(filePath: string, contents?: Buffer): string;
73
+ /**
74
+ * Load a theme from a JSON file.
75
+ *
76
+ * A `logoFile` field is read relative to the theme file and inlined, so a
77
+ * theme can be checked in next to its logo and stay portable.
78
+ */
79
+ export declare function loadTheme(filePath: string, io?: {
80
+ readFile?: (path: string) => string;
81
+ readBinary?: (path: string) => Buffer;
82
+ }): InvoiceTheme;
83
+ /** Build the document stylesheet from a resolved theme. */
84
+ export declare function buildStyles(theme: ResolvedTheme): string;
85
+ //# sourceMappingURL=theme.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"theme.d.ts","sourceRoot":"","sources":["../src/theme.ts"],"names":[],"mappings":"AAGA;;;;;;;GAOG;AACH,MAAM,WAAW,YAAY;IAC3B,gDAAgD;IAChD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,wBAAwB;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,sDAAsD;IACtD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,wCAAwC;IACxC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,yBAAyB;IACzB,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB,4EAA4E;IAC5E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,kEAAkE;IAClE,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB,oDAAoD;IACpD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,iDAAiD;IACjD,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB,yEAAyE;IACzE,IAAI,CAAC,EAAE,WAAW,CAAC;IAEnB,0EAA0E;IAC1E,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,0EAA0E;IAC1E,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IAEvB,oEAAoE;IACpE,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,qEAAqE;IACrE,cAAc,CAAC,EAAE,OAAO,CAAC;IAEzB,0EAA0E;IAC1E,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,WAAW;IAC1B;;;;;;OAMG;IACH,GAAG,EAAE,MAAM,CAAC;IACZ,uEAAuE;IACvE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,kEAAkE;IAClE,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,aAAc,SAAQ,QAAQ,CAAC,IAAI,CAAC,YAAY,EAAE,MAAM,GAAG,WAAW,CAAC,CAAC;IACvF,IAAI,CAAC,EAAE,WAAW,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,eAAO,MAAM,aAAa,EAAE,aAc3B,CAAC;AAEF,qBAAa,UAAW,SAAQ,KAAK;CAAG;AA8BxC,yEAAyE;AACzE,wBAAgB,YAAY,CAAC,KAAK,GAAE,YAAiB,GAAG,aAAa,CAmCpE;AAiBD,uDAAuD;AACvD,eAAO,MAAM,cAAc,QAAkB,CAAC;AAE9C;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAgBtE;AAED;;;;;GAKG;AACH,wBAAgB,SAAS,CACvB,QAAQ,EAAE,MAAM,EAChB,EAAE,GAAE;IAAE,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAA;CAAO,GACtF,YAAY,CAoBd;AAED,2DAA2D;AAC3D,wBAAgB,WAAW,CAAC,KAAK,EAAE,aAAa,GAAG,MAAM,CA4ExD"}
package/dist/theme.js ADDED
@@ -0,0 +1,210 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { dirname, extname, resolve } from 'node:path';
3
+ export const DEFAULT_THEME = {
4
+ accentColor: '#16181d',
5
+ inkColor: '#16181d',
6
+ mutedColor: '#5b6270',
7
+ panelColor: '#f4f5f8',
8
+ borderColor: '#c9cdd6',
9
+ fontFamily: '"Helvetica Neue", Arial, "Liberation Sans", sans-serif',
10
+ baseFontSize: '10pt',
11
+ pageSize: 'A4',
12
+ pageMargin: '16mm 14mm',
13
+ issuerContact: [],
14
+ footerLines: [],
15
+ zebraRows: false,
16
+ provenanceNote: true,
17
+ };
18
+ export class ThemeError extends Error {
19
+ }
20
+ /** Colours we accept: hex, rgb(), rgba(), hsl(), hsla(), or a CSS keyword. */
21
+ const COLOR = /^(#[0-9a-fA-F]{3,8}|(rgb|hsl)a?\([0-9.,%\s/deg]+\)|[a-zA-Z]{3,20})$/;
22
+ /** Lengths and simple length lists, e.g. `10pt` or `16mm 14mm`. */
23
+ const LENGTH_LIST = /^[0-9.]+(px|pt|pc|mm|cm|in|em|rem|%)?( +[0-9.]+(px|pt|pc|mm|cm|in|em|rem|%)?){0,3}$/;
24
+ /** Page sizes: a keyword, optionally with an orientation, or two lengths. */
25
+ const PAGE_SIZE = /^([A-Za-z][A-Za-z0-9]{0,10}( +(portrait|landscape))?|[0-9.]+[a-z]{2} +[0-9.]+[a-z]{2})$/;
26
+ /**
27
+ * A font stack: names, quotes, commas, spaces, hyphens.
28
+ *
29
+ * Parentheses are excluded deliberately. No font name contains one, and
30
+ * allowing them would admit `url(...)`, which is how a stylesheet is made to
31
+ * fetch something.
32
+ */
33
+ const FONT_STACK = /^[-A-Za-z0-9 ,."']+$/;
34
+ function check(value, pattern, field, expected) {
35
+ const trimmed = value.trim();
36
+ if (!pattern.test(trimmed)) {
37
+ throw new ThemeError(`theme.${field}: ${JSON.stringify(value)} is not ${expected}. ` +
38
+ `Values reach the stylesheet, so they are checked rather than trusted.`);
39
+ }
40
+ return trimmed;
41
+ }
42
+ /** Fill in the defaults, validating everything that lands in the CSS. */
43
+ export function resolveTheme(theme = {}) {
44
+ const merged = { ...DEFAULT_THEME, ...stripUndefined(theme) };
45
+ for (const field of [
46
+ 'accentColor',
47
+ 'inkColor',
48
+ 'mutedColor',
49
+ 'panelColor',
50
+ 'borderColor',
51
+ ]) {
52
+ merged[field] = check(merged[field], COLOR, field, 'a CSS colour');
53
+ }
54
+ merged.baseFontSize = check(merged.baseFontSize, LENGTH_LIST, 'baseFontSize', 'a CSS length');
55
+ merged.pageMargin = check(merged.pageMargin, LENGTH_LIST, 'pageMargin', 'a CSS length');
56
+ merged.pageSize = check(merged.pageSize, PAGE_SIZE, 'pageSize', 'a CSS page size');
57
+ merged.fontFamily = check(merged.fontFamily, FONT_STACK, 'fontFamily', 'a CSS font stack');
58
+ if (merged.logo) {
59
+ const src = merged.logo.src.trim();
60
+ if (!/^(data:image\/[a-z+.-]+;base64,[A-Za-z0-9+/=]+|https?:\/\/\S+)$/.test(src)) {
61
+ throw new ThemeError('theme.logo.src must be a data: image URI or an http(s) URL. ' +
62
+ 'Use embedImage() to inline a local file.');
63
+ }
64
+ merged.logo = {
65
+ src,
66
+ ...(merged.logo.width
67
+ ? { width: check(merged.logo.width, LENGTH_LIST, 'logo.width', 'a CSS length') }
68
+ : {}),
69
+ ...(merged.logo.alt ? { alt: merged.logo.alt } : {}),
70
+ };
71
+ }
72
+ return merged;
73
+ }
74
+ function stripUndefined(value) {
75
+ return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined));
76
+ }
77
+ const MIME_TYPES = {
78
+ '.png': 'image/png',
79
+ '.jpg': 'image/jpeg',
80
+ '.jpeg': 'image/jpeg',
81
+ '.gif': 'image/gif',
82
+ '.svg': 'image/svg+xml',
83
+ '.webp': 'image/webp',
84
+ };
85
+ /** Largest logo we inline, before base64 expansion. */
86
+ export const MAX_LOGO_BYTES = 2 * 1024 * 1024;
87
+ /**
88
+ * Read an image and return it as a `data:` URI.
89
+ *
90
+ * Inlining keeps the document self-contained, which is what makes it archive
91
+ * as one file and print the same offline.
92
+ */
93
+ export function embedImage(filePath, contents) {
94
+ const extension = extname(filePath).toLowerCase();
95
+ const mime = MIME_TYPES[extension];
96
+ if (!mime) {
97
+ throw new ThemeError(`Unsupported image type ${extension || filePath}. Supported: ${Object.keys(MIME_TYPES).join(', ')}`);
98
+ }
99
+ const bytes = contents ?? readFileSync(filePath);
100
+ if (bytes.length > MAX_LOGO_BYTES) {
101
+ throw new ThemeError(`${filePath} is ${Math.round(bytes.length / 1024)} kB; the limit is ${MAX_LOGO_BYTES / 1024} kB. ` +
102
+ `A logo this large bloats every document it appears in.`);
103
+ }
104
+ return `data:${mime};base64,${bytes.toString('base64')}`;
105
+ }
106
+ /**
107
+ * Load a theme from a JSON file.
108
+ *
109
+ * A `logoFile` field is read relative to the theme file and inlined, so a
110
+ * theme can be checked in next to its logo and stay portable.
111
+ */
112
+ export function loadTheme(filePath, io = {}) {
113
+ const readFile = io.readFile ?? ((path) => readFileSync(path, 'utf8'));
114
+ let parsed;
115
+ try {
116
+ parsed = JSON.parse(readFile(filePath));
117
+ }
118
+ catch (cause) {
119
+ throw new ThemeError(`${filePath} is not valid JSON: ${cause.message}`);
120
+ }
121
+ const { logoFile, ...theme } = parsed;
122
+ if (logoFile) {
123
+ const imagePath = resolve(dirname(filePath), logoFile);
124
+ const readBinary = io.readBinary ?? ((path) => readFileSync(path));
125
+ theme.logo = {
126
+ ...theme.logo,
127
+ src: embedImage(imagePath, readBinary(imagePath)),
128
+ };
129
+ }
130
+ return theme;
131
+ }
132
+ /** Build the document stylesheet from a resolved theme. */
133
+ export function buildStyles(theme) {
134
+ return `<style>
135
+ @page { size: ${theme.pageSize}; margin: ${theme.pageMargin}; }
136
+ :root {
137
+ --accent: ${theme.accentColor};
138
+ --ink: ${theme.inkColor};
139
+ --muted: ${theme.mutedColor};
140
+ --rule: ${theme.borderColor};
141
+ --panel: ${theme.panelColor};
142
+ }
143
+ * { box-sizing: border-box; }
144
+ body {
145
+ margin: 0;
146
+ color: var(--ink);
147
+ font: ${theme.baseFontSize}/1.45 ${theme.fontFamily};
148
+ -webkit-print-color-adjust: exact;
149
+ print-color-adjust: exact;
150
+ }
151
+ .invoice { max-width: 190mm; margin: 0 auto; padding: 8mm 0; }
152
+ .page-break { page-break-after: always; }
153
+
154
+ header { display: flex; justify-content: space-between; align-items: flex-start; gap: 12mm; }
155
+ .brand { display: flex; flex-direction: column; gap: 3mm; }
156
+ .logo { display: block; max-width: 70mm; max-height: 26mm; }
157
+ h1 { margin: 0; font-size: 2em; letter-spacing: 0.08em; font-weight: 700; color: var(--accent); }
158
+ .subtitle { color: var(--muted); font-size: 0.9em; margin-top: 2mm; }
159
+ .meta { text-align: right; font-size: 0.95em; min-width: 64mm; }
160
+ .meta div { margin-bottom: 1mm; }
161
+ .meta .value { font-weight: 700; white-space: nowrap; }
162
+
163
+ .parties { display: flex; gap: 6mm; margin: 7mm 0; }
164
+ .party { flex: 1; border: 1px solid var(--rule); border-radius: 2mm; padding: 4mm; }
165
+ .party h2 {
166
+ margin: 0 0 2mm; font-size: 0.8em; text-transform: uppercase;
167
+ letter-spacing: 0.1em; color: var(--muted); font-weight: 700;
168
+ }
169
+ .party .name { font-weight: 700; font-size: 1.1em; margin-bottom: 1mm; }
170
+ .party .address { font-size: 0.9em; margin-bottom: 1.5mm; }
171
+ .party dl { margin: 2mm 0 0; display: grid; grid-template-columns: auto 1fr; gap: 0.6mm 3mm; font-size: 0.9em; }
172
+ .party dt { color: var(--muted); }
173
+ .party dd { margin: 0; }
174
+ .party .contact { margin-top: 2mm; font-size: 0.9em; color: var(--muted); }
175
+
176
+ table { width: 100%; border-collapse: collapse; font-size: 0.9em; }
177
+ thead th {
178
+ text-align: left; font-size: 0.8em; text-transform: uppercase; letter-spacing: 0.06em;
179
+ color: var(--muted); border-bottom: 1px solid var(--rule); padding: 2mm 1.5mm;
180
+ }
181
+ tbody td { padding: 2mm 1.5mm; border-bottom: 1px solid var(--rule); vertical-align: top; }
182
+ ${theme.zebraRows ? 'tbody tr:nth-child(even) td { background: var(--panel); }' : ''}
183
+ .num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
184
+
185
+ .totals { display: flex; justify-content: flex-end; margin-top: 5mm; }
186
+ .totals table { width: auto; min-width: 80mm; }
187
+ .totals td { padding: 1.5mm 2mm; border: none; background: none; }
188
+ .totals .grand td {
189
+ border-top: 1.5px solid var(--accent); color: var(--accent);
190
+ font-weight: 700; font-size: 1.15em; padding-top: 2.5mm;
191
+ }
192
+
193
+ .vat-summary { margin-top: 6mm; }
194
+ .vat-summary h2, .markings h2 {
195
+ font-size: 0.8em; text-transform: uppercase; letter-spacing: 0.1em;
196
+ color: var(--muted); margin: 0 0 2mm; font-weight: 700;
197
+ }
198
+ .markings { margin-top: 6mm; background: var(--panel); border-radius: 2mm; padding: 4mm; }
199
+ .markings ul { margin: 0; padding-left: 4mm; }
200
+ .markings li { margin-bottom: 1mm; }
201
+ .markings .reference { color: var(--muted); font-size: 0.85em; }
202
+ .note { margin-top: 5mm; font-size: 0.9em; }
203
+ footer {
204
+ margin-top: 8mm; color: var(--muted); font-size: 0.8em;
205
+ border-top: 1px solid var(--rule); padding-top: 2mm;
206
+ }
207
+ footer div { margin-bottom: 0.8mm; }
208
+ ${theme.customCss ? `\n /* customCss */\n${theme.customCss}\n` : ''}</style>`;
209
+ }
210
+ //# sourceMappingURL=theme.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"theme.js","sourceRoot":"","sources":["../src/theme.ts"],"names":[],"mappings":"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"}