--- /dev/null
+{
+ "extends": "eslint-config-hapi",
+ "parserOptions": {
+ "ecmaVersion": 2017
+ },
+ "rules": {
+ "indent": [
+ 2,
+ 2
+ ],
+ "no-undef": 2,
+ "require-yield": 0
+ }
+}
.DS_Store
# Data store
-_posts
+.posts
# Generated files
static/assets
## [Unreleased]
### Added
-- Static HTML / CSS
+- Binary to add and update blog posts
+- Template for index
+- Static Files
- Simple contributing guidelines
- This CHANGELOG
- A Readme
--- /dev/null
+#!/usr/bin/env node
+'use strict';
+
+const Config = require('../config/config');
+const Blog = require('..');
+const Minimist = require('minimist');
+
+const internals = {
+ blog: new Blog(Config),
+ expectedKeys: ['add', 'update', 'publish'],
+
+ // Application entry point. Reads arguments and calls the
+ // corresponding method from the blog lib
+
+ async main() {
+
+ try {
+ const parsedArguments = this._parseArguments();
+
+ for (const argument in parsedArguments) {
+ if (parsedArguments.hasOwnProperty(argument)) {
+
+ const value = parsedArguments[argument];
+
+ if (argument === 'add') {
+ await internals.blog.add(value);
+ return;
+ }
+
+ if (argument === 'update') {
+ await internals.blog.update(value);
+ return;
+ }
+
+ if (argument === 'publish') {
+ await internals.blog.update(value);
+ return;
+ }
+ }
+ }
+ console.log('Not yet implemented');
+ }
+ catch (err) {
+ console.error(err.message || err);
+ this._printUsage();
+ process.exit(1);
+ }
+ },
+
+ // Parses arguments and returns them if valid. otherwise Throws
+
+ _parseArguments() {
+
+ const parsedArguments = Minimist(process.argv.slice(2));
+
+ if (!this._areArgumentsValid(parsedArguments)) {
+ throw new Error(internals.strings.invalidArguments);
+ }
+
+ return parsedArguments;
+ },
+
+ // Checks if the arguments are valid, returns a boolean value.
+
+ _areArgumentsValid(parsedArguments) {
+
+ const argumentKeys = Object.keys(parsedArguments);
+
+ return argumentKeys.some((key) => internals.expectedKeys.indexOf(key) >= 0);
+ },
+
+ // Prints the usage to stderr
+
+ _printUsage() {
+
+ console.error('\nUsage:\n');
+ console.error('blog --add path/to/blog_post\t\t(creates new blog post)');
+ console.error('blog --update path/to/blog_post\t(updates latest blog post)');
+ console.error('blog --publish \t\t\t(publishes the blog)');
+ }
+};
+
+// Add the strings, added after declaration so they can consume the
+// internals object.
+
+internals.strings = {
+ invalidArguments: `Invalid Arguments, expecting one of: ${internals.expectedKeys.join(', ')}`
+};
+
+
+
+internals.main();
--- /dev/null
+'use strict';
+
+const Path = require('path');
+const Getenv = require('getenv');
+
+const internals = {};
+
+/**
+ * The main configuration object for Blog. It will be used to
+ * initialize all of the sub-components. It can extend any property of
+ * the blog object.
+ *
+ * @memberof Blog
+ * @typedef {object} tConfiguration
+ * @property {number} maxPosts=3 the max number of posts that can exist
+ * at one time
+ * @property {string} postsDirectory=<project_root>/.posts the location of
+ * the directory where the posts will be stored.
+ * @property {string} staticDirectory=<project_root>/static the location of
+ * the directory where the generated files will be placed. NOTE: There
+ * are some pre-built style files in the default directory, if you
+ * select another one, make sure you include them manually.
+ * @property {string} templatesDirectory=<project_root>/templates the
+ * location of the templates we'll use to generate the index.html
+ */
+module.exports = internals.Config = {
+ maxPosts: Getenv.int('BLOG_MAX_POSTS', 3),
+ postsDirectory: Getenv('BLOG_POSTS_DIRECTORY', Path.resolve(Path.join(__dirname, '../.posts'))),
+ staticDirectory: Getenv('BLOG_STATIC_DIRECTORY', Path.resolve(Path.join(__dirname, '../static'))),
+ templatesDirectory: Getenv('BLOG_TEMPLATES_DIRECTORY', Path.resolve(Path.join(__dirname, '../templates')))
+};
--- /dev/null
+{
+ "plugins": ["plugins/markdown"],
+ "opts": {
+ "destination": "doc",
+ "readme": "README.md",
+ "template": "node_modules/docdash",
+ "recurse": true
+ }
+}
--- /dev/null
+# This is the title of another entry
+
+An entry will have paragraphs, these paragraphs will contain text. The
+text should be formatted correctly: visitors will want to read whatever
+is in the blog, so it should be readable. It should account for several
+types of tags, like **strong**, or *emphasised*. It should even support
+[links](/)
+
+An entry will have paragraphs, these paragraphs will contain text. The
+text should be formatted correctly: visitors will want to read whatever
+is in the blog, so it should be readable. It should account for several
+types of tags, like **strong**, or *emphasised*. It
+should even support [links](/)
+
+An entry will have paragraphs, these paragraphs will contain text. The
+text should be formatted correctly: visitors will want to read whatever
+is in the blog, so it should be readable. It should account for several
+types of tags, like **strong**, or *emphasised*. It
+should even support [links](/)
+
+## Subheading 1 (h2)
+
+An entry will have paragraphs, these paragraphs will contain text. The
+text should be formatted correctly: visitors will want to read whatever
+is in the blog, so it should be readable. It should account for several
+types of tags, like **strong**, or *emphasised*. It
+should even support [links](/)
+
+* There will be **lists**
+* Lists will have *tags*
+* And everything else [in the world](/)
+
+An entry will have paragraphs, these paragraphs will contain text. The
+text should be formatted correctly: visitors will want to read whatever
+is in the blog, so it should be readable. It should account for several
+types of tags, like **strong**, or *emphasised*. It
+should even support [links](/)
+
+![Picture: two persons in a ceremony][example-image]
+
+### Other types of subheadings, other types of lists (h3)
+
+1. There will be **lists**
+2. Lists will have *tags*
+3. And everything else [in the world](/)
+
+An entry will have paragraphs, these paragraphs will contain text. The
+text should be formatted correctly: visitors will want to read whatever
+is in the blog, so it should be readable. It should account for several
+types of tags, like **strong**, or *emphasised*. It
+should even support [links](/)
+
+#### Finally there are hfours (h4)
+
+And that's about it!
+
+[example-image]: /assets/example_image.png
+++ /dev/null
-<!doctype html>
-<html>
- <head>
- <meta charset="utf-8">
- <meta name="description" content="This is the blog at unlimited.pizza">
-
- <title>blog 🍕</title>
-
- <link href="css/style.css" rel="stylesheet">
-
- </head>
- <body>
- <header class="main-header">
- <a href="/">Blog</a>
- </header>
- <main>
- <article id="1">
- <h1>This is the title of an entry</h1>
- <p>
- An entry will have paragraphs, these paragraphs will contain text. The
- text should be formatted correctly: visitors will want to read whatever
- is in the blog, so it should be readable. It should account for several
- types of tags, like <strong>strong</strong>, or <em>emphasised</em>. It
- should even support <a href="/">links</a>
- </p>
- <p>
- An entry will have paragraphs, these paragraphs will contain text. The
- text should be formatted correctly: visitors will want to read whatever
- is in the blog, so it should be readable. It should account for several
- types of tags, like <strong>strong</strong>, or <em>emphasised</em>. It
- should even support <a href="/">links</a>
- </p>
- <p>
- An entry will have paragraphs, these paragraphs will contain text. The
- text should be formatted correctly: visitors will want to read whatever
- is in the blog, so it should be readable. It should account for several
- types of tags, like <strong>strong</strong>, or <em>emphasised</em>. It
- should even support <a href="/">links</a>
- </p>
- <h2>Subheading 1 (h2)</h2>
- <p>
- An entry will have paragraphs, these paragraphs will contain text. The
- text should be formatted correctly: visitors will want to read whatever
- is in the blog, so it should be readable. It should account for several
- types of tags, like <strong>strong</strong>, or <em>emphasised</em>. It
- should even support <a href="/">links</a>
- </p>
- <ul>
- <li>There will be <strong>lists</strong></li>
- <li>Lists will have <em>tags</em></li>
- <li>And everything else <a href="/">in the world</a></li>
- </ul>
- <p>
- An entry will have paragraphs, these paragraphs will contain text. The
- text should be formatted correctly: visitors will want to read whatever
- is in the blog, so it should be readable. It should account for several
- types of tags, like <strong>strong</strong>, or <em>emphasised</em>. It
- should even support <a href="/">links</a>
- </p>
- <h3>Other types of subheadings, other types of lists (h3)</h3>
- <ol>
- <li>There will be <strong>lists</strong></li>
- <li>Lists will have <em>tags</em></li>
- <li>And everything else <a href="/">in the world</a></li>
- </ol>
- <p>
- An entry will have paragraphs, these paragraphs will contain text. The
- text should be formatted correctly: visitors will want to read whatever
- is in the blog, so it should be readable. It should account for several
- types of tags, like <strong>strong</strong>, or <em>emphasised</em>. It
- should even support <a href="/">links</a>
- </p>
- </article>
- <hr>
- <article id="2">
- <h1>This is the title of another entry</h1>
- <p>
- An entry will have paragraphs, these paragraphs will contain text. The
- text should be formatted correctly: visitors will want to read whatever
- is in the blog, so it should be readable. It should account for several
- types of tags, like <strong>strong</strong>, or <em>emphasised</em>. It
- should even support <a href="/">links</a>
- </p>
- <p>
- An entry will have paragraphs, these paragraphs will contain text. The
- text should be formatted correctly: visitors will want to read whatever
- is in the blog, so it should be readable. It should account for several
- types of tags, like <strong>strong</strong>, or <em>emphasised</em>. It
- should even support <a href="/">links</a>
- </p>
- <p>
- An entry will have paragraphs, these paragraphs will contain text. The
- text should be formatted correctly: visitors will want to read whatever
- is in the blog, so it should be readable. It should account for several
- types of tags, like <strong>strong</strong>, or <em>emphasised</em>. It
- should even support <a href="/">links</a>
- </p>
- <h2>Subheading 1 (h2)</h2>
- <p>
- An entry will have paragraphs, these paragraphs will contain text. The
- text should be formatted correctly: visitors will want to read whatever
- is in the blog, so it should be readable. It should account for several
- types of tags, like <strong>strong</strong>, or <em>emphasised</em>. It
- should even support <a href="/">links</a>
- </p>
- <ul>
- <li>There will be <strong>lists</strong></li>
- <li>Lists will have <em>tags</em></li>
- <li>And everything else <a href="/">in the world</a></li>
- </ul>
- <p>
- An entry will have paragraphs, these paragraphs will contain text. The
- text should be formatted correctly: visitors will want to read whatever
- is in the blog, so it should be readable. It should account for several
- types of tags, like <strong>strong</strong>, or <em>emphasised</em>. It
- should even support <a href="/">links</a>
- </p>
- <img src="/images/example_image.png" alt="Picture: two persons in a ceremony">
- <h3>Other types of subheadings, other types of lists (h3)</h3>
- <ol>
- <li>There will be <strong>lists</strong></li>
- <li>Lists will have <em>tags</em></li>
- <li>And everything else <a href="/">in the world</a></li>
- </ol>
- <p>
- An entry will have paragraphs, these paragraphs will contain text. The
- text should be formatted correctly: visitors will want to read whatever
- is in the blog, so it should be readable. It should account for several
- types of tags, like <strong>strong</strong>, or <em>emphasised</em>. It
- should even support <a href="/">links</a>
- </p>
- </article>
- <hr>
- <article id="3">
- <h1>This is the title of the last entry</h1>
- <p>
- An entry will have paragraphs, these paragraphs will contain text. The
- text should be formatted correctly: visitors will want to read whatever
- is in the blog, so it should be readable. It should account for several
- types of tags, like <strong>strong</strong>, or <em>emphasised</em>. It
- should even support <a href="/">links</a>
- </p>
- <p>
- An entry will have paragraphs, these paragraphs will contain text. The
- text should be formatted correctly: visitors will want to read whatever
- is in the blog, so it should be readable. It should account for several
- types of tags, like <strong>strong</strong>, or <em>emphasised</em>. It
- should even support <a href="/">links</a>
- </p>
- <p>
- An entry will have paragraphs, these paragraphs will contain text. The
- text should be formatted correctly: visitors will want to read whatever
- is in the blog, so it should be readable. It should account for several
- types of tags, like <strong>strong</strong>, or <em>emphasised</em>. It
- should even support <a href="/">links</a>
- </p>
- <h2>Subheading 1 (h2)</h2>
- <p>
- An entry will have paragraphs, these paragraphs will contain text. The
- text should be formatted correctly: visitors will want to read whatever
- is in the blog, so it should be readable. It should account for several
- types of tags, like <strong>strong</strong>, or <em>emphasised</em>. It
- should even support <a href="/">links</a>
- </p>
- <ul>
- <li>There will be <strong>lists</strong></li>
- <li>Lists will have <em>tags</em></li>
- <li>And everything else <a href="/">in the world</a></li>
- </ul>
- <p>
- An entry will have paragraphs, these paragraphs will contain text. The
- text should be formatted correctly: visitors will want to read whatever
- is in the blog, so it should be readable. It should account for several
- types of tags, like <strong>strong</strong>, or <em>emphasised</em>. It
- should even support <a href="/">links</a>
- </p>
- <h3>Other types of subheadings, other types of lists (h3)</h3>
- <ol>
- <li>There will be <strong>lists</strong></li>
- <li>Lists will have <em>tags</em></li>
- <li>And everything else <a href="/">in the world</a></li>
- </ol>
- <p>
- An entry will have paragraphs, these paragraphs will contain text. The
- text should be formatted correctly: visitors will want to read whatever
- is in the blog, so it should be readable. It should account for several
- types of tags, like <strong>strong</strong>, or <em>emphasised</em>. It
- should even support <a href="/">links</a>
- </p>
- </article>
- </main>
- <footer>
- <p>Only 3 entries kept at any time. Press 1, 2, and 3 to switch. <a href="https://unlimited.pizza">unlimited.pizza</a></p>
- </footer>
- </body>
-</html>
-
--- /dev/null
+'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);
+ }
+};
--- /dev/null
+{
+ "name": "blog",
+ "version": "1.0.0",
+ "description": "An ephemeral blog",
+ "main": "lib/blog.js",
+ "bin": {
+ "blog": "./bin/blog.js"
+ },
+ "scripts": {
+ "document": "jsdoc -c ./config/jsdoc.json lib config",
+ "lint": "eslint .",
+ "test": "echo \":(\""
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/rbdr/blog.git"
+ },
+ "author": "Ben Beltran <ben@nsovocal.com>",
+ "license": "Apache-2.0",
+ "bugs": {
+ "url": "https://github.com/rbdr/blog/issues"
+ },
+ "homepage": "https://github.com/rbdr/blog#readme",
+ "dependencies": {
+ "getenv": "0.7.x",
+ "markdown": "0.5.x",
+ "minimist": "1.2.x",
+ "mustache": "2.3.x",
+ "ncp": "2.0.x",
+ "rimraf": "2.6.x"
+ },
+ "devDependencies": {
+ "docdash": "0.4.x",
+ "eslint": "4.1.x",
+ "eslint-config-hapi": "10.0.x",
+ "eslint-plugin-hapi": "4.0.x",
+ "jsdoc": "3.4.x"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+}
--- /dev/null
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <meta name="description" content="This is the blog at unlimited.pizza">
+
+ <title>blog 🍕</title>
+
+ <link href="css/style.css" rel="stylesheet">
+
+ </head>
+ <body>
+ <header class="main-header">
+ <a href="/">Blog</a>
+ </header>
+ <main>
+ {{#posts}}
+ <article id="{{id}}">
+ {{{html}}}
+ </article>
+ <hr>
+ {{/posts}}
+ {{^posts}}
+ <h1>This is a fresh blog!</h1>
+ <p>There are no posts yet.</p>
+ {{/posts}}
+ </main>
+ <footer>
+ <p>Only 3 entries kept at any time. Press 1, 2, and 3 to switch. <a href="https://unlimited.pizza">unlimited.pizza</a></p>
+ </footer>
+ </body>
+</html>