Passed
Push — master ( beba88...91d9d9 )
by Björn
02:26
created

lib/save.js (1 issue)

1
var extend        = require('deep-extend');
2
var curl          = require('curl'); // 0.1.4
3
var queryString   = require('query-string'); // 4.3.4
4
var fm            = require('front-matter');
5
var fs            = require('fs-extra');
6
var glob          = require('glob');
7
var path          = require('path');
8
var chalk         = require('chalk');
9
var through       = require('through2');
10
var slash         = require('slash');
11
var jsonfile      = require('jsonfile');
12
var Panini        = require('panini'); 
13
var superCollider = require('supercollider');
14
var marked        = require('marked');
15
16
// import Foundation Docs handlebar helpers
17
require('./vendor/handlebars');
18
19
//import Patternlibrary classes
20
var PatternlibraryConfig      = require('./patternlibrary/config');
21
22
var PatternlibraryFileStream  = require('./patternlibrary/filestream');
23
var PatternlibraryDocParser   = require('./patternlibrary/doc-parser');
24
var PatternlibraryDocRenderer = require('./patternlibrary/doc-renderer');
25
26
var PatternlibraryMiddleware  = require('./patternlibrary/middleware');
27
28
29
/** @var string patternlibrary version */
30
const PATTERNLIBRARY_VERSION = require('../package.json').version; // '0.0.1';
31
32
/**
33
 * Patternlibrary Generator
34
 * 
35
 * @package Patternlibrary
36
 * @namespace Patternlibrary
37
 * @author Björn Bartels <[email protected]>
38
 */
