#!/usr/bin/env node /** * Generates CLI option definitions for Node.js, Python, and documentation * from the single source of truth (options.json). * * Usage: node scripts/generate-options.mjs */ import { readFileSync, writeFileSync, mkdirSync } from 'node:fs'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; import { escapeMarkdown, formatTable } from './utils.mjs'; const __dirname = dirname(fileURLToPath(import.meta.url)); const ROOT_DIR = join(__dirname, '..'); // Read options.json const optionsPath = join(ROOT_DIR, 'options.json'); const options = JSON.parse(readFileSync(optionsPath, 'utf-8')); const AUTO_GENERATED_HEADER = `// AUTO-GENERATED FROM options.json - DO NOT EDIT DIRECTLY // Run \`npm run generate-options\` to regenerate `; const AUTO_GENERATED_HEADER_PYTHON = `# AUTO-GENERATED FROM options.json - DO NOT EDIT DIRECTLY # Run \`npm run generate-options\` to regenerate `; const AUTO_GENERATED_HEADER_MDX = `{/* AUTO-GENERATED FROM options.json - DO NOT EDIT DIRECTLY */} {/* Run \`npm run generate-options\` to regenerate */} `; /** * Convert kebab-case to camelCase */ function toCamelCase(str) { return str.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase()); } /** * Convert kebab-case to snake_case */ function toSnakeCase(str) { return str.replace(/-/g, '_'); } /** * Options that accept comma-separated list values. */ const LIST_OPTIONS = new Set(['format', 'content-safety-off']); /** * Check if option supports list values. */ function isListOption(opt) { return LIST_OPTIONS.has(opt.name); } /** * Escape string for use in generated code. * @param {string} str - The string to escape * @param {string} quote - The quote character (' or ") * @param {object} options - Additional escape options * @param {boolean} options.escapePercent - Escape % as %% for Python argparse */ function escapeString(str, quote = "'", { escapePercent = false } = {}) { let result = str.replace(/\\/g, '\\\\'); // escape backslashes first if (quote === "'") { result = result.replace(/'/g, "\\'"); } else { result = result.replace(/"/g, '\\"'); } if (escapePercent) { result = result.replace(/%/g, '%%'); } return result; } /** * Generate Node.js CLI options file */ function generateNodeCliOptions() { const lines = [AUTO_GENERATED_HEADER]; lines.push(`import { Command } from 'commander';`); lines.push(''); lines.push('/**'); lines.push(' * Register all CLI options on the given Commander program.'); lines.push(' */'); lines.push('export function registerCliOptions(program: Command): void {'); for (const opt of options.options) { const flags = opt.shortName ? `-${opt.shortName}, --${opt.name}${opt.type === 'string' ? ' ' : ''}` : `--${opt.name}${opt.type === 'string' ? ' ' : ''}`; const description = escapeString(opt.description, "'"); lines.push(` program.option('${flags}', '${description}');`); } lines.push('}'); lines.push(''); const outputPath = join(ROOT_DIR, 'node/opendataloader-pdf/src/cli-options.generated.ts'); writeFileSync(outputPath, lines.join('\n')); console.log(`Generated: ${outputPath}`); } /** * Generate Node.js ConvertOptions interface and helper functions */ function generateNodeConvertOptions() { const lines = [AUTO_GENERATED_HEADER]; // Generate ConvertOptions interface lines.push('/**'); lines.push(' * Options for the convert function.'); lines.push(' */'); lines.push('export interface ConvertOptions {'); for (const opt of options.options) { const camelName = toCamelCase(opt.name); let tsType = 'string'; if (opt.type === 'boolean') { tsType = 'boolean'; } else if (isListOption(opt)) { tsType = 'string | string[]'; } lines.push(` /** ${opt.description} */`); lines.push(` ${camelName}?: ${tsType};`); } lines.push('}'); lines.push(''); // Generate CliOptions interface (for CLI parsing - all values are strings from commander) lines.push('/**'); lines.push(' * Options as parsed from CLI (all values are strings from commander).'); lines.push(' */'); lines.push('export interface CliOptions {'); for (const opt of options.options) { const camelName = toCamelCase(opt.name); const tsType = opt.type === 'boolean' ? 'boolean' : 'string'; lines.push(` ${camelName}?: ${tsType};`); } lines.push('}'); lines.push(''); // Generate buildConvertOptions function lines.push('/**'); lines.push(' * Convert CLI options to ConvertOptions.'); lines.push(' */'); lines.push('export function buildConvertOptions(cliOptions: CliOptions): ConvertOptions {'); lines.push(' const convertOptions: ConvertOptions = {};'); lines.push(''); for (const opt of options.options) { const camelName = toCamelCase(opt.name); if (opt.type === 'boolean') { lines.push(` if (cliOptions.${camelName}) {`); lines.push(` convertOptions.${camelName} = true;`); lines.push(' }'); } else { lines.push(` if (cliOptions.${camelName}) {`); lines.push(` convertOptions.${camelName} = cliOptions.${camelName};`); lines.push(' }'); } } lines.push(''); lines.push(' return convertOptions;'); lines.push('}'); lines.push(''); // Generate buildArgs function lines.push('/**'); lines.push(' * Build CLI arguments array from ConvertOptions.'); lines.push(' */'); lines.push('export function buildArgs(options: ConvertOptions): string[] {'); lines.push(' const args: string[] = [];'); lines.push(''); for (const opt of options.options) { const camelName = toCamelCase(opt.name); const cliFlag = `--${opt.name}`; if (opt.type === 'boolean') { lines.push(` if (options.${camelName}) {`); lines.push(` args.push('${cliFlag}');`); lines.push(' }'); } else if (isListOption(opt)) { lines.push(` if (options.${camelName}) {`); lines.push(` if (Array.isArray(options.${camelName})) {`); lines.push(` if (options.${camelName}.length > 0) {`); lines.push(` args.push('${cliFlag}', options.${camelName}.join(','));`); lines.push(' }'); lines.push(' } else {'); lines.push(` args.push('${cliFlag}', options.${camelName});`); lines.push(' }'); lines.push(' }'); } else { lines.push(` if (options.${camelName}) {`); lines.push(` args.push('${cliFlag}', options.${camelName});`); lines.push(' }'); } } lines.push(''); lines.push(' return args;'); lines.push('}'); lines.push(''); const outputPath = join(ROOT_DIR, 'node/opendataloader-pdf/src/convert-options.generated.ts'); writeFileSync(outputPath, lines.join('\n')); console.log(`Generated: ${outputPath}`); } /** * Generate Python CLI options file (cli_options.py) */ function generatePythonCliOptions() { const lines = [AUTO_GENERATED_HEADER_PYTHON]; lines.push('"""'); lines.push('CLI option definitions for opendataloader-pdf.'); lines.push('"""'); lines.push('from typing import Any, Dict, List'); lines.push(''); lines.push(''); lines.push('# Option metadata list'); lines.push('CLI_OPTIONS: List[Dict[str, Any]] = ['); for (const opt of options.options) { const snakeName = toSnakeCase(opt.name); const defaultValue = opt.default === null ? 'None' : typeof opt.default === 'boolean' ? (opt.default ? 'True' : 'False') : `"${opt.default}"`; lines.push(' {'); lines.push(` "name": "${opt.name}",`); lines.push(` "python_name": "${snakeName}",`); lines.push(` "short_name": ${opt.shortName ? `"${opt.shortName}"` : 'None'},`); lines.push(` "type": "${opt.type}",`); lines.push(` "required": ${opt.required ? 'True' : 'False'},`); lines.push(` "default": ${defaultValue},`); lines.push(` "description": "${escapeString(opt.description, '"', { escapePercent: true })}",`); lines.push(' },'); } lines.push(']'); lines.push(''); lines.push(''); lines.push('def add_options_to_parser(parser) -> None:'); lines.push(' """Add all CLI options to an argparse.ArgumentParser."""'); lines.push(' for opt in CLI_OPTIONS:'); lines.push(' flags = []'); lines.push(' if opt["short_name"]:'); lines.push(" flags.append(f'-{opt[\"short_name\"]}')"); lines.push(" flags.append(f'--{opt[\"name\"]}')"); lines.push(''); lines.push(' kwargs = {"help": opt["description"]}'); lines.push(' if opt["type"] == "boolean":'); lines.push(' kwargs["action"] = "store_true"'); lines.push(' else:'); lines.push(' kwargs["default"] = None'); lines.push(''); lines.push(' parser.add_argument(*flags, **kwargs)'); lines.push(''); const outputPath = join(ROOT_DIR, 'python/opendataloader-pdf/src/opendataloader_pdf/cli_options_generated.py'); mkdirSync(dirname(outputPath), { recursive: true }); writeFileSync(outputPath, lines.join('\n')); console.log(`Generated: ${outputPath}`); } /** * Generate Python convert function (convert.py) */ function generatePythonConvert() { const lines = [AUTO_GENERATED_HEADER_PYTHON]; lines.push('"""'); lines.push('Auto-generated convert function for opendataloader-pdf.'); lines.push('"""'); lines.push('from typing import List, Optional, Union'); lines.push(''); lines.push('from .runner import run_jar'); lines.push(''); lines.push(''); // Generate function signature lines.push('def convert('); lines.push(' input_path: Union[str, List[str]],'); for (const opt of options.options) { const snakeName = toSnakeCase(opt.name); let typeHint; let defaultVal; if (opt.type === 'boolean') { typeHint = 'bool'; defaultVal = opt.default ? 'True' : 'False'; } else if (isListOption(opt)) { typeHint = 'Optional[Union[str, List[str]]]'; defaultVal = 'None'; } else { typeHint = 'Optional[str]'; defaultVal = 'None'; } lines.push(` ${snakeName}: ${typeHint} = ${defaultVal},`); } lines.push(') -> None:'); lines.push(' """'); lines.push(' Convert PDF(s) into the requested output format(s).'); lines.push(''); lines.push(' Args:'); lines.push(' input_path: One or more input PDF file paths or directories'); for (const opt of options.options) { const snakeName = toSnakeCase(opt.name); lines.push(` ${snakeName}: ${opt.description}`); } lines.push(' """'); // Generate function body lines.push(' args: List[str] = []'); lines.push(''); lines.push(' # Build input paths'); lines.push(' if isinstance(input_path, list):'); lines.push(' args.extend(input_path)'); lines.push(' else:'); lines.push(' args.append(input_path)'); lines.push(''); // Generate args building for each option for (const opt of options.options) { const snakeName = toSnakeCase(opt.name); const cliFlag = `--${opt.name}`; if (opt.type === 'boolean') { lines.push(` if ${snakeName}:`); lines.push(` args.append("${cliFlag}")`); } else if (isListOption(opt)) { lines.push(` if ${snakeName}:`); lines.push(` if isinstance(${snakeName}, list):`); lines.push(` if ${snakeName}:`); lines.push(` args.extend(["${cliFlag}", ",".join(${snakeName})])`); lines.push(` else:`); lines.push(` args.extend(["${cliFlag}", ${snakeName}])`); } else { lines.push(` if ${snakeName}:`); lines.push(` args.extend(["${cliFlag}", ${snakeName}])`); } } lines.push(''); lines.push(' run_jar(args, quiet)'); lines.push(''); const outputPath = join(ROOT_DIR, 'python/opendataloader-pdf/src/opendataloader_pdf/convert_generated.py'); mkdirSync(dirname(outputPath), { recursive: true }); writeFileSync(outputPath, lines.join('\n')); console.log(`Generated: ${outputPath}`); } /** * Generate Python convert() options table (MDX snippet) */ function generatePythonConvertOptionsMdx() { const lines = []; lines.push('---'); lines.push('title: Python Convert Options'); lines.push('description: Options for the Python convert function'); lines.push('---'); lines.push(''); lines.push(AUTO_GENERATED_HEADER_MDX); // Build rows array const rows = []; // Add input_path first (not in options.json) rows.push(['`input_path`', String.raw`\`str \| list[str]\``, 'required', 'One or more input PDF file paths or directories']); for (const opt of options.options) { const snakeName = toSnakeCase(opt.name); let pyType = 'str'; if (opt.type === 'boolean') { pyType = 'bool'; } else if (isListOption(opt)) { pyType = String.raw`str \| list[str]`; } const defaultVal = opt.default === null ? '-' : typeof opt.default === 'boolean' ? (opt.default ? '`True`' : '`False`') : `\`"${opt.default}"\``; const description = escapeMarkdown(opt.description); rows.push([`\`${snakeName}\``, `\`${pyType}\``, defaultVal, description]); } lines.push(...formatTable(['Parameter', 'Type', 'Default', 'Description'], rows)); lines.push(''); const outputPath = join(ROOT_DIR, 'content/docs/reference/python-convert-options.mdx'); mkdirSync(dirname(outputPath), { recursive: true }); writeFileSync(outputPath, lines.join('\n')); console.log(`Generated: ${outputPath}`); } /** * Generate Node.js convert() options table (MDX snippet) */ function generateNodeConvertOptionsMdx() { const lines = []; lines.push('---'); lines.push('title: Node.js Convert Options'); lines.push('description: Options for the Node.js convert function'); lines.push('---'); lines.push(''); lines.push(AUTO_GENERATED_HEADER_MDX); // Build rows array const rows = []; for (const opt of options.options) { const camelName = toCamelCase(opt.name); let tsType = 'string'; if (opt.type === 'boolean') { tsType = 'boolean'; } else if (isListOption(opt)) { tsType = String.raw`string \| string[]`; } const defaultVal = opt.default === null ? '-' : typeof opt.default === 'boolean' ? `\`${opt.default}\`` : `\`"${opt.default}"\``; const description = escapeMarkdown(opt.description); rows.push([`\`${camelName}\``, `\`${tsType}\``, defaultVal, description]); } lines.push(...formatTable(['Option', 'Type', 'Default', 'Description'], rows)); lines.push(''); const outputPath = join(ROOT_DIR, 'content/docs/reference/node-convert-options.mdx'); mkdirSync(dirname(outputPath), { recursive: true }); writeFileSync(outputPath, lines.join('\n')); console.log(`Generated: ${outputPath}`); } /** * Generate options reference documentation (MDX) */ function generateOptionsReferenceMdx() { // Build rows array const rows = []; for (const opt of options.options) { const longOpt = `\`--${opt.name}\``; const shortOpt = opt.shortName ? `\`-${opt.shortName}\`` : '-'; const type = `\`${opt.type}\``; const defaultVal = opt.default === null ? '-' : typeof opt.default === 'boolean' ? `\`${opt.default}\`` : `\`"${opt.default}"\``; const description = escapeMarkdown(opt.description); rows.push([longOpt, shortOpt, type, defaultVal, description]); } const lines = [ '---', 'title: CLI Options Reference', 'description: Complete reference for all CLI options', '---', '', AUTO_GENERATED_HEADER_MDX.trimEnd(), '# CLI Options Reference', '', 'This page documents all available CLI options for opendataloader-pdf.', '', '## Options', '', ...formatTable(['Option', 'Short', 'Type', 'Default', 'Description'], rows), '', '## Examples', '', '### Basic conversion', '', '```bash', 'opendataloader-pdf document.pdf -o ./output -f json,markdown', '```', '', '### Convert entire folder', '', '```bash', 'opendataloader-pdf ./pdf-folder -o ./output -f json', '```', '', '### Save images as external files', '', '```bash', 'opendataloader-pdf document.pdf -f markdown --image-output external', '```', '', '### Disable reading order sorting', '', '```bash', 'opendataloader-pdf document.pdf -f json --reading-order off', '```', '', '### Add page separators in output', '', '```bash', 'opendataloader-pdf document.pdf -f markdown --markdown-page-separator "--- Page %page-number% ---"', '```', '', '### Encrypted PDF', '', '```bash', 'opendataloader-pdf encrypted.pdf -p mypassword -o ./output', '```', '', ]; const outputPath = join(ROOT_DIR, 'content/docs/reference/cli-options.mdx'); mkdirSync(dirname(outputPath), { recursive: true }); writeFileSync(outputPath, lines.join('\n')); console.log(`Generated: ${outputPath}`); } // Run all generators console.log('Generating files from options.json...\n'); generateNodeCliOptions(); generateNodeConvertOptions(); generatePythonCliOptions(); generatePythonConvert(); generateOptionsReferenceMdx(); generatePythonConvertOptionsMdx(); generateNodeConvertOptionsMdx(); console.log('\nDone!');