From 73801a9cc57031d9ca2a3dd9dc0478c48b06e7f7 Mon Sep 17 00:00:00 2001 From: Kim Biesbjerg Date: Fri, 9 Dec 2016 05:18:04 +0100 Subject: [PATCH] Add StringCollection to make it easier to work with strings --- bin/extract.js | 6 +-- package.json | 2 - src/example.ts | 6 +-- src/extractor.ts | 44 +++++++------------- src/parsers/abstract-template.parser.ts | 4 +- src/parsers/directive.parser.ts | 15 +++---- src/parsers/parser.interface.ts | 4 +- src/parsers/pipe.parser.ts | 13 +++--- src/parsers/service.parser.ts | 13 +++--- src/serializers/json.serializer.ts | 10 ++--- src/serializers/pot.serializer.ts | 13 +++--- src/serializers/serializer.interface.ts | 4 +- src/utils/string.collection.ts | 51 +++++++++++++++++++++++ tests/parsers/directive.parser.spec.ts | 40 +++++++++--------- tests/parsers/pipe.parser.spec.ts | 12 +++--- tests/parsers/service.parser.spec.ts | 24 +++++------ tests/utils/string.collection.spec.ts | 54 +++++++++++++++++++++++++ 17 files changed, 204 insertions(+), 111 deletions(-) create mode 100644 src/utils/string.collection.ts create mode 100644 tests/utils/string.collection.spec.ts diff --git a/bin/extract.js b/bin/extract.js index 5fff4a3..a0ed57d 100755 --- a/bin/extract.js +++ b/bin/extract.js @@ -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}'`); } diff --git a/package.json b/package.json index ad9e4fb..615a919 100644 --- a/package.json +++ b/package.json @@ -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" } } diff --git a/src/example.ts b/src/example.ts index 482163e..7b01d31 100644 --- a/src/example.ts +++ b/src/example.ts @@ -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()}`); } diff --git a/src/extractor.ts b/src/extractor.ts index 218c870..7174d64 100644 --- a/src/extractor.ts +++ b/src/extractor.ts @@ -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; - } - } diff --git a/src/parsers/abstract-template.parser.ts b/src/parsers/abstract-template.parser.ts index 607314e..3260836 100644 --- a/src/parsers/abstract-template.parser.ts +++ b/src/parsers/abstract-template.parser.ts @@ -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); } /** diff --git a/src/parsers/directive.parser.ts b/src/parsers/directive.parser.ts index 90c4192..831d395 100644 --- a/src/parsers/directive.parser.ts +++ b/src/parsers/directive.parser.ts @@ -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; } } diff --git a/src/parsers/parser.interface.ts b/src/parsers/parser.interface.ts index 5820aba..a4ea930 100644 --- a/src/parsers/parser.interface.ts +++ b/src/parsers/parser.interface.ts @@ -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; } diff --git a/src/parsers/pipe.parser.ts b/src/parsers/pipe.parser.ts index 65c44e4..04355ee 100644 --- a/src/parsers/pipe.parser.ts +++ b/src/parsers/pipe.parser.ts @@ -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; } } diff --git a/src/parsers/service.parser.ts b/src/parsers/service.parser.ts index 608c2b1..ea0d95a 100644 --- a/src/parsers/service.parser.ts +++ b/src/parsers/service.parser.ts @@ -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; } /** diff --git a/src/serializers/json.serializer.ts b/src/serializers/json.serializer.ts index 8cf247c..2f12b32 100644 --- a/src/serializers/json.serializer.ts +++ b/src/serializers/json.serializer.ts @@ -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'); } } diff --git a/src/serializers/pot.serializer.ts b/src/serializers/pot.serializer.ts index ee5d6d5..f98b327 100644 --- a/src/serializers/pot.serializer.ts +++ b/src/serializers/pot.serializer.ts @@ -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)); }); } diff --git a/src/serializers/serializer.interface.ts b/src/serializers/serializer.interface.ts index 67f7b28..1cbbf63 100644 --- a/src/serializers/serializer.interface.ts +++ b/src/serializers/serializer.interface.ts @@ -1,5 +1,7 @@ +import { StringCollection } from '../utils/string.collection'; + export interface SerializerInterface { - serialize(messages: string[]): string; + serialize(collection: StringCollection): string; } diff --git a/src/utils/string.collection.ts b/src/utils/string.collection.ts new file mode 100644 index 0000000..7380e31 --- /dev/null +++ b/src/utils/string.collection.ts @@ -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; + } + +} diff --git a/tests/parsers/directive.parser.spec.ts b/tests/parsers/directive.parser.spec.ts index 823a392..003088b 100644 --- a/tests/parsers/directive.parser.spec.ts +++ b/tests/parsers/directive.parser.spec.ts @@ -15,20 +15,20 @@ describe('DirectiveParser', () => { it('should extract contents when no translate attribute value is provided', () => { const contents = '
Hello World
'; - 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 = '
Hello World
'; - 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 = `
Hello World
`; - 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 there
`; - 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', () => {
Hi there
`; - 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', () => {

Lorem Ipsum

`; - 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 = '
Hello World
'; - 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 = '
Hello World
'; - 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 = `
Hello World
`; - 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']); }); }); diff --git a/tests/parsers/pipe.parser.spec.ts b/tests/parsers/pipe.parser.spec.ts index c6c094e..b3bbb1e 100644 --- a/tests/parsers/pipe.parser.spec.ts +++ b/tests/parsers/pipe.parser.spec.ts @@ -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 = ``; - 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 = ``; - 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']); }); }); diff --git a/tests/parsers/service.parser.spec.ts b/tests/parsers/service.parser.spec.ts index e07ba0e..aac62e3 100644 --- a/tests/parsers/service.parser.spec.ts +++ b/tests/parsers/service.parser.spec.ts @@ -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([]); }); }); diff --git a/tests/utils/string.collection.spec.ts b/tests/utils/string.collection.spec.ts new file mode 100644 index 0000000..a00c70a --- /dev/null +++ b/tests/utils/string.collection.spec.ts @@ -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); + }); + +});