Revert "feat(i18n): add support for custom placeholder names"

This reverts commit 2abb414cfb.
This commit is contained in:
Robert Messerle 2016-04-14 15:06:00 -07:00
parent 2b34c88b69
commit 930f58718b
6 changed files with 16 additions and 133 deletions

View File

@ -49,7 +49,6 @@ import {
var _implicitReceiver = new ImplicitReceiver(); var _implicitReceiver = new ImplicitReceiver();
// TODO(tbosch): Cannot make this const/final right now because of the transpiler... // TODO(tbosch): Cannot make this const/final right now because of the transpiler...
var INTERPOLATION_REGEXP = /\{\{([\s\S]*?)\}\}/g; var INTERPOLATION_REGEXP = /\{\{([\s\S]*?)\}\}/g;
var COMMENT_REGEX = /\/\//g;
class ParseException extends BaseException { class ParseException extends BaseException {
constructor(message: string, input: string, errLocation: string, ctxLocation?: any) { constructor(message: string, input: string, errLocation: string, ctxLocation?: any) {
@ -68,7 +67,7 @@ export class Parser {
parseAction(input: string, location: any): ASTWithSource { parseAction(input: string, location: any): ASTWithSource {
this._checkNoInterpolation(input, location); this._checkNoInterpolation(input, location);
var tokens = this._lexer.tokenize(this._stripComments(input)); var tokens = this._lexer.tokenize(input);
var ast = new _ParseAST(input, location, tokens, true).parseChain(); var ast = new _ParseAST(input, location, tokens, true).parseChain();
return new ASTWithSource(ast, input, location); return new ASTWithSource(ast, input, location);
} }
@ -97,7 +96,7 @@ export class Parser {
} }
this._checkNoInterpolation(input, location); this._checkNoInterpolation(input, location);
var tokens = this._lexer.tokenize(this._stripComments(input)); var tokens = this._lexer.tokenize(input);
return new _ParseAST(input, location, tokens, false).parseChain(); return new _ParseAST(input, location, tokens, false).parseChain();
} }
@ -123,7 +122,7 @@ export class Parser {
let expressions = []; let expressions = [];
for (let i = 0; i < split.expressions.length; ++i) { for (let i = 0; i < split.expressions.length; ++i) {
var tokens = this._lexer.tokenize(this._stripComments(split.expressions[i])); var tokens = this._lexer.tokenize(split.expressions[i]);
var ast = new _ParseAST(input, location, tokens, false).parseChain(); var ast = new _ParseAST(input, location, tokens, false).parseChain();
expressions.push(ast); expressions.push(ast);
} }
@ -159,10 +158,6 @@ export class Parser {
return new ASTWithSource(new LiteralPrimitive(input), input, location); return new ASTWithSource(new LiteralPrimitive(input), input, location);
} }
private _stripComments(input: string): string {
return StringWrapper.split(input, COMMENT_REGEX)[0].trim();
}
private _checkNoInterpolation(input: string, location: any): void { private _checkNoInterpolation(input: string, location: any): void {
var parts = StringWrapper.split(input, INTERPOLATION_REGEXP); var parts = StringWrapper.split(input, INTERPOLATION_REGEXP);
if (parts.length > 1) { if (parts.length > 1) {

View File

@ -22,16 +22,14 @@ import {
partition, partition,
Part, Part,
stringifyNodes, stringifyNodes,
meaning, meaning
getPhNameFromBinding,
dedupePhName
} from './shared'; } from './shared';
const _I18N_ATTR = "i18n"; const _I18N_ATTR = "i18n";
const _PLACEHOLDER_ELEMENT = "ph"; const _PLACEHOLDER_ELEMENT = "ph";
const _NAME_ATTR = "name"; const _NAME_ATTR = "name";
const _I18N_ATTR_PREFIX = "i18n-"; const _I18N_ATTR_PREFIX = "i18n-";
let _PLACEHOLDER_EXPANDED_REGEXP = RegExpWrapper.create(`\\<ph(\\s)+name=("(\\w)+")\\>\\<\\/ph\\>`); let _PLACEHOLDER_EXPANDED_REGEXP = RegExpWrapper.create(`\\<ph(\\s)+name=("(\\d)+")\\>\\<\\/ph\\>`);
/** /**
* Creates an i18n-ed version of the parsed template. * Creates an i18n-ed version of the parsed template.
@ -315,31 +313,19 @@ export class I18nHtmlParser implements HtmlParser {
private _replacePlaceholdersWithExpressions(message: string, exps: string[], private _replacePlaceholdersWithExpressions(message: string, exps: string[],
sourceSpan: ParseSourceSpan): string { sourceSpan: ParseSourceSpan): string {
let expMap = this._buildExprMap(exps);
return RegExpWrapper.replaceAll(_PLACEHOLDER_EXPANDED_REGEXP, message, (match) => { return RegExpWrapper.replaceAll(_PLACEHOLDER_EXPANDED_REGEXP, message, (match) => {
let nameWithQuotes = match[2]; let nameWithQuotes = match[2];
let name = nameWithQuotes.substring(1, nameWithQuotes.length - 1); let name = nameWithQuotes.substring(1, nameWithQuotes.length - 1);
return this._convertIntoExpression(name, expMap, sourceSpan); let index = NumberWrapper.parseInt(name, 10);
return this._convertIntoExpression(index, exps, sourceSpan);
}); });
} }
private _buildExprMap(exps: string[]): Map<string, string> { private _convertIntoExpression(index: number, exps: string[], sourceSpan: ParseSourceSpan) {
let expMap = new Map<string, string>(); if (index >= 0 && index < exps.length) {
let usedNames = new Map<string, number>(); return `{{${exps[index]}}}`;
for (var i = 0; i < exps.length; i++) {
let phName = getPhNameFromBinding(exps[i], i);
expMap.set(dedupePhName(usedNames, phName), exps[i]);
}
return expMap;
}
private _convertIntoExpression(name: string, expMap: Map<string, string>,
sourceSpan: ParseSourceSpan) {
if (expMap.has(name)) {
return `{{${expMap.get(name)}}}`;
} else { } else {
throw new I18nError(sourceSpan, `Invalid interpolation name '${name}'`); throw new I18nError(sourceSpan, `Invalid interpolation index '${index}'`);
} }
} }
} }
@ -361,4 +347,4 @@ class _CreateNodeMapping implements HtmlAstVisitor {
} }
visitComment(ast: HtmlCommentAst, context: any): any { return ""; } visitComment(ast: HtmlCommentAst, context: any): any { return ""; }
} }

View File

@ -8,13 +8,12 @@ import {
HtmlCommentAst, HtmlCommentAst,
htmlVisitAll htmlVisitAll
} from 'angular2/src/compiler/html_ast'; } from 'angular2/src/compiler/html_ast';
import {isPresent, isBlank, StringWrapper} from 'angular2/src/facade/lang'; import {isPresent, isBlank} from 'angular2/src/facade/lang';
import {Message} from './message'; import {Message} from './message';
import {Parser} from 'angular2/src/compiler/expression_parser/parser'; import {Parser} from 'angular2/src/compiler/expression_parser/parser';
export const I18N_ATTR = "i18n"; export const I18N_ATTR = "i18n";
export const I18N_ATTR_PREFIX = "i18n-"; export const I18N_ATTR_PREFIX = "i18n-";
var CUSTOM_PH_EXP = /\/\/[\s\S]*i18n[\s\S]*\([\s\S]*ph[\s\S]*=[\s\S]*"([\s\S]*?)"[\s\S]*\)/g;
/** /**
* An i18n error. * An i18n error.
@ -114,15 +113,12 @@ export function removeInterpolation(value: string, source: ParseSourceSpan,
parser: Parser): string { parser: Parser): string {
try { try {
let parsed = parser.splitInterpolation(value, source.toString()); let parsed = parser.splitInterpolation(value, source.toString());
let usedNames = new Map<string, number>();
if (isPresent(parsed)) { if (isPresent(parsed)) {
let res = ""; let res = "";
for (let i = 0; i < parsed.strings.length; ++i) { for (let i = 0; i < parsed.strings.length; ++i) {
res += parsed.strings[i]; res += parsed.strings[i];
if (i != parsed.strings.length - 1) { if (i != parsed.strings.length - 1) {
let customPhName = getPhNameFromBinding(parsed.expressions[i], i); res += `<ph name="${i}"/>`;
customPhName = dedupePhName(usedNames, customPhName);
res += `<ph name="${customPhName}"/>`;
} }
} }
return res; return res;
@ -134,22 +130,6 @@ export function removeInterpolation(value: string, source: ParseSourceSpan,
} }
} }
export function getPhNameFromBinding(input: string, index: number): string {
let customPhMatch = StringWrapper.split(input, CUSTOM_PH_EXP);
return customPhMatch.length > 1 ? customPhMatch[1] : `${index}`;
}
export function dedupePhName(usedNames: Map<string, number>, name: string): string {
let duplicateNameCount = usedNames.get(name);
if (isPresent(duplicateNameCount)) {
usedNames.set(name, duplicateNameCount + 1);
return `${name}_${duplicateNameCount}`;
} else {
usedNames.set(name, 1);
return name;
}
}
export function stringifyNodes(nodes: HtmlAst[], parser: Parser): string { export function stringifyNodes(nodes: HtmlAst[], parser: Parser): string {
let visitor = new _StringifyVisitor(parser); let visitor = new _StringifyVisitor(parser);
return htmlVisitAll(visitor, nodes).join(""); return htmlVisitAll(visitor, nodes).join("");

View File

@ -103,8 +103,6 @@ export function main() {
it('should parse grouped expressions', () => { checkAction("(1 + 2) * 3", "1 + 2 * 3"); }); it('should parse grouped expressions', () => { checkAction("(1 + 2) * 3", "1 + 2 * 3"); });
it('should ignore comments in expressions', () => { checkAction('a //comment', 'a'); });
it('should parse an empty string', () => { checkAction(''); }); it('should parse an empty string', () => { checkAction(''); });
describe("literals", () => { describe("literals", () => {
@ -271,8 +269,6 @@ export function main() {
}); });
it('should parse conditional expression', () => { checkBinding('a < b ? a : b'); }); it('should parse conditional expression', () => { checkBinding('a < b ? a : b'); });
it('should ignore comments in bindings', () => { checkBinding('a //comment', 'a'); });
}); });
describe('parseTemplateBindings', () => { describe('parseTemplateBindings', () => {
@ -428,9 +424,6 @@ export function main() {
it('should parse expression with newline characters', () => { it('should parse expression with newline characters', () => {
checkInterpolation(`{{ 'foo' +\n 'bar' +\r 'baz' }}`, `{{ "foo" + "bar" + "baz" }}`); checkInterpolation(`{{ 'foo' +\n 'bar' +\r 'baz' }}`, `{{ "foo" + "bar" + "baz" }}`);
}); });
it('should ignore comments in interpolation expressions',
() => { checkInterpolation('{{a //comment}}', '{{ a }}'); });
}); });
describe("parseSimpleBinding", () => { describe("parseSimpleBinding", () => {

View File

@ -76,36 +76,6 @@ export function main() {
.toEqual([[HtmlElementAst, 'div', 0], [HtmlAttrAst, 'value', '{{b}} or {{a}}']]); .toEqual([[HtmlElementAst, 'div', 0], [HtmlAttrAst, 'value', '{{b}} or {{a}}']]);
}); });
it('should handle interpolation with custom placeholder names', () => {
let translations: {[key: string]: string} = {};
translations[id(new Message('<ph name="FIRST"/> and <ph name="SECOND"/>', null, null))] =
'<ph name="SECOND"/> or <ph name="FIRST"/>';
expect(
humanizeDom(parse(
`<div value='{{a //i18n(ph="FIRST")}} and {{b //i18n(ph="SECOND")}}' i18n-value></div>`,
translations)))
.toEqual([
[HtmlElementAst, 'div', 0],
[HtmlAttrAst, 'value', '{{b //i18n(ph="SECOND")}} or {{a //i18n(ph="FIRST")}}']
]);
});
it('should handle interpolation with duplicate placeholder names', () => {
let translations: {[key: string]: string} = {};
translations[id(new Message('<ph name="FIRST"/> and <ph name="FIRST_1"/>', null, null))] =
'<ph name="FIRST_1"/> or <ph name="FIRST"/>';
expect(
humanizeDom(parse(
`<div value='{{a //i18n(ph="FIRST")}} and {{b //i18n(ph="FIRST")}}' i18n-value></div>`,
translations)))
.toEqual([
[HtmlElementAst, 'div', 0],
[HtmlAttrAst, 'value', '{{b //i18n(ph="FIRST")}} or {{a //i18n(ph="FIRST")}}']
]);
});
it("should handle nested html", () => { it("should handle nested html", () => {
let translations: {[key: string]: string} = {}; let translations: {[key: string]: string} = {};
translations[id(new Message('<ph name="e0">a</ph><ph name="e2">b</ph>', null, null))] = translations[id(new Message('<ph name="e0">a</ph><ph name="e2">b</ph>', null, null))] =
@ -228,7 +198,7 @@ export function main() {
expect( expect(
humanizeErrors(parse("<div value='hi {{a}}' i18n-value></div>", translations).errors)) humanizeErrors(parse("<div value='hi {{a}}' i18n-value></div>", translations).errors))
.toEqual(["Invalid interpolation name '99'"]); .toEqual(["Invalid interpolation index '99'"]);
}); });
}); });
@ -237,4 +207,4 @@ export function main() {
function humanizeErrors(errors: ParseError[]): string[] { function humanizeErrors(errors: ParseError[]): string[] {
return errors.map(error => error.msg); return errors.map(error => error.msg);
} }

View File

@ -93,47 +93,6 @@ export function main() {
.toEqual([new Message('Hi <ph name="0"/> and <ph name="1"/>', null, null)]); .toEqual([new Message('Hi <ph name="0"/> and <ph name="1"/>', null, null)]);
}); });
it('should replace interpolation with named placeholders if provided (text nodes)', () => {
let res = extractor.extract(`
<div i18n>Hi {{one //i18n(ph="FIRST")}} and {{two //i18n(ph="SECOND")}}</div>`,
'someurl');
expect(res.messages)
.toEqual([
new Message('<ph name="t0">Hi <ph name="FIRST"/> and <ph name="SECOND"/></ph>', null,
null)
]);
});
it('should replace interpolation with named placeholders if provided (attributes)', () => {
let res = extractor.extract(`
<div title='Hi {{one //i18n(ph="FIRST")}} and {{two //i18n(ph="SECOND")}}'
i18n-title></div>`,
'someurl');
expect(res.messages)
.toEqual([new Message('Hi <ph name="FIRST"/> and <ph name="SECOND"/>', null, null)]);
});
it('should match named placeholders with extra spacing', () => {
let res = extractor.extract(`
<div title='Hi {{one // i18n ( ph = "FIRST" )}} and {{two // i18n ( ph = "SECOND" )}}'
i18n-title></div>`,
'someurl');
expect(res.messages)
.toEqual([new Message('Hi <ph name="FIRST"/> and <ph name="SECOND"/>', null, null)]);
});
it('should suffix duplicate placeholder names with numbers', () => {
let res = extractor.extract(`
<div title='Hi {{one //i18n(ph="FIRST")}} and {{two //i18n(ph="FIRST")}} and {{three //i18n(ph="FIRST")}}'
i18n-title></div>`,
'someurl');
expect(res.messages)
.toEqual([
new Message('Hi <ph name="FIRST"/> and <ph name="FIRST_1"/> and <ph name="FIRST_2"/>',
null, null)
]);
});
it("should handle html content", () => { it("should handle html content", () => {
let res = extractor.extract( let res = extractor.extract(
'<div i18n><div attr="value">zero<div>one</div></div><div>two</div></div>', "someurl"); '<div i18n><div attr="value">zero<div>one</div></div><div>two</div></div>', "someurl");