Repository: bem/bh (js)

Issue:
Add global matcher
For extension purposes of BH-core functionality it wood be good to have API to set global matcher. This matcher must be executed on each BEMJSON-node to make necessary modifications.
As I understand, it's possible to want to execute some code before and after other user matchers. So, probably, we need ``beforeEach`` and ``afterEach`` kind of matchers.

Pull request description:
Add global matchers
close #121 

Patch:
diff --git a/lib/bh.js b/lib/bh.js
index 41dda23..1d9e3c1 100644
--- a/lib/bh.js
+++ b/lib/bh.js
@@ -658,6 +658,34 @@ BH.prototype = {
         return this;
     },
 
+    /**
+     * Объявляет глобальный шаблон, применяемый перед остальными.
+     * ```javascript
+     * bh.beforeEach(function(ctx, json) {
+     *     ctx.attr('onclick', json.counter);
+     * });
+     * ```
+     * @param {Function} matcher
+     * @returns {BH}
+     */
+    beforeEach: function(matcher) {
+        return this.match('$before', matcher);
+    },
+
+    /**
+     * Объявляет глобальный шаблон, применяемый после остальных.
+     * ```javascript
+     * bh.afterEach(function(ctx) {
+     *     ctx.tag('xdiv');
+     * });
+     * ```
+     * @param {Function} matcher
+     * @returns {BH}
+     */
+    afterEach: function(matcher) {
+        return this.match('$after', matcher);
+    },
+
     /**
      * Вспомогательный метод для компиляции шаблонов с целью их быстрого дальнейшего исполнения.
      * @returns {String}
@@ -680,6 +708,20 @@ BH.prototype = {
             return res;
         }
 
+        /**
+         * Вставляет вызов шаблона в очередь вызова.
+         * @param {Array} res
+         * @param {String} fnId
+         * @returns {Number} index
+         */
+        function pushMatcher(res, fnId, index) {
+            res.push(
+                'json.' + fnId + ' = true;',
+                'subRes = _m' + index + '(ctx, json);',
+                'if (subRes !== undefined) return (subRes || "");',
+                'if (json._stop) return;');
+        }
+
         var i, j, l;
         var res = [];
         var vars = ['bh = this'];
@@ -716,9 +758,23 @@ BH.prototype = {
             declarations.push(decl);
         }
         var declByBlock = groupBy(declarations, 'block');
-        res.push('var ' + vars.join(', ') + ';');
-        res.push('function applyMatchers(ctx, json) {');
-        res.push('var subRes;');
+
+        var beforeEach = declByBlock.$before;
+        var afterEach = declByBlock.$after;
+        if (afterEach) delete declByBlock.$after;
+
+        res.push(
+            'var ' + vars.join(', ') + ';',
+            'function applyMatchers(ctx, json) {',
+            'var subRes;');
+
+        if (beforeEach) {
+            delete declByBlock.$before;
+            for (j = 0, l = beforeEach.length; j < l; j++) {
+                decl = beforeEach[j];
+                pushMatcher(res, decl.fn.__id, decl.index);
+            }
+        }
 
         res.push('switch (json.block) {');
         for (var blockName in declByBlock) {
@@ -749,21 +805,27 @@ BH.prototype = {
                                 (decl.blockModVal === true || '"' + decl.blockModVal + '"'));
                     }
                     res.push('if (' + conds.join(' && ') + ') {');
-                    res.push('json.' + fn.__id + ' = true;');
-                    res.push('subRes = _m' + decl.index + '(ctx, json);');
-                    res.push('if (subRes !== undefined) { return (subRes || "") }');
-                    res.push('if (json._stop) return;');
+                    pushMatcher(res, fn.__id, decl.index);
                     res.push('}');
                 }
-                res.push('return;');
+                res.push('break;');
             }
-            res.push('}');
-
-            res.push('return;');
+            res.push(
+                '}',
+                'break;');
         }
         res.push('}');
