1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 | const _ = require('underscore');
|
8 | const path = require('path');
|
9 | const fs = require('graceful-fs');
|
10 | const fse = require('fs-extra');
|
11 | const mkdirp = require('mkdirp').sync;
|
12 | const inspect = require('util').inspect;
|
13 | const request = require('needle');
|
14 | const Promise = require('bluebird');
|
15 | const Handlebars = require('handlebars');
|
16 | const EventEmitter = require('events').EventEmitter;
|
17 |
|
18 | const debug = require('debug')('firedoc:build');
|
19 | const utils = require('./utils');
|
20 | const DocView = require('./docview').DocView;
|
21 | const Locals = require('./locals').Locals;
|
22 | const defaultHelpers = require('./helpers');
|
23 |
|
24 |
|
25 |
|
26 |
|
27 |
|
28 |
|
29 | const NATIVES = require('./natives.json');
|
30 |
|
31 |
|
32 | Promise.promisifyAll(request);
|
33 | Promise.promisifyAll(fs);
|
34 | Promise.promisifyAll(fse);
|
35 |
|
36 |
|
37 |
|
38 |
|
39 |
|
40 |
|
41 | var BuilderContext = {
|
42 |
|
43 | |
44 |
|
45 |
|
46 | ast: null,
|
47 |
|
48 | |
49 |
|
50 |
|
51 | options: null,
|
52 |
|
53 | |
54 |
|
55 |
|
56 | helpers: {},
|
57 |
|
58 | |
59 |
|
60 |
|
61 | cacheTemplate: true,
|
62 |
|
63 | |
64 |
|
65 |
|
66 | template: null,
|
67 |
|
68 | |
69 |
|
70 |
|
71 | files: 0,
|
72 |
|
73 | |
74 |
|
75 |
|
76 | get extname () {
|
77 | return this.options.markdown ? '.md' : '.html';
|
78 | },
|
79 |
|
80 | |
81 |
|
82 |
|
83 |
|
84 |
|
85 | metadata: function () {
|
86 | if (!this._metadata) {
|
87 | try {
|
88 | var metadata;
|
89 | var themeJSON = path.join(this.options.theme, 'theme.json');
|
90 | if (fs.existsSync(themeJSON)) {
|
91 | debug('loading theme from ' + themeJSON);
|
92 | if (path.isAbsolute(themeJSON)) {
|
93 | metadata = require(themeJSON);
|
94 | } else {
|
95 | metadata = require(path.join(process.cwd(), themeJSON));
|
96 | }
|
97 | } else {
|
98 | debug('loading the default theme');
|
99 | metadata = require('../themes/default/theme.json');
|
100 | }
|
101 | this._metadata = metadata;
|
102 | } catch (err) {
|
103 | this._metadata = {};
|
104 | console.error(err.stack);
|
105 | }
|
106 | }
|
107 | return this._metadata;
|
108 | },
|
109 |
|
110 | |
111 |
|
112 |
|
113 |
|
114 |
|
115 |
|
116 |
|
117 | addHelper: function (name, helper) {
|
118 | this.helpers[name] = helper;
|
119 | Handlebars.registerHelper(name, helper.bind(this));
|
120 | },
|
121 |
|
122 | |
123 |
|
124 |
|
125 |
|
126 |
|
127 |
|
128 | addHelpers: function (helpers) {
|
129 | _.each(helpers, function (helper, name) {
|
130 | this.addHelper(name, helper);
|
131 | }, this);
|
132 | },
|
133 |
|
134 | |
135 |
|
136 |
|
137 |
|
138 |
|
139 |
|
140 |
|
141 |
|
142 | inlineCode: function (html) {
|
143 | if (this.options.markdown) return html;
|
144 | html = html.replace(/\\`/g, '__{{SELLECK_BACKTICK}}__');
|
145 | html = html.replace(/`(.+?)`/g, function (match, code) {
|
146 | return '<code>' + utils.escapeHTML(code) + '</code>';
|
147 | });
|
148 | html = html.replace(/__\{\{SELLECK_BACKTICK\}\}__/g, '`');
|
149 | return html;
|
150 | },
|
151 |
|
152 | |
153 |
|
154 |
|
155 |
|
156 |
|
157 |
|
158 |
|
159 |
|
160 |
|
161 | _parseCrossLink: function (item, raw, content) {
|
162 | item = item || 'unknown';
|
163 | var self = this;
|
164 | var base = '../';
|
165 | var baseItem;
|
166 | var newWin = false;
|
167 | var className = 'crosslink';
|
168 |
|
169 |
|
170 |
|
171 |
|
172 | item = baseItem = utils.safetrim(item.replace('{', '').replace('}', ''));
|
173 | item = item.replace('*', '').replace('[', '').replace(']', '');
|
174 | var link = false, href;
|
175 | var typeName = 'classes';
|
176 |
|
177 | if (self.ast.classes[item]) {
|
178 | link = true;
|
179 | if (self.ast.classes[item].type === 'enums') {
|
180 | typeName = 'enums';
|
181 | }
|
182 | } else if (self.ast.modules[item]) {
|
183 | link = true;
|
184 | typeName = 'modules';
|
185 | } else {
|
186 | if (self.ast.classes[item.replace('.', '')]) {
|
187 | link = true;
|
188 | item = item.replace('.', '');
|
189 | }
|
190 | }
|
191 | if (self.options.externalData) {
|
192 | if (self.ast.classes[item]) {
|
193 | if (self.ast.classes[item].external) {
|
194 | href = self.ast.classes[item].path;
|
195 | base = self.options.externalData.base;
|
196 | className += ' external';
|
197 | newWin = true;
|
198 | link = true;
|
199 | }
|
200 | }
|
201 | }
|
202 |
|
203 | if (item.indexOf('/') > -1) {
|
204 |
|
205 | var parts = item.split('/'),
|
206 | cls = parts[0],
|
207 | method = parts[1],
|
208 | type = 'method';
|
209 |
|
210 | if (method.indexOf(':') > -1) {
|
211 | parts = method.split(':');
|
212 | method = parts[0];
|
213 | type = parts[1];
|
214 | if (type.indexOf('attr') === 0) {
|
215 | type = 'attribute';
|
216 | }
|
217 | }
|
218 |
|
219 | if (cls && method) {
|
220 | if (self.ast.classes[cls]) {
|
221 | self.ast.members.forEach(function (i) {
|
222 | if (i.itemtype === type && i.name === method && i.clazz === cls) {
|
223 | link = true;
|
224 | baseItem = method;
|
225 | var t = type;
|
226 | if (t === 'attribute') {
|
227 | t = 'attr';
|
228 | }
|
229 | href = utils.webpath(base, 'classes', cls + '.html#' + t + '_' + method);
|
230 | }
|
231 | });
|
232 | } else if (self.ast.modules[cls]) {
|
233 | self.ast.members.forEach(function (i) {
|
234 | if (i.itemtype === type && i.name === method && i.module === cls) {
|
235 | link = true;
|
236 | baseItem = method;
|
237 | var t = type;
|
238 | if (t === 'attribute') {
|
239 | t = 'attr';
|
240 | }
|
241 | href = utils.webpath(base, 'modules', cls + '.html#' + t + '_' + method);
|
242 | }
|
243 | });
|
244 | }
|
245 | }
|
246 | }
|
247 |
|
248 | if (item === 'Object' || item === 'Array') {
|
249 | link = false;
|
250 | }
|
251 | if (!href) {
|
252 | href = utils.webpath(base, typeName, item + '.html');
|
253 | if (base.match(/^https?:\/\//)) {
|
254 | href = base + utils.webpath(typeName, item + '.html');
|
255 | }
|
256 | }
|
257 | if (!link && self.options.linkNatives) {
|
258 | item = utils.fixType(item);
|
259 | if (NATIVES && NATIVES[item]) {
|
260 | href = linkNativeType(item);
|
261 | if (href) {
|
262 | className += ' external';
|
263 | newWin = true;
|
264 | link = true;
|
265 | }
|
266 | }
|
267 | }
|
268 | if (link) {
|
269 | if (content !== undefined) {
|
270 | content = content.trim();
|
271 | }
|
272 | if (!content) {
|
273 | content = baseItem;
|
274 | }
|
275 | item = '<a href="' + href + '" class="' + className + '"' + ((newWin) ? ' target="_blank"' : '') + '>' + content + '</a>';
|
276 | }
|
277 | return (raw) ? href : item;
|
278 | },
|
279 |
|
280 | |
281 |
|
282 |
|
283 |
|
284 |
|
285 |
|
286 | populateClasses: function (opts) {
|
287 | var classes = [];
|
288 | var enums = [];
|
289 | _.each(opts.meta.classes, function (clazz) {
|
290 | if (clazz.external) return;
|
291 | if (clazz.access == 'private') {
|
292 | debug('skipping class ' + clazz.name);
|
293 | return;
|
294 | }
|
295 | if (clazz.isEnum) {
|
296 | clazz.type = 'enums';
|
297 | enums.push(clazz);
|
298 | } else {
|
299 | clazz.type = 'classes';
|
300 | classes.push(clazz);
|
301 | }
|
302 | });
|
303 | opts.meta.classes = classes;
|
304 | opts.meta.enums = enums;
|
305 | return opts;
|
306 | },
|
307 |
|
308 | |
309 |
|
310 |
|
311 |
|
312 |
|
313 |
|
314 | populateModules: function (opts) {
|
315 | var self = this;
|
316 | var modules = opts.meta.modules;
|
317 | _.each(modules, function (mod) {
|
318 | if (mod.external) return;
|
319 | if (!mod.isSubmodule && mod.submodules) {
|
320 | var submodules = [];
|
321 | _.each(mod.submodules, function (val, name) {
|
322 | var mod = self.ast.modules[name];
|
323 | if (val && mod) submodules.push(mod);
|
324 | });
|
325 | mod.type = 'modules';
|
326 | mod.submodules = _.sortBy(submodules, 'name');
|
327 | }
|
328 | });
|
329 | opts.meta.modules = _.sortBy(modules, 'name');
|
330 | return opts;
|
331 | },
|
332 |
|
333 | |
334 |
|
335 |
|
336 |
|
337 |
|
338 |
|
339 | populateFiles: function (opts) {
|
340 | var self = this;
|
341 | var files = [];
|
342 | _.each(this.ast.files, function (v) {
|
343 | if (v.external) return;
|
344 | v.name = utils.filterFileName(v.name);
|
345 | v.path = v.path || v.name;
|
346 | files.push(v);
|
347 | });
|
348 | files = _.sortBy(files, 'name');
|
349 | opts.meta.fileTree = utils.buildFileTree(files);
|
350 | return opts;
|
351 | },
|
352 |
|
353 | |
354 |
|
355 |
|
356 |
|
357 |
|
358 |
|
359 | addFoundAt: function (a) {
|
360 | a.foundAt = utils.getFoundAt(a, this.options);
|
361 | return a;
|
362 | },
|
363 |
|
364 | |
365 |
|
366 |
|
367 |
|
368 |
|
369 |
|
370 |
|
371 |
|
372 | mixExternal: function (callback) {
|
373 | var self = this;
|
374 | var external = this.options.external || {};
|
375 | var current = Promise.resolve();
|
376 | if (!external) return callback();
|
377 |
|
378 | external.merge = external.merge || 'mix';
|
379 | if (!external.data) {
|
380 | console.warn('External config found but no data path defined, skipping import.');
|
381 | if (_.isFunction(callback)) {
|
382 | callback();
|
383 | }
|
384 | return current;
|
385 | }
|
386 | if (!_.isArray(external.data)) {
|
387 | external.data = [external.data];
|
388 | }
|
389 | debug('Importing external documentation data');
|
390 |
|
391 | return Promise.map(external.data, function (item) {
|
392 | var base;
|
393 | if (_.isObject(item)) {
|
394 | base = item.base;
|
395 | item = item.json;
|
396 | }
|
397 | if (item.match(/^https?:\/\//)) {
|
398 | if (!base) {
|
399 | base = item.replace('data.json', '');
|
400 | }
|
401 | return current.then(function () {
|
402 | debug('fetching ' + item);
|
403 | return request.getAsync(item, {});
|
404 | }).then(function (results) {
|
405 | var data = JSON.parse(results[1]);
|
406 | data.base = base;
|
407 | return data;
|
408 | });
|
409 | } else {
|
410 | if (!base) {
|
411 | base = path.dirname(path.resolve(item));
|
412 | }
|
413 | var data = require(item);
|
414 | data.base = base;
|
415 | return data;
|
416 | }
|
417 | }).then(function (results) {
|
418 | function mixExternal (type, exdata) {
|
419 | self.ast[type] = (exdata[type] || []).map(setExternal);
|
420 | }
|
421 | function setExternal (item) {
|
422 | item.external = true;
|
423 | return item;
|
424 | }
|
425 | _.each(results, function (exdata) {
|
426 | mixExternal('files', exdata);
|
427 | mixExternal('classes', exdata);
|
428 | mixExternal('modules', exdata);
|
429 | mixExternal('members', exdata);
|
430 | });
|
431 | if (_.isFunction(callback)) {
|
432 | callback();
|
433 | }
|
434 | });
|
435 | },
|
436 |
|
437 | |
438 |
|
439 |
|
440 |
|
441 | makeDirs: function (callback) {
|
442 | var dirs = ['assets', 'classes', 'modules', 'enums'];
|
443 | if (this.options.withSrc) {
|
444 | dirs.push('files');
|
445 | }
|
446 | var root = this.options.dest || 'out';
|
447 | debug('Making default directories: ' + dirs.join(','));
|
448 | mkdirp(path.join(root, dirs[0]));
|
449 | mkdirp(path.join(root, dirs[1]));
|
450 | mkdirp(path.join(root, dirs[2]));
|
451 | mkdirp(path.join(root, dirs[3]));
|
452 | if (this.options.withSrc) {
|
453 | mkdirp(path.join(root, dirs[4]));
|
454 | }
|
455 | return dirs;
|
456 | },
|
457 |
|
458 | |
459 |
|
460 |
|
461 |
|
462 |
|
463 |
|
464 |
|
465 |
|
466 | init: function (ast, options) {
|
467 | this.ast = ast;
|
468 | this.options = options;
|
469 | this.addHelpers(defaultHelpers);
|
470 | this.cacheView = options.cacheView || this.cacheView;
|
471 | this.removeAllListeners();
|
472 | return this;
|
473 | },
|
474 |
|
475 | |
476 |
|
477 |
|
478 |
|
479 | correctTheme: function () {
|
480 | var root = path.join(__dirname, '../themes');
|
481 | var theme = this.options.theme;
|
482 | if (fs.existsSync(theme))
|
483 | return theme;
|
484 | var theme = root + '/firedoc-theme-' + this.options.theme;
|
485 | if (fs.existsSync(theme))
|
486 | return this.options.theme = theme;
|
487 | theme = root + '/firedoc-plugin-' + this.options.theme;
|
488 | if (fs.existsSync(theme))
|
489 | return this.options.theme = theme;
|
490 | theme = root + '/' + this.options.theme;
|
491 | if (fs.existsSync(theme))
|
492 | return this.options.theme = theme;
|
493 | this.options.theme = root + '/default';
|
494 | return this.options.theme;
|
495 | },
|
496 |
|
497 | |
498 |
|
499 |
|
500 |
|
501 |
|
502 |
|
503 | compile: function (callback) {
|
504 | debug('Compiling templates...');
|
505 | var self = this;
|
506 | this
|
507 | .mixExternal()
|
508 | .then(function makeDestDirs () {
|
509 | debug('make dest directories');
|
510 | self.makeDirs.call(self);
|
511 | })
|
512 | .then(function checkThemeDir () {
|
513 | debug('Checking theme folder');
|
514 | var theme = self.correctTheme.call(self);
|
515 | var metadata = self.metadata();
|
516 | debug('Using corrected theme: ' + theme);
|
517 | debug('Using the following metadata:' + inspect(metadata, {
|
518 | colors: true
|
519 | }));
|
520 | })
|
521 | .then(function copyAssets () {
|
522 | debug('Copying assets...');
|
523 | var src = self.options.theme + '/assets';
|
524 | var dest = self.options.dest + '/assets';
|
525 | return fse.copyAsync(src, dest);
|
526 | })
|
527 | .then(function createLocalsForTheme () {
|
528 | debug('Creating locals for theme...');
|
529 | return Locals.create(self);
|
530 | })
|
531 | .then(function render (locals) {
|
532 | debug('Popluating files, classes, modules');
|
533 | if (self.options.withSrc) {
|
534 | locals = self.populateFiles(locals);
|
535 | }
|
536 | locals = self.populateClasses(locals);
|
537 | locals = self.populateModules(locals);
|
538 | return Promise.all(
|
539 | [
|
540 | self.writeApiMeta(locals),
|
541 | self.writeIndex(locals),
|
542 | self.options.withSrc ? self.writeFiles(locals) : null,
|
543 | self.writeEnums(locals),
|
544 | self.writeClasses(locals),
|
545 | self.writeModules(locals)
|
546 | ].filter( function (entry, index) {
|
547 |
|
548 | if (index === 2 && !self.options.withSrc) {
|
549 | return false;
|
550 | }
|
551 | return true;
|
552 | })
|
553 | );
|
554 | })
|
555 | .then(function onfinish () {
|
556 | debug('Finished the build work');
|
557 | if (_.isFunction(callback)) {
|
558 | callback();
|
559 | }
|
560 | })
|
561 | .caught(callback);
|
562 | return this;
|
563 | },
|
564 |
|
565 | |
566 |
|
567 |
|
568 |
|
569 | render: function (name, view, locals) {
|
570 | var html = [];
|
571 | var partials = _.extend(locals.partials, {
|
572 | 'layout_content': '{{>' + name + '}}'
|
573 | });
|
574 | _.each(partials, function (source, name) {
|
575 | Handlebars.registerPartial(name, source);
|
576 | });
|
577 | if (!this.template || !this.cacheTemplate) {
|
578 | this.template = Handlebars.compile(locals.layouts.main);
|
579 | }
|
580 |
|
581 | var _view = {};
|
582 | for (var k in view) {
|
583 | if (_.isFunction(view[k])) {
|
584 | _view[k] = view[k]();
|
585 | } else {
|
586 | _view[k] = view[k];
|
587 | }
|
588 | }
|
589 | return this.inlineCode(this.template(_view));
|
590 | },
|
591 |
|
592 | |
593 |
|
594 |
|
595 |
|
596 |
|
597 | writeApiMeta: function (locals) {
|
598 | var self = this;
|
599 | var apimeta = {
|
600 | enums: [],
|
601 | classes: [],
|
602 | modules: []
|
603 | };
|
604 | _.each(
|
605 | ['classes', 'modules', 'enums'],
|
606 | function (id) {
|
607 | var items = locals.meta[id];
|
608 | var g = function (item) {
|
609 | apimeta[id].push({
|
610 | 'name': item.name,
|
611 | 'namespace': item.namespace,
|
612 | 'module': item.module,
|
613 | 'description': item.description,
|
614 | 'access': item.access
|
615 | });
|
616 | };
|
617 | _.each(locals.meta[id], g);
|
618 | apimeta[id] = _.sortBy(apimeta[id], 'name');
|
619 | }
|
620 | );
|
621 | return fs.writeFileAsync(
|
622 | this.options.dest + '/api.js',
|
623 | 'window.apimeta = ' + JSON.stringify(apimeta, null, 2),
|
624 | 'utf8'
|
625 | ).then(function () {
|
626 | self.emit('apimeta', apimeta);
|
627 | debug('api.js finished');
|
628 | });
|
629 | },
|
630 |
|
631 | writeIndex: function (locals) {
|
632 | debug('Start writing index');
|
633 | var self = this;
|
634 | var view = new DocView(locals.meta);
|
635 | view.base = '.';
|
636 | var html = this.render('index', view, locals);
|
637 | var filename = this.options.markdown ? '/readme.md' : '/index.html';
|
638 | var dest = this.options.dest + filename;
|
639 |
|
640 | debug('Start writing index.html');
|
641 | return fs.writeFileAsync(dest, html, 'utf8').then(function () {
|
642 | self.emit('index', view, html, dest);
|
643 | });
|
644 | },
|
645 |
|
646 | writeFiles: function (locals) {
|
647 | debug('Start writing files');
|
648 | var self = this;
|
649 | return Promise.map(
|
650 | locals.meta.files,
|
651 | function (file) {
|
652 | file.globals = locals.meta;
|
653 | var view = new DocView(file, null, '../');
|
654 | view.base = '..';
|
655 | var html = self.render('file', view, locals);
|
656 | var dest = path.join(self.options.dest, 'files', file.name.replace(/\//g, '_') + self.extname);
|
657 |
|
658 | debug('Start writing file: ' + file.name);
|
659 | return fs.writeFileAsync(dest, html, 'utf8').then(function () {
|
660 | self.emit('file', view, html, dest);
|
661 | });
|
662 | }
|
663 | );
|
664 | },
|
665 |
|
666 | writeEnums: function (locals) {
|
667 | debug('Start writing enums');
|
668 | var self = this;
|
669 | return Promise.map(
|
670 | locals.meta.enums,
|
671 | function (e) {
|
672 | e.globals = locals.meta;
|
673 | var view = new DocView(e, null, '../');
|
674 | view.base = '..';
|
675 | var html = self.render('enum', view, locals);
|
676 | var dest = path.join(self.options.dest, 'enums', e.name + self.extname);
|
677 |
|
678 | debug('Start writing enum: ' + e.name);
|
679 | return fs.writeFileAsync(dest, html, 'utf8').then(function () {
|
680 | self.emit('enum', view, html, dest);
|
681 | });
|
682 | }
|
683 | );
|
684 | },
|
685 |
|
686 | writeClasses: function (locals) {
|
687 | debug('Start writing classes');
|
688 | var self = this;
|
689 | return Promise.map(
|
690 | locals.meta.classes,
|
691 | function (clazz) {
|
692 | clazz.globals = locals.meta;
|
693 | var view = new DocView(clazz, null, '../');
|
694 | var dest = path.join(self.options.dest, 'classes', clazz.name + self.extname);
|
695 | view.base = '..';
|
696 | var html = self.render('class', view, locals);
|
697 |
|
698 | debug('Start writing class: ' + clazz.name);
|
699 | return fs.writeFileAsync(dest, html, 'utf8').then(function () {
|
700 | self.emit('class', view, html, dest);
|
701 | });
|
702 | }
|
703 | );
|
704 | },
|
705 |
|
706 | writeModules: function (locals) {
|
707 | debug('Start writing modules');
|
708 | var self = this;
|
709 | return Promise.map(
|
710 | locals.meta.modules,
|
711 | function (mod) {
|
712 | mod.globals = locals.meta;
|
713 | var view = new DocView(mod, null, '../');
|
714 | var dest = path.join(self.options.dest, 'modules', mod.name + self.extname);
|
715 | view.base = '..';
|
716 | var html = self.render('module', view, locals);
|
717 |
|
718 | debug('Start writing module: ' + mod.name);
|
719 | return fs.writeFileAsync(dest, html, 'utf8').then(function () {
|
720 | self.emit('module', view, html, dest);
|
721 | });
|
722 | }
|
723 | );
|
724 | }
|
725 |
|
726 | };
|
727 |
|
728 |
|
729 | var emitter = new EventEmitter();
|
730 | BuilderContext = _.extend(BuilderContext, emitter);
|
731 |
|
732 |
|
733 |
|
734 |
|
735 |
|
736 |
|
737 |
|
738 |
|
739 | function linkNativeType (name) {
|
740 | var url = 'https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/';
|
741 | if (NATIVES[name] !== 1) {
|
742 | url = NATIVES[name];
|
743 | }
|
744 | return url + name;
|
745 | }
|
746 |
|
747 |
|
748 |
|
749 |
|
750 |
|
751 |
|
752 |
|
753 |
|
754 |
|
755 | function compile (ast, options, onfinish) {
|
756 | var context = BuilderContext.init(ast, options);
|
757 | setImmediate(function () {
|
758 | context.compile(onfinish);
|
759 | });
|
760 | return context;
|
761 | }
|
762 | exports.compile = compile;
|