UNPKG

14.2 kBJavaScriptView Raw
1/**
2 A collection of functions relating to JSDoc symbol name manipulation.
3 @module jsdoc/name
4 @author Michael Mathews <micmath@gmail.com>
5 @license Apache License 2.0 - See file 'LICENSE.md' in this project.
6 */
7'use strict';
8
9var _ = require('underscore');
10var escape = require('escape-string-regexp');
11
12var hasOwnProp = Object.prototype.hasOwnProperty;
13
14/**
15 * Longnames that have a special meaning in JSDoc.
16 *
17 * @enum {string}
18 * @static
19 * @memberof module:jsdoc/name
20 */
21var LONGNAMES = exports.LONGNAMES = {
22 /** Longname used for doclets that do not have a longname, such as anonymous functions. */
23 ANONYMOUS: '<anonymous>',
24 /** Longname that represents global scope. */
25 GLOBAL: '<global>'
26};
27
28// Module namespace prefix.
29var MODULE_NAMESPACE = 'module:';
30
31/**
32 * Names and punctuation marks that identify doclet scopes.
33 *
34 * @enum {string}
35 * @static
36 * @memberof module:jsdoc/name
37 */
38var SCOPE = exports.SCOPE = {
39 NAMES: {
40 GLOBAL: 'global',
41 INNER: 'inner',
42 INSTANCE: 'instance',
43 STATIC: 'static'
44 },
45 PUNC: {
46 INNER: '~',
47 INSTANCE: '#',
48 STATIC: '.'
49 }
50};
51
52// For backwards compatibility, this enum must use lower-case keys
53var scopeToPunc = exports.scopeToPunc = {
54 'inner': SCOPE.PUNC.INNER,
55 'instance': SCOPE.PUNC.INSTANCE,
56 'static': SCOPE.PUNC.STATIC
57};
58var puncToScope = exports.puncToScope = _.invert(scopeToPunc);
59
60var DEFAULT_SCOPE = SCOPE.NAMES.STATIC;
61var SCOPE_PUNC = _.values(SCOPE.PUNC);
62var SCOPE_PUNC_STRING = '[' + SCOPE_PUNC.join() + ']';
63var REGEXP_LEADING_SCOPE = new RegExp('^(' + SCOPE_PUNC_STRING + ')');
64var REGEXP_TRAILING_SCOPE = new RegExp('(' + SCOPE_PUNC_STRING + ')$');
65
66var DESCRIPTION = '(?:(?:[ \\t]*\\-\\s*|\\s+)(\\S[\\s\\S]*))?$';
67var REGEXP_DESCRIPTION = new RegExp(DESCRIPTION);
68var REGEXP_NAME_DESCRIPTION = new RegExp('^(\\[[^\\]]+\\]|\\S+)' + DESCRIPTION);
69
70function nameIsLongname(name, memberof) {
71 var regexp = new RegExp('^' + escape(memberof) + SCOPE_PUNC_STRING);
72
73 return regexp.test(name);
74}
75
76function prototypeToPunc(name) {
77 // don't mangle symbols named "prototype"
78 if (name === 'prototype') {
79 return name;
80 }
81
82 return name.replace(/(?:^|\.)prototype\.?/g, SCOPE.PUNC.INSTANCE);
83}
84
85// TODO: deprecate exports.resolve in favor of a better name
86/**
87 Resolves the longname, memberof, variation and name values of the given doclet.
88 @param {module:jsdoc/doclet.Doclet} doclet
89 */
90exports.resolve = function(doclet) {
91 var about = {};
92 var memberof = doclet.memberof || '';
93 var name = doclet.name ? String(doclet.name) : '';
94
95 var parentDoc;
96
97 // change MyClass.prototype.instanceMethod to MyClass#instanceMethod
98 // (but not in function params, which lack doclet.kind)
99 // TODO: check for specific doclet.kind values (probably function, class, and module)
100 if (name && doclet.kind) {
101 name = prototypeToPunc(name);
102 }
103 doclet.name = name;
104
105 // member of a var in an outer scope?
106 if (name && !memberof && doclet.meta.code && doclet.meta.code.funcscope) {
107 name = doclet.longname = doclet.meta.code.funcscope + SCOPE.PUNC.INNER + name;
108 }
109
110 if (memberof || doclet.forceMemberof) { // @memberof tag given
111 memberof = prototypeToPunc(memberof);
112
113 // the name is a complete longname, like @name foo.bar, @memberof foo
114 if (name && nameIsLongname(name, memberof) && name !== memberof) {
115 about = exports.shorten(name, (doclet.forceMemberof ? memberof : undefined));
116 }
117 // the name and memberof are identical and refer to a module,
118 // like @name module:foo, @memberof module:foo (probably a member like 'var exports')
119 else if (name && name === memberof && name.indexOf(MODULE_NAMESPACE) === 0) {
120 about = exports.shorten(name, (doclet.forceMemberof ? memberof : undefined));
121 }
122 // the name and memberof are identical, like @name foo, @memberof foo
123 else if (name && name === memberof) {
124 doclet.scope = doclet.scope || DEFAULT_SCOPE;
125 name = memberof + scopeToPunc[doclet.scope] + name;
126 about = exports.shorten(name, (doclet.forceMemberof ? memberof : undefined));
127 }
128 // like @memberof foo# or @memberof foo~
129 else if (name && REGEXP_TRAILING_SCOPE.test(memberof) ) {
130 about = exports.shorten(memberof + name, (doclet.forceMemberof ? memberof : undefined));
131 }
132 else if (name && doclet.scope) {
133 about = exports.shorten(memberof + (scopeToPunc[doclet.scope] || '') + name,
134 (doclet.forceMemberof ? memberof : undefined));
135 }
136 }
137 else { // no @memberof
138 about = exports.shorten(name);
139 }
140
141 if (about.name) {
142 doclet.name = about.name;
143 }
144
145 if (about.memberof) {
146 doclet.setMemberof(about.memberof);
147 }
148
149 if (about.longname && (!doclet.longname || doclet.longname === doclet.name)) {
150 doclet.setLongname(about.longname);
151 }
152
153 if (doclet.scope === SCOPE.NAMES.GLOBAL) { // via @global tag?
154 doclet.setLongname(doclet.name);
155 delete doclet.memberof;
156 }
157 else if (about.scope) {
158 if (about.memberof === LONGNAMES.GLOBAL) { // via @memberof <global> ?
159 doclet.scope = SCOPE.NAMES.GLOBAL;
160 }
161 else {
162 doclet.scope = puncToScope[about.scope];
163 }
164 }
165 else if (doclet.name && doclet.memberof && !doclet.longname) {
166 if ( REGEXP_LEADING_SCOPE.test(doclet.name) ) {
167 doclet.scope = puncToScope[RegExp.$1];
168 doclet.name = doclet.name.substr(1);
169 }
170 else {
171 doclet.scope = DEFAULT_SCOPE;
172 }
173
174 doclet.setLongname(doclet.memberof + scopeToPunc[doclet.scope] + doclet.name);
175 }
176
177 if (about.variation) {
178 doclet.variation = about.variation;
179 }
180
181 // if we never found a longname, just use an empty string
182 if (!doclet.longname) {
183 doclet.longname = '';
184 }
185};
186
187/**
188 @method module:jsdoc/name.applyNamespace
189 @param {string} longname The full longname of the symbol.
190 @param {string} ns The namespace to be applied.
191 @returns {string} The longname with the namespace applied.
192 */
193exports.applyNamespace = function(longname, ns) {
194 var nameParts = exports.shorten(longname),
195 name = nameParts.name;
196 longname = nameParts.longname;
197
198 if ( !/^[a-zA-Z]+?:.+$/i.test(name) ) {
199 longname = longname.replace( new RegExp(escape(name) + '$'), ns + ':' + name );
200 }
201
202 return longname;
203};
204
205// TODO: docs
206exports.stripNamespace = function(longname) {
207 return longname.replace(/^[a-zA-Z]+:/, '');
208};
209
210/**
211 * Check whether a parent longname is an ancestor of a child longname.
212 *
213 * @param {string} parent - The parent longname.
214 * @param {string} child - The child longname.
215 * @return {boolean} `true` if the parent is an ancestor of the child; otherwise, `false`.
216 */
217exports.hasAncestor = function(parent, child) {
218 var hasAncestor = false;
219 var memberof = child;
220
221 if (!parent || !child) {
222 return hasAncestor;
223 }
224
225 // fast path for obvious non-ancestors
226 if (child.indexOf(parent) !== 0) {
227 return hasAncestor;
228 }
229
230 do {
231 memberof = exports.shorten(memberof).memberof;
232
233 if (memberof === parent) {
234 hasAncestor = true;
235 }
236 } while (!hasAncestor && memberof);
237
238 return hasAncestor;
239};
240
241// TODO: docs
242function atomize(longname, sliceChars, forcedMemberof) {
243 var i;
244 var memberof = '';
245 var name = '';
246 var parts;
247 var partsRegExp;
248 var scopePunc = '';
249 var token;
250 var tokens = [];
251 var variation;
252
253 // quoted strings in a longname are atomic, so we convert them to tokens
254 longname = longname.replace(/(\[?["'].+?["']\]?)/g, function($) {
255 var dot = '';
256 if ( /^\[/.test($) ) {
257 dot = '.';
258 $ = $.replace( /^\[/g, '' ).replace( /\]$/g, '' );
259 }
260
261 token = '@{' + tokens.length + '}@';
262 tokens.push($);
263
264 return dot + token; // foo["bar"] => foo.@{1}@
265 });
266
267 longname = prototypeToPunc(longname);
268
269 if (forcedMemberof !== undefined) {
270 partsRegExp = new RegExp('^(.*?)([' + sliceChars.join() + ']?)$');
271 name = longname.substr(forcedMemberof.length);
272 parts = forcedMemberof.match(partsRegExp);
273
274 if (parts[1]) {
275 memberof = parts[1] || forcedMemberof;
276 }
277 if (parts[2]) {
278 scopePunc = parts[2];
279 }
280 }
281 else if (longname) {
282 parts = (longname.match(new RegExp('^(:?(.+)([' + sliceChars.join() + ']))?(.+?)$')) || [])
283 .reverse();
284 name = parts[0] || '';
285 scopePunc = parts[1] || '';
286 memberof = parts[2] || '';
287 }
288
289 // like /** @name foo.bar(2) */
290 if ( /(.+)\(([^)]+)\)$/.test(name) ) {
291 name = RegExp.$1;
292 variation = RegExp.$2;
293 }
294
295 // restore quoted strings
296 i = tokens.length;
297 while (i--) {
298 longname = longname.replace('@{' + i + '}@', tokens[i]);
299 memberof = memberof.replace('@{' + i + '}@', tokens[i]);
300 scopePunc = scopePunc.replace('@{' + i + '}@', tokens[i]);
301 name = name.replace('@{' + i + '}@', tokens[i]);
302 }
303
304 return {
305 longname: longname,
306 memberof: memberof,
307 scope: scopePunc,
308 name: name,
309 variation: variation
310 };
311}
312
313// TODO: deprecate exports.shorten in favor of a better name
314/**
315 Given a longname like "a.b#c(2)", slice it up into an object
316 containing the memberof, the scope, the name, and variation.
317 @param {string} longname
318 @param {string} forcedMemberof
319 @returns {object} Representing the properties of the given name.
320 */
321exports.shorten = function(longname, forcedMemberof) {
322 return atomize(longname, SCOPE_PUNC, forcedMemberof);
323};
324
325// TODO: docs
326exports.combine = function(parts) {
327 return '' +
328 (parts.memberof || '') +
329 (parts.scope || '') +
330 (parts.name || '') +
331 (parts.variation || '');
332};
333
334// TODO: docs
335exports.stripVariation = function(name) {
336 var parts = exports.shorten(name);
337
338 parts.variation = '';
339
340 return exports.combine(parts);
341};
342
343function splitLongname(longname, options) {
344 var chunks = [];
345 var currentNameInfo;
346 var nameInfo = {};
347 var previousName = longname;
348 var splitters = SCOPE_PUNC.concat('/');
349
350 options = _.defaults(options || {}, {
351 includeVariation: true
352 });
353
354 do {
355 if (!options.includeVariation) {
356 previousName = exports.stripVariation(previousName);
357 }
358 currentNameInfo = nameInfo[previousName] = atomize(previousName, splitters);
359 previousName = currentNameInfo.memberof;
360 chunks.push(currentNameInfo.scope + currentNameInfo.name);
361 } while (previousName);
362
363 return {
364 chunks: chunks.reverse(),
365 nameInfo: nameInfo
366 };
367}
368
369// TODO: docs
370exports.longnamesToTree = function longnamesToTree(longnames, doclets) {
371 var splitOptions = { includeVariation: false };
372 var tree = {};
373
374 longnames.forEach(function(longname) {
375 var currentLongname = '';
376 var currentParent = tree;
377 var nameInfo;
378 var processed;
379
380 // don't try to add empty longnames to the tree
381 if (!longname) {
382 return;
383 }
384
385 processed = splitLongname(longname, splitOptions);
386 nameInfo = processed.nameInfo;
387
388 processed.chunks.forEach(function(chunk) {
389 currentLongname += chunk;
390
391 if (currentParent !== tree) {
392 currentParent.children = currentParent.children || {};
393 currentParent = currentParent.children;
394 }
395
396 if (!hasOwnProp.call(currentParent, chunk)) {
397 currentParent[chunk] = nameInfo[currentLongname];
398 }
399
400 if (currentParent[chunk]) {
401 currentParent[chunk].doclet = doclets ? doclets[currentLongname] : null;
402 currentParent = currentParent[chunk];
403 }
404 });
405 });
406
407 return tree;
408};
409
410/**
411 Split a string that starts with a name and ends with a description into its parts.
412 Allows the defaultvalue (if present) to contain brackets. If the name is found to have
413 mismatched brackets, null is returned.
414 @param {string} nameDesc
415 @returns {object} Hash with "name" and "description" properties.
416 */
417function splitNameMatchingBrackets(nameDesc) {
418 var buffer = [];
419 var c;
420 var stack = 0;
421 var stringEnd = null;
422
423 for (var i = 0; i < nameDesc.length; ++i) {
424 c = nameDesc[i];
425 buffer.push(c);
426
427 if (stringEnd) {
428 if (c === '\\' && i + 1 < nameDesc.length) {
429 buffer.push(nameDesc[++i]);
430 } else if (c === stringEnd) {
431 stringEnd = null;
432 }
433 } else if (c === '"' || c === "'") {
434 stringEnd = c;
435 } else if (c === '[') {
436 ++stack;
437 } else if (c === ']') {
438 if (--stack === 0) {
439 break;
440 }
441 }
442 }
443
444 if (stack || stringEnd) {
445 return null;
446 }
447
448 nameDesc.substr(i).match(REGEXP_DESCRIPTION);
449 return {
450 name: buffer.join(''),
451 description: RegExp.$1
452 };
453}
454
455
456// TODO: deprecate exports.splitName in favor of a better name
457/**
458 Split a string that starts with a name and ends with a description into its parts.
459 @param {string} nameDesc
460 @returns {object} Hash with "name" and "description" properties.
461 */
462exports.splitName = function(nameDesc) {
463 // like: name, [name], name text, [name] text, name - text, or [name] - text
464 // the hyphen must be on the same line as the name; this prevents us from treating a Markdown
465 // dash as a separator
466
467 // optional values get special treatment
468 var result = null;
469 if (nameDesc[0] === '[') {
470 result = splitNameMatchingBrackets(nameDesc);
471 if (result !== null) {
472 return result;
473 }
474 }
475
476 nameDesc.match(REGEXP_NAME_DESCRIPTION);
477 return {
478 name: RegExp.$1,
479 description: RegExp.$2
480 };
481};