diff --git a/blocks-common/i-bem/__dom/i-bem__dom.js b/blocks-common/i-bem/__dom/i-bem__dom.js
index 3c4fde69..5434ee72 100644
--- a/blocks-common/i-bem/__dom/i-bem__dom.js
+++ b/blocks-common/i-bem/__dom/i-bem__dom.js
@@ -1115,11 +1115,12 @@ var DOM = BEM.DOM = BEM.decl('i-bem__dom', {
* @param {jQuery|String} content New content
* @param {Function} [callback] Handler to be called after initialization
* @param {Object} [callbackCtx] Handler's context
+ * @returns {jQuery} ctx Initialization context
*/
update : function(ctx, content, callback, callbackCtx) {
this.destruct(ctx, true);
- this.init(ctx.html(content), callback, callbackCtx);
+ return this.init(ctx.html(content), callback, callbackCtx);
},
@@ -1127,11 +1128,12 @@ var DOM = BEM.DOM = BEM.decl('i-bem__dom', {
* Changes a fragment of the DOM tree including the context and initializes blocks.
* @param {jQuery} ctx Root DOM node
* @param {jQuery|String} content Content to be added
+ * @returns {jQuery} ctx Initialization context
*/
replace : function(ctx, content) {
this.destruct(true, ctx);
- this.init($(content).replaceAll(ctx));
+ return this.init($(content).replaceAll(ctx));
},
@@ -1139,10 +1141,11 @@ var DOM = BEM.DOM = BEM.decl('i-bem__dom', {
* Adds a fragment of the DOM tree at the end of the context and initializes blocks
* @param {jQuery} ctx Root DOM node
* @param {jQuery|String} content Content to be added
+ * @returns {jQuery} ctx Initialization context
*/
append : function(ctx, content) {
- this.init($(content).appendTo(ctx));
+ return this.init($(content).appendTo(ctx));
},
@@ -1150,10 +1153,11 @@ var DOM = BEM.DOM = BEM.decl('i-bem__dom', {
* Adds a fragment of the DOM tree at the beginning of the context and initializes blocks
* @param {jQuery} ctx Root DOM node
* @param {jQuery|String} content Content to be added
+ * @returns {jQuery} ctx Initialization context
*/
prepend : function(ctx, content) {
- this.init($(content).prependTo(ctx));
+ return this.init($(content).prependTo(ctx));
},
@@ -1161,10 +1165,11 @@ var DOM = BEM.DOM = BEM.decl('i-bem__dom', {
* Adds a fragment of the DOM tree before the context and initializes blocks
* @param {jQuery} ctx Contextual DOM node
* @param {jQuery|String} content Content to be added
+ * @returns {jQuery} ctx Initialization context
*/
before : function(ctx, content) {
- this.init($(content).insertBefore(ctx));
+ return this.init($(content).insertBefore(ctx));
},
@@ -1172,10 +1177,11 @@ var DOM = BEM.DOM = BEM.decl('i-bem__dom', {
* Adds a fragment of the DOM tree after the context and initializes blocks
* @param {jQuery} ctx Contextual DOM node
* @param {jQuery|String} content Content to be added
+ * @returns {jQuery} ctx Initialization context
*/
after : function(ctx, content) {
- this.init($(content).insertAfter(ctx));
+ return this.init($(content).insertAfter(ctx));
},
diff --git a/blocks-common/i-bem/__html/i-bem__html.bemhtml b/blocks-common/i-bem/__html/i-bem__html.bemhtml
index 030b6525..6b682f83 100644
--- a/blocks-common/i-bem/__html/i-bem__html.bemhtml
+++ b/blocks-common/i-bem/__html/i-bem__html.bemhtml
@@ -270,7 +270,7 @@ this._mode === '' {
this.block = vBlock || (vElem ? block : undefined),
this._currBlock = vBlock || vElem ? undefined : block,
this.elem = this.ctx.elem,
- this.mods = (vBlock ? this.ctx.mods : this.mods) || {},
+ this.mods = vBlock? this.ctx.mods || (this.ctx.mods = {}) : this.mods,
this.elemMods = this.ctx.elemMods || {}
) {
(this.block || this.elem) ?
diff --git a/blocks-common/i-bem/__html/lib/bemhtml/api.js b/blocks-common/i-bem/__html/lib/bemhtml/api.js
index 2820106e..b209fe6d 100644
--- a/blocks-common/i-bem/__html/lib/bemhtml/api.js
+++ b/blocks-common/i-bem/__html/lib/bemhtml/api.js
@@ -80,10 +80,13 @@ api.translate = function translate(source, options) {
var exportName = options.exportName;
// Replace known context lookups with context vars
- xjstJS = replaceContext(xjstJS);
+ var cr = new ContextReplacer();
+ xjstJS = cr.run(xjstJS);
return 'var ' + exportName + ' = function() {\n' +
' var ' + propValues.join(', ') + ';\n' +
+ cr.getCallWrap() + '\n' +
+ cr.getApplyWrap() + '\n' +
' var cache,\n' +
' exports = {},\n' +
' xjst = ' + xjstJS + ';\n' +
@@ -133,79 +136,260 @@ api.compile = function compile(source, options) {
return context.BEMHTML;
};
-function replaceContext(src) {
- function translateProp(prop) {
- if (properties.hasOwnProperty(prop))
- return properties[prop];
- else
- return false;
- };
+function ContextReplacer() {
+ // estraverse context
+ this.estraverse = null;
- function isHash(node) {
- var val = node.init;
- if (!val)
- return false;
+ this.applyc = null;
+ this.map = null;
- if (val.type !== 'ObjectExpression' || val.properties.length !== 3)
- return false;
+ this.needCallWrap = false;
+ this.needApplyWrap = false;
+};
- var props = val.properties;
- return props.every(function(prop) {
- var name = prop.key.name;
- var val = prop.value;
+ContextReplacer.prototype.translateProp = function translateProp(prop) {
+ if (properties.hasOwnProperty(prop))
+ return properties[prop];
+ else
+ return false;
+};
- if ((name === 'n' || name === 'm') && val.type === 'ObjectExpression')
- return true;
- if (name === 'd' && val.type === 'FunctionExpression')
- return true;
- return false;
- });
- }
+ContextReplacer.prototype.isHash = function isHash(node) {
+ var val = node.init;
+ if (!val)
+ return false;
- var applyc = null;
- var map = null;
+ if (val.type !== 'ObjectExpression' || val.properties.length !== 3)
+ return false;
+ var props = val.properties;
+ return props.every(function(prop) {
+ var name = prop.key.name;
+ var val = prop.value;
+
+ if ((name === 'n' || name === 'm') && val.type === 'ObjectExpression')
+ return true;
+ if (name === 'd' && val.type === 'FunctionExpression')
+ return true;
+ return false;
+ });
+};
+
+ContextReplacer.prototype.run = function run(src) {
var ast = esprima.parse(src);
- ast = estraverse.replace(ast, {
+
+ var self = this;
+ return escodegen.generate(estraverse.replace(ast, {
+ enter: function(node, parent) {
+ self.estraverse = this;
+ return self.enterNode(node, parent);
+ },
+ leave: function(node) {
+ return self.leaveNode(node);
+ }
+ }));
+};
+
+ContextReplacer.prototype.isApplyc = function isApplyc(node) {
+ var isFunction = node.type === 'FunctionDeclaration' ||
+ node.type === 'FunctionExpression';
+ var id = node.id && node.id.name;
+
+ return this.applyc === null &&
+ isFunction &&
+ (this.map !== null || /^(applyc|\$\d+)$/.test(id))
+};
+
+ContextReplacer.prototype.isMap = function isMap(node) {
+ return this.applyc === null &&
+ node.type === 'VariableDeclarator' &&
+ /^__(h|\$m)\d+$/.test(node.id && node.id.name) &&
+ this.isHash(node);
+};
+
+ContextReplacer.prototype.enterNode = function enterNode(node, parent) {
+ var isFunction = node.type === 'FunctionDeclaration' ||
+ node.type === 'FunctionExpression';
+
+ if (this.isApplyc(node)) {
+ this.applyc = node;
+ } else if (this.isMap(node)) {
+ this.map = node;
+ } else if (this.applyc === null) {
+ return;
+ }
+
+ if (this.applyc !== node && isFunction) {
+ this.replaceEscapingApplies(node, parent);
+ return this.estraverse.skip();
+ }
+
+ var res;
+
+ res = this.handleMember(node);
+ if (res)
+ return res;
+
+ res = this.handleEscapingThis(node, parent);
+ if (res)
+ return res;
+};
+
+ContextReplacer.prototype.handleMember = function handleMember(node) {
+ if (node.type !== 'MemberExpression' || node.computed)
+ return;
+
+ var obj = node.object;
+ if (obj.type !== 'Identifier' || obj.name !== '__$ctx')
+ return;
+
+ var prop = this.translateProp(node.property.name || node.property.value);
+ if (!prop)
+ return;
+
+ return { type: 'Identifier', name: prop };
+};
+
+ContextReplacer.prototype.isMapCall = function isMapCall(node) {
+ if (node.type !== 'LogicalExpression' || node.operator !== '||')
+ return false;
+
+ var match = false;
+ estraverse.traverse(node, {
enter: function(node) {
- var isFunction = node.type === 'FunctionDeclaration' ||
- node.type === 'FunctionExpression';
- var id = node.id && node.id.name;
- if (applyc === null &&
- isFunction &&
- (map !== null || /^(applyc|\$\d+)$/.test(id))) {
- applyc = node;
- } else if (applyc === null &&
- node.type === 'VariableDeclarator' &&
- /^__(h|\$m)\d+$/.test(id) &&
- isHash(node)) {
- map = node;
- } else if (applyc === null) {
- return;
+ if (node.type === 'Identifier' && /^__(h|\$m)\d+$/.test(node.name)) {
+ match = true;
+ this['break']();
}
+ }
+ });
- if (applyc !== node && isFunction) {
- this.skip();
+ return match;
+};
+
+ContextReplacer.prototype.handleEscapingThis =
+ function handleEscapingThis(node, parent) {
+ if (node.type !== 'Identifier' || node.name !== '__$ctx')
+ return;
+
+ if (/Expression$/.test(parent.type)) {
+ if (parent.type === 'FunctionExpression')
+ return;
+
+ var isMember = parent.type === 'MemberExpression' &&
+ (!parent.computed || parent.property.type === 'Literal');
+ if (isMember)
+ return;
+
+ if (parent.type === 'CallExpression') {
+ var c = parent.callee;
+
+ // Ignore $1(__$ctx) calls
+ if (c.type === 'Identifier' && /^(applyc|\$e|\$\d+)$/.test(c.name))
return;
- }
- if (node.type === 'MemberExpression' &&
- node.computed === false &&
- node.object.type === 'Identifier' &&
- node.object.name === '__$ctx') {
- var prop = translateProp(node.property.name || node.property.value);
- if (!prop)
- return;
+ // Ignore __$wrapThis(__$ctx)
+ if (c.type === 'Identifier' && c.name === '__$wrapThis')
+ return;
- return { type: 'Identifier', name: prop };
- }
+ // Ignore (typeof x === 'number' ? __$m\.n[x] : __$m.d)(__$ctx)
+ if (this.isMapCall(c))
+ return;
+ }
+ } else if (parent.type === 'VariableDeclarator') {
+ // Internal, xjst-thing
+ if (parent.id.type === 'Identifier' && parent.id.name === '__this')
+ return;
+
+ if (parent.init !== node)
+ return;
+ } else {
+ return;
+ }
+
+ this.needCallWrap = true;
+
+ // Wrap in call
+ return {
+ type: 'CallExpression',
+ callee: {
+ type: 'Identifier',
+ name: '__$wrapThis'
},
- leave: function(node) {
- if (node === applyc)
- applyc = null;
- if (node === map)
- applyc = null;
+ arguments: [ node ]
+ };
+};
+
+ContextReplacer.prototype.replaceEscapingApplies =
+ function replaceEscapingApplies(node, parent) {
+ if (this.applyc === null)
+ return;
+
+ var self = this;
+ estraverse.replace(node, {
+ enter: function(node, parent) {
+ return self.handleEscapingApply(node, parent);
}
});
- return escodegen.generate(ast);
-}
+};
+
+ContextReplacer.prototype.handleEscapingApply =
+ function handleEscapingApply(node, parent) {
+ if (node.type !== 'CallExpression')
+ return;
+
+ var callee = node.callee;
+ if (callee.type !== 'Identifier' || callee.name !== 'applyc')
+ return;
+
+ this.needApplyWrap = true;
+
+ // Wrap in call
+ return {
+ type: 'CallExpression',
+ callee: {
+ type: 'Identifier',
+ name: '__$wrapApply'
+ },
+ arguments: [ callee ].concat(node.arguments)
+ };
+};
+
+ContextReplacer.prototype.leaveNode = function leaveNode(node) {
+ if (node === this.applyc)
+ this.applyc = null;
+ if (node === this.map)
+ this.map = null;
+};
+
+ContextReplacer.prototype.getCallWrap = function getCallWrap() {
+ if (!this.needCallWrap)
+ return '';
+
+ return 'function __$wrapThis(ctx) {\n' +
+ propKeys.map(function(key) {
+ return 'ctx.' + key + ' = ' + properties[key] + ';';
+ }).join('\n') + '\n' +
+ 'return ctx;\n' +
+ '};'
+};
+
+ContextReplacer.prototype.getApplyWrap = function getApplyWrap() {
+ if (!this.needApplyWrap)
+ return '';
+
+ return 'function __$wrapApply(applyc, ctx) {\n' +
+ // var __t$prop = $$prop;
+ // $$prop = this.prop;
+ propKeys.map(function(key) {
+ return 'var __t$' + key + ' = ' + properties[key] + ';\n' +
+ properties[key] + ' = ctx.' + key + ';';
+ }).join('\n') + '\n' +
+ 'var r = applyc(ctx);\n' +
+ // $$prop = __t$prop;
+ propKeys.map(function(key) {
+ return properties[key] + ' = __t$' + key + ';';
+ }).join('\n') + '\n' +
+ 'return r;\n' +
+ '};'
+};
diff --git a/blocks-common/i-bem/__html/test/files/i-bem/escaping-this.bemhtml b/blocks-common/i-bem/__html/test/files/i-bem/escaping-this.bemhtml
new file mode 100644
index 00000000..7473fc08
--- /dev/null
+++ b/blocks-common/i-bem/__html/test/files/i-bem/escaping-this.bemhtml
@@ -0,0 +1,6 @@
+block b1, custom: 'ok'
+block b1, tag: {
+ return [ this ].map(function(ctx) {
+ return this._mode + '-' + ctx.block + ':' + apply({ _mode: 'custom' });
+ }, this);
+}
diff --git a/blocks-common/i-bem/__html/test/files/i-bem/escaping-this.html b/blocks-common/i-bem/__html/test/files/i-bem/escaping-this.html
new file mode 100644
index 00000000..964d13c9
--- /dev/null
+++ b/blocks-common/i-bem/__html/test/files/i-bem/escaping-this.html
@@ -0,0 +1 @@
+