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