feat(i18n): xtb serializer
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
+26
-27
@@ -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);
|
||||
}
|
||||
+81
-75
@@ -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 {
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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',
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user