UNPKG

11.3 kBJavaScriptView Raw
1/**
2 * @license MIT
3 * Copyright (c) 2016 Craig Monro (cmroanirgo)
4 **/
5"use strict";
6
7var l = require('ergo-utils').log.module('ergo-lib-fileinfo');
8var _ = require('ergo-utils')._;
9var fs = require('ergo-utils').fs.extend(require('fs-extra'));
10var path = require('path');
11const assert = require('assert');
12var plugin_api = require('../api/plugin')
13var Promise = require('bluebird');
14// promisify a few funcs we need
15"fileExists,stat,ensureDir,readFile,writeFile,copy".split(',').forEach(function(fn) {
16 fs[fn] = Promise.promisify(fs[fn])
17});
18
19
20function _fixupFilenameChars(str, space_char_replace) { // make sure the path is pointing the right way for the platform (windows/'nix)
21 str = fs.fixupPathSep(str).toLowerCase();
22 var dir = path.dirname(str); // assume dirs DON'T need fixing! (Probably a safe assumption)
23 str = path.basename(str).replace(/\s/g, space_char_replace || '-');
24 return path.join(dir, str);
25}
26
27//var _ThisBuild = new Date();
28
29function _shouldKeepExisting(file, text)
30{
31return Promise.coroutine(function *() {
32 if (!(yield fs.fileExists(file)))
33 return false;
34
35// var st = yield fs.stat(file);
36// if (st > _ThisBuild)
37// l.logw("File is used multiple times: '" + file)+"'");
38
39 // read the existing file and compare with new contents. ignore if nothing changed
40 try
41 {
42 var existingText = yield fs.readFile(file, "utf8");
43 if (existingText == text)
44 {
45 //l.vlog("Skipping (unchanged): '" + file + "'");
46 return true;
47 }
48 }
49 catch (e)
50 {
51 l.logw("Failed to compare existing file " + path.basename(file) + " (will try and overwrite): " + _.niceStackTrace(e));
52 // and continue below...
53 }
54 return false;
55})();
56}
57
58const USAGE = {
59 IGNORE: -1,
60 COPY: 1,
61 PROCESS: 2
62}
63
64function __determineUsage(_this, context){ // this==FileInfo. Done like this to keep functionality private.
65
66 // based on filename / location / whatev's, work out what renderers will be involved.
67 var chain = plugin_api.buildRenderChain(_this.path, context.config);
68 _this.renderers = chain.renderers;
69
70 // make the layout & partial relPaths look like: _layouts/xyz.html
71 // (NB: This is done b/c LayoutPath can actually be *anywhere*. So, this is a normalisation process,
72 // so that we can compare layout files against theme layout files)
73 if (!_this.isTheme && _this.isLayout)
74 _this.relPath = path.join("_layouts", path.relative(context.getLayoutPath(), _this.path));
75 if (!_this.isTheme && _this.isPartial)
76 _this.relPath = path.join("_partials", path.relative(context.getPartialsPath(), _this.path));
77 // make theme relPath relative to its theme folder
78 if (_this.isTheme)
79 _this.relPath = path.relative(context.getThemePath(), _this.path);
80
81
82 // calc the destination name
83 var pathofs = path.dirname(_this.relPath).toLowerCase(); // lowercase is best for all destination files
84 _this.destFilename = _fixupFilenameChars(chain.filename, context.config['filename_space_char']);
85 _this.destRelPath = path.join(pathofs, _this.destFilename); // NB: relPath is relative to SOURCE path, not basePath like everything in context obj
86 _this.destPath = path.join(context.getOutPath(), pathofs, _this.destFilename);
87
88 var inThemesRoot = _this.isInDir(context.getThemesRootPath());
89
90 var file_usage = USAGE.IGNORE; // -1=ignore as if it doesn't exist, 1=write to dest, 2=usable, so load it.
91
92 if (inThemesRoot && !_this.isTheme)
93 {
94 // this is NOT for the current theme. ALL files like this must be ignored!
95 // deliberately IGNORE anything in _themes folder that ISN"T part of current theme.
96 }
97 else if (_this.isTheme)
98 {
99 /*// theme files are placed in the root of the output folder
100 _this.themeRelPath = path.relative(context.getThemePath(), _this.path); // NB: relThemePath is relative to THEME path, not basePath nor SOURCE, like everything in context obj
101 var themepathofs = path.dirname(_this.themeRelPath).toLowerCase(); // this is the 'subfolder' of the theme folder that it exists in.
102
103 // remove traces of the theme path from the dest Paths.
104 _this.destRelPath = path.join(themepathofs, _this.destFilename); // NB: relPath is relative to SOURCE path, not basePath like everything in context obj
105 _this.destPath = path.join(context.getOutPath(), themepathofs, _this.destFilename);
106 */
107
108
109 // generally, files in the theme path are to be ignored.
110
111 // The exceptions are:
112 // - *if* they are _layouts or _partials, they get loaded
113 // - *if* they are in the the 'assets_path' AND won't overwrite an existing source file, they get copied.
114
115 if (_this.isPartial || _this.isLayout) {
116 file_usage = USAGE.PROCESS; // load it
117 }
118 else {
119 // work out if we can copy this file to the dest
120 var asset_paths = context.themeconfig.asset_paths;
121 if (asset_paths[0]=='*') {
122 if (_this.destRelPath.indexOf(path.sep)>0)
123 // in a subfolder. copy
124 file_usage = USAGE.COPY;
125 //else, skip it!
126 asset_paths = asset_paths.slice(1);
127 }
128
129 if (file_usage==USAGE.IGNORE)
130 asset_paths.forEach(function(folder) { // eg. 'css', 'images', etc
131 if (fs.isInDir(folder, _this.relPath) || // in a correct subfolder
132 folder == _this.relPath) // or explicitly mentioned
133 file_usage = USAGE.COPY;
134 });
135
136 /* These checks now done in context.addFile
137 // Now, triple check that if we're copying there's no equivalent file in the source tree:
138
139 // NB: There's an inherant bug here:
140 // - we generally use _fixupFilenameChars() transform a source file into all lowercase & to remove spaces, etc.
141 // - it *is* conceivable that "File 1.jpg" and "fiLE-1.jpg" will BOTH end up being written to the same location ('file1-1.jpg')
142 // (In this instance, it's a LAST MAN WINS)
143 // The FIX is to put all the files we're about to copy and then compare by destination. before actually copying
144 // NB: Other systems ALSO have this issue:
145 // (eg. index.tem.html and index.md and index.tex all become index.html)
146 // TODO. Fix
147 if (file_usage==USAGE.COPY && fs.fileExistsSync(path.join(context.getSourcePath(), _this.themeRelPath)))
148 // already exists in the source folder. DON'T use it.
149 file_usage = USAGE.IGNORE;
150 */
151 }
152
153 }
154 else
155 {
156 // Work out whether we need to process it through the plugin system at all.
157 // if not, just copy to the destination and be done with it!
158 if (_this.renderers.length==0 && !(_this.isLayout || _this.isPartial)) {
159 // there is nothing to transform this.
160 file_usage = USAGE.COPY; // copy it
161 }
162 else
163 {
164 file_usage = USAGE.PROCESS; // load it.
165 }
166 }
167 _this.usage = file_usage;
168}
169
170
171
172
173function FileInfo(_path, stats, context) {
174 this.path = _path;
175 this.stats = stats;
176
177 this.isLayout = this.isInDir(context.getLayoutsPath()) || this.isInDir(context.getThemeLayoutsPath());
178 this.isPartial = this.isInDir(context.getPartialsPath()) || this.isInDir(context.getThemePartialsPath());
179 this.isTheme = this.isInDir(context.getThemePath());
180
181 this.relPath = path.relative(context.getSourcePath(), this.path); // NB: relPath is relative to SOURCE path, not basePath like everything in context obj
182
183 this.fields = {content:null}; // eg. fields are added in here. eg. this.fields.title is a page's title (generally)
184 this.renderers = [];
185 __determineUsage(this, context);
186 l.logdIf(this, 2, "Fileinfo '" + this.relPath + "': isLayout("+this.isLayout+"), isPartial("+this.isPartial+"), isTheme("+this.isTheme+"), usage("+this.usage+")")
187}
188
189FileInfo.prototype.constructor = FileInfo;
190
191FileInfo.prototype.loadOrCopy = function(context) {
192 var _this = this;
193 return Promise.coroutine(function *() {
194
195 switch(_this.usage) {
196
197 case USAGE.PROCESS: // load it
198 l.vlog("Loading '" + _this.relPath+ "'...")
199 _this.fields.content = yield fs.readFile(_this.path);
200 return true;
201
202 case USAGE.COPY: // copy it to dest & do nothing more
203 l.log("Copying '" + _this.relPath + "' to '" + _this.destRelPath + "'...");
204 yield fs.copy(_this.path, _this.destPath, {preserveTimestamps:true});
205
206 // fall thru to to default...
207
208 default:
209 // Make sure we flag that we've finished with it
210 _this.fields.content = null;
211 _this.canSave = false; // changing the function to a false boolean is supported in this api.
212 return false; // don't try & process this file any more: it's dealt with
213 }
214 })();
215}
216
217FileInfo.prototype.canRender = function(context) {
218 return this.renderers.length>0 && !!this.fields && !!this.fields.content;
219};
220FileInfo.prototype.renderNext = function(context) {
221 if (this.canRender===false || !this.canRender(context))
222 return false;
223
224 var r = this.renderers[0];
225 if (r.name!='dummy') l.logdIf(this, 0, "Rendering '"+this.relPath+"' with '" + r.name + "'")
226 this.fields.content = r.render(this.fields, this, context);
227 if (r.name!='dummy') l.logdIf(this, 2, " Content of '"+this.relPath+"' is: " + this.fields.content.substr(0,300))
228 this.renderers.splice(0,1);
229 return this.canRender(context);
230};
231
232/*
233__renderStage(_this, label, funcName, context) {
234 if (_this.renderers.length>0 && _this.fields && _this.fields.content) {
235 // body...
236 l.vvlog(label+" '" + _this.relPath+"'...")
237 var r = _this.renderers[0];
238 l.logd(label+" '"+_this.relPath+"' with '" + r.name + "'")
239 _this.fields.content r[funcName].call(r, _this.fields.content, _this.fields, _this, context); // eg. r.render(...), or r.preRender(...)
240 }
241}
242
243FileInfo.prototype.preRender = function(context) {
244 return __renderStage(this, "Pre-Rendering", "preRender", context);
245};
246FileInfo.prototype.render = function(context) {
247 return __renderStage(this, "Rendering", "render", context);
248};
249FileInfo.prototype.postRender = function(context) {
250 return __renderStage(this, "Post-Rendering", "postRender", context);
251};
252*/
253
254FileInfo.prototype.canSave = function(context) {
255 // NB: This system allows this.canSave === false as well.
256 if (this.usage != USAGE.PROCESS)
257 return false;
258
259 if (!fs.isInDir(context.getOutPath(), this.destPath)) {
260 l.logd("Output folder is: " + context.getOutPath())
261 l.logw("Attempt to save a file outside the output folder!\nThe offending file is: " + this.destPath + "\n...and was loaded from: " + this.path);
262 return false;
263 }
264
265 return this.fields && ((!!this.fields.content||!!this.fields.template_content) || this.canRender());
266}
267
268FileInfo.prototype.save = function(context) {
269 var _this = this;
270
271 return Promise.coroutine(function *() {
272 if (_this.canSave===false || !_this.canSave(context)) {// NB: we allow plugins to set 'canSave = false'... but ONLY false! Useful for layouts & partials
273 l.log("Skipping save for '" + _this.relPath+"'")
274 return false;
275 }
276
277 yield fs.ensureDir(path.dirname(_this.destPath));
278 var template = !!_this.fields.template_content;
279 var contents = template ? _this.fields.template_content : _this.fields.content;
280 if ((yield _shouldKeepExisting(_this.destPath, contents))) {
281 l.log("Skipping unchanged '" + _this.relPath+"'")
282 return false;
283 }
284
285
286 l.log("Saving "+(template?"(templated) ":"") +"'" + _this.relPath+"' as '"+_this.destRelPath+"'...")
287 yield fs.writeFile(_this.destPath, contents);
288 l.vlog("Write OK")
289 return true;
290 })();
291};
292
293FileInfo.prototype.isRelInDir = function(relDir) { return fs.isInDir(relDir, this.relPath); }
294FileInfo.prototype.isInDir = function(dir) { return fs.isInDir(dir, this.path); }
295FileInfo.prototype.USAGE = USAGE;
296
297
298
299module.exports = FileInfo;
300
301