From 9a45e4a8a4897136be74a3c394f735c3e9d9e643 Mon Sep 17 00:00:00 2001 From: Michael Mathews Date: Tue, 15 Jun 2010 00:09:09 +0100 Subject: [PATCH] Added support for params, nullable, optional. --- modules/jsdoc/doclet.js | 57 +++++++++++++++------- modules/jsdoc/name.js | 2 +- modules/jsdoc/tag.js | 79 +++++++++++------------------- modules/jsdoc/test.js | 11 +++-- modules/jsdoc/type.js | 96 ++++++++++++++++++++++++++++++++++++ tests/tag_const.js | 18 +++---- tests/tag_param.js | 105 ++++++++++++++++++++++++++++++++++++++++ 7 files changed, 284 insertions(+), 84 deletions(-) create mode 100644 modules/jsdoc/type.js create mode 100644 tests/tag_param.js diff --git a/modules/jsdoc/doclet.js b/modules/jsdoc/doclet.js index 139d46d6..bbc8dcda 100644 --- a/modules/jsdoc/doclet.js +++ b/modules/jsdoc/doclet.js @@ -131,27 +131,48 @@ o = {}; for (var i = 0, leni = this.tags.length; i < leni; i++) { - if (exportTags.indexOf(this.tags[i].name) === -1) { continue; } - tag = this.tags[i]; + + if ( exportTags.indexOf(tag.name) === -1 ) { continue; } + tagName = tag.name; tagValue = {}; - - if (tag.type) { - tagValue.type = tag.type; - // not a long tag - if (!tag.pname && tag.text) { tagValue.text = tag.text; } - } + +// if (tag.type && tag.type.length) { +// tagValue.type = tag.type; +// // not a long tag +// if (!tag.pname && tag.text) { tagValue.text = tag.text; } +// } + // a long tag - if (tag.pname) { tagValue.name = tag.pname; } + if (tag.pname) { + + if ( /^\[(.+)\]$/.test(tag.pname) ) { + tagValue.name = RegExp.$1; + tag.poptional = true; + } + else { + tagValue.name = tag.pname; + } + tagValue.type = tag.type; +// print('```` name is '+tagName+': '+tagValue); + } 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.type) { tagValue = tag.text; } - - if (!o[tagName]) { o[tagName] = tagValue; } - else if (o[tagName].push) { o[tagName].push(tagValue); } - else { + if (!tag.pname) { + tagValue = tag.text; + } + + if (typeof o[tagName] === 'undefined') { // not defined + o[tagName] = tagValue; + } + else if (o[tagName].push) { // is an array + o[tagName].push(tagValue); + } + else { // is a string, but needs to be an array o[tagName] = [ o[tagName] ]; o[tagName].push(tagValue); } @@ -266,7 +287,7 @@ if (memberof) { throw new DocTagConflictError('doclet has too many tags of type: @memberof.'); } - taggedMemberof = memberof = tags[i].text; + taggedMemberof = memberof = tags[i].text+'ZZZ_0'; } if ( nameables.indexOf(tags[i].name) > -1 ) { @@ -282,7 +303,7 @@ } if (tags[i].type) { - tags[tags.length] = tag.fromTagText('type ' + tags[i].type); + tags[tags.length] = tag.fromTagText('type ' + tags[i].type.join('|')); } if (denom && denom !== tags[i].name) { @@ -297,7 +318,7 @@ if (memberof) { throw new DocTagConflictError('doclet has too many tags of type: @memberof.'); } - memberof = tags[i].text; + memberof = tags[i].text+'ZZZ_1'; } if (denom && denom !== memberofs[tags[i].name]) { @@ -316,7 +337,7 @@ } if (memberof && !taggedMemberof) { - tags[tags.length] = tag.fromTagText('memberof ' + memberof); + tags[tags.length] = tag.fromTagText('memberof ' + memberof+'ZZZ_2'); } } diff --git a/modules/jsdoc/name.js b/modules/jsdoc/name.js index 9d17091e..471faa29 100644 --- a/modules/jsdoc/name.js +++ b/modules/jsdoc/name.js @@ -76,7 +76,7 @@ var shortname = path.split(/([#.-])/).pop(), splitOn = RegExp.$1, splitAt = path.lastIndexOf(splitOn), - prefix = (splitAt === -1)? '' : path.slice(0, splitAt); + prefix = (splitOn && splitAt !== -1)? path.slice(0, splitAt) : ''; if (splitOn === '#') { prefix = prefix + splitOn; } return [prefix, shortname]; diff --git a/modules/jsdoc/tag.js b/modules/jsdoc/tag.js index d22110fd..b6412092 100644 --- a/modules/jsdoc/tag.js +++ b/modules/jsdoc/tag.js @@ -9,6 +9,7 @@ @module jsdoc/tag */ (function() { + var jsdoc_type = require('jsdoc/type'); exports.fromCommentText = function(commentText) { var tag, @@ -45,7 +46,7 @@ function Tag(tagText) { this.raw = tagText; this.name = ''; - this.type = ''; + this.type = []; this.text = ''; this.pname = ''; this.pdesc = ''; @@ -54,24 +55,29 @@ var bits = tagText.match(/^(\S+)(?:\s+([\s\S]*))?$/); if (bits) { - - this.name = (bits[1] || '').toLowerCase(); - this.text = bits[2] || ''; + this.name = (bits[1] || '').toLowerCase(); // like @name + this.name = synonym(this.name); - var typeText = splitType(this.text); + this.text = bits[2] || ''; // all the rest of the tag + + var type, text, optional, 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') { - typeText.text = typeText.text || typeText.type; - delete typeText.type; + text = text || type.join('|'); + type = []; } - this.type = typeText.type; - - this.text = trim(typeText.text); + 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 - var [pname, pdesc] = splitPname(this.text); + var [pname, pdesc] = parsePname(this.text); this.pname = pname; this.pdesc = pdesc; } @@ -85,55 +91,28 @@ /** Split the parameter name and parameter desc from the tag text. @private - @method splitPname + @method parsePname @param {string} tagText @returns Array. The pname and the pdesc. */ - function splitPname(tagText) { + function parsePname(tagText) { tagText.match(/^(\S+)(\s+(\S.*))?$/); return [RegExp.$1, RegExp.$3]; } - /** - Split the tag type and remaining tag text from the tag text. - @private - @method splitType - @param {string} tagText - @returns Object Like {type: tagType, text: tagText} - */ - function splitType(tagText) { - var type = '', - text = tagText, - count = 0; - - // I reserve the right to use {@whatever ...} for something unrelated to type - if (tagText[0] === '{' && tagText[1] !== '@') { - count++; - - for (var i = 1, leni = tagText.length; i < leni; i++) { - if (tagText[i] === '{') { count++; } - if (tagText[i] === '}') { count--; } - if (count === 0) { - type = trim(tagText.slice(1, i)); - text = trim(tagText.slice(i+1)); - break; - } - } + function synonym(name) { + if ( synonym.map.hasOwnProperty(name) ) { + return synonym.map[name]; + } + else { + return name; } - - return { type: type, text: text }; } - - /** - Remove leading and trailing whitespace. - @private - @method trim - @param {string} text - @returns {string} - */ - function trim(text) { - return text.replace(/^\s+|\s+$/g, ''); + synonym.map = { + 'description': 'desc', + 'function': 'method', + 'variable': 'member' } })(); \ No newline at end of file diff --git a/modules/jsdoc/test.js b/modules/jsdoc/test.js index c4732286..e1972355 100644 --- a/modules/jsdoc/test.js +++ b/modules/jsdoc/test.js @@ -14,12 +14,13 @@ 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_const.js'); + load(BASEDIR + 'tests/tag_enum.js'); + load(BASEDIR + 'tests/tag_param.js'); jsUnity.attachAssertions(); jsUnity.log = function (s) { print(s); }; diff --git a/modules/jsdoc/type.js b/modules/jsdoc/type.js new file mode 100644 index 00000000..199ee52f --- /dev/null +++ b/modules/jsdoc/type.js @@ -0,0 +1,96 @@ +/** + @overview + @author Michael Mathews + @license Apache License 2.0 - See file 'LICENSE.md' in this project. + */ + +/** + Parse type expressions. + @module jsdoc/type + */ +(function() { + + /** + @param {string} tagText + @returns {Array.} + */ + exports.parse = function(tagText) { + if (typeof tagText !== 'string') { tagText = ''; } + var type = '', + types = [], + text = '', + count = 0; + + // type expressions start with '{' + if (tagText[0] === '{') { + count++; + + // find matching closer '}' + for (var i = 1, leni = tagText.length; i < leni; i++) { + if (tagText[i] === '{') { count++; } + else if (tagText[i] === '}') { count--; } + + if (count === 0) { + type = trim(tagText.slice(1, i)); + text = trim(tagText.slice(i+1)); + break; + } + } + } + + if (type === '') { text = tagText; } + + [type, optional] = parseOptional(type); + [type, nullable] = parseNullable(type); + + types = parseTypes(type); // make it into an array + + return [types, text, optional, nullable]; + } + + function parseOptional(type) { + var optional = null; + + // {sometype=} means optional + if ( /(.+)=$/.test(type) ) { + type = RegExp.$1; + optional = true; + } + + return [type, optional]; + } + + function parseNullable(type) { + var nullable = null; + + // {?sometype} means nullable, {!sometype} means not-nullable + if ( /^([\?\!])(.+)$/.test(type) ) { + type = RegExp.$2; + nullable = (RegExp.$1 === '?')? true : false; + } + + return [type, nullable]; + } + + function parseTypes(type) { + var types = []; + + if (type.indexOf('|') > -1) { + // remove optional parens + if ( /^\s*\(\s*(.+)\s*\)\s*$/.test(type) ) { + type = RegExp.$1; + } + types = type.split(/\s*\|\s*/g); + } + else { + types = [type]; + } + + return types; + } + + /** @private */ + function trim(text) { + return text.replace(/^\s+|\s+$/g, ''); + } +})(); \ No newline at end of file diff --git a/tests/tag_const.js b/tests/tag_const.js index 0011a21c..29434136 100644 --- a/tests/tag_const.js +++ b/tests/tag_const.js @@ -18,15 +18,15 @@ }, testConstCompactTag: function() { - var doc = docset.getDocsByPath('pi'); - assertEqual(doc.length, 1, '1 doclet by that name is found.'); - - doc = doc[0]; + var docs = docset.getDocsByPath('pi'); + assertEqual(docs.length, 1, '1 doclet by that name is found.'); + var doc = docs[0].toObject(); + assertEqual(typeof doc, 'object', 'The found doclet is an object.'); - assertEqual(doc.tagText('path'), 'pi', 'The found doclet has the expected path.'); - assertEqual(doc.tagText('type'), 'number', 'The found doclet has the expected type.'); - assertEqual(doc.tagText('desc'), "The ratio of any circle's circumference to its diameter.", 'The found doclet has the expected desc.'); + assertEqual(doc.path, 'pi', 'The found doclet has the expected path.'); + assertEqual(doc.type, 'number', 'The found doclet has the expected type.'); + assertEqual(doc.desc, "The ratio of any circle's circumference to its diameter.", 'The found doclet has the expected desc.'); }, testConstCompactVerbose: function() { @@ -65,9 +65,7 @@ function sample() { /** * Euler's number. - * @const - * @name e - * @type number + * @const {number} e */ /** diff --git a/tests/tag_param.js b/tests/tag_param.js new file mode 100644 index 00000000..b96bf4ce --- /dev/null +++ b/tests/tag_param.js @@ -0,0 +1,105 @@ +(function() { + var jsdoc = { parser: require('jsdoc/parser') }; + + jsdoc.parser.parseFiles(BASEDIR + 'tests/tag_param.js'); + var docset = jsdoc.parser.result; + + var testSuite = { + suiteName: 'tag_param', + + setUp: function() { + }, + + tearDown: function() { + }, + + testParamWithSimpleType: function() { + var docs = docset.getDocsByPath('Shape'); + + assertEqual(docs.length, 1, 'All constructor doclets by that path name are found.'); + + var doc = docs[0].toObject(), + params = doc.param; +//print('>>> doc is '+doc.toSource()); +//print('>>> params is '+params.toSource()); + assertEqual(params[0].name, 'top', 'The found parameter has the correct name.'); + assertEqual(typeof params[0].type, 'object', 'The found parameter has types.'); + assertEqual(params[0].type.length, 1, 'The found parameter has the correct number of types.'); + assertEqual(params[0].type[0], 'number', 'The found parameter has the correct type value.'); + + }, + + testParamWithNullableType: function() { + var docs = docset.getDocsByPath('Shape'); + + assertEqual(docs.length, 1, 'All constructor doclets by that path name are found.'); + + var doc = docs[0].toObject(), + params = doc.param; + + assertEqual(params[1].name, 'left', 'The found parameter has the correct name.'); + assertEqual(typeof params[1].type, 'object', 'The found parameter has types.'); + assertEqual(params[1].type.length, 1, 'The found parameter has the correct number of types.'); + assertEqual(params[1].type[0], 'number', 'The found parameter has the correct type value.'); + assertEqual(params[1].nullable, false, 'The found parameter has the correct !nullable value.'); + assertEqual(params[2].nullable, true, 'The found parameter has the correct ?nullable value.'); + }, + + testParamWithOptionalType: function() { + var docs = docset.getDocsByPath('Shape'); + + assertEqual(docs.length, 1, 'All doclets by that path name are found.'); + + var doc = docs[0].toObject(), + params = doc.param; + + assertEqual(params[3].name, 'fixed', 'The found parameter has the correct name.'); + assertEqual(typeof params[1].type, 'object', 'The found parameter has types.'); + assertEqual(params[3].type.length, 1, 'The found parameter has the correct number of types.'); + assertEqual(params[3].type[0], 'boolean', 'The found parameter has the correct type value.'); + assertEqual(params[3].nullable, undefined, 'The found parameter has the default nullable value.'); + assertEqual(params[3].optional, true, 'The found parameter has the correct optional value.'); + }, + + testParamWithMultipleType: function() { + var docs = docset.getDocsByPath('rotate'); + + assertEqual(docs.length, 1, 'All doclets by that path name are found.'); + + var doc = docs[0].toObject(), + params = doc.param; + + assertEqual(params[0].name, 'deg', 'The found parameter has the correct name.'); + assertEqual(typeof params[0].type, 'object', 'The found parameter has types.'); + assertEqual(params[0].type.length, 2, 'The found parameter has the correct number of types.'); + assertEqual(params[0].type[0], 'Degree', 'The found parameter has the correct type[0] value.'); + assertEqual(params[0].type[1], 'number', 'The found parameter has the correct type[1] value.'); + + assertEqual(params[1].name, 'axis', 'The found parameter has the correct name.'); + assertEqual(params[1].optional, true, 'The found parameter has the correct optional.'); + + } + }; + + testSuites.push(testSuite); +})(); + +function sample() { + + /** @constructor + @param {number} top + @param {!number} left + @param {?number} sides + @param {boolean=} fixed + */ + function Shape(top, left, sides, fixed) { + } + + /** @method + @param {Degree|number} deg + @param [axis] + */ + function rotate(deg, axis) { + + } +}