Files
2026-07-13 12:59:42 +08:00

548 lines
17 KiB
JavaScript

#!/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' ? ' <value>' : ''}`
: `--${opt.name}${opt.type === 'string' ? ' <value>' : ''}`;
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!');