DRYMLA template engine for Node and Express. | |
| lib/buffer.js |
Module dependencies.
|
var _ = require('underscore');
|
Buffer object
param: Object callback return: Object api: public
|
var Buffer = function(callback) {
var self = {
callback: (callback) ? callback: function() {},
indexes: 0,
count: 0,
depth: 1,
buffers: [],
str: "",
stackTrace: [],
replacements: {},
shouldEnd: false,
ended: false,
fieldId: 9,
lastIf: null,
lastIfStack: [],
trace: function(filename, tag, line, column) {
self.stackTrace.push('at ' + tag + ' (' + filename + ':' + line + ':' + column + ')');
},
exit: function() {
self.stackTrace.pop();
},
print: function(str, middleware) {
if (str !== null) {
self.str = self.str.concat((middleware) ? middleware(str) : str);
}
},
async: function(scope, callback, trace) {
var buffer = self;
buffer.indexes++;
buffer.count++;
var index = buffer.indexes,
replacementStr = '§B' + index + '§';
if (trace) buffer.trace.apply(this, trace);
var asyncBuffer = buffer[index] = {
stackTrace: _.clone(buffer.stackTrace),
str: "",
lastIf: null,
lastIfStack: [],
newFieldId: buffer.newFieldId,
trace: function(filename, tag, line, column) {
asyncBuffer.stackTrace.push('at ' + tag + ' (' + filename + ':' + line + ':' + column + ')');
},
exit: function() {
asyncBuffer.stackTrace.pop();
},
print: function(str, middleware) {
if (str !== null) {
asyncBuffer.str = asyncBuffer.str.concat((middleware) ? middleware(str) : str);
}
},
end: function() {
buffer.replacements[replacementStr] = asyncBuffer.str;
buffer.count--;
if (buffer.shouldEnd) {
buffer.end();
}
},
async: function(scope, callback) {
buffer.depth = buffer.depth + 1;
return buffer.async(scope, callback);
},
error: function(err, errBuffer) {
buffer.error(err, errBuffer);
},
startIfContext: function() {
asyncBuffer.lastIfStack.push(asyncBuffer.lastIf);
asyncBuffer.lastIf = null;
},
endIfContext: function() {
asyncBuffer.lastIf = asyncBuffer.lastIfStack.pop();
}
};
try {
callback.call(scope, asyncBuffer);
} catch(err) {
asyncBuffer.error(err, asyncBuffer);
}
if (trace) buffer.exit();
return replacementStr;
},
end: function() {
if (self.count === 0) {
for (var i = 0; i < self.depth; i++) {
for (var replacementStr in self.replacements) {
if (typeof(self.replacements[replacementStr]) == 'string') {
self.str = self.str.replace(new RegExp(replacementStr, 'g'), self.replacements[replacementStr]);
}
}
}
self.str = self.str.replace(/\s*\n/g, "\n");
if (!self.ended) {
self.ended = true;
self.callback(null, self);
}
} else {
self.shouldEnd = true;
}
return self.str;
},
error: function(err, errBuffer) {
if (!errBuffer) errBuffer = self;
err = new Error(err.message);
err.stack = err.stack.replace('\n at', '\n ' + errBuffer.stackTrace.slice( - 5).reverse().join('\n ') + '\n at');
if (!self.ended) {
self.ended = true;
self.callback(err, self);
}
},
newFieldId: function() {
self.fieldId += 1;
return 'field-' + self.fieldId.toString(16);;
},
startIfContext: function() {
self.lastIfStack.push(self.lastIf);
self.lastIf = null;
},
endIfContext: function() {
self.lastIf = self.lastIfStack.pop();
}
};
return self;
};
module.exports = Buffer;
|
| lib/dryml.js |
Module dependencies.
|
var fs = require('fs'),
toStructure = require('./toStructure'),
toFunctionSource = require('./toFunctionSource'),
Buffer = require('./buffer'),
renderFunctions = exports.renderFunctions = require('./renderFunctions'),
isValidTagname = require('./isValidName').isValidTagname,
isValidAttributename = require('./isValidName').isValidAttributename,
cache = {};
|
Defaults
|
exports.root = process.cwd() + '/views';
exports.ext = 'dryml';
exports.version = "0.1.5";
exports.cache = true;
exports.encodeEntities = true;
|
Determine actual file path for specific view file
param: String view param: String root return: String api: public
|
function realPath(view, root) {
var returnPath = (view[0] == '/') ? view: fs.realpathSync((root || exports.root) + '/' + view + '.' + exports.ext);
return returnPath;
}
|
Render DRYML string to a buffer
param: String str param: Object options param: Object callback return: Object api: public
|
var render = exports.render = function(str, options, callback) {
var path,
compiled,
buffer = Buffer(callback);
try {
options = options || {};
options.locals = options.locals || {};
options.cache = (options.cache === null || typeof(options.cache) == 'undefined') ? exports.cache : options.cache;
options.encodeEntities = (options.encodeEntities === null || typeof(options.encodeEntities) == 'undefined') ? exports.encodeEntities : options.encodeEntities;
options.str = options.str || str;
if (options.filename && options.filename.indexOf('/') > 0) {
var lastSlashIndex = options.filename.lastIndexOf('/');
options.root = exports.root + '/' + options.filename.slice(0, lastSlashIndex);
options.filename = options.filename.slice(lastSlashIndex + 1);
} else {
options.root = exports.root;
}
var context = options.scope || {};
if (options.debug || options.cache != true) {
cache = {};
}
if (!options.str && options.filename) {
path = options.filepath = realPath(options.filename, options.root);
compiled = cache[path];
if (!compiled) {
if (options.debug) console.log('Compiling view at: ' + path);
if (!options.str) {
options.str = '' + fs.readFileSync(path);
}
compile(options.str, options,
function(err, compiled) {
if (err) {
callback(err);
} else {
try {
cache[path] = compiled;
compiled.fn(context, compiled.taglib, options.locals, buffer);
} catch(err) {
buffer.error(err);
}
}
});
} else {
compiled.fn(context, compiled.taglib, options.locals, buffer);
}
} else {
compile(options.str, options,
function(err, compiled) {
if (err) {
callback(err);
} else {
try {
compiled.fn(context, compiled.taglib, options.locals, buffer);
} catch(err) {
buffer.error(err);
}
}
});
}
} catch(err) {
buffer.error(err);
}
return buffer;
};
|
Render view to the given response (Express)
param: String view param: Object options param: Object response api: public
|
var renderView = exports.renderView = function(view, options, response) {
options = options || {};
options.filename = view;
render(null, options, (typeof(response) == 'function') ? response :
function(err, buffer) {
if (err) {
response.req.next(err);
} else {
if (options.debug) console.log(buffer.str);
response.send(buffer.str);
}
});
};
|
Compile the given dryml string into an object with function with compiled taglibs
param: String str param: String type param: Object options param: Function callback api: public
|
var compile = exports.compile = function(str, options, callback) {
var type = (options.taglib) ? 'taglib' : 'page';
toStructure(str, options,
function(err, structure) {
if (err) {
callback(err);
} else {
if (type == 'page') {
var corePath = realPath('core', __dirname + '/support');
structure.unshift({
type: "tag",
element: "taglib",
attrs: {
src: corePath
}
});
}
importTaglibs({},
structure, options,
function(err, taglib) {
if (err) {
callback(err);
} else {
if (type == 'page') {
var source = toFunctionSource(structure, 'page', options),
fn = new Function('locals, taglib, buffer, _renderFunctions', source);
if (options.debug) {
console.log('-- Debug:');
console.log(JSON.stringify(structure, null, ' '));
console.log(source);
console.log(JSON.stringify(taglib, null, ' '));
console.log('--');
}
callback(null, {
fn: function(context, taglib, locals, buffer) {
fn.call(context, locals, taglib, buffer, renderFunctions);
},
taglib: taglib
});
} else {
callback(null, {
fn: function() {},
taglib: taglib
});
}
}
});
}
});
};
|
| lib/isValidName.js |
HTML tags belonging to strict XHTML, not reserved tag names and characters
(see http://htmldog.com/reference/htmltags/, plus additional tags from http://remysharp.com/2009/01/07/html5-enabling-script/)
|
var html = exports.html = "a,abbr,acronym,address,area,b,base,bdo,big,blockquote,body,br,button,caption,cite,code,col,colgroup,dd,del,dfn,div,dl,DOCTYPE,dt,em,fieldset,form,h1,h2,h3,h4,h5,h6,head,html,hr,i,img,input,ins,kbd,label,legend,li,link,map,meta,noscript,object,ol,optgroup,option,p,param,pre,q,samp,script,select,small,span,strong,style,sub,sup,table,tbody,td,textarea,tfoot,th,thead,title,tr,tt,ul,var,abbr,article,aside,audio,canvas,details,figcaption,figure,footer,header,hgroup,mark,meter,nav,output,progress,section,summary,time,video".split(','),
restricted = exports.restricted = "def,tagbody,attr,taglib,document".split(','),
validTagCharacters = exports.validTagCharacters = new RegExp("^[A-Za-z]([A-Za-z0-9._\-])*"),
validAttributeCharacters = exports.validAttributeCharacters = new RegExp("^[a-z]([A-Za-z0-_])*"),
reservedWordsJS = "break,case,catch,continue,debugger,default,delete,do,else,finally,for,function,if,in,instanceof,new,return,switch,this,throw,try,typeof,var,void,while,with,class,enum,export,extends,import,super,implements,interface,let,package,private,protected,public,static,yield".split(','),
reservedWordsDRYML = "attributes,taglib,buffer,tagbody,sup,locals,with".split(','),
reservedWords = ['global', 'process', 'require', 'module', 'console', 'sys', 'fs'].concat(reservedWordsJS).concat(reservedWordsDRYML);
exports.isValidTagname = function(name) {
if (html.indexOf(name) == -1 && restricted.indexOf(name) == -1 && validTagCharacters.test(name)) {
return true;
} else {
return false;
}
};
exports.isValidAttributename = function(name) {
if (reservedWords.indexOf(name) == -1 && validAttributeCharacters.test(name)) {
return true;
} else {
return false;
}
}
|
| lib/renderFunctions.js |
Tags to self-close (see http://www.w3schools.com/tags/default.asp)
|
var selfClosingTags = ["area", "base", "basefont", "br", "col", "frame", "hr", "img", "input", "link", "meta", "param"];
|
Module dependencies.
|
var isValidAttributename = require('./isValidName').isValidAttributename;
|
Functions used during rendering
|
var self = module.exports = {
_encode: require('JS-Entities').xml.encode,
'_': require('underscore'),
_mergeAttributes: function(tagAttributes, contextAttributes, definedKeys) {
if (typeof(tagAttributes['merge-attrs'].split) == 'function' && typeof(contextAttributes) == 'object') {
var mergeKeys = tagAttributes['merge-attrs'].split(','),
allKeys = Object.keys(contextAttributes);
delete(tagAttributes['merge-attrs']);
var includeUnspecified = mergeKeys.indexOf('*');
if (includeUnspecified > -1) {
delete(mergeKeys[includeUnspecified]);
includeUnspecified = true;
} else {
includeUnspecified = false;
}
var includeAll = (mergeKeys.indexOf('') > -1);
for (var attr in contextAttributes) {
if (mergeKeys.indexOf(attr) > -1 || (attr != 'class' && (includeAll || (includeUnspecified && definedKeys.indexOf(attr) == -1))) && !tagAttributes[attr] && contextAttributes[attr])
tagAttributes[attr] = contextAttributes[attr];
}
}
},
_mergeClassAttribute: function(tagAttributes, contextAttributes ) {
var classAttribute = tagAttributes['merge-class'],
contextClassAttribute = contextAttributes['class'];
delete(tagAttributes['merge-class']);
if (self._.isString(classAttribute)) {
if (self._.isString(contextClassAttribute) ) {
tagAttributes['class'] = self._.uniq(self._.select(contextClassAttribute.split(' ').concat(classAttribute.split(' ')), function(v){return v;})).join(' ');
} else {
tagAttributes['class'] = classAttribute;
}
}
},
_applyTag: function(context, taglib, name, attributes, buffer, callback) {
var tag = taglib[name];
if (tag) {
if (attributes.obj) {
context = attributes.obj;
delete(attributes.obj);
}
var definedAttributes = {
attributes: attributes,
_keys: tag.attributes
};
for (var i in tag.attributes) {
var key = tag.attributes[i];
definedAttributes[key] = (typeof(attributes[key]) != 'undefined') ? attributes[key] : null;
}
tag.fn.call(context, this, definedAttributes, taglib, buffer, callback);
} else {
var attributesStr = '';
for (var attr in attributes) {
if (attributes[attr] != null) {
attributesStr = attributesStr.concat(attr + '="' + self._encode(attributes[attr]) + '" ');
}
}
if (attributesStr.length > 0) attributesStr = ' ' + attributesStr.trim();
if (callback) {
buffer.print('<' + name + attributesStr + '>');
callback.call(context, buffer);
buffer.print('</' + name + '>');
} else {
if (selfClosingTags.indexOf(name) > -1) {
buffer.print('<' + name + attributesStr + '/>');
} else {
buffer.print('<' + name + attributesStr + '></' + name + '>');
}
}
}
},
_argumentsAsObject: function(args, names) {
var obj = {},
attrs = (names) ? names.replace(/\s/, '').split(',') : [];
for (var i in attrs) {
if (isValidAttributename(attrs[i])) {
obj[attrs[i]] = args[i] || null;
}
}
return obj;
},
_clone: function(obj, mergeObj) {
var out = self._.clone(obj);
if (typeof(mergeObj) === 'object') {
for (i in mergeObj) {
out[i] = arguments.callee(mergeObj[i]);
}
}
return out;
},
_deepCopy: function(obj, mergeObj) {
if (Object.prototype.toString.call(obj) === '[object Array]') {
var out = [],
i = 0,
len = obj.length;
for (; i < len; i++) {
out[i] = arguments.callee(obj[i]);
}
return out;
}
if (typeof obj === 'object' && obj != null) {
var out = {},
i;
if (typeof mergeObj === 'object') {
for (i in obj) {
out[i] = arguments.callee(obj[i]);
}
for (i in mergeObj) {
out[i] = arguments.callee(mergeObj[i]);
}
} else {
for (i in obj) {
out[i] = obj[i];
}
}
return out;
}
return obj;
},
_functionInScope: function(scope, fn) {
return function() {
fn.apply(scope, arguments);
};
}
};
|
| lib/toFunctionSource.js |
Parse the given dryml structure, returning the function source.
|
var toFunctionSource = module.exports = function(structure, type, options) {
var tmp;
var end = ['}'];
switch (type) {
case 'page':
tmp = ["try { with(_renderFunctions) { with (_clone(locals, { attributes: locals, _attributes:{} })) { "];
end = ['}} buffer.end(); } catch (err) { buffer.error(err); }'];
break;
case 'definition':
tmp = ["try { with(_renderFunctions) { with (_clone(attributes, { _attributes:{} })) { "];
end = ['}}} catch (err) { buffer.error(err); }'];
break;
default:
tmp = ["with ({ _attributes:{} }) {"];
break;
}
tmp.push('var filepath = "' + options.filepath + '";');
for (var i in structure) {
var tag = structure[i];
switch (tag.type) {
case 'text':
tmp.push('buffer.print(unescape("' + escape(tag.content) + '"), ' + ((options.encodeEntities === true && !tag.encoded) ? '_encode' : 'null') + ');');
break;
case 'comment':
tmp.push('buffer.print(unescape("' + escape('<!-- ' + tag.content.trim() + ' -->') + '"));');
break;
default:
tmp.push('buffer.trace(filepath, "' + tag.element + '", ' + tag.line + ', ' + tag.column + ');');
switch (tag.element) {
case '%':
var ejsType = tag.attrs.ejs[0],
ejs = tag.attrs.ejs.slice(1, tag.attrs.ejs.length - 1).trim();
switch (ejsType) {
case '=':
tmp.push('buffer.print(' + ejs + ');');
break;
case '?':
tmp.push('buffer.print(buffer.async(this, function(buffer){ ' + ejs + ' }));');
break;
default:
tmp.push('\n' + ejsType + ejs + '\n');
break;
}
break;
case 'def':
break;
case 'taglib':
break;
case 'tagbody':
tmp.push('if (tagbody) { tagbody.call(this, buffer); };');
break;
case 'with':
var attrs = (tag.attrs && tag.attrs.attrs) ? tag.attrs.attrs : '',
attr = (tag.attrs && tag.attrs.async) ? tag.attrs.async : (tag.attrs && tag.attrs.obj) ? tag.attrs.obj : '',
ejs = (attr[attr.length - 1] == '}' && attr.indexOf('%{') === 0) ? attr.slice(2, attr.length - 1) : null,
async = (tag.attrs && tag.attrs.async);
tmp.push('(function(){');
var tagbodyVar = 'var withBody = function(){ with(_argumentsAsObject(arguments, unescape("' + escape(attrs) + '"))) {' + toFunctionSource(tag.children, 'with', options) + '} };';
if (ejs) {
if (async) {
tmp.push('var _self = this;');
tmp.push('buffer.print(buffer.async(this, function(buffer){ ' + tagbodyVar + 'withBody = _functionInScope(_self, withBody);' + ejs + '}));')
} else {
tmp.push(tagbodyVar);
tmp.push('withBody.call(' + ejs + ');');
}
} else {
tmp.push(tagbodyVar);
if (attr) {
tmp.push('withBody.call(unescape("' + escape(attr) + '"));');
} else {
tmp.push('withBody.call(this);');
}
}
tmp.push('}).call(this);');
break;
default:
if (tag.children) {
for (var j in tag.children) {
var child = tag.children[j];
if (child.type == 'tag' && child.prefix == 'attr') {
var name = child.element;
if (!tag.attrs) tag.attrs = {};
tag.attrs[name] = '%{ ' +
'buffer.async(this, function(buffer){ ' + toFunctionSource(child.children, 'attribute', options) + 'buffer.end(); },' +
' [filepath, "' + child.prefix + ':' + child.element + '", ' + child.line + ', ' + child.column + '])' +
' }';
delete(tag.children[j]);
}
}
}
tmp.push('_attributes = {');
for (var key in tag.attrs) {
var attr = tag.attrs[key];
if (attr[attr.length - 1] == '}' && attr.indexOf('%{') === 0) {
tmp.push('"' + key + '": (' + attr.slice(2, attr.length - 1) + '), ');
} else if (attr[attr.length - 1] == '}' && attr.indexOf('#{') === 0) {
tmp.push('"' + key + '": buffer.error(new Error("#{} encountered")), ');
} else {
tmp.push('"' + key + '": unescape("' + escape(attr) + '"), ');
}
}
tmp.push('};');
tmp.push('if (typeof(_attributes["merge-class"]) != "undefined") _mergeClassAttribute(_attributes, attributes);')
tmp.push('if (typeof(_attributes["merge-attrs"]) != "undefined") _mergeAttributes(_attributes, attributes, _keys);')
if (tag.children && tag.children.length > 0) {
tmp.push('_applyTag(this, taglib, "' + tag.element + '", _attributes, buffer, function(buffer){');
tmp.push(toFunctionSource(tag.children, 'tagbody', options));
tmp.push('});');
} else {
tmp.push('_applyTag(this, taglib, "' + tag.element + '", _attributes, buffer, null);');
}
break;
}
tmp.push('buffer.exit();');
break;
}
}
tmp.push(end);
return tmp.join('');
};
|
| lib/toStructure.js |
Module dependencies.
|
var sys = require('sys'),
fs = require('fs'),
xml = require("node-xml");
|
Use XML parser to convert into object structure
param: String str param: Object options param: Function callback api: public
|
module.exports = function(str, options, callback) {
str = prepareString(str);
var stack = [{
children: []
}],
parser = new xml.SaxParser(function(cb) {
function onCharacters(chars, encoded) {
chars = (options.trimWhitespace) ? chars.trim() : chars;
if (chars.length > 0) {
stack[stack.length - 1].children.push({
type: 'text',
content: chars,
line: parser.getLineNumber(),
column: parser.getColumnNumber(),
encoded: encoded
});
}
}
cb.onStartDocument(function() {
});
cb.onEndDocument(function() {
callback(null, stack[0].children[0].children);
});
cb.onStartElementNS(function(elem, attrs, prefix, uri, namespaces) {
var attributes = {};
if (elem == '%') {
attributes['ejs'] = unescape(attrs[0][1]);
} else {
for (var attr in attrs) {
var value = attrs[attr][1];
if (value[value.length - 1] == '}' && value.indexOf('%{') === 0) {
value = unescape(value);
}
attributes[attrs[attr][0]] = value;
}
}
current = {
element: elem,
type: 'tag',
attrs: attributes,
prefix: prefix,
uri: uri,
namespaces: namespaces,
line: parser.getLineNumber(),
column: parser.getColumnNumber(),
children: []
};
stack.push(current);
});
cb.onEndElementNS(function(elem, prefix, uri) {
var top = stack.pop();
stack[stack.length - 1].children.push(top);
});
cb.onCharacters(onCharacters);
cb.onCdata(function(chars) {
onCharacters(chars, true);
});
cb.onComment(function(msg) {
stack[stack.length - 1].children.push({
type: 'comment',
content: msg,
line: parser.getLineNumber(),
column: parser.getColumnNumber(),
});
});
cb.onWarning(function(msg) {
});
cb.onError(function(msg) {
var filename = (options.filepath) ? options.filepath + ':': '',
description = '\n at ' + stack[stack.length - 1].element + ' (' + filename + parser.getLineNumber() + ':' + parser.getColumnNumber() + ')';
var err = new Error(msg.replace(':', ''));
err.stack = err.stack.replace('\n at', description + '\n at');
callback(err);
});
});
parser.parseString(str);
}
|