| // Copyright Joyent, Inc. and other Node contributors. |
| // |
| // 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 OR COPYRIGHT HOLDERS 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. |
| |
| module.exports = doJSON; |
| |
| // Take the lexed input, and return a JSON-encoded object |
| // A module looks like this: https://gist.github.com/1777387 |
| |
| var marked = require('marked'); |
| |
| function doJSON(input, filename, cb) { |
| var root = {source: filename}; |
| var stack = [root]; |
| var depth = 0; |
| var current = root; |
| var state = null; |
| var lexed = marked.lexer(input); |
| lexed.forEach(function (tok) { |
| var type = tok.type; |
| var text = tok.text; |
| |
| // <!-- type = module --> |
| // This is for cases where the markdown semantic structure is lacking. |
| if (type === 'paragraph' || type === 'html') { |
| var metaExpr = /<!--([^=]+)=([^\-]+)-->\n*/g; |
| text = text.replace(metaExpr, function(_0, k, v) { |
| current[k.trim()] = v.trim(); |
| return ''; |
| }); |
| text = text.trim(); |
| if (!text) return; |
| } |
| |
| if (type === 'heading' && |
| !text.trim().match(/^example/i)) { |
| if (tok.depth - depth > 1) { |
| return cb(new Error('Inappropriate heading level\n'+ |
| JSON.stringify(tok))); |
| } |
| |
| // Sometimes we have two headings with a single |
| // blob of description. Treat as a clone. |
| if (current && |
| state === 'AFTERHEADING' && |
| depth === tok.depth) { |
| var clone = current; |
| current = newSection(tok); |
| current.clone = clone; |
| // don't keep it around on the stack. |
| stack.pop(); |
| } else { |
| // if the level is greater than the current depth, |
| // then it's a child, so we should just leave the stack |
| // as it is. |
| // However, if it's a sibling or higher, then it implies |
| // the closure of the other sections that came before. |
| // root is always considered the level=0 section, |
| // and the lowest heading is 1, so this should always |
| // result in having a valid parent node. |
| var d = tok.depth; |
| while (d <= depth) { |
| finishSection(stack.pop(), stack[stack.length - 1]); |
| d++; |
| } |
| current = newSection(tok); |
| } |
| |
| depth = tok.depth; |
| stack.push(current); |
| state = 'AFTERHEADING'; |
| return; |
| } // heading |
| |
| // Immediately after a heading, we can expect the following |
| // |
| // { type: 'code', text: 'Stability: ...' }, |
| // |
| // a list: starting with list_start, ending with list_end, |
| // maybe containing other nested lists in each item. |
| // |
| // If one of these isnt' found, then anything that comes between |
| // here and the next heading should be parsed as the desc. |
| var stability |
| if (state === 'AFTERHEADING') { |
| if (type === 'code' && |
| (stability = text.match(/^Stability: ([0-5])(?:\s*-\s*)?(.*)$/))) { |
| current.stability = parseInt(stability[1], 10); |
| current.stabilityText = stability[2].trim(); |
| return; |
| } else if (type === 'list_start' && !tok.ordered) { |
| state = 'AFTERHEADING_LIST'; |
| current.list = current.list || []; |
| current.list.push(tok); |
| current.list.level = 1; |
| } else { |
| current.desc = current.desc || []; |
| if (!Array.isArray(current.desc)) { |
| current.shortDesc = current.desc; |
| current.desc = []; |
| } |
| current.desc.push(tok); |
| state = 'DESC'; |
| } |
| return; |
| } |
| |
| if (state === 'AFTERHEADING_LIST') { |
| current.list.push(tok); |
| if (type === 'list_start') { |
| current.list.level++; |
| } else if (type === 'list_end') { |
| current.list.level--; |
| } |
| if (current.list.level === 0) { |
| state = 'AFTERHEADING'; |
| processList(current); |
| } |
| return; |
| } |
| |
| current.desc = current.desc || []; |
| current.desc.push(tok); |
| |
| }); |
| |
| // finish any sections left open |
| while (root !== (current = stack.pop())) { |
| finishSection(current, stack[stack.length - 1]); |
| } |
| |
| return cb(null, root) |
| } |
| |
| |
| // go from something like this: |
| // [ { type: 'list_item_start' }, |
| // { type: 'text', |
| // text: '`settings` Object, Optional' }, |
| // { type: 'list_start', ordered: false }, |
| // { type: 'list_item_start' }, |
| // { type: 'text', |
| // text: 'exec: String, file path to worker file. Default: `__filename`' }, |
| // { type: 'list_item_end' }, |
| // { type: 'list_item_start' }, |
| // { type: 'text', |
| // text: 'args: Array, string arguments passed to worker.' }, |
| // { type: 'text', |
| // text: 'Default: `process.argv.slice(2)`' }, |
| // { type: 'list_item_end' }, |
| // { type: 'list_item_start' }, |
| // { type: 'text', |
| // text: 'silent: Boolean, whether or not to send output to parent\'s stdio.' }, |
| // { type: 'text', text: 'Default: `false`' }, |
| // { type: 'space' }, |
| // { type: 'list_item_end' }, |
| // { type: 'list_end' }, |
| // { type: 'list_item_end' }, |
| // { type: 'list_end' } ] |
| // to something like: |
| // [ { name: 'settings', |
| // type: 'object', |
| // optional: true, |
| // settings: |
| // [ { name: 'exec', |
| // type: 'string', |
| // desc: 'file path to worker file', |
| // default: '__filename' }, |
| // { name: 'args', |
| // type: 'array', |
| // default: 'process.argv.slice(2)', |
| // desc: 'string arguments passed to worker.' }, |
| // { name: 'silent', |
| // type: 'boolean', |
| // desc: 'whether or not to send output to parent\'s stdio.', |
| // default: 'false' } ] } ] |
| |
| function processList(section) { |
| var list = section.list; |
| var values = []; |
| var current; |
| var stack = []; |
| |
| // for now, *just* build the heirarchical list |
| list.forEach(function(tok) { |
| var type = tok.type; |
| if (type === 'space') return; |
| if (type === 'list_item_start') { |
| if (!current) { |
| var n = {}; |
| values.push(n); |
| current = n; |
| } else { |
| current.options = current.options || []; |
| stack.push(current); |
| var n = {}; |
| current.options.push(n); |
| current = n; |
| } |
| return; |
| } else if (type === 'list_item_end') { |
| if (!current) { |
| throw new Error('invalid list - end without current item\n' + |
| JSON.stringify(tok) + '\n' + |
| JSON.stringify(list)); |
| } |
| current = stack.pop(); |
| } else if (type === 'text') { |
| if (!current) { |
| throw new Error('invalid list - text without current item\n' + |
| JSON.stringify(tok) + '\n' + |
| JSON.stringify(list)); |
| } |
| current.textRaw = current.textRaw || ''; |
| current.textRaw += tok.text + ' '; |
| } |
| }); |
| |
| // shove the name in there for properties, since they are always |
| // just going to be the value etc. |
| if (section.type === 'property' && values[0]) { |
| values[0].textRaw = '`' + section.name + '` ' + values[0].textRaw; |
| } |
| |
| // now pull the actual values out of the text bits. |
| values.forEach(parseListItem); |
| |
| // Now figure out what this list actually means. |
| // depending on the section type, the list could be different things. |
| |
| switch (section.type) { |
| case 'ctor': |
| case 'classMethod': |
| case 'method': |
| // each item is an argument, unless the name is 'return', |
| // in which case it's the return value. |
| section.signatures = section.signatures || []; |
| var sig = {} |
| section.signatures.push(sig); |
| sig.params = values.filter(function(v) { |
| if (v.name === 'return') { |
| sig.return = v; |
| return false; |
| } |
| return true; |
| }); |
| parseSignature(section.textRaw, sig); |
| break; |
| |
| case 'property': |
| // there should be only one item, which is the value. |
| // copy the data up to the section. |
| var value = values[0] || {}; |
| delete value.name; |
| section.typeof = value.type; |
| delete value.type; |
| Object.keys(value).forEach(function(k) { |
| section[k] = value[k]; |
| }); |
| break; |
| |
| case 'event': |
| // event: each item is an argument. |
| section.params = values; |
| break; |
| } |
| |
| // section.listParsed = values; |
| delete section.list; |
| } |
| |
| |
| // textRaw = "someobject.someMethod(a, [b=100], [c])" |
| function parseSignature(text, sig) { |
| var params = text.match(paramExpr); |
| if (!params) return; |
| params = params[1]; |
| // the ] is irrelevant. [ indicates optionalness. |
| params = params.replace(/\]/g, ''); |
| params = params.split(/,/) |
| params.forEach(function(p, i, _) { |
| p = p.trim(); |
| if (!p) return; |
| var param = sig.params[i]; |
| var optional = false; |
| var def; |
| // [foo] -> optional |
| if (p.charAt(0) === '[') { |
| optional = true; |
| p = p.substr(1); |
| } |
| var eq = p.indexOf('='); |
| if (eq !== -1) { |
| def = p.substr(eq + 1); |
| p = p.substr(0, eq); |
| } |
| if (!param) { |
| param = sig.params[i] = { name: p }; |
| } |
| // at this point, the name should match. |
| if (p !== param.name) { |
| console.error('Warning: invalid param "%s"', p); |
| console.error(' > ' + JSON.stringify(param)); |
| console.error(' > ' + text); |
| } |
| if (optional) param.optional = true; |
| if (def !== undefined) param.default = def; |
| }); |
| } |
| |
| |
| function parseListItem(item) { |
| if (item.options) item.options.forEach(parseListItem); |
| if (!item.textRaw) return; |
| |
| // the goal here is to find the name, type, default, and optional. |
| // anything left over is 'desc' |
| var text = item.textRaw.trim(); |
| // text = text.replace(/^(Argument|Param)s?\s*:?\s*/i, ''); |
| |
| text = text.replace(/^, /, '').trim(); |
| var retExpr = /^returns?\s*:?\s*/i; |
| var ret = text.match(retExpr); |
| if (ret) { |
| item.name = 'return'; |
| text = text.replace(retExpr, ''); |
| } else { |
| var nameExpr = /^['`"]?([^'`": \{]+)['`"]?\s*:?\s*/; |
| var name = text.match(nameExpr); |
| if (name) { |
| item.name = name[1]; |
| text = text.replace(nameExpr, ''); |
| } |
| } |
| |
| text = text.trim(); |
| var defaultExpr = /\(default\s*[:=]?\s*['"`]?([^, '"`]*)['"`]?\)/i; |
| var def = text.match(defaultExpr); |
| if (def) { |
| item.default = def[1]; |
| text = text.replace(defaultExpr, ''); |
| } |
| |
| text = text.trim(); |
| var typeExpr = /^\{([^\}]+)\}/; |
| var type = text.match(typeExpr); |
| if (type) { |
| item.type = type[1]; |
| text = text.replace(typeExpr, ''); |
| } |
| |
| text = text.trim(); |
| var optExpr = /^Optional\.|(?:, )?Optional$/; |
| var optional = text.match(optExpr); |
| if (optional) { |
| item.optional = true; |
| text = text.replace(optExpr, ''); |
| } |
| |
| text = text.replace(/^\s*-\s*/, ''); |
| text = text.trim(); |
| if (text) item.desc = text; |
| } |
| |
| |
| function finishSection(section, parent) { |
| if (!section || !parent) { |
| throw new Error('Invalid finishSection call\n'+ |
| JSON.stringify(section) + '\n' + |
| JSON.stringify(parent)); |
| } |
| |
| if (!section.type) { |
| section.type = 'module'; |
| if (parent && (parent.type === 'misc')) { |
| section.type = 'misc'; |
| } |
| section.displayName = section.name; |
| section.name = section.name.toLowerCase() |
| .trim().replace(/\s+/g, '_'); |
| } |
| |
| if (section.desc && Array.isArray(section.desc)) { |
| section.desc.links = section.desc.links || []; |
| section.desc = marked.parser(section.desc); |
| } |
| |
| if (!section.list) section.list = []; |
| processList(section); |
| |
| // classes sometimes have various 'ctor' children |
| // which are actually just descriptions of a constructor |
| // class signature. |
| // Merge them into the parent. |
| if (section.type === 'class' && section.ctors) { |
| section.signatures = section.signatures || []; |
| var sigs = section.signatures; |
| section.ctors.forEach(function(ctor) { |
| ctor.signatures = ctor.signatures || [{}]; |
| ctor.signatures.forEach(function(sig) { |
| sig.desc = ctor.desc; |
| }); |
| sigs.push.apply(sigs, ctor.signatures); |
| }); |
| delete section.ctors; |
| } |
| |
| // properties are a bit special. |
| // their "type" is the type of object, not "property" |
| if (section.properties) { |
| section.properties.forEach(function (p) { |
| if (p.typeof) p.type = p.typeof; |
| else delete p.type; |
| delete p.typeof; |
| }); |
| } |
| |
| // handle clones |
| if (section.clone) { |
| var clone = section.clone; |
| delete section.clone; |
| delete clone.clone; |
| deepCopy(section, clone); |
| finishSection(clone, parent); |
| } |
| |
| var plur; |
| if (section.type.slice(-1) === 's') { |
| plur = section.type + 'es'; |
| } else if (section.type.slice(-1) === 'y') { |
| plur = section.type.replace(/y$/, 'ies'); |
| } else { |
| plur = section.type + 's'; |
| } |
| |
| // if the parent's type is 'misc', then it's just a random |
| // collection of stuff, like the "globals" section. |
| // Make the children top-level items. |
| if (section.type === 'misc') { |
| Object.keys(section).forEach(function(k) { |
| switch (k) { |
| case 'textRaw': |
| case 'name': |
| case 'type': |
| case 'desc': |
| case 'miscs': |
| return; |
| default: |
| if (parent.type === 'misc') { |
| return; |
| } |
| if (Array.isArray(k) && parent[k]) { |
| parent[k] = parent[k].concat(section[k]); |
| } else if (!parent[k]) { |
| parent[k] = section[k]; |
| } else { |
| // parent already has, and it's not an array. |
| return; |
| } |
| } |
| }); |
| } |
| |
| parent[plur] = parent[plur] || []; |
| parent[plur].push(section); |
| } |
| |
| |
| // Not a general purpose deep copy. |
| // But sufficient for these basic things. |
| function deepCopy(src, dest) { |
| Object.keys(src).filter(function(k) { |
| return !dest.hasOwnProperty(k); |
| }).forEach(function(k) { |
| dest[k] = deepCopy_(src[k]); |
| }); |
| } |
| |
| function deepCopy_(src) { |
| if (!src) return src; |
| if (Array.isArray(src)) { |
| var c = new Array(src.length); |
| src.forEach(function(v, i) { |
| c[i] = deepCopy_(v); |
| }); |
| return c; |
| } |
| if (typeof src === 'object') { |
| var c = {}; |
| Object.keys(src).forEach(function(k) { |
| c[k] = deepCopy_(src[k]); |
| }); |
| return c; |
| } |
| return src; |
| } |
| |
| |
| // these parse out the contents of an H# tag |
| var eventExpr = /^Event(?::|\s)+['"]?([^"']+).*$/i; |
| var classExpr = /^Class:\s*([^ ]+).*?$/i; |
| var propExpr = /^(?:property:?\s*)?[^\.]+\.([^ \.\(\)]+)\s*?$/i; |
| var braceExpr = /^(?:property:?\s*)?[^\.\[]+(\[[^\]]+\])\s*?$/i; |
| var classMethExpr = |
| /^class\s*method\s*:?[^\.]+\.([^ \.\(\)]+)\([^\)]*\)\s*?$/i; |
| var methExpr = |
| /^(?:method:?\s*)?(?:[^\.]+\.)?([^ \.\(\)]+)\([^\)]*\)\s*?$/i; |
| var newExpr = /^new ([A-Z][a-z]+)\([^\)]*\)\s*?$/; |
| var paramExpr = /\((.*)\);?$/; |
| |
| function newSection(tok) { |
| var section = {}; |
| // infer the type from the text. |
| var text = section.textRaw = tok.text; |
| if (text.match(eventExpr)) { |
| section.type = 'event'; |
| section.name = text.replace(eventExpr, '$1'); |
| } else if (text.match(classExpr)) { |
| section.type = 'class'; |
| section.name = text.replace(classExpr, '$1'); |
| } else if (text.match(braceExpr)) { |
| section.type = 'property'; |
| section.name = text.replace(braceExpr, '$1'); |
| } else if (text.match(propExpr)) { |
| section.type = 'property'; |
| section.name = text.replace(propExpr, '$1'); |
| } else if (text.match(classMethExpr)) { |
| section.type = 'classMethod'; |
| section.name = text.replace(classMethExpr, '$1'); |
| } else if (text.match(methExpr)) { |
| section.type = 'method'; |
| section.name = text.replace(methExpr, '$1'); |
| } else if (text.match(newExpr)) { |
| section.type = 'ctor'; |
| section.name = text.replace(newExpr, '$1'); |
| } else { |
| section.name = text; |
| } |
| return section; |
| } |