bem-xjst
Version:
Declarative Template Engine for the browser and server
268 lines (212 loc) • 6.3 kB
JavaScript
var tree = require('./tree');
var PropertyMatch = tree.PropertyMatch;
var AddMatch = tree.AddMatch;
var WrapMatch = tree.WrapMatch;
var ExtendMatch = tree.ExtendMatch;
var CustomMatch = tree.CustomMatch;
function MatchNested(template, pred) {
this.template = template;
this.keys = pred.key;
this.value = pred.value;
}
MatchNested.prototype.exec = function(context) {
var val = context;
var keys = this.keys;
for (var i = 0; i < keys.length - 1; i++) {
val = val[keys[i]];
if (!val)
return false;
}
val = val[keys[i]];
if (this.value === true)
return val !== undefined && val !== '' && val !== false && val !== null;
return String(val) === this.value;
};
function MatchCustom(template, pred) {
this.template = template;
this.body = pred.body;
}
MatchCustom.prototype.exec = function(context) {
return this.body.call(context, context, context.ctx);
};
function MatchWrap(template) {
this.template = template;
this.wrap = null;
}
MatchWrap.prototype.exec = function(context) {
var res = this.wrap !== context.ctx;
this.wrap = context.ctx;
return res;
};
function MatchExtend(template) {
this.template = template;
this.wrap = null;
}
MatchExtend.prototype.exec = function(context) {
var res = this.ext !== context.ctx;
this.ext = context.ctx;
return res;
};
function AddWrap(template, pred) {
this.template = template;
this.key = pred.key;
this.value = pred.value;
}
AddWrap.prototype.exec = function(context) {
return context[this.key] === this.value;
};
function MatchTemplate(mode, template) {
this.mode = mode;
this.predicates = new Array(template.predicates.length);
this.body = template.body;
var postpone = [];
for (var i = 0, j = 0; i < this.predicates.length; i++, j++) {
var pred = template.predicates[i];
if (pred instanceof PropertyMatch) {
this.predicates[j] = new MatchNested(this, pred);
} else if (pred instanceof ExtendMatch) {
j--;
postpone.push(new MatchExtend(this));
} else if (pred instanceof AddMatch) {
this.predicates[j] = new AddWrap(this, pred);
} else if (pred instanceof CustomMatch) {
this.predicates[j] = new MatchCustom(this, pred);
// Push MatchWrap later, they should not be executed first.
// Otherwise they will set flag too early, and body might not be executed
} else if (pred instanceof WrapMatch) {
j--;
postpone.push(new MatchWrap(this));
} else {
// Skip
j--;
}
}
// Insert late predicates
for (var i = 0; i < postpone.length; i++, j++)
this.predicates[j] = postpone[i];
if (this.predicates.length !== j)
this.predicates.length = j;
}
exports.MatchTemplate = MatchTemplate;
function Match(entity, modeName) {
this.entity = entity;
this.modeName = modeName;
this.bemxjst = this.entity.bemxjst;
this.templates = [];
// applyNext mask
this.mask = [ 0 ];
// We are going to create copies of mask for nested `applyNext()`
this.maskSize = 0;
this.maskOffset = 0;
this.count = 0;
this.depth = -1;
this.thrownError = null;
}
exports.Match = Match;
Match.prototype.prepend = function(other) {
this.templates = other.templates.concat(this.templates);
this.count += other.count;
while (Math.ceil(this.count / 31) > this.mask.length)
this.mask.push(0);
this.maskSize = this.mask.length;
};
Match.prototype.push = function(template) {
this.templates.push(new MatchTemplate(this, template));
this.count++;
if (Math.ceil(this.count / 31) > this.mask.length)
this.mask.push(0);
this.maskSize = this.mask.length;
};
Match.prototype.tryCatch = function(fn, ctx) {
try {
return fn.call(ctx, ctx, ctx.ctx);
} catch (e) {
this.thrownError = e;
if (this.modeName) {
this.thrownError.ctx = ctx;
this.thrownError.name = 'BEMXJST ERROR';
var classBuilder = this.entity.bemxjst.classBuilder;
var cause = e.stack.split('\n')[1];
this.thrownError.message = 'Template error in mode ' +
this.modeName + ' in block ' +
classBuilder.build(ctx.ctx.block, ctx.ctx.elem) +
'\n ' + e.message + '\n';
this.thrownError.stack = this.thrownError.name + ': ' +
this.thrownError.message + ' ' + cause + '\n' + e.stack;
}
}
};
Match.prototype.exec = function(context) {
var save = this.checkDepth();
var template;
var bitIndex = this.maskOffset;
var mask = this.mask[bitIndex];
var bit = 1;
for (var i = 0; i < this.count; i++) {
if ((mask & bit) === 0) {
template = this.templates[i];
for (var j = 0; j < template.predicates.length; j++) {
var pred = template.predicates[j];
/* jshint maxdepth : false */
if (!pred.exec(context))
break;
}
// All predicates matched!
if (j === template.predicates.length)
break;
}
if (bit === 0x40000000) {
bitIndex++;
mask = this.mask[bitIndex];
bit = 1;
} else {
bit <<= 1;
}
}
if (i === this.count) {
this.restoreDepth(save);
if (this.modeName === 'mods')
return context.mods;
if (this.modeName === 'elemMods')
return context.elemMods;
return context.ctx[this.modeName];
}
var oldMask = mask;
var oldMatch = this.bemxjst.match;
this.mask[bitIndex] |= bit;
this.bemxjst.match = this;
this.thrownError = null;
var out;
if (typeof template.body === 'function')
out = this.tryCatch(template.body, context);
else
out = template.body;
this.mask[bitIndex] = oldMask;
this.bemxjst.match = oldMatch;
this.restoreDepth(save);
var e = this.thrownError;
if (e !== null) {
this.thrownError = null;
throw e;
}
return out;
};
Match.prototype.checkDepth = function() {
if (this.depth === -1) {
this.depth = this.bemxjst.depth;
return -1;
}
if (this.bemxjst.depth === this.depth)
return this.depth;
var depth = this.depth;
this.depth = this.bemxjst.depth;
this.maskOffset += this.maskSize;
while (this.mask.length < this.maskOffset + this.maskSize)
this.mask.push(0);
return depth;
};
Match.prototype.restoreDepth = function(depth) {
if (depth !== -1 && depth !== this.depth)
this.maskOffset -= this.maskSize;
this.depth = depth;
};