feat(i18n): xtb serializer
This commit is contained in:
@ -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';
|
||||
|
@ -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';
|
||||
|
@ -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';
|
||||
|
@ -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],
|
||||
|
@ -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(
|
||||
|
72
modules/@angular/compiler/src/html_parser/ast.ts
Normal file
72
modules/@angular/compiler/src/html_parser/ast.ts
Normal 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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
@ -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 `{` / `ƫ` 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;
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
@ -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 {
|
412
modules/@angular/compiler/src/html_parser/parser.ts
Normal file
412
modules/@angular/compiler/src/html_parser/parser.ts
Normal 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;
|
||||
}
|
310
modules/@angular/compiler/src/html_parser/tags.ts
Normal file
310
modules/@angular/compiler/src/html_parser/tags.ts
Normal 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 `{` / `ƫ` 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',
|
||||
};
|
20
modules/@angular/compiler/src/html_parser/xml_parser.ts
Normal file
20
modules/@angular/compiler/src/html_parser/xml_parser.ts
Normal 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);
|
||||
}
|
||||
}
|
30
modules/@angular/compiler/src/html_parser/xml_tags.ts
Normal file
30
modules/@angular/compiler/src/html_parser/xml_tags.ts
Normal 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;
|
||||
}
|
@ -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) {}
|
||||
}
|
||||
|
@ -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; }
|
||||
|
@ -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 {}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
12
modules/@angular/compiler/src/i18n/index.ts
Normal file
12
modules/@angular/compiler/src/i18n/index.ts
Normal 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';
|
@ -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}`);
|
||||
}
|
@ -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));
|
||||
}
|
@ -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)));
|
||||
}
|
||||
}
|
@ -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;
|
@ -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[]};
|
||||
}
|
@ -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)));
|
||||
}
|
||||
}
|
||||
|
149
modules/@angular/compiler/src/i18n/serializers/xtb.ts
Normal file
149
modules/@angular/compiler/src/i18n/serializers/xtb.ts
Normal 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));
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
24
modules/@angular/compiler/src/i18n/translation_bundle.ts
Normal file
24
modules/@angular/compiler/src/i18n/translation_bundle.ts
Normal 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));
|
||||
}
|
||||
}
|
@ -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, '&'],
|
||||
[/"/g, '"'],
|
||||
[/'/g, '''],
|
||||
[/</g, '<'],
|
||||
[/>/g, '>'],
|
||||
];
|
||||
|
||||
function _escapeXml(value: string): string {
|
||||
return _XML_ESCAPED_CHARS.reduce((value, escape) => value.replace(escape[0], escape[1]), value);
|
||||
}
|
@ -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';
|
||||
|
@ -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;
|
||||
|
@ -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}!`);
|
||||
|
@ -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;
|
||||
|
@ -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';
|
||||
|
@ -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,
|
||||
|
@ -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*/ '';
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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:';
|
||||
|
Reference in New Issue
Block a user