Skip to content

Commit

Permalink
BEMHTML: support unquoted attributes (fix for #364)
Browse files Browse the repository at this point in the history
  • Loading branch information
miripiruni committed Oct 4, 2016
1 parent 5ab2e2f commit 42f8b8c
Show file tree
Hide file tree
Showing 6 changed files with 183 additions and 16 deletions.
30 changes: 30 additions & 0 deletions docs/en/3-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
- [Delimiters in names of BEM entities](#delimiters-in-names-of-bem-entities)
- [Support JS-instances for elements (bem-core v4+)](#support-js-instances-for-elements-bem-core-v4)
- [XHTML option](#xhtml-option)
- [Unquoted attributes](#unquoted-attributes)
- [Escaping](#escaping)
- [Extending BEMContext](#extending-bemcontext)
- [Runtime linting](#runtime-linting)
Expand Down Expand Up @@ -211,6 +212,35 @@ Result:
<br>
```

### 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
<div class=b name=test></div>
```

### Escaping

You can set `escapeContent` option to `true` to escape string values of `content` field with [`xmlEscape`](6-templates-context.md#xmlescape).
Expand Down
31 changes: 31 additions & 0 deletions docs/ru/3-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
- [Разделители в именовании БЭМ-сущностей](#Разделители-в-именовании-БЭМ-сущностей)
- [Поддержка JS-экземпляров для элементов (bem-core v4+)](#Поддержка-js-экземпляров-для-элементов-bem-core-v4)
- [Закрытие одиночных элементов](#Закрытие-одиночных-элементов)
- [Атрибуты без кавычек](#Атрибуты-без-кавычек)
- [Экранирование](#Экранирование)
- [Расширение BEMContext](#Расширение-bemcontext)
- [Runtime проверки ошибок в шаблонах и входных данных](#Runtime-проверки-ошибок-в-шаблонах-и-входных-данных)
Expand Down Expand Up @@ -209,6 +210,36 @@ var html = templates.apply(bemjson);
<br>
```

### Атрибуты без кавычек

Спецификация 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
<div class=b name=test></div>
```

### Экранирование

Вы можете включить экранирование содержимого поля `content` опцией `escapeContent`.
Expand Down
43 changes: 27 additions & 16 deletions lib/bemhtml/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ function BEMHTML(options) {
this._shortTagCloser = xhtml ? '/>' : '>';

this._elemJsInstances = options.elemJsInstances;
this._unquotedAttrs = typeof options.unquotedAttrs === 'undefined' ?
false :
options.unquotedAttrs;
}

inherits(BEMHTML, BEMXJST);
Expand Down Expand Up @@ -95,32 +98,37 @@ BEMHTML.prototype.render = function render(context,
return this.renderClose(out, context, tag, attrs, isBEM, ctx, content);
}

out += ' class="';
out += ' class=';
var classValue = '';
if (isBEM) {
var mods = entity.elem ? context.elemMods : context.mods;

out += entity.jsClass;
out += this.buildModsClasses(entity.block, entity.elem, mods);
classValue += entity.jsClass;
classValue += this.buildModsClasses(entity.block, entity.elem, 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)) + '\'';
Expand Down Expand Up @@ -172,14 +180,17 @@ 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 (utils.isUnquotedAttr(attrVal))
out += this._unquotedAttrs ? attrVal : ('"' + attrVal + '"');
else
out += '"' + utils.attrEscape(attrVal) + '"';
}
}
}

Expand Down
12 changes: 12 additions & 0 deletions lib/bemxjst/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
};
30 changes: 30 additions & 0 deletions test/bemhtml-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,4 +119,34 @@ describe('BEMHTML engine tests', function() {
.should.equal('<br class="b">');
});
});

describe('unquotedAttrs option', function() {
it('should render class attr w/o quotes if spec allows', function() {
test(function() {},
{ block: 'b' },
'<div class=b></div>',
{ unquotedAttrs: true });
});

it('should’t render class attr w/o quotes if mix', function() {
test(function() {},
{ block: 'b', mix: 'mixed' },
'<div class="b mixed"></div>',
{ unquotedAttrs: true });
});

it('should render id attr if spec allows', function() {
test(function() {},
{ block: 'b', attrs: { id: 'nospace' } },
'<div class=b id=nospace></div>',
{ unquotedAttrs: true });
});

it('should’t render id attr if there is space in value', function() {
test(function() {},
{ block: 'b', attrs: { id: 'space test' } },
'<div class=b id="space test"></div>',
{ unquotedAttrs: true });
});
});
});
53 changes: 53 additions & 0 deletions test/utils-isunquotedattr-test.js
Original file line number Diff line number Diff line change
@@ -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);
});
});
});

0 comments on commit 42f8b8c

Please sign in to comment.