Refactor code. Add DirectiveParser (Thanks ocombe\!)

This commit is contained in:
Kim Biesbjerg 2016-12-07 06:10:48 +01:00
parent 085f5fe700
commit 1ee298f737
7 changed files with 115 additions and 121 deletions

View File

@ -1,17 +1,17 @@
import { Extractor } from './extractor'; import { Extractor } from './extractor';
import { JsonSerializer } from './serializers/json.serializer'; 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(); const serializer = new JsonSerializer();
// Or const serializer = new PotSerializer(); // Or const serializer = new PotSerializer();
const extractor = new Extractor(serializer); const extractor = new Extractor(serializer);
const src = '/your/project';
const dest = '/your/project/template.json';
try { try {
const messages: string[] = extractor.extract(dir); const messages: string[] = extractor.extract(src);
const output: string = extractor.save(dest); const output: string = extractor.save(dest);
console.log('Done!'); console.log({ messages, output });
} catch (e) { } catch (e) {
console.log(`Something went wrong: ${e.toString()}`); console.log(`Something went wrong: ${e.toString()}`);
} }

View File

@ -12,13 +12,13 @@ export class Extractor {
public parsers: ParserInterface[] = [ public parsers: ParserInterface[] = [
new PipeParser(), new PipeParser(),
new ServiceParser(), new DirectiveParser(),
new DirectiveParser() new ServiceParser()
]; ];
public globPatterns: string[] = [ public globPatterns: string[] = [
'/**/*.ts', '/**/*.html',
'/**/*.html' '/**/*.ts'
]; ];
public messages: string[] = []; public messages: string[] = [];
@ -30,15 +30,11 @@ export class Extractor {
*/ */
public extract(dir: string): string[] { public extract(dir: string): string[] {
let messages = []; let messages = [];
this.globPatterns.forEach(globPattern => {
const filePaths = glob.sync(dir + globPattern); this._getFiles(dir).forEach(filePath => {
filePaths
.filter(filePath => fs.statSync(filePath).isFile())
.forEach(filePath => {
const result = this._extractMessages(filePath); const result = this._extractMessages(filePath);
messages = [...messages, ...result]; messages = [...messages, ...result];
}); });
});
return this.messages = lodash.uniq(messages); return this.messages = lodash.uniq(messages);
} }
@ -56,19 +52,35 @@ export class Extractor {
public save(destination: string): string { public save(destination: string): string {
const data = this.serialize(); const data = this.serialize();
fs.writeFileSync(destination, data); fs.writeFileSync(destination, data);
return data; return data;
} }
/** /**
* Extract messages from file using specialized parser * Get all files in dir that matches glob patterns
*/
protected _getFiles(dir: string): string[] {
let results: string[] = [];
this.globPatterns.forEach(globPattern => {
const files = glob
.sync(dir + globPattern)
.filter(filePath => fs.statSync(filePath).isFile());
results = [...results, ...files];
});
return results;
}
/**
* Extract messages from file using parser
*/ */
protected _extractMessages(filePath: string): string[] { protected _extractMessages(filePath: string): string[] {
let results = []; let results: string[] = [];
const contents: string = fs.readFileSync(filePath, 'utf-8'); const contents: string = fs.readFileSync(filePath, 'utf-8');
this.parsers.forEach((parser: ParserInterface) => { this.parsers.forEach((parser: ParserInterface) => {
results = results.concat(parser.process(contents)); results = [...results, ...parser.process(filePath, contents)];
}); });
return results; return results;

View File

@ -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 '';
}
}

View File

@ -1,66 +1,45 @@
import {ParserInterface} from './parser.interface'; import { ParserInterface } from './parser.interface';
import { AbstractTemplateParser } from './abstract-template.parser';
import * as $ from 'cheerio'; import * as $ from 'cheerio';
export class DirectiveParser implements ParserInterface { export class DirectiveParser extends AbstractTemplateParser implements ParserInterface {
public patterns = { public process(filePath: string, contents: string): string[] {
template: `template:\\s?(("|'|\`)(.|[\\r\\n])+?[^\\\\]\\2)` 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__=');
$(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=
// only support string values for now
if(wrappedAttr && wrappedAttr.match(/^['"].*['"]$/)) {
key = wrappedAttr.substr(1, wrappedAttr.length - 2);
} else if(attr) {
key = attr;
} }
if(!key) { return this._parseTemplate(contents);
key = $this.text().replace(/\\n/gi, '').trim();
} }
if(key) { protected _parseTemplate(template: string): string[] {
results.push(key); let results: string[] = [];
template = this._normalizeTemplateAttributes(template);
$(template).find('[translate]')
.each((i: number, element: CheerioElement) => {
const $element = $(element);
const attr = $element.attr('translate');
const text = $element.text();
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'); * Angular's `[attr]="'val'"` syntax is not valid HTML,
* so Cheerio is not able to parse it.
let results: string[] = [], * This method replaces `[attr]="'val'""` with `attr="val"`
hasTemplate = false, */
matches; protected _normalizeTemplateAttributes(template: string): string {
return template.replace(/\[([^\]]+)\]="'([^\"]*)'"/g, '$1="$2"');
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;
} }
} }

View File

@ -1,5 +1,5 @@
export interface ParserInterface { export interface ParserInterface {
process(contents: string): string[]; process(filePath: string, contents: string): string[];
} }

View File

@ -1,22 +1,25 @@
import { ParserInterface } from './parser.interface'; 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 = { public process(filePath: string, contents: string): string[] {
pipe: `(['"\`])([^\\1\\r\\n]*)\\1\\s+\\|\\s*translate(:.*?)?` 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[] = []; let results: string[] = [];
for (let patternName in this.patterns) { const regExp = new RegExp('([\'"`])([^\\1\\r\\n]*)\\1\\s+\\|\\s*translate(:.*?)?', 'g');
const regExp = new RegExp(this.patterns[patternName], 'g');
let matches; let matches;
while (matches = regExp.exec(contents)) { while (matches = regExp.exec(template)) {
results.push(matches[2]); results.push(matches[2]);
} }
}
return results; return results;
} }

View File

@ -2,48 +2,25 @@ import { ParserInterface } from './parser.interface';
export class ServiceParser implements ParserInterface { export class ServiceParser implements ParserInterface {
public patterns = { public process(filePath: string, contents: string): string[] {
translateServiceMethods: `{{TRANSLATE_SERVICE}}\.(?:get|instant)\\s*\\\(\\s*(['"\`])([^\\1\\r\\n]+)\\1`,
};
public process(contents: string): string[] {
let results: string[] = []; let results: string[] = [];
const translateServiceVar = this._extractTranslateServiceVar(contents); const translateServiceVar = this._extractTranslateServiceVar(contents);
if (!translateServiceVar) { if (!translateServiceVar) {
return []; return results;
} }
for (let patternName in this.patterns) { const methodPattern: string = '(?:get|instant)\\s*\\\(\\s*([\'"`])([^\\1\\r\\n]+)\\1';
const regExp = this._createRegExp(patternName, { const regExp: RegExp = new RegExp(`${translateServiceVar}\.${methodPattern}`, 'g');
'TRANSLATE_SERVICE': translateServiceVar
});
let matches; let matches;
while (matches = regExp.exec(contents)) { while (matches = regExp.exec(contents)) {
results.push(matches[2]); results.push(matches[2]);
} }
}
return results; 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 * Extract name of TranslateService variable for use in patterns
*/ */