UNPKG

aglio-theme-planado

Version:

Custom theme for the Aglio API Blueprint renderer

895 lines (864 loc) 33.1 kB
// Generated by CoffeeScript 2.1.1 (function() { var ROOT, benchmark, cache, compileTemplate, crypto, decorate, errMsg, fs, getCached, getCss, getTemplate, highlight, hljs, less, markdownIt, modifyUriTemplate, moment, path, prepareNav, pug, querystring, readLocales, 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'); }; readLocales = function(root, language) { var dest_path, lang_file; dest_path = path.join(root, language + '.json'); lang_file = fs.readFileSync(dest_path); return JSON.parse(lang_file); }; // 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, '/'); }; prepareNav = function(navigation) { return navigation.map(function([tag, title, id, children]) { return { link: "#" + id[1], title: title, children: prepareNav(children) }; }); }; decorate = function(api, md, slugCache, verbose) { var action, category, content_sections, dataStructure, dataStructures, err, example, fn, i, item, j, k, knownParams, l, len, len1, len2, len3, len4, m, meta, name, newParams, param, ref, ref1, ref2, ref3, ref4, resource, resourceGroup, results, reversed, schema, section, 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) { content_sections = api.description.split('<!-- LHSCONTENT -->'); api.descriptionHtml = []; api.descriptionHtml.push([md.render(content_sections[0])]); ref2 = content_sections.slice(1); fn = function() { var rest, side, sides, sidesContent; sides = section.split('<!-- RHSCONTENT -->'); [sides[1], rest] = sides[1].split('<!-- ENDCONTENT -->'); sidesContent = (function() { var l, len3, results; results = []; for (l = 0, len3 = sides.length; l < len3; l++) { side = sides[l]; results.push(md.render(side)); } return results; })(); api.descriptionHtml.push(sidesContent); return api.descriptionHtml.push([md.render(rest)]); }; for (k = 0, len2 = ref2.length; k < len2; k++) { section = ref2[k]; fn(); } api.navItems = prepareNav(slugCache._nav); slugCache._nav = []; } ref3 = api.metadata || []; for (l = 0, len3 = ref3.length; l < len3; l++) { meta = ref3[l]; if (meta.name === 'HOST') { api.host = meta.value; } } ref4 = api.resourceGroups || []; results = []; for (m = 0, len4 = ref4.length; m < len4; m++) { resourceGroup = ref4[m]; // 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 len5, n, ref5, results1; ref5 = resourceGroup.resources || []; results1 = []; for (n = 0, len5 = ref5.length; n < len5; n++) { resource = ref5[n]; // Element ID and link resource.elementId = slugify(`${resourceGroup.name}-${resource.name}`, true); resource.elementLink = `#${resource.elementId}`; results1.push((function() { var len6, len7, o, p, ref6, results2; ref6 = resource.actions || []; results2 = []; for (o = 0, len6 = ref6.length; o < len6; o++) { action = ref6[o]; // 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 (p = 0, len7 = reversed.length; p < len7; p++) { param = reversed[p]; 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 len8, q, ref7, results3; ref7 = action.examples || []; results3 = []; for (q = 0, len8 = ref7.length; q < len8; q++) { example = ref7[q]; results3.push((function() { var len9, r, ref8, results4; ref8 = ['requests', 'responses']; results4 = []; for (r = 0, len9 = ref8.length; r < len9; r++) { name = ref8[r]; results4.push((function() { var len10, len11, len12, ref10, ref11, ref9, results5, s, u, w; ref9 = example[name] || []; results5 = []; for (s = 0, len10 = ref9.length; s < len10; s++) { item = ref9[s]; 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) { ref10 = item.content; for (u = 0, len11 = ref10.length; u < len11; u++) { dataStructure = ref10[u]; 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.body && item.content) { ref11 = item.content; for (w = 0, len12 = ref11.length; w < len12; w++) { dataStructure = ref11[w]; 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 }, { name: 'lang', description: 'Language code' }, { name: 'lang-dir', description: 'Language directory' } ] }; }; // Render the blueprint with the given options using pug and LESS exports.render = function(input, options, done) { var collectNavigationData, locale, 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; } if (options.themeLang == null) { options.themeLang = null; } if (options.themeLangDir == null) { options.themeLangDir = null; } // Transform built-in layout names to paths if (options.themeTemplate === 'default') { options.themeTemplate = path.join(ROOT, 'templates', 'index.pug'); } collectNavigationData = function(md, opts) { var getHeadingLevel, navigation, noop, originalHeadingOpen, recursivelyAdd, setupCallback; // How it works: // 1. We attach the function *after* all H-tags are processed // 2. Those tags have `id` as an attribute // 3. We build a nested list of items (pretty lispy) // 4. We convert the list into a dict-like structure and render it noop = function() { return []; }; setupCallback = opts && opts.callback ? opts.callback : noop; originalHeadingOpen = md.renderer.rules.heading_open; navigation = []; setupCallback(navigation); getHeadingLevel = function(heading) { return parseInt(heading[1]); }; recursivelyAdd = function(navigation, item) { var item_level, item_tag, parent, parent_children, parent_id, parent_level, parent_tag, parent_title; parent = navigation[navigation.length - 1]; if (parent && parent.length > 0) { [parent_tag, parent_title, parent_id, parent_children] = parent; item_tag = item[0]; parent_level = getHeadingLevel(parent_tag); item_level = getHeadingLevel(item_tag); if (parent_level < item_level) { // higher level = child return recursivelyAdd(parent_children, item); } else { return navigation.push(item); } } else { return navigation.push(item); } }; return md.renderer.rules.heading_open = function(tokens, idx, something, somethingelse, self) { var id, item, tag, title; title = tokens[idx + 1].children.reduce(function(acc, t) { return acc + t.content; }, "").replace(" ¶", ""); tag = tokens[idx].tag; id = tokens[idx].attrs.find(function([key, _]) { return key === "id"; }); item = [tag, title, id, []]; recursivelyAdd(navigation, item); if (originalHeadingOpen) { return originalHeadingOpen.apply(this, arguments); } else { return self.renderToken.apply(self, arguments); } }; }; // 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)}`; return output; }, permalink: true, permalinkClass: 'permalink' }).use(collectNavigationData, { callback: function(navigation) { return slugCache._nav = navigation; } }).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')); } if (options.themeLang && options.themeLangDir) { locale = readLocales(options.themeLangDir, options.themeLang); } else { locale = {}; } // 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); }, locale: locale }; 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);