forked from HackSoc/hacksoc.org
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmain.js
executable file
·361 lines (330 loc) · 14.6 KB
/
main.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
const Handlebars = require('handlebars');
const fse = require('fs-extra');
const path = require('path');
const yaml = require('js-yaml');
const docmatter = require('docmatter');
const Markdown = new require('markdown-it')({
html: true, // enable HTML in Markdown
typographer: true // have some nice pretty quotes
}).use(require('markdown-it-highlightjs'), {auto: true, code: false})
const exec = require('child_process').exec;
require("handlebars-helpers").html();
function cmd(command) {
return new Promise((resolve,reject) => {
exec(command, (err, stdout, stderr) => {
if(err) {
reject(err);
}
else {
resolve([stdout,stderr])
}
});
});
}
const outDir = 'html'; //TODO: canonicalise?
fse.mkdirpSync(outDir);
fse.emptyDirSync(outDir);
compileTemplate = template => Handlebars.compile(template.toString('UTF-8'));
// Count-based iteration helper for Handlebars. Used for generating the rows of the calendar.
Handlebars.registerHelper('times', (n, block) => [...Array(n).keys()].map(i => block.fn(i)).join(''))
/**
* Wraps HTML documents found in dirname with wrapperTemplate and writes them to outDir
* @param {string} dirname The directory to read HTML content from
* @param {Handlebars.TemplateDelegate} wrapperTemplate Handlebars template to apply to the content
* @param {Object} globalContext Context to use for every page
* @returns {Promise} no resolve value, wraps around a Promise.all of all of the files
*/
function regularDir(dirname, wrapperTemplate, globalContext) {
return fse.readdir(dirname).then(listing => {
/**
* {String[]} listing: filenames in regular/
*/
let promises = listing //.filter(fn => /.*\.html/.test(fn)) // Filter to only HTML files
.map(filename =>
fse.readFile(path.join(dirname, filename))// open each file
.then(content => {
/**
* Takes content and applies the wraper template to it
* content begins with a YAML header.
* {Buffer} content: contents of `filename`
*/
let matter = docmatter(content.toString('UTF-8'));
return wrapperTemplate(Object.assign({ // Merge this object with the properties in context.yaml
body: matter.body
}, globalContext, yaml.safeLoad(matter.header)))
})
.then(html => fse.writeFile(path.join(outDir, filename), html))
);
return Promise.all(promises)
}).catch(err => {
console.log(`Error processing regular directory ${dirname}/ - ${err}`);
});
}
/**
* Renders Markdown server READMEs found in dirname, wrap with template and write to outDir/dirname/
* @param {string} dirname name to find Markdown source files
* @param {Handbars.TemplateDelegate} serverTemplate Handlebars template to make the page
* @param {Object} globalContext Misc context (navbar, servers, etc)
* @returns {Promise} wraps around a Promise.all for each file in `dirname`
*/
function servers(dirname, serverTemplate, globalContext) {
return fse.readdir(dirname).then(listing => {
/**
* {String[]} listing: filenames in servers/
*/
let promises = listing.map(filename =>
fse.readFile(path.join(dirname,filename))
.then(content => {
/**
* {Buffer} content: contents of `filename`
*/
let matter = docmatter(content.toString('UTF-8'));
if( typeof matter.header !== "undefined" &&
typeof matter.body !== "undefined" &&
matter.body.trim().length > 0 ) {
return serverTemplate(
Object.assign({
body: Markdown.render(matter.body)
}, globalContext, yaml.safeLoad(matter.header))
);
}
else {
console.log(`[warn]\t${filename} has empty content; skipping.`);
}
}).then(html => fse.writeFile(path.join(outDir, dirname, filename.replace(/\.md$/i,".html")), html))
)
return fse.mkdirp(path.join(outDir, dirname)).then(Promise.all(promises));
}).catch(err => {
console.log(`Error processing server directory ${dirname}/ - ${err}`);
});
}
/**
* Formats a date "Month DD, YYYY"
* @param {Date} date
* @returns {String}
*/
function formatDate(date) {
return date.toLocaleDateString("en-UK", {month:"long", day:"2-digit", year:"numeric"});
}
/**
* Generates an index of a directories PDF files, and copies them to the HTML folder.
* @param {string} dirname Directory to find .PDF files in
* @param {string} filename Filename of this page
* @param {Handlebars.TemplateDelegate} wrapperTemplate Handlebars template to apply to the whole page
* @param {Handlebars.TemplateDelegate} minuteTemplate Template to use on the listing
* @param {Object} globalContext Context to use for every page
*/
function minutes(dirname, filename, wrapperTemplate, minuteTemplate, globalContext) {
return fse.readdir(dirname).then(listing => {
let minutes = listing.filter(l => /\.pdf$/.test(l))
.map(fn => {
let m = /^(\d{4}-\d\d-\d\d)-([a-z0-9 ]+)\.pdf$/i.exec(fn);
if(m) {
return {
url: path.join(dirname, fn),
meeting: m[2],
date: m[1],
date_t: new Date(m[1]) // Impossible dates (31st Feb) approximated by Date (-> 2nd Mar)
}
}
else {
console.log(`[warn]\t${fn} has invalid filename; skipping.`)
return null;
}
})
.filter(m => !!m) // null return is falsy
.sort((f1, f2) => Math.sign(f1.date_t - f2.date_t));
// Has the potential to have meeting on the same date in the wrong order
// Cross that bridge when etc
fse.writeFile(path.join(outDir, filename),
wrapperTemplate(Object.assign({
body: minuteTemplate(minutes),
title: "Minutes"
}, globalContext))
).then(fse.copy(dirname, path.join(outDir, dirname)))
}).catch(err => {
console.log(`Error processing minutes directory ${dirname}/ - ${err}`);
});
}
/**
* Reads Markdown news articles from dirname and returns an array of news objects
* @param {Object} results any previous results that this object carries on
* @param {String} dirname directory to search for news
* @return {Promise<Object>} `results` joined with news: {posts: Object[]}
*/
function readNews(results, dirname='news') {
const re_date = /^(\d{4})-([01]\d)-([0-3]\d)/;
return new Promise((resolve, reject) => {
fse.readdir(dirname).then(listing => {
let promises = listing.map(fn => fse.readFile(path.join(dirname,fn))
.then(content => {
let matter = docmatter(content.toString('UTF-8'));
if( typeof matter.header !== "undefined" &&
typeof matter.body !== "undefined" &&
matter.body.trim().length > 0 ) {
let header = yaml.safeLoad(matter.header);
let match_date = re_date.exec(fn);
if(match_date) {
let htBody = Markdown.render(matter.body);
let htMatch = /^(<p>[^]*?<\/p>)/i.exec(htBody.trim());
let date_t = new Date(match_date[0])
return {
body: htBody,
title: header.title,
date: formatDate(date_t),
date_t: date_t,
url: `${dirname}/${fn.replace(/\.md$/i,'.html')}`,
excerpt: htMatch?htMatch[1]:htBody
};
}
else {
console.log(`[warn]\t${fn} has invalid filename; skipping.`);
return null;
}
}
else {
console.log(`[warn]\t${fn} has empty content; skipping.`);
// if we move to a proper logging solution this can be warn or info level.
return null
}
})
);
return Promise.all(promises).then(posts => {
let obj = {
news: {posts: posts.filter(p=>!!p).sort((a,b) => Math.sign(b.date_t - a.date_t))}
};
let finalResult = Object.assign(obj, results)
resolve(finalResult);
})
}).catch(err => {
console.log(`Error processing news directory ${dirname}/ - ${err}`);
});
});
}
/**
* Writes news.html and news articles from news object
* @param {Object} results object including globalContext, news, and template objects
* @param {String} dirname folder to write news to
*/
function writeNews(results, dirname='news') {
// huh, stuff gets real simple when it's sync
fse.mkdirpSync(path.join(outDir, dirname));
let promises = results.news.posts.map(newsObj =>
fse.writeFile(path.join(outDir, newsObj.url),
results.wrapperTemplate(
Object.assign({
body: results.articleTemplate({
title: newsObj.title,
date: newsObj.date,
body: newsObj.body
}),
excerpt: newsObj.excerpt,
title: newsObj.title,
}, results.globalContext)
)
)
)
promises.push(fse.writeFile(path.join(outDir, 'news.html'),
results.wrapperTemplate(Object.assign({
title: "News",
body: results.newslistTemplate(results.news)
}, results.globalContext))
))
return Promise.all(promises);
}
/**
* Writes the index page
* @param {Object} results object including globalContext, news, and template objects.
*/
function writeIndex(results) {
return fse.writeFile(path.join(outDir, 'index.html'), results.wrapperTemplate(
Object.assign({
body: results.indexTemplate(Object.assign({
newslist: results.newslistTemplate({
posts: results.news.posts.slice(0,5),
link: true
}), // first 5 news articles (if present)
calendar: results.calendarTemplate()
}, results.globalContext))
// no title, handled by wrapper.handlebars
},
{css: ['/static/calendar.css']},
results.globalContext)
));
}
/**
* Reads context.yaml and returns its contents as an object
* @returns {Promise<Object>} Resolves to the global context object
*/
function getGlobalContext() {
return Promise.all([
fse.readFile('templates/context.yaml')
.then(contents => {
/**
* {Buffer} contents: the contents of the YAML file
*/
return yaml.safeLoad(contents.toString('UTF-8'));
}),
cmd(`git log -1 --format=format:"Commit: %H%nAuthor: %an%nCommit date: %cd%nMessage: %s%n%b"`).then(([stdout,stderr]) => {
return {commit:stdout.trim()};
}),
Promise.resolve({builddate: new Date().toString()})
]).then(([yamlContext,gitResult,builddate]) => {
return Object.assign(yamlContext,gitResult,builddate);
}).catch(err => {
console.log(`Error getting global context: ${err}`);
})
}
// Main stuff here.
let p_mkOutDir = fse.mkdirp(outDir)
.then(() => fse.emptyDir(outDir));
let p_copyStatic = fse.copy('static', path.join(outDir, 'static'))
.then(console.log("Copied static files!"));
p_mkOutDir.then(() => p_copyStatic);
let hljsStylesheet = 'solarized-light';
p_copyStatic.then(() => fse.copyFile(`node_modules/highlight.js/styles/${hljsStylesheet}.css`, path.join(outDir, 'static', 'highlight.css')));
let p_contextAndTemplates = Promise.all(
[
getGlobalContext(),
fse.readFile('templates/wrapper.handlebars').then(compileTemplate),
fse.readFile('templates/minutes.handlebars').then(compileTemplate),
fse.readFile('templates/newslist.handlebars').then(compileTemplate),
fse.readFile('templates/server.handlebars').then(compileTemplate),
fse.readFile('templates/article.handlebars').then(compileTemplate),
fse.readFile('templates/calendar.handlebars').then(compileTemplate),
fse.readFile('templates/index.handlebars').then(compileTemplate)
])
.then(([ // Take array of results
globalContext,
wrapperTemplate,
minutesTemplate,
newslistTemplate,
serverTemplate,
articleTemplate,
calendarTemplate,
indexTemplate
]) => ({ // Map to object with named keys
globalContext,
wrapperTemplate,
minutesTemplate,
newslistTemplate,
serverTemplate,
articleTemplate,
calendarTemplate,
indexTemplate
}))
p_contextAndTemplates.then(obj => regularDir('regular',obj.wrapperTemplate, obj.globalContext))
.then(()=>console.log("Written regular files!"))
.catch(console.log);
p_contextAndTemplates.then(obj => minutes('minutes', 'minutes.html', obj.wrapperTemplate, obj.minutesTemplate, obj.globalContext))
.then(()=>console.log("Written minutes files!"))
.catch(console.log);
p_contextAndTemplates.then(obj => servers('servers', obj.serverTemplate, obj.globalContext))
.then(()=>console.log("Written server files!"))
.catch(console.log)
p_contextAndTemplates.then(obj => {
let p_news = readNews(obj, 'news');
p_news.then(writeNews).then(()=>console.log("Written news!"));
p_news.then(writeIndex).then(()=>console.log("Written index!"));
});
p_mkOutDir.then(() => p_contextAndTemplates);