From: Ben Beltran Date: Mon, 3 Jul 2017 05:55:32 +0000 (-0500) Subject: Merge branch 'release/1.0.0' X-Git-Tag: 1.0.1~6 X-Git-Url: https://git.r.bdr.sh/rbdr/blog/commitdiff_plain/5e265f9d81bcce30949e88850892ea5dacec7386?hp=4ae55e0673a61c50f1f7f378f6461d0388f66bd5 Merge branch 'release/1.0.0' --- diff --git a/.eslintrc b/.eslintrc new file mode 100644 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 + } +} diff --git a/.gitignore b/.gitignore index 00cbbdf..997ace3 100644 --- a/.gitignore +++ b/.gitignore @@ -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 index 0000000..a468b24 --- /dev/null +++ b/CHANGELOG.md @@ -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 index 0000000..fe0c941 --- /dev/null +++ b/CONTRIBUTING.md @@ -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 diff --git a/README.md b/README.md index 2634854..4713730 100644 --- 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 index 0000000..5d27d4a --- /dev/null +++ b/bin/blog.js @@ -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 index 0000000..d16ff2d --- /dev/null +++ b/config/config.js @@ -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=/.posts the location of + * the directory where the posts will be stored. + * @property {string} staticDirectory=/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=/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 index 0000000..e69de29 diff --git a/config/jsdoc.json b/config/jsdoc.json new file mode 100644 index 0000000..9e05753 --- /dev/null +++ b/config/jsdoc.json @@ -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 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 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 index 0000000..88209ea --- /dev/null +++ b/example/test-blog-post/test-blog-post.md @@ -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 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} 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); + } +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..e5303b1 --- /dev/null +++ b/package.json @@ -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 ", + "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 index 0000000..aeda780 --- /dev/null +++ b/static/css/style.css @@ -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 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 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 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 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 index 0000000..a49a72f --- /dev/null +++ b/static/js/blog.js @@ -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 index 0000000..e741c68 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,34 @@ + + + + + + + blog 🍕 + + + + + + + +
+ Blog +
+
+ {{#posts}} +
+ {{{html}}} +
+
+ {{/posts}} + {{^posts}} +

This is a fresh blog!

+

There are no posts yet.

+ {{/posts}} +
+ + +