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
This commit is contained in:
		
				
					committed by
					
						 GitHub
						GitHub
					
				
			
			
				
	
			
			
			
						parent
						
							5e0da552b0
						
					
				
				
					commit
					619b3c56ea
				
			| @@ -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 { ParserInterface } from './parser.interface'; | ||||||
| import { TranslationCollection } from '../utils/translation.collection'; | import { TranslationCollection } from '../utils/translation.collection'; | ||||||
| @@ -36,9 +48,8 @@ export class PipeParser implements ParserInterface { | |||||||
| 			); | 			); | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		if (node?.value?.ast?.expressions) { | 		if (node?.value?.ast) { | ||||||
| 			const translateables = node.value.ast.expressions.filter((exp: any) => this.expressionIsOrHasBindingPipe(exp)); | 			ret.push(...this.getTranslatablesFromAst(node.value.ast)); | ||||||
| 			ret.push(...translateables); |  | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		if (node?.attributes) { | 		if (node?.attributes) { | ||||||
| @@ -51,17 +62,8 @@ export class PipeParser implements ParserInterface { | |||||||
| 		if (node?.inputs) { | 		if (node?.inputs) { | ||||||
| 			node.inputs.forEach((input: any) => { | 			node.inputs.forEach((input: any) => { | ||||||
| 				// <element [attrib]="'identifier' | translate"> | 				// <element [attrib]="'identifier' | translate"> | ||||||
| 				if (input?.value?.ast && this.expressionIsOrHasBindingPipe(input.value.ast)) { | 				if (input?.value?.ast) { | ||||||
| 					ret.push(input.value.ast); | 					ret.push(...this.getTranslatablesFromAst(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); |  | ||||||
| 						} |  | ||||||
| 					}); |  | ||||||
| 				} | 				} | ||||||
| 			}); | 			}); | ||||||
| 		} | 		} | ||||||
| @@ -84,7 +86,63 @@ export class PipeParser implements ParserInterface { | |||||||
| 		return ret; | 		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) { | 		if (exp.name && exp.name === TRANSLATE_PIPE_NAME) { | ||||||
| 			return true; | 			return true; | ||||||
| 		} | 		} | ||||||
|   | |||||||
| @@ -47,12 +47,48 @@ describe('PipeParser', () => { | |||||||
| 		expect(keys).to.deep.equal(['Hello', 'World']); | 		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', () => { | 	it('should extract strings from ternary operators inside attribute bindings', () => { | ||||||
| 		const contents = `<span [attr]="(condition ? 'Hello' : 'World') | translate"></span>`; | 		const contents = `<span [attr]="(condition ? 'Hello' : 'World') | translate"></span>`; | ||||||
| 		const keys = parser.extract(contents, templateFilename).keys(); | 		const keys = parser.extract(contents, templateFilename).keys(); | ||||||
| 		expect(keys).to.deep.equal(['Hello', 'World']); | 		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 ', () => { | 	it('should extract strings from nested ternary operators ', () => { | ||||||
| 		const contents = `<h3>{{ (condition ? 'Hello' : anotherCondition ? 'Nested' : 'World' ) | translate }}</h3>`; | 		const contents = `<h3>{{ (condition ? 'Hello' : anotherCondition ? 'Nested' : 'World' ) | translate }}</h3>`; | ||||||
| 		const keys = parser.extract(contents, templateFilename).keys(); | 		const keys = parser.extract(contents, templateFilename).keys(); | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user