UNPKG

20.3 kBJavaScriptView Raw
1
2/**
3 * The firedoc module
4 * @module firedoc
5 */
6
7const _ = require('underscore');
8const path = require('path');
9const utils = require('./utils');
10const debug = require('debug')('firedoc:local');
11const MarkdownIt = require('markdown-it');
12const Handlebars = require('handlebars');
13const md = new MarkdownIt();
14
15/**
16 * The Theme Locals
17 * @class Locals
18 */
19var Locals = {
20
21 /**
22 * @property {BuilderContext} context - Builder Context
23 */
24 context: null,
25
26 /**
27 * @property {Option} options - The options
28 */
29 options: {},
30
31 /**
32 * @property {AST} ast - The AST object
33 */
34 ast: {},
35
36 /**
37 * @property {Object} project - Get the project to export
38 */
39 get project () {
40 var root;
41 if (path.isAbsolute(this.options.dest)) {
42 root = this.options.dest;
43 } else {
44 root = path.join(process.cwd(), this.options.dest || '');
45 }
46 var proj = this.ast.project;
47 proj.root = root;
48 proj.base = this.options.http ? '' : proj.root;
49 proj.assets = path.join(root, '/assets');
50 return proj;
51 },
52
53 /**
54 * @property {Object} i18n - Get i18n object
55 */
56 get i18n () {
57 try {
58 var defaults = require(this.options.theme + '/i18n/en.json');
59 var extra = {};
60 if (this.options.lang) {
61 extra = require(this.options.theme + '/i18n/' + this.options.lang + '.json');
62 }
63 var ret = _.extend(defaults, extra);
64 ret.LANG = this.options.lang || 'en';
65 return ret;
66 } catch (e) {
67 return {};
68 }
69 },
70
71 /**
72 * @property {Object} modules - Get modules object to export
73 */
74 get modules () {
75 var self = this;
76 return Object.keys(self.ast.modules).map(
77 function (name) {
78 var mod = self.ast.modules[name];
79 if (self.options.withSrc) {
80 mod = self.context.addFoundAt(mod);
81 }
82 var description = utils.localize(mod.description, self.options.lang);
83 mod.description = self.parseCode(self.markdown(description));
84 if (mod.example) {
85 if (!_.isArray(mod.example)) {
86 mod.example = [mod.example];
87 }
88 mod.example = mod.example.map(function (v) {
89 return self.parseCode(self.markdown(v.trim()))
90 }).join('');
91 }
92 mod.members = mod.members || [];
93 mod.project = self.project;
94 mod.globals = self;
95 mod.i18n = self.i18n;
96 return mod;
97 }
98 );
99 },
100
101 /**
102 * @property {Object} classes - Get classes object to export
103 */
104 get classes () {
105 var self = this;
106 return Object.keys(self.ast.classes).map(
107 function (name) {
108 var clazz = self.ast.classes[name];
109 if (self.options.withSrc) {
110 clazz = self.context.addFoundAt(clazz);
111 }
112 clazz = self.appendClassToModule(clazz);
113 var description = utils.localize(clazz.description, self.options.lang);
114 clazz.description = self.parseCode(self.markdown(description));
115 if (clazz.example) {
116 if (!_.isArray(clazz.example)) {
117 clazz.example = [clazz.example];
118 }
119 clazz.example = clazz.example.map(function (v) {
120 return self.parseCode(self.markdown(v.trim()))
121 }).join('');
122 }
123 clazz.members = clazz.members || [];
124 clazz.project = self.project;
125 clazz.globals = self;
126 clazz.i18n = self.i18n;
127 clazz.inheritance = self.getInheritanceTree(clazz);
128 if (clazz.isConstructor) {
129 clazz.constructor = self.buildMember(clazz, true);
130 }
131 return clazz;
132 }
133 );
134 },
135
136 /**
137 * @property {Object} files - Get files object to export
138 */
139 get files () {
140 var self = this;
141 return Object.keys(self.ast.files).map(
142 function (name) {
143 var file = self.ast.files[name];
144 file.i18n = self.i18n;
145 return file;
146 }
147 );
148 },
149
150 /**
151 * Initialize the markdownit rulers
152 * @method initMarkdownRulers
153 */
154 initMarkdownRulers: function () {
155 var ast = this.ast;
156 var options = this.options;
157 md.renderer.rules.link_open = function (tokens, idx, ops, env, self) {
158 var token = tokens[idx];
159 if (token && _.isArray(token.attrs)) {
160 token.attrs = token.attrs.map(function (attr, idx) {
161 if (attr[0] === 'href' &&
162 /^https?:\/\//.test(attr[1]) === false) {
163 var target = ast.namespacesMap[attr[1]];
164 var ext = options.markdown ? '.md' : '.html';
165 if (target && target.parent && target.itemtype) {
166 var url = target.parent.type + '/' + target.parent.name + ext +
167 '#' + target.itemtype + '_' + target.name;
168 attr[1] = url;
169 }
170 }
171 return attr;
172 });
173 }
174 return self.renderToken(tokens, idx, options);
175 };
176 },
177
178 /**
179 * Parses file and line number from an item object and build's an HREF
180 * @method addFoundAt
181 * @param {Object} a The item to parse
182 * @return {String} The parsed HREF
183 */
184 addFoundAt: function (a) {
185 var self = this;
186 var ext = this.options.markdown ? '.md' : '.html';
187 if (a.file && a.line && !this.options.nocode) {
188 a.foundAt = '../files/' + utils.filterFileName(a.file) + ext + '#l' + a.line;
189 if (a.path) {
190 a.foundAt = a.path + '#l' + a.line;
191 }
192 }
193 return a;
194 },
195
196 /**
197 * build the method name by its name and parameters
198 *
199 * @method getMethodName
200 * @param {String} name - The function/method name
201 * @param {Array} params - The function/method parameters list
202 * @param {String} params.name - The name of the parameter
203 */
204 getMethodName: function (name, params) {
205 return name + '(' + (params || []).map(function (v) {
206 return v.name;
207 }).join(', ') + ')';
208 },
209
210 /**
211 * Parses `<pre><code>` tags and adds the __prettyprint__ `className` to them
212 * @method _parseCode
213 * @private
214 * @param {HTML} html The HTML to parse
215 * @return {HTML} The parsed HTML
216 */
217 parseCode: function (html) {
218 html = html || '';
219 html = html.replace(/<pre><code>/g, '<pre class="code prettyprint"><code>\n');
220 // TODO(Yorkie): request to underscore, this is not working with &#39;
221 html = html.replace(/&#39;/g, '\'');
222 return _.unescape(html);
223 },
224
225 /**
226 * Wrapper around the Markdown parser so it can be normalized or even side stepped
227 * @method markdown
228 * @private
229 * @param {String} data The Markdown string to parse
230 * @return {HTML} The rendered HTML
231 */
232 markdown: function (data) {
233 var self = this;
234 if (this.options.markdown) {
235 return data;
236 }
237 var html = _.unescape(md.render(data || ''));
238 //Only reprocess if helpers were asked for
239 if (this.options.helpers || (html.indexOf('{{#crossLink') > -1)) {
240 try {
241 // markdown-it auto-escapes quotation marks (and unfortunately
242 // does not expose the escaping function)
243 // html = html.replace(/&quot;/g, "\"");
244 html = (Handlebars.compile(html))({});
245 } catch (hError) {
246 console.error(hError.stack);
247 //Remove all the extra escapes
248 html = html.replace(/\\{/g, '{').replace(/\\}/g, '}');
249 console.warn('Failed to parse Handlebars, probably an unknown helper, skiped');
250 }
251 }
252 return html;
253 },
254
255 /**
256 * append the clazz to its module
257 *
258 * @method appendClassToModule
259 * @param {Object} clazz - The class object
260 * @param {String} clazz.module - The module name of this clazz object
261 */
262 appendClassToModule: function (clazz) {
263 var mod = this.ast.modules[clazz.module];
264 if (mod) {
265 if (!_.isArray(mod.classes)) mod.classes = [];
266 mod.classes.push(clazz);
267 }
268 return clazz;
269 },
270
271 /**
272 * get class inheritance tree
273 *
274 * @method getClassInheritanceTree
275 * @return {Object} return the inheritance tree object
276 */
277 getInheritanceTree: function (clazz) {
278 var children = [];
279 this.ast.inheritedMembers.forEach(function (inherit) {
280 var at = inherit.indexOf(clazz.name);
281 if (at > -1 && at < inherit.length) {
282 var curr = children;
283 for (var i = at + 1; i < inherit.length; i++) {
284 var name = inherit[i];
285 var temp = {'name': name, 'children': []};
286 var needNewChild = true;
287 var pos;
288
289 for (pos = 0; pos < curr.length; pos++) {
290 if (curr[pos].name === name) {
291 needNewChild = false;
292 curr = curr[pos].children;
293 break;
294 }
295 }
296 if (needNewChild) {
297 if (inherit.length - 1 === i) {
298 delete temp.children;
299 }
300 curr.push(temp);
301 if (temp.children) {
302 curr = curr[curr.length - 1].children;
303 }
304 }
305 }
306 }
307 });
308 return children;
309 },
310
311 /**
312 * build the member
313 *
314 * @method buildMember
315 * @param {Object} memeber - The member object
316 * @param {Boolean} forceBeMethod - force make the build process be for method
317 * @param {Object} parent - The parent context
318 * @return {Object} returned member object
319 */
320 buildMember: function (member, forceBeMethod ,parent) {
321 var self = this;
322 if (self.options.withSrc) {
323 member = self.addFoundAt(member);
324 }
325 var description = utils.localize(member.description || '', this.options.lang);
326 member.description = self.parseCode(self.markdown(description));
327 member.hasAccessType = !!member.access;
328 member.readonly = member.readonly === '';
329 member.final = member.final === '';
330 member.static = member.static === '';
331 member.type = member.type || 'Unknown';
332 member.config = member.itemtype === 'config';
333 member.i18n = self.i18n;
334
335
336
337 if (!member.class && member.module) {
338 member.parent = self.ast.modules[member.module];
339 } else {
340 member.parent = self.ast.classes[member.class];
341 }
342
343 if (this.options.markdown) {
344 member.markdownLink = utils.markdownLink(member.itemtype + ':' + member.name);
345 }
346 if (member.example) {
347 if (!_.isArray(member.example)) {
348 member.example = [member.example];
349 }
350 member.example = member.example.map(function (v) {
351 return self.parseCode(self.markdown(v.trim()))
352 }).join('');
353 }
354 if (parent) {
355 var classMod = member.submodule || member.module;
356 var parentMod = parent.submodule || parent.module;
357 if (classMod !== parentMod) {
358 member.providedBy = classMod;
359 }
360 }
361 if (member.itemtype === 'method' || forceBeMethod) {
362 member.methodDisplay = self.getMethodName(member.name, member.params);
363 member.hasParams = (member.params || []).length > 0;
364 if (member.hasParams) {
365 _.each(member.params, function (param) {
366 param.description = self.markdown(param.description);
367 if (param.props && param.props.length > 0) {
368 _.each(param.props, function (prop) {
369 prop.description = self.markdown(prop.description);
370 });
371 }
372 });
373 }
374 if (member['return']) {
375 member.hasReturn = true;
376 member.returnType = member['return'].type;
377 } else {
378 member.returnType = '';
379 }
380 }
381 if (member.itemtype === 'attribute') {
382 member.emit = self.options.attributesEmit;
383 }
384 return member;
385 },
386
387 /**
388 * build the members
389 *
390 * @method buildMembers
391 * @return {Boolean} always be true
392 */
393 buildMembers: function () {
394 _.each(
395 this.ast.members,
396 function (member) {
397 var parent;
398 if (member.clazz) {
399 parent = this.ast.classes[member.clazz];
400 } else if (member.module) {
401 parent = this.ast.modules[member.module];
402 }
403 if (!parent) return;
404 if (!parent.members) {
405 parent.members = [];
406 }
407 var item = this.buildMember(member, false, parent);
408 parent.members.push(item);
409 },
410 this
411 );
412 },
413
414 /**
415 * Augments the **DocParser** meta data to provide default values for certain keys as well as parses all descriptions
416 * with the `Markdown Parser`
417 * @method augmentData
418 * @param {Object} o The object to recurse and augment
419 * @return {Object} The augmented object
420 */
421 augmentData: function (o) {
422 var self = this;
423 if (self.options.withSrc) {
424 o = self.addFoundAt(o);
425 }
426 _.each(o, function (i, k1) {
427 if (i && i.forEach) {
428 _.each(i, function (a, k) {
429 if (!(a instanceof Object)) {
430 return;
431 }
432 if (!a.type) {
433 a.type = 'Object'; //Default type is Object
434 }
435 if (a.final === '') {
436 a.final = true;
437 }
438 if (a.static === '') {
439 a.static = true;
440 }
441 if (!a.description) {
442 a.description = ' ';
443 } else if (!o.extendedFrom) {
444 a.description = self.markdown(a.description);
445 }
446 if (a.example && !o.extendedFrom) {
447 a.example = self.markdown(a.example);
448 }
449 if (self.options.withSrc) {
450 a = self.addFoundAt(a);
451 }
452
453 _.each(a, function (c, d) {
454 if (c.forEach || (c instanceof Object)) {
455 c = self.augmentData(c);
456 a[d] = c;
457 }
458 });
459 o[k1][k] = a;
460 });
461 } else if (i instanceof Object) {
462 i.foundAt = utils.getFoundAt(i, self.options);
463 _.each(i, function (v, k) {
464 if (k === 'final') {
465 o[k1][k] = true;
466 } else if (k === 'static') {
467 o[k1][k] = true;
468 }
469 else if (k === 'description' || k === 'example') {
470 if (v.forEach || (v instanceof Object)) {
471 o[k1][k] = self.augmentData(v);
472 } else {
473 o[k1][k] = o.extendedFrom ? v : self.markdown(v);
474 }
475 }
476 });
477 } else if (k1 === 'description' || k1 === 'example') {
478 o[k1] = o.extendedFrom ? i : self.markdown(i);
479 }
480 });
481 return o;
482 },
483
484 /**
485 * Counter for stepping into merges
486 * @private
487 * @property _mergeCounter
488 * @type Number
489 */
490 _mergeCounter: null,
491
492 /**
493 * Merge superclass data into a child class
494 * @method mergeExtends
495 * @param {Object} info The item to extend
496 * @param {Array} members The list of items to merge in
497 * @param {Boolean} first Set for the first call
498 */
499 mergeExtends: function (info, members, first, onmember) {
500 var self = this;
501 self._mergeCounter = (first) ? 0 : (self._mergeCounter + 1);
502
503 if (self._mergeCounter === 100) {
504 throw new Error('YUIDoc detected a loop extending class ' + info.name);
505 }
506 if (info.extends || info.uses) {
507 var hasItems = {};
508 hasItems[info.extends] = 1;
509 if (info.uses) {
510 info.uses.forEach(function (v) {
511 hasItems[v] = 1;
512 });
513 }
514 self.ast.members.forEach(function (v) {
515 if (hasItems[v.clazz]) {
516 if (!v.static) {
517 var q;
518 var override = _.findWhere(members, {'name': v.name});
519 if (!override) {
520 //This method was extended from the parent class but not over written
521 q = _.extend({}, v);
522 q.extendedFrom = v.clazz;
523 members.push(q);
524 } else {
525 //This method was extended from the parent and overwritten in this class
526 q = _.extend({}, v);
527 q = self.augmentData(q);
528 override.overwrittenFrom = q;
529 }
530 if (typeof onmember === 'function') {
531 onmember(q);
532 }
533 }
534 }
535 });
536 if (self.ast.classes[info.extends]) {
537 if (self.ast.classes[info.extends].extends || self.ast.classes[info.extends].uses) {
538 members = self.mergeExtends(self.ast.classes[info.extends], members);
539 }
540 }
541 }
542 return members;
543 },
544
545 /**
546 * generate expand function
547 *
548 * @method getExpandIterator
549 * @private
550 * @param {Object} parent - The object to be set
551 */
552 getExpandIterator: function (parent) {
553 var self = this;
554 var pluralsMap = {
555 'property': 'properties'
556 };
557 return function (item) {
558 if (!item.itemtype) return;
559 var plural = pluralsMap[item.itemtype];
560 if (!plural) {
561 plural = item.itemtype + 's';
562 }
563 if (!parent[plural]) {
564 parent[plural] = [];
565 }
566 parent[plural].push(item);
567 }
568 },
569
570 /**
571 * extends members array
572 *
573 * @method extendMembers
574 * @param {Object} meta - The meta object
575 */
576 extendMembers: function (meta) {
577 _.each(
578 meta.classes,
579 function (clazz) {
580 var inherited = [];
581 clazz.members = this.mergeExtends(clazz, clazz.members, true, function (member) {
582 if (member.extendedFrom) inherited.push(member);
583 });
584 clazz.members.inherited = inherited;
585 },
586 this
587 );
588 },
589
590 /**
591 * extends modules
592 *
593 * @method expandMembersFromModules
594 * @param {Object} meta - The meta object
595 */
596 expandMembersFromModules: function (meta) {
597 _.each(
598 meta.modules,
599 function (mod) {
600 mod.properties = [];
601 mod.attributes = [];
602 mod.methods = [];
603 mod.events = [];
604 mod.members.forEach(
605 this.getExpandIterator(mod.members)
606 );
607 },
608 this
609 );
610 },
611
612 /**
613 * extends members from classes
614 *
615 * @method expandMembersFromModules
616 * @param {Object} meta - The meta object
617 */
618 expandMembersFromClasses: function (meta) {
619 _.each(
620 meta.classes,
621 function (clazz) {
622 clazz.members.forEach(
623 this.getExpandIterator(clazz.members)
624 );
625 clazz.members.inherited = clazz.members.inherited || [];
626 clazz.members.inherited.forEach(
627 this.getExpandIterator(clazz.members.inherited)
628 );
629 },
630 this
631 );
632 },
633
634 /**
635 * Create a locals object from context
636 *
637 * @method create
638 * @param {BuilderContext} context - The `BuilderContext` instance
639 */
640 create: function (context) {
641 this.context = context;
642 this.options = context.options;
643 this.ast = context.ast;
644
645 debug('Initializing markdown-it rulers');
646 this.initMarkdownRulers();
647
648 debug('Creating the instance by utils.prepare');
649 var instance = utils.prepare([this.options.theme], this.options);
650
651 debug('Done preparation, ready for setting classes and modules');
652 instance.meta.classes = this.classes;
653 instance.meta.modules = this.modules;
654
655 // attach/build members to classes and modules
656 debug('Building members');
657 this.buildMembers();
658
659 // set files i18n and globals
660 instance.meta.files = this.files;
661 instance.meta.i18n = this.i18n;
662
663 // merge extends
664 this.extendMembers(instance.meta);
665 this.expandMembersFromModules(instance.meta);
666 this.expandMembersFromClasses(instance.meta);
667
668 // build locals.js
669 var locals;
670 var meta = instance.meta;
671 try {
672 if (path.isAbsolute(this.options.theme)) {
673 locals = require(path.join(this.options.theme, '/locals.js'));
674 } else {
675 locals = require(path.join(process.cwd(), this.options.theme, '/locals.js'));
676 }
677 } catch (e) {
678 console.warn('Failed on loading theme script: ' + e);
679 locals = function () {};
680 }
681 locals(meta.modules, meta.classes, instance.meta);
682
683 // globals should be set after running the locals.js
684 instance.meta.globals = instance.meta;
685 return instance;
686 }
687
688};
689
690exports.Locals = Locals;