docma
Version:
A powerful dev-tool to easily generate beautiful HTML documentation from Javascript (JSDoc), Markdown and HTML files.
462 lines (415 loc) • 12.7 kB
JavaScript
/*! dustjs-helpers - v1.7.4
* http://dustjs.com/
* Copyright (c) 2017 Aleksander Williams; Released under the MIT License */
(function(root, factory) {
if (typeof define === 'function' && define.amd && define.amd.dust === true) {
define(['dust.core'], factory);
} else if (typeof exports === 'object') {
module.exports = factory(require('dustjs-linkedin'));
module.exports.registerWith = factory;
} else {
factory(root.dust);
}
}(this, function(dust) {
function log(helper, msg, level) {
level = level || "INFO";
helper = helper ? '{@' + helper + '}: ' : '';
dust.log(helper + msg, level);
}
var _deprecatedCache = {};
function _deprecated(target) {
if(_deprecatedCache[target]) { return; }
log(target, "Deprecation warning: " + target + " is deprecated and will be removed in a future version of dustjs-helpers", "WARN");
log(null, "For help and a deprecation timeline, see https://github.com/linkedin/dustjs-helpers/wiki/Deprecated-Features#" + target.replace(/\W+/g, ""), "WARN");
_deprecatedCache[target] = true;
}
function isSelect(context) {
return context.stack.tail &&
context.stack.tail.head &&
typeof context.stack.tail.head.__select__ !== "undefined";
}
function getSelectState(context) {
return isSelect(context) && context.get('__select__');
}
/**
* Adds a special __select__ key behind the head of the context stack. Used to maintain the state
* of {@select} blocks
* @param context {Context} add state to this Context
* @param opts {Object} add these properties to the state (`key` and `type`)
*/
function addSelectState(context, opts) {
var head = context.stack.head,
newContext = context.rebase(),
key;
if(context.stack && context.stack.tail) {
newContext.stack = context.stack.tail;
}
var state = {
isPending: false,
isResolved: false,
isDeferredComplete: false,
deferreds: []
};
for(key in opts) {
state[key] = opts[key];
}
return newContext
.push({ "__select__": state })
.push(head, context.stack.index, context.stack.of);
}
/**
* After a {@select} or {@math} block is complete, they invoke this function
*/
function resolveSelectDeferreds(state) {
var x, len;
state.isDeferredPending = true;
if(state.deferreds.length) {
state.isDeferredComplete = true;
for(x=0, len=state.deferreds.length; x<len; x++) {
state.deferreds[x]();
}
}
state.isDeferredPending = false;
}
/**
* Used by {@contextDump}
*/
function jsonFilter(key, value) {
if (typeof value === "function") {
return value.toString()
.replace(/(^\s+|\s+$)/mg, '')
.replace(/\n/mg, '')
.replace(/,\s*/mg, ', ')
.replace(/\)\{/mg, ') {');
}
return value;
}
/**
* Generate a truth test helper
*/
function truthTest(name, test) {
return function(chunk, context, bodies, params) {
return filter(chunk, context, bodies, params, name, test);
};
}
/**
* This function is invoked by truth test helpers
*/
function filter(chunk, context, bodies, params, helperName, test) {
var body = bodies.block,
skip = bodies['else'],
selectState = getSelectState(context) || {},
willResolve, key, value, type;
// Once one truth test in a select passes, short-circuit the rest of the tests
if (selectState.isResolved && !selectState.isDeferredPending) {
return chunk;
}
// First check for a key on the helper itself, then look for a key on the {@select}
if (params.hasOwnProperty('key')) {
key = params.key;
} else if (selectState.hasOwnProperty('key')) {
key = selectState.key;
} else {
log(helperName, "No key specified", "WARN");
return chunk;
}
type = params.type || selectState.type;
key = coerce(context.resolve(key), type);
value = coerce(context.resolve(params.value), type);
if (test(key, value)) {
// Once a truth test passes, put the select into "pending" state. Now we can render the body of
// the truth test (which may contain truth tests) without altering the state of the select.
if (!selectState.isPending) {
willResolve = true;
selectState.isPending = true;
}
if (body) {
chunk = chunk.render(body, context);
}
if (willResolve) {
selectState.isResolved = true;
}
} else if (skip) {
chunk = chunk.render(skip, context);
}
return chunk;
}
function coerce(value, type) {
if (type) {
type = type.toLowerCase();
}
switch (type) {
case 'number': return +value;
case 'string': return String(value);
case 'boolean':
value = (value === 'false' ? false : value);
return Boolean(value);
case 'date': return new Date(value);
}
return value;
}
var helpers = {
// Utility helping to resolve dust references in the given chunk
// uses native Dust Context#resolve (available since Dust 2.6.2)
"tap": function(input, chunk, context) {
// deprecated for removal in 1.8
_deprecated("tap");
return context.resolve(input);
},
"sep": function(chunk, context, bodies) {
var body = bodies.block;
if (context.stack.index === context.stack.of - 1) {
return chunk;
}
if (body) {
return body(chunk, context);
} else {
return chunk;
}
},
"first": function(chunk, context, bodies) {
if (context.stack.index === 0) {
return bodies.block(chunk, context);
}
return chunk;
},
"last": function(chunk, context, bodies) {
if (context.stack.index === context.stack.of - 1) {
return bodies.block(chunk, context);
}
return chunk;
},
/**
* {@contextDump}
* @param key {String} set to "full" to the full context stack, otherwise the current context is dumped
* @param to {String} set to "console" to log to console, otherwise outputs to the chunk
*/
"contextDump": function(chunk, context, bodies, params) {
var to = context.resolve(params.to),
key = context.resolve(params.key),
target, output;
switch(key) {
case 'full':
target = context.stack;
break;
default:
target = context.stack.head;
}
output = JSON.stringify(target, jsonFilter, 2);
switch(to) {
case 'console':
log('contextDump', output);
break;
default:
output = output.replace(/</g, '\\u003c');
chunk = chunk.write(output);
}
return chunk;
},
/**
* {@math}
* @param key first value
* @param method {String} operation to perform
* @param operand second value (not required for operations like `abs`)
* @param round if truthy, round() the result
*/
"math": function (chunk, context, bodies, params) {
var key = params.key,
method = params.method,
operand = params.operand,
round = params.round,
output, state, x, len;
if(!params.hasOwnProperty('key') || !params.method) {
log("math", "`key` or `method` was not provided", "ERROR");
return chunk;
}
key = parseFloat(context.resolve(key));
operand = parseFloat(context.resolve(operand));
switch(method) {
case "mod":
if(operand === 0) {
log("math", "Division by 0", "ERROR");
}
output = key % operand;
break;
case "add":
output = key + operand;
break;
case "subtract":
output = key - operand;
break;
case "multiply":
output = key * operand;
break;
case "divide":
if(operand === 0) {
log("math", "Division by 0", "ERROR");
}
output = key / operand;
break;
case "ceil":
case "floor":
case "round":
case "abs":
output = Math[method](key);
break;
case "toint":
output = parseInt(key, 10);
break;
default:
log("math", "Method `" + method + "` is not supported", "ERROR");
}
if (typeof output !== 'undefined') {
if (round) {
output = Math.round(output);
}
if (bodies && bodies.block) {
context = addSelectState(context, { key: output });
chunk = chunk.render(bodies.block, context);
resolveSelectDeferreds(getSelectState(context));
} else {
chunk = chunk.write(output);
}
}
return chunk;
},
/**
* {@select}
* Groups a set of truth tests and outputs the first one that passes.
* Also contains {@any} and {@none} blocks.
* @param key a value or reference to use as the left-hand side of comparisons
* @param type coerce all truth test keys without an explicit type to this type
*/
"select": function(chunk, context, bodies, params) {
var body = bodies.block,
state = {};
if (params.hasOwnProperty('key')) {
state.key = context.resolve(params.key);
}
if (params.hasOwnProperty('type')) {
state.type = params.type;
}
if (body) {
context = addSelectState(context, state);
chunk = chunk.render(body, context);
resolveSelectDeferreds(getSelectState(context));
} else {
log("select", "Missing body block", "WARN");
}
return chunk;
},
/**
* Truth test helpers
* @param key a value or reference to use as the left-hand side of comparisons
* @param value a value or reference to use as the right-hand side of comparisons
* @param type if specified, `key` and `value` will be forcibly cast to this type
*/
"eq": truthTest('eq', function(left, right) {
return left === right;
}),
"ne": truthTest('ne', function(left, right) {
return left !== right;
}),
"lt": truthTest('lt', function(left, right) {
return left < right;
}),
"lte": truthTest('lte', function(left, right) {
return left <= right;
}),
"gt": truthTest('gt', function(left, right) {
return left > right;
}),
"gte": truthTest('gte', function(left, right) {
return left >= right;
}),
/**
* {@any}
* Outputs as long as at least one truth test inside a {@select} has passed.
* Must be contained inside a {@select} block.
* The passing truth test can be before or after the {@any} block.
*/
"any": function(chunk, context, bodies, params) {
var selectState = getSelectState(context);
if(!selectState) {
log("any", "Must be used inside a {@select} block", "ERROR");
} else {
if(selectState.isDeferredComplete) {
log("any", "Must not be nested inside {@any} or {@none} block", "ERROR");
} else {
chunk = chunk.map(function(chunk) {
selectState.deferreds.push(function() {
if(selectState.isResolved) {
chunk = chunk.render(bodies.block, context);
}
chunk.end();
});
});
}
}
return chunk;
},
/**
* {@none}
* Outputs if no truth tests inside a {@select} pass.
* Must be contained inside a {@select} block.
* The position of the helper does not matter.
*/
"none": function(chunk, context, bodies, params) {
var selectState = getSelectState(context);
if(!selectState) {
log("none", "Must be used inside a {@select} block", "ERROR");
} else {
if(selectState.isDeferredComplete) {
log("none", "Must not be nested inside {@any} or {@none} block", "ERROR");
} else {
chunk = chunk.map(function(chunk) {
selectState.deferreds.push(function() {
if(!selectState.isResolved) {
chunk = chunk.render(bodies.block, context);
}
chunk.end();
});
});
}
}
return chunk;
},
/**
* {@size}
* Write the size of the target to the chunk
* Falsy values and true have size 0
* Numbers are returned as-is
* Arrays and Strings have size equal to their length
* Objects have size equal to the number of keys they contain
* Dust bodies are evaluated and the length of the string is returned
* Functions are evaluated and the length of their return value is evaluated
* @param key find the size of this value or reference
*/
"size": function(chunk, context, bodies, params) {
var key = params.key,
value, k;
key = context.resolve(params.key);
if (!key || key === true) {
value = 0;
} else if(dust.isArray(key)) {
value = key.length;
} else if (!isNaN(parseFloat(key)) && isFinite(key)) {
value = key;
} else if (typeof key === "object") {
value = 0;
for(k in key){
if(key.hasOwnProperty(k)){
value++;
}
}
} else {
value = (key + '').length;
}
return chunk.write(value);
}
};
for(var key in helpers) {
dust.helpers[key] = helpers[key];
}
return dust;
}));