-        res.push('};');
-        res.push('return applyMatchers;');
+
+        if (afterEach) {
+            for (j = 0, l = afterEach.length; j < l; j++) {
+                decl = afterEach[j];
+                pushMatcher(res, decl.fn.__id, decl.index);
+            }
+        }
+
+        res.push(
+            '};',
+            'return applyMatchers;');
         return res.join('\n');
     },
 
@@ -832,7 +894,7 @@ BH.prototype = {
                     blockMods = json.mods = json.mods || {};
                 }
 
-                if (json.block) {
+                if (typeof json === 'object') {
                     if (infiniteLoopDetection) {
                         json.__processCounter = (json.__processCounter || 0) + 1;
                         compiledMatcher.__processCounter = (compiledMatcher.__processCounter || 0) + 1;
@@ -861,6 +923,7 @@ BH.prototype = {
                         }
                     }
                 }
+
                 if (!stopProcess) {
                     if (processContent && (content = json.content)) {
                         if (Array.isArray(content)) {


Test patch:
diff --git a/test/test.matchGlobal.js b/test/test.matchGlobal.js
new file mode 100644
index 0000000..69e9dbf
--- /dev/null
+++ b/test/test.matchGlobal.js
@@ -0,0 +1,88 @@
+var BH = require('../lib/bh');
+require('chai').should();
+
+describe('bh.beforeEach()', function() {
+    var bh;
+    beforeEach(function() {
+        bh = new BH();
+    });
+
+    it('should apply beforeEach template', function() {
+        bh.beforeEach(function(ctx) {
+            ctx.tag('b');
+            ctx.bem(false);
+        });
+        bh.apply([
+            { content: 'foo' },
+            { block: 'button' },
+            { block: 'input', elem: 'control' }
+        ]).should.equal('<b>foo</b><b></b><b></b>');
+    });
+
+    it('should match beforeEach before other template', function() {
+        bh.match('button', function(ctx) {
+            ctx.tag('button');
+        });
+        bh.beforeEach(function(ctx) {
+            ctx.tag('span');
+        });
+        bh.match('button', function(ctx) {
+            ctx.tag('strong');
+        });
+
+        bh.apply({ block: 'button' }).should.equal('<span class="button"></span>');
+    });
+
+    it('should apply several beforeEach templates in proper order', function() {
+        bh.beforeEach(function(ctx, json) {
+            json.cls += '2';
+        });
+        bh.beforeEach(function(ctx, json) {
+            json.cls += '1';
+        });
+        bh.apply({ block: 'button', cls: 'foo' }).should.equal('<div class="button foo12"></div>');
+    });
+});
+
+describe('bh.afterEach()', function() {
+    var bh;
+    beforeEach(function() {
+        bh = new BH();
+    });
+
+    it('should apply afterEach template', function() {
+        bh.afterEach(function(ctx) {
+            ctx.tag('b');
+            ctx.bem(false);
+        });
+        bh.apply([
+            { content: 'foo' },
+            { block: 'button' },
+            { block: 'input', elem: 'control' }
+        ]).should.equal('<b>foo</b><b></b><b></b>');
+    });
+
+    it('should match afterEach after other template', function() {
+        bh.match('button', function(ctx) {
+            ctx.tag('button', true);
+        });
+        bh.afterEach(function(ctx) {
+            ctx.tag('span', true);
+        });
+        bh.match('button', function(ctx) {
+            ctx.tag('strong', true);
+        });
+
+        bh.apply({ block: 'button' }).should.equal('<span class="button"></span>');
+    });
+
+    it('should apply several afterEach templates in proper order', function() {
+        bh.afterEach(function(ctx, json) {
+            json.cls += '2';
+        });
+        bh.afterEach(function(ctx, json) {
+            json.cls += '1';
+        });
+        bh.apply({ block: 'button', cls: 'foo' }).should.equal('<div class="button foo12"></div>');
+    });
+});