Comparing version 0.1.4 to 0.1.5
1146
ltl.js
@@ -7,304 +7,304 @@ /** | ||
// Some HTML tags won't have end tags. | ||
var selfClosePattern = /^(!DOCTYPE|area|base|br|hr|img|input|link|meta|-|\/\/)(\b|$)/; | ||
// Some HTML tags won't have end tags. | ||
var selfClosePattern = /^(!DOCTYPE|area|base|br|hr|img|input|link|meta|-|\/\/)(\b|$)/; | ||
// Supported control keywords (usage appears like tags). | ||
var controlPattern = /^(for|if|else|else if)\b/; | ||
// Supported control keywords (usage appears like tags). | ||
var controlPattern = /^(for|if|else|else if)\b/; | ||
// Supported command keywords. | ||
var commandPattern = /^(call|get|set)\b/; | ||
// Supported command keywords. | ||
var commandPattern = /^(call|get|set)\b/; | ||
// JavaScript tokens that don't need contextVar prepended for interpolation. | ||
// TODO: Flesh out this list? | ||
var jsPattern = /^(true|false|null|NaN|Infinity|window|location|Math|console)$/; | ||
// JavaScript tokens that don't need contextVar prepended for interpolation. | ||
// TODO: Flesh out this list? | ||
var jsPattern = /^(true|false|null|NaN|Infinity|window|location|Math|console)$/; | ||
// Stores available single character variable names. | ||
var varCharacters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_$'; | ||
// Stores available single character variable names. | ||
var varCharacters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_$'; | ||
// Remove starting/ending whitespace. | ||
function trim(text) { | ||
return text.replace(/(^\s+|\s+$)/g, ''); | ||
} | ||
// Remove starting/ending whitespace. | ||
function trim(text) { | ||
return text.replace(/(^\s+|\s+$)/g, ''); | ||
} | ||
// Remove starting/ending whitespace. | ||
function repeat(text, times) { | ||
return (new Array(times + 1)).join(text); | ||
} | ||
// Remove starting/ending whitespace. | ||
function repeat(text, times) { | ||
return (new Array(times + 1)).join(text); | ||
} | ||
// Escape single quotes with a backslash. | ||
function escapeSingleQuotes(text) { | ||
return text.replace(/'/g, "\\'"); | ||
} | ||
// Escape single quotes with a backslash. | ||
function escapeSingleQuotes(text) { | ||
return text.replace(/'/g, "\\'"); | ||
} | ||
// Escape text with possible line breaks for appending to a string. | ||
function escapeBlock(text) { | ||
return escapeSingleQuotes(text).replace(/\n/g, '\\n'); | ||
} | ||
// Escape text with possible line breaks for appending to a string. | ||
function escapeBlock(text) { | ||
return escapeSingleQuotes(text).replace(/\n/g, '\\n'); | ||
} | ||
// Public API. | ||
var ltl = { | ||
// Public API. | ||
var ltl = { | ||
// Allow users to see what version of ltl they're using. | ||
version: '0.1.4', | ||
// Allow users to see what version of ltl they're using. | ||
version: '0.1.5', | ||
// Store all of the templates that have been compiled. | ||
cache: {}, | ||
// Store all of the templates that have been compiled. | ||
cache: {}, | ||
// Default compile options. | ||
_options: { | ||
tabWidth: 4, | ||
outputVar: 'o', | ||
contextVar: 'c', | ||
partsVar: 'p' | ||
}, | ||
// Default compile options. | ||
_options: { | ||
tabWidth: 4, | ||
outputVar: 'o', | ||
contextVar: 'c', | ||
partsVar: 'p' | ||
}, | ||
// Change compile options. | ||
setOption: function (name, value) { | ||
this._options[name] = value; | ||
}, | ||
// Change compile options. | ||
setOption: function (name, value) { | ||
this._options[name] = value; | ||
}, | ||
// Create a function that accepts context and returns markup. | ||
compile: function (code, options) { | ||
// Create a function that accepts context and returns markup. | ||
compile: function (code, options) { | ||
// Copy default options. | ||
options = options || {}; | ||
for (var name in this._options) { | ||
if (typeof options[name] == 'undefined') { | ||
options[name] = this._options[name]; | ||
} | ||
} | ||
// Copy default options. | ||
options = options || {}; | ||
for (var name in this._options) { | ||
if (typeof options[name] == 'undefined') { | ||
options[name] = this._options[name]; | ||
} | ||
} | ||
// Don't allow context/output/parts vars to become user vars. | ||
var vars = varCharacters; | ||
vars = vars.replace(options.contextVar, ''); | ||
vars = vars.replace(options.outputVar, ''); | ||
vars = vars.replace(options.partsVar, ''); | ||
// Don't allow context/output/parts vars to become user vars. | ||
var vars = varCharacters; | ||
vars = vars.replace(options.contextVar, ''); | ||
vars = vars.replace(options.outputVar, ''); | ||
vars = vars.replace(options.partsVar, ''); | ||
if (options.space) { | ||
options.space = escapeBlock(options.space); | ||
} | ||
if (options.space) { | ||
options.space = escapeBlock(options.space); | ||
} | ||
// Find out if we're in the browser. | ||
var inBrowser = false; | ||
try { | ||
inBrowser = window.document.body.tagName == 'BODY'; | ||
} | ||
catch (e) { | ||
} | ||
// Find out if we're in the browser. | ||
var inBrowser = false; | ||
try { | ||
inBrowser = window.document.body.tagName == 'BODY'; | ||
} | ||
catch (e) { | ||
} | ||
// Replace carriage returns for Windows compatibility. | ||
code = code.replace(/\r/g, ''); | ||
// Replace carriage returns for Windows compatibility. | ||
code = code.replace(/\r/g, ''); | ||
// Be lenient with mixed tabs and spaces, assuming tab width of 4. | ||
var tabReplacement = Array(options.tabWidth + 1).join(' '); | ||
code = code.replace(/\t/g, tabReplacement); | ||
// Be lenient with mixed tabs and spaces, assuming tab width of 4. | ||
var tabReplacement = Array(options.tabWidth + 1).join(' '); | ||
code = code.replace(/\t/g, tabReplacement); | ||
// We'll auto-detect tab width. | ||
var currentTabWidth = 0; | ||
// We'll auto-detect tab width. | ||
var currentTabWidth = 0; | ||
// Initialize the code, and start at level zero with no nesting. | ||
var lines = code.split('\n'); | ||
// Initialize the code, and start at level zero with no nesting. | ||
var lines = code.split('\n'); | ||
var indent = 0; | ||
var stack = []; | ||
var mode = 'html'; | ||
var previousTag; | ||
var hasHtmlOutput = false; | ||
var tagDepth = 0; | ||
var output = 'var ' + options.outputVar + "='"; | ||
var indent = 0; | ||
var stack = []; | ||
var mode = 'html'; | ||
var previousTag; | ||
var hasHtmlOutput = false; | ||
var tagDepth = 0; | ||
var output = 'var ' + options.outputVar + "='"; | ||
var varIndex = 0; | ||
var escapeVar = false; | ||
var loopVars = []; | ||
var varIndex = 0; | ||
var escapeVar = false; | ||
var loopVars = []; | ||
function appendText(textMode, text) { | ||
if (textMode != mode) { | ||
if (mode == 'html') { | ||
output += "'" + (text == '}' ? '' : ';'); | ||
} else { | ||
output += options.outputVar + "+='"; | ||
} | ||
mode = textMode; | ||
} | ||
if (mode == 'html') { | ||
text = interpolate(text); | ||
} | ||
output += text; | ||
} | ||
function appendText(textMode, text) { | ||
if (textMode != mode) { | ||
if (mode == 'html') { | ||
output += "'" + (text == '}' ? '' : ';'); | ||
} else { | ||
output += options.outputVar + "+='"; | ||
} | ||
mode = textMode; | ||
} | ||
if (mode == 'html') { | ||
text = interpolate(text); | ||
} | ||
output += text; | ||
} | ||
function startBlock(filter) { | ||
blockIndent = indent + 1; | ||
blockFilter = filter; | ||
blockLines = []; | ||
} | ||
function startBlock(filter) { | ||
blockIndent = indent + 1; | ||
blockFilter = filter; | ||
blockLines = []; | ||
} | ||
function appendBlock() { | ||
var text = blockLines.join('\n'); | ||
function appendBlock() { | ||
var text = blockLines.join('\n'); | ||
// Reset the blockage. | ||
blockIndent = 0; | ||
// Reset the blockage. | ||
blockIndent = 0; | ||
// If we're in a "call" block, compile the contents. | ||
if (blockFilter == 'call') { | ||
appendText('html', "'+this['" + blockName + "'].call(this," + options.contextVar + (text ? ',' + ltl.compile(text) : '') + ")+'"); | ||
return; | ||
} | ||
// For a "set" block. | ||
else if (blockFilter == 'set' || blockFilter == 'set:') { | ||
var block; | ||
if (blockFilter == 'set') { | ||
block = ltl.compile(text).toString(); | ||
} else { | ||
block = "function(){return '" + escapeBlock(text) + "'}"; | ||
} | ||
blockSets.push("'" + escapeSingleQuotes(blockName) + "':" + block); | ||
return; | ||
} | ||
// If there's a filter, get its module. | ||
else if (blockFilter) { | ||
if (blockFilter == 'coffee') { | ||
blockFilter = 'coffee-script'; | ||
} | ||
else if (blockFilter == 'md') { | ||
blockFilter = 'markdown'; | ||
} | ||
try { | ||
if (inBrowser) { | ||
blockFilter = window[blockFilter]; | ||
} | ||
else { | ||
blockFilter = require(blockFilter); | ||
} | ||
} | ||
catch (e) { | ||
throw new Error('Unknown filter "' + blockFilter + '". Try "npm install ' + blockFilter + '" first.'); | ||
} | ||
} else { | ||
blockFilter = 'text'; | ||
} | ||
// If we're in a "call" block, compile the contents. | ||
if (blockFilter == 'call') { | ||
appendText('html', "'+this['" + blockName + "'].call(this," + options.contextVar + (text ? ',' + ltl.compile(text) : '') + ")+'"); | ||
return; | ||
} | ||
// For a "set" block. | ||
else if (blockFilter == 'set' || blockFilter == 'set:') { | ||
var block; | ||
if (blockFilter == 'set') { | ||
block = ltl.compile(text).toString(); | ||
} else { | ||
block = "function(){return '" + escapeBlock(text) + "'}"; | ||
} | ||
blockSets.push("'" + escapeSingleQuotes(blockName) + "':" + block); | ||
return; | ||
} | ||
// If there's a filter, get its module. | ||
else if (blockFilter) { | ||
if (blockFilter == 'coffee') { | ||
blockFilter = 'coffee-script'; | ||
} | ||
else if (blockFilter == 'md') { | ||
blockFilter = 'markdown'; | ||
} | ||
try { | ||
if (inBrowser) { | ||
blockFilter = window[blockFilter]; | ||
} | ||
else { | ||
blockFilter = require(blockFilter); | ||
} | ||
} | ||
catch (e) { | ||
throw new Error('Unknown filter "' + blockFilter + '". Try "npm install ' + blockFilter + '" first.'); | ||
} | ||
} else { | ||
blockFilter = 'text'; | ||
} | ||
// Detect the module's API, and filter the text. | ||
if (blockFilter.compile) { | ||
var nowrap = /^[^A-Z]*NOWRAP/.test(text); | ||
text = blockFilter.compile(text); | ||
if (nowrap) { | ||
text = text.replace(/(^\(function\(\) \{\s*|\s*\}\)\.call\(this\);\s*$)/g, ''); | ||
} | ||
} | ||
else if (blockFilter.markdown) { | ||
text = blockFilter.markdown.toHTML(text); | ||
} | ||
else if (typeof blockFilter == 'function') { | ||
text = blockFilter(text); | ||
} | ||
// Detect the module's API, and filter the text. | ||
if (blockFilter.compile) { | ||
var nowrap = /^[^A-Z]*NOWRAP/.test(text); | ||
text = blockFilter.compile(text); | ||
if (nowrap) { | ||
text = text.replace(/(^\(function\(\) \{\s*|\s*\}\)\.call\(this\);\s*$)/g, ''); | ||
} | ||
} | ||
else if (blockFilter.markdown) { | ||
text = blockFilter.markdown.toHTML(text); | ||
} | ||
else if (typeof blockFilter == 'function') { | ||
text = blockFilter(text); | ||
} | ||
text = trim(text); | ||
if (options.space) { | ||
if (hasHtmlOutput) { | ||
text = '\n' + text; | ||
} | ||
text = text.replace(/\n/g, '\n' + repeat(options.space, tagDepth)); | ||
if (blockTag) { | ||
text += '\n' + repeat(options.space, tagDepth - 1); | ||
} | ||
} | ||
text = trim(text); | ||
if (options.space) { | ||
if (hasHtmlOutput) { | ||
text = '\n' + text; | ||
} | ||
text = text.replace(/\n/g, '\n' + repeat(options.space, tagDepth)); | ||
if (blockTag) { | ||
text += '\n' + repeat(options.space, tagDepth - 1); | ||
} | ||
} | ||
appendText('html', escapeBlock(text)); | ||
appendText('html', escapeBlock(text)); | ||
blockTag = null; | ||
blockFilter = null; | ||
} | ||
blockTag = null; | ||
blockFilter = null; | ||
} | ||
function backtrackIndent() { | ||
while (stack.length > indent) { | ||
var tags = stack.pop(); | ||
if (tags) { | ||
tags = tags.split(/,/g); | ||
for (var i = tags.length - 1; i >= 0; i--) { | ||
var tag = tags[i]; | ||
if (tag == '//') { | ||
inComment = false; | ||
} | ||
else if (tag == '-') { | ||
appendText('html', '-->'); | ||
} | ||
else if (controlPattern.test(tag)) { | ||
appendText('script', '}'); | ||
if (tag == 'for') { | ||
loopVars.pop(); | ||
} | ||
} | ||
else if (!selfClosePattern.test(tag)) { | ||
var html = '</' + tag + '>'; | ||
tagDepth--; | ||
if (tag == previousTag) { | ||
previousTag = null; | ||
} | ||
else if (options.space) { | ||
html = '\\n' + repeat(options.space, tagDepth) + html; | ||
} | ||
appendText('html', html); | ||
} | ||
} | ||
} | ||
} | ||
} | ||
function backtrackIndent() { | ||
while (stack.length > indent) { | ||
var tags = stack.pop(); | ||
if (tags) { | ||
tags = tags.split(/,/g); | ||
for (var i = tags.length - 1; i >= 0; i--) { | ||
var tag = tags[i]; | ||
if (tag == '//') { | ||
inComment = false; | ||
} | ||
else if (tag == '-') { | ||
appendText('html', '-->'); | ||
} | ||
else if (controlPattern.test(tag)) { | ||
appendText('script', '}'); | ||
if (tag == 'for') { | ||
loopVars.pop(); | ||
} | ||
} | ||
else if (!selfClosePattern.test(tag)) { | ||
var html = '</' + tag + '>'; | ||
tagDepth--; | ||
if (tag == previousTag) { | ||
previousTag = null; | ||
} | ||
else if (options.space) { | ||
html = '\\n' + repeat(options.space, tagDepth) + html; | ||
} | ||
appendText('html', html); | ||
} | ||
} | ||
} | ||
} | ||
} | ||
function transformScript(script) { | ||
var c = options.contextVar; | ||
var found = false; | ||
function transformScript(script) { | ||
var c = options.contextVar; | ||
var found = false; | ||
script = script.replace(/^(for)\s+([$a-zA-Z_][$a-zA-Z_0-9]*)\s+in\s+([$a-zA-Z_][$a-zA-Z_0-9]*)\s*$/i, | ||
function(match, keyword, item, array) { | ||
found = true; | ||
var i = vars[varIndex++]; | ||
var l = vars[varIndex++]; | ||
var e = vars[varIndex++]; | ||
loopVars.push([[item, e]]); | ||
return 'for(var ' + e + ',' + i + '=0,' + l + '=' + c + '.' + array + '.length;' + | ||
i + '<' + l + ';++' + i + ')' + | ||
'{' + e + '=' + c + '.' + array + '[' + i + ']' + ';'; | ||
}); | ||
if (found) { | ||
stack.push('for'); | ||
return script; | ||
} | ||
script = script.replace(/^(for)\s+([$a-zA-Z_][$a-zA-Z_0-9]*)\s+in\s+([$a-zA-Z_][$a-zA-Z_0-9]*)\s*$/i, | ||
function(match, keyword, item, array) { | ||
found = true; | ||
var i = vars[varIndex++]; | ||
var l = vars[varIndex++]; | ||
var e = vars[varIndex++]; | ||
loopVars.push([[item, e]]); | ||
return 'for(var ' + e + ',' + i + '=0,' + l + '=' + c + '.' + array + '.length;' + | ||
i + '<' + l + ';++' + i + ')' + | ||
'{' + e + '=' + c + '.' + array + '[' + i + ']' + ';'; | ||
}); | ||
if (found) { | ||
stack.push('for'); | ||
return script; | ||
} | ||
script = script.replace(/^(for)\s+([$a-zA-Z_][$a-zA-Z_0-9]*)\s*,\s*([$a-zA-Z_][$a-zA-Z_0-9]*)\s+of\s+([$a-zA-Z_][$a-zA-Z_0-9]*)\s*$/i, | ||
function(match, keyword, key, value, object) { | ||
found = true; | ||
var k = vars[varIndex++]; | ||
var v = vars[varIndex++]; | ||
loopVars.push([[key, k], [value, v]]); | ||
return 'for(var ' + k + ' in ' + c + '.' + object + ')' + | ||
'{if(!' + c + '.' + object + '.hasOwnProperty(' + k + '))continue;' + | ||
v + '=' + c + '.' + object + '[' + k + ']' + ';'; | ||
}); | ||
script = script.replace(/^(for)\s+([$a-zA-Z_][$a-zA-Z_0-9]*)\s*,\s*([$a-zA-Z_][$a-zA-Z_0-9]*)\s+of\s+([$a-zA-Z_][$a-zA-Z_0-9]*)\s*$/i, | ||
function(match, keyword, key, value, object) { | ||
found = true; | ||
var k = vars[varIndex++]; | ||
var v = vars[varIndex++]; | ||
loopVars.push([[key, k], [value, v]]); | ||
return 'for(var ' + k + ' in ' + c + '.' + object + ')' + | ||
'{if(!' + c + '.' + object + '.hasOwnProperty(' + k + '))continue;' + | ||
v + '=' + c + '.' + object + '[' + k + ']' + ';'; | ||
}); | ||
if (found) { | ||
stack.push('for'); | ||
return script; | ||
} | ||
if (found) { | ||
stack.push('for'); | ||
return script; | ||
} | ||
script = script.replace(/^(else if|else|if)\s*(.*)\s*$/i, | ||
function(match, keyword, condition) { | ||
found = true; | ||
return keyword + (condition ? '(' + contextify(condition) + ')' : '') + '{'; | ||
}); | ||
script = script.replace(/^(else if|else|if)\s*(.*)\s*$/i, | ||
function(match, keyword, condition) { | ||
found = true; | ||
return keyword + (condition ? '(' + contextify(condition) + ')' : '') + '{'; | ||
}); | ||
stack.push('if'); | ||
return script; | ||
} | ||
stack.push('if'); | ||
return script; | ||
} | ||
/** | ||
* Convert a JavaScripty expression to a scoped expression using contextVar. | ||
/** | ||
* Convert a JavaScripty expression to a scoped expression using contextVar. | ||
* TODO: Actually parse JavaScript so that strings can't be interpreted as vars. | ||
*/ | ||
function contextify(code) { | ||
var tokens = code.split(/\b/); | ||
var isProperty = false; | ||
var isLoopVar = false; | ||
*/ | ||
function contextify(code) { | ||
var tokens = code.split(/\b/); | ||
var isProperty = false; | ||
var isLoopVar = false; | ||
var isInString = false; | ||
for (var i = 0; i < tokens.length; i++) { | ||
var token = tokens[i]; | ||
for (var i = 0; i < tokens.length; i++) { | ||
var token = tokens[i]; | ||
var stringTokens = token.match(/['"]/g); | ||
@@ -315,367 +315,367 @@ if (stringTokens) { | ||
if (!isInString) { | ||
if (/^[a-z_]/i.test(token)) { | ||
if (!jsPattern.test(token)) { | ||
for (var j = 0; j < loopVars.length; j++) { | ||
for (var k = 0; k < loopVars[j].length; k++) { | ||
if (token == loopVars[j][k][0]) { | ||
isLoopVar = true; | ||
tokens[i] = loopVars[j][k][1]; | ||
} | ||
} | ||
} | ||
if (!isProperty && !isLoopVar) { | ||
tokens[i] = options.contextVar + '.' + token; | ||
} | ||
} | ||
} | ||
if (/^[a-z_]/i.test(token)) { | ||
if (!jsPattern.test(token)) { | ||
for (var j = 0; j < loopVars.length; j++) { | ||
for (var k = 0; k < loopVars[j].length; k++) { | ||
if (token == loopVars[j][k][0]) { | ||
isLoopVar = true; | ||
tokens[i] = loopVars[j][k][1]; | ||
} | ||
} | ||
} | ||
if (!isProperty && !isLoopVar) { | ||
tokens[i] = options.contextVar + '.' + token; | ||
} | ||
} | ||
} | ||
} | ||
isProperty = token[token.length - 1] == '.'; | ||
} | ||
return tokens.join(''); | ||
} | ||
isProperty = token[token.length - 1] == '.'; | ||
} | ||
return tokens.join(''); | ||
} | ||
/** | ||
* Find ${...} and ={...} and turn them into contextified insertions unless escaped. | ||
*/ | ||
function interpolate(code) { | ||
return code.replace(/(\\?)([$=])\{([^\}]+)\}/g, function(match, backslash, symbol, expression) { | ||
if (backslash) { | ||
return symbol + '{' + expression + '}'; | ||
} | ||
if (symbol == '$') { | ||
if (!escapeVar) { | ||
escapeVar = vars[varIndex++]; | ||
} | ||
return "'+" + escapeVar + '(' + contextify(expression) + ")+'"; | ||
} | ||
else { | ||
return "'+" + contextify(expression) + "+'"; | ||
} | ||
}); | ||
} | ||
/** | ||
* Find ${...} and ={...} and turn them into contextified insertions unless escaped. | ||
*/ | ||
function interpolate(code) { | ||
return code.replace(/(\\?)([$=])\{([^\}]+)\}/g, function(match, backslash, symbol, expression) { | ||
if (backslash) { | ||
return symbol + '{' + expression + '}'; | ||
} | ||
if (symbol == '$') { | ||
if (!escapeVar) { | ||
escapeVar = vars[varIndex++]; | ||
} | ||
return "'+" + escapeVar + '(' + contextify(expression) + ")+'"; | ||
} | ||
else { | ||
return "'+" + contextify(expression) + "+'"; | ||
} | ||
}); | ||
} | ||
// If we end up in a dot block, remember the starting indent and filter, and gather lines. | ||
var blockIndent = 0; | ||
var blockFilter = ''; | ||
var blockLines = []; | ||
var blockTag = null; | ||
// If we end up in a dot block, remember the starting indent and filter, and gather lines. | ||
var blockIndent = 0; | ||
var blockFilter = ''; | ||
var blockLines = []; | ||
var blockTag = null; | ||
var blockName = ''; | ||
var blockSets = []; | ||
var hasGets = false; | ||
var inComment = false; | ||
var blockName = ''; | ||
var blockSets = []; | ||
var hasGets = false; | ||
var inComment = false; | ||
// Iterate over each line. | ||
for (var i = 0; i < lines.length; i++) { | ||
var line = lines[i]; | ||
var maxT = 1e2; | ||
// Iterate over each line. | ||
for (var i = 0; i < lines.length; i++) { | ||
var line = lines[i]; | ||
var maxT = 1e2; | ||
// If the line is all whitespace, ignore it. | ||
if (!/\S/.test(line)) { | ||
continue; | ||
} | ||
// If the line is all whitespace, ignore it. | ||
if (!/\S/.test(line)) { | ||
continue; | ||
} | ||
// Find the number of leading spaces. | ||
var spaces = line.search(/[^ ]/); | ||
// Find the number of leading spaces. | ||
var spaces = line.search(/[^ ]/); | ||
// If this is our first time seeing leading spaces, that's our tab width. | ||
if (spaces > 0 && !currentTabWidth) { | ||
currentTabWidth = spaces; | ||
} | ||
// If this is our first time seeing leading spaces, that's our tab width. | ||
if (spaces > 0 && !currentTabWidth) { | ||
currentTabWidth = spaces; | ||
} | ||
// Calculate the number of levels of indentation. | ||
var indent = spaces ? Math.round(spaces / currentTabWidth) : 0; | ||
// Calculate the number of levels of indentation. | ||
var indent = spaces ? Math.round(spaces / currentTabWidth) : 0; | ||
// If we're in a block, we can append or close it. | ||
if (blockIndent) { | ||
// If we've gone back to where the block started, close the block. | ||
if (indent < blockIndent) { | ||
appendBlock(); | ||
} | ||
// If we're still in the block, append to the block code. | ||
else { | ||
line = line.substring(Math.min(spaces, currentTabWidth * blockIndent)); | ||
blockLines.push(line); | ||
continue; | ||
} | ||
} | ||
// If we're in a block, we can append or close it. | ||
if (blockIndent) { | ||
// If we've gone back to where the block started, close the block. | ||
if (indent < blockIndent) { | ||
appendBlock(); | ||
} | ||
// If we're still in the block, append to the block code. | ||
else { | ||
line = line.substring(Math.min(spaces, currentTabWidth * blockIndent)); | ||
blockLines.push(line); | ||
continue; | ||
} | ||
} | ||
// Strip the leading spaces. | ||
line = line.substring(spaces); | ||
// Strip the leading spaces. | ||
line = line.substring(spaces); | ||
// Backtrack, closing any nested tags that need to be closed. | ||
backtrackIndent(); | ||
if (inComment) { | ||
continue; | ||
} | ||
// Backtrack, closing any nested tags that need to be closed. | ||
backtrackIndent(); | ||
if (inComment) { | ||
continue; | ||
} | ||
// Control patterns such as if/else/for must transform into true JavaScript. | ||
if (controlPattern.test(line)) { | ||
var code = transformScript(line); | ||
appendText('script', code); | ||
} | ||
// Control patterns such as if/else/for must transform into true JavaScript. | ||
if (controlPattern.test(line)) { | ||
var code = transformScript(line); | ||
appendText('script', code); | ||
} | ||
// Expression patterns make things append. | ||
else if (commandPattern.test(line)) { | ||
var pair = trim(line).split(/\s+/); | ||
var command = pair[0]; | ||
blockName = pair[1]; | ||
pair = blockName.split(':'); | ||
blockName = pair[0]; | ||
if (command == 'get') { | ||
appendText('html', "'+" + options.partsVar + "['" + blockName + "'](" + options.contextVar + ")+'"); | ||
hasGets = true; | ||
} | ||
else { | ||
if (pair[1] === '') { | ||
command += ':'; | ||
} | ||
startBlock(command); | ||
} | ||
} | ||
// Expression patterns make things append. | ||
else if (commandPattern.test(line)) { | ||
var pair = trim(line).split(/\s+/); | ||
var command = pair[0]; | ||
blockName = pair[1]; | ||
pair = blockName.split(':'); | ||
blockName = pair[0]; | ||
if (command == 'get') { | ||
appendText('html', "'+" + options.partsVar + "['" + blockName + "'](" + options.contextVar + ")+'"); | ||
hasGets = true; | ||
} | ||
else { | ||
if (pair[1] === '') { | ||
command += ':'; | ||
} | ||
startBlock(command); | ||
} | ||
} | ||
// Tags must be parsed for id/class/attributes/content. | ||
else { | ||
var rest = line; | ||
var t = 0; | ||
// Tags must be parsed for id/class/attributes/content. | ||
else { | ||
var rest = line; | ||
var t = 0; | ||
// Process the rest of the line recursively. | ||
while (rest && (++t < maxT)) { | ||
var tag = ''; | ||
var id = ''; | ||
var className = ''; | ||
var attributes = ''; | ||
var content = ''; | ||
var character = ''; | ||
var end = 0; | ||
// Process the rest of the line recursively. | ||
while (rest && (++t < maxT)) { | ||
var tag = ''; | ||
var id = ''; | ||
var className = ''; | ||
var attributes = ''; | ||
var content = ''; | ||
var character = ''; | ||
var end = 0; | ||
// Process the rest of the line recursively. | ||
while (rest && (++t < maxT)) { | ||
character = rest[0]; | ||
// Process the rest of the line recursively. | ||
while (rest && (++t < maxT)) { | ||
character = rest[0]; | ||
// If it's an ID, read up to the next thing, and save the ID. | ||
if (character == '#') { | ||
end = rest.search(/([\.\(>:\s]|$)/); | ||
id = rest.substring(1, end); | ||
rest = rest.substring(end); | ||
} | ||
// If it's an ID, read up to the next thing, and save the ID. | ||
if (character == '#') { | ||
end = rest.search(/([\.\(>:\s]|$)/); | ||
id = rest.substring(1, end); | ||
rest = rest.substring(end); | ||
} | ||
// If it's a class, read up to the next thing, and save the className. | ||
else if (character == '.') { | ||
end = rest.search(/([\(>:\s]|$)/); | ||
className = rest.substring(1, end).replace(/\./g, ' '); | ||
rest = rest.substring(end); | ||
} | ||
// If it's a class, read up to the next thing, and save the className. | ||
else if (character == '.') { | ||
end = rest.search(/([\(>:\s]|$)/); | ||
className = rest.substring(1, end).replace(/\./g, ' '); | ||
rest = rest.substring(end); | ||
} | ||
// If it's the beginning of a list of attributes, iterate through them. | ||
else if (character == '(') { | ||
// If it's the beginning of a list of attributes, iterate through them. | ||
else if (character == '(') { | ||
// Move on from the parentheses. | ||
rest = rest.substring(1); | ||
// Move on from the parentheses. | ||
rest = rest.substring(1); | ||
// Build attributes. | ||
attributes = ''; | ||
while (rest && (++t < maxT)) { | ||
// Build attributes. | ||
attributes = ''; | ||
while (rest && (++t < maxT)) { | ||
// Find quoted attributes or the end of the list. | ||
end = rest.search(/[\)"']/); | ||
// Find quoted attributes or the end of the list. | ||
end = rest.search(/[\)"']/); | ||
// If there's no end, read what's left as attributes. | ||
if (end < 0) { | ||
attributes += rest; | ||
rest = ''; | ||
break; | ||
} | ||
character = rest[end]; | ||
// If there's no end, read what's left as attributes. | ||
if (end < 0) { | ||
attributes += rest; | ||
rest = ''; | ||
break; | ||
} | ||
character = rest[end]; | ||
// If it's the end, get any remaining attribute and get out. | ||
if (character == ')') { | ||
attributes += rest.substring(0, end); | ||
rest = rest.substring(end + 1); | ||
break; | ||
} | ||
// If it's the end, get any remaining attribute and get out. | ||
if (character == ')') { | ||
attributes += rest.substring(0, end); | ||
rest = rest.substring(end + 1); | ||
break; | ||
} | ||
// If it's not the end, read a quoted param. | ||
else { | ||
// Allow for attributes to be comma separated or not. | ||
// Also allow for valueless attributes. | ||
attributes += rest.substring(0, end).replace(/[,\s]+/g, ' '); | ||
rest = rest.substring(end); | ||
// If it's not the end, read a quoted param. | ||
else { | ||
// Allow for attributes to be comma separated or not. | ||
// Also allow for valueless attributes. | ||
attributes += rest.substring(0, end).replace(/[,\s]+/g, ' '); | ||
rest = rest.substring(end); | ||
// Find the next end quote. | ||
// TODO: Deal with backslash-delimited quotes. | ||
end = rest.indexOf(character, 1); | ||
// Find the next end quote. | ||
// TODO: Deal with backslash-delimited quotes. | ||
end = rest.indexOf(character, 1); | ||
// Read to the end of the attribute. | ||
attributes += rest.substring(0, end + 1); | ||
rest = rest.substring(end + 1); | ||
} | ||
} | ||
} | ||
// Read to the end of the attribute. | ||
attributes += rest.substring(0, end + 1); | ||
rest = rest.substring(end + 1); | ||
} | ||
} | ||
} | ||
// If the next character is a greater than symbol, break for inline nesting. | ||
else if (character == '>') { | ||
rest = rest.replace(/^>\s*/, ''); | ||
break; | ||
} | ||
// If the next character is a greater than symbol, break for inline nesting. | ||
else if (character == '>') { | ||
rest = rest.replace(/^>\s*/, ''); | ||
break; | ||
} | ||
// If the next character is a colon, enter a block. | ||
else if (character == ':') { | ||
blockTag = tag; | ||
rest = rest.substring(1).split(' '); | ||
startBlock(rest.shift()); | ||
if (rest.length > 0) { | ||
blockLines.push(rest.join(' ')); | ||
} | ||
rest = ''; | ||
break; | ||
} | ||
// If the next character is a colon, enter a block. | ||
else if (character == ':') { | ||
blockTag = tag; | ||
rest = rest.substring(1).split(' '); | ||
startBlock(rest.shift()); | ||
if (rest.length > 0) { | ||
blockLines.push(rest.join(' ')); | ||
} | ||
rest = ''; | ||
break; | ||
} | ||
// If the next character is a space, it's the start of content. | ||
else if (character == ' ') { | ||
content = trim(rest); | ||
rest = ''; | ||
} | ||
// If the next character is a space, it's the start of content. | ||
else if (character == ' ') { | ||
content = trim(rest); | ||
rest = ''; | ||
} | ||
// If the next character isn't special, it's part of a tag. | ||
else { | ||
end = rest.search(/([#\.\(>:\s]|$)/); | ||
// Prevent overwriting the tag. | ||
tag = tag || rest.substring(0, end); | ||
rest = rest.substring(end); | ||
} | ||
} | ||
// If the next character isn't special, it's part of a tag. | ||
else { | ||
end = rest.search(/([#\.\(>:\s]|$)/); | ||
// Prevent overwriting the tag. | ||
tag = tag || rest.substring(0, end); | ||
rest = rest.substring(end); | ||
} | ||
} | ||
// If it's a comment, set a boolean so we can ignore its contents. | ||
if (tag == '//') { | ||
inComment = true; | ||
} | ||
// If it's not a comment, we'll add some HTML. | ||
else { | ||
// Default to a <div> unless we're in a tagless block. | ||
if (!tag) { | ||
var useDefault = (blockTag === null) || id || className || attributes; | ||
if (useDefault) { | ||
tag = blockTag = 'div'; | ||
} | ||
} | ||
// If it's a comment, set a boolean so we can ignore its contents. | ||
if (tag == '//') { | ||
inComment = true; | ||
} | ||
// If it's not a comment, we'll add some HTML. | ||
else { | ||
// Default to a <div> unless we're in a tagless block. | ||
if (!tag) { | ||
var useDefault = (blockTag === null) || id || className || attributes; | ||
if (useDefault) { | ||
tag = blockTag = 'div'; | ||
} | ||
} | ||
// Convert ! or doctype into !DOCTYPE and assume html. | ||
if (tag == '!' || tag == 'doctype') { | ||
tag = '!DOCTYPE'; | ||
attributes = attributes || 'html'; | ||
} | ||
// Convert ! or doctype into !DOCTYPE and assume html. | ||
if (tag == '!' || tag == 'doctype') { | ||
tag = '!DOCTYPE'; | ||
attributes = attributes || 'html'; | ||
} | ||
// Add attributes to the tag. | ||
var html = tag; | ||
if (id) { | ||
html += ' id="' + id + '"'; | ||
} | ||
if (className) { | ||
html += ' class="' + className + '"'; | ||
} | ||
if (attributes) { | ||
html += ' ' + attributes; | ||
} | ||
// Add attributes to the tag. | ||
var html = tag; | ||
if (id) { | ||
html += ' id="' + id + '"'; | ||
} | ||
if (className) { | ||
html += ' class="' + className + '"'; | ||
} | ||
if (attributes) { | ||
html += ' ' + attributes; | ||
} | ||
// Convert minus to a comment. | ||
if (tag == '-') { | ||
html = '<!--' + content; | ||
} | ||
else { | ||
html = '<' + html + '>' + content; | ||
} | ||
// Convert minus to a comment. | ||
if (tag == '-') { | ||
html = '<!--' + content; | ||
} | ||
else { | ||
html = '<' + html + '>' + content; | ||
} | ||
html = escapeSingleQuotes(html); | ||
if (tag == 'html' && !/DOCTYPE/.test(output)) { | ||
html = '<!DOCTYPE html>' + (options.space ? '\\n' : '') + html; | ||
} | ||
html = escapeSingleQuotes(html); | ||
if (tag == 'html' && !/DOCTYPE/.test(output)) { | ||
html = '<!DOCTYPE html>' + (options.space ? '\\n' : '') + html; | ||
} | ||
// Prepend whitespace if requested via options.space. | ||
if (options.space) { | ||
html = repeat(options.space, tagDepth) + html; | ||
// Prepend a line break if this isn't the first tag. | ||
if (hasHtmlOutput) { | ||
html = '\\n' + html; | ||
} | ||
} | ||
// Prepend whitespace if requested via options.space. | ||
if (options.space) { | ||
html = repeat(options.space, tagDepth) + html; | ||
// Prepend a line break if this isn't the first tag. | ||
if (hasHtmlOutput) { | ||
html = '\\n' + html; | ||
} | ||
} | ||
// Add the HTML to the template function output. | ||
if (tag) { | ||
appendText('html', html); | ||
hasHtmlOutput = true; | ||
} | ||
} | ||
// Add the HTML to the template function output. | ||
if (tag) { | ||
appendText('html', html); | ||
hasHtmlOutput = true; | ||
} | ||
} | ||
if (tag) { | ||
if (tag) { | ||
// Make sure we can close this tag. | ||
if (stack[indent]) { | ||
stack[indent] += ',' + tag; | ||
} | ||
else { | ||
stack[indent] = tag; | ||
} | ||
// Make sure we can close this tag. | ||
if (stack[indent]) { | ||
stack[indent] += ',' + tag; | ||
} | ||
else { | ||
stack[indent] = tag; | ||
} | ||
// Allow same-line tag open/close in options.space mode. | ||
previousTag = tag; | ||
if (!selfClosePattern.test(tag)) { | ||
tagDepth++; | ||
} | ||
} | ||
// Allow same-line tag open/close in options.space mode. | ||
previousTag = tag; | ||
if (!selfClosePattern.test(tag)) { | ||
tagDepth++; | ||
} | ||
} | ||
tag = ''; | ||
id = ''; | ||
className = ''; | ||
attributes = ''; | ||
content = ''; | ||
} | ||
} | ||
} | ||
tag = ''; | ||
id = ''; | ||
className = ''; | ||
attributes = ''; | ||
content = ''; | ||
} | ||
} | ||
} | ||
// We've reached the end, so unindent all the way. | ||
indent = 0; | ||
if (blockIndent) { | ||
appendBlock(); | ||
} | ||
backtrackIndent(); | ||
// We've reached the end, so unindent all the way. | ||
indent = 0; | ||
if (blockIndent) { | ||
appendBlock(); | ||
} | ||
backtrackIndent(); | ||
// Add the return statement (ending concatenation, where applicable). | ||
appendText('script', 'return ' + options.outputVar); | ||
// Add the return statement (ending concatenation, where applicable). | ||
appendText('script', 'return ' + options.outputVar); | ||
if (blockSets.length) { | ||
return '{' + blockSets.join(',') + '}'; | ||
} | ||
if (blockSets.length) { | ||
return '{' + blockSets.join(',') + '}'; | ||
} | ||
// Create the function. | ||
if (escapeVar) { | ||
output = "function " + escapeVar + "(t){return (''+t).replace(/</g,'<')};" + output; | ||
} | ||
output = 'eval.f=function(' + options.contextVar + (hasGets ? ',' + options.partsVar : '') + '){' + output + '}'; | ||
try { | ||
eval(output); | ||
} | ||
catch (e) { | ||
// Anyone hitting an error here should submit an issue on GitHub. | ||
/* istanbul ignore next */ | ||
throw "Could not save output as a function: " + output; | ||
} | ||
var template = eval.f; | ||
// Create the function. | ||
if (escapeVar) { | ||
output = "function " + escapeVar + "(t){return (''+t).replace(/</g,'<')};" + output; | ||
} | ||
output = 'eval.f=function(' + options.contextVar + (hasGets ? ',' + options.partsVar : '') + '){' + output + '}'; | ||
try { | ||
eval(output); | ||
} | ||
catch (e) { | ||
// Anyone hitting an error here should submit an issue on GitHub. | ||
/* istanbul ignore next */ | ||
throw "Could not save output as a function: " + output; | ||
} | ||
var template = eval.f; | ||
// If there's a name specified, cache the template with that name. | ||
if (options.name) { | ||
this.cache[options.name] = template; | ||
} | ||
// If there's a name specified, cache the template with that name. | ||
if (options.name) { | ||
this.cache[options.name] = template; | ||
} | ||
return template; | ||
} | ||
}; | ||
return template; | ||
} | ||
}; | ||
if (typeof window == 'undefined') { | ||
module.exports = ltl; | ||
} | ||
else { | ||
window.ltl = ltl; | ||
} | ||
if (typeof window == 'undefined') { | ||
module.exports = ltl; | ||
} | ||
else { | ||
window.ltl = ltl; | ||
} | ||
})(); |
@@ -14,3 +14,3 @@ { | ||
], | ||
"version": "0.1.4", | ||
"version": "0.1.5", | ||
"main": "ltl", | ||
@@ -38,4 +38,4 @@ "homepage": "http://lighter.io/ltl", | ||
"assert-plus": "0.1.5", | ||
"mocha": "1.18.2", | ||
"istanbul": "0.2.9", | ||
"mocha": "1.20.0", | ||
"istanbul": "0.2.10", | ||
"coveralls": "2.10.0", | ||
@@ -46,3 +46,3 @@ "dot": "1.0.2", | ||
"coffee-script": "1.7.1", | ||
"zeriousify": "0.1.3" | ||
"zeriousify": "0.1.5" | ||
}, | ||
@@ -49,0 +49,0 @@ "bugs": { |
@@ -6,27 +6,27 @@ var jade = require('jade'); | ||
var ltlCode = | ||
'html\n' + | ||
' head\n' + | ||
' title Hello World\n' + | ||
' body\n' + | ||
' div#hey.a.b(style="display:block; width:100px") Here\'s my message: ={message}\n' + | ||
' ul\n' + | ||
' for item in items\n' + | ||
' li ={item}\n'; | ||
'html\n' + | ||
' head\n' + | ||
' title Hello World\n' + | ||
' body\n' + | ||
' div#hey.a.b(style="display:block; width:100px") Here\'s my message: ={message}\n' + | ||
' ul\n' + | ||
' for item in items\n' + | ||
' li ={item}\n'; | ||
var jadeCode = | ||
'doctype html\n' + | ||
'html\n' + | ||
' head\n' + | ||
' title Hello World\n' + | ||
' body\n' + | ||
' div#hey.a.b(style="display:block; width:100px") Here\'s my message: #{message}\n' + | ||
' ul\n' + | ||
' each item in items\n' + | ||
' li #{item}\n'; | ||
'doctype html\n' + | ||
'html\n' + | ||
' head\n' + | ||
' title Hello World\n' + | ||
' body\n' + | ||
' div#hey.a.b(style="display:block; width:100px") Here\'s my message: #{message}\n' + | ||
' ul\n' + | ||
' each item in items\n' + | ||
' li #{item}\n'; | ||
var dotCode = '<!DOCTYPE html><html><head><title>Hello World</title></head>' + | ||
'<body><div id="hey" class="a b" style="display:block;width:100px">' + | ||
'Here\'s my message: {{=it.message}}</div><ul>{{' + | ||
'for(var i=0;i<it.items.length;++i){it.item=it.items[i]; }}' + | ||
'<li>{{=it.item}}</li>{{ } }}</ul></body></html>'; | ||
'<body><div id="hey" class="a b" style="display:block;width:100px">' + | ||
'Here\'s my message: {{=it.message}}</div><ul>{{' + | ||
'for(var i=0;i<it.items.length;++i){it.item=it.items[i]; }}' + | ||
'<li>{{=it.item}}</li>{{ } }}</ul></body></html>'; | ||
@@ -46,30 +46,30 @@ var context = {message: 'hello', items: ['apples', 'apricots', 'bananas', 'cherries', 'grapes', 'kiwis', 'mangoes', 'oranges', 'pears', 'plums']}; | ||
var engines = [ | ||
{name: 'Jade', code: jadeCode, lib: jade}, | ||
{name: 'doT', code: dotCode, lib: dot}, | ||
{name: 'ltl', code: ltlCode, lib: ltl} | ||
{name: 'Jade', code: jadeCode, lib: jade}, | ||
{name: 'doT', code: dotCode, lib: dot}, | ||
{name: 'ltl', code: ltlCode, lib: ltl} | ||
]; | ||
var operations = [ | ||
{name: 'Compile', count: 1e3}, | ||
{name: 'Render', count: 1e6} | ||
{name: 'Compile', count: 1e3}, | ||
{name: 'Render', count: 1e6} | ||
]; | ||
operations.forEach(function (operation) { | ||
console.log('\n' + operation.name + ' x' + operation.count); | ||
engines.forEach(function (engine, engineIndex) { | ||
console.log('\n' + operation.name + ' x' + operation.count); | ||
engines.forEach(function (engine, engineIndex) { | ||
var count = operation.count; | ||
started = new Date(); | ||
for (i = 0; i < count; i++) { | ||
if (operation.name == 'Compile') { | ||
engine.template = engine.lib.compile(engine.code); | ||
} | ||
else { | ||
result = engine.template(context); | ||
} | ||
} | ||
elapsed = new Date() - started; | ||
console.log(engine.name + ': ' + elapsed); | ||
var count = operation.count; | ||
started = new Date(); | ||
for (i = 0; i < count; i++) { | ||
if (operation.name == 'Compile') { | ||
engine.template = engine.lib.compile(engine.code); | ||
} | ||
else { | ||
result = engine.template(context); | ||
} | ||
} | ||
elapsed = new Date() - started; | ||
console.log(engine.name + ': ' + elapsed); | ||
}); | ||
}); | ||
}); |
33746