From ed9aad04e29dc77147362f1dd835c8fb36995616 Mon Sep 17 00:00:00 2001 From: Will Boyd Date: Mon, 20 Jan 2025 15:30:19 -0500 Subject: [PATCH 01/20] Remove aliases --- src/wizard.js | 36 +----------------------------------- 1 file changed, 1 insertion(+), 35 deletions(-) diff --git a/src/wizard.js b/src/wizard.js index 2f95c16..e5d4b1a 100644 --- a/src/wizard.js +++ b/src/wizard.js @@ -27,42 +27,36 @@ const options = [ }, { name: 'year-folders', - aliases: ['yearfolders', 'yearmonthfolders'], type: 'boolean', description: 'Create year folders', default: false }, { name: 'month-folders', - aliases: ['yearmonthfolders'], type: 'boolean', description: 'Create month folders', default: false }, { name: 'post-folders', - aliases: ['postfolders'], type: 'boolean', description: 'Create a folder for each post', default: true }, { name: 'prefix-date', - aliases: ['prefixdate'], type: 'boolean', description: 'Prefix post folders/files with date', default: false }, { name: 'save-attached-images', - aliases: ['saveimages'], type: 'boolean', description: 'Save images attached to posts', default: true }, { name: 'save-scraped-images', - aliases: ['addcontentimages'], type: 'boolean', description: 'Save images scraped from post body content', default: true @@ -77,8 +71,7 @@ const options = [ export async function getConfig(argv) { extendOptionsData(); - const unaliasedArgv = replaceAliases(argv); - const opts = parseCommandLine(unaliasedArgv); + const opts = parseCommandLine(argv); let answers; if (opts.wizard) { @@ -127,33 +120,6 @@ function extendOptionsData() { }); } -function replaceAliases(argv) { - let paths = argv.slice(0, 2); - let replaced = []; - let unmodified = []; - - argv.slice(2).forEach(arg => { - let aliasFound = false; - - // this loop does not short circuit because an alias can map to multiple options - options.forEach(option => { - const aliases = option.aliases || []; - aliases.forEach(alias => { - if (arg.includes('--' + alias)) { - replaced.push(arg.replace('--' + alias, '--' + option.name)); - aliasFound = true; - } - }); - }); - - if (!aliasFound) { - unmodified.push(arg); - } - }); - - return [...paths, ...replaced, ...unmodified]; -} - function parseCommandLine(argv) { // setup for help output commander.program From 2add3804061f85365458203f49f135a94f38d90e Mon Sep 17 00:00:00 2001 From: Will Boyd Date: Mon, 20 Jan 2025 16:09:27 -0500 Subject: [PATCH 02/20] Remove include-other-types option --- src/parser.js | 20 +++++++------------- src/settings.js | 11 +++++++++++ src/wizard.js | 6 ------ src/writer.js | 11 +++-------- 4 files changed, 21 insertions(+), 27 deletions(-) diff --git a/src/parser.js b/src/parser.js index 4083dd0..a4cbadc 100644 --- a/src/parser.js +++ b/src/parser.js @@ -14,7 +14,7 @@ export async function parseFilePromise(config) { }); const channelData = allData.rss.channel[0].item; - const postTypes = getPostTypes(channelData, config); + const postTypes = getPostTypes(channelData); const posts = collectPosts(channelData, postTypes, config); const images = []; @@ -31,18 +31,12 @@ export async function parseFilePromise(config) { return posts; } -function getPostTypes(channelData, config) { - if (config.includeOtherTypes) { - // search export file for all post types minus some default types we don't want - // effectively this will be 'post', 'page', and custom post types - const types = channelData - .map(item => item.post_type[0]) - .filter(type => !['attachment', 'revision', 'nav_menu_item', 'custom_css', 'customize_changeset'].includes(type)); - return [...new Set(types)]; // remove duplicates - } else { - // just plain old vanilla "post" posts - return ['post']; - } +function getPostTypes(channelData) { + // search export file for all post types minus some specific types we don't want + const types = channelData + .map(item => item.post_type[0]) + .filter(type => !settings.filter_post_types.includes(type)); + return [...new Set(types)]; // remove duplicates } function getItemsOfType(channelData, type) { diff --git a/src/settings.js b/src/settings.js index 111ed0e..58e67b3 100644 --- a/src/settings.js +++ b/src/settings.js @@ -38,3 +38,14 @@ export const filter_categories = ['uncategorized']; // Strict SSL is enabled as the safe default when downloading images, but will not work with // self-signed servers. You can disable it if you're getting a "self-signed certificate" error. export const strict_ssl = true; + +// Post types to exclude from output. +export const filter_post_types = [ + 'attachment', + 'revision', + 'nav_menu_item', + 'custom_css', + 'customize_changeset', + 'wp_global_styles', + 'wp_navigation' +]; diff --git a/src/wizard.js b/src/wizard.js index e5d4b1a..e625a47 100644 --- a/src/wizard.js +++ b/src/wizard.js @@ -60,12 +60,6 @@ const options = [ type: 'boolean', description: 'Save images scraped from post body content', default: true - }, - { - name: 'include-other-types', - type: 'boolean', - description: 'Include custom post types and pages', - default: false } ]; diff --git a/src/writer.js b/src/writer.js index 510eaf3..7df7520 100644 --- a/src/writer.js +++ b/src/writer.js @@ -55,7 +55,7 @@ async function writeMarkdownFilesPromise(posts, config) { } else { const payload = { item: post, - name: (config.includeOtherTypes ? post.meta.type + ' - ' : '') + post.meta.slug, + name: post.meta.type + ' - ' + post.meta.slug, destinationPath, delay }; @@ -179,13 +179,8 @@ function getPostPath(post, config) { dt = luxon.DateTime.fromISO(post.frontmatter.date); } - // start with base output dir - const pathSegments = [config.output]; - - // create segment for post type if we're dealing with more than just "post" - if (config.includeOtherTypes) { - pathSegments.push(post.meta.type); - } + // start with base output dir and post type + const pathSegments = [config.output, post.meta.type]; if (config.yearFolders) { pathSegments.push(dt.toFormat('yyyy')); From bafe8692213d778190bdcf4bce783d986e57e4f7 Mon Sep 17 00:00:00 2001 From: Will Boyd Date: Mon, 20 Jan 2025 16:28:36 -0500 Subject: [PATCH 03/20] Remove output option --- index.js | 3 ++- src/settings.js | 3 +++ src/wizard.js | 6 ------ src/writer.js | 2 +- 4 files changed, 6 insertions(+), 8 deletions(-) diff --git a/index.js b/index.js index cd286c6..f1a9298 100644 --- a/index.js +++ b/index.js @@ -3,6 +3,7 @@ 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'; import * as writer from './src/writer.js'; @@ -18,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(config.output)); + console.log('Look for your output files in: ' + path.resolve(settings.output_directory)); })().catch(ex => { // sad goodbye console.log('\nSomething went wrong, execution halted early.'); diff --git a/src/settings.js b/src/settings.js index 58e67b3..28ef891 100644 --- a/src/settings.js +++ b/src/settings.js @@ -49,3 +49,6 @@ export const filter_post_types = [ 'wp_global_styles', 'wp_navigation' ]; + +// Output directory. +export const output_directory = 'output'; diff --git a/src/wizard.js b/src/wizard.js index e625a47..2436439 100644 --- a/src/wizard.js +++ b/src/wizard.js @@ -19,12 +19,6 @@ const options = [ description: 'Path to WordPress export file', default: 'export.xml' }, - { - name: 'output', - type: 'folder', - description: 'Path to output folder', - default: 'output' - }, { name: 'year-folders', type: 'boolean', diff --git a/src/writer.js b/src/writer.js index 7df7520..0b4ea54 100644 --- a/src/writer.js +++ b/src/writer.js @@ -180,7 +180,7 @@ function getPostPath(post, config) { } // start with base output dir and post type - const pathSegments = [config.output, post.meta.type]; + const pathSegments = [settings.output_directory, post.meta.type]; if (config.yearFolders) { pathSegments.push(dt.toFormat('yyyy')); From 3f4b2449350a493e3f64995e4db192ade0db74ed Mon Sep 17 00:00:00 2001 From: Will Boyd Date: Tue, 21 Jan 2025 10:32:28 -0500 Subject: [PATCH 04/20] Update inquirer and fix code --- package-lock.json | 305 +++++++++++++++++++++------------------------- package.json | 2 +- src/wizard.js | 32 ++--- 3 files changed, 155 insertions(+), 184 deletions(-) diff --git a/package-lock.json b/package-lock.json index 70998ba..73a51c9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,11 +9,11 @@ "version": "2.4.2", "license": "MIT", "dependencies": { + "@inquirer/prompts": "^7.2.3", "axios": "^1.7.9", "camelcase": "^8.0.0", "chalk": "^5.4.1", "commander": "^13.0.0", - "inquirer": "^10.2.2", "luxon": "^3.5.0", "turndown": "^7.2.0", "turndown-plugin-gfm": "^1.0.2", @@ -124,45 +124,48 @@ "dev": true }, "node_modules/@inquirer/checkbox": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-2.5.0.tgz", - "integrity": "sha512-sMgdETOfi2dUHT8r7TT1BTKOwNvdDGFDXYWtQ2J69SvlYNntk9I/gJe7r5yvMwwsuKnYbuRs3pNhx4tgNck5aA==", + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.0.6.tgz", + "integrity": "sha512-PgP35JfmGjHU0LSXOyRew0zHuA9N6OJwOlos1fZ20b7j8ISeAdib3L+n0jIxBtX958UeEpte6xhG/gxJ5iUqMw==", "dependencies": { - "@inquirer/core": "^9.1.0", - "@inquirer/figures": "^1.0.5", - "@inquirer/type": "^1.5.3", + "@inquirer/core": "^10.1.4", + "@inquirer/figures": "^1.0.9", + "@inquirer/type": "^3.0.2", "ansi-escapes": "^4.3.2", "yoctocolors-cjs": "^2.1.2" }, "engines": { "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" } }, "node_modules/@inquirer/confirm": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-3.2.0.tgz", - "integrity": "sha512-oOIwPs0Dvq5220Z8lGL/6LHRTEr9TgLHmiI99Rj1PJ1p1czTys+olrgBqZk4E2qC0YTzeHprxSQmoHioVdJ7Lw==", + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.3.tgz", + "integrity": "sha512-fuF9laMmHoOgWapF9h9hv6opA5WvmGFHsTYGCmuFxcghIhEhb3dN0CdQR4BUMqa2H506NCj8cGX4jwMsE4t6dA==", "dependencies": { - "@inquirer/core": "^9.1.0", - "@inquirer/type": "^1.5.3" + "@inquirer/core": "^10.1.4", + "@inquirer/type": "^3.0.2" }, "engines": { "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" } }, "node_modules/@inquirer/core": { - "version": "9.2.1", - "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-9.2.1.tgz", - "integrity": "sha512-F2VBt7W/mwqEU4bL0RnHNZmC/OxzNx9cOYxHqnXX3MP6ruYvZUZAW9imgN9+h/uBT/oP8Gh888J2OZSbjSeWcg==", + "version": "10.1.4", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.4.tgz", + "integrity": "sha512-5y4/PUJVnRb4bwWY67KLdebWOhOc7xj5IP2J80oWXa64mVag24rwQ1VAdnj7/eDY/odhguW0zQ1Mp1pj6fO/2w==", "dependencies": { - "@inquirer/figures": "^1.0.6", - "@inquirer/type": "^2.0.0", - "@types/mute-stream": "^0.0.4", - "@types/node": "^22.5.5", - "@types/wrap-ansi": "^3.0.0", + "@inquirer/figures": "^1.0.9", + "@inquirer/type": "^3.0.2", "ansi-escapes": "^4.3.2", "cli-width": "^4.1.0", - "mute-stream": "^1.0.0", + "mute-stream": "^2.0.0", "signal-exit": "^4.1.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^6.2.0", @@ -172,41 +175,36 @@ "node": ">=18" } }, - "node_modules/@inquirer/core/node_modules/@inquirer/type": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-2.0.0.tgz", - "integrity": "sha512-XvJRx+2KR3YXyYtPUUy+qd9i7p+GO9Ko6VIIpWlBrpWwXDv8WLFeHTxz35CfQFUiBMLXlGHhGzys7lqit9gWag==", - "dependencies": { - "mute-stream": "^1.0.0" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/@inquirer/editor": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-2.2.0.tgz", - "integrity": "sha512-9KHOpJ+dIL5SZli8lJ6xdaYLPPzB8xB9GZItg39MBybzhxA16vxmszmQFrRwbOA918WA2rvu8xhDEg/p6LXKbw==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.3.tgz", + "integrity": "sha512-S9KnIOJuTZpb9upeRSBBhoDZv7aSV3pG9TECrBj0f+ZsFwccz886hzKBrChGrXMJwd4NKY+pOA9Vy72uqnd6Eg==", "dependencies": { - "@inquirer/core": "^9.1.0", - "@inquirer/type": "^1.5.3", + "@inquirer/core": "^10.1.4", + "@inquirer/type": "^3.0.2", "external-editor": "^3.1.0" }, "engines": { "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" } }, "node_modules/@inquirer/expand": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-2.3.0.tgz", - "integrity": "sha512-qnJsUcOGCSG1e5DTOErmv2BPQqrtT6uzqn1vI/aYGiPKq+FgslGZmtdnXbhuI7IlT7OByDoEEqdnhUnVR2hhLw==", + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.6.tgz", + "integrity": "sha512-TRTfi1mv1GeIZGyi9PQmvAaH65ZlG4/FACq6wSzs7Vvf1z5dnNWsAAXBjWMHt76l+1hUY8teIqJFrWBk5N6gsg==", "dependencies": { - "@inquirer/core": "^9.1.0", - "@inquirer/type": "^1.5.3", + "@inquirer/core": "^10.1.4", + "@inquirer/type": "^3.0.2", "yoctocolors-cjs": "^2.1.2" }, "engines": { "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" } }, "node_modules/@inquirer/figures": { @@ -218,113 +216,134 @@ } }, "node_modules/@inquirer/input": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-2.3.0.tgz", - "integrity": "sha512-XfnpCStx2xgh1LIRqPXrTNEEByqQWoxsWYzNRSEUxJ5c6EQlhMogJ3vHKu8aXuTacebtaZzMAHwEL0kAflKOBw==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.1.3.tgz", + "integrity": "sha512-zeo++6f7hxaEe7OjtMzdGZPHiawsfmCZxWB9X1NpmYgbeoyerIbWemvlBxxl+sQIlHC0WuSAG19ibMq3gbhaqQ==", "dependencies": { - "@inquirer/core": "^9.1.0", - "@inquirer/type": "^1.5.3" + "@inquirer/core": "^10.1.4", + "@inquirer/type": "^3.0.2" }, "engines": { "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" } }, "node_modules/@inquirer/number": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-1.1.0.tgz", - "integrity": "sha512-ilUnia/GZUtfSZy3YEErXLJ2Sljo/mf9fiKc08n18DdwdmDbOzRcTv65H1jjDvlsAuvdFXf4Sa/aL7iw/NanVA==", + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.6.tgz", + "integrity": "sha512-xO07lftUHk1rs1gR0KbqB+LJPhkUNkyzV/KhH+937hdkMazmAYHLm1OIrNKpPelppeV1FgWrgFDjdUD8mM+XUg==", "dependencies": { - "@inquirer/core": "^9.1.0", - "@inquirer/type": "^1.5.3" + "@inquirer/core": "^10.1.4", + "@inquirer/type": "^3.0.2" }, "engines": { "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" } }, "node_modules/@inquirer/password": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-2.2.0.tgz", - "integrity": "sha512-5otqIpgsPYIshqhgtEwSspBQE40etouR8VIxzpJkv9i0dVHIpyhiivbkH9/dGiMLdyamT54YRdGJLfl8TFnLHg==", + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.6.tgz", + "integrity": "sha512-QLF0HmMpHZPPMp10WGXh6F+ZPvzWE7LX6rNoccdktv/Rov0B+0f+eyXkAcgqy5cH9V+WSpbLxu2lo3ysEVK91w==", "dependencies": { - "@inquirer/core": "^9.1.0", - "@inquirer/type": "^1.5.3", + "@inquirer/core": "^10.1.4", + "@inquirer/type": "^3.0.2", "ansi-escapes": "^4.3.2" }, "engines": { "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" } }, "node_modules/@inquirer/prompts": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-5.5.0.tgz", - "integrity": "sha512-BHDeL0catgHdcHbSFFUddNzvx/imzJMft+tWDPwTm3hfu8/tApk1HrooNngB2Mb4qY+KaRWF+iZqoVUPeslEog==", + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.2.3.tgz", + "integrity": "sha512-hzfnm3uOoDySDXfDNOm9usOuYIaQvTgKp/13l1uJoe6UNY+Zpcn2RYt0jXz3yA+yemGHvDOxVzqWl3S5sQq53Q==", "dependencies": { - "@inquirer/checkbox": "^2.5.0", - "@inquirer/confirm": "^3.2.0", - "@inquirer/editor": "^2.2.0", - "@inquirer/expand": "^2.3.0", - "@inquirer/input": "^2.3.0", - "@inquirer/number": "^1.1.0", - "@inquirer/password": "^2.2.0", - "@inquirer/rawlist": "^2.3.0", - "@inquirer/search": "^1.1.0", - "@inquirer/select": "^2.5.0" + "@inquirer/checkbox": "^4.0.6", + "@inquirer/confirm": "^5.1.3", + "@inquirer/editor": "^4.2.3", + "@inquirer/expand": "^4.0.6", + "@inquirer/input": "^4.1.3", + "@inquirer/number": "^3.0.6", + "@inquirer/password": "^4.0.6", + "@inquirer/rawlist": "^4.0.6", + "@inquirer/search": "^3.0.6", + "@inquirer/select": "^4.0.6" }, "engines": { "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" } }, "node_modules/@inquirer/rawlist": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-2.3.0.tgz", - "integrity": "sha512-zzfNuINhFF7OLAtGHfhwOW2TlYJyli7lOUoJUXw/uyklcwalV6WRXBXtFIicN8rTRK1XTiPWB4UY+YuW8dsnLQ==", + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.0.6.tgz", + "integrity": "sha512-QoE4s1SsIPx27FO4L1b1mUjVcoHm1pWE/oCmm4z/Hl+V1Aw5IXl8FYYzGmfXaBT0l/sWr49XmNSiq7kg3Kd/Lg==", "dependencies": { - "@inquirer/core": "^9.1.0", - "@inquirer/type": "^1.5.3", + "@inquirer/core": "^10.1.4", + "@inquirer/type": "^3.0.2", "yoctocolors-cjs": "^2.1.2" }, "engines": { "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" } }, "node_modules/@inquirer/search": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-1.1.0.tgz", - "integrity": "sha512-h+/5LSj51dx7hp5xOn4QFnUaKeARwUCLs6mIhtkJ0JYPBLmEYjdHSYh7I6GrLg9LwpJ3xeX0FZgAG1q0QdCpVQ==", + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.0.6.tgz", + "integrity": "sha512-eFZ2hiAq0bZcFPuFFBmZEtXU1EarHLigE+ENCtpO+37NHCl4+Yokq1P/d09kUblObaikwfo97w+0FtG/EXl5Ng==", "dependencies": { - "@inquirer/core": "^9.1.0", - "@inquirer/figures": "^1.0.5", - "@inquirer/type": "^1.5.3", + "@inquirer/core": "^10.1.4", + "@inquirer/figures": "^1.0.9", + "@inquirer/type": "^3.0.2", "yoctocolors-cjs": "^2.1.2" }, "engines": { "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" } }, "node_modules/@inquirer/select": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-2.5.0.tgz", - "integrity": "sha512-YmDobTItPP3WcEI86GvPo+T2sRHkxxOq/kXmsBjHS5BVXUgvgZ5AfJjkvQvZr03T81NnI3KrrRuMzeuYUQRFOA==", + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.0.6.tgz", + "integrity": "sha512-yANzIiNZ8fhMm4NORm+a74+KFYHmf7BZphSOBovIzYPVLquseTGEkU5l2UTnBOf5k0VLmTgPighNDLE9QtbViQ==", "dependencies": { - "@inquirer/core": "^9.1.0", - "@inquirer/figures": "^1.0.5", - "@inquirer/type": "^1.5.3", + "@inquirer/core": "^10.1.4", + "@inquirer/figures": "^1.0.9", + "@inquirer/type": "^3.0.2", "ansi-escapes": "^4.3.2", "yoctocolors-cjs": "^2.1.2" }, "engines": { "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" } }, "node_modules/@inquirer/type": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-1.5.5.tgz", - "integrity": "sha512-MzICLu4yS7V8AA61sANROZ9vT1H3ooca5dSmI1FjZkzq7o/koMsRfQSzRtFo+F3Ao4Sf1C0bpLKejpKB/+j6MA==", - "dependencies": { - "mute-stream": "^1.0.0" - }, + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.2.tgz", + "integrity": "sha512-ZhQ4TvhwHZF+lGhQ2O/rsjo80XoZR5/5qhOY3t6FJuX5XBg5Be8YzYTvaUGJnc12AUGI2nr4QSUE4PhKSigx7g==", "engines": { "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" } }, "node_modules/@mixmark-io/domino": { @@ -367,27 +386,15 @@ "node": ">= 8" } }, - "node_modules/@types/mute-stream": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/@types/mute-stream/-/mute-stream-0.0.4.tgz", - "integrity": "sha512-CPM9nzrCPPJHQNA9keH9CVkVI+WR5kMa+7XEs5jcGQ0VoAGnLv242w8lIVgwAEfmE4oufJRaTc9PNLQl0ioAow==", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/node": { "version": "22.10.7", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.7.tgz", "integrity": "sha512-V09KvXxFiutGp6B7XkpaDXlNadZxrzajcY50EuoLIpQ6WWYCSvf19lVIazzfIzQvhUN2HjX12spLojTnhuKlGg==", + "peer": true, "dependencies": { "undici-types": "~6.20.0" } }, - "node_modules/@types/wrap-ansi": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/wrap-ansi/-/wrap-ansi-3.0.0.tgz", - "integrity": "sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g==" - }, "node_modules/@ungap/structured-clone": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.1.tgz", @@ -445,17 +452,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/ansi-escapes/node_modules/type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -587,9 +583,9 @@ } }, "node_modules/commander": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-13.0.0.tgz", - "integrity": "sha512-oPYleIY8wmTVzkvQq10AEok6YcTC4sRUBl8F9gVuwchGVUCTbl/vhLTaQqutuuySYOsu8YTgV+OxKc/8Yvx+mQ==", + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", + "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", "engines": { "node": ">=18" } @@ -1007,6 +1003,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/globals/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", @@ -1084,24 +1092,6 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true }, - "node_modules/inquirer": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-10.2.2.tgz", - "integrity": "sha512-tyao/4Vo36XnUItZ7DnUXX4f1jVao2mSrleV/5IPtW/XAEA26hRVsbc68nuTEKWcr5vMP/1mVoT2O7u8H4v1Vg==", - "dependencies": { - "@inquirer/core": "^9.1.0", - "@inquirer/prompts": "^5.5.0", - "@inquirer/type": "^1.5.3", - "@types/mute-stream": "^0.0.4", - "ansi-escapes": "^4.3.2", - "mute-stream": "^1.0.0", - "run-async": "^3.0.0", - "rxjs": "^7.8.1" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -1265,11 +1255,11 @@ "dev": true }, "node_modules/mute-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz", - "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/natural-compare": { @@ -1459,14 +1449,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/run-async": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-3.0.0.tgz", - "integrity": "sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==", - "engines": { - "node": ">=0.12.0" - } - }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -1490,14 +1472,6 @@ "queue-microtask": "^1.2.2" } }, - "node_modules/rxjs": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", - "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", - "dependencies": { - "tslib": "^2.1.0" - } - }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -1605,11 +1579,6 @@ "node": ">=0.6.0" } }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" - }, "node_modules/turndown": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/turndown/-/turndown-7.2.0.tgz", @@ -1636,10 +1605,9 @@ } }, "node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", "engines": { "node": ">=10" }, @@ -1650,7 +1618,8 @@ "node_modules/undici-types": { "version": "6.20.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==" + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "peer": true }, "node_modules/uri-js": { "version": "4.4.1", diff --git a/package.json b/package.json index cb12728..55c4509 100644 --- a/package.json +++ b/package.json @@ -21,11 +21,11 @@ }, "type": "module", "dependencies": { + "@inquirer/prompts": "^7.2.3", "axios": "^1.7.9", "camelcase": "^8.0.0", "chalk": "^5.4.1", "commander": "^13.0.0", - "inquirer": "^10.2.2", "luxon": "^3.5.0", "turndown": "^7.2.0", "turndown-plugin-gfm": "^1.0.2", diff --git a/src/wizard.js b/src/wizard.js index 2436439..da550c4 100644 --- a/src/wizard.js +++ b/src/wizard.js @@ -1,7 +1,8 @@ +import * as inquirer from '@inquirer/prompts'; import camelcase from 'camelcase'; +import chalk from 'chalk'; import * as commander from 'commander'; import fs from 'fs'; -import inquirer from 'inquirer'; import path from 'path'; // all user options for command line and wizard are declared here @@ -61,24 +62,25 @@ export async function getConfig(argv) { extendOptionsData(); const opts = parseCommandLine(argv); - let answers; + const answers = {}; if (opts.wizard) { console.log('\nStarting wizard...'); - const questions = options.map(option => ({ - when: option.name !== 'wizard' && !option.isProvided, - name: camelcase(option.name), - type: option.prompt, - message: option.description + '?', - default: option.default, - - // these are not used for all option types and that's fine - filter: option.coerce, - validate: option.validate - })); - answers = await inquirer.prompt(questions); + const questions = options.filter(option => (option.name !== 'wizard' && !option.isProvided)); + for (const question of questions) { + answers[camelcase(question.name)] = await inquirer[question.prompt]({ + message: question.description + '?', + default: question.default, + validate: question.validate, // not all questions have this, which is fine + theme: { + prefix: { + idle: chalk.cyan('?'), + done: chalk.green('✓') + } + } + }); + } } else { console.log('\nSkipping wizard...'); - answers = {}; } const config = { ...opts, ...answers }; From 1f890b407912d3b53f62730f3c579a0d9a0bd9e5 Mon Sep 17 00:00:00 2001 From: Will Boyd Date: Sat, 25 Jan 2025 10:27:24 -0500 Subject: [PATCH 05/20] 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); -} From 6a6e5074f0a59b25bbaad43b8188f133dd0bac9d Mon Sep 17 00:00:00 2001 From: Will Boyd Date: Sat, 25 Jan 2025 13:46:47 -0500 Subject: [PATCH 06/20] Fixed validation/normalization for wizard --- src/wizard.js | 36 ++++++++++++++++++++++++++---------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/src/wizard.js b/src/wizard.js index b2d47a8..9e4cfd4 100644 --- a/src/wizard.js +++ b/src/wizard.js @@ -148,23 +148,41 @@ export async function getConfig(argv) { console.log('\nStarting wizard...'); const questions = options.filter(option => (option.name !== 'wizard' && !option.isProvided)); for (const question of questions) { - let answer = await question.prompt({ + const promptConfig = { 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('\n?'), + idle: chalk.gray('\n?'), done: chalk.green('✓') }, style: { description: (text) => chalk.gray('example: ' + text) } } - }).catch((ex) => { + }; + + if (question.choices) { + promptConfig.choices = question.choices; + promptConfig.loop = false; + } else { + const validator = validators[question.type]; + if (validator) { + promptConfig.validate = (value) => { + try { + normalized = validator(value); + } catch (ex) { + return ex.toString(); + } + + return true; + } + } + } + + let normalized = undefined; + let answer = await question.prompt(promptConfig).catch((ex) => { if (ex instanceof Error && ex.name === 'ExitPromptError') { console.log('\nUser quit wizard early.'); process.exit(0); @@ -173,11 +191,9 @@ export async function getConfig(argv) { } }); - if (question.normalize) { - answer = question.normalize(answer); - } + console.log(normalized, answer); - answers[camelcase(question.name)] = answer; + answers[camelcase(question.name)] = normalized ?? answer; } } else { console.log('\nSkipping wizard...'); From 8841c607e7cacdf3751a4212614c285bc332f2f4 Mon Sep 17 00:00:00 2001 From: Will Boyd Date: Sat, 25 Jan 2025 16:23:45 -0500 Subject: [PATCH 07/20] Separate normalizers out, fix config flag checks --- src/normalizers.js | 32 ++++++++++++++++++++++++++++ src/parser.js | 4 ++-- src/wizard.js | 53 +++++++++------------------------------------- src/writer.js | 4 ++-- 4 files changed, 46 insertions(+), 47 deletions(-) create mode 100644 src/normalizers.js diff --git a/src/normalizers.js b/src/normalizers.js new file mode 100644 index 0000000..44f4121 --- /dev/null +++ b/src/normalizers.js @@ -0,0 +1,32 @@ +import fs from 'fs'; +import path from 'path'; + +export function 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.'; +} + +export function filePath(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 + '.'; + } +} diff --git a/src/parser.js b/src/parser.js index a4cbadc..08775ad 100644 --- a/src/parser.js +++ b/src/parser.js @@ -18,10 +18,10 @@ export async function parseFilePromise(config) { const posts = collectPosts(channelData, postTypes, config); const images = []; - if (config.saveAttachedImages) { + if (config.saveImages === 'attached' || config.saveImages === 'all') { images.push(...collectAttachedImages(channelData)); } - if (config.saveScrapedImages) { + if (config.saveImages === 'scraped' || config.saveImages === 'all') { images.push(...collectScrapedImages(channelData, postTypes)); } diff --git a/src/wizard.js b/src/wizard.js index 9e4cfd4..eb24b54 100644 --- a/src/wizard.js +++ b/src/wizard.js @@ -2,8 +2,7 @@ import * as inquirer from '@inquirer/prompts'; import camelcase from 'camelcase'; import chalk from 'chalk'; import * as commander from 'commander'; -import fs from 'fs'; -import path from 'path'; +import * as normalizers from './normalizers.js'; // all user options for command line and wizard are declared here const options = [ @@ -109,37 +108,6 @@ const options = [ } ]; -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) { const opts = parseCommandLine(argv); @@ -148,6 +116,8 @@ export async function getConfig(argv) { console.log('\nStarting wizard...'); const questions = options.filter(option => (option.name !== 'wizard' && !option.isProvided)); for (const question of questions) { + let normalizedAnswer = undefined; + const promptConfig = { message: question.description + '?', default: question.default, @@ -167,11 +137,11 @@ export async function getConfig(argv) { promptConfig.choices = question.choices; promptConfig.loop = false; } else { - const validator = validators[question.type]; - if (validator) { + const normalizer = normalizers[camelcase(question.type)]; + if (normalizer) { promptConfig.validate = (value) => { try { - normalized = validator(value); + normalizedAnswer = normalizer(value); } catch (ex) { return ex.toString(); } @@ -181,7 +151,6 @@ export async function getConfig(argv) { } } - let normalized = undefined; let answer = await question.prompt(promptConfig).catch((ex) => { if (ex instanceof Error && ex.name === 'ExitPromptError') { console.log('\nUser quit wizard early.'); @@ -191,9 +160,7 @@ export async function getConfig(argv) { } }); - console.log(normalized, answer); - - answers[camelcase(question.name)] = normalized ?? answer; + answers[camelcase(question.name)] = normalizedAnswer ?? answer; } } else { console.log('\nSkipping wizard...'); @@ -222,13 +189,13 @@ function parseCommandLine() { option.choices(input.choices.map((choice) => choice.value)); } else { option.argParser((value) => { - const validator = validators[input.type]; - if (!validator) { + const normalizer = normalizers[camelcase(input.type)]; + if (!normalizer) { return value; } try { - return validator(value); + return normalizer(value); } catch (ex) { commander.program.error(`error: option '${flag}' argument '${value}' is invalid. ${ex.toString()}`); } diff --git a/src/writer.js b/src/writer.js index 0b4ea54..61d4fcf 100644 --- a/src/writer.js +++ b/src/writer.js @@ -182,11 +182,11 @@ function getPostPath(post, config) { // start with base output dir and post type const pathSegments = [settings.output_directory, post.meta.type]; - if (config.yearFolders) { + if (config.dateFolders === 'year' || config.dateFolders === 'year-month') { pathSegments.push(dt.toFormat('yyyy')); } - if (config.monthFolders) { + if (config.dateFolders === 'year-month') { pathSegments.push(dt.toFormat('LL')); } From 07fbdc31e77632c6093709b3c63e511f8e92b131 Mon Sep 17 00:00:00 2001 From: Will Boyd Date: Sun, 26 Jan 2025 19:22:59 -0500 Subject: [PATCH 08/20] Option validation and edge cases --- src/wizard.js | 56 +++++++++++++++++++++++++++++++-------------------- 1 file changed, 34 insertions(+), 22 deletions(-) diff --git a/src/wizard.js b/src/wizard.js index eb24b54..a58c211 100644 --- a/src/wizard.js +++ b/src/wizard.js @@ -114,7 +114,9 @@ export async function getConfig(argv) { const answers = {}; if (opts.wizard) { console.log('\nStarting wizard...'); - const questions = options.filter(option => (option.name !== 'wizard' && !option.isProvided)); + const questions = options + .filter((option) => option.name !== 'wizard') + .filter((option) => !opts[camelcase(option.name)]); for (const question of questions) { let normalizedAnswer = undefined; @@ -180,37 +182,47 @@ function parseCommandLine() { }); - options.forEach(input => { - const flag = '--' + input.name + ' <' + input.type + '>'; - const option = new commander.Option(flag, input.description); + options.forEach((input) => { + const option = new commander.Option('--' + input.name + ' <' + input.type + '>', 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 normalizer = normalizers[camelcase(input.type)]; - if (!normalizer) { - return value; - } - - try { - return normalizer(value); - } catch (ex) { - commander.program.error(`error: option '${flag}' argument '${value}' is invalid. ${ex.toString()}`); - } - }); + option.argParser((value) => normalizeCommandLineArg(input.type, input.name, value)); } commander.program.addOption(option); }); - commander.program.parse(); + const opts = commander.program.parse().opts(); - options.forEach((option) => { - const opt = camelcase(option.name); - option.isProvided = commander.program.getOptionValueSource(opt) === 'cli'; - }); + for (const [key, value] of Object.entries(opts)) { + if (key === 'wizard' || commander.program.getOptionValueSource(key) !== 'default') { + continue; + } - return commander.program.opts(); + if (opts.wizard) { + delete opts[key]; + } else { + const option = options.find((option) => camelcase(option.name) === key); + opts[key] = normalizeCommandLineArg(option.type, option.name, value); + } + } + + return opts; +} + +function normalizeCommandLineArg(type, name, value) { + const normalizer = normalizers[camelcase(type)]; + if (!normalizer) { + return value; + } + + try { + return normalizer(value); + } catch (ex) { + commander.program.error(`error: option '--${name} <${type}>' argument '${value}' is invalid. ${ex.toString()}`); + // throw new commander.InvalidArgumentError('potato'); + } } From 7cd5722195eab34199fff2d68cd51415542269ad Mon Sep 17 00:00:00 2001 From: Will Boyd Date: Mon, 27 Jan 2025 08:37:37 -0500 Subject: [PATCH 09/20] Throw Error instead of string --- src/writer.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/writer.js b/src/writer.js index 61d4fcf..7a642c9 100644 --- a/src/writer.js +++ b/src/writer.js @@ -22,7 +22,7 @@ async function processPayloadsPromise(payloads, loadFunc) { console.log(chalk.green('[OK]') + ' ' + payload.name); resolve(); } catch (ex) { - console.log(chalk.red('[FAILED]') + ' ' + payload.name + ' ' + chalk.red('(' + ex.toString() + ')')); + console.log(chalk.red('[FAILED]') + ' ' + payload.name + ' ' + chalk.red('(' + ex.message + ')')); reject(); } }, payload.delay); @@ -162,7 +162,7 @@ async function loadImageFilePromise(imageUrl) { } catch (ex) { if (ex.response) { // request was made, but server responded with an error status code - throw 'StatusCodeError: ' + ex.response.status; + throw new Error('StatusCodeError: ' + ex.response.status); } else { // something else went wrong, rethrow throw ex; From 19c65b21e485ac9b64d8a050f6a980f12171acc6 Mon Sep 17 00:00:00 2001 From: Will Boyd Date: Mon, 27 Jan 2025 08:42:08 -0500 Subject: [PATCH 10/20] Common normalize logic --- src/normalizers.js | 4 ++-- src/wizard.js | 52 +++++++++++++++++++++++----------------------- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/src/normalizers.js b/src/normalizers.js index 44f4121..5234646 100644 --- a/src/normalizers.js +++ b/src/normalizers.js @@ -10,7 +10,7 @@ export function boolean(value) { return false; } - throw 'Must be true or false.'; + throw new Error('Must be true or false.'); } export function filePath(value) { @@ -27,6 +27,6 @@ export function filePath(value) { if (fileExists) { return absolute; } else { - throw 'File not found at ' + absolute + '.'; + throw new Error('File not found at ' + absolute + '.'); } } diff --git a/src/wizard.js b/src/wizard.js index a58c211..24a9e26 100644 --- a/src/wizard.js +++ b/src/wizard.js @@ -4,6 +4,16 @@ import chalk from 'chalk'; import * as commander from 'commander'; import * as normalizers from './normalizers.js'; +const promptTheme = { + prefix: { + idle: chalk.gray('\n?'), + done: chalk.green('✓') + }, + style: { + description: (text) => chalk.gray('example: ' + text) + } +}; + // all user options for command line and wizard are declared here const options = [ { @@ -121,35 +131,21 @@ export async function getConfig(argv) { let normalizedAnswer = undefined; const promptConfig = { + theme: promptTheme, message: question.description + '?', default: question.default, - - theme: { - prefix: { - idle: chalk.gray('\n?'), - done: chalk.green('✓') - }, - style: { - description: (text) => chalk.gray('example: ' + text) - } - } }; if (question.choices) { promptConfig.choices = question.choices; promptConfig.loop = false; } else { - const normalizer = normalizers[camelcase(question.type)]; - if (normalizer) { - promptConfig.validate = (value) => { - try { - normalizedAnswer = normalizer(value); - } catch (ex) { - return ex.toString(); - } - - return true; - } + promptConfig.validate = (value) => { + let validationResult; + normalizedAnswer = normalize(value, question.type, (errorMessage) => { + validationResult = errorMessage; + }); + return validationResult ?? true; } } @@ -189,7 +185,9 @@ function parseCommandLine() { if (input.choices && input.type !== 'boolean') { option.choices(input.choices.map((choice) => choice.value)); } else { - option.argParser((value) => normalizeCommandLineArg(input.type, input.name, value)); + option.argParser((value) => normalize(value, input.type, (errorMessage) => { + throw new commander.InvalidArgumentError(errorMessage); + })); } commander.program.addOption(option); @@ -198,6 +196,7 @@ function parseCommandLine() { const opts = commander.program.parse().opts(); for (const [key, value] of Object.entries(opts)) { + console.log(key, value); if (key === 'wizard' || commander.program.getOptionValueSource(key) !== 'default') { continue; } @@ -206,14 +205,16 @@ function parseCommandLine() { delete opts[key]; } else { const option = options.find((option) => camelcase(option.name) === key); - opts[key] = normalizeCommandLineArg(option.type, option.name, value); + opts[key] = normalize(value, option.type, (errorMessage) => { + commander.program.error(`error: option '--${option.name} <${option.type}>' argument '${value}' is invalid. ${errorMessage}`); + }); } } return opts; } -function normalizeCommandLineArg(type, name, value) { +function normalize(value, type, onError) { const normalizer = normalizers[camelcase(type)]; if (!normalizer) { return value; @@ -222,7 +223,6 @@ function normalizeCommandLineArg(type, name, value) { try { return normalizer(value); } catch (ex) { - commander.program.error(`error: option '--${name} <${type}>' argument '${value}' is invalid. ${ex.toString()}`); - // throw new commander.InvalidArgumentError('potato'); + onError(ex.message); } } From a61e2be9e3220b67b765c9fe1eb4c2103e90a61e Mon Sep 17 00:00:00 2001 From: Will Boyd Date: Mon, 27 Jan 2025 09:30:28 -0500 Subject: [PATCH 11/20] Move options out, refactor getConfig() --- src/options.js | 104 ++++++++++++++++++++++++++ src/wizard.js | 196 +++++++++++++------------------------------------ 2 files changed, 154 insertions(+), 146 deletions(-) create mode 100644 src/options.js diff --git a/src/options.js b/src/options.js new file mode 100644 index 0000000..1eb50df --- /dev/null +++ b/src/options.js @@ -0,0 +1,104 @@ +import * as inquirer from '@inquirer/prompts'; + +export const all = [ + { + name: 'wizard', + type: 'boolean', + description: 'Use wizard', + default: true + }, + { + name: 'input', + type: 'file-path', + description: 'Path to WordPress export file', + default: 'export.xml', + prompt: inquirer.input + }, + { + name: 'post-folders', + type: 'boolean', + 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 with date', + default: false, + choices: [ + { + name: 'Yes', + value: true, + description: '' + }, + { + name: 'No', + value: false, + description: '' + } + ], + prompt: inquirer.select + }, + { + 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-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 + } +]; diff --git a/src/wizard.js b/src/wizard.js index 24a9e26..b996cf8 100644 --- a/src/wizard.js +++ b/src/wizard.js @@ -1,8 +1,8 @@ -import * as inquirer from '@inquirer/prompts'; import camelcase from 'camelcase'; import chalk from 'chalk'; import * as commander from 'commander'; import * as normalizers from './normalizers.js'; +import * as options from './options.js'; const promptTheme = { prefix: { @@ -14,161 +14,26 @@ const promptTheme = { } }; -// all user options for command line and wizard are declared here -const options = [ - { - name: 'wizard', - type: 'boolean', - description: 'Use wizard', - default: true - }, - { - name: 'input', - type: 'file-path', - description: 'Path to WordPress export file', - default: 'export.xml', - prompt: inquirer.input - }, - { - name: 'post-folders', - type: 'boolean', - 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 with date', - default: false, - choices: [ - { - name: 'Yes', - value: true, - description: '' - }, - { - name: 'No', - value: false, - description: '' - } - ], - prompt: inquirer.select - }, - { - 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-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 - } -]; +export async function getConfig() { + const config = {}; -export async function getConfig(argv) { - const opts = parseCommandLine(argv); + const commandLineOptions = options.all; + Object.assign(config, getCommandLineAnswers(commandLineOptions)); + console.log(1, config); - const answers = {}; - if (opts.wizard) { + if (config.wizard) { console.log('\nStarting wizard...'); - const questions = options - .filter((option) => option.name !== 'wizard') - .filter((option) => !opts[camelcase(option.name)]); - for (const question of questions) { - let normalizedAnswer = undefined; - - const promptConfig = { - theme: promptTheme, - message: question.description + '?', - default: question.default, - }; - - if (question.choices) { - promptConfig.choices = question.choices; - promptConfig.loop = false; - } else { - promptConfig.validate = (value) => { - let validationResult; - normalizedAnswer = normalize(value, question.type, (errorMessage) => { - validationResult = errorMessage; - }); - return validationResult ?? true; - } - } - - let answer = await question.prompt(promptConfig).catch((ex) => { - if (ex instanceof Error && ex.name === 'ExitPromptError') { - console.log('\nUser quit wizard early.'); - process.exit(0); - } else { - throw ex; - } - }); - - answers[camelcase(question.name)] = normalizedAnswer ?? answer; - } + const wizardOptions = options.all.filter((option) => option.name !== 'wizard' && !(camelcase(option.name) in config)); + Object.assign(config, await getWizardAnswers(wizardOptions)); } else { console.log('\nSkipping wizard...'); } - const config = { ...opts, ...answers }; + console.log(2, config); return config; } -function parseCommandLine() { +function getCommandLineAnswers(options) { commander.program .name('node index.js') .helpOption('-h, --help', 'See the thing you\'re looking at right now') @@ -214,6 +79,45 @@ function parseCommandLine() { return opts; } +export async function getWizardAnswers(options) { + const answers = {}; + for (const question of options) { + let normalizedAnswer = undefined; + + const promptConfig = { + theme: promptTheme, + message: question.description + '?', + default: question.default, + }; + + if (question.choices) { + promptConfig.choices = question.choices; + promptConfig.loop = false; + } else { + promptConfig.validate = (value) => { + let validationResult; + normalizedAnswer = normalize(value, question.type, (errorMessage) => { + validationResult = errorMessage; + }); + return validationResult ?? true; + } + } + + let answer = await question.prompt(promptConfig).catch((ex) => { + if (ex instanceof Error && ex.name === 'ExitPromptError') { + console.log('\nUser quit wizard early.'); + process.exit(0); + } else { + throw ex; + } + }); + + answers[camelcase(question.name)] = normalizedAnswer ?? answer; + } + + return answers; +} + function normalize(value, type, onError) { const normalizer = normalizers[camelcase(type)]; if (!normalizer) { From d68c3feaff4097e67530eb9ca7cc5e3f392a5a1f Mon Sep 17 00:00:00 2001 From: Will Boyd Date: Mon, 27 Jan 2025 09:33:42 -0500 Subject: [PATCH 12/20] Clean up console logs --- src/wizard.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/wizard.js b/src/wizard.js index b996cf8..7653b51 100644 --- a/src/wizard.js +++ b/src/wizard.js @@ -19,7 +19,6 @@ export async function getConfig() { const commandLineOptions = options.all; Object.assign(config, getCommandLineAnswers(commandLineOptions)); - console.log(1, config); if (config.wizard) { console.log('\nStarting wizard...'); @@ -29,7 +28,6 @@ export async function getConfig() { console.log('\nSkipping wizard...'); } - console.log(2, config); return config; } @@ -61,7 +59,6 @@ function getCommandLineAnswers(options) { const opts = commander.program.parse().opts(); for (const [key, value] of Object.entries(opts)) { - console.log(key, value); if (key === 'wizard' || commander.program.getOptionValueSource(key) !== 'default') { continue; } From 80eb589d4b162325484011b0136d31af713b2fd6 Mon Sep 17 00:00:00 2001 From: Will Boyd Date: Mon, 27 Jan 2025 12:59:21 -0500 Subject: [PATCH 13/20] Rename wizard/questions things --- index.js | 14 +++++++-- src/{options.js => questions.js} | 0 src/wizard.js | 51 +++++++++++++------------------- 3 files changed, 33 insertions(+), 32 deletions(-) rename src/{options.js => questions.js} (100%) diff --git a/index.js b/index.js index 9745d45..a6715ec 100644 --- a/index.js +++ b/index.js @@ -1,5 +1,7 @@ #!/usr/bin/env node +import chalk from 'chalk'; +import * as commander from 'commander'; import path from 'path'; import * as parser from './src/parser.js'; import * as settings from './src/settings.js'; @@ -7,13 +9,21 @@ import * as wizard from './src/wizard.js'; import * as writer from './src/writer.js'; (async () => { - // parse any command line arguments and run wizard + 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') + .configureOutput({ + outputError: (str, write) => write(chalk.red(str)) + }); + + // gather config options from command line and wizard const config = await wizard.getConfig(); // parse data from XML and do Markdown translations const posts = await parser.parseFilePromise(config) - // write files, downloading images as needed + // write files and download images await writer.writeFilesPromise(posts, config); // happy goodbye diff --git a/src/options.js b/src/questions.js similarity index 100% rename from src/options.js rename to src/questions.js diff --git a/src/wizard.js b/src/wizard.js index 7653b51..8835834 100644 --- a/src/wizard.js +++ b/src/wizard.js @@ -2,7 +2,7 @@ import camelcase from 'camelcase'; import chalk from 'chalk'; import * as commander from 'commander'; import * as normalizers from './normalizers.js'; -import * as options from './options.js'; +import * as questions from './questions.js'; const promptTheme = { prefix: { @@ -17,13 +17,13 @@ const promptTheme = { export async function getConfig() { const config = {}; - const commandLineOptions = options.all; - Object.assign(config, getCommandLineAnswers(commandLineOptions)); + const commandLineQuestions = questions.all; + Object.assign(config, getCommandLineAnswers(commandLineQuestions)); if (config.wizard) { console.log('\nStarting wizard...'); - const wizardOptions = options.all.filter((option) => option.name !== 'wizard' && !(camelcase(option.name) in config)); - Object.assign(config, await getWizardAnswers(wizardOptions)); + const wizardQuestions = questions.all.filter((question) => question.name !== 'wizard' && !(camelcase(question.name) in config)); + Object.assign(config, await getWizardAnswers(wizardQuestions)); } else { console.log('\nSkipping wizard...'); } @@ -31,24 +31,15 @@ export async function getConfig() { return config; } -function getCommandLineAnswers(options) { - 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') - .configureOutput({ - outputError: (str, write) => write(chalk.red(str)) - }); +function getCommandLineAnswers(questions) { + questions.forEach((question) => { + const option = new commander.Option('--' + question.name + ' <' + question.type + '>', question.description); + option.default(question.default); - - options.forEach((input) => { - const option = new commander.Option('--' + input.name + ' <' + input.type + '>', input.description); - option.default(input.default); - - if (input.choices && input.type !== 'boolean') { - option.choices(input.choices.map((choice) => choice.value)); + if (question.choices && question.type !== 'boolean') { + option.choices(question.choices.map((choice) => choice.value)); } else { - option.argParser((value) => normalize(value, input.type, (errorMessage) => { + option.argParser((value) => normalize(value, question.type, (errorMessage) => { throw new commander.InvalidArgumentError(errorMessage); })); } @@ -56,29 +47,29 @@ function getCommandLineAnswers(options) { commander.program.addOption(option); }); - const opts = commander.program.parse().opts(); + const answers = commander.program.parse().opts(); - for (const [key, value] of Object.entries(opts)) { + for (const [key, value] of Object.entries(answers)) { if (key === 'wizard' || commander.program.getOptionValueSource(key) !== 'default') { continue; } - if (opts.wizard) { - delete opts[key]; + if (answers.wizard) { + delete answers[key]; } else { - const option = options.find((option) => camelcase(option.name) === key); - opts[key] = normalize(value, option.type, (errorMessage) => { + const option = questions.find((option) => camelcase(option.name) === key); + answers[key] = normalize(value, option.type, (errorMessage) => { commander.program.error(`error: option '--${option.name} <${option.type}>' argument '${value}' is invalid. ${errorMessage}`); }); } } - return opts; + return answers; } -export async function getWizardAnswers(options) { +export async function getWizardAnswers(questions) { const answers = {}; - for (const question of options) { + for (const question of questions) { let normalizedAnswer = undefined; const promptConfig = { From fad9d974888dd7f789437f2b38dddb0defb5cddc Mon Sep 17 00:00:00 2001 From: Will Boyd Date: Mon, 27 Jan 2025 14:11:31 -0500 Subject: [PATCH 14/20] Some wizard comments and renamed variables --- index.js | 5 +---- src/wizard.js | 37 +++++++++++++++++++++++++++---------- 2 files changed, 28 insertions(+), 14 deletions(-) diff --git a/index.js b/index.js index a6715ec..2c04919 100644 --- a/index.js +++ b/index.js @@ -1,6 +1,5 @@ #!/usr/bin/env node -import chalk from 'chalk'; import * as commander from 'commander'; import path from 'path'; import * as parser from './src/parser.js'; @@ -9,13 +8,11 @@ import * as wizard from './src/wizard.js'; import * as writer from './src/writer.js'; (async () => { + // configure command line help output 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') - .configureOutput({ - outputError: (str, write) => write(chalk.red(str)) - }); // gather config options from command line and wizard const config = await wizard.getConfig(); diff --git a/src/wizard.js b/src/wizard.js index 8835834..3e7ce28 100644 --- a/src/wizard.js +++ b/src/wizard.js @@ -4,6 +4,7 @@ import * as commander from 'commander'; import * as normalizers from './normalizers.js'; import * as questions from './questions.js'; +// visual formatting for wizard const promptTheme = { prefix: { idle: chalk.gray('\n?'), @@ -17,12 +18,15 @@ const promptTheme = { export async function getConfig() { const config = {}; + // check command line for any config options const commandLineQuestions = questions.all; Object.assign(config, getCommandLineAnswers(commandLineQuestions)); if (config.wizard) { console.log('\nStarting wizard...'); - const wizardQuestions = questions.all.filter((question) => question.name !== 'wizard' && !(camelcase(question.name) in config)); + + // run wizard for remaining config options + const wizardQuestions = questions.all.filter((question) => !(camelcase(question.name) in config)); Object.assign(config, await getWizardAnswers(wizardQuestions)); } else { console.log('\nSkipping wizard...'); @@ -32,11 +36,17 @@ export async function getConfig() { } function getCommandLineAnswers(questions) { + // show errors in red + commander.program.configureOutput({ + outputError: (str, write) => write(chalk.red(str)) + }); + questions.forEach((question) => { const option = new commander.Option('--' + question.name + ' <' + question.type + '>', question.description); option.default(question.default); if (question.choices && question.type !== 'boolean') { + // let commander handle non-boolean multiple choice validation option.choices(question.choices.map((choice) => choice.value)); } else { option.argParser((value) => normalize(value, question.type, (errorMessage) => { @@ -49,17 +59,21 @@ function getCommandLineAnswers(questions) { const answers = commander.program.parse().opts(); + // do some post-processing on the answers for (const [key, value] of Object.entries(answers)) { + // the "wizard" answer and any user-provided (not defaulted) answers are left alone if (key === 'wizard' || commander.program.getOptionValueSource(key) !== 'default') { continue; } if (answers.wizard) { + // remove this default answer so the wizard will ask about it later delete answers[key]; } else { - const option = questions.find((option) => camelcase(option.name) === key); - answers[key] = normalize(value, option.type, (errorMessage) => { - commander.program.error(`error: option '--${option.name} <${option.type}>' argument '${value}' is invalid. ${errorMessage}`); + // normalize and validate default answer + const question = questions.find((question) => camelcase(question.name) === key); + answers[key] = normalize(value, question.type, (errorMessage) => { + commander.program.error(`error: option '--${question.name} <${question.type}>' argument '${value}' is invalid. ${errorMessage}`); }); } } @@ -70,7 +84,8 @@ function getCommandLineAnswers(questions) { export async function getWizardAnswers(questions) { const answers = {}; for (const question of questions) { - let normalizedAnswer = undefined; + // this will be set to the normalized answer during validation + let normalizedAnswer; const promptConfig = { theme: promptTheme, @@ -83,15 +98,17 @@ export async function getWizardAnswers(questions) { promptConfig.loop = false; } else { promptConfig.validate = (value) => { - let validationResult; + let validationErrorMessage; normalizedAnswer = normalize(value, question.type, (errorMessage) => { - validationResult = errorMessage; + validationErrorMessage = errorMessage; }); - return validationResult ?? true; + return validationErrorMessage ?? true; } } - let answer = await question.prompt(promptConfig).catch((ex) => { + // don't care about the return value of prompt() because normalizedAnswer will be used + await question.prompt(promptConfig).catch((ex) => { + // exit gracefully if user hits ctrl + c during wizard if (ex instanceof Error && ex.name === 'ExitPromptError') { console.log('\nUser quit wizard early.'); process.exit(0); @@ -100,7 +117,7 @@ export async function getWizardAnswers(questions) { } }); - answers[camelcase(question.name)] = normalizedAnswer ?? answer; + answers[camelcase(question.name)] = normalizedAnswer; } return answers; From 91073742faa81a14bc890777fe3dc048f7916c4b Mon Sep 17 00:00:00 2001 From: Will Boyd Date: Mon, 27 Jan 2025 14:57:04 -0500 Subject: [PATCH 15/20] Fix undefined answer bug, start on example path --- src/questions.js | 21 +++++++-------------- src/wizard.js | 31 +++++++++++++++++++++++++++---- 2 files changed, 34 insertions(+), 18 deletions(-) diff --git a/src/questions.js b/src/questions.js index 1eb50df..8c96b69 100644 --- a/src/questions.js +++ b/src/questions.js @@ -22,13 +22,11 @@ export const all = [ choices: [ { name: 'Yes', - value: true, - description: '/my-post/index.md' + value: true }, { name: 'No', - value: false, - description: '/my-post.md' + value: false } ], prompt: inquirer.select @@ -41,13 +39,11 @@ export const all = [ choices: [ { name: 'Yes', - value: true, - description: '' + value: true }, { name: 'No', - value: false, - description: '' + value: false } ], prompt: inquirer.select @@ -60,18 +56,15 @@ export const all = [ choices: [ { name: 'Year folders', - value: 'year', - description: '' + value: 'year' }, { name: 'Year and month folders', - value: 'year-month', - description: '' + value: 'year-month' }, { name: 'No', - value: 'none', - description: '' + value: 'none' } ], prompt: inquirer.select diff --git a/src/wizard.js b/src/wizard.js index 3e7ce28..e2654a0 100644 --- a/src/wizard.js +++ b/src/wizard.js @@ -84,7 +84,6 @@ function getCommandLineAnswers(questions) { export async function getWizardAnswers(questions) { const answers = {}; for (const question of questions) { - // this will be set to the normalized answer during validation let normalizedAnswer; const promptConfig = { @@ -106,8 +105,7 @@ export async function getWizardAnswers(questions) { } } - // don't care about the return value of prompt() because normalizedAnswer will be used - await question.prompt(promptConfig).catch((ex) => { + const answer = await question.prompt(promptConfig).catch((ex) => { // exit gracefully if user hits ctrl + c during wizard if (ex instanceof Error && ex.name === 'ExitPromptError') { console.log('\nUser quit wizard early.'); @@ -117,7 +115,7 @@ export async function getWizardAnswers(questions) { } }); - answers[camelcase(question.name)] = normalizedAnswer; + answers[camelcase(question.name)] = normalizedAnswer ?? answer; } return answers; @@ -135,3 +133,28 @@ function normalize(value, type, onError) { onError(ex.message); } } + +function buildSamplePath(config) { + const pathSegments = []; + + if (config.dateFolders === 'year' || config.dateFolders === 'year-month') { + pathSegments.push('2025'); + } + + if (config.dateFolders === 'year-month') { + pathSegments.push('01'); + } + + let slugFragment = 'my-post'; + if (config.prefixDate) { + slugFragment = '2025-01-31-' + slugFragment; + } + + if (config.postFolders) { + pathSegments.push(slugFragment, 'index.md'); + } else { + pathSegments.push(slugFragment + '.md'); + } + + return '/' + pathSegments.join('/'); +} From 8d4e7a03fc992c55c2698161cedd000103070f6c Mon Sep 17 00:00:00 2001 From: Will Boyd Date: Mon, 27 Jan 2025 17:12:17 -0500 Subject: [PATCH 16/20] Show example paths during wizard --- src/questions.js | 4 ++++ src/wizard.js | 31 +++++++++++++++++++++---------- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/src/questions.js b/src/questions.js index 8c96b69..86da5b1 100644 --- a/src/questions.js +++ b/src/questions.js @@ -29,6 +29,7 @@ export const all = [ value: false } ], + isPathQuestion: true, prompt: inquirer.select }, { @@ -46,6 +47,7 @@ export const all = [ value: false } ], + isPathQuestion: true, prompt: inquirer.select }, { @@ -67,6 +69,7 @@ export const all = [ value: 'none' } ], + isPathQuestion: true, prompt: inquirer.select }, { @@ -92,6 +95,7 @@ export const all = [ value: 'none' } ], + isPathQuestion: true, prompt: inquirer.select } ]; diff --git a/src/wizard.js b/src/wizard.js index e2654a0..de22e55 100644 --- a/src/wizard.js +++ b/src/wizard.js @@ -16,23 +16,22 @@ const promptTheme = { }; export async function getConfig() { - const config = {}; - // check command line for any config options const commandLineQuestions = questions.all; - Object.assign(config, getCommandLineAnswers(commandLineQuestions)); + const commandLineAnswers = getCommandLineAnswers(commandLineQuestions); - if (config.wizard) { + let wizardAnswers; + if (commandLineAnswers.wizard) { console.log('\nStarting wizard...'); // run wizard for remaining config options - const wizardQuestions = questions.all.filter((question) => !(camelcase(question.name) in config)); - Object.assign(config, await getWizardAnswers(wizardQuestions)); + const wizardQuestions = questions.all.filter((question) => !(camelcase(question.name) in commandLineAnswers)); + wizardAnswers = await getWizardAnswers(wizardQuestions, commandLineAnswers); } else { console.log('\nSkipping wizard...'); } - return config; + return { ...commandLineAnswers, ...wizardAnswers }; } function getCommandLineAnswers(questions) { @@ -81,9 +80,10 @@ function getCommandLineAnswers(questions) { return answers; } -export async function getWizardAnswers(questions) { +export async function getWizardAnswers(questions, commandLineAnswers) { const answers = {}; for (const question of questions) { + let answerKey = camelcase(question.name); let normalizedAnswer; const promptConfig = { @@ -95,6 +95,17 @@ export async function getWizardAnswers(questions) { if (question.choices) { promptConfig.choices = question.choices; promptConfig.loop = false; + + if (question.isPathQuestion) { + // create a snapshot config of command line answers and wizard answers so far + const config = { ...commandLineAnswers, ...answers }; + + promptConfig.choices.forEach((choice) => { + // show example path if this choice is selected + config[answerKey] = choice.value; + choice.description = buildExamplePath(config); + }); + } } else { promptConfig.validate = (value) => { let validationErrorMessage; @@ -115,7 +126,7 @@ export async function getWizardAnswers(questions) { } }); - answers[camelcase(question.name)] = normalizedAnswer ?? answer; + answers[answerKey] = normalizedAnswer ?? answer; } return answers; @@ -134,7 +145,7 @@ function normalize(value, type, onError) { } } -function buildSamplePath(config) { +function buildExamplePath(config) { const pathSegments = []; if (config.dateFolders === 'year' || config.dateFolders === 'year-month') { From 1877e43611b47697d65eef9d3d4ffe9a61527873 Mon Sep 17 00:00:00 2001 From: Will Boyd Date: Tue, 28 Jan 2025 08:14:15 -0500 Subject: [PATCH 17/20] buildSamplePostPath() --- src/questions.js | 1 - src/shared.js | 39 +++++++++++++++++++++++++++++++++++++++ src/wizard.js | 35 +++++++++++------------------------ 3 files changed, 50 insertions(+), 25 deletions(-) diff --git a/src/questions.js b/src/questions.js index 86da5b1..44f475d 100644 --- a/src/questions.js +++ b/src/questions.js @@ -95,7 +95,6 @@ export const all = [ value: 'none' } ], - isPathQuestion: true, prompt: inquirer.select } ]; diff --git a/src/shared.js b/src/shared.js index 0853bfe..6557462 100644 --- a/src/shared.js +++ b/src/shared.js @@ -1,3 +1,42 @@ +import * as luxon from 'luxon'; +import path from 'path'; +import * as settings from './settings.js'; + +export function buildPostPath(outputDir, type, date, slug, config) { + let dt; + if (settings.custom_date_formatting) { + dt = luxon.DateTime.fromFormat(date, settings.custom_date_formatting); + } else { + dt = luxon.DateTime.fromISO(date); + } + + // start with base output dir and post type + const pathSegments = [outputDir, type]; + + if (config.dateFolders === 'year' || config.dateFolders === 'year-month') { + pathSegments.push(dt.toFormat('yyyy')); + } + + if (config.dateFolders === 'year-month') { + pathSegments.push(dt.toFormat('LL')); + } + + // create slug fragment, possibly date prefixed + let slugFragment = slug; + if (config.prefixDate) { + slugFragment = dt.toFormat('yyyy-LL-dd') + '-' + slugFragment; + } + + // use slug fragment as folder or filename as specified + if (config.postFolders) { + pathSegments.push(slugFragment, 'index.md'); + } else { + pathSegments.push(slugFragment + '.md'); + } + + return path.join(...pathSegments); +} + export function getFilenameFromUrl(url) { let filename = url.split('/').slice(-1)[0]; try { diff --git a/src/wizard.js b/src/wizard.js index de22e55..e8ef5f1 100644 --- a/src/wizard.js +++ b/src/wizard.js @@ -1,8 +1,11 @@ import camelcase from 'camelcase'; import chalk from 'chalk'; import * as commander from 'commander'; +import * as luxon from 'luxon'; +import path from 'path'; import * as normalizers from './normalizers.js'; import * as questions from './questions.js'; +import * as shared from './shared.js'; // visual formatting for wizard const promptTheme = { @@ -84,7 +87,7 @@ export async function getWizardAnswers(questions, commandLineAnswers) { const answers = {}; for (const question of questions) { let answerKey = camelcase(question.name); - let normalizedAnswer; + let normalizedAnswer; // holds normalized answer value potentially returned during validation const promptConfig = { theme: promptTheme, @@ -103,7 +106,7 @@ export async function getWizardAnswers(questions, commandLineAnswers) { promptConfig.choices.forEach((choice) => { // show example path if this choice is selected config[answerKey] = choice.value; - choice.description = buildExamplePath(config); + choice.description = buildSamplePostPath(config); }); } } else { @@ -145,27 +148,11 @@ function normalize(value, type, onError) { } } -function buildExamplePath(config) { - const pathSegments = []; +export function buildSamplePostPath(config) { + const outputDir = path.sep; + const type = '' + const date = luxon.DateTime.now().toFormat('yyyy-LL-dd'); + const slug = 'my-post'; - if (config.dateFolders === 'year' || config.dateFolders === 'year-month') { - pathSegments.push('2025'); - } - - if (config.dateFolders === 'year-month') { - pathSegments.push('01'); - } - - let slugFragment = 'my-post'; - if (config.prefixDate) { - slugFragment = '2025-01-31-' + slugFragment; - } - - if (config.postFolders) { - pathSegments.push(slugFragment, 'index.md'); - } else { - pathSegments.push(slugFragment + '.md'); - } - - return '/' + pathSegments.join('/'); + return shared.buildPostPath(outputDir, type, date, slug, config); } From afc22e7ba5c0688337ae5da61fdc1cf4c8576b29 Mon Sep 17 00:00:00 2001 From: Will Boyd Date: Tue, 28 Jan 2025 16:38:29 -0500 Subject: [PATCH 18/20] Rename wizard.js to intake.js --- index.js | 4 ++-- src/{wizard.js => intake.js} | 0 2 files changed, 2 insertions(+), 2 deletions(-) rename src/{wizard.js => intake.js} (100%) diff --git a/index.js b/index.js index 2c04919..70349a6 100644 --- a/index.js +++ b/index.js @@ -4,7 +4,7 @@ import * as commander from 'commander'; import path from 'path'; import * as parser from './src/parser.js'; import * as settings from './src/settings.js'; -import * as wizard from './src/wizard.js'; +import * as intake from './src/intake.js'; import * as writer from './src/writer.js'; (async () => { @@ -15,7 +15,7 @@ import * as writer from './src/writer.js'; .addHelpText('after', '\nMore documentation is at https://github.com/lonekorean/wordpress-export-to-markdown') // gather config options from command line and wizard - const config = await wizard.getConfig(); + const config = await intake.getConfig(); // parse data from XML and do Markdown translations const posts = await parser.parseFilePromise(config) diff --git a/src/wizard.js b/src/intake.js similarity index 100% rename from src/wizard.js rename to src/intake.js From 896eaadb4fc55607a47180b4924314ed86627c40 Mon Sep 17 00:00:00 2001 From: Will Boyd Date: Tue, 28 Jan 2025 16:54:27 -0500 Subject: [PATCH 19/20] Have writer use shared.buildPostPath() --- src/intake.js | 1 + src/writer.js | 43 ++++++++----------------------------------- 2 files changed, 9 insertions(+), 35 deletions(-) diff --git a/src/intake.js b/src/intake.js index e8ef5f1..16c15c9 100644 --- a/src/intake.js +++ b/src/intake.js @@ -75,6 +75,7 @@ function getCommandLineAnswers(questions) { // normalize and validate default answer const question = questions.find((question) => camelcase(question.name) === key); answers[key] = normalize(value, question.type, (errorMessage) => { + // this is formatted to match how commander displays other errors commander.program.error(`error: option '--${question.name} <${question.type}>' argument '${value}' is invalid. ${errorMessage}`); }); } diff --git a/src/writer.js b/src/writer.js index 7a642c9..52af1de 100644 --- a/src/writer.js +++ b/src/writer.js @@ -3,7 +3,6 @@ import chalk from 'chalk'; import fs from 'fs'; import http from 'http'; import https from 'https'; -import * as luxon from 'luxon'; import path from 'path'; import * as settings from './settings.js'; import * as shared from './shared.js'; @@ -47,7 +46,7 @@ async function writeMarkdownFilesPromise(posts, config) { let skipCount = 0; let delay = 0; const payloads = posts.flatMap(post => { - const destinationPath = getPostPath(post, config); + const destinationPath = buildPostPath(post, config); if (checkFile(destinationPath)) { // already exists, don't need to save again skipCount++; @@ -105,7 +104,7 @@ async function writeImageFilesPromise(posts, config) { let skipCount = 0; let delay = 0; const payloads = posts.flatMap(post => { - const postPath = getPostPath(post, config); + const postPath = buildPostPath(post, config); const imagesDir = path.join(path.dirname(postPath), 'images'); return post.meta.imageUrls.flatMap(imageUrl => { const filename = shared.getFilenameFromUrl(imageUrl); @@ -171,39 +170,13 @@ async function loadImageFilePromise(imageUrl) { return buffer; } -function getPostPath(post, config) { - let dt; - if (settings.custom_date_formatting) { - dt = luxon.DateTime.fromFormat(post.frontmatter.date, settings.custom_date_formatting); - } else { - dt = luxon.DateTime.fromISO(post.frontmatter.date); - } +function buildPostPath(post, config) { + const outputDir = settings.output_directory; + const type = post.meta.type; + const date = post.frontmatter.date; + const slug = post.meta.slug; - // start with base output dir and post type - const pathSegments = [settings.output_directory, post.meta.type]; - - if (config.dateFolders === 'year' || config.dateFolders === 'year-month') { - pathSegments.push(dt.toFormat('yyyy')); - } - - if (config.dateFolders === 'year-month') { - pathSegments.push(dt.toFormat('LL')); - } - - // create slug fragment, possibly date prefixed - let slugFragment = post.meta.slug; - if (config.prefixDate) { - slugFragment = dt.toFormat('yyyy-LL-dd') + '-' + slugFragment; - } - - // use slug fragment as folder or filename as specified - if (config.postFolders) { - pathSegments.push(slugFragment, 'index.md'); - } else { - pathSegments.push(slugFragment + '.md'); - } - - return path.join(...pathSegments); + return shared.buildPostPath(outputDir, type, date, slug, config); } function checkFile(path) { From fbd36cf3a26153aff02364232e676c955084b575 Mon Sep 17 00:00:00 2001 From: Will Boyd Date: Tue, 28 Jan 2025 16:56:37 -0500 Subject: [PATCH 20/20] Semicolon kinda life --- src/intake.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/intake.js b/src/intake.js index 16c15c9..aedf47e 100644 --- a/src/intake.js +++ b/src/intake.js @@ -151,7 +151,7 @@ function normalize(value, type, onError) { export function buildSamplePostPath(config) { const outputDir = path.sep; - const type = '' + const type = ''; const date = luxon.DateTime.now().toFormat('yyyy-LL-dd'); const slug = 'my-post';