template-tal
Version:
XML Lightweight Template Attribute Language implementation for Javascript
768 lines (659 loc) • 21.6 kB
JavaScript
// REX/Javascript 1.0
// Robert D. Cameron "REX: XML Shallow Parsing with Regular Expressions",
// Technical Report TR 1998-17, School of Computing Science, Simon Fraser
// University, November, 1998.
// Copyright (c) 1998, Robert D. Cameron.
// The following code may be freely used and distributed provided that
// this copyright and citation notice remains intact and that modifications
// or additions are clearly identified.
var TextSE = "[^<]+";
var UntilHyphen = "[^-]*-";
var Until2Hyphens = UntilHyphen + "([^-]" + UntilHyphen + ")*-";
var CommentCE = Until2Hyphens + ">?";
var UntilRSBs = "[^]]*]([^]]+])*]+";
var CDATA_CE = UntilRSBs + "([^]>]" + UntilRSBs + ")*>";
var S = "[ \\n\\t\\r]+";
var NameStrt = "[A-Za-z_:]|[^\\x00-\\x7F]";
var NameChar = "[A-Za-z0-9_:.-]|[^\\x00-\\x7F]";
var Name = "(" + NameStrt + ")(" + NameChar + ")*";
var QuoteSE = '"[^"]' + "*" + '"' + "|'[^']*'";
var DT_IdentSE = S + Name + "(" + S + "(" + Name + "|" + QuoteSE + "))*";
var MarkupDeclCE = "([^]\"'><]+|" + QuoteSE + ")*>";
var S1 = "[\\n\\r\\t ]";
var UntilQMs = "[^?]*\\?+";
var PI_Tail = "\\?>|" + S1 + UntilQMs + "([^>?]" + UntilQMs + ")*>";
var DT_ItemSE = "<(!(--" + Until2Hyphens + ">|[^-]" + MarkupDeclCE + ")|\\?" + Name + "(" + PI_Tail + "))|%" + Name + ";|" + S;
var DocTypeCE = DT_IdentSE + "(" + S + ")?(\\[(" + DT_ItemSE + ")*](" + S + ")?)?>?";
var DeclCE = "--(" + CommentCE + ")?|\\[CDATA\\[(" + CDATA_CE + ")?|DOCTYPE(" + DocTypeCE + ")?";
var PI_CE = Name + "(" + PI_Tail + ")?";
var EndTagCE = Name + "(" + S + ")?>?";
var AttValSE = '"[^<"]' + "*" + '"' + "|'[^<']*'";
var ElemTagCE = Name + "(" + S + Name + "(" + S + ")?=(" + S + ")?(" + AttValSE + "))*(" + S + ")?/?>?";
var MarkupSPE = "<(!(" + DeclCE + ")?|\\?(" + PI_CE + ")?|/(" + EndTagCE + ")?|(" + ElemTagCE + ")?)";
var XML_SPE = TextSE + "|" + MarkupSPE;
function ShallowParse(XMLdoc) {
return XMLdoc.match(new RegExp(XML_SPE, "g"));
}
// REX END - thank you Robert! Awesome... I just added "var" keyword everywhere.
var ElemTagCE_Mod = S + Name + "(" + S + ")?=(" + S + ")?(" + AttValSE + ')';
var RE_1 = ElemTagCE;
var RE_2 = ElemTagCE_Mod;
var VARIABLE_RE_SIMPLE = "\$[A-Za-z_][A-Za-z0-9_\.:\/]+";
var VARIABLE_RE_BRACKETS = "(?<!\$)\\{.*?(?<!\\\\)\\}";
var STRING_TOKEN_RE = "(" + VARIABLE_RE_SIMPLE + '|' + VARIABLE_RE_BRACKETS + ")"
var TAL = 'tal';
var STOP_RECURSE = 0;
String.prototype.tal_supplant = function (self) {
return this.replace(/\${([^{}]*)}/g, function (a, b) {
var r;
try {
r = eval(b);
}
catch (err) {
r = a;
}
return r;
});
}
var MODIFIERS = {
true: function (expr, context, callback) {
resolve(expr, context, function(error, arg) {
if(error) { return callback(error) }
if (arg === null) { return callback(null, false) }
if ((typeof(arg) == 'object') && arg.length) { return callback(null, true) }
if ((typeof(arg) == 'object') && !arg.length) { return callback(null, false) }
if ((typeof(arg) == 'array') && arg.length) { return callback(null, true) }
if ((typeof(arg) == 'array') && !arg.length) { return callback(null, false) }
if (arg) { return callback(null, true) }
return callback(null, false)
})
},
false: function (expr, context, callback) {
resolve(expr, context, function(error, arg) {
if(error) { return callback(error) }
if (arg === null) { return callback(null, true) }
if ((typeof(arg) == 'object') && arg.length) { return callback(null, false) }
if ((typeof(arg) == 'object') && !arg.length) { return callback(null, true) }
if ((typeof(arg) == 'array') && arg.length) { return callback(null, false) }
if ((typeof(arg) == 'array') && !arg.length) { return callback(null, true) }
if (arg) { return callback(null, false) }
return callback(null, true)
})
},
string: function (string, context, callback) {
return callback(null, string.tal_supplant(context))
}
}
function Process (xml, context, callback) {
var head = [];
var body = [];
var tail = [];
if (typeof(xml) == "string") { xml = ShallowParse(xml); }
if (xml.length == 0) { return callback(null, '') };
while(xml.length) {
var elem = xml.shift();
if (tag_self_close (elem)) {
body.push(elem);
tail = xml;
break;
}
var opentag = tag_open (elem);
if (opentag) {
body.push(elem);
var elemOrig = elem;
var balance = 1;
while (balance) {
if (xml.length == 0) { return callback ("cannot find closing tag for " + elemOrig) }
var elem = xml.shift();
if (tag_open(elem)) { balance++ };
if (tag_close(elem)) { balance-- };
body.push(elem);
}
tail = xml;
break;
}
if(tag_close(elem)) { return callback ("cannot find opening tag for " + elem) }
head.push(elem);
}
var res = [];
if (head) { res = res.concat(head); }
Process_block(body, context, function (error, block_result) {
if(error) return callback(error)
block_result = block_result || ''
res = res.concat(block_result);
process.nextTick(function() {
Process(tail, context, function (error, tail_result){
if (error) return callback(error)
tail_result = tail_result || ''
res = res.concat(tail_result);
return callback(null, res.join(''))
})
})
})
}
function shallowCopy (item) {
if (typeof(item) === 'object') {
if (Object.prototype.toString.call(item) === '[object Array]') {
var copy = [];
for (i=0; i<item.length; i++) {
copy[i] = item[i];
}
return copy;
}
else if (Object.prototype.toString.call(item) === '[object Object]') {
var copy = {};
for (var key in item) {
copy[key] = item[key];
}
return copy;
}
else {
return item;
}
}
else {
return item;
}
}
function namespace (node) {
for (var k in node) {
if (k.match(/^xmlns\:/)) {
var v = node[k];
if (v == 'http://xml.zope.org/namespaces/tal') {
delete node[k];
k = k.replace(/^xmlns\:/, '');
return k;
}
}
}
}
function Process_block (xml, context, callback) {
if (xml == null) { return callback(null, null) }
if (typeof(xml) == "string") { xml = ShallowParse(xml); }
var tag = xml.shift();
var gat = xml.pop();
var node = tag_open(tag) || tag_self_close(tag);
var ns = namespace(node);
var TAL = ns || TAL;
if (has_instructions (node)) {
context = shallowCopy(context);
return tal_on_error (node, xml, gat, context, callback);
}
else {
if (ns) { tag = node2tag(node); }
if (gat) {
process.nextTick(function() {
Process(xml, context, function (error, result){
if (error) return callback(error)
return callback(null, tag+result+gat)
})
})
}
else {
return callback(null, tag) // self-closing tag
}
}
}
function tal_on_error (node, xml, end, context, callback) {
var stuff = node[TAL + ':on-error']; delete node[TAL + ':on-error'];
if (!stuff) {
return tal_define(node, xml, end, context, callback);
}
var nodeCopy = shallowCopy(node);
tal_define (node, xml, end, context, function(err, result){
var result = [];
for (var k in nodeCopy) {
if (k.match(new RegExp('^' + TAL + ':'))) { delete nodeCopy[k] }
}
if (nodeCopy._close) {
delete nodeCopy['_close'];
end = '</' + nodeCopy._tag + '>'; // deal with self closing tags
}
result.push (node2tag(nodeCopy));
resolve_expression(stuff, context, function(error, resolveResult){
if (error) return callback(error)
resolveResult = resolveResult || ''
result.push(resolveResult)
result.push (end);
return callback(null, result.join(''))
})
});
}
function tal_define_each (array, node, xml, end, context, callback) {
if(array.length == 0) { return callback() }
var def = trim(array.shift())
var def_split = def.split(/\s+/)
var symbol = def_split.shift()
var expression = def_split.join(' ')
resolve_expression(expression, context, function(error, result) {
if (error) {
console.error("error", error)
}
else {
context[symbol] = result;
}
return tal_define_each(array, node, xml, end, context, callback);
});
}
function tal_define (node, xml, end, context, callback) {
var stuff = node[TAL + ':define']; delete node[TAL + ':define'];
if (!stuff) { return tal_condition (node, xml, end, context, callback); }
stuff = trim(stuff);
var newContext = shallowCopy(context);
var array = stuff.split(/;(?!;)/);
tal_define_each(array, node, xml, end, newContext, function() {
return tal_condition (node, xml, end, newContext, callback);
});
}
function tal_condition_each (array, node, xml, end, context, callback) {
if(array.length == 0) { return callback(true) }
cond = trim(array.shift())
resolve_expression(cond, context, function (error, result) {
if (error) { console.error(error) }
if (result) {
return tal_condition_each (array, node, xml, end, context, callback);
}
else {
return callback(false);
}
})
}
function tal_condition (node, xml, end, context, callback) {
var stuff = node[TAL + ':condition']; delete node[TAL + ':condition'];
if (!stuff) { return tal_repeat (node, xml, end, context, callback) }
stuff = trim(stuff);
var array = stuff.split(/;(?!;)/);
tal_condition_each (array, node, xml, end, context, function (is_true) {
if(is_true) {
return tal_repeat (node, xml, end, context, callback)
}
else {
return callback(null, '')
}
})
}
function tal_repeat_each (symbol, array, node, xml, end, context, callback, count, temp) {
temp = temp || []
if(array.length == 0) { return callback(null, temp) }
item = array.shift()
var newContext = shallowCopy(context)
var newNode = shallowCopy(node)
if(count) {
count++
}
else {
count = 1
}
newContext.repeat = {}
newContext.repeat.index = count
newContext.repeat.number = count
newContext.repeat.even = !(count%2)
newContext.repeat.odd = count%2
newContext.repeat.start = (count == 1)
newContext.repeat.end = array.length == 0
newContext.repeat.inner = (!newContext.repeat.start && !newContext.repeat.end)
newContext[symbol] = item
tal_content(newNode, shallowCopy(xml), end, newContext, function (error, result) {
if (error) return callback(error)
result = result || ''
temp.push(result)
return tal_repeat_each(symbol, array, node, xml, end, context, callback, count, temp)
})
}
function tal_repeat (node, xml, end, context, callback) {
var stuff = node[TAL + ':repeat']; delete node[TAL + ':repeat'];
if (!stuff) { return tal_content (node, xml, end, context, callback); }
stuff = trim(stuff);
var array = stuff.split(/\s+/);
var symbol = array.shift();
var expression = array.join (' ');
resolve_expression(expression, context, function(error, array) {
if (error) return callback(error)
array = array || [];
new_array = [];
for (i in array) {
new_array.push(array[i])
}
tal_repeat_each (symbol, new_array, node, xml, end, context, function (error, result){
if (error) return callback(error)
result = result || []
return callback(null, result.join(''))
})
})
}
function tal_content (node, xml, end, context, callback) {
var stuff = node[TAL + ':content']; delete node[TAL + ':content'];
if (!stuff) { return tal_replace (node, xml, end, context, callback); }
stuff = trim(stuff);
resolve_expression(stuff, context, function(error, res) {
if (error) return callback(error)
res = res || '';
var xml = [];
if (res) { xml.push (res); }
if (node['_close']) {
delete node["_close"];
end = "</" + node._tag + '>';
}
return tal_replace (node, xml, end, context, callback);
});
}
function tal_replace (node, xml, end, context, callback) {
var stuff = node[TAL + ':replace']; delete node[TAL + ':replace'];
if (!stuff) { return tal_attributes (node, xml, end, context, callback) }
stuff = trim(stuff)
resolve_expression(stuff, context, function(error, res) {
if (error) return callback(error)
if (res != null) { return callback(null, res) }
else { return callback(null, '') }
})
}
function tal_attributes_each (array, node, xml, end, context, callback) {
if(array.length == 0) { return callback() }
var att = trim(array.shift())
var args = att.split(/\s+/)
var symbol = args.shift()
var expression = args.join (' ')
resolve_expression(expression, context, function (error, result){
if (error) { console.error(error) }
if (result === undefined || result === null || result === false) {
delete node[symbol]
}
else {
node[symbol] = result
}
return tal_attributes_each(array, node, xml, end, context, callback)
})
}
function tal_attributes (node, xml, end, context, callback) {
var stuff = node[TAL + ':attributes']; delete node[TAL + ':attributes'];
if (!stuff) { return tal_omit_tag (node, xml, end, context, callback) }
stuff = trim(stuff);
var array = stuff.split(/;(?!;)/)
tal_attributes_each(array, node, xml, end, context, function(){
return tal_omit_tag (node, xml, end, context, callback)
})
}
function tal_omit_tag (node, xml, end, context, callback) {
var stuff = node[TAL + ':omit-tag']; delete node[TAL + ':omit-tag'];
var omit;
if (stuff === undefined) {
omitProcessor = function (callback) { return callback(null, false) }
}
else if (stuff === null) {
omitProcessor = function (callback) { return callback(null, false) }
}
else if (stuff === false) {
omitProcessor = function (callback) { return callback(null, false) }
}
else if (stuff === 0) {
omitProcessor = function (callback) { return callback(null, false) }
}
else if (stuff === "") {
omitProcessor = function (callback) { return callback(null, true) }
}
else {
node[TAL + ':omit-tag'];
omitProcessor = function(callback) {
resolve_expression (stuff, context, callback)
}
}
omitProcessor(function(error, omit) {
if (omit && !end) { return callback (null,'') } // omit-tag on a self-closing tag means *poof*, nothing left
process.nextTick(function() {
Process(xml, context, function (error, content){
if (error) return callback(error)
content = content || '';
var result = [];
if (!omit) { result.push(node2tag(node)); }
if (end) {
result.push(content)
if (!omit) {
result.push(end)
}
}
return callback(null, result.join (''))
})
})
})
}
function resolve_expression (expr, context, callback) {
expr = unescape(expr);
if (expr == 'nothing') { return null; }
var structure = false;
var template = false;
if (expr.match(/^structure\s+/)) {
expr = expr.replace (/^structure\s+/, '')
structure = true;
}
else if (expr.match(/^template\s+/)) {
expr = expr.replace (/^template\s+/, '')
template = true;
}
resolve(expr, context, function (error, result) {
if(error) return callback(error, null)
// this is a hack so that template-tal doesn't think
// the result is an XML open tag, &structure; string
// will be stripped out off the result at the end of
// processing.
if (structure) {
return callback(null, '&structure;' + result)
}
// template re-evaluates the resulting expression with the
// same context, as a template. Once evaluated, the resulting
// XML string is treated the same way as with structure.
else if (template) {
process.nextTick(function(){
Process(result, context, function (error, result) {
if(error) return callback(error)
return callback(null, '&structure;' + result)
})
})
}
else {
return callback(null, xmlencode(result))
}
})
}
function resolve (expr, $ctx, callback) {
expr = trim(expr);
for (var mod in MODIFIERS) {
var regex = new RegExp('^' + mod + ':');
if (expr.match (regex)) {
expr = expr.replace(regex, '');
return MODIFIERS[mod] (expr, $ctx, callback)
}
}
if (expr.match (/^--/)) {
return callback(null, expr.replace (/^--/, ''))
}
try {
string_to_eval = []
string_to_eval.push("(function(){")
for(var property in $ctx){
string_to_eval.push(` var ${property} = $ctx['${property}']`)
}
string_to_eval.push(` return ${expr}`)
string_to_eval.push("})()")
string_to_eval = string_to_eval.join("\n")
result = eval(string_to_eval)
if(result instanceof Promise) {
result.then(
function(value) { callback(null, value) },
function(error) { callback(error, null)}
);
}
else {
return callback(null, result)
}
}
catch (err) {
$ctx.ERROR = err
return callback(err, null)
}
}
function node2tag (node) {
var tag = node._tag; delete node['_tag'];
var open = node._open; delete node['_open'];
var close = node._close; delete node['_close'];
var array = [];
for (var k in node) {
if (k.match(new RegExp('^' + TAL + ':'))) { delete node[k]; continue; }
array.push (k + '="' + node[k] + '"');
}
var att;
if (array.length == 0) { att = ''; }
else { att = array.join (' '); }
if (open && close) {
if (att) {
return '<' + tag + ' ' + att + ' />';
}
else {
return '<' + tag + ' />';
}
}
if (close) {
return '</' + tag + '>';
}
if (att) {
return '<' + tag + ' ' + att + '>';
}
else {
return '<' + tag + '>';
}
}
function trim (string) {
if(!string) { return string }
if (typeof(string) == 'string') {
return string.replace(/\r/g, '')
.replace(/\n/g, ' ')
.replace(/^\s+/, '')
.replace(/\s+$/, '');
}
else {
return string;
}
}
function has_instructions (node) {
for (var k in node) {
if (k.match(new RegExp('^' + TAL + ':'))) { return true; }
}
return false;
}
function tag (elem) {
return tag_open(elem) || tag_close(elem) || tag_self_close(elem);
}
function tag_open (elem, node) {
if (!elem) { return null }
if (typeof(elem) != 'string') { return null }
if (!elem.match(/^</)) { return null; }
if (elem.match(/^<\!/)) { return null; }
if (elem.match(/^<\//)) { return null; }
if (elem.match(/\/>$/)) { return null; }
if (elem.match(/^<\?/)) { return null; }
if (elem.match(/^</)) {
var node = extract_attributes(elem);
var capt = elem.match(/.*?([A-Za-z0-9][A-Za-z0-9_:-]*)/);
node._tag = capt[1];
node._open = true;
node._close = false;
return node;
}
return null;
}
function tag_close (elem, node) {
if (!elem) { return null }
if (typeof(elem) != 'string') { return null }
if (!elem.match(/^</)) { return null; }
if (elem.match(/^<\!/)) { return null; }
if (elem.match(/\/>$/)) { return null; }
if (elem.match(/^<\//)) {
node = node || {};
var capt = elem.match(/.*?([A-Za-z0-9][A-Za-z0-9_:-]*)/);
node._tag = capt[1];
node._open = false;
node._close = true;
return node;
}
return null;
}
function tag_self_close (elem, node) {
if (!elem) { return null }
if (typeof(elem) != 'string') { return null }
if (!elem.match(/^</)) { return null; }
if (elem.match(/^<\!/)) { return null; }
if (elem.match(/^<\//)) { return null; }
if (elem.match(/\/>$/) && elem.match(/^</)) {
var node = extract_attributes (elem);
var capt = elem.match(/.*?([A-Za-z0-9][A-Za-z0-9_:-]*)/);
node._tag = capt[1];
node._open = true;
node._close = true;
return node;
}
return null;
}
function text (elem) {
if (elem.match(/^</)) { return null; }
else { return elem; }
}
function extract_attributes (tag) {
var regex = new RegExp(RE_2, "g");
var match = tag.match(regex);
var attr = {};
for (index in match) {
var token = trim(match[index]);
var array = token.split('=');
var key = array.shift();
var val = array.join ('=');
val = val.replace(/^('|")/, '').replace (/('|")$/, '');
attr[key] = val;
}
return attr;
}
function xmlencode (string) {
if (!string) { return string; }
if (typeof(string) == 'string') {
return string.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
else {
return string;
}
}
function unescape (string) {
if (!string) { return string; }
if (typeof(string) == 'string') {
return trim (string).replace(/\;\;/g, ';').replace(/\$\$/g, '$');
}
else {
return string;
}
}
exports.process = function(xml, data, callback) {
if(callback) {
process.nextTick(function() {
Process(xml,data,function(error, result){
if(error) return callback(error)
return callback(null, result.replace(/\&structure;/g, ''))
})
})
}
else {
return new Promise(function(resolve, reject) {
process.nextTick(function() {
Process(xml, data, function(error, result){
if(error) {
return reject(error)
}
else {
return resolve(result.replace(/\&structure;/g, ''))
}
})
})
})
}
}
exports.MODIFIERS = MODIFIERS