diff --git a/.gitignore b/.gitignore index 2f3dacac..04ae28a8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ build-files/java/build jsdoc.jar +test/tutorials/out diff --git a/Jake/templates/package.json.tmpl b/Jake/templates/package.json.tmpl index 2754bcf6..092cc3bc 100644 --- a/Jake/templates/package.json.tmpl +++ b/Jake/templates/package.json.tmpl @@ -21,6 +21,10 @@ { "name": "Michael Mathews", "email": "micmath@gmail.com" + }, + { + "name": "Rafa\u0105 Wrzeszcz", + "email": "rafal.wrzeszcz@wrzasq.pl" } ], "maintainers": [ diff --git a/jsdoc.js b/jsdoc.js index 5f8447a0..a0077d33 100644 --- a/jsdoc.js +++ b/jsdoc.js @@ -142,7 +142,8 @@ function main() { opts: { parser: require('jsdoc/opts/parser'), } - }; + }, + resolver; env.opts = jsdoc.opts.parser.parse(env.args); @@ -240,6 +241,15 @@ function main() { exit(0); } + // load this module anyway to ensure root instance exists + // it's not a problem since without tutorials root node will have empty children list + resolver = require('jsdoc/tutorial/resolver'); + + if (env.opts.tutorials) { + resolver.load(env.opts.tutorials); + resolver.resolve(); + } + env.opts.template = env.opts.template || 'templates/default'; // should define a global "publish" function @@ -248,7 +258,8 @@ function main() { if (typeof publish === 'function') { publish( new (require('typicaljoe/taffy'))(docs), - env.opts + env.opts, + resolver.root ); } else { // TODO throw no publish warning? diff --git a/package.json b/package.json index 69d41b1c..ed6f2d26 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "JSDoc", "version": "3.0.0alpha", - "revision": "1323214202201", + "revision": "1323947228470", "description": "An automatic documentation generator for javascript.", "keywords": [ "documentation", "javascript" ], "licenses": [ @@ -21,6 +21,10 @@ { "name": "Michael Mathews", "email": "micmath@gmail.com" + }, + { + "name": "Rafa\u0105 Wrzeszcz", + "email": "rafal.wrzeszcz@wrzasq.pl" } ], "maintainers": [ @@ -29,4 +33,4 @@ "email": "micmath@gmail.com" } ] -} \ No newline at end of file +} diff --git a/rhino_modules/fs.js b/rhino_modules/fs.js index 8022dd6e..5b1c38ce 100644 --- a/rhino_modules/fs.js +++ b/rhino_modules/fs.js @@ -133,7 +133,7 @@ exports.copyFile = function(inFile, outDir, fileName) { bis.close(); }; -function toFile(path) { +var toFile = exports.toFile = function(path) { var parts = path.split(/[\\\/]/); return parts.pop(); } diff --git a/rhino_modules/jsdoc/augment.js b/rhino_modules/jsdoc/augment.js index 36085bdd..bcbd470a 100644 --- a/rhino_modules/jsdoc/augment.js +++ b/rhino_modules/jsdoc/augment.js @@ -48,6 +48,8 @@ members = getMembers(parents[j], docs); for (var k=0, kk=members.length; k + @license Apache License 2.0 - See file 'LICENSE.md' in this project. + */ + +var mdParser = require('evilstreak/markdown'); + +/** + @module jsdoc/tutorial + */ + +/** + @class + @classdesc Represents a single JSDoc tutorial. + @param {string} name - Tutorial name. + @param {string} content - Text content. + @param {number} type - Source formating. + */ +exports.Tutorial = function(name, content, type) { + this.title = this.name = name; + this.content = content; + this.type = type; + + // default values + this.parent = null; + this.children = []; +}; + +/** Moves children from current parent to different one. + @param {Tutorial} parent - New parent. + */ +exports.Tutorial.prototype.setParent = function(parent) { + // removes node from old parent + if (this.parent) { + this.parent.removeChild(this); + } + + this.parent = parent; + this.parent.addChild(this); +}; + +/** Removes children from current node. + @param {Tutorial} child - Old child. + */ +exports.Tutorial.prototype.removeChild = function(child) { + var index = this.children.indexOf(child); + if (index != -1) { + this.children.splice(index, 1); + } +}; + +/** Adds new children to current node. + @param {Tutorial} child - New child. + */ +exports.Tutorial.prototype.addChild = function(child) { + this.children.push(child); +}; + +/** Prepares source. + @return {string} HTML source. + */ +exports.Tutorial.prototype.parse = function() { + switch (this.type) { + // nothing to do + case exports.TYPES.HTML: + return this.content; + + // markdown + case exports.TYPES.MARKDOWN: + return mdParser.toHTML(this.content) + .replace(/&/g, '&') // because markdown escapes these + .replace(/</g, '<') + .replace(/>/g, '>'); + + // uhm... should we react somehow? + // if not then this case can be merged with TYPES.HTML + default: + return this.content; + } +}; + +/** Tutorial source types. + @enum {number} + */ +exports.TYPES = { + HTML: 1, + MARKDOWN: 2 +}; diff --git a/rhino_modules/jsdoc/tutorial/resolver.js b/rhino_modules/jsdoc/tutorial/resolver.js new file mode 100644 index 00000000..fc78ac02 --- /dev/null +++ b/rhino_modules/jsdoc/tutorial/resolver.js @@ -0,0 +1,126 @@ +/** + @overview + @author Rafał Wrzeszcz + @license Apache License 2.0 - See file 'LICENSE.md' in this project. + */ + +/** + @module jsdoc/tutorial/resolver + */ + +var tutorial = require('jsdoc/tutorial'), + fs = require('fs'), + conf = {}, + tutorials = {}, + finder = /^(.*)\.(x(?:ht)?ml|html?|md|markdown|js(?:on)?)$/i; + +/** Adds new tutorial. + @param {tutorial.Tutorial} current - New tutorial. + */ +exports.addTutorial = function(current) { + tutorials[current.name] = current; + + // default temporary parent + current.setParent(exports.root); +}; + +/** Root tutorial. + @type tutorial.Tutorial + */ +exports.root = new tutorial.Tutorial('', ''); + +/** Additional instance method for root node. + @param {string} name - Tutorial name. + @reutrn {tutorial.Tutorial} Tutorial instance. + */ +exports.root.getByName = function(name) { + return tutorials[name]; +}; + +/** Load tutorials from given path. + @param {string} path - Tutorials directory. + */ +exports.load = function(path) { + var match, + type, + name, + current, + files = fs.ls(path); + + // tutorials handling + files.forEach(function(file) { + match = file.match(finder); + + // any filetype that can apply to tutorials + if (match) { + name = fs.toFile(match[1]); + content = fs.readFileSync(file); + + switch (match[2].toLowerCase()) { + // HTML type + case 'xml': + case 'xhtml': + case 'html': + case 'htm': + type = tutorial.TYPES.HTML; + break; + + // Markdown typs + case 'md': + case 'markdown': + type = tutorial.TYPES.MARKDOWN; + break; + + // configuration file + case 'js': + case 'json': + conf[name] = JSON.parse(content); + + // how can it be? check `finder' regexp + default: + // not a file we want to work with + return; + } + + current = new tutorial.Tutorial(name, content, type); + exports.addTutorial(current); + } + }); +}; + +/** Resolves hierarchical structure. + @param {object} map - Contents map. + */ +exports.resolve = function() { + var item, + current; + for (var name in conf) { + // should we be restrictive here? + // what is someone just wants to keep sample sources in same directory with tutorials? + // I've decided to leave such cases alone + if (!(name in tutorials)) { + continue; + } + + item = conf[name]; + current = tutorials[name] + + // set title + if (item.title) { + current.title = item.title; + } + + // add children + if (item.children) { + item.children.forEach(function(child) { + // I really didn't want to throw you an exception in most cases + // but now, user, you pissed me off ;) + if (!(child in tutorials)) { + throw new Error("Missing child tutorial: " + child); + } + + tutorials[child].setParent(current); + }); + } + } +}; diff --git a/rhino_modules/jsdoc/util/templateHelper.js b/rhino_modules/jsdoc/util/templateHelper.js index 3ff14c87..37c0579d 100644 --- a/rhino_modules/jsdoc/util/templateHelper.js +++ b/rhino_modules/jsdoc/util/templateHelper.js @@ -8,7 +8,7 @@ var dictionary = require('jsdoc/tag/dictionary'); exports.globalName = 'global'; exports.fileExtension = '.html'; -/** Find symbol {@link ...} strings in text and turn into html links */ +/** Find symbol {@link ...} and {@tutorial ...} strings in text and turn into html links */ exports.resolveLinks = function(str) { str = str.replace(/(?:\[(.+?)\])?\{@link +(.+?)\}/gi, function(match, content, longname) { @@ -16,6 +16,12 @@ exports.resolveLinks = function(str) { } ); + str = str.replace(/(?:\[(.+?)\])?\{@tutorial +(.+?)\}/gi, + function(match, content, tutorial) { + return toTutorial(tutorial, content); + } + ); + return str; } @@ -102,4 +108,40 @@ function toLink(longname, content) { } } -exports.longnameToUrl = linkMap.longnameToUrl; \ No newline at end of file +/** @external {jsdoc.tutorial.Tutorial} */ +var tutorials; + +/** Sets tutorials map. + @param {jsdoc.tutorial.Tutorial} root - Root tutorial node. + */ +exports.setTutorials = function(root) { + tutorials = root; +}; + +exports.toTutorial = toTutorial = function(tutorial, content) { + if (!tutorial) { + throw new Error('Missing required parameter: tutorial'); + } + + var node = tutorials.getByName(tutorial); + // no such tutorial + if (!node) { + return 'Tutorial: '+tutorial+''; + } + + content = content || node.title; + + return ''+content+''; +} + +exports.longnameToUrl = linkMap.longnameToUrl; + +exports.tutorialToUrl = function(tutorial) { + var node = tutorials.getByName(tutorial); + // no such tutorial + if (!node) { + throw new Error('No such tutorial: '+tutorial); + } + + return 'tutorial-'+strToFilename(node.name)+exports.fileExtension; +}; diff --git a/templates/default/publish.js b/templates/default/publish.js index b40efc9d..5741dfa7 100644 --- a/templates/default/publish.js +++ b/templates/default/publish.js @@ -13,11 +13,16 @@ @global @param {TAFFY} data See . @param {object} opts + @param {Tutorial} tutorials */ - publish = function(data, opts) { + publish = function(data, opts, tutorials) { var out = '', - containerTemplate = template.render(fs.readFileSync(__dirname + '/templates/default/tmpl/container.tmpl')); + containerTemplate = template.render(fs.readFileSync(__dirname + '/templates/default/tmpl/container.tmpl')), + tutorialTemplate = template.render(fs.readFileSync(__dirname + '/templates/default/tmpl/tutorial.tmpl')); + // set up tutorials for helper + helper.setTutorials(tutorials); + function render(tmpl, partialData) { var renderFunction = arguments.callee.cache[tmpl]; if (!renderFunction) { @@ -26,6 +31,7 @@ partialData.render = arguments.callee; partialData.find = find; partialData.linkto = linkto; + partialData.tutoriallink = tutoriallink; partialData.htmlsafe = htmlsafe; return renderFunction.call(partialData, partialData); @@ -162,7 +168,7 @@ }; }); } - else if (doclet.see) { + if (doclet.see) { doclet.see.forEach(function(seeItem, i) { doclet.see[i] = hashToLink(doclet, seeItem); }); @@ -199,6 +205,10 @@ return url? ''+(linktext || longname)+'' : (linktext || longname); } + function tutoriallink(tutorial) { + return helper.toTutorial(tutorial); + } + var containers = ['class', 'module', 'external', 'namespace', 'mixin']; data.forEach(function(doclet) { @@ -245,29 +255,29 @@ var moduleNames = find({kind: 'module'}); if (moduleNames.length) { - nav = nav + '

