]> git.r.bdr.sh - rbdr/blog/blob - lib/blog.js
f31234b526f7289f767bbadb04bb47134c0d9798
[rbdr/blog] / lib / blog.js
1 'use strict';
2
3 const Fs = require('fs');
4 const Mustache = require('mustache');
5 const Ncp = require('ncp');
6 const Path = require('path');
7 const Rimraf = require('rimraf');
8 const Showdown = require('showdown');
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
116 try {
117 await internals.fs.access(this.postsDirectory);
118
119 const assetsSource = Path.join(sourcePath, internals.kAssetsDirectoryName);
120 const postContentPath = await this._findBlogContent(sourcePath);
121
122 internals.debuglog(`Copying ${assetsSource} to ${assetsTarget}`);
123 await internals.ncp(assetsSource, assetsTarget);
124
125 internals.debuglog(`Reading ${postContentPath}`);
126 const postContent = await internals.fs.readFile(postContentPath, { encoding: 'utf8' });
127
128 internals.debuglog('Parsing markdown');
129 const parser = new Showdown.Converter();
130 posts.push({
131 html: parser.makeHtml(postContent),
132 id: i + 1
133 });
134 }
135 catch (error) {
136 if (error.code === internals.kFileNotFoundError) {
137 internals.debuglog(`Skipping ${i}`);
138 continue;
139 }
140
141 throw error;
142 }
143 }
144
145 internals.debuglog(`Reading ${indexLocation}`);
146 const indexTemplate = await internals.fs.readFile(indexLocation, { encoding: 'utf8' });
147
148 internals.debuglog('Generating HTML');
149 const indexHtml = Mustache.render(indexTemplate, { posts });
150 await internals.fs.writeFile(indexTarget, indexHtml);
151 }
152
153 // Shift the posts, delete any remainder.
154
155 async _shift() {
156
157 await this._ensurePostsDirectoryExists();
158
159 for (let i = this.maxPosts - 1; i > 0; --i) {
160 const targetPath = Path.join(this.postsDirectory, `${i}`);
161 const sourcePath = Path.join(this.postsDirectory, `${i - 1}`);
162
163 try {
164 await internals.fs.access(sourcePath);
165
166 internals.debuglog(`Removing ${targetPath}`);
167 await internals.rimraf(targetPath);
168
169 internals.debuglog(`Shifting blog post ${sourcePath} to ${targetPath}`);
170 await internals.ncp(sourcePath, targetPath);
171 }
172 catch (error) {
173 if (error.code === internals.kFileNotFoundError) {
174 internals.debuglog(`Skipping ${sourcePath}: Does not exist.`);
175 continue;
176 }
177
178 throw error;
179 }
180 }
181 }
182
183 // Copies a post directory to the latest slot.
184
185 async _copyPost(postLocation) {
186
187 await this._ensurePostsDirectoryExists();
188
189 const targetPath = Path.join(this.postsDirectory, '0');
190
191 internals.debuglog(`Removing ${targetPath}`);
192 await internals.rimraf(targetPath);
193
194 internals.debuglog(`Adding ${postLocation} to ${targetPath}`);
195 await internals.ncp(postLocation, targetPath);
196 }
197
198 // Ensures the posts directory exists.
199
200 async _ensurePostsDirectoryExists() {
201
202 internals.debuglog(`Checking if ${this.postsDirectory} exists.`);
203 try {
204 await internals.fs.access(this.postsDirectory);
205 }
206 catch (error) {
207 if (error.code === internals.kFileNotFoundError) {
208 internals.debuglog('Creating posts directory');
209 await internals.fs.mkdir(this.postsDirectory);
210 return;
211 }
212
213 throw error;
214 }
215 }
216
217 // Looks for a `.md` file in the blog directory, and returns the path
218
219 async _findBlogContent(directory) {
220
221 const entries = await internals.fs.readdir(directory);
222
223 const markdownEntries = entries
224 .filter((entry) => internals.kMarkdownRe.test(entry))
225 .map((entry) => Path.join(directory, entry));
226
227 if (markdownEntries.length > 0) {
228 internals.debuglog(`Found markdown file: ${markdownEntries[0]}`);
229 return markdownEntries[0];
230 }
231
232 throw new Error(internals.strings.markdownNotFound);
233 }
234 };