'use strict'; var fs = require('fs'); var template = require('./template.js'); var currentObj; var processFile = function(filename) { var contents = fs.readFileSync(filename, 'utf8'); return processFileContents(contents); }; var processFileContents = function(contents) { var fragments = contents.split(/^----+$/m); var obj = processObject(fragments.shift()); if (fragments.length) { fragments.forEach(function(member) { processFragment(obj, member); }); // combine instance and prototype properties and methods into a common array obj.instanceMethods = []; obj.instanceProperties = []; obj.instanceIndexers = []; var placeMember = function(m) { var place = obj.instanceProperties; if (m.type === 'Function') { place = obj.instanceMethods; } else if (m.type === 'Indexer') { place = obj.instanceIndexers; } place.push(m); }; obj.instanceMembers.forEach(placeMember); obj.prototypeMembers.forEach(placeMember); obj.methods = []; obj.properties = []; obj.members.forEach(function(m) { (m.type === 'Function' ? obj.methods : obj.properties).push(m); }); var nameComparison = function(m1, m2) { if (!m1.name) { console.log('ERROR: no name for ', m1); throw Error('ERROR: no name for ' + m1); } var comparison = m1.name.toLowerCase().localeCompare(m2.name.toLowerCase()); return comparison; }; var parametersComparison = function(params1, params2) { var comparison = params1.length - params2.length; if (comparison === 0) { // Parameters lengths are the same, sort by parameter type names for (var i = 0; i < params1.length; i++) { var p1 = params1[i]; var p2 = params2[i]; if (Array.isArray(p1) && !Array.isArray(p2)) { comparison = -1; } else if (!Array.isArray(p1) && Array.isArray(p2)) { comparison = 1; } else if (Array.isArray(p1) && Array.isArray(p2)) { comparison = parametersComparison(p1, p2); } else { comparison = p1.name.localeCompare(p2.name); } if (comparison !== 0) { break; } } } return comparison; }; var functionComparison = function(f1, f2) { var comparison = nameComparison(f1, f2); if (comparison === 0) { // Names are the same, sort by parameters comparison = parametersComparison(f1.parameters, f2.parameters); } return comparison; }; obj.instanceMethods.sort(functionComparison); obj.instanceProperties.sort(nameComparison); obj.methods.sort(functionComparison); obj.properties.sort(nameComparison); var join = function(members) { var joined = []; //console.log('members.length = ' + members.length); var prev; for (var i = 0; i < members.length; i++) { var member = members[i]; if (prev && member.name === prev.name) { if (Array.isArray(prev)) { prev.push(member); } else { joined.pop(); prev = [prev, member]; prev.name = member.name; joined.push(prev); } } else { joined.push(member); prev = member; } } return joined; }; obj.instanceMethods = join(obj.instanceMethods); obj.methods = join(obj.methods); } else { obj.constructors = []; obj.instanceMethods = []; obj.instanceProperties = []; obj.instanceIndexers = []; obj.methods = []; obj.properties = []; } return obj; }; // TODO: merge with processFragment? var processObject = function(fragment) { var lines = fragment.split('\n'); var obj = parseMember(lines.shift()); currentObj = obj; processMetadata(obj, lines); obj.overloads = []; // Parameter overloads if Obj is a Function obj.constructors = []; // Constructor overloads if Obj is a Constructor Function obj.members = []; // Obj members obj.instanceMembers = []; // Instance members obj.instanceEvents = []; // Instance events obj.prototypeMembers = []; // Prototype members return obj; }; var processInstanceMember = function(obj, frag) { frag = frag.replace(/^instance\.?/, ''); var member = parseMember(frag); obj.instanceMembers.push(member); member.source = 'instance'; return member; }; var processInstanceEvent = function(obj, frag) { frag = frag.replace(/^event\./, ''); var member = parseMember(frag); member.onname = member.onname || 'on' + member.name; obj.instanceEvents.push(member); member.source = 'event'; return member; }; var processPrototypeMember = function(obj, frag) { frag = frag.replace(/^prototype\./, ''); var member = parseMember(frag); obj.prototypeMembers.push(member); member.source = 'prototype'; return member; }; var processConstructor = function(obj, frag) { frag = frag.replace(/^new /, ''); var member = parseMember(frag); obj.constructors.push(member); member.isConstructor = true; return member; }; var processOverload = function(obj, frag) { var member = parseMember(frag); obj.overloads.push(member); return member; }; var processMember = function(obj, frag) { var member = parseMember(frag); obj.members.push(member); return member; }; var processFragment = function(obj, desc) { var lines = desc.trim().split('\n'); var first = lines.shift(); var trailingBackslash = /\\\s*$/; while (trailingBackslash.test(first)) { first = first.replace(trailingBackslash, ' '); first += lines.shift(); } if (first === '' && lines.length === 0) { return; } var member; if (/^instance(\.|\[)/.test(first)) { member = processInstanceMember(obj, first); } else if (/^event\./.test(first)) { member = processInstanceEvent(obj, first); } else if (/^prototype\./.test(first)) { member = processPrototypeMember(obj, first); } else if (/^new /.test(first)) { member = processConstructor(obj, first); } else if (new RegExp('^'+obj.name).test(first)) { member = processOverload(obj, first); } else if (/^Details:\s*$/.test(first)) { obj.details = processMarkup(obj, lines.join('\n').trim()); return; } else { member = processMember(obj, first); } if (member) { processMetadata(member, lines); } }; var processMetadata = function(member, lines) { var metadataProcessors = { description: processMarkup, }; // Process 'Label:' lines var prop = 'description'; for (var i = 0; lines.length > 0 && i <= lines.length; i++) { var res; if (i === lines.length || (res = /^(\w+):\s*$/.exec(lines[i]))) { var value = lines.splice(0, i).join('\n').trim(); if (prop in metadataProcessors) { value = metadataProcessors[prop](member, value); } member[prop] = value; lines.shift(); // pull off this line ('ReadOnly:') prop = res ? res[1].toLowerCase() : ''; // set the property value i = 0; } } }; var createHtml = function(html) { html = replaceLinks(html); html = replaceCode(html); return html; }; var createTag = function(html, obj, tag) { html = replaceLinks(html); html = replaceCode(html); return '<' + tag + '>' + html + ''; }; var createHtmlExample = function(code, obj) { obj.hasExample = true; return template.render('htmlexample', { code: code, objectName: currentObj.name, propertyName: obj.name }); }; var createExample = function(code, obj) { obj.hasExample = true; return template.render('example', { code: code, objectName: currentObj.name, propertyName: obj.name }); }; var tags = { 'html': createHtml, 'h3': createTag, 'p': createTag, 'em': createTag, 'code': createTag, 'htmlexample': createHtmlExample, 'example': createExample, }; var processMarkup = function(obj, desc) { var res = ''; var parts; var rest = desc; var regexp = RegExp('([\\s\\S]*?)\\<(' + Object.keys(tags).join('|') + ')\\>([\\s\\S]*?)\\<\\/\\2\\>([\\s\\S]*)', 'm'); while (parts = rest.match(regexp)) { var before = parts[1]; var tag = parts[2]; var content = parts[3]; var converter = tags[tag]; res += linkify(before); res += converter(content, obj, tag); rest = parts[4]; } res += linkify(rest); return res; }; var htmlEscape = function(str) { str = str.replace(/&/g, '&'); str = str.replace(//g, '>'); return str; }; var replaceLinks = function(str) { return str.replace(/%%(.*?)(?:\|(.*?))%%/g, function(str, p1, p2) { p2 = p2 || p1; return "" + p2 + ""; }); }; var replaceCode = function(str) { return str.replace(/\*\*(.*?)\*\*/gm, function(str, p1) { return "" + p1 + ""; }); }; var linkify = function(description) { var res = description; res = res.replace(/\n/g, ' '); res = htmlEscape(res); res = replaceLinks(res); res = replaceCode(res); return res; }; var parseMember = function(member) { member = member.trim(); if (member === '...') { return { name: '...'}; } if (!member.match(/:/)) { var res; // See if member has a if (res = member.match(/\s*(\w*)\s*\<(.*)\>/)) { name = res[1]; return { name: name, templates: parseParamList(res[2]) }; } return member; } var split = -1; var depth = 0; var defaultValueSplit = -1; var inComment = false; var parenStack = []; var parens = [['(', ')'], ['{', '}'], ['[', ']'], ['<', '>']]; for (var i = 0; i < member.length; i++) { if (inComment) { if (member[i] === '*' && member[i + 1] === '/') { inComment = false; } continue; } // See if the character is one of the parens for (var j = 0; j < parens.length; j++) { var pair = parens[j]; // If it is a left paren, push it on the stack if (member[i] === pair[0]) { parenStack.push(member[i]); } else if (member[i] === pair[1]) { // If it is a right paren, see if it matches the top paren of on the stack if (!parenStack.length) { throw Error('empty paren stack'); } if (parenStack[parenStack.length - 1] !== pair[0]) { throw Error('Mismatched parens. Expected: \'' + pair[0] + '\' but got \'' + parenStack[parenStack.length - 1] + '\'. (processing ' + member + ')'); } parenStack.pop(); } } if (member[i] === '/' && member[i + 1] === '*') inComment = true; if (member[i] === '=' && parenStack.length === 0) { defaultValueSplit = i; } if (member[i] === ':' && parenStack.length === 0) { split = i; break; } } var name; var defaultValue; if (defaultValueSplit !== -1) { name = member.substring(0, defaultValueSplit).trim(); defaultValue = member.substring(defaultValueSplit + 1, split).trim(); } else if (split === -1) { var res = member.match(/\{(.*)\}/); if (!res) throw Error('parse error'); return { type: 'Object', properties: parseParamList(res[1])}; } else { if (split === -1) throw Error('parse error'); name = member.substring(0, split).trim(); } var type = member.substring(split + 1).trim(); var description; var res; if (type.length > 2 && type.lastIndexOf('*/') === type.length - 2) { var commentStartIndex = type.lastIndexOf('/*'); description = processMarkup(currentObj, type.substring(commentStartIndex + 2, type.length - 2).trim()); type = type.substring(0, commentStartIndex).trim(); } // Process Indexer if (res = name.match(/^\s*\[(.*)\]/)) { return { parameters: parseParamList(res[1]), type: 'Indexer', returnType: parseMember(type), description: description }; } // Process method if (res = name.match(/^\s*(\w*)\s*\((.*)\)/)) { name = res[1]; return { name: name, parameters: parseParamList(res[2]), type: 'Function', returnType: parseMember(type), defaultValue: defaultValue, description: description }; } if (res = type.match(/\s*\{(.*)\}/)) { return { name: name, type: 'Object', properties: parseParamList(res[1]), description: description }; } return { name: name, type: parseMember(type), defaultValue: defaultValue, description: description }; }; //jsdoc.parseMember('a(b : c, [..., [f : g]]) : h'); var parseParamList = function(params) { var split = -1; var paramsList = []; var depth = 0; var optionalDepth = 0; var inQuote = false; var inComment = false; for (var i = 0; i < params.length; i++) { if (inComment) { if (params[i] === '*' && params[i + 1] === '/') inComment = false; continue; } if (params[i] === '(') depth++; if (params[i] === ')') depth--; if (params[i] === '{') depth++; if (params[i] === '}') depth--; if (params[i] === '<') depth++; if (params[i] === '>') depth--; if (params[i] === '/' && params[i + 1] === '*') inComment = true; if (params[i] === '\'') inQuote=!inQuote; if (params[i] === '[') { optionalDepth++; split = i; } //if (params[i] === ']') optionalDepth--; if ((params[i] === ',' || params[i] === ']') && depth === 0 && !inQuote) { var param = params.substring(split + 1, i); // if there are no 'word' characters, ignore this substring. // This can happen if you have ], if (param.match(/\w/) || param.match(/\.\.\./)) { paramsList.push({param: param, optionalDepth: optionalDepth}); } split = i; } if (params[i] === ']') optionalDepth--; } // Add the last one var param = params.substring(split + 1); if (param.match(/\w/) || param.match(/\.\.\./)) { paramsList.push({param: param, optionalDepth: optionalDepth}); } params = paramsList.map(function(m) { m.param = m.param.replace(/\[/g, ''); m.param = m.param.replace(/\]/g, ''); var member = parseMember(m.param); if (typeof member !== 'string') { // When parsing templates (like Array), the param may not have a // name and be of type string, which can't have properties set on it. member.optionalDepth = m.optionalDepth; } return member; }); // Group the params var grouped = [[]]; while (params.length) { var param = params.shift(); if (param.optionalDepth > grouped.length - 1) { grouped.unshift([]); } while (param.optionalDepth < grouped.length - 1) { var finishedGroup = grouped.shift(); grouped[0].push(finishedGroup); } grouped[0].push(param); delete param.optionalDepth; } while (grouped.length > 1) { var finishedGroup = grouped.shift(); grouped[0].push(finishedGroup); } return grouped[0]; }; exports.processFile = processFile; exports.processFileContents = processFileContents; if (false) { // Better regexp syntax? var notQuoteOrBackslash = zeroOrMore(anyCharBut("\"\\")); notQuoteOrBackslash.then( zeroOrMore( "\\.".then(notQuoteOrBackSlash) )); } exports.parseMember = parseMember;