UNPKG

bem-xjst

Version:

Declarative Template Engine for the browser and server

549 lines (437 loc) 13.3 kB
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; };