Skip to content

Commit

Permalink
fix(pipe-parser): add support for more sophisticated expressions that…
Browse files Browse the repository at this point in the history
… worked in version 4.2.0 with the regex based parser (#185)

* fix(pipe-parser): add support for more sophisticated expressions
  • Loading branch information
sod authored May 20, 2020
1 parent 5e0da55 commit 619b3c5
Show file tree
Hide file tree
Showing 2 changed files with 110 additions and 16 deletions.
90 changes: 74 additions & 16 deletions src/parsers/pipe.parser.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,16 @@
import { TmplAstNode, parseTemplate, BindingPipe, LiteralPrimitive, Conditional, TmplAstTextAttribute } from '@angular/compiler';
import {
AST,
TmplAstNode,
parseTemplate,
BindingPipe,
LiteralPrimitive,
Conditional,
TmplAstTextAttribute,
Binary,
LiteralMap,
LiteralArray,
Interpolation
} from '@angular/compiler';

import { ParserInterface } from './parser.interface';
import { TranslationCollection } from '../utils/translation.collection';
Expand Down Expand Up @@ -36,9 +48,8 @@ export class PipeParser implements ParserInterface {
);
}

if (node?.value?.ast?.expressions) {
const translateables = node.value.ast.expressions.filter((exp: any) => this.expressionIsOrHasBindingPipe(exp));
ret.push(...translateables);
if (node?.value?.ast) {
ret.push(...this.getTranslatablesFromAst(node.value.ast));
}

if (node?.attributes) {
Expand All @@ -51,17 +62,8 @@ export class PipeParser implements ParserInterface {
if (node?.inputs) {
node.inputs.forEach((input: any) => {
// <element [attrib]="'identifier' | translate">
if (input?.value?.ast && this.expressionIsOrHasBindingPipe(input.value.ast)) {
ret.push(input.value.ast);
}

// <element attrib="{{'identifier' | translate}}>"
if (input?.value?.ast?.expressions) {
input.value.ast.expressions.forEach((exp: BindingPipe) => {
if (this.expressionIsOrHasBindingPipe(exp)) {
ret.push(exp);
}
});
if (input?.value?.ast) {
ret.push(...this.getTranslatablesFromAst(input.value.ast));
}
});
}
Expand All @@ -84,7 +86,63 @@ export class PipeParser implements ParserInterface {
return ret;
}

protected expressionIsOrHasBindingPipe(exp: any): boolean {
protected getTranslatablesFromAst(ast: AST): BindingPipe[] {
// the entire expression is the translate pipe, e.g.:
// - 'foo' | translate
// - (condition ? 'foo' : 'bar') | translate
if (this.expressionIsOrHasBindingPipe(ast)) {
return [ast];
}

// angular double curly bracket interpolation, e.g.:
// - {{ expressions }}
if (ast instanceof Interpolation) {
return this.getTranslatablesFromAsts(ast.expressions);
}

// ternary operator, e.g.:
// - condition ? null : ('foo' | translate)
// - condition ? ('foo' | translate) : null
if (ast instanceof Conditional) {
return this.getTranslatablesFromAsts([ast.trueExp, ast.falseExp]);
}

// string concatenation, e.g.:
// - 'foo' + 'bar' + ('baz' | translate)
if (ast instanceof Binary) {
return this.getTranslatablesFromAsts([ast.left, ast.right]);
}

// a pipe on the outer expression, but not the translate pipe - ignore the pipe, visit the expression, e.g.:
// - { foo: 'Hello' | translate } | json
if (ast instanceof BindingPipe) {
return this.getTranslatablesFromAst(ast.exp);
}

// object - ignore the keys, visit all values, e.g.:
// - { key1: 'value1' | translate, key2: 'value2' | translate }
if (ast instanceof LiteralMap) {
return this.getTranslatablesFromAsts(ast.values);
}

// array - visit all its values, e.g.:
// - [ 'value1' | translate, 'value2' | translate ]
if (ast instanceof LiteralArray) {
return this.getTranslatablesFromAsts(ast.expressions);
}

return [];
}

protected getTranslatablesFromAsts(asts: AST[]): BindingPipe[] {
return this.flatten(asts.map(ast => this.getTranslatablesFromAst(ast)));
}

protected flatten<T extends AST>(array: T[][]): T[] {
return [].concat(...array);
}

protected expressionIsOrHasBindingPipe(exp: any): exp is BindingPipe {
if (exp.name && exp.name === TRANSLATE_PIPE_NAME) {
return true;
}
Expand Down
36 changes: 36 additions & 0 deletions tests/parsers/pipe.parser.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,48 @@ describe('PipeParser', () => {
expect(keys).to.deep.equal(['Hello', 'World']);
});

it('should extract strings from ternary operators right expression', () => {
const contents = `{{ condition ? null : ('World' | translate) }}`;
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['World']);
});

it('should extract strings from ternary operators inside attribute bindings', () => {
const contents = `<span [attr]="condition ? null : ('World' | translate)"></span>`;
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['World']);
});

it('should extract strings from ternary operators left expression', () => {
const contents = `{{ condition ? ('World' | translate) : null }}`;
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['World']);
});

it('should extract strings inside string concatenation', () => {
const contents = `{{ 'a' + ('Hello' | translate) + 'b' + 'c' + ('World' | translate) + 'd' }}`;
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['Hello', 'World']);
});

it('should extract strings from object', () => {
const contents = `{{ { foo: 'Hello' | translate, bar: ['World' | translate], deep: { nested: { baz: 'Yes' | translate } } } | json }}`;
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['Hello', 'World', 'Yes']);
});

it('should extract strings from ternary operators inside attribute bindings', () => {
const contents = `<span [attr]="(condition ? 'Hello' : 'World') | translate"></span>`;
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['Hello', 'World']);
});

it('should extract strings from nested expressions', () => {
const contents = `<span [attr]="{ foo: ['a' + ((condition ? 'Hello' : 'World') | translate) + 'b'] }"></span>`;
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['Hello', 'World']);
});

it('should extract strings from nested ternary operators ', () => {
const contents = `<h3>{{ (condition ? 'Hello' : anotherCondition ? 'Nested' : 'World' ) | translate }}</h3>`;
const keys = parser.extract(contents, templateFilename).keys();
Expand Down

0 comments on commit 619b3c5

Please sign in to comment.