diff --git a/package.json b/package.json index 6811328..d7d25b0 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,6 @@ "config": {}, "devDependencies": { "@types/chai": "4.1.7", - "@types/cheerio": "0.22.11", "@types/flat": "0.0.28", "@types/glob": "7.1.1", "@types/mkdirp": "0.5.2", @@ -58,7 +57,7 @@ "tslint-eslint-rules": "5.4.0" }, "dependencies": { - "cheerio": "1.0.0-rc.3", + "@angular/compiler": "^8.0.0", "colorette": "^1.0.8", "flat": "4.1.0", "fs": "0.0.1-security", diff --git a/src/cli/cli.ts b/src/cli/cli.ts index 545b9ad..384e4ac 100755 --- a/src/cli/cli.ts +++ b/src/cli/cli.ts @@ -103,8 +103,8 @@ const extractTask = new ExtractTask(cli.input, cli.output, { // Parsers const parsers: ParserInterface[] = [ new PipeParser(), - new DirectiveParser(), - new ServiceParser() + // new DirectiveParser() + // new ServiceParser() ]; if (cli.marker) { parsers.push(new FunctionParser({ diff --git a/src/parsers/directive-ast.parser.ts b/src/parsers/directive-ast.parser.ts new file mode 100644 index 0000000..028b6e1 --- /dev/null +++ b/src/parsers/directive-ast.parser.ts @@ -0,0 +1,85 @@ +import { ParserInterface } from './parser.interface'; +import { TranslationCollection } from '../utils/translation.collection'; +import { isPathAngularComponent, extractComponentInlineTemplate } from '../utils/utils'; + +import { parseTemplate, TmplAstNode, TmplAstElement, TmplAstTextAttribute } from '@angular/compiler'; + +export class DirectiveAstParser implements ParserInterface { + + public extract(template: string, path: string): TranslationCollection { + if (path && isPathAngularComponent(path)) { + template = extractComponentInlineTemplate(template); + } + + let collection: TranslationCollection = new TranslationCollection(); +throw new Error('noåpe'); + const nodes: TmplAstNode[] = this.parseTemplate(template, path); + this.getTranslatableElements(nodes).forEach(element => { + const key = this.getElementTranslateAttrValue(element) || this.getElementContents(element); + collection = collection.add(key); + }); + + console.log(collection); + + return collection; + } + + protected getTranslatableElements(nodes: TmplAstNode[]): TmplAstElement[] { + return nodes + .filter(element => this.isElement(element)) + .reduce((result: TmplAstElement[], element: TmplAstElement) => { + return result.concat(this.findChildrenElements(element)); + }, []) + .filter(element => this.isTranslatable(element)); + } + + protected findChildrenElements(node: TmplAstNode): TmplAstElement[] { + if (!this.isElement(node)) { + return []; + } + + // If element has translate attribute all its contents is translatable + // so we don't need to traverse any deeper + if (this.isTranslatable(node)) { + return [node]; + } + + return node.children.reduce((result: TmplAstElement[], childNode: TmplAstNode) => { + if (this.isElement(childNode)) { + const children = this.findChildrenElements(childNode); + return result.concat(children); + } + return result; + }, [node]); + } + + protected parseTemplate(template: string, path: string): TmplAstNode[] { + return parseTemplate(template, path).nodes; + } + + protected isElement(node: any): node is TmplAstElement { + return node + && node.attributes !== undefined + && node.children !== undefined; + } + + protected isTranslatable(node: TmplAstNode): boolean { + if (this.isElement(node) && node.attributes.some(attribute => attribute.name === 'translate')) { + return true; + } + return false; + } + + protected getElementTranslateAttrValue(element: TmplAstElement): string { + const attr: TmplAstTextAttribute = element.attributes.find(attribute => attribute.name === 'translate'); + return attr && attr.value || ''; + } + + protected getElementContents(element: TmplAstElement): string { + const contents = element.sourceSpan.start.file.content; + const start = element.startSourceSpan.end.offset; + const end = element.endSourceSpan.start.offset; + return contents.substring(start, end).trim(); + } + +} diff --git a/src/parsers/directive.parser.ts b/src/parsers/directive.parser.ts index 137ba6c..9e4ecf0 100644 --- a/src/parsers/directive.parser.ts +++ b/src/parsers/directive.parser.ts @@ -2,69 +2,78 @@ import { ParserInterface } from './parser.interface'; import { TranslationCollection } from '../utils/translation.collection'; import { isPathAngularComponent, extractComponentInlineTemplate } from '../utils/utils'; -import * as cheerio from 'cheerio'; - -const $ = cheerio.load('', { xmlMode: true }); +import { parseTemplate, TmplAstNode, TmplAstElement, TmplAstTextAttribute } from '@angular/compiler'; export class DirectiveParser implements ParserInterface { - public extract(contents: string, path?: string): TranslationCollection { + public extract(template: string, path: string): TranslationCollection { if (path && isPathAngularComponent(path)) { - contents = extractComponentInlineTemplate(contents); + template = extractComponentInlineTemplate(template); } - return this.parseTemplate(contents); - } - - protected parseTemplate(template: string): TranslationCollection { let collection: TranslationCollection = new TranslationCollection(); - - const selector = '[translate], [ng2-translate]'; - - template = this.prepareTemplate(template); - $(template) - .find(selector) - .addBack(selector) - .each((i: number, element: CheerioElement) => { - const $element = $(element); - const attr = $element.attr('translate') || $element.attr('ng2-translate'); - - if (attr) { - collection = collection.add(attr); - } else { - $element - .contents() - .toArray() - .filter(node => node.type === 'text') - .map(node => node.nodeValue.trim()) - .filter(text => text.length > 0) - .forEach(text => collection = collection.add(text)); - } - }); + const nodes: TmplAstNode[] = this.parseTemplate(template, path); + this.getTranslatableElements(nodes).forEach(element => { + const translateAttr = this.getTranslateAttribute(element); + const key = translateAttr.value || this.getContents(element); + collection = collection.add(key); + }); return collection; } - protected prepareTemplate(template: string): string { - return this.wrapTemplate( - this.normalizeTemplateAttributes(template) - ); + protected getTranslatableElements(nodes: TmplAstNode[]): TmplAstElement[] { + return nodes + .filter(element => this.isElement(element)) + .reduce((result: TmplAstElement[], element: TmplAstElement) => { + return result.concat(this.findChildrenElements(element)); + }, []) + .filter(element => this.hasTranslateAttribute(element)); } - /** - * Angular's `[attr]="'val'"` syntax is not valid HTML, - * so it can't be parsed by standard HTML parsers. - * This method replaces `[attr]="'val'""` with `attr="val"` - */ - protected normalizeTemplateAttributes(template: string): string { - return template.replace(/\[([^\]]+)\]="'([^']*)'"/g, '$1="$2"'); + protected findChildrenElements(node: TmplAstNode): TmplAstElement[] { + // Safe guard, since only elements have children + if (!this.isElement(node)) { + return []; + } + + // If element has translate attribute it means all of its contents + // is translatable, so we don't need to go any deeper + if (this.hasTranslateAttribute(node)) { + return [node]; + } + + return node.children.reduce((result: TmplAstElement[], childNode: TmplAstNode) => { + if (this.isElement(childNode)) { + const children = this.findChildrenElements(childNode); + return result.concat(children); + } + return result; + }, [node]); } - /** - * Wraps template in tag - */ - protected wrapTemplate(template: string, tag: string = 'div'): string { - return `<${tag}>${template}${tag}>`; + protected parseTemplate(template: string, path: string): TmplAstNode[] { + return parseTemplate(template, path).nodes; + } + + protected isElement(node: any): node is TmplAstElement { + return node + && node.attributes !== undefined + && node.children !== undefined; + } + + protected getContents(element: TmplAstElement): string { + const start = element.startSourceSpan.end.offset; + const end = element.endSourceSpan.start.offset; + return element.sourceSpan.start.file.content.substring(start, end).trim(); + } + + protected hasTranslateAttribute(element: TmplAstElement): boolean { + return !!this.getTranslateAttribute(element); + } + + protected getTranslateAttribute(element: TmplAstElement): TmplAstTextAttribute { + return element.attributes.find(attribute => attribute.name === 'translate'); } } diff --git a/src/parsers/function.parser.ts b/src/parsers/function.parser.ts index 747f64a..834b80d 100644 --- a/src/parsers/function.parser.ts +++ b/src/parsers/function.parser.ts @@ -15,10 +15,10 @@ export class FunctionParser extends AbstractAstParser implements ParserInterface } } - public extract(contents: string, path?: string): TranslationCollection { + public extract(template: string, path: string): TranslationCollection { let collection: TranslationCollection = new TranslationCollection(); - this.sourceFile = this.createSourceFile(path, contents); + this.sourceFile = this.createSourceFile(path, template); const callNodes = this.findCallNodes(); callNodes.forEach(callNode => { diff --git a/src/parsers/parser.interface.ts b/src/parsers/parser.interface.ts index 74a1d86..bc395d0 100644 --- a/src/parsers/parser.interface.ts +++ b/src/parsers/parser.interface.ts @@ -2,6 +2,6 @@ import { TranslationCollection } from '../utils/translation.collection'; export interface ParserInterface { - extract(contents: string, path?: string): TranslationCollection; + extract(template: string, path: string): TranslationCollection; } diff --git a/src/parsers/pipe.parser.ts b/src/parsers/pipe.parser.ts index bd04f14..536084c 100644 --- a/src/parsers/pipe.parser.ts +++ b/src/parsers/pipe.parser.ts @@ -4,12 +4,12 @@ import { isPathAngularComponent, extractComponentInlineTemplate } from '../utils export class PipeParser implements ParserInterface { - public extract(contents: string, path?: string): TranslationCollection { + public extract(template: string, path: string): TranslationCollection { if (path && isPathAngularComponent(path)) { - contents = extractComponentInlineTemplate(contents); + template = extractComponentInlineTemplate(template); } - return this.parseTemplate(contents); + return this.parseTemplate(template); } protected parseTemplate(template: string): TranslationCollection { @@ -21,6 +21,8 @@ export class PipeParser implements ParserInterface { collection = collection.add(matches[2].split('\\\'').join('\'')); } + console.log(collection); + return collection; } diff --git a/src/parsers/service.parser.ts b/src/parsers/service.parser.ts index 8ac5227..4bb4cb5 100644 --- a/src/parsers/service.parser.ts +++ b/src/parsers/service.parser.ts @@ -18,10 +18,10 @@ export class ServiceParser extends AbstractAstParser implements ParserInterface protected sourceFile: SourceFile; - public extract(contents: string, path?: string): TranslationCollection { + public extract(template: string, path: string): TranslationCollection { let collection: TranslationCollection = new TranslationCollection(); - this.sourceFile = this.createSourceFile(path, contents); + this.sourceFile = this.createSourceFile(path, template); const classNodes = this.findClassNodes(this.sourceFile); classNodes.forEach(classNode => { const constructorNode = this.findConstructorNode(classNode); diff --git a/tests/parsers/directive.parser.spec.ts b/tests/parsers/directive.parser.spec.ts index b0028c0..cc13705 100644 --- a/tests/parsers/directive.parser.spec.ts +++ b/tests/parsers/directive.parser.spec.ts @@ -2,78 +2,39 @@ import { expect } from 'chai'; import { DirectiveParser } from '../../src/parsers/directive.parser'; -class TestDirectiveParser extends DirectiveParser { - - public normalizeTemplateAttributes(template: string): string { - return super.normalizeTemplateAttributes(template); - } - -} - describe('DirectiveParser', () => { const templateFilename: string = 'test.template.html'; const componentFilename: string = 'test.component.ts'; - let parser: TestDirectiveParser; + let parser: DirectiveParser; beforeEach(() => { - parser = new TestDirectiveParser(); + parser = new DirectiveParser(); }); - it('should extract contents when no translate attribute value is provided', () => { + it('should not choke when no html is present in template', () => { + const contents = 'Hello World'; + const keys = parser.extract(contents, templateFilename).keys(); + expect(keys).to.deep.equal([]); + }); + + it('should use contents as key when there is no translate attribute value provided', () => { const contents = '
Hi there
-Lorem Ipsum
-Hello World
`; - const template = parser.normalizeTemplateAttributes(contents); - expect(template).to.equal('Hello World
'); - }); - it('should extract contents from within custom tags', () => { const contents = `