├── .babelrc ├── .editorconfig ├── .eslintrc ├── .gitignore ├── .travis.yml ├── lib ├── index.d.ts ├── index.js └── validate.js ├── license.md ├── package.json ├── readme.md └── test ├── .eslintrc └── lib ├── index.js └── validate.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ "es2015" ] 3 | } 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org/ 2 | root = true 3 | 4 | [*] 5 | end_of_line = lf 6 | insert_final_newline = true 7 | trim_trailing_whitespace = true 8 | charset = utf-8 9 | indent_style = space 10 | indent_size = 4 11 | 12 | [*.{json,yml}] 13 | indent_size = 2 14 | 15 | [{.babelrc,.eslintrc}] 16 | indent_size = 2 17 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "rebem/configs/common", 4 | "rebem/configs/babel" 5 | ], 6 | "rules": { 7 | "no-console": 0 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | tmp/ 3 | build/ 4 | coverage/ 5 | *.sublime-* 6 | *.log 7 | .DS_Store 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # https://docs.travis-ci.com/user/customizing-the-build/ 2 | 3 | sudo: false 4 | 5 | language: node_js 6 | 7 | node_js: 8 | - "0.12" 9 | - "4" 10 | - "5" 11 | 12 | branches: 13 | only: 14 | - master 15 | 16 | matrix: 17 | fast_finish: true 18 | 19 | before_install: 20 | - npm install -g npm 21 | - npm --version 22 | 23 | script: npm start ci 24 | -------------------------------------------------------------------------------- /lib/index.d.ts: -------------------------------------------------------------------------------- 1 | export interface BEMEntity { 2 | tag?: string; 3 | block?: string; 4 | elem?: string; 5 | mods?: { 6 | [key: string]: string | number | boolean; 7 | }; 8 | mix?: BEMEntity | BEMEntity[]; 9 | className?: string; 10 | } 11 | 12 | export declare function stringify(entity: BEMEntity): string; 13 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | const modDelim = process.env.REBEM_MOD_DELIM || '_'; 2 | const elemDelim = process.env.REBEM_ELEM_DELIM || '__'; 3 | 4 | export function stringify(props) { 5 | if (!props) { 6 | return ''; 7 | } 8 | 9 | let out = ''; 10 | 11 | // block 12 | if (typeof props.block !== 'undefined') { 13 | out += (out ? ' ' : '') + props.block; 14 | 15 | // elem 16 | if (typeof props.elem !== 'undefined') { 17 | out += elemDelim + props.elem; 18 | } 19 | 20 | const entity = out; 21 | 22 | if (typeof props.mods !== 'undefined') { 23 | Object.keys(props.mods).forEach(modName => { 24 | const modValue = props.mods[modName]; 25 | let modValueString = ''; 26 | 27 | if (modValue !== false) { 28 | // 'short' boolean mods 29 | if (modValue !== true) { 30 | modValueString += modDelim + modValue; 31 | } 32 | 33 | out += ' ' + entity + modDelim + modName + modValueString; 34 | } 35 | }); 36 | } 37 | } 38 | 39 | if (typeof props.mix !== 'undefined') { 40 | // convert object or array into array 41 | const mixes = [].concat(props.mix); 42 | 43 | mixes 44 | // filter holes in array 45 | .filter(mix => mix) 46 | .forEach(mix => { 47 | out += (out ? ' ' : '') + stringify(mix); 48 | }); 49 | } 50 | 51 | if (typeof props.className !== 'undefined') { 52 | out += (out ? ' ' : '') + props.className; 53 | } 54 | 55 | return out; 56 | } 57 | 58 | export { default as validate } from './validate.js'; 59 | -------------------------------------------------------------------------------- /lib/validate.js: -------------------------------------------------------------------------------- 1 | function isPlainObject(target) { 2 | return Object.prototype.toString.call(target) === '[object Object]'; 3 | } 4 | 5 | export default function validate(props) { 6 | if (typeof props.block === 'undefined') { 7 | if (typeof props.elem !== 'undefined') { 8 | console.warn('you should provide block along with elem', props); 9 | } 10 | 11 | if (typeof props.mods !== 'undefined') { 12 | console.warn('you should provide block along with mods', props); 13 | } 14 | } else { 15 | if (typeof props.block !== 'string') { 16 | console.warn('block should be string', props); 17 | } 18 | 19 | if (typeof props.elem !== 'undefined') { 20 | if (typeof props.elem !== 'string') { 21 | console.warn('elem should be string', props); 22 | } 23 | } 24 | 25 | if (typeof props.mods !== 'undefined') { 26 | if (!isPlainObject(props.mods)) { 27 | console.warn('mods should be a plain object', props); 28 | } 29 | } 30 | } 31 | 32 | if (typeof props.mix !== 'undefined') { 33 | if (!isPlainObject(props.mix) && !Array.isArray(props.mix)) { 34 | console.warn('mix should be a plain object or array on plain objects', props); 35 | } 36 | } 37 | 38 | return props; 39 | } 40 | -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | * Copyright (c) 2015–present Kir Belevich 4 | * Copyright (c) 2015–present Denis Koltsov 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rebem-classname", 3 | "version": "0.4.0", 4 | "description": "Helpers for composing and parsing BEM classNames", 5 | "keywords": [ "bem", "helper", "classname", "bemjson", "react" ], 6 | "homepage": "https://github.com/rebem/classname", 7 | "repository": "rebem/classname", 8 | "maintainers": [ 9 | "Kir Belevich (https://github.com/deepsweet)", 10 | "Denis Koltsov (https://github.com/mistadikay)" 11 | ], 12 | "main": "build/index.js", 13 | "typings": "build/index.d.ts", 14 | "files": [ "build/" ], 15 | "devDependencies": { 16 | "start-babel-cli": "1.x.x", 17 | "start-rebem-preset": "0.x.x", 18 | 19 | "babel-preset-es2015": "6.9.x", 20 | 21 | "babel-eslint": "6.1.x", 22 | "eslint-plugin-babel": "3.3.x", 23 | "eslint-config-rebem": "1.1.x", 24 | 25 | "babel-istanbul": "0.11.x", 26 | "require-uncached": "1.0.x", 27 | "husky": "0.11.x" 28 | }, 29 | "scripts": { 30 | "start": "start-runner start-rebem-preset", 31 | "prepush": "npm start prepush", 32 | "prepublish": "npm start build" 33 | }, 34 | "engines": { 35 | "node": ">=0.12.0", 36 | "npm": ">=2.7.0" 37 | }, 38 | "license": "MIT" 39 | } 40 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | [![maintenance](https://img.shields.io/badge/maintained-no-red.svg?style=flat-square)](http://unmaintained.tech) 2 | [![npm](https://img.shields.io/npm/v/rebem-classname.svg?style=flat-square)](https://www.npmjs.com/package/rebem-classname) 3 | [![travis](http://img.shields.io/travis/rebem/classname.svg?style=flat-square)](https://travis-ci.org/rebem/classname) 4 | [![coverage](https://img.shields.io/codecov/c/github/rebem/classname.svg?style=flat-square)](https://codecov.io/github/rebem/classname) 5 | [![deps](https://img.shields.io/gemnasium/rebem/classname.svg?style=flat-square)](https://gemnasium.com/rebem/classname) 6 | [![gitter](https://img.shields.io/badge/gitter-join_chat_%E2%86%92-46bc99.svg?style=flat-square)](https://gitter.im/rebem/rebem) 7 | 8 | Set of helpers for composing and parsing [BEM](http://getbem.com/) classNames. 9 | 10 | ## Install 11 | 12 | ``` 13 | npm i -S rebem-classname 14 | ``` 15 | 16 | ## Usage 17 | 18 | ### `stringify` 19 | 20 | ```js 21 | import { stringify } from 'rebem-classname'; 22 | 23 | const className = stringify(props); 24 | ``` 25 | 26 | #### props: 27 | 28 | ##### `block` 29 | 30 | [Reference](https://en.bem.info/method/key-concepts/#block). 31 | 32 | ```js 33 | stringify({ 34 | block: 'beep' 35 | }); 36 | // "beep" 37 | ``` 38 | 39 | ##### `elem` 40 | 41 | [Reference](https://en.bem.info/method/key-concepts/#element). 42 | 43 | ```js 44 | stringify({ 45 | block: 'beep', 46 | elem: 'boop' 47 | }); 48 | // "beep__boop" 49 | ``` 50 | 51 | ##### `mods` 52 | 53 | [Reference](https://en.bem.info/method/key-concepts/#modifier). 54 | 55 | ```js 56 | stringify({ 57 | block: 'beep', 58 | mods: { 59 | foo: 'bar' 60 | } 61 | }); 62 | // "beep beep_foo_bar" 63 | ``` 64 | 65 | ```js 66 | stringify({ 67 | block: 'beep', 68 | mods: { 69 | foo: true, 70 | bar: false 71 | } 72 | }); 73 | // "beep beep_foo" 74 | ``` 75 | 76 | ```js 77 | stringify({ 78 | block: 'beep', 79 | elem: 'boop', 80 | mods: { 81 | foo: 'bar' 82 | } 83 | }); 84 | // "beep__boop beep__boop_foo_bar" 85 | ``` 86 | 87 | ##### `mix` 88 | 89 | [Reference](https://en.bem.info/method/key-concepts/#mix). 90 | 91 | ```js 92 | stringify({ 93 | block: 'beep', 94 | mix: { 95 | block: 'boop', 96 | elem: 'foo' 97 | } 98 | }); 99 | // "beep boop__foo" 100 | ``` 101 | 102 | ```js 103 | stringify({ 104 | block: 'beep', 105 | mix: [ 106 | { 107 | block: 'boop', 108 | elem: 'foo' 109 | }, 110 | { 111 | block: 'bar', 112 | elem: 'baz', 113 | mods: { 114 | test: true 115 | } 116 | } 117 | ] 118 | }); 119 | // "beep boop__foo bar__baz bar__baz_test" 120 | ``` 121 | 122 | ##### `className` 123 | 124 | ```js 125 | stringify({ 126 | block: 'boop' 127 | className: 'beep' 128 | }); 129 | // "boop beep" 130 | ``` 131 | 132 | ### `validate` 133 | 134 | Checks if BEMJSON is valid and throws warnings into console if it's not. Returns the same BEMJSON back. 135 | 136 | ```js 137 | import { validate } from 'rebem-classname'; 138 | 139 | validate({ 140 | elem: 'boop' 141 | }); 142 | // "you should provide block along with elem Object{elem: 'boop'}" 143 | ``` 144 | 145 | ### `parse` 146 | 147 | *TODO* 148 | 149 | ## Custom delimeters 150 | 151 | Default delimeters are `_` for modifiers and `__` for elements, but you can change them with special environment variables: 152 | 153 | ```js 154 | plugins: [ 155 | new webpack.DefinePlugin({ 156 | 'process.env': { 157 | REBEM_MOD_DELIM: JSON.stringify('--'), 158 | REBEM_ELEM_DELIM: JSON.stringify('~~') 159 | } 160 | }) 161 | ] 162 | ``` 163 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "rebem/configs/test" 4 | ], 5 | "rules": { 6 | "no-undefined": 0 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test/lib/index.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import requireUncached from 'require-uncached'; 3 | 4 | import { stringify } from '../../lib/'; 5 | 6 | function test(props, className) { 7 | assert.strictEqual( 8 | stringify(props), 9 | className 10 | ); 11 | } 12 | 13 | describe('stringify', function() { 14 | it('is function', function() { 15 | assert(typeof stringify === 'function'); 16 | }); 17 | 18 | it('empty class if called without argument', function() { 19 | assert(stringify() === ''); 20 | }); 21 | 22 | it('empty class if no block and mix', function() { 23 | test({}, ''); 24 | }); 25 | 26 | it('empty class if no block and mix, but with className', function() { 27 | test({ className: 'lol' }, 'lol'); 28 | }); 29 | 30 | describe('block', function() { 31 | it('simple', function() { 32 | test({ block: 'block' }, 'block'); 33 | }); 34 | 35 | it('props.className + block', function() { 36 | test( 37 | { 38 | block: 'block2', 39 | className: 'block1' 40 | }, 'block2 block1' 41 | ); 42 | }); 43 | }); 44 | 45 | describe('mods', function() { 46 | describe('block', function() { 47 | it('block + mod', function() { 48 | test( 49 | { 50 | block: 'block', 51 | mods: { 52 | mod: 'val' 53 | } 54 | }, 'block block_mod_val' 55 | ); 56 | }); 57 | 58 | it('block + few mods', function() { 59 | test( 60 | { 61 | block: 'block', 62 | mods: { 63 | mod1: 'val1', 64 | mod2: 'val2' 65 | } 66 | }, 'block block_mod1_val1 block_mod2_val2' 67 | ); 68 | }); 69 | 70 | it('block + shorthand mod = true', function() { 71 | test( 72 | { 73 | block: 'block', 74 | mods: { 75 | mod: true 76 | } 77 | }, 'block block_mod' 78 | ); 79 | }); 80 | 81 | it('block + shorthand mod = false', function() { 82 | test( 83 | { 84 | block: 'block', 85 | mods: { 86 | mod: false 87 | } 88 | }, 'block' 89 | ); 90 | }); 91 | }); 92 | 93 | describe('elem', function() { 94 | it('block + elem + mod', function() { 95 | test( 96 | { 97 | block: 'block', 98 | elem: 'elem', 99 | mods: { 100 | mod: 'val' 101 | } 102 | }, 'block__elem block__elem_mod_val' 103 | ); 104 | }); 105 | 106 | it('block + elem + few mods', function() { 107 | test( 108 | { 109 | block: 'block', 110 | elem: 'elem', 111 | mods: { 112 | mod1: 'val1', 113 | mod2: 'val2' 114 | } 115 | }, 'block__elem block__elem_mod1_val1 block__elem_mod2_val2' 116 | ); 117 | }); 118 | 119 | it('block + shorthand mod = true', function() { 120 | test( 121 | { 122 | block: 'block', 123 | elem: 'elem', 124 | mods: { 125 | mod: true 126 | } 127 | }, 'block__elem block__elem_mod' 128 | ); 129 | }); 130 | 131 | it('block + shorthand mod = false', function() { 132 | test( 133 | { 134 | block: 'block', 135 | elem: 'elem', 136 | mods: { 137 | mod: false 138 | } 139 | }, 'block__elem' 140 | ); 141 | }); 142 | }); 143 | }); 144 | 145 | describe('mix', function() { 146 | it('mix without block', function() { 147 | test( 148 | { 149 | mix: { 150 | block: 'block' 151 | } 152 | }, 'block' 153 | ); 154 | }); 155 | 156 | it('block + mix', function() { 157 | test( 158 | { 159 | block: 'block1', 160 | mix: { 161 | block: 'block2' 162 | } 163 | }, 'block1 block2' 164 | ); 165 | }); 166 | 167 | it('block + mods + mix', function() { 168 | test( 169 | { 170 | block: 'block1', 171 | mods: { 172 | mod: 'val' 173 | }, 174 | mix: { 175 | block: 'block2' 176 | } 177 | }, 'block1 block1_mod_val block2' 178 | ); 179 | }); 180 | 181 | it('block + elem + mix', function() { 182 | test( 183 | { 184 | block: 'block1', 185 | elem: 'elem', 186 | mix: { 187 | block: 'block2' 188 | } 189 | }, 'block1__elem block2' 190 | ); 191 | }); 192 | 193 | it('block + elem + mods + mix', function() { 194 | test( 195 | { 196 | block: 'block1', 197 | elem: 'elem', 198 | mods: { 199 | mod: 'val' 200 | }, 201 | mix: { 202 | block: 'block2' 203 | } 204 | }, 'block1__elem block1__elem_mod_val block2' 205 | ); 206 | }); 207 | 208 | it('block + elem + mods + mix + className', function() { 209 | test( 210 | { 211 | block: 'block1', 212 | elem: 'elem', 213 | mods: { 214 | mod: 'val' 215 | }, 216 | mix: { 217 | block: 'block2' 218 | }, 219 | className: 'hello' 220 | }, 'block1__elem block1__elem_mod_val block2 hello' 221 | ); 222 | }); 223 | 224 | it('complex mix', function() { 225 | test( 226 | { 227 | block: 'block1', 228 | mix: { 229 | block: 'block2', 230 | elem: 'elem', 231 | mods: { 232 | mod1: 'val1', 233 | mod2: 'val2' 234 | } 235 | } 236 | }, 'block1 block2__elem block2__elem_mod1_val1 block2__elem_mod2_val2' 237 | ); 238 | }); 239 | 240 | it('multiple mixes', function() { 241 | test( 242 | { 243 | block: 'block1', 244 | mix: [ 245 | { 246 | block: 'block2' 247 | }, 248 | { 249 | block: 'block3' 250 | } 251 | ] 252 | }, 'block1 block2 block3' 253 | ); 254 | }); 255 | 256 | it('multiple mixes with holes', function() { 257 | test( 258 | { 259 | block: 'block1', 260 | mix: [ 261 | undefined, 262 | { 263 | block: 'block2' 264 | }, 265 | null 266 | ] 267 | }, 'block1 block2' 268 | ); 269 | }); 270 | 271 | it('recursive mixes', function() { 272 | test( 273 | { 274 | block: 'block1', 275 | mix: { 276 | block: 'block2', 277 | mix: { 278 | block: 'block3' 279 | } 280 | } 281 | }, 'block1 block2 block3' 282 | ); 283 | }); 284 | }); 285 | 286 | describe('custom delimeters', function() { 287 | it('mods', function() { 288 | process.env.REBEM_MOD_DELIM = '~~'; 289 | 290 | const customStringify = requireUncached('../../lib/').stringify; 291 | 292 | assert.strictEqual( 293 | customStringify({ 294 | block: 'block', 295 | mods: { 296 | mod: 'val' 297 | } 298 | }), 299 | 'block block~~mod~~val' 300 | ); 301 | }); 302 | 303 | it('elem', function() { 304 | process.env.REBEM_ELEM_DELIM = '--'; 305 | 306 | const customStringify = requireUncached('../../lib/').stringify; 307 | 308 | assert.strictEqual( 309 | customStringify({ 310 | block: 'block', 311 | elem: 'elem' 312 | }), 313 | 'block--elem' 314 | ); 315 | }); 316 | }); 317 | }); 318 | -------------------------------------------------------------------------------- /test/lib/validate.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | 3 | import validate from '../../lib/validate'; 4 | 5 | function test(props, message) { 6 | const origConsoleWarn = console.warn; 7 | 8 | console.warn = function(warning, obj) { 9 | assert.strictEqual(warning, message); 10 | assert.deepEqual(obj, props); 11 | }; 12 | 13 | validate(props); 14 | 15 | console.warn = origConsoleWarn; 16 | } 17 | 18 | describe('validate', function() { 19 | describe('block', function() { 20 | it('invalid block', function() { 21 | test( 22 | { 23 | block: true 24 | }, 25 | 'block should be string' 26 | ); 27 | }); 28 | }); 29 | 30 | describe('elem', function() { 31 | it('elem without block', function() { 32 | test( 33 | { 34 | elem: 'elem' 35 | }, 36 | 'you should provide block along with elem' 37 | ); 38 | }); 39 | 40 | it('invalid elem', function() { 41 | test( 42 | { 43 | block: 'block', 44 | elem: true 45 | }, 46 | 'elem should be string' 47 | ); 48 | }); 49 | }); 50 | 51 | describe('mods', function() { 52 | it('mods without block', function() { 53 | test( 54 | { 55 | mods: { 56 | mod: 'val' 57 | } 58 | }, 59 | 'you should provide block along with mods' 60 | ); 61 | }); 62 | 63 | it('block + invalid mods', function() { 64 | test( 65 | { 66 | block: 'block', 67 | mods: true 68 | }, 69 | 'mods should be a plain object' 70 | ); 71 | }); 72 | }); 73 | 74 | describe('mix', function() { 75 | it('invalid mix', function() { 76 | test( 77 | { 78 | mix: false 79 | }, 80 | 'mix should be a plain object or array on plain objects' 81 | ); 82 | }); 83 | }); 84 | }); 85 | --------------------------------------------------------------------------------