diff --git a/rhino_modules/jsdoc/doclet.js b/rhino_modules/jsdoc/doclet.js index 66dd1177..9ff06dc5 100644 --- a/rhino_modules/jsdoc/doclet.js +++ b/rhino_modules/jsdoc/doclet.js @@ -51,8 +51,9 @@ exports.Doclet.prototype.postProcess = function() { this.setLongname(this.name); } if (this.memberof === '') { - delete(this.memberof); + delete(this.memberof); } + if (!this.kind && this.meta && this.meta.code) { this.addTag( 'kind', codetypeToKind(this.meta.code.type) ); } @@ -229,7 +230,7 @@ function codetypeToKind(type) { var kind = (type || '').toLowerCase(); if (kind !== 'function') { - return 'property'; + return 'member'; } return kind; diff --git a/rhino_modules/jsdoc/name.js b/rhino_modules/jsdoc/name.js index fcc56701..f0a17d6b 100644 --- a/rhino_modules/jsdoc/name.js +++ b/rhino_modules/jsdoc/name.js @@ -31,18 +31,18 @@ exports.resolve = function(doclet) { name = doclet.longname = doclet.meta.code.funcscope + '~' + name; } - if (memberof) { // @memberof tag given - memberof = memberof.replace(/\.prototype\.?/g, '#'); + if (memberof || doclet.forceMemberof) { // @memberof tag given + memberof = ('' || memberof).replace(/\.prototype\.?/g, '#'); // the name is a fullname, like @name foo.bar, @memberof foo if (name && name.indexOf(memberof) === 0) { - about = exports.shorten(name); + about = exports.shorten(name, (doclet.forceMemberof? memberof : undefined)); } else if (name && /([#.~])$/.test(memberof) ) { // like @memberof foo# or @memberof foo~ - about = exports.shorten(memberof + name); + about = exports.shorten(memberof + name, (doclet.forceMemberof? memberof : undefined)); } else if (name && doclet.scope ) { // like @memberof foo# or @memberof foo~ - about = exports.shorten(memberof + scopeToPunc[doclet.scope] + name); + about = exports.shorten(memberof + (scopeToPunc[doclet.scope]||'') + name, (doclet.forceMemberof? memberof : undefined)); } } else { // no @memberof @@ -67,7 +67,7 @@ exports.resolve = function(doclet) { } else if (about.scope) { if (about.memberof === '') { // via @memberof ? - delete doclet.scope; + doclet.scope = 'global'; } else { doclet.scope = puncToScope[about.scope]; @@ -130,9 +130,10 @@ exports.applyNamespace = function(longname, ns) { Given a longname like "a.b#c(2)", slice it up into ["a.b", "#", 'c', '2'], representing the memberof, the scope, the name, and variation. @param {string} longname + @param {string} forcedMemberof @returns {object} Representing the properties of the given name. */ -exports.shorten = function(longname) { +exports.shorten = function(longname, forcedMemberof) { // quoted strings in a longname are atomic, convert to tokens var atoms = [], token; @@ -149,18 +150,32 @@ exports.shorten = function(longname) { return dot + token; // foo["bar"] => foo.@{1}@ }); - - longname = longname.replace( /\.prototype\.?/g, '#' ); - - var parts = longname? - (longname.match( /^(:?(.+)([#.~]))?(.+?)$/ ) || []).reverse() - : ['']; - var name = parts[0] || '', // ensure name is always initialised to avoid error being thrown when calling replace on undefined [gh-24] - scope = parts[1] || '', // ., ~, or # - memberof = parts[2] || '', + var name = '', + scope = '', // ., ~, or # + memberof = '', variation; + longname = longname.replace( /\.prototype\.?/g, '#' ); +//console.log(forcedMemberof); + if (typeof forcedMemberof !== 'undefined') { +//console.log('forcedMemberof'); + name = longname.substr(forcedMemberof.length); + var parts = forcedMemberof.match(/^(.*?)([#.~]?)$/); +//console.log(parts); + if (parts[1]) memberof = parts[1] || forcedMemberof; + if (parts[2]) scope = parts[2]; + } + else { + var parts = longname? + (longname.match( /^(:?(.+)([#.~]))?(.+?)$/ ) || []).reverse() + : ['']; + + name = parts[0] || ''; // ensure name is always initialised to avoid error being thrown when calling replace on undefined [gh-24] + scope = parts[1] || ''; // ., ~, or # + memberof = parts[2] || ''; + } + // like /** @name foo.bar(2) */ if ( /(.+)\(([^)]+)\)$/.test(name) ) { name = RegExp.$1, variation = RegExp.$2; @@ -179,3 +194,45 @@ exports.shorten = function(longname) { return {longname: longname, memberof: memberof, scope: scope, name: name, variation: variation}; } +/** + Split a string that starts with a name and ends with a description, into its parts. + @param {string} nameDesc + @returns {object} Hash with "name" and "description" properties. + */ +exports.splitName = function(nameDesc) { + var name = '', + desc = '', + thisChar = '', + inQuote = false; + + for (var i = 0, len = nameDesc.length; i < len; i++) { + thisChar = nameDesc.charAt(i); + + if (thisChar === '\\') { + name += thisChar + nameDesc.charAt(++i); + continue; + } + + if (thisChar === '"') { + inQuote = !inQuote; + } + + if (inQuote) { + name += thisChar; + continue; + } + + if (!inQuote) { + if ( /\s/.test(thisChar) ) { + desc = nameDesc.substr(i); + desc = desc.replace(/^[\s-\s]+/, '').trim(); + break; + } + else { + name += thisChar; + } + } + } + + return { name: name, description: desc }; +} \ No newline at end of file diff --git a/rhino_modules/jsdoc/schema.js b/rhino_modules/jsdoc/schema.js index be52250f..5cbc85e2 100644 --- a/rhino_modules/jsdoc/schema.js +++ b/rhino_modules/jsdoc/schema.js @@ -100,7 +100,7 @@ exports.jsdocSchema = { "kind": { // what kind of symbol is this? "type": "string", "maxItems": 1, - "enum": ["constructor", "module", "event", "namespace", "method", "property", "enum", "class", "interface", "constant", "mixin", "file", "version"] + "enum": ["constructor", "module", "event", "namespace", "method", "member", "enum", "class", "interface", "constant", "mixin", "file", "version"] }, "refersto": { // the path to another doc: this doc is simply a renamed alias to that "type": "string", diff --git a/rhino_modules/jsdoc/src/handlers.js b/rhino_modules/jsdoc/src/handlers.js index fd431ab6..e53064ae 100644 --- a/rhino_modules/jsdoc/src/handlers.js +++ b/rhino_modules/jsdoc/src/handlers.js @@ -10,7 +10,7 @@ var currentModule = null; @param parser */ exports.attachTo = function(parser) { - var jsdoc = {doclet: require('jsdoc/doclet')}; + var jsdoc = {doclet: require('jsdoc/doclet'), name: require('jsdoc/name')}; // handles JSDoc comments that include a @name tag -- the code is ignored in such a case parser.on('jsdocCommentFound', function(e) { @@ -40,11 +40,11 @@ exports.attachTo = function(parser) { var newDoclet = new jsdoc.doclet.Doclet(docletSrc, e); // an undocumented symbol right after a virtual comment? rhino mistakenly connected the two - if (newDoclet.name) { // there was a @name in comment - // try again, without the comment - e.comment = '@undocumented'; - newDoclet = new jsdoc.doclet.Doclet(e.comment, e); - } + if (newDoclet.name) { // there was a @name in comment + // try again, without the comment + e.comment = '@undocumented'; + newDoclet = new jsdoc.doclet.Doclet(e.comment, e); + } if (newDoclet.alias) { if (newDoclet.alias === '{@thisClass}') { @@ -117,6 +117,21 @@ exports.attachTo = function(parser) { return false; } + // find name and description from each property tag text + if (newDoclet.properties) { + for (var i = 0, len = newDoclet.properties.length; i < len; i++) { + var property = newDoclet.properties[i]; + + var parts = jsdoc.name.splitName(property.description); + property.name = parts.name; + property.description = parts.description; + } + } + + if (!newDoclet.memberof) { + newDoclet.scope = 'global'; + } + addDoclet.call(this, newDoclet); e.doclet = newDoclet; } diff --git a/rhino_modules/jsdoc/src/parser.js b/rhino_modules/jsdoc/src/parser.js index 04051e74..0e04d3f3 100644 --- a/rhino_modules/jsdoc/src/parser.js +++ b/rhino_modules/jsdoc/src/parser.js @@ -200,6 +200,23 @@ exports.Parser.prototype.resolveThis = function(node) { } } +/** + Given: foo = { x:1 }, find foo from x. + */ +exports.Parser.prototype.resolvePropertyParent = function(node) { + var memberof = {}; + + if (node.parent) { + var parent = node.parent; + if (parent.type === Token.COLON) parent = parent.parent; // go up one more + + memberof.id = 'astnode'+parent.hashCode(); + memberof.doclet = this.refs[memberof.id]; + + if (memberof.doclet) { return memberof; } + } +} + /** * Resolve what function a var is limited to. * @param {astnode} node @@ -285,6 +302,14 @@ function visitNode(node) { if (e.doclet) { currentParser.refs['astnode'+e.code.node.hashCode()] = e.doclet; // allow lookup from value => doclet } + + var parent = currentParser.resolvePropertyParent(node); + if (parent && parent.doclet.isEnum) { + if (!parent.doclet.properties) { parent.doclet.properties = []; } + // members of an enum inherit the enum's type + if (parent.doclet.type && !e.doclet.type) { e.doclet.type = parent.doclet.type; } + parent.doclet.properties.push(e.doclet); + } } else if (node.type == Token.VAR || node.type == Token.LET || node.type == Token.CONST) { diff --git a/rhino_modules/jsdoc/tag/dictionary/definitions.js b/rhino_modules/jsdoc/tag/dictionary/definitions.js index 62e09b40..52fd2881 100644 --- a/rhino_modules/jsdoc/tag/dictionary/definitions.js +++ b/rhino_modules/jsdoc/tag/dictionary/definitions.js @@ -161,6 +161,15 @@ exports.defineTags = function(dictionary) { }) .synonym('desc'); + dictionary.defineTag('enum', { + canHaveType: true, + onTagged: function(doclet, tag) { + doclet.kind = 'member'; + doclet.isEnum = true; + doclet.type = tag.value.type; + } + }); + dictionary.defineTag('event', { onTagged: function(doclet, tag) { setDocletKindToTitle(doclet, tag); @@ -291,10 +300,17 @@ exports.defineTags = function(dictionary) { dictionary.defineTag('memberof', { mustHaveValue: true, onTagged: function(doclet, tag) { + if (tag.originalTitle === 'memberof!') { + doclet.forceMemberof = true; + if (tag.value === '') { + doclet.addTag('global'); + delete doclet.memberof; + } + } setDocletMemberof(doclet, tag); } }) - .synonym('member'); + .synonym('memberof!'); dictionary.defineTag('mixin', { onTagged: function(doclet, tag) { @@ -351,6 +367,15 @@ exports.defineTags = function(dictionary) { }); dictionary.defineTag('property', { + canHaveType: true, + onTagged: function(doclet, tag) { + if (!doclet.properties) { doclet.properties = []; } + doclet.properties.push(tag.value); + } + }) + .synonym('prop'); + + dictionary.defineTag('member', { canHaveType: true, onTagged: function(doclet, tag) { setDocletKindToTitle(doclet, tag); @@ -360,7 +385,6 @@ exports.defineTags = function(dictionary) { } } }) - .synonym('prop') .synonym('var'); dictionary.defineTag('protected', { @@ -401,7 +425,7 @@ exports.defineTags = function(dictionary) { mustHaveValue: true, canHaveType: true, onTagged: function(doclet, tag) { - if (!doclet.returns) { doclet.returns = []; } + if (!doclet.returns) { doclet.returns = []; } doclet.returns.push(tag.value); } }) @@ -538,7 +562,9 @@ function setNameToFile(doclet, tag) { } function setDocletMemberof(doclet, tag) { - doclet.setMemberof(tag.value); + if (tag.value && tag.value !== '') { + doclet.setMemberof(tag.value); + } } function applyNamespace(docletOrNs, tag) { diff --git a/rhino_modules/jsdoc/tag/type.js b/rhino_modules/jsdoc/tag/type.js index 457e1607..812d7fbd 100644 --- a/rhino_modules/jsdoc/tag/type.js +++ b/rhino_modules/jsdoc/tag/type.js @@ -107,5 +107,5 @@ function parseTypes(type) { /** @private */ function trim(text) { - return text.replace(/^\s+|\s+$/g, ''); + return text.trim(); } diff --git a/templates/default/publish.js b/templates/default/publish.js index e114a84c..798355d5 100644 --- a/templates/default/publish.js +++ b/templates/default/publish.js @@ -96,11 +96,11 @@ } if (f.scope && f.scope !== 'instance') { - if (f.kind == 'function' || f.kind == 'property') attribs.push(f.scope); + if (f.kind == 'function' || f.kind == 'member') attribs.push(f.scope); } if (f.readonly === true) { - if (f.kind == 'property') attribs.push('readonly'); + if (f.kind == 'member') attribs.push('readonly'); } f.attribs = ''+htmlsafe(attribs.length? '<'+attribs.join(', ')+'> ' : '')+''; @@ -126,7 +126,7 @@ addAttribs(doclet); } - if (doclet.kind === 'property') { + if (doclet.kind === 'member') { addSignatureType(doclet); addAttribs(doclet) } @@ -156,7 +156,7 @@ data.orderBy(['longname', 'version', 'since']); // kinds of containers - var globals = find( {kind: ['property', 'function'], memberof: {isUndefined: true}} ), + var globals = find( {kind: ['member', 'function'], memberof: {isUndefined: true}} ), modules = find({kind: 'module'}), externals = find({kind: 'external'}), mixins = find({kind: 'mixin'}), @@ -262,7 +262,7 @@ nav = nav + ''; } - var globalNames = find({kind: ['property', 'function'], 'memberof': {'isUndefined': true}}); + var globalNames = find({kind: ['member', 'function'], 'memberof': {'isUndefined': true}}); if (globalNames.length) { nav = nav + '

Global

    '; diff --git a/templates/default/static/styles/jsdoc-default.css b/templates/default/static/styles/jsdoc-default.css index 3c167ccd..2f86708a 100644 --- a/templates/default/static/styles/jsdoc-default.css +++ b/templates/default/static/styles/jsdoc-default.css @@ -134,7 +134,7 @@ h4 color: #A35A00; } -h5 +h5, .container-overview .subsection-title { font-size: 16px; font-weight: bold; @@ -210,16 +210,16 @@ h6 border-left: 3px #ddd solid; } -.params +.params, .props { border-spacing: 0; border: 0; border-collapse: collapse; } -.params .name { color: #1C02A3; } +.params .name, .props .name { color: #1C02A3; } -.params td, .params th +.params td, .params th, .props td, .props th { border: 1px solid #ddd; margin: 0px; @@ -229,17 +229,17 @@ h6 display: table-cell; } -.params thead tr +.params thead tr, .props thead tr { background-color: #ddd; font-weight: bold; } -.params .params thead tr +.params .params thead tr, .props .props thead tr { background-color: #fff; font-weight: bold; } -.params th { border-right: 1px solid #aaa; } -.params thead .last { border-right: 1px solid #ddd; } \ No newline at end of file +.params th, .props th { border-right: 1px solid #aaa; } +.params thead .last, .props thead .last { border-right: 1px solid #ddd; } diff --git a/templates/default/tmpl/container.tmpl b/templates/default/tmpl/container.tmpl index e4f213d4..64f6a8b0 100644 --- a/templates/default/tmpl/container.tmpl +++ b/templates/default/tmpl/container.tmpl @@ -46,7 +46,8 @@ ?> -
    +
    +
    +
    -

    Properties

    +

    Members

    diff --git a/templates/default/tmpl/details.tmpl b/templates/default/tmpl/details.tmpl index 85eb04f7..d8326fc3 100644 --- a/templates/default/tmpl/details.tmpl +++ b/templates/default/tmpl/details.tmpl @@ -1,4 +1,17 @@
    + + +
    Properties:
    + +
    + + +
    Version:
    diff --git a/templates/default/tmpl/members.tmpl b/templates/default/tmpl/members.tmpl new file mode 100644 index 00000000..49c4a1eb --- /dev/null +++ b/templates/default/tmpl/members.tmpl @@ -0,0 +1,24 @@ + +
    +

    + + +

    + +
    +
    + +

    + +

    + + + + + Example' + (examples.length > 1? 's':'') + ''); + print( render('examples.tmpl', examples) ); + } + ?> +
    diff --git a/templates/default/tmpl/method.tmpl b/templates/default/tmpl/method.tmpl index b8fc745d..b9a8fd5a 100644 --- a/templates/default/tmpl/method.tmpl +++ b/templates/default/tmpl/method.tmpl @@ -13,8 +13,6 @@

    - - This:'); @@ -29,6 +27,8 @@ } ?> + +
    Fires:
      "> -

      + -

      - - -
      - -

      - -

      - - - - - Example' + (examples.length > 1? 's':'') + ''); - print( render('examples.tmpl', examples) ); + /* sort subprops under their parent props (like opts.classname) */ + var parentProp = null; + props.forEach(function(prop, i) { + if (!prop) { return; } + if ( parentProp && prop.name.indexOf(parentProp.name + '.') === 0 ) { + prop.name = prop.name.substr(parentProp.name.length+1); + parentProp.subprops = parentProp.subprops || []; + parentProp.subprops.push(prop); + props[i] = null; } - ?> -
      + else { + parentProp = prop; + } + }); + + /* determine if we need extra columns, "attributes" and "default" */ + props.hasAttributes = false; + props.hasDefault = false; + props.hasName = false; + + props.forEach(function(prop) { + if (!prop) { return; } + + if (prop.optional || prop.nullable) { + props.hasAttributes = true; + } + + if (prop.name) { + props.hasName = true; + } + + if (typeof prop.defaultvalue !== 'undefined') { + props.hasDefault = true; + } + }); +?> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      NameTypeArgumentDefaultDescription
      + + +
      ' ); + } + + if (prop.nullable) { + print( '<nullable>
      ' ); + } + ?> +
      + + Properties' + render('properties.tmpl', prop.subprops) ); + }?>
      \ No newline at end of file diff --git a/test/cases/enum.js b/test/cases/enum.js new file mode 100644 index 00000000..0cc95d0e --- /dev/null +++ b/test/cases/enum.js @@ -0,0 +1,29 @@ + +/** @constructor */ +function Data() { + + /** + The current position. + @enum {number} + */ + this.point = { + /** The x coordinate of the point. */ + x: 0, + + /** The y coordinate of the point. */ + y: 0 + }; +} + +/** + * Enum for tri-state values. + * @enum {number} + */ +TriState = { + /** true */ + TRUE: 1, + /** false */ + FALSE: -1, + /** @type {boolean} */ + MAYBE: true +}; \ No newline at end of file diff --git a/test/cases/memberoftag.js b/test/cases/memberoftag.js index 2337c49f..27993933 100644 --- a/test/cases/memberoftag.js +++ b/test/cases/memberoftag.js @@ -1,9 +1,9 @@ /** @constructor - @member mathlib + @memberof mathlib */ function Data() { - /** @property */ + /** @member */ this.point = {}; } diff --git a/test/cases/memberoftagforced.js b/test/cases/memberoftagforced.js new file mode 100644 index 00000000..c1b495de --- /dev/null +++ b/test/cases/memberoftagforced.js @@ -0,0 +1,44 @@ + + /** @constructor + */ + function Data() { + + /** + The current position. + @type {object} + @property {boolean} needsRevalidate Does this point need to be revalidated? + */ + this.point = { + /** + The x coordinate of the point. + @type {number} + @name point.x + @memberof! Data# + */ + x: 0, + + /** + The y coordinate of the point. + @type {number} + @name point.y + @memberof! Data# + @see {@link Data#point.x} + */ + y: 0, + + needsRevalidate: false + }; + } + +var map = { + /** + @type {Array} + @name map.routes + @memberof! + @property {Data#point} point + */ + routes: [] +} + +/** The current cursor. */ +var cursor = {}; \ No newline at end of file diff --git a/test/cases/propertytag.js b/test/cases/propertytag.js new file mode 100644 index 00000000..7b9a1cb8 --- /dev/null +++ b/test/cases/propertytag.js @@ -0,0 +1,19 @@ +/** + * @namespace + * @property {Object} defaults The default values. + * @property {Number} defaults.a The a property of the defaults. + * @property {String} defaults.b The b property of the defaults. + */ +myobject = { + defaults: { + a: 1, + b: "Hit the light", + /** + * The c property of the defaults. + * @member + * @type {Boolean} + * @property {String} prop The property of c. + */ + c: true + } +}; \ No newline at end of file diff --git a/test/cases/quotename.js b/test/cases/quotename.js index e484db5e..aa7577c1 100644 --- a/test/cases/quotename.js +++ b/test/cases/quotename.js @@ -8,7 +8,7 @@ chat["#channel"] = {}; /** - @property + @member @type {boolean} @defaultvalue */ diff --git a/test/cases/typekind.js b/test/cases/typekind.js index cc4c5450..687eb7d7 100644 --- a/test/cases/typekind.js +++ b/test/cases/typekind.js @@ -12,6 +12,6 @@ module.exports = require('connect').createServer( ); /** - @property {number} module:blog/server.port + @member {number} module:blog/server.port @default 8080 */ \ No newline at end of file diff --git a/test/t/cases/aliasglobal.js b/test/t/cases/aliasglobal.js index 2e7b4fe6..c0f6680a 100644 --- a/test/t/cases/aliasglobal.js +++ b/test/t/cases/aliasglobal.js @@ -2,8 +2,8 @@ var docSet = testhelpers.getDocSetFromFile('test/cases/aliasglobal.js'), log = docSet.getByLongname('log')[0]; - test('When a symbol is documented as a static member of it is not static.', function() { - assert.equal(typeof log.scope, 'undefined'); + test('When a symbol is documented as a static member of it\'s scope is "global" and not "static".', function() { + assert.equal(log.scope, 'global'); }); })(); \ No newline at end of file diff --git a/test/t/cases/var.js b/test/t/cases/var.js index deb31faa..c7736daa 100644 --- a/test/t/cases/var.js +++ b/test/t/cases/var.js @@ -13,7 +13,7 @@ assert.equal(found[0][0].comment, '/** document me */', 'The first constant should get the docs.'); assert.equal(found[0][0].name, 'GREEN', 'The short name should be correct.'); assert.equal(found[0][0].memberof, undefined, 'The memberof should be undefined.'); - assert.equal(found[0][0].scope, undefined, 'The scope should be undefined.'); + assert.equal(found[0][0].scope, 'global', 'The scope should be global.'); assert.equal(found[1].length, 1, 'The second constant should be found'); assert.equal(found[1][0].undocumented, true, 'The second constant should not get the docs.'); @@ -23,7 +23,7 @@ assert.equal(found[4][0].comment, '/** document me */', 'The correct var should get the docs.'); assert.equal(found[4][0].name, 'results', 'The short name should be correct.'); assert.equal(found[4][0].memberof, undefined, 'The memberof should be undefined.'); - assert.equal(found[4][0].scope, undefined, 'The scope should be undefined.'); + assert.equal(found[4][0].scope, 'global', 'The scope should be global.'); }); })(); \ No newline at end of file diff --git a/test/t/jsdoc/name.js b/test/t/jsdoc/name.js index 570d144a..207c7573 100644 --- a/test/t/jsdoc/name.js +++ b/test/t/jsdoc/name.js @@ -117,3 +117,11 @@ test('The module:jsdoc/name.shorten function finds the variation.', function() { assert.equal(parts.name, 'fadein'); assert.equal(parts.longname, 'anim.fadein(2)'); }); + +test('The module:jsdoc/name.splitName function finds the name and description.', function() { + var startName = 'ns.Page#"last \\"sentence\\"".words~sort(2) - This is a description. ', + parts = jsdoc.name.splitName(startName); + + assert.equal(parts.name, 'ns.Page#"last \\"sentence\\"".words~sort(2)'); + assert.equal(parts.description, 'This is a description.'); +}); \ No newline at end of file