Modules

    '; + nav += '

    Modules

      '; moduleNames.forEach(function(m) { if ( !seen.hasOwnProperty(m.longname) ) nav += '
    • '+linkto(m.longname, m.name)+'
    • '; seen[m.longname] = true; }); - nav = nav + '
    '; + nav += '
'; } var externalNames = find({kind: 'external'}); if (externalNames.length) { - nav = nav + '

Externals

    '; + nav += '

    Externals

      '; externalNames.forEach(function(e) { if ( !seen.hasOwnProperty(e.longname) ) nav += '
    • '+linkto( e.longname, e.name.replace(/(^"|"$)/g, '') )+'
    • '; seen[e.longname] = true; }); - nav = nav + '
    '; + nav += '
'; } var classNames = find({kind: 'class'}); if (classNames.length) { - nav = nav + '

Classes

    '; + nav += '

    Classes

      '; classNames.forEach(function(c) { var moduleSameName = find({kind: 'module', longname: c.longname}); if (moduleSameName.length) { @@ -279,52 +289,61 @@ seen[c.longname] = true; }); - nav = nav + '
    '; + nav += '
'; } var namespaceNames = find({kind: 'namespace'}); if (namespaceNames.length) { - nav = nav + '

Namespaces

    '; + nav += '

    Namespaces

      '; namespaceNames.forEach(function(n) { if ( !seen.hasOwnProperty(n.longname) ) nav += '
    • '+linkto(n.longname, n.name)+'
    • '; seen[n.longname] = true; }); - nav = nav + '
    '; + nav += '
