arrow-docgen
Version: 
Arrow API Documentation Generator
263 lines (236 loc) • 7.16 kB
JavaScript
// jscs:disable jsDoc
var fs = require('fs'),
	path = require('path'),
	ejs = require('ejs'),
	fsextra = require('fs-extra'),
	async = require('async'),
	util = require('arrow-util'),
	_ = require('lodash'),
// in case the admin changes which requires a re-gen
	pkgHash = md5(JSON.stringify(require('./package.json')));
/**
 * create an <img> tag
 */
function image_tag(img, width, height) {
	var html = '<img src="images/' + img + '" ';
	if (width) {
		html += 'width="' + width + '" ';
	}
	if (height) {
		html += 'height="' + height + '" ';
	}
	html += '>';
	return html;
}
function md5(value) {
	return require('crypto').createHash('md5').update(value).digest('hex');
}
function generate(config, object, outdir, callback) {
	// check to see if the doc has already been generated from the same object model + config
	var key = md5(JSON.stringify(config) + JSON.stringify(object) + pkgHash);
	var cacheFile = path.join(outdir, '.generated');
	if (fs.existsSync(cacheFile)) {
		var cached = fs.readFileSync(cacheFile).toString();
		if (cached === key) {
			return callback();
		}
	}
	var baseurl = object.baseurl || ('http://127.0.0.1:' + object.server.port),
		source = path.join(__dirname, 'source'),
		adminurl = object.adminurl || (baseurl + object.config.admin.prefix),
		auth = object.server && object.server.auth,
		layoutEJS = ejs.compile(fs.readFileSync(path.join(__dirname, 'source', 'layouts', 'layout.ejs'), 'utf8')),
		sections = [
			{title: 'Overview', pages: []}
		],
		context = {
			apiCount: 0,
			modelCount: 0,
			blockCount: 0,
			connectorCount: 0,
			connectors: {},
			blocks: {}
		};
	var staticPages = [
		{url: 'introduction', title: 'Introduction', name: 'index'},
		{url: 'authentication', title: 'Authentication'},
		{url: 'content_formats', title: 'Content Formats'},
		{url: 'response', title: 'Response'},
		{url: 'errors', title: 'Errors'}
	];
	for (var i = 0; i < staticPages.length; i++) {
		var staticPage = staticPages[i];
		staticPage.markdown = fs.readFileSync(path.join(source, staticPage.url + '.md'), 'utf8');
		if (i === 0 && object.metadata.documentation) {
			// TODO: Place object.metadata.documentation on its own page.
			staticPage.markdown = object.metadata.documentation + '\n' + staticPage.markdown;
		}
		sections[0].pages.push(staticPage);
	}
	sections.push({
		url: 'apis',
		title: 'APIs',
		generator: require('./lib/apis').generate
	});
	// these are only available in development
	if (object.config.env === 'development') {
		sections.push({
			url: 'models',
			title: 'Models',
			generator: require('./lib/models').generate
		});
		sections.push({
			url: 'blocks',
			title: 'Blocks',
			generator: require('./lib/blocks').generate
		});
		sections.push({
			url: 'connectors',
			title: 'Connectors',
			generator: require('./lib/connectors').generate
		});
	}
	var replaceVars = _.merge(context, {
		ENDPOINT_URL: baseurl,
		ADMIN_URL: adminurl,
		AUTH_TYPE: auth && auth.type || 'none',
		AUTH_APIKEY_PRODUCTION: auth && auth.apikey_production || '',
		AUTH_APIKEY_DEVELOPMENT: auth && auth.apikey_development || '',
		AUTH_APIKEY: auth && auth.apikey || (auth && object.config.env === 'development' ? auth.apikey_development : auth.apikey_production),
		AUTH_USER: auth && auth.username,
		AUTH_PASS: auth && auth.password,
		ENV: object.config.env || ''
	});
	var pageConfig = _.merge({
		directory: 'outdir',
		value: replaceVars
	}, config);
	function makeTemplate(value) {
		if (_.isString(value)) {
			return value.split(',');
		}
		return value;
	}
	var pageCacheFn = path.join(outdir, '_pagecache.json'),
		pageCache = fs.existsSync(pageCacheFn) && JSON.parse(fs.readFileSync(pageCacheFn)) || {};
	async.each(sections,
		function eachSection(section, nextSection) {
			section.pages = section.pages || section.generator(object, baseurl, adminurl, context);
			var token = md5(JSON.stringify(section.pages) + key + pkgHash);
			if (section.url in pageCache) {
				var cache = pageCache[section.url];
				if (cache === token) {
					return nextSection();
				}
			}
			pageCache[section.url] = token;
			async.each(section.pages, function eachPage(page, nextPage) {
				var pageToken = md5(JSON.stringify(page) + key + pkgHash),
					name = (section.url ? section.url + '/' : '') + (page.name || page.url);
				if (name in pageCache) {
					var cache = pageCache[name];
					if (cache === pageToken) {
						return nextPage();
					}
				}
				pageCache[name] = pageToken;
				pageConfig.content = page.markdown;
				pageConfig.appendJS = makeTemplate(config.js || []).map(stripWebPublic);
				pageConfig.appendCSS = makeTemplate(config.css || []).map(stripWebPublic);
				var html = generateHTML(layoutEJS, pageConfig);
				var fn = path.join(outdir, name.toLowerCase() + '.html'),
					dir = path.dirname(fn);
				if (!fs.existsSync(dir)) {
					fsextra.mkdirsSync(dir);
				}
				delete page.markdown;
				fs.writeFile(fn, html, nextPage);
			}, nextSection);
		},
		function allDone(err) {
			if (err) {
				callback(err);
			} else {
				var menu = [];
				for (var i = 0; i < sections.length; i++) {
					var section = sections[i],
						pages = [];
					for (var j = 0; j < section.pages.length; j++) {
						var page = section.pages[j];
						pages.push({
							url: page.url,
							title: page.title
						});
					}
					// Sort all pages except for Overview's.
					if (i !== 0) {
						pages.sort(function (a, b) {
							var at = a.title,
								bt = b.title;
							var aHasSlash = at.indexOf('/') >= 0,
								bHasSlash = bt.indexOf('/') >= 0;
							if (aHasSlash === bHasSlash) {
								return compareStrings(at, bt);
							} else if (aHasSlash) {
								return 1;
							} else {
								return -1;
							}
						});
					}
					menu.push({
						url: section.url,
						title: section.title,
						pages: pages
					});
				}
				// write our page cache of content to hash
				fs.writeFileSync(pageCacheFn, JSON.stringify(pageCache));
				// write our cache file so we don't need to generate if no docs have changed
				fs.writeFileSync(cacheFile, key);
				fs.writeFile(path.join(outdir, 'menu.json'), JSON.stringify(menu), callback);
			}
		});
}
function compareStrings(a, b) {
	if (a < b) {
		return -1;
	}
	if (a > b) {
		return 1;
	}
	return 0;
}
function generateHTML(layoutEJS, options) {
	var page = _.merge({
		page_classes: [],
		title: 'API Documentation',
		search: true,
		image_tag: image_tag,
		toc_footers: []
	}, options);
	// convert markup
	var generateObj = util.content.generate(page.content, options.value);
	page.content = generateObj.markup;
	// pull out the languages we found
	page.languages = generateObj.languages;
	var args = _.merge(page, options.value);
	// generate the main page
	return layoutEJS(args);
}
function stripWebPublic(file) {
	if (file.indexOf('web/public') === 0) {
		return file.split('web/public').pop();
	} else {
		return file;
	}
}
if (module.id === '.') {
	// for testing
	var config = {},
		object = JSON.parse(fs.readFileSync('./test/fixtures/example.json').toString());
	generate(config, object, function (err, result) {
		console.log(result);
	});
}
exports.generate = generate;