Merge pull request #143 from lonekorean/new-wizard

New wizard
This commit is contained in:
Will Boyd
2025-01-28 17:09:25 -05:00
committed by GitHub
11 changed files with 516 additions and 430 deletions
+14 -7
View File
@@ -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);
+137 -168
View File
@@ -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",
+1 -1
View File
@@ -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",
+159
View File
@@ -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);
}
+32
View File
@@ -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 + '.');
}
}
+9 -15
View File
@@ -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) {
+100
View File
@@ -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
}
];
+14
View File
@@ -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';
+39
View File
@@ -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 {
-196
View File
@@ -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);
}
+11 -43
View File
@@ -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) {