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