]> git.r.bdr.sh - rbdr/blog/commitdiff
Merge branch 'release/1.0.0'
authorBen Beltran <redacted>
Mon, 3 Jul 2017 05:55:32 +0000 (00:55 -0500)
committerBen Beltran <redacted>
Mon, 3 Jul 2017 05:55:32 +0000 (00:55 -0500)
21 files changed:
.eslintrc [new file with mode: 0644]
.gitignore
CHANGELOG.md [new file with mode: 0644]
CONTRIBUTING.md [new file with mode: 0644]
README.md
bin/blog.js [new file with mode: 0755]
config/config.js [new file with mode: 0644]
config/env.dist [new file with mode: 0644]
config/jsdoc.json [new file with mode: 0644]
example/test-blog-post/assets/example_image.png [new file with mode: 0644]
example/test-blog-post/assets/ok/ok.txt [new file with mode: 0644]
example/test-blog-post/test-blog-post.md [new file with mode: 0644]
lib/blog.js [new file with mode: 0644]
package.json [new file with mode: 0644]
static/css/style.css [new file with mode: 0644]
static/favicon.ico [new file with mode: 0644]
static/favicon.png [new file with mode: 0644]
static/images/header_background.png [new file with mode: 0644]
static/images/header_foreground.png [new file with mode: 0644]
static/js/blog.js [new file with mode: 0644]
templates/index.html [new file with mode: 0644]

diff --git a/.eslintrc b/.eslintrc
new file mode 100644 (file)
index 0000000..0a1dc80
--- /dev/null
+++ b/.eslintrc
@@ -0,0 +1,14 @@
+{
+  "extends": "eslint-config-hapi",
+  "parserOptions": {
+    "ecmaVersion": 2017
+  },
+  "rules": {
+    "indent": [
+      2,
+      2
+    ],
+    "no-undef": 2,
+    "require-yield": 0
+  }
+}
index 00cbbdf53f6c487c8392d936ce7225824c11446e..997ace3b331b9aaaf08c851a69a2b9d87afaef87 100644 (file)
@@ -57,3 +57,12 @@ typings/
 # dotenv environment variables file
 .env
 
