Skip to content

Commit

Permalink
ensure compatibility with Handlebars.SafeString and mimic old empty-s…
Browse files Browse the repository at this point in the history
…tring-key behavior
  • Loading branch information
bc-evan-johnson committed Jun 20, 2022
1 parent afed08a commit ffe758c
Show file tree
Hide file tree
Showing 8 changed files with 93 additions and 38 deletions.
4 changes: 2 additions & 2 deletions helpers/get.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const { getValue } = require('./lib/common');
* Get a value from the given context object. Property paths (`a.b.c`) may be used
* to get nested properties.
*/
const factory = () => {
const factory = (globals) => {
return function (path, context) {
let options = arguments[arguments.length - 1];

Expand All @@ -17,7 +17,7 @@ const factory = () => {
context = {};
}

let value = getValue(context, path);
let value = getValue(globals, context, path);
if (options && options.fn) {
return value ? options.fn(value) : options.inverse(context);
}
Expand Down
7 changes: 4 additions & 3 deletions helpers/getObject.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const { getValue } = require('./lib/common');
* Get an object or array containing a value from the given context object.
* Property paths (`a.b.c`) may be used to get nested properties.
*/
const factory = () => {
const factory = (globals) => {
return function (path, context) {
// use an empty context if none was given
// (expect 3 args: `path`, `context`, and the `options` object
Expand All @@ -25,7 +25,9 @@ const factory = () => {

path = String(path);

let value = getValue(context, path);
const parts = path.split(/[[.\]]/).filter(Boolean);

let value = getValue(globals, context, parts);

// for backwards compatibility: `get-object` returns on empty object instead of
// getting props with 'false' values (not just undefined)
Expand All @@ -34,7 +36,6 @@ const factory = () => {
}

// return an array if the final path part is numeric to mimic behavior of `get-object`
const parts = path.split(/[[.\]]/).filter(Boolean);
const last = parts[parts.length - 1];
if (Number.isFinite(Number(last))) {
return [ value ];
Expand Down
24 changes: 15 additions & 9 deletions helpers/lib/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,25 @@ function isValidURL(val) {
}

/*
* Based on https://github.com/jonschlinkert/get-value/blob/3.0.1/index.js, but
* with configurability that was not used in handlebars-helpers removed.
* Based on https://github.com/jonschlinkert/get-value/blob/2.0.6/index.js with some enhancements.
*
* - Performs "hasOwnProperty" checks for safety.
* - Now accepts Handlebars.SafeString paths.
*/
function getValue(object, path) {
function getValue(globals, object, path) {
let parts;

// unwrap Handlebars.SafeString for compatibility with `concat` etc.
path = unwrapIfSafeString(globals.handlebars, path);

// accept array or string for backwards compatibility
if (!Array.isArray(path)) {
if (typeof path !== 'string') {
return object;
}
parts = path.split(/[[.\]]/).filter(Boolean);
if (typeof path === 'string') {
parts = path.split('.');
} else if (Array.isArray(path)) {
parts = path;
} else {
parts = path.map(v => String(v));
let key = String(path);
return Object.prototype.hasOwnProperty.call(object, key) ? object[key] : undefined;
}

let result = object;
Expand Down
4 changes: 2 additions & 2 deletions helpers/option.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const { getValue } = require('./lib/common');
* Get a value from the options object. Property paths (`a.b.c`) may be used
* to get nested properties.
*/
const factory = () => {
const factory = (globals) => {
return function (path, locals) {
// preserve `option` behavior with missing args while ensuring the correct
// options object is used
Expand All @@ -23,7 +23,7 @@ const factory = () => {

let opts = util.options(this, locals, options);

return getValue(opts, path);
return getValue(globals, opts, path);
};
};

Expand Down
17 changes: 16 additions & 1 deletion spec/helpers/get.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ const Lab = require('lab'),
describe('get helper', function () {
const context = {
array: [1, 2, 3, 4, 5],
options: { a: { b: { c: 'd' } } }
options: { a: { b: { c: 'd' } } },
aa: 'a',
ab: 'b',
};

const runTestCases = testRunner({ context });
Expand All @@ -30,4 +32,17 @@ describe('get helper', function () {
}
], done);
});

it('accepts SafeString paths', (done) => {
runTestCases([
{
input: `{{get (concat 'a' 'a') this}}`,
output: `a`,
},
{
input: `{{get (concat 'a' 'b') this}}`,
output: `b`,
}
], done);
});
});
17 changes: 16 additions & 1 deletion spec/helpers/getObject.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ describe('getObject helper', function () {
c: 'd'
},
array: [1, 2, 3, 4, 5]
}
},
aa: 'a',
ab: 'b',
}
};

Expand Down Expand Up @@ -40,4 +42,17 @@ describe('getObject helper', function () {
},
], done);
});

it('accepts SafeString paths', (done) => {
runTestCases([
{
input: `{{get 'aa' (getObject (concat 'a' 'a') obj)}}`,
output: `a`,
},
{
input: `{{get 'ab' (getObject (concat 'a' 'b') obj)}}`,
output: `b`,
}
], done);
});
});
49 changes: 29 additions & 20 deletions spec/helpers/lib/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ const Lab = require('lab'),
lab = exports.lab = Lab.script(),
describe = lab.experiment,
it = lab.it;
const Handlebars = require('handlebars');

describe('common utils', function () {
describe('getValue', function () {
const globals = {handlebars: Handlebars};
const obj = {
a: {
a: [{x: 'a'}, {y: 'b'}],
Expand All @@ -21,63 +23,70 @@ describe('common utils', function () {
b: [2, 3, 5, 7, 11, 13, 17, 19],
c: 3,
d: false,
'42': 42,
};
obj.__proto__ = {x: 'yz'};

it('should get a value from an object', (done) => {
expect(getValue(obj, 'c')).to.equal(3);
expect(getValue(obj, 'd')).to.equal(false);
expect(getValue(globals, obj, 'c')).to.equal(3);
expect(getValue(globals, obj, 'd')).to.equal(false);
done();
});

it('should get nested values', (done) => {
expect(getValue(obj, 'a.b.b.a')).to.equal(1);
expect(getValue(obj, ['a', 'b', 'b', 'a'])).to.equal(1);
expect(getValue(globals, obj, 'a.b.b.a')).to.equal(1);
expect(getValue(globals, obj, ['a', 'b', 'b', 'a'])).to.equal(1);
done();
});

it('should get nested values from arrays', (done) => {
expect(getValue(obj, 'b.0')).to.equal(2);
expect(getValue(obj, 'a.c.5')).to.equal(8);
expect(getValue(globals, obj, 'b.0')).to.equal(2);
expect(getValue(globals, obj, 'a.c.5')).to.equal(8);
done();
});

it('should get nested values from objects in arrays', (done) => {
expect(getValue(obj, 'a.a.1.y')).to.equal('b');
expect(getValue(globals, obj, 'a.a.1.y')).to.equal('b');
done();
});

it('should return the whole object if path is not a string or array', (done) => {
expect(getValue(obj, {})).to.equal(obj);
expect(getValue(obj)).to.equal(obj);
it('should return obj[String(path)] or undefined if path is not a string or array', (done) => {
expect(getValue(globals, obj, {})).to.equal(undefined);
expect(getValue(globals, obj)).to.equal(undefined);
expect(getValue(globals, obj, 42)).to.equal(42);
done();
});

it('should return the whole object if path is empty', (done) => {
expect(getValue(obj, '')).to.equal(obj);
expect(getValue(obj, [])).to.equal(obj);
expect(getValue(globals, obj, [])).to.equal(obj);
done();
});

it('should get empty string key if path is \'\'', (done) => {
expect(getValue(globals, obj, '')).to.equal(undefined);
expect(getValue(globals, {'': 0}, '')).to.equal(0);
done();
});

it('should return undefined if prop does not exist', (done) => {
expect(getValue(obj, 'a.a.a.a')).to.equal(undefined);
expect(getValue(obj, 'a.c.23')).to.equal(undefined);
expect(getValue(obj, 'ab')).to.equal(undefined);
expect(getValue(obj, 'nonexistent')).to.equal(undefined);
expect(getValue(globals, obj, 'a.a.a.a')).to.equal(undefined);
expect(getValue(globals, obj, 'a.c.23')).to.equal(undefined);
expect(getValue(globals, obj, 'ab')).to.equal(undefined);
expect(getValue(globals, obj, 'nonexistent')).to.equal(undefined);
done();
});

it('should treat backslash-escaped . characters as part of a prop name', (done) => {
const data = {'a.b': {'c.d.e': 42, z: 'xyz'}};

expect(getValue(data, 'a\\.b.z')).to.equal('xyz');
expect(getValue(data, 'a\\.b.c\\.d\\.e')).to.equal(42);
expect(getValue(globals, data, 'a\\.b.z')).to.equal('xyz');
expect(getValue(globals, data, 'a\\.b.c\\.d\\.e')).to.equal(42);
done()
});

it('should not access inherited props', (done) => {
expect(getValue(obj, 'x')).to.equal(undefined);
expect(getValue(obj, 'a.constructor')).to.equal(undefined);
expect(getValue(globals, obj, 'x')).to.equal(undefined);
expect(getValue(globals, obj, 'a.constructor')).to.equal(undefined);
done();
});
});
Expand Down
9 changes: 9 additions & 0 deletions spec/helpers/option.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,13 @@ describe('option helper', function () {
},
], done);
});

it('accepts SafeString paths', (done) => {
runTestCases([
{
input: `{{option (concat 'a.b.' 'c')}}`,
output: `d`,
},
], done);
});
});

0 comments on commit ffe758c

Please sign in to comment.