diff --git a/modules/jsdoc/doclet.js b/modules/jsdoc/doclet.js index 11d9060c..90532b4b 100644 --- a/modules/jsdoc/doclet.js +++ b/modules/jsdoc/doclet.js @@ -10,7 +10,7 @@ */ (function() { var name = require('jsdoc/name'), - tag = require('jsdoc/tag'); + parse_tag = require('jsdoc/tag'); /** Factory that builds a Doclet object. @@ -30,8 +30,8 @@ commentSrc = unwrapComment(commentSrc); commentSrc = fixDesc(commentSrc); - tags = parseTags(commentSrc); - + tags = parse_tag.parse(commentSrc); + try { preprocess(tags); } @@ -95,7 +95,7 @@ // still here? if (text) { - this.tags.push( tag.fromTagText(tagName + ' ' + text) ); + this.tags.push( parse_tag.fromTagText(tagName + ' ' + text) ); return text; } @@ -119,7 +119,7 @@ } // safe to export to JSON - var exportTags = ['name', 'path', 'denom', 'desc', 'type', 'param', 'returns', 'exports', 'requires', 'memberof', 'access', 'attribute']; + var exportTags = ['name', 'path', 'denom', 'desc', 'type', 'param', 'returns', 'exports', 'requires', 'memberof', 'access', 'attribute', 'example', 'see']; /** Get a JSON-compatible object representing this Doclet. @@ -132,7 +132,7 @@ for (var i = 0, leni = this.tags.length; i < leni; i++) { tag = this.tags[i]; - + if ( exportTags.indexOf(tag.name) === -1 ) { continue; } tagName = tag.name; @@ -150,20 +150,20 @@ tagValue.name = tag.pname; } } - - if (tag.type && tag.type.length) { - tagValue.type = tag.type; - } } + if (tag.type && tag.type.length) { + tagValue.type = tag.type; + } + if (tag.pdesc) { tagValue.desc = tag.pdesc; } if (typeof tag.poptional === 'boolean') { tagValue.optional = tag.poptional; } if (typeof tag.pnullable === 'boolean') { tagValue.nullable = tag.pnullable; } // tag value is not an object, it's just a simple string - if (!tag.pname && !tag.pdesc) { // TODO: should check the list instead? + if (!tag.pname && !tag.pdesc && !(tag.type && tag.type.length)) { // TODO: should check the list instead? tagValue = tag.text; } - + if (tagValue) { if (typeof o[tagName] === 'undefined') { // not defined o[tagName] = tagValue; @@ -192,8 +192,10 @@ function unwrapComment(commentSrc) { if (!commentSrc) { return ''; } - // TODO keep leading white space for @examples - return commentSrc ? commentSrc.replace(/(^\/\*\*+\s*|\s*\**\*\/$)/g, "").replace(/^\s*\* ?/gm, "") : ""; + // note: keep trailing whitespace for @examples + // extra opening/closing stars are ignored + // left margin is considered a star and a space + return commentSrc ? commentSrc.replace(/^\/\*\*+/, "").replace(/\**\*\/$/, "\\Z").replace(/^\s*(\* ?|\\Z)/gm, "") : ""; } /** @@ -210,31 +212,6 @@ return commentSrc; } - /** - Given the source of a jsdoc comment, finds the tags. - @private - @function parseTags - @param {string} commentSrc Unwrapped. - @returns Array. - */ - function parseTags(commentSrc) { - var tags = []; - - // split out the basic tags - commentSrc - .split(/(^|[\r\n])\s*@/) - .filter( function($){ return $.match(/\S/); } ) - .forEach(function($) { - var newTag = tag.fromTagText($); - - if (newTag.name) { - tags.push(newTag); - } - }); - - return tags; - } - // other tags that can provide the memberof var memberofs = {methodof: 'method', eventof: 'event'}; // other tags that can provide the symbol name @@ -259,19 +236,19 @@ while(i--) { if (tags[i].name === 'private') { - tags[tags.length] = tag.fromTagText('access private'); + tags[tags.length] = parse_tag.fromTagText('access private'); } else if (tags[i].name === 'protected') { - tags[tags.length] = tag.fromTagText('access protected'); + tags[tags.length] = parse_tag.fromTagText('access protected'); } else if (tags[i].name === 'public') { - tags[tags.length] = tag.fromTagText('access public'); + tags[tags.length] = parse_tag.fromTagText('access public'); } else if (tags[i].name === 'const') { - tags[tags.length] = tag.fromTagText('attribute constant'); + tags[tags.length] = parse_tag.fromTagText('attribute constant'); } else if (tags[i].name === 'readonly') { - tags[tags.length] = tag.fromTagText('attribute readonly'); + tags[tags.length] = parse_tag.fromTagText('attribute readonly'); } else if (tags[i].name === 'name') { if (name && name !== tags[i].text) { @@ -289,7 +266,7 @@ if (memberof) { throw new DocTagConflictError('doclet has too many tags of type: @memberof.'); } - taggedMemberof = memberof = tags[i].text+'ZZZ_0'; + taggedMemberof = memberof = tags[i].text; } if ( nameables.indexOf(tags[i].name) > -1 ) { @@ -301,11 +278,11 @@ } if (tags[i].pdesc) { - tags[tags.length] = tag.fromTagText('desc ' + tags[i].pdesc); + tags[tags.length] = parse_tag.fromTagText('desc ' + tags[i].pdesc); } if (tags[i].type) { - tags[tags.length] = tag.fromTagText('type ' + tags[i].type.join('|')); + tags[tags.length] = parse_tag.fromTagText('type ' + tags[i].type.join('|')); } if (denom && denom !== tags[i].name) { @@ -320,7 +297,7 @@ if (memberof) { throw new DocTagConflictError('doclet has too many tags of type: @memberof.'); } - memberof = tags[i].text+'ZZZ_1'; + memberof = tags[i].text; } if (denom && denom !== memberofs[tags[i].name]) { @@ -331,40 +308,40 @@ } if (name && !taggedName) { - tags[tags.length] = tag.fromTagText('name ' + name); + tags[tags.length] = parse_tag.fromTagText('name ' + name); } if (denom && !taggedDenom) { - tags[tags.length] = tag.fromTagText('denom ' + denom); + tags[tags.length] = parse_tag.fromTagText('denom ' + denom); } if (memberof && !taggedMemberof) { - tags[tags.length] = tag.fromTagText('memberof ' + memberof+'ZZZ_2'); + tags[tags.length] = parse_tag.fromTagText('memberof ' + memberof); } } function postprocess(doclet) { if ( doclet.hasTag('class') && !doclet.hasTag('constructor') ) { - doclet.tags[doclet.tags.length] = tag.fromTagText('denom constructor'); + doclet.tags[doclet.tags.length] = parse_tag.fromTagText('denom constructor'); } if ( doclet.hasTag('enum')) { if (!doclet.hasTag('type')) { - doclet.tags[doclet.tags.length] = tag.fromTagText('type number'); + doclet.tags[doclet.tags.length] = parse_tag.fromTagText('type number'); } if (!doclet.hasTag('readonly') && !doclet.hasTag('const')) { - doclet.tags[doclet.tags.length] = tag.fromTagText('attribute constant'); + doclet.tags[doclet.tags.length] = parse_tag.fromTagText('attribute constant'); } } if ( doclet.hasTag('const')) { if (!doclet.hasTag('denom')) { - doclet.tags[doclet.tags.length] = tag.fromTagText('denom member'); + doclet.tags[doclet.tags.length] = parse_tag.fromTagText('denom member'); } if (!doclet.hasTag('readonly') && !doclet.hasTag('const')) { - doclet.tags[doclet.tags.length] = tag.fromTagText('attribute constant'); + doclet.tags[doclet.tags.length] = parse_tag.fromTagText('attribute constant'); } } } diff --git a/modules/jsdoc/tag.js b/modules/jsdoc/tag.js index 01b9751b..88feb155 100644 --- a/modules/jsdoc/tag.js +++ b/modules/jsdoc/tag.js @@ -1,134 +1,143 @@ -/** - @overview - @author Michael Mathews - @license Apache License 2.0 - See file 'LICENSE.md' in this project. - */ - -/** - Create tag objects. - @module jsdoc/tag - */ -(function() { - var jsdoc_type = require('jsdoc/type'); - - exports.fromCommentText = function(commentText) { - var tag, - tags = []; - - // split out the basic tags - commentText - .split(/(^|[\r\n])\s*@/) - .filter( function($){ return $.match(/\S/); } ) - .forEach(function($) { - tag = fromTagText($); - - if (tag.name) { - tags.push(tag); - } - else { - // TODO: warn about tag with no name? - } - }); - - return tags; - } - - exports.fromTagText = function(tagText) { - return new Tag(tagText); - } - - var longTags = ['param', 'constructor', 'const', 'module', 'event', 'namespace', 'method', 'member', 'function', 'variable', 'enum', 'returns']; - var anonTags = ['returns']; - /** - @private - @constructor Tag - @param {string} tagText - */ - function Tag(tagText) { - this.raw = tagText; - this.name = ''; - this.type = []; - this.text = ''; - this.pname = ''; - this.pdesc = ''; - - // tagText is like: "tagname tag text" - var bits = tagText.match(/^(\S+)(?:\s+([\s\S]*))?$/); - - if (bits) { - this.name = (bits[1] || '').toLowerCase(); // like @name - this.name = trim( resolveSynonyms(this.name) ); - - this.text = trim( bits[2] ) || ''; // all the rest of the tag - - var /*Array.*/ type, - /*string*/ text, - /*?boolean*/ optional, - /*?boolean*/ nullable; - [type, text, optional, nullable] = jsdoc_type.parse(this.text); - - // @type tags are the only tag that is not allowed to have a {type}! - if (this.name === 'type') { - text = text || type.join('|'); - type = []; - } - - // don't add an empty type or null attributes - if (type && type.length) { this.type = type; } - if (optional !== null) { this.poptional = optional; } - if (nullable !== null) { this.pnullable = nullable; } - - this.text = text; - if (longTags.indexOf(this.name) > -1) { // is a tag that uses the long format - if (anonTags.indexOf(this.name) > -1) { - this.pdesc = this.text; - } - else { - var [pname, pdesc] = parsePname(this.text); - this.pname = pname; - this.pdesc = pdesc; - } - } - } - } - - Tag.prototype.toString = function() { - return '@'+this.raw; - } - - /** - Split the parameter name and parameter desc from the tag text. - @private - @method parsePname - @param {string} tagText - @returns Array. The pname and the pdesc. - */ - function parsePname(tagText) { - tagText.match(/^(\S+)(\s+(\S[\s\S]*))?$/); - - return [RegExp.$1, RegExp.$3]; - } - - function resolveSynonyms(name) { - if ( exports.synonyms.hasOwnProperty(name) ) { - return exports.synonyms[name]; - } - else { - return name; - } - } - exports.synonyms = { - 'description': 'desc', - 'function': 'method', - 'variable': 'member', - 'return': 'returns' - } - - //TODO: move into a shared module? - /** @private */ - function trim(text) { - if (!text) { return ''; } - return text.replace(/^\s+|\s+$/g, ''); - } - +/** + @overview + @author Michael Mathews + @license Apache License 2.0 - See file 'LICENSE.md' in this project. + */ + +/** + Create tag objects. + @module jsdoc/tag + */ +(function() { + var jsdoc_type = require('jsdoc/type'); + + exports.fromTagText = function(tagText) { + return new Tag(tagText); + } + + // tags that have {type} (name desc|text) + var longTags = ['param', 'constructor', 'const', 'module', 'event', 'namespace', 'method', 'member', 'function', 'variable', 'enum', 'returns']; + // tags that have {type} text + var anonTags = ['returns']; + + /** + @private + @constructor Tag + @param {string} tagText + */ + function Tag(tagText) { + this.raw = tagText; + this.name = ''; + this.type = []; + this.text = ''; + this.pname = ''; + this.pdesc = ''; + + // tagText is like: "tagname tag text" + var bits = tagText.match(/^\s*(\S+)(?:\s([\s\S]*))?$/); + + if (bits) { + this.name = (bits[1] || '').toLowerCase(); // like @name + this.name = trim( resolveSynonyms(this.name) ); + + this.text = bits[2] || ''; // all the rest of the tag + + if (this.name !== 'example') { // example is the only tag that preserves whitespace + this.text = trim( this.text ); + } + + if (longTags.indexOf(this.name) > -1) { // is a tag that uses the long format + var /*Array.*/ type, + /*string*/ text, + /*?boolean*/ optional, + /*?boolean*/ nullable; + [type, text, optional, nullable] = jsdoc_type.parse(this.text); + + // @type tags are the only tag that is not allowed to have a {type}! + if (this.name === 'type') { + text = text || type.join('|'); + type = []; + } + + // don't add an empty type or null attributes + if (type && type.length) { this.type = type; } + if (optional !== null) { this.poptional = optional; } + if (nullable !== null) { this.pnullable = nullable; } + + this.text = text; + + if (anonTags.indexOf(this.name) > -1) { + this.pdesc = this.text; + } + else { + var [pname, pdesc] = parsePname(this.text); + this.pname = pname; + this.pdesc = pdesc; + } + } + } + } + + /** + Given the source of a jsdoc comment, finds the tags. + @private + @function parse + @param {string} commentSrc Unwrapped. + @returns Array. + */ + exports.parse = function(commentSrc) { + var tags = []; + + // split out the basic tags, keep surrounding whitespace + commentSrc + .replace(/^(\s*)@(\S)/gm, '$1\\@$2') // replace splitter ats with an arbitrary sequence (unicode_recordseperator+@) + .split('\\@') // then split on that arbitrary sequence + .forEach(function($) { + var newTag = exports.fromTagText($); + + if (newTag.name) { tags.push(newTag); } + }); + + return tags; + } + + Tag.prototype.toString = function() { + return '@'+this.raw; + } + + /** + Split the parameter name and parameter desc from the tag text. + @private + @method parsePname + @param {string} tagText + @returns Array. The pname and the pdesc. + */ + function parsePname(tagText) { + tagText.match(/^(\S+)(\s+(\S[\s\S]*))?$/); + + return [RegExp.$1, RegExp.$3]; + } + + function resolveSynonyms(name) { + if ( exports.synonyms.hasOwnProperty(name) ) { + return exports.synonyms[name]; + } + else { + return name; + } + } + exports.synonyms = { + 'description': 'desc', + 'function': 'method', + 'variable': 'member', + 'return': 'returns' + } + + //TODO: move into a shared module? + /** @private */ + function trim(text) { + if (!text) { return ''; } + return text.replace(/^\s+|\s+$/g, ''); + } + })(); \ No newline at end of file diff --git a/modules/jsdoc/test.js b/modules/jsdoc/test.js index e1972355..edfd769e 100644 --- a/modules/jsdoc/test.js +++ b/modules/jsdoc/test.js @@ -14,13 +14,14 @@ load(BASEDIR + 'lib/jsunity.js'); testSuites = []; - load(BASEDIR + 'tests/opts.js'); - load(BASEDIR + 'tests/docset.js'); - load(BASEDIR + 'tests/tag_namespace.js'); + load(BASEDIR + 'tests/opts.js'); + load(BASEDIR + 'tests/docset.js'); + load(BASEDIR + 'tests/tag_namespace.js'); load(BASEDIR + 'tests/tag_constructor.js'); - load(BASEDIR + 'tests/tag_const.js'); - load(BASEDIR + 'tests/tag_enum.js'); - load(BASEDIR + 'tests/tag_param.js'); + load(BASEDIR + 'tests/tag_const.js'); + load(BASEDIR + 'tests/tag_enum.js'); + load(BASEDIR + 'tests/tag_param.js'); + load(BASEDIR + 'tests/tag_example.js'); jsUnity.attachAssertions(); jsUnity.log = function (s) { print(s); }; diff --git a/modules/jsdoc/type.js b/modules/jsdoc/type.js index ca3e2930..38db8ea1 100644 --- a/modules/jsdoc/type.js +++ b/modules/jsdoc/type.js @@ -36,7 +36,7 @@ } } } - + if (type === '') { text = tagText; } [type, optional] = parseOptional(type); diff --git a/tests/tag_example.js b/tests/tag_example.js new file mode 100644 index 00000000..b36307d9 --- /dev/null +++ b/tests/tag_example.js @@ -0,0 +1,47 @@ +(function() { + var jsdoc = { parser: require('jsdoc/parser') }; + + jsdoc.parser.parseFiles(BASEDIR + 'tests/tag_example.js'); + var docset = jsdoc.parser.result; + + var testSuite = { + suiteName: 'tag_example', + + setUp: function() { + }, + + tearDown: function() { + }, + + testExample: function() { + var docs = docset.getDocsByPath('rotate'); + + assertEqual(docs.length, 1, 'All doclets by that path name are found.'); + + var doc = docs[0].toObject(), + examples = doc.example; + + assertEqual(typeof examples, 'object', 'The doclet has examples.'); + assertEqual(examples.length, 2, 'The doclet has the expected number of examples.'); + assertEqual(examples[0], ' var myShape = new Shape();\n rotate(myShape, 90, {0, 0});\n', 'The doclet has the expected text.'); + + assertEqual(examples[1], '{key: rotate(myShape, -45) } // thats not a type expression\n', 'The doclet has the expected text when braces are at the start.'); + + } + }; + + testSuites.push(testSuite); +})(); + +function sample() { + + /** + * @method + * @example + * var myShape = new Shape(); + * rotate(myShape, 90, {0, 0}); + * @example {key: rotate(myShape, -45) } // thats not a type expression + */ + function rotate(shape, deg, axis) { + } +} diff --git a/tests/tag_returns.js b/tests/tag_returns.js new file mode 100644 index 00000000..4e4bb851 --- /dev/null +++ b/tests/tag_returns.js @@ -0,0 +1,43 @@ +(function() { + var jsdoc = { parser: require('jsdoc/parser') }; + + jsdoc.parser.parseFiles(BASEDIR + 'tests/tag_returns.js'); + var docset = jsdoc.parser.result; + + var testSuite = { + suiteName: 'tag_returns', + + setUp: function() { + }, + + tearDown: function() { + }, + + testExample: function() { + var docs = docset.getDocsByPath('data'); + + assertEqual(docs.length, 1, 'All doclets by that path name are found.'); + + var doc = docs[0].toObject(), + returns = doc.returns; + + assertEqual(typeof returns, 'object', 'The doclet has examples.'); + assertEqual(returns.length, 2, 'The doclet has the expected number of examples.'); + assertEqual(returns[0].text, 'blah blah', 'The tag has the expected text.'); + + assertEqual(returns[1].type, 'boolean', 'The tag has the expected type.'); + } + }; + + testSuites.push(testSuite); +})(); + +function sample() { + + /** + * @method + * @returns {boolean} + */ + function data(name, value) { + } +}