forked from emmetio/emmet
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
JS implementation of BEM filter: http://kizu.ru/issues/bemto-concept/
- Loading branch information
Showing
5 changed files
with
379 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,264 @@ | ||
/** | ||
* @memberOf __zen_filter_bem | ||
* @constructor | ||
*/ | ||
zen_coding.registerFilter('bem', (function() { | ||
var separators = { | ||
element: '__', | ||
modifier: '_' | ||
}; | ||
|
||
var toString = Object.prototype.toString; | ||
var isArray = Array.isArray || function(obj) { | ||
return toString.call(obj) == '[object Array]'; | ||
}; | ||
|
||
var shouldRunHtmlFilter = false; | ||
|
||
/** | ||
* @param {ZenNode} item | ||
*/ | ||
function bemParse(item) { | ||
if (item.type != 'tag') | ||
return item; | ||
|
||
// save BEM stuff in cache for faster lookups | ||
item.__bem = { | ||
block: '', | ||
element: '', | ||
modifier: '' | ||
}; | ||
|
||
var classNames = normalizeClassName(item.getAttribute('class')).split(' '); | ||
|
||
// process class names | ||
var processedClassNames = []; | ||
var i, il, _item; | ||
for (i = 0, il = classNames.length; i < il; i++) { | ||
processedClassNames.push(processClassName(classNames[i], item)); | ||
} | ||
|
||
// flatten array | ||
var allClassNames = []; | ||
for (i = 0, il = processedClassNames.length; i < il; i++) { | ||
_item = processedClassNames[i]; | ||
if (isArray(_item)) { | ||
for (var j = 0, jl = _item.length; j < jl; j++) { | ||
allClassNames.push(_item[j]); | ||
} | ||
} else { | ||
allClassNames.push(_item); | ||
} | ||
} | ||
|
||
// remove duplicates | ||
var memo = []; | ||
for (i = 0, il = allClassNames.length; i < il; i++) { | ||
_item = allClassNames[i]; | ||
if (!arrayInclude(memo, _item)) | ||
memo.push(_item); | ||
} | ||
|
||
allClassNames = memo; | ||
item.setAttribute('class', allClassNames.join(' ')); | ||
|
||
if (!item.__bem.block) { | ||
// guess best match for block name | ||
var reBlockName = /^[a-z]\-/i; | ||
for (i = 0, il = allClassNames.length; i < il; i++) { | ||
/** @type String */ | ||
if (reBlockName.test(allClassNames[i])) { | ||
item.__bem.block = allClassNames[i]; | ||
break; | ||
} | ||
} | ||
|
||
// guessing doesn't worked, pick first class name as block name | ||
if (!item.__bem.block) { | ||
item.__bem.block = allClassNames[0]; | ||
} | ||
|
||
} | ||
|
||
return item; | ||
|
||
} | ||
|
||
/** | ||
* @param {String} className | ||
* @returns {String} | ||
*/ | ||
function normalizeClassName(className) { | ||
return (className || '').replace(/\s+/g, ' ').replace(/[\u2013|\u2014]/g, separators.element); | ||
} | ||
|
||
/** | ||
* Processes class name | ||
* @param {String} name Class name item to process | ||
* @param {ZenNode} item Host node for provided class name | ||
* @returns {String} Processed class name. May return <code>Array</code> of | ||
* class names | ||
*/ | ||
function processClassName(name, item) { | ||
name = transformClassName(name, item, 'element'); | ||
name = transformClassName(name, item, 'modifier'); | ||
|
||
// expand class name | ||
// possible values: | ||
// * block__element | ||
// * block__element_modifier | ||
// * block__element_modifier1_modifier2 | ||
// * block_modifier | ||
var result, block = '', element = '', modifier = ''; | ||
if (~name.indexOf(separators.element)) { | ||
var blockElem = name.split(separators.element); | ||
var elemModifiers = blockElem[1].split(separators.modifier); | ||
|
||
block = blockElem[0]; | ||
element = elemModifiers.shift(); | ||
modifier = elemModifiers.join(separators.modifier); | ||
} else if (~name.indexOf(separators.modifier)) { | ||
var blockModifiers = name.split(separators.modifier); | ||
|
||
block = blockModifiers.shift(); | ||
modifier = blockModifiers.join(separators.modifier); | ||
} | ||
|
||
if (block) { | ||
// produce multiple classes | ||
var prefix = block; | ||
var result = []; | ||
|
||
if (element) { | ||
prefix += separators.element + element; | ||
result.push(prefix); | ||
} else { | ||
result.push(prefix); | ||
} | ||
|
||
if (modifier) { | ||
result.push(prefix + separators.modifier + modifier); | ||
} | ||
|
||
|
||
item.__bem.block = block; | ||
item.__bem.element = element; | ||
item.__bem.modifier = modifier; | ||
|
||
return result; | ||
} | ||
|
||
// ...otherwise, return processed or original class name | ||
return name; | ||
} | ||
|
||
/** | ||
* Low-level function to transform user-typed class name into full BEM class | ||
* @param {String} name Class name item to process | ||
* @param {ZenNode} item Host node for provided class name | ||
* @param {String} entityType Type of entity to be tried to transform | ||
* ('element' or 'modifier') | ||
* @returns {String} Processed class name or original one if it can't be | ||
* transformed | ||
*/ | ||
function transformClassName(name, item, entityType) { | ||
var reSep = new RegExp('^(' + separators[entityType] + ')+', 'g'); | ||
if (reSep.test(name)) { | ||
var depth = 0; // parent lookup depth | ||
var cleanName = name.replace(reSep, function(str, p1) { | ||
depth = str.length / separators[entityType].length; | ||
return ''; | ||
}); | ||
|
||
// find donor element | ||
var donor = item; | ||
while (donor.parent && depth--) { | ||
donor = donor.parent; | ||
} | ||
|
||
if (donor && donor.__bem) { | ||
var prefix = donor.__bem.block; | ||
if (entityType == 'modifier' && donor.__bem.element) | ||
prefix += separators.element + donor.__bem.element; | ||
|
||
return prefix + separators[entityType] + cleanName; | ||
} | ||
} | ||
|
||
return name; | ||
} | ||
|
||
/** | ||
* Utility function, checks if <code>arr</code> contains <code>value</code> | ||
* @param {Array} arr | ||
* @param {Object} value | ||
* @returns {Boolean} | ||
*/ | ||
function arrayInclude(arr, value) { | ||
var result = -1; | ||
if (arr.indexOf) { | ||
result = arr.indexOf(value); | ||
} else { | ||
for (var i = 0, il = arr.length; i < il; i++) { | ||
if (arr[i] === value) { | ||
result = i; | ||
break; | ||
} | ||
} | ||
} | ||
|
||
return result != -1; | ||
} | ||
|
||
/** | ||
* Recursive function for processing tags, which extends class names | ||
* according to BEM specs: http://bem.github.com/bem-method/pages/beginning/beginning.ru.html | ||
* <br><br> | ||
* It does several things:<br> | ||
* <ul> | ||
* <li>Expands complex class name (according to BEM symbol semantics): | ||
* .block__elem_modifier → .block.block__elem.block__elem_modifier | ||
* </li> | ||
* <li>Inherits block name on child elements: | ||
* .b-block > .__el > .__el → .b-block > .b-block__el > .b-block__el__el | ||
* </li> | ||
* <li>Treats typographic '—' symbol as '__'</li> | ||
* <li>Double underscore (or typographic '–') is also treated as an element | ||
* level lookup, e.g. ____el will search for element definition in parent’s | ||
* parent element: | ||
* .b-block > .__el1 > .____el2 → .b-block > .b-block__el1 > .b-block__el2 | ||
* </li> | ||
* </ul> | ||
* | ||
* @param {ZenNode} tree | ||
* @param {Object} profile | ||
* @param {Number} [level] Depth level | ||
*/ | ||
function process(tree, profile, level) { | ||
for (var i = 0, il = tree.children.length; i < il; i++) { | ||
var item = tree.children[i]; | ||
process(bemParse(item), profile); | ||
if (item.type == 'tag' && item.start) | ||
shouldRunHtmlFilter = true; | ||
} | ||
|
||
return tree; | ||
}; | ||
|
||
/** | ||
* @param {ZenNode} tree | ||
* @param {Object} profile | ||
* @param {Number} [level] Depth level | ||
*/ | ||
return function(tree, profile, level) { | ||
shouldRunHtmlFilter = false; | ||
tree = process(tree, profile, level); | ||
// in case 'bem' filter is applied after 'html' filter: run it again | ||
// to update output | ||
if (shouldRunHtmlFilter) { | ||
tree = zen_coding.runFilters(tree, profile, 'html'); | ||
} | ||
|
||
return tree; | ||
}; | ||
})()); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> | ||
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> | ||
<head> | ||
<title>JavaScript unit test file</title> | ||
<meta http-equiv="content-type" content="text/html; charset=utf-8" /> | ||
<link rel="stylesheet" href="unittest.css" type="text/css" /> | ||
|
||
<script src="../zen_settings.js" type="text/javascript"></script> | ||
<script src="../zen_parser.js" type="text/javascript"></script> | ||
<script src="../zen_resources.js" type="text/javascript"></script> | ||
<script src="../zen_coding.js" type="text/javascript"></script> | ||
<script type="text/javascript" src="../filters/format.js"></script> | ||
<script type="text/javascript" src="../filters/html.js"></script> | ||
<script type="text/javascript" src="../filters/bem.js"></script> | ||
<script type="text/javascript"> | ||
zen_settings.html.filters = 'bem,html'; | ||
</script> | ||
</head> | ||
<body> | ||
<div id="content"> | ||
<div id="header"> | ||
<h1>BEM filter</h1> | ||
</div> | ||
<!-- Log output (one per Runner, via {testLog: "testlog"} option)--> | ||
<pre id="testlog"></pre> | ||
</div> | ||
<script type="text/javascript"> | ||
var htmlChars = { | ||
'<' : '<', | ||
'>' : '>' | ||
}; | ||
|
||
zen_coding.setCaretPlaceholder(''); | ||
|
||
function expandAbbr(abbr, type, profile) { | ||
return zen_coding.expandAbbreviation(abbr, 'html', 'xhtml'); | ||
} | ||
|
||
function test(abbr) { | ||
var result = expandAbbr(abbr.replace(/\s+/g, '')); | ||
document.getElementById('testlog').innerHTML += '<b>' + abbr + '</b> → \n' + result.replace(/[<>]/g, function(str) { | ||
return htmlChars[str]; | ||
}) + '\n\n'; | ||
console.log('%s → %s', abbr, result); | ||
} | ||
|
||
/* new Test.Unit.Runner({ | ||
"test bem filter": function(){ with (this) { | ||
assertEqual('<div class="block"></div>', expandAbbr('.block')); | ||
assertEqual('<div class="block block__elem"></div>', expandAbbr('.block__elem')); | ||
assertEqual('<div class="block"><div class="block block__elem"></div></div>', expandAbbr('.block>.__elem')); | ||
assertEqual('<div class="block"><div class="block block__elem"></div></div>', expandAbbr('.block>.__elem')); | ||
assertEqual('<div class="b-block"><div class="b-block b-block__el"><div class="b-block b-block__el"></div></div></div>', expandAbbr('.b-block>.__el >.__el')); | ||
}} | ||
}); */ | ||
|
||
test('.b-block_type_foo'); | ||
test('.b-block > ._type_foo'); | ||
test('.b-block > .__element'); | ||
test('.b-block > .—element'); | ||
test('.b-block > .__el >.__el'); | ||
test('.b-block > .__el1>.____el2'); | ||
test('.b-block > .—el1>.–el2'); | ||
test('.b-bl1 > .b-bl2_foo > .__el + .____el_bar'); | ||
|
||
</script> | ||
</body> | ||
</html> |
Oops, something went wrong.