UNPKG

11.4 kBJavaScriptView Raw
1/**
2 * @overview
3 * @author Michael Mathews <micmath@gmail.com>
4 * @license Apache License 2.0 - See file 'LICENSE.md' in this project.
5 */
6
7/**
8 * @module jsdoc/doclet
9 */
10'use strict';
11
12var _ = require('underscore');
13var jsdoc = {
14 name: require('jsdoc/name'),
15 src: {
16 astnode: require('jsdoc/src/astnode'),
17 Syntax: require('jsdoc/src/syntax').Syntax
18 },
19 tag: {
20 Tag: require('jsdoc/tag').Tag,
21 dictionary: require('jsdoc/tag/dictionary')
22 }
23};
24var path = require('jsdoc/path');
25var Syntax = jsdoc.src.Syntax;
26var util = require('util');
27
28function applyTag(doclet, tag) {
29 if (tag.title === 'name') {
30 doclet.name = tag.value;
31 }
32
33 if (tag.title === 'kind') {
34 doclet.kind = tag.value;
35 }
36
37 if (tag.title === 'description') {
38 doclet.description = tag.value;
39 }
40}
41
42// use the meta info about the source code to guess what the doclet kind should be
43function codeToKind(code) {
44 var isFunction = jsdoc.src.astnode.isFunction;
45 var kind = 'member';
46 var node = code.node;
47
48 if ( isFunction(code.type) ) {
49 kind = 'function';
50 }
51 else if ( code.node && code.node.parent && isFunction(code.node.parent) ) {
52 kind = 'param';
53 }
54
55 return kind;
56}
57
58function unwrap(docletSrc) {
59 if (!docletSrc) { return ''; }
60
61 // note: keep trailing whitespace for @examples
62 // extra opening/closing stars are ignored
63 // left margin is considered a star and a space
64 // use the /m flag on regex to avoid having to guess what this platform's newline is
65 docletSrc =
66 docletSrc.replace(/^\/\*\*+/, '') // remove opening slash+stars
67 .replace(/\**\*\/$/, '\\Z') // replace closing star slash with end-marker
68 .replace(/^\s*(\* ?|\\Z)/gm, '') // remove left margin like: spaces+star or spaces+end-marker
69 .replace(/\s*\\Z$/g, ''); // remove end-marker
70
71 return docletSrc;
72}
73
74/**
75 * Convert the raw source of the doclet comment into an array of pseudo-Tag objects.
76 * @private
77 */
78function toTags(docletSrc) {
79 var parsedTag;
80 var tagData = [];
81 var tagText;
82 var tagTitle;
83
84 // split out the basic tags, keep surrounding whitespace
85 // like: @tagTitle tagBody
86 docletSrc
87 // replace splitter ats with an arbitrary sequence
88 .replace(/^(\s*)@(\S)/gm, '$1\\@$2')
89 // then split on that arbitrary sequence
90 .split('\\@')
91 .forEach(function($) {
92 if ($) {
93 parsedTag = $.match(/^(\S+)(?:\s+(\S[\s\S]*))?/);
94
95 if (parsedTag) {
96 tagTitle = parsedTag[1];
97 tagText = parsedTag[2];
98
99 if (tagTitle) {
100 tagData.push({
101 title: tagTitle,
102 text: tagText
103 });
104 }
105 }
106 }
107 });
108
109 return tagData;
110}
111
112function fixDescription(docletSrc) {
113 if (!/^\s*@/.test(docletSrc) && docletSrc.replace(/\s/g, '').length) {
114 docletSrc = '@description ' + docletSrc;
115 }
116 return docletSrc;
117}
118
119/**
120 * Replace the existing tag dictionary with a new tag dictionary.
121 *
122 * Used for testing only.
123 *
124 * @private
125 * @param {module:jsdoc/tag/dictionary.Dictionary} dict - The new tag dictionary.
126 */
127exports._replaceDictionary = function _replaceDictionary(dict) {
128 jsdoc.tag.dictionary = dict;
129 require('jsdoc/tag')._replaceDictionary(dict);
130 require('jsdoc/util/templateHelper')._replaceDictionary(dict);
131};
132
133/**
134 * @class
135 * @classdesc Represents a single JSDoc comment.
136 * @param {string} docletSrc - The raw source code of the jsdoc comment.
137 * @param {object=} meta - Properties describing the code related to this comment.
138 */
139var Doclet = exports.Doclet = function(docletSrc, meta) {
140 var newTags = [];
141
142 /** The original text of the comment from the source code. */
143 this.comment = docletSrc;
144 this.setMeta(meta);
145
146 docletSrc = unwrap(docletSrc);
147 docletSrc = fixDescription(docletSrc);
148
149 newTags = toTags.call(this, docletSrc);
150
151 for (var i = 0, l = newTags.length; i < l; i++) {
152 this.addTag(newTags[i].title, newTags[i].text);
153 }
154
155 this.postProcess();
156};
157
158/** Called once after all tags have been added. */
159Doclet.prototype.postProcess = function() {
160 var i;
161 var l;
162
163 if (!this.preserveName) {
164 jsdoc.name.resolve(this);
165 }
166 if (this.name && !this.longname) {
167 this.setLongname(this.name);
168 }
169 if (this.memberof === '') {
170 delete this.memberof;
171 }
172
173 if (!this.kind && this.meta && this.meta.code) {
174 this.addTag( 'kind', codeToKind(this.meta.code) );
175 }
176
177 if (this.variation && this.longname && !/\)$/.test(this.longname) ) {
178 this.longname += '(' + this.variation + ')';
179 }
180
181 // add in any missing param names
182 if (this.params && this.meta && this.meta.code && this.meta.code.paramnames) {
183 for (i = 0, l = this.params.length; i < l; i++) {
184 if (!this.params[i].name) {
185 this.params[i].name = this.meta.code.paramnames[i] || '';
186 }
187 }
188 }
189};
190
191/**
192 * Add a tag to the doclet.
193 *
194 * @param {string} title - The title of the tag being added.
195 * @param {string} [text] - The text of the tag being added.
196 */
197Doclet.prototype.addTag = function(title, text) {
198 var tagDef = jsdoc.tag.dictionary.lookUp(title),
199 newTag = new jsdoc.tag.Tag(title, text, this.meta);
200
201 if (tagDef && tagDef.onTagged) {
202 tagDef.onTagged(this, newTag);
203 }
204
205 if (!tagDef) {
206 this.tags = this.tags || [];
207 this.tags.push(newTag);
208 }
209
210 applyTag(this, newTag);
211};
212
213function removeGlobal(longname) {
214 var globalRegexp = new RegExp('^' + jsdoc.name.LONGNAMES.GLOBAL + '\\.?');
215
216 return longname.replace(globalRegexp, '');
217}
218
219/**
220 * Set the doclet's `memberof` property.
221 *
222 * @param {string} sid - The longname of the doclet's parent symbol.
223 */
224Doclet.prototype.setMemberof = function(sid) {
225 /**
226 * The longname of the symbol that contains this one, if any.
227 * @type string
228 */
229 this.memberof = removeGlobal(sid)
230 .replace(/\.prototype/g, jsdoc.name.SCOPE.PUNC.INSTANCE);
231};
232
233/**
234 * Set the doclet's `longname` property.
235 *
236 * @param {string} name - The longname for the doclet.
237 */
238Doclet.prototype.setLongname = function(name) {
239 /**
240 * The fully resolved symbol name.
241 * @type string
242 */
243 this.longname = removeGlobal(name);
244 if (jsdoc.tag.dictionary.isNamespace(this.kind)) {
245 this.longname = jsdoc.name.applyNamespace(this.longname, this.kind);
246 }
247};
248
249/**
250 * Get the full path to the source file that is associated with a doclet.
251 *
252 * @private
253 * @param {module:jsdoc/doclet.Doclet} The doclet to check for a filepath.
254 * @return {string} The path to the doclet's source file, or an empty string if the path is not
255 * available.
256 */
257function getFilepath(doclet) {
258 if (!doclet || !doclet.meta || !doclet.meta.filename) {
259 return '';
260 }
261
262 return path.join(doclet.meta.path || '', doclet.meta.filename);
263}
264
265/**
266 * Set the doclet's `scope` property. Must correspond to a scope name that is defined in
267 * {@link module:jsdoc/name.SCOPE.NAMES}.
268 *
269 * @param {module:jsdoc/name.SCOPE.NAMES} scope - The scope for the doclet relative to the symbol's
270 * parent.
271 * @throws {Error} If the scope name is not recognized.
272 */
273Doclet.prototype.setScope = function(scope) {
274 var errorMessage;
275 var filepath;
276 var scopeNames = _.values(jsdoc.name.SCOPE.NAMES);
277
278 if (scopeNames.indexOf(scope) === -1) {
279 filepath = getFilepath(this);
280
281 errorMessage = util.format('The scope name "%s" is not recognized. Use one of the ' +
282 'following values: %j', scope, scopeNames);
283 if (filepath) {
284 errorMessage += util.format(' (Source file: %s)', filepath);
285 }
286
287 throw new Error(errorMessage);
288 }
289
290 this.scope = scope;
291};
292
293/**
294 * Add a symbol to this doclet's `borrowed` array.
295 *
296 * @param {string} source - The longname of the symbol that is the source.
297 * @param {string} target - The name the symbol is being assigned to.
298 */
299Doclet.prototype.borrow = function(source, target) {
300 var about = { from: source };
301 if (target) {
302 about.as = target;
303 }
304
305 if (!this.borrowed) {
306 /**
307 * A list of symbols that are borrowed by this one, if any.
308 * @type Array.<string>
309 */
310 this.borrowed = [];
311 }
312 this.borrowed.push(about);
313};
314
315Doclet.prototype.mix = function(source) {
316 /**
317 * A list of symbols that are mixed into this one, if any.
318 * @type Array.<string>
319 */
320 this.mixes = this.mixes || [];
321 this.mixes.push(source);
322};
323
324/**
325 * Add a symbol to the doclet's `augments` array.
326 *
327 * @param {string} base - The longname of the base symbol.
328 */
329Doclet.prototype.augment = function(base) {
330 /**
331 * A list of symbols that are augmented by this one, if any.
332 * @type Array.<string>
333 */
334 this.augments = this.augments || [];
335 this.augments.push(base);
336};
337
338/**
339 * Set the `meta` property of this doclet.
340 *
341 * @param {object} meta
342 */
343Doclet.prototype.setMeta = function(meta) {
344 /**
345 * Information about the source code associated with this doclet.
346 * @namespace
347 */
348 this.meta = this.meta || {};
349
350 if (meta.range) {
351 /**
352 * The positions of the first and last characters of the code associated with this doclet.
353 * @type Array.<number>
354 */
355 this.meta.range = meta.range.slice(0);
356 }
357
358 if (meta.lineno) {
359 /**
360 * The name of the file containing the code associated with this doclet.
361 * @type string
362 */
363 this.meta.filename = path.basename(meta.filename);
364 /**
365 * The line number of the code associated with this doclet.
366 * @type number
367 */
368 this.meta.lineno = meta.lineno;
369
370 var pathname = path.dirname(meta.filename);
371 if (pathname && pathname !== '.') {
372 this.meta.path = pathname;
373 }
374 }
375
376 /**
377 * Information about the code symbol.
378 * @namespace
379 */
380 this.meta.code = this.meta.code || {};
381 if (meta.id) { this.meta.code.id = meta.id; }
382 if (meta.code) {
383 if (meta.code.name) {
384 /** The name of the symbol in the source code. */
385 this.meta.code.name = meta.code.name;
386 }
387 if (meta.code.type) {
388 /** The type of the symbol in the source code. */
389 this.meta.code.type = meta.code.type;
390 }
391 // the AST node is only enumerable in debug mode, which reduces clutter for the
392 // --explain/-X option
393 if (meta.code.node) {
394 Object.defineProperty(this.meta.code, 'node', {
395 value: meta.code.node,
396 enumerable: global.env.opts.debug ? true : false
397 });
398 }
399 if (meta.code.funcscope) {
400 this.meta.code.funcscope = meta.code.funcscope;
401 }
402 if (typeof meta.code.value !== 'undefined') {
403 /** The value of the symbol in the source code. */
404 this.meta.code.value = meta.code.value;
405 }
406 if (meta.code.paramnames) {
407 this.meta.code.paramnames = meta.code.paramnames.slice(0);
408 }
409 }
410};