]> git.r.bdr.sh - rbdr/blog/blame - lib/blog.js
Update gitignore to be more specific
[rbdr/blog] / lib / blog.js
CommitLineData
cf630290
BB
1'use strict';
2
d92ac8cc 3const { access, mkdir, readdir, readFile, rmdir, writeFile } = require('fs/promises');
d92ac8cc
BB
4const { ncp } = require('ncp');
5const { join } = require('path');
6const Marked = require('marked');
7const { debuglog, promisify } = require('util');
cf630290 8
67fdfa7c
BB
9const StaticGenerator = require('./generators/static');
10const HTMLGenerator = require('./generators/html');
11const RSSGenerator = require('./generators/rss');
5f31ea34 12const TXTGenerator = require('./generators/txt');
67fdfa7c 13
cf630290
BB
14const internals = {
15
16 // Promisified functions
d92ac8cc 17 ncp: promisify(ncp),
cf630290 18
d92ac8cc 19 debuglog: debuglog('blog'),
cf630290
BB
20
21 // constants
22
cf630290
BB
23 kFileNotFoundError: 'ENOENT',
24 kMarkdownRe: /\.md$/i,
6f72ad0f 25 kMetadataFilename: 'metadata.json',
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
67fdfa7c 39 * @param {Blog.tConfiguration} config the initialization options to
cf630290
BB
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
6f72ad0f 62 await this._ensurePostsDirectoryExists();
cf630290
BB
63 await this._shift();
64 await this.update(postLocation);
65 }
66
67 /**
68 * Adds the passed path to slot 0, and generates files.
69 *
70 * @function update
71 * @memberof Blog
72 * @param {string} postLocation the path to the directory containing
73 * the post structure
74 * @return {Promise<undefined>} empty promise, returns no value
75 * @instance
76 */
77 async update(postLocation) {
78
6f72ad0f
BB
79 const metadata = await this._getMetadata();
80 await this._ensurePostsDirectoryExists();
cf630290 81 await this._copyPost(postLocation);
6f72ad0f
BB
82 await this._writeMetadata(metadata);
83
cf630290
BB
84 await this._generate();
85 }
86
87 /**
88 * Publishes the files to a static host.
89 *
90 * @function publish
91 * @memberof Blog
92 * @return {Promise<undefined>} empty promise, returns no value
93 * @instance
94 */
d92ac8cc 95 publish() {
cf630290
BB
96
97 console.error('Publishing not yet implemented');
d92ac8cc 98 return Promise.resolve();
cf630290
BB
99 }
100
101 // Parses markdown for each page, copies assets and generates index.
102
103 async _generate() {
104
67fdfa7c
BB
105 internals.debuglog('Generating output');
106
107 const posts = await this._readPosts(this.postsDirectory);
108
109 await StaticGenerator(this.postsDirectory, this.staticDirectory, posts);
110 await HTMLGenerator(this.templatesDirectory, this.staticDirectory, posts);
111 await RSSGenerator(this.templatesDirectory, this.staticDirectory, posts);
5f31ea34 112 await TXTGenerator(this.templatesDirectory, this.staticDirectory, posts);
67fdfa7c
BB
113 }
114
115 // Reads the posts into an array
cf630290 116
67fdfa7c
BB
117 async _readPosts(source) {
118
119 internals.debuglog('Reading posts');
120 const posts = [];
cf630290
BB
121
122 for (let i = 0; i < this.maxPosts; ++i) {
67fdfa7c 123 const postSourcePath = join(source, `${i}`);
cf630290 124
67fdfa7c 125 internals.debuglog(`Reading ${postSourcePath} into posts array`);
cf630290 126
67fdfa7c
BB
127 try {
128 await access(postSourcePath);
cf630290 129
67fdfa7c 130 const metadata = await this._getMetadata(i);
eccb3cc4 131
67fdfa7c 132 const postContentPath = await this._findBlogContent(postSourcePath);
eccb3cc4 133 internals.debuglog(`Reading ${postContentPath}`);
d92ac8cc 134 const postContent = await readFile(postContentPath, { encoding: 'utf8' });
eccb3cc4
BB
135
136 internals.debuglog('Parsing markdown');
137 posts.push({
67fdfa7c 138 ...metadata,
5f31ea34
BB
139 html: Marked(postContent),
140 raw: postContent
eccb3cc4
BB
141 });
142 }
143 catch (error) {
144 if (error.code === internals.kFileNotFoundError) {
145 internals.debuglog(`Skipping ${i}`);
146 continue;
147 }
148
149 throw error;
150 }
cf630290
BB
151 }
152
67fdfa7c 153 return posts;
cf630290
BB
154 }
155
156 // Shift the posts, delete any remainder.
157
158 async _shift() {
159
cf630290 160
6f72ad0f 161 for (let i = this.maxPosts - 1; i >= 0; --i) {
d92ac8cc
BB
162 const targetPath = join(this.postsDirectory, `${i}`);
163 const sourcePath = join(this.postsDirectory, `${i - 1}`);
cf630290
BB
164
165 try {
cf630290 166 internals.debuglog(`Removing ${targetPath}`);
d92ac8cc 167 await rmdir(targetPath, { recursive: true });
cf630290 168
6f72ad0f
BB
169 await access(sourcePath); // check the source path
170
cf630290
BB
171 internals.debuglog(`Shifting blog post ${sourcePath} to ${targetPath}`);
172 await internals.ncp(sourcePath, targetPath);
173 }
174 catch (error) {
175 if (error.code === internals.kFileNotFoundError) {
176 internals.debuglog(`Skipping ${sourcePath}: Does not exist.`);
177 continue;
178 }
179
180 throw error;
181 }
182 }
183 }
184
6f72ad0f
BB
185 // Attempts to read existing metadata. Otherwise generates new set.
186
67fdfa7c 187 async _getMetadata(index = 0) {
6f72ad0f 188
67fdfa7c 189 const metadataTarget = join(this.postsDirectory, String(index), internals.kMetadataFilename);
6f72ad0f
BB
190
191 try {
192 internals.debuglog(`Looking for metadata at ${metadataTarget}`);
67fdfa7c 193 return JSON.parse(await readFile(metadataTarget, { encoding: 'utf8' }));
6f72ad0f
BB
194 }
195 catch (e) {
196 internals.debuglog(`Metadata not found or unreadable. Generating new set.`);
197 const createdOn = Date.now();
198 const metadata = {
199 id: String(createdOn),
200 createdOn
201 };
202
67fdfa7c 203 return metadata;
6f72ad0f
BB
204 }
205 }
206
207 // Writes metadata. Assumes post 0 since it only gets written
208 // on create
209
210 async _writeMetadata(metadata) {
211
212 const metadataTarget = join(this.postsDirectory, '0', internals.kMetadataFilename);
213 internals.debuglog(`Writing ${metadataTarget}`);
67fdfa7c 214 await writeFile(metadataTarget, JSON.stringify(metadata, null, 2));
6f72ad0f
BB
215 }
216
cf630290
BB
217 // Copies a post directory to the latest slot.
218
219 async _copyPost(postLocation) {
220
d92ac8cc 221 const targetPath = join(this.postsDirectory, '0');
cf630290
BB
222
223 internals.debuglog(`Removing ${targetPath}`);
d92ac8cc 224 await rmdir(targetPath, { recursive: true });
cf630290
BB
225
226 internals.debuglog(`Adding ${postLocation} to ${targetPath}`);
227 await internals.ncp(postLocation, targetPath);
228 }
229
230 // Ensures the posts directory exists.
231
232 async _ensurePostsDirectoryExists() {
233
234 internals.debuglog(`Checking if ${this.postsDirectory} exists.`);
235 try {
d92ac8cc 236 await access(this.postsDirectory);
cf630290
BB
237 }
238 catch (error) {
239 if (error.code === internals.kFileNotFoundError) {
240 internals.debuglog('Creating posts directory');
d92ac8cc 241 await mkdir(this.postsDirectory);
cf630290
BB
242 return;
243 }
244
245 throw error;
246 }
247 }
248
249 // Looks for a `.md` file in the blog directory, and returns the path
250
251 async _findBlogContent(directory) {
252
d92ac8cc 253 const entries = await readdir(directory);
cf630290
BB
254
255 const markdownEntries = entries
256 .filter((entry) => internals.kMarkdownRe.test(entry))
d92ac8cc 257 .map((entry) => join(directory, entry));
cf630290
BB
258
259 if (markdownEntries.length > 0) {
260 internals.debuglog(`Found markdown file: ${markdownEntries[0]}`);
261 return markdownEntries[0];
262 }
263
264 throw new Error(internals.strings.markdownNotFound);
265 }
266};