From 53f8444bca25d3dbbb4ff34566101945890b7c75 Mon Sep 17 00:00:00 2001 From: miripiruni Date: Tue, 4 Oct 2016 16:57:27 +0300 Subject: [PATCH] BEMHTML: support unquoted attributes (fix for #364) --- docs/en/3-api.md | 31 ++++++++++++++++++ docs/ru/3-api.md | 32 +++++++++++++++++++ lib/bemhtml/index.js | 47 +++++++++++++++++---------- lib/bemxjst/utils.js | 12 +++++++ test/bemhtml-test.js | 37 +++++++++++++++++++++ test/utils-isunquotedattr-test.js | 53 +++++++++++++++++++++++++++++++ 6 files changed, 195 insertions(+), 17 deletions(-) create mode 100644 test/utils-isunquotedattr-test.js diff --git a/docs/en/3-api.md b/docs/en/3-api.md index 6d7ed5c0..560caf67 100644 --- a/docs/en/3-api.md +++ b/docs/en/3-api.md @@ -7,6 +7,7 @@ - [Support JS-instances for elements (bem-core v4+)](#support-js-instances-for-elements-bem-core-v4) - [XHTML option](#xhtml-option) - [Optional End Tags](#optional-end-tags) + - [Unquoted attributes](#unquoted-attributes) - [Escaping](#escaping) - [Extending BEMContext](#extending-bemcontext) - [Runtime linting](#runtime-linting) @@ -252,6 +253,36 @@ Result:
table headertable cell
``` +### Unquoted attributes + +HTML specification allows us ommit unnececary quotes in some cases. See +[HTML4](https://www.w3.org/TR/html4/intro/sgmltut.html#h-3.2.2) and +[HTML5](https://www.w3.org/TR/html5/syntax.html#attributes) specs. + +You can use `unquotedAttrs` option to do so. + +```js +var bemxjst = require('bem-xjst'); +var templates = bemxjst.bemhtml.compile(function() { + // In this example we will add no templates. + // Default behaviour is used for HTML rendering. + }, { + // allow unqouted attributes if it’s possible + unquotedAttrs: true + }); + +var bemjson = { block: 'b', attrs: { name: 'test' } }; + +var html = templates.apply(bemjson); +``` + +Result: + +```html +
+``` + + ### Escaping You can set `escapeContent` option to `true` to escape string values of `content` field with [`xmlEscape`](6-templates-context.md#xmlescape). diff --git a/docs/ru/3-api.md b/docs/ru/3-api.md index dab1150e..fe6f3f77 100644 --- a/docs/ru/3-api.md +++ b/docs/ru/3-api.md @@ -7,6 +7,7 @@ - [Поддержка JS-экземпляров для элементов (bem-core v4+)](#Поддержка-js-экземпляров-для-элементов-bem-core-v4) - [Закрытие одиночных элементов](#Закрытие-одиночных-элементов) - [Опциональные закрывающие теги](#Опциональные-закрывающие-теги) + - [Атрибуты без кавычек](#Атрибуты-без-кавычек) - [Экранирование](#Экранирование) - [Расширение BEMContext](#Расширение-bemcontext) - [Runtime проверки ошибок в шаблонах и входных данных](#Runtime-проверки-ошибок-в-шаблонах-и-входных-данных) @@ -250,6 +251,37 @@ var html = templates.apply(bemjson);
table headertable cell
``` + +### Атрибуты без кавычек + +Спецификация HTML позволяет опустить необязательные кавычки у атрибутов, которые +не содержат пробелов и прочих специальных символов. Подробности читайте в +спецификациях [HTML4](https://www.w3.org/TR/html4/intro/sgmltut.html#h-3.2.2) и +[HTML5](https://www.w3.org/TR/html5/syntax.html#attributes). + +С помощью опции `unquotedAttrs` вы можете включить такое поведение в рендеринге BEMHTML. + +```js +var bemxjst = require('bem-xjst'); +var templates = bemxjst.bemhtml.compile(function() { + // В этом примере мы не добавляем пользовательских шаблонов. + // Для рендеринга HTML будет использовано поведение шаблонизатора по умолчанию. + }, { + // Разрешаем пропускать кавычки в атрибутах если это возможно: + unquotedAttrs: true + }); + +var bemjson = { block: 'b', attrs: { name: 'test' } }; + +var html = templates.apply(bemjson); +``` + +В результате `html` будет содержать строку: + +```html +
+``` + ### Экранирование Вы можете включить экранирование содержимого поля `content` опцией `escapeContent`. diff --git a/lib/bemhtml/index.js b/lib/bemhtml/index.js index 78c4cb04..0373b7ed 100644 --- a/lib/bemhtml/index.js +++ b/lib/bemhtml/index.js @@ -11,6 +11,9 @@ function BEMHTML(options) { this._elemJsInstances = options.elemJsInstances; this._omitOptionalEndTags = options.omitOptionalEndTags; + this._unquotedAttrs = typeof options.unquotedAttrs === 'undefined' ? + false : + options.unquotedAttrs; } inherits(BEMHTML, BEMXJST); @@ -98,31 +101,36 @@ BEMHTML.prototype.render = function render(context, return this.renderClose(out, context, tag, attrs, isBEM, ctx, content); } - out += ' class="'; + out += ' class='; + var classValue = ''; if (isBEM) { - out += entity.jsClass; - out += this.buildModsClasses(entity.block, entity.elem, - entity.elem ? elemMods : mods); + classValue += entity.jsClass; + classValue += this.buildModsClasses(entity.block, entity.elem, + entity.elem ? elemMods : mods); if (mix) { var m = this.renderMix(entity, mix, jsParams, addJSInitClass); - out += m.out; + classValue += m.out; jsParams = m.jsParams; addJSInitClass = m.addJSInitClass; } if (cls) - out += ' ' + (typeof cls === 'string' ? + classValue += ' ' + (typeof cls === 'string' ? utils.attrEscape(cls).trim() : cls); } else { if (cls) - out += cls.trim ? utils.attrEscape(cls).trim() : cls; + classValue += cls.trim ? utils.attrEscape(cls).trim() : cls; } if (addJSInitClass) - out += ' i-bem"'; - else - out += '"'; + classValue += ' i-bem'; + + if (this._unquotedAttrs && utils.isUnquotedAttr(classValue)) { + out += classValue; + } else { + out += '"' + classValue + '"'; + } if (isBEM && jsParams) out += ' data-bem=\'' + utils.jsAttrEscape(JSON.stringify(jsParams)) + '\''; @@ -183,14 +191,19 @@ BEMHTML.prototype.renderAttrs = function renderAttrs(attrs) { if (attr === undefined || attr === false || attr === null) continue; - if (attr === true) + if (attr === true) { out += ' ' + name; - else - out += ' ' + name + '="' + - utils.attrEscape(utils.isSimple(attr) ? - attr : - this.context.reapply(attr)) + - '"'; + } else { + var attrVal = utils.isSimple(attr) ? attr : this.context.reapply(attr); + out += ' ' + name + '='; + + if (this._unquotedAttrs) + out += utils.isUnquotedAttr(attrVal) ? + attrVal : + ('"' + attrVal + '"'); + else + out += '"' + utils.attrEscape(attrVal) + '"'; + } } } diff --git a/lib/bemxjst/utils.js b/lib/bemxjst/utils.js index 2b443b63..18dbf956 100644 --- a/lib/bemxjst/utils.js +++ b/lib/bemxjst/utils.js @@ -212,3 +212,15 @@ exports.fnToString = function fnToString(code) { return code; }; + +/** + * regexp for check may attribute be unquoted + * + * https://www.w3.org/TR/html4/intro/sgmltut.html#h-3.2.2 + * https://www.w3.org/TR/html5/syntax.html#attributes + */ +var UNQUOTED_ATTR_REGEXP = /^[:\w.-]+$/; + +exports.isUnquotedAttr = function isUnquotedAttr(str) { + return str && UNQUOTED_ATTR_REGEXP.exec(str); +}; diff --git a/test/bemhtml-test.js b/test/bemhtml-test.js index 758e0ab5..ae5cfeda 100644 --- a/test/bemhtml-test.js +++ b/test/bemhtml-test.js @@ -161,4 +161,41 @@ describe('BEMHTML engine tests', function() { .should.equal('
test
'); }); }); + + describe('unquotedAttrs option', function() { + it('should render class attr w/o quotes if spec allows', function() { + test(function() {}, + { block: 'b' }, + '
', + { unquotedAttrs: true }); + }); + + it('should’t render class attr w/o quotes if mix', function() { + test(function() {}, + { block: 'b', mix: 'mixed' }, + '
', + { unquotedAttrs: true }); + }); + + it('should render id attr if spec allows', function() { + test(function() {}, + { block: 'b', attrs: { id: 'nospace' } }, + '
', + { unquotedAttrs: true }); + }); + + it('should’t render id attr if there is space in value', function() { + test(function() {}, + { block: 'b', attrs: { id: 'space test' } }, + '
', + { unquotedAttrs: true }); + }); + + it('should render class if there is no space in value', function() { + test(function() {}, + { block: 'b', bem: false, cls: 'test' }, + '
', + { unquotedAttrs: true }); + }); + }); }); diff --git a/test/utils-isunquotedattr-test.js b/test/utils-isunquotedattr-test.js new file mode 100644 index 00000000..5253b153 --- /dev/null +++ b/test/utils-isunquotedattr-test.js @@ -0,0 +1,53 @@ +var utils = require('../lib/bemxjst/utils'); + +describe('Utils', function() { + + var attrCheck = function attrCheck(str) { + return !!utils.isUnquotedAttr(str); + }; + describe('isUnquotedAttr()', function() { + it('should return true with simple class', function() { + attrCheck('b').should.equal(true); + }); + + it('should return false with class with space', function() { + attrCheck('block mixed').should.equal(false); + }); + + it('should return true with class with hyphens', function() { + attrCheck('b-page').should.equal(true); + }); + + it('should return true with class with uppercase', function() { + attrCheck('bPage').should.equal(true); + }); + + it('should return true with class with period', function() { + attrCheck('b.page').should.equal(true); + }); + + it('should return true with class with underscores', function() { + attrCheck('page__content').should.equal(true); + }); + + it('should return true with class with colons', function() { + attrCheck('test:test').should.equal(true); + }); + + it('should return false with double quote', function() { + attrCheck('"test').should.equal(false); + }); + + it('should return true with class with digits', function() { + attrCheck('color333').should.equal(true); + }); + + it('should return true with class with combination of above', function() { + attrCheck('b-page__content_test_100').should.equal(true); + }); + + it('should return false with empty string', function() { + attrCheck('').should.equal(false); + }); + }); +});