Skip to content

Commit

Permalink
Add parse, convert, stringify, load and improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
skad0 committed Oct 2, 2016
1 parent 0901015 commit c123ada
Show file tree
Hide file tree
Showing 17 changed files with 315 additions and 50 deletions.
38 changes: 38 additions & 0 deletions lib/convert.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
'use strict';

const assert = require('assert');

const isNotSupported = () => {
throw new Error(
'This format isn\'t supported yet, file an issue: https://github.com/bem-sdk/bem-decl/issues/new'
);
};

const converters = {
enb: require('./convert/enb'),
v2: isNotSupported,
v1: isNotSupported,
harmony: isNotSupported
};

/**
* Convert normalized declaration to target format
*
* @param {Array|Object} decl normalized declaration
* @param {Object} opts Addtional options
* @param {String} opts.version format version
* @return {Array} Array with converted declaration
*/
module.exports = function (decl, opts) {
opts || (opts = {});

const version = opts.version;

assert(version, 'You must declare target version');

if (!converters[version]) {
throw new Error('Unknown format');
}

return converters[version](decl);
};
23 changes: 23 additions & 0 deletions lib/convert/enb.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
'use strict';

/**
* Convert normalized declaration to enb format
*
* @param {Array|Object} decl Source declaration
* @return {Array}
*/
module.exports = function (decl) {
Array.isArray(decl) || (decl = [decl]);

return decl.map(item => {
const entity = item.entity;
let tmp = {};

tmp.block = entity.block;
entity.elem && (tmp.elem = entity.elem);
entity.modName && (tmp.mod = entity.modName);
entity.modVal && (tmp.val = entity.modVal);

return tmp;
});
};
21 changes: 21 additions & 0 deletions lib/detect.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
'use strict';

const assert = require('assert');

/**
* Detects decl version
*
* @param {Object} obj Declaration object
* @return {String}
*/
module.exports = function (obj) {
assert(typeof obj === 'object', 'Argument must be an object');

if (typeof obj.blocks === 'object') {
return 'v1';
} else if (typeof obj.deps === 'object') {
return 'enb';
} else if (typeof obj.decl === 'object' || Array.isArray(obj)) {
return 'v2';
}
};
11 changes: 7 additions & 4 deletions lib/index.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
'use strict';

const normalize = {
default: require('./normalize'),
v1: require('./normalize'),
v2: require('./normalize2'),
harmony: require('./normalize-harmony'),
v2: require('./normalize2')
enb: require('./normalize2')
};

module.exports = {
Expand All @@ -18,12 +19,14 @@ module.exports = {
return normalize.harmony(decl);
}

return normalize.default(decl);
return normalize.v1(decl);
},
merge: require('./merge'),
subtract: require('./subtract'),
intersect: require('./intersect'),
parse: require('./parse'),
assign: require('./assign'),
normalizer: (version) => (normalize[version] || normalize.default)
load: require('./load'),
stringify: require('./stringify'),
normalizer: version => (normalize[version] || normalize.v1)
};
13 changes: 13 additions & 0 deletions lib/load.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
'use strict';

const fs = require('mz/fs');

const parse = require('./parse');

/**
* Read file and call parse on its content
*
* @param {String} filePath path to file
* @return {Promise}
*/
module.exports = filePath => fs.readFile(filePath, 'utf-8').then(parse);
53 changes: 30 additions & 23 deletions lib/parse.js
Original file line number Diff line number Diff line change
@@ -1,38 +1,45 @@
'use strict';

const assert = require('assert');

const nodeEval = require('node-eval');

const detect = require('./detect');
const normalize = require('./normalize');
const normalize2 = require('./normalize2');
const normalizeHarmony = require('./normalize-harmony');

const normalizers = {
'1.0': normalize,
next: normalizeHarmony
v1: normalize,
v2: normalize2,
harmony: normalizeHarmony
};

