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:
parent
56a5ab31bf
commit
a17ad9c373
@ -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) => {
|
||||
// <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']);
|
||||
});
|
||||
|
||||
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 = `<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', () => {
|
||||
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 = `
|
||||
<ion-header>
|
||||
<ion-navbar color="brand">
|
||||
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user