• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2 * @fileoverview Main Doctrine object
3 * @author Yusuke Suzuki <utatane.tea@gmail.com>
4 * @author Dan Tao <daniel.tao@gmail.com>
5 * @author Andrew Eisenberg <andrew@eisenberg.as>
6 */
7
8(function () {
9    'use strict';
10
11    var typed,
12        utility,
13        jsdoc,
14        esutils,
15        hasOwnProperty;
16
17    esutils = require('esutils');
18    typed = require('./typed');
19    utility = require('./utility');
20
21    function sliceSource(source, index, last) {
22        return source.slice(index, last);
23    }
24
25    hasOwnProperty = (function () {
26        var func = Object.prototype.hasOwnProperty;
27        return function hasOwnProperty(obj, name) {
28            return func.call(obj, name);
29        };
30    }());
31    function shallowCopy(obj) {
32        var ret = {}, key;
33        for (key in obj) {
34            if (obj.hasOwnProperty(key)) {
35                ret[key] = obj[key];
36            }
37        }
38        return ret;
39    }
40
41    function isASCIIAlphanumeric(ch) {
42        return (ch >= 0x61  /* 'a' */ && ch <= 0x7A  /* 'z' */) ||
43            (ch >= 0x41  /* 'A' */ && ch <= 0x5A  /* 'Z' */) ||
44            (ch >= 0x30  /* '0' */ && ch <= 0x39  /* '9' */);
45    }
46
47    function isParamTitle(title) {
48        return title === 'param' || title === 'argument' || title === 'arg';
49    }
50
51    function isReturnTitle(title) {
52        return title === 'return' || title === 'returns';
53    }
54
55    function isProperty(title) {
56        return title === 'property' || title === 'prop';
57    }
58
59    function isNameParameterRequired(title) {
60        return isParamTitle(title) || isProperty(title) ||
61            title === 'alias' || title === 'this' || title === 'mixes' || title === 'requires';
62    }
63
64    function isAllowedName(title) {
65        return isNameParameterRequired(title) || title === 'const' || title === 'constant';
66    }
67
68    function isAllowedNested(title) {
69        return isProperty(title) || isParamTitle(title);
70    }
71
72    function isAllowedOptional(title) {
73        return isProperty(title) || isParamTitle(title);
74    }
75
76    function isTypeParameterRequired(title) {
77        return isParamTitle(title) || isReturnTitle(title) ||
78            title === 'define' || title === 'enum' ||
79            title === 'implements' || title === 'this' ||
80            title === 'type' || title === 'typedef' || isProperty(title);
81    }
82
83    // Consider deprecation instead using 'isTypeParameterRequired' and 'Rules' declaration to pick when a type is optional/required
84    // This would require changes to 'parseType'
85    function isAllowedType(title) {
86        return isTypeParameterRequired(title) || title === 'throws' || title === 'const' || title === 'constant' ||
87            title === 'namespace' || title === 'member' || title === 'var' || title === 'module' ||
88            title === 'constructor' || title === 'class' || title === 'extends' || title === 'augments' ||
89            title === 'public' || title === 'private' || title === 'protected';
90    }
91
92    // A regex character class that contains all whitespace except linebreak characters (\r, \n, \u2028, \u2029)
93    var WHITESPACE = '[ \\f\\t\\v\\u00a0\\u1680\\u180e\\u2000-\\u200a\\u202f\\u205f\\u3000\\ufeff]';
94
95    var STAR_MATCHER = '(' + WHITESPACE + '*(?:\\*' + WHITESPACE + '?)?)(.+|[\r\n\u2028\u2029])';
96
97    function unwrapComment(doc) {
98        // JSDoc comment is following form
99        //   /**
100        //    * .......
101        //    */
102
103        return doc.
104            // remove /**
105            replace(/^\/\*\*?/, '').
106            // remove */
107            replace(/\*\/$/, '').
108            // remove ' * ' at the beginning of a line
109            replace(new RegExp(STAR_MATCHER, 'g'), '$2').
110            // remove trailing whitespace
111            replace(/\s*$/, '');
112    }
113
114    /**
115     * Converts an index in an "unwrapped" JSDoc comment to the corresponding index in the original "wrapped" version
116     * @param {string} originalSource The original wrapped comment
117     * @param {number} unwrappedIndex The index of a character in the unwrapped string
118     * @returns {number} The index of the corresponding character in the original wrapped string
119     */
120    function convertUnwrappedCommentIndex(originalSource, unwrappedIndex) {
121        var replacedSource = originalSource.replace(/^\/\*\*?/, '');
122        var numSkippedChars = 0;
123        var matcher = new RegExp(STAR_MATCHER, 'g');
124        var match;
125
126        while ((match = matcher.exec(replacedSource))) {
127            numSkippedChars += match[1].length;
128
129            if (match.index + match[0].length > unwrappedIndex + numSkippedChars) {
130                return unwrappedIndex + numSkippedChars + originalSource.length - replacedSource.length;
131            }
132        }
133
134        return originalSource.replace(/\*\/$/, '').replace(/\s*$/, '').length;
135    }
136
137    // JSDoc Tag Parser
138
139    (function (exports) {
140        var Rules,
141            index,
142            lineNumber,
143            length,
144            source,
145            originalSource,
146            recoverable,
147            sloppy,
148            strict;
149
150        function advance() {
151            var ch = source.charCodeAt(index);
152            index += 1;
153            if (esutils.code.isLineTerminator(ch) && !(ch === 0x0D  /* '\r' */ && source.charCodeAt(index) === 0x0A  /* '\n' */)) {
154                lineNumber += 1;
155            }
156            return String.fromCharCode(ch);
157        }
158
159        function scanTitle() {
160            var title = '';
161            // waste '@'
162            advance();
163
164            while (index < length && isASCIIAlphanumeric(source.charCodeAt(index))) {
165                title += advance();
166            }
167
168            return title;
169        }
170
171        function seekContent() {
172            var ch, waiting, last = index;
173
174            waiting = false;
175            while (last < length) {
176                ch = source.charCodeAt(last);
177                if (esutils.code.isLineTerminator(ch) && !(ch === 0x0D  /* '\r' */ && source.charCodeAt(last + 1) === 0x0A  /* '\n' */)) {
178                    waiting = true;
179                } else if (waiting) {
180                    if (ch === 0x40  /* '@' */) {
181                        break;
182                    }
183                    if (!esutils.code.isWhiteSpace(ch)) {
184                        waiting = false;
185                    }
186                }
187                last += 1;
188            }
189            return last;
190        }
191
192        // type expression may have nest brace, such as,
193        // { { ok: string } }
194        //
195        // therefore, scanning type expression with balancing braces.
196        function parseType(title, last, addRange) {
197            var ch, brace, type, startIndex, direct = false;
198
199
200            // search '{'
201            while (index < last) {
202                ch = source.charCodeAt(index);
203                if (esutils.code.isWhiteSpace(ch)) {
204                    advance();
205                } else if (ch === 0x7B  /* '{' */) {
206                    advance();
207                    break;
208                } else {
209                    // this is direct pattern
210                    direct = true;
211                    break;
212                }
213            }
214
215
216            if (direct) {
217                return null;
218            }
219
220            // type expression { is found
221            brace = 1;
222            type = '';
223            while (index < last) {
224                ch = source.charCodeAt(index);
225                if (esutils.code.isLineTerminator(ch)) {
226                    advance();
227                } else {
228                    if (ch === 0x7D  /* '}' */) {
229                        brace -= 1;
230                        if (brace === 0) {
231                            advance();
232                            break;
233                        }
234                    } else if (ch === 0x7B  /* '{' */) {
235                        brace += 1;
236                    }
237                    if (type === '') {
238                        startIndex = index;
239                    }
240                    type += advance();
241                }
242            }
243
244            if (brace !== 0) {
245                // braces is not balanced
246                return utility.throwError('Braces are not balanced');
247            }
248
249            if (isAllowedOptional(title)) {
250                return typed.parseParamType(type, {startIndex: convertIndex(startIndex), range: addRange});
251            }
252
253            return typed.parseType(type, {startIndex: convertIndex(startIndex), range: addRange});
254        }
255
256        function scanIdentifier(last) {
257            var identifier;
258            if (!esutils.code.isIdentifierStartES5(source.charCodeAt(index)) && !source[index].match(/[0-9]/)) {
259                return null;
260            }
261            identifier = advance();
262            while (index < last && esutils.code.isIdentifierPartES5(source.charCodeAt(index))) {
263                identifier += advance();
264            }
265            return identifier;
266        }
267
268        function skipWhiteSpace(last) {
269            while (index < last && (esutils.code.isWhiteSpace(source.charCodeAt(index)) || esutils.code.isLineTerminator(source.charCodeAt(index)))) {
270                advance();
271            }
272        }
273
274        function parseName(last, allowBrackets, allowNestedParams) {
275            var name = '',
276                useBrackets,
277                insideString;
278
279
280            skipWhiteSpace(last);
281
282            if (index >= last) {
283                return null;
284            }
285
286            if (source.charCodeAt(index) === 0x5B  /* '[' */) {
287                if (allowBrackets) {
288                    useBrackets = true;
289                    name = advance();
290                } else {
291                    return null;
292                }
293            }
294
295            name += scanIdentifier(last);
296
297            if (allowNestedParams) {
298                if (source.charCodeAt(index) === 0x3A /* ':' */ && (
299                        name === 'module' ||
300                        name === 'external' ||
301                        name === 'event')) {
302                    name += advance();
303                    name += scanIdentifier(last);
304
305                }
306                if(source.charCodeAt(index) === 0x5B  /* '[' */ && source.charCodeAt(index + 1) === 0x5D  /* ']' */){
307                    name += advance();
308                    name += advance();
309                }
310                while (source.charCodeAt(index) === 0x2E  /* '.' */ ||
311                        source.charCodeAt(index) === 0x2F  /* '/' */ ||
312                        source.charCodeAt(index) === 0x23  /* '#' */ ||
313                        source.charCodeAt(index) === 0x2D  /* '-' */ ||
314                        source.charCodeAt(index) === 0x7E  /* '~' */) {
315                    name += advance();
316                    name += scanIdentifier(last);
317                }
318            }
319
320            if (useBrackets) {
321                skipWhiteSpace(last);
322                // do we have a default value for this?
323                if (source.charCodeAt(index) === 0x3D  /* '=' */) {
324                    // consume the '='' symbol
325                    name += advance();
326                    skipWhiteSpace(last);
327
328                    var ch;
329                    var bracketDepth = 1;
330
331                    // scan in the default value
332                    while (index < last) {
333                        ch = source.charCodeAt(index);
334
335                        if (esutils.code.isWhiteSpace(ch)) {
336                            if (!insideString) {
337                                skipWhiteSpace(last);
338                                ch = source.charCodeAt(index);
339                            }
340                        }
341
342                        if (ch === 0x27 /* ''' */) {
343                            if (!insideString) {
344                                insideString = '\'';
345                            } else {
346                                if (insideString === '\'') {
347                                    insideString = '';
348                                }
349                            }
350                        }
351
352                        if (ch === 0x22 /* '"' */) {
353                            if (!insideString) {
354                                insideString = '"';
355                            } else {
356                                if (insideString === '"') {
357                                    insideString = '';
358                                }
359                            }
360                        }
361
362                        if (ch === 0x5B /* '[' */) {
363                            bracketDepth++;
364                        } else if (ch === 0x5D  /* ']' */ &&
365                            --bracketDepth === 0) {
366                            break;
367                        }
368
369                        name += advance();
370                    }
371                }
372
373                skipWhiteSpace(last);
374
375                if (index >= last || source.charCodeAt(index) !== 0x5D  /* ']' */) {
376                    // we never found a closing ']'
377                    return null;
378                }
379
380                // collect the last ']'
381                name += advance();
382            }
383
384            return name;
385        }
386
387        function skipToTag() {
388            while (index < length && source.charCodeAt(index) !== 0x40  /* '@' */) {
389                advance();
390            }
391            if (index >= length) {
392                return false;
393            }
394            utility.assert(source.charCodeAt(index) === 0x40  /* '@' */);
395            return true;
396        }
397
398        function convertIndex(rangeIndex) {
399            if (source === originalSource) {
400                return rangeIndex;
401            }
402            return convertUnwrappedCommentIndex(originalSource, rangeIndex);
403        }
404
405        function TagParser(options, title) {
406            this._options = options;
407            this._title = title.toLowerCase();
408            this._tag = {
409                title: title,
410                description: null
411            };
412            if (this._options.lineNumbers) {
413                this._tag.lineNumber = lineNumber;
414            }
415            this._first = index - title.length - 1;
416            this._last = 0;
417            // space to save special information for title parsers.
418            this._extra = { };
419        }
420
421        // addError(err, ...)
422        TagParser.prototype.addError = function addError(errorText) {
423            var args = Array.prototype.slice.call(arguments, 1),
424                msg = errorText.replace(
425                    /%(\d)/g,
426                    function (whole, index) {
427                        utility.assert(index < args.length, 'Message reference must be in range');
428                        return args[index];
429                    }
430                );
431
432            if (!this._tag.errors) {
433                this._tag.errors = [];
434            }
435            if (strict) {
436                utility.throwError(msg);
437            }
438            this._tag.errors.push(msg);
439            return recoverable;
440        };
441
442        TagParser.prototype.parseType = function () {
443            // type required titles
444            if (isTypeParameterRequired(this._title)) {
445                try {
446                    this._tag.type = parseType(this._title, this._last, this._options.range);
447                    if (!this._tag.type) {
448                        if (!isParamTitle(this._title) && !isReturnTitle(this._title)) {
449                            if (!this.addError('Missing or invalid tag type')) {
450                                return false;
451                            }
452                        }
453                    }
454                } catch (error) {
455                    this._tag.type = null;
456                    if (!this.addError(error.message)) {
457                        return false;
458                    }
459                }
460            } else if (isAllowedType(this._title)) {
461                // optional types
462                try {
463                    this._tag.type = parseType(this._title, this._last, this._options.range);
464                } catch (e) {
465                    //For optional types, lets drop the thrown error when we hit the end of the file
466                }
467            }
468            return true;
469        };
470
471        TagParser.prototype._parseNamePath = function (optional) {
472            var name;
473            name = parseName(this._last, sloppy && isAllowedOptional(this._title), true);
474            if (!name) {
475                if (!optional) {
476                    if (!this.addError('Missing or invalid tag name')) {
477                        return false;
478                    }
479                }
480            }
481            this._tag.name = name;
482            return true;
483        };
484
485        TagParser.prototype.parseNamePath = function () {
486            return this._parseNamePath(false);
487        };
488
489        TagParser.prototype.parseNamePathOptional = function () {
490            return this._parseNamePath(true);
491        };
492
493
494        TagParser.prototype.parseName = function () {
495            var assign, name;
496
497            // param, property requires name
498            if (isAllowedName(this._title)) {
499                this._tag.name = parseName(this._last, sloppy && isAllowedOptional(this._title), isAllowedNested(this._title));
500                if (!this._tag.name) {
501                    if (!isNameParameterRequired(this._title)) {
502                        return true;
503                    }
504
505                    // it's possible the name has already been parsed but interpreted as a type
506                    // it's also possible this is a sloppy declaration, in which case it will be
507                    // fixed at the end
508                    if (isParamTitle(this._title) && this._tag.type && this._tag.type.name) {
509                        this._extra.name = this._tag.type;
510                        this._tag.name = this._tag.type.name;
511                        this._tag.type = null;
512                    } else {
513                        if (!this.addError('Missing or invalid tag name')) {
514                            return false;
515                        }
516                    }
517                } else {
518                    name = this._tag.name;
519                    if (name.charAt(0) === '[' && name.charAt(name.length - 1) === ']') {
520                        // extract the default value if there is one
521                        // example: @param {string} [somebody=John Doe] description
522                        assign = name.substring(1, name.length - 1).split('=');
523                        if (assign.length > 1) {
524                            this._tag['default'] = assign.slice(1).join('=');
525                        }
526                        this._tag.name = assign[0];
527
528                        // convert to an optional type
529                        if (this._tag.type && this._tag.type.type !== 'OptionalType') {
530                            this._tag.type = {
531                                type: 'OptionalType',
532                                expression: this._tag.type
533                            };
534                        }
535                    }
536                }
537            }
538
539
540            return true;
541        };
542
543        TagParser.prototype.parseDescription = function parseDescription() {
544            var description = sliceSource(source, index, this._last).trim();
545            if (description) {
546                if ((/^-\s+/).test(description)) {
547                    description = description.substring(2);
548                }
549                this._tag.description = description;
550            }
551            return true;
552        };
553
554        TagParser.prototype.parseCaption = function parseDescription() {
555            var description = sliceSource(source, index, this._last).trim();
556            var captionStartTag = '<caption>';
557            var captionEndTag = '</caption>';
558            var captionStart = description.indexOf(captionStartTag);
559            var captionEnd = description.indexOf(captionEndTag);
560            if (captionStart >= 0 && captionEnd >= 0) {
561                this._tag.caption = description.substring(
562                    captionStart + captionStartTag.length, captionEnd).trim();
563                this._tag.description = description.substring(captionEnd + captionEndTag.length).trim();
564            } else {
565                this._tag.description = description;
566            }
567            return true;
568        };
569
570        TagParser.prototype.parseKind = function parseKind() {
571            var kind, kinds;
572            kinds = {
573                'class': true,
574                'constant': true,
575                'event': true,
576                'external': true,
577                'file': true,
578                'function': true,
579                'member': true,
580                'mixin': true,
581                'module': true,
582                'namespace': true,
583                'typedef': true
584            };
585            kind = sliceSource(source, index, this._last).trim();
586            this._tag.kind = kind;
587            if (!hasOwnProperty(kinds, kind)) {
588                if (!this.addError('Invalid kind name \'%0\'', kind)) {
589                    return false;
590                }
591            }
592            return true;
593        };
594
595        TagParser.prototype.parseAccess = function parseAccess() {
596            var access;
597            access = sliceSource(source, index, this._last).trim();
598            this._tag.access = access;
599            if (access !== 'private' && access !== 'protected' && access !== 'public') {
600                if (!this.addError('Invalid access name \'%0\'', access)) {
601                    return false;
602                }
603            }
604            return true;
605        };
606
607        TagParser.prototype.parseThis = function parseThis() {
608            // this name may be a name expression (e.g. {foo.bar}),
609            // an union (e.g. {foo.bar|foo.baz}) or a name path (e.g. foo.bar)
610            var value = sliceSource(source, index, this._last).trim();
611            if (value && value.charAt(0) === '{') {
612                var gotType = this.parseType();
613                if (gotType && this._tag.type.type === 'NameExpression' || this._tag.type.type === 'UnionType') {
614                    this._tag.name = this._tag.type.name;
615                    return true;
616                } else {
617                    return this.addError('Invalid name for this');
618                }
619            } else {
620                return this.parseNamePath();
621            }
622        };
623
624        TagParser.prototype.parseVariation = function parseVariation() {
625            var variation, text;
626            text = sliceSource(source, index, this._last).trim();
627            variation = parseFloat(text, 10);
628            this._tag.variation = variation;
629            if (isNaN(variation)) {
630                if (!this.addError('Invalid variation \'%0\'', text)) {
631                    return false;
632                }
633            }
634            return true;
635        };
636
637        TagParser.prototype.ensureEnd = function () {
638            var shouldBeEmpty = sliceSource(source, index, this._last).trim();
639            if (shouldBeEmpty) {
640                if (!this.addError('Unknown content \'%0\'', shouldBeEmpty)) {
641                    return false;
642                }
643            }
644            return true;
645        };
646
647        TagParser.prototype.epilogue = function epilogue() {
648            var description;
649
650            description = this._tag.description;
651            // un-fix potentially sloppy declaration
652            if (isAllowedOptional(this._title) && !this._tag.type && description && description.charAt(0) === '[') {
653                this._tag.type = this._extra.name;
654                if (!this._tag.name) {
655                    this._tag.name = undefined;
656                }
657
658                if (!sloppy) {
659                    if (!this.addError('Missing or invalid tag name')) {
660                        return false;
661                    }
662                }
663            }
664
665            return true;
666        };
667
668        Rules = {
669            // http://usejsdoc.org/tags-access.html
670            'access': ['parseAccess'],
671            // http://usejsdoc.org/tags-alias.html
672            'alias': ['parseNamePath', 'ensureEnd'],
673            // http://usejsdoc.org/tags-augments.html
674            'augments': ['parseType', 'parseNamePathOptional', 'ensureEnd'],
675            // http://usejsdoc.org/tags-constructor.html
676            'constructor': ['parseType', 'parseNamePathOptional', 'ensureEnd'],
677            // Synonym: http://usejsdoc.org/tags-constructor.html
678            'class': ['parseType', 'parseNamePathOptional', 'ensureEnd'],
679            // Synonym: http://usejsdoc.org/tags-extends.html
680            'extends': ['parseType', 'parseNamePathOptional', 'ensureEnd'],
681            // http://usejsdoc.org/tags-example.html
682            'example': ['parseCaption'],
683            // http://usejsdoc.org/tags-deprecated.html
684            'deprecated': ['parseDescription'],
685            // http://usejsdoc.org/tags-global.html
686            'global': ['ensureEnd'],
687            // http://usejsdoc.org/tags-inner.html
688            'inner': ['ensureEnd'],
689            // http://usejsdoc.org/tags-instance.html
690            'instance': ['ensureEnd'],
691            // http://usejsdoc.org/tags-kind.html
692            'kind': ['parseKind'],
693            // http://usejsdoc.org/tags-mixes.html
694            'mixes': ['parseNamePath', 'ensureEnd'],
695            // http://usejsdoc.org/tags-mixin.html
696            'mixin': ['parseNamePathOptional', 'ensureEnd'],
697            // http://usejsdoc.org/tags-member.html
698            'member': ['parseType', 'parseNamePathOptional', 'ensureEnd'],
699            // http://usejsdoc.org/tags-method.html
700            'method': ['parseNamePathOptional', 'ensureEnd'],
701            // http://usejsdoc.org/tags-module.html
702            'module': ['parseType', 'parseNamePathOptional', 'ensureEnd'],
703            // Synonym: http://usejsdoc.org/tags-method.html
704            'func': ['parseNamePathOptional', 'ensureEnd'],
705            // Synonym: http://usejsdoc.org/tags-method.html
706            'function': ['parseNamePathOptional', 'ensureEnd'],
707            // Synonym: http://usejsdoc.org/tags-member.html
708            'var': ['parseType', 'parseNamePathOptional', 'ensureEnd'],
709            // http://usejsdoc.org/tags-name.html
710            'name': ['parseNamePath', 'ensureEnd'],
711            // http://usejsdoc.org/tags-namespace.html
712            'namespace': ['parseType', 'parseNamePathOptional', 'ensureEnd'],
713            // http://usejsdoc.org/tags-private.html
714            'private': ['parseType', 'parseDescription'],
715            // http://usejsdoc.org/tags-protected.html
716            'protected': ['parseType', 'parseDescription'],
717            // http://usejsdoc.org/tags-public.html
718            'public': ['parseType', 'parseDescription'],
719            // http://usejsdoc.org/tags-readonly.html
720            'readonly': ['ensureEnd'],
721            // http://usejsdoc.org/tags-requires.html
722            'requires': ['parseNamePath', 'ensureEnd'],
723            // http://usejsdoc.org/tags-since.html
724            'since': ['parseDescription'],
725            // http://usejsdoc.org/tags-static.html
726            'static': ['ensureEnd'],
727            // http://usejsdoc.org/tags-summary.html
728            'summary': ['parseDescription'],
729            // http://usejsdoc.org/tags-this.html
730            'this': ['parseThis', 'ensureEnd'],
731            // http://usejsdoc.org/tags-todo.html
732            'todo': ['parseDescription'],
733            // http://usejsdoc.org/tags-typedef.html
734            'typedef': ['parseType', 'parseNamePathOptional'],
735            // http://usejsdoc.org/tags-variation.html
736            'variation': ['parseVariation'],
737            // http://usejsdoc.org/tags-version.html
738            'version': ['parseDescription']
739        };
740
741        TagParser.prototype.parse = function parse() {
742            var i, iz, sequences, method;
743
744
745            // empty title
746            if (!this._title) {
747                if (!this.addError('Missing or invalid title')) {
748                    return null;
749                }
750            }
751
752            // Seek to content last index.
753            this._last = seekContent(this._title);
754
755            if (this._options.range) {
756                this._tag.range = [this._first, source.slice(0, this._last).replace(/\s*$/, '').length].map(convertIndex);
757            }
758
759            if (hasOwnProperty(Rules, this._title)) {
760                sequences = Rules[this._title];
761            } else {
762                // default sequences
763                sequences = ['parseType', 'parseName', 'parseDescription', 'epilogue'];
764            }
765
766            for (i = 0, iz = sequences.length; i < iz; ++i) {
767                method = sequences[i];
768                if (!this[method]()) {
769                    return null;
770                }
771            }
772
773            return this._tag;
774        };
775
776        function parseTag(options) {
777            var title, parser, tag;
778
779            // skip to tag
780            if (!skipToTag()) {
781                return null;
782            }
783
784            // scan title
785            title = scanTitle();
786
787            // construct tag parser
788            parser = new TagParser(options, title);
789            tag = parser.parse();
790
791            // Seek global index to end of this tag.
792            while (index < parser._last) {
793                advance();
794            }
795
796            return tag;
797        }
798
799        //
800        // Parse JSDoc
801        //
802
803        function scanJSDocDescription(preserveWhitespace) {
804            var description = '', ch, atAllowed;
805
806            atAllowed = true;
807            while (index < length) {
808                ch = source.charCodeAt(index);
809
810                if (atAllowed && ch === 0x40  /* '@' */) {
811                    break;
812                }
813
814                if (esutils.code.isLineTerminator(ch)) {
815                    atAllowed = true;
816                } else if (atAllowed && !esutils.code.isWhiteSpace(ch)) {
817                    atAllowed = false;
818                }
819
820                description += advance();
821            }
822
823            return preserveWhitespace ? description : description.trim();
824        }
825
826        function parse(comment, options) {
827            var tags = [], tag, description, interestingTags, i, iz;
828
829            if (options === undefined) {
830                options = {};
831            }
832
833            if (typeof options.unwrap === 'boolean' && options.unwrap) {
834                source = unwrapComment(comment);
835            } else {
836                source = comment;
837            }
838
839            originalSource = comment;
840
841            // array of relevant tags
842            if (options.tags) {
843                if (Array.isArray(options.tags)) {
844                    interestingTags = { };
845                    for (i = 0, iz = options.tags.length; i < iz; i++) {
846                        if (typeof options.tags[i] === 'string') {
847                            interestingTags[options.tags[i]] = true;
848                        } else {
849                            utility.throwError('Invalid "tags" parameter: ' + options.tags);
850                        }
851                    }
852                } else {
853                    utility.throwError('Invalid "tags" parameter: ' + options.tags);
854                }
855            }
856
857            length = source.length;
858            index = 0;
859            lineNumber = 0;
860            recoverable = options.recoverable;
861            sloppy = options.sloppy;
862            strict = options.strict;
863
864            description = scanJSDocDescription(options.preserveWhitespace);
865
866            while (true) {
867                tag = parseTag(options);
868                if (!tag) {
869                    break;
870                }
871                if (!interestingTags || interestingTags.hasOwnProperty(tag.title)) {
872                    tags.push(tag);
873                }
874            }
875
876            return {
877                description: description,
878                tags: tags
879            };
880        }
881        exports.parse = parse;
882    }(jsdoc = {}));
883
884    exports.version = utility.VERSION;
885    exports.parse = jsdoc.parse;
886    exports.parseType = typed.parseType;
887    exports.parseParamType = typed.parseParamType;
888    exports.unwrapComment = unwrapComment;
889    exports.Syntax = shallowCopy(typed.Syntax);
890    exports.Error = utility.DoctrineError;
891    exports.type = {
892        Syntax: exports.Syntax,
893        parseType: typed.parseType,
894        parseParamType: typed.parseParamType,
895        stringify: typed.stringify
896    };
897}());
898/* vim: set sw=4 ts=4 et tw=80 : */
899