Added implementation and tests for @returns and @example.

This commit is contained in:
Michael Mathews 2010-06-15 22:00:57 +01:00
parent 1dac5e534d
commit 95647fc526
6 changed files with 273 additions and 196 deletions

View File

@ -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.<Object>
*/
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');
}
}
}

View File

@ -1,134 +1,143 @@
/**
@overview
@author Michael Mathews <micmath@gmail.com>
@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.<string>*/ 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.<string> 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 <micmath@gmail.com>
@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.<string>*/ 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.<Object>
*/
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.<string> 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, '');
}
})();

View File

@ -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); };

View File

@ -36,7 +36,7 @@
}
}
}
if (type === '') { text = tagText; }
[type, optional] = parseOptional(type);

47
tests/tag_example.js Normal file
View File

@ -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) {
}
}

43
tests/tag_returns.js Normal file
View File

@ -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) {
}
}