feat(i18n): xtb serializer

This commit is contained in:
Victor Berchet
2016-07-21 13:56:58 -07:00
parent 1b77604ee2
commit 0eee1d5de3
71 changed files with 3152 additions and 4240 deletions

View File

@ -10,9 +10,9 @@ import {AUTO_STYLE} from '@angular/core';
import {ANY_STATE, DEFAULT_STATE, EMPTY_STATE} from '../../core_private';
import {CompileDirectiveMetadata} from '../compile_metadata';
import {ListWrapper, Map, StringMapWrapper} from '../facade/collection';
import {StringMapWrapper} from '../facade/collection';
import {BaseException} from '../facade/exceptions';
import {isArray, isBlank, isPresent} from '../facade/lang';
import {isBlank, isPresent} from '../facade/lang';
import {Identifiers} from '../identifiers';
import * as o from '../output/output_ast';
import * as t from '../template_parser/template_ast';

View File

@ -8,10 +8,10 @@
import {ChangeDetectionStrategy, SchemaMetadata, ViewEncapsulation} from '@angular/core';
import {CHANGE_DETECTION_STRATEGY_VALUES, LIFECYCLE_HOOKS_VALUES, LifecycleHooks, VIEW_ENCAPSULATION_VALUES, reflector} from '../core_private';
import {ListWrapper, StringMapWrapper} from '../src/facade/collection';
import {BaseException, unimplemented} from '../src/facade/exceptions';
import {NumberWrapper, RegExpWrapper, Type, isArray, isBlank, isBoolean, isNumber, isPresent, isString, isStringMap, normalizeBlank, normalizeBool, serializeEnum} from '../src/facade/lang';
import {LifecycleHooks, reflector} from '../core_private';
import {ListWrapper, StringMapWrapper} from './facade/collection';
import {BaseException, unimplemented} from './facade/exceptions';
import {RegExpWrapper, Type, isBlank, isPresent, isStringMap, normalizeBlank, normalizeBool} from './facade/lang';
import {CssSelector} from './selector';
import {getUrlScheme} from './url_resolver';

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {Compiler, CompilerFactory, CompilerOptions, Component, ComponentResolver, Inject, Injectable, NgModule, PLATFORM_DIRECTIVES, PLATFORM_INITIALIZER, PLATFORM_PIPES, PlatformRef, ReflectiveInjector, Type, ViewEncapsulation, createPlatformFactory, disposePlatform, isDevMode, platformCore} from '@angular/core';
import {Compiler, CompilerFactory, CompilerOptions, Component, Inject, Injectable, PLATFORM_DIRECTIVES, PLATFORM_INITIALIZER, PLATFORM_PIPES, PlatformRef, ReflectiveInjector, Type, ViewEncapsulation, createPlatformFactory, isDevMode, platformCore} from '@angular/core';
export * from './template_parser/template_ast';
export {TEMPLATE_TRANSFORMS} from './template_parser/template_parser';

View File

