legal-markdown-js
Version:
Node.js implementation of LegalMarkdown for processing legal documents with markdown and YAML - Complete feature parity with Ruby version
161 lines • 6.66 kB
JavaScript
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import * as fsp from 'fs/promises';
import { execFile, spawn } from 'child_process';
import { pathToFileURL } from 'url';
function execFileAsync(command, args) {
return new Promise((resolve, reject) => {
execFile(command, args, (error, stdout, stderr) => {
if (error) {
reject(error);
return;
}
resolve({ stdout, stderr });
});
});
}
export class SystemChromeConnector {
runtimePlatform;
name = 'system-chrome';
detectedExecutablePath = null;
constructor(runtimePlatform = process.platform) {
this.runtimePlatform = runtimePlatform;
}
async detectExecutablePath() {
if (this.detectedExecutablePath) {
return this.detectedExecutablePath;
}
let candidate = null;
if (this.runtimePlatform === 'darwin') {
// Chromium-family browsers checked in preference order: Chrome first
// (most common), then open-source Chromium, then Edge, Brave, and Arc.
const macCandidates = [
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
'/Applications/Chromium.app/Contents/MacOS/Chromium',
'/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
'/Applications/Brave Browser.app/Contents/MacOS/Brave Browser',
'/Applications/Arc.app/Contents/MacOS/Arc',
];
candidate = macCandidates.find(entry => fs.existsSync(entry)) ?? null;
}
else if (this.runtimePlatform === 'linux') {
for (const executable of [
'google-chrome',
'chromium',
'chromium-browser',
'microsoft-edge',
'brave-browser',
]) {
try {
const { stdout } = await execFileAsync('which', [executable]);
const resolved = stdout.trim();
if (resolved) {
candidate = resolved;
break;
}
}
catch {
continue;
}
}
}
else if (this.runtimePlatform === 'win32') {
const programFiles = process.env.PROGRAMFILES;
const programFilesX86 = process.env['PROGRAMFILES(X86)'];
const localAppData = process.env.LOCALAPPDATA;
const windowsCandidates = [
programFiles && path.join(programFiles, 'Google', 'Chrome', 'Application', 'chrome.exe'),
programFilesX86 &&
path.join(programFilesX86, 'Google', 'Chrome', 'Application', 'chrome.exe'),
programFiles && path.join(programFiles, 'Microsoft', 'Edge', 'Application', 'msedge.exe'),
programFilesX86 &&
path.join(programFilesX86, 'Microsoft', 'Edge', 'Application', 'msedge.exe'),
localAppData && path.join(localAppData, 'Google', 'Chrome', 'Application', 'chrome.exe'),
].filter((entry) => Boolean(entry));
candidate = windowsCandidates.find(entry => fs.existsSync(entry)) ?? null;
}
this.detectedExecutablePath = candidate;
return candidate;
}
async isAvailable() {
return Boolean(await this.detectExecutablePath());
}
withPageCss(html, options) {
const size = options.landscape ? `${options.format} landscape` : options.format;
const pageCss = `@page { size: ${size}; margin: ${options.margin.top} ${options.margin.right} ${options.margin.bottom} ${options.margin.left}; }`;
if (html.includes('</head>')) {
return html.replace('</head>', `<style>${pageCss}</style></head>`);
}
return `<html><head><style>${pageCss}</style></head><body>${html}</body></html>`;
}
async generatePdf(html, outputPath, options) {
const executablePath = await this.detectExecutablePath();
if (!executablePath) {
throw new Error('No system Chrome/Chromium/Edge/Brave/Arc executable found.');
}
const outputDir = path.dirname(outputPath);
await fsp.mkdir(outputDir, { recursive: true });
const tempDir = path.join(os.tmpdir(), 'legal-md-system-chrome');
await fsp.mkdir(tempDir, { recursive: true });
// Combine timestamp + random suffix to avoid filename collisions when
// multiple documents are rendered in rapid succession (e.g. batch mode).
const tempHtmlPath = path.join(tempDir, `input-${Date.now()}-${Math.random()}.html`);
try {
await fsp.writeFile(tempHtmlPath, this.withPageCss(html, options), 'utf8');
const fileUrl = pathToFileURL(path.resolve(tempHtmlPath)).toString();
const args = [
'--headless',
'--disable-gpu',
'--no-sandbox',
'--print-to-pdf-no-header',
`--print-to-pdf=${path.resolve(outputPath)}`,
fileUrl,
];
await new Promise((resolve, reject) => {
const child = spawn(executablePath, args, {
stdio: 'ignore',
});
child.on('error', error => {
reject(error);
});
child.on('exit', code => {
if (code === 0) {
resolve();
return;
}
reject(new Error(`System chrome print-to-pdf exited with code ${code ?? 'unknown'}`));
});
});
}
finally {
await fsp.unlink(tempHtmlPath).catch(() => undefined);
}
}
async getInfo() {
const executablePath = await this.detectExecutablePath();
if (!executablePath) {
return {
name: this.name,
version: 'not-installed',
};
}
try {
const { stdout, stderr } = await execFileAsync(executablePath, ['--version']);
const version = (stdout || stderr).trim() || 'unknown';
return {
name: this.name,
version,
executablePath,
};
}
catch {
return {
name: this.name,
version: 'unknown',
executablePath,
};
}
}
}
//# sourceMappingURL=system-chrome-connector.js.map