+# Apple files
+.DS_Store
+
+# Data store
+.posts
+
+# Generated files
+static/assets
+static/index.html
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644 (file)
index 0000000..a468b24
--- /dev/null
@@ -0,0 +1,18 @@
+# Changelog
+All notable changes to this project will be documented in this file.
+
+The format is based on [Keep a Changelog](http://keepachangelog.com/)
+and this project adheres to [Semantic Versioning](http://semver.org/).
+
+## v1.0.0 - 2017-07-03
+### Added
+- JSDoc config
+- Eslint config
+- Binary to add and update blog posts
+- Template for index
+- Static Files
+- Simple contributing guidelines
+- This CHANGELOG
+- A Readme
+
+[Unreleased]: https://github.com/rbdr/blog/compare/master...develop
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644 (file)
index 0000000..fe0c941
--- /dev/null
@@ -0,0 +1,24 @@
+# Contributing to Blog
+
+This blog is a personal project, as such it may not be in the best
+condition for anyone to jump in or roll their own. However, if you find
+this useful and would like to send some improvements, please feel free
+to do so. I really appreciate any contribution!
+
+## The objective of blog
+
+The goal of blog is to have an ephemeral static blog that is generated from
+markdown files and their linked assets. It has a max number of posts at
+a time (the default is 3), and every time you publish it removes
+another.
+
+## How to contribute
+
+Above All: Be nice, always.
+
+* Ensure the linter shows no warnings or errors
+* Don't break the CI
+* Make the PRs according to [Git Flow][gitflow]: (features go to
+  develop, hotfixes go to master)
+
+[gitflow]: https://github.com/nvie/gitflow
index 2634854dd4ec037996a2c2b1ccf78d2b40e52431..47137302da81ce2f8dc010920da3dd8d56336b32 100644 (file)
--- a/README.md
+++ b/README.md
@@ -1,2 +1,48 @@
 # blog
-A temporary blog
+
+Blog at unlimited.pizza -> Only 
+
+## How to install
+
+`npm install -g .` will expose the `blog` binary to your CLI.
+
+## How to add a new entry
+
+Create a directory with a `.md` markdown file, and an `/assets`
+directory with anything you want in there. This can be in any directory.
+
+```
+.
+└── this-is-an-example-post
+    ├── assets
+    │   └── example.png
+    └── this-is-an-example-post.md
+```
+
+You can add this to the blog using the following command, it will shift
+all entries and remove the oldest one if limit of posts is reached
+(defualts to 3):
+
+`blog --add path/to/blog_post`
+
+These commands will regenerate the static files. At that point you can
+preview your blog by serving the files on the `static` directory. 
+
+If you need to make corrections use:
+
+`blog --update path/to/blog_post`
+
+This will replace the latest with the contents of the `path` without
+shifting the existing entries.
+
+`blog --publish`
+
+Will publish the blog.
+
+## How to publish
+
+At the moment, the app does not include any publishers. [surge][surge] is an easy
+way to do it, just point it to your static directory.
+
+
+[surge]: https://surge.sh
diff --git a/bin/blog.js b/bin/blog.js
new file mode 100755 (executable)
index 0000000..5d27d4a
--- /dev/null
@@ -0,0 +1,92 @@
+#!/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.publish(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();
diff --git a/config/config.js b/config/config.js
new file mode 100644 (file)
index 0000000..d16ff2d
--- /dev/null
@@ -0,0 +1,31 @@
+'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')))
+};
diff --git a/config/env.dist b/config/env.dist
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/config/jsdoc.json b/config/jsdoc.json
new file mode 100644 (file)
index 0000000..9e05753
--- /dev/null
@@ -0,0 +1,9 @@
+{
+  "plugins": ["plugins/markdown"],
+  "opts": {
+    "destination": "doc",
+    "readme": "README.md",
+    "template": "node_modules/docdash",
+    "recurse": true
+  }
+}
diff --git a/example/test-blog-post/assets/example_image.png b/example/test-blog-post/assets/example_image.png
new file mode 100644 (file)
index 0000000..672e3a1
Binary files /dev/null and b/example/test-blog-post/assets/example_image.png differ
diff --git a/example/test-blog-post/assets/ok/ok.txt b/example/test-blog-post/assets/ok/ok.txt
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/example/test-blog-post/test-blog-post.md b/example/test-blog-post/test-blog-post.md
new file mode 100644 (file)
index 0000000..88209ea
--- /dev/null
@@ -0,0 +1,57 @@
+# 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
diff --git a/lib/blog.js b/lib/blog.js
new file mode 100644 (file)
index 0000000..483577f
--- /dev/null
@@ -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);
+  }
+};
diff --git a/package.json b/package.json
new file mode 100644 (file)
index 0000000..e5303b1
--- /dev/null
@@ -0,0 +1,42 @@
+{
+  "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"
+  }
+}
diff --git a/static/css/style.css b/static/css/style.css
new file mode 100644 (file)
index 0000000..aeda780
--- /dev/null
@@ -0,0 +1,52 @@
+* {
+  margin: 0;
+  padding: 0;
+}
+
+body {
+  background-image: url('/images/header_background.png');
+  background-size: auto 300px;
+  background-attachment: fixed;
+  line-height: 1.45;
+}
+
+header {
+  background-image: url('/images/header_foreground.png');
+  background-repeat: no-repeat;
+  background-size: auto 300px;
+  height: 300px;
+}
+
+header a {
+  color: transparent;
+  display: block;
+  max-height: 500px;
+}
+
+main {
+  background-color: #fff;
+  padding: 1.414em;
+}
+
+h1, h2, h3, h4 {
+  margin: 1.414em 0 0.5em;
+  font-weight: 400;
+}
+
+p, ul, ol, img {
+  width: 100%;
+  margin: 1.414em 0;
+  max-width: 30em;
+}
+
+ul, ol { margin-left: 1.414em; }
+
+h1 { font-size: 3.998em; }
+h2 { font-size: 2.827em; }
+h3 { font-size: 1.999em; }
+h4 { font-size: 1.414em; }
+
+footer {
+  background-color: pink;
+  padding: 1.414em;
+}
diff --git a/static/favicon.ico b/static/favicon.ico
new file mode 100644 (file)
index 0000000..81582e2
Binary files /dev/null and b/static/favicon.ico differ
diff --git a/static/favicon.png b/static/favicon.png
new file mode 100644 (file)
index 0000000..b77a959
Binary files /dev/null and b/static/favicon.png differ
diff --git a/static/images/header_background.png b/static/images/header_background.png
new file mode 100644 (file)
index 0000000..8352b5e
Binary files /dev/null and b/static/images/header_background.png differ
diff --git a/static/images/header_foreground.png b/static/images/header_foreground.png
new file mode 100644 (file)
index 0000000..eb9b34d
Binary files /dev/null and b/static/images/header_foreground.png differ
diff --git a/static/js/blog.js b/static/js/blog.js
new file mode 100644 (file)
index 0000000..a49a72f
--- /dev/null
@@ -0,0 +1,28 @@
+'use strict';
+
+/* globals window */
+
+((window) => {
+
+  const internals = {
+    kEntryPoint: '.event-details',
+
+    // Application entry point, for now just band to keyboard
+
+    main() {
+
+      window.document.addEventListener('keydown', internals._onKeyDown);
+    },
+
+    // Handles key events to point to the correct blog post.
+
+    _onKeyDown(event) {
+
+      if (['1','2','3'].indexOf(event.key) > -1) {
+        window.location.hash = event.key;
+      }
+    }
+  };
+
+  window.addEventListener('load', internals.main);
+})(window);
diff --git a/templates/index.html b/templates/index.html
new file mode 100644 (file)
index 0000000..e741c68
--- /dev/null
@@ -0,0 +1,34 @@
+<!doctype html>
+<html>
+  <head>
+    <meta charset="utf-8">
+    <meta name="description" content="This is the blog at unlimited.pizza">
+
+    <title>blog 🍕</title>
+
+    <script src="/js/blog.js"></script>
+
+    <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>