aboutsummaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/blog.js220
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);
+ }
+};