Repository: bem/bh
Pull request #144 (2015-04-29 14:30:42)

Replace `toHtml` method for only node

coveralls: 
[![Coverage Status](https://coveralls.io/builds/2449363/badge)](https://coveralls.io/builds/2449363)

Coverage remained the same at 100.0% when pulling **40d2af5dc17b4dad904d60d838809a6370a1c4f7 on yield** into **8f49e4aa643dcd165be2ac1992f91ad8fd57cf7d on master**.

zxqfox: А что ты паешься сделать?

Кстати, есть вариант вообще вынести toHTML в отдельный файл/функцию. Она вполне самодостаточна.
mishanga: @zxqfox я тут пытаюсь убить несколько зайцев.
Во-первых, появится возможность генерировать совершенно кастомный HTML для любого узла, чтобы не костылять.
Во-вторых, можно будет в любой момент посмотреть, что уже сгенерировано, чтобы отправлять готовый HTML чанками в браузер для ускорения загрузки и отрисовки.

zxqfox: Про чанки не очень понял. Допустим, это будет работать для `[ { block1 }, { block2 }, ... ]`.
А для `{ block: 'page', contents: ... }` ? Последнее же чаще.
mishanga: Чанки: https://ru.wikipedia.org/wiki/Chunked_transfer_encoding

Принципиально ничего не изменится. Ты можешь не пользоваться возможностью переопределять toHtml для узла.
zxqfox: Я понимаю про чанки, но не понимаю, как ты можешь `<html><head><body class="page">`, потом подождать, потом отдать еще кусок, и потом конец `</body></html>`. Для этого же надо будет отрисовать целую строку, распилить, отправить начало, ждать, потом отправить конец, и как-то это все попахивает.

Но если это возможно, то стоит заодно подумать про создание DOM-нод, вместо генерации строк, и про вызов из `toHTML` классического алгоритма отрисовки.
coveralls: 
[![Coverage Status](https://coveralls.io/builds/2642039/badge)](https://coveralls.io/builds/2642039)

Coverage remained the same at 100.0% when pulling **287b572ba011d3472325c674c13692392f7cec98 on yield** into **060d7061b6180029b7ff73dd87a24be87d50bf44 on master**.

zxqfox: Вообще, выглядит круто ;-)

Patch:
diff --git a/lib/bh.js b/lib/bh.js
index 6fb2612..ac8cc39 100644
--- a/lib/bh.js
+++ b/lib/bh.js
@@ -968,23 +968,44 @@ BH.prototype = {
      * @returns {String}
      */
     toHtml: function(json) {
-        var res, i, l, item;
-        if (json === false || json == null) return '';
+        this._buf = '';
+        this._html(json);
+        var buf = this._buf;
+        delete this._buf;
+        return buf;
+    },
+
+    /**
+     * Наполняет HTML-строку.
+     * @param {BemJson} json
+     * @returns {undefined}
+     */
+    _html: function(json) {
+        var i, l, item;
+        if (json === false || json == null) return;
         if (typeof json !== 'object') {
-            return this._optEscapeContent ? xmlEscape(json) : json;
+            this._buf += this._optEscapeContent ? xmlEscape(json) : json;
         } else if (Array.isArray(json)) {
-            res = '';
             for (i = 0, l = json.length; i < l; i++) {
                 item = json[i];
                 if (item !== false && item != null) {
-                    res += this.toHtml(item);
+                    this._html(item);
                 }
             }
-            return res;
         } else {
+            if (json.toHtml) {
+                var html = json.toHtml.call(this, json) || '';
+                this._buf += html;
+                return;
+            }
             var isBEM = json.bem !== false;
             if (typeof json.tag !== 'undefined' && !json.tag) {
-                return json.html || json.content ? this.toHtml(json.content) : '';
+                if (json.html) {
+                    this._buf += json.html;
+                } else {
+                    this._html(json.content);
+                }
+                return;
             }
             if (json.mix && !Array.isArray(json.mix)) {
                 json.mix = [json.mix];
@@ -1052,30 +1073,20 @@ BH.prototype = {
                 cls = (cls ? cls + ' ' : '') + attrEscape(json.cls).trim();
             }
 
-            var content, tag = (json.tag || 'div');
-            res = '<' + tag + (cls ? ' class="' + cls + '"' : '') + (attrs ? attrs : '');
+            var tag = (json.tag || 'div');
+            this._buf += '<' + tag + (cls ? ' class="' + cls + '"' : '') + (attrs ? attrs : '');
 
             if (selfCloseHtmlTags[tag]) {
-                res += '/>';
+                this._buf += '/>';
             } else {
-                res += '>';
+                this._buf += '>';
                 if (json.html) {
-                    res += json.html;
-                } else if ((content = json.content) != null) {
-                    if (Array.isArray(content)) {
-                        for (i = 0, l = content.length; i < l; i++) {
-                            item = content[i];
-                            if (item !== false && item != null) {
-                                res += this.toHtml(item);
-                            }
-                        }
-                    } else {
-                        res += this.toHtml(content);
-                    }
+                    this._buf += json.html;
+                } else {
+                    this._html(json.content);
                 }
-                res += '</' + tag + '>';
+                this._buf += '</' + tag + '>';
             }
-            return res;
         }
     }
 };


Test patch:
diff --git a/test/test.toHtml.js b/test/test.toHtml.js
index a4083eb..2a0e5d7 100644
--- a/test/test.toHtml.js
+++ b/test/test.toHtml.js
@@ -2,6 +2,24 @@ var BH = require('../lib/bh');
 require('chai').should();
 
 describe('bh.toHtml()', function() {
+    describe('json.toHtml', function() {
+        var bh;
+        beforeEach(function() {
+            bh = new BH();
+        });
+
+        it('should redefine `toHtml` method for only node', function() {
+            bh.match('doctype', function(ctx) {
+                var type = ctx.mod('type');
+                return { toHtml: function() { return '<!DOCTYPE ' + type + '>'; } };
+            });
+            bh.apply([
+                { block: 'doctype', mods: { type: 'html' } },
+                { block: 'page' }
+            ]).should.equal('<!DOCTYPE html><div class="page"></div>');
+        });
+    });
+
     describe('content', function() {
         var bh;
         beforeEach(function() {
@@ -39,8 +57,16 @@ describe('bh.toHtml()', function() {
         it('should prefer `html` field', function() {
             bh.apply({
                 content: '<br/>',
-                html: '<br/>'
-            }).should.equal('<div><br/></div>');
+                html: '<hr/>'
+            }).should.equal('<div><hr/></div>');
+        });
+
+        it('should prefer `html` field when tag is empty', function() {
+            bh.apply({
+                tag: '',
+                content: '<br/>',
+                html: '<hr/>'
+            }).should.equal('<hr/>');
         });
     });