aglio-theme-goose
Version:
Custom theme for the Aglio API Blueprint renderer
780 lines (751 loc) • 28.9 kB
JavaScript
// Generated by CoffeeScript 2.2.1
(function() {
var ROOT, benchmark, cache, compileTemplate, crypto, decorate, errMsg, fs, getCached, getCss, getTemplate, highlight, hljs, less, markdownIt, modifyUriTemplate, moment, path, pug, querystring, renderExample, renderSchema, sha1, slug;
crypto = require('crypto');
fs = require('fs');
hljs = require('highlight.js');
pug = require('pug');
less = require('less');
markdownIt = require('markdown-it');
moment = require('moment');
path = require('path');
querystring = require('querystring');
renderExample = require('./example');
renderSchema = require('./schema');
// The root directory of this project
ROOT = path.dirname(__dirname);
cache = {};
// Utility for benchmarking
benchmark = {
start: function(message) {
if (process.env.BENCHMARK) {
return console.time(message);
}
},
end: function(message) {
if (process.env.BENCHMARK) {
return console.timeEnd(message);
}
}
};
// Extend an error's message. Returns the modified error.
errMsg = function(message, err) {
err.message = `${message}: ${err.message}`;
return err;
};
// Generate a SHA1 hash
sha1 = function(value) {
return crypto.createHash('sha1').update(value.toString()).digest('hex');
};
// A function to create ID-safe slugs. If `unique` is passed, then
// unique slugs are returned for the same input. The cache is just
// a plain object where the keys are the sluggified name.
slug = function(cache = {}, value = '', unique = false) {
var sluggified;
sluggified = value.toLowerCase().replace(/[ \t\n\\<>"'=:\/]/g, '-').replace(/-+/g, '-').replace(/^-/, '');
if (unique) {
while (cache[sluggified]) {
// Already exists, so let's try to make it unique.
if (sluggified.match(/\d+$/)) {
sluggified = sluggified.replace(/\d+$/, function(value) {
return parseInt(value) + 1;
});
} else {
sluggified = sluggified + '-1';
}
}
}
cache[sluggified] = true;
return sluggified;
};
// A function to highlight snippets of code. lang is optional and
// if given, is used to set the code language. If lang is no-highlight
// then no highlighting is performed.
highlight = function(code, lang, subset) {
var response;
benchmark.start(`highlight ${lang}`);
response = (function() {
switch (lang) {
case 'no-highlight':
return code;
case void 0:
case null:
case '':
return hljs.highlightAuto(code, subset).value;
default:
return hljs.highlight(lang, code).value;
}
})();
benchmark.end(`highlight ${lang}`);
return response.trim();
};
getCached = function(key, compiledPath, sources, load, done) {
var compiledStats, err, i, len, loadErr, source, sourceStats;
// Disable the template/css caching?
if (process.env.NOCACHE) {
return done(null);
}
// Already loaded? Just return it!
if (cache[key]) {
return done(null, cache[key]);
}
try {
// Next, try to check if the compiled path exists and is newer than all of
// the sources. If so, load the compiled path into the in-memory cache.
if (fs.existsSync(compiledPath)) {
compiledStats = fs.statSync(compiledPath);
for (i = 0, len = sources.length; i < len; i++) {
source = sources[i];
sourceStats = fs.statSync(source);
if (sourceStats.mtime > compiledStats.mtime) {
// There is a newer source file, so we ignore the compiled
// version on disk. It'll be regenerated later.
return done(null);
}
}
try {
return load(compiledPath, function(err, item) {
if (err) {
return done(errMsg('Error loading cached resource', err));
}
cache[key] = item;
return done(null, cache[key]);
});
} catch (error) {
loadErr = error;
return done(errMsg('Error loading cached resource', loadErr));
}
} else {
return done(null);
}
} catch (error) {
err = error;
return done(err);
}
};
getCss = function(variables, styles, verbose, done) {
var compiledPath, customPath, defaultVariablePath, i, item, j, key, len, len1, load, sources, stylePaths, variablePaths;
// Get the CSS for the given variables and style. This method caches
// its output, so subsequent calls will be extremely fast but will
// not reload potentially changed data from disk.
// The CSS is generated via a dummy LESS file with imports to the
// default variables, any custom override variables, and the given
// layout style. Both variables and style support special values,
// for example `flatly` might load `styles/variables-flatly.less`.
// See the `styles` directory for available options.
key = `css-${variables}-${styles}`;
if (cache[key]) {
return done(null, cache[key]);
}
// Not cached in memory, but maybe it's already compiled on disk?
compiledPath = path.join(ROOT, 'cache', `${sha1(key)}.css`);
defaultVariablePath = path.join(ROOT, 'styles', 'variables-default.less');
sources = [defaultVariablePath];
if (!Array.isArray(variables)) {
variables = [variables];
}
if (!Array.isArray(styles)) {
styles = [styles];
}
variablePaths = [defaultVariablePath];
for (i = 0, len = variables.length; i < len; i++) {
item = variables[i];
if (item !== 'default') {
customPath = path.join(ROOT, 'styles', `variables-${item}.less`);
if (!fs.existsSync(customPath)) {
customPath = item;
if (!fs.existsSync(customPath)) {
return done(new Error(`${customPath} does not exist!`));
}
}
variablePaths.push(customPath);
sources.push(customPath);
}
}
stylePaths = [];
for (j = 0, len1 = styles.length; j < len1; j++) {
item = styles[j];
customPath = path.join(ROOT, 'styles', `layout-${item}.less`);
if (!fs.existsSync(customPath)) {
customPath = item;
if (!fs.existsSync(customPath)) {
return done(new Error(`${customPath} does not exist!`));
}
}
stylePaths.push(customPath);
sources.push(customPath);
}
load = function(filename, loadDone) {
return fs.readFile(filename, 'utf-8', loadDone);
};
if (verbose) {
console.log(`Using variables ${variablePaths}`);
console.log(`Using styles ${stylePaths}`);
console.log(`Checking cache ${compiledPath}`);
}
return getCached(key, compiledPath, sources, load, function(err, css) {
var k, l, len2, len3, tmp;
if (err) {
return done(err);
}
if (css) {
if (verbose) {
console.log('Cached version loaded');
}
return done(null, css);
}
// Not cached, so let's create the file.
if (verbose) {
console.log('Not cached or out of date. Generating CSS...');
}
tmp = '';
for (k = 0, len2 = variablePaths.length; k < len2; k++) {
customPath = variablePaths[k];
tmp += `@import "${customPath}";\n`;
}
for (l = 0, len3 = stylePaths.length; l < len3; l++) {
customPath = stylePaths[l];
tmp += `@import "${customPath}";\n`;
}
benchmark.start('less-compile');
return less.render(tmp, {
compress: true
}, function(err, result) {
var writeErr;
if (err) {
return done(msgErr('Error processing LESS -> CSS', err));
}
try {
css = result.css;
fs.writeFileSync(compiledPath, css, 'utf-8');
} catch (error) {
writeErr = error;
return done(errMsg('Error writing cached CSS to file', writeErr));
}
benchmark.end('less-compile');
cache[key] = css;
return done(null, cache[key]);
});
});
};
compileTemplate = function(filename, options) {
var compiled;
return compiled = `var pug = require('pug');\n${pug.compileFileClient(filename, options)}\nmodule.exports = compiledFunc;`;
};
getTemplate = function(name, verbose, done) {
var builtin, compiledPath, key, load;
// Check if this is a built-in template name
builtin = path.join(ROOT, 'templates', `${name}.pug`);
if (!fs.existsSync(name) && fs.existsSync(builtin)) {
name = builtin;
}
// Get the template function for the given path. This will load and
// compile the template if necessary, and cache it for future use.
key = `template-${name}`;
// Check if it is cached in memory. If not, then we'll check the disk.
if (cache[key]) {
return done(null, cache[key]);
}
// Check if it is compiled on disk and not older than the template file.
// If not present or outdated, then we'll need to compile it.
compiledPath = path.join(ROOT, 'cache', `${sha1(key)}.js`);
load = function(filename, loadDone) {
var loadErr, loaded;
try {
loaded = require(filename);
} catch (error) {
loadErr = error;
return loadDone(errMsg('Unable to load template', loadErr));
}
return loadDone(null, require(filename));
};
if (verbose) {
console.log(`Using template ${name}`);
console.log(`Checking cache ${compiledPath}`);
}
return getCached(key, compiledPath, [name], load, function(err, template) {
var compileErr, compileOptions, compiled, writeErr;
if (err) {
return done(err);
}
if (template) {
if (verbose) {
console.log('Cached version loaded');
}
return done(null, template);
}
if (verbose) {
console.log('Not cached or out of date. Generating template JS...');
}
// We need to compile the template, then cache it. This is interesting
// because we are compiling to a client-side template, then adding some
// module-specific code to make it work here. This allows us to save time
// in the future by just loading the generated javascript function.
benchmark.start('pug-compile');
compileOptions = {
filename: name,
name: 'compiledFunc',
self: true,
compileDebug: false
};
try {
compiled = compileTemplate(name, compileOptions);
} catch (error) {
compileErr = error;
return done(errMsg('Error compiling template', compileErr));
}
if (compiled.indexOf('self.') === -1) {
// Not using self, so we probably need to recompile into compatibility
// mode. This is slower, but keeps things working with pug files
// designed for Aglio 1.x.
compileOptions.self = false;
try {
compiled = compileTemplate(name, compileOptions);
} catch (error) {
compileErr = error;
return done(errMsg('Error compiling template', compileErr));
}
}
try {
fs.writeFileSync(compiledPath, compiled, 'utf-8');
} catch (error) {
writeErr = error;
return done(errMsg('Error writing cached template file', writeErr));
}
benchmark.end('pug-compile');
cache[key] = require(compiledPath);
return done(null, cache[key]);
});
};
modifyUriTemplate = function(templateUri, parameters, colorize) {
var block, closeIndex, index, lastIndex, param, parameterBlocks, parameterNames, parameterSet, parameterValidator;
// Modify a URI template to only include the parameter names from
// the given parameters. For example:
// URI template: /pages/{id}{?verbose}
// Parameters contains a single `id` parameter
// Output: /pages/{id}
parameterValidator = function(b) {
// Compare the names, removing the special `*` operator
return parameterNames.indexOf(querystring.unescape(b.replace(/^\*|\*$/, ''))) !== -1;
};
parameterNames = (function() {
var i, len, results;
results = [];
for (i = 0, len = parameters.length; i < len; i++) {
param = parameters[i];
results.push(param.name);
}
return results;
})();
parameterBlocks = [];
lastIndex = index = 0;
while ((index = templateUri.indexOf("{", index)) !== -1) {
parameterBlocks.push(templateUri.substring(lastIndex, index));
block = {};
closeIndex = templateUri.indexOf("}", index);
block.querySet = templateUri.indexOf("{?", index) === index;
block.formSet = templateUri.indexOf("{&", index) === index;
block.reservedSet = templateUri.indexOf("{+", index) === index;
lastIndex = closeIndex + 1;
index++;
if (block.querySet || block.formSet || block.reservedSet) {
index++;
}
parameterSet = templateUri.substring(index, closeIndex);
block.parameters = parameterSet.split(",").filter(parameterValidator);
if (block.parameters.length) {
parameterBlocks.push(block);
}
}
parameterBlocks.push(templateUri.substring(lastIndex, templateUri.length));
return parameterBlocks.reduce(function(uri, v) {
var segment;
if (typeof v === "string") {
uri.push(v);
} else {
segment = !colorize ? ["{"] : [];
if (v.querySet) {
segment.push("?");
}
if (v.formSet) {
segment.push("&");
}
if (v.reservedSet && !colorize) {
segment.push("+");
}
segment.push(v.parameters.map(function(name) {
if (!colorize) {
return name;
} else {
// TODO: handle errors here?
name = name.replace(/^\*|\*$/, '');
param = parameters[parameterNames.indexOf(querystring.unescape(name))];
if (v.querySet || v.formSet) {
return `<span class="hljs-attribute">${name}=</span>` + `<span class="hljs-literal">${param.example || ''}</span>`;
} else {
return `<span class="hljs-attribute" title="${name}">${param.example || name}</span>`;
}
}
}).join(colorize ? '&' : ','));
if (!colorize) {
segment.push("}");
}
uri.push(segment.join(""));
}
return uri;
}, []).join('').replace(/\/+/g, '/');
};
decorate = function(api, md, slugCache, verbose) {
var action, category, dataStructure, dataStructures, err, example, i, item, j, k, knownParams, l, len, len1, len2, len3, meta, name, newParams, param, ref, ref1, ref2, ref3, resource, resourceGroup, results, reversed, schema, slugify;
// Decorate an API Blueprint AST with various pieces of information that
// will be useful for the theme. Anything that would significantly
// complicate the pug template should probably live here instead!
// Use the slug caching mechanism
slugify = slug.bind(slug, slugCache);
// Find data structures. This is a temporary workaround until Drafter is
// updated to support JSON Schema again.
// TODO: Remove me when Drafter is released.
dataStructures = {};
ref = api.content || [];
for (i = 0, len = ref.length; i < len; i++) {
category = ref[i];
ref1 = category.content || [];
for (j = 0, len1 = ref1.length; j < len1; j++) {
item = ref1[j];
if (item.element === 'dataStructure') {
dataStructure = item.content[0];
dataStructures[dataStructure.meta.id] = dataStructure;
}
}
}
if (verbose) {
console.log(`Known data structures: ${Object.keys(dataStructures)}`);
}
// API overview description
if (api.description) {
api.descriptionHtml = md.render(api.description);
api.navItems = slugCache._nav;
slugCache._nav = [];
}
ref2 = api.metadata || [];
for (k = 0, len2 = ref2.length; k < len2; k++) {
meta = ref2[k];
if (meta.name === 'HOST') {
api.host = meta.value;
}
}
ref3 = api.resourceGroups || [];
results = [];
for (l = 0, len3 = ref3.length; l < len3; l++) {
resourceGroup = ref3[l];
// Element ID and link
resourceGroup.elementId = slugify(resourceGroup.name, true);
resourceGroup.elementLink = `#${resourceGroup.elementId}`;
// Description
if (resourceGroup.description) {
resourceGroup.descriptionHtml = md.render(resourceGroup.description);
resourceGroup.navItems = slugCache._nav;
slugCache._nav = [];
}
results.push((function() {
var len4, m, ref4, results1;
ref4 = resourceGroup.resources || [];
results1 = [];
for (m = 0, len4 = ref4.length; m < len4; m++) {
resource = ref4[m];
// Element ID and link
resource.elementId = slugify(`${resourceGroup.name}-${resource.name}`, true);
resource.elementLink = `#${resource.elementId}`;
results1.push((function() {
var len5, len6, n, o, ref5, results2;
ref5 = resource.actions || [];
results2 = [];
for (n = 0, len5 = ref5.length; n < len5; n++) {
action = ref5[n];
// Element ID and link
action.elementId = slugify(`${resourceGroup.name}-${resource.name}-${action.method}`, true);
action.elementLink = `#${action.elementId}`;
// Lowercase HTTP method name
action.methodLower = action.method.toLowerCase();
// Parameters may be defined on the action or on the
// parent resource. Resource parameters should be concatenated
// to the action-specific parameters if set.
if (!(action.attributes || {}).uriTemplate) {
if (!action.parameters || !action.parameters.length) {
action.parameters = resource.parameters;
} else if (resource.parameters) {
action.parameters = resource.parameters.concat(action.parameters);
}
}
// Remove any duplicates! This gives precedence to the parameters
// defined on the action.
knownParams = {};
newParams = [];
reversed = (action.parameters || []).concat([]).reverse();
for (o = 0, len6 = reversed.length; o < len6; o++) {
param = reversed[o];
if (knownParams[param.name]) {
continue;
}
knownParams[param.name] = true;
newParams.push(param);
}
action.parameters = newParams.reverse();
// Set up the action's template URI
action.uriTemplate = modifyUriTemplate((action.attributes || {}).uriTemplate || resource.uriTemplate || '', action.parameters);
action.colorizedUriTemplate = modifyUriTemplate((action.attributes || {}).uriTemplate || resource.uriTemplate || '', action.parameters, true);
// Examples have a content section only if they have a
// description, headers, body, or schema.
action.hasRequest = false;
results2.push((function() {
var len7, p, ref6, results3;
ref6 = action.examples || [];
results3 = [];
for (p = 0, len7 = ref6.length; p < len7; p++) {
example = ref6[p];
results3.push((function() {
var len8, q, ref7, results4;
ref7 = ['requests', 'responses'];
results4 = [];
for (q = 0, len8 = ref7.length; q < len8; q++) {
name = ref7[q];
results4.push((function() {
var len10, len11, len9, r, ref10, ref8, ref9, results5, s, t;
ref8 = example[name] || [];
results5 = [];
for (r = 0, len9 = ref8.length; r < len9; r++) {
item = ref8[r];
if (name === 'requests' && !action.hasRequest) {
action.hasRequest = true;
}
// If there is no schema, but there are MSON attributes, then try
// to generate the schema. This will fail sometimes.
// TODO: Remove me when Drafter is released.
if (!item.schema && item.content) {
ref9 = item.content;
for (s = 0, len10 = ref9.length; s < len10; s++) {
dataStructure = ref9[s];
if (dataStructure.element === 'dataStructure') {
try {
schema = renderSchema(dataStructure.content[0], dataStructures);
schema['$schema'] = 'http://json-schema.org/draft-04/schema#';
item.schema = JSON.stringify(schema, null, 2);
} catch (error) {
err = error;
if (verbose) {
console.log(JSON.stringify(dataStructure.content[0], null, 2));
console.log(err);
}
}
}
}
}
if (item.content && !process.env.DRAFTER_EXAMPLES) {
ref10 = item.content;
for (t = 0, len11 = ref10.length; t < len11; t++) {
dataStructure = ref10[t];
if (dataStructure.element === 'dataStructure') {
try {
item.body = JSON.stringify(renderExample(dataStructure.content[0], dataStructures), null, 2);
} catch (error) {
err = error;
if (verbose) {
console.log(JSON.stringify(dataStructure.content[0], null, 2));
console.log(err);
}
}
}
}
}
item.hasContent = item.description || Object.keys(item.headers).length || item.body || item.schema;
try {
// If possible, make the body/schema pretty
if (item.body) {
item.body = JSON.stringify(JSON.parse(item.body), null, 2);
}
if (item.schema) {
results5.push(item.schema = JSON.stringify(JSON.parse(item.schema), null, 2));
} else {
results5.push(void 0);
}
} catch (error) {
err = error;
results5.push(false);
}
}
return results5;
})());
}
return results4;
})());
}
return results3;
})());
}
return results2;
})());
}
return results1;
})());
}
return results;
};
// Get the theme's configuration, used by Aglio to present available
// options and confirm that the input blueprint is a supported
// version.
exports.getConfig = function() {
return {
formats: ['1A'],
options: [
{
name: 'variables',
description: 'Color scheme name or path to custom variables',
default: 'default'
},
{
name: 'condense-nav',
description: 'Condense navigation links',
boolean: true,
default: true
},
{
name: 'full-width',
description: 'Use full window width',
boolean: true,
default: false
},
{
name: 'template',
description: 'Template name or path to custom template',
default: 'default'
},
{
name: 'style',
description: 'Layout style name or path to custom stylesheet'
},
{
name: 'emoji',
description: 'Enable support for emoticons',
boolean: true,
default: true
}
]
};
};
// Render the blueprint with the given options using pug and LESS
exports.render = function(input, options, done) {
var md, slugCache, themeStyle, themeVariables, verbose;
if (done == null) {
done = options;
options = {};
}
// Disable the template/css caching?
if (process.env.NOCACHE) {
cache = {};
}
// This is purely for backward-compatibility
if (options.condenseNav) {
options.themeCondenseNav = options.condenseNav;
}
if (options.fullWidth) {
options.themeFullWidth = options.fullWidth;
}
// Setup defaults
if (options.themeVariables == null) {
options.themeVariables = 'default';
}
if (options.themeStyle == null) {
options.themeStyle = 'default';
}
if (options.themeTemplate == null) {
options.themeTemplate = 'default';
}
if (options.themeCondenseNav == null) {
options.themeCondenseNav = true;
}
if (options.themeFullWidth == null) {
options.themeFullWidth = false;
}
// Transform built-in layout names to paths
if (options.themeTemplate === 'default') {
options.themeTemplate = path.join(ROOT, 'templates', 'index.pug');
}
// Setup markdown with code highlighting and smartypants. This also enables
// automatically inserting permalinks for headers.
slugCache = {
_nav: []
};
md = markdownIt({
html: true,
linkify: true,
typographer: true,
highlight: highlight
}).use(require('markdown-it-anchor'), {
slugify: function(value) {
var output;
output = `header-${slug(slugCache, value, true)}`;
slugCache._nav.push([value, `#${output}`]);
return output;
},
permalink: true,
permalinkClass: 'permalink'
}).use(require('markdown-it-checkbox')).use(require('markdown-it-container'), 'note').use(require('markdown-it-container'), 'warning');
if (options.themeEmoji) {
md.use(require('markdown-it-emoji'));
}
// Enable code highlighting for unfenced code blocks
md.renderer.rules.code_block = md.renderer.rules.fence;
benchmark.start('decorate');
decorate(input, md, slugCache, options.verbose);
benchmark.end('decorate');
benchmark.start('css-total');
({themeVariables, themeStyle, verbose} = options);
return getCss(themeVariables, themeStyle, verbose, function(err, css) {
var key, locals, ref, value;
if (err) {
return done(errMsg('Could not get CSS', err));
}
benchmark.end('css-total');
locals = {
api: input,
condenseNav: options.themeCondenseNav,
css: css,
fullWidth: options.themeFullWidth,
date: moment,
hash: function(value) {
return crypto.createHash('md5').update(value.toString()).digest('hex');
},
highlight: highlight,
markdown: function(content) {
return md.render(content);
},
slug: slug.bind(slug, slugCache),
urldec: function(value) {
return querystring.unescape(value);
}
};
ref = options.locals || {};
for (key in ref) {
value = ref[key];
locals[key] = value;
}
benchmark.start('get-template');
return getTemplate(options.themeTemplate, verbose, function(getTemplateErr, renderer) {
var html;
if (getTemplateErr) {
return done(errMsg('Could not get template', getTemplateErr));
}
benchmark.end('get-template');
benchmark.start('call-template');
try {
html = renderer(locals);
} catch (error) {
err = error;
return done(errMsg('Error calling template during rendering', err));
}
benchmark.end('call-template');
return done(null, html);
});
});
};
}).call(this);