]>
Commit | Line | Data |
---|---|---|
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 | }; |