'use strict'; const Fs = require('fs'); const Markdown = require('markdown'); const Mustache = require('mustache'); const Ncp = require('ncp'); const Path = require('path'); const Rimraf = require('rimraf'); const Util = require('util'); const internals = { // Promisified functions fs: { access: Util.promisify(Fs.access), mkdir: Util.promisify(Fs.mkdir), readdir: Util.promisify(Fs.readdir), readFile: Util.promisify(Fs.readFile), writeFile: Util.promisify(Fs.writeFile) }, ncp: Util.promisify(Ncp.ncp), rimraf: Util.promisify(Rimraf), debuglog: Util.debuglog('blog'), // constants kAssetsDirectoryName: 'assets', kIndexName: 'index.html', kFileNotFoundError: 'ENOENT', kMarkdownRe: /\.md$/i, // 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 */ async publish() { console.error('Publishing not yet implemented'); } // Parses markdown for each page, copies assets and generates index. async _generate() { const assetsTarget = Path.join(this.staticDirectory, internals.kAssetsDirectoryName); const indexTarget = Path.join(this.staticDirectory, internals.kIndexName); const indexLocation = Path.join(this.templatesDirectory, internals.kIndexName); const posts = []; internals.debuglog(`Removing ${assetsTarget}`); await internals.rimraf(assetsTarget); for (let i = 0; i < this.maxPosts; ++i) { const sourcePath = Path.join(this.postsDirectory, `${i}`); const assetsSource = Path.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 internals.fs.readFile(postContentPath, { encoding: 'utf8' }); internals.debuglog('Parsing markdown'); posts.push({ html: Markdown.markdown.toHTML(postContent), id: i + 1 }); } internals.debuglog(`Reading ${indexLocation}`); const indexTemplate = await internals.fs.readFile(indexLocation, { encoding: 'utf8' }); internals.debuglog('Generating HTML'); const indexHtml = Mustache.render(indexTemplate, { posts }); await internals.fs.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 = Path.join(this.postsDirectory, `${i}`); const sourcePath = Path.join(this.postsDirectory, `${i - 1}`); try { await internals.fs.access(sourcePath); internals.debuglog(`Removing ${targetPath}`); await internals.rimraf(targetPath); 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 = Path.join(this.postsDirectory, '0'); internals.debuglog(`Removing ${targetPath}`); await internals.rimraf(targetPath); 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 internals.fs.access(this.postsDirectory); } catch (error) { if (error.code === internals.kFileNotFoundError) { internals.debuglog('Creating posts directory'); await internals.fs.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 internals.fs.readdir(directory); const markdownEntries = entries .filter((entry) => internals.kMarkdownRe.test(entry)) .map((entry) => Path.join(directory, entry)); if (markdownEntries.length > 0) { internals.debuglog(`Found markdown file: ${markdownEntries[0]}`); return markdownEntries[0]; } throw new Error(internals.strings.markdownNotFound); } };