-
Notifications
You must be signed in to change notification settings - Fork 49
/
index.js
143 lines (118 loc) · 4.11 KB
/
index.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
const debug = require('debug')('metalsmith-layouts');
const match = require('multimatch');
const path = require('path');
const isUtf8 = require('is-utf8');
const getTransformer = require('./get-transformer');
/**
* Resolves layouts, in the following order:
* 1. Layouts in the frontmatter
* 2. Skips file if layout: false in frontmatter
* 3. Default layout in the settings
*/
function getLayout({ file, settings }) {
if (file.layout || file.layout === false) {
return file.layout;
}
return settings.default;
}
/**
* Engine, renders file with the appropriate layout
*/
function render({ filename, files, metadata, settings, metalsmith }) {
const file = files[filename];
const layout = getLayout({ file, settings });
const extension = layout.split('.').pop();
debug(`rendering ${filename} with layout ${layout}`);
// Stringify file contents
const contents = file.contents.toString();
const transform = getTransformer(extension);
const locals = Object.assign({}, metadata, file, { contents });
const layoutPath = path.join(metalsmith.path(settings.directory), layout);
// Transform the contents
return transform
.renderFileAsync(layoutPath, settings.engineOptions, locals)
.then(rendered => {
// Update file with results
// eslint-disable-next-line no-param-reassign
file.contents = Buffer.from(rendered.body);
debug(`done rendering ${filename}`);
})
.catch(err => {
// Prepend error message with file path
// eslint-disable-next-line no-param-reassign
err.message = `${filename}: ${err.message}`;
throw err;
});
}
/**
* Validate, checks whether a file should be processed
*/
function validate({ filename, files, settings }) {
const file = files[filename];
const layout = getLayout({ file, settings });
debug(`validating ${filename}`);
// Files without a layout cannot be processed
if (!layout) {
debug(`validation failed, ${filename} does not have a layout set`);
return false;
}
// Layouts without an extension cannot be processed
if (!layout.includes('.')) {
debug(`validation failed, layout for ${filename} does not have an extension`);
return false;
}
// Files that are not utf8 are ignored
if (!isUtf8(file.contents)) {
debug(`validation failed, ${filename} is not utf-8`);
return false;
}
// Files without an applicable jstransformer are ignored
const extension = layout.split('.').pop();
const transformer = getTransformer(extension);
if (!transformer) {
debug(`validation failed, no jstransformer found for layout for ${filename}`);
}
return transformer;
}
/**
* Plugin, the main plugin used by metalsmith
*/
module.exports = options => (files, metalsmith, done) => {
const metadata = metalsmith.metadata();
const defaults = {
pattern: '**',
directory: 'layouts',
engineOptions: {},
suppressNoFilesError: false
};
const settings = Object.assign({}, defaults, options);
// Check whether the pattern option is valid
if (!(typeof settings.pattern === 'string' || Array.isArray(settings.pattern))) {
done(
new Error(
'invalid pattern, the pattern option should be a string or array of strings. See https://www.npmjs.com/package/metalsmith-layouts#pattern'
)
);
}
// Filter files by the pattern
const matchedFiles = match(Object.keys(files), settings.pattern);
// Filter files by validity
const validFiles = matchedFiles.filter(filename => validate({ filename, files, settings }));
// Let the user know when there are no files to process, unless the check is suppressed
if (validFiles.length === 0) {
const msg =
'no files to process. See https://www.npmjs.com/package/metalsmith-layouts#no-files-to-process';
if (settings.suppressNoFilesError) {
debug(msg);
done();
} else {
done(new Error(msg));
}
}
// Map all files that should be processed to an array of promises and call done when finished
Promise.all(
validFiles.map(filename => render({ filename, files, metadata, settings, metalsmith }))
)
.then(() => done())
.catch(/* istanbul ignore next */ error => done(error));
};