'use strict'; const { exec } = require('child_process'); const { access, mkdir, readdir, readFile, rmdir, writeFile } = require('fs/promises'); const { template } = require('dot'); const { ncp } = require('ncp'); const { join } = require('path'); const Marked = require('marked'); const { debuglog, promisify } = require('util'); const internals = { // Promisified functions ncp: promisify(ncp), exec: promisify(exec), debuglog: debuglog('blog'), // constants kAssetsDirectoryName: 'assets', kIndexName: 'index.html', kFileNotFoundError: 'ENOENT', kMarkdownRe: /\.md$/i, kRemoveCommand: 'rm -rf', // Strings strings: { markdownNotFound: 'Markdown file was not found in blog directory. Please update.' } }; /** * The Blog class is the blog generator, it's in charge of adding and * updating posts, and handling the publishing. * * @class Blog * @param {Potluck.tConfiguration} config the initialization options to * extend the instance */ module.exports = class Blog { constructor(config) { Object.assign(this, config); } /** * Shifts the blog posts, adds the passed path to slot 0, and * generates files. * * @function add * @memberof Blog * @param {string} postLocation the path to the directory containing * the post structure * @return {Promise} empty promise, returns no value * @instance */ async add(postLocation) { await this._shift(); await this.update(postLocation); } /** * Adds the passed path to slot 0, and generates files. * * @function update * @memberof Blog * @param {string} postLocation the path to the directory containing * the post structure * @return {Promise} empty promise, returns no value * @instance */ async update(postLocation) { await this._copyPost(postLocation); await this._generate(); } /** * Publishes the files to a static host. * * @function publish * @memberof Blog * @return {Promise} empty promise, returns no value * @instance */ publish() { console.error('Publishing not yet implemented'); return Promise.resolve(); } // Parses markdown for each page, copies assets and generates index. async _generate() { const assetsTarget = join(this.staticDirectory, internals.kAssetsDirectoryName); const indexTarget = join(this.staticDirectory, internals.kIndexName); const indexLocation = join(this.templatesDirectory, internals.kIndexName); const posts = []; internals.debuglog(`Removing ${assetsTarget}`); await rmdir(assetsTarget, { recursive: true }); for (let i = 0; i < this.maxPosts; ++i) { const sourcePath = join(this.postsDirectory, `${i}`); try { await access(this.postsDirectory); const assetsSource = join(sourcePath, internals.kAssetsDirectoryName); const postContentPath = await this._findBlogContent(sourcePath); internals.debuglog(`Copying ${assetsSource} to ${assetsTarget}`); await internals.ncp(assetsSource, assetsTarget); internals.debuglog(`Reading ${postContentPath}`); const postContent = await readFile(postContentPath, { encoding: 'utf8' }); internals.debuglog('Parsing markdown'); posts.push({ html: Marked(postContent), id: i + 1 }); } catch (error) { if (error.code === internals.kFileNotFoundError) { internals.debuglog(`Skipping ${i}`); continue; } throw error; } } internals.debuglog(`Reading ${indexLocation}`); const indexTemplate = await readFile(indexLocation, { encoding: 'utf8' }); internals.debuglog('Generating HTML'); const indexHtml = template(indexTemplate)({ posts }); await writeFile(indexTarget, indexHtml); } // Shift the posts, delete any remainder. async _shift() { await this._ensurePostsDirectoryExists(); for (let i = this.maxPosts - 1; i > 0; --i) { const targetPath = join(this.postsDirectory, `${i}`); const sourcePath = join(this.postsDirectory, `${i - 1}`); try { await access(sourcePath); internals.debuglog(`Removing ${targetPath}`); await rmdir(targetPath, { recursive: true }); internals.debuglog(`Shifting blog post ${sourcePath} to ${targetPath}`); await internals.ncp(sourcePath, targetPath); } catch (error) { if (error.code === internals.kFileNotFoundError) { internals.debuglog(`Skipping ${sourcePath}: Does not exist.`); continue; } throw error; } } } // Copies a post directory to the latest slot. async _copyPost(postLocation) { await this._ensurePostsDirectoryExists(); const targetPath = join(this.postsDirectory, '0'); internals.debuglog(`Removing ${targetPath}`); await rmdir(targetPath, { recursive: true }); internals.debuglog(`Adding ${postLocation} to ${targetPath}`); await internals.ncp(postLocation, targetPath); } // Ensures the posts directory exists. async _ensurePostsDirectoryExists() { internals.debuglog(`Checking if ${this.postsDirectory} exists.`); try { await access(this.postsDirectory); } catch (error) { if (error.code === internals.kFileNotFoundError) { internals.debuglog('Creating posts directory'); await mkdir(this.postsDirectory); return; } throw error; } } // Looks for a `.md` file in the blog directory, and returns the path async _findBlogContent(directory) { const entries = await readdir(directory); const markdownEntries = entries .filter((entry) => internals.kMarkdownRe.test(entry)) .map((entry) => join(directory, entry)); if (markdownEntries.length > 0) { internals.debuglog(`Found markdown file: ${markdownEntries[0]}`); return markdownEntries[0]; } throw new Error(internals.strings.markdownNotFound); } };