Skip to content

Commit d9045c1

Browse files
authored
extending transform with parent (#15)
1 parent ef5a395 commit d9045c1

5 files changed

Lines changed: 45 additions & 24 deletions

File tree

src/parser.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export function createFormatter(base: IFormatter) {
3939
return text.replace(formatRegEx, (...args: string[]) => {
4040
const prop = args[4]; // property name
4141
const filters = args[5]; // filters, if specified
42-
let {exists, value} = resolveProperty(prop, params);
42+
let {exists, value, parent} = resolveProperty(prop, params);
4343
if (!exists) {
4444
if (typeof base.getDefaultValue !== 'function') {
4545
throw new Error(`Property ${JSON.stringify(prop)} does not exist`);
@@ -61,7 +61,7 @@ export function createFormatter(base: IFormatter) {
6161
throw new Error(`Filter ${JSON.stringify(fName)} not recognized`);
6262
}
6363
const decodedArgs = typeof f.decodeArguments === 'function' ? f.decodeArguments(args) : args.map(a => decodeFilterArg(a));
64-
return f.transform(p, decodedArgs);
64+
return f.transform(p, decodedArgs, parent);
6565
}, value);
6666
}
6767
return base.format(value);

src/protocol.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,13 @@ export interface IFilter<T = any, R = any> {
1717
* By default, each argument is HTML-decoded, unless override
1818
* {@link decodeArguments} is implemented.
1919
*
20+
* @param parent
21+
* Container for the resolved property - value/property immediately preceding it (in the resolution chain).
22+
*
23+
* For example, for a simple property reference (not a nested one), the container is the formatting object itself.
24+
*
25+
* It is `undefined` when the property chain contains only `this`, because in that case there is no container.
26+
*
2027
* @returns
2128
* Result of the value transformation.
2229
*
@@ -45,7 +52,7 @@ export interface IFilter<T = any, R = any> {
4552
*
4653
* // Both approaches will work the same as the example above.
4754
*/
48-
transform(value: T, args: string[]): R;
55+
transform(value: T, args: string[], parent: any): R;
4956

5057
/**
5158
* Optional override for decoding filter arguments.

src/resolver.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,16 @@ export interface IProperty {
99
exists: boolean;
1010

1111
/**
12-
* The resolved value, set only when 'exists' = true
12+
* The resolved value, set only when 'exists' = true.
1313
*/
1414
value?: any;
15+
16+
/**
17+
* Container for the resolved property - value/property immediately preceding it (in the resolution chain).
18+
*
19+
* It is set only when 'exists' = true.
20+
*/
21+
parent?: any;
1522
}
1623

1724
/**
@@ -21,7 +28,7 @@ export interface IProperty {
2128
*/
2229
export function resolveProperty(prop: string, obj: { [key: string]: any }): IProperty {
2330
const names = prop.split('.').filter(a => a);
24-
let exists = false, value = obj;
31+
let exists = false, value = obj, parent = undefined;
2532
for (const [i, n] of names.entries()) {
2633
if (!i && n === 'this') {
2734
exists = true;
@@ -31,7 +38,8 @@ export function resolveProperty(prop: string, obj: { [key: string]: any }): IPro
3138
return {exists: false};
3239
}
3340
exists = true;
41+
parent = value;
3442
value = value[n];
3543
}
36-
return exists ? {exists, value} : {exists};
44+
return exists ? {exists, value, parent} : {exists};
3745
}

test/parser.spec.ts

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -100,33 +100,37 @@ describe('createFormatter', () => {
100100
describe('for filter arguments', () => {
101101
it('must handle empty args', () => {
102102
const cb = jest.spyOn(fullFormatter.filters.json, 'transform');
103-
fullFormat('${value|json:}', {value: 'message'});
104-
expect(cb).toHaveBeenCalledWith('message', ['']);
105-
106-
fullFormat('${value|json:::Hello World!}', {value: 'message'});
107-
expect(cb).toHaveBeenCalledWith('message', ['', '', 'Hello World!']);
103+
const obj = {value: 'message'};
104+
fullFormat('${value|json:}', obj);
105+
expect(cb).toHaveBeenCalledWith('message', [''], obj);
106+
fullFormat('${value|json:::Hello World!}', obj);
107+
expect(cb).toHaveBeenCalledWith('message', ['', '', 'Hello World!'], obj);
108108
});
109109
it('must resolve numbers', () => {
110110
const cb = jest.spyOn(fullFormatter.filters.json, 'transform');
111-
fullFormat('${value|json: 1: 22.33: -45.678}', {value: 'message'});
112-
expect(cb).toHaveBeenCalledWith('message', ['1', '22.33', '-45.678']);
111+
const obj = {value: 'message'};
112+
fullFormat('${value|json: 1: 22.33: -45.678}', obj);
113+
expect(cb).toHaveBeenCalledWith('message', ['1', '22.33', '-45.678'], obj);
113114
});
114115
it('must resolve random text with spaces', () => {
115116
const cb1 = jest.spyOn(fullFormatter.filters.json, 'transform');
116117
const cb2 = jest.spyOn(fullFormatter, 'getDefaultFilter');
117-
fullFormat('${value|object: Hello World! : Where are we? }', {value: 'message'});
118-
expect(cb1).toHaveBeenCalledWith('message', ['Hello World!', 'Where are we?']);
118+
const obj = {value: 'message'};
119+
fullFormat('${value|object: Hello World! : Where are we? }', obj);
120+
expect(cb1).toHaveBeenCalledWith('message', ['Hello World!', 'Where are we?'], obj);
119121
expect(cb2).toHaveBeenCalledWith('object', ['Hello World!', 'Where are we?']);
120122
});
121123
it('must decode HTML symbols by default', () => {
122124
const cb = jest.spyOn(fullFormatter.filters.json, 'transform');
123-
fullFormat('${value|json: &#58;}', {value: 'message'});
124-
expect(cb).toHaveBeenCalledWith('message', [':']);
125+
const obj = {value: 'message'};
126+
fullFormat('${value|json: &#58;}', obj);
127+
expect(cb).toHaveBeenCalledWith('message', [':'], obj);
125128
});
126129
it('must not decode HTML symbols with override present', () => {
127130
const cb = jest.spyOn(fullFormatter.filters.append, 'transform');
128-
fullFormat('${value|append: &#58;}', {value: 'message'});
129-
expect(cb).toHaveBeenCalledWith('message', ['&#58;']);
131+
const obj = {value: 'message'};
132+
fullFormat('${value|append: &#58;}', obj);
133+
expect(cb).toHaveBeenCalledWith('message', ['&#58;'], obj);
130134
});
131135
});
132136
});

test/resolver.spec.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@ import {resolveProperty} from '../src/resolver';
33
describe('resolveProperty', () => {
44
it('must resolve "this"', () => {
55
const obj = {prop: 123};
6-
expect(resolveProperty('this', obj)).toStrictEqual({exists: true, value: obj});
7-
expect(resolveProperty('this.prop', obj)).toStrictEqual({exists: true, value: obj.prop});
6+
expect(resolveProperty('this', obj)).toStrictEqual({exists: true, value: obj, parent: undefined});
7+
expect(resolveProperty('this.prop', obj)).toStrictEqual({exists: true, value: obj.prop, parent: obj});
88
});
99
it('must resolve deep properties', () => {
10-
const obj = {a: {b: {c: {d: 123}}}};
11-
expect(resolveProperty('a.b.c.d', obj)).toEqual({exists: true, value: 123});
10+
const parent = {d: 123};
11+
const obj = {a: {b: {c: parent}}};
12+
expect(resolveProperty('a.b.c.d', obj)).toEqual({exists: true, value: 123, parent});
1213
});
1314
it('it must handle missing properties', () => {
1415
const obj = {a: {b: null}};
@@ -20,7 +21,8 @@ describe('resolveProperty', () => {
2021
expect(resolveProperty('...', {})).toEqual({exists: false});
2122
});
2223
it('must handle array indexes', () => {
23-
expect(resolveProperty('a.1.value', {a: [0, {value: 123}]})).toEqual({exists: true, value: 123});
24+
const obj = {value: 123};
25+
expect(resolveProperty('a.1.value', {a: [0, obj]})).toEqual({exists: true, value: 123, parent: obj});
2426
});
2527
it('must handle array indexes outside range', () => {
2628
expect(resolveProperty('a.2', {a: [0, 1]})).toEqual({exists: false});

0 commit comments

Comments
 (0)