Merge pull request #145 from lonekorean/config-refactor

Config refactor
This commit is contained in:
Will Boyd
2025-02-07 17:58:24 -05:00
committed by GitHub
7 changed files with 217 additions and 212 deletions
+6 -5
View File
@@ -2,8 +2,9 @@
import * as commander from 'commander'; import * as commander from 'commander';
import path from 'path'; import path from 'path';
import * as parser from './src/parser.js';
import * as intake from './src/intake.js'; import * as intake from './src/intake.js';
import * as parser from './src/parser.js';
import * as shared from './src/shared.js';
import * as writer from './src/writer.js'; import * as writer from './src/writer.js';
(async () => { (async () => {
@@ -14,17 +15,17 @@ import * as writer from './src/writer.js';
.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')
// gather config options from command line and wizard // gather config options from command line and wizard
const config = await intake.getConfig(); await intake.getConfig();
// parse data from XML and do Markdown translations // parse data from XML and do Markdown translations
const posts = await parser.parseFilePromise(config) const posts = await parser.parseFilePromise()
// write files and download images // write files and download images
await writer.writeFilesPromise(posts, config); await writer.writeFilesPromise(posts);
// happy goodbye // happy goodbye
console.log('\nAll done!'); 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(shared.config.output));
})().catch((ex) => { })().catch((ex) => {
// sad goodbye // sad goodbye
console.log('\nSomething went wrong, execution halted early.'); console.log('\nSomething went wrong, execution halted early.');
+11 -15
View File
@@ -19,7 +19,7 @@ const promptTheme = {
export async function getConfig() { export async function getConfig() {
// check command line for any config options // check command line for any config options
const commandLineQuestions = questions.all; const commandLineQuestions = questions.load();
const commandLineAnswers = getCommandLineAnswers(commandLineQuestions); const commandLineAnswers = getCommandLineAnswers(commandLineQuestions);
let wizardAnswers; let wizardAnswers;
@@ -27,7 +27,7 @@ export async function getConfig() {
console.log('\nStarting wizard...'); console.log('\nStarting wizard...');
// run wizard for questions with prompts that were not answered via the command line // run wizard for questions with prompts that were not answered via the command line
const wizardQuestions = questions.all.filter((question) => { const wizardQuestions = questions.load().filter((question) => {
return question.prompt && !(shared.camelCase(question.name) in commandLineAnswers); return question.prompt && !(shared.camelCase(question.name) in commandLineAnswers);
}); });
wizardAnswers = await getWizardAnswers(wizardQuestions, commandLineAnswers); wizardAnswers = await getWizardAnswers(wizardQuestions, commandLineAnswers);
@@ -35,7 +35,7 @@ export async function getConfig() {
console.log('\nSkipping wizard...'); console.log('\nSkipping wizard...');
} }
return { ...commandLineAnswers, ...wizardAnswers }; Object.assign(shared.config, commandLineAnswers, wizardAnswers);
} }
function getCommandLineAnswers(questions) { function getCommandLineAnswers(questions) {
@@ -106,13 +106,14 @@ export async function getWizardAnswers(questions, commandLineAnswers) {
promptConfig.loop = false; promptConfig.loop = false;
if (question.isPathQuestion) { if (question.isPathQuestion) {
// create a snapshot config of command line answers and wizard answers so far
const config = { ...commandLineAnswers, ...answers };
promptConfig.choices.forEach((choice) => { promptConfig.choices.forEach((choice) => {
// show example path if this choice is selected // show example path if this choice is selected
config[answerKey] = choice.value; choice.description = buildSamplePostPath({
choice.description = buildSamplePostPath(config); ...commandLineAnswers, // with command line answers
...answers, // and wizard answers so far
output: path.sep, // and a simplified output folder
[answerKey]: choice.value // and this choice selected
});
}); });
} }
} else { } else {
@@ -154,11 +155,6 @@ function normalize(value, type, onError) {
} }
} }
export function buildSamplePostPath(config) { export function buildSamplePostPath(overrideConfig) {
const outputDir = path.sep; return shared.buildPostPath('', luxon.DateTime.now(), 'my-post', overrideConfig);
const type = '';
const date = luxon.DateTime.now();
const slug = 'my-post';
return shared.buildPostPath(outputDir, type, date, slug, config);
} }
+14 -14
View File
@@ -5,9 +5,9 @@ import * as frontmatter from './frontmatter.js';
import * as shared from './shared.js'; import * as shared from './shared.js';
import * as translator from './translator.js'; import * as translator from './translator.js';
export async function parseFilePromise(config) { export async function parseFilePromise() {
console.log('\nParsing...'); console.log('\nParsing...');
const content = await fs.promises.readFile(config.input, 'utf8'); const content = await fs.promises.readFile(shared.config.input, 'utf8');
const allData = await xml2js.parseStringPromise(content, { const allData = await xml2js.parseStringPromise(content, {
trim: true, trim: true,
tagNameProcessors: [xml2js.processors.stripPrefix] tagNameProcessors: [xml2js.processors.stripPrefix]
@@ -15,18 +15,18 @@ export async function parseFilePromise(config) {
const channelData = allData.rss.channel[0].item; const channelData = allData.rss.channel[0].item;
const postTypes = getPostTypes(channelData); const postTypes = getPostTypes(channelData);
const posts = collectPosts(channelData, postTypes, config); const posts = collectPosts(channelData, postTypes);
const images = []; const images = [];
if (config.saveImages === 'attached' || config.saveImages === 'all') { if (shared.config.saveImages === 'attached' || shared.config.saveImages === 'all') {
images.push(...collectAttachedImages(channelData)); images.push(...collectAttachedImages(channelData));
} }
if (config.saveImages === 'scraped' || config.saveImages === 'all') { if (shared.config.saveImages === 'scraped' || shared.config.saveImages === 'all') {
images.push(...collectScrapedImages(channelData, postTypes)); images.push(...collectScrapedImages(channelData, postTypes));
} }
mergeImagesIntoPosts(images, posts); mergeImagesIntoPosts(images, posts);
populateFrontmatter(posts, config); populateFrontmatter(posts);
return posts; return posts;
} }
@@ -51,7 +51,7 @@ function getItemsOfType(channelData, type) {
return channelData.filter(item => item.post_type[0] === type); return channelData.filter(item => item.post_type[0] === type);
} }
function collectPosts(channelData, postTypes, config) { function collectPosts(channelData, postTypes) {
// this is passed into getPostContent() for the markdown conversion // this is passed into getPostContent() for the markdown conversion
const turndownService = translator.initTurndownService(); const turndownService = translator.initTurndownService();
@@ -60,7 +60,7 @@ function collectPosts(channelData, postTypes, config) {
const postsForType = getItemsOfType(channelData, postType) const postsForType = getItemsOfType(channelData, postType)
.filter(postData => postData.status[0] !== 'trash' && postData.status[0] !== 'draft') .filter(postData => postData.status[0] !== 'trash' && postData.status[0] !== 'draft')
.filter(postData => !(postType === 'page' && postData.post_name[0] === 'sample-page')) .filter(postData => !(postType === 'page' && postData.post_name[0] === 'sample-page'))
.map(postData => buildPost(postData, turndownService, config)); .map(postData => buildPost(postData, turndownService));
if (postsForType.length > 0) { if (postsForType.length > 0) {
console.log(`${postsForType.length} posts of type "${postType}" found.`); console.log(`${postsForType.length} posts of type "${postType}" found.`);
@@ -72,19 +72,19 @@ function collectPosts(channelData, postTypes, config) {
return allPosts; return allPosts;
} }
function buildPost(data, turndownService, config) { function buildPost(data, turndownService) {
return { return {
// full raw post data, used by some frontmatter getters // full raw post data, used by some frontmatter getters
data, data,
// contents of the post in markdown // contents of the post in markdown
content: translator.getPostContent(data, turndownService, config), content: translator.getPostContent(data, turndownService),
// these are not written to file, but help with other things // these are not written to file, but help with other things
type: data.post_type[0], type: data.post_type[0],
id: data.post_id[0], id: data.post_id[0],
slug: decodeURIComponent(data.post_name[0]), slug: decodeURIComponent(data.post_name[0]),
date: luxon.DateTime.fromRFC2822(data.pubDate[0], { zone: config.customDateTimezone }), date: luxon.DateTime.fromRFC2822(data.pubDate[0], { zone: shared.config.customDateTimezone }),
coverImageId: getPostMetaValue(data.postmeta, '_thumbnail_id'), coverImageId: getPostMetaValue(data.postmeta, '_thumbnail_id'),
// these are possibly set later in mergeImagesIntoPosts() // these are possibly set later in mergeImagesIntoPosts()
@@ -160,10 +160,10 @@ function mergeImagesIntoPosts(images, posts) {
}); });
} }
function populateFrontmatter(posts, config) { function populateFrontmatter(posts) {
posts.forEach(post => { posts.forEach(post => {
post.frontmatter = {}; post.frontmatter = {};
config.frontmatterFields.forEach(field => { shared.config.frontmatterFields.forEach(field => {
const [key, alias] = field.split(':'); const [key, alias] = field.split(':');
let frontmatterGetter = frontmatter[key]; let frontmatterGetter = frontmatter[key];
@@ -171,7 +171,7 @@ function populateFrontmatter(posts, config) {
throw `Could not find a frontmatter getter named "${key}".`; throw `Could not find a frontmatter getter named "${key}".`;
} }
post.frontmatter[alias || key] = frontmatterGetter(post, config); post.frontmatter[alias || key] = frontmatterGetter(post);
}); });
}); });
} }
+6 -4
View File
@@ -1,8 +1,9 @@
import * as inquirer from '@inquirer/prompts'; import * as inquirer from '@inquirer/prompts';
// questions with a description are displayed in command line help export function load() {
// questions with a prompt are included in the wizard (if not set on the command line) // questions with a description are displayed in command line help
export const all = [ // questions with a prompt are included in the wizard (if not set on the command line)
return [
{ {
name: 'wizard', name: 'wizard',
type: 'boolean', type: 'boolean',
@@ -145,4 +146,5 @@ export const all = [
type: 'boolean', type: 'boolean',
default: true default: true
} }
]; ];
}
+12 -7
View File
@@ -1,29 +1,34 @@
import path from 'path'; import path from 'path';
// simple data store, populated via intake, used everywhere
export const config = {};
export function camelCase(str) { export function camelCase(str) {
return str.replace(/-(.)/g, (match) => match[1].toUpperCase()); return str.replace(/-(.)/g, (match) => match[1].toUpperCase());
} }
export function buildPostPath(outputDir, type, date, slug, config) { export function buildPostPath(type, date, slug, overrideConfig) {
// start with base output dir and post type const pathConfig = overrideConfig ?? config;
const pathSegments = [outputDir, type];
if (config.dateFolders === 'year' || config.dateFolders === 'year-month') { // start with base output dir and post type
const pathSegments = [pathConfig.output, type];
if (pathConfig.dateFolders === 'year' || pathConfig.dateFolders === 'year-month') {
pathSegments.push(date.toFormat('yyyy')); pathSegments.push(date.toFormat('yyyy'));
} }
if (config.dateFolders === 'year-month') { if (pathConfig.dateFolders === 'year-month') {
pathSegments.push(date.toFormat('LL')); pathSegments.push(date.toFormat('LL'));
} }
// create slug fragment, possibly date prefixed // create slug fragment, possibly date prefixed
let slugFragment = slug; let slugFragment = slug;
if (config.prefixDate) { if (pathConfig.prefixDate) {
slugFragment = date.toFormat('yyyy-LL-dd') + '-' + slugFragment; slugFragment = date.toFormat('yyyy-LL-dd') + '-' + slugFragment;
} }
// use slug fragment as folder or filename as specified // use slug fragment as folder or filename as specified
if (config.postFolders) { if (pathConfig.postFolders) {
pathSegments.push(slugFragment, 'index.md'); pathSegments.push(slugFragment, 'index.md');
} else { } else {
pathSegments.push(slugFragment + '.md'); pathSegments.push(slugFragment + '.md');
+4 -3
View File
@@ -1,5 +1,6 @@
import turndown from 'turndown';
import turndownPluginGfm from '@guyplusplus/turndown-plugin-gfm'; import turndownPluginGfm from '@guyplusplus/turndown-plugin-gfm';
import turndown from 'turndown';
import * as shared from './shared.js';
export function initTurndownService() { export function initTurndownService() {
const turndownService = new turndown({ const turndownService = new turndown({
@@ -94,7 +95,7 @@ export function initTurndownService() {
return turndownService; return turndownService;
} }
export function getPostContent(postData, turndownService, config) { export function getPostContent(postData, turndownService) {
let content = postData.encoded[0]; let content = postData.encoded[0];
// insert an empty div element between double line breaks // insert an empty div element between double line breaks
@@ -102,7 +103,7 @@ export function getPostContent(postData, turndownService, config) {
// without mucking up content inside of other elements (like <code> blocks) // without mucking up content inside of other elements (like <code> blocks)
content = content.replace(/(\r?\n){2}/g, '\n<div></div>\n'); content = content.replace(/(\r?\n){2}/g, '\n<div></div>\n');
if (config.saveImages === 'scraped' || config.saveImages === 'all') { if (shared.config.saveImages === 'scraped' || shared.config.saveImages === 'all') {
// writeImageFile() will save all content images to a relative /images // writeImageFile() will save all content images to a relative /images
// folder so update references in post content to match // folder so update references in post content to match
content = content.replace(/(<img[^>]*src=").*?([^/"]+\.(?:gif|jpe?g|png|webp))("[^>]*>)/gi, '$1images/$2$3'); content = content.replace(/(<img[^>]*src=").*?([^/"]+\.(?:gif|jpe?g|png|webp))("[^>]*>)/gi, '$1images/$2$3');
+22 -22
View File
@@ -7,16 +7,16 @@ import * as luxon from 'luxon';
import path from 'path'; import path from 'path';
import * as shared from './shared.js'; import * as shared from './shared.js';
export async function writeFilesPromise(posts, config) { export async function writeFilesPromise(posts) {
await writeMarkdownFilesPromise(posts, config); await writeMarkdownFilesPromise(posts);
await writeImageFilesPromise(posts, config); await writeImageFilesPromise(posts);
} }
async function processPayloadsPromise(payloads, loadFunc, config) { async function processPayloadsPromise(payloads, loadFunc) {
const promises = payloads.map(payload => new Promise((resolve, reject) => { const promises = payloads.map(payload => new Promise((resolve, reject) => {
setTimeout(async () => { setTimeout(async () => {
try { try {
const data = await loadFunc(payload.item, config); const data = await loadFunc(payload.item);
await writeFile(payload.destinationPath, data); await writeFile(payload.destinationPath, data);
logPayloadResult(payload); logPayloadResult(payload);
resolve(); resolve();
@@ -41,12 +41,12 @@ async function writeFile(destinationPath, data) {
await fs.promises.writeFile(destinationPath, data); await fs.promises.writeFile(destinationPath, data);
} }
async function writeMarkdownFilesPromise(posts, config) { async function writeMarkdownFilesPromise(posts) {
// package up posts into payloads // package up posts into payloads
let skipCount = 0; let skipCount = 0;
let delay = 0; let delay = 0;
const payloads = posts.flatMap(post => { const payloads = posts.flatMap(post => {
const destinationPath = buildPostPath(post, config); const destinationPath = buildPostPath(post);
if (checkFile(destinationPath)) { if (checkFile(destinationPath)) {
// already exists, don't need to save again // already exists, don't need to save again
skipCount++; skipCount++;
@@ -59,7 +59,7 @@ async function writeMarkdownFilesPromise(posts, config) {
destinationPath, destinationPath,
delay delay
}; };
delay += config.markdownFileWriteDelay; delay += shared.config.markdownFileWriteDelay;
return [payload]; return [payload];
} }
}); });
@@ -69,11 +69,11 @@ async function writeMarkdownFilesPromise(posts, config) {
console.log('\nNo posts to save...'); console.log('\nNo posts to save...');
} else { } else {
console.log(`\nSaving ${remainingCount} posts (${skipCount} already exist)...`); console.log(`\nSaving ${remainingCount} posts (${skipCount} already exist)...`);
await processPayloadsPromise(payloads, loadMarkdownFilePromise, config); await processPayloadsPromise(payloads, loadMarkdownFilePromise);
} }
} }
async function loadMarkdownFilePromise(post, config) { async function loadMarkdownFilePromise(post) {
let output = '---\n'; let output = '---\n';
Object.entries(post.frontmatter).forEach(([key, value]) => { Object.entries(post.frontmatter).forEach(([key, value]) => {
@@ -84,13 +84,13 @@ async function loadMarkdownFilePromise(post, config) {
outputValue = value.reduce((list, item) => `${list}\n - "${item}"`, ''); outputValue = value.reduce((list, item) => `${list}\n - "${item}"`, '');
} }
} else if (value instanceof luxon.DateTime) { } else if (value instanceof luxon.DateTime) {
if (config.customDateFormatting) { if (shared.config.customDateFormatting) {
outputValue = value.toFormat(config.customDateFormatting); outputValue = value.toFormat(shared.config.customDateFormatting);
} else { } else {
outputValue = config.includeTimeWithDate ? value.toISO() : value.toISODate(); outputValue = shared.config.includeTimeWithDate ? value.toISO() : value.toISODate();
} }
if (config.quoteDate) { if (shared.config.quoteDate) {
outputValue = `"${outputValue}"`; outputValue = `"${outputValue}"`;
} }
} else { } else {
@@ -110,12 +110,12 @@ async function loadMarkdownFilePromise(post, config) {
return output; return output;
} }
async function writeImageFilesPromise(posts, config) { async function writeImageFilesPromise(posts) {
// collect image data from all posts into a single flattened array of payloads // collect image data from all posts into a single flattened array of payloads
let skipCount = 0; let skipCount = 0;
let delay = 0; let delay = 0;
const payloads = posts.flatMap(post => { const payloads = posts.flatMap(post => {
const postPath = buildPostPath(post, config); const postPath = buildPostPath(post);
const imagesDir = path.join(path.dirname(postPath), 'images'); const imagesDir = path.join(path.dirname(postPath), 'images');
return post.imageUrls.flatMap(imageUrl => { return post.imageUrls.flatMap(imageUrl => {
const filename = shared.getFilenameFromUrl(imageUrl); const filename = shared.getFilenameFromUrl(imageUrl);
@@ -132,7 +132,7 @@ async function writeImageFilesPromise(posts, config) {
destinationPath, destinationPath,
delay delay
}; };
delay += config.imageFileRequestDelay; delay += shared.config.imageFileRequestDelay;
return [payload]; return [payload];
} }
}); });
@@ -143,11 +143,11 @@ async function writeImageFilesPromise(posts, config) {
console.log('\nNo images to download and save...'); console.log('\nNo images to download and save...');
} else { } else {
console.log(`\nDownloading and saving ${remainingCount} images (${skipCount} already exist)...`); console.log(`\nDownloading and saving ${remainingCount} images (${skipCount} already exist)...`);
await processPayloadsPromise(payloads, loadImageFilePromise, config); await processPayloadsPromise(payloads, loadImageFilePromise);
} }
} }
async function loadImageFilePromise(imageUrl, config) { async function loadImageFilePromise(imageUrl) {
// only encode the URL if it doesn't already have encoded characters // only encode the URL if it doesn't already have encoded characters
const url = (/%[\da-f]{2}/i).test(imageUrl) ? imageUrl : encodeURI(imageUrl); const url = (/%[\da-f]{2}/i).test(imageUrl) ? imageUrl : encodeURI(imageUrl);
@@ -160,7 +160,7 @@ async function loadImageFilePromise(imageUrl, config) {
responseType: 'arraybuffer' responseType: 'arraybuffer'
}; };
if (!config.strictSsl) { if (!shared.config.strictSsl) {
// custom agents to disable SSL errors (adding both http and https, just in case) // custom agents to disable SSL errors (adding both http and https, just in case)
requestConfig.httpAgent = new http.Agent({ rejectUnauthorized: false }); requestConfig.httpAgent = new http.Agent({ rejectUnauthorized: false });
requestConfig.httpsAgent = new https.Agent({ rejectUnauthorized: false }); requestConfig.httpsAgent = new https.Agent({ rejectUnauthorized: false });
@@ -182,8 +182,8 @@ async function loadImageFilePromise(imageUrl, config) {
return buffer; return buffer;
} }
function buildPostPath(post, config) { function buildPostPath(post) {
return shared.buildPostPath(config.output, post.type, post.date, post.slug, config); return shared.buildPostPath(post.type, post.date, post.slug);
} }
function checkFile(path) { function checkFile(path) {