UNPKG

13.2 kBJavaScriptView Raw
1/**
2 * @license MIT
3 * Copyright (c) 2016 Craig Monro (cmroanirgo)
4 **/
5
6/*
7* This is the public api for plugins
8*/
9"use strict";
10
11var l = require('ergo-utils').log.module('ergo-api-plugin');
12var _ = require('ergo-utils')._;
13var fs = require('ergo-utils').fs.extend(require('fs-extra'));
14var path = require('path');
15var Promise = require('bluebird');
16
17//l.color = l._colors.Dim+l._colors.FgRed;
18
19// Yes, globals. Live with it. I like using Goto and Jumps too. I like uneven indents and inconsistent strings.
20// I like redundant semi colons;
21var _renderers = [];
22
23function Renderer(name, options) {
24 options = options || {};
25 if (_.isEmptyString(name))
26 throw new Error("A 'name' parameter is required when registering a plugin");
27 if (!_.isFunction(options.renderFn))
28 throw new Error("'renderFn' callback is required for plugin " + name)
29 this.name = name;
30 this.preRender = []; // an array of { name, priority }
31 this.postRender = []; // an array of { name, priority }
32
33
34 // re-applies the configuration for this renderer.
35 // This gives websites the ability to completely change default actions
36 this.options = _.extend( {
37 priority: 50
38 , binary: false
39 // , calcExtensionFn: function(origFilename) { return _api.DEF_EXTENSION; }
40 // , extensions: []
41 // , renderFn
42 // , reconfigureFn:
43 // , saveFn
44 }, options);
45 this.plugin_options = {}; // specfic options for the renderer. Set by 'plugin_options' in config.js
46
47 this.extensions = _.toRealArray(this.options.extensions || "", ",").map(_normaliseExt);
48 //this.render = this.options.renderFn;
49}
50
51Renderer.prototype.constructor = Renderer;
52
53function __addToRenderList(list, name, priority) {
54 var pre = {
55 name: name
56 , priority: priority || 50
57 }
58 list.push(pre);
59 list.sort(function(a,b) { return b.priority - a.priority; });
60}
61Renderer.prototype.addPreRenderer = function(name, priority) {
62 if (!_findRendererByName(name))
63 // maybe it hasn't been registered yet?
64 l.logw(this.name + ".addPreRenderer(). '" + name + "' not found in list of renderers... yet");
65 __addToRenderList(this.preRender, name, priority);
66 return this; // for chaining
67};
68Renderer.prototype.addPostRenderer = function(name, priority) {
69 if (!_findRendererByName(name))
70 // maybe it hasn't been registered yet?
71 l.logw(this.name + ".addPostRenderer(). '" + name + "' not found in list of renderers... yet");
72 __addToRenderList(this.postRender, name, priority);
73 return this; // for chaining
74};
75
76Renderer.prototype.calcExtension = function(filename, currentExt) {
77 if (this.options.calcExtensionFn)
78 return this.options.calcExtensionFn.call(this, filename, currentExt);
79 return _api.DEF_EXTENSION; // by default, just return 'html'
80}
81
82Renderer.prototype.reconfigure = function(plugin_options) {
83 // this func is called just after it's file has been 'require' -ed. in _loadPlugin
84 l.vvlog("Renderer settings for '"+this.name+"' set to: " + l.dump(plugin_options));
85 this.plugin_options = plugin_options || {};
86 if (this.options.reconfigureFn)
87 this.options.reconfigureFn.call(this, plugin_options);
88
89 return this; // for chaining
90};
91
92Renderer.prototype.render = function(fields, fileInfo, context) {
93 if (this.name!='dummy') l.vvlogd('Rendering ' + this.name)
94 fields.content = this.options.binary ? fields.content : fields.content.toString();
95 return this.options.renderFn.call(this, fields.content, fields, fileInfo, context);
96}
97
98
99Renderer.prototype.save = function(context) {
100 if (this.options.saveFn)
101 return Promise.resolve(this.options.saveFn.call(this, context));
102 return Promise.resolve(true);
103};
104
105
106
107
108
109
110
111
112
113
114function _normaliseExt(ext) { // we don't use '.' in our extension info... but some might leak in here and there
115 if (ext && ext.length && ext[0]=='.')
116 return ext.substr(1);
117 return ext;
118}
119
120function _findRendererByNameIndex(name) {
121 return _renderers.findIndex(function(r) { return r.name==name; });
122}
123function _findRendererByName(name) {
124 return _renderers.find(function(r) { return r.name==name; }) || null;
125}
126
127function _findRendererByExt(ext) {
128 ext = _normaliseExt(ext);
129 // NB: since we've already sorted the plugins by priority then we only need find the first plugin
130 return _renderers.find(function(r) {
131 return r.extensions.indexOf(ext)>-1;
132 }) || null; // return null, rather than undefined
133}
134
135function _getExtensions(filename) {
136 // given a filename, splits into components, by 'dots'
137 // this can given false positives,eg. i.like.to.use.dots.in.myfilename.html
138 var sections = path.basename(filename).split('.'); // strip off any path info & seperate
139 return sections.slice(1); // the first one is the base filename, so ignore it, the rest are ext's
140}
141
142function _addRenderer(name, options) {
143 if (_findRendererByName(name))
144 throw new Error("Plugin already defined for " + name);
145 var newRenderer = new Renderer(name, options);
146 _renderers.push(newRenderer);
147
148 l.logd("Added renderer: " + name)
149 l.vvlogd("renderer "+name+" is: " + l.dump(newRenderer))
150
151 // makes find/searching consistent if sorted by priorty now
152 // NB: If someone goes & changes priority AFTER being created then this barfs.
153 // We assume ppl will call resort() if needed. (eg AFTER a reconfigure)
154 _api.resort(); // which actually resorts the renderers.
155
156 return newRenderer;
157}
158
159function _buildRenderChain(filename, configObj) {
160 // various scenarios:
161 // blogpost.tex:
162 // simple => textile => (save)
163 // or, if moustache & html renderers added:
164 // moustache => textile => minify => (save)
165 // somecss.less:
166 // => (save)
167 // or, if less & minifier installed :
168 // less => cssminify => (save)
169 // someimage.jpg: <===== These are untested
170 // => (save)
171 // or, if some watermarking thing present
172 // watermark => (save)
173 // somefile.tem.xyz
174 // simepltags => xyz filter => (save)
175
176 // So, we build a list starting from the 'left-most' extension
177 filename = path.basename(filename); // we're not interested in retaining folder structure of the original filename
178 l.vvlogd("building render chain for '" + filename+ "'");
179 var basefilename = filename.substr(0, filename.indexOf('.'))
180 var exts = _getExtensions(filename);
181 // l.vvlogd("Extensions are: " + exts)
182 var chain = [];
183
184 // NB:
185 // markdown & textile renderers BOTH use "simple" as a preRenderer,
186 // so "simple" is implicitly included here, when .tex is used.
187 var nextExt = exts.slice(-1) || _api.DEF_EXTENSION;
188 for (var e=0; e<exts.length; e++) {
189 // find the best renderer for this extension
190 var ext = exts[e];
191 var r = _findRendererByExt(ext);
192 if (!r) {
193 l.vvlogd("Failed to find renderer for '"+ext+"' in '"+filename+"'. Skipping...")
194 continue;
195 }
196 if (chain.indexOf(r)<0) {
197 l.vvlogd("Chaining renderer '"+ext+"' in '"+filename+"'")
198 chain.push(r);
199 }
200 nextExt = r.calcExtension(filename, ext); // basefilename+'.'+(exts.slice(0,e+1)).join('.'))
201 l.vvlogd("calcExtension("+filename+","+ext+") ==> '"+nextExt+"'")
202 var nextAt = exts.findIndex(function(ex) { return ex==nextExt;});
203 if (nextAt<0 && !!_findRendererByExt(nextExt)) { // then, we should add this extension now... we'll need it
204 // this allows "blogpost.tex" to then become "blogpost.tex.html" and allow a minifier
205 l.vlogd("Added missing link '"+nextExt+"'' to '" + filename + "'")
206 exts.push(nextExt);
207 }
208 }
209
210
211 var ordered = [];
212 // Now we have the main 'renderers' required, we walk the complete render tree and generate an in-order list
213 function _walk(r) {
214 function _fetchAndWalk(obj) { // obj is { name, priority }
215 var renderer = obj.renderer || _findRendererByName(obj.name);
216 if (!renderer)
217 throw new Error("Failed to ever find the renderer named '"+obj.name+"'");
218 if (!obj.renderer) //save it for later
219 obj.renderer = renderer;
220 _walk(renderer);
221 }
222 if (ordered.indexOf(r)<0) { // check that we've already not added this renderer.
223 // walk the pre-render list
224 r.preRender.forEach(_fetchAndWalk)
225 ordered.push(r); // finally push this renderer
226 // walk the post-render list
227 r.postRender.forEach(_fetchAndWalk)
228 }
229 }
230 chain.forEach(_walk);
231
232 var finalFilename = filename;
233 if (ordered.length>0) // only futz with the 'extensions' if we have a valid render chain...? Even then, this might NOT be a good idea!
234 finalFilename = basefilename+'.'+nextExt;
235 l.vvlog("Render chain for '" + filename+ "' is: " + l.dump(ordered.map(function(r) { return r.name; })));
236 return { renderers:ordered, filename: finalFilename };
237}
238
239function _reconfigurePlugin(renderer, context) {
240 if (!!renderer &&
241 _.isDefined(context.config['plugin_options']) &&
242 _.isDefined(context.config.plugin_options[renderer.name]))
243 {
244 renderer.reconfigure(context.config.plugin_options[renderer.name]);
245 }
246 return renderer;
247}
248
249function _loadDefaultPlugins(context) {
250 var default_plugins = [ // the order of these is not important... but if they're not in this order, a few warnings will appear
251 _api.RENDERER_ADD_DATA
252 , _api.RENDERER_HEADER_READ
253 , _api.RENDERER_TAG
254 , _api.RENDERER_TEMPLATE_MAN
255 , _api.RENDERER_TEXTILE
256 , _api.RENDERER_MARKDOWN
257 ];
258 l.vlog("Loading default plugins...");
259 var p = [];
260 default_plugins.forEach(function(def_name) {
261 p.push(_api.loadPlugin(def_name, context));
262 });
263 return p;
264}
265
266
267var dummy_renderer = null;
268
269function _loadplugin(name, context) {
270 if (!dummy_renderer) {
271 // add the dummy renderer on the first plugin load. it MUST be present for the build system
272 dummy_renderer = _addRenderer("dummy", { priority:100, renderFn: function(text) { return text; } } );
273 }
274 if (name=="{default}" || name=="default")
275 return _loadDefaultPlugins(context);
276
277 l.vlog("Loading plugin '"+name+"'");
278 var userPath = context.getPluginsPath();
279 var renderer = _findRendererByName(name);
280 if (!!renderer) {
281 // unsure if an error shouldn't be raised if already loaded!
282 //l.logw("Unexpected. The plugin ("+name+") has already been loaded. The existing plugin will be used")
283 //_reconfigurePlugin(renderer, context); // We definitely SHOULDN'T reconfigure.... probably! ;)
284
285 // Other problems:
286 // user might specify in config:
287 // plugins: "default,textile", which will load the textile plugin again!
288 l.vlogw("Plugin '"+name+"' has already been loaded")
289 return renderer; // already loaded & configured.
290 }
291
292 if (fs.dirExistsSync(userPath)) {
293 var userLib = path.join(userPath, name);
294 try {
295 require(userLib)
296 }
297 catch (e) {
298 // we expect to fail to load plugins... but generate a *real* error if there is a file in there
299 if (fs.fileExistsSync(userLib+'.js')) {
300 l.loge("Error loading plugin '" + name+ "' in '"+userPath+"':\n"+_.niceStackTrace(e))
301 return null;
302 }
303 }
304
305 // try & load our plugin
306 renderer = _findRendererByName(name);
307 }
308
309 if (!renderer) {
310 // else fall thru to trying to load it from our in-built plugins.
311 var inbuiltLib = path.join(path.dirname(__dirname), 'lib','plugins', name);
312 try {
313 require(inbuiltLib)
314 }
315 catch (e) {
316 // we expect to fail to load plugins... but generate a *real* error if there is a file in there
317 if (!fs.fileExistsSync(inbuiltLib+'.js'))
318 l.loge("Cannot find plugin '" + name+ "' in '"+userPath+"' or from internal library")
319 else
320 l.loge("Error loading plugin '"+name+"' from internal library:\n" + _.niceStackTrace(e))
321 throw e;
322 }
323 renderer = _findRendererByName(name);
324 }
325
326 _reconfigurePlugin(renderer, context);
327 return renderer;
328
329}
330
331function _saveAll(context) {
332 return Promise.coroutine( function *() {
333 for (var i=0; i<_renderers.length; i++) {
334 yield _renderers[i].save(context);
335 }
336 return true;
337 })();
338}
339
340
341var _api = {
342 // some common names
343 DEF_EXTENSION: "html" // This CAN be changed by configuration at run-time, through the config.default_extension property.
344
345 // These render names only here, because it's inbuilt & someone might have a different library to swap in
346 , RENDERER_TAG: "usematch"
347 , RENDERER_TEMPLATE_MAN: "template_man"
348 , RENDERER_HEADER_READ: "header_read"
349 , RENDERER_ADD_DATA: "add_data"
350 , RENDERER_TEXTILE: "textile"
351 , RENDERER_MARKDOWN: "marked"
352 //, RENDERER_DUMMY: "dummy" // keep undocumented
353
354 //
355 , addRenderer: _addRenderer
356 , removeRenderer: function(name) {
357 var i = _findRendererByNameIndex(name);
358 if (i<0) return null;
359 var prevRenderer = _renderers[i];
360 _renderers.splice(i,1)
361 return prevRenderer;
362 }
363 , findRendererByName: _findRendererByName
364 //, getRenderers: function() {
365 // return _renderers.slice(); // return a *copy*
366 //}
367 , resort: function() {
368 _renderers.sort(function(a,b) { return b.priority - a.priority; });
369 }
370 , changeDefaultExtension: function(defExt) {
371 var defExt = _normaliseExt(defExt);
372 l.vlog("Changing default Extension: " + defExt)
373 if (!_.isEmptyString(defExt) && defExt!=_api.DEF_EXTENSION) {
374 l.vlog("Changed default extension to '" +defExt+ "'")
375 _api.DEF_EXTENSION = defExt;
376 if (_renderers.length) {
377 l.logw("Changed default extension after plugins have loaded. Expect the unexpected");
378 return -1;
379 }
380 return true;
381 }
382 return false;
383
384 }
385 , buildRenderChain: _buildRenderChain
386 , loadPlugin: _loadplugin
387
388 //, renderAll: _renderAll
389 , saveAll: _saveAll
390};
391
392
393module.exports = _api;
394