From a4114e84d9f1f66c3e7fa76d5d1790fa64392f9a Mon Sep 17 00:00:00 2001 From: Jukka Kurkela Date: Tue, 26 Jul 2022 15:30:52 +0300 Subject: [PATCH] parsing: support dot(s) in object keys (#10517) --- docs/general/data-structures.md | 18 +++++++++- src/helpers/helpers.core.js | 57 +++++++++++++++++++++++--------- test/specs/helpers.core.tests.js | 38 +++++++++++++++++++++ 3 files changed, 97 insertions(+), 16 deletions(-) diff --git a/docs/general/data-structures.md b/docs/general/data-structures.md index 37d3e63df3e..117a7e38834 100644 --- a/docs/general/data-structures.md +++ b/docs/general/data-structures.md @@ -85,11 +85,27 @@ options: { } ``` +If the key contains a dot, it needs to be escaped with a double slash: + +```javascript +type: 'line', +data: { + datasets: [{ + data: [{ 'data.key': 'one', 'data.value': 20 }, { 'data.key': 'two', 'data.value': 30 }] + }] +}, +options: { + parsing: { + xAxisKey: 'data\\.key', + yAxisKey: 'data\\.value' + } +} +``` + :::warning When using object notation in a radar chart you still need a labels array with labels for the chart to show correctly. ::: - ## Object ```javascript diff --git a/src/helpers/helpers.core.js b/src/helpers/helpers.core.js index 00c143aa325..ce15bc63108 100644 --- a/src/helpers/helpers.core.js +++ b/src/helpers/helpers.core.js @@ -293,25 +293,52 @@ export function _deprecated(scope, value, previous, current) { } } -const emptyString = ''; -const dot = '.'; -function indexOfDotOrLength(key, start) { - const idx = key.indexOf(dot, start); - return idx === -1 ? key.length : idx; -} +// resolveObjectKey resolver cache +const keyResolvers = { + // Chart.helpers.core resolveObjectKey should resolve empty key to root object + '': v => v, + // default resolvers + x: o => o.x, + y: o => o.y +}; export function resolveObjectKey(obj, key) { - if (key === emptyString) { + const resolver = keyResolvers[key] || (keyResolvers[key] = _getKeyResolver(key)); + return resolver(obj); +} + +function _getKeyResolver(key) { + const keys = _splitKey(key); + return obj => { + for (const k of keys) { + if (k === '') { + // For backward compatibility: + // Chart.helpers.core resolveObjectKey should break at empty key + break; + } + obj = obj && obj[k]; + } return obj; + }; +} + +/** + * @private + */ +export function _splitKey(key) { + const parts = key.split('.'); + const keys = []; + let tmp = ''; + for (const part of parts) { + tmp += part; + if (tmp.endsWith('\\')) { + tmp = tmp.slice(0, -1) + '.'; + } else { + keys.push(tmp); + tmp = ''; + } } - let pos = 0; - let idx = indexOfDotOrLength(key, pos); - while (obj && idx > pos) { - obj = obj[key.slice(pos, idx)]; - pos = idx + 1; - idx = indexOfDotOrLength(key, pos); - } - return obj; + return keys; } /** diff --git a/test/specs/helpers.core.tests.js b/test/specs/helpers.core.tests.js index e3d1400b17e..c5c51434c88 100644 --- a/test/specs/helpers.core.tests.js +++ b/test/specs/helpers.core.tests.js @@ -456,6 +456,44 @@ describe('Chart.helpers.core', function() { expect(() => helpers.resolveObjectKey({}, true)).toThrow(); expect(() => helpers.resolveObjectKey({}, 1)).toThrow(); }); + it('should allow escaping dot symbol', function() { + expect(helpers.resolveObjectKey({'test.dot': 10}, 'test\\.dot')).toEqual(10); + expect(helpers.resolveObjectKey({test: {dot: 10}}, 'test\\.dot')).toEqual(undefined); + }); + it('should allow nested keys with a dot', function() { + expect(helpers.resolveObjectKey({ + a: { + 'bb.ccc': 'works', + bb: { + ccc: 'fails' + } + } + }, 'a.bb\\.ccc')).toEqual('works'); + }); + + }); + + describe('_splitKey', function() { + it('should return array with one entry for string without a dot', function() { + expect(helpers._splitKey('')).toEqual(['']); + expect(helpers._splitKey('test')).toEqual(['test']); + const asciiWithoutDot = ' !"#$%&\'()*+,-/0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~'; + expect(helpers._splitKey(asciiWithoutDot)).toEqual([asciiWithoutDot]); + }); + + it('should split on dot', function() { + expect(helpers._splitKey('test1.test2')).toEqual(['test1', 'test2']); + expect(helpers._splitKey('a.b.c')).toEqual(['a', 'b', 'c']); + expect(helpers._splitKey('a.b.')).toEqual(['a', 'b', '']); + expect(helpers._splitKey('a..c')).toEqual(['a', '', 'c']); + }); + + it('should preserve escaped dot', function() { + expect(helpers._splitKey('test1\\.test2')).toEqual(['test1.test2']); + expect(helpers._splitKey('a\\.b.c')).toEqual(['a.b', 'c']); + expect(helpers._splitKey('a.b\\.c')).toEqual(['a', 'b.c']); + expect(helpers._splitKey('a.\\.c')).toEqual(['a', '.c']); + }); }); describe('setsEqual', function() {