UNPKG

bem-xjst

Version:

Declarative Template Engine for the browser and server

329 lines (262 loc) 8.81 kB
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; };