diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 00000000..6f14f11b --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,81 @@ +License +======= + +JSDoc Toolkit Version 3 is free software. + +Copyright 2010 (c) Michael Mathews + +Licensed under the Apache License, Version 2.0 (the "License"); you +may not use this file except in compliance with the License. You may +obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +implied. See the License for the specific language governing +permissions and limitations under the License. + +In Addition +=========== + +Third party software is included, used by or distributed +with JSDoc Toolkit Version 3. Each is provided under its own license +and/or has source available from other locations. + +Rhino +----- + +Rhino is open source and licensed by Mozilla under the MPL 1.1 or +later/GPL 2.0 or later licenses. + +https://developer.mozilla.org/en/Rhino_License + +You can obtain the source code for Rhino from the Mozilla web site at +http://www.mozilla.org/rhino/download.html + +Normal Template +--------------- + +The source code for Normal Template is hosted at +http://github.com/gmosx/normal-template + +Normal Template is Copyright (c) 2010 George Moschovitis + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +jsDump +------ + +jsDump is copyright (c) 2008 Ariel Flesler, aflesler(at)gmail(dot)com + +Licensed under BSD http://www.opensource.org/licenses/bsd-license.php + +json2xml +-------- + +json2xml is copyright (c) Stefan Goessner 2006 + +json2xml is licensed under Creative Commons GNU LGPL License, +http://creativecommons.org/licenses/LGPL/2.1/ + +http://goessner.net/ +http://goessner.net/download/prj/jsonxml/ diff --git a/README.md b/README.md index e69de29b..5dd33a0e 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,23 @@ +JSDoc Toolkit Version 3 +======================= + +This is pre-release software, under active development. It is not intended for +general use. + +Usage +----- + + $ java -jar jsdoc.jar -d jsdoc.json myscript.js + +See +--- + +Project Wiki: + +License +------- + +JSDoc Toolkit Version 3 is copyright 2010 +(c) Michael Mathews + +See file "LICENSE.md" in this distribution for more details about terms of use. \ No newline at end of file diff --git a/about.json b/about.json new file mode 100644 index 00000000..6ea264cd --- /dev/null +++ b/about.json @@ -0,0 +1,6 @@ +{ + "app": { + "name": "jsdoc-toolkit-3", + "version": "0.0.0+2010-06-06-2109" + } +} diff --git a/build.xml b/build.xml new file mode 100644 index 00000000..e8f377cf --- /dev/null +++ b/build.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/build/build.properties b/build/build.properties new file mode 100644 index 00000000..51bbde74 --- /dev/null +++ b/build/build.properties @@ -0,0 +1,2 @@ +app.name=jsdoc-toolkit-3 +app.version=0.0.0 diff --git a/build/templates/about.json b/build/templates/about.json new file mode 100644 index 00000000..5a4fc5f3 --- /dev/null +++ b/build/templates/about.json @@ -0,0 +1,6 @@ +{ + "app": { + "name": "@app.name@", + "version": "@app.version@+@timestamp@" + } +} diff --git a/java/build.xml b/java/build.xml new file mode 100644 index 00000000..226b6729 --- /dev/null +++ b/java/build.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/java/classes/js.jar b/java/classes/js.jar new file mode 100644 index 00000000..64d9e3af Binary files /dev/null and b/java/classes/js.jar differ diff --git a/java/src/Run.java b/java/src/Run.java new file mode 100644 index 00000000..5063cf5b --- /dev/null +++ b/java/src/Run.java @@ -0,0 +1,56 @@ +/* + OVERVIEW: + A bootstrap tool for running main.js. Assumes main.js is in + the same directory as the run.jar file. + + Its duty is simply to add the absolute path for main.js as + the first argument to the main.js script itself. This enables + the script to know it's own directory, useful for accessing + resources via relative filepaths. + + AUTHOR: Michael Mathews + LICENSE: Apache License 2.0 - See file 'LICENSE.markdown' in this project. + USAGE: java -jar run.jar + */ + +import java.io.File; +import java.net.URL; +import java.util.*; + +public class Run { + + // requires java.io.File, java.net.URL + public static void main(String[] args) throws java.io.IOException { + // get the absolute file path to the jar file containing this class + ClassLoader loader = Run.class.getClassLoader(); + + // url is like "file:/Users/michael/WorkArea/jsdoc/run.jar!/Run.class" + String jarUrl = loader.getResource("Run.class").getPath(); + + // parse the filepath out of the URL + String delims = "[:!]"; + String[] tokens = jarUrl.split(delims); + String jarPath = tokens[1]; + + // the base directory, assumed to contain main.js + String jarDir = new File(jarPath).getParent(); + String mainPath = jarDir + "/main.js"; + + // Rhino eats the first arg (the path to the script file it is running) + // so we add it twice: one for Rhino the next for us + String[] mainArgs = {mainPath, mainPath}; + String[] allArgs = concat(mainArgs, args); + + // main.js will now get arguments like: + // ["/abs/path/to/main.js", "-a", "aval", "-b", "bval"] + org.mozilla.javascript.tools.shell.Main.main(allArgs); + } + + // requires java.util + public static String[] concat(String[] a, String[] b) { + List ab = new ArrayList(a.length + b.length); + Collections.addAll(ab, a); + Collections.addAll(ab, b); + return ab.toArray(new String[] {}); + } +} diff --git a/jsdoc.jar b/jsdoc.jar new file mode 100644 index 00000000..ad3d127f Binary files /dev/null and b/jsdoc.jar differ diff --git a/main.js b/main.js new file mode 100644 index 00000000..125d6057 --- /dev/null +++ b/main.js @@ -0,0 +1,49 @@ +//// bootstrap + function require(id) { + var path = require.base + id + '.js', + source = ''; + + try { + var file = new java.io.File(path), + scanner = new java.util.Scanner(file).useDelimiter('\Z'), + source = String( scanner.next() ); + } + catch (e) { print(e); } + + var f = new Function('require', 'exports', 'module', source), + exports = require.cache[path] || {}, + module = { id: id, uri: path }; + + require.cache[path] = exports; + f.call({}, require, exports, module); + + return exports; + } + require.base = 'modules/'; + require.cache = {}; + + function print(msg) { + java.lang.System.out.println(msg); + } + + const BASE = arguments[0]; // path to application base folder + var args = arguments.slice(1); +//// + +(function() { + var jsdoc = { + parser: require('jsdoc/parser'), + opts: require('jsdoc/opts'), + src: require('jsdoc/src') + }, + opts, + sourceFiles, + fs = require('common/fs'); + + opts = jsdoc.opts.set(args); + sourceFiles = jsdoc.src.getFilePaths(opts._); + + jsdoc.parser.parseFiles(sourceFiles); + + print( jsdoc.parser.result.asString(opts.destination) ); +})(); \ No newline at end of file diff --git a/modules/common/args.js b/modules/common/args.js new file mode 100644 index 00000000..34e4ae22 --- /dev/null +++ b/modules/common/args.js @@ -0,0 +1,144 @@ +/** + @overview Parse command line options. + @author Michael Mathews + @license Apache License 2.0 - See file 'LICENSE.markdown' in this project. + */ + +/** + @module common/args + */ +(function() { + + /** + Create an instance of the parser. + @constructor + */ + exports.Parser = function() { + this._options = []; + } + + exports.Parser.prototype._getOptionByShortName = function(name) { + for (var i = this._options.length; i--;) { + if (this._options[i].shortName === name) { return this._options[i]; } + } + return null; + } + + exports.Parser.prototype._getOptionByLongName = function(name) { + for (var i = this._options.length; i--;) { + if (this._options[i].longName === name) { return this._options[i]; } + } + return null; + } + + /** + * Provide information about a legal option. + * @method Parser#addOption + * @param shortName + * @param longName + * @param hasValue + * @param helpText + * @example + * myParser.addOption('t', 'template', true, 'The path to the template.'); + * myParser.addOption('h', 'help', false, 'Show the help message.'); + */ + exports.Parser.prototype.addOption = function(shortName, longName, hasValue, helpText) { + this._options.push({shortName: shortName, longName: longName, hasValue: hasValue, helpText: helpText}); + }; + + /** + Generate a summary of all the options with corresponding help text. + @method Parser#help + @returns {string} + */ + exports.Parser.prototype.help = function() { + var help = 'OPTIONS:\n', + option; + + for (var i = this._options.length; i--;) { + option = this._options[i]; + + if (option.shortName) { + help += '-' + option.shortName + (option.longName? ' or ' : ''); + } + + if (option.longName) { + help += '--' + option.longName; + } + + if (option.hasValue) { + help += ' '; + } + + help += ' ' + option.helpText + '\n'; + } + + return help; + }; + + /** + Get the options. + @method Parser#parse + @param args An array, like ['-x', 'hello'] + @param defaults An optional collection of default values. + @returns {Object} The keys will be the longNames, or the shortName if + no longName is defined for that option. The values will be the values + provided, or `true` if the option accepts no value. + */ + exports.Parser.prototype.parse = function(args, defaults) { + var result = defaults || {}; + + result._ = []; + + for (var i = 0, leni = args.length; i < leni; i++) { + var arg = '' + args[i], + next = (i < leni-1)? '' + args[i+1] : null, + option, + shortName, + longName, + name, + value = null; + + // like -t + if (arg.charAt(0) === '-') { + + // like: --template + if (arg.charAt(1) === '-') { + name = longName = arg.slice(2); + option = this._getOptionByLongName(longName); + } + else { + name = shortName = arg.slice(1); + option = this._getOptionByShortName(shortName); + } + + if (option === null) { + throw new Error( 'Unknown command line option found: ' + name ); + } + + if (option.hasValue) { + value = next; + i++; + + if (value === null || value.charAt(0) === '-') { + throw new Error( 'Command line option requires a value: ' + name ); + } + } + else { + value = true; + } + + if (option.longName && shortName) { + name = option.longName; + } + + result[name] = value; + } + else { + result._.push(arg); + } + } + + return result; + } +})(); \ No newline at end of file diff --git a/modules/common/fs.js b/modules/common/fs.js new file mode 100644 index 00000000..0c2e601a --- /dev/null +++ b/modules/common/fs.js @@ -0,0 +1,109 @@ +/** + @overview File system stuff. + @author Michael Mathews + @license Apache License 2.0 - See file 'LICENSE.markdown' in this project. + */ + +(function() { + var slash = java.lang.System.getProperty('file.separator') || '/', + File = Packages.java.io.File, + defaultEncoding = java.lang.System.getProperty('file.encoding'); + + exports.read = function(path, options) { + var options = options || {}, + encoding = options.encoding || defaultEncoding; + + return readFile(path, encoding); + } + + exports.write = function(path, content, options) { + var options = options || {}, + encoding = options.encoding || defaultEncoding, + out; + + out = new Packages.java.io.PrintWriter( + new Packages.java.io.OutputStreamWriter( + new Packages.java.io.FileOutputStream(path), + encoding + ) + ); + + out.write(content); + out.flush(); + out.close(); + } + + /** + * Check if a file exists. + * @param {string} path The file to check. + * @returns {boolean} + */ + exports.exists = function(path) { + var file = new File(path); + + if (file.isDirectory()){ + return true; + } + if (!file.exists()){ + return false; + } + if (!file.canRead()){ + return false; + } + return true; + } + + /** + * Get a list of all files in a given directory. Will not include files that + * start with a dot. + * @type string[] + * @param {string} dir The starting directory to look in. + * @param {number} [recurse=1] How many levels deep to scan. + * @returns {string[]} An array of {string} paths to the files in the given directory. + */ + exports.ls = function(dir, recurse, _allFiles, _path) { + var files, + file; + + if (typeof _path === 'undefined') { // initially + _allFiles = []; + _path = [dir]; + } + + if (_path.length === 0) { return _allFiles; } + if (typeof recurse === 'undefined') { recurse = 1; } + + dir = new File(dir); + if (!dir.directory) { return [String(dir)]; } + files = dir.list(); + + for (var f = 0, lenf = files.length; f < lenf; f++) { + file = String(files[f]); + + if (file.match(/^\.[^\.\/\\]/)) { continue; } // skip dot files + + if ((new File(_path.join(slash) + slash + file)).list()) { // it's a directory + _path.push(file); + + if (_path.length - 1 < recurse) { + exports.ls(_path.join(slash), recurse, _allFiles, _path); + } + _path.pop(); + } + else { // it's a file + _allFiles.push( + fixSlash( (_path.join(slash) + slash + file) ) + ); + } + } + + return _allFiles; + } + + // fix multiple slashes, like one//two + function fixSlash(path) { + return path.replace(/[\/\\]+/g, slash); + } + +})(); + diff --git a/modules/flesler/jsdump.js b/modules/flesler/jsdump.js new file mode 100644 index 00000000..f0d00409 --- /dev/null +++ b/modules/flesler/jsdump.js @@ -0,0 +1,168 @@ + +// Ported by Tom Robinson + +/** + * jsDump + * Copyright (c) 2008 Ariel Flesler - aflesler(at)gmail(dot)com | http://flesler.blogspot.com + * Licensed under BSD (http://www.opensource.org/licenses/bsd-license.php) + * Date: 5/15/2008 + * @projectDescription Advanced and extensible data dumping for Javascript. + * @version 1.0.0 + * @author Ariel Flesler + */ +var jsDump; + +(function(){ + function quote( str ){ + return '"' + str.toString().replace(/"/g, '\\"') + '"'; + }; + function literal( o ){ + return o + ''; + }; + function join( pre, arr, post ){ + var s = jsDump.separator(), + base = jsDump.indent(), + inner = jsDump.indent(1); + if( arr.join ) + arr = arr.join( ',' + s + inner ); + if( !arr ) + return pre + post; + return [ pre, inner + arr, base + post ].join(s); + }; + function array( arr ){ + var i = arr.length, ret = Array(i); + this.up(); + while( i-- ) + ret[i] = this.parse( arr[i] ); + this.down(); + return join( '[', ret, ']' ); + }; + + var reName = /^function (\w+)/; + + jsDump = { + parse:function( obj, type ){//type is used mostly internally, you can fix a (custom)type in advance + var parser = this.parsers[ type || this.typeOf(obj) ]; + type = typeof parser; + + return type == 'function' ? parser.call( this, obj ) : + type == 'string' ? parser : + this.parsers.error; + }, + typeOf:function( obj ){ + var type = typeof obj, + f = 'function';//we'll use it 3 times, save it + return type != 'object' && type != f ? type : + !obj ? 'null' : + obj.exec ? 'regexp' :// some browsers (FF) consider regexps functions + obj.getHours ? 'date' : + obj.scrollBy ? 'window' : + obj.nodeName == '#document' ? 'document' : + obj.nodeName ? 'node' : + obj.item ? 'nodelist' : // Safari reports nodelists as functions + obj.callee ? 'arguments' : + obj.call || obj.constructor != Array && //an array would also fall on this hack + (obj+'').indexOf(f) != -1 ? f : //IE reports functions like alert, as objects + 'length' in obj ? 'array' : + type; + }, + separator:function(){ + return this.multiline ? this.HTML ? '
' : '\n' : this.HTML ? ' ' : ' '; + }, + indent:function( extra ){// extra can be a number, shortcut for increasing-calling-decreasing + if( !this.multiline ) + return ''; + var chr = this.indentChar; + if( this.HTML ) + chr = chr.replace(/\t/g,' ').replace(/ /g,' '); + return Array( this._depth_ + (extra||0) ).join(chr); + }, + up:function( a ){ + this._depth_ += a || 1; + }, + down:function( a ){ + this._depth_ -= a || 1; + }, + setParser:function( name, parser ){ + this.parsers[name] = parser; + }, + // The next 3 are exposed so you can use them + quote:quote, + literal:literal, + join:join, + // + _depth_: 1, + // This is the list of parsers, to modify them, use jsDump.setParser + parsers:{ + window: '[Window]', + document: '[Document]', + error:'[ERROR]', //when no parser is found, shouldn't happen + unknown: '[Unknown]', + 'null':'null', + undefined:'undefined', + 'function':function( fn ){ + var ret = 'function', + name = 'name' in fn ? fn.name : (reName.exec(fn)||[])[1];//functions never have name in IE + if( name ) + ret += ' ' + name; + ret += '('; + + ret = [ ret, this.parse( fn, 'functionArgs' ), '){'].join(''); + return join( ret, this.parse(fn,'functionCode'), '}' ); + }, + array: array, + nodelist: array, + arguments: array, + object:function( map ){ + var ret = [ ]; + this.up(); + for( var key in map ) + ret.push( this.parse(key,'key') + ': ' + this.parse(map[key]) ); + this.down(); + return join( '{', ret, '}' ); + }, + node:function( node ){ + var open = this.HTML ? '<' : '<', + close = this.HTML ? '>' : '>'; + + var tag = node.nodeName.toLowerCase(), + ret = open + tag; + + for( var a in this.DOMAttrs ){ + var val = node[this.DOMAttrs[a]]; + if( val ) + ret += ' ' + a + '=' + this.parse( val, 'attribute' ); + } + return ret + close + open + '/' + tag + close; + }, + functionArgs:function( fn ){//function calls it internally, it's the arguments part of the function + var l = fn.length; + if( !l ) return ''; + + var args = Array(l); + while( l-- ) + args[l] = String.fromCharCode(97+l);//97 is 'a' + return ' ' + args.join(', ') + ' '; + }, + key:quote, //object calls it internally, the key part of an item in a map + functionCode:'[code]', //function calls it internally, it's the content of the function + attribute:quote, //onode calls it internally, it's an html attribute value + string:quote, + date:quote, + regexp:literal, //regex + number:literal, + 'boolean':literal + }, + DOMAttrs:{//attributes to dump from nodes, name=>realName + id:'id', + name:'name', + 'class':'className' + }, + HTML:false,//if true, entities are escaped ( <, >, \t, space and \n ) + indentChar:' ',//indentation unit + multiline:true //if true, items in a collection, are separated by a \n, else just a space. + }; + +})(); + +exports.jsDump = jsDump; \ No newline at end of file diff --git a/modules/goessner/json2xml.js b/modules/goessner/json2xml.js new file mode 100644 index 00000000..07d6526f --- /dev/null +++ b/modules/goessner/json2xml.js @@ -0,0 +1,56 @@ +/* This work is licensed under Creative Commons GNU LGPL License. + + License: http://creativecommons.org/licenses/LGPL/2.1/ + Version: 0.9/modified to conform to commonjs modules pattern + Author: Stefan Goessner/2006 + Web: http://goessner.net/ +*/ + +var json2xml = (typeof exports === 'undefined')? {} : exports; // like commonjs + +(function() { + + json2xml.convert = function(o) { + var toXml = function(v, name, ind) { + var xml = ""; + if (v instanceof Array) { + for (var i=0, n=v.length; i\n" : "/>"; + if (hasChild) { + for (var m in v) { + if (m == "#text") + xml += v[m]; + else if (m == "#cdata") + xml += ""; + else if (m.charAt(0) != "@") + xml += toXml(v[m], m, ind+"\t"); + } + xml += (xml.charAt(xml.length-1)=="\n"?ind:"") + "\n"; + } + } + else { + xml += ind + "<" + name + ">" + v.toString().replace(/\n"; + } + return xml; + }, + xml=""; + + for (var m in o) { + xml += toXml(o[m], m, ""); + } + + return xml; + } + +})(); \ No newline at end of file diff --git a/modules/jsdoc/doclet.js b/modules/jsdoc/doclet.js new file mode 100644 index 00000000..b3e4f1af --- /dev/null +++ b/modules/jsdoc/doclet.js @@ -0,0 +1,346 @@ +/** + @overview + @author Michael Mathews + @license Apache License 2.0 - See file 'LICENSE.markdown' in this project. + */ + +/** + Functionality relating to jsdoc comments and their tags. + @module jsdoc/doclet + */ +(function() { + var name = require('jsdoc/name'), + tag = require('jsdoc/tag'); + + /** + Factory that builds a Doclet object. + @param {string} commentSrc + @param {ASTNode} node + @param {string} sourceName + @returns {Doclet} + */ + exports.makeDoclet = function(commentSrc, node, sourceName) { + var tags = [], + doclet; + + commentSrc = unwrapComment(commentSrc); + commentSrc = fixDesc(commentSrc); + + tags = parseTags(commentSrc); + preprocess(tags); + + doclet = new Doclet(tags); + + postprocess(doclet); + + name.resolve(doclet); + + doclet.meta = { + line: node.getLineno(), + file: sourceName + }; + + return doclet + } + + /** + @private + @constructor Doclet + @param {Array.} tags + */ + function Doclet(tags) { + /** + An array of Objects representing tags. + @type Array. + @property Doclet#tags + */ + this.tags = tags; + } + + /** + Set the name of the Doclet. + @method Doclet#setName + @param {string name + */ + Doclet.prototype.setName = function(nameToSet) { + this.tagText('name', nameToSet); + + nameToSet = name.resolve(this); + } + + /** + Return the text of the first tag with the given name. + @method Doclet#tagText + @param {String} tagName + @returns {String} The text of the found tag. + */ + Doclet.prototype.tagText = function(tagName, text) { + var i = this.tags.length; + while(i--) { + if (this.tags[i].name === tagName) { + if (text) { this.tags[i].text = text; } + return this.tags[i].text; + } + } + + // still here? + if (text) { + this.tags.push( tag.fromTagText(tagName + ' ' + text) ); + return text; + } + + return ''; + } + + /** + Does a tag with the given name exist in this doclet? + @method Doclet#hasTag + @param {String} tagName + @returns {boolean} True if the tag is found, false otherwise. + */ + Doclet.prototype.hasTag = function(tagName) { + var i = this.tags.length; + while(i--) { + if (this.tags[i].name === tagName) { + return true; + } + } + return false; + } + + // safe to export to JSON + var exportTags = ['name', 'path', 'kind', 'desc', 'type', 'param', 'returns', 'exports', 'requires', 'memberof', 'access', 'attribute']; + + /** + Get a JSON-compatible object representing this Doclet. + @method Doclet#toObject + @returns {Object} + */ + Doclet.prototype.toObject = function() { + var tag, tagName, tagValue, + 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]; + tagName = tag.name; + tagValue = {}; + + if (tag.type) { + 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.pdesc) { tagValue.desc = tag.pdesc; } + + // 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 { + o[tagName] = [ o[tagName] ]; + o[tagName].push(tagValue); + } + + o.meta = this.meta; + } + + return o; + } + + /** + Remove JsDoc comment slash-stars. Trims white space. + @private + @function unwrapComment + @param {string} commentSrc + @return {string} Coment wit stars and slashes removed. + */ + function unwrapComment(commentSrc) { + if (!commentSrc) { return ''; } + + // TODO keep leading white space for @examples + return commentSrc ? commentSrc.replace(/(^\/\*\*+\s*|\s*\**\*\/$)/g, "").replace(/^\s*\* ?/gm, "") : ""; + } + + /** + Add a @desc tag if none exists on untagged text at start of comment. + @private + @function fixDesc + @param {string} commentSrc + @return {string} With needed @desc tag added. + */ + function fixDesc(commentSrc) { + if (!/^\s*@/.test(commentSrc)) { + commentSrc = '@desc ' + commentSrc; + } + 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', propertyof: 'property', eventof: 'event'}; + // other tags that can provide the symbol name + var nameables = ['constructor', 'module', 'event', 'namespace', 'method', 'property', 'function', 'variable', 'enum']; + + /** + Expand some shortcut tags. Modifies the tags argument in-place. + @private + @method preprocess + @param {Array.} tags + @returns undefined + */ + function preprocess(tags) { + var name = '', + taggedName = '', + kind = '', + taggedKind = '', + memberof = '', + taggedMemberof = ''; + + var i = tags.length; + while(i--) { + + if (tags[i].name === 'private') { + tags[tags.length] = tag.fromTagText('access private'); + } + else if (tags[i].name === 'protected') { + tags[tags.length] = tag.fromTagText('access protected'); + } + else if (tags[i].name === 'const') { + tags[tags.length] = tag.fromTagText('attribute constant'); + } + else if (tags[i].name === 'readonly') { + tags[tags.length] = tag.fromTagText('attribute readonly'); + } + else if (tags[i].name === 'name') { + if (name && name !== tags[i].text) { tooManyNames(name, tags[i].text); } + taggedName = name = tags[i].text; + } + else if (tags[i].name === 'kind') { + if (kind && kind !== tags[i].text) { tooManyKinds(kind, tags[i].text); } + taggedKind = kind = tags[i].text; + } + else if (tags[i].name === 'memberof') { + if (memberof) { tooManyTags('memberof'); } + taggedMemberof = memberof = tags[i].text; + } + + if ( nameables.indexOf(tags[i].name) > -1 ) { + if (tags[i].text) { + if (name && name !== tags[i].text) { tooManyNames(name, tags[i].text); } + name = tags[i].text; + } + + if (tags[i].type) { + tags[tags.length] = tag.fromTagText('type ' + tags[i].type); + } + + if (kind && kind !== tags[i].name) { tooManyKinds(kind, tags[i].name); } + kind = tags[i].name; + } + + if ( memberofs.hasOwnProperty(tags[i].name) ) { + if (tags[i].text) { + if (memberof) { tooManyTags(tags[i].name); } + memberof = tags[i].text; + } + + if (kind && kind !== memberofs[tags[i].name]) { tooManyKinds(kind, memberofs[tags[i].name]); } + kind = memberofs[tags[i].name]; + } + } + + if (name && !taggedName) { + tags[tags.length] = tag.fromTagText('name ' + name); + } + + if (kind && !taggedKind) { + tags[tags.length] = tag.fromTagText('kind ' + kind); + } + + if (memberof && !taggedMemberof) { + tags[tags.length] = tag.fromTagText('memberof ' + memberof); + } + } + + function postprocess(doclet) { + if ( doclet.hasTag('class') && !doclet.hasTag('constructor') ) { + doclet.tags[doclet.tags.length] = tag.fromTagText('kind constructor'); + } + + if ( doclet.hasTag('enum')) { + if (!doclet.hasTag('type')) { + doclet.tags[doclet.tags.length] = tag.fromTagText('type number'); + } + + if (!doclet.hasTag('readonly') && !doclet.hasTag('const')) { + doclet.tags[doclet.tags.length] = tag.fromTagText('attribute constant'); + } + } + + if ( doclet.hasTag('const')) { + if (!doclet.hasTag('kind')) { + doclet.tags[doclet.tags.length] = tag.fromTagText('kind property'); + } + + if (!doclet.hasTag('readonly') && !doclet.hasTag('const')) { + doclet.tags[doclet.tags.length] = tag.fromTagText('attribute constant'); + } + } + } + + /** + Throw error when two conflicting names are defined in the same doc. + @private + @function tooManyNames + */ + function tooManyNames(name1, name2) { + throw new Error('Conflicting names in documentation: '+name1+', '+name2); + } + + /** + Throw error when two conflicting kinds are defined in the same doc. + @private + @function tooManyKinds + */ + function tooManyKinds(kind1, kind2) { + throw new Error('Conflicting kinds in documentation: '+kind1+', '+kind2); + } + + /** + Throw error when conflicting tags are found. + @private + @function tooManyTags + */ + function tooManyTags(tagName) { + throw new Error('Symbol has too many tags of type: @'+tagName); + } +})(); \ No newline at end of file diff --git a/modules/jsdoc/docset.js b/modules/jsdoc/docset.js new file mode 100644 index 00000000..4241007a --- /dev/null +++ b/modules/jsdoc/docset.js @@ -0,0 +1,61 @@ +(function() { + var dumper = require('flesler/jsdump'), + xml = require('goessner/json2xml'), + doclets = exports.doclets = []; + + doclets.getDocsByName = function getDocsByName(docName) { + var foundDocs = [], + i = doclets.length; + + while (i--) { + if (doclets[i].tagText('path') === docName) { + foundDocs.unshift( doclets[i] ); + } + } + + return foundDocs; + } + + doclets.toObject = function toObject() { + var docsObjects = [], + i = doclets.length; + + while (i--) { + docsObjects.unshift( doclets[i].toObject() ); + } + + return { doc: docsObjects }; + } + + doclets.asString = function asString(destinationName) { + if ( /xml$/i.test(destinationName) ) { + return doclets.toXML(); + } + else { // default + return doclets.toJSON(); + } + } + + doclets.toJSON = function toJSON() { + return dumper.jsDump.parse( doclets.toObject() ); + } + + doclets.toXML = function toXML() { + var o = doclets.toObject(); + + // make `id` an attribute of the doc tag + for (var i = 0, leni = o.doc.length; i < leni; i++) { + for (var p in o.doc[i]) { + if (p === 'id') { + o.doc[i]['@id'] = o.doc[i].id; + delete o.doc[i].id; + } + } + } + + return xml.convert( + { jsdoc: o } + ); + } + +})(); \ No newline at end of file diff --git a/modules/jsdoc/name.js b/modules/jsdoc/name.js new file mode 100644 index 00000000..d82f8f7c --- /dev/null +++ b/modules/jsdoc/name.js @@ -0,0 +1,163 @@ +/** + @overview + @author Michael Mathews + @license Apache License 2.0 - See file 'LICENSE.markdown' in this project. + */ + +/** + Functionality relating to symbol name manipulation. + @module jsdoc/name + */ +(function() { + var Token = Packages.org.mozilla.javascript.Token, + currentModule = ''; + + exports.setCurrentModule = function(moduleName) { + currentModule = moduleName; + } + + /** + @method resolve + @param {Doclet} doclet + */ + exports.resolve = function(doclet) { + var kind = doclet.tagText('kind'), + name = doclet.tagText('name'), + memberof = doclet.tagText('memberof'), + path, + shortname, + prefix, + supportedNamespaces = ['module', 'event']; + + // only keep the first word of the tagged name + name = doclet.tagText('name', name.split(/\s+/g)[0]); + + if (currentModule) { + name = name.replace(/^exports\.(?=.+$)/, currentModule + '.'); + } + + name = name.replace(/\.prototype\.?/g, '#'); + + // if name doesn't already have a doc-namespace and needs one + if (!/^[a-z_$-]+:\S+/i.test(name) && supportedNamespaces.indexOf(kind) > -1) { + // add doc-namespace to path + name = kind + '(' + name + ')'; + } + + path = shortname = name; + + doclet.tagText('name', shortname); + + if (memberof) { + if (name.indexOf(memberof) === 0) { + path = name; + [prefix, shortname] = exports.shorten(name); + doclet.tagText('name', shortname); + } + } + else { + [prefix, shortname] = exports.shorten(name); + doclet.tagText('memberof', prefix); + doclet.tagText('name', shortname); + } + + // overlapping member of, like @name foo.Bar, @memberof foo + if (memberof && name.indexOf(memberof) !== 0) { + path = memberof + (/#$/.test(memberof)? '' : '.') + name; + } + + if (path) doclet.tagText('path', path); + + return path; + } + + exports.shorten = function(path) { + var shortname = path.split(/([#.-])/).pop(), + splitOn = RegExp.$1, + splitAt = path.lastIndexOf(splitOn), + prefix = (splitAt === -1)? '' : path.slice(0, splitAt); + + if (splitOn === '#') { prefix = prefix + splitOn; } + return [prefix, shortname]; + } + + /** + Resolve how to document the `this.` portion of a symbol name. + */ + exports.resolveThis = function(name, node, doclet) { + var enclosing, + enclosingDoc, + memberof = (doclet.tagText('memberof') || '').replace(/\.prototype\.?/g, '#'); + + if (node.parent && node.parent.type === Token.OBJECTLIT) { + if (enclosing = node.parent) { + enclosingDoc = exports.docFromNode(enclosing) || {}; + memberof = (enclosingDoc.tagText('path') || '').replace(/\.prototype\.?/g, '#'); + + if (!memberof) { + memberof = enclosingDoc.path; + memberof = memberof || '[[anonymousObject]]'; + } + + if (memberof) { + name = memberof + (memberof[memberof.length-1] === '#'?'':'.') + name; + } + } + } + else if (name.indexOf('this.') === 0) { // assume `this` refers to innermost constructor + if (!memberof || memberof === 'this') { + enclosing = node.getEnclosingFunction() + + enclosingDoc = exports.docFromNode(enclosing); + memberof = enclosingDoc? enclosingDoc.tagText('path') : ''; + + if (enclosing && !memberof) { + memberof = ''; //[[anonymousFunction]] + name = name.slice(5); // remove `this.` + } + else if (!enclosing) { + memberof = ''; // [[globalObject]] + } + + if (memberof || !enclosing) { + // `this` refers to nearest instance in the name path + if (enclosingDoc && enclosingDoc.tagText('kind') !== 'constructor') { + var parts = memberof.split('#'); + parts.pop(); + memberof = parts.join('#'); + } + + name = memberof + (memberof? '#':'') + name.slice(5); // replace `this.` with memberof + } + } + else { + name = name.slice(5); + } + } + return name; + } + + /** + Keep track of anonymous functions that have been assigned to documented symbols. + @private + @method docFromNode + @param {org.mozilla.javascript.ast.AstNode} node + @return {Object} The associated doclet. + */ + exports.docFromNode = function(node) { + var i = exports.refs.length; + while (i--) { + if (exports.refs[i][0] === node) { + return exports.refs[i][1]; + } + } + + return null; + } + // tuples, like [ [noderef, doclet], [noderef, doclet] ] + exports.refs = []; + + function getTypeName(node) { + return node ? ''+org.mozilla.javascript.Token.typeToName(node.getType()) : '' ; + } +})(); \ No newline at end of file diff --git a/modules/jsdoc/opts.js b/modules/jsdoc/opts.js new file mode 100644 index 00000000..25e8ab19 --- /dev/null +++ b/modules/jsdoc/opts.js @@ -0,0 +1,66 @@ +/** + @overview Get or set options for this app. + @author Michael Mathews + @license Apache License 2.0 - See file 'LICENSE.markdown' in this project. + */ + +/** + @module jsdoc/opts + @requires common/args + */ +(function() { + var args = args || require('common/args'); + + var argsParser = new args.Parser(), + ourOptions, + defaults = { + template: 'default', + destination: 'jsdoc.xml' + }; + + argsParser.addOption('t', 'template', true, 'The name of the template to use.'); + argsParser.addOption('T', 'test', false, 'Run unit tests and quit.'); + argsParser.addOption('d', 'destination', true, 'The path to output folder.'); + argsParser.addOption('h', 'help', false, 'Print help message and quit.'); + argsParser.addOption('V', 'validate', false, 'Validate the results produced by parsing the source code.'); + + /** + Set the options for this app. + @method set + @throws {Error} Illegal arguments will throw errors. + @param {string|String[]} args The command line arguments for this app. + */ + exports.set = function(args) { + args = args || []; + + if (typeof args === 'string' || args.constructor === String) { + args = (''+args).split(/\s+/g); + } + + ourOptions = argsParser.parse(args, defaults); + + return ourOptions; + } + + /** + Display help message for options. + @method help + */ + exports.help = function() { return argsParser.help(); } + + /** + Get a single option or all the options for this app. + @method get + @param {String} [name] The name of the option. + @return {String|Object} Either the value associated with the given name, + or a collection of key/values representing all the options. + */ + exports.get = function(name) { + if (typeof name === 'undefined') { + return ourOptions; + } + else { + return ourOptions[name]; + } + } +})(); \ No newline at end of file diff --git a/modules/jsdoc/parser.js b/modules/jsdoc/parser.js new file mode 100644 index 00000000..09e2bfbd --- /dev/null +++ b/modules/jsdoc/parser.js @@ -0,0 +1,203 @@ +(function() { + var name = require('jsdoc/name'), + doclet = require('jsdoc/doclet'), + doclets = require('jsdoc/docset').doclets, + Token = Packages.org.mozilla.javascript.Token; + + exports.result = doclets; + + /** + */ + function visitNode(node) { + var commentSrc = '', + thisDoclet = null, + thisDocletName = ''; + + // look for all comments that have names provided + if (node.type === Token.SCRIPT && node.comments) { + for each (var comment in node.comments.toArray()) { + if (comment.commentType === Token.CommentType.JSDOC) { + commentSrc = '' + comment.toSource(); + + if (commentSrc) { + thisDoclet = doclet.makeDoclet(commentSrc, node, currentSourceName); + if ( thisDoclet.hasTag('name') ) { + doclets.push(thisDoclet); + if (thisDoclet.tagText('kind') === 'module') { + name.setCurrentModule( thisDoclet.tagText('path') ); + } + } + } + } + } + } + + // like function foo() {} + if (node.type == Token.FUNCTION) { + if (node.jsDoc) { + commentSrc = '' + node.jsDoc; + + if (commentSrc) { + thisDoclet = doclet.makeDoclet(commentSrc, node, currentSourceName); + thisDocletName = thisDoclet.tagText('path'); + + if (!thisDocletName) { + thisDoclet.setName('' + node.name); + doclets.push(thisDoclet); + } + + name.refs.push([node, thisDoclet]); + } + } + + return true; + } + + // like foo = function(){} or foo: function(){} + if (node.type === Token.ASSIGN || node.type === Token.COLON) { + var nodeName = nodeToString(node.left), + nodeKind = ''; + commentSrc = node.jsDoc || node.left.jsDoc; + + if (commentSrc) { + commentSrc = '' + commentSrc; + + thisDoclet = doclet.makeDoclet(commentSrc, node, currentSourceName); + thisDocletName = thisDoclet.tagText('name'); + nodeKind = thisDoclet.tagText('kind'); + + if (!thisDocletName) { + nodeName = name.resolveThis( nodeName, node, thisDoclet ); + thisDoclet.setName(nodeName); + doclets.push(thisDoclet); + } + name.refs.push([node.right, thisDoclet]); + } + + return true; + } + + // like var foo = function(){} or var bar = {} + if (node.type == Token.VAR || node.type == Token.LET || node.type == Token.CONST) { + var counter = 0, + nodeKind; + + if (node.variables) for each (var n in node.variables.toArray()) { + + if (n.target.type === Token.NAME && n.initializer) { + commentSrc = (counter++ === 0 && !n.jsDoc)? node.jsDoc : n.jsDoc; + if (commentSrc) { + thisDoclet = doclet.makeDoclet('' + commentSrc, node, currentSourceName); + thisDocletName = thisDoclet.tagText('path'); + nodeKind = thisDoclet.tagText('kind'); + + if ( !thisDocletName ) { + thisDocletName = n.target.string; + thisDoclet.setName(thisDocletName); + doclets.push(thisDoclet); + } + } + } + name.refs.push([n.initializer, thisDoclet]); + } + return true; + } + + return true; + } + + currentSourceName = ''; + + /** + */ + exports.parseSource = function(source, sourceName) { + currentSourceName = sourceName; + var ast = getParser().parse(source, sourceName, 1); + + ast.visit( + new Packages.org.mozilla.javascript.ast.NodeVisitor({ + visit: visitNode + }) + ); + + currentSourceName = ''; + } + + /** + */ + exports.parseFiles = function(sourceFiles) { + var ast = getParser(), + fs = require('common/fs'), + source = ''; + + for (i = 0, leni = sourceFiles.length; i < leni; i++) { + try { + source = fs.read(sourceFiles[i]); + } + catch(e) { + print('ERROR: ' + e); + continue; + } + + exports.parseSource(source, sourceFiles[i]); + } + } + + /** + @private + @function getParser + */ + function getParser() { + var cx = Packages.org.mozilla.javascript.Context.getCurrentContext(); + + var ce = new Packages.org.mozilla.javascript.CompilerEnvirons(); + ce.setRecordingComments(true); + ce.setRecordingLocalJsDocComments(true); + ce.initFromContext(cx); + return new Packages.org.mozilla.javascript.Parser(ce, ce.getErrorReporter()); + } + + /** + @private + @function nodeToString + @param {org.mozilla.javascript.ast.AstNode} node + @returns {string} + */ + // credit: ringojs ninjas + function nodeToString(node) { + var str; + + if (node.type === Token.GETPROP) { + str = [nodeToString(node.target), node.property.string].join('.'); + } + else if (node.type === Token.NAME) { + str = node.string; + } + else if (node.type === Token.STRING) { + str = node.value; + } + else if (node.type === Token.THIS) { + str = 'this'; + } + else if (node.type === Token.GETELEM) { + str = node.toSource(); // like: Foo['Bar'] + } + else { + str = getTypeName(node); + } + + return '' + str; + }; + + /** + @private + @function getTypeName + @param {org.mozilla.javascript.ast.AstNode} node + @returns {string} + */ + // credit: ringojs ninjas + function getTypeName(node) { + return node ? ''+Packages.org.mozilla.javascript.Token.typeToName(node.getType()) : '' ; + } + +})(); \ No newline at end of file diff --git a/modules/jsdoc/src.js b/modules/jsdoc/src.js new file mode 100644 index 00000000..b597e50d --- /dev/null +++ b/modules/jsdoc/src.js @@ -0,0 +1,42 @@ +/** + @overview Find source files to be parsed for docs. + @author Michael Mathews + @license Apache License 2.0 - See file 'LICENSE.markdown' in this project. + */ + +/** + @module jsdoc/src + @namespace jsdoc.src + @requires common/fs + */ +var jsdoc = jsdoc || {}; +jsdoc.src = (typeof exports === 'undefined')? {} : exports; // like commonjs + +(function() { + var fs = fs || require('common/fs'); + + /** + Recursively searches the given searchPaths for js files. + @method getFilePaths + @param {Array.} searchPaths + @param {number} [depth=1] + */ + jsdoc.src.getFilePaths = function(searchPaths, depth) { + var filePaths = []; + + searchPaths = searchPaths || []; + depth = depth || 1; + + searchPaths.forEach(function($) { + filePaths = filePaths.concat(fs.ls($, depth)); + }); + + // TODO: allow user-defined filtering of files + filePaths = filePaths.filter(function($) { + return /.+\.js(doc)?$/i.test($); + }); + + return filePaths; + } + +})(); \ No newline at end of file diff --git a/modules/jsdoc/tag.js b/modules/jsdoc/tag.js new file mode 100644 index 00000000..6338ef8f --- /dev/null +++ b/modules/jsdoc/tag.js @@ -0,0 +1,133 @@ +/** + @overview + @author Michael Mathews + @license Apache License 2.0 - See file 'LICENSE.markdown' in this project. + */ + +/** + Create tag objects. + @module jsdoc/tag + */ +(function() { + + 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); + } + + /** + @private + @constructor Tag + @param {string} tagText + */ + function Tag(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(); + this.text = bits[2] || ''; + + var typeText = splitType(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; + } + + this.type = typeText.type; + + this.text = trim(typeText.text); + + if (this.name === 'param') { // is a parameter w/ long format + var [pname, pdesc] = splitPname(this.text); + this.pname = pname; + this.pdesc = pdesc; + } + } + } + + /** + Split the parameter name and parameter desc from the tag text. + @private + @method splitPname + @param {string} tagText + @returns Array. The pname and the pdesc. + */ + function splitPname(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; + } + } + } + + 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, ''); + } + +})(); \ No newline at end of file