tag:include

This commit is contained in:
harttle
2016-06-24 02:44:20 +08:00
parent 21bde3cb6d
commit 8fbbec089f
24 changed files with 226 additions and 52 deletions
+21
View File
@@ -0,0 +1,21 @@
function TokenizationError(message, input, line, stack) {
this.name = "TokenizationError";
this.message = (message || "");
this.input = input;
this.line = line;
if(stack) this.stack = stack;
}
TokenizationError.prototype = Error.prototype;
function ParseError(message, input, line, stack) {
this.name = "ParseError";
this.message = (message || "");
this.input = input;
this.line = line;
if(stack) this.stack = stack;
}
ParseError.prototype = Error.prototype;
module.exports = {
TokenizationError, ParseError
};
+54
View File
@@ -0,0 +1,54 @@
const syntax = require('./syntax.js');
const Exp = require('./expression.js');
const lexical = require('./lexical.js');
const _ = require('lodash');
function evalExp(exp, scope) {
if (!scope) throw new Error('unable to evalExp: scope undefined');
var operatorREs = lexical.operators,
match;
for (var i = 0; i < operatorREs.length; i++) {
var operatorRE = operatorREs[i];
var expRE = new RegExp(`^(${lexical.quoteBalanced.source})(${operatorRE.source})(${lexical.quoteBalanced.source})$`);
if (match = exp.match(expRE)) {
var l = evalExp(match[1], scope);
var op = syntax.operators[match[2].trim()];
var r = evalExp(match[3], scope);
return op(l, r);
}
}
if (match = exp.match(lexical.rangeLine)) {
var low = evalValue(match[1], scope),
high = evalValue(match[2], scope) + 1;
return _.range(low, high);
}
return evalValue(exp, scope);
}
function evalValue(str, scope) {
str = str && str.trim();
if (!str) return undefined;
if (lexical.isLiteral(str)) {
var a = lexical.parseLiteral(str);
return lexical.parseLiteral(str);
}
if (lexical.isVariable(str)) {
return scope.get(str);
}
}
function isTruthy(val) {
if (val instanceof Array) return !!val.length;
return !!val;
}
function isFalsy(val) {
return !isTruthy(val);
}
module.exports = {
evalExp, evalValue, isTruthy, isFalsy
};
+55
View File
@@ -0,0 +1,55 @@
const lexical = require('./lexical.js');
const _ = require('lodash');
const Exp = require('./expression.js');
var valueRE = new RegExp(`${lexical.value.source}`, 'g');
module.exports = function() {
var filters = {};
var _filterInstance = {
render: function(output, scope) {
var args = this.args.map(arg => Exp.evalValue(arg, scope));
args.unshift(output);
return this.filter.apply(null, args);
},
parse: function(str) {
var match = lexical.filterLine.exec(str);
if (!match) throw new Error('illegal filter: ' + str);
var name = match[1], argList = match[2] || '', filter = filters[name];
if (typeof filter !== 'function'){
throw new Error(`filter "${name}" not found`);
}
var args = [];
while(match = valueRE.exec(argList.trim())){
args.push(match[0]);
}
this.name = name;
this.filter = filter;
this.args = args;
return this;
}
};
function construct(str) {
var instance = Object.create(_filterInstance);
instance.parse(str);
return instance;
}
function register(name, filter) {
filters[name] = filter;
}
function clear() {
filters = {};
}
return {
construct, register, clear
};
};
+80
View File
@@ -0,0 +1,80 @@
const _ = require('lodash');
// quote related
var singleQuoted = /'[^']*'/;
var doubleQuoted = /"[^"]*"/;
var quoteBalanced = new RegExp(`(?:${singleQuoted.source}|${doubleQuoted.source}|[^'"])*`);
var number = /(?:-?\d+\.?\d*|\.?\d+)/;
var bool = /true|false/;
var identifier = /[a-zA-Z_$][a-zA-Z_$0-9]*/;
var subscript = /\[\d+\]/;
var quoted = new RegExp(`(?:${singleQuoted.source}|${doubleQuoted.source})`);
var literal = new RegExp(`(?:${quoted.source}|${bool.source}|${number.source})`);
var variable = new RegExp(`${identifier.source}(?:\\.${identifier.source}|${subscript.source})*`);
// range related
var rangeLimit = new RegExp(`(?:${variable.source}|${number.source})`);
var range = new RegExp(`\\(${rangeLimit.source}\\.\\.${rangeLimit.source}\\)`);
var rangeCapture = new RegExp(`\\((${rangeLimit.source})\\.\\.(${rangeLimit.source})\\)`);
var value = new RegExp(`(?:${literal.source}|${variable.source}|${range.source})`);
// hash related
var hash = new RegExp(`(?:${identifier.source})\\s*:\\s*(?:${value.source})`);
var hashCapture = new RegExp(`(${identifier.source})\\s*:\\s*(${value.source})`, 'g');
var tagLine = new RegExp(`^\\s*(${identifier.source})\\s*(.*)\\s*$`);
var literalLine = new RegExp(`^${literal.source}$`, 'i');
var variableLine = new RegExp(`^${variable.source}$`);
var numberLine = new RegExp(`^${number.source}$`);
var boolLine = new RegExp(`^${bool.source}$`, 'i');
var quotedLine = new RegExp(`^${quoted.source}$`);
var rangeLine = new RegExp(`^${rangeCapture.source}$`);
// filter related
var valueList = new RegExp(`${value.source}(\\s*,\\s*${value.source})*`);
var filter = new RegExp(`${identifier.source}(?:\\s*:\\s*${valueList.source})?`, 'g');
var filterCapture = new RegExp(`(${identifier.source})(?:\\s*:\\s*(${valueList.source}))?`);
var filterLine = new RegExp(`^${filterCapture.source}$`);
var operators = [
/\s+or\s+/,
/\s+and\s+/,
/==|!=|<=|>=|<|>|\s+contains\s+/
];
function isLiteral(str) {
return literalLine.test(str);
}
function isRange(str) {
return rangeLine.test(str);
}
function isVariable(str) {
return variableLine.test(str);
}
function parseLiteral(str) {
var res;
if (res = str.match(numberLine)) {
return Number(str);
}
if (res = str.match(boolLine)) {
return str.toLowerCase() === 'true';
}
if (res = str.match(quotedLine)) {
return str.slice(1, -1);
}
}
module.exports = {
quoted, number, bool, literal, filter,
hash, hashCapture,
range, rangeCapture,
identifier, value, quoteBalanced, operators,
quotedLine, numberLine, boolLine, rangeLine, literalLine, filterLine, tagLine,
isLiteral, isVariable, parseLiteral, isRange
};
+99
View File
@@ -0,0 +1,99 @@
const lexical = require('./lexical.js');
const error = require('./error.js');
const ParseError = require('./error.js').ParseError;
module.exports = function(Tag, Filter) {
var stream = {
init: function(tokens) {
this.tokens = tokens;
this.handlers = {};
return this;
},
on: function(name, cb) {
this.handlers[name] = cb;
return this;
},
trigger: function(event, arg) {
var h = this.handlers[event];
if (typeof h === 'function') {
h(arg);
return true;
}
},
start: function() {
this.trigger('start');
while (!this.stopRequested && (token = this.tokens.shift())) {
if (this.trigger('token', token)) continue;
if (token.type == 'tag' &&
this.trigger(`tag:${token.name}`, token)) {
continue;
}
var template = parseToken(token, this.tokens);
this.trigger('template', template);
}
if (!this.stopRequested) this.trigger('end');
return this;
},
stop: function() {
this.stopRequested = true;
return this;
}
};
function parse(tokens) {
var token, templates = [];
while (token = tokens.shift()) {
templates.push(parseToken(token, tokens));
}
return templates;
}
function parseToken(token, tokens) {
try {
switch (token.type) {
case 'tag':
return parseTag(token, tokens);
case 'output':
return parseOutput(token.value);
case 'html':
return token;
}
} catch (e) {
throw new ParseError(e.message, token.input, token.line, e.stack);
}
}
function parseTag(token, tokens) {
if (token.name === 'continue' || token.name === 'break') return token;
return Tag.construct(token, tokens);
}
function parseOutput(str) {
var match = lexical.value.exec(str);
if(!match) throw new Error(`illegal output string: ${str}`);
var initial = match[0];
str = str.substr(match.index + match[0].length);
var filters = [];
while(match = lexical.filter.exec(str)){
filters.push([match[0].trim()]);
}
return {
type: 'output',
initial: initial,
filters: filters.map(str => Filter.construct(str))
};
}
function parseStream(tokens) {
var s = Object.create(stream);
return s.init(tokens);
}
return {
parse, parseTag, parseStream, parseOutput
};
};
+66
View File
@@ -0,0 +1,66 @@
const error = require('./error.js');
const Exp = require('./expression.js');
const assert = require('assert');
var render = {
renderTemplates: function(templates, scope) {
assert(scope, 'unable to evalTemplates: scope undefined');
var html = '',
partial;
templates.some(template => {
if (scope.get('forloop.skip')) return true;
switch (template.type) {
case 'tag':
partial = this.renderTag(template, scope, this.register);
if (partial === undefined) return true;
html += partial;
break;
case 'html':
html += template.value;
break;
case 'output':
var val = this.evalOutput(template, scope);
html += stringify(val);
}
});
return html;
},
renderTag: function(template, scope, register) {
if (template.name === 'continue') {
scope.set('forloop.skip', true);
return;
}
if (template.name === 'break') {
scope.set('forloop.stop', true);
scope.set('forloop.skip', true);
return;
}
return template.render(scope, register);
},
evalOutput: function(template, scope) {
assert(scope, 'unable to evalOutput: scope undefined');
var val = Exp.evalExp(template.initial, scope);
return template.filters
.reduce((v, filter) => filter.render(v, scope), val);
},
resetRegisters: function(){
return this.register = {};
}
};
function factory() {
var instance = Object.create(render);
instance.register = {};
return instance;
}
function stringify(val) {
if (typeof val === 'string') return val;
return JSON.stringify(val);
}
module.exports = factory;
+30
View File
@@ -0,0 +1,30 @@
const _ = require('lodash');
const lexical = require('./lexical.js');
const error = require('./error.js');
var scope = {
get: function(str) {
for (var i = this.scopes.length - 1; i >= 0; i--) {
var v = _.get(this.scopes[i], str);
if (v !== undefined) return v;
}
return '';
},
set: function(k, v) {
_.set(this.scopes[this.scopes.length - 1], k, v);
return this;
},
push: function(ctx) {
if (!ctx) error(`trying to push ${ctx} into scopes`);
return this.scopes.push(ctx);
},
pop: function() {
return this.scopes.pop();
}
};
exports.factory = function(_ctx) {
var ctx = Object.create(scope);
ctx.scopes = _ctx ? [_ctx] : [];
return ctx;
};
+13
View File
@@ -0,0 +1,13 @@
var operators = {
'==': (l, r) => l == r,
'!=': (l, r) => l != r,
'>': (l, r) => l > r,
'<': (l, r) => l < r,
'>=': (l, r) => l >= r,
'<=': (l, r) => l <= r,
'contains': (l, r) => l.indexOf(r) > -1,
'and': (l, r) => l && r,
'or': (l, r) => l || r
};
exports.operators = operators;
+57
View File
@@ -0,0 +1,57 @@
const lexical = require('./lexical.js');
const Exp = require('./expression.js');
const TokenizationError = require('./error.js').TokenizationError;
function hash(markup, scope) {
var obj = {};
lexical.hashCapture.lastIndex = 0;
while (match = lexical.hashCapture.exec(markup)) {
var k = match[1],
v = match[2];
obj[k] = Exp.evalValue(v, scope);
}
return obj;
}
module.exports = function() {
var tagImpls = {};
var _tagInstance = {
render: function(scope, register) {
var reg = register[this.name];
if(!reg) reg = register[this.name] = {};
var obj = hash(this.token.args, scope);
return this.tagImpl.render && this.tagImpl.render(scope, obj, reg) || '';
},
parse: function(token, tokens){
this.type = 'tag';
this.token = token;
this.name = token.name;
var tagImpl = tagImpls[this.name];
if (!tagImpl) throw new Error(`tag ${this.name} not found`);
this.tagImpl = Object.create(tagImpl);
if(this.tagImpl.parse){
this.tagImpl.parse(token, tokens);
}
}
};
function register(name, tag) {
tagImpls[name] = tag;
}
function construct(token, tokens) {
var instance = Object.create(_tagInstance);
instance.parse(token, tokens);
return instance;
}
function clear() {
tagImpls = {};
}
return {
construct, register, clear
};
};
+80
View File
@@ -0,0 +1,80 @@
const lexical = require('./lexical.js');
const TokenizationError = require('./error.js').TokenizationError;
function parse(html) {
var tokens = [];
if (!html) return tokens;
var syntax = /({%(.*?)%})|({{(.*?)}})/g;
var result, htmlFragment, token;
var lastMatchEnd = 0, lastMatchBegin = -1, parsedLinesCount = 0;
while ((result = syntax.exec(html)) !== null) {
// passed html fragments
if (result.index > lastMatchEnd) {
htmlFragment = html.slice(lastMatchEnd, result.index);
tokens.push({
type: 'html',
raw: htmlFragment,
value: htmlFragment
});
}
// tag appeared
if (result[1]) {
token = factory('tag', 1, result);
var match = token.value.match(lexical.tagLine);
if (!match) {
throw new TokenizationError(`illegal tag: ${token.raw}`,
token.input, token.line);
}
token.name = match[1];
token.args = match[2];
tokens.push(token);
}
// output
else {
token = factory('output', 3, result);
tokens.push(token);
}
lastMatchEnd = syntax.lastIndex;
}
// remaining html
if (html.length > lastMatchEnd) {
htmlFragment = html.slice(lastMatchEnd, html.length);
tokens.push({
type: 'html',
raw: htmlFragment,
value: htmlFragment
});
}
return tokens;
function factory(type, offset, match) {
return {
type: type,
raw: match[offset],
value: match[offset + 1].trim(),
line: getLineNum(match),
input: getLineContent(match)
};
}
function getLineContent(match) {
var idx1 = match.input.lastIndexOf('\n', match.index);
var idx2 = match.input.indexOf('\n', match.index);
if (idx2 === -1) idx2 = match.input.length;
return match.input.slice(idx1 + 1, idx2);
}
function getLineNum(match) {
var lines = match.input.slice(lastMatchBegin + 1, match.index).split('\n');
parsedLinesCount += lines.length - 1;
lastMatchBegin = match.index;
return parsedLinesCount + 1;
}
}
exports.parse = parse;