@ -9,8 +9,8 @@
import {OnInit, OnDestroy, DoCheck, OnChanges, AfterContentInit, AfterContentChecked, AfterViewInit, AfterViewChecked,} from '@angular/core';
import {reflector, LifecycleHooks} from '../core_private';
import {Type} from '../src/facade/lang';
import {MapWrapper} from '../src/facade/collection';
import {Type} from './facade/lang';
import {MapWrapper} from './facade/collection';
const LIFECYCLE_INTERFACES: Map<any, Type> = MapWrapper.createFromPairs([
[LifecycleHooks.OnInit, OnInit],

View File

@ -7,12 +7,13 @@
*/
import {Injectable, ViewEncapsulation} from '@angular/core';
import {MapWrapper} from '../src/facade/collection';
import {BaseException} from '../src/facade/exceptions';
import {isBlank, isPresent} from '../src/facade/lang';
import {CompileDirectiveMetadata, CompileStylesheetMetadata, CompileTemplateMetadata, CompileTypeMetadata} from './compile_metadata';
import {CompilerConfig} from './config';
import {HtmlAstVisitor, HtmlAttrAst, HtmlCommentAst, HtmlElementAst, HtmlExpansionAst, HtmlExpansionCaseAst, HtmlTextAst, htmlVisitAll} from './html_parser/html_ast';
import {MapWrapper} from './facade/collection';
import {BaseException} from './facade/exceptions';
import {isBlank, isPresent} from './facade/lang';
import * as html from './html_parser/ast';
import {HtmlParser} from './html_parser/html_parser';
import {InterpolationConfig} from './html_parser/interpolation_config';
import {extractStyleUrls, isStyleUrlResolvable} from './style_url_resolver';
@ -111,7 +112,7 @@ export class DirectiveNormalizer {
}));
const visitor = new TemplatePreparseVisitor();
htmlVisitAll(visitor, rootNodesAndErrors.rootNodes);
html.visitAll(visitor, rootNodesAndErrors.rootNodes);
const templateStyles = this.normalizeStylesheet(new CompileStylesheetMetadata(
{styles: visitor.styles, styleUrls: visitor.styleUrls, moduleUrl: templateAbsUrl}));
@ -187,13 +188,13 @@ export class DirectiveNormalizer {
}
}
class TemplatePreparseVisitor implements HtmlAstVisitor {
class TemplatePreparseVisitor implements html.Visitor {
ngContentSelectors: string[] = [];
styles: string[] = [];
styleUrls: string[] = [];
ngNonBindableStackCount: number = 0;
visitElement(ast: HtmlElementAst, context: any): any {
visitElement(ast: html.Element, context: any): any {
var preparsedElement = preparseElement(ast);
switch (preparsedElement.type) {
case PreparsedElementType.NG_CONTENT:
@ -204,7 +205,7 @@ class TemplatePreparseVisitor implements HtmlAstVisitor {
case PreparsedElementType.STYLE:
var textContent = '';
ast.children.forEach(child => {
if (child instanceof HtmlTextAst) {
if (child instanceof html.Text) {
textContent += child.value;
}
});
@ -221,18 +222,18 @@ class TemplatePreparseVisitor implements HtmlAstVisitor {
if (preparsedElement.nonBindable) {
this.ngNonBindableStackCount++;
}
htmlVisitAll(this, ast.children);
html.visitAll(this, ast.children);
if (preparsedElement.nonBindable) {
this.ngNonBindableStackCount--;
}
return null;
}
visitComment(ast: HtmlCommentAst, context: any): any { return null; }
visitAttr(ast: HtmlAttrAst, context: any): any { return null; }
visitText(ast: HtmlTextAst, context: any): any { return null; }
visitExpansion(ast: HtmlExpansionAst, context: any): any { return null; }
visitComment(ast: html.Comment, context: any): any { return null; }
visitAttribute(ast: html.Attribute, context: any): any { return null; }
visitText(ast: html.Text, context: any): any { return null; }
visitExpansion(ast: html.Expansion, context: any): any { return null; }
visitExpansionCase(ast: HtmlExpansionCaseAst, context: any): any { return null; }
visitExpansionCase(ast: html.ExpansionCase, context: any): any { return null; }
}
function _cloneDirectiveWithTemplate(

View File

@ -0,0 +1,72 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {ParseSourceSpan} from '../parse_util';
export interface Node {
sourceSpan: ParseSourceSpan;
visit(visitor: Visitor, context: any): any;
}
export class Text implements Node {
constructor(public value: string, public sourceSpan: ParseSourceSpan) {}
visit(visitor: Visitor, context: any): any { return visitor.visitText(this, context); }
}
export class Expansion implements Node {
constructor(
public switchValue: string, public type: string, public cases: ExpansionCase[],
public sourceSpan: ParseSourceSpan, public switchValueSourceSpan: ParseSourceSpan) {}
visit(visitor: Visitor, context: any): any { return visitor.visitExpansion(this, context); }
}
export class ExpansionCase implements Node {
constructor(
public value: string, public expression: Node[], public sourceSpan: ParseSourceSpan,
public valueSourceSpan: ParseSourceSpan, public expSourceSpan: ParseSourceSpan) {}
visit(visitor: Visitor, context: any): any { return visitor.visitExpansionCase(this, context); }
}
export class Attribute implements Node {
constructor(public name: string, public value: string, public sourceSpan: ParseSourceSpan) {}
visit(visitor: Visitor, context: any): any { return visitor.visitAttribute(this, context); }
}
export class Element implements Node {
constructor(
public name: string, public attrs: Attribute[], public children: Node[],
public sourceSpan: ParseSourceSpan, public startSourceSpan: ParseSourceSpan,
public endSourceSpan: ParseSourceSpan) {}
visit(visitor: Visitor, context: any): any { return visitor.visitElement(this, context); }
}
export class Comment implements Node {
constructor(public value: string, public sourceSpan: ParseSourceSpan) {}
visit(visitor: Visitor, context: any): any { return visitor.visitComment(this, context); }
}
export interface Visitor {
visitElement(element: Element, context: any): any;
visitAttribute(attribute: Attribute, context: any): any;
visitText(text: Text, context: any): any;
visitComment(comment: Comment, context: any): any;
visitExpansion(expansion: Expansion, context: any): any;
visitExpansionCase(expansionCase: ExpansionCase, context: any): any;
}
export function visitAll(visitor: Visitor, nodes: Node[], context: any = null): any[] {
let result: any[] = [];
nodes.forEach(ast => {
const astResult = ast.visit(visitor, context);
if (astResult) {
result.push(astResult);
}
});
return result;
}

View File

@ -1,78 +0,0 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {isPresent} from '../facade/lang';
import {ParseSourceSpan} from '../parse_util';
export interface HtmlAst {
sourceSpan: ParseSourceSpan;
visit(visitor: HtmlAstVisitor, context: any): any;
}
export class HtmlTextAst implements HtmlAst {
constructor(public value: string, public sourceSpan: ParseSourceSpan) {}
visit(visitor: HtmlAstVisitor, context: any): any { return visitor.visitText(this, context); }
}
export class HtmlExpansionAst implements HtmlAst {
constructor(
public switchValue: string, public type: string, public cases: HtmlExpansionCaseAst[],
public sourceSpan: ParseSourceSpan, public switchValueSourceSpan: ParseSourceSpan) {}
visit(visitor: HtmlAstVisitor, context: any): any {
return visitor.visitExpansion(this, context);
}
}
export class HtmlExpansionCaseAst implements HtmlAst {
constructor(
public value: string, public expression: HtmlAst[], public sourceSpan: ParseSourceSpan,
public valueSourceSpan: ParseSourceSpan, public expSourceSpan: ParseSourceSpan) {}
visit(visitor: HtmlAstVisitor, context: any): any {
return visitor.visitExpansionCase(this, context);
}
}
export class HtmlAttrAst implements HtmlAst {
constructor(public name: string, public value: string, public sourceSpan: ParseSourceSpan) {}
visit(visitor: HtmlAstVisitor, context: any): any { return visitor.visitAttr(this, context); }
}
export class HtmlElementAst implements HtmlAst {
constructor(
public name: string, public attrs: HtmlAttrAst[], public children: HtmlAst[],
public sourceSpan: ParseSourceSpan, public startSourceSpan: ParseSourceSpan,
public endSourceSpan: ParseSourceSpan) {}
visit(visitor: HtmlAstVisitor, context: any): any { return visitor.visitElement(this, context); }
}
export class HtmlCommentAst implements HtmlAst {
constructor(public value: string, public sourceSpan: ParseSourceSpan) {}
visit(visitor: HtmlAstVisitor, context: any): any { return visitor.visitComment(this, context); }
}
export interface HtmlAstVisitor {
visitElement(ast: HtmlElementAst, context: any): any;
visitAttr(ast: HtmlAttrAst, context: any): any;
visitText(ast: HtmlTextAst, context: any): any;
visitComment(ast: HtmlCommentAst, context: any): any;
visitExpansion(ast: HtmlExpansionAst, context: any): any;
visitExpansionCase(ast: HtmlExpansionCaseAst, context: any): any;
}
export function htmlVisitAll(visitor: HtmlAstVisitor, asts: HtmlAst[], context: any = null): any[] {
var result: any[] = [];
asts.forEach(ast => {
var astResult = ast.visit(visitor, context);
if (isPresent(astResult)) {
result.push(astResult);
}
});
return result;
}

View File

@ -6,403 +6,21 @@
* found in the LICENSE file at https://angular.io/license
*/
import {Injectable} from '../../../core/index';
import {isPresent, isBlank,} from '../facade/lang';
import {ListWrapper} from '../facade/collection';
import {HtmlAst, HtmlAttrAst, HtmlTextAst, HtmlCommentAst, HtmlElementAst, HtmlExpansionAst, HtmlExpansionCaseAst} from './html_ast';
import {HtmlToken, HtmlTokenType, tokenizeHtml} from './html_lexer';
import {ParseError, ParseSourceSpan} from '../parse_util';
import {getHtmlTagDefinition, getNsPrefix, mergeNsAndName} from './html_tags';
import {Injectable} from '@angular/core';
import {getHtmlTagDefinition} from './html_tags';
import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from './interpolation_config';
import {ParseTreeResult, Parser} from './parser';
export class HtmlTreeError extends ParseError {
static create(elementName: string, span: ParseSourceSpan, msg: string): HtmlTreeError {
return new HtmlTreeError(elementName, span, msg);
}
constructor(public elementName: string, span: ParseSourceSpan, msg: string) { super(span, msg); }
}
export class HtmlParseTreeResult {
constructor(public rootNodes: HtmlAst[], public errors: ParseError[]) {}
}
export {ParseTreeResult, TreeError} from './parser';
@Injectable()
export class HtmlParser {
export class HtmlParser extends Parser {
constructor() { super(getHtmlTagDefinition); }
parse(
sourceContent: string, sourceUrl: string, parseExpansionForms: boolean = false,
interpolationConfig: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG):
HtmlParseTreeResult {
const tokensAndErrors =
tokenizeHtml(sourceContent, sourceUrl, parseExpansionForms, interpolationConfig);
const treeAndErrors = new TreeBuilder(tokensAndErrors.tokens).build();
return new HtmlParseTreeResult(
treeAndErrors.rootNodes,
(<ParseError[]>tokensAndErrors.errors).concat(treeAndErrors.errors));
source: string, url: string, parseExpansionForms: boolean = false,
interpolationConfig: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG): ParseTreeResult {
return super.parse(source, url, parseExpansionForms, interpolationConfig);
}
}
class TreeBuilder {
private index: number = -1;
private peek: HtmlToken;
private rootNodes: HtmlAst[] = [];
private errors: HtmlTreeError[] = [];
private elementStack: HtmlElementAst[] = [];
constructor(private tokens: HtmlToken[]) { this._advance(); }
build(): HtmlParseTreeResult {
while (this.peek.type !== HtmlTokenType.EOF) {
if (this.peek.type === HtmlTokenType.TAG_OPEN_START) {
this._consumeStartTag(this._advance());
} else if (this.peek.type === HtmlTokenType.TAG_CLOSE) {
this._consumeEndTag(this._advance());
} else if (this.peek.type === HtmlTokenType.CDATA_START) {
this._closeVoidElement();
this._consumeCdata(this._advance());
} else if (this.peek.type === HtmlTokenType.COMMENT_START) {
this._closeVoidElement();
this._consumeComment(this._advance());
} else if (
this.peek.type === HtmlTokenType.TEXT || this.peek.type === HtmlTokenType.RAW_TEXT ||
this.peek.type === HtmlTokenType.ESCAPABLE_RAW_TEXT) {
this._closeVoidElement();
this._consumeText(this._advance());
} else if (this.peek.type === HtmlTokenType.EXPANSION_FORM_START) {
this._consumeExpansion(this._advance());
} else {
// Skip all other tokens...
this._advance();
}
}
return new HtmlParseTreeResult(this.rootNodes, this.errors);
}
private _advance(): HtmlToken {
const prev = this.peek;
if (this.index < this.tokens.length - 1) {
// Note: there is always an EOF token at the end
this.index++;
}
this.peek = this.tokens[this.index];
return prev;
}
private _advanceIf(type: HtmlTokenType): HtmlToken {
if (this.peek.type === type) {
return this._advance();
}
return null;
}
private _consumeCdata(startToken: HtmlToken) {
this._consumeText(this._advance());
this._advanceIf(HtmlTokenType.CDATA_END);
}
private _consumeComment(token: HtmlToken) {
const text = this._advanceIf(HtmlTokenType.RAW_TEXT);
this._advanceIf(HtmlTokenType.COMMENT_END);
const value = isPresent(text) ? text.parts[0].trim() : null;
this._addToParent(new HtmlCommentAst(value, token.sourceSpan));
}
private _consumeExpansion(token: HtmlToken) {
const switchValue = this._advance();
const type = this._advance();
const cases: HtmlExpansionCaseAst[] = [];
// read =
while (this.peek.type === HtmlTokenType.EXPANSION_CASE_VALUE) {
let expCase = this._parseExpansionCase();
if (isBlank(expCase)) return; // error
cases.push(expCase);
}
// read the final }
if (this.peek.type !== HtmlTokenType.EXPANSION_FORM_END) {
this.errors.push(
HtmlTreeError.create(null, this.peek.sourceSpan, `Invalid ICU message. Missing '}'.`));
return;
}
this._advance();
const mainSourceSpan = new ParseSourceSpan(token.sourceSpan.start, this.peek.sourceSpan.end);
this._addToParent(new HtmlExpansionAst(
switchValue.parts[0], type.parts[0], cases, mainSourceSpan, switchValue.sourceSpan));
}
private _parseExpansionCase(): HtmlExpansionCaseAst {
const value = this._advance();
// read {
if (this.peek.type !== HtmlTokenType.EXPANSION_CASE_EXP_START) {
this.errors.push(
HtmlTreeError.create(null, this.peek.sourceSpan, `Invalid ICU message. Missing '{'.`));
return null;
}
// read until }
const start = this._advance();
const exp = this._collectExpansionExpTokens(start);
if (isBlank(exp)) return null;
const end = this._advance();
exp.push(new HtmlToken(HtmlTokenType.EOF, [], end.sourceSpan));
// parse everything in between { and }
const parsedExp = new TreeBuilder(exp).build();
if (parsedExp.errors.length > 0) {
this.errors = this.errors.concat(<HtmlTreeError[]>parsedExp.errors);
return null;
}
const sourceSpan = new ParseSourceSpan(value.sourceSpan.start, end.sourceSpan.end);
const expSourceSpan = new ParseSourceSpan(start.sourceSpan.start, end.sourceSpan.end);
return new HtmlExpansionCaseAst(
value.parts[0], parsedExp.rootNodes, sourceSpan, value.sourceSpan, expSourceSpan);
}
private _collectExpansionExpTokens(start: HtmlToken): HtmlToken[] {
const exp: HtmlToken[] = [];
const expansionFormStack = [HtmlTokenType.EXPANSION_CASE_EXP_START];
while (true) {
if (this.peek.type === HtmlTokenType.EXPANSION_FORM_START ||
this.peek.type === HtmlTokenType.EXPANSION_CASE_EXP_START) {
expansionFormStack.push(this.peek.type);
}
if (this.peek.type === HtmlTokenType.EXPANSION_CASE_EXP_END) {
if (lastOnStack(expansionFormStack, HtmlTokenType.EXPANSION_CASE_EXP_START)) {
expansionFormStack.pop();
if (expansionFormStack.length == 0) return exp;
} else {
this.errors.push(
HtmlTreeError.create(null, start.sourceSpan, `Invalid ICU message. Missing '}'.`));
return null;
}
}
if (this.peek.type === HtmlTokenType.EXPANSION_FORM_END) {
if (lastOnStack(expansionFormStack, HtmlTokenType.EXPANSION_FORM_START)) {
expansionFormStack.pop();
} else {
this.errors.push(
HtmlTreeError.create(null, start.sourceSpan, `Invalid ICU message. Missing '}'.`));
return null;
}
}
if (this.peek.type === HtmlTokenType.EOF) {
this.errors.push(
HtmlTreeError.create(null, start.sourceSpan, `Invalid ICU message. Missing '}'.`));
return null;
}
exp.push(this._advance());
}
}
private _consumeText(token: HtmlToken) {
let text = token.parts[0];
if (text.length > 0 && text[0] == '\n') {
const parent = this._getParentElement();
if (isPresent(parent) && parent.children.length == 0 &&
getHtmlTagDefinition(parent.name).ignoreFirstLf) {
text = text.substring(1);
}
}
if (text.length > 0) {
this._addToParent(new HtmlTextAst(text, token.sourceSpan));
}
}
private _closeVoidElement(): void {
if (this.elementStack.length > 0) {
const el = ListWrapper.last(this.elementStack);
if (getHtmlTagDefinition(el.name).isVoid) {
this.elementStack.pop();
}
}
}
private _consumeStartTag(startTagToken: HtmlToken) {
const prefix = startTagToken.parts[0];
const name = startTagToken.parts[1];
const attrs: HtmlAttrAst[] = [];
while (this.peek.type === HtmlTokenType.ATTR_NAME) {
attrs.push(this._consumeAttr(this._advance()));
}
const fullName = getElementFullName(prefix, name, this._getParentElement());
let selfClosing = false;
// Note: There could have been a tokenizer error
// so that we don't get a token for the end tag...
if (this.peek.type === HtmlTokenType.TAG_OPEN_END_VOID) {
this._advance();
selfClosing = true;
if (getNsPrefix(fullName) == null && !getHtmlTagDefinition(fullName).isVoid) {
this.errors.push(HtmlTreeError.create(
fullName, startTagToken.sourceSpan,
`Only void and foreign elements can be self closed "${startTagToken.parts[1]}"`));
}
} else if (this.peek.type === HtmlTokenType.TAG_OPEN_END) {
this._advance();
selfClosing = false;
}
const end = this.peek.sourceSpan.start;
const span = new ParseSourceSpan(startTagToken.sourceSpan.start, end);
const el = new HtmlElementAst(fullName, attrs, [], span, span, null);
this._pushElement(el);
if (selfClosing) {
this._popElement(fullName);
el.endSourceSpan = span;
}
}
private _pushElement(el: HtmlElementAst) {
if (this.elementStack.length > 0) {
const parentEl = ListWrapper.last(this.elementStack);
if (getHtmlTagDefinition(parentEl.name).isClosedByChild(el.name)) {
this.elementStack.pop();
}
}
const tagDef = getHtmlTagDefinition(el.name);
const {parent, container} = this._getParentElementSkippingContainers();
if (isPresent(parent) && tagDef.requireExtraParent(parent.name)) {
const newParent = new HtmlElementAst(
tagDef.parentToAdd, [], [], el.sourceSpan, el.startSourceSpan, el.endSourceSpan);
this._insertBeforeContainer(parent, container, newParent);
}
this._addToParent(el);
this.elementStack.push(el);
}
private _consumeEndTag(endTagToken: HtmlToken) {
const fullName =
getElementFullName(endTagToken.parts[0], endTagToken.parts[1], this._getParentElement());
if (this._getParentElement()) {
this._getParentElement().endSourceSpan = endTagToken.sourceSpan;
}
if (getHtmlTagDefinition(fullName).isVoid) {
this.errors.push(HtmlTreeError.create(
fullName, endTagToken.sourceSpan,
`Void elements do not have end tags "${endTagToken.parts[1]}"`));
} else if (!this._popElement(fullName)) {
this.errors.push(HtmlTreeError.create(
fullName, endTagToken.sourceSpan, `Unexpected closing tag "${endTagToken.parts[1]}"`));
}
}
private _popElement(fullName: string): boolean {
for (let stackIndex = this.elementStack.length - 1; stackIndex >= 0; stackIndex--) {
const el = this.elementStack[stackIndex];
if (el.name == fullName) {
ListWrapper.splice(this.elementStack, stackIndex, this.elementStack.length - stackIndex);
return true;
}
if (!getHtmlTagDefinition(el.name).closedByParent) {
return false;
}
}
return false;
}
private _consumeAttr(attrName: HtmlToken): HtmlAttrAst {
const fullName = mergeNsAndName(attrName.parts[0], attrName.parts[1]);
let end = attrName.sourceSpan.end;
let value = '';
if (this.peek.type === HtmlTokenType.ATTR_VALUE) {
const valueToken = this._advance();
value = valueToken.parts[0];
end = valueToken.sourceSpan.end;
}
return new HtmlAttrAst(fullName, value, new ParseSourceSpan(attrName.sourceSpan.start, end));
}
private _getParentElement(): HtmlElementAst {
return this.elementStack.length > 0 ? ListWrapper.last(this.elementStack) : null;
}
/**
* Returns the parent in the DOM and the container.
*
* `<ng-container>` elements are skipped as they are not rendered as DOM element.
*/
private _getParentElementSkippingContainers():
{parent: HtmlElementAst, container: HtmlElementAst} {
let container: HtmlElementAst = null;
for (let i = this.elementStack.length - 1; i >= 0; i--) {
if (this.elementStack[i].name !== 'ng-container') {
return {parent: this.elementStack[i], container};
}
container = this.elementStack[i];
}
return {parent: ListWrapper.last(this.elementStack), container};
}
private _addToParent(node: HtmlAst) {
const parent = this._getParentElement();
if (isPresent(parent)) {
parent.children.push(node);
} else {
this.rootNodes.push(node);
}
}
/**
* Insert a node between the parent and the container.
* When no container is given, the node is appended as a child of the parent.
* Also updates the element stack accordingly.
*
* @internal
*/
private _insertBeforeContainer(
parent: HtmlElementAst, container: HtmlElementAst, node: HtmlElementAst) {
if (!container) {
this._addToParent(node);
this.elementStack.push(node);
} else {
if (parent) {
// replace the container with the new node in the children
const index = parent.children.indexOf(container);
parent.children[index] = node;
} else {
this.rootNodes.push(node);
}
node.children.push(container);
this.elementStack.splice(this.elementStack.indexOf(container), 0, node);
}
}
}
function getElementFullName(
prefix: string, localName: string, parentElement: HtmlElementAst): string {
if (isBlank(prefix)) {
prefix = getHtmlTagDefinition(localName).implicitNamespacePrefix;
if (isBlank(prefix) && isPresent(parentElement)) {
prefix = getNsPrefix(parentElement.name);
}
}
return mergeNsAndName(prefix, localName);
}
function lastOnStack(stack: any[], element: any): boolean {
return stack.length > 0 && stack[stack.length - 1] === element;
}

View File

@ -6,299 +6,37 @@
* found in the LICENSE file at https://angular.io/license
*/
import {normalizeBool, RegExpWrapper,} from '../facade/lang';
import {TagContentType, TagDefinition} from './tags';
// see http://www.w3.org/TR/html51/syntax.html#named-character-references
// see https://html.spec.whatwg.org/multipage/entities.json
// This list is not exhaustive to keep the compiler footprint low.
// The `&#123;` / `&#x1ab;` syntax should be used when the named character reference does not exist.
export const NAMED_ENTITIES = /*@ts2dart_const*/ {
'Aacute': '\u00C1',
'aacute': '\u00E1',
'Acirc': '\u00C2',
'acirc': '\u00E2',
'acute': '\u00B4',
'AElig': '\u00C6',
'aelig': '\u00E6',
'Agrave': '\u00C0',
'agrave': '\u00E0',
'alefsym': '\u2135',
'Alpha': '\u0391',
'alpha': '\u03B1',
'amp': '&',
'and': '\u2227',
'ang': '\u2220',
'apos': '\u0027',
'Aring': '\u00C5',
'aring': '\u00E5',
'asymp': '\u2248',
'Atilde': '\u00C3',
'atilde': '\u00E3',
'Auml': '\u00C4',
'auml': '\u00E4',
'bdquo': '\u201E',
'Beta': '\u0392',
'beta': '\u03B2',
'brvbar': '\u00A6',
'bull': '\u2022',
'cap': '\u2229',
'Ccedil': '\u00C7',
'ccedil': '\u00E7',
'cedil': '\u00B8',
'cent': '\u00A2',
'Chi': '\u03A7',
'chi': '\u03C7',
'circ': '\u02C6',
'clubs': '\u2663',
'cong': '\u2245',
'copy': '\u00A9',
'crarr': '\u21B5',
'cup': '\u222A',
'curren': '\u00A4',
'dagger': '\u2020',
'Dagger': '\u2021',
'darr': '\u2193',
'dArr': '\u21D3',
'deg': '\u00B0',
'Delta': '\u0394',
'delta': '\u03B4',
'diams': '\u2666',
'divide': '\u00F7',
'Eacute': '\u00C9',
'eacute': '\u00E9',
'Ecirc': '\u00CA',
'ecirc': '\u00EA',
'Egrave': '\u00C8',
'egrave': '\u00E8',
'empty': '\u2205',
'emsp': '\u2003',
'ensp': '\u2002',
'Epsilon': '\u0395',
'epsilon': '\u03B5',
'equiv': '\u2261',
'Eta': '\u0397',
'eta': '\u03B7',
'ETH': '\u00D0',
'eth': '\u00F0',
'Euml': '\u00CB',
'euml': '\u00EB',
'euro': '\u20AC',
'exist': '\u2203',
'fnof': '\u0192',
'forall': '\u2200',
'frac12': '\u00BD',
'frac14': '\u00BC',
'frac34': '\u00BE',
'frasl': '\u2044',
'Gamma': '\u0393',
'gamma': '\u03B3',
'ge': '\u2265',
'gt': '>',
'harr': '\u2194',
'hArr': '\u21D4',
'hearts': '\u2665',
'hellip': '\u2026',
'Iacute': '\u00CD',
'iacute': '\u00ED',
'Icirc': '\u00CE',
'icirc': '\u00EE',
'iexcl': '\u00A1',
'Igrave': '\u00CC',
'igrave': '\u00EC',
'image': '\u2111',
'infin': '\u221E',
'int': '\u222B',
'Iota': '\u0399',
'iota': '\u03B9',
'iquest': '\u00BF',
'isin': '\u2208',
'Iuml': '\u00CF',
'iuml': '\u00EF',
'Kappa': '\u039A',
'kappa': '\u03BA',
'Lambda': '\u039B',
'lambda': '\u03BB',
'lang': '\u27E8',
'laquo': '\u00AB',
'larr': '\u2190',
'lArr': '\u21D0',
'lceil': '\u2308',
'ldquo': '\u201C',
'le': '\u2264',
'lfloor': '\u230A',
'lowast': '\u2217',
'loz': '\u25CA',
'lrm': '\u200E',
'lsaquo': '\u2039',
'lsquo': '\u2018',
'lt': '<',
'macr': '\u00AF',
'mdash': '\u2014',
'micro': '\u00B5',
'middot': '\u00B7',
'minus': '\u2212',
'Mu': '\u039C',
'mu': '\u03BC',
'nabla': '\u2207',
'nbsp': '\u00A0',
'ndash': '\u2013',
'ne': '\u2260',
'ni': '\u220B',
'not': '\u00AC',
'notin': '\u2209',
'nsub': '\u2284',
'Ntilde': '\u00D1',
'ntilde': '\u00F1',
'Nu': '\u039D',
'nu': '\u03BD',
'Oacute': '\u00D3',
'oacute': '\u00F3',
'Ocirc': '\u00D4',
'ocirc': '\u00F4',
'OElig': '\u0152',
'oelig': '\u0153',
'Ograve': '\u00D2',
'ograve': '\u00F2',
'oline': '\u203E',
'Omega': '\u03A9',
'omega': '\u03C9',
'Omicron': '\u039F',
'omicron': '\u03BF',
'oplus': '\u2295',
'or': '\u2228',
'ordf': '\u00AA',
'ordm': '\u00BA',
'Oslash': '\u00D8',
'oslash': '\u00F8',
'Otilde': '\u00D5',
'otilde': '\u00F5',
'otimes': '\u2297',
'Ouml': '\u00D6',
'ouml': '\u00F6',
'para': '\u00B6',
'permil': '\u2030',
'perp': '\u22A5',
'Phi': '\u03A6',
'phi': '\u03C6',
'Pi': '\u03A0',
'pi': '\u03C0',
'piv': '\u03D6',
'plusmn': '\u00B1',
'pound': '\u00A3',
'prime': '\u2032',
'Prime': '\u2033',
'prod': '\u220F',
'prop': '\u221D',
'Psi': '\u03A8',
'psi': '\u03C8',
'quot': '\u0022',
'radic': '\u221A',
'rang': '\u27E9',
'raquo': '\u00BB',
'rarr': '\u2192',
'rArr': '\u21D2',
'rceil': '\u2309',
'rdquo': '\u201D',
'real': '\u211C',
'reg': '\u00AE',
'rfloor': '\u230B',
'Rho': '\u03A1',
'rho': '\u03C1',
'rlm': '\u200F',
'rsaquo': '\u203A',
'rsquo': '\u2019',
'sbquo': '\u201A',
'Scaron': '\u0160',
'scaron': '\u0161',
'sdot': '\u22C5',
'sect': '\u00A7',
'shy': '\u00AD',
'Sigma': '\u03A3',
'sigma': '\u03C3',
'sigmaf': '\u03C2',
'sim': '\u223C',
'spades': '\u2660',
'sub': '\u2282',
'sube': '\u2286',
'sum': '\u2211',
'sup': '\u2283',
'sup1': '\u00B9',
'sup2': '\u00B2',
'sup3': '\u00B3',
'supe': '\u2287',
'szlig': '\u00DF',
'Tau': '\u03A4',
'tau': '\u03C4',
'there4': '\u2234',
'Theta': '\u0398',
'theta': '\u03B8',
'thetasym': '\u03D1',
'thinsp': '\u2009',
'THORN': '\u00DE',
'thorn': '\u00FE',
'tilde': '\u02DC',
'times': '\u00D7',
'trade': '\u2122',
'Uacute': '\u00DA',
'uacute': '\u00FA',
'uarr': '\u2191',
'uArr': '\u21D1',
'Ucirc': '\u00DB',
'ucirc': '\u00FB',
'Ugrave': '\u00D9',
'ugrave': '\u00F9',
'uml': '\u00A8',
'upsih': '\u03D2',
'Upsilon': '\u03A5',
'upsilon': '\u03C5',
'Uuml': '\u00DC',
'uuml': '\u00FC',
'weierp': '\u2118',
'Xi': '\u039E',
'xi': '\u03BE',
'Yacute': '\u00DD',
'yacute': '\u00FD',
'yen': '\u00A5',
'yuml': '\u00FF',
'Yuml': '\u0178',
'Zeta': '\u0396',
'zeta': '\u03B6',
'zwj': '\u200D',
'zwnj': '\u200C',
};
export enum HtmlTagContentType {
RAW_TEXT,
ESCAPABLE_RAW_TEXT,
PARSABLE_DATA
}
export class HtmlTagDefinition {
export class HtmlTagDefinition implements TagDefinition {
private closedByChildren: {[key: string]: boolean} = {};
public closedByParent: boolean = false;
public requiredParents: {[key: string]: boolean};
public parentToAdd: string;
public implicitNamespacePrefix: string;
public contentType: HtmlTagContentType;
public isVoid: boolean;
public ignoreFirstLf: boolean;
closedByParent: boolean = false;
requiredParents: {[key: string]: boolean};
parentToAdd: string;
implicitNamespacePrefix: string;
contentType: TagContentType;
isVoid: boolean;
ignoreFirstLf: boolean;
canSelfClose: boolean = false;
constructor(
{closedByChildren, requiredParents, implicitNamespacePrefix, contentType, closedByParent,
isVoid, ignoreFirstLf}: {
{closedByChildren, requiredParents, implicitNamespacePrefix,
contentType = TagContentType.PARSABLE_DATA, closedByParent = false, isVoid = false,
ignoreFirstLf = false}: {
closedByChildren?: string[],
closedByParent?: boolean,
requiredParents?: string[],
implicitNamespacePrefix?: string,
contentType?: HtmlTagContentType,
contentType?: TagContentType,
isVoid?: boolean,
ignoreFirstLf?: boolean
} = {}) {
if (closedByChildren && closedByChildren.length > 0) {
closedByChildren.forEach(tagName => this.closedByChildren[tagName] = true);
}
this.isVoid = normalizeBool(isVoid);
this.closedByParent = normalizeBool(closedByParent) || this.isVoid;
this.isVoid = isVoid;
this.closedByParent = closedByParent || isVoid;
if (requiredParents && requiredParents.length > 0) {
this.requiredParents = {};
// The first parent is the list is automatically when none of the listed parents are present
@ -306,8 +44,8 @@ export class HtmlTagDefinition {
requiredParents.forEach(tagName => this.requiredParents[tagName] = true);
}
this.implicitNamespacePrefix = implicitNamespacePrefix;
this.contentType = contentType || HtmlTagContentType.PARSABLE_DATA;
this.ignoreFirstLf = normalizeBool(ignoreFirstLf);
this.contentType = contentType;
this.ignoreFirstLf = ignoreFirstLf;
}
requireExtraParent(currentParent: string): boolean {
@ -324,13 +62,13 @@ export class HtmlTagDefinition {
}
isClosedByChild(name: string): boolean {
return this.isVoid || normalizeBool(this.closedByChildren[name.toLowerCase()]);
return this.isVoid || name.toLowerCase() in this.closedByChildren;
}
}
// see http://www.w3.org/TR/html51/syntax.html#optional-tags
// This implementation does not fully conform to the HTML5 spec.
var TAG_DEFINITIONS: {[key: string]: HtmlTagDefinition} = {
const TAG_DEFINITIONS: {[key: string]: HtmlTagDefinition} = {
'base': new HtmlTagDefinition({isVoid: true}),
'meta': new HtmlTagDefinition({isVoid: true}),
'area': new HtmlTagDefinition({isVoid: true}),
@ -376,11 +114,11 @@ var TAG_DEFINITIONS: {[key: string]: HtmlTagDefinition} = {
'option': new HtmlTagDefinition({closedByChildren: ['option', 'optgroup'], closedByParent: true}),
'pre': new HtmlTagDefinition({ignoreFirstLf: true}),
'listing': new HtmlTagDefinition({ignoreFirstLf: true}),
'style': new HtmlTagDefinition({contentType: HtmlTagContentType.RAW_TEXT}),
'script': new HtmlTagDefinition({contentType: HtmlTagContentType.RAW_TEXT}),
'title': new HtmlTagDefinition({contentType: HtmlTagContentType.ESCAPABLE_RAW_TEXT}),
'textarea': new HtmlTagDefinition(
{contentType: HtmlTagContentType.ESCAPABLE_RAW_TEXT, ignoreFirstLf: true}),
'style': new HtmlTagDefinition({contentType: TagContentType.RAW_TEXT}),
'script': new HtmlTagDefinition({contentType: TagContentType.RAW_TEXT}),
'title': new HtmlTagDefinition({contentType: TagContentType.ESCAPABLE_RAW_TEXT}),
'textarea':
new HtmlTagDefinition({contentType: TagContentType.ESCAPABLE_RAW_TEXT, ignoreFirstLf: true}),
};
const _DEFAULT_TAG_DEFINITION = new HtmlTagDefinition();
@ -388,21 +126,3 @@ const _DEFAULT_TAG_DEFINITION = new HtmlTagDefinition();
export function getHtmlTagDefinition(tagName: string): HtmlTagDefinition {
return TAG_DEFINITIONS[tagName.toLowerCase()] || _DEFAULT_TAG_DEFINITION;
}
const _NS_PREFIX_RE = /^:([^:]+):(.+)/g;
export function splitNsName(elementName: string): [string, string] {
if (elementName[0] != ':') {
return [null, elementName];
}
const match = RegExpWrapper.firstMatch(_NS_PREFIX_RE, elementName);
return [match[1], match[2]];
}
export function getNsPrefix(elementName: string): string {
return splitNsName(elementName)[0];
}
export function mergeNsAndName(prefix: string, localName: string): string {
return prefix ? `:${prefix}:${localName}` : localName;
}

View File

@ -8,8 +8,7 @@
import {ParseError, ParseSourceSpan} from '../parse_util';
import {HtmlAst, HtmlAstVisitor, HtmlAttrAst, HtmlCommentAst, HtmlElementAst, HtmlExpansionAst, HtmlExpansionCaseAst, HtmlTextAst, htmlVisitAll} from './html_ast';
import * as html from './ast';
// http://cldr.unicode.org/index/cldr-spec/plural-rules
const PLURAL_CASES: string[] = ['zero', 'one', 'two', 'few', 'many', 'other'];
@ -37,13 +36,13 @@ const PLURAL_CASES: string[] = ['zero', 'one', 'two', 'few', 'many', 'other'];
* </ng-container>
* ```
*/
export function expandNodes(nodes: HtmlAst[]): ExpansionResult {
export function expandNodes(nodes: html.Node[]): ExpansionResult {
const expander = new _Expander();
return new ExpansionResult(htmlVisitAll(expander, nodes), expander.isExpanded, expander.errors);
return new ExpansionResult(html.visitAll(expander, nodes), expander.isExpanded, expander.errors);
}
export class ExpansionResult {
constructor(public nodes: HtmlAst[], public expanded: boolean, public errors: ParseError[]) {}
constructor(public nodes: html.Node[], public expanded: boolean, public errors: ParseError[]) {}
}
export class ExpansionError extends ParseError {
@ -55,34 +54,34 @@ export class ExpansionError extends ParseError {
*
* @internal
*/
class _Expander implements HtmlAstVisitor {
class _Expander implements html.Visitor {
isExpanded: boolean = false;
errors: ParseError[] = [];
visitElement(ast: HtmlElementAst, context: any): any {
return new HtmlElementAst(
ast.name, ast.attrs, htmlVisitAll(this, ast.children), ast.sourceSpan, ast.startSourceSpan,
ast.endSourceSpan);
visitElement(element: html.Element, context: any): any {
return new html.Element(
element.name, element.attrs, html.visitAll(this, element.children), element.sourceSpan,
element.startSourceSpan, element.endSourceSpan);
}
visitAttr(ast: HtmlAttrAst, context: any): any { return ast; }
visitAttribute(attribute: html.Attribute, context: any): any { return attribute; }
visitText(ast: HtmlTextAst, context: any): any { return ast; }
visitText(text: html.Text, context: any): any { return text; }
visitComment(ast: HtmlCommentAst, context: any): any { return ast; }
visitComment(comment: html.Comment, context: any): any { return comment; }
visitExpansion(ast: HtmlExpansionAst, context: any): any {
visitExpansion(icu: html.Expansion, context: any): any {
this.isExpanded = true;
return ast.type == 'plural' ? _expandPluralForm(ast, this.errors) :
_expandDefaultForm(ast, this.errors);
return icu.type == 'plural' ? _expandPluralForm(icu, this.errors) :
_expandDefaultForm(icu, this.errors);
}
visitExpansionCase(ast: HtmlExpansionCaseAst, context: any): any {
visitExpansionCase(icuCase: html.ExpansionCase, context: any): any {
throw new Error('Should not be reached');
}
}
function _expandPluralForm(ast: HtmlExpansionAst, errors: ParseError[]): HtmlElementAst {
function _expandPluralForm(ast: html.Expansion, errors: ParseError[]): html.Element {
const children = ast.cases.map(c => {
if (PLURAL_CASES.indexOf(c.value) == -1 && !c.value.match(/^=\d+$/)) {
errors.push(new ExpansionError(
@ -93,25 +92,25 @@ function _expandPluralForm(ast: HtmlExpansionAst, errors: ParseError[]): HtmlEle
const expansionResult = expandNodes(c.expression);
errors.push(...expansionResult.errors);
return new HtmlElementAst(
`template`, [new HtmlAttrAst('ngPluralCase', `${c.value}`, c.valueSourceSpan)],
return new html.Element(
`template`, [new html.Attribute('ngPluralCase', `${c.value}`, c.valueSourceSpan)],
expansionResult.nodes, c.sourceSpan, c.sourceSpan, c.sourceSpan);
});
const switchAttr = new HtmlAttrAst('[ngPlural]', ast.switchValue, ast.switchValueSourceSpan);
return new HtmlElementAst(
const switchAttr = new html.Attribute('[ngPlural]', ast.switchValue, ast.switchValueSourceSpan);
return new html.Element(
'ng-container', [switchAttr], children, ast.sourceSpan, ast.sourceSpan, ast.sourceSpan);
}
function _expandDefaultForm(ast: HtmlExpansionAst, errors: ParseError[]): HtmlElementAst {
function _expandDefaultForm(ast: html.Expansion, errors: ParseError[]): html.Element {
let children = ast.cases.map(c => {
const expansionResult = expandNodes(c.expression);
errors.push(...expansionResult.errors);
return new HtmlElementAst(
`template`, [new HtmlAttrAst('ngSwitchCase', `${c.value}`, c.valueSourceSpan)],
return new html.Element(
`template`, [new html.Attribute('ngSwitchCase', `${c.value}`, c.valueSourceSpan)],
expansionResult.nodes, c.sourceSpan, c.sourceSpan, c.sourceSpan);
});
const switchAttr = new HtmlAttrAst('[ngSwitch]', ast.switchValue, ast.switchValueSourceSpan);
return new HtmlElementAst(
const switchAttr = new html.Attribute('[ngSwitch]', ast.switchValue, ast.switchValueSourceSpan);
return new html.Element(
'ng-container', [switchAttr], children, ast.sourceSpan, ast.sourceSpan, ast.sourceSpan);
}

View File

@ -7,13 +7,12 @@
*/
import * as chars from '../chars';
import {isBlank, isPresent} from '../facade/lang';
import {ParseError, ParseLocation, ParseSourceFile, ParseSourceSpan} from '../parse_util';
import {HtmlTagContentType, NAMED_ENTITIES, getHtmlTagDefinition} from './html_tags';
import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from './interpolation_config';
import {NAMED_ENTITIES, TagContentType, TagDefinition} from './tags';
export enum HtmlTokenType {
export enum TokenType {
TAG_OPEN_START,
TAG_OPEN_END,
TAG_OPEN_END_VOID,
@ -36,26 +35,26 @@ export enum HtmlTokenType {
EOF
}
export class HtmlToken {
constructor(
public type: HtmlTokenType, public parts: string[], public sourceSpan: ParseSourceSpan) {}
export class Token {
constructor(public type: TokenType, public parts: string[], public sourceSpan: ParseSourceSpan) {}
}
export class HtmlTokenError extends ParseError {
constructor(errorMsg: string, public tokenType: HtmlTokenType, span: ParseSourceSpan) {
export class TokenError extends ParseError {
constructor(errorMsg: string, public tokenType: TokenType, span: ParseSourceSpan) {
super(span, errorMsg);
}
}
export class HtmlTokenizeResult {
constructor(public tokens: HtmlToken[], public errors: HtmlTokenError[]) {}
export class TokenizeResult {
constructor(public tokens: Token[], public errors: TokenError[]) {}
}
export function tokenizeHtml(
sourceContent: string, sourceUrl: string, tokenizeExpansionForms: boolean = false,
interpolationConfig: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG): HtmlTokenizeResult {
return new _HtmlTokenizer(
new ParseSourceFile(sourceContent, sourceUrl), tokenizeExpansionForms,
export function tokenize(
source: string, url: string, getTagDefinition: (tagName: string) => TagDefinition,
tokenizeExpansionForms: boolean = false,
interpolationConfig: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG): TokenizeResult {
return new _Tokenizer(
new ParseSourceFile(source, url), getTagDefinition, tokenizeExpansionForms,
interpolationConfig)
.tokenize();
}
@ -72,11 +71,11 @@ function _unknownEntityErrorMsg(entitySrc: string): string {
}
class _ControlFlowError {
constructor(public error: HtmlTokenError) {}
constructor(public error: TokenError) {}
}
// See http://www.w3.org/TR/html51/syntax.html#writing
class _HtmlTokenizer {
class _Tokenizer {
private _input: string;
private _length: number;
// Note: this is always lowercase!
@ -86,20 +85,22 @@ class _HtmlTokenizer {
private _line: number = 0;
private _column: number = -1;
private _currentTokenStart: ParseLocation;
private _currentTokenType: HtmlTokenType;
private _expansionCaseStack: HtmlTokenType[] = [];
private _currentTokenType: TokenType;
private _expansionCaseStack: TokenType[] = [];
private _inInterpolation: boolean = false;
tokens: HtmlToken[] = [];
errors: HtmlTokenError[] = [];
tokens: Token[] = [];
errors: TokenError[] = [];
/**
* @param _file The html source
* @param _getTagDefinition
* @param _tokenizeIcu Whether to tokenize ICU messages (considered as text nodes when false)
* @param _interpolationConfig
*/
constructor(
private _file: ParseSourceFile, private _tokenizeIcu: boolean,
private _file: ParseSourceFile, private _getTagDefinition: (tagName: string) => TagDefinition,
private _tokenizeIcu: boolean,
private _interpolationConfig: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG) {
this._input = _file.content;
this._length = _file.content.length;
@ -114,7 +115,7 @@ class _HtmlTokenizer {
return content.replace(_CR_OR_CRLF_REGEXP, '\n');
}
tokenize(): HtmlTokenizeResult {
tokenize(): TokenizeResult {
while (this._peek !== chars.$EOF) {
const start = this._getLocation();
try {
@ -143,9 +144,9 @@ class _HtmlTokenizer {
}
}
}
this._beginToken(HtmlTokenType.EOF);
this._beginToken(TokenType.EOF);
this._endToken([]);
return new HtmlTokenizeResult(mergeTextTokens(this.tokens), this.errors);
return new TokenizeResult(mergeTextTokens(this.tokens), this.errors);
}
/**
@ -188,14 +189,14 @@ class _HtmlTokenizer {
return new ParseSourceSpan(start, end);
}
private _beginToken(type: HtmlTokenType, start: ParseLocation = this._getLocation()) {
private _beginToken(type: TokenType, start: ParseLocation = this._getLocation()) {
this._currentTokenStart = start;
this._currentTokenType = type;
}
private _endToken(parts: string[], end: ParseLocation = this._getLocation()): HtmlToken {
const token = new HtmlToken(
this._currentTokenType, parts, new ParseSourceSpan(this._currentTokenStart, end));
private _endToken(parts: string[], end: ParseLocation = this._getLocation()): Token {
const token =
new Token(this._currentTokenType, parts, new ParseSourceSpan(this._currentTokenStart, end));
this.tokens.push(token);
this._currentTokenStart = null;
this._currentTokenType = null;
@ -204,9 +205,9 @@ class _HtmlTokenizer {
private _createError(msg: string, span: ParseSourceSpan): _ControlFlowError {
if (this._isInExpansionForm()) {
msg += ' (Do you have an unescaped "{" in your template?).';
msg += ` (Do you have an unescaped "{" in your template? Use "{{ '{' }}") to escape it.)`;
}
const error = new HtmlTokenError(msg, this._currentTokenType, span);
const error = new TokenError(msg, this._currentTokenType, span);
this._currentTokenStart = null;
this._currentTokenType = null;
return new _ControlFlowError(error);
@ -343,9 +344,9 @@ class _HtmlTokenizer {
return '&';
}
this._advance();
let name = this._input.substring(start.offset + 1, this._index - 1);
let char = (NAMED_ENTITIES as any)[name];
if (isBlank(char)) {
const name = this._input.substring(start.offset + 1, this._index - 1);
const char = NAMED_ENTITIES[name];
if (!char) {
throw this._createError(_unknownEntityErrorMsg(name), this._getSpan(start));
}
return char;
@ -353,11 +354,10 @@ class _HtmlTokenizer {
}
private _consumeRawText(
decodeEntities: boolean, firstCharOfEnd: number, attemptEndRest: () => boolean): HtmlToken {
decodeEntities: boolean, firstCharOfEnd: number, attemptEndRest: () => boolean): Token {
let tagCloseStart: ParseLocation;
const textStart = this._getLocation();
this._beginToken(
decodeEntities ? HtmlTokenType.ESCAPABLE_RAW_TEXT : HtmlTokenType.RAW_TEXT, textStart);
this._beginToken(decodeEntities ? TokenType.ESCAPABLE_RAW_TEXT : TokenType.RAW_TEXT, textStart);
const parts: string[] = [];
while (true) {
tagCloseStart = this._getLocation();
@ -376,25 +376,25 @@ class _HtmlTokenizer {
}
private _consumeComment(start: ParseLocation) {
this._beginToken(HtmlTokenType.COMMENT_START, start);
this._beginToken(TokenType.COMMENT_START, start);
this._requireCharCode(chars.$MINUS);
this._endToken([]);
const textToken = this._consumeRawText(false, chars.$MINUS, () => this._attemptStr('->'));
this._beginToken(HtmlTokenType.COMMENT_END, textToken.sourceSpan.end);
this._beginToken(TokenType.COMMENT_END, textToken.sourceSpan.end);
this._endToken([]);
}
private _consumeCdata(start: ParseLocation) {
this._beginToken(HtmlTokenType.CDATA_START, start);
this._beginToken(TokenType.CDATA_START, start);
this._requireStr('CDATA[');
this._endToken([]);
const textToken = this._consumeRawText(false, chars.$RBRACKET, () => this._attemptStr(']>'));
this._beginToken(HtmlTokenType.CDATA_END, textToken.sourceSpan.end);
this._beginToken(TokenType.CDATA_END, textToken.sourceSpan.end);
this._endToken([]);
}
private _consumeDocType(start: ParseLocation) {
this._beginToken(HtmlTokenType.DOC_TYPE, start);
this._beginToken(TokenType.DOC_TYPE, start);
this._attemptUntilChar(chars.$GT);
this._advance();
this._endToken([this._input.substring(start.offset + 2, this._index - 1)]);
@ -421,6 +421,7 @@ class _HtmlTokenizer {
private _consumeTagOpen(start: ParseLocation) {
let savedPos = this._savePosition();
let tagName: string;
let lowercaseTagName: string;
try {
if (!chars.isAsciiLetter(this._peek)) {
@ -428,7 +429,8 @@ class _HtmlTokenizer {
}
const nameStart = this._index;
this._consumeTagOpenStart(start);
lowercaseTagName = this._input.substring(nameStart, this._index).toLowerCase();
tagName = this._input.substring(nameStart, this._index);
lowercaseTagName = tagName.toLowerCase();
this._attemptCharCodeUntilFn(isNotWhitespace);
while (this._peek !== chars.$SLASH && this._peek !== chars.$GT) {
this._consumeAttributeName();
@ -445,7 +447,7 @@ class _HtmlTokenizer {
// When the start tag is invalid, assume we want a "<"
this._restorePosition(savedPos);
// Back to back text tokens are merged at the end
this._beginToken(HtmlTokenType.TEXT, start);
this._beginToken(TokenType.TEXT, start);
this._endToken(['<']);
return;
}
@ -453,10 +455,11 @@ class _HtmlTokenizer {
throw e;
}
const contentTokenType = getHtmlTagDefinition(lowercaseTagName).contentType;
if (contentTokenType === HtmlTagContentType.RAW_TEXT) {
const contentTokenType = this._getTagDefinition(tagName).contentType;
if (contentTokenType === TagContentType.RAW_TEXT) {
this._consumeRawTextWithTagClose(lowercaseTagName, false);
} else if (contentTokenType === HtmlTagContentType.ESCAPABLE_RAW_TEXT) {
} else if (contentTokenType === TagContentType.ESCAPABLE_RAW_TEXT) {
this._consumeRawTextWithTagClose(lowercaseTagName, true);
}
}
@ -469,24 +472,24 @@ class _HtmlTokenizer {
this._attemptCharCodeUntilFn(isNotWhitespace);
return this._attemptCharCode(chars.$GT);
});
this._beginToken(HtmlTokenType.TAG_CLOSE, textToken.sourceSpan.end);
this._beginToken(TokenType.TAG_CLOSE, textToken.sourceSpan.end);
this._endToken([null, lowercaseTagName]);
}
private _consumeTagOpenStart(start: ParseLocation) {
this._beginToken(HtmlTokenType.TAG_OPEN_START, start);
this._beginToken(TokenType.TAG_OPEN_START, start);
const parts = this._consumePrefixAndName();
this._endToken(parts);
}
private _consumeAttributeName() {
this._beginToken(HtmlTokenType.ATTR_NAME);
this._beginToken(TokenType.ATTR_NAME);
const prefixAndName = this._consumePrefixAndName();
this._endToken(prefixAndName);
}
private _consumeAttributeValue() {
this._beginToken(HtmlTokenType.ATTR_VALUE);
this._beginToken(TokenType.ATTR_VALUE);
var value: string;
if (this._peek === chars.$SQ || this._peek === chars.$DQ) {
var quoteChar = this._peek;
@ -506,15 +509,15 @@ class _HtmlTokenizer {
}
private _consumeTagOpenEnd() {
const tokenType = this._attemptCharCode(chars.$SLASH) ? HtmlTokenType.TAG_OPEN_END_VOID :
HtmlTokenType.TAG_OPEN_END;
const tokenType =
this._attemptCharCode(chars.$SLASH) ? TokenType.TAG_OPEN_END_VOID : TokenType.TAG_OPEN_END;
this._beginToken(tokenType);
this._requireCharCode(chars.$GT);
this._endToken([]);
}
private _consumeTagClose(start: ParseLocation) {
this._beginToken(HtmlTokenType.TAG_CLOSE, start);
this._beginToken(TokenType.TAG_CLOSE, start);
this._attemptCharCodeUntilFn(isNotWhitespace);
let prefixAndName = this._consumePrefixAndName();
this._attemptCharCodeUntilFn(isNotWhitespace);
@ -523,19 +526,19 @@ class _HtmlTokenizer {
}
private _consumeExpansionFormStart() {
this._beginToken(HtmlTokenType.EXPANSION_FORM_START, this._getLocation());
this._beginToken(TokenType.EXPANSION_FORM_START, this._getLocation());
this._requireCharCode(chars.$LBRACE);
this._endToken([]);
this._expansionCaseStack.push(HtmlTokenType.EXPANSION_FORM_START);
this._expansionCaseStack.push(TokenType.EXPANSION_FORM_START);
this._beginToken(HtmlTokenType.RAW_TEXT, this._getLocation());
this._beginToken(TokenType.RAW_TEXT, this._getLocation());
const condition = this._readUntil(chars.$COMMA);
this._endToken([condition], this._getLocation());
this._requireCharCode(chars.$COMMA);
this._attemptCharCodeUntilFn(isNotWhitespace);
this._beginToken(HtmlTokenType.RAW_TEXT, this._getLocation());
this._beginToken(TokenType.RAW_TEXT, this._getLocation());
let type = this._readUntil(chars.$COMMA);
this._endToken([type], this._getLocation());
this._requireCharCode(chars.$COMMA);
@ -543,21 +546,21 @@ class _HtmlTokenizer {
}
private _consumeExpansionCaseStart() {
this._beginToken(HtmlTokenType.EXPANSION_CASE_VALUE, this._getLocation());
this._beginToken(TokenType.EXPANSION_CASE_VALUE, this._getLocation());
const value = this._readUntil(chars.$LBRACE).trim();
this._endToken([value], this._getLocation());
this._attemptCharCodeUntilFn(isNotWhitespace);
this._beginToken(HtmlTokenType.EXPANSION_CASE_EXP_START, this._getLocation());
this._beginToken(TokenType.EXPANSION_CASE_EXP_START, this._getLocation());
this._requireCharCode(chars.$LBRACE);
this._endToken([], this._getLocation());
this._attemptCharCodeUntilFn(isNotWhitespace);
this._expansionCaseStack.push(HtmlTokenType.EXPANSION_CASE_EXP_START);
this._expansionCaseStack.push(TokenType.EXPANSION_CASE_EXP_START);
}
private _consumeExpansionCaseEnd() {
this._beginToken(HtmlTokenType.EXPANSION_CASE_EXP_END, this._getLocation());
this._beginToken(TokenType.EXPANSION_CASE_EXP_END, this._getLocation());
this._requireCharCode(chars.$RBRACE);
this._endToken([], this._getLocation());
this._attemptCharCodeUntilFn(isNotWhitespace);
@ -566,7 +569,7 @@ class _HtmlTokenizer {
}
private _consumeExpansionFormEnd() {
this._beginToken(HtmlTokenType.EXPANSION_FORM_END, this._getLocation());
this._beginToken(TokenType.EXPANSION_FORM_END, this._getLocation());
this._requireCharCode(chars.$RBRACE);
this._endToken([]);
@ -575,14 +578,16 @@ class _HtmlTokenizer {
private _consumeText() {
const start = this._getLocation();
this._beginToken(HtmlTokenType.TEXT, start);
this._beginToken(TokenType.TEXT, start);
const parts: string[] = [];
do {
if (this._attemptStr(this._interpolationConfig.start)) {
if (this._interpolationConfig && this._attemptStr(this._interpolationConfig.start)) {
parts.push(this._interpolationConfig.start);
this._inInterpolation = true;
} else if (this._attemptStr(this._interpolationConfig.end) && this._inInterpolation) {
} else if (
this._interpolationConfig && this._attemptStr(this._interpolationConfig.end) &&
this._inInterpolation) {
parts.push(this._interpolationConfig.end);
this._inInterpolation = false;
} else {
@ -638,13 +643,13 @@ class _HtmlTokenizer {
private _isInExpansionCase(): boolean {
return this._expansionCaseStack.length > 0 &&
this._expansionCaseStack[this._expansionCaseStack.length - 1] ===
HtmlTokenType.EXPANSION_CASE_EXP_START;
TokenType.EXPANSION_CASE_EXP_START;
}
private _isInExpansionForm(): boolean {
return this._expansionCaseStack.length > 0 &&
this._expansionCaseStack[this._expansionCaseStack.length - 1] ===
HtmlTokenType.EXPANSION_FORM_START;
TokenType.EXPANSION_FORM_START;
}
}
@ -672,8 +677,10 @@ function isNamedEntityEnd(code: number): boolean {
function isExpansionFormStart(
input: string, offset: number, interpolationConfig: InterpolationConfig): boolean {
return input.charCodeAt(offset) == chars.$LBRACE &&
input.indexOf(interpolationConfig.start, offset) != offset;
const isInterpolationStart =
interpolationConfig ? input.indexOf(interpolationConfig.start, offset) == offset : false;
return input.charCodeAt(offset) == chars.$LBRACE && !isInterpolationStart;
}
function isExpansionCaseStart(peek: number): boolean {
@ -688,13 +695,12 @@ function toUpperCaseCharCode(code: number): number {
return code >= chars.$a && code <= chars.$z ? code - chars.$a + chars.$A : code;
}
function mergeTextTokens(srcTokens: HtmlToken[]): HtmlToken[] {
let dstTokens: HtmlToken[] = [];
let lastDstToken: HtmlToken;
function mergeTextTokens(srcTokens: Token[]): Token[] {
let dstTokens: Token[] = [];
let lastDstToken: Token;
for (let i = 0; i < srcTokens.length; i++) {
let token = srcTokens[i];
if (isPresent(lastDstToken) && lastDstToken.type == HtmlTokenType.TEXT &&
token.type == HtmlTokenType.TEXT) {
if (lastDstToken && lastDstToken.type == TokenType.TEXT && token.type == TokenType.TEXT) {
lastDstToken.parts[0] += token.parts[0];
lastDstToken.sourceSpan.end = token.sourceSpan.end;
} else {

View File

@ -0,0 +1,412 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {isPresent, isBlank,} from '../facade/lang';
import {ListWrapper} from '../facade/collection';
import * as html from './ast';
import * as lex from './lexer';
import {ParseSourceSpan, ParseError} from '../parse_util';
import {TagDefinition, getNsPrefix, mergeNsAndName} from './tags';
import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from './interpolation_config';
export class TreeError extends ParseError {
static create(elementName: string, span: ParseSourceSpan, msg: string): TreeError {
return new TreeError(elementName, span, msg);
}
constructor(public elementName: string, span: ParseSourceSpan, msg: string) { super(span, msg); }
}
export class ParseTreeResult {
constructor(public rootNodes: html.Node[], public errors: ParseError[]) {}
}
export class Parser {
constructor(private _getTagDefinition: (tagName: string) => TagDefinition) {}
parse(
source: string, url: string, parseExpansionForms: boolean = false,
interpolationConfig: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG): ParseTreeResult {
const tokensAndErrors =
lex.tokenize(source, url, this._getTagDefinition, parseExpansionForms, interpolationConfig);
const treeAndErrors = new _TreeBuilder(tokensAndErrors.tokens, this._getTagDefinition).build();
return new ParseTreeResult(
treeAndErrors.rootNodes,
(<ParseError[]>tokensAndErrors.errors).concat(treeAndErrors.errors));
}
}
class _TreeBuilder {
private _index: number = -1;
private _peek: lex.Token;
private _rootNodes: html.Node[] = [];
private _errors: TreeError[] = [];
private _elementStack: html.Element[] = [];
constructor(
private tokens: lex.Token[], private getTagDefinition: (tagName: string) => TagDefinition) {
this._advance();
}
build(): ParseTreeResult {
while (this._peek.type !== lex.TokenType.EOF) {
if (this._peek.type === lex.TokenType.TAG_OPEN_START) {
this._consumeStartTag(this._advance());
} else if (this._peek.type === lex.TokenType.TAG_CLOSE) {
this._consumeEndTag(this._advance());
} else if (this._peek.type === lex.TokenType.CDATA_START) {
this._closeVoidElement();
this._consumeCdata(this._advance());
} else if (this._peek.type === lex.TokenType.COMMENT_START) {
this._closeVoidElement();
this._consumeComment(this._advance());
} else if (
this._peek.type === lex.TokenType.TEXT || this._peek.type === lex.TokenType.RAW_TEXT ||
this._peek.type === lex.TokenType.ESCAPABLE_RAW_TEXT) {
this._closeVoidElement();
this._consumeText(this._advance());
} else if (this._peek.type === lex.TokenType.EXPANSION_FORM_START) {
this._consumeExpansion(this._advance());
} else {
// Skip all other tokens...
this._advance();
}
}
return new ParseTreeResult(this._rootNodes, this._errors);
}
private _advance(): lex.Token {
const prev = this._peek;
if (this._index < this.tokens.length - 1) {
// Note: there is always an EOF token at the end
this._index++;
}
this._peek = this.tokens[this._index];
return prev;
}
private _advanceIf(type: lex.TokenType): lex.Token {
if (this._peek.type === type) {
return this._advance();
}
return null;
}
private _consumeCdata(startToken: lex.Token) {
this._consumeText(this._advance());
this._advanceIf(lex.TokenType.CDATA_END);
}
private _consumeComment(token: lex.Token) {
const text = this._advanceIf(lex.TokenType.RAW_TEXT);
this._advanceIf(lex.TokenType.COMMENT_END);
const value = isPresent(text) ? text.parts[0].trim() : null;
this._addToParent(new html.Comment(value, token.sourceSpan));
}
private _consumeExpansion(token: lex.Token) {
const switchValue = this._advance();
const type = this._advance();
const cases: html.ExpansionCase[] = [];
// read =
while (this._peek.type === lex.TokenType.EXPANSION_CASE_VALUE) {
let expCase = this._parseExpansionCase();
if (isBlank(expCase)) return; // error
cases.push(expCase);
}
// read the final }
if (this._peek.type !== lex.TokenType.EXPANSION_FORM_END) {
this._errors.push(
TreeError.create(null, this._peek.sourceSpan, `Invalid ICU message. Missing '}'.`));
return;
}
const sourceSpan = new ParseSourceSpan(token.sourceSpan.start, this._peek.sourceSpan.end);
this._addToParent(new html.Expansion(
switchValue.parts[0], type.parts[0], cases, sourceSpan, switchValue.sourceSpan));
this._advance();
}
private _parseExpansionCase(): html.ExpansionCase {
const value = this._advance();
// read {
if (this._peek.type !== lex.TokenType.EXPANSION_CASE_EXP_START) {
this._errors.push(
TreeError.create(null, this._peek.sourceSpan, `Invalid ICU message. Missing '{'.`));
return null;
}
// read until }
const start = this._advance();
const exp = this._collectExpansionExpTokens(start);
if (isBlank(exp)) return null;
const end = this._advance();
exp.push(new lex.Token(lex.TokenType.EOF, [], end.sourceSpan));
// parse everything in between { and }
const parsedExp = new _TreeBuilder(exp, this.getTagDefinition).build();
if (parsedExp.errors.length > 0) {
this._errors = this._errors.concat(<TreeError[]>parsedExp.errors);
return null;
}
const sourceSpan = new ParseSourceSpan(value.sourceSpan.start, end.sourceSpan.end);
const expSourceSpan = new ParseSourceSpan(start.sourceSpan.start, end.sourceSpan.end);
return new html.ExpansionCase(
value.parts[0], parsedExp.rootNodes, sourceSpan, value.sourceSpan, expSourceSpan);
}
private _collectExpansionExpTokens(start: lex.Token): lex.Token[] {
const exp: lex.Token[] = [];
const expansionFormStack = [lex.TokenType.EXPANSION_CASE_EXP_START];
while (true) {
if (this._peek.type === lex.TokenType.EXPANSION_FORM_START ||
this._peek.type === lex.TokenType.EXPANSION_CASE_EXP_START) {
expansionFormStack.push(this._peek.type);
}
if (this._peek.type === lex.TokenType.EXPANSION_CASE_EXP_END) {
if (lastOnStack(expansionFormStack, lex.TokenType.EXPANSION_CASE_EXP_START)) {
expansionFormStack.pop();
if (expansionFormStack.length == 0) return exp;
} else {
this._errors.push(
TreeError.create(null, start.sourceSpan, `Invalid ICU message. Missing '}'.`));
return null;
}
}
if (this._peek.type === lex.TokenType.EXPANSION_FORM_END) {
if (lastOnStack(expansionFormStack, lex.TokenType.EXPANSION_FORM_START)) {
expansionFormStack.pop();
} else {
this._errors.push(
TreeError.create(null, start.sourceSpan, `Invalid ICU message. Missing '}'.`));
return null;
}
}
if (this._peek.type === lex.TokenType.EOF) {
this._errors.push(
TreeError.create(null, start.sourceSpan, `Invalid ICU message. Missing '}'.`));
return null;
}
exp.push(this._advance());
}
}
private _consumeText(token: lex.Token) {
let text = token.parts[0];
if (text.length > 0 && text[0] == '\n') {
const parent = this._getParentElement();
if (isPresent(parent) && parent.children.length == 0 &&
this.getTagDefinition(parent.name).ignoreFirstLf) {
text = text.substring(1);
}
}
if (text.length > 0) {
this._addToParent(new html.Text(text, token.sourceSpan));
}
}
private _closeVoidElement(): void {
if (this._elementStack.length > 0) {
const el = ListWrapper.last(this._elementStack);
if (this.getTagDefinition(el.name).isVoid) {
this._elementStack.pop();
}
}
}
private _consumeStartTag(startTagToken: lex.Token) {
const prefix = startTagToken.parts[0];
const name = startTagToken.parts[1];
const attrs: html.Attribute[] = [];
while (this._peek.type === lex.TokenType.ATTR_NAME) {
attrs.push(this._consumeAttr(this._advance()));
}
const fullName = this._getElementFullName(prefix, name, this._getParentElement());
let selfClosing = false;
// Note: There could have been a tokenizer error
// so that we don't get a token for the end tag...
if (this._peek.type === lex.TokenType.TAG_OPEN_END_VOID) {
this._advance();
selfClosing = true;
const tagDef = this.getTagDefinition(fullName);
if (!(tagDef.canSelfClose || getNsPrefix(fullName) !== null || tagDef.isVoid)) {
this._errors.push(TreeError.create(
fullName, startTagToken.sourceSpan,
`Only void and foreign elements can be self closed "${startTagToken.parts[1]}"`));
}
} else if (this._peek.type === lex.TokenType.TAG_OPEN_END) {
this._advance();
selfClosing = false;
}
const end = this._peek.sourceSpan.start;
const span = new ParseSourceSpan(startTagToken.sourceSpan.start, end);
const el = new html.Element(fullName, attrs, [], span, span, null);
this._pushElement(el);
if (selfClosing) {
this._popElement(fullName);
el.endSourceSpan = span;
}
}
private _pushElement(el: html.Element) {
if (this._elementStack.length > 0) {
const parentEl = ListWrapper.last(this._elementStack);
if (this.getTagDefinition(parentEl.name).isClosedByChild(el.name)) {
this._elementStack.pop();
}
}
const tagDef = this.getTagDefinition(el.name);
const {parent, container} = this._getParentElementSkippingContainers();
if (isPresent(parent) && tagDef.requireExtraParent(parent.name)) {
const newParent = new html.Element(
tagDef.parentToAdd, [], [], el.sourceSpan, el.startSourceSpan, el.endSourceSpan);
this._insertBeforeContainer(parent, container, newParent);
}
this._addToParent(el);
this._elementStack.push(el);
}
private _consumeEndTag(endTagToken: lex.Token) {
const fullName = this._getElementFullName(
endTagToken.parts[0], endTagToken.parts[1], this._getParentElement());
if (this._getParentElement()) {
this._getParentElement().endSourceSpan = endTagToken.sourceSpan;
}
if (this.getTagDefinition(fullName).isVoid) {
this._errors.push(TreeError.create(
fullName, endTagToken.sourceSpan,
`Void elements do not have end tags "${endTagToken.parts[1]}"`));
} else if (!this._popElement(fullName)) {
this._errors.push(TreeError.create(
fullName, endTagToken.sourceSpan, `Unexpected closing tag "${endTagToken.parts[1]}"`));
}
}
private _popElement(fullName: string): boolean {
for (let stackIndex = this._elementStack.length - 1; stackIndex >= 0; stackIndex--) {
const el = this._elementStack[stackIndex];
if (el.name == fullName) {
ListWrapper.splice(this._elementStack, stackIndex, this._elementStack.length - stackIndex);
return true;
}
if (!this.getTagDefinition(el.name).closedByParent) {
return false;
}
}
return false;
}
private _consumeAttr(attrName: lex.Token): html.Attribute {
const fullName = mergeNsAndName(attrName.parts[0], attrName.parts[1]);
let end = attrName.sourceSpan.end;
let value = '';
if (this._peek.type === lex.TokenType.ATTR_VALUE) {
const valueToken = this._advance();
value = valueToken.parts[0];
end = valueToken.sourceSpan.end;
}
return new html.Attribute(fullName, value, new ParseSourceSpan(attrName.sourceSpan.start, end));
}
private _getParentElement(): html.Element {
return this._elementStack.length > 0 ? ListWrapper.last(this._elementStack) : null;
}
/**
* Returns the parent in the DOM and the container.
*
* `<ng-container>` elements are skipped as they are not rendered as DOM element.
*/
private _getParentElementSkippingContainers(): {parent: html.Element, container: html.Element} {
let container: html.Element = null;
for (let i = this._elementStack.length - 1; i >= 0; i--) {
if (this._elementStack[i].name !== 'ng-container') {
return {parent: this._elementStack[i], container};
}
container = this._elementStack[i];
}
return {parent: ListWrapper.last(this._elementStack), container};
}
private _addToParent(node: html.Node) {
const parent = this._getParentElement();
if (isPresent(parent)) {
parent.children.push(node);
} else {
this._rootNodes.push(node);
}
}
/**
* Insert a node between the parent and the container.
* When no container is given, the node is appended as a child of the parent.
* Also updates the element stack accordingly.
*
* @internal
*/
private _insertBeforeContainer(
parent: html.Element, container: html.Element, node: html.Element) {
if (!container) {
this._addToParent(node);
this._elementStack.push(node);
} else {
if (parent) {
// replace the container with the new node in the children
const index = parent.children.indexOf(container);
parent.children[index] = node;
} else {
this._rootNodes.push(node);
}
node.children.push(container);
this._elementStack.splice(this._elementStack.indexOf(container), 0, node);
}
}
private _getElementFullName(prefix: string, localName: string, parentElement: html.Element):
string {
if (isBlank(prefix)) {
prefix = this.getTagDefinition(localName).implicitNamespacePrefix;
if (isBlank(prefix) && isPresent(parentElement)) {
prefix = getNsPrefix(parentElement.name);
}
}
return mergeNsAndName(prefix, localName);
}
}
function lastOnStack(stack: any[], element: any): boolean {
return stack.length > 0 && stack[stack.length - 1] === element;
}

View File

@ -0,0 +1,310 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
export enum TagContentType {
RAW_TEXT,
ESCAPABLE_RAW_TEXT,
PARSABLE_DATA
}
// TODO(vicb): read-only when TS supports it
export interface TagDefinition {
closedByParent: boolean;
requiredParents: {[key: string]: boolean};
parentToAdd: string;
implicitNamespacePrefix: string;
contentType: TagContentType;
isVoid: boolean;
ignoreFirstLf: boolean;
canSelfClose: boolean;
requireExtraParent(currentParent: string): boolean;
isClosedByChild(name: string): boolean;
}
export function splitNsName(elementName: string): [string, string] {
if (elementName[0] != ':') {
return [null, elementName];
}
const parts = elementName.substring(1).split(':', 2);
if (parts.length != 2) {
throw new Error(`Unsupported format "${elementName}" expecting ":namespace:name"`);
}
return parts as[string, string];
}
export function getNsPrefix(fullName: string): string {
return fullName === null ? null : splitNsName(fullName)[0];
}
export function mergeNsAndName(prefix: string, localName: string): string {
return prefix ? `:${prefix}:${localName}` : localName;
}
// see http://www.w3.org/TR/html51/syntax.html#named-character-references
// see https://html.spec.whatwg.org/multipage/entities.json
// This list is not exhaustive to keep the compiler footprint low.
// The `&#123;` / `&#x1ab;` syntax should be used when the named character reference does not exist.
export const NAMED_ENTITIES: {[k: string]: string} = {
'Aacute': '\u00C1',
'aacute': '\u00E1',
'Acirc': '\u00C2',
'acirc': '\u00E2',
'acute': '\u00B4',
'AElig': '\u00C6',
'aelig': '\u00E6',
'Agrave': '\u00C0',
'agrave': '\u00E0',
'alefsym': '\u2135',
'Alpha': '\u0391',
'alpha': '\u03B1',
'amp': '&',
'and': '\u2227',
'ang': '\u2220',
'apos': '\u0027',
'Aring': '\u00C5',
'aring': '\u00E5',
'asymp': '\u2248',
'Atilde': '\u00C3',
'atilde': '\u00E3',
'Auml': '\u00C4',
'auml': '\u00E4',
'bdquo': '\u201E',
'Beta': '\u0392',
'beta': '\u03B2',
'brvbar': '\u00A6',
'bull': '\u2022',
'cap': '\u2229',
'Ccedil': '\u00C7',
'ccedil': '\u00E7',
'cedil': '\u00B8',
'cent': '\u00A2',
'Chi': '\u03A7',
'chi': '\u03C7',
'circ': '\u02C6',
'clubs': '\u2663',
'cong': '\u2245',
'copy': '\u00A9',
'crarr': '\u21B5',
'cup': '\u222A',
'curren': '\u00A4',
'dagger': '\u2020',
'Dagger': '\u2021',
'darr': '\u2193',
'dArr': '\u21D3',
'deg': '\u00B0',
'Delta': '\u0394',
'delta': '\u03B4',
'diams': '\u2666',
'divide': '\u00F7',
'Eacute': '\u00C9',
'eacute': '\u00E9',
'Ecirc': '\u00CA',
'ecirc': '\u00EA',
'Egrave': '\u00C8',
'egrave': '\u00E8',
'empty': '\u2205',
'emsp': '\u2003',
'ensp': '\u2002',
'Epsilon': '\u0395',
'epsilon': '\u03B5',
'equiv': '\u2261',
'Eta': '\u0397',
'eta': '\u03B7',
'ETH': '\u00D0',
'eth': '\u00F0',
'Euml': '\u00CB',
'euml': '\u00EB',
'euro': '\u20AC',
'exist': '\u2203',
'fnof': '\u0192',
'forall': '\u2200',
'frac12': '\u00BD',
'frac14': '\u00BC',
'frac34': '\u00BE',
'frasl': '\u2044',
'Gamma': '\u0393',
'gamma': '\u03B3',
'ge': '\u2265',
'gt': '>',
'harr': '\u2194',
'hArr': '\u21D4',
'hearts': '\u2665',
'hellip': '\u2026',
'Iacute': '\u00CD',
'iacute': '\u00ED',
'Icirc': '\u00CE',
'icirc': '\u00EE',
'iexcl': '\u00A1',
'Igrave': '\u00CC',
'igrave': '\u00EC',
'image': '\u2111',
'infin': '\u221E',
'int': '\u222B',
'Iota': '\u0399',
'iota': '\u03B9',
'iquest': '\u00BF',
'isin': '\u2208',
'Iuml': '\u00CF',
'iuml': '\u00EF',
'Kappa': '\u039A',
'kappa': '\u03BA',
'Lambda': '\u039B',
'lambda': '\u03BB',
'lang': '\u27E8',
'laquo': '\u00AB',
'larr': '\u2190',
'lArr': '\u21D0',
'lceil': '\u2308',
'ldquo': '\u201C',
'le': '\u2264',
'lfloor': '\u230A',
'lowast': '\u2217',
'loz': '\u25CA',
'lrm': '\u200E',
'lsaquo': '\u2039',
'lsquo': '\u2018',
'lt': '<',
'macr': '\u00AF',
'mdash': '\u2014',
'micro': '\u00B5',
'middot': '\u00B7',
'minus': '\u2212',
'Mu': '\u039C',
'mu': '\u03BC',
'nabla': '\u2207',
'nbsp': '\u00A0',
'ndash': '\u2013',
'ne': '\u2260',
'ni': '\u220B',
'not': '\u00AC',
'notin': '\u2209',
'nsub': '\u2284',
'Ntilde': '\u00D1',
'ntilde': '\u00F1',
'Nu': '\u039D',
'nu': '\u03BD',
'Oacute': '\u00D3',
'oacute': '\u00F3',
'Ocirc': '\u00D4',
'ocirc': '\u00F4',
'OElig': '\u0152',
'oelig': '\u0153',
'Ograve': '\u00D2',
'ograve': '\u00F2',
'oline': '\u203E',
'Omega': '\u03A9',
'omega': '\u03C9',
'Omicron': '\u039F',
'omicron': '\u03BF',
'oplus': '\u2295',
'or': '\u2228',
'ordf': '\u00AA',
'ordm': '\u00BA',
'Oslash': '\u00D8',
'oslash': '\u00F8',
'Otilde': '\u00D5',
'otilde': '\u00F5',
'otimes': '\u2297',
'Ouml': '\u00D6',
'ouml': '\u00F6',
'para': '\u00B6',
'permil': '\u2030',
'perp': '\u22A5',
'Phi': '\u03A6',
'phi': '\u03C6',
'Pi': '\u03A0',
'pi': '\u03C0',
'piv': '\u03D6',
'plusmn': '\u00B1',
'pound': '\u00A3',
'prime': '\u2032',
'Prime': '\u2033',
'prod': '\u220F',
'prop': '\u221D',
'Psi': '\u03A8',
'psi': '\u03C8',
'quot': '\u0022',
'radic': '\u221A',
'rang': '\u27E9',
'raquo': '\u00BB',
'rarr': '\u2192',
'rArr': '\u21D2',
'rceil': '\u2309',
'rdquo': '\u201D',
'real': '\u211C',
'reg': '\u00AE',
'rfloor': '\u230B',
'Rho': '\u03A1',
'rho': '\u03C1',
'rlm': '\u200F',
'rsaquo': '\u203A',
'rsquo': '\u2019',
'sbquo': '\u201A',
'Scaron': '\u0160',
'scaron': '\u0161',
'sdot': '\u22C5',
'sect': '\u00A7',
'shy': '\u00AD',
'Sigma': '\u03A3',
'sigma': '\u03C3',
'sigmaf': '\u03C2',
'sim': '\u223C',
'spades': '\u2660',
'sub': '\u2282',
'sube': '\u2286',
'sum': '\u2211',
'sup': '\u2283',
'sup1': '\u00B9',
'sup2': '\u00B2',
'sup3': '\u00B3',
'supe': '\u2287',
'szlig': '\u00DF',
'Tau': '\u03A4',
'tau': '\u03C4',
'there4': '\u2234',
'Theta': '\u0398',
'theta': '\u03B8',
'thetasym': '\u03D1',
'thinsp': '\u2009',
'THORN': '\u00DE',
'thorn': '\u00FE',
'tilde': '\u02DC',
'times': '\u00D7',
'trade': '\u2122',
'Uacute': '\u00DA',
'uacute': '\u00FA',
'uarr': '\u2191',
'uArr': '\u21D1',
'Ucirc': '\u00DB',
'ucirc': '\u00FB',
'Ugrave': '\u00D9',
'ugrave': '\u00F9',
'uml': '\u00A8',
'upsih': '\u03D2',
'Upsilon': '\u03A5',
'upsilon': '\u03C5',
'Uuml': '\u00DC',
'uuml': '\u00FC',
'weierp': '\u2118',
'Xi': '\u039E',
'xi': '\u03BE',
'Yacute': '\u00DD',
'yacute': '\u00FD',
'yen': '\u00A5',
'yuml': '\u00FF',
'Yuml': '\u0178',
'Zeta': '\u0396',
'zeta': '\u03B6',
'zwj': '\u200D',
'zwnj': '\u200C',
};

View File

@ -0,0 +1,20 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {ParseTreeResult, Parser} from './parser';
import {getXmlTagDefinition} from './xml_tags';
export {ParseTreeResult, TreeError} from './parser';
export class XmlParser extends Parser {
constructor() { super(getXmlTagDefinition); }
parse(source: string, url: string, parseExpansionForms: boolean = false): ParseTreeResult {
return super.parse(source, url, parseExpansionForms, null);
}
}

View File

@ -0,0 +1,30 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {TagContentType, TagDefinition} from './tags';
export class XmlTagDefinition implements TagDefinition {
closedByParent: boolean = false;
requiredParents: {[key: string]: boolean};
parentToAdd: string;
implicitNamespacePrefix: string;
contentType: TagContentType = TagContentType.PARSABLE_DATA;
isVoid: boolean = false;
ignoreFirstLf: boolean = false;
canSelfClose: boolean = true;
requireExtraParent(currentParent: string): boolean { return false; }
isClosedByChild(name: string): boolean { return false; }
}
const _TAG_DEFINITION = new XmlTagDefinition();
export function getXmlTagDefinition(tagName: string): XmlTagDefinition {
return _TAG_DEFINITION;
}

View File

@ -6,29 +6,28 @@
* found in the LICENSE file at https://angular.io/license
*/
import {HtmlAst, HtmlAstVisitor, HtmlAttrAst, HtmlCommentAst, HtmlElementAst, HtmlExpansionAst, HtmlExpansionCaseAst, HtmlTextAst} from '../html_parser/html_ast';
import * as html from '../html_parser/ast';
import {I18nError, I18N_ATTR_PREFIX, getI18nAttr, meaning, description, isOpeningComment, isClosingComment,} from './shared';
import {htmlVisitAll} from '../html_parser/html_ast';
export function extractAstMessages(
sourceAst: HtmlAst[], implicitTags: string[],
sourceAst: html.Node[], implicitTags: string[],
implicitAttrs: {[k: string]: string[]}): ExtractionResult {
const visitor = new _ExtractVisitor(implicitTags, implicitAttrs);
return visitor.extract(sourceAst);
}
export class ExtractionResult {
constructor(public messages: AstMessage[], public errors: I18nError[]) {}
constructor(public messages: Message[], public errors: I18nError[]) {}
}
class _ExtractVisitor implements HtmlAstVisitor {
class _ExtractVisitor implements html.Visitor {
// <el i18n>...</el>
private _inI18nNode = false;
private _depth: number = 0;
// <!--i18n-->...<!--/i18n-->
private _blockMeaningAndDesc: string;
private _blockChildren: HtmlAst[];
private _blockChildren: html.Node[];
private _blockStartDepth: number;
private _inI18nBlock: boolean;
@ -40,8 +39,8 @@ class _ExtractVisitor implements HtmlAstVisitor {
constructor(private _implicitTags: string[], private _implicitAttrs: {[k: string]: string[]}) {}
extract(source: HtmlAst[]): ExtractionResult {
const messages: AstMessage[] = [];
extract(nodes: html.Node[]): ExtractionResult {
const messages: Message[] = [];
this._inI18nBlock = false;
this._inI18nNode = false;
this._depth = 0;
@ -49,20 +48,20 @@ class _ExtractVisitor implements HtmlAstVisitor {
this._sectionStartIndex = void 0;
this._errors = [];
source.forEach(node => node.visit(this, messages));
nodes.forEach(node => node.visit(this, messages));
if (this._inI18nBlock) {
this._reportError(source[source.length - 1], 'Unclosed block');
this._reportError(nodes[nodes.length - 1], 'Unclosed block');
}
return new ExtractionResult(messages, this._errors);
}
visitExpansionCase(part: HtmlExpansionCaseAst, messages: AstMessage[]): any {
htmlVisitAll(this, part.expression, messages);
visitExpansionCase(icuCase: html.ExpansionCase, messages: Message[]): any {
html.visitAll(this, icuCase.expression, messages);
}
visitExpansion(icu: HtmlExpansionAst, messages: AstMessage[]): any {
visitExpansion(icu: html.Expansion, messages: Message[]): any {
this._mayBeAddBlockChildren(icu);
const wasInIcu = this._inIcu;
@ -74,12 +73,12 @@ class _ExtractVisitor implements HtmlAstVisitor {
this._inIcu = true;
}
htmlVisitAll(this, icu.cases, messages);
html.visitAll(this, icu.cases, messages);
this._inIcu = wasInIcu;
}
visitComment(comment: HtmlCommentAst, messages: AstMessage[]): any {
visitComment(comment: html.Comment, messages: Message[]): any {
const isOpening = isOpeningComment(comment);
if (isOpening && (this._inI18nBlock || this._inI18nNode)) {
@ -118,9 +117,9 @@ class _ExtractVisitor implements HtmlAstVisitor {
}
}
visitText(text: HtmlTextAst, messages: AstMessage[]): any { this._mayBeAddBlockChildren(text); }
visitText(text: html.Text, messages: Message[]): any { this._mayBeAddBlockChildren(text); }
visitElement(el: HtmlElementAst, messages: AstMessage[]): any {
visitElement(el: html.Element, messages: Message[]): any {
this._mayBeAddBlockChildren(el);
this._depth++;
const wasInI18nNode = this._inI18nNode;
@ -152,19 +151,21 @@ class _ExtractVisitor implements HtmlAstVisitor {
if (useSection) {
this._startSection(messages);
htmlVisitAll(this, el.children, messages);
html.visitAll(this, el.children, messages);
this._endSection(messages, el.children);
} else {
htmlVisitAll(this, el.children, messages);
html.visitAll(this, el.children, messages);
}
this._depth--;
this._inI18nNode = wasInI18nNode;
}
visitAttr(ast: HtmlAttrAst, messages: AstMessage[]): any { throw new Error('unreachable code'); }
visitAttribute(attribute: html.Attribute, messages: Message[]): any {
throw new Error('unreachable code');
}
private _extractFromAttributes(el: HtmlElementAst, messages: AstMessage[]): void {
private _extractFromAttributes(el: html.Element, messages: Message[]): void {
const explicitAttrNameToValue: Map<string, string> = new Map();
const implicitAttrNames: string[] = this._implicitAttrs[el.name] || [];
@ -182,13 +183,13 @@ class _ExtractVisitor implements HtmlAstVisitor {
});
}
private _addMessage(messages: AstMessage[], ast: HtmlAst[], meaningAndDesc?: string): void {
private _addMessage(messages: Message[], ast: html.Node[], meaningAndDesc?: string): void {
if (ast.length == 0 ||
ast.length == 1 && ast[0] instanceof HtmlAttrAst && !(<HtmlAttrAst>ast[0]).value) {
ast.length == 1 && ast[0] instanceof html.Attribute && !(<html.Attribute>ast[0]).value) {
// Do not create empty messages
return;
}
messages.push(new AstMessage(ast, meaning(meaningAndDesc), description(meaningAndDesc)));
messages.push(new Message(ast, meaning(meaningAndDesc), description(meaningAndDesc)));
}
/**
@ -197,16 +198,16 @@ class _ExtractVisitor implements HtmlAstVisitor {
* - we are not inside a ICU message (those are handled separately),
* - the node is a "direct child" of the block
*/
private _mayBeAddBlockChildren(ast: HtmlAst): void {
private _mayBeAddBlockChildren(node: html.Node): void {
if (this._inI18nBlock && !this._inIcu && this._depth == this._blockStartDepth) {
this._blockChildren.push(ast);
this._blockChildren.push(node);
}
}
/**
* Marks the start of a section, see `_endSection`
*/
private _startSection(messages: AstMessage[]): void {
private _startSection(messages: Message[]): void {
if (this._sectionStartIndex !== void 0) {
throw new Error('Unexpected section start');
}
@ -231,20 +232,20 @@ class _ExtractVisitor implements HtmlAstVisitor {
* Note that we should still keep messages extracted from attributes inside the section (ie in the
* ICU message here)
*/
private _endSection(messages: AstMessage[], directChildren: HtmlAst[]): void {
private _endSection(messages: Message[], directChildren: html.Node[]): void {
if (this._sectionStartIndex === void 0) {
throw new Error('Unexpected section end');
}
const startIndex = this._sectionStartIndex;
const significantChildren: number = directChildren.reduce(
(count: number, node: HtmlAst): number => count + (node instanceof HtmlCommentAst ? 0 : 1),
(count: number, node: html.Node): number => count + (node instanceof html.Comment ? 0 : 1),
0);
if (significantChildren == 1) {
for (let i = startIndex; i < messages.length; i++) {
let ast = messages[i].nodes;
if (!(ast.length == 1 && ast[0] instanceof HtmlAttrAst)) {
if (!(ast.length == 1 && ast[0] instanceof html.Attribute)) {
messages.splice(i, 1);
break;
}
@ -254,11 +255,14 @@ class _ExtractVisitor implements HtmlAstVisitor {
this._sectionStartIndex = void 0;
}
private _reportError(astNode: HtmlAst, msg: string): void {
this._errors.push(new I18nError(astNode.sourceSpan, msg));
private _reportError(node: html.Node, msg: string): void {
this._errors.push(new I18nError(node.sourceSpan, msg));
}
}
export class AstMessage {
constructor(public nodes: HtmlAst[], public meaning: string, public description: string) {}
/**
* A Message contain a fragment (= a subtree) of the source html AST.
*/
export class Message {
constructor(public nodes: html.Node[], public meaning: string, public description: string) {}
}

View File

@ -9,7 +9,9 @@
import {ParseSourceSpan} from '../parse_util';
export class Message {
constructor(public nodes: Node[], public meaning: string, public description: string) {}
constructor(
public nodes: Node[], public placeholders: {[name: string]: string}, public meaning: string,
public description: string) {}
}
export interface Node { visit(visitor: Visitor, context?: any): any; }

View File

@ -1,340 +0,0 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Parser as ExpressionParser} from '../expression_parser/parser';
import {ListWrapper, StringMapWrapper} from '../facade/collection';
import {BaseException} from '../facade/exceptions';
import {NumberWrapper, RegExpWrapper, isPresent} from '../facade/lang';
import {HtmlAst, HtmlAstVisitor, HtmlAttrAst, HtmlCommentAst, HtmlElementAst, HtmlExpansionAst, HtmlExpansionCaseAst, HtmlTextAst, htmlVisitAll} from '../html_parser/html_ast';
import {HtmlParseTreeResult, HtmlParser} from '../html_parser/html_parser';
import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from '../html_parser/interpolation_config';
import {ParseError, ParseSourceSpan} from '../parse_util';
import {Message, id} from './message';
import {I18N_ATTR, I18N_ATTR_PREFIX, I18nError, Part, dedupePhName, extractPhNameFromInterpolation, messageFromAttribute, messageFromI18nAttribute, partition} from './shared';
const _PLACEHOLDER_ELEMENT = 'ph';
const _NAME_ATTR = 'name';
const _PLACEHOLDER_EXPANDED_REGEXP = /<ph\s+name="(\w+)"><\/ph>/gi;
/**
* Creates an i18n-ed version of the parsed template.
*
* Algorithm:
*
* See `message_extractor.ts` for details on the partitioning algorithm.
*
* This is how the merging works:
*
* 1. Use the stringify function to get the message id. Look up the message in the map.
* 2. Get the translated message. At this point we have two trees: the original tree
* and the translated tree, where all the elements are replaced with placeholders.
* 3. Use the original tree to create a mapping Index:number -> HtmlAst.
* 4. Walk the translated tree.
* 5. If we encounter a placeholder element, get its name property.
* 6. Get the type and the index of the node using the name property.
* 7. If the type is 'e', which means element, then:
* - translate the attributes of the original element
* - recurse to merge the children
* - create a new element using the original element name, original position,
* and translated children and attributes
* 8. If the type if 't', which means text, then:
* - get the list of expressions from the original node.
* - get the string version of the interpolation subtree
* - find all the placeholders in the translated message, and replace them with the
* corresponding original expressions
*/
export class I18nHtmlParser implements HtmlParser {
private _errors: ParseError[];
private _interpolationConfig: InterpolationConfig;
constructor(
private _htmlParser: HtmlParser, public _expressionParser: ExpressionParser,
private _messagesContent: string, private _messages: {[key: string]: HtmlAst[]},
private _implicitTags: string[], private _implicitAttrs: {[k: string]: string[]}) {}
parse(
sourceContent: string, sourceUrl: string, parseExpansionForms: boolean = false,
interpolationConfig: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG):
HtmlParseTreeResult {
this._errors = [];
this._interpolationConfig = interpolationConfig;
let res = this._htmlParser.parse(sourceContent, sourceUrl, true, interpolationConfig);
if (res.errors.length > 0) {
return res;
}
const nodes = this._recurse(res.rootNodes);
return this._errors.length > 0 ? new HtmlParseTreeResult([], this._errors) :
new HtmlParseTreeResult(nodes, []);
}
// Merge the translation recursively
private _processI18nPart(part: Part): HtmlAst[] {
try {
return part.hasI18n ? this._mergeI18Part(part) : this._recurseIntoI18nPart(part);
} catch (e) {
if (e instanceof I18nError) {
this._errors.push(e);
return [];
} else {
throw e;
}
}
}
private _recurseIntoI18nPart(p: Part): HtmlAst[] {
// we found an element without an i18n attribute
// we need to recurse in case its children may have i18n set
// we also need to translate its attributes
if (isPresent(p.rootElement)) {
const root = p.rootElement;
const children = this._recurse(p.children);
const attrs = this._i18nAttributes(root);
return [new HtmlElementAst(
root.name, attrs, children, root.sourceSpan, root.startSourceSpan, root.endSourceSpan)];
}
if (isPresent(p.rootTextNode)) {
// a text node without i18n or interpolation, nothing to do
return [p.rootTextNode];
}
return this._recurse(p.children);
}
private _recurse(nodes: HtmlAst[]): HtmlAst[] {
let parts = partition(nodes, this._errors, this._implicitTags);
return ListWrapper.flatten(parts.map(p => this._processI18nPart(p)));
}
// Look for the translated message and merge it back to the tree
private _mergeI18Part(part: Part): HtmlAst[] {
let messages = part.createMessages(this._expressionParser, this._interpolationConfig);
// TODO - dirty smoke fix
let message = messages[0];
let messageId = id(message);
if (!StringMapWrapper.contains(this._messages, messageId)) {
throw new I18nError(
part.sourceSpan,
`Cannot find message for id '${messageId}', content '${message.content}'.`);
}
const translation = this._messages[messageId];
return this._mergeTrees(part, translation);
}
private _mergeTrees(part: Part, translation: HtmlAst[]): HtmlAst[] {
if (isPresent(part.rootTextNode)) {
// this should never happen with a part. Parts that have root text node should not be merged.
throw new BaseException('should not be reached');
}
const visitor = new _NodeMappingVisitor();
htmlVisitAll(visitor, part.children);
// merge the translated tree with the original tree.
// we do it by preserving the source code position of the original tree
const translatedAst = this._expandPlaceholders(translation, visitor.mapping);
// if the root element is present, we need to create a new root element with its attributes
// translated
if (part.rootElement) {
const root = part.rootElement;
const attrs = this._i18nAttributes(root);
return [new HtmlElementAst(
root.name, attrs, translatedAst, root.sourceSpan, root.startSourceSpan,
root.endSourceSpan)];
}
return translatedAst;
}
/**
* The translation AST is composed on text nodes and placeholder elements
*/
private _expandPlaceholders(translation: HtmlAst[], mapping: HtmlAst[]): HtmlAst[] {
return translation.map(node => {
if (node instanceof HtmlElementAst) {
// This node is a placeholder, replace with the original content
return this._expandPlaceholdersInNode(node, mapping);
}
if (node instanceof HtmlTextAst) {
return node;
}
throw new BaseException('should not be reached');
});
}
private _expandPlaceholdersInNode(node: HtmlElementAst, mapping: HtmlAst[]): HtmlAst {
let name = this._getName(node);
let index = NumberWrapper.parseInt(name.substring(1), 10);
let originalNode = mapping[index];
if (originalNode instanceof HtmlTextAst) {
return this._mergeTextInterpolation(node, originalNode);
}
if (originalNode instanceof HtmlElementAst) {
return this._mergeElement(node, originalNode, mapping);
}
throw new BaseException('should not be reached');
}
// Extract the value of a <ph> name attribute
private _getName(node: HtmlElementAst): string {
if (node.name != _PLACEHOLDER_ELEMENT) {
throw new I18nError(
node.sourceSpan,
`Unexpected tag "${node.name}". Only "${_PLACEHOLDER_ELEMENT}" tags are allowed.`);
}
const nameAttr = node.attrs.find(a => a.name == _NAME_ATTR);
if (nameAttr) {
return nameAttr.value;
}
throw new I18nError(node.sourceSpan, `Missing "${_NAME_ATTR}" attribute.`);
}
private _mergeTextInterpolation(node: HtmlElementAst, originalNode: HtmlTextAst): HtmlTextAst {
const split = this._expressionParser.splitInterpolation(
originalNode.value, originalNode.sourceSpan.toString(), this._interpolationConfig);
const exps = split ? split.expressions : [];
const messageSubstring = this._messagesContent.substring(
node.startSourceSpan.end.offset, node.endSourceSpan.start.offset);
let translated = this._replacePlaceholdersWithInterpolations(
messageSubstring, exps, originalNode.sourceSpan);
return new HtmlTextAst(translated, originalNode.sourceSpan);
}
private _mergeElement(node: HtmlElementAst, originalNode: HtmlElementAst, mapping: HtmlAst[]):
HtmlElementAst {
const children = this._expandPlaceholders(node.children, mapping);
return new HtmlElementAst(
originalNode.name, this._i18nAttributes(originalNode), children, originalNode.sourceSpan,
originalNode.startSourceSpan, originalNode.endSourceSpan);
}
private _i18nAttributes(el: HtmlElementAst): HtmlAttrAst[] {
let res: HtmlAttrAst[] = [];
let implicitAttrs: string[] =
isPresent(this._implicitAttrs[el.name]) ? this._implicitAttrs[el.name] : [];
el.attrs.forEach(attr => {
if (attr.name.startsWith(I18N_ATTR_PREFIX) || attr.name == I18N_ATTR) return;
let message: Message;
let i18nAttr = el.attrs.find(a => a.name == `${I18N_ATTR_PREFIX}${attr.name}`);
if (!i18nAttr) {
if (implicitAttrs.indexOf(attr.name) == -1) {
res.push(attr);
return;
}
message = messageFromAttribute(this._expressionParser, this._interpolationConfig, attr);
} else {
message = messageFromI18nAttribute(
this._expressionParser, this._interpolationConfig, el, i18nAttr);
}
let messageId = id(message);
if (StringMapWrapper.contains(this._messages, messageId)) {
const updatedMessage = this._replaceInterpolationInAttr(attr, this._messages[messageId]);
res.push(new HtmlAttrAst(attr.name, updatedMessage, attr.sourceSpan));
} else {
throw new I18nError(
attr.sourceSpan,
`Cannot find message for id '${messageId}', content '${message.content}'.`);
}
});
return res;
}
private _replaceInterpolationInAttr(attr: HtmlAttrAst, msg: HtmlAst[]): string {
const split = this._expressionParser.splitInterpolation(
attr.value, attr.sourceSpan.toString(), this._interpolationConfig);
const exps = isPresent(split) ? split.expressions : [];
const first = msg[0];
const last = msg[msg.length - 1];
const start = first.sourceSpan.start.offset;
const end =
last instanceof HtmlElementAst ? last.endSourceSpan.end.offset : last.sourceSpan.end.offset;
const messageSubstring = this._messagesContent.substring(start, end);
return this._replacePlaceholdersWithInterpolations(messageSubstring, exps, attr.sourceSpan);
};
private _replacePlaceholdersWithInterpolations(
message: string, exps: string[], sourceSpan: ParseSourceSpan): string {
const expMap = this._buildExprMap(exps);
return message.replace(
_PLACEHOLDER_EXPANDED_REGEXP,
(_: string, name: string) => this._convertIntoExpression(name, expMap, sourceSpan));
}
private _buildExprMap(exps: string[]): Map<string, string> {
const expMap = new Map<string, string>();
const usedNames = new Map<string, number>();
for (let i = 0; i < exps.length; i++) {
const phName = extractPhNameFromInterpolation(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 `${this._interpolationConfig.start}${expMap.get(name)}${this._interpolationConfig.end}`;
}
throw new I18nError(sourceSpan, `Invalid interpolation name '${name}'`);
}
}
// Creates a list of elements and text nodes in the AST
// The indexes match the placeholders indexes
class _NodeMappingVisitor implements HtmlAstVisitor {
mapping: HtmlAst[] = [];
visitElement(ast: HtmlElementAst, context: any): any {
this.mapping.push(ast);
htmlVisitAll(this, ast.children);
}
visitText(ast: HtmlTextAst, context: any): any { this.mapping.push(ast); }
visitAttr(ast: HtmlAttrAst, context: any): any {}
visitExpansion(ast: HtmlExpansionAst, context: any): any {}
visitExpansionCase(ast: HtmlExpansionCaseAst, context: any): any {}
visitComment(ast: HtmlCommentAst, context: any): any {}
}

View File

@ -8,44 +8,58 @@
import {Lexer as ExpressionLexer} from '../expression_parser/lexer';
import {Parser as ExpressionParser} from '../expression_parser/parser';
import * as hAst from '../html_parser/html_ast';
import * as html from '../html_parser/ast';
import {getHtmlTagDefinition} from '../html_parser/html_tags';
import {InterpolationConfig} from '../html_parser/interpolation_config';
import {ParseSourceSpan} from '../parse_util';
import {extractAstMessages} from './extractor';
import * as i18nAst from './i18n_ast';
import {PlaceholderRegistry} from './serializers/util';
import * as i18n from './i18n_ast';
import {PlaceholderRegistry} from './serializers/placeholder';
import {extractPlaceholderName} from './shared';
/**
* Extract all the i18n messages from a component template.
*/
export function extractI18nMessages(
sourceAst: hAst.HtmlAst[], interpolationConfig: InterpolationConfig, implicitTags: string[],
implicitAttrs: {[k: string]: string[]}): i18nAst.Message[] {
sourceAst: html.Node[], interpolationConfig: InterpolationConfig, implicitTags: string[],
implicitAttrs: {[k: string]: string[]}): i18n.Message[] {
const extractionResult = extractAstMessages(sourceAst, implicitTags, implicitAttrs);
if (extractionResult.errors.length) {
return [];
}
const visitor =
new _I18nVisitor(new ExpressionParser(new ExpressionLexer()), interpolationConfig);
const expParser = new ExpressionParser(new ExpressionLexer());
const visitor = new _I18nVisitor(expParser, interpolationConfig);
return extractionResult.messages.map((msg): i18nAst.Message => {
return new i18nAst.Message(visitor.convertToI18nAst(msg.nodes), msg.meaning, msg.description);
});
return extractionResult.messages.map(
(msg) => visitor.toI18nMessage(msg.nodes, msg.meaning, msg.description));
}
class _I18nVisitor implements hAst.HtmlAstVisitor {
class _I18nVisitor implements html.Visitor {
private _isIcu: boolean;
private _icuDepth: number;
private _placeholderRegistry: PlaceholderRegistry;
private _placeholderToContent: {[name: string]: string};
constructor(
private _expressionParser: ExpressionParser,
private _interpolationConfig: InterpolationConfig) {}
visitElement(el: hAst.HtmlElementAst, context: any): i18nAst.Node {
const children = hAst.htmlVisitAll(this, el.children);
public toI18nMessage(nodes: html.Node[], meaning: string, description: string): i18n.Message {
this._isIcu = nodes.length == 1 && nodes[0] instanceof html.Expansion;
this._icuDepth = 0;
this._placeholderRegistry = new PlaceholderRegistry();
this._placeholderToContent = {};
const i18nodes: i18n.Node[] = html.visitAll(this, nodes, {});
return new i18n.Message(i18nodes, this._placeholderToContent, meaning, description);
}
visitElement(el: html.Element, context: any): i18n.Node {
const children = html.visitAll(this, el.children);
const attrs: {[k: string]: string} = {};
el.attrs.forEach(attr => {
// Do not visit the attributes, translatable ones are top-level ASTs
@ -55,66 +69,67 @@ class _I18nVisitor implements hAst.HtmlAstVisitor {
const isVoid: boolean = getHtmlTagDefinition(el.name).isVoid;
const startPhName =
this._placeholderRegistry.getStartTagPlaceholderName(el.name, attrs, isVoid);
const closePhName = isVoid ? '' : this._placeholderRegistry.getCloseTagPlaceholderName(el.name);
this._placeholderToContent[startPhName] = el.sourceSpan.toString();
return new i18nAst.TagPlaceholder(
let closePhName = '';
if (!isVoid) {
closePhName = this._placeholderRegistry.getCloseTagPlaceholderName(el.name);
this._placeholderToContent[closePhName] = `</${el.name}>`;
}
return new i18n.TagPlaceholder(
el.name, attrs, startPhName, closePhName, children, isVoid, el.sourceSpan);
}
visitAttr(attr: hAst.HtmlAttrAst, context: any): i18nAst.Node {
return this._visitTextWithInterpolation(attr.value, attr.sourceSpan);
visitAttribute(attribute: html.Attribute, context: any): i18n.Node {
return this._visitTextWithInterpolation(attribute.value, attribute.sourceSpan);
}
visitText(text: hAst.HtmlTextAst, context: any): i18nAst.Node {
visitText(text: html.Text, context: any): i18n.Node {
return this._visitTextWithInterpolation(text.value, text.sourceSpan);
}
visitComment(comment: hAst.HtmlCommentAst, context: any): i18nAst.Node { return null; }
visitComment(comment: html.Comment, context: any): i18n.Node { return null; }
visitExpansion(icu: hAst.HtmlExpansionAst, context: any): i18nAst.Node {
visitExpansion(icu: html.Expansion, context: any): i18n.Node {
this._icuDepth++;
const i18nIcuCases: {[k: string]: i18nAst.Node} = {};
const i18nIcu = new i18nAst.Icu(icu.switchValue, icu.type, i18nIcuCases, icu.sourceSpan);
const i18nIcuCases: {[k: string]: i18n.Node} = {};
const i18nIcu = new i18n.Icu(icu.switchValue, icu.type, i18nIcuCases, icu.sourceSpan);
icu.cases.forEach((caze): void => {
i18nIcuCases[caze.value] = new i18nAst.Container(
caze.expression.map((hAst) => hAst.visit(this, {})), caze.expSourceSpan);
i18nIcuCases[caze.value] = new i18n.Container(
caze.expression.map((node) => node.visit(this, {})), caze.expSourceSpan);
});
this._icuDepth--;
if (this._isIcu || this._icuDepth > 0) {
// If the message (vs a part of the message) is an ICU message return its
// If the message (vs a part of the message) is an ICU message returns it
return i18nIcu;
}
// else returns a placeholder
const phName = this._placeholderRegistry.getPlaceholderName('ICU', icu.sourceSpan.toString());
return new i18nAst.IcuPlaceholder(i18nIcu, phName, icu.sourceSpan);
this._placeholderToContent[phName] = icu.sourceSpan.toString();
return new i18n.IcuPlaceholder(i18nIcu, phName, icu.sourceSpan);
}
visitExpansionCase(icuCase: hAst.HtmlExpansionCaseAst, context: any): i18nAst.Node {
visitExpansionCase(icuCase: html.ExpansionCase, context: any): i18n.Node {
throw new Error('Unreachable code');
}
public convertToI18nAst(htmlAsts: hAst.HtmlAst[]): i18nAst.Node[] {
this._isIcu = htmlAsts.length == 1 && htmlAsts[0] instanceof hAst.HtmlExpansionAst;
this._icuDepth = 0;
this._placeholderRegistry = new PlaceholderRegistry();
return hAst.htmlVisitAll(this, htmlAsts, {});
}
private _visitTextWithInterpolation(text: string, sourceSpan: ParseSourceSpan): i18nAst.Node {
private _visitTextWithInterpolation(text: string, sourceSpan: ParseSourceSpan): i18n.Node {
const splitInterpolation = this._expressionParser.splitInterpolation(
text, sourceSpan.start.toString(), this._interpolationConfig);
if (!splitInterpolation) {
// No expression, return a single text
return new i18nAst.Text(text, sourceSpan);
return new i18n.Text(text, sourceSpan);
}
// Return a group of text + expressions
const nodes: i18nAst.Node[] = [];
const container = new i18nAst.Container(nodes, sourceSpan);
const nodes: i18n.Node[] = [];
const container = new i18n.Container(nodes, sourceSpan);
const {start: sDelimiter, end: eDelimiter} = this._interpolationConfig;
for (let i = 0; i < splitInterpolation.strings.length - 1; i++) {
const expression = splitInterpolation.expressions[i];
@ -122,16 +137,18 @@ class _I18nVisitor implements hAst.HtmlAstVisitor {
const phName = this._placeholderRegistry.getPlaceholderName(baseName, expression);
if (splitInterpolation.strings[i].length) {
nodes.push(new i18nAst.Text(splitInterpolation.strings[i], sourceSpan));
// No need to add empty strings
nodes.push(new i18n.Text(splitInterpolation.strings[i], sourceSpan));
}
nodes.push(new i18nAst.Placeholder(expression, phName, sourceSpan));
nodes.push(new i18n.Placeholder(expression, phName, sourceSpan));
this._placeholderToContent[phName] = sDelimiter + expression + eDelimiter;
}
// The last index contains no expression
const lastStringIdx = splitInterpolation.strings.length - 1;
if (splitInterpolation.strings[lastStringIdx].length) {
nodes.push(new i18nAst.Text(splitInterpolation.strings[lastStringIdx], sourceSpan));
nodes.push(new i18n.Text(splitInterpolation.strings[lastStringIdx], sourceSpan));
}
return container;
}

View File

@ -0,0 +1,12 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
export {MessageBundle} from './message_bundle';
export {Serializer} from './serializers/serializer';
export {Xmb} from './serializers/xmb';
export {Xtb} from './serializers/xtb';

View File

@ -1,30 +0,0 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {escape, isPresent} from '../facade/lang';
/**
* A message extracted from a template.
*
* The identity of a message is comprised of `content` and `meaning`.
*
* `description` is additional information provided to the translator.
*/
export class Message {
constructor(public content: string, public meaning: string, public description: string = null) {}
}
/**
* Computes the id of a message
*/
export function id(m: Message): string {
let meaning = isPresent(m.meaning) ? m.meaning : '';
let content = isPresent(m.content) ? m.content : '';
return escape(`$ng|${meaning}|${content}`);
}

View File

@ -8,24 +8,29 @@
import {HtmlParser} from '../html_parser/html_parser';
import {InterpolationConfig} from '../html_parser/interpolation_config';
import {ParseError} from '../parse_util';
import * as i18nAst from './i18n_ast';
import * as i18n from './i18n_ast';
import {extractI18nMessages} from './i18n_parser';
import {Serializer} from './serializers/serializer';
export class Catalog {
private _messageMap: {[k: string]: i18nAst.Message} = {};
/**
* A container for message extracted from the templates.
*/
export class MessageBundle {
private _messageMap: {[id: string]: i18n.Message} = {};
constructor(
private _htmlParser: HtmlParser, private _implicitTags: string[],
private _implicitAttrs: {[k: string]: string[]}) {}
public updateFromTemplate(html: string, url: string, interpolationConfig: InterpolationConfig):
void {
updateFromTemplate(html: string, url: string, interpolationConfig: InterpolationConfig):
ParseError[] {
const htmlParserResult = this._htmlParser.parse(html, url, true, interpolationConfig);
if (htmlParserResult.errors.length) {
throw new Error();
return htmlParserResult.errors;
}
const messages = extractI18nMessages(
@ -37,18 +42,9 @@ export class Catalog {
});
}
public load(content: string, serializer: Serializer): void {
const nodeMap = serializer.load(content);
this._messageMap = {};
Object.getOwnPropertyNames(nodeMap).forEach(
(id) => { this._messageMap[id] = new i18nAst.Message(nodeMap[id], '', ''); });
}
public write(serializer: Serializer): string { return serializer.write(this._messageMap); }
write(serializer: Serializer): string { return serializer.write(this._messageMap); }
}
/**
* String hash function similar to java.lang.String.hashCode().
* The hash code for a string is computed as
@ -78,35 +74,35 @@ export function strHash(str: string): string {
*
* @internal
*/
class _SerializerVisitor implements i18nAst.Visitor {
visitText(text: i18nAst.Text, context: any): any { return text.value; }
class _SerializerVisitor implements i18n.Visitor {
visitText(text: i18n.Text, context: any): any { return text.value; }
visitContainer(container: i18nAst.Container, context: any): any {
visitContainer(container: i18n.Container, context: any): any {
return `[${container.children.map(child => child.visit(this)).join(', ')}]`;
}
visitIcu(icu: i18nAst.Icu, context: any): any {
visitIcu(icu: i18n.Icu, context: any): any {
let strCases = Object.keys(icu.cases).map((k: string) => `${k} {${icu.cases[k].visit(this)}}`);
return `{${icu.expression}, ${icu.type}, ${strCases.join(', ')}}`;
}
visitTagPlaceholder(ph: i18nAst.TagPlaceholder, context: any): any {
visitTagPlaceholder(ph: i18n.TagPlaceholder, context: any): any {
return ph.isVoid ?
`<ph tag name="${ph.startName}"/>` :
`<ph tag name="${ph.startName}">${ph.children.map(child => child.visit(this)).join(', ')}</ph name="${ph.closeName}">`;
}
visitPlaceholder(ph: i18nAst.Placeholder, context: any): any {
visitPlaceholder(ph: i18n.Placeholder, context: any): any {
return `<ph name="${ph.name}">${ph.value}</ph>`;
}
visitIcuPlaceholder(ph: i18nAst.IcuPlaceholder, context?: any): any {
visitIcuPlaceholder(ph: i18n.IcuPlaceholder, context?: any): any {
return `<ph icu name="${ph.name}">${ph.value.visit(this)}</ph>`;
}
}
const serializerVisitor = new _SerializerVisitor();
export function serializeAst(ast: i18nAst.Node[]): string[] {
export function serializeAst(ast: i18n.Node[]): string[] {
return ast.map(a => a.visit(serializerVisitor, null));
}

View File

@ -1,183 +0,0 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Parser as ExpressionParser} from '../expression_parser/parser';
import {StringMapWrapper} from '../facade/collection';
import {isPresent} from '../facade/lang';
import {HtmlAst, HtmlElementAst} from '../html_parser/html_ast';
import {HtmlParser} from '../html_parser/html_parser';
import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from '../html_parser/interpolation_config';
import {ParseError} from '../parse_util';
import {Message, id} from './message';
import {I18N_ATTR_PREFIX, I18nError, Part, messageFromAttribute, messageFromI18nAttribute, partition} from './shared';
/**
* All messages extracted from a template.
*/
export class ExtractionResult {
constructor(public messages: Message[], public errors: ParseError[]) {}
}
/**
* Removes duplicate messages.
*/
export function removeDuplicates(messages: Message[]): Message[] {
let uniq: {[key: string]: Message} = {};
messages.forEach(m => {
if (!StringMapWrapper.contains(uniq, id(m))) {
uniq[id(m)] = m;
}
});
return StringMapWrapper.values(uniq);
}
/**
* Extracts all messages from a template.
*
* Algorithm:
*
* To understand the algorithm, you need to know how partitioning works.
* Partitioning is required as we can use two i18n comments to group node siblings together.
* That is why we cannot just use nodes.
*
* Partitioning transforms an array of HtmlAst into an array of Part.
* A part can optionally contain a root element or a root text node. And it can also contain
* children.
* A part can contain i18n property, in which case it needs to be extracted.
*
* Example:
*
* The following array of nodes will be split into four parts:
*
* ```
* <a>A</a>
* <b i18n>B</b>
* <!-- i18n -->
* <c>C</c>
* D
* <!-- /i18n -->
* E
* ```
*
* Part 1 containing the a tag. It should not be translated.
* Part 2 containing the b tag. It should be translated.
* Part 3 containing the c tag and the D text node. It should be translated.
* Part 4 containing the E text node. It should not be translated..
*
* It is also important to understand how we stringify nodes to create a message.
*
* We walk the tree and replace every element node with a placeholder. We also replace
* all expressions in interpolation with placeholders. We also insert a placeholder element
* to wrap a text node containing interpolation.
*
* Example:
*
* The following tree:
*
* ```
* <a>A{{I}}</a><b>B</b>
* ```
*
* will be stringified into:
* ```
* <ph name="e0"><ph name="t1">A<ph name="0"/></ph></ph><ph name="e2">B</ph>
* ```
*
* This is what the algorithm does:
*
* 1. Use the provided html parser to get the html AST of the template.
* 2. Partition the root nodes, and process each part separately.
* 3. If a part does not have the i18n attribute, recurse to process children and attributes.
* 4. If a part has the i18n attribute, stringify the nodes to create a Message.
*/
export class MessageExtractor {
private _messages: Message[];
private _errors: ParseError[];
constructor(
private _htmlParser: HtmlParser, private _expressionParser: ExpressionParser,
private _implicitTags: string[], private _implicitAttrs: {[k: string]: string[]}) {}
extract(
template: string, sourceUrl: string,
interpolationConfig: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG): ExtractionResult {
this._messages = [];
this._errors = [];
const res = this._htmlParser.parse(template, sourceUrl, true, interpolationConfig);
if (res.errors.length == 0) {
this._recurse(res.rootNodes, interpolationConfig);
}
return new ExtractionResult(this._messages, this._errors.concat(res.errors));
}
private _extractMessagesFromPart(part: Part, interpolationConfig: InterpolationConfig): void {
if (part.hasI18n) {
this._messages.push(...part.createMessages(this._expressionParser, interpolationConfig));
this._recurseToExtractMessagesFromAttributes(part.children, interpolationConfig);
} else {
this._recurse(part.children, interpolationConfig);
}
if (isPresent(part.rootElement)) {
this._extractMessagesFromAttributes(part.rootElement, interpolationConfig);
}
}
private _recurse(nodes: HtmlAst[], interpolationConfig: InterpolationConfig): void {
if (isPresent(nodes)) {
let parts = partition(nodes, this._errors, this._implicitTags);
parts.forEach(part => this._extractMessagesFromPart(part, interpolationConfig));
}
}
private _recurseToExtractMessagesFromAttributes(
nodes: HtmlAst[], interpolationConfig: InterpolationConfig): void {
nodes.forEach(n => {
if (n instanceof HtmlElementAst) {
this._extractMessagesFromAttributes(n, interpolationConfig);
this._recurseToExtractMessagesFromAttributes(n.children, interpolationConfig);
}
});
}
private _extractMessagesFromAttributes(
p: HtmlElementAst, interpolationConfig: InterpolationConfig): void {
let transAttrs: string[] =
isPresent(this._implicitAttrs[p.name]) ? this._implicitAttrs[p.name] : [];
let explicitAttrs: string[] = [];
// `i18n-` prefixed attributes should be translated
p.attrs.filter(attr => attr.name.startsWith(I18N_ATTR_PREFIX)).forEach(attr => {
try {
explicitAttrs.push(attr.name.substring(I18N_ATTR_PREFIX.length));
this._messages.push(
messageFromI18nAttribute(this._expressionParser, interpolationConfig, p, attr));
} catch (e) {
if (e instanceof I18nError) {
this._errors.push(e);
} else {
throw e;
}
}
});
// implicit attributes should also be translated
p.attrs.filter(attr => !attr.name.startsWith(I18N_ATTR_PREFIX))
.filter(attr => explicitAttrs.indexOf(attr.name) == -1)
.filter(attr => transAttrs.indexOf(attr.name) > -1)
.forEach(
attr => this._messages.push(
messageFromAttribute(this._expressionParser, interpolationConfig, attr)));
}
}

View File

@ -45,7 +45,9 @@ const TAG_TO_PLACEHOLDER_NAMES: {[k: string]: string} = {
* @internal
*/
export class PlaceholderRegistry {
// Count the occurrence of the base name top generate a unique name
private _placeHolderNameCounts: {[k: string]: number} = {};
// Maps signature to placeholder names
private _signatureToName: {[k: string]: string} = {};
getStartTagPlaceholderName(tag: string, attrs: {[k: string]: string}, isVoid: boolean): string {
@ -91,18 +93,17 @@ export class PlaceholderRegistry {
return uniqueName;
}
// Generate a hash for a tag - does not take attribute order into account
private _hashTag(tag: string, attrs: {[k: string]: string}, isVoid: boolean): string {
const start = `<${tag.toUpperCase()}`;
const start = `<${tag}`;
const strAttrs =
Object.getOwnPropertyNames(attrs).sort().map((name) => ` ${name}=${attrs[name]}`).join('');
const end = isVoid ? '/>' : `></${tag.toUpperCase()}>`;
const end = isVoid ? '/>' : `></${tag}>`;
return start + strAttrs + end;
}
private _hashClosingTag(tag: string): string {
return this._hashTag(`/${tag.toUpperCase()}`, {}, false);
}
private _hashClosingTag(tag: string): string { return this._hashTag(`/${tag}`, {}, false); }
private _generateUniqueName(base: string): string {
let name = base;

View File

@ -6,10 +6,12 @@
* found in the LICENSE file at https://angular.io/license
*/
import * as i18nAst from '../i18n_ast';
import * as html from '../../html_parser/ast';
import * as i18n from '../i18n_ast';
export interface Serializer {
write(messageMap: {[k: string]: i18nAst.Message}): string;
write(messageMap: {[id: string]: i18n.Message}): string;
load(content: string): {[k: string]: i18nAst.Node[]};
load(content: string, url: string, placeholders: {[id: string]: {[name: string]: string}}):
{[id: string]: html.Node[]};
}

View File

@ -7,7 +7,8 @@
*/
import {ListWrapper} from '../../facade/collection';
import * as i18nAst from '../i18n_ast';
import * as html from '../../html_parser/ast';
import * as i18n from '../i18n_ast';
import {Serializer} from './serializer';
import * as xml from './xml_helper';
@ -17,9 +18,9 @@ const _MESSAGE_TAG = 'msg';
const _PLACEHOLDER_TAG = 'ph';
const _EXEMPLE_TAG = 'ex';
export class XmbSerializer implements Serializer {
export class Xmb implements Serializer {
// TODO(vicb): DOCTYPE
write(messageMap: {[k: string]: i18nAst.Message}): string {
write(messageMap: {[k: string]: i18n.Message}): string {
const visitor = new _Visitor();
const declaration = new xml.Declaration({version: '1.0', encoding: 'UTF-8'});
let rootNode = new xml.Tag(_MESSAGES_TAG);
@ -49,19 +50,22 @@ export class XmbSerializer implements Serializer {
]);
}
load(content: string): {[k: string]: i18nAst.Node[]} { throw new Error('Unsupported'); }
load(content: string, url: string, placeholders: {[id: string]: {[name: string]: string}}):
{[id: string]: html.Node[]} {
throw new Error('Unsupported');
}
}
class _Visitor implements i18nAst.Visitor {
visitText(text: i18nAst.Text, context?: any): xml.Node[] { return [new xml.Text(text.value)]; }
class _Visitor implements i18n.Visitor {
visitText(text: i18n.Text, context?: any): xml.Node[] { return [new xml.Text(text.value)]; }
visitContainer(container: i18nAst.Container, context?: any): xml.Node[] {
visitContainer(container: i18n.Container, context?: any): xml.Node[] {
const nodes: xml.Node[] = [];
container.children.forEach((node: i18nAst.Node) => nodes.push(...node.visit(this)));
container.children.forEach((node: i18n.Node) => nodes.push(...node.visit(this)));
return nodes;
}
visitIcu(icu: i18nAst.Icu, context?: any): xml.Node[] {
visitIcu(icu: i18n.Icu, context?: any): xml.Node[] {
const nodes = [new xml.Text(`{${icu.expression}, ${icu.type}, `)];
Object.getOwnPropertyNames(icu.cases).forEach((c: string) => {
@ -73,7 +77,7 @@ class _Visitor implements i18nAst.Visitor {
return nodes;
}
visitTagPlaceholder(ph: i18nAst.TagPlaceholder, context?: any): xml.Node[] {
visitTagPlaceholder(ph: i18n.TagPlaceholder, context?: any): xml.Node[] {
const startEx = new xml.Tag(_EXEMPLE_TAG, {}, [new xml.Text(`<${ph.tag}>`)]);
const startTagPh = new xml.Tag(_PLACEHOLDER_TAG, {name: ph.startName}, [startEx]);
if (ph.isVoid) {
@ -87,15 +91,15 @@ class _Visitor implements i18nAst.Visitor {
return [startTagPh, ...this.serialize(ph.children), closeTagPh];
}
visitPlaceholder(ph: i18nAst.Placeholder, context?: any): xml.Node[] {
visitPlaceholder(ph: i18n.Placeholder, context?: any): xml.Node[] {
return [new xml.Tag(_PLACEHOLDER_TAG, {name: ph.name})];
}
visitIcuPlaceholder(ph: i18nAst.IcuPlaceholder, context?: any): xml.Node[] {
visitIcuPlaceholder(ph: i18n.IcuPlaceholder, context?: any): xml.Node[] {
return [new xml.Tag(_PLACEHOLDER_TAG, {name: ph.name})];
}
serialize(nodes: i18nAst.Node[]): xml.Node[] {
serialize(nodes: i18n.Node[]): xml.Node[] {
return ListWrapper.flatten(nodes.map(node => node.visit(this)));
}
}

View File

@ -0,0 +1,149 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as xml from '../../html_parser/ast';
import {HtmlParser} from '../../html_parser/html_parser';
import {InterpolationConfig} from '../../html_parser/interpolation_config';
import {XmlParser} from '../../html_parser/xml_parser';
import {ParseError} from '../../parse_util';
import * as i18n from '../i18n_ast';
import {I18nError} from '../shared';
import {Serializer} from './serializer';
const _TRANSLATIONS_TAG = 'translationbundle';
const _TRANSLATION_TAG = 'translation';
const _PLACEHOLDER_TAG = 'ph';
export class Xtb implements Serializer {
constructor(private _htmlParser: HtmlParser, private _interpolationConfig: InterpolationConfig) {}
write(messageMap: {[id: string]: i18n.Message}): string { throw new Error('Unsupported'); }
load(content: string, url: string, placeholders: {[id: string]: {[name: string]: string}}):
{[id: string]: xml.Node[]} {
// Parse the xtb file into xml nodes
const result = new XmlParser().parse(content, url);
if (result.errors.length) {
throw new Error(`xtb parse errors:\n${result.errors.join('\n')}`);
}
// Replace the placeholders, messages are now string
const {messages, errors} = new _Serializer().parse(result.rootNodes, placeholders);
if (errors.length) {
throw new Error(`xtb parse errors:\n${errors.join('\n')}`);
}
// Convert the string messages to html ast
// TODO(vicb): map error message back to the original message in xtb
let messageMap: {[id: string]: xml.Node[]} = {};
let parseErrors: ParseError[] = [];
Object.getOwnPropertyNames(messages).forEach((id) => {
const res = this._htmlParser.parse(messages[id], url, true, this._interpolationConfig);
parseErrors.push(...res.errors);
messageMap[id] = res.rootNodes;
});
if (parseErrors.length) {
throw new Error(`xtb parse errors:\n${parseErrors.join('\n')}`);
}
return messageMap;
}
}
class _Serializer implements xml.Visitor {
private _messages: {[id: string]: string};
private _bundleDepth: number;
private _translationDepth: number;
private _errors: I18nError[];
private _placeholders: {[id: string]: {[name: string]: string}};
private _currentPlaceholders: {[name: string]: string};
parse(nodes: xml.Node[], _placeholders: {[id: string]: {[name: string]: string}}):
{messages: {[k: string]: string}, errors: I18nError[]} {
this._messages = {};
this._bundleDepth = 0;
this._translationDepth = 0;
this._errors = [];
this._placeholders = _placeholders;
xml.visitAll(this, nodes, null);
return {messages: this._messages, errors: this._errors};
}
visitElement(element: xml.Element, context: any): any {
switch (element.name) {
case _TRANSLATIONS_TAG:
this._bundleDepth++;
if (this._bundleDepth > 1) {
this._addError(element, `<${_TRANSLATIONS_TAG}> elements can not be nested`);
}
xml.visitAll(this, element.children, null);
this._bundleDepth--;
break;
case _TRANSLATION_TAG:
this._translationDepth++;
if (this._translationDepth > 1) {
this._addError(element, `<${_TRANSLATION_TAG}> elements can not be nested`);
}
const idAttr = element.attrs.find((attr) => attr.name === 'id');
if (!idAttr) {
this._addError(element, `<${_TRANSLATION_TAG}> misses the "id" attribute`);
} else {
this._currentPlaceholders = this._placeholders[idAttr.value] || {};
this._messages[idAttr.value] = xml.visitAll(this, element.children).join('');
}
this._translationDepth--;
break;
case _PLACEHOLDER_TAG:
const nameAttr = element.attrs.find((attr) => attr.name === 'name');
if (!nameAttr) {
this._addError(element, `<${_PLACEHOLDER_TAG}> misses the "name" attribute`);
} else {
if (this._currentPlaceholders.hasOwnProperty(nameAttr.value)) {
return this._currentPlaceholders[nameAttr.value];
}
this._addError(
element, `The placeholder "${nameAttr.value}" does not exists in the source message`);
}
break;
default:
this._addError(element, 'Unexpected tag');
}
}
visitAttribute(attribute: xml.Attribute, context: any): any {
throw new Error('unreachable code');
}
visitText(text: xml.Text, context: any): any { return text.value; }
visitComment(comment: xml.Comment, context: any): any { return ''; }
visitExpansion(expansion: xml.Expansion, context: any): any {
const strCases = expansion.cases.map(c => c.visit(this, null));
return `{${expansion.switchValue}, ${expansion.type}, strCases.join(' ')}`;
}
visitExpansionCase(expansionCase: xml.ExpansionCase, context: any): any {
return `${expansionCase.value} {${xml.visitAll(this, expansionCase.expression, null)}}`;
}
private _addError(node: xml.Node, message: string): void {
this._errors.push(new I18nError(node.sourceSpan, message));
}
}

View File

@ -6,14 +6,10 @@
* found in the LICENSE file at https://angular.io/license
*/
import {normalizeBlank} from '../../../router-deprecated/src/facade/lang';
import {Parser as ExpressionParser} from '../expression_parser/parser';
import {StringWrapper, isBlank, isPresent} from '../facade/lang';
import {HtmlAst, HtmlAstVisitor, HtmlAttrAst, HtmlCommentAst, HtmlElementAst, HtmlExpansionAst, HtmlExpansionCaseAst, HtmlTextAst, htmlVisitAll} from '../html_parser/html_ast';
import {InterpolationConfig} from '../html_parser/interpolation_config';
import {StringWrapper, isBlank, isPresent, normalizeBlank} from '../facade/lang';
import * as html from '../html_parser/ast';
import {ParseError, ParseSourceSpan} from '../parse_util';
import {Message} from './message';
export const I18N_ATTR = 'i18n';
export const I18N_ATTR_PREFIX = 'i18n-';
@ -26,74 +22,15 @@ export class I18nError extends ParseError {
constructor(span: ParseSourceSpan, msg: string) { super(span, msg); }
}
export function partition(nodes: HtmlAst[], errors: ParseError[], implicitTags: string[]): Part[] {
let parts: Part[] = [];
for (let i = 0; i < nodes.length; ++i) {
let node = nodes[i];
let msgNodes: HtmlAst[] = [];
// Nodes between `<!-- i18n -->` and `<!-- /i18n -->`
if (isOpeningComment(node)) {
let i18n = (<HtmlCommentAst>node).value.replace(/^i18n:?/, '').trim();
while (++i < nodes.length && !isClosingComment(nodes[i])) {
msgNodes.push(nodes[i]);
}
if (i === nodes.length) {
errors.push(new I18nError(node.sourceSpan, 'Missing closing \'i18n\' comment.'));
break;
}
parts.push(new Part(null, null, msgNodes, i18n, true));
} else if (node instanceof HtmlElementAst) {
// Node with an `i18n` attribute
let i18n = getI18nAttr(node);
let hasI18n: boolean = isPresent(i18n) || implicitTags.indexOf(node.name) > -1;
parts.push(new Part(node, null, node.children, isPresent(i18n) ? i18n.value : null, hasI18n));
} else if (node instanceof HtmlTextAst) {
parts.push(new Part(null, node, null, null, false));
}
}
return parts;
export function isOpeningComment(n: html.Node): boolean {
return n instanceof html.Comment && isPresent(n.value) && n.value.startsWith('i18n');
}
export class Part {
constructor(
public rootElement: HtmlElementAst, public rootTextNode: HtmlTextAst,
public children: HtmlAst[], public i18n: string, public hasI18n: boolean) {}
get sourceSpan(): ParseSourceSpan {
if (isPresent(this.rootElement)) {
return this.rootElement.sourceSpan;
}
if (isPresent(this.rootTextNode)) {
return this.rootTextNode.sourceSpan;
}
return new ParseSourceSpan(
this.children[0].sourceSpan.start, this.children[this.children.length - 1].sourceSpan.end);
}
createMessages(parser: ExpressionParser, interpolationConfig: InterpolationConfig): Message[] {
let {message, icuMessages} = stringifyNodes(this.children, parser, interpolationConfig);
return [
new Message(message, meaning(this.i18n), description(this.i18n)),
...icuMessages.map(icu => new Message(icu, null))
];
}
export function isClosingComment(n: html.Node): boolean {
return n instanceof html.Comment && isPresent(n.value) && n.value === '/i18n';
}
export function isOpeningComment(n: HtmlAst): boolean {
return n instanceof HtmlCommentAst && isPresent(n.value) && n.value.startsWith('i18n');
}
export function isClosingComment(n: HtmlAst): boolean {
return n instanceof HtmlCommentAst && isPresent(n.value) && n.value === '/i18n';
}
export function getI18nAttr(p: HtmlElementAst): HtmlAttrAst {
export function getI18nAttr(p: html.Element): html.Attribute {
return normalizeBlank(p.attrs.find(attr => attr.name === I18N_ATTR));
}
@ -108,148 +45,6 @@ export function description(i18n: string): string {
return parts.length > 1 ? parts[1] : '';
}
/**
* Extract a translation string given an `i18n-` prefixed attribute.
*
* @internal
*/
export function messageFromI18nAttribute(
parser: ExpressionParser, interpolationConfig: InterpolationConfig, p: HtmlElementAst,
i18nAttr: HtmlAttrAst): Message {
const expectedName = i18nAttr.name.substring(5);
const attr = p.attrs.find(a => a.name == expectedName);
if (attr) {
return messageFromAttribute(
parser, interpolationConfig, attr, meaning(i18nAttr.value), description(i18nAttr.value));
}
throw new I18nError(p.sourceSpan, `Missing attribute '${expectedName}'.`);
}
export function messageFromAttribute(
parser: ExpressionParser, interpolationConfig: InterpolationConfig, attr: HtmlAttrAst,
meaning: string = null, description: string = null): Message {
const value = removeInterpolation(attr.value, attr.sourceSpan, parser, interpolationConfig);
return new Message(value, meaning, description);
}
/**
* Replace interpolation in the `value` string with placeholders
*/
export function removeInterpolation(
value: string, source: ParseSourceSpan, expressionParser: ExpressionParser,
interpolationConfig: InterpolationConfig): string {
try {
const parsed =
expressionParser.splitInterpolation(value, source.toString(), interpolationConfig);
const usedNames = new Map<string, number>();
if (isPresent(parsed)) {
let res = '';
for (let i = 0; i < parsed.strings.length; ++i) {
res += parsed.strings[i];
if (i != parsed.strings.length - 1) {
let customPhName = extractPhNameFromInterpolation(parsed.expressions[i], i);
customPhName = dedupePhName(usedNames, customPhName);
res += `<ph name="${customPhName}"/>`;
}
}
return res;
}
return value;
} catch (e) {
return value;
}
}
/**
* Extract the placeholder name from the interpolation.
*
* Use a custom name when specified (ie: `{{<expression> //i18n(ph="FIRST")}}`) otherwise generate a
* unique name.
*/
export function extractPhNameFromInterpolation(input: string, index: number): string {
let customPhMatch = StringWrapper.split(input, _CUSTOM_PH_EXP);
return customPhMatch.length > 1 ? customPhMatch[1] : `INTERPOLATION_${index}`;
}
export function extractPlaceholderName(input: string): string {
return StringWrapper.split(input, _CUSTOM_PH_EXP)[1];
}
/**
* Return a unique placeholder name based on the given name
*/
export function dedupePhName(usedNames: Map<string, number>, name: string): string {
const duplicateNameCount = usedNames.get(name);
if (duplicateNameCount) {
usedNames.set(name, duplicateNameCount + 1);
return `${name}_${duplicateNameCount}`;
}
usedNames.set(name, 1);
return name;
}
/**
* Convert a list of nodes to a string message.
*
*/
export function stringifyNodes(
nodes: HtmlAst[], expressionParser: ExpressionParser,
interpolationConfig: InterpolationConfig): {message: string, icuMessages: string[]} {
const visitor = new _StringifyVisitor(expressionParser, interpolationConfig);
const icuMessages: string[] = [];
const message = htmlVisitAll(visitor, nodes, icuMessages).join('');
return {message, icuMessages};
}
class _StringifyVisitor implements HtmlAstVisitor {
private _index: number = 0;
private _nestedExpansion = 0;
constructor(
private _expressionParser: ExpressionParser,
private _interpolationConfig: InterpolationConfig) {}
visitElement(ast: HtmlElementAst, context: any): any {
const index = this._index++;
const children = this._join(htmlVisitAll(this, ast.children), '');
return `<ph name="e${index}">${children}</ph>`;
}
visitAttr(ast: HtmlAttrAst, context: any): any { return null; }
visitText(ast: HtmlTextAst, context: any): any {
const index = this._index++;
const noInterpolation = removeInterpolation(
ast.value, ast.sourceSpan, this._expressionParser, this._interpolationConfig);
if (noInterpolation != ast.value) {
return `<ph name="t${index}">${noInterpolation}</ph>`;
}
return ast.value;
}
visitComment(ast: HtmlCommentAst, context: any): any { return ''; }
visitExpansion(ast: HtmlExpansionAst, context: any): any {
const index = this._index++;
this._nestedExpansion++;
const content = `{${ast.switchValue}, ${ast.type}${htmlVisitAll(this, ast.cases).join('')}}`;
this._nestedExpansion--;
return this._nestedExpansion == 0 ? `<ph name="x${index}">${content}</ph>` : content;
}
visitExpansionCase(ast: HtmlExpansionCaseAst, context: any): any {
const expStr = htmlVisitAll(this, ast.expression).join('');
return ` ${ast.value} {${expStr}}`;
}
private _join(strs: string[], str: string): string {
return strs.filter(s => s.length > 0).join(str);
}
}

View File

@ -0,0 +1,24 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as html from '../html_parser/ast';
import {Serializer} from './serializers/serializer';
/**
* A container for translated messages
*/
export class TranslationBundle {
constructor(private _messageMap: {[id: string]: html.Node[]} = {}) {}
static load(
content: string, url: string, placeholders: {[id: string]: {[name: string]: string}},
serializer: Serializer): TranslationBundle {
return new TranslationBundle(serializer.load(content, 'url', placeholders));
}
}

View File

@ -1,112 +0,0 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {RegExpWrapper, isPresent} from '../facade/lang';
import {HtmlAst, HtmlElementAst} from '../html_parser/html_ast';
import {HtmlParser} from '../html_parser/html_parser';
import {ParseError, ParseSourceSpan} from '../parse_util';
import {Message, id} from './message';
let _PLACEHOLDER_REGEXP = RegExpWrapper.create(`\\<ph(\\s)+name=("(\\w)+")\\/\\>`);
const _ID_ATTR = 'id';
const _MSG_ELEMENT = 'msg';
const _BUNDLE_ELEMENT = 'message-bundle';
export function serializeXmb(messages: Message[]): string {
let ms = messages.map((m) => _serializeMessage(m)).join('');
return `<message-bundle>${ms}</message-bundle>`;
}
export class XmbDeserializationResult {
constructor(
public content: string, public messages: {[key: string]: HtmlAst[]},
public errors: ParseError[]) {}
}
export class XmbDeserializationError extends ParseError {
constructor(span: ParseSourceSpan, msg: string) { super(span, msg); }
}
export function deserializeXmb(content: string, url: string): XmbDeserializationResult {
const normalizedContent = _expandPlaceholder(content.trim());
const parsed = new HtmlParser().parse(normalizedContent, url);
if (parsed.errors.length > 0) {
return new XmbDeserializationResult(null, {}, parsed.errors);
}
if (_checkRootElement(parsed.rootNodes)) {
return new XmbDeserializationResult(
null, {}, [new XmbDeserializationError(null, `Missing element "${_BUNDLE_ELEMENT}"`)]);
}
const bundleEl = <HtmlElementAst>parsed.rootNodes[0]; // test this
const errors: ParseError[] = [];
const messages: {[key: string]: HtmlAst[]} = {};
_createMessages(bundleEl.children, messages, errors);
return (errors.length == 0) ?
new XmbDeserializationResult(normalizedContent, messages, []) :
new XmbDeserializationResult(null, <{[key: string]: HtmlAst[]}>{}, errors);
}
function _checkRootElement(nodes: HtmlAst[]): boolean {
return nodes.length < 1 || !(nodes[0] instanceof HtmlElementAst) ||
(<HtmlElementAst>nodes[0]).name != _BUNDLE_ELEMENT;
}
function _createMessages(
nodes: HtmlAst[], messages: {[key: string]: HtmlAst[]}, errors: ParseError[]): void {
nodes.forEach((node) => {
if (node instanceof HtmlElementAst) {
let msg = <HtmlElementAst>node;
if (msg.name != _MSG_ELEMENT) {
errors.push(
new XmbDeserializationError(node.sourceSpan, `Unexpected element "${msg.name}"`));
return;
}
let idAttr = msg.attrs.find(a => a.name == _ID_ATTR);
if (idAttr) {
messages[idAttr.value] = msg.children;
} else {
errors.push(
new XmbDeserializationError(node.sourceSpan, `"${_ID_ATTR}" attribute is missing`));
}
}
});
}
function _serializeMessage(m: Message): string {
const desc = isPresent(m.description) ? ` desc='${_escapeXml(m.description)}'` : '';
const meaning = isPresent(m.meaning) ? ` meaning='${_escapeXml(m.meaning)}'` : '';
return `<msg id='${id(m)}'${desc}${meaning}>${m.content}</msg>`;
}
function _expandPlaceholder(input: string): string {
return RegExpWrapper.replaceAll(_PLACEHOLDER_REGEXP, input, (match: string[]) => {
let nameWithQuotes = match[2];
return `<ph name=${nameWithQuotes}></ph>`;
});
}
const _XML_ESCAPED_CHARS: [RegExp, string][] = [
[/&/g, '&amp;'],
[/"/g, '&quot;'],
[/'/g, '&apos;'],
[/</g, '&lt;'],
[/>/g, '&gt;'],
];
function _escapeXml(value: string): string {
return _XML_ESCAPED_CHARS.reduce((value, escape) => value.replace(escape[0], escape[1]), value);
}

View File

@ -9,15 +9,15 @@
import {AnimationAnimateMetadata, AnimationEntryMetadata, AnimationGroupMetadata, AnimationKeyframesSequenceMetadata, AnimationMetadata, AnimationStateDeclarationMetadata, AnimationStateMetadata, AnimationStateTransitionMetadata, AnimationStyleMetadata, AnimationWithStepsMetadata, AttributeMetadata, ChangeDetectionStrategy, ComponentMetadata, HostMetadata, Inject, InjectMetadata, Injectable, ModuleWithProviders, NgModule, NgModuleMetadata, Optional, OptionalMetadata, Provider, QueryMetadata, SchemaMetadata, SelfMetadata, SkipSelfMetadata, ViewMetadata, ViewQueryMetadata, resolveForwardRef} from '@angular/core';
import {Console, LIFECYCLE_HOOKS_VALUES, ReflectorReader, createProvider, isProviderLiteral, reflector} from '../core_private';
import {MapWrapper, StringMapWrapper} from '../src/facade/collection';
import {BaseException} from '../src/facade/exceptions';
import {Type, isArray, isBlank, isPresent, isString, stringify} from '../src/facade/lang';
import {StringMapWrapper} from '../src/facade/collection';
import {assertArrayOfStrings, assertInterpolationSymbols} from './assertions';
import * as cpl from './compile_metadata';
import {CompilerConfig} from './config';
import {hasLifecycleHook} from './directive_lifecycle_reflector';
import {DirectiveResolver} from './directive_resolver';
import {BaseException} from './facade/exceptions';
import {Type, isArray, isBlank, isPresent, isString, stringify} from './facade/lang';
import {Identifiers, identifierToken} from './identifiers';
import {NgModuleResolver} from './ng_module_resolver';
import {PipeResolver} from './pipe_resolver';

View File

@ -10,7 +10,7 @@ import {Injectable, NgModuleMetadata} from '@angular/core';
import {ReflectorReader, reflector} from '../core_private';
import {BaseException} from '../src/facade/exceptions';
import {Type, isBlank, isPresent, stringify} from '../src/facade/lang';
import {Type, isPresent, stringify} from './facade/lang';
function _isNgModuleMetadata(obj: any): obj is NgModuleMetadata {
return obj instanceof NgModuleMetadata;

View File

@ -70,7 +70,7 @@ export class OfflineCompiler {
return Promise
.all(components.map((compType) => {
const compMeta = this._metadataResolver.getDirectiveMetadata(<any>compType);
let ngModule = ngModulesSummary.ngModuleByComponent.get(compType);
const ngModule = ngModulesSummary.ngModuleByComponent.get(compType);
if (!ngModule) {
throw new BaseException(
`Cannot determine the module for component ${compMeta.type.name}!`);

View File

@ -9,8 +9,8 @@
import {Injectable, PipeMetadata, resolveForwardRef} from '@angular/core';
import {ReflectorReader, reflector} from '../core_private';
import {BaseException} from '../src/facade/exceptions';
import {Type, isPresent, stringify} from '../src/facade/lang';
import {BaseException} from './facade/exceptions';
import {Type, isPresent, stringify} from './facade/lang';
function _isPipeMetadata(type: any): boolean {
return type instanceof PipeMetadata;

View File

@ -6,11 +6,10 @@
* found in the LICENSE file at https://angular.io/license
*/
import {ListWrapper} from '../src/facade/collection';
import {BaseException} from '../src/facade/exceptions';
import {isArray, isBlank, isPresent, normalizeBlank} from '../src/facade/lang';
import {CompileDiDependencyMetadata, CompileDirectiveMetadata, CompileIdentifierMap, CompileNgModuleMetadata, CompileProviderMetadata, CompileQueryMetadata, CompileTokenMetadata, CompileTypeMetadata} from './compile_metadata';
import {ListWrapper} from './facade/collection';
import {BaseException} from './facade/exceptions';
import {isArray, isBlank, isPresent, normalizeBlank} from './facade/lang';
import {Identifiers, identifierToken} from './identifiers';
import {ParseError, ParseSourceSpan} from './parse_util';
import {AttrAst, DirectiveAst, ProviderAst, ProviderAstType, ReferenceAst, VariableAst} from './template_parser/template_ast';

View File

@ -9,22 +9,24 @@
import {Compiler, ComponentFactory, ComponentResolver, ComponentStillLoadingError, Injectable, Injector, ModuleWithComponentFactories, NgModule, NgModuleFactory, NgModuleMetadata, OptionalMetadata, Provider, SchemaMetadata, SkipSelfMetadata} from '@angular/core';
import {Console} from '../core_private';
import {BaseException} from '../src/facade/exceptions';
import {ConcreteType, IS_DART, Type, isBlank, isString, stringify} from '../src/facade/lang';
import {PromiseWrapper} from '../src/facade/async';
import {createHostComponentMeta, CompileDirectiveMetadata, CompilePipeMetadata, CompileIdentifierMetadata, CompileNgModuleMetadata} from './compile_metadata';
import {StyleCompiler, CompiledStylesheet} from './style_compiler';
import {ViewCompiler, ViewFactoryDependency, ComponentFactoryDependency} from './view_compiler/view_compiler';
import {NgModuleCompiler} from './ng_module_compiler';
import {TemplateParser} from './template_parser/template_parser';
import {DirectiveNormalizer} from './directive_normalizer';
import {CompileMetadataResolver} from './metadata_resolver';
import {CompileDirectiveMetadata, CompileIdentifierMetadata, CompileNgModuleMetadata, CompilePipeMetadata, createHostComponentMeta} from './compile_metadata';
import {CompilerConfig} from './config';
import {DirectiveNormalizer} from './directive_normalizer';
import {PromiseWrapper} from './facade/async';
import {BaseException} from './facade/exceptions';
import {ConcreteType, IS_DART, Type, isBlank, isString, stringify} from './facade/lang';
import {CompileMetadataResolver} from './metadata_resolver';
import {NgModuleCompiler} from './ng_module_compiler';
import * as ir from './output/output_ast';
import {jitStatements} from './output/output_jit';
import {interpretStatements} from './output/output_interpreter';
import {jitStatements} from './output/output_jit';
import {CompiledStylesheet, StyleCompiler} from './style_compiler';
import {TemplateParser} from './template_parser/template_parser';
import {SyncAsyncResult} from './util';
import {ComponentFactoryDependency, ViewCompiler, ViewFactoryDependency} from './view_compiler/view_compiler';
/**
* An internal module of the Angular compiler that begins with component types,

View File

@ -6,9 +6,9 @@
* found in the LICENSE file at https://angular.io/license
*/
import {ListWrapper} from '../src/facade/collection';
import {BaseException} from '../src/facade/exceptions';
import {RegExpMatcherWrapper, RegExpWrapper, StringWrapper, isBlank, isPresent} from '../src/facade/lang';
import {ListWrapper} from './facade/collection';
import {BaseException} from './facade/exceptions';
import {RegExpMatcherWrapper, RegExpWrapper, StringWrapper, isBlank, isPresent} from './facade/lang';
const _EMPTY_ATTR_VALUE = /*@ts2dart_const*/ '';

View File

@ -6,8 +6,8 @@
* found in the LICENSE file at https://angular.io/license
*/
import {ListWrapper} from '../src/facade/collection';
import {RegExpMatcherWrapper, RegExpWrapper, StringWrapper, isBlank, isPresent} from '../src/facade/lang';
import {ListWrapper} from './facade/collection';
import {RegExpMatcherWrapper, RegExpWrapper, StringWrapper, isBlank, isPresent} from './facade/lang';
/**
* This file is a port of shadowCSS from webcomponents.js to TypeScript.

View File

@ -9,7 +9,7 @@
// Some of the code comes from WebComponents.JS
// https://github.com/webcomponents/webcomponentsjs/blob/master/src/HTMLImports/path.js
import {RegExpWrapper, StringWrapper, isBlank, isPresent} from '../src/facade/lang';
import {RegExpWrapper, StringWrapper, isBlank, isPresent} from './facade/lang';
import {UrlResolver} from './url_resolver';

View File

@ -11,7 +11,7 @@ import {isPresent} from '../facade/lang';
import {CompileDirectiveMetadata, CompileTokenMetadata, CompileProviderMetadata,} from '../compile_metadata';
import {ParseSourceSpan} from '../parse_util';
import {SecurityContext} from '../../../core/index';
import {SecurityContext} from '@angular/core';
/**
* An Abstract Syntax Tree node representing part of a parsed Angular template.

View File

@ -6,30 +6,28 @@
* found in the LICENSE file at https://angular.io/license
*/
import {Inject, Injectable, OpaqueToken, Optional, SecurityContext, SchemaMetadata} from '../../../core/index';
import {Inject, Injectable, OpaqueToken, Optional, SchemaMetadata, SecurityContext} from '@angular/core';
import {Console, MAX_INTERPOLATION_VALUES} from '../../core_private';
import {ListWrapper, StringMapWrapper, SetWrapper,} from '../facade/collection';
import {RegExpWrapper, isPresent, StringWrapper, isBlank} from '../facade/lang';
import {RegExpWrapper, isPresent, isBlank} from '../facade/lang';
import {BaseException} from '../facade/exceptions';
import {AST, Interpolation, ASTWithSource, TemplateBinding, RecursiveAstVisitor, BindingPipe, ParserError} from '../expression_parser/ast';
import {Parser} from '../expression_parser/parser';
import {
CompileDirectiveMetadata, CompilePipeMetadata, CompileTokenMetadata,
removeIdentifierDuplicates,
} from '../compile_metadata';
import {HtmlParser, HtmlParseTreeResult} from '../html_parser/html_parser';
import {splitNsName, mergeNsAndName} from '../html_parser/html_tags';
import {CompileDirectiveMetadata, CompilePipeMetadata, CompileTokenMetadata, removeIdentifierDuplicates,} from '../compile_metadata';
import {HtmlParser, ParseTreeResult} from '../html_parser/html_parser';
import {splitNsName, mergeNsAndName} from '../html_parser/tags';
import {ParseSourceSpan, ParseError, ParseErrorLevel} from '../parse_util';
import {InterpolationConfig} from '../html_parser/interpolation_config';
import {ElementAst, BoundElementPropertyAst, BoundEventAst, ReferenceAst, TemplateAst, TemplateAstVisitor, templateVisitAll, TextAst, BoundTextAst, EmbeddedTemplateAst, AttrAst, NgContentAst, PropertyBindingType, DirectiveAst, BoundDirectivePropertyAst, ProviderAst, ProviderAstType, VariableAst} from './template_ast';
import {ElementAst, BoundElementPropertyAst, BoundEventAst, ReferenceAst, TemplateAst, TemplateAstVisitor, templateVisitAll, TextAst, BoundTextAst, EmbeddedTemplateAst, AttrAst, NgContentAst, PropertyBindingType, DirectiveAst, BoundDirectivePropertyAst, VariableAst} from './template_ast';
import {CssSelector, SelectorMatcher} from '../selector';
import {ElementSchemaRegistry} from '../schema/element_schema_registry';
import {preparseElement, PreparsedElementType} from './template_preparser';
import {isStyleUrlResolvable} from '../style_url_resolver';
import {HtmlAstVisitor, HtmlElementAst, HtmlAttrAst, HtmlTextAst, HtmlCommentAst, HtmlExpansionAst, HtmlExpansionCaseAst, htmlVisitAll} from '../html_parser/html_ast';
import * as html from '../html_parser/ast';
import {splitAtColon} from '../util';
import {identifierToken, Identifiers} from '../identifiers';
import {expandNodes} from '../html_parser/expander';
import {expandNodes} from '../html_parser/icu_ast_expander';
import {ProviderElementContext, ProviderViewContext} from '../provider_analyzer';
// Group 1 = "bind-"
@ -118,7 +116,7 @@ export class TemplateParser {
// Transform ICU messages to angular directives
const expandedHtmlAst = expandNodes(htmlAstWithErrors.rootNodes);
errors.push(...expandedHtmlAst.errors);
htmlAstWithErrors = new HtmlParseTreeResult(expandedHtmlAst.nodes, errors);
htmlAstWithErrors = new ParseTreeResult(expandedHtmlAst.nodes, errors);
}
if (htmlAstWithErrors.rootNodes.length > 0) {
@ -130,7 +128,7 @@ export class TemplateParser {
providerViewContext, uniqDirectives, uniqPipes, schemas, this._exprParser,
this._schemaRegistry);
result = htmlVisitAll(parseVisitor, htmlAstWithErrors.rootNodes, EMPTY_ELEMENT_CONTEXT);
result = html.visitAll(parseVisitor, htmlAstWithErrors.rootNodes, EMPTY_ELEMENT_CONTEXT);
errors.push(...parseVisitor.errors, ...providerViewContext.errors);
} else {
result = [];
@ -170,7 +168,7 @@ export class TemplateParser {
}
}
class TemplateParseVisitor implements HtmlAstVisitor {
class TemplateParseVisitor implements html.Visitor {
selectorMatcher: SelectorMatcher;
errors: TemplateParseError[] = [];
directivesIndex = new Map<CompileDirectiveMetadata, number>();
@ -291,27 +289,27 @@ class TemplateParseVisitor implements HtmlAstVisitor {
}
}
visitExpansion(ast: HtmlExpansionAst, context: any): any { return null; }
visitExpansion(expansion: html.Expansion, context: any): any { return null; }
visitExpansionCase(ast: HtmlExpansionCaseAst, context: any): any { return null; }
visitExpansionCase(expansionCase: html.ExpansionCase, context: any): any { return null; }
visitText(ast: HtmlTextAst, parent: ElementContext): any {
visitText(text: html.Text, parent: ElementContext): any {
const ngContentIndex = parent.findNgContentIndex(TEXT_CSS_SELECTOR);
const expr = this._parseInterpolation(ast.value, ast.sourceSpan);
const expr = this._parseInterpolation(text.value, text.sourceSpan);
if (isPresent(expr)) {
return new BoundTextAst(expr, ngContentIndex, ast.sourceSpan);
return new BoundTextAst(expr, ngContentIndex, text.sourceSpan);
} else {
return new TextAst(ast.value, ngContentIndex, ast.sourceSpan);
return new TextAst(text.value, ngContentIndex, text.sourceSpan);
}
}
visitAttr(ast: HtmlAttrAst, contex: any): any {
return new AttrAst(ast.name, ast.value, ast.sourceSpan);
visitAttribute(attribute: html.Attribute, contex: any): any {
return new AttrAst(attribute.name, attribute.value, attribute.sourceSpan);
}
visitComment(ast: HtmlCommentAst, context: any): any { return null; }
visitComment(comment: html.Comment, context: any): any { return null; }
visitElement(element: HtmlElementAst, parent: ElementContext): any {
visitElement(element: html.Element, parent: ElementContext): any {
const nodeName = element.name;
const preparsedElement = preparseElement(element);
if (preparsedElement.type === PreparsedElementType.SCRIPT ||
@ -359,7 +357,7 @@ class TemplateParseVisitor implements HtmlAstVisitor {
if (!hasBinding && !hasTemplateBinding) {
// don't include the bindings as attributes as well in the AST
attrs.push(this.visitAttr(attr, null));
attrs.push(this.visitAttribute(attr, null));
matchableAttrs.push([attr.name, attr.value]);
}
if (hasTemplateBinding) {
@ -380,7 +378,7 @@ class TemplateParseVisitor implements HtmlAstVisitor {
const providerContext = new ProviderElementContext(
this.providerViewContext, parent.providerContext, isViewRoot, directiveAsts, attrs,
references, element.sourceSpan);
const children = htmlVisitAll(
const children = html.visitAll(
preparsedElement.nonBindable ? NON_BINDABLE_VISITOR : this, element.children,
ElementContext.create(
isTemplateElement, directiveAsts,
@ -448,7 +446,7 @@ class TemplateParseVisitor implements HtmlAstVisitor {
}
private _parseInlineTemplateBinding(
attr: HtmlAttrAst, targetMatchableAttrs: string[][],
attr: html.Attribute, targetMatchableAttrs: string[][],
targetProps: BoundElementOrDirectiveProperty[], targetVars: VariableAst[]): boolean {
let templateBindingsSource: string = null;
if (this._normalizeAttributeName(attr.name) == TEMPLATE_ATTR) {
@ -477,7 +475,7 @@ class TemplateParseVisitor implements HtmlAstVisitor {
}
private _parseAttr(
isTemplateElement: boolean, attr: HtmlAttrAst, targetMatchableAttrs: string[][],
isTemplateElement: boolean, attr: html.Attribute, targetMatchableAttrs: string[][],
targetProps: BoundElementOrDirectiveProperty[],
targetAnimationProps: BoundElementPropertyAst[], targetEvents: BoundEventAst[],
targetRefs: ElementOrDirectiveRef[], targetVars: VariableAst[]): boolean {
@ -910,8 +908,8 @@ class TemplateParseVisitor implements HtmlAstVisitor {
}
}
class NonBindableVisitor implements HtmlAstVisitor {
visitElement(ast: HtmlElementAst, parent: ElementContext): ElementAst {
class NonBindableVisitor implements html.Visitor {
visitElement(ast: html.Element, parent: ElementContext): ElementAst {
const preparsedElement = preparseElement(ast);
if (preparsedElement.type === PreparsedElementType.SCRIPT ||
preparsedElement.type === PreparsedElementType.STYLE ||
@ -925,21 +923,25 @@ class NonBindableVisitor implements HtmlAstVisitor {
const attrNameAndValues = ast.attrs.map(attrAst => [attrAst.name, attrAst.value]);
const selector = createElementCssSelector(ast.name, attrNameAndValues);
const ngContentIndex = parent.findNgContentIndex(selector);
const children = htmlVisitAll(this, ast.children, EMPTY_ELEMENT_CONTEXT);
const children = html.visitAll(this, ast.children, EMPTY_ELEMENT_CONTEXT);
return new ElementAst(
ast.name, htmlVisitAll(this, ast.attrs), [], [], [], [], [], false, children,
ast.name, html.visitAll(this, ast.attrs), [], [], [], [], [], false, children,
ngContentIndex, ast.sourceSpan);
}
visitComment(ast: HtmlCommentAst, context: any): any { return null; }
visitAttr(ast: HtmlAttrAst, context: any): AttrAst {
return new AttrAst(ast.name, ast.value, ast.sourceSpan);
visitComment(comment: html.Comment, context: any): any { return null; }
visitAttribute(attribute: html.Attribute, context: any): AttrAst {
return new AttrAst(attribute.name, attribute.value, attribute.sourceSpan);
}
visitText(ast: HtmlTextAst, parent: ElementContext): TextAst {
visitText(text: html.Text, parent: ElementContext): TextAst {
const ngContentIndex = parent.findNgContentIndex(TEXT_CSS_SELECTOR);
return new TextAst(ast.value, ngContentIndex, ast.sourceSpan);
return new TextAst(text.value, ngContentIndex, text.sourceSpan);
}
visitExpansion(ast: HtmlExpansionAst, context: any): any { return ast; }
visitExpansionCase(ast: HtmlExpansionCaseAst, context: any): any { return ast; }
visitExpansion(expansion: html.Expansion, context: any): any { return expansion; }
visitExpansionCase(expansionCase: html.ExpansionCase, context: any): any { return expansionCase; }
}
class BoundElementOrDirectiveProperty {
@ -953,7 +955,7 @@ class ElementOrDirectiveRef {
}
export function splitClasses(classAttrValue: string): string[] {
return StringWrapper.split(classAttrValue.trim(), /\s+/g);
return classAttrValue.trim().split(/\s+/g);
}
class ElementContext {
@ -967,7 +969,7 @@ class ElementContext {
const ngContentSelectors = component.directive.template.ngContentSelectors;
for (let i = 0; i < ngContentSelectors.length; i++) {
const selector = ngContentSelectors[i];
if (StringWrapper.equals(selector, '*')) {
if (selector === '*') {
wildcardNgContentIndex = i;
} else {
matcher.addSelectables(CssSelector.parse(ngContentSelectors[i]), i);

View File

@ -6,10 +6,8 @@
* found in the LICENSE file at https://angular.io/license
*/
import {isBlank} from '../facade/lang';
import {HtmlElementAst} from '../html_parser/html_ast';
import {splitNsName} from '../html_parser/html_tags';
import * as html from '../html_parser/ast';
import {splitNsName} from '../html_parser/tags';
const NG_CONTENT_SELECT_ATTR = 'select';
const NG_CONTENT_ELEMENT = 'ng-content';
@ -22,7 +20,7 @@ const SCRIPT_ELEMENT = 'script';
const NG_NON_BINDABLE_ATTR = 'ngNonBindable';
const NG_PROJECT_AS = 'ngProjectAs';
export function preparseElement(ast: HtmlElementAst): PreparsedElement {
export function preparseElement(ast: html.Element): PreparsedElement {
var selectAttr: string = null;
var hrefAttr: string = null;
var relAttr: string = null;
@ -75,7 +73,7 @@ export class PreparsedElement {
function normalizeNgContentSelect(selectAttr: string): string {
if (isBlank(selectAttr) || selectAttr.length === 0) {
if (selectAttr === null || selectAttr.length === 0) {
return '*';
}
return selectAttr;

View File

@ -8,7 +8,7 @@
import {Inject, Injectable, PACKAGE_ROOT_URL} from '@angular/core';
import {StringWrapper, isPresent, isBlank, RegExpWrapper,} from '../src/facade/lang';
import {StringWrapper, isPresent, isBlank, RegExpWrapper,} from './facade/lang';
const _ASSET_SCHEME = 'asset:';