From 751d1879bc314e260e5dd87ba67a5bfffa399eae Mon Sep 17 00:00:00 2001 From: Vitaly Kosenko Date: Fri, 9 Mar 2018 13:11:18 +0500 Subject: [PATCH 1/4] refactor(import-notation): move tmpl to separate file and add jsdoc --- packages/import-notation/index.js | 36 ++----- packages/import-notation/lib/helpers.js | 135 ++++++++++++++++++++++++ 2 files changed, 145 insertions(+), 26 deletions(-) create mode 100644 packages/import-notation/lib/helpers.js diff --git a/packages/import-notation/index.js b/packages/import-notation/index.js index 844a76ec..49e4a452 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 @@ -93,17 +77,17 @@ function parse(importString, ctx) { */ 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 = { diff --git a/packages/import-notation/lib/helpers.js b/packages/import-notation/lib/helpers.js new file mode 100644 index 00000000..cfab7a44 --- /dev/null +++ b/packages/import-notation/lib/helpers.js @@ -0,0 +1,135 @@ +/** + * @type {ImportNotationTemplates} + */ +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}` : '' +}; + +/** + * Helpers for BemCell + * + * @type {ImportNotationTemplates} + */ +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 ? `${templates._mn(m['name'])}${templates._mv([m['val']])}` : '' +}); + +/** + * Returns import-notation stringify for set of templates + * + * @param {ImportNotationTemplates} ts - set of templates to build parts of import-notation + * + * @returns {String} + */ +const stringifyBuilder = ts => x => ['block', 'elem', 'mod', 'tech'].map(k => ts[k[0]](x[k])).join(''); + +module.exports = { + stringifyMergedCells : stringifyBuilder(templates), + stringifyCell : stringifyBuilder(cellTemplates) +}; + +/** + * Helpers to build parts of import-notation. All parts concatenated by '' gives import-notation string + * + * @typedef {Object} ImportNotationTemplates + * + * @property {Function} b - returns block-part of notation + * @property {Function} e - returns element-part of notation + * @property {Function} m - returns modifiers-part of notation + * @property {Function} t - returns technology-part of notation + */ From 28099be889b69eae29bc94a0a1f71161e576692a Mon Sep 17 00:00:00 2001 From: Vitaly Kosenko Date: Fri, 9 Mar 2018 23:19:30 +0500 Subject: [PATCH 2/4] refactor(import-notation): add expand helper and fix parse method --- packages/import-notation/index.js | 56 ++++++++++----------- packages/import-notation/lib/helpers.js | 52 +++++++++++++++++++ packages/import-notation/test/parse.test.js | 15 ++++++ 3 files changed, 95 insertions(+), 28 deletions(-) diff --git a/packages/import-notation/index.js b/packages/import-notation/index.js index 49e4a452..ad506c5a 100644 --- a/packages/import-notation/index.js +++ b/packages/import-notation/index.js @@ -21,43 +21,43 @@ const BemCellSet = hashSet(helpers.stringifyCell); * @returns {BemCell[]} */ function parse(importString, ctx) { - const main = {}; - ctx || (ctx = {}); + const cell = {}; + + ctx && (importString = helpers.expand(importString, ctx)); return Array.from(importString.split(' ').reduce((acc, importToken) => { const split = importToken.split(':'), type = split[0], tail = split[1]; - if(type === 'b') { - main.block = tail; - acc.add(main); - } else if(type === 'e') { - main.elem = tail; - if(!main.block && ctx.elem !== tail) { - main.block = ctx.block; - acc.add(main); - } - } else if(type === 'm' || type === 't') { - if(!main.block) { - main.block = ctx.block; - main.elem || ctx.elem && (main.elem = ctx.elem); - } + switch(type) { + case 'b': + cell.block = tail; + acc.add(cell); + break; + + case 'e': + cell.elem = tail; + break; - if(type === 'm') { - const splitMod = tail.split('='), - modName = splitMod[0], - modVals = splitMod[1]; + case 'm': { + const splitMod = tail.split('='), + modName = splitMod[0], + modVals = splitMod[1]; - acc.add(Object.assign({}, main, { mod : { name : modName } })); + acc.add(Object.assign({}, cell, { mod : { name : modName } })); + + modVals && modVals.split('|').forEach(modVal => { + acc.add(Object.assign({}, cell, { mod : { name : modName, val : modVal } })); + }); + break; + } - 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)); - } + case 't': + acc.forEach(e => { + e.tech = tail; + }); + break; } return acc; }, new BemCellSet())); diff --git a/packages/import-notation/lib/helpers.js b/packages/import-notation/lib/helpers.js index cfab7a44..8aca2304 100644 --- a/packages/import-notation/lib/helpers.js +++ b/packages/import-notation/lib/helpers.js @@ -1,3 +1,38 @@ +/** + * 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) +}; + /** * @type {ImportNotationTemplates} */ @@ -118,7 +153,24 @@ const cellTemplates = Object.assign({}, templates, { */ const stringifyBuilder = ts => x => ['block', 'elem', 'mod', 'tech'].map(k => ts[k[0]](x[k])).join(''); +/** + * Returns import-notation expanded by context entity + * + * @param {String} n - notation + * @param {BemEntity} ctx - context entity + * + * @returns {String} + */ +const expand = (n, ctx) => { + if(tests.b(n)) { + return n; + } + + return templates.b(ctx.block) + (tests.e(n) ? '' : templates.e(ctx.elem)) + ' ' + n; +}; + module.exports = { + expand, stringifyMergedCells : stringifyBuilder(templates), stringifyCell : stringifyBuilder(cellTemplates) }; diff --git a/packages/import-notation/test/parse.test.js b/packages/import-notation/test/parse.test.js index 31b35f87..78c5a695 100644 --- a/packages/import-notation/test/parse.test.js +++ b/packages/import-notation/test/parse.test.js @@ -70,12 +70,14 @@ describe('block', () => { describe('context is block', () => { it('should extract blockMod', () => { expect(p('m:autoclosable', { block : 'popup' })).to.eql([ + { block : 'popup' }, { block : 'popup', mod : { name : 'autoclosable' } } ]); }); it('should extract block with modifier', () => { expect(p('m:autoclosable=yes', { block : 'popup' })).to.eql([ + { block : 'popup' }, { block : 'popup', mod : { name : 'autoclosable' } }, { block : 'popup', mod : { name : 'autoclosable', val : 'yes' } } ]); @@ -83,6 +85,7 @@ describe('block', () => { it('should extract blockMod with several values', () => { expect(p('m:theme=normal|action', { block : 'popup' })).to.eql([ + { block : 'popup' }, { block : 'popup', mod : { name : 'theme' } }, { block : 'popup', mod : { name : 'theme', val : 'normal' } }, { block : 'popup', mod : { name : 'theme', val : 'action' } } @@ -91,6 +94,7 @@ describe('block', () => { it('should extract blockMod with several modifiers', () => { expect(p('m:theme m:autoclosable', { block : 'popup' })).to.eql([ + { block : 'popup' }, { block : 'popup', mod : { name : 'theme' } }, { block : 'popup', mod : { name : 'autoclosable' } } ]); @@ -98,6 +102,7 @@ describe('block', () => { it('should extract blockMods with several modifiers and several values', () => { expect(p('m:theme=normal|action m:autoclosable=yes', { block : 'popup' })).to.eql([ + { block : 'popup' }, { block : 'popup', mod : { name : 'theme' } }, { block : 'popup', mod : { name : 'theme', val : 'normal' } }, { block : 'popup', mod : { name : 'theme', val : 'action' } }, @@ -323,12 +328,14 @@ describe('elem', () => { describe('context is elem', () => { it('should extract elem with simple modifier', () => { expect(p('m:pseudo', { block : 'button2', elem : 'text' })).to.eql([ + { block : 'button2', elem : 'text' }, { block : 'button2', elem : 'text', mod : { name : 'pseudo' } } ]); }); it('should extract elem with modifier', () => { expect(p('m:pseudo=yes', { block : 'button2', elem : 'text' })).to.eql([ + { block : 'button2', elem : 'text' }, { block : 'button2', elem : 'text', mod : { name : 'pseudo' } }, { block : 'button2', elem : 'text', mod : { name : 'pseudo', val : 'yes' } } ]); @@ -336,6 +343,7 @@ describe('elem', () => { it('should extract elem with modifier and several values', () => { expect(p('m:theme=normal|action', { block : 'button2', elem : 'text' })).to.eql([ + { block : 'button2', elem : 'text' }, { block : 'button2', elem : 'text', mod : { name : 'theme' } }, { block : 'button2', elem : 'text', mod : { name : 'theme', val : 'normal' } }, { block : 'button2', elem : 'text', mod : { name : 'theme', val : 'action' } } @@ -344,6 +352,7 @@ describe('elem', () => { it('should extract elem with several modifiers', () => { expect(p('m:theme m:autoclosable', { block : 'popup', elem : 'tail' })).to.eql([ + { block : 'popup', elem : 'tail' }, { block : 'popup', elem : 'tail', mod : { name : 'theme' } }, { block : 'popup', elem : 'tail', mod : { name : 'autoclosable' } } ]); @@ -353,6 +362,7 @@ describe('elem', () => { expect( p('m:theme=normal|action m:autoclosable=yes', { block : 'popup', elem : 'tail' }) ).to.eql([ + { block : 'popup', elem : 'tail' }, { block : 'popup', elem : 'tail', mod : { name : 'theme' } }, { block : 'popup', elem : 'tail', mod : { name : 'theme', val : 'normal' } }, { block : 'popup', elem : 'tail', mod : { name : 'theme', val : 'action' } }, @@ -418,12 +428,14 @@ describe('elem', () => { describe('context is current elem', () => { it('should extract elem with simple modifier', () => { expect(p('e:text m:pseudo', { block : 'button2', elem : 'text' })).to.eql([ + { block : 'button2', elem : 'text' }, { block : 'button2', elem : 'text', mod : { name : 'pseudo' } } ]); }); it('should extract elem with modifier', () => { expect(p('e:text m:pseudo=yes', { block : 'button2', elem : 'text' })).to.eql([ + { block : 'button2', elem : 'text' }, { block : 'button2', elem : 'text', mod : { name : 'pseudo' } }, { block : 'button2', elem : 'text', mod : { name : 'pseudo', val : 'yes' } } ]); @@ -431,6 +443,7 @@ describe('elem', () => { it('should extract elem with modifier and several values', () => { expect(p('e:text m:theme=normal|action', { block : 'button2', elem : 'text' })).to.eql([ + { block : 'button2', elem : 'text' }, { block : 'button2', elem : 'text', mod : { name : 'theme' } }, { block : 'button2', elem : 'text', mod : { name : 'theme', val : 'normal' } }, { block : 'button2', elem : 'text', mod : { name : 'theme', val : 'action' } } @@ -439,6 +452,7 @@ describe('elem', () => { it('should extract elem with several modifiers', () => { expect(p('e:tail m:theme m:autoclosable', { block : 'popup', elem : 'tail' })).to.eql([ + { block : 'popup', elem : 'tail' }, { block : 'popup', elem : 'tail', mod : { name : 'theme' } }, { block : 'popup', elem : 'tail', mod : { name : 'autoclosable' } } ]); @@ -448,6 +462,7 @@ describe('elem', () => { expect( p('e:tail m:theme=normal|action m:autoclosable=yes', { block : 'popup', elem : 'tail' }) ).to.eql([ + { block : 'popup', elem : 'tail' }, { block : 'popup', elem : 'tail', mod : { name : 'theme' } }, { block : 'popup', elem : 'tail', mod : { name : 'theme', val : 'normal' } }, { block : 'popup', elem : 'tail', mod : { name : 'theme', val : 'action' } }, From 8b94a32966b6ed637f70d86072d8ba4df9803506 Mon Sep 17 00:00:00 2001 From: Vitaly Kosenko Date: Fri, 9 Mar 2018 23:21:43 +0500 Subject: [PATCH 3/4] feat(import-notation): add expand to api --- packages/import-notation/index.js | 3 ++- packages/import-notation/test/expand.test.js | 28 ++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 packages/import-notation/test/expand.test.js diff --git a/packages/import-notation/index.js b/packages/import-notation/index.js index ad506c5a..f5613bed 100644 --- a/packages/import-notation/index.js +++ b/packages/import-notation/index.js @@ -92,5 +92,6 @@ function stringify(cells) { module.exports = { parse, - stringify + stringify, + expand : helpers.expand }; diff --git a/packages/import-notation/test/expand.test.js b/packages/import-notation/test/expand.test.js new file mode 100644 index 00000000..28fe8ed1 --- /dev/null +++ b/packages/import-notation/test/expand.test.js @@ -0,0 +1,28 @@ +var expect = require('chai').expect, + e = require('..').expand; + +it('should return notation if no ctx specified', () => { + expect(e('b:button')).to.eql('b:button'); +}); + +describe('ctx', () => { + describe('block', () => { + it('should copy block if notation has no block', () => { + expect(e('e:icon', { block : 'button' })).to.eql('b:button e:icon'); + }); + + it('should not copy block if notation has block', () => { + expect(e('b:button e:icon', { block : 'input' })).to.eql('b:button e:icon'); + }); + }); + + describe('elem', () => { + it('should copy elem if notation has no elem', () => { + expect(e('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(e('b:button e:icon', { block : 'input', elem : 'clear' })).to.eql('b:button e:icon'); + }); + }); +}); From 550bbbfc59632f398e2ad7dc47c180cd9baf38a7 Mon Sep 17 00:00:00 2001 From: Vitaly Kosenko Date: Sat, 10 Mar 2018 11:39:02 +0500 Subject: [PATCH 4/4] docs(import-notation): add expand and fix parse in readme --- packages/import-notation/README.md | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/packages/import-notation/README.md b/packages/import-notation/README.md index 2e0a4bf1..af5d4e28 100644 --- a/packages/import-notation/README.md +++ b/packages/import-notation/README.md @@ -38,13 +38,14 @@ API * [parse](#parsestr-ctx) * [stringify](#stringify) +* [expand](#parsestr-ctx) ### parse(str, ctx) Parameter | Type | Description ----------|----------|-------------------------------------------------------- `str` | `string` | BEM import notation check [notation section](#notation) -`ctx` | `object` | BEM entity name representation. +`[ctx]` | `object` | BEM entity name representation Parses the string into BEM entities. @@ -58,19 +59,16 @@ entity.elem // → 'text' #### ctx -Context allows to extract portion of entities. +Context allows to use short form of notation. ```js var enties = parse('m:theme=normal', { block: 'button' }); -// → [ { block: 'button', mod: { name: 'theme' } }, +// → [ { block: 'button' }, +// { block: 'button', mod: { name: 'theme' } }, // { block: 'button', mod: { name: 'theme', val: 'normal' } } ] ``` -Note that, using context exludes `{ block: 'button'}` from result. - -So `parse('m:theme=normal', { block: 'button' })` is not same as `parse('b:button m:theme=normal')` - ### stringify Parameter | Type | Description @@ -80,6 +78,23 @@ Parameter | Type | Description Forms a string from [BEM entities]. Be aware to merge only one type of entities. The array should contains one block or one elem and optionally it's modifiers. +### expand(str, ctx) + +Parameter | Type | Description +----------|----------|-------------------------------------------------------- +`str` | `string` | BEM import notation check [notation section](#notation) +`ctx` | `object` | BEM entity name representation + +Expand notation string to full form by context BEM entity. + +Example: + +```js +var notation = parse('e:text', { block: 'button' }); + +// → 'b:button e:text' +``` + Notation --------