Add StringCollection to make it easier to work with strings

This commit is contained in:
Kim Biesbjerg 2016-12-09 05:18:04 +01:00
parent befd841457
commit 73801a9cc5
17 changed files with 204 additions and 111 deletions

View File

@ -34,10 +34,10 @@ var destination = path.join(options.output, filename);
try {
var extractor = new Extractor(serializer);
var messages = extractor.extract(options.dir);
if (messages.length > 0) {
var collection = extractor.process(options.dir);
if (collection.count() > 0) {
extractor.save(destination);
cli.ok(`Extracted ${messages.length} strings: '${destination}'`);
cli.ok(`Extracted ${collection.count()} strings: '${destination}'`);
} else {
cli.info(`Found no extractable strings in the supplied directory path: '${options.dir}'`);
}

View File

@ -46,7 +46,6 @@
"@types/chai": "^3.4.34",
"@types/cheerio": "^0.17.31",
"@types/glob": "^5.0.30",
"@types/lodash": "^4.14.41",
"@types/mocha": "^2.2.33",
"chai": "^3.5.0",
"mocha": "^3.2.0",
@ -60,7 +59,6 @@
"cli": "^1.0.1",
"fs": "0.0.1-security",
"glob": "^7.1.1",
"lodash": "^4.17.2",
"path": "^0.12.7"
}
}

View File

@ -1,17 +1,17 @@
import { Extractor } from './extractor';
import { JsonSerializer } from './serializers/json.serializer';
import { StringCollection } from './utils/string.collection';
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(src);
const collection: StringCollection = extractor.process(src);
const output: string = extractor.save(dest);
console.log({ messages, output });
console.log({ strings: collection.keys(), output: output });
} catch (e) {
console.log(`Something went wrong: ${e.toString()}`);
}

View File

