diff --git a/index.js b/index.js index cd286c6..70349a6 100644 --- a/index.js +++ b/index.js @@ -1,25 +1,32 @@ #!/usr/bin/env node +import * as commander from 'commander'; import path from 'path'; -import process from 'process'; import * as parser from './src/parser.js'; -import * as wizard from './src/wizard.js'; +import * as settings from './src/settings.js'; +import * as intake from './src/intake.js'; import * as writer from './src/writer.js'; (async () => { - // parse any command line arguments and run wizard - const config = await wizard.getConfig(process.argv); + // 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') + + // gather config options from command line and wizard + const config = await intake.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 console.log('\nAll done!'); - console.log('Look for your output files in: ' + path.resolve(config.output)); -})().catch(ex => { + 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.'); console.error(ex); 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/intake.js b/src/intake.js new file mode 100644 index 0000000..aedf47e --- /dev/null +++ b/src/intake.js @@ -0,0 +1,159 @@ +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 = { + prefix: { + idle: chalk.gray('\n?'), + done: chalk.green('✓') + }, + style: { + description: (text) => chalk.gray('example: ' + text) + } +}; + +export async function getConfig() { + // check command line for any config options + const commandLineQuestions = questions.all; + const commandLineAnswers = getCommandLineAnswers(commandLineQuestions); + + 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 commandLineAnswers)); + wizardAnswers = await getWizardAnswers(wizardQuestions, commandLineAnswers); + } else { + console.log('\nSkipping wizard...'); + } + + return { ...commandLineAnswers, ...wizardAnswers }; +} + +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) => { + throw new commander.InvalidArgumentError(errorMessage); + })); + } + + commander.program.addOption(option); + }); + + 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 { + // 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}`); + }); + } + } + + return answers; +} + +export async function getWizardAnswers(questions, commandLineAnswers) { + const answers = {}; + for (const question of questions) { + let answerKey = camelcase(question.name); + let normalizedAnswer; // holds normalized answer value potentially returned during validation + + const promptConfig = { + theme: promptTheme, + message: question.description + '?', + default: question.default, + }; + + 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 = buildSamplePostPath(config); + }); + } + } else { + promptConfig.validate = (value) => { + let validationErrorMessage; + normalizedAnswer = normalize(value, question.type, (errorMessage) => { + validationErrorMessage = errorMessage; + }); + return validationErrorMessage ?? true; + } + } + + 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.'); + process.exit(0); + } else { + throw ex; + } + }); + + answers[answerKey] = normalizedAnswer ?? answer; + } + + return answers; +} + +function normalize(value, type, onError) { + const normalizer = normalizers[camelcase(type)]; + if (!normalizer) { + return value; + } + + try { + return normalizer(value); + } catch (ex) { + onError(ex.message); + } +} + +export function buildSamplePostPath(config) { + const outputDir = path.sep; + const type = ''; + const date = luxon.DateTime.now().toFormat('yyyy-LL-dd'); + const slug = 'my-post'; + + return shared.buildPostPath(outputDir, type, date, slug, config); +} diff --git a/src/normalizers.js b/src/normalizers.js new file mode 100644 index 0000000..5234646 --- /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 new Error('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 new Error('File not found at ' + absolute + '.'); + } +} diff --git a/src/parser.js b/src/parser.js index 4083dd0..08775ad 100644 --- a/src/parser.js +++ b/src/parser.js @@ -14,14 +14,14 @@ 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 = []; - 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)); } @@ -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/questions.js b/src/questions.js new file mode 100644 index 0000000..44f475d --- /dev/null +++ b/src/questions.js @@ -0,0 +1,100 @@ +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 + }, + { + name: 'No', + value: false + } + ], + isPathQuestion: true, + prompt: inquirer.select + }, + { + name: 'prefix-date', + type: 'boolean', + description: 'Prefix with date', + default: false, + choices: [ + { + name: 'Yes', + value: true + }, + { + name: 'No', + value: false + } + ], + isPathQuestion: true, + prompt: inquirer.select + }, + { + name: 'date-folders', + type: 'choice', + description: 'Organize into folders based on date', + default: 'none', + choices: [ + { + name: 'Year folders', + value: 'year' + }, + { + name: 'Year and month folders', + value: 'year-month' + }, + { + name: 'No', + value: 'none' + } + ], + isPathQuestion: true, + 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/settings.js b/src/settings.js index 111ed0e..28ef891 100644 --- a/src/settings.js +++ b/src/settings.js @@ -38,3 +38,17 @@ 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' +]; + +// Output directory. +export const output_directory = 'output'; 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 deleted file mode 100644 index 2f95c16..0000000 --- a/src/wizard.js +++ /dev/null @@ -1,196 +0,0 @@ -import camelcase from 'camelcase'; -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 -const options = [ - // wizard must always be first - { - name: 'wizard', - type: 'boolean', - description: 'Use wizard', - default: true - }, - { - name: 'input', - type: 'file', - description: 'Path to WordPress export file', - default: 'export.xml' - }, - { - name: 'output', - type: 'folder', - description: 'Path to output folder', - default: 'output' - }, - { - 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 - }, - { - name: 'include-other-types', - type: 'boolean', - description: 'Include custom post types and pages', - default: false - } -]; - -export async function getConfig(argv) { - extendOptionsData(); - const unaliasedArgv = replaceAliases(argv); - const opts = parseCommandLine(unaliasedArgv); - - let 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); - } else { - console.log('\nSkipping wizard...'); - answers = {}; - } - - const config = { ...opts, ...answers }; - 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 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 - .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'); - - 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); - }); - - 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); -} diff --git a/src/writer.js b/src/writer.js index 510eaf3..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'; @@ -22,7 +21,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); @@ -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++; @@ -55,7 +54,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 }; @@ -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); @@ -162,7 +161,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; @@ -171,44 +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 - 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); - } - - if (config.yearFolders) { - pathSegments.push(dt.toFormat('yyyy')); - } - - if (config.monthFolders) { - 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) {