Repository: bem/bh (js)

Issue:
Make single quoted data-bem attribute
Inspired by https://github.com/bem/bem-core/pull/742#issuecomment-72166752

Pull request description:
Wrap data-bem attribute by single quotes
Fixes #114
Closes gh-115

Patch:
diff --git a/lib/bh.js b/lib/bh.js
index 50a5490..c6dbf85 100644
--- a/lib/bh.js
+++ b/lib/bh.js
@@ -965,10 +965,10 @@ BH.prototype = {
                 if (jsParams) {
                     if (addJSInitClass) cls += ' i-bem';
                     var jsData = (!hasMixJsParams && json.js === true ?
-                        '{&quot;' + base + '&quot;:{}}' :
-                        attrEscape(JSON.stringify(jsParams)));
-                    attrs += ' ' + (json.jsAttr || this._optJsAttrName) + '="' +
-                        (this._optJsAttrIsJs ? 'return ' + jsData : jsData) + '"';
+                        '{"' + base + '":{}}' :
+                        jsAttrEscape(JSON.stringify(jsParams)));
+                    attrs += ' ' + (json.jsAttr || this._optJsAttrName) + '=\'' +
+                        (this._optJsAttrIsJs ? 'return ' + jsData : jsData) + '\'';
                 }
             }
 
