Compare commits
	
		
			4 Commits
		
	
	
		
			v7.0.0
			...
			refactor-p
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 0d74fb4f55 | ||
|  | b928ca7555 | ||
|  | c60705d5fa | ||
|  | 85cd1e4a46 | 
| @@ -74,7 +74,7 @@ export class ExtractTask implements TaskInterface { | ||||
| 			try { | ||||
| 				let event = 'CREATED'; | ||||
| 				if (fs.existsSync(outputPath)) { | ||||
| 					this.options.replace ? event = 'REPLACED' : event = 'MERGED'; | ||||
| 					this.options.replace ? (event = 'REPLACED') : (event = 'MERGED'); | ||||
| 				} | ||||
| 				this.save(outputPath, final); | ||||
| 				this.out(`%s %s`, dim(`- ${outputPath}`), green(`[${event}]`)); | ||||
|   | ||||
| @@ -3,7 +3,18 @@ import { | ||||
| 	TmplAstNode as Node, | ||||
| 	TmplAstElement as Element, | ||||
| 	TmplAstText as Text, | ||||
| 	TmplAstTemplate as Template | ||||
| 	TmplAstTemplate as Template, | ||||
| 	TmplAstTextAttribute as TextAttribute, | ||||
| 	TmplAstBoundAttribute as BoundAttribute, | ||||
| 	AST, | ||||
| 	ASTWithSource, | ||||
| 	LiteralPrimitive, | ||||
| 	Conditional, | ||||
| 	Binary, | ||||
| 	BindingPipe, | ||||
| 	Interpolation, | ||||
| 	LiteralArray, | ||||
| 	LiteralMap | ||||
| } from '@angular/compiler'; | ||||
|  | ||||
| import { ParserInterface } from './parser.interface'; | ||||
| @@ -24,13 +35,22 @@ export class DirectiveParser implements ParserInterface { | ||||
| 		const elements: ElementLike[] = this.getElementsWithTranslateAttribute(nodes); | ||||
|  | ||||
| 		elements.forEach((element) => { | ||||
| 			const attr = this.getAttribute(element, TRANSLATE_ATTR_NAME); | ||||
| 			if (attr) { | ||||
| 				collection = collection.add(attr); | ||||
| 			const attribute = this.getAttribute(element, TRANSLATE_ATTR_NAME); | ||||
| 			if (attribute?.value) { | ||||
| 				collection = collection.add(attribute.value); | ||||
| 				return; | ||||
| 			} | ||||
|  | ||||
| 			const boundAttribute = this.getBoundAttribute(element, TRANSLATE_ATTR_NAME); | ||||
| 			if (boundAttribute?.value) { | ||||
| 				this.getLiteralPrimitives(boundAttribute.value).forEach((literalPrimitive) => { | ||||
| 					collection = collection.add(literalPrimitive.value); | ||||
| 				}); | ||||
| 				return; | ||||
| 			} | ||||
|  | ||||
| 			const textNodes = this.getTextNodes(element); | ||||
| 			textNodes.forEach((textNode: Text) => { | ||||
| 			textNodes.forEach((textNode) => { | ||||
| 				collection = collection.add(textNode.value.trim()); | ||||
| 			}); | ||||
| 		}); | ||||
| @@ -43,16 +63,18 @@ export class DirectiveParser implements ParserInterface { | ||||
| 	 */ | ||||
| 	protected getElementsWithTranslateAttribute(nodes: Node[]): ElementLike[] { | ||||
| 		let elements: ElementLike[] = []; | ||||
| 		nodes.filter(this.isElementLike) | ||||
| 			.forEach((element) => { | ||||
| 				if (this.hasAttribute(element, TRANSLATE_ATTR_NAME)) { | ||||
| 					elements = [...elements, element]; | ||||
| 				} | ||||
| 				const childElements = this.getElementsWithTranslateAttribute(element.children); | ||||
| 				if (childElements.length) { | ||||
| 					elements = [...elements, ...childElements]; | ||||
| 				} | ||||
| 			}); | ||||
| 		nodes.filter(this.isElementLike).forEach((element) => { | ||||
| 			if (this.hasAttribute(element, TRANSLATE_ATTR_NAME)) { | ||||
| 				elements = [...elements, element]; | ||||
| 			} | ||||
| 			if (this.hasBoundAttribute(element, TRANSLATE_ATTR_NAME)) { | ||||
| 				elements = [...elements, element]; | ||||
| 			} | ||||
| 			const childElements = this.getElementsWithTranslateAttribute(element.children); | ||||
| 			if (childElements.length) { | ||||
| 				elements = [...elements, ...childElements]; | ||||
| 			} | ||||
| 		}); | ||||
| 		return elements; | ||||
| 	} | ||||
|  | ||||
| @@ -76,8 +98,59 @@ export class DirectiveParser implements ParserInterface { | ||||
| 	 * Get attribute value if present on element | ||||
| 	 * @param element | ||||
| 	 */ | ||||
| 	protected getAttribute(element: ElementLike, name: string): string | undefined { | ||||
| 		return element.attributes.find((attribute) => attribute.name === name)?.value; | ||||
| 	protected getAttribute(element: ElementLike, name: string): TextAttribute { | ||||
| 		return element.attributes.find((attribute) => attribute.name === name); | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * Check if bound attribute is present on element | ||||
| 	 * @param element | ||||
| 	 * @param name | ||||
| 	 */ | ||||
| 	protected hasBoundAttribute(element: ElementLike, name: string): boolean { | ||||
| 		return this.getBoundAttribute(element, name) !== undefined; | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * Get bound attribute if present on element | ||||
| 	 * @param element | ||||
| 	 * @param name | ||||
| 	 */ | ||||
| 	protected getBoundAttribute(element: ElementLike, name: string): BoundAttribute { | ||||
| 		return element.inputs.find((input) => input.name === name); | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * Get literal primitives from expression | ||||
| 	 * @param exp | ||||
| 	 */ | ||||
| 	protected getLiteralPrimitives(exp: AST): LiteralPrimitive[] { | ||||
| 		if (exp instanceof LiteralPrimitive) { | ||||
| 			return [exp]; | ||||
| 		} | ||||
|  | ||||
| 		let visit: AST[] = []; | ||||
| 		if (exp instanceof Interpolation) { | ||||
| 			visit = exp.expressions; | ||||
| 		} else if (exp instanceof LiteralArray) { | ||||
| 			visit = exp.expressions; | ||||
| 		} else if (exp instanceof LiteralMap) { | ||||
| 			visit = exp.values; | ||||
| 		} else if (exp instanceof BindingPipe) { | ||||
| 			visit = [exp.exp]; | ||||
| 		} else if (exp instanceof Conditional) { | ||||
| 			visit = [exp.trueExp, exp.falseExp]; | ||||
| 		} else if (exp instanceof Binary) { | ||||
| 			visit = [exp.left, exp.right]; | ||||
| 		} else if (exp instanceof ASTWithSource) { | ||||
| 			visit = [exp.ast]; | ||||
| 		} | ||||
|  | ||||
| 		let results: LiteralPrimitive[] = []; | ||||
| 		visit.forEach((child) => { | ||||
| 			results = [...results, ...this.getLiteralPrimitives(child)]; | ||||
| 		}); | ||||
| 		return results; | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| @@ -104,5 +177,4 @@ export class DirectiveParser implements ParserInterface { | ||||
| 	protected parseTemplate(template: string, path: string): Node[] { | ||||
| 		return parseTemplate(template, path).nodes; | ||||
| 	} | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -1,11 +1,16 @@ | ||||
| import { | ||||
| 	AST, | ||||
| 	TmplAstNode, | ||||
| 	ASTWithSource, | ||||
| 	TmplAstNode as Node, | ||||
| 	TmplAstBoundText as BoundText, | ||||
| 	TmplAstElement as Element, | ||||
| 	TmplAstTemplate as Template, | ||||
| 	TmplAstBoundAttribute as BoundAttribute, | ||||
| 	TmplAstBoundEvent as BoundEvent, | ||||
| 	parseTemplate, | ||||
| 	BindingPipe, | ||||
| 	LiteralPrimitive, | ||||
| 	Conditional, | ||||
| 	TmplAstTextAttribute, | ||||
| 	Binary, | ||||
| 	LiteralMap, | ||||
| 	LiteralArray, | ||||
| @@ -17,142 +22,99 @@ import { TranslationCollection } from '../utils/translation.collection'; | ||||
| import { isPathAngularComponent, extractComponentInlineTemplate } from '../utils/utils'; | ||||
|  | ||||
| const TRANSLATE_PIPE_NAME = 'translate'; | ||||
| type ElementLike = Element | Template; | ||||
|  | ||||
| export class PipeParser implements ParserInterface { | ||||
| 	public extract(source: string, filePath: string): TranslationCollection | null { | ||||
| 		let collection: TranslationCollection = new TranslationCollection(); | ||||
|  | ||||
| 		if (filePath && isPathAngularComponent(filePath)) { | ||||
| 			source = extractComponentInlineTemplate(source); | ||||
| 		} | ||||
| 		const nodes: Node[] = this.parseTemplate(source, filePath); | ||||
| 		const pipes = this.getBindingPipes(nodes, TRANSLATE_PIPE_NAME).filter((pipe) => !this.pipeHasConcatenatedString(pipe)); | ||||
|  | ||||
| 		let collection: TranslationCollection = new TranslationCollection(); | ||||
| 		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); | ||||
| 			this.visitEachChild(pipe, (child) => { | ||||
| 				if (child instanceof LiteralPrimitive) { | ||||
| 					collection = collection.add(child.value); | ||||
| 				} | ||||
| 			}); | ||||
| 		}); | ||||
|  | ||||
| 		return collection; | ||||
| 	} | ||||
|  | ||||
| 	protected findPipesInNode(node: any): BindingPipe[] { | ||||
| 		let ret: BindingPipe[] = []; | ||||
| 	protected getBindingPipes(nodes: any[], name: string): BindingPipe[] { | ||||
| 		let pipes: BindingPipe[] = []; | ||||
| 		nodes.forEach((node) => { | ||||
| 			if (this.isElementLike(node)) { | ||||
| 				pipes = [ | ||||
| 					...pipes, | ||||
| 					...this.getBindingPipes([ | ||||
| 						...node.inputs, | ||||
| 						...node.children | ||||
| 					], name) | ||||
| 				]; | ||||
| 			} | ||||
|  | ||||
| 		if (node?.children) { | ||||
| 			ret = node.children.reduce( | ||||
| 				(result: BindingPipe[], childNode: TmplAstNode) => { | ||||
| 					const children = this.findPipesInNode(childNode); | ||||
| 					return result.concat(children); | ||||
| 				}, | ||||
| 				[ret] | ||||
| 			); | ||||
| 		} | ||||
|  | ||||
| 		if (node?.value?.ast) { | ||||
| 			ret.push(...this.getTranslatablesFromAst(node.value.ast)); | ||||
| 		} | ||||
|  | ||||
| 		if (node?.attributes) { | ||||
| 			const translateableAttributes = node.attributes.filter((attr: TmplAstTextAttribute) => { | ||||
| 				return attr.name === TRANSLATE_PIPE_NAME; | ||||
| 			}); | ||||
| 			ret = [...ret, ...translateableAttributes]; | ||||
| 		} | ||||
|  | ||||
| 		if (node?.inputs) { | ||||
| 			node.inputs.forEach((input: any) => { | ||||
| 				// <element [attrib]="'identifier' | translate"> | ||||
| 				if (input?.value?.ast) { | ||||
| 					ret.push(...this.getTranslatablesFromAst(input.value.ast)); | ||||
| 			this.visitEachChild(node, (exp) => { | ||||
| 				if (exp instanceof BindingPipe && exp.name === name) { | ||||
| 					pipes = [...pipes, exp]; | ||||
| 				} | ||||
| 			}); | ||||
| 		} | ||||
|  | ||||
| 		return ret; | ||||
| 		}); | ||||
| 		return pipes; | ||||
| 	} | ||||
|  | ||||
| 	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)); | ||||
| 	protected visitEachChild(exp: AST, visitor: (child: AST) => void): void { | ||||
| 		visitor(exp); | ||||
|  | ||||
| 		let children: AST[] = []; | ||||
| 		if (exp instanceof BoundText) { | ||||
| 			children = [exp.value]; | ||||
| 		} else if (exp instanceof BoundAttribute) { | ||||
| 			children = [exp.value]; | ||||
| 		} else if (exp instanceof BoundEvent) { | ||||
| 			children = [exp.handler]; | ||||
| 		} else if (exp instanceof Interpolation) { | ||||
| 			children = exp.expressions; | ||||
| 		} else if (exp instanceof LiteralArray) { | ||||
| 			children = exp.expressions; | ||||
| 		} else if (exp instanceof LiteralMap) { | ||||
| 			children = exp.values; | ||||
| 		} else if (exp instanceof BindingPipe) { | ||||
| 			children = [exp.exp, ...exp.args]; | ||||
| 		} else if (exp instanceof Conditional) { | ||||
| 			children = [exp.trueExp, exp.falseExp]; | ||||
| 		} else if (exp instanceof Binary) { | ||||
| 			children = [exp.left, exp.right]; | ||||
| 		} else if (exp instanceof ASTWithSource) { | ||||
| 			children = [exp.ast]; | ||||
| 		} | ||||
| 		return ret; | ||||
|  | ||||
| 		children.forEach((child) => { | ||||
| 			this.visitEachChild(child, visitor); | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| 	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 []; | ||||
| 	/** | ||||
| 	 * Check if node type is ElementLike | ||||
| 	 * @param node | ||||
| 	 */ | ||||
| 	protected isElementLike(node: Node): node is ElementLike { | ||||
| 		return node instanceof Element || node instanceof Template; | ||||
| 	} | ||||
|  | ||||
| 	protected getTranslatablesFromAsts(asts: AST[]): BindingPipe[] { | ||||
| 		return this.flatten(asts.map(ast => this.getTranslatablesFromAst(ast))); | ||||
| 	/** | ||||
| 	 * Check if pipe concatenates string (in that case we don't want to extract it) | ||||
| 	 * @param pipe | ||||
| 	 */ | ||||
| 	protected pipeHasConcatenatedString(pipe: BindingPipe): boolean { | ||||
| 		return pipe?.exp instanceof Binary && pipe.exp.operation === '+'; | ||||
| 	} | ||||
|  | ||||
| 	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; | ||||
| 		} | ||||
| 		if (exp.exp && exp.exp instanceof BindingPipe) { | ||||
| 			return this.expressionIsOrHasBindingPipe(exp.exp); | ||||
| 		} | ||||
| 		return false; | ||||
| 	} | ||||
|  | ||||
| 	protected parseTemplate(template: string, path: string): TmplAstNode[] { | ||||
| 	protected parseTemplate(template: string, path: string): Node[] { | ||||
| 		return parseTemplate(template, path).nodes; | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -12,6 +12,55 @@ describe('DirectiveParser', () => { | ||||
| 		parser = new DirectiveParser(); | ||||
| 	}); | ||||
|  | ||||
|  | ||||
| 	it('should extract keys when using literal map in bound attribute', () => { | ||||
| 		const contents = `<div [translate]="{ key1: 'value1' | translate, key2: 'value2' | translate }"></div>`; | ||||
| 		const keys = parser.extract(contents, templateFilename).keys(); | ||||
| 		expect(keys).to.deep.equal(['value1', 'value2']); | ||||
| 	}); | ||||
|  | ||||
| 	it('should extract keys when using literal arrays in bound attribute', () => { | ||||
| 		const contents = `<div [translate]="[ 'value1' | translate, 'value2' | translate ]"></div>`; | ||||
| 		const keys = parser.extract(contents, templateFilename).keys(); | ||||
| 		expect(keys).to.deep.equal(['value1', 'value2']); | ||||
| 	}); | ||||
|  | ||||
| 	it('should extract keys when using binding pipe in bound attribute', () => { | ||||
| 		const contents = `<div [translate]="'KEY1' | withPipe"></div>`; | ||||
| 		const keys = parser.extract(contents, templateFilename).keys(); | ||||
| 		expect(keys).to.deep.equal(['KEY1']); | ||||
| 	}); | ||||
|  | ||||
| 	it('should extract keys when using binary expression in bound attribute', () => { | ||||
| 		const contents = `<div [translate]="keyVar || 'KEY1'"></div>`; | ||||
| 		const keys = parser.extract(contents, templateFilename).keys(); | ||||
| 		expect(keys).to.deep.equal(['KEY1']); | ||||
| 	}); | ||||
|  | ||||
| 	it('should extract keys when using literal primitive in bound attribute', () => { | ||||
| 		const contents = `<div [translate]="'KEY1'"></div>`; | ||||
| 		const keys = parser.extract(contents, templateFilename).keys(); | ||||
| 		expect(keys).to.deep.equal(['KEY1']); | ||||
| 	}); | ||||
|  | ||||
| 	it('should extract keys when using conditional in bound attribute', () => { | ||||
| 		const contents = `<div [translate]="condition ? 'KEY1' : 'KEY2'"></div>`; | ||||
| 		const keys = parser.extract(contents, templateFilename).keys(); | ||||
| 		expect(keys).to.deep.equal(['KEY1', 'KEY2']); | ||||
| 	}); | ||||
|  | ||||
| 	it('should extract keys when using nested conditionals in bound attribute', () => { | ||||
| 		const contents = `<div [translate]="isSunny ? (isWarm ? 'Sunny and warm' : 'Sunny but cold') : 'Not sunny'"></div>`; | ||||
| 		const keys = parser.extract(contents, templateFilename).keys(); | ||||
| 		expect(keys).to.deep.equal(['Sunny and warm', 'Sunny but cold', 'Not sunny']); | ||||
| 	}); | ||||
|  | ||||
| 	it('should extract keys when using interpolation', () => { | ||||
| 		const contents = `<div translate="{{ 'KEY1' + key2 + 'KEY3' }}"></div>`; | ||||
| 		const keys = parser.extract(contents, templateFilename).keys(); | ||||
| 		expect(keys).to.deep.equal(['KEY1', 'KEY3']); | ||||
| 	}); | ||||
|  | ||||
| 	it('should extract keys keeping proper whitespace', () => { | ||||
| 		const contents = ` | ||||
| 			<div translate> | ||||
| @@ -120,4 +169,5 @@ describe('DirectiveParser', () => { | ||||
| 		const keys = parser.extract(contents, templateFilename).keys(); | ||||
| 		expect(keys).to.deep.equal(['this is an example']); | ||||
| 	}); | ||||
|  | ||||
| }); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user