From: Ben Beltran Date: Mon, 3 Jul 2017 05:26:08 +0000 (-0500) Subject: ✨📝🔧 Add generator bin and files X-Git-Tag: 1.0.0~2 X-Git-Url: https://git.r.bdr.sh/rbdr/blog/commitdiff_plain/cf6302904d61ead65e6294e7f1be406eb68ef5f9?hp=7a5a585ead8c4e967980adc2ab9c7502e47694fd ✨📝🔧 Add generator bin and files Includes config for eslint, and jsdoc, plus the executable Squashed commit of the following: commit 7726a2743994800f5c0afbc23e5b529107b79450 Author: Ben Beltran Date: Mon Jul 3 00:25:43 2017 -0500 Update changelog commit 78ad8ab788020c09848a852e48827781aa17341a Author: Ben Beltran Date: Mon Jul 3 00:25:38 2017 -0500 Remove unused image commit 1cd659307a39669cf0fc8d0f676b25625fd16ba2 Author: Ben Beltran Date: Mon Jul 3 00:23:53 2017 -0500 Remove trailing slash on index commit 944160fcbc364e4a829be91e6ad32521b4f94987 Author: Ben Beltran Date: Mon Jul 3 00:21:50 2017 -0500 Add example blog post commit 70645d0214d5d363accfedc3e2583e97232c930e Author: Ben Beltran Date: Mon Jul 3 00:20:44 2017 -0500 ✨ Add binary / generator to lib commit bb8f8bb6e12c89bc4b9b3766fd2ef8e924422005 Author: Ben Beltran Date: Mon Jul 3 00:20:29 2017 -0500 🔧 Add package.json commit 7e9b6f52c1c91bf963319ddeb1696bfac4130b95 Author: Ben Beltran Date: Mon Jul 3 00:19:57 2017 -0500 🔧 Add eslint config commit 26ee06bbf1c2f20620c81fa94c4a9b40460bd401 Author: Ben Beltran Date: Mon Jul 3 00:19:31 2017 -0500 Move static files to template / static commit db84351cbb8e8c4729123650d1cb3a6150da0502 Author: Ben Beltran Date: Sun Jul 2 21:39:20 2017 -0500 Use a hidden name for posts directory --- 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 719089d..997ace3 100644 --- a/.gitignore +++ b/.gitignore @@ -61,7 +61,7 @@ typings/ .DS_Store # Data store -_posts +.posts # Generated files static/assets diff --git a/CHANGELOG.md b/CHANGELOG.md index a695952..728047b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] ### Added -- Static HTML / CSS +- Binary to add and update blog posts +- Template for index +- Static Files - Simple contributing guidelines - This CHANGELOG - A Readme diff --git a/bin/blog.js b/bin/blog.js new file mode 100755 index 0000000..c88a8aa --- /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.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(); 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/images/example_image.png b/example/test-blog-post/assets/example_image.png similarity index 100% rename from images/example_image.png rename to example/test-blog-post/assets/example_image.png 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/index.html b/index.html deleted file mode 100644 index 10d4919..0000000 --- a/index.html +++ /dev/null @@ -1,197 +0,0 @@ - - - - - - - blog 🍕 - - - - - -
- Blog -
-
-
-

This is the title of an 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 -

-

Other types of subheadings, other types of lists (h3)

-
    -
  1. There will be lists
  2. -
  3. Lists will have tags
  4. -
  5. And everything else in the world
  6. -
-

- 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 -

-
-
-
-

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 -

Other types of subheadings, other types of lists (h3)

-
    -
  1. There will be lists
  2. -
  3. Lists will have tags
  4. -
  5. And everything else in the world
  6. -
-

- 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 -

-
-
-
-

This is the title of the last 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 -

-

Other types of subheadings, other types of lists (h3)

-
    -
  1. There will be lists
  2. -
  3. Lists will have tags
  4. -
  5. And everything else in the world
  6. -
-

- 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 -

-
-
-
-

Only 3 entries kept at any time. Press 1, 2, and 3 to switch. unlimited.pizza

-
- - - 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/css/style.css b/static/css/style.css similarity index 100% rename from css/style.css rename to static/css/style.css diff --git a/images/header_background.png b/static/images/header_background.png similarity index 100% rename from images/header_background.png rename to static/images/header_background.png diff --git a/images/header_foreground.png b/static/images/header_foreground.png similarity index 100% rename from images/header_foreground.png rename to static/images/header_foreground.png diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..858396b --- /dev/null +++ b/templates/index.html @@ -0,0 +1,32 @@ + + + + + + + blog 🍕 + + + + + +
+ Blog +
+
+ {{#posts}} +
+ {{{html}}} +
+
+ {{/posts}} + {{^posts}} +

This is a fresh blog!

+

There are no posts yet.

+ {{/posts}} +
+ + +