Parse Pipes with Angular Compiler AST, enable ternary operator parsing (#159)
(feature) Use AST-based approach to translate pipe parsing. Also enables parsing translate pipes from any position in a pipe chain. Fixes #111, Fixes #154. (Thanks @TekSiDoT)
This commit is contained in:
		| @@ -1,6 +1,7 @@ | |||||||
| import { ParserInterface } from './parser.interface'; | import { ParserInterface } from './parser.interface'; | ||||||
| import { TranslationCollection } from '../utils/translation.collection'; | import { TranslationCollection } from '../utils/translation.collection'; | ||||||
| import { isPathAngularComponent, extractComponentInlineTemplate } from '../utils/utils'; | import { isPathAngularComponent, extractComponentInlineTemplate } from '../utils/utils'; | ||||||
|  | import { TmplAstNode, parseTemplate, BindingPipe, LiteralPrimitive, Conditional, TmplAstTextAttribute } from '@angular/compiler'; | ||||||
|  |  | ||||||
| export class PipeParser implements ParserInterface { | export class PipeParser implements ParserInterface { | ||||||
| 	public extract(source: string, filePath: string): TranslationCollection | null { | 	public extract(source: string, filePath: string): TranslationCollection | null { | ||||||
| @@ -8,18 +9,89 @@ export class PipeParser implements ParserInterface { | |||||||
| 			source = extractComponentInlineTemplate(source); | 			source = extractComponentInlineTemplate(source); | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		return this.parseTemplate(source); |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	protected parseTemplate(template: string): TranslationCollection { |  | ||||||
| 		let collection: TranslationCollection = new TranslationCollection(); | 		let collection: TranslationCollection = new TranslationCollection(); | ||||||
|  | 		const nodes: TmplAstNode[] = this.parseTemplate(source, filePath); | ||||||
| 		const regExp: RegExp = /(['"`])((?:(?!\1).|\\\1)+)\1\s*\|\s*translate/g; | 		const pipes: BindingPipe[] = nodes.map(node => this.findPipesInNode(node)).flat(); | ||||||
| 		let matches: RegExpExecArray; | 		pipes.forEach(pipe => { | ||||||
| 		while ((matches = regExp.exec(template))) { | 			this.parseTranslationKeysFromPipe(pipe).forEach((key: string) => { | ||||||
| 			collection = collection.add(matches[2].split("\\'").join("'")); | 				collection = collection.add(key); | ||||||
| 		} | 			}); | ||||||
|  | 		}); | ||||||
| 		return collection; | 		return collection; | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	protected findPipesInNode(node: any): BindingPipe[] { | ||||||
|  | 		let ret: BindingPipe[] = []; | ||||||
|  |  | ||||||
|  | 		if (node.children) { | ||||||
|  | 			ret = node.children.reduce( | ||||||
|  | 				(result: BindingPipe[], childNode: TmplAstNode) => { | ||||||
|  | 					const children = this.findPipesInNode(childNode); | ||||||
|  | 					return result.concat(children); | ||||||
|  | 				}, | ||||||
|  | 				[ret] | ||||||
|  | 			); | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if (node.value && node.value.ast && node.value.ast.expressions) { | ||||||
|  | 			const translateables = node.value.ast.expressions.filter((exp: any) => this.expressionIsOrHasBindingPipe(exp)); | ||||||
|  | 			ret.push(...translateables); | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if (node.attributes) { | ||||||
|  | 			const translateableAttributes = node.attributes.filter((attr: TmplAstTextAttribute) => { | ||||||
|  | 				return attr.name === 'translate'; | ||||||
|  | 			}); | ||||||
|  | 			ret = [...ret, ...translateableAttributes]; | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if (node.inputs) { | ||||||
|  | 			node.inputs.forEach((input: any) => { | ||||||
|  | 				// <element [attrib]="'identifier' | translate"> | ||||||
|  | 				if (input.value && input.value.ast && this.expressionIsOrHasBindingPipe(input.value.ast)) { | ||||||
|  | 					ret.push(input.value.ast); | ||||||
|  | 				} | ||||||
|  |  | ||||||
|  | 				// <element attrib="{{'identifier' | translate}}>" | ||||||
|  | 				if (input.value && input.value.ast && input.value.ast.expressions) { | ||||||
|  | 					input.value.ast.expressions.forEach((exp: BindingPipe) => { | ||||||
|  | 						if (this.expressionIsOrHasBindingPipe(exp)) { | ||||||
|  | 							ret.push(exp); | ||||||
|  | 						} | ||||||
|  | 					}); | ||||||
|  | 				} | ||||||
|  | 			}); | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		return ret; | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	protected parseTranslationKeysFromPipe(pipeContent: BindingPipe | LiteralPrimitive | Conditional): string[] { | ||||||
|  | 		const ret: string[] = []; | ||||||
|  | 		if (pipeContent instanceof LiteralPrimitive) { | ||||||
|  | 			ret.push(pipeContent.value); | ||||||
|  | 		} else if (pipeContent instanceof Conditional) { | ||||||
|  | 			const trueExp: LiteralPrimitive | Conditional = pipeContent.trueExp as any; | ||||||
|  | 			ret.push(...this.parseTranslationKeysFromPipe(trueExp)); | ||||||
|  | 			const falseExp: LiteralPrimitive | Conditional = pipeContent.falseExp as any; | ||||||
|  | 			ret.push(...this.parseTranslationKeysFromPipe(falseExp)); | ||||||
|  | 		} else if (pipeContent instanceof BindingPipe) { | ||||||
|  | 			ret.push(...this.parseTranslationKeysFromPipe(pipeContent.exp as any)); | ||||||
|  | 		} | ||||||
|  | 		return ret; | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	protected expressionIsOrHasBindingPipe(exp: any): boolean { | ||||||
|  | 		if (exp.name && exp.name === 'translate') { | ||||||
|  | 			return true; | ||||||
|  | 		} | ||||||
|  | 		if (exp.exp && exp.exp instanceof BindingPipe) { | ||||||
|  | 			return this.expressionIsOrHasBindingPipe(exp.exp); | ||||||
|  | 		} | ||||||
|  | 		return false; | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	protected parseTemplate(template: string, path: string): TmplAstNode[] { | ||||||
|  | 		return parseTemplate(template, path).nodes; | ||||||
|  | 	} | ||||||
| } | } | ||||||
|   | |||||||
| @@ -29,12 +29,42 @@ describe('PipeParser', () => { | |||||||
| 		expect(keys).to.deep.equal(['World']); | 		expect(keys).to.deep.equal(['World']); | ||||||
| 	}); | 	}); | ||||||
|  |  | ||||||
| 	it('should extract interpolated strings when translate pipe is used in conjunction with other pipes', () => { | 	it('should extract interpolated strings when translate pipe is used before other pipes', () => { | ||||||
| 		const contents = `Hello {{ 'World' | translate | upper }}`; | 		const contents = `Hello {{ 'World' | translate | upper }}`; | ||||||
| 		const keys = parser.extract(contents, templateFilename).keys(); | 		const keys = parser.extract(contents, templateFilename).keys(); | ||||||
| 		expect(keys).to.deep.equal(['World']); | 		expect(keys).to.deep.equal(['World']); | ||||||
| 	}); | 	}); | ||||||
|  |  | ||||||
|  | 	it('should extract interpolated strings when translate pipe is used after other pipes', () => { | ||||||
|  | 		const contents = `Hello {{ 'World'  | upper | translate }}`; | ||||||
|  | 		const keys = parser.extract(contents, templateFilename).keys(); | ||||||
|  | 		expect(keys).to.deep.equal(['World']); | ||||||
|  | 	}); | ||||||
|  |  | ||||||
|  | 	it('should extract strings from ternary operators inside interpolations', () => { | ||||||
|  | 		const contents = `{{ (condition ? 'Hello' : 'World') | translate }}`; | ||||||
|  | 		const keys = parser.extract(contents, templateFilename).keys(); | ||||||
|  | 		expect(keys).to.deep.equal(['Hello', 'World']); | ||||||
|  | 	}); | ||||||
|  |  | ||||||
|  | 	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 ternary operators ', () => { | ||||||
|  | 		const contents = `<h3>{{ (condition ? 'Hello' : anotherCondition ? 'Nested' : 'World' ) | translate }}</h3>`; | ||||||
|  | 		const keys = parser.extract(contents, templateFilename).keys(); | ||||||
|  | 		expect(keys).to.deep.equal(['Hello', 'Nested', 'World']); | ||||||
|  | 	}); | ||||||
|  |  | ||||||
|  | 	it('should extract strings from ternary operators inside attribute interpolations', () => { | ||||||
|  | 		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 with escaped quotes', () => { | 	it('should extract strings with escaped quotes', () => { | ||||||
| 		const contents = `Hello {{ 'World\\'s largest potato' | translate }}`; | 		const contents = `Hello {{ 'World\\'s largest potato' | translate }}`; | ||||||
| 		const keys = parser.extract(contents, templateFilename).keys(); | 		const keys = parser.extract(contents, templateFilename).keys(); | ||||||
| @@ -59,7 +89,7 @@ describe('PipeParser', () => { | |||||||
| 		expect(keys).to.deep.equal(['Hello World']); | 		expect(keys).to.deep.equal(['Hello World']); | ||||||
| 	}); | 	}); | ||||||
|  |  | ||||||
| 	it('should not use a greedy regular expression', () => { | 	it('should extract multiple entries from nodes', () => { | ||||||
| 		const contents = ` | 		const contents = ` | ||||||
| 			<ion-header> | 			<ion-header> | ||||||
| 				<ion-navbar color="brand"> | 				<ion-navbar color="brand"> | ||||||
|   | |||||||
| @@ -11,7 +11,7 @@ | |||||||
|         "target": "es2015", |         "target": "es2015", | ||||||
|         "lib": [ |         "lib": [ | ||||||
|             "dom", |             "dom", | ||||||
|             "es2018" |             "esnext.array" | ||||||
|         ], |         ], | ||||||
|         "module": "commonjs", |         "module": "commonjs", | ||||||
|         "outDir": "./dist/", |         "outDir": "./dist/", | ||||||
| @@ -25,7 +25,6 @@ | |||||||
|         "tests/**/*.ts" |         "tests/**/*.ts" | ||||||
|     ], |     ], | ||||||
|     "exclude": [ |     "exclude": [ | ||||||
|         "node_modules", |         "node_modules" | ||||||
|         "tests/**/*.ts" |  | ||||||
|     ] |     ] | ||||||
| } | } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user