]>
Commit | Line | Data |
---|---|---|
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}`); | |
115 | const assetsSource = Path.join(sourcePath, internals.kAssetsDirectoryName); | |
116 | const postContentPath = await this._findBlogContent(sourcePath); | |
117 | ||
118 | internals.debuglog(`Copying ${assetsSource} to ${assetsTarget}`); | |
119 | await internals.ncp(assetsSource, assetsTarget); | |
120 | ||
121 | internals.debuglog(`Reading ${postContentPath}`); | |
122 | const postContent = await internals.fs.readFile(postContentPath, { encoding: 'utf8' }); | |
123 | ||
124 | internals.debuglog('Parsing markdown'); | |
125 | posts.push({ | |
126 | html: Markdown.markdown.toHTML(postContent), | |
127 | id: i + 1 | |
128 | }); | |
129 | } | |
130 | ||
131 | internals.debuglog(`Reading ${indexLocation}`); | |
132 | const indexTemplate = await internals.fs.readFile(indexLocation, { encoding: 'utf8' }); | |
133 | ||
134 | internals.debuglog('Generating HTML'); | |
135 | const indexHtml = Mustache.render(indexTemplate, { posts }); | |
136 | await internals.fs.writeFile(indexTarget, indexHtml); | |
137 | } | |
138 | ||
139 | // Shift the posts, delete any remainder. | |
140 | ||
141 | async _shift() { | |
142 | ||
143 | await this._ensurePostsDirectoryExists(); | |
144 | ||
145 | for (let i = this.maxPosts - 1; i > 0; --i) { | |
146 | const targetPath = Path.join(this.postsDirectory, `${i}`); | |
147 | const sourcePath = Path.join(this.postsDirectory, `${i - 1}`); | |
148 | ||
149 | try { | |
150 | await internals.fs.access(sourcePath); | |
151 | ||
152 | internals.debuglog(`Removing ${targetPath}`); | |
153 | await internals.rimraf(targetPath); | |
154 | ||
155 | internals.debuglog(`Shifting blog post ${sourcePath} to ${targetPath}`); | |
156 | await internals.ncp(sourcePath, targetPath); | |
157 | } | |
158 | catch (error) { | |
159 | if (error.code === internals.kFileNotFoundError) { | |
160 | internals.debuglog(`Skipping ${sourcePath}: Does not exist.`); | |
161 | continue; | |
162 | } | |
163 | ||
164 | throw error; | |
165 | } | |
166 | } | |
167 | } | |
168 | ||
169 | // Copies a post directory to the latest slot. | |
170 | ||
171 | async _copyPost(postLocation) { | |
172 | ||
173 | await this._ensurePostsDirectoryExists(); | |
174 | ||
175 | const targetPath = Path.join(this.postsDirectory, '0'); | |
176 | ||
177 | internals.debuglog(`Removing ${targetPath}`); | |
178 | await internals.rimraf(targetPath); | |
179 | ||
180 | internals.debuglog(`Adding ${postLocation} to ${targetPath}`); | |
181 | await internals.ncp(postLocation, targetPath); | |
182 | } | |
183 | ||
184 | // Ensures the posts directory exists. | |
185 | ||
186 | async _ensurePostsDirectoryExists() { | |
187 | ||
188 | internals.debuglog(`Checking if ${this.postsDirectory} exists.`); | |
189 | try { | |
190 | await internals.fs.access(this.postsDirectory); | |
191 | } | |
192 | catch (error) { | |
193 | if (error.code === internals.kFileNotFoundError) { | |
194 | internals.debuglog('Creating posts directory'); | |
195 | await internals.fs.mkdir(this.postsDirectory); | |
196 | return; | |
197 | } | |
198 | ||
199 | throw error; | |
200 | } | |
201 | } | |
202 | ||
203 | // Looks for a `.md` file in the blog directory, and returns the path | |
204 | ||
205 | async _findBlogContent(directory) { | |
206 | ||
207 | const entries = await internals.fs.readdir(directory); | |
208 | ||
209 | const markdownEntries = entries | |
210 | .filter((entry) => internals.kMarkdownRe.test(entry)) | |
211 | .map((entry) => Path.join(directory, entry)); | |
212 | ||
213 | if (markdownEntries.length > 0) { | |
214 | internals.debuglog(`Found markdown file: ${markdownEntries[0]}`); | |
215 | return markdownEntries[0]; | |
216 | } | |
217 | ||
218 | throw new Error(internals.strings.markdownNotFound); | |
219 | } | |
220 | }; |