@azu/docco
Version:
The Quick and Dirty Literate Programming Documentation Generator
482 lines (457 loc) • 16.5 kB
JavaScript
// Generated by CoffeeScript 1.12.4
(function() {
var Docco, _, buildMatchers, commander, configure, defaults, document, format, fs, getLanguage, highlightjs, languages, make_destination, marked, normalize, outputCode, parse, path, qualifiedName, run, version, write,
slice = [].slice;
document = function(options, user_callback) {
var callback, complete, config, copyAsset, files, nextFile, outputFiles, source_infos;
if (options == null) {
options = {};
}
config = configure(options);
source_infos = [];
fs.mkdirsSync(config.output);
if (config.source) {
fs.mkdirsSync(config.source);
}
callback = function(error) {
if (error) {
if (user_callback) {
user_callback(error);
}
throw error;
}
if (user_callback) {
return user_callback(null, {
source_infos: source_infos,
config: config
});
}
};
copyAsset = function(file, callback) {
if (!fs.existsSync(file)) {
return callback();
}
return fs.copy(file, path.join(config.output, path.basename(file)), callback);
};
complete = function() {
return copyAsset(config.css, function(error) {
if (error) {
return callback(error);
}
return copyAsset(config["public"], callback);
});
};
files = config.sources.slice();
nextFile = function() {
var source;
source = files.shift();
return fs.readFile(source, function(error, buffer) {
var code, first, firstSection, hasTitle, sections, title;
if (error) {
return callback(error);
}
code = buffer.toString();
sections = parse(source, code, config);
format(source, sections, config);
firstSection = _.find(sections, function(section) {
return section.docsText.length > 0;
});
if (firstSection) {
first = marked.lexer(firstSection.docsText)[0];
}
hasTitle = first && first.type === 'heading' && first.depth === 1;
title = hasTitle ? first.text : path.basename(source);
source_infos.push({
source: source,
hasTitle: hasTitle,
title: title,
sections: sections
});
if (files.length) {
return nextFile();
} else {
return outputFiles();
}
});
};
outputFiles = function() {
var i, info, j, len;
for (i = j = 0, len = source_infos.length; j < len; i = ++j) {
info = source_infos[i];
write(info.source, i, source_infos, config);
outputCode(info.source, info.sections, i, source_infos, config);
}
return complete();
};
if (files.length) {
return nextFile();
} else {
return outputFiles();
}
};
parse = function(source, code, config) {
var codeText, docsText, hasCode, i, ignore_this_block, in_block, isText, j, k, lang, len, len1, line, lines, match, maybeCode, oldLen, param, raw_line, save, sections, single;
if (config == null) {
config = {};
}
lines = code.split('\n');
sections = [];
lang = getLanguage(source, config);
hasCode = docsText = codeText = '';
param = '';
in_block = 0;
ignore_this_block = 0;
save = function() {
sections.push({
docsText: docsText,
codeText: codeText
});
return hasCode = docsText = codeText = '';
};
if (lang.literate && lang.name !== 'markdown') {
isText = maybeCode = true;
for (i = j = 0, len = lines.length; j < len; i = ++j) {
line = lines[i];
lines[i] = maybeCode && (match = /^([ ]{4}|[ ]{0,3}\t)/.exec(line)) ? (isText = false, line.slice(match[0].length)) : (maybeCode = /^\s*$/.test(line)) ? isText ? lang.symbol : '' : (isText = true, lang.symbol + ' ' + line);
}
}
for (k = 0, len1 = lines.length; k < len1; k++) {
line = lines[k];
if (in_block) {
++in_block;
}
raw_line = line;
if (!in_block && config.blocks && lang.blocks && line.match(lang.commentEnter)) {
line = line.replace(lang.commentEnter, '');
in_block = 1;
if (lang.commentIgnore && line.match(lang.commentIgnore)) {
ignore_this_block = 1;
}
}
single = !in_block && lang.commentMatcher && line.match(lang.commentMatcher) && !line.match(lang.commentFilter);
if (single) {
line = line.replace(lang.commentMatcher, '');
if (lang.commentIgnore && line.match(lang.commentIgnore)) {
ignore_this_block = 1;
}
}
if (in_block || single) {
if (in_block && line.match(lang.commentExit)) {
line = line.replace(lang.commentExit, '');
in_block = -1;
}
if (in_block > 1 && lang.commentNext) {
line = line.replace(lang.commentNext, '');
}
if (lang.commentParam) {
param = line.match(lang.commentParam);
if (param) {
line = line.replace(param[0], '\n' + '<b>' + param[1] + '</b>');
}
}
}
if (!ignore_this_block && (in_block || single)) {
if (hasCode) {
save();
}
docsText += line + '\n';
if (/^(---+|===+)$/.test(line || in_block === -1)) {
save();
}
} else {
hasCode = true;
if (config.indent) {
oldLen = 0;
while (oldLen !== line.length) {
oldLen = line.length;
line = line.replace(/^(\x20*)\t/, '$1' + config.indent);
}
}
codeText += line + '\n';
}
if (in_block === -1) {
in_block = 0;
}
if (!in_block) {
ignore_this_block = 0;
}
}
save();
return sections;
};
format = function(source, sections, config) {
var code, doc, err, i, j, k, language, len, len1, markedOptions, results, section;
language = getLanguage(source, config);
markedOptions = config.marked_options;
marked.setOptions(markedOptions);
marked.setOptions({
highlight: function(code, lang) {
lang || (lang = language.name);
if (highlightjs.getLanguage(lang)) {
return highlightjs.highlight(lang, code).value;
} else {
console.warn("docco: couldn't highlight code block with unknown language '" + lang + "' in " + source);
return code;
}
}
});
marked.setOptions({
linksCollector: {},
execPrepPhaseOnly: true
});
for (i = j = 0, len = sections.length; j < len; i = ++j) {
section = sections[i];
if (language.name === 'markdown') {
if (language.literate) {
code = section.codeText;
section.codeText = code = code.replace(/\s+$/, '');
section.codeHtml = marked(code);
doc = section.docsText;
section.docsText = doc = doc.replace(/\s+$/, '');
marked(doc);
} else {
section.codeHtml = '';
code = section.codeText;
section.codeText = code = code.replace(/\s+$/, '');
marked(code);
}
} else {
code = section.codeText;
section.codeText = code = code.replace(/\s+$/, '');
try {
code = highlightjs.highlight(language.name, code).value;
} catch (error1) {
err = error1;
if (!config.ignore) {
throw err;
}
code = section.codeText;
}
section.codeHtml = "<div class='highlight'><pre>" + code + "</pre></div>";
doc = section.docsText;
section.docsText = doc = doc.replace(/\s+$/, '');
marked(doc);
}
}
marked.setOptions({
execPrepPhaseOnly: false
});
results = [];
for (i = k = 0, len1 = sections.length; k < len1; i = ++k) {
section = sections[i];
if (language.name === 'markdown') {
if (language.literate) {
doc = section.docsText;
results.push(section.docsHtml = marked(doc));
} else {
code = section.codeText;
results.push(section.docsHtml = marked(code));
}
} else {
doc = section.docsText;
results.push(section.docsHtml = marked(doc));
}
}
return results;
};
write = function(source, title_idx, source_infos, config) {
var css, destfile, destination, html, relative;
destination = function(file) {
return make_destination(config.output, config.separator, file, '.html', config);
};
destfile = destination(source);
relative = function(srcfile) {
var dstfile, from, to;
to = path.dirname(path.resolve(srcfile));
dstfile = destination(srcfile);
from = path.dirname(path.resolve(dstfile));
return path.join(path.relative(from, to), path.basename(srcfile));
};
css = config.css ? relative(path.join(config.output, path.basename(config.css))) : null;
html = config.template({
sources: config.sources,
titles: source_infos.map(function(info) {
return info.title;
}),
css: css,
title: source_infos[title_idx].title,
hasTitle: source_infos[title_idx].hasTitle,
sections: source_infos[title_idx].sections,
path: path,
destination: destination,
relative: relative,
language: getLanguage(source, config)
});
console.log("docco: " + source + " -> " + destfile);
fs.mkdirsSync(path.dirname(destfile));
fs.writeFileSync(destfile, html);
return source_infos[title_idx].destDocFile = destfile;
};
outputCode = function(source, sections, title_idx, source_infos, config) {
var code, destfile, lang;
lang = getLanguage(source, config);
if (config.source) {
destfile = make_destination(config.source, config.separator, source, lang.source, config);
code = _.pluck(sections, 'codeText').join('\n');
code = code.trim().replace(/(\n{2,})/g, '\n\n');
console.log("docco: " + source + " -> " + destfile);
fs.mkdirsSync(path.dirname(destfile));
fs.writeFileSync(destfile, code);
return source_infos[title_idx].destCodeFile = destfile;
}
};
normalize = function(path) {
return path.replace(/[\\\/]/g, '/');
};
qualifiedName = function(file, separator, extension, config) {
var cwd, nameParts;
cwd = config && config.cwd ? config.cwd : process.cwd();
file = normalize(file);
nameParts = path.dirname(file).replace(normalize(cwd), '').split('/');
while (nameParts[0] === '' || nameParts[0] === '.' || nameParts[0] === '..') {
nameParts.shift();
}
nameParts.push(path.basename(file, path.extname(file)));
return nameParts.join(separator) + extension;
};
make_destination = function(basepath, separator, file, extension, config) {
return path.join(basepath, qualifiedName(file, separator, extension, config));
};
defaults = {
sources: [],
layout: 'parallel',
output: 'docs',
template: null,
css: null,
extension: null,
languages: {},
source: null,
cwd: process.cwd(),
separator: '-',
blocks: false,
marked_options: {
gfm: true,
tables: true,
breaks: false,
pedantic: false,
sanitize: false,
smartLists: true,
smartypants: true,
langPrefix: 'language-',
highlight: function(code, lang) {
return code;
}
},
ignore: false,
tabSize: null,
indent: null
};
configure = function(options) {
var config, dir;
config = _.extend({}, defaults, _.pick.apply(_, [options].concat(slice.call(_.keys(defaults)))));
config.languages = buildMatchers(config.languages);
if (config.tabSize) {
config.indent = Array(parseInt(config.tabSize) + 1).join(' ');
}
if (options.template) {
if (!options.css) {
console.warn("docco: no stylesheet file specified");
}
config.layout = null;
} else {
dir = config.layout = fs.existsSync(path.join(__dirname, 'resources', config.layout)) ? path.join(__dirname, 'resources', config.layout) : path.join(process.cwd(), config.layout);
if (fs.existsSync(path.join(dir, 'public'))) {
config["public"] = path.join(dir, 'public');
}
config.template = path.join(dir, 'docco.jst');
config.css = options.css || path.join(dir, 'docco.css');
}
config.template = _.template(fs.readFileSync(config.template).toString());
if (options.marked_options) {
config.marked_options = _.extend(config.marked_options, JSON.parse(fs.readFileSync(options.marked_options)));
}
if (options.args) {
config.sources = options.args.filter(function(source) {
var lang;
lang = getLanguage(source, config);
if (!lang) {
console.warn("docco: skipped unknown type (" + (path.basename(source)) + ")");
}
return lang;
}).sort();
}
return config;
};
_ = require('underscore');
fs = require('fs-extra');
path = require('path');
marked = require('marked');
commander = require('commander');
highlightjs = require('highlight.js');
languages = JSON.parse(fs.readFileSync(path.join(__dirname, 'resources', 'languages.json')));
buildMatchers = function(languages) {
var ext, l;
for (ext in languages) {
l = languages[ext];
if (l.symbol) {
l.commentMatcher = RegExp("^\\s*" + l.symbol + "\\s?");
}
if (l.enter && l.exit) {
l.blocks = true;
l.commentEnter = new RegExp(l.enter);
l.commentExit = new RegExp(l.exit);
if (l.next) {
l.commentNext = new RegExp(l.next);
}
}
if (l.param) {
l.commentParam = new RegExp(l.param);
}
l.commentFilter = /(^#![\/]|^\s*#\{)/;
l.commentIgnore = new RegExp(/^:/);
}
return languages;
};
languages = buildMatchers(languages);
getLanguage = function(source, config) {
var codeExt, codeLang, ext, lang, ref, ref1;
ext = config.extension || path.extname(source) || path.basename(source);
lang = ((ref = config.languages) != null ? ref[ext] : void 0) || languages[ext] || languages['text'];
if (lang) {
if (lang.name === 'markdown') {
codeExt = path.extname(path.basename(source, ext));
if (codeExt && (codeLang = ((ref1 = config.languages) != null ? ref1[codeExt] : void 0) || languages[codeExt] || languages['text'])) {
lang = _.extend({}, codeLang, {
literate: true,
source: ''
});
}
} else if (!lang.source) {
lang.source = ext;
}
}
return lang;
};
version = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'))).version;
run = function(args) {
var c;
if (args == null) {
args = process.argv;
}
c = defaults;
commander.version(version).usage('[options] files').option('-L, --languages [file]', 'use a custom languages.json', _.compose(JSON.parse, fs.readFileSync)).option('-l, --layout [name]', 'choose a layout (parallel, linear, pretty or classic) or external layout', c.layout).option('-o, --output [path]', 'output to a given folder', c.output).option('-c, --css [file]', 'use a custom css file', c.css).option('-t, --template [file]', 'use a custom .jst template', c.template).option('-b, --blocks', 'parse block comments where available', c.blocks).option('-e, --extension [ext]', 'assume a file extension for all inputs', c.extension).option('-s, --source [path]', 'output code in a given folder', c.source).option('--cwd [path]', 'specify the Current Working Directory path for the purpose of generating qualified output filenames', c.cwd).option('-x, --separator [sep]', 'the source path is included in the output filename, separated by this separator (default: "-")', c.separator).option('-m, --marked-options [file]', 'use custom Marked options', c.marked_options).option('-i, --ignore [file]', 'ignore unsupported languages', c.ignore).option('-T, --tab-size [size]', 'convert leading tabs to X spaces').parse(args).name = "docco";
if (commander.args.length) {
return document(commander);
} else {
return console.log(commander.helpInformation());
}
};
Docco = module.exports = {
run: run,
document: document,
parse: parse,
format: format,
configure: configure,
version: version
};
}).call(this);