]> git.r.bdr.sh - rbdr/blog/blame - lib/blog.js
Add the spec for this feature
[rbdr/blog] / lib / blog.js
CommitLineData
cf630290
BB
1'use strict';
2
d92ac8cc
BB
3const { exec } = require('child_process');
4const { access, mkdir, readdir, readFile, rmdir, writeFile } = require('fs/promises');
5const { template } = require('dot');
6const { ncp } = require('ncp');
7const { join } = require('path');
8const Marked = require('marked');
9const { debuglog, promisify } = require('util');
cf630290
BB
10
11const internals = {
12
13 // Promisified functions
d92ac8cc 14 ncp: promisify(ncp),
cf630290 15
d92ac8cc
BB
16 exec: promisify(exec),
17 debuglog: debuglog('blog'),
cf630290
BB
18
19 // constants
20
21 kAssetsDirectoryName: 'assets',
22 kIndexName: 'index.html',
23 kFileNotFoundError: 'ENOENT',
24 kMarkdownRe: /\.md$/i,
d92ac8cc 25 kRemoveCommand: 'rm -rf',
cf630290
BB
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 */
42module.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 */
d92ac8cc 90 publish() {
cf630290
BB
91
92 console.error('Publishing not yet implemented');
d92ac8cc 93 return Promise.resolve();
cf630290
BB
94 }
95
96 // Parses markdown for each page, copies assets and generates index.
97
98 async _generate() {
99
d92ac8cc
BB
100 const assetsTarget = join(this.staticDirectory, internals.kAssetsDirectoryName);
101 const indexTarget = join(this.staticDirectory, internals.kIndexName);
102 const indexLocation = join(this.templatesDirectory, internals.kIndexName);
cf630290
BB
103 const posts = [];
104
105 internals.debuglog(`Removing ${assetsTarget}`);
d92ac8cc 106 await rmdir(assetsTarget, { recursive: true });
cf630290
BB
107
108 for (let i = 0; i < this.maxPosts; ++i) {
d92ac8cc 109 const sourcePath = join(this.postsDirectory, `${i}`);
cf630290 110
eccb3cc4 111 try {
d92ac8cc 112 await access(this.postsDirectory);
cf630290 113
d92ac8cc 114 const assetsSource = join(sourcePath, internals.kAssetsDirectoryName);
eccb3cc4 115 const postContentPath = await this._findBlogContent(sourcePath);
cf630290 116
eccb3cc4
BB
117 internals.debuglog(`Copying ${assetsSource} to ${assetsTarget}`);
118 await internals.ncp(assetsSource, assetsTarget);
119
120 internals.debuglog(`Reading ${postContentPath}`);
d92ac8cc 121 const postContent = await readFile(postContentPath, { encoding: 'utf8' });
eccb3cc4
BB
122
123 internals.debuglog('Parsing markdown');
124 posts.push({
d92ac8cc 125 html: Marked(postContent),
eccb3cc4
BB
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 }
cf630290
BB
137 }
138
139 internals.debuglog(`Reading ${indexLocation}`);
d92ac8cc 140 const indexTemplate = await readFile(indexLocation, { encoding: 'utf8' });
cf630290
BB
141
142 internals.debuglog('Generating HTML');
d92ac8cc
BB
143 const indexHtml = template(indexTemplate)({ posts });
144 await writeFile(indexTarget, indexHtml);
cf630290
BB
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) {
d92ac8cc
BB
154 const targetPath = join(this.postsDirectory, `${i}`);
155 const sourcePath = join(this.postsDirectory, `${i - 1}`);
cf630290
BB
156
157 try {
d92ac8cc 158 await access(sourcePath);
cf630290
BB
159
160 internals.debuglog(`Removing ${targetPath}`);
d92ac8cc 161 await rmdir(targetPath, { recursive: true });
cf630290
BB
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
d92ac8cc 183 const targetPath = join(this.postsDirectory, '0');
cf630290
BB
184
185 internals.debuglog(`Removing ${targetPath}`);
d92ac8cc 186 await rmdir(targetPath, { recursive: true });
cf630290
BB
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 {
d92ac8cc 198 await access(this.postsDirectory);
cf630290
BB
199 }
200 catch (error) {
201 if (error.code === internals.kFileNotFoundError) {
202 internals.debuglog('Creating posts directory');
d92ac8cc 203 await mkdir(this.postsDirectory);
cf630290
BB
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
d92ac8cc 215 const entries = await readdir(directory);
cf630290
BB
216
217 const markdownEntries = entries
218 .filter((entry) => internals.kMarkdownRe.test(entry))
d92ac8cc 219 .map((entry) => join(directory, entry));
cf630290
BB
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};