'; } // var constantNames = find({kind: 'constants'}); // if (constantNames.length) { -// nav = nav + '

Constants

    '; +// nav += '

    Constants

      '; // constantNames.forEach(function(c) { // if ( !seen.hasOwnProperty(c.longname) ) nav += '
    • '+linkto(c.longname, c.name)+'
    • '; // seen[c.longname] = true; // }); // -// nav = nav + '
    '; +// nav += '
'; // } var mixinNames = find({kind: 'mixin'}); if (mixinNames.length) { - nav = nav + '

Mixins

    '; + nav += '

    Mixins

      '; mixinNames.forEach(function(m) { if ( !seen.hasOwnProperty(m.longname) ) nav += '
    • '+linkto(m.longname, m.name)+'
    • '; seen[m.longname] = true; }); - nav = nav + '
    '; + nav += '
'; } + if (tutorials.children.length) { + nav += '

Tutorials

    '; + tutorials.children.forEach(function(t) { + nav += '
  • '+tutoriallink(t.name)+'
  • '; + }); + + nav += '
'; + } + var globalNames = find({kind: ['member', 'function', 'constant', 'typedef'], 'memberof': {'isUndefined': true}}); if (globalNames.length) { - nav = nav + '

Global

    '; + nav += '

    Global

      '; globalNames.forEach(function(g) { if ( g.kind !== 'typedef' && !seen.hasOwnProperty(g.longname) ) nav += '
    • '+linkto(g.longname, g.name)+'
    • '; seen[g.longname] = true; }); - nav = nav + '
    '; + nav += '
