From a17ad9c37369fcbec2be7a5921b056e81487f671 Mon Sep 17 00:00:00 2001 From: Jens Habegger Date: Sun, 8 Mar 2020 09:54:44 +0100 Subject: [PATCH] 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) --- src/parsers/pipe.parser.ts | 94 +++++++++++++++++++++++++++---- tests/parsers/pipe.parser.spec.ts | 34 ++++++++++- tsconfig.json | 5 +- 3 files changed, 117 insertions(+), 16 deletions(-) diff --git a/src/parsers/pipe.parser.ts b/src/parsers/pipe.parser.ts index ce180d7..e894fec 100644 --- a/src/parsers/pipe.parser.ts +++ b/src/parsers/pipe.parser.ts @@ -1,6 +1,7 @@ import { ParserInterface } from './parser.interface'; import { TranslationCollection } from '../utils/translation.collection'; import { isPathAngularComponent, extractComponentInlineTemplate } from '../utils/utils'; +import { TmplAstNode, parseTemplate, BindingPipe, LiteralPrimitive, Conditional, TmplAstTextAttribute } from '@angular/compiler'; export class PipeParser implements ParserInterface { public extract(source: string, filePath: string): TranslationCollection | null { @@ -8,18 +9,89 @@ export class PipeParser implements ParserInterface { source = extractComponentInlineTemplate(source); } - return this.parseTemplate(source); - } - - protected parseTemplate(template: string): TranslationCollection { let collection: TranslationCollection = new TranslationCollection(); - - const regExp: RegExp = /(['"`])((?:(?!\1).|\\\1)+)\1\s*\|\s*translate/g; - let matches: RegExpExecArray; - while ((matches = regExp.exec(template))) { - collection = collection.add(matches[2].split("\\'").join("'")); - } - + const nodes: TmplAstNode[] = this.parseTemplate(source, filePath); + const pipes: BindingPipe[] = nodes.map(node => this.findPipesInNode(node)).flat(); + pipes.forEach(pipe => { + this.parseTranslationKeysFromPipe(pipe).forEach((key: string) => { + collection = collection.add(key); + }); + }); 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) => { + // + if (input.value && input.value.ast && this.expressionIsOrHasBindingPipe(input.value.ast)) { + ret.push(input.value.ast); + } + + // { + 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; + } } diff --git a/tests/parsers/pipe.parser.spec.ts b/tests/parsers/pipe.parser.spec.ts index 64a6ce5..c391a02 100644 --- a/tests/parsers/pipe.parser.spec.ts +++ b/tests/parsers/pipe.parser.spec.ts @@ -29,12 +29,42 @@ describe('PipeParser', () => { 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 keys = parser.extract(contents, templateFilename).keys(); 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 = ``; + const keys = parser.extract(contents, templateFilename).keys(); + expect(keys).to.deep.equal(['Hello', 'World']); + }); + + it('should extract strings from nested ternary operators ', () => { + const contents = `

{{ (condition ? 'Hello' : anotherCondition ? 'Nested' : 'World' ) | translate }}

`; + 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 = ``; + const keys = parser.extract(contents, templateFilename).keys(); + expect(keys).to.deep.equal(['Hello', 'World']); + }); + it('should extract strings with escaped quotes', () => { const contents = `Hello {{ 'World\\'s largest potato' | translate }}`; const keys = parser.extract(contents, templateFilename).keys(); @@ -59,7 +89,7 @@ describe('PipeParser', () => { expect(keys).to.deep.equal(['Hello World']); }); - it('should not use a greedy regular expression', () => { + it('should extract multiple entries from nodes', () => { const contents = ` diff --git a/tsconfig.json b/tsconfig.json index c8f3c43..563c83c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,7 +11,7 @@ "target": "es2015", "lib": [ "dom", - "es2018" + "esnext.array" ], "module": "commonjs", "outDir": "./dist/", @@ -25,7 +25,6 @@ "tests/**/*.ts" ], "exclude": [ - "node_modules", - "tests/**/*.ts" + "node_modules" ] }