bem-xjst
Version:
Declarative Template Engine for the browser and server
329 lines (262 loc) • 8.81 kB
JavaScript
var inherits = require('inherits');
var utils = require('../bemxjst/utils');
var Entity = require('./entity').Entity;
var BEMXJST = require('../bemxjst');
function BEMHTML(options) {
BEMXJST.apply(this, arguments);
this._shortTagCloser = typeof options.xhtml !== 'undefined' &&
options.xhtml ? '/>' : '>';
this._elemJsInstances = options.elemJsInstances;
this._omitOptionalEndTags = options.omitOptionalEndTags;
this._singleQuotesForDataAttrs =
typeof options.singleQuotesForDataAttrs === 'undefined' ?
false :
options.singleQuotesForDataAttrs;
this._unquotedAttrs = typeof options.unquotedAttrs === 'undefined' ?
false :
options.unquotedAttrs;
}
inherits(BEMHTML, BEMXJST);
module.exports = BEMHTML;
BEMHTML.prototype.Entity = Entity;
BEMHTML.prototype.runMany = function(arr) {
var out = '';
var context = this.context;
var prevPos = context.position;
var prevNotNewList = context._notNewList;
if (prevNotNewList) {
context._listLength += arr.length - 1;
} else {
context.position = 0;
context._listLength = arr.length;
}
context._notNewList = true;
if (this.canFlush) {
for (var i = 0; i < arr.length; i++)
out += context._flush(this._run(arr[i]));
} else {
for (var i = 0; i < arr.length; i++)
out += this._run(arr[i]);
}
if (!prevNotNewList)
context.position = prevPos;
return out;
};
BEMHTML.prototype.render = function(context, entity, tag, js, bem, cls, mix,
attrs, content, mods, elemMods) {
if (tag === undefined)
tag = 'div';
else if (!tag)
return (content || content === 0) ? this._run(content) : '';
var ctx = context.ctx;
var out = '<' + tag;
var isBEM = !!(typeof bem !== 'undefined' ?
bem : entity.block || entity.elem);
if (!isBEM && !cls)
return this.renderClose(out, context, tag, attrs, isBEM, ctx, content);
if (js === true)
js = {};
var jsParams;
if (js) {
jsParams = {};
jsParams[entity.jsClass] = js;
}
var addJSInitClass = isBEM && jsParams && (
this._elemJsInstances ?
entity.block :
(entity.block && !entity.elem)
);
out += ' class=';
var classValue = '';
if (isBEM) {
classValue += entity.jsClass;
classValue += this.buildModsClasses(entity.block, entity.elem,
entity.elem ? elemMods : mods);
if (mix) {
var m = this.renderMix(entity, mix, jsParams, addJSInitClass);
classValue += m.out;
jsParams = m.jsParams;
addJSInitClass = m.addJSInitClass;
}
if (cls)
classValue += ' ' + (typeof cls === 'string' ?
utils.attrEscape(cls).trim() : cls);
} else {
classValue += typeof cls === 'string' ?
utils.attrEscape(cls).trim() : cls;
}
if (addJSInitClass)
classValue += ' i-bem';
out += this._unquotedAttrs && utils.isUnquotedAttr(classValue) ?
classValue :
('"' + classValue + '"');
if (isBEM && jsParams)
out += ' data-bem=\'' + utils.jsAttrEscape(JSON.stringify(jsParams)) + '\'';
return this.renderClose(out, context, tag, attrs, isBEM, ctx, content);
};
var OPTIONAL_END_TAGS = {
// https://www.w3.org/TR/html4/index/elements.html
html: 1, head: 1, body: 1, p: 1, li: 1, dt: 1, dd: 1,
colgroup: 1, thead: 1, tbody: 1, tfoot: 1, tr: 1, th: 1, td: 1, option: 1,
// html5 https://www.w3.org/TR/html5/syntax.html#optional-tags
/* dl — Neither tag is omissible */ rb: 1, rt: 1, rtc: 1, rp: 1, optgroup: 1
};
BEMHTML.prototype.renderClose = function(prefix, context, tag, attrs, isBEM,
ctx, content) {
var out = prefix;
out += this.renderAttrs(attrs);
if (utils.isShortTag(tag)) {
out += this._shortTagCloser;
if (this.canFlush)
out = context._flush(out);
} else {
out += '>';
if (this.canFlush)
out = context._flush(out);
// TODO(indutny): skip apply next flags
if (content || content === 0)
out += this.renderContent(content, isBEM);
if (!this._omitOptionalEndTags || !OPTIONAL_END_TAGS.hasOwnProperty(tag))
out += '</' + tag + '>';
}
if (this.canFlush)
out = context._flush(out);
return out;
};
BEMHTML.prototype.renderAttrs = function(attrs) {
var out = '';
// NOTE: maybe we need to make an array for quicker serialization
if (utils.isObj(attrs)) {
/* jshint forin : false */
for (var name in attrs) {
var attr = attrs[name];
if (attr === undefined || attr === false || attr === null)
continue;
if (attr === true) {
out += ' ' + name;
} else {
var attrVal = utils.isSimple(attr) ? attr : this.run(attr);
out += ' ' + name + '=';
out += (this._singleQuotesForDataAttrs && name.indexOf('data-') === 0) ?
this.getAttrValue(attrVal, utils.jsAttrEscape(attrVal), '\'') :
this.getAttrValue(attrVal, utils.attrEscape(attrVal), '"');
}
}
}
return out;
};
BEMHTML.prototype.getAttrValue = function(attrVal, escapedAttrVal, quote) {
return this._unquotedAttrs && utils.isUnquotedAttr(attrVal) ?
attrVal :
(quote + escapedAttrVal + quote);
};
BEMHTML.prototype.renderMix = function(entity, mix, jsParams, addJSInitClass) {
var visited = {};
var context = this.context;
var js = jsParams;
var addInit = addJSInitClass;
visited[entity.jsClass] = true;
// Transform mix to the single-item array if it's not array
if (!Array.isArray(mix))
mix = [ mix ];
var classBuilder = this.classBuilder;
var out = '';
for (var i = 0; i < mix.length; i++) {
var item = mix[i];
if (!item)
continue;
if (typeof item === 'string')
item = { block: item, elem: undefined };
var hasItem = false;
if (item.elem) {
hasItem = item.elem !== entity.elem && item.elem !== context.elem ||
item.block && item.block !== entity.block;
} else if (item.block) {
hasItem = !(item.block === entity.block && item.mods) ||
item.mods && entity.elem;
}
var block = item.block || item._block || context.block;
var elem = item.elem || item._elem || context.elem;
var key = classBuilder.build(block, elem);
var classElem = item.elem ||
item._elem ||
(item.block ? undefined : context.elem);
if (hasItem)
out += ' ' + classBuilder.build(block, classElem);
out += this.buildModsClasses(block, classElem,
(item.elem || !item.block && (item._elem || context.elem)) ?
item.elemMods : item.mods);
if (item.js) {
if (!js)
js = {};
js[classBuilder.build(block, item.elem)] =
item.js === true ? {} : item.js;
if (!addInit)
addInit = this._elemJsInstances ?
(item.elem || block) :
(block && !item.elem);
}
// Process nested mixes from BEMJSON
if (item.mix) {
var nested = this.renderMix(entity, item.mix, js, addInit);
js = utils.extend(js, nested.jsParams);
addInit = nested.addJSInitClass;
out += nested.out;
}
// Process nested mixes from templates
if (!hasItem || visited[key])
continue;
visited[key] = true;
var nestedEntity = this.entities[key];
if (!nestedEntity)
continue;
var oldBlock = context.block;
var oldElem = context.elem;
var nestedMix = nestedEntity.mix.exec(context);
context.elem = oldElem;
context.block = oldBlock;
if (!nestedMix)
continue;
for (var j = 0; j < nestedMix.length; j++) {
var nestedItem = nestedMix[j];
if (!nestedItem) continue;
if (!nestedItem.block &&
!nestedItem.elem ||
!visited[classBuilder.build(nestedItem.block, nestedItem.elem)]) {
if (nestedItem.block) continue;
nestedItem._block = block;
nestedItem._elem = elem;
// make a copy, do not modify original array
mix = mix.slice(0, i + 1).concat(
nestedItem,
mix.slice(i + 1)
);
}
}
}
return {
out: out,
jsParams: js,
addJSInitClass: addInit
};
};
BEMHTML.prototype.buildModsClasses = function(block, elem, mods) {
if (!mods)
return '';
var res = '';
var modName;
/*jshint -W089 */
for (modName in mods) {
if (!mods.hasOwnProperty(modName) || modName === '')
continue;
var modVal = mods[modName];
if (!modVal && modVal !== 0) continue;
if (typeof modVal !== 'boolean')
modVal += '';
var builder = this.classBuilder;
res += ' ' + (elem ?
builder.buildElemClass(block, elem, modName, modVal) :
builder.buildBlockClass(block, modName, modVal));
}
return res;
};