bem-xjst
Version:
Declarative Template Engine for the browser and server
549 lines (437 loc) • 13.3 kB
JavaScript
var inherits = require('inherits');
var Tree = require('./tree').Tree;
var PropertyMatch = require('./tree').PropertyMatch;
var AddMatch = require('./tree').AddMatch;
var Context = require('./context').Context;
var ClassBuilder = require('./class-builder').ClassBuilder;
var utils = require('./utils');
function BEMXJST(options) {
this.options = options;
this.entities = null;
this.defaultEnt = null;
// Current tree
this.tree = null;
// Current match
this.match = null;
// Create new Context constructor for overriding prototype
this.contextConstructor = function ContextChild(bemxjst) {
Context.call(this, bemxjst);
};
inherits(this.contextConstructor, Context);
this.context = null;
this.classBuilder = new ClassBuilder(this.options.naming || {});
// Execution depth, used to invalidate `applyNext` bitfields
this.depth = 0;
// Do not call `_flush` on overridden `def()` mode
this.canFlush = false;
// oninit templates
this.oninit = null;
// Initialize default entity (no block/elem match)
this.defaultEnt = new this.Entity(this, '', '', []);
this.defaultElemEnt = new this.Entity(this, '', '', []);
}
module.exports = BEMXJST;
BEMXJST.prototype.locals = Tree.methods
.concat('local', 'applyCtx', 'applyNext', 'apply');
BEMXJST.prototype.runOninit = function(oninits, ret) {
var self = ret || this;
self.BEMContext = this.contextConstructor;
for (var i = 0; i < oninits.length; i++) {
// NOTE: oninit has global context instead of BEMXJST
var oninit = oninits[i];
oninit(self, { BEMContext: self.BEMContext });
}
};
BEMXJST.prototype.compile = function(code) {
var self = this;
function applyCtx() {
return self.run(self.context.ctx);
}
function _applyCtx() {
return self._run(self.context.ctx);
}
function applyCtxWrap(ctx, changes) {
// Fast case
if (!changes)
return self.local({ ctx: ctx }, applyCtx);
return self.local(changes, function() {
return self.local({ ctx: ctx }, _applyCtx);
});
}
function _applyCtxWrap(ctx, changes) {
// Fast case
if (!changes)
return self.local({ ctx: ctx }, _applyCtx);
return self.local(changes, function() {
return self.local({ ctx: ctx }, applyCtx);
});
}
function apply(mode, changes) {
return self.applyMode(mode, changes);
}
function localWrap(changes) {
return function localBody(body) {
return self.local(changes, body);
};
}
var tree = new Tree({
refs: {
applyCtx: applyCtxWrap,
_applyCtx: _applyCtxWrap,
apply: apply
}
});
// Yeah, let people pass functions to us!
var templates = this.recompileInput(code);
var out = tree.build(templates, [
localWrap,
applyCtxWrap,
function applyNextWrap(changes) {
if (changes)
return self.local(changes, applyNextWrap);
return self.applyNext();
},
apply
]);
// Concatenate templates with existing ones
// TODO(indutny): it should be possible to incrementally add templates
if (this.tree) {
this.runOninit(out.oninit);
out = {
templates: out.templates.concat(this.tree.templates),
oninit: this.tree.oninit.concat(out.oninit)
};
}
this.tree = out;
// Group block+elem entities into a hashmap
var ent = this.groupEntities(out.templates);
// Transform entities from arrays to Entity instances
ent = this.transformEntities(ent);
this.entities = ent;
this.oninit = out.oninit;
};
BEMXJST.prototype.getTemplate = function(code, options) {
this.compile(code, options);
return this.exportApply();
};
BEMXJST.prototype.recompileInput = function(code) {
var args = BEMXJST.prototype.locals;
// Reuse function if it already has right arguments
if (typeof code === 'function' && code.length === args.length)
return code;
return new Function(args.join(', '), utils.fnToString(code));
};
BEMXJST.prototype.groupEntities = function(tree) {
var res = {};
for (var i = 0; i < tree.length; i++) {
// Make sure to change only the copy, the original is cached in `this.tree`
var template = tree[i].clone();
var block = null;
var elem;
elem = undefined;
for (var j = 0; j < template.predicates.length; j++) {
var pred = template.predicates[j];
if (!(pred instanceof PropertyMatch) &&
!(pred instanceof AddMatch))
continue;
if (pred.key === 'block')
block = pred.value;
else if (pred.key === 'elem')
elem = pred.value;
else
continue;
// Remove predicate, we won't much against it
template.predicates.splice(j, 1);
j--;
}
if (block === null) {
var msg = 'block(…) subpredicate is not found.\n' +
' See template with subpredicates:\n * ';
for (var j = 0; j < template.predicates.length; j++) {
var pred = template.predicates[j];
if (j !== 0)
msg += '\n * ';
if (pred.key === '_mode') {
msg += pred.value + '()';
} else {
if (Array.isArray(pred.key)) {
msg += pred.key[0].replace('mods', 'mod')
.replace('elemMods', 'elemMod') +
'(\'' + pred.key[1] + '\', \'' + pred.value + '\')';
} else {
msg += 'match(…)';
}
}
}
msg += '\n And template body: \n (' +
(typeof template.body === 'function' ?
template.body :
JSON.stringify(template.body)) + ')';
if (typeof BEMXJSTError === 'undefined') {
BEMXJSTError = require('./error').BEMXJSTError;
}
throw new BEMXJSTError(msg);
}
var key = this.classBuilder.build(block, elem);
if (!res[key])
res[key] = [];
res[key].push(template);
}
return res;
};
BEMXJST.prototype.transformEntities = function(entities) {
var wildcardElems = [];
var keys = Object.keys(entities);
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
// TODO(indutny): pass this values over
var parts = this.classBuilder.split(key);
var block = parts[0];
var elem = parts[1];
if (elem === '*')
wildcardElems.push(block);
entities[key] = new this.Entity(
this, block, elem, entities[key]);
}
// Merge wildcard block templates
if (entities.hasOwnProperty('*')) {
var wildcard = entities['*'];
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
if (key === '*')
continue;
entities[key].prepend(wildcard);
}
this.defaultEnt.prepend(wildcard);
this.defaultElemEnt.prepend(wildcard);
}
// Merge wildcard elem templates
for (var i = 0; i < wildcardElems.length; i++) {
var block = wildcardElems[i];
var wildcardKey = this.classBuilder.build(block, '*');
var wildcard = entities[wildcardKey];
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
if (key === wildcardKey)
continue;
var entity = entities[key];
if (entity.block !== block || entity.elem === undefined)
continue;
entities[key].prepend(wildcard);
}
this.defaultElemEnt.prepend(wildcard);
}
// Set default templates after merging with wildcard
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
entities[key].setDefaults();
this.defaultEnt.setDefaults();
this.defaultElemEnt.setDefaults();
}
return entities;
};
BEMXJST.prototype._run = function(context) {
if (context === undefined || context === '' || context === null)
return this.runEmpty();
else if (Array.isArray(context))
return this.runMany(context);
else if (
typeof context.html === 'string' &&
!context.tag &&
typeof context.block === 'undefined' &&
typeof context.elem === 'undefined' &&
typeof context.cls === 'undefined' &&
typeof context.attrs === 'undefined'
)
return this.runUnescaped(context);
else if (utils.isSimple(context))
return this.runSimple(context);
return this.runOne(context);
};
BEMXJST.prototype.run = function(json) {
var match = this.match;
var context = this.context;
var depth = this.depth;
this.match = null;
this.context = new this.contextConstructor(this);
this.canFlush = this.context._flush !== null;
this.depth = 0;
var res = this._run(json);
if (this.canFlush)
res = this.context._flush(res);
this.match = match;
this.context = context;
this.depth = depth;
return res;
};
BEMXJST.prototype.runEmpty = function() {
this.context._listLength--;
return '';
};
BEMXJST.prototype.runUnescaped = function(context) {
this.context._listLength--;
return '' + context.html;
};
BEMXJST.prototype.runSimple = function(simple) {
this.context._listLength--;
if (!simple && simple !== 0 || simple === true)
return '';
return typeof simple === 'string' && this.context.escapeContent ?
utils.xmlEscape(simple) :
simple;
};
BEMXJST.prototype.runOne = function(json) {
var context = this.context;
var oldCtx = context.ctx;
var oldBlock = context.block;
var oldCurrBlock = context._currBlock;
var oldElem = context.elem;
var oldMods = context.mods;
var oldElemMods = context.elemMods;
if (json.block || json.elem)
context._currBlock = '';
else
context._currBlock = context.block;
context.ctx = json;
if (json.block) {
context.block = json.block;
if (json.mods)
context.mods = json.mods;
else if (json.block !== oldBlock || !json.elem)
context.mods = {};
} else {
if (!json.elem)
context.block = '';
else if (oldCurrBlock)
context.block = oldCurrBlock;
}
context.elem = json.elem;
context.elemMods = json.elemMods || {};
var block = context.block || '';
var elem = context.elem;
// Control list position
if (block || elem)
context.position++;
else
context._listLength--;
// To invalidate `applyNext` flags
this.depth++;
var restoreFlush = false;
var ent = this.entities[this.classBuilder.build(block, elem)];
if (ent) {
if (this.canFlush && !ent.canFlush) {
// Entity does not support flushing, do not flush anything nested
restoreFlush = true;
this.canFlush = false;
}
} else {
// No entity - use default one
ent = this.defaultEnt;
if (elem !== undefined)
ent = this.defaultElemEnt;
ent.init(block, elem);
}
var res = this.options.production === true ?
this.tryRun(context, ent) :
ent.run(context);
context.ctx = oldCtx;
context.block = oldBlock;
context.elem = oldElem;
context.mods = oldMods;
context.elemMods = oldElemMods;
context._currBlock = oldCurrBlock;
this.depth--;
if (restoreFlush)
this.canFlush = true;
return res;
};
BEMXJST.prototype.tryRun = function(context, ent) {
try {
return ent.run(context);
} catch (e) {
return context.onError(context, e) || '';
}
};
BEMXJST.prototype.renderContent = function(content, isBEM) {
var context = this.context;
var oldPos = context.position;
var oldListLength = context._listLength;
var oldNotNewList = context._notNewList;
context._notNewList = false;
if (isBEM) {
context.position = 0;
context._listLength = 1;
}
var res = this._run(content);
context.position = oldPos;
context._listLength = oldListLength;
context._notNewList = oldNotNewList;
return res;
};
BEMXJST.prototype.local = function(changes, body) {
var keys = Object.keys(changes);
var restore = [];
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
var parts = key.split('.');
var value = this.context;
for (var j = 0; j < parts.length - 1; j++)
value = value[parts[j]];
restore.push({
parts: parts,
value: value[parts[j]]
});
value[parts[j]] = changes[key];
}
var res = body.call(this.context);
for (var i = 0; i < restore.length; i++) {
var parts = restore[i].parts;
var value = this.context;
for (var j = 0; j < parts.length - 1; j++)
value = value[parts[j]];
value[parts[j]] = restore[i].value;
}
return res;
};
BEMXJST.prototype.applyNext = function() {
return this.match.exec(this.context);
};
BEMXJST.prototype.applyMode = function(mode, changes) {
var match = this.match;
if (!match) {
var key = this.classBuilder.build(this.context.block, this.context.elem);
match = this.entities[key].rest[mode];
} else {
match = match.entity.rest[mode];
}
if (!match) {
if (mode === 'mods')
return this.context.mods;
if (mode === 'elemMods')
return this.context.elemMods;
return this.context.ctx[mode];
}
if (!changes)
return match.exec(this.context);
var self = this;
// Allocate function this way, to prevent allocation at the top of the
// `applyMode`
var localBody = function() {
return match.exec(self.context);
};
return this.local(changes, localBody);
};
BEMXJST.prototype.exportApply = function(exports) {
var self = this;
var ret = exports || {};
ret.apply = function(context) {
return self.run(context);
};
// Add templates at run time
ret.compile = function(templates) {
self.compile(templates);
return ret;
};
this.runOninit(self.oninit, ret);
return ret;
};