diff --git a/src/index.js b/src/index.js index c434f86..a7f50a3 100644 --- a/src/index.js +++ b/src/index.js @@ -1,38 +1,10 @@ const placing = require('./placing'); const rendering = require('./rendering'); -/** - * Merge resources by reading object keys and keeping reference value only if it's type is different from the source - * @param ref - reference object/value - * @param src - source object/value - * @returns {*} - */ -const merge = (ref, src) => { - if (typeof ref !== typeof src) { - return ref; - } else if (ref.length && !src.length) { - return ref; - } else if (ref.length && src.length) { - return src; - } else if (typeof ref === 'object') { - const out = {}; - Object.keys(ref).forEach((key) => out[key] = merge(ref[key], src[key])); - return out; - } else { - return src; - } -}; - -const DEFAULT_OPTIONS = { - 'placing': placing().defaultOptions, - 'rendering': rendering().defaultOptions -}; - const self = { - options: DEFAULT_OPTIONS, compute: (data) => { - const options = merge(DEFAULT_OPTIONS, data['options']); + const options = data['options'] || {}; let nodes = {}; const nodeList = (data['nodes'] || []).filter(n => typeof n.name === 'string'); diff --git a/src/placing.js b/src/placing.js index 5ba9862..cad2350 100644 --- a/src/placing.js +++ b/src/placing.js @@ -1,5 +1,4 @@ -const ezclone = (a) => JSON.parse(JSON.stringify(a)); -const newmap = (w, h, fill) => new Array(w).fill(0).map(() => new Array(h).fill(fill)); +const utils = require('./utils'); /** * @typedef Node1 @@ -21,10 +20,11 @@ const DEFAULT_OPTIONS = { 'diagonals': true, }; -module.exports = (options = DEFAULT_OPTIONS) => { +module.exports = (options) => { + + options = utils.merge(DEFAULT_OPTIONS, options); const self = { - defaultOptions: DEFAULT_OPTIONS, /** * Get the current bounds of the graph of nodes @@ -55,7 +55,7 @@ module.exports = (options = DEFAULT_OPTIONS) => { */ getNewPos: (nodes) => { const b = self.getBounds(nodes); - const map = newmap(b.w, b.h, false); + const map = utils.newMap(b.w, b.h, false); const list = Object.values(nodes).filter(n => n.x !== undefined); list.forEach(n => { map[n.x - b.x][n.y - b.y] = true; @@ -250,7 +250,7 @@ module.exports = (options = DEFAULT_OPTIONS) => { const free = []; const tryPos = (key, x, y) => { - const nodes2 = ezclone(nodes); + const nodes2 = utils.ezClone(nodes); nodes2[key].x = x; nodes2[key].y = y; return self.applyLinks(nodes2, links, depth + 1); diff --git a/src/rendering.js b/src/rendering.js index 34680fd..005420c 100644 --- a/src/rendering.js +++ b/src/rendering.js @@ -1,4 +1,5 @@ const convert = require('xml-js'); +const utils = require('./utils'); let list = {}; try { @@ -39,9 +40,11 @@ const DEFAULT_OPTIONS = { const DEFAULT_SCALE = 0.4; const LINK_MARGIN = (1 - DEFAULT_SCALE) / 2; -module.exports = (options = DEFAULT_OPTIONS) => { +module.exports = (options) => { + + options = utils.merge(DEFAULT_OPTIONS, options); + const self = { - defaultOptions: DEFAULT_OPTIONS, /** * Find icon data from given name diff --git a/src/utils.js b/src/utils.js new file mode 100644 index 0000000..57558f1 --- /dev/null +++ b/src/utils.js @@ -0,0 +1,41 @@ +const self = { + /** + * Merge resources by reading object keys and keeping reference value only if it's type is different from the source + * @param ref - reference object/value + * @param src - source object/value + * @returns {*} + */ + merge: (ref, src) => { + if (typeof ref !== typeof src) { + return ref; + } else if (ref.length && !src.length) { + return ref; + } else if (ref.length && src.length) { + return src; + } else if (typeof ref === 'object') { + const out = {}; + Object.keys(ref).forEach((key) => out[key] = self.merge(ref[key], src[key])); + return out; + } else { + return src; + } + }, + + /** + * Clone any JS variable or object + * @param {*} arg + * @returns {any} + */ + ezClone: (arg) => JSON.parse(JSON.stringify(arg)), + + /** + * Create a new map of the defined bounds and filling + * @param {number} w + * @param {number} h + * @param {*} fill + * @returns {any[][]} + */ + newMap: (w, h, fill) => new Array(w).fill(0).map(() => new Array(h).fill(fill)) +}; + +module.exports = self; \ No newline at end of file diff --git a/test/placing.test.js b/test/placing.test.js index e9ec256..1500174 100644 --- a/test/placing.test.js +++ b/test/placing.test.js @@ -3,35 +3,35 @@ const placing = require('../src/placing'); describe('getBounds', () => { test('no nodes', () => { - const res = placing({}).getBounds({}); + const res = placing().getBounds({}); expect(res).toEqual({x: 0, y: 0, w: 0, h: 0}); }); test('no placed nodes', () => { - const res = placing({}).getBounds({ + const res = placing().getBounds({ 'a': {}, 'b': {} }); expect(res).toEqual({x: 0, y: 0, w: 0, h: 0}); }); test('first node', () => { - const res = placing({}).getBounds({ + const res = placing().getBounds({ 'a': {x: 0, y: 0}, 'b': {} }); expect(res).toEqual({x: 0, y: 0, w: 1, h: 1}); }); test('one node not 0,0', () => { - const res = placing({}).getBounds({ + const res = placing().getBounds({ 'a': {x: 5, y: 6}, 'b': {} }); expect(res).toEqual({x: 5, y: 6, w: 1, h: 1}); }); test('2 nodes', () => { - const res = placing({}).getBounds({ + const res = placing().getBounds({ 'a': {x: 0, y: 0}, 'b': {x: 1, y: 1}, 'c': {} }); expect(res).toEqual({x: 0, y: 0, w: 2, h: 2}); }); test('2 nodes special', () => { - const res = placing({}).getBounds({ + const res = placing().getBounds({ 'a': {x: 1, y: 2}, 'b': {x: -5, y: 6}, 'c': {} }); expect(res).toEqual({x: -5, y: 2, w: 7, h: 5}); @@ -40,29 +40,29 @@ describe('getBounds', () => { describe('getNewPos', () => { test('no nodes', () => { - const res = placing({}).getNewPos({}); + const res = placing().getNewPos({}); expect(res).toEqual({x: 0, y: 0}); }); test('no placed nodes', () => { - const res = placing({}).getNewPos({ + const res = placing().getNewPos({ 'a': {}, 'b': {} }); expect(res).toEqual({x: 0, y: 0}); }); test('one node', () => { - const res = placing({}).getNewPos({ + const res = placing().getNewPos({ 'a': {x: 0, y: 0}, 'b': {} }); expect(res).toEqual({x: 1, y: 0}); }); test('one node not 0,0', () => { - const res = placing({}).getNewPos({ + const res = placing().getNewPos({ 'a': {x: 5, y: 6}, 'b': {} }); expect(res).toEqual({x: 6, y: 6}); }); test('2 nodes', () => { - const res = placing({}).getNewPos({ + const res = placing().getNewPos({ 'a': {x: 0, y: 0}, 'b': {x: 1, y: 1} }); expect(res).toEqual({x: 1, y: 0}); @@ -71,43 +71,43 @@ describe('getNewPos', () => { describe('nodeBetween', () => { test('only 2 nodes', () => { - const res = placing({}).nodeBetween({ + const res = placing().nodeBetween({ 'a': {name: 'a', x: 0, y: 0}, 'b': {name: 'b', x: 1, y: 0} }, 'a', 'b'); expect(res).toBe(false); }); test('other node not between', () => { - const res = placing({}).nodeBetween({ + const res = placing().nodeBetween({ 'a': {name: 'a', x: 0, y: 0}, 'b': {name: 'b', x: 0, y: 1}, 'c': {name: 'c', x: 1, y: 0} }, 'a', 'b'); expect(res).toBe(false); }); test('between aligned h', () => { - const res = placing({}).nodeBetween({ + const res = placing().nodeBetween({ 'a': {name: 'a', x: 0, y: 0}, 'b': {name: 'b', x: 2, y: 0}, 'c': {name: 'c', x: 1, y: 0} }, 'a', 'b'); expect(res).toBe(true); }); test('between aligned v', () => { - const res = placing({}).nodeBetween({ + const res = placing().nodeBetween({ 'a': {name: 'a', x: 0, y: 0}, 'b': {name: 'b', x: 0, y: 2}, 'c': {name: 'c', x: 0, y: 1} }, 'a', 'b'); expect(res).toBe(true); }); test('between diagonal', () => { - const res = placing({}).nodeBetween({ + const res = placing().nodeBetween({ 'a': {name: 'a', x: 0, y: 0}, 'b': {name: 'b', x: 2, y: 2}, 'c': {name: 'c', x: 1, y: 1} }, 'a', 'b'); expect(res).toBe(true); }); test('between diagonal 2', () => { - const res = placing({}).nodeBetween({ + const res = placing().nodeBetween({ 'a': {name: 'a', x: 0, y: 0}, 'b': {name: 'b', x: 2, y: 1}, 'c': {name: 'c', x: 1, y: 1} }, 'a', 'b'); expect(res).toBe(true); }); test('between diagonal 3', () => { - const res = placing({}).nodeBetween({ + const res = placing().nodeBetween({ '1': {'name': '1', 'x': 1, 'y': 0}, '2': {'name': '2', 'x': 2, 'y': 0}, '3': {'name': '3', 'x': 1, 'y': 1}, @@ -121,7 +121,7 @@ describe('nodeBetween', () => { describe('getPosition', () => { test('free node', () => { - const res = placing({'max-link-length': 2}).getPosition({ + const res = placing().getPosition({ 'a': { const: { afterX: [], @@ -135,7 +135,7 @@ describe('getPosition', () => { expect(res).toEqual({x: null, y: null, free: true}); }); test('constrained to another not placed', () => { - const res = placing({'max-link-length': 2}).getPosition({ + const res = placing().getPosition({ 'a': { const: { afterX: [], @@ -149,7 +149,7 @@ describe('getPosition', () => { expect(res).toEqual({x: null, y: null, free: true}); }); test('constrained to another left', () => { - const res = placing({'max-link-length': 2}).getPosition({ + const res = placing().getPosition({ 'a': { const: { afterX: [], @@ -163,7 +163,7 @@ describe('getPosition', () => { expect(res).toEqual({x: 1, y: 0, free: false}); }); test('constrained to another right', () => { - const res = placing({'max-link-length': 2}).getPosition({ + const res = placing().getPosition({ 'a': { const: { afterX: ['b'], @@ -177,7 +177,7 @@ describe('getPosition', () => { expect(res).toEqual({x: -1, y: 0, free: false}); }); test('constrained to another up', () => { - const res = placing({'max-link-length': 2}).getPosition({ + const res = placing().getPosition({ 'a': { const: { afterX: [], @@ -191,7 +191,7 @@ describe('getPosition', () => { expect(res).toEqual({x: 0, y: 1, free: false}); }); test('constrained to another down', () => { - const res = placing({'max-link-length': 2}).getPosition({ + const res = placing().getPosition({ 'a': { const: { afterX: [], @@ -205,7 +205,7 @@ describe('getPosition', () => { expect(res).toEqual({x: 0, y: -1, free: false}); }); test('double constrained diagonal', () => { - const res = placing({'max-link-length': 2}).getPosition({ + const res = placing().getPosition({ 'a': { const: { afterX: [], @@ -220,7 +220,7 @@ describe('getPosition', () => { expect(res).toEqual({x: 1, y: 0, free: false}); }); test('double constrained no diagonal', () => { - const res = placing({'max-link-length': 2}).getPosition({ + const res = placing().getPosition({ 'a': { const: { afterX: [], @@ -235,7 +235,7 @@ describe('getPosition', () => { expect(res).toEqual({x: 2, y: 0, free: false}); }); test('double constrained impossible', () => { - const res = placing({'max-link-length': 2}).getPosition({ + const res = placing().getPosition({ 'a': { const: { afterX: [], @@ -253,11 +253,11 @@ describe('getPosition', () => { describe('isValid', () => { test('no nodes', () => { - const res = placing({diagonals: true}).isValid({}, []); + const res = placing().isValid({}, []); expect(res).toBe(true); }); test('no placed nodes', () => { - const res = placing({diagonals: true}).isValid({ + const res = placing().isValid({ 'a': {}, 'b': {} }, [ {from: 'a', to: 'b'} @@ -265,7 +265,7 @@ describe('isValid', () => { expect(res).toBe(true); }); test('one nodes', () => { - const res = placing({diagonals: true}).isValid({ + const res = placing().isValid({ 'a': {name: 'a', x: 0, y: 0}, 'b': {} }, [ {from: 'a', to: 'b'} @@ -273,13 +273,13 @@ describe('isValid', () => { expect(res).toBe(true); }); test('overlapping nodes', () => { - const res = placing({diagonals: true}).isValid({ + const res = placing().isValid({ 'a': {name: 'a', x: 0, y: 0}, 'b': {name: 'b', x: 0, y: 0} }, []); expect(res).toBe(false); }); test('in between node', () => { - const res = placing({diagonals: true}).isValid({ + const res = placing().isValid({ 'a': {name: 'a', x: 0, y: 0}, 'b': {name: 'b', x: 2, y: 0}, 'c': {name: 'c', x: 1, y: 0} }, [ {from: 'a', to: 'b'} @@ -295,7 +295,7 @@ describe('isValid', () => { expect(res).toBe(false); }); test('invalid right link', () => { - const res = placing({diagonals: true}).isValid({ + const res = placing().isValid({ 'a': {name: 'a', x: 0, y: 0}, 'b': {name: 'b', x: -2, y: 2} }, [ {from: 'a', to: 'b', direction: 'right'} @@ -303,7 +303,7 @@ describe('isValid', () => { expect(res).toBe(false); }); test('invalid left link', () => { - const res = placing({diagonals: true}).isValid({ + const res = placing().isValid({ 'a': {name: 'a', x: 0, y: 0}, 'b': {name: 'b', x: 2, y: 2} }, [ {from: 'a', to: 'b', direction: 'left'} @@ -311,7 +311,7 @@ describe('isValid', () => { expect(res).toBe(false); }); test('invalid up link', () => { - const res = placing({diagonals: true}).isValid({ + const res = placing().isValid({ 'a': {name: 'a', x: 0, y: 0}, 'b': {name: 'b', x: 2, y: 2} }, [ {from: 'a', to: 'b', direction: 'up'} @@ -319,7 +319,7 @@ describe('isValid', () => { expect(res).toBe(false); }); test('invalid down link', () => { - const res = placing({diagonals: true}).isValid({ + const res = placing().isValid({ 'a': {name: 'a', x: 0, y: 0}, 'b': {name: 'b', x: 2, y: -2} }, [ {from: 'a', to: 'b', direction: 'down'} @@ -327,7 +327,7 @@ describe('isValid', () => { expect(res).toBe(false); }); test('valid right link', () => { - const res = placing({diagonals: true}).isValid({ + const res = placing().isValid({ 'a': {name: 'a', x: 0, y: 0}, 'b': {name: 'b', x: 2, y: 2} }, [ {from: 'a', to: 'b', direction: 'right'} @@ -335,7 +335,7 @@ describe('isValid', () => { expect(res).toBe(true); }); test('valid left link', () => { - const res = placing({diagonals: true}).isValid({ + const res = placing().isValid({ 'a': {name: 'a', x: 0, y: 0}, 'b': {name: 'b', x: -2, y: 2} }, [ {from: 'a', to: 'b', direction: 'left'} @@ -343,7 +343,7 @@ describe('isValid', () => { expect(res).toBe(true); }); test('valid up link', () => { - const res = placing({diagonals: true}).isValid({ + const res = placing().isValid({ 'a': {name: 'a', x: 0, y: 0}, 'b': {name: 'b', x: 2, y: -2} }, [ {from: 'a', to: 'b', direction: 'up'} @@ -351,7 +351,7 @@ describe('isValid', () => { expect(res).toBe(true); }); test('valid down link', () => { - const res = placing({diagonals: true}).isValid({ + const res = placing().isValid({ 'a': {name: 'a', x: 0, y: 0}, 'b': {name: 'b', x: 2, y: 2} }, [ {from: 'a', to: 'b', direction: 'down'} @@ -371,7 +371,7 @@ describe('isValid', () => { describe('correctPlacing', () => { test('no nodes', () => { const nodes = {}; - placing({}).correctPlacing(nodes); + placing().correctPlacing(nodes); expect(nodes).toEqual({}); }); test('several nodes', () => { @@ -380,7 +380,7 @@ describe('correctPlacing', () => { 'b': {x: -2, y: 5}, 'c': {x: -4, y: 3} }; - placing({}).correctPlacing(nodes); + placing().correctPlacing(nodes); expect(nodes).toEqual({ 'a': {x: 7, y: 0}, 'b': {x: 2, y: 3}, @@ -397,11 +397,11 @@ describe('compute', () => { }; test('no nodes', () => { - const nodes = placing({'max-link-length': 2, diagonals: false}).compute({}, []); + const nodes = placing({diagonals: false}).compute({}, []); expect(nodes).toEqual({}); }); test('3 nodes no link', () => { - const nodes = placing({'max-link-length': 2, diagonals: false}).compute(createNodes(3), []); + const nodes = placing({diagonals: false}).compute(createNodes(3), []); expect(nodes).toEqual({ '1': {name: '1', x: 0, y: 0}, '2': {name: '2', x: 1, y: 0}, @@ -409,7 +409,7 @@ describe('compute', () => { }); }); test('6 nodes 6 links with directions', () => { - const nodes = placing({'max-link-length': 2, diagonals: true}).compute(createNodes(6), [ + const nodes = placing().compute(createNodes(6), [ {from: '1', to: '2', direction: 'right'}, {from: '1', to: '3', direction: 'down'}, {from: '3', to: '4', direction: 'right'}, @@ -426,7 +426,7 @@ describe('compute', () => { }); }); test('6 nodes 6 links with directions no diagonals', () => { - const nodes = placing({'max-link-length': 2, diagonals: false}).compute(createNodes(6), [ + const nodes = placing({diagonals: false}).compute(createNodes(6), [ {from: '1', to: '2', direction: 'right'}, {from: '1', to: '3', direction: 'down'}, {from: '3', to: '4', direction: 'right'}, @@ -443,7 +443,7 @@ describe('compute', () => { }); }); test('6 nodes 6 links no directions', () => { - const nodes = placing({'max-link-length': 2, diagonals: true}).compute(createNodes(6), [ + const nodes = placing().compute(createNodes(6), [ {from: '1', to: '2'}, {from: '1', to: '3'}, {from: '3', to: '4'}, @@ -460,7 +460,7 @@ describe('compute', () => { }); }); test('6 nodes 6 links no directions no diagonals', () => { - const nodes = placing({'max-link-length': 2, diagonals: false}).compute(createNodes(6), [ + const nodes = placing({diagonals: false}).compute(createNodes(6), [ {from: '1', to: '2'}, {from: '1', to: '3'}, {from: '3', to: '4'}, @@ -477,7 +477,7 @@ describe('compute', () => { }); }); test('3 nodes impossible', () => { - const nodes = placing({'max-link-length': 2, diagonals: false}).compute(createNodes(3), [ + const nodes = placing({diagonals: false}).compute(createNodes(3), [ {from: '1', to: '2', direction: 'left'}, {from: '1', to: '3', direction: 'left'}, ]); diff --git a/test/rendering.test.js b/test/rendering.test.js index 739bb88..ec920ab 100644 --- a/test/rendering.test.js +++ b/test/rendering.test.js @@ -83,17 +83,17 @@ describe('getIcon', () => { describe('getBounds', () => { test('no nodes', () => { - const res = rendering({beautify: false}).getBounds({}); + const res = rendering().getBounds({}); expect(res).toEqual({w: 0, h: 0}); }); test('1 node', () => { - const res = rendering({beautify: false}).getBounds({ + const res = rendering().getBounds({ '1': {x: 0, y: 0} }); expect(res).toEqual({w: 1, h: 1}); }); test('3 nodes', () => { - const res = rendering({beautify: false}).getBounds({ + const res = rendering().getBounds({ '1': {x: 0, y: 0}, '2': {x: 5, y: 2}, '3': {x: 3, y: 8}, }); expect(res).toEqual({w: 6, h: 9}); @@ -102,11 +102,11 @@ describe('getBounds', () => { describe('toXML', () => { test('no data', () => { - const res = rendering({beautify: false, scale: 20, 'h-spacing': 1}).toXML({}, {w: 0, h: 0}); + const res = rendering({scale: 20, 'h-spacing': 1}).toXML({}, {w: 0, h: 0}); expect(res).toEqual(''); }); test('sample svg data', () => { - const res = rendering({beautify: false, scale: 2, 'h-spacing': 1}).toXML({ + const res = rendering({scale: 2, 'h-spacing': 1}).toXML({ 'circle': { '_attributes': { 'cx': 50, diff --git a/test/utils.test.js b/test/utils.test.js new file mode 100644 index 0000000..cd85da4 --- /dev/null +++ b/test/utils.test.js @@ -0,0 +1,67 @@ +/* jshint -W117 */ +const utils = require('../src/utils'); + +describe('merge', () => { + test('undefined', () => { + const res = utils.merge({'a': 1}, undefined); + expect(res).toEqual({'a': 1}); + }); + test('redefine', () => { + const res = utils.merge({'a': 1}, {'a': 2}); + expect(res).toEqual({'a': 2}); + }); + test('wrong type', () => { + const res = utils.merge({'a': 'hello'}, {'a': 2}); + expect(res).toEqual({'a': 'hello'}); + }); + test('array redefine', () => { + const res = utils.merge({'a': [1, 2, 3]}, {'a': [4, 5, 6]}); + expect(res).toEqual({'a': [4, 5, 6]}); + }); + test('sub object wrong type', () => { + const res = utils.merge({'a': {'b': 5}}, {'a': 5}); + expect(res).toEqual({'a': {'b': 5}}); + }); + test('sub object redefine', () => { + const res = utils.merge({'a': {'b': 5}}, {'a': {'b': 6}}); + expect(res).toEqual({'a': {'b': 6}}); + }); + test('add missing keys', () => { + const res = utils.merge({'a': 1, 'b': 3}, {'a': 2}); + expect(res).toEqual({'a': 2, 'b': 3}); + }); + test('extra keys ignore', () => { + const res = utils.merge({'a': 1}, {'a': 2, 'b': 3}); + expect(res).toEqual({'a': 2}); + }); +}); + +test('ezClone', () => { + const a = { + 'a': 5, + 'b': { + 'c': [1, 2, 3] + } + }; + const b = utils.ezClone(a); + expect(b).toEqual(a); + b.b.c[1] = 3; + expect(b).toEqual({ + 'a': 5, + 'b': { + 'c': [1, 3, 3] + } + }); + expect(a).toEqual({ + 'a': 5, + 'b': { + 'c': [1, 2, 3] + } + }); +}); + +test('newMap', () => { + expect(utils.newMap(2, 3, 3)).toEqual([ + [3, 3, 3], [3, 3, 3] + ]); +}); \ No newline at end of file