]>
Commit | Line | Data |
---|---|---|
cf630290 BB |
1 | 'use strict'; |
2 | ||
3 | const Fs = require('fs'); | |
cf630290 BB |
4 | const Mustache = require('mustache'); |
5 | const Ncp = require('ncp'); | |
6 | const Path = require('path'); | |
7 | const Rimraf = require('rimraf'); | |
2afa1e2b | 8 | const Showdown = require('showdown'); |
cf630290 BB |
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'); | |
2afa1e2b | 129 | const parser = new Showdown.Converter(); |
eccb3cc4 | 130 | posts.push({ |
2afa1e2b | 131 | html: parser.makeHtml(postContent), |
eccb3cc4 BB |
132 | id: i + 1 |
133 | }); | |
134 | } | |
135 | catch (error) { | |
136 | if (error.code === internals.kFileNotFoundError) { | |
137 | internals.debuglog(`Skipping ${i}`); | |
138 | continue; | |
139 | } | |
140 | ||
141 | throw error; | |
142 | } | |
cf630290 BB |
143 | } |
144 | ||
145 | internals.debuglog(`Reading ${indexLocation}`); | |
146 | const indexTemplate = await internals.fs.readFile(indexLocation, { encoding: 'utf8' }); | |
147 | ||
148 | internals.debuglog('Generating HTML'); | |
149 | const indexHtml = Mustache.render(indexTemplate, { posts }); | |
150 | await internals.fs.writeFile(indexTarget, indexHtml); | |
151 | } | |
152 | ||
153 | // Shift the posts, delete any remainder. | |
154 | ||
155 | async _shift() { | |
156 | ||
157 | await this._ensurePostsDirectoryExists(); | |
158 | ||
159 | for (let i = this.maxPosts - 1; i > 0; --i) { | |
160 | const targetPath = Path.join(this.postsDirectory, `${i}`); | |
161 | const sourcePath = Path.join(this.postsDirectory, `${i - 1}`); | |
162 | ||
163 | try { | |
164 | await internals.fs.access(sourcePath); | |
165 | ||
166 | internals.debuglog(`Removing ${targetPath}`); | |
167 | await internals.rimraf(targetPath); | |
168 | ||
169 | internals.debuglog(`Shifting blog post ${sourcePath} to ${targetPath}`); | |
170 | await internals.ncp(sourcePath, targetPath); | |
171 | } | |
172 | catch (error) { | |
173 | if (error.code === internals.kFileNotFoundError) { | |
174 | internals.debuglog(`Skipping ${sourcePath}: Does not exist.`); | |
175 | continue; | |
176 | } | |
177 | ||
178 | throw error; | |
179 | } | |
180 | } | |
181 | } | |
182 | ||
183 | // Copies a post directory to the latest slot. | |
184 | ||
185 | async _copyPost(postLocation) { | |
186 | ||
187 | await this._ensurePostsDirectoryExists(); | |
188 | ||
189 | const targetPath = Path.join(this.postsDirectory, '0'); | |
190 | ||
191 | internals.debuglog(`Removing ${targetPath}`); | |
192 | await internals.rimraf(targetPath); | |
193 | ||
194 | internals.debuglog(`Adding ${postLocation} to ${targetPath}`); | |
195 | await internals.ncp(postLocation, targetPath); | |
196 | } | |
197 | ||
198 | // Ensures the posts directory exists. | |
199 | ||
200 | async _ensurePostsDirectoryExists() { | |
201 | ||
202 | internals.debuglog(`Checking if ${this.postsDirectory} exists.`); | |
203 | try { | |
204 | await internals.fs.access(this.postsDirectory); | |
205 | } | |
206 | catch (error) { | |
207 | if (error.code === internals.kFileNotFoundError) { | |
208 | internals.debuglog('Creating posts directory'); | |
209 | await internals.fs.mkdir(this.postsDirectory); | |
210 | return; | |
211 | } | |
212 | ||
213 | throw error; | |
214 | } | |
215 | } | |
216 | ||
217 | // Looks for a `.md` file in the blog directory, and returns the path | |
218 | ||
219 | async _findBlogContent(directory) { | |
220 | ||
221 | const entries = await internals.fs.readdir(directory); | |
222 | ||
223 | const markdownEntries = entries | |
224 | .filter((entry) => internals.kMarkdownRe.test(entry)) | |
225 | .map((entry) => Path.join(directory, entry)); | |
226 | ||
227 | if (markdownEntries.length > 0) { | |
228 | internals.debuglog(`Found markdown file: ${markdownEntries[0]}`); | |
229 | return markdownEntries[0]; | |
230 | } | |
231 | ||
232 | throw new Error(internals.strings.markdownNotFound); | |
233 | } | |
234 | }; |