mirror of
https://github.com/harttle/liquidjs.git
synced 2026-09-15 04:10:40 -07:00
tag:include
This commit is contained in:
@@ -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
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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
@@ -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
|
||||
};
|
||||
};
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user