Comparing version 0.1.15 to 0.2.0
1379
ltl.js
/** | ||
* ltl is a template language designed to be simple, beautiful and fast. | ||
* Ltl is a template language designed to be simple, beautiful and fast. | ||
* There is single Ltl reference in the process or window. | ||
*/ | ||
var ltl = this.ltl = this.ltl || { | ||
(function () { | ||
// The scope is a window or process. | ||
scope: this, | ||
var isBrowser = (typeof window == 'object'); | ||
// Allow users to see what version of Ltl they're using. | ||
version: '0.2.0', | ||
// Some HTML tags won't have end tags. | ||
var selfClosePattern = /^(!DOCTYPE|area|base|br|hr|img|input|link|meta|-|\/\/)(\b|$)/; | ||
selfClosePattern: /^(!DOCTYPE|area|base|br|hr|img|input|link|meta|-|\/\/|space|js|css)(\b|$)/, | ||
// Supported control keywords (usage appears like tags). | ||
var controlPattern = /^(for|if|else|else if)\b/; | ||
controlPattern: /^(for|if|else)\b/, | ||
// Pattern for a JavaScript assignment. | ||
var assignmentPattern = /^([$A-Za-z_][$A-Za-z_0-9\.\[\]'"]*\s*=[^\{])/; | ||
// Pattern for a Jasignment. | ||
assignmentPattern: /^([$A-Za-z_][$A-Za-z_0-9\.\[\]'"]*\s*=[^\{])/, | ||
// Supported command keywords. | ||
var commandPattern = /^(call|get|set)\b/; | ||
commandPattern: /^(call|get|set)\b/, | ||
// JavaScript tokens that don't need contextVar prepended for interpolation. | ||
// JavaScript tokens that don't need the state "s" prepended for interpolation. | ||
// TODO: Flesh out this list? | ||
var jsPattern = /^(true|false|null|NaN|Infinity|window|location|Math|console|this)$/; | ||
jsPattern: /^(undefined|true|false|null|function|NaN|Infinity|window|location|document|console|this|Math|Object|Date|Error|RegExp|JSON)$/, | ||
// Stores available single character variable names. | ||
var varCharacters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_$'; | ||
vars: 'abcdefghijklmnqrtuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', | ||
// Register several languages and their targets. | ||
languages: { | ||
js: 'js', | ||
coffee: 'js', | ||
es6: 'js', | ||
ts: 'js', | ||
css: 'css', | ||
less: 'css', | ||
scss: 'css', | ||
styl: 'css' | ||
}, | ||
// Remove starting/ending whitespace. | ||
function trim(text) { | ||
trim: function(text) { | ||
return text.replace(/(^\s+|\s+$)/g, ''); | ||
} | ||
}, | ||
// Remove starting/ending whitespace. | ||
function repeat(text, times) { | ||
return (new Array(times + 1)).join(text); | ||
} | ||
// Repeat a string. | ||
repeat: function(text, times) { | ||
return times > 0 ? (new Array(times + 1)).join(text) : ''; | ||
}, | ||
// Escape single quotes with a backslash. | ||
function escapeSingleQuotes(text) { | ||
return text.replace(/'/g, "\\'"); | ||
} | ||
escapeSingleQuotes: function(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'); | ||
} | ||
escapeBlock: function(text) { | ||
return text.replace(/'/g, '\\\'').replace(/\n/g, '\\n'); | ||
}, | ||
// When compilation fails, we can re-run in debug mode. | ||
function debug(output, settings) { | ||
var fs = require('fs'); | ||
var dir = process.cwd() + '/.debug'; | ||
var name = (settings.name || 'template').replace(/[\/\\]/g, '__'); | ||
try { | ||
fs.mkdirSync(dir); | ||
// Get a module for filtering. | ||
getFilter: function(name) { | ||
var filters = ltl.filters; | ||
var filter = filters[name]; | ||
if (!filter) { | ||
filter = filters[name] = ltl.scope[name] || (typeof require != 'undefined' ? require(name) : null); | ||
} | ||
catch (e) { | ||
// Probably already exists. | ||
if (!filter) { | ||
var todo; | ||
var into = ' into function that accepts a string and returns a string.'; | ||
if (ltl.scope.cwd) { | ||
var cmd = 'cd ' + ltl.scope.cwd() + '; npm install --save ' + name; | ||
todo = 'Run "' + cmd + '", or make require("ltl").filters.' + name; | ||
} else { | ||
todo = 'Set window.ltl.filters.' + name; | ||
} | ||
throw new Error('[Ltl] Unknown filter: "' + name + '". ' + todo + into); | ||
} | ||
fs.writeFileSync(dir + '/' + name + '.js', 'module.exports=' + output); | ||
try { | ||
require(dir + '/' + name); | ||
return filter; | ||
}, | ||
// In browsers and istanbul, run JS with `eval`, otherwise run with `vm`. | ||
run: ( | ||
(typeof require == 'function') ? | ||
require('./common/vm/run') : | ||
function (src, name) { | ||
var f; | ||
try { | ||
eval('f=' + src); // jshint ignore:line | ||
} | ||
catch (e) { | ||
e.message = '[Ltl] ' + e.message; | ||
if (name) { | ||
e.message += '\nTemplate: ' + name; | ||
} | ||
e.message += '\nFunction: ' + src; | ||
throw e.stack && e; | ||
} | ||
return f; | ||
} | ||
catch (e) { | ||
name = (settings.name ? '"' + settings.name + '"' : 'template'); | ||
e.message = '[Ltl] Failed to compile ' + name + '. ' + e.message; | ||
throw e; | ||
} | ||
} | ||
), | ||
// Public API. | ||
var ltl = { | ||
// Store all of the templates that have been compiled. | ||
cache: { | ||
'-': function(v){return '<!--'+(JSON.stringify(v)||'').replace(/-->/g,'--\\>')+'-->';}, | ||
'$': function(v){return (!v&&v!==0?'':(typeof v=='object'?JSON.stringify(v)||'':''+v)).replace(/</g,'<');}, | ||
'&': function(v){return encodeURIComponent(!v&&v!==0?'':''+v);} | ||
}, | ||
// Allow users to see what version of ltl they're using. | ||
version: '0.1.15', | ||
// Last value of an auto-incremented ID. | ||
lastId: 0, | ||
// Store all of the templates that have been compiled. | ||
cache: {}, | ||
// Store filter modules, such as "coffee-script" and "marked". | ||
filters: { | ||
js: 'js', | ||
css: 'css' | ||
}, | ||
// Default compile settings. | ||
_options: { | ||
tabWidth: 4, | ||
outputVar: 'o', | ||
contextVar: 'c', | ||
partsVar: 'p', | ||
enableDebug: false | ||
}, | ||
// Store tags that evaluate elsewhere. | ||
tags: {}, | ||
// Change compile options. | ||
setOption: function (name, value) { | ||
this._options[name] = value; | ||
}, | ||
// Default compile settings. | ||
options: { | ||
tabWidth: 4, | ||
enableDebug: false | ||
}, | ||
// Create a function that accepts context and returns markup. | ||
compile: function (code, options) { | ||
// Change compile options. | ||
setOption: function (name, value) { | ||
this.options[name] = value; | ||
}, | ||
// Copy the default options. | ||
var settings = { | ||
tabWidth: this._options.tabWidth, | ||
outputVar: this._options.outputVar, | ||
contextVar: this._options.contextVar, | ||
partsVar: this._options.partsVar, | ||
space: this._options.space, | ||
enableDebug: this._options.enableDebug | ||
}; | ||
for (var name in options) { | ||
settings[name] = options[name]; | ||
} | ||
if (settings.enableDebug && !settings.space) { | ||
settings.space = ' '; | ||
} | ||
var getPattern = new RegExp(settings.contextVar + '\\.get\\.([$A-Za-z_][$A-Za-z_\d]*)', 'ig'); | ||
// Create a function that accepts state and returns markup. | ||
compile: function (code, options) { | ||
// Don't allow context/output/parts vars to become user vars. | ||
var vars = varCharacters; | ||
vars = vars.replace(settings.contextVar, ''); | ||
vars = vars.replace(settings.outputVar, ''); | ||
vars = vars.replace(settings.partsVar, ''); | ||
// Copy the default options. | ||
var settings = { | ||
tabWidth: this.options.tabWidth, | ||
space: this.options.space, | ||
enableDebug: this.options.enableDebug | ||
}; | ||
for (var name in options) { | ||
settings[name] = options[name]; | ||
} | ||
if (settings.enableDebug && !settings.space) { | ||
settings.space = ' '; | ||
} | ||
var getPattern = /s\.get\.([$A-Za-z_][$A-Za-z_\d]*)/g; | ||
if (settings.space) { | ||
settings.space = escapeBlock(settings.space); | ||
} | ||
if (settings.space) { | ||
settings.space = ltl.escapeBlock(settings.space); | ||
} | ||
// 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(settings.tabWidth + 1).join(' '); | ||
code = code.replace(/\t/g, tabReplacement); | ||
// Be lenient with mixed tabs and spaces, assuming tab width of 4. | ||
var tabReplacement = Array(settings.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'); | ||
var lineIndex; | ||
var lineCount = lines.length; | ||
// Initialize the code, and start at level zero with no nesting. | ||
var lines = code.split('\n'); | ||
var globalSpaces = 0; | ||
var indent = 0; | ||
var stack = []; | ||
var mode = 'html'; | ||
var previousTag; | ||
var hasHtmlTag = false; | ||
var hasHtmlOutput = false; | ||
var hasAssignments = false; | ||
var hasContent = false; | ||
var tagDepth = 0; | ||
var indent = 0; | ||
var stack = []; | ||
var mode = 'html'; | ||
var previousTag; | ||
var hasHtmlOutput = false; | ||
var tagDepth = 0; | ||
var output = 'var ' + settings.outputVar + "='"; | ||
var output = "var o='"; | ||
var varIndex = 0; | ||
var escapeHtmlVar = false; | ||
var encodeUriVar = false; | ||
var loopVars = []; | ||
var varIndex = 0; | ||
var escapeHtmlVar = false; | ||
var encodeUriVar = false; | ||
var escapeCommentVar = false; | ||
var loopVars = []; | ||
function appendText(textMode, text) { | ||
if (textMode != mode) { | ||
if (mode == 'html') { | ||
output += "'" + (text == '}' ? '' : ';'); | ||
} else { | ||
output += settings.outputVar + "+='"; | ||
} | ||
mode = textMode; | ||
} | ||
// If we end up in a dot block, remember the starting indent and filter, and gather lines. | ||
var blockIndent = 0; | ||
var blockFilter = ''; | ||
var blockLines = null; | ||
var blockTag = null; | ||
var blockName = ''; | ||
var blockContent = ''; | ||
var blockSets = null; | ||
var hasGets = false; | ||
var inComment = false; | ||
// Support adding properties like "js" to the template function. | ||
var properties = {}; | ||
var blockProperty; | ||
var blockTarget; | ||
var eventLanguage; | ||
// Allow event listeners to be added. | ||
var bindings = {}; | ||
function appendText(textMode, text) { | ||
if (textMode != mode) { | ||
if (mode == 'html') { | ||
text = interpolate(text); | ||
output += "'" + (text == '}' ? '' : ';'); | ||
} | ||
output += text; | ||
else { | ||
output += "o+='"; | ||
} | ||
mode = textMode; | ||
} | ||
if (mode == 'html') { | ||
text = interpolate(text); | ||
} | ||
output += text; | ||
} | ||
function startBlock(filter, line) { | ||
blockIndent = indent + 1; | ||
blockFilter = filter; | ||
blockLines = []; | ||
if (line) { | ||
blockLines.push(line); | ||
} | ||
function startBlock(filter, content) { | ||
blockIndent = indent + 1; | ||
blockFilter = filter; | ||
blockLines = []; | ||
if (content) { | ||
(blockLines = blockLines || []).push(content); | ||
} | ||
} | ||
function appendBlock() { | ||
var text = blockLines.join('\n'); | ||
function appendBlock() { | ||
var text = blockLines.join('\n'); | ||
// Reset the blockage. | ||
blockIndent = 0; | ||
// Reset the blockage. | ||
blockIndent = 0; | ||
// Some options should be passed through. | ||
var blockOptions = { | ||
outputVar: settings.outputVar, | ||
contextVar: settings.contextVar, | ||
partsVar: settings.partsVar, | ||
space: settings.space, | ||
enableDebug: settings.enableDebug | ||
}; | ||
// Some options should be passed through. | ||
var blockOptions = { | ||
space: settings.space, | ||
enableDebug: settings.enableDebug | ||
}; | ||
// If we're in a "call" block, compile the contents. | ||
if (blockFilter == 'call') { | ||
appendText('html', | ||
"'+this['" + blockName + "'].call(this," + settings.contextVar + | ||
(text ? ',' + ltl.compile(text, blockOptions) : '') + ")+'"); | ||
return; | ||
// If we're in a "call" block, call another template with compiled parts. | ||
if (blockFilter == 'call') { | ||
// If there's a key, pass a sub-state. | ||
// * With a sub-state: this['VIEW'].call((s['KEY']._='KEY')&&s['KEY']) | ||
// * Without sub-state: this['VIEW'].call(s) | ||
var key = ltl.trim(blockContent || ''); | ||
var state = key ? "s['" + key + "']" : 's'; | ||
appendText('html', | ||
"'+this['" + blockName + "'].call(this," + state + | ||
(text ? ',' + ltl.compile(text, blockOptions) : '') + ")+'"); | ||
return; | ||
} | ||
// For a "set" block, add to the array of "set" block values. | ||
else if (blockFilter == 'set' || blockFilter == 'set:') { | ||
var block; | ||
if (blockFilter == 'set') { | ||
block = ltl.compile(text, blockOptions).toString(); | ||
} else { | ||
block = "function(){return '" + ltl.escapeBlock(text) + "'}"; | ||
} | ||
// For a "set" block. | ||
else if (blockFilter == 'set' || blockFilter == 'set:') { | ||
var block; | ||
if (blockFilter == 'set') { | ||
block = ltl.compile(text, blockOptions).toString(); | ||
} else { | ||
block = "function(){return '" + escapeBlock(text) + "'}"; | ||
} | ||
blockSets.push("'" + escapeSingleQuotes(blockName) + "':" + block); | ||
return; | ||
(blockSets = blockSets || []).push("'" + ltl.escapeSingleQuotes(blockName) + "':" + block); | ||
return; | ||
} | ||
// If there's a filter, get its module. | ||
else if (blockFilter) { | ||
if (blockFilter == 'coffee') { | ||
blockFilter = 'coffee-script'; | ||
} | ||
// If there's a filter, get its module. | ||
else if (blockFilter) { | ||
if (blockFilter == 'coffee') { | ||
blockFilter = 'coffee-script'; | ||
else if (blockFilter == 'md') { | ||
blockFilter = 'marked'; | ||
} | ||
var compiler = ltl.getFilter(blockFilter); | ||
if (compiler.renderSync) { | ||
text = compiler.renderSync(text, { | ||
data: text, | ||
compressed: true | ||
}); | ||
} | ||
else if (compiler.render) { | ||
if (blockFilter == 'less') { | ||
compiler.render(text, function (err, result) { | ||
if (err) { | ||
throw err; | ||
} | ||
text = result.css; | ||
}); | ||
} | ||
else if (blockFilter == 'md') { | ||
blockFilter = 'marked'; | ||
else { | ||
text = compiler.render(text); | ||
} | ||
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) { | ||
else if (compiler.compile) { | ||
var nowrap = /^[^A-Z]*NOWRAP/.test(text); | ||
text = blockFilter.compile(text); | ||
text = compiler.compile(text); | ||
if (nowrap) { | ||
@@ -245,56 +307,71 @@ text = text.replace(/(^\(function\(\) \{\s*|\s*\}\)\.call\(this\);\s*$)/g, ''); | ||
} | ||
else if (blockFilter.parse) { | ||
text = blockFilter.parse(text); | ||
else if (compiler.parse) { | ||
text = compiler.parse(text); | ||
} | ||
else if (typeof blockFilter == 'function') { | ||
text = blockFilter(text); | ||
else if (typeof compiler == 'function') { | ||
text = compiler(text); | ||
} | ||
} | ||
else { | ||
blockFilter = 'text'; | ||
} | ||
text = trim(text); | ||
if (settings.space) { | ||
if (hasHtmlOutput) { | ||
text = '\n' + text; | ||
} | ||
text = text.replace(/\n/g, '\n' + repeat(settings.space, tagDepth)); | ||
if (blockTag) { | ||
text += '\n' + repeat(settings.space, tagDepth - 1); | ||
} | ||
text = ltl.trim(text); | ||
if (settings.space) { | ||
if (hasHtmlOutput) { | ||
text = '\n' + text; | ||
} | ||
text = text.replace(/\n/g, '\n' + ltl.repeat(settings.space, tagDepth)); | ||
if (blockTag) { | ||
text += '\n' + ltl.repeat(settings.space, tagDepth - 1); | ||
} | ||
} | ||
appendText('html', escapeBlock(text)); | ||
blockTag = null; | ||
blockFilter = null; | ||
if (blockProperty) { | ||
var value = properties[blockProperty]; | ||
value = (value ? value + '\n' : '') + text; | ||
properties[blockProperty] = value; | ||
} | ||
else if (blockTarget == 'js') { | ||
appendText('script', text); | ||
} | ||
else { | ||
appendText('html', ltl.escapeBlock(text)); | ||
} | ||
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; | ||
blockTag = null; | ||
blockFilter = null; | ||
blockProperty = null; | ||
blockTarget = 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 (ltl.controlPattern.test(tag)) { | ||
appendText('script', '}'); | ||
if (tag == 'for') { | ||
loopVars.pop(); | ||
} | ||
else if (tag == '-') { | ||
appendText('html', '-->'); | ||
} | ||
else if (!ltl.selfClosePattern.test(tag)) { | ||
var html = '</' + tag + '>'; | ||
tagDepth--; | ||
if (tag == previousTag) { | ||
previousTag = null; | ||
} | ||
else if (controlPattern.test(tag)) { | ||
appendText('script', '}'); | ||
if (tag == 'for') { | ||
loopVars.pop(); | ||
} | ||
else if (settings.space) { | ||
html = '\\n' + ltl.repeat(settings.space, tagDepth) + html; | ||
} | ||
else if (!selfClosePattern.test(tag)) { | ||
var html = '</' + tag + '>'; | ||
tagDepth--; | ||
if (tag == previousTag) { | ||
previousTag = null; | ||
} | ||
else if (settings.space) { | ||
html = '\\n' + repeat(settings.space, tagDepth) + html; | ||
} | ||
appendText('html', html); | ||
} | ||
appendText('html', html); | ||
} | ||
@@ -304,472 +381,568 @@ } | ||
} | ||
} | ||
function transformScript(script) { | ||
var c = settings.contextVar; | ||
var found = false; | ||
function transformScript(script) { | ||
var found = false; | ||
script = script.replace(/^(for)\s+([$A-Za-z_][$A-Za-z_\d]*)\s+in\s+([$A-Za-z_][$A-Za-z_\d\.]*)\s*$/, | ||
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_\d]*)\s+in\s+([$A-Za-z_][$A-Za-z_\d\.]*)\s*$/, | ||
function(match, keyword, item, array) { | ||
found = true; | ||
var i = ltl.vars[varIndex++]; | ||
var l = ltl.vars[varIndex++]; | ||
var e = ltl.vars[varIndex++]; | ||
loopVars.push([[item, e]]); | ||
return 'for(var ' + e + ',' + i + '=0,' + l + '=s.' + array + '.length;' + | ||
i + '<' + l + ';++' + i + ')' + | ||
'{' + e + '=s.' + array + '[' + i + ']' + ';'; | ||
}); | ||
if (found) { | ||
stack.push('for'); | ||
return script; | ||
} | ||
script = script.replace(/^(for)\s+([$A-Za-z_][$A-Za-z_\d]*)\s*,\s*([$A-Za-z_][$A-Za-z_\d]*)\s+of\s+([$A-Za-z_][$A-Za-z_\d\.]*)\s*$/, | ||
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_\d]*)\s*,\s*([$A-Za-z_][$A-Za-z_\d]*)\s+of\s+([$A-Za-z_][$A-Za-z_\d\.]*)\s*$/, | ||
function(match, keyword, key, value, object) { | ||
found = true; | ||
var k = ltl.vars[varIndex++]; | ||
var v = ltl.vars[varIndex++]; | ||
loopVars.push([[key, k], [value, v]]); | ||
return 'for(var ' + k + ' in s.' + object + ')' + | ||
'{if(!s.' + object + '.hasOwnProperty(' + k + '))continue;' + | ||
v + '=s.' + 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 ? '(' + prependState(condition) + ')' : '') + '{'; | ||
}); | ||
stack.push('if'); | ||
return script; | ||
stack.push('if'); | ||
return script; | ||
} | ||
/** | ||
* Give scope to a JavaScript expression by prepending the state variable "s". | ||
* TODO: Parse using acorn so that strings can't be interpreted as ltl.vars. | ||
*/ | ||
function prependState(code, unescapeSingleQuotes) { | ||
// Interpolations got escaped as HTML and must be unescaped to be JS. | ||
if (unescapeSingleQuotes) { | ||
code = code.replace(/\\'/g, "'"); | ||
} | ||
/** | ||
* 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; | ||
var isInString = false; | ||
for (var i = 0; i < tokens.length; i++) { | ||
var token = tokens[i]; | ||
var stringTokens = token.match(/['"]/g); | ||
if (stringTokens) { | ||
isInString = ((isInString ? 1 : 0 ) + stringTokens.length) % 2; | ||
} | ||
else 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]; | ||
} | ||
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]; | ||
var stringTokens = token.match(/['"]/g); | ||
if (stringTokens) { | ||
isInString = ((isInString ? 1 : 0 ) + stringTokens.length) % 2; | ||
} | ||
else if (!isInString) { | ||
if (/^[a-z_]/i.test(token)) { | ||
if (!ltl.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] = settings.contextVar + '.' + token; | ||
} | ||
} | ||
if (!isProperty && !isLoopVar) { | ||
tokens[i] = 's.' + token; | ||
} | ||
} | ||
isProperty = token[token.length - 1] == '.'; | ||
isLoopVar = false; | ||
} | ||
isProperty = token[token.length - 1] == '.'; | ||
isLoopVar = false; | ||
} | ||
code = tokens.join(''); | ||
getPattern = /c\.get\.([$A-Za-z_][$A-Za-z_\d]*)/g; | ||
code = code.replace(getPattern, function (match, part) { | ||
hasGets = true; | ||
return settings.partsVar + "['" + part + "'].call(this," + settings.contextVar + ")"; | ||
}); | ||
return code; | ||
} | ||
code = tokens.join(''); | ||
getPattern = /s\.get\.([$A-Za-z_][$A-Za-z_\d]*)/g; | ||
code = code.replace(getPattern, function (match, part) { | ||
hasGets = true; | ||
return "p['" + part + "'].call(this,s)"; | ||
}); | ||
return code; | ||
} | ||
/** | ||
* 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 + '}'; | ||
/** | ||
* Find ={...}, ${...}, &{...}, and -{...} interpolations. | ||
* Turn them into state-aware insertions unless escaped. | ||
*/ | ||
function interpolate(code) { | ||
return code.replace(/(\\?)([$=&-])\{([^\}]+)\}/g, function(match, backslash, symbol, expression) { | ||
if (backslash) { | ||
return symbol + '{' + expression + '}'; | ||
} | ||
if (symbol == '-') { | ||
if (!escapeCommentVar) { | ||
escapeCommentVar = ltl.vars[varIndex++]; | ||
} | ||
if (symbol == '$') { | ||
if (!escapeHtmlVar) { | ||
escapeHtmlVar = vars[varIndex++]; | ||
} | ||
return "'+" + escapeHtmlVar + '(' + contextify(expression) + ")+'"; | ||
return "'+" + escapeCommentVar + '(' + prependState(expression, true) + ")+'"; | ||
} | ||
else if (symbol == '$') { | ||
if (!escapeHtmlVar) { | ||
escapeHtmlVar = ltl.vars[varIndex++]; | ||
} | ||
else if (symbol == '&') { | ||
if (!encodeUriVar) { | ||
encodeUriVar = vars[varIndex++]; | ||
} | ||
return "'+" + encodeUriVar + '(' + contextify(expression) + ")+'"; | ||
return "'+" + escapeHtmlVar + '(' + prependState(expression, true) + ")+'"; | ||
} | ||
else if (symbol == '&') { | ||
if (!encodeUriVar) { | ||
encodeUriVar = ltl.vars[varIndex++]; | ||
} | ||
else { | ||
return "'+" + contextify(expression) + "+'"; | ||
} | ||
}); | ||
} | ||
return "'+" + encodeUriVar + '(' + prependState(expression, true) + ")+'"; | ||
} | ||
else { | ||
return "'+" + prependState(expression, true) + "+'"; | ||
} | ||
}); | ||
} | ||
// 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; | ||
// Iterate over each line. | ||
for (lineIndex = 0; lineIndex < lineCount; lineIndex++) { | ||
var line = lines[lineIndex]; | ||
var blockName = ''; | ||
var blockSets = []; | ||
var hasGets = false; | ||
var hasAssignments = false; | ||
var inComment = false; | ||
// Mitigate recursion past 100 deep. | ||
var maxT = 1e2; | ||
// Iterate over each line. | ||
for (var i = 0; i < lines.length; i++) { | ||
var line = lines[i]; | ||
// If the line is all whitespace, ignore it. | ||
if (!/\S/.test(line)) { | ||
if (blockIndent) { | ||
(blockLines = blockLines || []).push(''); | ||
} | ||
continue; | ||
} | ||
// Mitigate recursion past 100 deep. | ||
var maxT = 1e2; | ||
// Find the number of leading spaces. | ||
var spaces = line.search(/[^ ]/); | ||
// If the line is all whitespace, ignore it. | ||
if (!/\S/.test(line)) { | ||
continue; | ||
} | ||
// If the first line with content has leading spaces, assume they all do. | ||
if (!hasContent) { | ||
globalSpaces = spaces; | ||
hasContent = true; | ||
} | ||
// Find the number of leading spaces. | ||
var spaces = line.search(/[^ ]/); | ||
var adjustedSpaces = Math.max(spaces - globalSpaces, 0); | ||
// 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 (adjustedSpaces > 0 && !currentTabWidth) { | ||
currentTabWidth = adjustedSpaces; | ||
} | ||
// Calculate the number of levels of indentation. | ||
indent = spaces ? Math.round(spaces / currentTabWidth) : 0; | ||
// Calculate the number of levels of indentation. | ||
indent = adjustedSpaces ? Math.round(adjustedSpaces / 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(); | ||
} | ||
// Strip the leading spaces. | ||
line = line.substring(spaces); | ||
// Backtrack, closing any nested tags that need to be closed. | ||
backtrackIndent(); | ||
if (inComment) { | ||
// If we're still in the block, append to the block code. | ||
else { | ||
line = line.substring(Math.min(spaces, currentTabWidth * blockIndent)); | ||
(blockLines = blockLines || []).push(line); | ||
continue; | ||
} | ||
} | ||
// Control patterns such as if/else/for must transform into true JavaScript. | ||
if (controlPattern.test(line)) { | ||
var script = transformScript(line); | ||
appendText('script', script); | ||
} | ||
// Strip the leading spaces. | ||
line = line.substring(spaces); | ||
// Assignment patterns just need to be contextified. | ||
else if (assignmentPattern.test(line)) { | ||
hasAssignments = true; | ||
line = contextify(line) + ';'; | ||
appendText('script', line); | ||
// 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 (ltl.controlPattern.test(line)) { | ||
var script = transformScript(line); | ||
appendText('script', script); | ||
} | ||
// Assignment patterns just need to be stateified. | ||
else if (ltl.assignmentPattern.test(line)) { | ||
hasAssignments = true; | ||
line = prependState(line) + ';'; | ||
appendText('script', line); | ||
} | ||
// Expression patterns make things append. | ||
else if (ltl.commandPattern.test(line)) { | ||
var data = ltl.trim(line).split(/\s+/); | ||
var command = data.shift(); | ||
blockName = data.shift(); | ||
blockContent = data.join(' '); | ||
var pair = blockName.split(':'); | ||
blockName = pair[0]; | ||
if (command == 'get') { | ||
appendText('html', "'+" + "p['" + blockName + "'].call(this,s)+'"); | ||
hasGets = true; | ||
} | ||
// Expression patterns make things append. | ||
else if (commandPattern.test(line)) { | ||
var pair = trim(line).split(/\s+/); | ||
var command = pair[0]; | ||
blockName = pair[1]; | ||
var content = pair[2]; | ||
pair = blockName.split(':'); | ||
blockName = pair[0]; | ||
if (command == 'get') { | ||
appendText('html', "'+" + settings.partsVar + "['" + blockName + "'].call(this," + settings.contextVar + ")+'"); | ||
hasGets = true; | ||
else { | ||
if (pair[1] === '') { | ||
command += ':'; | ||
} | ||
else { | ||
if (pair[1] === '') { | ||
command += ':'; | ||
} | ||
startBlock(command, content); | ||
} | ||
startBlock(command, blockContent); | ||
} | ||
} | ||
// 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 autoClass = ''; | ||
var classes = []; | ||
var attributes = ''; | ||
var character = ''; | ||
var end = 0; | ||
var content = ''; | ||
// Process the rest of the line recursively. | ||
while (rest && (++t < maxT)) { | ||
var tag = ''; | ||
var id = ''; | ||
var classes = []; | ||
var attributes = ''; | ||
var content = ''; | ||
var character = ''; | ||
var end = 0; | ||
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 = 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 = id || rest.substring(1, end); | ||
rest = rest.substring(end); | ||
} | ||
// If it's a class, read up to the next thing, and save the class. | ||
else if (character == '.') { | ||
end = rest.search(/([@#\(>:\s]|$)/); | ||
classes.push(rest.substring(1, end).replace(/\./g, ' ')); | ||
rest = rest.substring(end); | ||
} | ||
// If it's a class, read up to the next thing, and save the class. | ||
else if (character == '.') { | ||
end = rest.search(/([#\(>:\s]|$)/); | ||
classes.push(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(' ')); | ||
// If the next character is a colon, enter a filter block. | ||
else if (character == ':') { | ||
blockTag = tag; | ||
rest = rest.substring(1).split(' '); | ||
startBlock(rest.shift(), rest.join(' ')); | ||
rest = ''; | ||
break; | ||
} | ||
// If the next character is a plus, store it as a property. | ||
else if (character == '+') { | ||
rest.replace(/^\+([^:\s]+):?(\S*)\s?(.*)$/, function (match, name, filter, content) { | ||
var target = ltl.languages[name] || name; | ||
blockProperty = target; | ||
blockTag = ''; | ||
filter = (name == target ? '' : name) || filter; | ||
startBlock(filter, content); | ||
}); | ||
rest = ''; | ||
break; | ||
} | ||
// If the next character is an "at" symbol, create event listener references. | ||
else if (character == '@') { | ||
rest = rest.replace(/^@([$a-z0-9_~@]*)/i, function (match, events) { | ||
autoClass = autoClass || '_ltl' + (++ltl.lastId); | ||
classes.push(autoClass); | ||
var bind = bindings[autoClass] = bindings[autoClass] || {}; | ||
events = events.split('@'); | ||
for (var e = 0; e < events.length; e++) { | ||
var listeners = events[e].split('~'); | ||
var eventName = listeners.shift(); | ||
var listen = bind[eventName] = bind[eventName] || []; | ||
for (var l = 0; l < listeners.length; l++) { | ||
listen.push(listeners[l]); | ||
} | ||
} | ||
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 tilde, it's an event listener or language. | ||
else if (character == '~') { | ||
rest.replace(/^(~[^:\s]+):?(\S*)\s?(.*)$/, function (match, name, filter, content) { | ||
var target = ltl.languages[name]; | ||
if (target) { | ||
eventLanguage = name; | ||
} | ||
else { | ||
blockProperty = name; | ||
blockTag = ''; | ||
startBlock(filter || eventLanguage, content); | ||
} | ||
}); | ||
rest = ''; | ||
break; | ||
} | ||
// 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 is a space, it's the start of content. | ||
else if (character == ' ') { | ||
content = ltl.trim(rest); | ||
rest = ''; | ||
} | ||
// If it's a comment, set a boolean so we can ignore its contents. | ||
if (tag.indexOf('//') === 0) { | ||
tag = '//'; | ||
inComment = true; | ||
// 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 not a comment, we'll add some HTML. | ||
else { | ||
var className = classes.join(' '); | ||
} | ||
// 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 JS/CSS language, start an inline block. | ||
if (ltl.languages[tag]) { | ||
blockTarget = ltl.languages[tag]; | ||
var filter = (tag == blockTarget ? '' : tag) || filter; | ||
tag = blockTag = blockTarget == 'css' ? 'style' : '//'; | ||
startBlock(filter, content); | ||
} | ||
// Convert ! or doctype into !DOCTYPE and assume html. | ||
if (tag == '!' || tag == 'doctype') { | ||
tag = '!DOCTYPE'; | ||
attributes = attributes || 'html'; | ||
} | ||
// If it's a comment, set a boolean so we can ignore its contents. | ||
if (tag.indexOf('//') === 0) { | ||
tag = '//'; | ||
inComment = true; | ||
} | ||
// Add attributes to the tag. | ||
var html = tag; | ||
if (id) { | ||
html += ' id="' + id + '"'; | ||
} | ||
if (className) { | ||
html += ' class="' + className + '"'; | ||
} | ||
if (attributes) { | ||
html += ' ' + attributes; | ||
} | ||
// If it's not a comment, we'll add some HTML. | ||
else { | ||
var className = classes.join(' '); | ||
// Convert minus to a comment. | ||
if (tag == '-') { | ||
html = '<!--' + content; | ||
// 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'; | ||
} | ||
else { | ||
html = '<' + html + '>' + content; | ||
} | ||
} | ||
html = escapeSingleQuotes(html); | ||
if (tag == 'html' && !/DOCTYPE/.test(output)) { | ||
// 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; | ||
} | ||
// Convert space tag to a space character. | ||
if (tag == 'space') { | ||
html = ' ' + content; | ||
} | ||
// Convert minus to a comment. | ||
else if (tag == '-') { | ||
html = '<!--' + content; | ||
} | ||
else { | ||
html = '<' + html + '>' + content; | ||
} | ||
html = ltl.escapeSingleQuotes(html); | ||
if (tag == 'html') { | ||
// If there's an HTML tag, don't wrap with a state. | ||
hasHtmlTag = true; | ||
if (!/DOCTYPE/.test(output)) { | ||
html = '<!DOCTYPE html>' + (settings.space ? '\\n' : '') + html; | ||
} | ||
} | ||
// Prepend whitespace if requested via settings.space. | ||
if (settings.space) { | ||
html = repeat(settings.space, tagDepth) + html; | ||
// Prepend a line break if this isn't the first tag. | ||
if (hasHtmlOutput) { | ||
html = '\\n' + html; | ||
} | ||
// Prepend whitespace if requested via settings.space. | ||
if (settings.space) { | ||
html = ltl.repeat(settings.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; | ||
} | ||
} | ||
// Make sure we can close this tag. | ||
if (stack[indent]) { | ||
stack[indent] += ',' + tag; | ||
} | ||
else { | ||
stack[indent] = tag; | ||
} | ||
if (tag) { | ||
// Allow same-line tag open/close in settings.space mode. | ||
previousTag = tag; | ||
if (!selfClosePattern.test(tag)) { | ||
tagDepth++; | ||
} | ||
// Make sure we can close this tag. | ||
if (stack[indent]) { | ||
stack[indent] += ',' + tag; | ||
} | ||
else { | ||
stack[indent] = tag; | ||
} | ||
tag = ''; | ||
id = ''; | ||
classes = []; | ||
attributes = ''; | ||
content = ''; | ||
// Allow same-line tag open/close in settings.space mode. | ||
previousTag = tag; | ||
if (!ltl.selfClosePattern.test(tag)) { | ||
tagDepth++; | ||
} | ||
} | ||
tag = ''; | ||
id = ''; | ||
classes = []; | ||
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 ' + settings.outputVar); | ||
// Add the return statement (ending concatenation, where applicable). | ||
appendText('script', 'return o'); | ||
if (blockSets.length) { | ||
return '{' + blockSets.join(',') + '}'; | ||
} | ||
if (blockSets) { | ||
return '{' + blockSets.join(',') + '}'; | ||
} | ||
// Create the function. | ||
if (escapeHtmlVar) { | ||
output = "function " + escapeHtmlVar + "(t){return (t==null?'':''+t).replace(/</g,'<')};" + output; | ||
} | ||
if (encodeUriVar) { | ||
output = "function " + encodeUriVar + "(t){return (encodeURIComponent||escape)(t==null?'':''+t)};" + output; | ||
} | ||
if (hasAssignments) { | ||
output = settings.contextVar + '=' + settings.contextVar + '||{};' + output; | ||
} | ||
output = 'function(' + settings.contextVar + (hasGets ? ',' + settings.partsVar : '') + '){' + output + '}'; | ||
// Create the function. | ||
if (escapeCommentVar) { | ||
output = (settings.name ? | ||
'var ' + escapeCommentVar + "=this['-'];" : | ||
ltl.cache['-'].toString().replace(/\(/, escapeCommentVar + '(') + ';') + output; | ||
} | ||
if (escapeHtmlVar) { | ||
output = (settings.name ? | ||
'var ' + escapeHtmlVar + "=this.$;" : | ||
ltl.cache.$.toString().replace(/\(/, escapeHtmlVar + '(') + ';') + output; | ||
} | ||
if (encodeUriVar) { | ||
output = (settings.name ? | ||
'var ' + encodeUriVar + "=this['&'];" : | ||
ltl.cache['&'].toString().replace(/\(/, encodeUriVar + '(') + ';') + output; | ||
} | ||
if (hasAssignments) { | ||
output = 's=s||{};' + output; | ||
} | ||
output = 'function(s' + (hasGets ? ',p' : '') + '){' + output + '}'; | ||
// Evaluate the template as a function. | ||
try { | ||
eval('eval.f=' + output); // jshint ignore:line | ||
} | ||
catch (e) { | ||
// If we failed in a dev environment in Node, we can try to debug it. | ||
if (process && settings.enableDebug) { | ||
debug(output, settings); | ||
// Evaluate the template as a function. | ||
name = settings.name; | ||
var template; | ||
try { | ||
template = ltl.run(output, name); | ||
} | ||
catch (e) { | ||
name = (name ? '"' + name + '"' : 'template'); | ||
e.message = '[Ltl] Failed to compile ' + name + '. ' + e.message; | ||
throw e; | ||
} | ||
// If there's a name specified, cache it and refer to it. | ||
if (name) { | ||
ltl.cache[name] = template; | ||
template.key = name; | ||
template.cache = ltl.cache; | ||
} | ||
// Add event bindings to the JS property. | ||
for (var key in bindings) { | ||
var events = bindings[key]; | ||
for (var event in events) { | ||
var listeners = events[event]; | ||
for (var i = 0; i < listeners.length; i++) { | ||
var listener = listeners[i]; | ||
var js = properties['~' + listener]; | ||
properties.js = properties.js ? properties.js + '\n' : ''; | ||
properties.js += 'Jymin.on(".' + key + '","' + event + '",' + | ||
'function(element,event,target){' + js + '});'; | ||
} | ||
// Otherwise, just fail. | ||
else { | ||
var name = (settings.name ? '"' + settings.name + '"' : 'template'); | ||
e.message = '[Ltl] Failed to compile ' + name + '. ' + e.message; | ||
throw e; | ||
} | ||
} | ||
var template = eval.f; | ||
} | ||
// If there's a name specified, cache the template with that name. | ||
if (settings.name) { | ||
this.cache[settings.name] = template; | ||
// Add any discovered properties to the template. | ||
for (name in properties) { | ||
if (name[0] != '~') { | ||
template[name] = template[name] || properties[name]; | ||
} | ||
return template; | ||
} | ||
}; | ||
if (isBrowser) { | ||
window.ltl = ltl; | ||
return template; | ||
} | ||
else { | ||
module.exports = ltl; | ||
} | ||
}; | ||
})(); | ||
// Export Ltl as a CommonJS module. | ||
if (typeof module !== 'undefined') { | ||
module.exports = ltl; | ||
} |
{ | ||
"name": "ltl", | ||
"version": "0.1.15", | ||
"version": "0.2.0", | ||
"description": "Lean Template Language for JavaScript and HTML", | ||
"dependencies": {}, | ||
"devDependencies": { | ||
"coffee-script": "1.8.0", | ||
"coveralls": "2.11.1", | ||
"dot": "1.0.2", | ||
"exam": "^0.0.7", | ||
"istanbul": "0.3.1", | ||
"jade": "1.6.0", | ||
"coffee-script": "1.9.0", | ||
"dot": "^1.0.3", | ||
"exam": "^0.2.4", | ||
"jade": "^1.9.2", | ||
"jsx": "0.9.89", | ||
"less": "^2.4.0", | ||
"markdown": "0.5.0", | ||
"marked": "0.3.2", | ||
"zeriousify": "^0.1.10" | ||
"marked": "0.3.3" | ||
}, | ||
@@ -33,5 +32,5 @@ "keywords": [ | ||
"test": "./node_modules/exam/exam.js", | ||
"cover": "istanbul cover ./node_modules/exam/exam.js", | ||
"cover": "./node_modules/exam/exam-cover.js", | ||
"report": "open coverage/lcov-report/index.html", | ||
"coveralls": "istanbul cover ./node_modules/exam/exam.js --report lcovonly -- -R spec && cat ./coverage/lcov.info | coveralls && rm -rf ./coverage", | ||
"coveralls": "./node_modules/exam/exam-coveralls.js", | ||
"perf": "node perf" | ||
@@ -38,0 +37,0 @@ }, |
281
README.md
@@ -1,4 +0,4 @@ | ||
# Ltl | ||
[![NPM Version](https://img.shields.io/npm/v/ltl.svg) ![Downloads](https://img.shields.io/npm/dm/ltl.svg)](https://npmjs.org/package/ltl) | ||
# <a href="http://lighter.io/ltl" style="font-size:40px;text-decoration:none;color:#000"><img src="https://cdn.rawgit.com/lighterio/lighter.io/master/public/ltl.svg" style="width:90px;height:90px"> Ltl</a> | ||
[![NPM Version](https://img.shields.io/npm/v/ltl.svg)](https://npmjs.org/package/ltl) | ||
[![Downloads](https://img.shields.io/npm/dm/ltl.svg)](https://npmjs.org/package/ltl) | ||
[![Build Status](https://img.shields.io/travis/lighterio/ltl.svg)](https://travis-ci.org/lighterio/ltl) | ||
@@ -9,3 +9,4 @@ [![Code Coverage](https://img.shields.io/coveralls/lighterio/ltl/master.svg)](https://coveralls.io/r/lighterio/ltl) | ||
The Ltl template language (pronounced "little") uses a clean | ||
The [Ltl](http://lighter.io/ltl) template language (pronounced "little") uses a clean | ||
[Jade](http://jade-lang.com/reference/)-like syntax to generate | ||
@@ -16,18 +17,23 @@ HTML at [doT](https://github.com/olado/doT)-like speeds. | ||
## Getting Started | ||
Add Ltl to your project. | ||
## Quick Start | ||
Add `ltl` to your project: | ||
```bash | ||
$ npm install --save ltl | ||
npm install --save ltl | ||
``` | ||
Compile and render templates. | ||
Compile and render templates: | ||
```javascript | ||
var ltl = require('ltl'); | ||
var template = ltl.compile('#hi Hello #{name}!'); | ||
var result = template({name: 'World'}); | ||
// result: '<div id="hi">Hello World!</div>' | ||
var ltl = require("ltl"); | ||
var template = ltl.compile("#hi Hello ${who}!"); | ||
var result = template({who: "World"}); | ||
``` | ||
```html | ||
<div id="hi">Hello World!</div> | ||
``` | ||
## API | ||
### ltl.compile(code, [options]) | ||
@@ -44,7 +50,7 @@ * `code` is a string of Ltl code. | ||
Supported options: | ||
* **outputVar** is the name of the variable that Ltl concatenates to. (Default: "o") | ||
* **contextVar** is the name of the argument that passes context into a template. (Default: "c") | ||
* **partsVar** is the name of the argument that a Ltl template receives from callers. (Default: "p") | ||
* **tabWidth** is the number of spaces that tabs are converted to before compilation. (Default: 4) | ||
* `tabWidth` is the number of spaces that tabs are converted to before compilation. (Default: 4) | ||
### ltl.targets | ||
Targets are key-value pairs of transpiler names and target language names. | ||
## Language | ||
@@ -153,4 +159,4 @@ | ||
:markdown | ||
# Ltl | ||
It's a recursive acronym for "Ltl Template Language". | ||
# Ltl | ||
It's a recursive acronym for "Ltl Template Language". | ||
``` | ||
@@ -165,3 +171,3 @@ ```html | ||
A filter must have a function named `compile` or `parse` which accepts a context | ||
A filter must have a function named `compile` or `parse` which accepts a state | ||
and returns a string, or it can be such a function itself. | ||
@@ -199,3 +205,3 @@ | ||
You can output the value of a context property with `${..}`, | ||
You can output the value of a state property with `${..}`, | ||
and special HTML characters will be escaped for you to | ||
@@ -214,3 +220,3 @@ prevent silly little XSS attacks. | ||
Context: `{query: "good brewpubs"}` | ||
State: `{query: "good brewpubs"}` | ||
```jade | ||
@@ -227,3 +233,3 @@ a(href="?q=&{query}") | ||
Context: `{unsafe: "<script>alert('Gotcha!')</script>"}` | ||
State: `{unsafe: "<script>alert('Gotcha!')</script>"}` | ||
```jade | ||
@@ -247,3 +253,3 @@ . ={unsafe} | ||
You can assign a value to a variable in the template context using `=`. | ||
You can assign a value to a variable in the template state using `=`. | ||
```jade | ||
@@ -258,5 +264,5 @@ who = 'World' | ||
### Control | ||
Use `for..in` to iterate over an array inside the context. | ||
Use `for..in` to iterate over an array inside the state. | ||
*Context:* `{list: ['IPA', 'Porter', 'Stout']}` | ||
*State:* `{list: ['IPA', 'Porter', 'Stout']}` | ||
@@ -266,3 +272,3 @@ ```jade | ||
for item in list | ||
li #{item} | ||
li ${item} | ||
``` | ||
@@ -275,6 +281,6 @@ ```html | ||
*Context:* `{pairings: {Coffee: 'coding', Beer: 'bloviating'}}` | ||
*State:* `{pairings: {Coffee: 'coding', Beer: 'bloviating'}}` | ||
```jade | ||
for drink, activity of pairings | ||
. #{field} is for #{value}. | ||
. ${field} is for ${value}. | ||
``` | ||
@@ -306,3 +312,3 @@ ```html | ||
### Using templates within templates | ||
### Calling templates within templates | ||
@@ -312,6 +318,6 @@ A template can call another template with `call`. To accomplish | ||
they will be stored in `ltl.cache`. The template that's being | ||
called can access the data context. | ||
```jade | ||
called can access the data state. | ||
```js | ||
var temp = ltl.compile('p\n call bold', {name: 'temp'}); | ||
var bold = ltl.compile('b #{text}', {name: 'bold'}); | ||
var bold = ltl.compile('b ${text}', {name: 'bold'}); | ||
ltl.cache.temp({text: 'Hi!'}); | ||
@@ -327,3 +333,3 @@ ``` | ||
reads data with `get` blocks. | ||
```jade | ||
```js | ||
var layout = ltl.compile('#nav\n get nav\n#content\n get content', {name: 'layout'}); | ||
@@ -337,46 +343,199 @@ var page = ltl.compile('call layout\n set nav\n . Nav\n set content\n . Content', {name: 'page'}); | ||
#### Passing sub-states | ||
## Contributing | ||
A template can pass a portion of its state to another template by naming the | ||
sub-state property after the template name in a call block: | ||
Clone the repository. | ||
```bash | ||
$ git clone https://github.com/lighterio/ltl.git | ||
**parent/view.ltl**: | ||
```jade | ||
p Expect a state like... {child: {name: "only child"}} | ||
call child/view child | ||
``` | ||
Install dependencies. | ||
```bash | ||
$ npm install | ||
**child/view.ltl** | ||
``` | ||
p This child is called ${name}. | ||
``` | ||
### Testing | ||
### Template properties | ||
Run all tests. | ||
```bash | ||
$ npm test | ||
A template can have properties applied to it by using a plus symbol. | ||
**extra.ltl**: | ||
```jade | ||
html | ||
head>title Template Properties | ||
body:md | ||
Properties can be used to provide hidden values to systems that compile | ||
Ltl templates, such as [Chug](http://lighter.io/chug). | ||
+extra | ||
When compiled, the template will become a JavaScript function as usual. | ||
In addition, it will have a property called "extra", whose value will be | ||
a string containing the contents of this block. | ||
+extra | ||
If the plus symbol is used more than once for the same property, the value | ||
of that property will be a concatenation of multiple blocks. | ||
+also:md | ||
# Also supports filters | ||
Properties can have filters. This block will be evaluated as markdown, | ||
and the resulting value will be set as the "also" property of the template. | ||
// Note: | ||
There are several reserved | ||
``` | ||
Run tests an rerun them after changes are made. | ||
```bash | ||
$ npm run retest | ||
### JS and CSS properties | ||
The **js** and **css** properties of a template can be set using the plus | ||
symbol, just like other properties. Unlike including JS or CSS in a script or | ||
style tag block, these properties would need to be added to a page externally | ||
in order to affect the HTML. | ||
**js-and-css.ltl**: | ||
```jade | ||
p This will be included in the template's rendered HTML. | ||
+js | ||
console.log("This will not be included in the template's js property."); | ||
+css | ||
p {color: black} | ||
``` | ||
Run individual test files. | ||
```bash | ||
$ mocha test/api | ||
$ mocha test/blocks | ||
$ mocha test/control | ||
$ mocha test/interpolation | ||
... | ||
In addition, several languages that compile to JS/CSS are supported. Their | ||
compilers can be invoked using their corresponding file extensions. For | ||
JS, Ltl supports **coffee**, **litcoffee**, **iced**, **es6**, | ||
and **ts**. For CSS, it supports **less**, **scss** and **styl**. | ||
**coffee-and-less.ltl**: | ||
```jade | ||
p:md | ||
This template will compile to a function which returns this paragraph, and | ||
the function will have **js** and **css** properties. | ||
+coffee | ||
console.log "Hello from CoffeeScript!" | ||
+styl | ||
@textColor: #000; | ||
p { | ||
color: @textColor; | ||
} | ||
``` | ||
Test coverage (100% required). | ||
```bash | ||
$ npm run cover | ||
### Inline JS and CSS | ||
JavaScript and CSS can also be included inline in a template using directives | ||
that appear as tags. Just as with JS and CSS properties, these support | ||
compilers such as CoffeeScript and LESS. | ||
**inline-js-and-css.ltl** | ||
```jade | ||
less | ||
a {color: #000;} | ||
coffee | ||
s.linkText = 'hello' | ||
a ${linkText} | ||
``` | ||
View coverage report in a browser (uses Mac OS-friendly `open`). | ||
```bash | ||
$ npm run report | ||
The state variable in a template is called `s`, so the above would set the | ||
`linkText` value in the state object, and then it would render the following | ||
HTML if called: | ||
```html | ||
<style>a {color: #000;}</style><a>hello</a> | ||
``` | ||
### Write something awesome and submit a pull request! | ||
## Acknowledgements | ||
We would like to thank all of the amazing people who use, support, | ||
promote, enhance, document, patch, and submit comments & issues. | ||
Ltl couldn't exist without you. | ||
Additionally, huge thanks go to [TUNE](http://www.tune.com) for employing | ||
and supporting [Ltl](http://lighter.io/ltl) project maintainers, | ||
and for being an epically awesome place to work (and play). | ||
## MIT License | ||
Copyright (c) 2014 Sam Eubank | ||
Permission is hereby granted, free of charge, to any person obtaining a copy | ||
of this software and associated documentation files (the "Software"), to deal | ||
in the Software without restriction, including without limitation the rights | ||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
copies of the Software, and to permit persons to whom the Software is | ||
furnished to do so, subject to the following conditions: | ||
The above copyright notice and this permission notice shall be included in all | ||
copies or substantial portions of the Software. | ||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
SOFTWARE. | ||
## How to Contribute | ||
We welcome contributions from the community and are happy to have them. | ||
Please follow this guide when logging issues or making code changes. | ||
### Logging Issues | ||
All issues should be created using the | ||
[new issue form](https://github.com/lighterio/ltl/issues/new). | ||
Please describe the issue including steps to reproduce. Also, make sure | ||
to indicate the version that has the issue. | ||
### Changing Code | ||
Code changes are welcome and encouraged! Please follow our process: | ||
1. Fork the repository on GitHub. | ||
2. Fix the issue ensuring that your code follows the | ||
[style guide](http://lighter.io/style-guide). | ||
3. Add tests for your new code, ensuring that you have 100% code coverage. | ||
(If necessary, we can help you reach 100% prior to merging.) | ||
* Run `npm test` to run tests quickly, without testing coverage. | ||
* Run `npm run cover` to test coverage and generate a report. | ||
* Run `npm run report` to open the coverage report you generated. | ||
4. [Pull requests](http://help.github.com/send-pull-requests/) should be made | ||
to the [master branch](https://github.com/lighterio/ltl/tree/master). | ||
### Contributor Code of Conduct | ||
As contributors and maintainers of Ltl, we pledge to respect all | ||
people who contribute through reporting issues, posting feature requests, | ||
updating documentation, submitting pull requests or patches, and other | ||
activities. | ||
If any participant in this project has issues or takes exception with a | ||
contribution, they are obligated to provide constructive feedback and never | ||
resort to personal attacks, trolling, public or private harassment, insults, or | ||
other unprofessional conduct. | ||
Project maintainers have the right and responsibility to remove, edit, or | ||
reject comments, commits, code, edits, issues, and other contributions | ||
that are not aligned with this Code of Conduct. Project maintainers who do | ||
not follow the Code of Conduct may be removed from the project team. | ||
Instances of abusive, harassing, or otherwise unacceptable behavior may be | ||
reported by opening an issue or contacting one or more of the project | ||
maintainers. | ||
We promise to extend courtesy and respect to everyone involved in this project | ||
regardless of gender, gender identity, sexual orientation, ability or | ||
disability, ethnicity, religion, age, location, native language, or level of | ||
experience. |
Debug access
Supply chain riskUses debug, reflection and dynamic code execution features.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
Unidentified License
License(Experimental) Something that seems like a license was found, but its contents could not be matched with a known license.
Found 1 instance in 1 package
52488
8
7
0
100
1113
526
3
1