var fs = require('fs'),
path = require('path'),
_ = require('underscore'),
YAML = require('yamljs'),
modules = require('./modules'),
helpers = require('./helpers');
/** @class KMDocInst */
function KMDocInst(options) {
this.options = _.extend({}, this.options, options);
this._head = [];
this._preprocess = [];
this._postprocess = [];
this.addStyle(this.options.componentsPath+'bootstrap/css/bootstrap.css');
this.addStyle(this.options.componentsPath+'kmdoc/assets/css/style.css');
//this.addStyle(this.options.componentsPath+'jquery-ui/themes/cupertino/jquery-ui.css');
this.addStyle(this.options.componentsPath+'kmdoc/assets/jquery-bootstrap/jquery-ui-1.9.2.custom.css');
this.addScript('https://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js');
this.addScript(this.options.componentsPath+'jquery-ui/ui/jquery-ui.js');
this.addScript(this.options.componentsPath+'underscore/underscore.js');
this.addScript(this.options.componentsPath+'kmdoc/assets/js/main.js');
}
_.extend(KMDocInst.prototype,
/** @lends KMDocInst# */
{
modules: modules,
helpers: helpers,
options: {
fileIn: '',
fileOut: '',
basename: '',
title: '',
lang: 'en',
encoding: 'utf-8',
baseUrl: '',
componentsPath: 'components/',
defTmpl: _.template('\n<div class="definition" id="<%= id %>"><dt><%= name %></dt><dd><%= definition %></dd></div>\n'),
fileTmpl: _.template('<!DOCTYPE html>\n<html lang="<%= options.lang %>">\n<head>\n <meta charset="<%= options.encoding %>"/>\n <title><%= options.title %></title><%= head %>\n</head>\n<body>\n\n<div class="navbar navbar-inverse navbar-fixed-top"><div class="navbar-inner"><a class="brand" href="#"><%= options.title %></a><div class="control-toolbar"></div></div></div>\n\n<%= content %>\n\n</body>\n</html>\n'),
defaultHelpers: ['trim']
},
/** Run building process */
build: function() {
// ensure filenames
if (!this.options.fileIn) {
this.options.fileIn = this._detectFilename();
}
if (!this.options.basename) {
this.options.basename = this.options.fileIn.replace(/\.md$/, '');
}
if (!this.options.fileOut) {
this.options.fileOut = this.options.basename + '.html';
}
this.load();
this._applyFns(this._preprocess);
// dictionary for unique indexes
this.defsIdx = {};
// parse definitions
var tmp = this.parse(this.input);
this.input = tmp.str;
this.definitions = tmp.definitions;
// include and save extracted definitions
fs.writeFileSync(this.options.basename + '-definitions.json', JSON.stringify(this.defsIdx));
this.addHead('<script>KMDoc.definitionsUrl = "' + (this.options.basename.match(/[^\/]+$/)[0]) + '-definitions.json";</script>');
// convert with markdown
this.output = this.options.fileTmpl({
options: this.options,
head: this._buildAssets(),
content: this._applyHelpers(this.input, ['md'])
});
this._applyFns(this._postprocess);
this.save();
},
/** Load file into memory */
load: function() {
this.input = fs.readFileSync(this.options.fileIn, this.options.encoding);
},
/** Save file */
save: function() {
fs.writeFileSync(this.options.fileOut, this.output);
},
/** Parse and extract definitions from input string
@param {string} str */
parse: function(str) {
var lines = str.split('\n'),
out = [],
headers = [],
defs = [];
for (var i = 0; i < lines.length; i+=1) {
var line = lines[i];
if (line.match(/^#/)) {
// line contains header, parse it and store it in the hierarchy
var n = line.match(/^#+/)[0].length-1;
var h = line.replace(/^\s+|\s+$/g, '').replace(/^#+/, '');
headers = headers.slice(0, n+1);
headers[n] = h;
headers = headers.filter(function(x) {return x});
out.push(line);
} else if (line.match(/^\s*:/)) {
// line contains definition
var def = {
name: out.pop(),
headers: headers
};
var buf = [line.replace(/^\s*:/, '')];
i += 1;
// load all lines until blank line to buffer
while (i < lines.length) {
line = lines[i];
if (line.match(/^\s*$/)) {
break;
} else {
buf.push(line);
}
i += 1;
}
// split buffer into definition body and properties
for (var j = 0; j < buf.length; j+=1) {
if (buf[j].match(/^[^ ]+:/)) {
// parse the properties with YAML
try {
_.extend(def, YAML.parse(buf.slice(j).join('\n')));
} catch (e) {
console.log('YAML error:', e);
}
buf = buf.slice(0,j);
break;
}
}
// handle and store definition
def.definition = buf.join('\n');
def = this.transformDef(def);
this._addToIndex(def);
defs.push(def);
// render definition to output
out.push(this.options.defTmpl(def));
} else {
// ordinary line, copy to output
out.push(line);
}
}
return {
str: out.join('\n'),
definitions: defs
};
},
/** Transform definition object
This function is applied to every definition after parsing. By default it applies attribute helpers and generates unique id.
@param {object} def - definition*/
transformDef: function(def) {
// apply helpers
for (var k in def) {
if (typeof def[k] !== 'string') {
continue;
}
var helpers = this._parseHelpers(k);
if (helpers.length > 1) {
def[helpers[0]] = this._applyHelpers(def[k], helpers.slice(1));
delete def[k];
} else if (this.options.defaultHelpers && this.options.defaultHelpers.length) {
def[k] = this._applyHelpers(def[k], this.options.defaultHelpers);
}
}
return def;
},
/** Use specified modules
Parameters can be either functions or strings */
use: function() {
var self = this;
_.each(arguments, function(mod) {
if (typeof mod === 'function') {
// function apply directly
mod.call(self);
} else {
if (typeof mod === 'string') {
var tmp = {};
tmp[mod] = {};
mod = tmp;
}
// support for options
_.each(mod, function(opts, m) {
if (m in self.modules) {
self.modules[m].call(self, opts);
}
});
}
});
return this;
},
/** Register preprocessing function
@param {function} fn */
preprocess: function(fn) {
this._preprocess.push(fn);
return this;
},
/** Register postprocessing function
@param {function} fn */
postprocess: function(fn) {
this._postprocess.push(fn);
return this;
},
/** Add stylesheet file into document
@param {string} filename */
addStyle: function(filename) {
this._head.push({type: 'style', value: filename});
return this;
},
/** Add script file into document
@param {string} filename */
addScript: function(filename) {
this._head.push({type: 'script', value: filename});
return this;
},
/** Add html code into head of document
@param {string} html */
addHead: function(html) {
this._head.push({type: 'raw', value: html});
return this;
},
/** Generates code of included assets
@returns {string}
@private */
_buildAssets: function() {
var self = this;
return this._head.map(function(x) {
switch (x.type) {
case 'style':
return '<link rel="stylesheet" href="'+self._url(x.value)+'"/>';
case 'script':
return '<script src="'+self._url(x.value)+'"></script>';
}
return x.value;
}).join('');
},
/** Automatic filename detection
@returns {string}
@private */
_detectFilename: function() {
var f;
// first check commnd line arguments
if (process.argv.length > 2 ) {
f = process.argv[2];
if (fs.existsSync(f) && fs.statSync(f).isFile()) {
return f;
}
}
// then look for markdown files in current directory
var files = fs.readdirSync(process.cwd());
files.sort();
for (var i = 0; i < files.length; i+=1) {
f = files[i];
if (f.match(/\.md$|\.markdown$/)) {
return f;
}
}
},
/** Adds baseUrl for non-absolute urls
@param {string} url
@returns {string}
@private */
_url: function(url) {
return url.match(/^https?:\/\//) ? url : this.options.baseUrl + url;
},
/** Apply list of functions
@param {array} fns
@private */
_applyFns: function(fns) {
var self = this;
fns.forEach(function(f) {
f.call(self);
});
},
/** Apply list of helpers to string
@param {string} str
@param {array} helpers
@returns {string}
@private */
_applyHelpers: function(str, helpers) {
var self = this;
helpers.forEach(function(h) {
if (h in self.helpers) {
str = self.helpers[h](str);
}
});
return str;
},
/** Parse attribute for occurence of helpers
@param {string} str
@returns {array}
@private */
_parseHelpers: function(str) {
return str.split('|');
},
/** Create unique ID and add to index */
_addToIndex: function(def) {
// generate unique id
var id = this.helpers.normalizeTag(def.name);
if (id in this.defsIdx) {
for (var j = 2; true; j+=1) {
if (!(id+'-'+j in this.defsIdx)) {
id = id+'-'+j;
break;
}
}
}
def.id = id;
this.defsIdx[id] = def;
}
});
/** @module KMDoc */
module.exports = {
/** Factory method to create instance */
create: function(options) {
return new KMDocInst(options);
},
template: _.template
};