diff --git a/packages/import-notation/README.md b/packages/import-notation/README.md index c2c865f3..63cc590c 100644 --- a/packages/import-notation/README.md +++ b/packages/import-notation/README.md @@ -38,13 +38,29 @@ API * [parse](#parsestr-scope) * [stringify](#stringify) +* [fillup](#fillupstr-scope) + +### fillup(str, scope) + +Parameter | Type | Description +----------|----------|-------------------------------------------------------- +`str` | `string` | BEM import notation check [notation section](#notation) +`scope` | `object` | BEM entity name representation + +Fills up given import-notation by context entity to its full form. + +Example: + +```js +fillup('e:text', { block: 'button' }) // → 'b:button e:text' +``` ### parse(str, [scope]) Parameter | Type | Description ----------|----------|-------------------------------------------------------- `str` | `string` | BEM import notation check [notation section](#notation) -[`scope`] | `object` | BEM entity name representation. +[`scope`] | `object` | BEM entity name representation Parses the string into BEM entities. diff --git a/packages/import-notation/index.js b/packages/import-notation/index.js index 009ac10e..f0cb6595 100644 --- a/packages/import-notation/index.js +++ b/packages/import-notation/index.js @@ -1,23 +1,7 @@ const hashSet = require('hash-set'); +const helpers = require('./lib/helpers'); -const tmpl = { - b : b => `b:${b}`, - e : e => e ? ` e:${e}` : '', - m : m => Object.keys(m).map(name => `${tmpl.mn(name)}${tmpl.mv(m[name])}`).join(''), - mn : m => ` m:${m}`, - mv : v => v.length ? `=${v.join('|')}` : '', - t : t => t ? ` t:${t}` : '' -}; - -const btmpl = Object.assign({}, tmpl, { - m : m => m ? `${tmpl.mn(m['name'])}${tmpl.mv([m['val']])}` : '' -}); - -const BemCellSet = hashSet(cell => - ['block', 'elem', 'mod', 'tech'] - .map(k => btmpl[k[0]](cell[k])) - .join('') -); +const BemCellSet = hashSet(helpers.stringifyCell); /** * Parse import statement and extract bem entities @@ -38,43 +22,41 @@ const BemCellSet = hashSet(cell => */ function parse(importString, scope) { const main = {}; - scope || (scope = {}); + + scope && (importString = helpers.fillup(importString, scope)); return Array.from(importString.split(' ').reduce((acc, importToken) => { const split = importToken.split(':'), type = split[0], - tail = split[1]; + val = split[1]; - if(type === 'b') { - main.block = tail; + switch(type) { + case 'b': + main.block = val; acc.add(main); - } else if(type === 'e') { - main.elem = tail; - if(!main.block && scope.elem !== tail) { - main.block = scope.block; - acc.add(main); - } - } else if(type === 'm' || type === 't') { - if(!main.block) { - main.block = scope.block; - main.elem || scope.elem && (main.elem = scope.elem); - acc.add(main); - } - - if(type === 'm') { - const splitMod = tail.split('='), - modName = splitMod[0], - modVals = splitMod[1]; + break; + case 'e': + // mutates already added item + main.elem = val; + break; + case 'm': { + const splitMod = val.split('='), + modName = splitMod[0], + modVals = splitMod[1]; - acc.add(Object.assign({}, main, { mod : { name : modName } })); + acc.add(Object.assign({}, main, { mod : { name : modName } })); - modVals && modVals.split('|').forEach(modVal => { - acc.add(Object.assign({}, main, { mod : { name : modName, val : modVal } })); - }); - } else { - acc.size || acc.add(main); - acc.forEach(e => (e.tech = tail)); - } + modVals && modVals.split('|').forEach(modVal => { + acc.add(Object.assign({}, main, { mod : { name : modName, val : modVal } })); + }); + break; + } + case 't': + // mutates already added items + acc.forEach(e => { + e.tech = val; + }); + break; } return acc; }, new BemCellSet())); @@ -94,20 +76,21 @@ function parse(importString, scope) { */ function stringify(cells) { const merged = [].concat(cells).reduce((acc, cell) => { - cell.block && (acc.b = cell.block); - cell.elem && (acc.e = cell.elem); - cell.mod && (acc.m[cell.mod.name] || (acc.m[cell.mod.name] = [])) + cell.block && (acc.block = cell.block); + cell.elem && (acc.elem = cell.elem); + cell.mod && (acc.mod[cell.mod.name] || (acc.mod[cell.mod.name] = [])) && cell.mod.val && typeof cell.mod.val !== 'boolean' - && !~acc.m[cell.mod.name].indexOf(cell.mod.val) - && acc.m[cell.mod.name].push(cell.mod.val); - cell.tech && (acc.t = cell.tech); + && !~acc.mod[cell.mod.name].indexOf(cell.mod.val) + && acc.mod[cell.mod.name].push(cell.mod.val); + cell.tech && (acc.tech = cell.tech); return acc; - }, { m : {} }); + }, { mod : {} }); - return ['b', 'e', 'm', 't'].map(k => tmpl[k](merged[k])).join(''); + return helpers.stringifyMergedCells(merged); } module.exports = { parse, - stringify + stringify, + fillup : helpers.fillup }; diff --git a/packages/import-notation/lib/helpers.js b/packages/import-notation/lib/helpers.js new file mode 100644 index 00000000..fca4eb25 --- /dev/null +++ b/packages/import-notation/lib/helpers.js @@ -0,0 +1,176 @@ +/** + * Helpers to test import-notation first part + */ +const tests = { + /** + * Returns true if notation starts with block-part + * + * Example: + * ```js + * b('b:button e:icon') // true + * b('e:icon') // false + * ``` + * + * @param {String} n - notation + * + * @returns {Boolean} + */ + b : n => /^b:/.test(n), + + /** + * Returns true if notation starts with element-part + * + * Example: + * ```js + * b('b:button e:icon') // false + * b('e:icon') // true + * ``` + * + * @param {String} n - notation + * + * @returns {Boolean} + */ + e : n => /^e:/.test(n) +}; + +/** + * Helpers to build parts of import-notation. + * All parts concatenated by '' gives full import-notation string + */ +const templates = { + /** + * Returns block-part of notation + * + * Example: + * ```js + * b('button') // 'b:button' + * ``` + * + * @param {String} b - name of block + * + * @returns {String} + */ + b : b => `b:${b}`, + + /** + * Returns element-part of notation + * + * Example: + * ```js + * e('icon') // ' e:icon' + * ``` + * + * @param {String} e - name of element + * + * @returns {String} + */ + e : e => e ? ` e:${e}` : '', + + /** + * Returns modifiers-part of notation + * + * Example: + * ```js + * m({ color: ['red', 'yellow'], theme: ['default'] }) + * // ' m:color=red|yellow m:theme=default' + * ``` + * + * @param {Object} m - modifiers with values in arrays + * + * @returns {String} + */ + m : m => Object.keys(m).map(name => `${templates._mn(name)}${templates._mv(m[name])}`).join(''), + + /** + * Returns modifier-name-part of notation + * + * Example: + * ```js + * _mn('color') // ' m:color' + * ``` + * + * @param {String} mn - name of modifier + * + * @returns {String} + */ + _mn : mn => ` m:${mn}`, + + /** + * Returns modifier-values-part of notation + * + * Example: + * ```js + * _mv(['red', 'yellow']) // '=red|yellow' + * ``` + * + * @param {String[]} mv - modifier values in array + * + * @returns {String} + */ + _mv : mv => mv.length ? `=${mv.join('|')}` : '', + + /** + * Returns technology-part of notation + * + * Example: + * ```js + * t('js') // ' t:js' + * ``` + * + * @param {String[]} t - name of technology + * + * @returns {String} + */ + t : t => t ? ` t:${t}` : '' +}; + +/** + * BemCell variant of templates + */ +const cellTemplates = Object.assign({}, templates, { + /** + * Returns modifiers-part of notation + * + * Example: + * ```js + * m({ name: 'theme', val: 'default' }) // ' m:theme=default' + * ``` + * + * @param {{ name: String, val: String }} m - modifier with single value + * + * @returns {String} + */ + m : m => m ? ` m:${m.name}=${m.val}` : '' +}); + +/** + * Returns import-notation stringify method for set of templates + * + * @param {Object} ts - helpers to build parts of import-notation + * + * @returns {String} + */ +const stringifyMethod = ts => x => ['block', 'elem', 'mod', 'tech'].map(k => ts[k[0]](x[k])).join(''); + +/** + * Returns import-notation filled up by context entity + * + * @param {String} n - notation + * @param {BemEntity} scope - context entity + * + * @returns {String} + */ +const fillup = (n, scope) => { + if(tests.b(n)) { + return n; + } + + return templates.b(scope.block) + + (tests.e(n) ? '' : templates.e(scope.elem)) + ' ' + n; +}; + +module.exports = { + fillup, + stringifyMergedCells : stringifyMethod(templates), + stringifyCell : stringifyMethod(cellTemplates) +}; diff --git a/packages/import-notation/package.json b/packages/import-notation/package.json index cdb6dac6..f01c7f7d 100644 --- a/packages/import-notation/package.json +++ b/packages/import-notation/package.json @@ -17,6 +17,9 @@ "import" ], "author": "Vasiliy Loginevskiy ", + "files": [ + "lib" + ], "license": "MPL-2.0", "bugs": { "url": "https://github.com/bem/bem-sdk/issues?q=label%3Apkg%3Aimport-notation" diff --git a/packages/import-notation/test/fillup.test.js b/packages/import-notation/test/fillup.test.js new file mode 100644 index 00000000..1b9e0f8b --- /dev/null +++ b/packages/import-notation/test/fillup.test.js @@ -0,0 +1,28 @@ +var expect = require('chai').expect, + f = require('..').fillup; + +describe('scope', () => { + describe('block', () => { + it('should copy block if notation has no block #1', () => { + expect(f('e:icon', { block : 'button' })).to.eql('b:button e:icon'); + }); + + it('should copy block if notation has no block #2', () => { + expect(f('m:theme=default', { block : 'button' })).to.eql('b:button m:theme=default'); + }); + + it('should not copy block if notation has block', () => { + expect(f('b:button e:icon', { block : 'input' })).to.eql('b:button e:icon'); + }); + }); + + describe('elem', () => { + it('should copy elem if notation has no elem', () => { + expect(f('m:theme=default', { block : 'button', elem : 'icon' })).to.eql('b:button e:icon m:theme=default'); + }); + + it('should not copy elem if notation has elem', () => { + expect(f('b:button e:icon', { block : 'input', elem : 'clear' })).to.eql('b:button e:icon'); + }); + }); +});