kitchensink
Version:
Dispatch's awesome components and style guide
353 lines (282 loc) • 8.68 kB
JavaScript
var RE_COMMENT_START = /^\s*\/\*\*\s*$/m;
var RE_COMMENT_LINE = /^\s*\*(?:\s|$)/m;
var RE_COMMENT_END = /^\s*\*\/\s*$/m;
var RE_COMMENT_1LINE = /^\s*\/\*\*\s*(.*)\s*\*\/\s*$/;
/* ------- util functions ------- */
function merge(/* ...objects */) {
var k, obj, res = {}, objs = Array.prototype.slice.call(arguments);
while (objs.length) {
obj = objs.shift();
for (k in obj) { if (obj.hasOwnProperty(k)) {
res[k] = obj[k];
}}
}
return res;
}
function find(list, filter) {
var k, i = list.length, matchs = true;
while (i--) {
for (k in filter) { if (filter.hasOwnProperty(k)) {
matchs = (filter[k] === list[i][k]) && matchs;
}}
if (matchs) { return list[i]; }
}
return null;
}
function skipws(str) {
var i = 0;
do {
if (str[i] !== ' ') { return i; }
} while (++i < str.length);
return i;
}
/* ------- default parsers ------- */
var PARSERS = {};
PARSERS.parse_tag = function parse_tag(str) {
var result = str.match(/^\s*@(\S+)/);
if (!result) { throw new Error('Invalid `@tag`, missing @ symbol'); }
return {
source : result[0],
data : {tag: result[1]}
};
};
PARSERS.parse_type = function parse_type(str, data) {
if (data.errors && data.errors.length) { return null; }
var pos = skipws(str);
var res = '';
var curlies = 0;
if (str[pos] !== '{') { return null; }
while (pos < str.length) {
curlies += (str[pos] === '{' ? 1 : (str[pos] === '}' ? -1 : 0));
res += str[pos];
pos ++;
if (curlies === 0) { break; }
}
if (curlies !== 0) { throw new Error('Invalid `{type}`, unpaired curlies'); }
return {
source : str.slice(0, pos),
data : {type: res.slice(1, -1)}
};
};
PARSERS.parse_name = function parse_name(str, data) {
if (data.errors && data.errors.length) { return null; }
var pos = skipws(str);
var name = '';
var brackets = 0;
while (pos < str.length) {
brackets += (str[pos] === '[' ? 1 : (str[pos] === ']' ? -1 : 0));
name += str[pos];
pos ++;
if (brackets === 0 && /\s/.test(str[pos])) { break; }
}
if (brackets !== 0) { throw new Error('Invalid `name`, unpaired brackets'); }
var res = {name: name, optional: false};
if (name[0] === '[' && name[name.length - 1] === ']') {
res.optional = true;
name = name.slice(1, -1);
if (name.indexOf('=') !== -1) {
var parts = name.split('=');
name = parts[0];
res.default = parts[1].replace(/^(["'])(.+)(\1)$/, '$2');
}
}
res.name = name;
return {
source : str.slice(0, pos),
data : res
};
};
PARSERS.parse_description = function parse_description(str, data) {
if (data.errors && data.errors.length) { return null; }
var result = str.match(/^\s+([^$]+)?/);
if (result) {
return {
source : result[0],
data : {description: result[1] === undefined ? '' : result[1]}
};
}
return null;
};
/* ------- parsing ------- */
/**
* Parses "@tag {type} name description"
* @param {string} str Raw doc string
* @param {Array[function]} parsers Array of parsers to be applied to the source
* @returns {object} parsed tag node
*/
function parse_tag(str, parsers) {
if (typeof str !== 'string' || str[0] !== '@') { return null; }
var data = parsers.reduce(function(state, parser) {
var result;
try {
result = parser(state.source, merge({}, state.data));
// console.log('----------------');
// console.log(parser.name, ':', result);
} catch (err) {
// console.warn('Parser "%s" failed: %s', parser.name, err.message);
state.data.errors = (state.data.errors || [])
.concat(parser.name + ': ' + err.message);
}
if (result) {
state.source = state.source.slice(result.source.length);
state.data = merge(state.data, result.data);
}
return state;
}, {
source : str,
data : {}
}).data;
data.optional = !!data.optional;
data.type = data.type === undefined ? '' : data.type;
data.name = data.name === undefined ? '' : data.name;
data.description = data.description === undefined ? '' : data.description;
return data;
}
/**
* Parses comment block (array of String lines)
*/
function parse_block(source, opts) {
var source_str = source
.map(function(line) { return line.source; })
.join('\n')
.trim();
var start = source[0].number;
// merge source lines into tags
// we assume tag starts with "@"
source = source
.reduce(function(tags, line) {
line.source = line.source.trim();
if (line.source.match(/^@(\w+)/)) {
tags.push({source: [line.source], line: line.number});
} else {
var tag = tags[tags.length - 1];
tag.source.push(line.source);
}
return tags;
}, [{source: []}])
.map(function(tag) {
tag.source = tag.source.join('\n').trim();
return tag;
});
// Block description
var description = source.shift();
// skip if no descriptions and no tags
if (description.source === '' && source.length === 0) {
return null;
}
var tags = source.reduce(function(tags, tag) {
var tag_node = parse_tag(tag.source, opts.parsers || [
PARSERS.parse_tag,
PARSERS.parse_type,
PARSERS.parse_name,
PARSERS.parse_description
]);
if (!tag_node) { return tags; }
tag_node.line = tag.line;
tag_node.source = tag.source;
if (opts.dotted_names && tag_node.name.indexOf('.') !== -1) {
var parent_name;
var parent_tag;
var parent_tags = tags;
var parts = tag_node.name.split('.');
while (parts.length > 1) {
parent_name = parts.shift();
parent_tag = find(parent_tags, {
tag : tag_node.tag,
name : parent_name
});
if (!parent_tag) {
parent_tag = {
tag : tag_node.tag,
line : Number(tag_node.line),
name : parent_name,
type : '',
description : ''
};
parent_tags.push(parent_tag);
}
parent_tag.tags = parent_tag.tags || [];
parent_tags = parent_tag.tags;
}
tag_node.name = parts[0];
parent_tags.push(tag_node);
return tags;
}
return tags.concat(tag_node);
}, []);
// console.log('-----------');
// console.log(description, tags);
return {
tags : tags,
line : start,
description : description.source,
source : source_str
};
}
/**
* Produces `extract` function with internal state initialized
*/
function mkextract(opts) {
var chunk = null;
var number = 0;
/**
* Cumulatively reading lines until they make one comment block
* Returns block object or null.
*/
return function extract(line) {
// if oneliner
// then parse it immediately
if (line.match(RE_COMMENT_1LINE)) {
// console.log('line (1)', line);
// console.log(' clean:', line.replace(RE_COMMENT_1LINE, '$1'));
return parse_block([{
source: line.replace(RE_COMMENT_1LINE, '$1'),
number: number}], opts);
}
number += 1;
// if start of comment
// then init the chunk
if (line.match(RE_COMMENT_START)) {
// console.log('line (1)', line);
// console.log(' clean:', line.replace(RE_COMMENT_START, ''));
chunk = [{source: line.replace(RE_COMMENT_START, ''), number: number - 1}];
return null;
}
// if comment line and chunk started
// then append
if (chunk && line.match(RE_COMMENT_LINE)) {
// console.log('line (2)', line);
// console.log(' clean:', line.replace(RE_COMMENT_LINE, ''));
chunk.push({source: line.replace(RE_COMMENT_LINE, ''), number: number - 1});
return null;
}
// if comment end and chunk started
// then parse the chunk and push
if (chunk && line.match(RE_COMMENT_END)) {
// console.log('line (3)', line);
// console.log(' clean:', line.replace(RE_COMMENT_END, ''));
chunk.push({source: line.replace(RE_COMMENT_END, ''), number: number - 1});
return parse_block(chunk, opts);
}
// if non-comment line
// then reset the chunk
chunk = null;
};
}
/* ------- Public API ------- */
module.exports = function parse(source, opts) {
opts = opts || {};
var block;
var blocks = [];
var extract = mkextract(opts);
var lines = source.split(/\n/);
while (lines.length) {
block = extract(lines.shift());
if (block) {
blocks.push(block);
}
}
return blocks;
};
module.exports.PARSERS = PARSERS;
module.exports.mkextract = mkextract;