DRYMLA template engine for Node and Express. | |
| lib/buffer.js |
Buffer object
param: Object callback return: Object api: public
|
var Buffer = function(callback) {
var self = {
callback: (callback) ? callback : function(){},
indexes: 0,
count: 0,
buffers: [],
str: "",
stackTrace:[],
replacements: {},
shouldEnd: false,
trace: function(filename, tag, line, column) {
self.stackTrace.push('at ' + tag + ' (' + filename + ':' + line + ':' + column + ')');
},
print: function(str) {
if (str != null) self.str = self.str.concat(str);
},
async: function(scope, callback) {
var buffer = self;
buffer.indexes++;
buffer.count++;
var index = buffer.indexes,
replacementStr = '§B' + index + '§';
var asyncBuffer = buffer[index] = {
stackTrace: [],
str: "",
trace: function(filename, tag, line, column) {
asyncBuffer.stackTrace.push(' at ' + tag + ' (' + filename + ':' + line + ':' + column + ')');
},
print: function(str) {
asyncBuffer.str = asyncBuffer.str.concat(str);
},
end: function() {
self.replacements[replacementStr] = asyncBuffer.str;
buffer.count--;
if (buffer.shouldEnd) {
buffer.end();
}
},
async: function(scope, callback) {
return buffer.async(scope, callback);
}
};
callback.call(scope, asyncBuffer);
return replacementStr;
},
end: function() {
if (self.count == 0) {
for (var replacementStr in self.replacements) {
self.str = self.str.replace(new RegExp(replacementStr), self.replacements[replacementStr]);
}
for (var replacementStr in self.replacements) {
self.str = self.str.replace(new RegExp(replacementStr), self.replacements[replacementStr]);
}
self.str = self.str.replace(/\s*\n/g, "\n");
self.callback(null, self);
} else {
self.shouldEnd = true;
}
return self.str;
},
error: function(err) {
var err = new Error(err.message + '\n' + self.stackTrace.slice(-5).reverse().join('\n'));
self.callback(err, self);
}
}
return self;
}
module.exports = Buffer;
|
| lib/dryml.js |
Module dependencies.
|
var fs = require('fs'),
toStructure = require('./toStructure'),
toFunctionSource = require('./toFunctionSource'),
Buffer = require('./buffer'),
renderFunctions = require('./renderFunctions'),
isValidTagname = require('./isValidTagname').isValidTagname,
cache = {};
|
Defaults
|
exports.root = process.cwd() + '/views';
exports.ext = 'dryml';
|
Determine actual file path for specific view file
param: String view param: String root return: String api: public
|
function realPath(view, root) {
return (view[0] == '/') ? view : fs.realpathSync((root || exports.root) + '/' + view + '.' + exports.ext);
}
|
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 compiled,
buffer = Buffer(callback);
options = options || {};
options.locals = options.locals || {};
options.str = options.str || str;
options.root = (options.filename && options.filename.indexOf('/') > 0) ? options.filename.slice(0, options.filename.lastIndexOf('/')) : exports.root;
var context = options.scope || {};
if (options.filename && !options.debug) {
var path = realPath(options.filename, options.root);
compiled = cache[path];
if (!compiled) {
if (options.debug) console.log('Compiling view at: ' + path);
if (!options.str) {
var path = realPath(options.filename, options.root);
options.str = '' + fs.readFileSync(path);
}
compile(options.str, 'page', options, function(compiled){
cache[path] = compiled;
compiled.function(context, compiled.taglib, options.locals, buffer);
})
} else {
compiled.function(context, compiled.taglib, options.locals, buffer);
}
} else {
if (!options.str) {
var path = realPath(options.filename, options.root);
options.str = '' + fs.readFileSync(path);
}
compile(options.str, 'page', options, function(compiled){
compiled.function(context, compiled.taglib, options.locals, buffer);
})
}
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, function(err, buffer){
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, type, options, callback) {
toStructure(str, options, function(structure){
if (type == 'page') {
var corePath = realPath('core', __dirname + '/support');
structure.unshift({
type: "tag",
element: "taglib",
attrs: {
src: corePath
}
});
}
importTaglibs({}, structure, options, function(taglib){
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({ function: function(context, taglib, locals, buffer){
fn.call(context, locals, taglib, buffer, renderFunctions);
}, taglib: taglib });
} else {
callback({ function: function(){}, taglib: taglib});
}
});
});
}
|
| lib/isValidTagname.js |
HTML tags belonging to strict XHTML, not reserved tag names and characters
(see http://htmldog.com/reference/htmltags/)
|
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".split(','),
restricted = exports.restricted = "def,tagbody,attr,taglib,document".split(','),
validCharacters = exports.validCharacters = new RegExp("^[A-Za-z]([A-Za-z0-9._]|'-')*");
exports.isValidTagname = function(name) {
if (html.indexOf(name) == -1 && restricted.indexOf(name) == -1 && validCharacters.test(name)) {
return true;
} else {
return false;
}
}
|
| lib/renderFunctions.js |
Functions used during rendering
|
module.exports = {
_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 };
for (var i in tag.attributes) {
var key = tag.attributes[i];
definedAttributes[key] = (typeof(attributes[key]) != 'undefined') ? attributes[key] : null;
}
tag.function.call(context, this, definedAttributes, taglib, buffer, callback);
} else {
var attributesStr = '';
for (var attr in attributes) {
attributesStr = attributesStr.concat(attr + '="' + attributes[attr] + '" ')
}
if (attributesStr.length > 0) attributesStr = ' ' + attributesStr.trim();
if (callback) {
buffer.print('<' + name + attributesStr + '>');
callback.call(context);
buffer.print('</' + name + '>');
} else {
buffer.print('<' + name + attributesStr + '/>');
}
}
},
_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') {
var out = {}, i;
for ( i in obj ) {
out[i] = arguments.callee(obj[i]);
}
if (typeof mergeObj === 'object') {
for (i in mergeObj) {
out[i] = arguments.callee(mergeObj[i]);
}
}
return out;
}
return obj;
}
}
|
| 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 (_deepCopy(locals, { _attributes:{} })) {"];
end = ['}} buffer.end(); } catch (err) { buffer.error(err); }'];
tmp = ["with(_renderFunctions) { with (_deepCopy(locals, { _attributes:{} })) {"];
end = ['}} buffer.end();'];
break;
case 'definition':
tmp = ["with(_renderFunctions) { with (_deepCopy(attributes, { _attributes:{} })) {"];
end = ['}}'];
break;
default:
tmp = ["with ({ _attributes:{} }) {"];
break;
}
for (var i in structure) {
var tag = structure[i];
switch(tag.type) {
case 'text':
tmp.push('buffer.print(unescape("' + escape(tag.content) + '"));');
break;
case 'comment':
tmp.push('buffer.print(unescape("' + escape('<!-- ' + tag.content.trim() + ' -->') + '"));');
break;
default:
tmp.push('buffer.trace("' + options.filename + '", "' + 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();
if (options.debug) console.log('ASYNC: ' + ejs);
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 (callback) { callback.call(this); };')
break;
default:
if (tag.children) {
for (var i in tag.children) {
var child = tag.children[i];
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(); })' + ' }';
delete(tag.children[i]);
}
}
}
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 {
tmp.push('"' + key + '": "' + attr + '", ');
}
}
tmp.push('};');
if (tag.children && tag.children.length > 0) {
tmp.push('_applyTag(this, taglib, "' + tag.element + '", _attributes, buffer, function(){');
tmp.push(toFunctionSource(tag.children, 'tagbody', options));
tmp.push('});')
} else {
tmp.push('_applyTag(this, taglib, "' + tag.element + '", _attributes, buffer, null);');
}
break;
}
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) {
cb.onStartDocument(function() {
});
cb.onEndDocument(function() {
callback(stack[0].children[0].children);
});
cb.onStartElementNS(function(elem, attrs, prefix, uri, namespaces) {
var attributes = {};
if (elem == '%') {
attributes['ejs'] = attrs[0][1].replace(/§/g, '"');
} else {
for (var attr in attrs) {
attributes[attrs[attr][0]] = attrs[attr][1];
}
}
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(function(chars) {
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(),
});
}
});
cb.onCdata(function(cdata) {
});
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 = msg + '\n at <' + stack[stack.length - 1].element + '> (' + filename + parser.getLineNumber() + ':' + parser.getColumnNumber() + ')';
if (options.callbackErrors) {
callback([{
type: 'error',
description: description,
element: stack[stack.length - 1].element,
line: parser.getLineNumber(),
column: parser.getColumnNumber()
}]);
} else {
throw new Error(description);
}
});
});
parser.parseString(str);
}
|