-
Notifications
You must be signed in to change notification settings - Fork 104
/
engine.js
184 lines (164 loc) · 6.89 KB
/
engine.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
'use strict';
const path = require('path');
const fs = require('fs');
const pify = require('pify');
const Handlebars = require('handlebars');
const extractStack = require('extract-stack');
const File = require('vinyl');
const replaceExt = require('replace-ext');
const through = require('through2');
const glob = pify(require('glob'));
const stripBom = require('strip-bom');
const folders = require('./folders');
const readFile = pify(fs.readFile);
let errorTemplate;
/**
* Base class for a Panini rendering engine.
* @abstract
*/
class PaniniEngine {
/**
* Get all files matching a pattern and run a function that gets each file's path and contents.
* Use this method in an engine to fetch the contents of files to load layouts, partials, etc.
* The callback can return a promise if an asynchronous action is needed.
* @param {String} base - Root path to search in. This is usually `this.options.input`.
* @param {String} dir - Directory to search in. This is usually a member of `this.options`.
* @param {String} pattern - Glob pattern to append. This is usually a series of file extensions.
* @param {Function} cb - Function to run on each file. Gets `path` and `contents` arguments.
* @returns {Promise} Promise which resolves when all files have been processed.
*/
static mapFiles(base, dir, pattern, cb) {
const globPath = path.join(process.cwd(), base, dir, pattern);
return glob(globPath).then(paths => Promise.all(paths.map(p =>
readFile(p).then(contents => cb(p, stripBom(contents.toString())))
)));
}
/**
* Get all files matching a pattern and run a function that gets each file's path.
* Use this method in an engine to fetch and store all paths matching a set of criteria.
* The callback can return a promise if an asynchronous action is needed.
* @param {String} base - Root path to search in. This is usually `this.options.input`.
* @param {String} dir - Directory to search in. This is usually a member of `this.options`.
* @param {String} pattern - Glob pattern to append. This is usually a series of file extensions.
* @param {Function} cb - Function to run on each file. Gets `path` argument.
* @returns {Promise} Promise which resolves when all files have been processed.
*/
static mapPaths(base, dir, pattern, cb) {
const globPath = path.join(process.cwd(), base, dir, pattern);
return glob(globPath).then(paths => Promise.all(paths.map(p => cb(p))));
}
get i18n() {
return this.locales.length > 0;
}
/**
* Set up common settings for all rendering engines. Because `PaniniEngine` is considered an
* abstract class, this constructor will never be called directly.
* @param {Object} options - Panini options.
*/
constructor(options) {
if (this.constructor === PaniniEngine) {
throw new TypeError('Do not call the PaniniEngine class directly. Create a sub-class instead.');
}
this.options = options || {};
this.data = {};
this.locales = [];
this.localeData = {};
this.collections = {};
this.collectionPages = {};
if (this.supports('layouts')) {
this.layouts = {};
}
}
/**
* Process all collection types in a project.
* @returns {Promise} Promise which resolves when all collections have been processed.
*/
buildCollections() {
return Promise.all(Object.keys(this.collections).map(name => this.buildCollection(name)));
}
/**
* Build a series of pages using a collection configuration. A collection defines these settings:
* - `input`: glob pattern relative to project base to scan for files with.
* - `output`: folder relative to output to write pages to.
* - `transform(filePath, contents)`: function to process each file from `input` and return a filename and template data for a new page.
* - `read`: if the contents of each file should be passed to the `transform` function. Defaults to `true`.
*
* Using these settings, this method processes all files in a collection and generates Vinyl files that are eventually mixed in with normal pages during the building process.
*/
buildCollection(name) {
const collection = this.collections[name];
if (!collection) {
return;
}
this.collectionPages[name] = [];
const basePath = path.join(process.cwd(), this.options.input, folders.pages);
const baseFile = new File({
base: basePath,
path: basePath,
contents: collection.template
});
// Grab every file for a given collection type
return glob(path.join(this.options.input, collection.input)).then(paths => Promise.all(paths.map(filePath => {
// Read the contents of the file if ncessary
let p;
if (collection.read === false || filePath.match(/\/$/)) {
p = Promise.resolve();
} else {
p = readFile(filePath).then(res => res.toString());
}
// Call the transform function and then generate a file with the result
return p.then(res => collection.transform(filePath, res)).then(res => {
const file = baseFile.clone();
const filePath = path.join(file.path, collection.output, res.name);
file.path = replaceExt(filePath, '.html');
file.data = res.data;
this.collectionPages[name].push(file);
});
})));
}
/**
* Create a transform stream function which will insert collection pages into a stream, to be rendered by `Panini.compile()`.
* @returns {Object} Stream transform function.
*/
getCollectionStream() {
const _this = this;
return through.obj(
// For files being read, this a passthrough. They aren't modified
(file, enc, cb) => cb(null, file),
// Once all pages have gone through, we insert collection pages into the stream
function (cb) { // eslint-disable-line prefer-arrow-callback
Object.keys(_this.collectionPages).forEach(i => {
_this.collectionPages[i].forEach(page => this.push(page));
});
cb();
}
);
}
/**
* Check if a rendering engine supports a specific feature, such as layouts or partials.
* @param {String} feature - Feature to check for.
* @returns {Boolean} If feature is supported.
*/
supports(feature) {
return (this.constructor.features || []).indexOf(feature) > -1;
}
/**
* Create an HTML page to display a rendering error. This page will be passed to the final file instead of the actual page.
* @param {Error} error - Error instance.
* @param {String} filePath - Path to source file with error.
* @returns {String} HTML error page.
*/
error(error, filePath) {
const fileName = path.relative(process.cwd(), filePath);
if (!errorTemplate) {
const errorPage = fs.readFileSync(path.join(__dirname, 'error-template.hbs'));
errorTemplate = Handlebars.compile(errorPage.toString());
}
return errorTemplate({
message: error.message,
stack: extractStack.lines(error).join('\n'),
fileName
});
}
}
module.exports = PaniniEngine;