'; } for (var longname in helper.longnameToUrl) { @@ -348,8 +367,8 @@ } if (globals.length) generate('Global', [{kind: 'globalobj'}], 'global.html'); - - + + function generate(title, docs, filename) { var data = { title: title, @@ -360,6 +379,7 @@ render: render, find: find, linkto: linkto, + tutoriallink: tutoriallink, htmlsafe: htmlsafe }; @@ -370,6 +390,39 @@ fs.writeFileSync(path, html) } + + function generateTutorial(title, tutorial, filename) { + var data = { + title: title, + header: tutorial.title, + content: tutorial.parse(), + children: tutorial.children, + nav: nav, + + // helpers + render: render, + find: find, + linkto: linkto, + tutoriallink: tutoriallink, + htmlsafe: htmlsafe + }; + + var path = outdir + '/' + filename, + html = tutorialTemplate.call(data, data); + + // yes, you can use {@link} in tutorials too! + html = helper.resolveLinks(html); // turn {@link foo} into foo + + fs.writeFileSync(path, html) + } + + // tutorials can have only one parent so there is no risk for loops + function saveChildren(node) { + node.children.forEach(function(child) { + generateTutorial('Tutorial: '+child.title, child, helper.tutorialToUrl(child.name)); + }); + } + saveChildren(tutorials); } function hashToLink(doclet, hash) { diff --git a/templates/default/static/styles/jsdoc-default.css b/templates/default/static/styles/jsdoc-default.css index ea0628ea..ad5f5e0b 100644 --- a/templates/default/static/styles/jsdoc-default.css +++ b/templates/default/static/styles/jsdoc-default.css @@ -254,3 +254,7 @@ h6 .params th, .props th { border-right: 1px solid #aaa; } .params thead .last, .props thead .last { border-right: 1px solid #ddd; } + +.disabled { + color: #454545; +} diff --git a/templates/default/tmpl/details.tmpl b/templates/default/tmpl/details.tmpl index d8326fc3..2147a9bc 100644 --- a/templates/default/tmpl/details.tmpl +++ b/templates/default/tmpl/details.tmpl @@ -60,6 +60,17 @@
Source:
  • , line
