UNPKG

15.5 kBJavaScriptView Raw
1/*!
2 * load-templates <https://github.com/jonschlinkert/load-templates>
3 *
4 * Copyright (c) 2014 Jon Schlinkert, contributors.
5 * Licensed under the MIT License
6 */
7
8'use strict';
9
10// process.env.DEBUG = 'load-templates';
11
12var fs = require('fs');
13var arr = require('arr');
14var path = require('path');
15var debug = require('debug')('load-templates');
16var hasAny = require('has-any');
17var hasAnyDeep = require('has-any-deep');
18var mapFiles = require('map-files');
19var matter = require('gray-matter');
20var omit = require('omit-keys');
21var omitEmpty = require('omit-empty');
22var reduce = require('reduce-object');
23var typeOf = require('kind-of');
24var utils = require('./lib/utils');
25
26
27/**
28 * Initialize a new `Loader`
29 *
30 * ```js
31 * var loader = new Loader();
32 * ```
33 *
34 * @class Loader
35 * @param {Object} `obj` Optionally pass an `options` object to initialize with.
36 * @api public
37 */
38
39function Loader(options) {
40 this.options = options || {};
41}
42
43
44/**
45 * Set or get an option.
46 *
47 * ```js
48 * loader.option('a', true)
49 * loader.option('a')
50 * // => true
51 * ```
52 *
53 * @param {String} `key` The option name.
54 * @param {*} `value` The value to set.
55 * @return {*|Object} Returns `value` if `key` is supplied, or `Loader` for chaining when an option is set.
56 * @api public
57 */
58
59Loader.prototype.option = function(key, value) {
60 var args = [].slice.call(arguments);
61
62 if (args.length === 1 && isString(key)) {
63 return this.options[key];
64 }
65
66 if (isObject(key)) {
67 merge.apply(merge, [this.options].concat(args));
68 return this;
69 }
70
71 this.options[key] = value;
72 return this;
73};
74
75
76/**
77 * Rename the key of a template object.
78 *
79 * Pass a custom `renameKey` function on the options to change
80 * how keys are renamed.
81 *
82 * @param {String} `key`
83 * @param {Object} `options`
84 * @return {Object}
85 */
86
87Loader.prototype.renameKey = function(key, options) {
88 debug('renaming key:', key);
89
90 var opts = merge({}, this.options, options);
91 if (opts.renameKey) {
92 return opts.renameKey(key, omit(opts, 'renameKey'));
93 }
94
95 return key;
96};
97
98
99/**
100 * Default function for reading any files resolved.
101 *
102 * Pass a custom `readFn` function on the options to change
103 * how files are read.
104 *
105 * @param {String} `filepath`
106 * @param {Object} `options`
107 * @return {Object}
108 */
109
110Loader.prototype.readFn = function(filepath, options) {
111 var opts = merge({ enc: 'utf8' }, this.options, options);
112
113 if (opts.readFn) {
114 return opts.readFn(filepath, options);
115 }
116
117 return fs.readFileSync(filepath, opts.enc);
118};
119
120
121/**
122 * Default function for parsing any files resolved.
123 *
124 * Pass a custom `parseFn` function on the options to change
125 * how files are parsed.
126 *
127 * @param {String} `filepath`
128 * @param {Object} `options`
129 * @return {Object}
130 */
131
132Loader.prototype.parseFn = function(str, options) {
133 var opts = merge({ autodetect: true }, this.options, options);
134 if (opts.noparse === true) {
135 return str;
136 }
137 if (opts.parseFn) {
138 return opts.parseFn(str, options);
139 }
140 return matter(str, omit(options, ['delims']));
141};
142
143
144/**
145 * Unless a custom parse function is passed, by default YAML
146 * front matter is parsed from the string in the `content`
147 * property.
148 *
149 * @param {Object} `value`
150 * @param {Object} `options`
151 * @return {Object}
152 */
153
154Loader.prototype.parseContent = function(obj, options) {
155 debug('parsing content', obj);
156 var copy = omit(obj, ['content']);
157 var o = {};
158
159 if (isString(o.content) && !hasOwn(o, 'orig')) {
160 var orig = o.content;
161 o = this.parseFn(o.content, options);
162 o.orig = orig;
163 }
164
165 merge(o, copy);
166 o._parsed = true;
167 return o;
168};
169
170
171/**
172 * Rename option keys the way [mapFiles] expects.
173 *
174 * @param {String} `patterns`
175 * @param {Object} `options`
176 * @return {Object}
177 */
178
179Loader.prototype.mapFiles = function(patterns, options) {
180 return mapFiles(patterns, merge({}, options, {
181 name: this.renameKey,
182 read: this.readFn
183 }));
184};
185
186
187/**
188 * Map files resolved from glob patterns or file paths.
189 *
190 *
191 * @param {String|Array} `patterns`
192 * @param {Object} `options`
193 * @return {Object}
194 */
195
196Loader.prototype.parseFiles = function(patterns, options) {
197 debug('mapping files:', patterns);
198
199 var files = this.mapFiles(patterns, options);
200
201 return reduce(files, function (acc, value, key) {
202 debug('reducing file: %s', key, value);
203
204 if (isString(value)) {
205 value = this.parseFn(value);
206 value.path = value.path || key;
207 }
208
209 value._parsed = true;
210 value._mappedFile = true;
211 acc[key] = value;
212 return acc;
213 }.bind(this), {});
214};
215
216
217/**
218 * First arg is a file path or glob pattern.
219 *
220 * ```js
221 * loader('a/b/c.md', ...);
222 * loader('a/b/*.md', ...);
223 * ```
224 *
225 * @param {String} `key`
226 * @param {Object} `value`
227 * @return {Object}
228 */
229
230Loader.prototype.normalizeFiles = function(patterns, locals, options) {
231 debug('normalizing patterns: %s', patterns);
232 options = options || {};
233 locals = locals || {};
234
235 merge(locals, locals.locals, options.locals);
236 merge(options, locals.options);
237
238 var files = this.parseFiles(patterns, options);
239 if (files && Object.keys(files).length === 0) {
240 return null;
241 }
242
243 return reduce(files, function (acc, value, key) {
244 debug('reducing normalized file: %s', key);
245
246 value.options = utils.flattenOptions(options);
247 value.locals = utils.flattenLocals(locals);
248 acc[key] = value;
249 return acc;
250 }, {});
251};
252
253
254/**
255 * When the first arg is an array, assume it's glob
256 * patterns or file paths.
257 *
258 * ```js
259 * loader(['a/b/c.md', 'a/b/*.md']);
260 * loader(['a/b/c.md', 'a/b/*.md'], {a: 'b'}, {foo: true});
261 * ```
262 *
263 * @param {Object} `patterns` Template object
264 * @param {Object} `locals` Possibly locals, with `options` property
265 * @return {Object} `options` Possibly options
266 */
267
268Loader.prototype.normalizeArray = function(patterns, locals, options) {
269 debug('normalizing array:', patterns);
270 return this.normalizeFiles(patterns, locals, options);
271};
272
273
274/**
275 * First value is a string, second value is a string or
276 * an object.
277 *
278 * {%= docs("dev-normalize-string") %}
279 *
280 * @param {Object} `value` Always an object.
281 * @param {Object} `locals` Always an object.
282 * @param {Object} `options` Always an object.
283 * @return {Object} Returns a normalized object.
284 */
285
286Loader.prototype.normalizeString = function(key, value, locals, options) {
287 debug('normalizing string: %s', key, value);
288 var args = [].slice.call(arguments, 1);
289 var objects = arr.objects(arguments);
290 var props = utils.siftProps.apply(this, args);
291 var opts = options || props.options;
292 var locs = props.locals;
293 var files;
294 var root = {};
295 var opt = {};
296 var o = {};
297 o[key] = {};
298
299 // If only `value` is defined
300 if (value == null) {
301
302 // check if `key` is a file path
303 files = this.normalizeFiles(key);
304 if (files != null) {
305 return files;
306
307 // if not, add a heuristic
308 } else {
309 o[key]._hasPath = true;
310 o[key].path = o[key].path || key;
311 return o;
312 }
313 }
314
315 if ((value && isObject(value)) || objects == null) {
316 debug('[value] s1o1: %s, %j', key, value);
317 files = this.normalizeFiles(key, value, locals, options);
318
319 if (files != null) {
320 return files;
321 } else {
322 debug('[value] s1o2: %s, %j', key, value);
323 root = utils.pickRoot(value);
324 var loc = {};
325 opt = {};
326
327 merge(loc, utils.pickLocals(value));
328 merge(loc, locals);
329
330 merge(root, utils.pickRoot(loc));
331
332 merge(opt, loc.options);
333 merge(opt, value.options);
334 merge(opt, options);
335
336 merge(root, utils.pickRoot(opt));
337
338 o[key] = root;
339 o[key].locals = loc;
340 o[key].options = opt;
341
342 var content = value && value.content;
343 if (o[key].content == null && content != null) {
344 o[key].content = content;
345 }
346 }
347 }
348
349 if (hasOwn(opt, '_hasPath') && opt._hasPath === false) {
350 o[key].path = null;
351 } else {
352 o[key].path = value.path || key;
353 }
354
355 if (value && isString(value)) {
356 debug('[value] string: %s, %s', key, value);
357
358 root = utils.pickRoot(locals);
359 o[key] = root;
360 o[key].content = value;
361 o[key].path = o[key].path = key;
362
363 o[key]._s1s2 = true;
364 if (objects == null) {
365 return o;
366 }
367 }
368
369 if (locals && isObject(locals)) {
370 debug('[value] string: %s, %s', key, value);
371 merge(locs, locals.locals);
372 merge(opts, locals.options);
373 o[key]._s1s2o1 = true;
374 }
375
376 if (options && isObject(options)) {
377 debug('[value] string: %s, %s', key, value);
378 merge(opts, options);
379 o[key]._s1s2o1o2 = true;
380 }
381
382 opt = utils.flattenOptions(opts);
383 merge(opt, o[key].options);
384 o[key].options = opt;
385
386 locs = omit(locs, 'options');
387 o[key].locals = utils.flattenLocals(locs);
388 return o;
389};
390
391
392/**
393 * Normalize objects that have `rootKeys` directly on
394 * the root of the object.
395 *
396 * **Example**
397 *
398 * ```js
399 * {path: 'a/b/c.md', content: 'this is content.'}
400 * ```
401 *
402 * @param {Object} `value` Always an object.
403 * @param {Object} `locals` Always an object.
404 * @param {Object} `options` Always an object.
405 * @return {Object} Returns a normalized object.
406 */
407
408Loader.prototype.normalizeShallowObject = function(value, locals, options) {
409 debug('normalizing shallow object: %j', value);
410 var o = utils.siftLocals(value);
411 o.options = merge({}, options, o.options);
412 o.locals = merge({}, locals, o.locals);
413 return o;
414};
415
416
417/**
418 * Normalize nested templates that have the following pattern:
419 *
420 * ```js
421 * => {'a/b/c.md': {path: 'a/b/c.md', content: 'this is content.'}}
422 * ```
423 * or:
424 *
425 * ```js
426 * { 'a/b/a.md': {path: 'a/b/a.md', content: 'this is content.'},
427 * 'a/b/b.md': {path: 'a/b/b.md', content: 'this is content.'},
428 * 'a/b/c.md': {path: 'a/b/c.md', content: 'this is content.'} }
429 *```
430 */
431
432Loader.prototype.normalizeDeepObject = function(obj, locals, options) {
433 debug('normalizing deep object: %j', obj);
434
435 return reduce(obj, function (acc, value, key) {
436 acc[key] = this.normalizeShallowObject(value, locals, options);
437 return acc;
438 }.bind(this), {});
439};
440
441
442/**
443 * When the first arg is an object, all arguments
444 * should be objects.
445 *
446 * ```js
447 * loader({'a/b/c.md', ...});
448 *
449 * // or
450 * loader({path: 'a/b/c.md', ...});
451 * ```
452 *
453 * @param {Object} `object` Template object
454 * @param {Object} `locals` Possibly locals, with `options` property
455 * @return {Object} `options` Possibly options
456 */
457
458Loader.prototype.normalizeObject = function(o) {
459 debug('normalizing object: %j', o);
460
461 var args = [].slice.call(arguments);
462 var locals1 = utils.pickLocals(args[1]);
463 var locals2 = utils.pickLocals(args[2]);
464 var val;
465
466 var opts = args.length === 3 ? locals2 : {};
467
468 if (hasAny(o, ['path', 'content'])) {
469 val = this.normalizeShallowObject(o, locals1, opts);
470 return createKeyFromPath(val.path, val);
471 }
472
473 if (hasAnyDeep(o, ['path', 'content'])) {
474 val = this.normalizeDeepObject(o, locals1, opts);
475 return createPathFromStringKey(val);
476 }
477
478 throw new Error('Invalid template object. Must' +
479 'have a `path` or `content` property.');
480};
481
482
483/**
484 * When the first arg is an array, assume it's glob
485 * patterns or file paths.
486 *
487 * ```js
488 * loader(['a/b/c.md', 'a/b/*.md']);
489 * ```
490 *
491 * @param {Object} `patterns` Template object
492 * @param {Object} `locals` Possibly locals, with `options` property
493 * @return {Object} `options` Possibly options
494 */
495
496Loader.prototype.normalizeFunction = function(fn) {
497 debug('normalizing fn:', arguments);
498 return fn.apply(this, arguments);
499};
500
501
502/**
503 * Select the template normalization function to start
504 * with based on the first argument passed.
505 */
506
507Loader.prototype._format = function() {
508 var args = [].slice.call(arguments);
509 debug('normalize format', args);
510
511 switch (typeOf(args[0])) {
512 case 'array':
513 return this.normalizeArray.apply(this, args);
514 case 'string':
515 return this.normalizeString.apply(this, args);
516 case 'object':
517 return this.normalizeObject.apply(this, args);
518 case 'function':
519 return this.normalizeFunction.apply(this, args);
520 default:
521 return {};
522 }
523};
524
525
526/**
527 * Final normalization step to remove empty values and rename
528 * the object key. By now the template should be _mostly_
529 * loaderd.
530 *
531 * @param {Object} `object` Template object
532 * @return {Object}
533 */
534
535Loader.prototype.load = function() {
536 debug('loader', options);
537
538 var tmpl = this._format.apply(this, arguments);
539 var options = this.options || {};
540
541 return reduce(tmpl, function (acc, value, key) {
542 if (value && Object.keys(value).length === 0) {
543 return acc;
544 }
545 // Normalize the template
546 this.normalize(options, acc, value, key);
547 return acc;
548 }.bind(this), {});
549};
550
551
552/**
553 * Base normalize method, abstracted to make it easier to
554 * pass in custom methods.
555 *
556 * @param {Object} `options`
557 * @param {Object} `acc`
558 * @param {String|Object} `value`
559 * @param {String} `key`
560 * @return {Object} Normalized template object.
561 */
562
563Loader.prototype.normalize = function (options, acc, value, key) {
564 debug('normalize: %s, %value', key);
565 if (options && options.normalize) {
566 return options.normalize(acc, value, key);
567 }
568 value.ext = value.ext || path.extname(value.path);
569
570 var parsed = this.parseContent(value, options);
571 merge(value, parsed);
572
573 // Cleanup
574 value = cleanupProps(value, options);
575 value.content = value.content || null;
576
577 // Rename the object key
578 acc[this.renameKey(key, options)] = value;
579 return acc;
580};
581
582/**
583 * Clean up some properties before return the final
584 * normalized template object.
585 *
586 * @param {Object} `template`
587 * @param {Object} `options`
588 * @return {Object}
589 */
590
591function cleanupProps(template, options) {
592 if (template.content === template.orig) {
593 template = omit(template, 'orig');
594 }
595 if (options.debug == null) {
596 template = omit(template, utils.heuristics);
597 }
598 return omitEmpty(template);
599}
600
601
602/**
603 * Create a `path` property from the template object's key.
604 *
605 * If we detected a `path` property directly on the object that was
606 * passed, this means that the object is not formatted as a key/value
607 * pair the way we want our normalized templates.
608 *
609 * ```js
610 * // before
611 * loader({path: 'a/b/c.md', content: 'this is foo'});
612 *
613 * // after
614 * loader('a/b/c.md': {path: 'a/b/c.md', content: 'this is foo'});
615 * ```
616 *
617 * @param {String} `filepath`
618 * @param {Object} `value`
619 * @return {Object}
620 * @api private
621 */
622
623function createKeyFromPath(filepath, value) {
624 var o = {};
625 o[filepath] = value;
626 return o;
627}
628
629/**
630 * Create the `path` property from the string
631 * passed in the first arg. This is only used
632 * when the second arg is a string.
633 *
634 * ```js
635 * loader('abc', {content: 'this is content'});
636 * //=> normalize('abc', {path: 'abc', content: 'this is content'});
637 * ```
638 *
639 * @param {Object} `obj`
640 * @return {Object}
641 */
642
643function createPathFromStringKey(o) {
644 for (var key in o) {
645 if (hasOwn(o, key)) {
646 o[key].path = o[key].path || key;
647 }
648 }
649 return o;
650}
651
652/**
653 * Merge util. This is temporarily until benchmarks are done
654 * so we can easily swap in a different function.
655 *
656 * @param {Object} `obj`
657 * @return {Object}
658 * @api private
659 */
660
661function merge() {
662 return utils.extend.apply(null, arguments);
663}
664
665/**
666 * Utilities for returning the native `typeof` a value.
667 *
668 * @api private
669 */
670
671function isString(val) {
672 return typeOf(val) === 'string';
673}
674
675function isObject(val) {
676 return typeOf(val) === 'object';
677}
678
679function hasOwn(o, prop) {
680 return {}.hasOwnProperty.call(o, prop);
681}
682
683
684/**
685 * Expose `loader`
686 *
687 * @type {Object}
688 */
689
690module.exports = Loader;