]> git.r.bdr.sh - rbdr/blog/blob - lib/blog.js
483577ff3f147b30b02d818970d6ceb38e7ef514
[rbdr/blog] / lib / blog.js
1 'use strict';
2
3 const Fs = require('fs');
4 const Markdown = require('markdown');
5 const Mustache = require('mustache');
6 const Ncp = require('ncp');
7 const Path = require('path');
8 const Rimraf = require('rimraf');
9 const Util = require('util');
10
11 const internals = {
12
13 // Promisified functions
14
15 fs: {
16 access: Util.promisify(Fs.access),
17 mkdir: Util.promisify(Fs.mkdir),
18 readdir: Util.promisify(Fs.readdir),
19 readFile: Util.promisify(Fs.readFile),
20 writeFile: Util.promisify(Fs.writeFile)
21 },
22 ncp: Util.promisify(Ncp.ncp),
23 rimraf: Util.promisify(Rimraf),
24 debuglog: Util.debuglog('blog'),
25
26 // constants
27
28 kAssetsDirectoryName: 'assets',
29 kIndexName: 'index.html',
30 kFileNotFoundError: 'ENOENT',
31 kMarkdownRe: /\.md$/i,
32
33 // Strings
34
35 strings: {
36 markdownNotFound: 'Markdown file was not found in blog directory. Please update.'
37 }
38 };
39
40 /**
41 * The Blog class is the blog generator, it's in charge of adding and
42 * updating posts, and handling the publishing.
43 *
44 * @class Blog
45 * @param {Potluck.tConfiguration} config the initialization options to
46 * extend the instance
47 */
48 module.exports = class Blog {
49
50 constructor(config) {
51
52 Object.assign(this, config);
53 }
54
55 /**
56 * Shifts the blog posts, adds the passed path to slot 0, and
57 * generates files.
58 *
59 * @function add
60 * @memberof Blog
61 * @param {string} postLocation the path to the directory containing
62 * the post structure
63 * @return {Promise<undefined>} empty promise, returns no value
64 * @instance
65 */
66 async add(postLocation) {
67
68 await this._shift();
69 await this.update(postLocation);
70 }
71
72 /**
73 * Adds the passed path to slot 0, and generates files.
74 *
75 * @function update
76 * @memberof Blog
77 * @param {string} postLocation the path to the directory containing
78 * the post structure
79 * @return {Promise<undefined>} empty promise, returns no value
80 * @instance
81 */
82 async update(postLocation) {
83
84 await this._copyPost(postLocation);
85 await this._generate();
86 }
87
88 /**
89 * Publishes the files to a static host.
90 *
91 * @function publish
92 * @memberof Blog
93 * @return {Promise<undefined>} empty promise, returns no value
94 * @instance
95 */
96 async publish() {
97
98 console.error('Publishing not yet implemented');
99 }
100
101 // Parses markdown for each page, copies assets and generates index.
102
103 async _generate() {
104
105 const assetsTarget = Path.join(this.staticDirectory, internals.kAssetsDirectoryName);
106 const indexTarget = Path.join(this.staticDirectory, internals.kIndexName);
107 const indexLocation = Path.join(this.templatesDirectory, internals.kIndexName);
108 const posts = [];
109
110 internals.debuglog(`Removing ${assetsTarget}`);
111 await internals.rimraf(assetsTarget);
112
113 for (let i = 0; i < this.maxPosts; ++i) {
114 const sourcePath = Path.join(this.postsDirectory, `${i}`);
115 const assetsSource = Path.join(sourcePath, internals.kAssetsDirectoryName);
116 const postContentPath = await this._findBlogContent(sourcePath);
117
118 internals.debuglog(`Copying ${assetsSource} to ${assetsTarget}`);
119 await internals.ncp(assetsSource, assetsTarget);
120
121 internals.debuglog(`Reading ${postContentPath}`);
122 const postContent = await internals.fs.readFile(postContentPath, { encoding: 'utf8' });
123
124 internals.debuglog('Parsing markdown');
125 posts.push({
126 html: Markdown.markdown.toHTML(postContent),
127 id: i + 1
128 });
129 }
130
131 internals.debuglog(`Reading ${indexLocation}`);
132 const indexTemplate = await internals.fs.readFile(indexLocation, { encoding: 'utf8' });
133
134 internals.debuglog('Generating HTML');
135 const indexHtml = Mustache.render(indexTemplate, { posts });
136 await internals.fs.writeFile(indexTarget, indexHtml);
137 }
138
139 // Shift the posts, delete any remainder.
140
141 async _shift() {
142
143 await this._ensurePostsDirectoryExists();
144
145 for (let i = this.maxPosts - 1; i > 0; --i) {
146 const targetPath = Path.join(this.postsDirectory, `${i}`);
147 const sourcePath = Path.join(this.postsDirectory, `${i - 1}`);
148
149 try {
150 await internals.fs.access(sourcePath);
151
152 internals.debuglog(`Removing ${targetPath}`);
153 await internals.rimraf(targetPath);
154
155 internals.debuglog(`Shifting blog post ${sourcePath} to ${targetPath}`);
156 await internals.ncp(sourcePath, targetPath);
157 }
158 catch (error) {
159 if (error.code === internals.kFileNotFoundError) {
160 internals.debuglog(`Skipping ${sourcePath}: Does not exist.`);
161 continue;
162 }
163
164 throw error;
165 }
166 }
167 }
168
169 // Copies a post directory to the latest slot.
170
171 async _copyPost(postLocation) {
172
173 await this._ensurePostsDirectoryExists();
174
175 const targetPath = Path.join(this.postsDirectory, '0');
176
177 internals.debuglog(`Removing ${targetPath}`);
178 await internals.rimraf(targetPath);
179
180 internals.debuglog(`Adding ${postLocation} to ${targetPath}`);
181 await internals.ncp(postLocation, targetPath);
182 }
183
184 // Ensures the posts directory exists.
185
186 async _ensurePostsDirectoryExists() {
187
188 internals.debuglog(`Checking if ${this.postsDirectory} exists.`);
189 try {
190 await internals.fs.access(this.postsDirectory);
191 }
192 catch (error) {
193 if (error.code === internals.kFileNotFoundError) {
194 internals.debuglog('Creating posts directory');
195 await internals.fs.mkdir(this.postsDirectory);
196 return;
197 }
198
199 throw error;
200 }
201 }
202
203 // Looks for a `.md` file in the blog directory, and returns the path
204
205 async _findBlogContent(directory) {
206
207 const entries = await internals.fs.readdir(directory);
208
209 const markdownEntries = entries
210 .filter((entry) => internals.kMarkdownRe.test(entry))
211 .map((entry) => Path.join(directory, entry));
212
213 if (markdownEntries.length > 0) {
214 internals.debuglog(`Found markdown file: ${markdownEntries[0]}`);
215 return markdownEntries[0];
216 }
217
218 throw new Error(internals.strings.markdownNotFound);
219 }
220 };