+ + +
Tutorials:
+
+
    '+tutoriallink(t)+''); + }); + ?>
+
+
See:
diff --git a/templates/default/tmpl/tutorial.tmpl b/templates/default/tmpl/tutorial.tmpl new file mode 100644 index 00000000..b0e1354b --- /dev/null +++ b/templates/default/tmpl/tutorial.tmpl @@ -0,0 +1,53 @@ + + + + + JSDoc: <?js= title ?> + + + + + + + + + + +
+ +

+ +
+ +
+ 0) { ?> +
    '+tutoriallink(t.name)+''); + }); + ?>
+ + +

+
+ +
+ +
+ +
+
+ + + +
+ +
+ Documentation generated by JSDoc 3 on +
+ + + + \ No newline at end of file diff --git a/test/tutorials/build.sh b/test/tutorials/build.sh new file mode 100755 index 00000000..8937f26d --- /dev/null +++ b/test/tutorials/build.sh @@ -0,0 +1,2 @@ +rm -rf out +../../jsdoc -u tutorials src -d out diff --git a/test/tutorials/src/x.js b/test/tutorials/src/x.js new file mode 100644 index 00000000..987f8f98 --- /dev/null +++ b/test/tutorials/src/x.js @@ -0,0 +1,8 @@ +/** + * Test {@tutorial test2} {@tutorial dupa} + * + * @class + * @tutorial test + * @tutorial jasia + */ +function Test() {} diff --git a/test/tutorials/tutorials/test.html b/test/tutorials/tutorials/test.html new file mode 100644 index 00000000..aa4a4ff9 --- /dev/null +++ b/test/tutorials/tutorials/test.html @@ -0,0 +1,3 @@ +

Test.html

+ +

{@link Test}

diff --git a/test/tutorials/tutorials/test.js b/test/tutorials/tutorials/test.js new file mode 100644 index 00000000..d894f21e --- /dev/null +++ b/test/tutorials/tutorials/test.js @@ -0,0 +1 @@ +{"title": "Test tutorial", "children": ["test2"]} diff --git a/test/tutorials/tutorials/test2.json b/test/tutorials/tutorials/test2.json new file mode 100644 index 00000000..3c7d98c5 --- /dev/null +++ b/test/tutorials/tutorials/test2.json @@ -0,0 +1 @@ +{"title": "Test 2"} diff --git a/test/tutorials/tutorials/test2.markdown b/test/tutorials/tutorials/test2.markdown new file mode 100644 index 00000000..09510c03 --- /dev/null +++ b/test/tutorials/tutorials/test2.markdown @@ -0,0 +1 @@ +# test2.markdown