Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added regex parser #44

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ npm install gettext-extractor
Let's start with a code example:

```javascript
const { GettextExtractor, JsExtractors, HtmlExtractors } = require('gettext-extractor');
const { GettextExtractor, JsExtractors, HtmlExtractors, RegexExtractors } = require('gettext-extractor');

let extractor = new GettextExtractor();

Expand All @@ -42,7 +42,7 @@ extractor
JsExtractors.callExpression('getText', {
arguments: {
text: 0,
context: 1
context: 1
}
}),
JsExtractors.callExpression('getPlural', {
Expand All @@ -61,6 +61,15 @@ extractor
])
.parseFilesGlob('./src/**/*.html');

extractor
.createRegexParser([
RegexExtractors.addCondition({
regex: /\_translate\((.*)\)/i,
text: 1
})
])
.parseFilesGlob('./src/**/*.tmpl');

extractor.savePotFile('./messages.pot');

extractor.printStats();
Expand Down
7 changes: 7 additions & 0 deletions src/extractor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import * as pofile from 'pofile';
import { CatalogBuilder, IContext, IMessage } from './builder';
import { JsParser, IJsExtractorFunction } from './js/parser';
import { HtmlParser, IHtmlExtractorFunction } from './html/parser';
import { RegexParser, IRegexExtractorFunction } from './regex/parser';
import { StatsOutput } from './utils/output';
import { Validate } from './utils/validate';

Expand Down Expand Up @@ -45,6 +46,12 @@ export class GettextExtractor {
return new HtmlParser(this.builder, extractors, this.stats);
}

public createRegexParser(extractors?: IRegexExtractorFunction[]): RegexParser {
Validate.optional.nonEmptyArray({ extractors });

return new RegexParser(this.builder, extractors, this.stats);
}

public addMessage(message: IMessage): void {
Validate.required.stringProperty(message, 'message.text');
Validate.optional.stringProperty(message, 'message.textPlural');
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { GettextExtractor } from './extractor';
export { JsExtractors } from './js/extractors';
export { HtmlExtractors } from './html/extractors';
export { RegexExtractors } from './regex/extractors';
61 changes: 61 additions & 0 deletions src/regex/extractors/factories/addCondition.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { IRegexExtractorFunction } from '../../parser';
import { RegexUtils } from '../../utils';
import { IMessage } from '../../../builder';
import { Validate } from '../../../utils/validate';

type IAddConditionExtractorArgument = {
regex: RegExp,
text: number,
textPlural?: number
};

export function addConditionExtractor({ regex, text, textPlural }: IAddConditionExtractorArgument): IRegexExtractorFunction {

Validate.required.argument({ regex });
Validate.required.argument({ text });
Validate.required.regexProperty({ regex }, 'arguments[0].regex');
Validate.required.numberProperty({ text }, 'arguments[0].text');
Validate.optional.numberProperty({ textPlural }, 'arguments[0].textPlural');

return (sourceFileContent:string, sourceFilePath:string, messages:Array<IMessage>) => {

// Check if the flags have a global flag
if (regex.flags.indexOf('g') === -1) {
// If not, add it.
regex = new RegExp(regex.source, regex.flags + 'g');
}

// Check if the flags have a multiline flag
if (regex.flags.indexOf('m') === -1) {
// If not, add it.
regex = new RegExp(regex.source, regex.flags + 'm');
}

const matches = sourceFileContent.match(regex);

if (matches !== null) {
for (let match of matches) {

const matchGroups = regex.exec(match);

if (matchGroups !== null) {

const message: IMessage = {
text: matchGroups[text],
references: [`${sourceFilePath}:${RegexUtils.getLineNumber(sourceFileContent, match)}`],
comments: []
};

if (textPlural) {
message.textPlural = matchGroups[textPlural];
}

messages.push(message);
}

// Reset last index for next string
regex.lastIndex = 0;
}
}
}
}
6 changes: 6 additions & 0 deletions src/regex/extractors/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@

import { addConditionExtractor } from './factories/addCondition';

export abstract class RegexExtractors {
public static addCondition: typeof addConditionExtractor = addConditionExtractor;
}
24 changes: 24 additions & 0 deletions src/regex/parser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import * as ts from 'typescript';
import { Parser, IParseOptions } from '../parser';
import { IMessage } from '../builder';

export type IRegexExtractorFunction = (file: string, filename: string, messages: Array<IMessage>) => void;

export class RegexParser extends Parser<IRegexExtractorFunction, IParseOptions> {

protected parse(source: string, fileName: string): Array<IMessage> {
return this.parseSourceFile(source, fileName);
}

protected parseSourceFile(file: string, fileName: string): Array<IMessage> {
let messages: Array<IMessage> = [];

for (let extractor of this.extractors) {
extractor(file, fileName, messages);
}

return messages;
}
}

exports.RegexParser = RegexParser;
5 changes: 5 additions & 0 deletions src/regex/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export abstract class RegexUtils {
public static getLineNumber(fileContent: string, searchString: string): number {
return fileContent.substring(0, fileContent.indexOf(searchString)).split('\n').length;
}
}
130 changes: 130 additions & 0 deletions tests/regex/extractors/factories/addCondition.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { RegexParser } from '../../../../src/regex/parser';
import { CatalogBuilder, IMessage } from '../../../../src/builder';
import { addConditionExtractor } from '../../../../src/regex/extractors/factories/addCondition';

describe('HTML: Element Attribute Extractor', () => {

let builder: CatalogBuilder,
messages: IMessage[],
parser: RegexParser;

beforeEach(() => {
messages = [];

builder = <any>{
addMessage: jest.fn((message: IMessage) => {
messages.push(message);
})
};
});

describe('standard', () => {

beforeEach(() => {
parser = new RegexParser(builder, [
addConditionExtractor({
regex: /\[Translate\:\s?(.*?)\s?(\|\s?(.*))?\]/is,
text: 1,
textPlural: 3
})
]);
});

test('just singular text', () => {
parser.parseString(`This is some text containing [Translate: a translatable string] that should be translated`);
expect(messages).toEqual([{
comments: [],
references: ["gettext-extractor-string-literal:1"],
text: "a translatable string",
textPlural: undefined
}]);
});

test('just plural text', () => {
parser.parseString(`This is some text containing [Translate: a translatable string | some translatable strings] that should be translated`);
expect(messages).toEqual([{
comments: [],
references: ["gettext-extractor-string-literal:1"],
text: "a translatable string",
textPlural: "some translatable strings"
}]);
});

test('just multiline text', () => {
parser.parseString(
`This is some text containing a [Translate: Translatable
multiline
text]`
);

expect(messages).toEqual([{
comments: [],
references: ["gettext-extractor-string-literal:1"],
text: `Translatable
multiline
text`,
textPlural: undefined
}]);
});
});

describe('argument validation', () => {

test('arguments[0]: none', () => {
expect(() => {
(<any>addConditionExtractor)();
}).toThrowError(/Cannot destructure property `(.*)` of 'undefined' or 'null'/);
});

test('arguments[0]: wrong type', () => {
expect(() => {
(<any>addConditionExtractor)('test');
}).toThrowError(/Missing argument (.*)/);
});

test('arguments[0].regex: Missing', () => {
expect(() => {
(<any>addConditionExtractor)({
text: 1
});
}).toThrowError('Missing argument \'regex\'');
});

test('arguments[0].regex: Wrong type', () => {
expect(() => {
(<any>addConditionExtractor)({
regex: 'test',
text: 1
});
}).toThrowError(/Property 'arguments\[0\].regex' must be a regular expression/);
});

test('arguments[0].text: Missing', () => {
expect(() => {
(<any>addConditionExtractor)({
regex: /(.*)/
});
}).toThrowError('Missing argument \'text\'');
});


test('arguments[0].text: Wrong type', () => {
expect(() => {
(<any>addConditionExtractor)({
regex: /(.*)/,
text: 'test'
});
}).toThrowError(/Property 'arguments\[0\].text' must be a number/);
});

test('arguments[0].textPlural: Wrong type', () => {
expect(() => {
(<any>addConditionExtractor)({
regex: /(.*)/,
text: 1,
textPlural: 'test'
});
}).toThrowError(/Property 'arguments\[0\].textPlural' must be a number/);
});
});
});
17 changes: 17 additions & 0 deletions tests/regex/utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import * as ts from 'typescript';
import { RegexUtils } from '../../src/regex/utils';

describe('RegEx: Utils', () => {

const sampleText =
` This is a sample text to check
if we can find the line number
of a random word like dragon
and a little extra line`

describe('getLineNumber', () => {
test('standard case', () => {
expect(RegexUtils.getLineNumber(sampleText, 'dragon')).toBe(3);
})
});
});