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