From 1f890b407912d3b53f62730f3c579a0d9a0bd9e5 Mon Sep 17 00:00:00 2001 From: Will Boyd Date: Sat, 25 Jan 2025 10:27:24 -0500 Subject: [PATCH] Just a whole lot of stuff in progress --- index.js | 5 +- src/wizard.js | 247 +++++++++++++++++++++++++++++++++----------------- 2 files changed, 166 insertions(+), 86 deletions(-) diff --git a/index.js b/index.js index f1a9298..9745d45 100644 --- a/index.js +++ b/index.js @@ -1,7 +1,6 @@ #!/usr/bin/env node import path from 'path'; -import process from 'process'; import * as parser from './src/parser.js'; import * as settings from './src/settings.js'; import * as wizard from './src/wizard.js'; @@ -9,7 +8,7 @@ import * as writer from './src/writer.js'; (async () => { // parse any command line arguments and run wizard - const config = await wizard.getConfig(process.argv); + const config = await wizard.getConfig(); // parse data from XML and do Markdown translations const posts = await parser.parseFilePromise(config) @@ -20,7 +19,7 @@ import * as writer from './src/writer.js'; // happy goodbye console.log('\nAll done!'); console.log('Look for your output files in: ' + path.resolve(settings.output_directory)); -})().catch(ex => { +})().catch((ex) => { // sad goodbye console.log('\nSomething went wrong, execution halted early.'); console.error(ex); diff --git a/src/wizard.js b/src/wizard.js index da550c4..b2d47a8 100644 --- a/src/wizard.js +++ b/src/wizard.js @@ -7,7 +7,6 @@ import path from 'path'; // all user options for command line and wizard are declared here const options = [ - // wizard must always be first { name: 'wizard', type: 'boolean', @@ -16,50 +15,132 @@ const options = [ }, { name: 'input', - type: 'file', + type: 'file-path', description: 'Path to WordPress export file', - default: 'export.xml' - }, - { - name: 'year-folders', - type: 'boolean', - description: 'Create year folders', - default: false - }, - { - name: 'month-folders', - type: 'boolean', - description: 'Create month folders', - default: false + default: 'export.xml', + prompt: inquirer.input }, { name: 'post-folders', type: 'boolean', - description: 'Create a folder for each post', - default: true + description: 'Put each post into its own folder', + default: true, + choices: [ + { + name: 'Yes', + value: true, + description: '/my-post/index.md' + }, + { + name: 'No', + value: false, + description: '/my-post.md' + } + ], + prompt: inquirer.select }, { name: 'prefix-date', type: 'boolean', - description: 'Prefix post folders/files with date', - default: false + description: 'Prefix with date', + default: false, + choices: [ + { + name: 'Yes', + value: true, + description: '' + }, + { + name: 'No', + value: false, + description: '' + } + ], + prompt: inquirer.select }, { - name: 'save-attached-images', - type: 'boolean', - description: 'Save images attached to posts', - default: true + name: 'date-folders', + type: 'choice', + description: 'Organize into folders based on date', + default: 'none', + choices: [ + { + name: 'Year folders', + value: 'year', + description: '' + }, + { + name: 'Year and month folders', + value: 'year-month', + description: '' + }, + { + name: 'No', + value: 'none', + description: '' + } + ], + prompt: inquirer.select }, { - name: 'save-scraped-images', - type: 'boolean', - description: 'Save images scraped from post body content', - default: true + name: 'save-images', + type: 'choice', + description: 'Save images', + default: 'all', + choices: [ + { + name: 'Images attached to posts', + value: 'attached' + }, + { + name: 'Images scraped from post body content', + value: 'scraped' + }, + { + name: 'Both', + value: 'all' + }, + { + name: 'No', + value: 'none' + } + ], + prompt: inquirer.select } ]; +const validators = { + 'boolean': (value) => { + if (typeof value === 'boolean') { + return value; + } else if (value === 'true') { + return true; + } else if (value === 'false') { + return false; + } + + throw 'Must be true or false.'; + }, + 'file-path': (value) => { + const unwrapped = value.replace(/"(.*?)"/, '$1'); + const absolute = path.resolve(unwrapped); + + let fileExists; + try { + fileExists = fs.existsSync(absolute) && fs.statSync(absolute).isFile(); + } catch (ex) { + fileExists = false; + } + + if (fileExists) { + return absolute; + } else { + throw 'File not found at ' + absolute + '.'; + } + } +}; + export async function getConfig(argv) { - extendOptionsData(); const opts = parseCommandLine(argv); const answers = {}; @@ -67,17 +148,36 @@ export async function getConfig(argv) { console.log('\nStarting wizard...'); const questions = options.filter(option => (option.name !== 'wizard' && !option.isProvided)); for (const question of questions) { - answers[camelcase(question.name)] = await inquirer[question.prompt]({ + let answer = await question.prompt({ message: question.description + '?', + choices: question.choices, + loop: false, default: question.default, validate: question.validate, // not all questions have this, which is fine + theme: { prefix: { - idle: chalk.cyan('?'), + idle: chalk.cyan('\n?'), done: chalk.green('✓') + }, + style: { + description: (text) => chalk.gray('example: ' + text) } } + }).catch((ex) => { + if (ex instanceof Error && ex.name === 'ExitPromptError') { + console.log('\nUser quit wizard early.'); + process.exit(0); + } else { + throw ex; + } }); + + if (question.normalize) { + answer = question.normalize(answer); + } + + answers[camelcase(question.name)] = answer; } } else { console.log('\nSkipping wizard...'); @@ -87,66 +187,47 @@ export async function getConfig(argv) { return config; } -function extendOptionsData() { - // add more data to each option based on its type - const map = { - boolean: { - prompt: 'confirm', - coerce: coerceBoolean, - }, - file: { - prompt: 'input', - coerce: coercePath, - validate: validateFile - }, - folder: { - prompt: 'input', - coerce: coercePath - } - }; - - options.forEach(option => { - Object.assign(option, map[option.type]); - }); -} - -function parseCommandLine(argv) { - // setup for help output +function parseCommandLine() { commander.program .name('node index.js') .helpOption('-h, --help', 'See the thing you\'re looking at right now') - .addHelpText('after', '\nMore documentation is at https://github.com/lonekorean/wordpress-export-to-markdown'); + .addHelpText('after', '\nMore documentation is at https://github.com/lonekorean/wordpress-export-to-markdown') + .configureOutput({ + outputError: (str, write) => write(chalk.red(str)) + }); + options.forEach(input => { const flag = '--' + input.name + ' <' + input.type + '>'; - const coerce = (value) => { - // commander only calls coerce when an input is provided on the command line, which - // makes for an easy way to flag (for later) if it should be excluded from the wizard - input.isProvided = true; - return input.coerce(value); - }; - commander.program.option(flag, input.description, coerce, input.default); + const option = new commander.Option(flag, input.description); + option.default(input.default); + + if (input.choices && input.type !== 'boolean') { + option.choices(input.choices.map((choice) => choice.value)); + } else { + option.argParser((value) => { + const validator = validators[input.type]; + if (!validator) { + return value; + } + + try { + return validator(value); + } catch (ex) { + commander.program.error(`error: option '${flag}' argument '${value}' is invalid. ${ex.toString()}`); + } + }); + } + + commander.program.addOption(option); + }); + + commander.program.parse(); + + options.forEach((option) => { + const opt = camelcase(option.name); + option.isProvided = commander.program.getOptionValueSource(opt) === 'cli'; }); - commander.program.parse(argv); return commander.program.opts(); } - -function coerceBoolean(value) { - return !['false', 'no', '0'].includes(value.toString().toLowerCase()); -} - -function coercePath(value) { - return path.normalize(value); -} - -function validateFile(value) { - let isValid; - try { - isValid = fs.existsSync(value) && fs.statSync(value).isFile(); - } catch (ex) { - isValid = false; - } - - return isValid ? true : 'Unable to find file: ' + path.resolve(value); -}