diff options
| author | Ben Beltran <ben@nsovocal.com> | 2017-07-03 00:26:08 -0500 |
|---|---|---|
| committer | Ben Beltran <ben@nsovocal.com> | 2017-07-03 00:26:08 -0500 |
| commit | cf6302904d61ead65e6294e7f1be406eb68ef5f9 (patch) | |
| tree | 2d034809bbf8db2db79e413ace9cab44a7044ae6 /lib | |
| parent | 7a5a585ead8c4e967980adc2ab9c7502e47694fd (diff) | |
✨📝🔧 Add generator bin and files
Includes config for eslint, and jsdoc, plus the executable
Squashed commit of the following:
commit 7726a2743994800f5c0afbc23e5b529107b79450
Author: Ben Beltran <ben@nsovocal.com>
Date: Mon Jul 3 00:25:43 2017 -0500
Update changelog
commit 78ad8ab788020c09848a852e48827781aa17341a
Author: Ben Beltran <ben@nsovocal.com>
Date: Mon Jul 3 00:25:38 2017 -0500
Remove unused image
commit 1cd659307a39669cf0fc8d0f676b25625fd16ba2
Author: Ben Beltran <ben@nsovocal.com>
Date: Mon Jul 3 00:23:53 2017 -0500
Remove trailing slash on index
commit 944160fcbc364e4a829be91e6ad32521b4f94987
Author: Ben Beltran <ben@nsovocal.com>
Date: Mon Jul 3 00:21:50 2017 -0500
Add example blog post
commit 70645d0214d5d363accfedc3e2583e97232c930e
Author: Ben Beltran <ben@nsovocal.com>
Date: Mon Jul 3 00:20:44 2017 -0500
✨ Add binary / generator to lib
commit bb8f8bb6e12c89bc4b9b3766fd2ef8e924422005
Author: Ben Beltran <ben@nsovocal.com>
Date: Mon Jul 3 00:20:29 2017 -0500
🔧 Add package.json
commit 7e9b6f52c1c91bf963319ddeb1696bfac4130b95
Author: Ben Beltran <ben@nsovocal.com>
Date: Mon Jul 3 00:19:57 2017 -0500
🔧 Add eslint config
commit 26ee06bbf1c2f20620c81fa94c4a9b40460bd401
Author: Ben Beltran <ben@nsovocal.com>
Date: Mon Jul 3 00:19:31 2017 -0500
Move static files to template / static
commit db84351cbb8e8c4729123650d1cb3a6150da0502
Author: Ben Beltran <ben@nsovocal.com>
Date: Sun Jul 2 21:39:20 2017 -0500
Use a hidden name for posts directory
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/blog.js | 220 |
1 files changed, 220 insertions, 0 deletions
diff --git a/lib/blog.js b/lib/blog.js new file mode 100644 index 0000000..483577f --- /dev/null +++ b/lib/blog.js @@ -0,0 +1,220 @@ +'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<undefined>} 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<undefined>} 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<undefined>} 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); + } +}; |