Skip to content

Commit 619b3c5

Browse files
authored
fix(pipe-parser): add support for more sophisticated expressions that worked in version 4.2.0 with the regex based parser (#185)
* fix(pipe-parser): add support for more sophisticated expressions
1 parent 5e0da55 commit 619b3c5

File tree

2 files changed

+110
-16
lines changed

2 files changed

+110
-16
lines changed

src/parsers/pipe.parser.ts

Lines changed: 74 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,16 @@
1-
import { TmplAstNode, parseTemplate, BindingPipe, LiteralPrimitive, Conditional, TmplAstTextAttribute } from '@angular/compiler';
1+
import {
2+
AST,
3+
TmplAstNode,
4+
parseTemplate,
5+
BindingPipe,
6+
LiteralPrimitive,
7+
Conditional,
8+
TmplAstTextAttribute,
9+
Binary,
10+
LiteralMap,
11+
LiteralArray,
12+
Interpolation
13+
} from '@angular/compiler';
214

315
import { ParserInterface } from './parser.interface';
416
import { TranslationCollection } from '../utils/translation.collection';
@@ -36,9 +48,8 @@ export class PipeParser implements ParserInterface {
3648
);
3749
}
3850

39-
if (node?.value?.ast?.expressions) {
40-
const translateables = node.value.ast.expressions.filter((exp: any) => this.expressionIsOrHasBindingPipe(exp));
41-
ret.push(...translateables);
51+
if (node?.value?.ast) {
52+
ret.push(...this.getTranslatablesFromAst(node.value.ast));
4253
}
4354

4455
if (node?.attributes) {
@@ -51,17 +62,8 @@ export class PipeParser implements ParserInterface {
5162
if (node?.inputs) {
5263
node.inputs.forEach((input: any) => {
5364
// <element [attrib]="'identifier' | translate">
54-
if (input?.value?.ast && this.expressionIsOrHasBindingPipe(input.value.ast)) {
55-
ret.push(input.value.ast);
56-
}
57-
58-
// <element attrib="{{'identifier' | translate}}>"
59-
if (input?.value?.ast?.expressions) {
60-
input.value.ast.expressions.forEach((exp: BindingPipe) => {
61-
if (this.expressionIsOrHasBindingPipe(exp)) {
62-
ret.push(exp);
63-
}
64-
});
65+
if (input?.value?.ast) {
66+
ret.push(...this.getTranslatablesFromAst(input.value.ast));
6567
}
6668
});
6769
}
@@ -84,7 +86,63 @@ export class PipeParser implements ParserInterface {
8486
return ret;
8587
}
8688

87-
protected expressionIsOrHasBindingPipe(exp: any): boolean {
89+
protected getTranslatablesFromAst(ast: AST): BindingPipe[] {
90+
// the entire expression is the translate pipe, e.g.:
91+
// - 'foo' | translate
92+
// - (condition ? 'foo' : 'bar') | translate
93+
if (this.expressionIsOrHasBindingPipe(ast)) {
94+
return [ast];
95+
}
96+
97+
// angular double curly bracket interpolation, e.g.:
98+
// - {{ expressions }}
99+
if (ast instanceof Interpolation) {
100+
return this.getTranslatablesFromAsts(ast.expressions);
101+
}
102+
103+
// ternary operator, e.g.:
104+
// - condition ? null : ('foo' | translate)
105+
// - condition ? ('foo' | translate) : null
106+
if (ast instanceof Conditional) {
107+
return this.getTranslatablesFromAsts([ast.trueExp, ast.falseExp]);
108+
}
109+
110+
// string concatenation, e.g.:
111+
// - 'foo' + 'bar' + ('baz' | translate)
112+
if (ast instanceof Binary) {
113+
return this.getTranslatablesFromAsts([ast.left, ast.right]);
114+
}
115+
116+
// a pipe on the outer expression, but not the translate pipe - ignore the pipe, visit the expression, e.g.:
117+
// - { foo: 'Hello' | translate } | json
118+
if (ast instanceof BindingPipe) {
119+
return this.getTranslatablesFromAst(ast.exp);
120+
}
121+
122+
// object - ignore the keys, visit all values, e.g.:
123+
// - { key1: 'value1' | translate, key2: 'value2' | translate }
124+
if (ast instanceof LiteralMap) {
125+
return this.getTranslatablesFromAsts(ast.values);
126+
}
127+
128+
// array - visit all its values, e.g.:
129+
// - [ 'value1' | translate, 'value2' | translate ]
130+
if (ast instanceof LiteralArray) {
131+
return this.getTranslatablesFromAsts(ast.expressions);
132+
}
133+
134+
return [];
135+
}
136+
137+
protected getTranslatablesFromAsts(asts: AST[]): BindingPipe[] {
138+
return this.flatten(asts.map(ast => this.getTranslatablesFromAst(ast)));
139+
}
140+
141+
protected flatten<T extends AST>(array: T[][]): T[] {
142+
return [].concat(...array);
143+
}
144+
145+
protected expressionIsOrHasBindingPipe(exp: any): exp is BindingPipe {
88146
if (exp.name && exp.name === TRANSLATE_PIPE_NAME) {
89147
return true;
90148
}

tests/parsers/pipe.parser.spec.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,48 @@ describe('PipeParser', () => {
4747
expect(keys).to.deep.equal(['Hello', 'World']);
4848
});
4949

50+
it('should extract strings from ternary operators right expression', () => {
51+
const contents = `{{ condition ? null : ('World' | translate) }}`;
52+
const keys = parser.extract(contents, templateFilename).keys();
53+
expect(keys).to.deep.equal(['World']);
54+
});
55+
56+
it('should extract strings from ternary operators inside attribute bindings', () => {
57+
const contents = `<span [attr]="condition ? null : ('World' | translate)"></span>`;
58+
const keys = parser.extract(contents, templateFilename).keys();
59+
expect(keys).to.deep.equal(['World']);
60+
});
61+
62+
it('should extract strings from ternary operators left expression', () => {
63+
const contents = `{{ condition ? ('World' | translate) : null }}`;
64+
const keys = parser.extract(contents, templateFilename).keys();
65+
expect(keys).to.deep.equal(['World']);
66+
});
67+
68+
it('should extract strings inside string concatenation', () => {
69+
const contents = `{{ 'a' + ('Hello' | translate) + 'b' + 'c' + ('World' | translate) + 'd' }}`;
70+
const keys = parser.extract(contents, templateFilename).keys();
71+
expect(keys).to.deep.equal(['Hello', 'World']);
72+
});
73+
74+
it('should extract strings from object', () => {
75+
const contents = `{{ { foo: 'Hello' | translate, bar: ['World' | translate], deep: { nested: { baz: 'Yes' | translate } } } | json }}`;
76+
const keys = parser.extract(contents, templateFilename).keys();
77+
expect(keys).to.deep.equal(['Hello', 'World', 'Yes']);
78+
});
79+
5080
it('should extract strings from ternary operators inside attribute bindings', () => {
5181
const contents = `<span [attr]="(condition ? 'Hello' : 'World') | translate"></span>`;
5282
const keys = parser.extract(contents, templateFilename).keys();
5383
expect(keys).to.deep.equal(['Hello', 'World']);
5484
});
5585

86+
it('should extract strings from nested expressions', () => {
87+
const contents = `<span [attr]="{ foo: ['a' + ((condition ? 'Hello' : 'World') | translate) + 'b'] }"></span>`;
88+
const keys = parser.extract(contents, templateFilename).keys();
89+
expect(keys).to.deep.equal(['Hello', 'World']);
90+
});
91+
5692
it('should extract strings from nested ternary operators ', () => {
5793
const contents = `<h3>{{ (condition ? 'Hello' : anotherCondition ? 'Nested' : 'World' ) | translate }}</h3>`;
5894
const keys = parser.extract(contents, templateFilename).keys();

0 commit comments

Comments
 (0)