diff --git a/src/example.ts b/src/example.ts index 4fd8f97..482163e 100644 --- a/src/example.ts +++ b/src/example.ts @@ -1,17 +1,17 @@ import { Extractor } from './extractor'; import { JsonSerializer } from './serializers/json.serializer'; -const dir = '/path/to/extract/strings/from'; -const dest = '/path/to/save/template/to/template.pot'; - const serializer = new JsonSerializer(); // Or const serializer = new PotSerializer(); const extractor = new Extractor(serializer); +const src = '/your/project'; +const dest = '/your/project/template.json'; + try { - const messages: string[] = extractor.extract(dir); + const messages: string[] = extractor.extract(src); const output: string = extractor.save(dest); - console.log('Done!'); + console.log({ messages, output }); } catch (e) { console.log(`Something went wrong: ${e.toString()}`); } diff --git a/src/extractor.ts b/src/extractor.ts index 3a3446a..8885643 100644 --- a/src/extractor.ts +++ b/src/extractor.ts @@ -12,13 +12,13 @@ export class Extractor { public parsers: ParserInterface[] = [ new PipeParser(), - new ServiceParser(), - new DirectiveParser() + new DirectiveParser(), + new ServiceParser() ]; public globPatterns: string[] = [ - '/**/*.ts', - '/**/*.html' + '/**/*.html', + '/**/*.ts' ]; public messages: string[] = []; @@ -30,14 +30,10 @@ export class Extractor { */ public extract(dir: string): string[] { let messages = []; - this.globPatterns.forEach(globPattern => { - const filePaths = glob.sync(dir + globPattern); - filePaths - .filter(filePath => fs.statSync(filePath).isFile()) - .forEach(filePath => { - const result = this._extractMessages(filePath); - messages = [...messages, ...result]; - }); + + this._getFiles(dir).forEach(filePath => { + const result = this._extractMessages(filePath); + messages = [...messages, ...result]; }); return this.messages = lodash.uniq(messages); @@ -56,22 +52,38 @@ export class Extractor { public save(destination: string): string { const data = this.serialize(); fs.writeFileSync(destination, data); - return data; } /** - * Extract messages from file using specialized parser + * Get all files in dir that matches glob patterns */ - protected _extractMessages(filePath: string): string[] { - let results = []; + protected _getFiles(dir: string): string[] { + let results: string[] = []; - const contents: string = fs.readFileSync(filePath, 'utf-8'); - this.parsers.forEach((parser: ParserInterface) => { - results = results.concat(parser.process(contents)); + this.globPatterns.forEach(globPattern => { + const files = glob + .sync(dir + globPattern) + .filter(filePath => fs.statSync(filePath).isFile()); + + results = [...results, ...files]; }); return results; } -} \ No newline at end of file + /** + * Extract messages from file using parser + */ + protected _extractMessages(filePath: string): string[] { + let results: string[] = []; + + const contents: string = fs.readFileSync(filePath, 'utf-8'); + this.parsers.forEach((parser: ParserInterface) => { + results = [...results, ...parser.process(filePath, contents)]; + }); + + return results; + } + +} diff --git a/src/parsers/abstract-template.parser.ts b/src/parsers/abstract-template.parser.ts new file mode 100644 index 0000000..ae36faa --- /dev/null +++ b/src/parsers/abstract-template.parser.ts @@ -0,0 +1,23 @@ +export abstract class AbstractTemplateParser { + + /** + * Checks if file is of type javascript or typescript and + * makes the assumption that it is an Angular Component + */ + protected _isAngularComponent(filePath: string): boolean { + return new RegExp('\.(ts|js)$', 'i').test(filePath); + } + + /** + * Extracts inline template from components + */ + protected _extractInlineTemplate(contents: string): string { + const match = new RegExp('template\\s?:\\s?(("|\'|`)(.|[\\r\\n])+?[^\\\\]\\2)').exec(contents); + if (match !== null) { + return match[1]; + } + + return ''; + } + +} diff --git a/src/parsers/directive.parser.ts b/src/parsers/directive.parser.ts index 446b81b..872481b 100644 --- a/src/parsers/directive.parser.ts +++ b/src/parsers/directive.parser.ts @@ -1,66 +1,45 @@ -import {ParserInterface} from './parser.interface'; +import { ParserInterface } from './parser.interface'; +import { AbstractTemplateParser } from './abstract-template.parser'; import * as $ from 'cheerio'; -export class DirectiveParser implements ParserInterface { +export class DirectiveParser extends AbstractTemplateParser implements ParserInterface { - public patterns = { - template: `template:\\s?(("|'|\`)(.|[\\r\\n])+?[^\\\\]\\2)` - }; + public process(filePath: string, contents: string): string[] { + if (this._isAngularComponent(filePath)) { + contents = this._extractInlineTemplate(contents); + } - protected _parseTemplate(content) { - let results: string[] = [], - template = content.trim() - // hack for cheerio that doesn't support wrapped attributes - .replace('[translate]=', '__translate__='); + return this._parseTemplate(contents); + } - $(template).find('[translate],[__translate__]').contents().filter(function() { - return this.nodeType === 3; // node type 3 = text node - }).each(function() { - let key, - $this = $(this), - element = $(this).parent(), - wrappedAttr = element.attr('__translate__'), // previously [translate]= - attr = element.attr('translate'); // translate= + protected _parseTemplate(template: string): string[] { + let results: string[] = []; - // only support string values for now - if(wrappedAttr && wrappedAttr.match(/^['"].*['"]$/)) { - key = wrappedAttr.substr(1, wrappedAttr.length - 2); - } else if(attr) { - key = attr; - } + template = this._normalizeTemplateAttributes(template); - if(!key) { - key = $this.text().replace(/\\n/gi, '').trim(); - } + $(template).find('[translate]') + .each((i: number, element: CheerioElement) => { + const $element = $(element); + const attr = $element.attr('translate'); + const text = $element.text(); - if(key) { - results.push(key); - } - }); + if (attr) { + results.push(attr); + } else if (text) { + results.push(text); + } + }); - return results; - } + return results; + } - public process(contents: string): string[] { - const regExp = new RegExp(this.patterns.template, 'gi'); - - let results: string[] = [], - hasTemplate = false, - matches; - - while(matches = regExp.exec(contents)) { - let content = matches[1] - .substr(1, matches[1].length - 2); - - hasTemplate = true; - results = results.concat(this._parseTemplate(content)); - } - - if(!hasTemplate) { - this._parseTemplate(contents); - } - - return results; - } + /** + * Angular's `[attr]="'val'"` syntax is not valid HTML, + * so Cheerio is not able to parse it. + * This method replaces `[attr]="'val'""` with `attr="val"` + */ + protected _normalizeTemplateAttributes(template: string): string { + return template.replace(/\[([^\]]+)\]="'([^\"]*)'"/g, '$1="$2"'); + } } diff --git a/src/parsers/parser.interface.ts b/src/parsers/parser.interface.ts index 5c06327..5820aba 100644 --- a/src/parsers/parser.interface.ts +++ b/src/parsers/parser.interface.ts @@ -1,5 +1,5 @@ export interface ParserInterface { - process(contents: string): string[]; + process(filePath: string, contents: string): string[]; } diff --git a/src/parsers/pipe.parser.ts b/src/parsers/pipe.parser.ts index 17baf3a..cce8479 100644 --- a/src/parsers/pipe.parser.ts +++ b/src/parsers/pipe.parser.ts @@ -1,21 +1,24 @@ import { ParserInterface } from './parser.interface'; +import { AbstractTemplateParser } from './abstract-template.parser'; -export class PipeParser implements ParserInterface { +export class PipeParser extends AbstractTemplateParser implements ParserInterface { - public patterns = { - pipe: `(['"\`])([^\\1\\r\\n]*)\\1\\s+\\|\\s*translate(:.*?)?` - }; + public process(filePath: string, contents: string): string[] { + if (this._isAngularComponent(filePath)) { + contents = this._extractInlineTemplate(contents); + } - public process(contents: string): string[] { + return this._parseTemplate(contents); + } + + protected _parseTemplate(template: string): string[] { let results: string[] = []; - for (let patternName in this.patterns) { - const regExp = new RegExp(this.patterns[patternName], 'g'); + const regExp = new RegExp('([\'"`])([^\\1\\r\\n]*)\\1\\s+\\|\\s*translate(:.*?)?', 'g'); - let matches; - while (matches = regExp.exec(contents)) { - results.push(matches[2]); - } + let matches; + while (matches = regExp.exec(template)) { + results.push(matches[2]); } return results; diff --git a/src/parsers/service.parser.ts b/src/parsers/service.parser.ts index e3b0b29..976d49c 100644 --- a/src/parsers/service.parser.ts +++ b/src/parsers/service.parser.ts @@ -2,48 +2,25 @@ import { ParserInterface } from './parser.interface'; export class ServiceParser implements ParserInterface { - public patterns = { - translateServiceMethods: `{{TRANSLATE_SERVICE}}\.(?:get|instant)\\s*\\\(\\s*(['"\`])([^\\1\\r\\n]+)\\1`, - }; - - public process(contents: string): string[] { + public process(filePath: string, contents: string): string[] { let results: string[] = []; const translateServiceVar = this._extractTranslateServiceVar(contents); if (!translateServiceVar) { - return []; + return results; } - for (let patternName in this.patterns) { - const regExp = this._createRegExp(patternName, { - 'TRANSLATE_SERVICE': translateServiceVar - }); + const methodPattern: string = '(?:get|instant)\\s*\\\(\\s*([\'"`])([^\\1\\r\\n]+)\\1'; + const regExp: RegExp = new RegExp(`${translateServiceVar}\.${methodPattern}`, 'g'); - let matches; - while (matches = regExp.exec(contents)) { - results.push(matches[2]); - } + let matches; + while (matches = regExp.exec(contents)) { + results.push(matches[2]); } return results; } - /** - * Create regular expression, replacing placeholders with real values - */ - protected _createRegExp(patternName: string, replaceVars: {} = {}): RegExp { - if (!this.patterns.hasOwnProperty(patternName)) { - throw new Error('Invalid pattern name'); - } - - let pattern = this.patterns[patternName]; - Object.keys(replaceVars).forEach(key => { - pattern = pattern.replace('{{' + key + '}}', replaceVars[key]); - }); - - return new RegExp(pattern, 'g'); - } - /** * Extract name of TranslateService variable for use in patterns */