]> git.r.bdr.sh - rbdr/blog/blob - lib/blog.js
98efabdf1fc618636390ef5874704aa0f9028bc7
[rbdr/blog] / lib / blog.js
1 'use strict';
2
3 const { access, mkdir, readdir, readFile, rmdir, writeFile } = require('fs/promises');
4 const { ncp } = require('ncp');
5 const { join } = require('path');
6 const Marked = require('marked');
7 const { debuglog, promisify } = require('util');
8
9 const StaticGenerator = require('./generators/static');
10 const HTMLGenerator = require('./generators/html');
11 const RSSGenerator = require('./generators/rss');
12
13 const internals = {
14
15 // Promisified functions
16 ncp: promisify(ncp),
17
18 debuglog: debuglog('blog'),
19
20 // constants
21
22 kFileNotFoundError: 'ENOENT',
23 kMarkdownRe: /\.md$/i,
24 kMetadataFilename: 'metadata.json',
25
26 // Strings
27
28 strings: {
29 markdownNotFound: 'Markdown file was not found in blog directory. Please update.'
30 }
31 };
32
33 /**
34 * The Blog class is the blog generator, it's in charge of adding and
35 * updating posts, and handling the publishing.
36 *
37 * @class Blog
38 * @param {Blog.tConfiguration} config the initialization options to
39 * extend the instance
40 */
41 module.exports = class Blog {
42
43 constructor(config) {
44
45 Object.assign(this, config);
46 }
47
48 /**
49 * Shifts the blog posts, adds the passed path to slot 0, and
50 * generates files.
51 *
52 * @function add
53 * @memberof Blog
54 * @param {string} postLocation the path to the directory containing
55 * the post structure
56 * @return {Promise<undefined>} empty promise, returns no value
57 * @instance
58 */
59 async add(postLocation) {
60
61 await this._ensurePostsDirectoryExists();
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 const metadata = await this._getMetadata();
79 await this._ensurePostsDirectoryExists();
80 await this._copyPost(postLocation);
81 await this._writeMetadata(metadata);
82
83 await this._generate();
84 }
85
86 /**
87 * Publishes the files to a static host.
88 *
89 * @function publish
90 * @memberof Blog
91 * @return {Promise<undefined>} empty promise, returns no value
92 * @instance
93 */
94 publish() {
95
96 console.error('Publishing not yet implemented');
97 return Promise.resolve();
98 }
99
100 // Parses markdown for each page, copies assets and generates index.
101
102 async _generate() {
103
104 internals.debuglog('Generating output');
105
106 const posts = await this._readPosts(this.postsDirectory);
107
108 await StaticGenerator(this.postsDirectory, this.staticDirectory, posts);
109 await HTMLGenerator(this.templatesDirectory, this.staticDirectory, posts);
110 await RSSGenerator(this.templatesDirectory, this.staticDirectory, posts);
111 }
112
113 // Reads the posts into an array
114
115 async _readPosts(source) {
116
117 internals.debuglog('Reading posts');
118 const posts = [];
119
120 for (let i = 0; i < this.maxPosts; ++i) {
121 const postSourcePath = join(source, `${i}`);
122
123 internals.debuglog(`Reading ${postSourcePath} into posts array`);
124
125 try {
126 await access(postSourcePath);
127
128 const metadata = await this._getMetadata(i);
129
130 const postContentPath = await this._findBlogContent(postSourcePath);
131 internals.debuglog(`Reading ${postContentPath}`);
132 const postContent = await readFile(postContentPath, { encoding: 'utf8' });
133
134 internals.debuglog('Parsing markdown');
135 posts.push({
136 ...metadata,
137 html: Marked(postContent)
138 });
139 }
140 catch (error) {
141 if (error.code === internals.kFileNotFoundError) {
142 internals.debuglog(`Skipping ${i}`);
143 continue;
144 }
145
146 throw error;
147 }
148 }
149
150 return posts;
151 }
152
153 // Shift the posts, delete any remainder.
154
155 async _shift() {
156
157
158 for (let i = this.maxPosts - 1; i >= 0; --i) {
159 const targetPath = join(this.postsDirectory, `${i}`);
160 const sourcePath = join(this.postsDirectory, `${i - 1}`);
161
162 try {
163 internals.debuglog(`Removing ${targetPath}`);
164 await rmdir(targetPath, { recursive: true });
165
166 await access(sourcePath); // check the source path
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 // Attempts to read existing metadata. Otherwise generates new set.
183
184 async _getMetadata(index = 0) {
185
186 const metadataTarget = join(this.postsDirectory, String(index), internals.kMetadataFilename);
187
188 try {
189 internals.debuglog(`Looking for metadata at ${metadataTarget}`);
190 return JSON.parse(await readFile(metadataTarget, { encoding: 'utf8' }));
191 }
192 catch (e) {
193 internals.debuglog(`Metadata not found or unreadable. Generating new set.`);
194 const createdOn = Date.now();
195 const metadata = {
196 id: String(createdOn),
197 createdOn
198 };
199
200 return metadata;
201 }
202 }
203
204 // Writes metadata. Assumes post 0 since it only gets written
205 // on create
206
207 async _writeMetadata(metadata) {
208
209 const metadataTarget = join(this.postsDirectory, '0', internals.kMetadataFilename);
210 internals.debuglog(`Writing ${metadataTarget}`);
211 await writeFile(metadataTarget, JSON.stringify(metadata, null, 2));
212 }
213
214 // Copies a post directory to the latest slot.
215
216 async _copyPost(postLocation) {
217
218 const targetPath = join(this.postsDirectory, '0');
219
220 internals.debuglog(`Removing ${targetPath}`);
221 await rmdir(targetPath, { recursive: true });
222
223 internals.debuglog(`Adding ${postLocation} to ${targetPath}`);
224 await internals.ncp(postLocation, targetPath);
225 }
226
227 // Ensures the posts directory exists.
228
229 async _ensurePostsDirectoryExists() {
230
231 internals.debuglog(`Checking if ${this.postsDirectory} exists.`);
232 try {
233 await access(this.postsDirectory);
234 }
235 catch (error) {
236 if (error.code === internals.kFileNotFoundError) {
237 internals.debuglog('Creating posts directory');
238 await mkdir(this.postsDirectory);
239 return;
240 }
241
242 throw error;
243 }
244 }
245
246 // Looks for a `.md` file in the blog directory, and returns the path
247
248 async _findBlogContent(directory) {
249
250 const entries = await readdir(directory);
251
252 const markdownEntries = entries
253 .filter((entry) => internals.kMarkdownRe.test(entry))
254 .map((entry) => join(directory, entry));
255
256 if (markdownEntries.length > 0) {
257 internals.debuglog(`Found markdown file: ${markdownEntries[0]}`);
258 return markdownEntries[0];
259 }
260
261 throw new Error(internals.strings.markdownNotFound);
262 }
263 };