/**
* Parses BEMDECL file data
*
* @param {{version: string, decl: BEMEntityPart[]}|{blocks: BEMEntityPart[]}} bemdecl [description]
* @returns {[type]} [description]
* @param {String|Object} bemdecl - string of bemdecl or object
* @returns {Array<BemEntity>} Array of normalized entities
*/
module.exports = function parse(bemdecl) {
const hasOwn = Object.prototype.hasOwnProperty.bind(Object(bemdecl));

let version, decl;

// Legacy 1.0 format
if (hasOwn('blocks')) {
version = '1.0';
decl = bemdecl.blocks;
} else if (hasOwn('version') && hasOwn('decl')) {
version = bemdecl.version;
decl = bemdecl.decl;
assert(typeof bemdecl === 'object' || typeof bemdecl === 'string', 'Bemdecl must be String or Object');

const declaration = (typeof bemdecl === 'string') ? nodeEval(bemdecl) : bemdecl;
const hasOwn = Object.prototype.hasOwnProperty.bind(Object(declaration));
const version = declaration.version || detect(declaration);

switch (version) {
case 'v1':
assert(hasOwn('blocks'), 'Invalid declaration format');
return normalizers.v1(declaration.blocks);
case 'v2':
case 'enb':
assert(hasOwn('decl') || hasOwn('deps'), 'Invalid format of declaration.');
return normalizers.v2(declaration.decl || declaration.deps);
case 'harmony':
assert(hasOwn('decl'), 'Invalid format of declaration.');
return normalizers.harmony(declaration.decl);
default:
throw new Error('Unknown BEMDECL format.');
}

const normalizer = normalizers[version];

if (!decl || !normalizer) {
throw new Error('Unknown BEMDECL format.');
}

return normalizer(decl);
};
37 changes: 37 additions & 0 deletions lib/stringify.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
'use strict';

const assert = require('assert');

const convert = require('./convert');

/**
* Create string representation of declaration
*
* @param {Array|Object} decl source declaration
* @param {Object} opts additional options
* @param {String} opts.version format version
* @return {String} string representation
*/
module.exports = function (decl, opts) {
opts || (opts = {});
assert(opts.version, 'You must declare target version');

const version = opts.version;

Array.isArray(decl) || (decl = [decl]);

const formatedDecl = convert(decl, { version });
const jsonStr = JSON.stringify(formatedDecl, null, 4);

let exportsStr = '';

if (version === 'v1') {
exportsStr = 'exports.blocks';
} else if (version === 'enb') {
exportsStr = 'exports.deps';
} else {
exportsStr = 'module.exports';
}

return `${exportsStr} = ${jsonStr};\n`;
};
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@
"lib/**"
],
"dependencies": {
"bem-naming": "1.0.1"
"bem-naming": "1.0.1",
"mz": "2.4.0",
"node-eval": "1.0.3"
},
"devDependencies": {
"ava": "^0.16.0",
Expand Down
35 changes: 35 additions & 0 deletions test/convert/enb.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
'use strict';

const test = require('ava');

const convert = require('../../lib/convert');

test('should throw exception if no version given', t => {
t.throws(() => convert({ entity: { block: 'block' }, tech: null }), 'You must declare target version');
});

test('should convert to enb format', t => {
t.deepEqual(convert({ entity: { block: 'block' }, tech: null }, { version: 'enb' }), [{ block: 'block' }]);
});

test('should convert with elem', t => {
t.deepEqual(
convert([
{ entity: { block: 'block' }, tech: null },
{ entity: { block: 'block', elem: 'elem' }, tech: null }
], { version: 'enb' }),
[
{ block: 'block' },
{ block: 'block', elem: 'elem' }
]
);
});

test('should convert with mod', t => {
t.deepEqual(
convert([
{ entity: { block: 'block', modName: 'mod', modVal: 'val' }, tech: null }
], { version: 'enb' }),
[{ block: 'block', mod: 'mod', val: 'val' }]
)
});
6 changes: 5 additions & 1 deletion test/parse/common.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,13 @@ const test = require('ava');
const parse = require('../..').parse;

test('should throw if undefined', t => {
t.throws(() => parse(), /Unknown BEMDECL format/);
t.throws(() => parse(), /Bemdecl must be String or Object/);
});

test('should throw if unsupported', t => {
t.throws(() => parse('({ version: \'unknown\', components: [] })'), /Unknown BEMDECL format/);
});

test('should throw if unsupported in object', t => {
t.throws(() => parse({ version: 'unknown', components: [] }), /Unknown BEMDECL format/);
});
25 changes: 25 additions & 0 deletions test/parse/enb.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
'use strict';

const test = require('ava');

const parse = require('../../lib/parse');

test('should throw error while parsing empty deps property if version not given', t => {
t.deepEqual(parse('({ deps: [] })'), []);
});

test('should parse blocks property with single entity', t => {
t.deepEqual(parse('({ version: \'enb\', deps: [{ block: \'doesnt-matter\', elems: [\'elem\'] }] })'),
[{ entity: { block: 'doesnt-matter' }, tech: null },
{ entity: { block: 'doesnt-matter', elem: 'elem' }, tech: null }]);
});

test('should parse empty legacy blocks property of object', t => {
t.deepEqual(parse({ version: 'enb', deps: [] }), []);
});

test('should parse blocks property with single entity of object', t => {
t.deepEqual(parse({ version: 'enb', deps: [{ block: 'doesnt-matter', elems: ['elem'] }] }),
[{ entity: { block: 'doesnt-matter' }, tech: null },
{ entity: { block: 'doesnt-matter', elem: 'elem' }, tech: null }]);
});
24 changes: 24 additions & 0 deletions test/parse/harmony.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
'use strict';

const test = require('ava');
const parse = require('../..').parse;

test('should parse empty legacy blocks property', t => {
t.deepEqual(parse('({ version: \'harmony\', decl: [] })'), []);
});

test('should parse blocks property with single entity', t => {
t.deepEqual(parse('({ version: \'harmony\', decl: [{ block: \'doesnt-matter\', elems: [\'elem\'] }] })'),
[{ entity: { block: 'doesnt-matter' }, tech: null },
{ entity: { block: 'doesnt-matter', elem: 'elem' }, tech: null }]);
});

test('should parse empty legacy blocks property of object', t => {
t.deepEqual(parse({ version: 'harmony', decl: [] }), []);
});

test('should parse blocks property with single entity of object', t => {
t.deepEqual(parse({ version: 'harmony', decl: [{ block: 'doesnt-matter', elems: ['elem'] }] }),
[{ entity: { block: 'doesnt-matter' }, tech: null },
{ entity: { block: 'doesnt-matter', elem: 'elem' }, tech: null }]);
});
12 changes: 11 additions & 1 deletion test/parse/legacy.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,20 @@ const test = require('ava');
const parse = require('../..').parse;

test('should parse empty legacy blocks property', t => {
t.deepEqual(parse({ blocks: [] }), []);
t.deepEqual(parse('({ blocks: [] })'), []);
});

test('should parse blocks property with single entity', t => {
t.deepEqual(parse('({ blocks: [{ name: \'doesnt-matter\' }] })'),
[{ entity: { block: 'doesnt-matter' }, tech: null }]);
});

test('should parse empty legacy blocks property of object', t => {
t.deepEqual(parse({ blocks: [] }), []);
});

test('should parse blocks property with single entity of object', t => {
t.deepEqual(parse({ blocks: [{ name: 'doesnt-matter' }] }),
[{ entity: { block: 'doesnt-matter' }, tech: null }]);
});

14 changes: 0 additions & 14 deletions test/parse/next.test.js

This file was deleted.

Loading

0 comments on commit c123ada

Please sign in to comment.