@@ -1043,6 +1043,9 @@ var xmlEscape = BH.prototype.xmlEscape = function(str) {
 var attrEscape = BH.prototype.attrEscape = function(str) {
     return (str + '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
 };
+var jsAttrEscape = BH.prototype.jsAttrEscape = function(str) {
+    return (str + '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/'/g, '&#39;');
+};
 
 var toBemCssClasses = function(json, base, parentBase) {
     var mods, mod, res = '', i;


Test patch:
diff --git a/test/test.escape.js b/test/test.escape.js
index 32708e4..b9dac05 100644
--- a/test/test.escape.js
+++ b/test/test.escape.js
@@ -28,3 +28,17 @@ describe('bh.attrEscape()', function() {
         bh.apply({ block: 'button' });
     });
 });
+
+describe('bh.jsAttrEscape()', function() {
+    var bh;
+    beforeEach(function() {
+        bh = new BH();
+    });
+
+    it('should escape xml attr js string', function() {
+        bh.match('button', function() {
+            bh.jsAttrEscape('<b id="a">\'&\'</b>').should.equal('&lt;b id="a"&gt;&#39;&amp;&#39;&lt;/b&gt;');
+        });
+        bh.apply({ block: 'button' });
+    });
+});
diff --git a/test/test.js.js b/test/test.js.js
index 28c490c..3bcf2b3 100644
--- a/test/test.js.js
+++ b/test/test.js.js
@@ -18,7 +18,7 @@ describe('ctx.js()', function() {
         bh.match('button', function(ctx) {
             ctx.js(true);
         });
-        bh.apply({ block: 'button' }).should.equal('<div class="button i-bem" onclick="return {&quot;button&quot;:{}}"></div>');
+        bh.apply({ block: 'button' }).should.equal('<div class="button i-bem" onclick=\'return {"button":{}}\'></div>');
     });
 
     it('should not set js', function() {
@@ -33,7 +33,7 @@ describe('ctx.js()', function() {
             ctx.js({ a: 2 });
         });
         bh.apply({ block: 'button', js: { x: 1 } })
-            .should.equal('<div class="button i-bem" onclick="return {&quot;button&quot;:{&quot;x&quot;:1,&quot;a&quot;:2}}"></div>');
+            .should.equal('<div class="button i-bem" onclick=\'return {"button":{"x":1,"a":2}}\'></div>');
     });
 
     it('should not override later declarations', function() {
@@ -43,7 +43,7 @@ describe('ctx.js()', function() {
         bh.match('button', function(ctx) {
             ctx.js(true);
         });
-        bh.apply({ block: 'button' }).should.equal('<div class="button i-bem" onclick="return {&quot;button&quot;:{}}"></div>');
+        bh.apply({ block: 'button' }).should.equal('<div class="button i-bem" onclick=\'return {"button":{}}\'></div>');
     });
 
     it('should override later declarations with force flag', function() {
@@ -53,7 +53,7 @@ describe('ctx.js()', function() {
         bh.match('button', function(ctx) {
             ctx.js(false);
         });
-        bh.apply({ block: 'button' }).should.equal('<div class="button i-bem" onclick="return {&quot;button&quot;:{}}"></div>');
+        bh.apply({ block: 'button' }).should.equal('<div class="button i-bem" onclick=\'return {"button":{}}\'></div>');
     });
 
     it('should override user declarations with force flag', function() {
diff --git a/test/test.options.js b/test/test.options.js
index 7e42e90..f8f0523 100644
--- a/test/test.options.js
+++ b/test/test.options.js
@@ -9,7 +9,7 @@ describe('options', function() {
         });
         it('should use onclick and js format as default', function() {
             bh.apply({ block: 'button', js: true }).should.equal(
-                '<div class="button i-bem" onclick="return {&quot;button&quot;:{}}"></div>'
+                '<div class="button i-bem" onclick=\'return {"button":{}}\'></div>'
             );
         });
         it('should return current options', function() {
@@ -21,7 +21,7 @@ describe('options', function() {
                 jsAttrName: 'ondblclick'
             });
             bh.apply({ block: 'button', js: true }).should.equal(
-                '<div class="button i-bem" ondblclick="return {&quot;button&quot;:{}}"></div>'
+                '<div class="button i-bem" ondblclick=\'return {"button":{}}\'></div>'
             );
         });
         it('should use onclick as default and use jsAttrScheme option', function() {
@@ -29,7 +29,7 @@ describe('options', function() {
                 jsAttrScheme: 'json'
             });
             bh.apply({ block: 'button', js: true }).should.equal(
-                '<div class="button i-bem" onclick="{&quot;button&quot;:{}}"></div>'
+                '<div class="button i-bem" onclick=\'{"button":{}}\'></div>'
             );
         });
         it('should use jsAttrName and jsAttrScheme options', function() {
@@ -38,7 +38,7 @@ describe('options', function() {
                 jsAttrScheme: 'json'
             });
             bh.apply({ block: 'button', js: true }).should.equal(
-                '<div class="button i-bem" data-bem="{&quot;button&quot;:{}}"></div>'
+                '<div class="button i-bem" data-bem=\'{"button":{}}\'></div>'
             );
         });
     });
diff --git a/test/test.toHtml.js b/test/test.toHtml.js
index 58d6d3a..552c007 100644
--- a/test/test.toHtml.js
+++ b/test/test.toHtml.js
@@ -228,7 +228,7 @@ describe('bh.toHtml()', function() {
                 mix: [{ block: 'link', js: true }, { elem: 'control', js: { foo: 'bar' } }]
             }).should.equal(
                 '<div class="button link button__control i-bem" ' +
-                    'onclick="return {&quot;link&quot;:{},&quot;button__control&quot;:{&quot;foo&quot;:&quot;bar&quot;}}"' +
+                    'onclick=\'return {"link":{},"button__control":{"foo":"bar"}}\'' +
                 '></div>'
             );
         });
@@ -254,11 +254,11 @@ describe('bh.toHtml()', function() {
         });
         it('should not set `i-bem` class on element', function() {
             bh.apply({ block: 'button', elem: 'control', js: true, content: 'submit' }).should.equal(
-                '<div class="button__control" onclick="return {&quot;button__control&quot;:{}}">submit</div>');
+                '<div class="button__control" onclick=\'return {"button__control":{}}\'>submit</div>');
         });
         it('should not set `i-bem` class on mixed element', function() {
             bh.apply({ block: 'icon', content: 'submit', mix: { block: 'button', elem: 'control', js: true } }).should.equal(
-                '<div class="icon button__control" onclick="return {&quot;button__control&quot;:{}}">submit</div>');
+                '<div class="icon button__control" onclick=\'return {"button__control":{}}\'>submit</div>');
         });
     });