DRYML

A template engine for Node and Express.

buffer

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;

dryml

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);
		// Use cached if available, otherwise compile
		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) {
	// Convert string to structure
	toStructure(str, options, function(structure){
	    if (type == 'page') {
	        // Inject core taglib
    	    var corePath = realPath('core', __dirname + '/support');
            structure.unshift({
                type: "tag",
                element: "taglib",
                attrs: {
                    src: corePath
                }
            });
        }
		// Import all taglibs and definitions in structure into this taglib
		importTaglibs({}, structure, options, function(taglib){
			if (type == 'page') {	
				// Parse rest of file for buffer output and compile into function
				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});
			}		
		});
	});
}

isValidTagname

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;
	}
}

renderFunctions

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 &lt; 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;
	}		
}

toFunctionSource

lib/toFunctionSource.js

Parse the given dryml structure, returning the function source.

  • param: Object structure

  • param: String type [page, definition, attribute, tagbody]

  • param: Object options

  • return: String

  • api: public

var toFunctionSource = module.exports = function(structure, type, options) {
	var tmp;
	var end = ['}'];
	
	switch (type) {
		case 'page':
			tmp = [&quot;try { with(_renderFunctions) { with (_deepCopy(locals, { _attributes:{} })) {&quot;];
			end = ['}} buffer.end(); } catch (err) { buffer.error(err); }'];
			tmp = [&quot;with(_renderFunctions) { with (_deepCopy(locals, { _attributes:{} })) {&quot;];
			end = ['}} buffer.end();'];
		break;
		case 'definition':
			tmp = [&quot;with(_renderFunctions) { with (_deepCopy(attributes, { _attributes:{} })) {&quot;];
			end = ['}}'];
		break;
		default:
			tmp = [&quot;with ({ _attributes:{} }) {&quot;];
		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':
						// Ignore
						// TODO: Error
					break;
					case 'taglib':
						// Ignore
						// TODO: Error
					break;	
					case 'tagbody':
						tmp.push('if (callback) { callback.call(this); };')
					break;
					default:
						// Extract attributes from direct children											
						if (tag.children) {
							for (var i in tag.children) {
								var child = tag.children[i];
								if (child.type == 'tag' &amp;&amp; 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]);
								}								
							}
						}

						// Parse attributes with ejs and merge
						tmp.push('_attributes = {');	
						for (var key in tag.attrs) {
							var attr = tag.attrs[key];
							if (attr[attr.length - 1] == '}' &amp;&amp; attr.indexOf('%{') == 0) {
								tmp.push('"' + key + '": (' + attr.slice(2, attr.length - 1) + '), ');
							} else {
								tmp.push('"' + key + '": "' + attr + '", ');
							}
						}						
						tmp.push('};');
						// if (options.debug) tmp.push('buffer.print(JSON.stringify(_attributes));');
						if (tag.children &amp;&amp; tag.children.length &gt; 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('')
};

toStructure

lib/toStructure.js

Module dependencies.

var sys = require('sys'),
fs = require('fs'),
xml = require(&quot;node-xml&quot;);

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() {
            // Ignore
            });
        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 &gt; 0) {
                stack[stack.length - 1].children.push({
                    type: 'text',
                    content: chars,
                    line: parser.getLineNumber(),
                    column: parser.getColumnNumber(),
                });
            }
        });
        cb.onCdata(function(cdata) {
            // Ignore
            });
        cb.onComment(function(msg) {
            stack[stack.length - 1].children.push({
                type: 'comment',
                content: msg,
                line: parser.getLineNumber(),
                column: parser.getColumnNumber(),
            });
        });
        cb.onWarning(function(msg) {
            // Ignore
            });
        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);
}