├── package.json ├── LICENSE ├── preprocess.js ├── addon-verify.js ├── generate.js ├── README.md ├── html.js └── json.js /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Isaac Z. Schlueter (http://blog.izs.me/)", 3 | "name": "iojs-doctool", 4 | "description": "Internal tool for generating io.js API docs", 5 | "version": "1.0.0", 6 | "engines": { 7 | "node": ">=0.6.10" 8 | }, 9 | "dependencies": { 10 | "marked": "~0.1.9" 11 | }, 12 | "devDependencies": {}, 13 | "optionalDependencies": {}, 14 | "bin": "./generate.js" 15 | } 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright Joyent, Inc. and other Node contributors. All rights reserved. 2 | Permission is hereby granted, free of charge, to any person obtaining a copy 3 | of this software and associated documentation files (the "Software"), to 4 | deal in the Software without restriction, including without limitation the 5 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 6 | sell copies of the Software, and to permit persons to whom the Software is 7 | furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in 10 | all copies or substantial portions of the Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 13 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 14 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 15 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 16 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 17 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 18 | IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /preprocess.js: -------------------------------------------------------------------------------- 1 | module.exports = preprocess; 2 | 3 | var path = require('path'); 4 | var fs = require('fs'); 5 | 6 | var includeExpr = /^@include\s+([A-Za-z0-9-_]+)(?:\.)?([a-zA-Z]*)$/gmi; 7 | var includeData = {}; 8 | 9 | function preprocess(inputFile, input, cb) { 10 | input = stripComments(input); 11 | processIncludes(inputFile, input, function (err, data) { 12 | if (err) return cb(err); 13 | 14 | cb(null, data); 15 | }); 16 | } 17 | 18 | function stripComments(input) { 19 | return input.replace(/^@\/\/.*$/gmi, ''); 20 | } 21 | 22 | function processIncludes(inputFile, input, cb) { 23 | var includes = input.match(includeExpr); 24 | if (includes === null) return cb(null, input); 25 | var errState = null; 26 | console.error(includes); 27 | var incCount = includes.length; 28 | if (incCount === 0) cb(null, input); 29 | includes.forEach(function(include) { 30 | var fname = include.replace(/^@include\s+/, ''); 31 | if (!fname.match(/\.markdown$/)) fname += '.markdown'; 32 | 33 | if (includeData.hasOwnProperty(fname)) { 34 | input = input.split(include).join(includeData[fname]); 35 | incCount--; 36 | if (incCount === 0) { 37 | return cb(null, input); 38 | } 39 | } 40 | 41 | var fullFname = path.resolve(path.dirname(inputFile), fname); 42 | fs.readFile(fullFname, 'utf8', function(er, inc) { 43 | if (errState) return; 44 | if (er) return cb(errState = er); 45 | preprocess(inputFile, inc, function(er, inc) { 46 | if (errState) return; 47 | if (er) return cb(errState = er); 48 | incCount--; 49 | includeData[fname] = inc; 50 | input = input.split(include+'\n').join(includeData[fname]+'\n'); 51 | if (incCount === 0) { 52 | return cb(null, input); 53 | } 54 | }); 55 | }); 56 | }); 57 | } 58 | 59 | -------------------------------------------------------------------------------- /addon-verify.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var path = require('path'); 3 | var marked = require('marked'); 4 | 5 | var doc = path.resolve(__dirname, '..', '..', 'doc', 'api', 'addons.markdown'); 6 | var verifyDir = path.resolve(__dirname, '..', '..', 'test', 'addons'); 7 | 8 | var contents = fs.readFileSync(doc).toString(); 9 | 10 | var tokens = marked.lexer(contents, {}); 11 | var files = null; 12 | var id = 0; 13 | 14 | // Just to make sure that all examples will be processed 15 | tokens.push({ type: 'heading' }); 16 | 17 | var oldDirs = fs.readdirSync(verifyDir); 18 | oldDirs = oldDirs.filter(function(dir) { 19 | return /^doc-/.test(dir); 20 | }).map(function(dir) { 21 | return path.resolve(verifyDir, dir); 22 | }); 23 | 24 | for (var i = 0; i < tokens.length; i++) { 25 | var token = tokens[i]; 26 | if (token.type === 'heading') { 27 | if (files && Object.keys(files).length !== 0) { 28 | verifyFiles(files, function(err) { 29 | if (err) 30 | console.log(err); 31 | else 32 | console.log('done'); 33 | }); 34 | } 35 | files = {}; 36 | } else if (token.type === 'code') { 37 | var match = token.text.match(/^\/\/\s+(.*\.(?:cc|h|js))[\r\n]/); 38 | if (match === null) 39 | continue; 40 | files[match[1]] = token.text; 41 | } 42 | } 43 | 44 | function once(fn) { 45 | var once = false; 46 | return function() { 47 | if (once) 48 | return; 49 | once = true; 50 | fn.apply(this, arguments); 51 | }; 52 | } 53 | 54 | function verifyFiles(files, callback) { 55 | var dir = path.resolve(verifyDir, 'doc-' + id++); 56 | 57 | files = Object.keys(files).map(function(name) { 58 | return { 59 | path: path.resolve(dir, name), 60 | name: name, 61 | content: files[name] 62 | }; 63 | }); 64 | files.push({ 65 | path: path.resolve(dir, 'binding.gyp'), 66 | content: JSON.stringify({ 67 | targets: [ 68 | { 69 | target_name: 'addon', 70 | sources: files.map(function(file) { 71 | return file.name; 72 | }) 73 | } 74 | ] 75 | }) 76 | }); 77 | 78 | fs.mkdir(dir, function() { 79 | // Ignore errors 80 | 81 | var waiting = files.length; 82 | for (var i = 0; i < files.length; i++) 83 | fs.writeFile(files[i].path, files[i].content, next); 84 | 85 | var done = once(callback); 86 | function next(err) { 87 | if (err) 88 | return done(err); 89 | 90 | if (--waiting === 0) 91 | done(); 92 | } 93 | }); 94 | } 95 | -------------------------------------------------------------------------------- /generate.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | // Copyright Joyent, Inc. and other Node contributors. 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a 5 | // copy of this software and associated documentation files (the 6 | // "Software"), to deal in the Software without restriction, including 7 | // without limitation the rights to use, copy, modify, merge, publish, 8 | // distribute, sublicense, and/or sell copies of the Software, and to permit 9 | // persons to whom the Software is furnished to do so, subject to the 10 | // following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included 13 | // in all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 16 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN 18 | // NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 19 | // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 20 | // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE 21 | // USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | var processIncludes = require('./preprocess.js'); 24 | var marked = require('marked'); 25 | var fs = require('fs'); 26 | var path = require('path'); 27 | 28 | // parse the args. 29 | // Don't use nopt or whatever for this. It's simple enough. 30 | 31 | var args = process.argv.slice(2); 32 | var format = 'json'; 33 | var template = null; 34 | var inputFile = null; 35 | 36 | args.forEach(function (arg) { 37 | if (!arg.match(/^\-\-/)) { 38 | inputFile = arg; 39 | } else if (arg.match(/^\-\-format=/)) { 40 | format = arg.replace(/^\-\-format=/, ''); 41 | } else if (arg.match(/^\-\-template=/)) { 42 | template = arg.replace(/^\-\-template=/, ''); 43 | } 44 | }) 45 | 46 | 47 | if (!inputFile) { 48 | throw new Error('No input file specified'); 49 | } 50 | 51 | 52 | console.error('Input file = %s', inputFile); 53 | fs.readFile(inputFile, 'utf8', function(er, input) { 54 | if (er) throw er; 55 | // process the input for @include lines 56 | processIncludes(inputFile, input, next); 57 | }); 58 | 59 | 60 | 61 | 62 | function next(er, input) { 63 | if (er) throw er; 64 | switch (format) { 65 | case 'json': 66 | require('./json.js')(input, inputFile, function(er, obj) { 67 | console.log(JSON.stringify(obj, null, 2)); 68 | if (er) throw er; 69 | }); 70 | break; 71 | 72 | case 'html': 73 | require('./html.js')(input, inputFile, template, function(er, html) { 74 | if (er) throw er; 75 | console.log(html); 76 | }); 77 | break; 78 | 79 | default: 80 | throw new Error('Invalid format: ' + format); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Here's how the node docs work. 2 | 3 | 1:1 relationship from `lib/.js` to `doc/api/.markdown` 4 | 5 | Each type of heading has a description block. 6 | 7 | 8 | ## module 9 | 10 | Stability: 3 - Stable 11 | 12 | description and examples. 13 | 14 | ### module.property 15 | 16 | * Type 17 | 18 | description of the property. 19 | 20 | ### module.someFunction(x, y, [z=100]) 21 | 22 | * `x` {String} the description of the string 23 | * `y` {Boolean} Should I stay or should I go? 24 | * `z` {Number} How many zebras to bring. 25 | 26 | A description of the function. 27 | 28 | ### Event: 'blerg' 29 | 30 | * Argument: SomeClass object. 31 | 32 | Modules don't usually raise events on themselves. `cluster` is the 33 | only exception. 34 | 35 | ## Class: SomeClass 36 | 37 | description of the class. 38 | 39 | ### Class Method: SomeClass.classMethod(anArg) 40 | 41 | * `anArg` {Object} Just an argument 42 | * `field` {String} anArg can have this field. 43 | * `field2` {Boolean} Another field. Default: `false`. 44 | * Return: {Boolean} `true` if it worked. 45 | 46 | Description of the method for humans. 47 | 48 | ### someClass.nextSibling() 49 | 50 | * Return: {SomeClass object | null} The next someClass in line. 51 | 52 | ### someClass.someProperty 53 | 54 | * String 55 | 56 | The indication of what someProperty is. 57 | 58 | ### Event: 'grelb' 59 | 60 | * `isBlerg` {Boolean} 61 | 62 | This event is emitted on instances of SomeClass, not on the module itself. 63 | 64 | 65 | * Modules have (description, Properties, Functions, Classes, Examples) 66 | * Properties have (type, description) 67 | * Functions have (list of arguments, description) 68 | * Classes have (description, Properties, Methods, Events) 69 | * Events have (list of arguments, description) 70 | * Methods have (list of arguments, description) 71 | * Properties have (type, description) 72 | 73 | ## Stability ratings: 0-5 74 | 75 | These can show up below any section heading, and apply to that section. 76 | 77 | 0 - Deprecated. This feature is known to be problematic, and changes are 78 | planned. Do not rely on it. Use of the feature may cause warnings. Backwards 79 | compatibility should not be expected. 80 | 81 | 1 - Experimental. This feature was introduced recently, and may change 82 | or be removed in future versions. Please try it out and provide feedback. 83 | If it addresses a use-case that is important to you, tell the node core team. 84 | 85 | 2 - Unstable. The API is in the process of settling, but has not yet had 86 | sufficient real-world testing to be considered stable. Backwards-compatibility 87 | will be maintained if reasonable. 88 | 89 | 3 - Stable. The API has proven satisfactory, but cleanup in the underlying 90 | code may cause minor changes. Backwards-compatibility is guaranteed. 91 | 92 | 4 - API Frozen. This API has been tested extensively in production and is 93 | unlikely to ever have to change. 94 | 95 | 5 - Locked. Unless serious bugs are found, this code will not ever 96 | change. Please do not suggest changes in this area, they will be refused. 97 | -------------------------------------------------------------------------------- /html.js: -------------------------------------------------------------------------------- 1 | // Copyright Joyent, Inc. and other Node contributors. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a 4 | // copy of this software and associated documentation files (the 5 | // "Software"), to deal in the Software without restriction, including 6 | // without limitation the rights to use, copy, modify, merge, publish, 7 | // distribute, sublicense, and/or sell copies of the Software, and to permit 8 | // persons to whom the Software is furnished to do so, subject to the 9 | // following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included 12 | // in all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 15 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN 17 | // NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 18 | // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 19 | // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE 20 | // USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | var fs = require('fs'); 23 | var marked = require('marked'); 24 | var path = require('path'); 25 | var preprocess = require('./preprocess.js'); 26 | 27 | module.exports = toHTML; 28 | 29 | // TODO(chrisdickinson): never stop vomitting / fix this. 30 | var gtocPath = path.resolve(path.join(__dirname, '..', '..', 'doc', 'api', '_toc.markdown')); 31 | var gtocLoading = null; 32 | var gtocData = null; 33 | 34 | function toHTML(input, filename, template, cb) { 35 | if (gtocData) { 36 | return onGtocLoaded(); 37 | } 38 | 39 | if (gtocLoading === null) { 40 | gtocLoading = [onGtocLoaded]; 41 | return loadGtoc(function(err, data) { 42 | if (err) throw err; 43 | gtocData = data; 44 | gtocLoading.forEach(function(xs) { 45 | xs(); 46 | }); 47 | }); 48 | } 49 | 50 | if (gtocLoading) { 51 | return gtocLoading.push(onGtocLoaded); 52 | } 53 | 54 | function onGtocLoaded() { 55 | var lexed = marked.lexer(input); 56 | fs.readFile(template, 'utf8', function(er, template) { 57 | if (er) return cb(er); 58 | render(lexed, filename, template, cb); 59 | }); 60 | } 61 | } 62 | 63 | function loadGtoc(cb) { 64 | fs.readFile(gtocPath, 'utf8', function(err, data) { 65 | if (err) return cb(err); 66 | 67 | preprocess(gtocPath, data, function(err, data) { 68 | if (err) return cb(err); 69 | 70 | data = marked(data).replace(/' }); 142 | } 143 | depth++; 144 | output.push(tok); 145 | return; 146 | } 147 | state = null; 148 | output.push(tok); 149 | return; 150 | } 151 | if (state === 'LIST') { 152 | if (tok.type === 'list_start') { 153 | depth++; 154 | output.push(tok); 155 | return; 156 | } 157 | if (tok.type === 'list_end') { 158 | depth--; 159 | if (depth === 0) { 160 | state = null; 161 | output.push({ type:'html', text: '' }); 162 | } 163 | output.push(tok); 164 | return; 165 | } 166 | if (tok.text) { 167 | tok.text = parseListItem(tok.text); 168 | } 169 | } 170 | output.push(tok); 171 | }); 172 | 173 | return output; 174 | } 175 | 176 | 177 | function parseListItem(text) { 178 | var parts = text.split('`'); 179 | var i; 180 | 181 | for (i = 0; i < parts.length; i += 2) { 182 | parts[i] = parts[i].replace(/\{([^\}]+)\}/, '$1'); 183 | } 184 | 185 | //XXX maybe put more stuff here? 186 | return parts.join('`'); 187 | } 188 | 189 | function parseAPIHeader(text) { 190 | text = text.replace(/(.*:)\s(\d)([\s\S]*)/, 191 | '
$1 $2$3
'); 192 | return text; 193 | } 194 | 195 | // section is just the first heading 196 | function getSection(lexed) { 197 | var section = ''; 198 | for (var i = 0, l = lexed.length; i < l; i++) { 199 | var tok = lexed[i]; 200 | if (tok.type === 'heading') return tok.text; 201 | } 202 | return ''; 203 | } 204 | 205 | 206 | function buildToc(lexed, filename, cb) { 207 | var indent = 0; 208 | var toc = []; 209 | var depth = 0; 210 | lexed.forEach(function(tok) { 211 | if (tok.type !== 'heading') return; 212 | if (tok.depth - depth > 1) { 213 | return cb(new Error('Inappropriate heading level\n' + 214 | JSON.stringify(tok))); 215 | } 216 | 217 | depth = tok.depth; 218 | var id = getId(filename + '_' + tok.text.trim()); 219 | toc.push(new Array((depth - 1) * 2 + 1).join(' ') + 220 | '*
' + 221 | tok.text + ''); 222 | tok.text += '#'; 224 | }); 225 | 226 | toc = marked.parse(toc.join('\n')); 227 | cb(null, toc); 228 | } 229 | 230 | var idCounters = {}; 231 | function getId(text) { 232 | text = text.toLowerCase(); 233 | text = text.replace(/[^a-z0-9]+/g, '_'); 234 | text = text.replace(/^_+|_+$/, ''); 235 | text = text.replace(/^([^a-z])/, '_$1'); 236 | if (idCounters.hasOwnProperty(text)) { 237 | text += '_' + (++idCounters[text]); 238 | } else { 239 | idCounters[text] = 0; 240 | } 241 | return text; 242 | } 243 | 244 | -------------------------------------------------------------------------------- /json.js: -------------------------------------------------------------------------------- 1 | // Copyright Joyent, Inc. and other Node contributors. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a 4 | // copy of this software and associated documentation files (the 5 | // "Software"), to deal in the Software without restriction, including 6 | // without limitation the rights to use, copy, modify, merge, publish, 7 | // distribute, sublicense, and/or sell copies of the Software, and to permit 8 | // persons to whom the Software is furnished to do so, subject to the 9 | // following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included 12 | // in all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 15 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN 17 | // NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 18 | // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 19 | // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE 20 | // USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | module.exports = doJSON; 23 | 24 | // Take the lexed input, and return a JSON-encoded object 25 | // A module looks like this: https://gist.github.com/1777387 26 | 27 | var marked = require('marked'); 28 | 29 | function doJSON(input, filename, cb) { 30 | var root = {source: filename}; 31 | var stack = [root]; 32 | var depth = 0; 33 | var current = root; 34 | var state = null; 35 | var lexed = marked.lexer(input); 36 | lexed.forEach(function (tok) { 37 | var type = tok.type; 38 | var text = tok.text; 39 | 40 | // 41 | // This is for cases where the markdown semantic structure is lacking. 42 | if (type === 'paragraph' || type === 'html') { 43 | var metaExpr = /\n*/g; 44 | text = text.replace(metaExpr, function(_0, k, v) { 45 | current[k.trim()] = v.trim(); 46 | return ''; 47 | }); 48 | text = text.trim(); 49 | if (!text) return; 50 | } 51 | 52 | if (type === 'heading' && 53 | !text.trim().match(/^example/i)) { 54 | if (tok.depth - depth > 1) { 55 | return cb(new Error('Inappropriate heading level\n'+ 56 | JSON.stringify(tok))); 57 | } 58 | 59 | // Sometimes we have two headings with a single 60 | // blob of description. Treat as a clone. 61 | if (current && 62 | state === 'AFTERHEADING' && 63 | depth === tok.depth) { 64 | var clone = current; 65 | current = newSection(tok); 66 | current.clone = clone; 67 | // don't keep it around on the stack. 68 | stack.pop(); 69 | } else { 70 | // if the level is greater than the current depth, 71 | // then it's a child, so we should just leave the stack 72 | // as it is. 73 | // However, if it's a sibling or higher, then it implies 74 | // the closure of the other sections that came before. 75 | // root is always considered the level=0 section, 76 | // and the lowest heading is 1, so this should always 77 | // result in having a valid parent node. 78 | var d = tok.depth; 79 | while (d <= depth) { 80 | finishSection(stack.pop(), stack[stack.length - 1]); 81 | d++; 82 | } 83 | current = newSection(tok); 84 | } 85 | 86 | depth = tok.depth; 87 | stack.push(current); 88 | state = 'AFTERHEADING'; 89 | return; 90 | } // heading 91 | 92 | // Immediately after a heading, we can expect the following 93 | // 94 | // { type: 'code', text: 'Stability: ...' }, 95 | // 96 | // a list: starting with list_start, ending with list_end, 97 | // maybe containing other nested lists in each item. 98 | // 99 | // If one of these isnt' found, then anything that comes between 100 | // here and the next heading should be parsed as the desc. 101 | var stability 102 | if (state === 'AFTERHEADING') { 103 | if (type === 'code' && 104 | (stability = text.match(/^Stability: ([0-5])(?:\s*-\s*)?(.*)$/))) { 105 | current.stability = parseInt(stability[1], 10); 106 | current.stabilityText = stability[2].trim(); 107 | return; 108 | } else if (type === 'list_start' && !tok.ordered) { 109 | state = 'AFTERHEADING_LIST'; 110 | current.list = current.list || []; 111 | current.list.push(tok); 112 | current.list.level = 1; 113 | } else { 114 | current.desc = current.desc || []; 115 | if (!Array.isArray(current.desc)) { 116 | current.shortDesc = current.desc; 117 | current.desc = []; 118 | } 119 | current.desc.push(tok); 120 | state = 'DESC'; 121 | } 122 | return; 123 | } 124 | 125 | if (state === 'AFTERHEADING_LIST') { 126 | current.list.push(tok); 127 | if (type === 'list_start') { 128 | current.list.level++; 129 | } else if (type === 'list_end') { 130 | current.list.level--; 131 | } 132 | if (current.list.level === 0) { 133 | state = 'AFTERHEADING'; 134 | processList(current); 135 | } 136 | return; 137 | } 138 | 139 | current.desc = current.desc || []; 140 | current.desc.push(tok); 141 | 142 | }); 143 | 144 | // finish any sections left open 145 | while (root !== (current = stack.pop())) { 146 | finishSection(current, stack[stack.length - 1]); 147 | } 148 | 149 | return cb(null, root) 150 | } 151 | 152 | 153 | // go from something like this: 154 | // [ { type: 'list_item_start' }, 155 | // { type: 'text', 156 | // text: '`settings` Object, Optional' }, 157 | // { type: 'list_start', ordered: false }, 158 | // { type: 'list_item_start' }, 159 | // { type: 'text', 160 | // text: 'exec: String, file path to worker file. Default: `__filename`' }, 161 | // { type: 'list_item_end' }, 162 | // { type: 'list_item_start' }, 163 | // { type: 'text', 164 | // text: 'args: Array, string arguments passed to worker.' }, 165 | // { type: 'text', 166 | // text: 'Default: `process.argv.slice(2)`' }, 167 | // { type: 'list_item_end' }, 168 | // { type: 'list_item_start' }, 169 | // { type: 'text', 170 | // text: 'silent: Boolean, whether or not to send output to parent\'s stdio.' }, 171 | // { type: 'text', text: 'Default: `false`' }, 172 | // { type: 'space' }, 173 | // { type: 'list_item_end' }, 174 | // { type: 'list_end' }, 175 | // { type: 'list_item_end' }, 176 | // { type: 'list_end' } ] 177 | // to something like: 178 | // [ { name: 'settings', 179 | // type: 'object', 180 | // optional: true, 181 | // settings: 182 | // [ { name: 'exec', 183 | // type: 'string', 184 | // desc: 'file path to worker file', 185 | // default: '__filename' }, 186 | // { name: 'args', 187 | // type: 'array', 188 | // default: 'process.argv.slice(2)', 189 | // desc: 'string arguments passed to worker.' }, 190 | // { name: 'silent', 191 | // type: 'boolean', 192 | // desc: 'whether or not to send output to parent\'s stdio.', 193 | // default: 'false' } ] } ] 194 | 195 | function processList(section) { 196 | var list = section.list; 197 | var values = []; 198 | var current; 199 | var stack = []; 200 | 201 | // for now, *just* build the heirarchical list 202 | list.forEach(function(tok) { 203 | var type = tok.type; 204 | if (type === 'space') return; 205 | if (type === 'list_item_start') { 206 | if (!current) { 207 | var n = {}; 208 | values.push(n); 209 | current = n; 210 | } else { 211 | current.options = current.options || []; 212 | stack.push(current); 213 | var n = {}; 214 | current.options.push(n); 215 | current = n; 216 | } 217 | return; 218 | } else if (type === 'list_item_end') { 219 | if (!current) { 220 | throw new Error('invalid list - end without current item\n' + 221 | JSON.stringify(tok) + '\n' + 222 | JSON.stringify(list)); 223 | } 224 | current = stack.pop(); 225 | } else if (type === 'text') { 226 | if (!current) { 227 | throw new Error('invalid list - text without current item\n' + 228 | JSON.stringify(tok) + '\n' + 229 | JSON.stringify(list)); 230 | } 231 | current.textRaw = current.textRaw || ''; 232 | current.textRaw += tok.text + ' '; 233 | } 234 | }); 235 | 236 | // shove the name in there for properties, since they are always 237 | // just going to be the value etc. 238 | if (section.type === 'property' && values[0]) { 239 | values[0].textRaw = '`' + section.name + '` ' + values[0].textRaw; 240 | } 241 | 242 | // now pull the actual values out of the text bits. 243 | values.forEach(parseListItem); 244 | 245 | // Now figure out what this list actually means. 246 | // depending on the section type, the list could be different things. 247 | 248 | switch (section.type) { 249 | case 'ctor': 250 | case 'classMethod': 251 | case 'method': 252 | // each item is an argument, unless the name is 'return', 253 | // in which case it's the return value. 254 | section.signatures = section.signatures || []; 255 | var sig = {} 256 | section.signatures.push(sig); 257 | sig.params = values.filter(function(v) { 258 | if (v.name === 'return') { 259 | sig.return = v; 260 | return false; 261 | } 262 | return true; 263 | }); 264 | parseSignature(section.textRaw, sig); 265 | break; 266 | 267 | case 'property': 268 | // there should be only one item, which is the value. 269 | // copy the data up to the section. 270 | var value = values[0] || {}; 271 | delete value.name; 272 | section.typeof = value.type; 273 | delete value.type; 274 | Object.keys(value).forEach(function(k) { 275 | section[k] = value[k]; 276 | }); 277 | break; 278 | 279 | case 'event': 280 | // event: each item is an argument. 281 | section.params = values; 282 | break; 283 | } 284 | 285 | // section.listParsed = values; 286 | delete section.list; 287 | } 288 | 289 | 290 | // textRaw = "someobject.someMethod(a[, b=100][, c])" 291 | function parseSignature(text, sig) { 292 | var params = text.match(paramExpr); 293 | if (!params) return; 294 | params = params[1]; 295 | // the [ is irrelevant. ] indicates optionalness. 296 | params = params.replace(/\[/g, ''); 297 | params = params.split(/,/) 298 | params.forEach(function(p, i, _) { 299 | p = p.trim(); 300 | if (!p) return; 301 | var param = sig.params[i]; 302 | var optional = false; 303 | var def; 304 | // [foo] -> optional 305 | if (p.charAt(p.length - 1) === ']') { 306 | optional = true; 307 | p = p.substr(0, p.length - 1); 308 | p = p.trim(); 309 | } 310 | var eq = p.indexOf('='); 311 | if (eq !== -1) { 312 | def = p.substr(eq + 1); 313 | p = p.substr(0, eq); 314 | } 315 | if (!param) { 316 | param = sig.params[i] = { name: p }; 317 | } 318 | // at this point, the name should match. 319 | if (p !== param.name) { 320 | console.error('Warning: invalid param "%s"', p); 321 | console.error(' > ' + JSON.stringify(param)); 322 | console.error(' > ' + text); 323 | } 324 | if (optional) param.optional = true; 325 | if (def !== undefined) param.default = def; 326 | }); 327 | } 328 | 329 | 330 | function parseListItem(item) { 331 | if (item.options) item.options.forEach(parseListItem); 332 | if (!item.textRaw) return; 333 | 334 | // the goal here is to find the name, type, default, and optional. 335 | // anything left over is 'desc' 336 | var text = item.textRaw.trim(); 337 | // text = text.replace(/^(Argument|Param)s?\s*:?\s*/i, ''); 338 | 339 | text = text.replace(/^, /, '').trim(); 340 | var retExpr = /^returns?\s*:?\s*/i; 341 | var ret = text.match(retExpr); 342 | if (ret) { 343 | item.name = 'return'; 344 | text = text.replace(retExpr, ''); 345 | } else { 346 | var nameExpr = /^['`"]?([^'`": \{]+)['`"]?\s*:?\s*/; 347 | var name = text.match(nameExpr); 348 | if (name) { 349 | item.name = name[1]; 350 | text = text.replace(nameExpr, ''); 351 | } 352 | } 353 | 354 | text = text.trim(); 355 | var defaultExpr = /\(default\s*[:=]?\s*['"`]?([^, '"`]*)['"`]?\)/i; 356 | var def = text.match(defaultExpr); 357 | if (def) { 358 | item.default = def[1]; 359 | text = text.replace(defaultExpr, ''); 360 | } 361 | 362 | text = text.trim(); 363 | var typeExpr = /^\{([^\}]+)\}/; 364 | var type = text.match(typeExpr); 365 | if (type) { 366 | item.type = type[1]; 367 | text = text.replace(typeExpr, ''); 368 | } 369 | 370 | text = text.trim(); 371 | var optExpr = /^Optional\.|(?:, )?Optional$/; 372 | var optional = text.match(optExpr); 373 | if (optional) { 374 | item.optional = true; 375 | text = text.replace(optExpr, ''); 376 | } 377 | 378 | text = text.replace(/^\s*-\s*/, ''); 379 | text = text.trim(); 380 | if (text) item.desc = text; 381 | } 382 | 383 | 384 | function finishSection(section, parent) { 385 | if (!section || !parent) { 386 | throw new Error('Invalid finishSection call\n'+ 387 | JSON.stringify(section) + '\n' + 388 | JSON.stringify(parent)); 389 | } 390 | 391 | if (!section.type) { 392 | section.type = 'module'; 393 | if (parent && (parent.type === 'misc')) { 394 | section.type = 'misc'; 395 | } 396 | section.displayName = section.name; 397 | section.name = section.name.toLowerCase() 398 | .trim().replace(/\s+/g, '_'); 399 | } 400 | 401 | if (section.desc && Array.isArray(section.desc)) { 402 | section.desc.links = section.desc.links || []; 403 | section.desc = marked.parser(section.desc); 404 | } 405 | 406 | if (!section.list) section.list = []; 407 | processList(section); 408 | 409 | // classes sometimes have various 'ctor' children 410 | // which are actually just descriptions of a constructor 411 | // class signature. 412 | // Merge them into the parent. 413 | if (section.type === 'class' && section.ctors) { 414 | section.signatures = section.signatures || []; 415 | var sigs = section.signatures; 416 | section.ctors.forEach(function(ctor) { 417 | ctor.signatures = ctor.signatures || [{}]; 418 | ctor.signatures.forEach(function(sig) { 419 | sig.desc = ctor.desc; 420 | }); 421 | sigs.push.apply(sigs, ctor.signatures); 422 | }); 423 | delete section.ctors; 424 | } 425 | 426 | // properties are a bit special. 427 | // their "type" is the type of object, not "property" 428 | if (section.properties) { 429 | section.properties.forEach(function (p) { 430 | if (p.typeof) p.type = p.typeof; 431 | else delete p.type; 432 | delete p.typeof; 433 | }); 434 | } 435 | 436 | // handle clones 437 | if (section.clone) { 438 | var clone = section.clone; 439 | delete section.clone; 440 | delete clone.clone; 441 | deepCopy(section, clone); 442 | finishSection(clone, parent); 443 | } 444 | 445 | var plur; 446 | if (section.type.slice(-1) === 's') { 447 | plur = section.type + 'es'; 448 | } else if (section.type.slice(-1) === 'y') { 449 | plur = section.type.replace(/y$/, 'ies'); 450 | } else { 451 | plur = section.type + 's'; 452 | } 453 | 454 | // if the parent's type is 'misc', then it's just a random 455 | // collection of stuff, like the "globals" section. 456 | // Make the children top-level items. 457 | if (section.type === 'misc') { 458 | Object.keys(section).forEach(function(k) { 459 | switch (k) { 460 | case 'textRaw': 461 | case 'name': 462 | case 'type': 463 | case 'desc': 464 | case 'miscs': 465 | return; 466 | default: 467 | if (parent.type === 'misc') { 468 | return; 469 | } 470 | if (Array.isArray(k) && parent[k]) { 471 | parent[k] = parent[k].concat(section[k]); 472 | } else if (!parent[k]) { 473 | parent[k] = section[k]; 474 | } else { 475 | // parent already has, and it's not an array. 476 | return; 477 | } 478 | } 479 | }); 480 | } 481 | 482 | parent[plur] = parent[plur] || []; 483 | parent[plur].push(section); 484 | } 485 | 486 | 487 | // Not a general purpose deep copy. 488 | // But sufficient for these basic things. 489 | function deepCopy(src, dest) { 490 | Object.keys(src).filter(function(k) { 491 | return !dest.hasOwnProperty(k); 492 | }).forEach(function(k) { 493 | dest[k] = deepCopy_(src[k]); 494 | }); 495 | } 496 | 497 | function deepCopy_(src) { 498 | if (!src) return src; 499 | if (Array.isArray(src)) { 500 | var c = new Array(src.length); 501 | src.forEach(function(v, i) { 502 | c[i] = deepCopy_(v); 503 | }); 504 | return c; 505 | } 506 | if (typeof src === 'object') { 507 | var c = {}; 508 | Object.keys(src).forEach(function(k) { 509 | c[k] = deepCopy_(src[k]); 510 | }); 511 | return c; 512 | } 513 | return src; 514 | } 515 | 516 | 517 | // these parse out the contents of an H# tag 518 | var eventExpr = /^Event(?::|\s)+['"]?([^"']+).*$/i; 519 | var classExpr = /^Class:\s*([^ ]+).*?$/i; 520 | var propExpr = /^(?:property:?\s*)?[^\.]+\.([^ \.\(\)]+)\s*?$/i; 521 | var braceExpr = /^(?:property:?\s*)?[^\.\[]+(\[[^\]]+\])\s*?$/i; 522 | var classMethExpr = 523 | /^class\s*method\s*:?[^\.]+\.([^ \.\(\)]+)\([^\)]*\)\s*?$/i; 524 | var methExpr = 525 | /^(?:method:?\s*)?(?:[^\.]+\.)?([^ \.\(\)]+)\([^\)]*\)\s*?$/i; 526 | var newExpr = /^new ([A-Z][a-z]+)\([^\)]*\)\s*?$/; 527 | var paramExpr = /\((.*)\);?$/; 528 | 529 | function newSection(tok) { 530 | var section = {}; 531 | // infer the type from the text. 532 | var text = section.textRaw = tok.text; 533 | if (text.match(eventExpr)) { 534 | section.type = 'event'; 535 | section.name = text.replace(eventExpr, '$1'); 536 | } else if (text.match(classExpr)) { 537 | section.type = 'class'; 538 | section.name = text.replace(classExpr, '$1'); 539 | } else if (text.match(braceExpr)) { 540 | section.type = 'property'; 541 | section.name = text.replace(braceExpr, '$1'); 542 | } else if (text.match(propExpr)) { 543 | section.type = 'property'; 544 | section.name = text.replace(propExpr, '$1'); 545 | } else if (text.match(classMethExpr)) { 546 | section.type = 'classMethod'; 547 | section.name = text.replace(classMethExpr, '$1'); 548 | } else if (text.match(methExpr)) { 549 | section.type = 'method'; 550 | section.name = text.replace(methExpr, '$1'); 551 | } else if (text.match(newExpr)) { 552 | section.type = 'ctor'; 553 | section.name = text.replace(newExpr, '$1'); 554 | } else { 555 | section.name = text; 556 | } 557 | return section; 558 | } 559 | --------------------------------------------------------------------------------