@ -3,8 +3,8 @@ import { PipeParser } from './parsers/pipe.parser';
import { DirectiveParser } from './parsers/directive.parser';
import { ServiceParser } from './parsers/service.parser';
import { SerializerInterface } from './serializers/serializer.interface';
import { StringCollection } from './utils/string.collection';
import * as lodash from 'lodash';
import * as glob from 'glob';
import * as fs from 'fs';
@ -16,35 +16,35 @@ export class Extractor {
new ServiceParser()
];
public globPatterns: string[] = [
public find: string[] = [
'/**/*.html',
'/**/*.ts',
'/**/*.js'
];
public messages: string[] = [];
public collection: StringCollection = new StringCollection();
public constructor(public serializer: SerializerInterface) { }
/**
* Extracts messages from paths
* Process dir
*/
public extract(dir: string): string[] {
let messages = [];
this._getFiles(dir).forEach(filePath => {
const result = this._extractMessages(filePath);
messages = [...messages, ...result];
public process(dir: string): StringCollection {
this._getFiles(dir).forEach(path => {
const contents: string = fs.readFileSync(path, 'utf-8');
this.parsers.forEach((parser: ParserInterface) => {
this.collection.merge(parser.extract(contents, path));
});
});
return this.messages = lodash.uniq(messages);
return this.collection;
}
/**
* Serialize and return output
*/
public serialize(): string {
return this.serializer.serialize(this.messages);
return this.serializer.serialize(this.collection);
}
/**
@ -62,10 +62,10 @@ export class Extractor {
protected _getFiles(dir: string): string[] {
let results: string[] = [];
this.globPatterns.forEach(globPattern => {
this.find.forEach(pattern => {
const files = glob
.sync(dir + globPattern)
.filter(filePath => fs.statSync(filePath).isFile());
.sync(dir + pattern)
.filter(path => fs.statSync(path).isFile());
results = [...results, ...files];
});
@ -73,18 +73,4 @@ export class Extractor {
return results;
}
/**
* 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;
}
}

View File

@ -4,8 +4,8 @@ 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);
protected _isAngularComponent(path: string): boolean {
return new RegExp(/\.(ts|js)$/, 'i').test(path);
}
/**

View File

@ -1,20 +1,21 @@
import { ParserInterface } from './parser.interface';
import { AbstractTemplateParser } from './abstract-template.parser';
import { StringCollection } from '../utils/string.collection';
import * as $ from 'cheerio';
export class DirectiveParser extends AbstractTemplateParser implements ParserInterface {
public process(filePath: string, contents: string): string[] {
if (this._isAngularComponent(filePath)) {
public extract(contents: string, path?: string): StringCollection {
if (path && this._isAngularComponent(path)) {
contents = this._extractInlineTemplate(contents);
}
return this._parseTemplate(contents);
}
protected _parseTemplate(template: string): string[] {
let results: string[] = [];
protected _parseTemplate(template: string): StringCollection {
const collection = new StringCollection();
template = this._normalizeTemplateAttributes(template);
$(template)
@ -25,7 +26,7 @@ export class DirectiveParser extends AbstractTemplateParser implements ParserInt
const attr = $element.attr('translate') || $element.attr('ng2-translate');
if (attr) {
results.push(attr);
collection.add(attr);
} else {
$element
.contents()
@ -33,11 +34,11 @@ export class DirectiveParser extends AbstractTemplateParser implements ParserInt
.filter(textNode => textNode.type === 'text')
.map(textNode => textNode.nodeValue.trim())
.filter(text => text.length > 0)
.forEach(text => results.push(text));
.forEach(text => collection.add(text));
}
});
return results;
return collection;
}
}

View File

@ -1,5 +1,7 @@
import { StringCollection } from '../utils/string.collection';
export interface ParserInterface {
process(filePath: string, contents: string): string[];
extract(contents: string, path?: string): StringCollection;
}

View File

@ -1,27 +1,28 @@
import { ParserInterface } from './parser.interface';
import { AbstractTemplateParser } from './abstract-template.parser';
import { StringCollection } from '../utils/string.collection';
export class PipeParser extends AbstractTemplateParser implements ParserInterface {
public process(filePath: string, contents: string): string[] {
if (this._isAngularComponent(filePath)) {
public extract(contents: string, path?: string): StringCollection {
if (path && this._isAngularComponent(path)) {
contents = this._extractInlineTemplate(contents);
}
return this._parseTemplate(contents);
}
protected _parseTemplate(template: string): string[] {
let results: string[] = [];
protected _parseTemplate(template: string): StringCollection {
const collection = new StringCollection();
const regExp = new RegExp(/([\'"`])([^\1\r\n]*)\1\s+\|\s*translate(:.*?)?/, 'g');
let matches;
while (matches = regExp.exec(template)) {
results.push(matches[2]);
collection.add(matches[2]);
}
return results;
return collection;
}
}

View File

@ -1,13 +1,14 @@
import { ParserInterface } from './parser.interface';
import { StringCollection } from '../utils/string.collection';
export class ServiceParser implements ParserInterface {
public process(filePath: string, contents: string): string[] {
let results: string[] = [];
public extract(contents: string, path?: string): StringCollection {
const collection = new StringCollection();
const translateServiceVar = this._extractTranslateServiceVar(contents);
if (!translateServiceVar) {
return results;
return collection;
}
const methodRegExp: RegExp = new RegExp(/(?:get|instant)\s*\(\s*(\[?([\'"`])([^\1\r\n]+)\2\]?)/);
@ -16,13 +17,13 @@ export class ServiceParser implements ParserInterface {
let matches;
while (matches = regExp.exec(contents)) {
if (this._stringContainsArray(matches[1])) {
results.push(...this._stringToArray(matches[1]));
collection.add(this._stringToArray(matches[1]));
} else {
results.push(matches[3]);
collection.add(matches[3]);
}
}
return results;
return collection;
}
/**

View File

@ -1,14 +1,10 @@
import { SerializerInterface } from './serializer.interface';
import { StringCollection } from '../utils/string.collection';
export class JsonSerializer implements SerializerInterface {
public serialize(messages: string[]): string {
let result = {};
messages.forEach(message => {
result[message] = '';
});
return JSON.stringify(result, null, '\t');
public serialize(collection: StringCollection): string {
return JSON.stringify(collection.values, null, '\t');
}
}

View File

@ -1,4 +1,5 @@
import { SerializerInterface } from './serializer.interface';
import { StringCollection } from '../utils/string.collection';
export class PotSerializer implements SerializerInterface {
@ -9,10 +10,10 @@ export class PotSerializer implements SerializerInterface {
protected _buffer: string[] = [];
public serialize(messages: string[]): string {
public serialize(collection: StringCollection): string {
this._reset();
this._addHeader(this._headers);
this._addMessages(messages);
this._addMessages(collection);
return this._buffer.join('\n');
}
@ -25,10 +26,10 @@ export class PotSerializer implements SerializerInterface {
});
}
protected _addMessages(messages: string[]): void {
messages.forEach(message => {
this._add('msgid', message);
this._add('msgstr', '');
protected _addMessages(collection: StringCollection): void {
Object.keys(collection.values).forEach(key => {
this._add('msgid', key);
this._add('msgstr', collection.get(key));
});
}

View File

@ -1,5 +1,7 @@
import { StringCollection } from '../utils/string.collection';
export interface SerializerInterface {
serialize(messages: string[]): string;
serialize(collection: StringCollection): string;
}

View File

@ -0,0 +1,51 @@
export interface StringType {
[key: string]: string
};
export class StringCollection {
public values: StringType = {};
public static fromObject(values: StringType): StringCollection {
const collection = new StringCollection();
Object.keys(values).forEach(key => collection.add(key, values[key]));
return collection;
}
public static fromArray(keys: string[]): StringCollection {
const collection = new StringCollection();
keys.forEach(key => collection.add(key));
return collection;
}
public add(keys: string | string[], val: string = ''): StringCollection {
if (!Array.isArray(keys)) {
keys = [keys];
}
keys.forEach(key => this.values[key] = val);
return this;
}
public remove(key: string): StringCollection {
delete this.values[key];
return this;
}
public merge(collection: StringCollection): StringCollection {
this.values = Object.assign({}, this.values, collection.values);
return this;
}
public get(key: string): string {
return this.values[key];
}
public keys(): string[] {
return Object.keys(this.values);
}
public count(): number {
return Object.keys(this.values).length;
}
}

View File

@ -15,20 +15,20 @@ describe('DirectiveParser', () => {
it('should extract contents when no translate attribute value is provided', () => {
const contents = '<div translate>Hello World</div>';
const messages = parser.process(templateFilename, contents);
expect(messages).to.deep.equal(['Hello World']);
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['Hello World']);
});
it('should extract translate attribute if provided', () => {
const contents = '<div translate="KEY">Hello World<div>';
const messages = parser.process(templateFilename, contents);
expect(messages).to.deep.equal(['KEY']);
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['KEY']);
});
it('should extract bound translate attribute as key if provided', () => {
const contents = `<div [translate]="'KEY'">Hello World<div>`;
const messages = parser.process(templateFilename, contents);
expect(messages).to.deep.equal(['KEY']);
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['KEY']);
});
it('should extract direct text nodes when no translate attribute value is provided', () => {
@ -39,8 +39,8 @@ describe('DirectiveParser', () => {
Hi <em>there</em>
</div>
`;
const messages = parser.process(templateFilename, contents);
expect(messages).to.deep.equal(['Hello', 'Hi']);
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['Hello', 'Hi']);
});
it('should extract direct text nodes of tags with a translate attribute', () => {
@ -51,8 +51,8 @@ describe('DirectiveParser', () => {
<div translate>Hi there</div>
</div>
`;
const messages = parser.process(templateFilename, contents);
expect(messages).to.deep.equal(['Hello World', 'Hi there']);
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['Hello World', 'Hi there']);
});
it('should extract translate attribute if provided or direct text nodes if not', () => {
@ -64,8 +64,8 @@ describe('DirectiveParser', () => {
<p [translate]="'OTHER_KEY'">Lorem Ipsum</p>
</div>
`;
const messages = parser.process(templateFilename, contents);
expect(messages).to.deep.equal(['KEY', 'Hi there', 'OTHER_KEY']);
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['KEY', 'Hi there', 'OTHER_KEY']);
});
it('should extract and parse inline template', () => {
@ -76,26 +76,26 @@ describe('DirectiveParser', () => {
})
export class TestComponent { }
`;
const messages = parser.process(componentFilename, contents);
expect(messages).to.deep.equal(['Hello World']);
const keys = parser.extract(contents, componentFilename).keys();
expect(keys).to.deep.equal(['Hello World']);
});
it('should extract contents when no ng2-translate attribute value is provided', () => {
const contents = '<div ng2-translate>Hello World</div>';
const messages = parser.process(templateFilename, contents);
expect(messages).to.deep.equal(['Hello World']);
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['Hello World']);
});
it('should extract ng2-translate attribute if provided', () => {
const contents = '<div ng2-translate="KEY">Hello World<div>';
const messages = parser.process(templateFilename, contents);
expect(messages).to.deep.equal(['KEY']);
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['KEY']);
});
it('should extract bound ng2-translate attribute as key if provided', () => {
const contents = `<div [ng2-translate]="'KEY'">Hello World<div>`;
const messages = parser.process(templateFilename, contents);
expect(messages).to.deep.equal(['KEY']);
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['KEY']);
});
});

View File

@ -14,20 +14,20 @@ describe('PipeParser', () => {
it('should extract interpolated strings using translate pipe', () => {
const contents = `Hello {{ 'World' | translate }}`;
const messages = parser.process(templateFilename, contents);
expect(messages).to.deep.equal(['World']);
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['World']);
});
it('should extract interpolated strings using translate pipe in attributes', () => {
const contents = `<span attr="{{ 'Hello World' | translate }}"></span>`;
const messages = parser.process(templateFilename, contents);
expect(messages).to.deep.equal(['Hello World']);
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['Hello World']);
});
it('should extract bound strings using translate pipe in attributes', () => {
const contents = `<span [attr]="'Hello World' | translate"></span>`;
const messages = parser.process(templateFilename, contents);
expect(messages).to.deep.equal(['Hello World']);
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['Hello World']);
});
});

View File

@ -30,8 +30,8 @@ describe('ServiceParser', () => {
protected _translateService: TranslateService
) { }
`;
const messages = parser.extractTranslateServiceVar(contents);
expect(messages).to.equal('_translateService');
const name = parser.extractTranslateServiceVar(contents);
expect(name).to.equal('_translateService');
});
it('should extract strings in TranslateService\'s get() method', () => {
@ -43,8 +43,8 @@ describe('ServiceParser', () => {
this._translateService.get('Hello World');
}
`;
const messages = parser.process(componentFilename, contents);
expect(messages).to.deep.equal(['Hello World']);
const keys = parser.extract(contents, componentFilename).keys();
expect(keys).to.deep.equal(['Hello World']);
});
it('should extract strings in TranslateService\'s instant() method', () => {
@ -56,8 +56,8 @@ describe('ServiceParser', () => {
this._translateService.instant('Hello World');
}
`;
const messages = parser.process(componentFilename, contents);
expect(messages).to.deep.equal(['Hello World']);
const keys = parser.extract(contents, componentFilename).keys();
expect(keys).to.deep.equal(['Hello World']);
});
it('should extract array of strings in TranslateService\'s get() method', () => {
@ -69,8 +69,8 @@ describe('ServiceParser', () => {
this._translateService.get(['Hello', 'World']);
}
`;
const messages = parser.process(componentFilename, contents);
expect(messages).to.deep.equal(['Hello', 'World']);
const keys = parser.extract(contents, componentFilename).keys();
expect(keys).to.deep.equal(['Hello', 'World']);
});
it('should extract array of strings in TranslateService\'s instant() method', () => {
@ -82,8 +82,8 @@ describe('ServiceParser', () => {
this._translateService.instant(['Hello', 'World']);
}
`;
const messages = parser.process(componentFilename, contents);
expect(messages).to.deep.equal(['Hello', 'World']);
const key = parser.extract(contents, componentFilename).keys();
expect(key).to.deep.equal(['Hello', 'World']);
});
it('should not extract strings in get()/instant() methods of other services', () => {
@ -99,8 +99,8 @@ describe('ServiceParser', () => {
this._otherService.instant('Hi there');
}
`;
const messages = parser.process(componentFilename, contents);
expect(messages).to.deep.equal([]);
const keys = parser.extract(contents, componentFilename).keys();
expect(keys).to.deep.equal([]);
});
});

View File

@ -0,0 +1,54 @@
import { expect } from 'chai';
import { StringCollection } from '../../src/utils/string.collection';
describe('StringCollection', () => {
let collection: StringCollection;
beforeEach(() => {
collection = new StringCollection();
});
it('should add item with value', () => {
collection.add('key', 'translation');
expect(collection.get('key')).to.equal('translation');
});
it('should add item with default value', () => {
collection.add('key');
expect(collection.get('key')).to.equal('');
});
it('should add array of items with default values', () => {
collection.add(['key', 'key2']);
expect(collection.count()).to.equal(2);
});
it('should remove item', () => {
collection.add('key1').add('key2').remove('key1');
expect(collection.count()).to.equal(1);
});
it('should return number of items', () => {
collection.add('key1').add('key2');
expect(collection.count()).to.equal(2);
});
it('should initialize with array of keys', () => {
const newCollection = StringCollection.fromArray(['Hello', 'World']);
expect(newCollection.count()).to.equal(2);
});
it('should initialize with key/value pairs', () => {
const newCollection = StringCollection.fromObject({'key': 'translation'});
expect(newCollection.get('key')).to.equal('translation');
});
it('should merge with other collection', () => {
collection.add('Hello');
const newCollection = StringCollection.fromArray(['World']);
expect(collection.merge(newCollection).count()).to.equal(2);
});
});