UNPKG

19.2 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.type = member.type || 'Unknown';
331 member.config = member.itemtype === 'config';
332 member.i18n = self.i18n;
333
334
335
336 if (!member.class && member.module) {
337 member.parent = self.ast.modules[member.module];
338 } else {
339 member.parent = self.ast.classes[member.class];
340 }
341
342 if (this.options.markdown) {
343 member.markdownLink = utils.markdownLink(member.itemtype + ':' + member.name);
344 }
345 if (member.example) {
346 if (!_.isArray(member.example)) {
347 member.example = [member.example];
348 }
349 member.example = member.example.map(function (v) {
350 return self.parseCode(self.markdown(v.trim()))
351 }).join('');
352 }
353 if (parent) {
354 var classMod = member.submodule || member.module;
355 var parentMod = parent.submodule || parent.module;
356 if (classMod !== parentMod) {
357 member.providedBy = classMod;
358 }
359 }
360 if (member.itemtype === 'method' || forceBeMethod) {
361 member.methodDisplay = self.getMethodName(member.name, member.params);
362 member.hasParams = (member.params || []).length > 0;
363 if (member.hasParams) {
364 _.each(member.params, function (param) {
365 param.description = self.markdown(param.description);
366 });
367 }
368 if (member['return']) {
369 member.hasReturn = true;
370 member.returnType = member['return'].type;
371 } else {
372 member.returnType = '';
373 }
374 }
375 if (member.itemtype === 'attribute') {
376 member.emit = self.options.attributesEmit;
377 }
378 return member;
379 },
380
381 /**
382 * build the members
383 *
384 * @method buildMembers
385 * @return {Boolean} always be true
386 */
387 buildMembers: function () {
388 _.each(
389 this.ast.members,
390 function (member) {
391 var parent;
392 if (member.clazz) {
393 parent = this.ast.classes[member.clazz];
394 } else if (member.module) {
395 parent = this.ast.modules[member.module];
396 }
397 if (!parent) return;
398 if (!parent.members) {
399 parent.members = [];
400 }
401 var item = this.buildMember(member, false, parent);
402 parent.members.push(item);
403 },
404 this
405 );
406 },
407
408 /**
409 * Augments the **DocParser** meta data to provide default values for certain keys as well as parses all descriptions
410 * with the `Markdown Parser`
411 * @method augmentData
412 * @param {Object} o The object to recurse and augment
413 * @return {Object} The augmented object
414 */
415 augmentData: function (o) {
416 var self = this;
417 if (self.options.withSrc) {
418 o = self.addFoundAt(o);
419 }
420 _.each(o, function (i, k1) {
421 if (i && i.forEach) {
422 _.each(i, function (a, k) {
423 if (!(a instanceof Object)) {
424 return;
425 }
426 if (!a.type) {
427 a.type = 'Object'; //Default type is Object
428 }
429 if (a.final === '') {
430 a.final = true;
431 }
432 if (!a.description) {
433 a.description = ' ';
434 } else if (!o.extendedFrom) {
435 a.description = self.markdown(a.description);
436 }
437 if (a.example && !o.extendedFrom) {
438 a.example = self.markdown(a.example);
439 }
440 if (self.options.withSrc) {
441 a = self.addFoundAt(a);
442 }
443
444 _.each(a, function (c, d) {
445 if (c.forEach || (c instanceof Object)) {
446 c = self.augmentData(c);
447 a[d] = c;
448 }
449 });
450 o[k1][k] = a;
451 });
452 } else if (i instanceof Object) {
453 i.foundAt = utils.getFoundAt(i, self.options);
454 _.each(i, function (v, k) {
455 if (k === 'final') {
456 o[k1][k] = true;
457 } else if (k === 'description' || k === 'example') {
458 if (v.forEach || (v instanceof Object)) {
459 o[k1][k] = self.augmentData(v);
460 } else {
461 o[k1][k] = o.extendedFrom ? v : self.markdown(v);
462 }
463 }
464 });
465 } else if (k1 === 'description' || k1 === 'example') {
466 o[k1] = o.extendedFrom ? i : self.markdown(i);
467 }
468 });
469 return o;
470 },
471
472 /**
473 * Counter for stepping into merges
474 * @private
475 * @property _mergeCounter
476 * @type Number
477 */
478 _mergeCounter: null,
479
480 /**
481 * Merge superclass data into a child class
482 * @method mergeExtends
483 * @param {Object} info The item to extend
484 * @param {Array} members The list of items to merge in
485 * @param {Boolean} first Set for the first call
486 */
487 mergeExtends: function (info, members, first, onmember) {
488 var self = this;
489 self._mergeCounter = (first) ? 0 : (self._mergeCounter + 1);
490
491 if (self._mergeCounter === 100) {
492 throw new Error('YUIDoc detected a loop extending class ' + info.name);
493 }
494 if (info.extends || info.uses) {
495 var hasItems = {};
496 hasItems[info.extends] = 1;
497 if (info.uses) {
498 info.uses.forEach(function (v) {
499 hasItems[v] = 1;
500 });
501 }
502 self.ast.members.forEach(function (v) {
503 if (hasItems[v.clazz]) {
504 if (!v.static) {
505 var q;
506 var override = _.findWhere(members, {'name': v.name});
507 if (!override) {
508 //This method was extended from the parent class but not over written
509 q = _.extend({}, v);
510 q.extendedFrom = v.clazz;
511 members.push(q);
512 } else {
513 //This method was extended from the parent and overwritten in this class
514 q = _.extend({}, v);
515 q = self.augmentData(q);
516 override.overwrittenFrom = q;
517 }
518 if (typeof onmember === 'function') {
519 onmember(q);
520 }
521 }
522 }
523 });
524 if (self.ast.classes[info.extends]) {
525 if (self.ast.classes[info.extends].extends || self.ast.classes[info.extends].uses) {
526 members = self.mergeExtends(self.ast.classes[info.extends], members);
527 }
528 }
529 }
530 return members;
531 },
532
533 /**
534 * generate expand function
535 *
536 * @method getExpandIterator
537 * @private
538 * @param {Object} parent - The object to be set
539 */
540 getExpandIterator: function (parent) {
541 var self = this;
542 var pluralsMap = {
543 'property': 'properties'
544 };
545 return function (item) {
546 if (!item.itemtype) return;
547 var plural = pluralsMap[item.itemtype];
548 if (!plural) {
549 plural = item.itemtype + 's';
550 }
551 if (!parent[plural]) {
552 parent[plural] = [];
553 }
554 parent[plural].push(item);
555 }
556 },
557
558 /**
559 * extends members array
560 *
561 * @method extendMembers
562 * @param {Object} meta - The meta object
563 */
564 extendMembers: function (meta) {
565 _.each(
566 meta.classes,
567 function (clazz) {
568 var inherited = [];
569 clazz.members = this.mergeExtends(clazz, clazz.members, true, function (member) {
570 if (member.extendedFrom) inherited.push(member);
571 });
572 clazz.members.inherited = inherited;
573 },
574 this
575 );
576 },
577
578 /**
579 * extends modules
580 *
581 * @method expandMembersFromModules
582 * @param {Object} meta - The meta object
583 */
584 expandMembersFromModules: function (meta) {
585 _.each(
586 meta.modules,
587 function (mod) {
588 mod.properties = [];
589 mod.attributes = [];
590 mod.methods = [];
591 mod.events = [];
592 mod.members.forEach(
593 this.getExpandIterator(mod.members)
594 );
595 },
596 this
597 );
598 },
599
600 /**
601 * extends members from classes
602 *
603 * @method expandMembersFromModules
604 * @param {Object} meta - The meta object
605 */
606 expandMembersFromClasses: function (meta) {
607 _.each(
608 meta.classes,
609 function (clazz) {
610 clazz.members.forEach(
611 this.getExpandIterator(clazz.members)
612 );
613 clazz.members.inherited = clazz.members.inherited || [];
614 clazz.members.inherited.forEach(
615 this.getExpandIterator(clazz.members.inherited)
616 );
617 },
618 this
619 );
620 },
621
622 /**
623 * Create a locals object from context
624 *
625 * @method create
626 * @param {BuilderContext} context - The `BuilderContext` instance
627 */
628 create: function (context) {
629 this.context = context;
630 this.options = context.options;
631 this.ast = context.ast;
632
633 debug('Initializing markdown-it rulers');
634 this.initMarkdownRulers();
635
636 debug('Creating the instance by utils.prepare');
637 var instance = utils.prepare([this.options.theme], this.options);
638
639 debug('Done preparation, ready for setting classes and modules');
640 instance.meta.classes = this.classes;
641 instance.meta.modules = this.modules;
642
643 // attach/build members to classes and modules
644 debug('Building members');
645 this.buildMembers();
646
647 // set files i18n and globals
648 instance.meta.files = this.files;
649 instance.meta.i18n = this.i18n;
650
651 // merge extends
652 this.extendMembers(instance.meta);
653 this.expandMembersFromModules(instance.meta);
654 this.expandMembersFromClasses(instance.meta);
655
656 // build locals.js
657 var locals;
658 var meta = instance.meta;
659 try {
660 if (path.isAbsolute(this.options.theme)) {
661 locals = require(path.join(this.options.theme, '/locals.js'));
662 } else {
663 locals = require(path.join(process.cwd(), this.options.theme, '/locals.js'));
664 }
665 } catch (e) {
666 console.warn('Failed on loading theme script: ' + e);
667 locals = function () {};
668 }
669 locals(meta.modules, meta.classes, instance.meta);
670
671 // globals should be set after running the locals.js
672 instance.meta.globals = instance.meta;
673 return instance;
674 }
675
676};
677
678exports.Locals = Locals;