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