39
class Patternlibrary {
40
    
41
    /**
42
     * Initializes an instance of Patternlibrary.
43
     * @constructor
44
     * @param {object} options - Configuration options to use.
45
     */
46
    constructor ( options ) {
47
        
48
        /** @var string */
49
        this.patternlibrary_version = PATTERNLIBRARY_VERSION;
50
        
51
        /** @var object */
52
        this.options = extend(this.defaults, options);
53
        
54
        if (!this.options.patterns) {
55
            throw new Error('Patternlibrary error: you must specify the directories for patterns.');
56
        }
57
58
        if (!this.options.root) {
59
            throw new Error('Patternlibrary error: you must specify a root directory.')
60
        }
61
        if (!this.options.layouts) {
62
            throw new Error('Patternlibrary error: you must specify a layouts directory.')
63
        }
64
        if (!this.options.partials) {
65
            throw new Error('Patternlibrary error: you must specify a partials directory.')
66
        }
67
68
        /** @var object */
69
        this.layouts = {};
70
71
        /** @var object */
72
        this.data = {};
73
        
74
        this._init();
75
        
76
    }
77
    
78
    /**
79
     * initialize pattern-library object
80
     */
81
    _init ( ) {
82
        
83
        this.log('setup pattern-library');
84
        
85
        /** @var PatternlibraryDocParser */
86
        this.DocParser = null;
87
88
        /** @var PatternlibraryRenderer */
89
        this.DocRenderer = null;
90
        
91
        /** @var PatternlibraryMiddleware */
92
        this.Middleware = null;
93
94
        /** @var PatternlibraryFileStream */
95
        this.FileStream = null;
96
97
        
98
        /** @var PatternlibraryConfig */
99
        this.Config = new PatternlibraryConfig(this.options);
100
        
101
        
102
        this._initDestination();
103
        
104
        this._initPanini();
105
106
        /** @var Handlebars */
107
        this.Handlebars = this.Panini.Handlebars;
108
        
109
        this._initSupercollider();
110
        
111
    }
112
    
113
    
114
    
115
    
116
    /**
117
     * retrieve middleware instance
118
     * 
119
     * @param boolean reset force to initialize new middleware instance
120
     * @return PatternlibraryMiddleware
121
     */
122
    getMiddleware ( reset ) {
123
        
124
        if ( reset || (this.Middleware === null) ) {
125
            this.log('initialize middleware hook');
126
            this.Middleware = new PatternlibraryMiddleware(
127
                this.Config.get('plugins').middleware
128
            );
129
            this.Middleware.bind(this);
130
        }
131
        return (this.Middleware);
132
        
133
    }
134
    
135
136
    /**
137
     * reset middleware instance
138
     * 
139
     * @return Patternlibrary
140
     */
141
    resetMiddleware ( ) {
142
        
143
        this.Middleware = null;
144
        this.getMiddleware( true );
145
        return (this);
146
        
147
    }
148
    
149
    
150
    
151
    /**
152
     * retrieve (vinyl/gulp) file-stream instance
153
     * 
154
     * @param boolean reset force to initialize new file-stream instance
155
     * @return PatternlibraryFileStream
156
     */
157
    getFileStream ( reset ) {
158
        
159
        if ( reset || (this.FileStream === null) ) {
160
            this.log('initialize file-stream hook');
161
            this.FileStream = new PatternlibraryFileStream(
162
                this.Config.get('plugins').filestream
163
            );
164
            this.FileStream.bind(this);
165
        }
166
        return (this.FileStream);
167
        
168
    }
169
    
170
171
    /**
172
     * reset file-stream instance
173
     * 
174
     * @return Patternlibrary
175
     */
176
    resetFileStream ( ) {
177
        
178
        this.FileStream = null;
179
        this.getFileStream( true );
180
        return (this);
181
        
182
    }
183
    
184
    
185
    
186
    /**
187
     * retrieve doc-parser instance
188
     * 
189
     * @param boolean reset force to initialize new file-stream instance
190
     * @return PatternlibraryDocParser
191
     */
192
    getDocParser ( reset ) {
193
        
194
        if ( reset || (this.DocParser === null) ) {
195
            this.log('initialize documentaion info parser');
196
            this.DocParser = new PatternlibraryDocParser(
197
                this.Config.get('plugins').docparser
198
            );
199
            this.DocParser.bind(this);
200
        }
201
        return (this.DocParser);
202
        
203
    }
204
    
205
206
    /**
207
     * reset doc-parser instance
208
     * 
209
     * @return Patternlibrary
210
     */
211
    resetDocParser ( ) {
212
        
213
        this.DocParser = null;
214
        this.getDocParser( true );
215
        return (this);
216
        
217
    }
218
    
219
    
220
    
221
    /**
222
     * retrieve doc-renderer instance
223
     * 
224
     * @param boolean reset force to initialize new file-stream instance
225
     * @return PatternlibraryDocRenderer
226
     */
227
    getDocRenderer ( reset ) {
228
        
229
        if ( reset || (this.DocRenderer === null) ) {
230
            this.log('initialize documentation pages generator');
231
            this.DocRenderer = new PatternlibraryDocRenderer(
232
                this.Config.get('plugins').docrenderer
233
            );
234
            this.DocRenderer.bind(this);
235
        }
236
        return (this.DocRenderer);
237
        
238
    }
239
    
240
241
    /**
242
     * reset doc-renderer instance
243
     * 
244
     * @return Patternlibrary
245
     */
246
    resetDocRenderer ( ) {
247
        
248
        this.DocRenderer = null;
249
        this.getDocRenderer( true );
250
        return (this);
251
        
252
    }
253
    
254
    
255
    
256
    
257
258
    /**
259
     * generate the whole Patternlibrary data, pages and stuff...
260
     * 
261
     * @return Patternlibrary
262
     */
263
    run ( ) {
264
        
265
        try {
266
            // ... do stuff ^^
267
            
268
            // -> parse patterns
269
            this.getDocParser().parse();
270
            
271
            // -> generate docs
272
            this.getDocRenderer().renderDocs();
273
            
274
            // -> write data files
275
            
276
            // -> generate index pages
277
            
278
            // ...?!
279
        } catch (err) {
280
            this.warn(err);
281
            this.debug(err);
282
        }
283
        return (this);
284
    }
285
    
286
287
    
288
    
289
    
290
291
    /**
292
     * output warn message to console
293
     * 
294
     * @return Patternlibrary
295
     */
296
    warn ( msg, err ) {
297
        var strings = {
298
            time    : chalk.yellow( (new Date()).toLocaleTimeString() ),
299
            message : chalk.yellow( msg )
300
        };
301
        console.log(
302
            '['+(strings.time)+'] Patternlibrary: ' + (strings.message) // + "\n"
303
        );
304
        if (err) this.debug(err);
305
        return (this);
306
    }
307
308
    /**
309
     * output log message to console if verbose option is set to `true`
310
     * 
311
     * @return Patternlibrary
312
     */
313
    log ( msg ) {
314
        if (this.options.verbose === true) {
315
            var strings = {
316
                time    : chalk.grey( (new Date()).toLocaleTimeString() ),
317
                message : chalk.cyan( msg )
318
            };
319
            console.log(
320
                '['+(strings.time)+'] Patternlibrary: ' + (strings.message) // + "\n"
321
            );
322
        }
323
        return (this);
324
    }
325
326
    /**
327
     * output debug info to console if verbose and debug options are set to `true`
328
     * 
329
     * @return Patternlibrary
330
     */
331
    debug ( ) {
332
        if (this.options.verbose === true) {
333
            var strings = {
334
                time : chalk.grey( (new Date()).toLocaleTimeString() ),
335
                title : chalk.yellow( 'Patternlibrary Debug:' )
336
            };
337
            console.log(
338
                '['+(strings.time)+'] '+(strings.title)+' '+"\n",
339
                chalk.yellow(' >>> =================================================== <<< ') // +"\n"
340
            );
341
            for (var arg of arguments) {
342
                if (arg) console.log(arg);
343
            }
344
            console.log(
345
                chalk.yellow(' >>> =================================================== <<< ') // +"\n"
346
            );
347
        }
348
        return (this);
349
    }
350
    
351
    
352
    /**
353
     * increase usage on a given pattern by a given consumer
354
     * 
355
     * @param string patternname
356
     * @param object consumer
357
     */
358
    patternUsage ( patternname, consumer ) { 
359
360
        if ( typeof this.data[patternname] != 'undefined') {
361
            if ( typeof this.data[patternname].usage == 'undefined') {
362
                this.data[patternname].usage = {
363
                    count: 0,
364
                    consumers: {}
365
                };
366
                
367
                // increase counter
368
                this.data[patternname].usage.count = this.data[patternname].usage.count + 1;
369
                
370
                if (consumer && consumer.pattern) {
371
                    // add consumer, increase consumer counter
372
                    var consumerName = consumer.pattern.name;
373
                    if (!this.data[patternname].usage.consumers[consumerName]) {
374
                        this.data[patternname].usage.consumers[consumerName] = 1;
375
                    } else {
376
                        this.data[patternname].usage.consumers[consumerName] = 
377
                            this.data[patternname].usage.consumers[consumerName] + 1;
378
                    }
379
                }
380
            }    
381
        }
382
        
383
    }
384
    
385
    /**
386
     * updates the pattern-library JSON data-file
387
     */
388
    updateDataFile ( ) { 
389
        var patterns    = this.getPatterns();
390
        var categories  = this.getCategories();
391
        var tree        = this.Supercollider.tree;
392
        
393
        // complete library data
394
        var file = this.options.dest+'/patternlibrary.json';
395
        jsonfile.writeFileSync(file, {
396
            patterns  : patterns,
397
            categories: categories,
398
            tree      : tree,
399
        }, {spaces: 4, flag: 'w'});
400
        
401
        // search data
402
        this.Supercollider.options.pageRoot = this.Config.get('root');
403
        this.Supercollider.buildSearch(this.options.dest+'/search.json', function() {
404
            // file written...
405
        });
406
407
408
        // categorie data
409
        var file = this.options.dest+'/patterns.json';
410
        jsonfile.writeFileSync(file, patterns, {spaces: 4, flag: 'w'});
411
412
        // categorie data
413
        var file = this.options.dest+'/categories.json';
414
        jsonfile.writeFileSync(file, categories, {spaces: 4, flag: 'w'});
415
416
        // categorie data
417
        var file = this.options.dest+'/tree.json';
418
        jsonfile.writeFileSync(file, tree, {spaces: 4, flag: 'w'});
419
        
420
    }
421
    
422
    
423
    
424
    /**
425
     * refresh/reload pattern-library data
426
     */
427
    refresh ( ) { 
428
        
429
        return this.run();
430
        
431
    }
432
    
433
    
434
    
435
    /**
436
     * retrieve current pattern-library data
437
     */
438
    getPatterns ( type_or_category ) { 
439
        if (type_or_category) {
440
            var patterns = {};
441
            for (var patternkey in this.data) {
442
                var pattern = this.data[patternkey];
443
                if ( 
444
                    ( String(patternkey).indexOf(type_or_category) != -1 ) ||
445
                    ( pattern.pattern.categories && ( pattern.pattern.categories.indexOf(type_or_category) != -1 ) )
446
                ) {
447
                    patterns[patternkey] = pattern;                
448
                }
449
            }
450
            return patterns;
451
        }
452
        
453
        return this.data; 
454
    
455
    }
456
    
457
    
458
    
459
    
460
    /**
461
     * retrieve current list of pattern categories
462
     */
463
    getCategories ( ) { 
464
        var patterns  = this.getPatterns();
465
466
        function isCatInList ( arr, obj, idx ) {
467
        	for (var i in arr) {
468
        		if ( arr[i][idx] && obj[idx] && (arr[i][idx] == obj[idx]) ) {
469
        			return (true);
470
        		}
471
        	}
472
        	return false;
473
        }
474
        function sortByKey(array, key) {
475
            return array.sort(function(a, b) {
476
                var x = a[key]; var y = b[key];
477
                return ((x < y) ? -1 : ((x > y) ? 1 : 0));
478
            });
479
        }
480
        
481
        // categories data
482
        var categories = [];
483
        for (var pattern in patterns) {
484
            var patterncategories = 
485
                !Array.isArray(patterns[pattern].pattern.categories) ? 
486
                    [patterns[pattern].pattern.categories] : 
487
                        patterns[pattern].pattern.categories;
488
            for (var i in patterncategories) {
489
                var cat = { 
490
                    name: String(patterncategories[i]),
491
                    slug: String(patterncategories[i])
492
                };
493
                if ( !isCatInList( categories, cat, 'slug' ) ) {
494
                	var catPatterns = this.getPatterns(cat.slug);
495
                	cat.patterns = catPatterns;
496
                	cat.patternsCount = ((b) => {
497
                		var c = 0; for (var a in b) { c++; }; return c;
498
                	})(catPatterns);
499
                    categories.push(cat);
500
                }
501
            }
502
        }
503
        
504
        categories = sortByKey(categories, 'slug');
505
        return (categories);
506
    }
507
    
508
    
509
    
510
    
511
    
512
    //
513
    // helpers
514
    //
515
    
516
    /**
517
     * recursivly read a list of files from a given directory 
518
     * according to a given file-path pattern
519
     * 
520
     * scans relative to project root
521
     * 
522
     * @param string|[string] dir
523
     * @param string pattern
524
     * @return [string]  
525
     */
526
    _readdir ( dir, pattern ) {
527
        
528
        var files = [];
529
530
        dir = !Array.isArray(dir) ? [dir] : dir;
531
532
        for (var i in dir) {
533
            files = files.concat(
534
                glob.sync( path.join( process.cwd(), dir[i], pattern ) )
535
            );
536
        }
537
538
        return files;
539
        
540
    }
541
    
542
    /**
543
     * initialize destination directory
544
     */
545
    _initDestination ( ) {
546
        try {
547
            fs.ensureDirSync(this.options.dest);
548
        } catch (err) {
549
            if (err.code !== 'EEXIST') throw err
550
        }
551
    }
552
    
553
    /**
554
     * initialize 'Panini' object
555
     */
556
    _initPanini ( ) {
557
        
558
        if (!this.Panini) {
559
            
560
            let paniniOptions = {};
561
            
562
            paniniOptions.root     = this.options.root;
563
            paniniOptions.layouts  = this.options.layouts;
564
            paniniOptions.partials = this.options.partials;
565
            paniniOptions.data     = this.options.data || null;
566
            paniniOptions.helpers  = this.options.helpers || null;
567
            
568
            this.Panini = new Panini.Panini(paniniOptions);
569
            this.Panini.loadBuiltinHelpers();
570
            
571
            // import Foundation Docs handlebar helpers
572
            this.Panini.Handlebars = require('./vendor/handlebars');
573
574
            this._registerPatternHelpers();
575
576
            
577
            // detect layouts, pages, data, etc...
578
            this.Panini.refresh();
579
            
580
        }
581
    }
582
    
583
    /**
584
     * register new Panini/Handlebars helpers
585
     */
586
    _registerPatternHelpers ( ) {
587
        
588
        // Capitalizes the first letter of a string
589
        this.Panini.Handlebars.registerHelper('toUpper', function(str) {
590
          return str[0].toUpperCase() + str.slice(1);
591
        });
592
593
        // Formats a mixin using a SassDoc mixin object to look like this:
594
        // @include mixinName($param, $param) { }
595
        this.Panini.Handlebars.registerHelper('writeMixin', function(mixin) {
596
          var name = mixin['context']['name'];
597
          var params = mixin['parameter'];
598
599
          var str = '@include ';
600
          str += name + '(';
601
          for (var i in params) {
602
            str += '$' + params[i]['name'] + ', ';
603
          }
604
          if (params) str = str.slice(0, -2);
605
          str += ') { }';
606
607
          return str;
608
        });
609
610
        // Formats a function using a SassDoc function object to look like this:
611
        // function($param, $param)
612
        this.Panini.Handlebars.registerHelper('writeFunction', function(func) {
613
          var name = func['context']['name'];
614
          var params = func['parameter'];
615
616
          var str = '';
617
          str += name + '(';
618
          for (var i in params) {
619
            str += '$' + params[i]['name'] + ', ';
620
          }
621
          if (params) str = str.slice(0, -2);
622
          str += ')';
623
624
          return str;
625
        });
626
627
        // Converts a Markdown string to HTML
628
        this.Panini.Handlebars.registerHelper('md', function(text) {
629
          return marked(text);
630
        });
631
632
        var relPath = path.relative( process.cwd(), path.join(__dirname, '') );
633
        // register 'local' helper stuff
634
        this.Panini.loadLayouts([path.join(relPath, '../layouts/'), '../layouts/','layouts',
635
            (this.options.layouts || 'layouts')
636
        ]);
637
        this.Panini.loadHelpers([path.join(relPath, '../helpers/'), '../helpers/', 'helpers',
638
            (this.options.helpers || 'helpers')
639
        ]);
640
641
        // special partial helper
642
        this.Panini.Handlebars.registerHelper('PL', require( path.join('../helpers/patternlibrary') ));
643
    }
644
    
645
    /**
646
     * initialize 'Panini' object
647
     */
648
    _initSupercollider ( ) {
649
650
        if (!this.Supercollider) {
651
            var $this = this;
0 ignored issues
show
The variable $this seems to be never used. Consider removing it.
Loading history...
652
            
653
            this.Supercollider       = new require('supercollider');
654
            this.Supercollider.$PL   = this; // Patternlibrary reference to Supercollider
655
            this.Supercollider.init  = require('./supercollider/supercollider_init_path_awareness');
656
            this.Supercollider.parse = require('./supercollider/supercollider_parse_extended');
657
            this.Supercollider
658
                //.adapter('sass')
659
                .adapter('js')
660
            ;
661
            this.Supercollider.adapter('sass',      require('./supercollider/adapters/sass.js'));
662
            this.Supercollider.adapter('source',    require('./supercollider/adapters/sourcecode.js'));
663
            this.Supercollider.adapter('example',   require('./supercollider/adapters/sourceexample.js'));
664
            this.Supercollider.adapter('specs',     require('./supercollider/adapters/patternspecs.js'));
665
            this.Supercollider.adapter('changelog', require('./supercollider/adapters/changelog.js'));
666
            this.Supercollider.adapter('gitinfo',   require('./supercollider/adapters/gitinfo.js'));
667
            this.Supercollider.adapter('tests',     require('./supercollider/adapters/testresults.js'));
668
            
669
            
670
        }
671
    }
672
    
673
    /**
674
     * determine the relative path between 2 given paths
675
     * 
676
     * @param string page
677
     * @param string root
678
     * @return string
679
     */
680
    _relativeRoot ( page, root ) {
681
        var pagePath = path.dirname(page);
682
        var rootPath = path.join(process.cwd(), root);
683
684
        var relativePath = path.relative(pagePath, rootPath);
685
686
        if (relativePath.length > 0) {
687
            relativePath += '/';
688
        }
689
690
        // On Windows, paths are separated with a "\"
691
        // However, web browsers use "/" no matter the platform
692
        return slash(relativePath);
693
    }
694
}
695
696
/**
697
 * pattern-library default options
698
 */
699
Patternlibrary.prototype.defaults = require('./config/default.js');
700
701
/**
702
 * export pattern-library object
703
 */
704
module.exports = Patternlibrary;