feat(i18n): xtb serializer

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

View File

@ -7,7 +7,6 @@
*/
import './init';
let serializer = require('@angular/compiler/src/i18n/xmb_serializer.js');
import * as fs from 'fs';
import * as path from 'path';
@ -16,13 +15,14 @@ describe('template i18n extraction output', () => {
const outDir = '';
it('should extract i18n messages', () => {
const EXPECTED = `<? xml version="1.0" encoding="UTF-8" ?>
<messagebundle>
<msg id="5a2858f1" desc="desc" meaning="meaning">translate me</msg>
</messagebundle>`;
const xmbOutput = path.join(outDir, 'messages.xmb');
expect(fs.existsSync(xmbOutput)).toBeTruthy();
const xmb = fs.readFileSync(xmbOutput, {encoding: 'utf-8'});
const res = serializer.deserializeXmb(xmb);
const keys = Object.keys(res.messages);
expect(keys.length).toEqual(1);
expect(res.errors.length).toEqual(0);
expect(res.messages[keys[0]][0].value).toEqual('translate me');
expect(xmb).toEqual(EXPECTED);
});
});

View File

@ -84,14 +84,14 @@ export class CodeGenerator {
}
codegen(): Promise<any> {
let filePaths =
const filePaths =
this.program.getSourceFiles().map(sf => sf.fileName).filter(f => !GENERATED_FILES.test(f));
let fileMetas = filePaths.map((filePath) => this.readFileMetadata(filePath));
let ngModules = fileMetas.reduce((ngModules, fileMeta) => {
const fileMetas = filePaths.map((filePath) => this.readFileMetadata(filePath));
const ngModules = fileMetas.reduce((ngModules, fileMeta) => {
ngModules.push(...fileMeta.ngModules);
return ngModules;
}, <StaticSymbol[]>[]);
let analyzedNgModules = this.compiler.analyzeModules(ngModules);
const analyzedNgModules = this.compiler.analyzeModules(ngModules);
return Promise
.all(fileMetas.map(
(fileMeta) => this.compiler

View File

@ -20,25 +20,12 @@ export var CompileMetadataResolver: typeof _c.CompileMetadataResolver = _c.Compi
export type HtmlParser = _c.HtmlParser;
export var HtmlParser: typeof _c.HtmlParser = _c.HtmlParser;
export type I18nHtmlParser = _c.I18nHtmlParser;
export var I18nHtmlParser: typeof _c.I18nHtmlParser = _c.I18nHtmlParser;
export type MessageExtractor = _c.MessageExtractor;
export var MessageExtractor: typeof _c.MessageExtractor = _c.MessageExtractor;
export type ExtractionResult = _c.ExtractionResult;
export var ExtractionResult: typeof _c.ExtractionResult = _c.ExtractionResult;
export type Message = _c.Message;
export var Message: typeof _c.Message = _c.Message;
export var removeDuplicates: typeof _c.removeDuplicates = _c.removeDuplicates;
export var serializeXmb: typeof _c.serializeXmb = _c.serializeXmb;
export var deserializeXmb: typeof _c.deserializeXmb = _c.deserializeXmb;
export type ParseError = _c.ParseError;
export var ParseError: typeof _c.ParseError = _c.ParseError;
export type InterpolationConfig = _c.InterpolationConfig;
export var InterpolationConfig: typeof _c.InterpolationConfig = _c.InterpolationConfig;
export type DirectiveNormalizer = _c.DirectiveNormalizer;
export var DirectiveNormalizer: typeof _c.DirectiveNormalizer = _c.DirectiveNormalizer;

View File

@ -10,121 +10,124 @@
/**
* Extract i18n messages from source code
*
* TODO(vicb): factorize code with the CodeGenerator
*/
// Must be imported first, because angular2 decorators throws on load.
import 'reflect-metadata';
import * as compiler from '@angular/compiler';
import {ComponentMetadata, NgModuleMetadata, ViewEncapsulation} from '@angular/core';
import * as path from 'path';
import * as ts from 'typescript';
import * as tsc from '@angular/tsc-wrapped';
import * as path from 'path';
import * as compiler from '@angular/compiler';
import {ViewEncapsulation} from '@angular/core';
import {StaticReflector} from './static_reflector';
import {CompileMetadataResolver, HtmlParser, DirectiveNormalizer, Lexer, Parser, DomElementSchemaRegistry, TypeScriptEmitter, MessageExtractor, removeDuplicates, ExtractionResult, Message, ParseError, serializeXmb,} from './compiler_private';
import {CompileMetadataResolver, DirectiveNormalizer, DomElementSchemaRegistry, HtmlParser, Lexer, NgModuleCompiler, Parser, StyleCompiler, TemplateParser, TypeScriptEmitter, ViewCompiler, ParseError} from './compiler_private';
import {Console} from './core_private';
import {ReflectorHost} from './reflector_host';
import {ReflectorHost, ReflectorHostContext} from './reflector_host';
import {StaticAndDynamicReflectionCapabilities} from './static_reflection_capabilities';
import {StaticReflector, StaticSymbol} from './static_reflector';
function extract(
ngOptions: tsc.AngularCompilerOptions, program: ts.Program, host: ts.CompilerHost) {
return Extractor.create(ngOptions, program, host).extract();
const extractor = Extractor.create(ngOptions, program, host);
const bundlePromise: Promise<compiler.i18n.MessageBundle> = extractor.extract();
return (bundlePromise).then(messageBundle => {
const serializer = new compiler.i18n.Xmb();
const dstPath = path.join(ngOptions.genDir, 'messages.xmb');
host.writeFile(dstPath, messageBundle.write(serializer), false);
});
}
const _dirPaths = new Map<compiler.CompileDirectiveMetadata, string>();
const GENERATED_FILES = /\.ngfactory\.ts$|\.css\.ts$|\.css\.shim\.ts$/;
const _GENERATED_FILES = /\.ngfactory\.ts$|\.css\.ts$|\.css\.shim\.ts$/;
class Extractor {
export class Extractor {
constructor(
private _options: tsc.AngularCompilerOptions, private _program: ts.Program,
public host: ts.CompilerHost, private staticReflector: StaticReflector,
private _resolver: CompileMetadataResolver, private _normalizer: DirectiveNormalizer,
private _reflectorHost: ReflectorHost, private _extractor: MessageExtractor) {}
private program: ts.Program, public host: ts.CompilerHost,
private staticReflector: StaticReflector, private messageBundle: compiler.i18n.MessageBundle,
private reflectorHost: ReflectorHost, private metadataResolver: CompileMetadataResolver,
private directiveNormalizer: DirectiveNormalizer,
private compiler: compiler.OfflineCompiler) {}
private _extractCmpMessages(components: compiler.CompileDirectiveMetadata[]): ExtractionResult {
if (!components || !components.length) {
return null;
}
let messages: Message[] = [];
let errors: ParseError[] = [];
components.forEach(metadata => {
let url = _dirPaths.get(metadata);
let result = this._extractor.extract(metadata.template.template, url);
errors = errors.concat(result.errors);
messages = messages.concat(result.messages);
});
// Extraction Result might contain duplicate messages at this point
return new ExtractionResult(messages, errors);
}
private _readComponents(absSourcePath: string): Promise<compiler.CompileDirectiveMetadata>[] {
const result: Promise<compiler.CompileDirectiveMetadata>[] = [];
const metadata = this.staticReflector.getModuleMetadata(absSourcePath);
if (!metadata) {
private readFileMetadata(absSourcePath: string): FileMetadata {
const moduleMetadata = this.staticReflector.getModuleMetadata(absSourcePath);
const result: FileMetadata = {components: [], ngModules: [], fileUrl: absSourcePath};
if (!moduleMetadata) {
console.log(`WARNING: no metadata found for ${absSourcePath}`);
return result;
}
const symbols = Object.keys(metadata['metadata']);
const metadata = moduleMetadata['metadata'];
const symbols = metadata && Object.keys(metadata);
if (!symbols || !symbols.length) {
return result;
}
for (const symbol of symbols) {
const staticType = this._reflectorHost.findDeclaration(absSourcePath, symbol, absSourcePath);
let directive: compiler.CompileDirectiveMetadata;
directive = this._resolver.getDirectiveMetadata(<any>staticType, false);
if (directive && directive.isComponent) {
let promise = this._normalizer.normalizeDirective(directive).asyncResult;
promise.then(md => _dirPaths.set(md, absSourcePath));
result.push(promise);
if (metadata[symbol] && metadata[symbol].__symbolic == 'error') {
// Ignore symbols that are only included to record error information.
continue;
}
const staticType = this.reflectorHost.findDeclaration(absSourcePath, symbol, absSourcePath);
const annotations = this.staticReflector.annotations(staticType);
annotations.forEach((annotation) => {
if (annotation instanceof NgModuleMetadata) {
result.ngModules.push(staticType);
} else if (annotation instanceof ComponentMetadata) {
result.components.push(staticType);
}
});
}
return result;
}
extract(): Promise<any> {
_dirPaths.clear();
extract(): Promise<compiler.i18n.MessageBundle> {
const filePaths =
this.program.getSourceFiles().map(sf => sf.fileName).filter(f => !GENERATED_FILES.test(f));
const fileMetas = filePaths.map((filePath) => this.readFileMetadata(filePath));
const ngModules = fileMetas.reduce((ngModules, fileMeta) => {
ngModules.push(...fileMeta.ngModules);
return ngModules;
}, <StaticSymbol[]>[]);
const analyzedNgModules = this.compiler.analyzeModules(ngModules);
const errors: ParseError[] = [];
const promises = this._program.getSourceFiles()
.map(sf => sf.fileName)
.filter(f => !_GENERATED_FILES.test(f))
.map(
(absSourcePath: string): Promise<any> =>
Promise.all(this._readComponents(absSourcePath))
.then(metadatas => this._extractCmpMessages(metadatas))
.catch(e => console.error(e.stack)));
let bundlePromise =
Promise
.all(fileMetas.map((fileMeta) => {
const url = fileMeta.fileUrl;
return Promise.all(fileMeta.components.map(compType => {
const compMeta = this.metadataResolver.getDirectiveMetadata(<any>compType);
const ngModule = analyzedNgModules.ngModuleByComponent.get(compType);
if (!ngModule) {
throw new Error(
`Cannot determine the module for component ${compMeta.type.name}!`);
}
return Promise
.all([compMeta, ...ngModule.transitiveModule.directives].map(
dirMeta =>
this.directiveNormalizer.normalizeDirective(dirMeta).asyncResult))
.then((normalizedCompWithDirectives) => {
const compMeta = normalizedCompWithDirectives[0];
const html = compMeta.template.template;
const interpolationConfig =
compiler.InterpolationConfig.fromArray(compMeta.template.interpolation);
errors.push(
...this.messageBundle.updateFromTemplate(html, url, interpolationConfig));
});
}));
}))
.then(_ => this.messageBundle)
.catch((e) => { console.error(e.stack); });
let messages: Message[] = [];
let errors: ParseError[] = [];
if (errors.length) {
throw new Error(errors.map(e => e.toString()).join('\n'));
}
return Promise.all(promises).then(extractionResults => {
extractionResults.filter(result => !!result).forEach(result => {
messages = messages.concat(result.messages);
errors = errors.concat(result.errors);
});
if (errors.length) {
throw new Error(errors.map(e => e.toString()).join('\n'));
}
messages = removeDuplicates(messages);
let genPath = path.join(this._options.genDir, 'messages.xmb');
let msgBundle = serializeXmb(messages);
this.host.writeFile(genPath, msgBundle, false);
});
return bundlePromise;
}
static create(
options: tsc.AngularCompilerOptions, program: ts.Program,
compilerHost: ts.CompilerHost): Extractor {
options: tsc.AngularCompilerOptions, program: ts.Program, compilerHost: ts.CompilerHost,
reflectorHostContext?: ReflectorHostContext): Extractor {
const xhr: compiler.XHR = {
get: (s: string) => {
if (!compilerHost.fileExists(s)) {
@ -134,35 +137,49 @@ class Extractor {
return Promise.resolve(compilerHost.readFile(s));
}
};
const urlResolver: compiler.UrlResolver = compiler.createOfflineCompileUrlResolver();
const reflectorHost = new ReflectorHost(program, compilerHost, options);
const reflectorHost = new ReflectorHost(program, compilerHost, options, reflectorHostContext);
const staticReflector = new StaticReflector(reflectorHost);
StaticAndDynamicReflectionCapabilities.install(staticReflector);
const htmlParser = new HtmlParser();
const config = new compiler.CompilerConfig({
genDebugInfo: true,
genDebugInfo: options.debug === true,
defaultEncapsulation: ViewEncapsulation.Emulated,
logBindingUpdate: false,
useJit: false
});
const normalizer = new DirectiveNormalizer(xhr, urlResolver, htmlParser, config);
const expressionParser = new Parser(new Lexer());
const elementSchemaRegistry = new DomElementSchemaRegistry();
const console = new Console();
const tmplParser =
new TemplateParser(expressionParser, elementSchemaRegistry, htmlParser, console, []);
const resolver = new CompileMetadataResolver(
new compiler.NgModuleResolver(staticReflector),
new compiler.DirectiveResolver(staticReflector), new compiler.PipeResolver(staticReflector),
config, console, elementSchemaRegistry, staticReflector);
const offlineCompiler = new compiler.OfflineCompiler(
resolver, normalizer, tmplParser, new StyleCompiler(urlResolver), new ViewCompiler(config),
new NgModuleCompiler(), new TypeScriptEmitter(reflectorHost));
// TODO(vicb): handle implicit
const extractor = new MessageExtractor(htmlParser, expressionParser, [], {});
// TODO(vicb): implicit tags & attributes
let messageBundle = new compiler.i18n.MessageBundle(htmlParser, [], {});
return new Extractor(
options, program, compilerHost, staticReflector, resolver, normalizer, reflectorHost,
extractor);
program, compilerHost, staticReflector, messageBundle, reflectorHost, resolver, normalizer,
offlineCompiler);
}
}
interface FileMetadata {
fileUrl: string;
components: StaticSymbol[];
ngModules: StaticSymbol[];
}
// Entry point
if (require.main === module) {
const args = require('minimist')(process.argv.slice(2));
@ -170,7 +187,7 @@ if (require.main === module) {
.then(exitCode => process.exit(exitCode))
.catch(e => {
console.error(e.stack);
console.error('Compilation failed');
console.error('Extraction failed');
process.exit(1);
});
}
}

View File

@ -1,18 +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
*/
/**
* @module
* @description
* Starting point to import all compiler APIs.
*/
export {COMPILER_PROVIDERS, CompileDiDependencyMetadata, CompileDirectiveMetadata, CompileFactoryMetadata, CompileIdentifierMetadata, CompileMetadataWithIdentifier, CompilePipeMetadata, CompileProviderMetadata, CompileQueryMetadata, CompileTemplateMetadata, CompileTokenMetadata, CompileTypeMetadata, CompilerConfig, DEFAULT_PACKAGE_URL_PROVIDER, DirectiveResolver, NgModuleResolver, OfflineCompiler, PipeResolver, RenderTypes, RuntimeCompiler, SourceModule, TEMPLATE_TRANSFORMS, UrlResolver, XHR, analyzeAppProvidersForDeprecatedConfiguration, createOfflineCompileUrlResolver, platformCoreDynamic} from './src/compiler';
export {ElementSchemaRegistry} from './src/schema/element_schema_registry';
export * from './src/template_parser/template_ast';
export * from './private_export';

View File

@ -6,4 +6,17 @@
* found in the LICENSE file at https://angular.io/license
*/
export * from './compiler';
/**
* @module
* @description
* Starting point to import all compiler APIs.
*/
import * as i18n from './src/i18n/index';
export {COMPILER_PROVIDERS, CompileDiDependencyMetadata, CompileDirectiveMetadata, CompileFactoryMetadata, CompileIdentifierMetadata, CompileMetadataWithIdentifier, CompilePipeMetadata, CompileProviderMetadata, CompileQueryMetadata, CompileTemplateMetadata, CompileTokenMetadata, CompileTypeMetadata, CompilerConfig, DEFAULT_PACKAGE_URL_PROVIDER, DirectiveResolver, NgModuleResolver, OfflineCompiler, PipeResolver, RenderTypes, RuntimeCompiler, SourceModule, TEMPLATE_TRANSFORMS, UrlResolver, ViewResolver, XHR, analyzeAppProvidersForDeprecatedConfiguration, createOfflineCompileUrlResolver, platformCoreDynamic} from './src/compiler';
export {InterpolationConfig} from './src/html_parser/interpolation_config';
export {ElementSchemaRegistry} from './src/schema/element_schema_registry';
export {i18n};
export * from './src/template_parser/template_ast';
export * from './private_export';

View File

@ -10,10 +10,7 @@ import * as directive_normalizer from './src/directive_normalizer';
import * as lexer from './src/expression_parser/lexer';
import * as parser from './src/expression_parser/parser';
import * as html_parser from './src/html_parser/html_parser';
import * as i18n_html_parser from './src/i18n/i18n_html_parser';
import * as i18n_message from './src/i18n/message';
import * as i18n_extractor from './src/i18n/message_extractor';
import * as xmb_serializer from './src/i18n/xmb_serializer';
import * as interpolation_config from './src/html_parser/interpolation_config';
import * as metadata_resolver from './src/metadata_resolver';
import * as ng_module_compiler from './src/ng_module_compiler';
import * as path_util from './src/output/path_util';
@ -44,22 +41,8 @@ export var CompileMetadataResolver = metadata_resolver.CompileMetadataResolver;
export type HtmlParser = html_parser.HtmlParser;
export var HtmlParser = html_parser.HtmlParser;
export type I18nHtmlParser = i18n_html_parser.I18nHtmlParser;
export var I18nHtmlParser = i18n_html_parser.I18nHtmlParser;
export type ExtractionResult = i18n_extractor.ExtractionResult;
export var ExtractionResult = i18n_extractor.ExtractionResult;
export type Message = i18n_message.Message;
export var Message = i18n_message.Message;
export type MessageExtractor = i18n_extractor.MessageExtractor;
export var MessageExtractor = i18n_extractor.MessageExtractor;
export var removeDuplicates = i18n_extractor.removeDuplicates;
export var serializeXmb = xmb_serializer.serializeXmb;
export var deserializeXmb = xmb_serializer.deserializeXmb;
export type InterpolationConfig = interpolation_config.InterpolationConfig;
export var InterpolationConfig = interpolation_config.InterpolationConfig;
export type DirectiveNormalizer = directive_normalizer.DirectiveNormalizer;
export var DirectiveNormalizer = directive_normalizer.DirectiveNormalizer;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,9 +1,17 @@
/**
* @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 {beforeEach, ddescribe, describe, expect, it} from '../../../core/testing/testing_internal';
import {HtmlAst, HtmlAstVisitor, HtmlAttrAst, HtmlCommentAst, HtmlElementAst, HtmlExpansionAst, HtmlExpansionCaseAst, HtmlTextAst, htmlVisitAll} from '../../src/html_parser/html_ast';
import * as html from '../../src/html_parser/ast';
import {HtmlParser} from '../../src/html_parser/html_parser';
export function main() {
describe('HtmlAst serilaizer', () => {
describe('Node serilaizer', () => {
var parser: HtmlParser;
beforeEach(() => { parser = new HtmlParser(); });
@ -52,35 +60,37 @@ export function main() {
});
}
class _SerializerVisitor implements HtmlAstVisitor {
visitElement(ast: HtmlElementAst, context: any): any {
return `<${ast.name}${this._visitAll(ast.attrs, ' ')}>${this._visitAll(ast.children)}</${ast.name}>`;
class _SerializerVisitor implements html.Visitor {
visitElement(element: html.Element, context: any): any {
return `<${element.name}${this._visitAll(element.attrs, ' ')}>${this._visitAll(element.children)}</${element.name}>`;
}
visitAttr(ast: HtmlAttrAst, context: any): any { return `${ast.name}="${ast.value}"`; }
visitText(ast: HtmlTextAst, context: any): any { return ast.value; }
visitComment(ast: HtmlCommentAst, context: any): any { return `<!--${ast.value}-->`; }
visitExpansion(ast: HtmlExpansionAst, context: any): any {
return `{${ast.switchValue}, ${ast.type},${this._visitAll(ast.cases)}}`;
visitAttribute(attribute: html.Attribute, context: any): any {
return `${attribute.name}="${attribute.value}"`;
}
visitExpansionCase(ast: HtmlExpansionCaseAst, context: any): any {
return ` ${ast.value} {${this._visitAll(ast.expression)}}`;
visitText(text: html.Text, context: any): any { return text.value; }
visitComment(comment: html.Comment, context: any): any { return `<!--${comment.value}-->`; }
visitExpansion(expansion: html.Expansion, context: any): any {
return `{${expansion.switchValue}, ${expansion.type},${this._visitAll(expansion.cases)}}`;
}
private _visitAll(ast: HtmlAst[], join: string = ''): string {
if (ast.length == 0) {
visitExpansionCase(expansionCase: html.ExpansionCase, context: any): any {
return ` ${expansionCase.value} {${this._visitAll(expansionCase.expression)}}`;
}
private _visitAll(nodes: html.Node[], join: string = ''): string {
if (nodes.length == 0) {
return '';
}
return join + ast.map(a => a.visit(this, null)).join(join);
return join + nodes.map(a => a.visit(this, null)).join(join);
}
}
const serializerVisitor = new _SerializerVisitor();
export function serializeAst(ast: HtmlAst[]): string[] {
return ast.map(a => a.visit(serializerVisitor, null));
export function serializeAst(nodes: html.Node[]): string[] {
return nodes.map(node => node.visit(serializerVisitor, null));
}

View File

@ -0,0 +1,85 @@
/**
* @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 {BaseException} from '../../src/facade/exceptions';
import * as html from '../../src/html_parser/ast';
import {ParseTreeResult} from '../../src/html_parser/html_parser';
import {ParseLocation} from '../../src/parse_util';
export function humanizeDom(parseResult: ParseTreeResult, addSourceSpan: boolean = false): any[] {
if (parseResult.errors.length > 0) {
var errorString = parseResult.errors.join('\n');
throw new BaseException(`Unexpected parse errors:\n${errorString}`);
}
return humanizeNodes(parseResult.rootNodes, addSourceSpan);
}
export function humanizeDomSourceSpans(parseResult: ParseTreeResult): any[] {
return humanizeDom(parseResult, true);
}
export function humanizeNodes(nodes: html.Node[], addSourceSpan: boolean = false): any[] {
var humanizer = new _Humanizer(addSourceSpan);
html.visitAll(humanizer, nodes);
return humanizer.result;
}
export function humanizeLineColumn(location: ParseLocation): string {
return `${location.line}:${location.col}`;
}
class _Humanizer implements html.Visitor {
result: any[] = [];
elDepth: number = 0;
constructor(private includeSourceSpan: boolean){};
visitElement(element: html.Element, context: any): any {
var res = this._appendContext(element, [html.Element, element.name, this.elDepth++]);
this.result.push(res);
html.visitAll(this, element.attrs);
html.visitAll(this, element.children);
this.elDepth--;
}
visitAttribute(attribute: html.Attribute, context: any): any {
var res = this._appendContext(attribute, [html.Attribute, attribute.name, attribute.value]);
this.result.push(res);
}
visitText(text: html.Text, context: any): any {
var res = this._appendContext(text, [html.Text, text.value, this.elDepth]);
this.result.push(res);
}
visitComment(comment: html.Comment, context: any): any {
var res = this._appendContext(comment, [html.Comment, comment.value, this.elDepth]);
this.result.push(res);
}
visitExpansion(expansion: html.Expansion, context: any): any {
var res = this._appendContext(
expansion, [html.Expansion, expansion.switchValue, expansion.type, this.elDepth++]);
this.result.push(res);
html.visitAll(this, expansion.cases);
this.elDepth--;
}
visitExpansionCase(expansionCase: html.ExpansionCase, context: any): any {
var res =
this._appendContext(expansionCase, [html.ExpansionCase, expansionCase.value, this.elDepth]);
this.result.push(res);
}
private _appendContext(ast: html.Node, input: any[]): any[] {
if (!this.includeSourceSpan) return input;
input.push(ast.sourceSpan.toString());
return input;
}
}

View File

@ -1,85 +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 {BaseException} from '../../src/facade/exceptions';
import {HtmlAst, HtmlAstVisitor, HtmlAttrAst, HtmlCommentAst, HtmlElementAst, HtmlExpansionAst, HtmlExpansionCaseAst, HtmlTextAst, htmlVisitAll} from '../../src/html_parser/html_ast';
import {HtmlParseTreeResult} from '../../src/html_parser/html_parser';
import {ParseLocation} from '../../src/parse_util';
export function humanizeDom(
parseResult: HtmlParseTreeResult, addSourceSpan: boolean = false): any[] {
if (parseResult.errors.length > 0) {
var errorString = parseResult.errors.join('\n');
throw new BaseException(`Unexpected parse errors:\n${errorString}`);
}
return humanizeNodes(parseResult.rootNodes, addSourceSpan);
}
export function humanizeDomSourceSpans(parseResult: HtmlParseTreeResult): any[] {
return humanizeDom(parseResult, true);
}
export function humanizeNodes(nodes: HtmlAst[], addSourceSpan: boolean = false): any[] {
var humanizer = new _Humanizer(addSourceSpan);
htmlVisitAll(humanizer, nodes);
return humanizer.result;
}
export function humanizeLineColumn(location: ParseLocation): string {
return `${location.line}:${location.col}`;
}
class _Humanizer implements HtmlAstVisitor {
result: any[] = [];
elDepth: number = 0;
constructor(private includeSourceSpan: boolean){};
visitElement(ast: HtmlElementAst, context: any): any {
var res = this._appendContext(ast, [HtmlElementAst, ast.name, this.elDepth++]);
this.result.push(res);
htmlVisitAll(this, ast.attrs);
htmlVisitAll(this, ast.children);
this.elDepth--;
}
visitAttr(ast: HtmlAttrAst, context: any): any {
var res = this._appendContext(ast, [HtmlAttrAst, ast.name, ast.value]);
this.result.push(res);
}
visitText(ast: HtmlTextAst, context: any): any {
var res = this._appendContext(ast, [HtmlTextAst, ast.value, this.elDepth]);
this.result.push(res);
}
visitComment(ast: HtmlCommentAst, context: any): any {
var res = this._appendContext(ast, [HtmlCommentAst, ast.value, this.elDepth]);
this.result.push(res);
}
visitExpansion(ast: HtmlExpansionAst, context: any): any {
var res =
this._appendContext(ast, [HtmlExpansionAst, ast.switchValue, ast.type, this.elDepth++]);
this.result.push(res);
htmlVisitAll(this, ast.cases);
this.elDepth--;
}
visitExpansionCase(ast: HtmlExpansionCaseAst, context: any): any {
var res = this._appendContext(ast, [HtmlExpansionCaseAst, ast.value, this.elDepth]);
this.result.push(res);
}
private _appendContext(ast: HtmlAst, input: any[]): any[] {
if (!this.includeSourceSpan) return input;
input.push(ast.sourceSpan.toString());
return input;
}
}

View File

@ -1,801 +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 {afterEach, beforeEach, ddescribe, describe, expect, iit, it, xit} from '../../../core/testing/testing_internal';
import {HtmlToken, HtmlTokenError, HtmlTokenType, tokenizeHtml} from '../../src/html_parser/html_lexer';
import {InterpolationConfig} from '../../src/html_parser/interpolation_config';
import {ParseLocation, ParseSourceFile, ParseSourceSpan} from '../../src/parse_util';
export function main() {
describe('HtmlLexer', () => {
describe('line/column numbers', () => {
it('should work without newlines', () => {
expect(tokenizeAndHumanizeLineColumn('<t>a</t>')).toEqual([
[HtmlTokenType.TAG_OPEN_START, '0:0'],
[HtmlTokenType.TAG_OPEN_END, '0:2'],
[HtmlTokenType.TEXT, '0:3'],
[HtmlTokenType.TAG_CLOSE, '0:4'],
[HtmlTokenType.EOF, '0:8'],
]);
});
it('should work with one newline', () => {
expect(tokenizeAndHumanizeLineColumn('<t>\na</t>')).toEqual([
[HtmlTokenType.TAG_OPEN_START, '0:0'],
[HtmlTokenType.TAG_OPEN_END, '0:2'],
[HtmlTokenType.TEXT, '0:3'],
[HtmlTokenType.TAG_CLOSE, '1:1'],
[HtmlTokenType.EOF, '1:5'],
]);
});
it('should work with multiple newlines', () => {
expect(tokenizeAndHumanizeLineColumn('<t\n>\na</t>')).toEqual([
[HtmlTokenType.TAG_OPEN_START, '0:0'],
[HtmlTokenType.TAG_OPEN_END, '1:0'],
[HtmlTokenType.TEXT, '1:1'],
[HtmlTokenType.TAG_CLOSE, '2:1'],
[HtmlTokenType.EOF, '2:5'],
]);
});
it('should work with CR and LF', () => {
expect(tokenizeAndHumanizeLineColumn('<t\n>\r\na\r</t>')).toEqual([
[HtmlTokenType.TAG_OPEN_START, '0:0'],
[HtmlTokenType.TAG_OPEN_END, '1:0'],
[HtmlTokenType.TEXT, '1:1'],
[HtmlTokenType.TAG_CLOSE, '2:1'],
[HtmlTokenType.EOF, '2:5'],
]);
});
});
describe('comments', () => {
it('should parse comments', () => {
expect(tokenizeAndHumanizeParts('<!--t\ne\rs\r\nt-->')).toEqual([
[HtmlTokenType.COMMENT_START],
[HtmlTokenType.RAW_TEXT, 't\ne\ns\nt'],
[HtmlTokenType.COMMENT_END],
[HtmlTokenType.EOF],
]);
});
it('should store the locations', () => {
expect(tokenizeAndHumanizeSourceSpans('<!--t\ne\rs\r\nt-->')).toEqual([
[HtmlTokenType.COMMENT_START, '<!--'],
[HtmlTokenType.RAW_TEXT, 't\ne\rs\r\nt'],
[HtmlTokenType.COMMENT_END, '-->'],
[HtmlTokenType.EOF, ''],
]);
});
it('should report <!- without -', () => {
expect(tokenizeAndHumanizeErrors('<!-a')).toEqual([
[HtmlTokenType.COMMENT_START, 'Unexpected character "a"', '0:3']
]);
});
it('should report missing end comment', () => {
expect(tokenizeAndHumanizeErrors('<!--')).toEqual([
[HtmlTokenType.RAW_TEXT, 'Unexpected character "EOF"', '0:4']
]);
});
it('should accept comments finishing by too many dashes (even number)', () => {
expect(tokenizeAndHumanizeSourceSpans('<!-- test ---->')).toEqual([
[HtmlTokenType.COMMENT_START, '<!--'],
[HtmlTokenType.RAW_TEXT, ' test --'],
[HtmlTokenType.COMMENT_END, '-->'],
[HtmlTokenType.EOF, ''],
]);
});
it('should accept comments finishing by too many dashes (odd number)', () => {
expect(tokenizeAndHumanizeSourceSpans('<!-- test --->')).toEqual([
[HtmlTokenType.COMMENT_START, '<!--'],
[HtmlTokenType.RAW_TEXT, ' test -'],
[HtmlTokenType.COMMENT_END, '-->'],
[HtmlTokenType.EOF, ''],
]);
});
});
describe('doctype', () => {
it('should parse doctypes', () => {
expect(tokenizeAndHumanizeParts('<!doctype html>')).toEqual([
[HtmlTokenType.DOC_TYPE, 'doctype html'],
[HtmlTokenType.EOF],
]);
});
it('should store the locations', () => {
expect(tokenizeAndHumanizeSourceSpans('<!doctype html>')).toEqual([
[HtmlTokenType.DOC_TYPE, '<!doctype html>'],
[HtmlTokenType.EOF, ''],
]);
});
it('should report missing end doctype', () => {
expect(tokenizeAndHumanizeErrors('<!')).toEqual([
[HtmlTokenType.DOC_TYPE, 'Unexpected character "EOF"', '0:2']
]);
});
});
describe('CDATA', () => {
it('should parse CDATA', () => {
expect(tokenizeAndHumanizeParts('<![CDATA[t\ne\rs\r\nt]]>')).toEqual([
[HtmlTokenType.CDATA_START],
[HtmlTokenType.RAW_TEXT, 't\ne\ns\nt'],
[HtmlTokenType.CDATA_END],
[HtmlTokenType.EOF],
]);
});
it('should store the locations', () => {
expect(tokenizeAndHumanizeSourceSpans('<![CDATA[t\ne\rs\r\nt]]>')).toEqual([
[HtmlTokenType.CDATA_START, '<![CDATA['],
[HtmlTokenType.RAW_TEXT, 't\ne\rs\r\nt'],
[HtmlTokenType.CDATA_END, ']]>'],
[HtmlTokenType.EOF, ''],
]);
});
it('should report <![ without CDATA[', () => {
expect(tokenizeAndHumanizeErrors('<![a')).toEqual([
[HtmlTokenType.CDATA_START, 'Unexpected character "a"', '0:3']
]);
});
it('should report missing end cdata', () => {
expect(tokenizeAndHumanizeErrors('<![CDATA[')).toEqual([
[HtmlTokenType.RAW_TEXT, 'Unexpected character "EOF"', '0:9']
]);
});
});
describe('open tags', () => {
it('should parse open tags without prefix', () => {
expect(tokenizeAndHumanizeParts('<test>')).toEqual([
[HtmlTokenType.TAG_OPEN_START, null, 'test'],
[HtmlTokenType.TAG_OPEN_END],
[HtmlTokenType.EOF],
]);
});
it('should parse namespace prefix', () => {
expect(tokenizeAndHumanizeParts('<ns1:test>')).toEqual([
[HtmlTokenType.TAG_OPEN_START, 'ns1', 'test'],
[HtmlTokenType.TAG_OPEN_END],
[HtmlTokenType.EOF],
]);
});
it('should parse void tags', () => {
expect(tokenizeAndHumanizeParts('<test/>')).toEqual([
[HtmlTokenType.TAG_OPEN_START, null, 'test'],
[HtmlTokenType.TAG_OPEN_END_VOID],
[HtmlTokenType.EOF],
]);
});
it('should allow whitespace after the tag name', () => {
expect(tokenizeAndHumanizeParts('<test >')).toEqual([
[HtmlTokenType.TAG_OPEN_START, null, 'test'],
[HtmlTokenType.TAG_OPEN_END],
[HtmlTokenType.EOF],
]);
});
it('should store the locations', () => {
expect(tokenizeAndHumanizeSourceSpans('<test>')).toEqual([
[HtmlTokenType.TAG_OPEN_START, '<test'],
[HtmlTokenType.TAG_OPEN_END, '>'],
[HtmlTokenType.EOF, ''],
]);
});
});
describe('attributes', () => {
it('should parse attributes without prefix', () => {
expect(tokenizeAndHumanizeParts('<t a>')).toEqual([
[HtmlTokenType.TAG_OPEN_START, null, 't'],
[HtmlTokenType.ATTR_NAME, null, 'a'],
[HtmlTokenType.TAG_OPEN_END],
[HtmlTokenType.EOF],
]);
});
it('should parse attributes with interpolation', () => {
expect(tokenizeAndHumanizeParts('<t a="{{v}}" b="s{{m}}e" c="s{{m//c}}e">')).toEqual([
[HtmlTokenType.TAG_OPEN_START, null, 't'],
[HtmlTokenType.ATTR_NAME, null, 'a'],
[HtmlTokenType.ATTR_VALUE, '{{v}}'],
[HtmlTokenType.ATTR_NAME, null, 'b'],
[HtmlTokenType.ATTR_VALUE, 's{{m}}e'],
[HtmlTokenType.ATTR_NAME, null, 'c'],
[HtmlTokenType.ATTR_VALUE, 's{{m//c}}e'],
[HtmlTokenType.TAG_OPEN_END],
[HtmlTokenType.EOF],
]);
});
it('should parse attributes with prefix', () => {
expect(tokenizeAndHumanizeParts('<t ns1:a>')).toEqual([
[HtmlTokenType.TAG_OPEN_START, null, 't'],
[HtmlTokenType.ATTR_NAME, 'ns1', 'a'],
[HtmlTokenType.TAG_OPEN_END],
[HtmlTokenType.EOF],
]);
});
it('should parse attributes whose prefix is not valid', () => {
expect(tokenizeAndHumanizeParts('<t (ns1:a)>')).toEqual([
[HtmlTokenType.TAG_OPEN_START, null, 't'],
[HtmlTokenType.ATTR_NAME, null, '(ns1:a)'],
[HtmlTokenType.TAG_OPEN_END],
[HtmlTokenType.EOF],
]);
});
it('should parse attributes with single quote value', () => {
expect(tokenizeAndHumanizeParts('<t a=\'b\'>')).toEqual([
[HtmlTokenType.TAG_OPEN_START, null, 't'],
[HtmlTokenType.ATTR_NAME, null, 'a'],
[HtmlTokenType.ATTR_VALUE, 'b'],
[HtmlTokenType.TAG_OPEN_END],
[HtmlTokenType.EOF],
]);
});
it('should parse attributes with double quote value', () => {
expect(tokenizeAndHumanizeParts('<t a="b">')).toEqual([
[HtmlTokenType.TAG_OPEN_START, null, 't'],
[HtmlTokenType.ATTR_NAME, null, 'a'],
[HtmlTokenType.ATTR_VALUE, 'b'],
[HtmlTokenType.TAG_OPEN_END],
[HtmlTokenType.EOF],
]);
});
it('should parse attributes with unquoted value', () => {
expect(tokenizeAndHumanizeParts('<t a=b>')).toEqual([
[HtmlTokenType.TAG_OPEN_START, null, 't'],
[HtmlTokenType.ATTR_NAME, null, 'a'],
[HtmlTokenType.ATTR_VALUE, 'b'],
[HtmlTokenType.TAG_OPEN_END],
[HtmlTokenType.EOF],
]);
});
it('should allow whitespace', () => {
expect(tokenizeAndHumanizeParts('<t a = b >')).toEqual([
[HtmlTokenType.TAG_OPEN_START, null, 't'],
[HtmlTokenType.ATTR_NAME, null, 'a'],
[HtmlTokenType.ATTR_VALUE, 'b'],
[HtmlTokenType.TAG_OPEN_END],
[HtmlTokenType.EOF],
]);
});
it('should parse attributes with entities in values', () => {
expect(tokenizeAndHumanizeParts('<t a="&#65;&#x41;">')).toEqual([
[HtmlTokenType.TAG_OPEN_START, null, 't'],
[HtmlTokenType.ATTR_NAME, null, 'a'],
[HtmlTokenType.ATTR_VALUE, 'AA'],
[HtmlTokenType.TAG_OPEN_END],
[HtmlTokenType.EOF],
]);
});
it('should not decode entities without trailing ";"', () => {
expect(tokenizeAndHumanizeParts('<t a="&amp" b="c&&d">')).toEqual([
[HtmlTokenType.TAG_OPEN_START, null, 't'],
[HtmlTokenType.ATTR_NAME, null, 'a'],
[HtmlTokenType.ATTR_VALUE, '&amp'],
[HtmlTokenType.ATTR_NAME, null, 'b'],
[HtmlTokenType.ATTR_VALUE, 'c&&d'],
[HtmlTokenType.TAG_OPEN_END],
[HtmlTokenType.EOF],
]);
});
it('should parse attributes with "&" in values', () => {
expect(tokenizeAndHumanizeParts('<t a="b && c &">')).toEqual([
[HtmlTokenType.TAG_OPEN_START, null, 't'],
[HtmlTokenType.ATTR_NAME, null, 'a'],
[HtmlTokenType.ATTR_VALUE, 'b && c &'],
[HtmlTokenType.TAG_OPEN_END],
[HtmlTokenType.EOF],
]);
});
it('should parse values with CR and LF', () => {
expect(tokenizeAndHumanizeParts('<t a=\'t\ne\rs\r\nt\'>')).toEqual([
[HtmlTokenType.TAG_OPEN_START, null, 't'],
[HtmlTokenType.ATTR_NAME, null, 'a'],
[HtmlTokenType.ATTR_VALUE, 't\ne\ns\nt'],
[HtmlTokenType.TAG_OPEN_END],
[HtmlTokenType.EOF],
]);
});
it('should store the locations', () => {
expect(tokenizeAndHumanizeSourceSpans('<t a=b>')).toEqual([
[HtmlTokenType.TAG_OPEN_START, '<t'],
[HtmlTokenType.ATTR_NAME, 'a'],
[HtmlTokenType.ATTR_VALUE, 'b'],
[HtmlTokenType.TAG_OPEN_END, '>'],
[HtmlTokenType.EOF, ''],
]);
});
});
describe('closing tags', () => {
it('should parse closing tags without prefix', () => {
expect(tokenizeAndHumanizeParts('</test>')).toEqual([
[HtmlTokenType.TAG_CLOSE, null, 'test'],
[HtmlTokenType.EOF],
]);
});
it('should parse closing tags with prefix', () => {
expect(tokenizeAndHumanizeParts('</ns1:test>')).toEqual([
[HtmlTokenType.TAG_CLOSE, 'ns1', 'test'],
[HtmlTokenType.EOF],
]);
});
it('should allow whitespace', () => {
expect(tokenizeAndHumanizeParts('</ test >')).toEqual([
[HtmlTokenType.TAG_CLOSE, null, 'test'],
[HtmlTokenType.EOF],
]);
});
it('should store the locations', () => {
expect(tokenizeAndHumanizeSourceSpans('</test>')).toEqual([
[HtmlTokenType.TAG_CLOSE, '</test>'],
[HtmlTokenType.EOF, ''],
]);
});
it('should report missing name after </', () => {
expect(tokenizeAndHumanizeErrors('</')).toEqual([
[HtmlTokenType.TAG_CLOSE, 'Unexpected character "EOF"', '0:2']
]);
});
it('should report missing >', () => {
expect(tokenizeAndHumanizeErrors('</test')).toEqual([
[HtmlTokenType.TAG_CLOSE, 'Unexpected character "EOF"', '0:6']
]);
});
});
describe('entities', () => {
it('should parse named entities', () => {
expect(tokenizeAndHumanizeParts('a&amp;b')).toEqual([
[HtmlTokenType.TEXT, 'a&b'],
[HtmlTokenType.EOF],
]);
});
it('should parse hexadecimal entities', () => {
expect(tokenizeAndHumanizeParts('&#x41;&#X41;')).toEqual([
[HtmlTokenType.TEXT, 'AA'],
[HtmlTokenType.EOF],
]);
});
it('should parse decimal entities', () => {
expect(tokenizeAndHumanizeParts('&#65;')).toEqual([
[HtmlTokenType.TEXT, 'A'],
[HtmlTokenType.EOF],
]);
});
it('should store the locations', () => {
expect(tokenizeAndHumanizeSourceSpans('a&amp;b')).toEqual([
[HtmlTokenType.TEXT, 'a&amp;b'],
[HtmlTokenType.EOF, ''],
]);
});
it('should report malformed/unknown entities', () => {
expect(tokenizeAndHumanizeErrors('&tbo;')).toEqual([[
HtmlTokenType.TEXT,
'Unknown entity "tbo" - use the "&#<decimal>;" or "&#x<hex>;" syntax', '0:0'
]]);
expect(tokenizeAndHumanizeErrors('&#asdf;')).toEqual([
[HtmlTokenType.TEXT, 'Unexpected character "s"', '0:3']
]);
expect(tokenizeAndHumanizeErrors('&#xasdf;')).toEqual([
[HtmlTokenType.TEXT, 'Unexpected character "s"', '0:4']
]);
expect(tokenizeAndHumanizeErrors('&#xABC')).toEqual([
[HtmlTokenType.TEXT, 'Unexpected character "EOF"', '0:6']
]);
});
});
describe('regular text', () => {
it('should parse text', () => {
expect(tokenizeAndHumanizeParts('a')).toEqual([
[HtmlTokenType.TEXT, 'a'],
[HtmlTokenType.EOF],
]);
});
it('should parse interpolation', () => {
expect(tokenizeAndHumanizeParts('{{ a }}b{{ c // comment }}')).toEqual([
[HtmlTokenType.TEXT, '{{ a }}b{{ c // comment }}'],
[HtmlTokenType.EOF],
]);
});
it('should parse interpolation with custom markers', () => {
expect(tokenizeAndHumanizeParts('{% a %}', null, {start: '{%', end: '%}'})).toEqual([
[HtmlTokenType.TEXT, '{% a %}'],
[HtmlTokenType.EOF],
]);
});
it('should handle CR & LF', () => {
expect(tokenizeAndHumanizeParts('t\ne\rs\r\nt')).toEqual([
[HtmlTokenType.TEXT, 't\ne\ns\nt'],
[HtmlTokenType.EOF],
]);
});
it('should parse entities', () => {
expect(tokenizeAndHumanizeParts('a&amp;b')).toEqual([
[HtmlTokenType.TEXT, 'a&b'],
[HtmlTokenType.EOF],
]);
});
it('should parse text starting with "&"', () => {
expect(tokenizeAndHumanizeParts('a && b &')).toEqual([
[HtmlTokenType.TEXT, 'a && b &'],
[HtmlTokenType.EOF],
]);
});
it('should store the locations', () => {
expect(tokenizeAndHumanizeSourceSpans('a')).toEqual([
[HtmlTokenType.TEXT, 'a'],
[HtmlTokenType.EOF, ''],
]);
});
it('should allow "<" in text nodes', () => {
expect(tokenizeAndHumanizeParts('{{ a < b ? c : d }}')).toEqual([
[HtmlTokenType.TEXT, '{{ a < b ? c : d }}'],
[HtmlTokenType.EOF],
]);
expect(tokenizeAndHumanizeSourceSpans('<p>a<b</p>')).toEqual([
[HtmlTokenType.TAG_OPEN_START, '<p'],
[HtmlTokenType.TAG_OPEN_END, '>'],
[HtmlTokenType.TEXT, 'a<b'],
[HtmlTokenType.TAG_CLOSE, '</p>'],
[HtmlTokenType.EOF, ''],
]);
expect(tokenizeAndHumanizeParts('< a>')).toEqual([
[HtmlTokenType.TEXT, '< a>'],
[HtmlTokenType.EOF],
]);
});
it('should parse valid start tag in interpolation', () => {
expect(tokenizeAndHumanizeParts('{{ a <b && c > d }}')).toEqual([
[HtmlTokenType.TEXT, '{{ a '],
[HtmlTokenType.TAG_OPEN_START, null, 'b'],
[HtmlTokenType.ATTR_NAME, null, '&&'],
[HtmlTokenType.ATTR_NAME, null, 'c'],
[HtmlTokenType.TAG_OPEN_END],
[HtmlTokenType.TEXT, ' d }}'],
[HtmlTokenType.EOF],
]);
});
it('should be able to escape {', () => {
expect(tokenizeAndHumanizeParts('{{ "{" }}')).toEqual([
[HtmlTokenType.TEXT, '{{ "{" }}'],
[HtmlTokenType.EOF],
]);
});
it('should be able to escape {{', () => {
expect(tokenizeAndHumanizeParts('{{ "{{" }}')).toEqual([
[HtmlTokenType.TEXT, '{{ "{{" }}'],
[HtmlTokenType.EOF],
]);
});
});
describe('raw text', () => {
it('should parse text', () => {
expect(tokenizeAndHumanizeParts(`<script>t\ne\rs\r\nt</script>`)).toEqual([
[HtmlTokenType.TAG_OPEN_START, null, 'script'],
[HtmlTokenType.TAG_OPEN_END],
[HtmlTokenType.RAW_TEXT, 't\ne\ns\nt'],
[HtmlTokenType.TAG_CLOSE, null, 'script'],
[HtmlTokenType.EOF],
]);
});
it('should not detect entities', () => {
expect(tokenizeAndHumanizeParts(`<script>&amp;</SCRIPT>`)).toEqual([
[HtmlTokenType.TAG_OPEN_START, null, 'script'],
[HtmlTokenType.TAG_OPEN_END],
[HtmlTokenType.RAW_TEXT, '&amp;'],
[HtmlTokenType.TAG_CLOSE, null, 'script'],
[HtmlTokenType.EOF],
]);
});
it('should ignore other opening tags', () => {
expect(tokenizeAndHumanizeParts(`<script>a<div></script>`)).toEqual([
[HtmlTokenType.TAG_OPEN_START, null, 'script'],
[HtmlTokenType.TAG_OPEN_END],
[HtmlTokenType.RAW_TEXT, 'a<div>'],
[HtmlTokenType.TAG_CLOSE, null, 'script'],
[HtmlTokenType.EOF],
]);
});
it('should ignore other closing tags', () => {
expect(tokenizeAndHumanizeParts(`<script>a</test></script>`)).toEqual([
[HtmlTokenType.TAG_OPEN_START, null, 'script'],
[HtmlTokenType.TAG_OPEN_END],
[HtmlTokenType.RAW_TEXT, 'a</test>'],
[HtmlTokenType.TAG_CLOSE, null, 'script'],
[HtmlTokenType.EOF],
]);
});
it('should store the locations', () => {
expect(tokenizeAndHumanizeSourceSpans(`<script>a</script>`)).toEqual([
[HtmlTokenType.TAG_OPEN_START, '<script'],
[HtmlTokenType.TAG_OPEN_END, '>'],
[HtmlTokenType.RAW_TEXT, 'a'],
[HtmlTokenType.TAG_CLOSE, '</script>'],
[HtmlTokenType.EOF, ''],
]);
});
});
describe('escapable raw text', () => {
it('should parse text', () => {
expect(tokenizeAndHumanizeParts(`<title>t\ne\rs\r\nt</title>`)).toEqual([
[HtmlTokenType.TAG_OPEN_START, null, 'title'],
[HtmlTokenType.TAG_OPEN_END],
[HtmlTokenType.ESCAPABLE_RAW_TEXT, 't\ne\ns\nt'],
[HtmlTokenType.TAG_CLOSE, null, 'title'],
[HtmlTokenType.EOF],
]);
});
it('should detect entities', () => {
expect(tokenizeAndHumanizeParts(`<title>&amp;</title>`)).toEqual([
[HtmlTokenType.TAG_OPEN_START, null, 'title'],
[HtmlTokenType.TAG_OPEN_END],
[HtmlTokenType.ESCAPABLE_RAW_TEXT, '&'],
[HtmlTokenType.TAG_CLOSE, null, 'title'],
[HtmlTokenType.EOF],
]);
});
it('should ignore other opening tags', () => {
expect(tokenizeAndHumanizeParts(`<title>a<div></title>`)).toEqual([
[HtmlTokenType.TAG_OPEN_START, null, 'title'],
[HtmlTokenType.TAG_OPEN_END],
[HtmlTokenType.ESCAPABLE_RAW_TEXT, 'a<div>'],
[HtmlTokenType.TAG_CLOSE, null, 'title'],
[HtmlTokenType.EOF],
]);
});
it('should ignore other closing tags', () => {
expect(tokenizeAndHumanizeParts(`<title>a</test></title>`)).toEqual([
[HtmlTokenType.TAG_OPEN_START, null, 'title'],
[HtmlTokenType.TAG_OPEN_END],
[HtmlTokenType.ESCAPABLE_RAW_TEXT, 'a</test>'],
[HtmlTokenType.TAG_CLOSE, null, 'title'],
[HtmlTokenType.EOF],
]);
});
it('should store the locations', () => {
expect(tokenizeAndHumanizeSourceSpans(`<title>a</title>`)).toEqual([
[HtmlTokenType.TAG_OPEN_START, '<title'],
[HtmlTokenType.TAG_OPEN_END, '>'],
[HtmlTokenType.ESCAPABLE_RAW_TEXT, 'a'],
[HtmlTokenType.TAG_CLOSE, '</title>'],
[HtmlTokenType.EOF, ''],
]);
});
});
describe('expansion forms', () => {
it('should parse an expansion form', () => {
expect(tokenizeAndHumanizeParts('{one.two, three, =4 {four} =5 {five} foo {bar} }', true))
.toEqual([
[HtmlTokenType.EXPANSION_FORM_START],
[HtmlTokenType.RAW_TEXT, 'one.two'],
[HtmlTokenType.RAW_TEXT, 'three'],
[HtmlTokenType.EXPANSION_CASE_VALUE, '=4'],
[HtmlTokenType.EXPANSION_CASE_EXP_START],
[HtmlTokenType.TEXT, 'four'],
[HtmlTokenType.EXPANSION_CASE_EXP_END],
[HtmlTokenType.EXPANSION_CASE_VALUE, '=5'],
[HtmlTokenType.EXPANSION_CASE_EXP_START],
[HtmlTokenType.TEXT, 'five'],
[HtmlTokenType.EXPANSION_CASE_EXP_END],
[HtmlTokenType.EXPANSION_CASE_VALUE, 'foo'],
[HtmlTokenType.EXPANSION_CASE_EXP_START],
[HtmlTokenType.TEXT, 'bar'],
[HtmlTokenType.EXPANSION_CASE_EXP_END],
[HtmlTokenType.EXPANSION_FORM_END],
[HtmlTokenType.EOF],
]);
});
it('should parse an expansion form with text elements surrounding it', () => {
expect(tokenizeAndHumanizeParts('before{one.two, three, =4 {four}}after', true)).toEqual([
[HtmlTokenType.TEXT, 'before'],
[HtmlTokenType.EXPANSION_FORM_START],
[HtmlTokenType.RAW_TEXT, 'one.two'],
[HtmlTokenType.RAW_TEXT, 'three'],
[HtmlTokenType.EXPANSION_CASE_VALUE, '=4'],
[HtmlTokenType.EXPANSION_CASE_EXP_START],
[HtmlTokenType.TEXT, 'four'],
[HtmlTokenType.EXPANSION_CASE_EXP_END],
[HtmlTokenType.EXPANSION_FORM_END],
[HtmlTokenType.TEXT, 'after'],
[HtmlTokenType.EOF],
]);
});
it('should parse an expansion forms with elements in it', () => {
expect(tokenizeAndHumanizeParts('{one.two, three, =4 {four <b>a</b>}}', true)).toEqual([
[HtmlTokenType.EXPANSION_FORM_START],
[HtmlTokenType.RAW_TEXT, 'one.two'],
[HtmlTokenType.RAW_TEXT, 'three'],
[HtmlTokenType.EXPANSION_CASE_VALUE, '=4'],
[HtmlTokenType.EXPANSION_CASE_EXP_START],
[HtmlTokenType.TEXT, 'four '],
[HtmlTokenType.TAG_OPEN_START, null, 'b'],
[HtmlTokenType.TAG_OPEN_END],
[HtmlTokenType.TEXT, 'a'],
[HtmlTokenType.TAG_CLOSE, null, 'b'],
[HtmlTokenType.EXPANSION_CASE_EXP_END],
[HtmlTokenType.EXPANSION_FORM_END],
[HtmlTokenType.EOF],
]);
});
it('should parse an expansion forms containing an interpolation', () => {
expect(tokenizeAndHumanizeParts('{one.two, three, =4 {four {{a}}}}', true)).toEqual([
[HtmlTokenType.EXPANSION_FORM_START],
[HtmlTokenType.RAW_TEXT, 'one.two'],
[HtmlTokenType.RAW_TEXT, 'three'],
[HtmlTokenType.EXPANSION_CASE_VALUE, '=4'],
[HtmlTokenType.EXPANSION_CASE_EXP_START],
[HtmlTokenType.TEXT, 'four {{a}}'],
[HtmlTokenType.EXPANSION_CASE_EXP_END],
[HtmlTokenType.EXPANSION_FORM_END],
[HtmlTokenType.EOF],
]);
});
it('should parse nested expansion forms', () => {
expect(tokenizeAndHumanizeParts(`{one.two, three, =4 { {xx, yy, =x {one}} }}`, true))
.toEqual([
[HtmlTokenType.EXPANSION_FORM_START],
[HtmlTokenType.RAW_TEXT, 'one.two'],
[HtmlTokenType.RAW_TEXT, 'three'],
[HtmlTokenType.EXPANSION_CASE_VALUE, '=4'],
[HtmlTokenType.EXPANSION_CASE_EXP_START],
[HtmlTokenType.EXPANSION_FORM_START],
[HtmlTokenType.RAW_TEXT, 'xx'],
[HtmlTokenType.RAW_TEXT, 'yy'],
[HtmlTokenType.EXPANSION_CASE_VALUE, '=x'],
[HtmlTokenType.EXPANSION_CASE_EXP_START],
[HtmlTokenType.TEXT, 'one'],
[HtmlTokenType.EXPANSION_CASE_EXP_END],
[HtmlTokenType.EXPANSION_FORM_END],
[HtmlTokenType.TEXT, ' '],
[HtmlTokenType.EXPANSION_CASE_EXP_END],
[HtmlTokenType.EXPANSION_FORM_END],
[HtmlTokenType.EOF],
]);
});
});
describe('errors', () => {
it('should parse nested expansion forms', () => {
expect(tokenizeAndHumanizeErrors(`<p>before { after</p>`, true)).toEqual([[
HtmlTokenType.RAW_TEXT,
'Unexpected character "EOF" (Do you have an unescaped "{" in your template?).',
'0:21',
]]);
});
it('should include 2 lines of context in message', () => {
let src = '111\n222\n333\nE\n444\n555\n666\n';
let file = new ParseSourceFile(src, 'file://');
let location = new ParseLocation(file, 12, 123, 456);
let span = new ParseSourceSpan(location, location);
let error = new HtmlTokenError('**ERROR**', null, span);
expect(error.toString())
.toEqual(`**ERROR** ("\n222\n333\n[ERROR ->]E\n444\n555\n"): file://@123:456`);
});
});
describe('unicode characters', () => {
it('should support unicode characters', () => {
expect(tokenizeAndHumanizeSourceSpans(`<p>İ</p>`)).toEqual([
[HtmlTokenType.TAG_OPEN_START, '<p'],
[HtmlTokenType.TAG_OPEN_END, '>'],
[HtmlTokenType.TEXT, 'İ'],
[HtmlTokenType.TAG_CLOSE, '</p>'],
[HtmlTokenType.EOF, ''],
]);
});
});
});
}
function tokenizeWithoutErrors(
input: string, tokenizeExpansionForms: boolean = false,
interpolationConfig?: InterpolationConfig): HtmlToken[] {
var tokenizeResult = tokenizeHtml(input, 'someUrl', tokenizeExpansionForms, interpolationConfig);
if (tokenizeResult.errors.length > 0) {
const errorString = tokenizeResult.errors.join('\n');
throw new Error(`Unexpected parse errors:\n${errorString}`);
}
return tokenizeResult.tokens;
}
function tokenizeAndHumanizeParts(
input: string, tokenizeExpansionForms: boolean = false,
interpolationConfig?: InterpolationConfig): any[] {
return tokenizeWithoutErrors(input, tokenizeExpansionForms, interpolationConfig)
.map(token => [<any>token.type].concat(token.parts));
}
function tokenizeAndHumanizeSourceSpans(input: string): any[] {
return tokenizeWithoutErrors(input).map(token => [<any>token.type, token.sourceSpan.toString()]);
}
function humanizeLineColumn(location: ParseLocation): string {
return `${location.line}:${location.col}`;
}
function tokenizeAndHumanizeLineColumn(input: string): any[] {
return tokenizeWithoutErrors(input).map(
token => [<any>token.type, humanizeLineColumn(token.sourceSpan.start)]);
}
function tokenizeAndHumanizeErrors(input: string, tokenizeExpansionForms: boolean = false): any[] {
return tokenizeHtml(input, 'someUrl', tokenizeExpansionForms)
.errors.map(e => [<any>e.tokenType, e.msg, humanizeLineColumn(e.span.start)]);
}

View File

@ -7,12 +7,12 @@
*/
import {afterEach, beforeEach, ddescribe, describe, expect, iit, it, xit} from '../../../core/testing/testing_internal';
import {HtmlAttrAst, HtmlCommentAst, HtmlElementAst, HtmlExpansionAst, HtmlExpansionCaseAst, HtmlTextAst} from '../../src/html_parser/html_ast';
import {HtmlTokenType} from '../../src/html_parser/html_lexer';
import {HtmlParseTreeResult, HtmlParser, HtmlTreeError} from '../../src/html_parser/html_parser';
import * as html from '../../src/html_parser/ast';
import {HtmlParser, ParseTreeResult, TreeError} from '../../src/html_parser/html_parser';
import {TokenType} from '../../src/html_parser/lexer';
import {ParseError} from '../../src/parse_util';
import {humanizeDom, humanizeDomSourceSpans, humanizeLineColumn} from './html_ast_spec_utils';
import {humanizeDom, humanizeDomSourceSpans, humanizeLineColumn} from './ast_spec_utils';
export function main() {
describe('HtmlParser', () => {
@ -23,24 +23,24 @@ export function main() {
describe('parse', () => {
describe('text nodes', () => {
it('should parse root level text nodes', () => {
expect(humanizeDom(parser.parse('a', 'TestComp'))).toEqual([[HtmlTextAst, 'a', 0]]);
expect(humanizeDom(parser.parse('a', 'TestComp'))).toEqual([[html.Text, 'a', 0]]);
});
it('should parse text nodes inside regular elements', () => {
expect(humanizeDom(parser.parse('<div>a</div>', 'TestComp'))).toEqual([
[HtmlElementAst, 'div', 0], [HtmlTextAst, 'a', 1]
[html.Element, 'div', 0], [html.Text, 'a', 1]
]);
});
it('should parse text nodes inside template elements', () => {
expect(humanizeDom(parser.parse('<template>a</template>', 'TestComp'))).toEqual([
[HtmlElementAst, 'template', 0], [HtmlTextAst, 'a', 1]
[html.Element, 'template', 0], [html.Text, 'a', 1]
]);
});
it('should parse CDATA', () => {
expect(humanizeDom(parser.parse('<![CDATA[text]]>', 'TestComp'))).toEqual([
[HtmlTextAst, 'text', 0]
[html.Text, 'text', 0]
]);
});
});
@ -48,27 +48,27 @@ export function main() {
describe('elements', () => {
it('should parse root level elements', () => {
expect(humanizeDom(parser.parse('<div></div>', 'TestComp'))).toEqual([
[HtmlElementAst, 'div', 0]
[html.Element, 'div', 0]
]);
});
it('should parse elements inside of regular elements', () => {
expect(humanizeDom(parser.parse('<div><span></span></div>', 'TestComp'))).toEqual([
[HtmlElementAst, 'div', 0], [HtmlElementAst, 'span', 1]
[html.Element, 'div', 0], [html.Element, 'span', 1]
]);
});
it('should parse elements inside of template elements', () => {
expect(humanizeDom(parser.parse('<template><span></span></template>', 'TestComp')))
.toEqual([[HtmlElementAst, 'template', 0], [HtmlElementAst, 'span', 1]]);
.toEqual([[html.Element, 'template', 0], [html.Element, 'span', 1]]);
});
it('should support void elements', () => {
expect(humanizeDom(parser.parse('<link rel="author license" href="/about">', 'TestComp')))
.toEqual([
[HtmlElementAst, 'link', 0],
[HtmlAttrAst, 'rel', 'author license'],
[HtmlAttrAst, 'href', '/about'],
[html.Element, 'link', 0],
[html.Attribute, 'rel', 'author license'],
[html.Attribute, 'href', '/about'],
]);
});
@ -87,30 +87,30 @@ export function main() {
it('should close void elements on text nodes', () => {
expect(humanizeDom(parser.parse('<p>before<br>after</p>', 'TestComp'))).toEqual([
[HtmlElementAst, 'p', 0],
[HtmlTextAst, 'before', 1],
[HtmlElementAst, 'br', 1],
[HtmlTextAst, 'after', 1],
[html.Element, 'p', 0],
[html.Text, 'before', 1],
[html.Element, 'br', 1],
[html.Text, 'after', 1],
]);
});
it('should support optional end tags', () => {
expect(humanizeDom(parser.parse('<div><p>1<p>2</div>', 'TestComp'))).toEqual([
[HtmlElementAst, 'div', 0],
[HtmlElementAst, 'p', 1],
[HtmlTextAst, '1', 2],
[HtmlElementAst, 'p', 1],
[HtmlTextAst, '2', 2],
[html.Element, 'div', 0],
[html.Element, 'p', 1],
[html.Text, '1', 2],
[html.Element, 'p', 1],
[html.Text, '2', 2],
]);
});
it('should support nested elements', () => {
expect(humanizeDom(parser.parse('<ul><li><ul><li></li></ul></li></ul>', 'TestComp')))
.toEqual([
[HtmlElementAst, 'ul', 0],
[HtmlElementAst, 'li', 1],
[HtmlElementAst, 'ul', 2],
[HtmlElementAst, 'li', 3],
[html.Element, 'ul', 0],
[html.Element, 'li', 1],
[html.Element, 'ul', 2],
[html.Element, 'li', 3],
]);
});
@ -120,19 +120,19 @@ export function main() {
'<table><thead><tr head></tr></thead><tr noparent></tr><tbody><tr body></tr></tbody><tfoot><tr foot></tr></tfoot></table>',
'TestComp')))
.toEqual([
[HtmlElementAst, 'table', 0],
[HtmlElementAst, 'thead', 1],
[HtmlElementAst, 'tr', 2],
[HtmlAttrAst, 'head', ''],
[HtmlElementAst, 'tbody', 1],
[HtmlElementAst, 'tr', 2],
[HtmlAttrAst, 'noparent', ''],
[HtmlElementAst, 'tbody', 1],
[HtmlElementAst, 'tr', 2],
[HtmlAttrAst, 'body', ''],
[HtmlElementAst, 'tfoot', 1],
[HtmlElementAst, 'tr', 2],
[HtmlAttrAst, 'foot', ''],
[html.Element, 'table', 0],
[html.Element, 'thead', 1],
[html.Element, 'tr', 2],
[html.Attribute, 'head', ''],
[html.Element, 'tbody', 1],
[html.Element, 'tr', 2],
[html.Attribute, 'noparent', ''],
[html.Element, 'tbody', 1],
[html.Element, 'tr', 2],
[html.Attribute, 'body', ''],
[html.Element, 'tfoot', 1],
[html.Element, 'tr', 2],
[html.Attribute, 'foot', ''],
]);
});
@ -140,10 +140,10 @@ export function main() {
expect(humanizeDom(parser.parse(
'<table><ng-container><tr></tr></ng-container></table>', 'TestComp')))
.toEqual([
[HtmlElementAst, 'table', 0],
[HtmlElementAst, 'tbody', 1],
[HtmlElementAst, 'ng-container', 2],
[HtmlElementAst, 'tr', 3],
[html.Element, 'table', 0],
[html.Element, 'tbody', 1],
[html.Element, 'ng-container', 2],
[html.Element, 'tr', 3],
]);
});
@ -152,42 +152,43 @@ export function main() {
'<table><thead><ng-container><tr></tr></ng-container></thead></table>',
'TestComp')))
.toEqual([
[HtmlElementAst, 'table', 0],
[HtmlElementAst, 'thead', 1],
[HtmlElementAst, 'ng-container', 2],
[HtmlElementAst, 'tr', 3],
[html.Element, 'table', 0],
[html.Element, 'thead', 1],
[html.Element, 'ng-container', 2],
[html.Element, 'tr', 3],
]);
});
it('should not add the requiredParent when the parent is a template', () => {
expect(humanizeDom(parser.parse('<template><tr></tr></template>', 'TestComp'))).toEqual([
[HtmlElementAst, 'template', 0],
[HtmlElementAst, 'tr', 1],
[html.Element, 'template', 0],
[html.Element, 'tr', 1],
]);
});
// https://github.com/angular/angular/issues/5967
it('should not add the requiredParent to a template root element', () => {
expect(humanizeDom(parser.parse('<tr></tr>', 'TestComp'))).toEqual([
[HtmlElementAst, 'tr', 0],
[html.Element, 'tr', 0],
]);
});
it('should support explicit mamespace', () => {
expect(humanizeDom(parser.parse('<myns:div></myns:div>', 'TestComp'))).toEqual([
[HtmlElementAst, ':myns:div', 0]
[html.Element, ':myns:div', 0]
]);
});
it('should support implicit mamespace', () => {
expect(humanizeDom(parser.parse('<svg></svg>', 'TestComp'))).toEqual([
[HtmlElementAst, ':svg:svg', 0]
[html.Element, ':svg:svg', 0]
]);
});
it('should propagate the namespace', () => {
expect(humanizeDom(parser.parse('<myns:div><p></p></myns:div>', 'TestComp'))).toEqual([
[HtmlElementAst, ':myns:div', 0], [HtmlElementAst, ':myns:p', 1]
[html.Element, ':myns:div', 0],
[html.Element, ':myns:p', 1],
]);
});
@ -202,13 +203,13 @@ export function main() {
it('should support self closing void elements', () => {
expect(humanizeDom(parser.parse('<input />', 'TestComp'))).toEqual([
[HtmlElementAst, 'input', 0]
[html.Element, 'input', 0]
]);
});
it('should support self closing foreign elements', () => {
expect(humanizeDom(parser.parse('<math />', 'TestComp'))).toEqual([
[HtmlElementAst, ':math:math', 0]
[html.Element, ':math:math', 0]
]);
});
@ -217,13 +218,13 @@ export function main() {
'<p>\n</p><textarea>\n</textarea><pre>\n\n</pre><listing>\n\n</listing>',
'TestComp')))
.toEqual([
[HtmlElementAst, 'p', 0],
[HtmlTextAst, '\n', 1],
[HtmlElementAst, 'textarea', 0],
[HtmlElementAst, 'pre', 0],
[HtmlTextAst, '\n', 1],
[HtmlElementAst, 'listing', 0],
[HtmlTextAst, '\n', 1],
[html.Element, 'p', 0],
[html.Text, '\n', 1],
[html.Element, 'textarea', 0],
[html.Element, 'pre', 0],
[html.Text, '\n', 1],
[html.Element, 'listing', 0],
[html.Text, '\n', 1],
]);
});
@ -232,33 +233,37 @@ export function main() {
describe('attributes', () => {
it('should parse attributes on regular elements case sensitive', () => {
expect(humanizeDom(parser.parse('<div kEy="v" key2=v2></div>', 'TestComp'))).toEqual([
[HtmlElementAst, 'div', 0],
[HtmlAttrAst, 'kEy', 'v'],
[HtmlAttrAst, 'key2', 'v2'],
[html.Element, 'div', 0],
[html.Attribute, 'kEy', 'v'],
[html.Attribute, 'key2', 'v2'],
]);
});
it('should parse attributes without values', () => {
expect(humanizeDom(parser.parse('<div k></div>', 'TestComp'))).toEqual([
[HtmlElementAst, 'div', 0], [HtmlAttrAst, 'k', '']
[html.Element, 'div', 0],
[html.Attribute, 'k', ''],
]);
});
it('should parse attributes on svg elements case sensitive', () => {
expect(humanizeDom(parser.parse('<svg viewBox="0"></svg>', 'TestComp'))).toEqual([
[HtmlElementAst, ':svg:svg', 0], [HtmlAttrAst, 'viewBox', '0']
[html.Element, ':svg:svg', 0],
[html.Attribute, 'viewBox', '0'],
]);
});
it('should parse attributes on template elements', () => {
expect(humanizeDom(parser.parse('<template k="v"></template>', 'TestComp'))).toEqual([
[HtmlElementAst, 'template', 0], [HtmlAttrAst, 'k', 'v']
[html.Element, 'template', 0],
[html.Attribute, 'k', 'v'],
]);
});
it('should support namespace', () => {
expect(humanizeDom(parser.parse('<svg:use xlink:href="Port" />', 'TestComp'))).toEqual([
[HtmlElementAst, ':svg:use', 0], [HtmlAttrAst, ':xlink:href', 'Port']
[html.Element, ':svg:use', 0],
[html.Attribute, ':xlink:href', 'Port'],
]);
});
});
@ -266,7 +271,8 @@ export function main() {
describe('comments', () => {
it('should preserve comments', () => {
expect(humanizeDom(parser.parse('<!-- comment --><div></div>', 'TestComp'))).toEqual([
[HtmlCommentAst, 'comment', 0], [HtmlElementAst, 'div', 0]
[html.Comment, 'comment', 0],
[html.Element, 'div', 0],
]);
});
});
@ -278,54 +284,54 @@ export function main() {
'TestComp', true);
expect(humanizeDom(parsed)).toEqual([
[HtmlElementAst, 'div', 0],
[HtmlTextAst, 'before', 1],
[HtmlExpansionAst, 'messages.length', 'plural', 1],
[HtmlExpansionCaseAst, '=0', 2],
[HtmlExpansionCaseAst, '=1', 2],
[HtmlTextAst, 'after', 1],
[html.Element, 'div', 0],
[html.Text, 'before', 1],
[html.Expansion, 'messages.length', 'plural', 1],
[html.ExpansionCase, '=0', 2],
[html.ExpansionCase, '=1', 2],
[html.Text, 'after', 1],
]);
let cases = (<any>parsed.rootNodes[0]).children[1].cases;
expect(humanizeDom(new HtmlParseTreeResult(cases[0].expression, []))).toEqual([
[HtmlTextAst, 'You have ', 0],
[HtmlElementAst, 'b', 0],
[HtmlTextAst, 'no', 1],
[HtmlTextAst, ' messages', 0],
expect(humanizeDom(new ParseTreeResult(cases[0].expression, []))).toEqual([
[html.Text, 'You have ', 0],
[html.Element, 'b', 0],
[html.Text, 'no', 1],
[html.Text, ' messages', 0],
]);
expect(humanizeDom(new HtmlParseTreeResult(cases[1].expression, [
]))).toEqual([[HtmlTextAst, 'One {{message}}', 0]]);
expect(humanizeDom(new ParseTreeResult(cases[1].expression, [
]))).toEqual([[html.Text, 'One {{message}}', 0]]);
});
it('should parse out nested expansion forms', () => {
let parsed = parser.parse(
`{messages.length, plural, =0 { {p.gender, gender, =m {m}} }}`, 'TestComp', true);
expect(humanizeDom(parsed)).toEqual([
[HtmlExpansionAst, 'messages.length', 'plural', 0],
[HtmlExpansionCaseAst, '=0', 1],
[html.Expansion, 'messages.length', 'plural', 0],
[html.ExpansionCase, '=0', 1],
]);
let firstCase = (<any>parsed.rootNodes[0]).cases[0];
expect(humanizeDom(new HtmlParseTreeResult(firstCase.expression, []))).toEqual([
[HtmlExpansionAst, 'p.gender', 'gender', 0],
[HtmlExpansionCaseAst, '=m', 1],
[HtmlTextAst, ' ', 0],
expect(humanizeDom(new ParseTreeResult(firstCase.expression, []))).toEqual([
[html.Expansion, 'p.gender', 'gender', 0],
[html.ExpansionCase, '=m', 1],
[html.Text, ' ', 0],
]);
});
it('should error when expansion form is not closed', () => {
let p = parser.parse(`{messages.length, plural, =0 {one}`, 'TestComp', true);
expect(humanizeErrors(p.errors)).toEqual([
[null, 'Invalid expansion form. Missing \'}\'.', '0:34']
[null, 'Invalid ICU message. Missing \'}\'.', '0:34']
]);
});
it('should error when expansion case is not closed', () => {
let p = parser.parse(`{messages.length, plural, =0 {one`, 'TestComp', true);
expect(humanizeErrors(p.errors)).toEqual([
[null, 'Invalid expansion form. Missing \'}\'.', '0:29']
[null, 'Invalid ICU message. Missing \'}\'.', '0:29']
]);
});
@ -342,17 +348,17 @@ export function main() {
expect(humanizeDomSourceSpans(parser.parse(
'<div [prop]="v1" (e)="do()" attr="v2" noValue>\na\n</div>', 'TestComp')))
.toEqual([
[HtmlElementAst, 'div', 0, '<div [prop]="v1" (e)="do()" attr="v2" noValue>'],
[HtmlAttrAst, '[prop]', 'v1', '[prop]="v1"'],
[HtmlAttrAst, '(e)', 'do()', '(e)="do()"'],
[HtmlAttrAst, 'attr', 'v2', 'attr="v2"'],
[HtmlAttrAst, 'noValue', '', 'noValue'],
[HtmlTextAst, '\na\n', 1, '\na\n'],
[html.Element, 'div', 0, '<div [prop]="v1" (e)="do()" attr="v2" noValue>'],
[html.Attribute, '[prop]', 'v1', '[prop]="v1"'],
[html.Attribute, '(e)', 'do()', '(e)="do()"'],
[html.Attribute, 'attr', 'v2', 'attr="v2"'],
[html.Attribute, 'noValue', '', 'noValue'],
[html.Text, '\na\n', 1, '\na\n'],
]);
});
it('should set the start and end source spans', () => {
let node = <HtmlElementAst>parser.parse('<div>a</div>', 'TestComp').rootNodes[0];
let node = <html.Element>parser.parse('<div>a</div>', 'TestComp').rootNodes[0];
expect(node.startSourceSpan.start.offset).toEqual(0);
expect(node.startSourceSpan.end.offset).toEqual(5);
@ -360,6 +366,16 @@ export function main() {
expect(node.endSourceSpan.start.offset).toEqual(6);
expect(node.endSourceSpan.end.offset).toEqual(12);
});
it('should support expansion form', () => {
expect(humanizeDomSourceSpans(
parser.parse('<div>{count, plural, =0 {msg}}</div>', 'TestComp', true)))
.toEqual([
[html.Element, 'div', 0, '<div>'],
[html.Expansion, 'count', 'plural', 1, '{count, plural, =0 {msg}}'],
[html.ExpansionCase, '=0', 2, '=0 {msg}'],
]);
});
});
describe('errors', () => {
@ -403,7 +419,7 @@ export function main() {
let errors = parser.parse('<!-err--><div></p></div>', 'TestComp').errors;
expect(errors.length).toEqual(2);
expect(humanizeErrors(errors)).toEqual([
[HtmlTokenType.COMMENT_START, 'Unexpected character "e"', '0:3'],
[TokenType.COMMENT_START, 'Unexpected character "e"', '0:3'],
['p', 'Unexpected closing tag "p"', '0:14']
]);
});
@ -414,7 +430,7 @@ export function main() {
export function humanizeErrors(errors: ParseError[]): any[] {
return errors.map(e => {
if (e instanceof HtmlTreeError) {
if (e instanceof TreeError) {
// Parser errors
return [<any>e.elementName, e.msg, humanizeLineColumn(e.span.start)];
}

View File

@ -7,12 +7,12 @@
*/
import {ddescribe, describe, expect, iit, it} from '../../../core/testing/testing_internal';
import {ExpansionResult, expandNodes} from '../../src/html_parser/expander';
import {HtmlAttrAst, HtmlElementAst, HtmlTextAst} from '../../src/html_parser/html_ast';
import * as html from '../../src/html_parser/ast';
import {HtmlParser} from '../../src/html_parser/html_parser';
import {ExpansionResult, expandNodes} from '../../src/html_parser/icu_ast_expander';
import {ParseError} from '../../src/parse_util';
import {humanizeNodes} from './html_ast_spec_utils';
import {humanizeNodes} from './ast_spec_utils';
export function main() {
describe('Expander', () => {
@ -26,13 +26,13 @@ export function main() {
const res = expand(`{messages.length, plural,=0 {zero<b>bold</b>}}`);
expect(humanizeNodes(res.nodes)).toEqual([
[HtmlElementAst, 'ng-container', 0],
[HtmlAttrAst, '[ngPlural]', 'messages.length'],
[HtmlElementAst, 'template', 1],
[HtmlAttrAst, 'ngPluralCase', '=0'],
[HtmlTextAst, 'zero', 2],
[HtmlElementAst, 'b', 2],
[HtmlTextAst, 'bold', 3],
[html.Element, 'ng-container', 0],
[html.Attribute, '[ngPlural]', 'messages.length'],
[html.Element, 'template', 1],
[html.Attribute, 'ngPluralCase', '=0'],
[html.Text, 'zero', 2],
[html.Element, 'b', 2],
[html.Text, 'bold', 3],
]);
});
@ -40,23 +40,23 @@ export function main() {
const res = expand(`{messages.length, plural, =0 { {p.gender, gender, =m {m}} }}`);
expect(humanizeNodes(res.nodes)).toEqual([
[HtmlElementAst, 'ng-container', 0],
[HtmlAttrAst, '[ngPlural]', 'messages.length'],
[HtmlElementAst, 'template', 1],
[HtmlAttrAst, 'ngPluralCase', '=0'],
[HtmlElementAst, 'ng-container', 2],
[HtmlAttrAst, '[ngSwitch]', 'p.gender'],
[HtmlElementAst, 'template', 3],
[HtmlAttrAst, 'ngSwitchCase', '=m'],
[HtmlTextAst, 'm', 4],
[HtmlTextAst, ' ', 2],
[html.Element, 'ng-container', 0],
[html.Attribute, '[ngPlural]', 'messages.length'],
[html.Element, 'template', 1],
[html.Attribute, 'ngPluralCase', '=0'],
[html.Element, 'ng-container', 2],
[html.Attribute, '[ngSwitch]', 'p.gender'],
[html.Element, 'template', 3],
[html.Attribute, 'ngSwitchCase', '=m'],
[html.Text, 'm', 4],
[html.Text, ' ', 2],
]);
});
it('should correctly set source code positions', () => {
const nodes = expand(`{messages.length, plural,=0 {<b>bold</b>}}`).nodes;
const container: HtmlElementAst = <HtmlElementAst>nodes[0];
const container: html.Element = <html.Element>nodes[0];
expect(container.sourceSpan.start.col).toEqual(0);
expect(container.sourceSpan.end.col).toEqual(42);
expect(container.startSourceSpan.start.col).toEqual(0);
@ -68,7 +68,7 @@ export function main() {
expect(switchExp.sourceSpan.start.col).toEqual(1);
expect(switchExp.sourceSpan.end.col).toEqual(16);
const template: HtmlElementAst = <HtmlElementAst>container.children[0];
const template: html.Element = <html.Element>container.children[0];
expect(template.sourceSpan.start.col).toEqual(25);
expect(template.sourceSpan.end.col).toEqual(41);
@ -76,7 +76,7 @@ export function main() {
expect(switchCheck.sourceSpan.start.col).toEqual(25);
expect(switchCheck.sourceSpan.end.col).toEqual(28);
const b: HtmlElementAst = <HtmlElementAst>template.children[0];
const b: html.Element = <html.Element>template.children[0];
expect(b.sourceSpan.start.col).toEqual(29);
expect(b.endSourceSpan.end.col).toEqual(40);
});
@ -85,11 +85,11 @@ export function main() {
const res = expand(`{person.gender, gender,=male {m}}`);
expect(humanizeNodes(res.nodes)).toEqual([
[HtmlElementAst, 'ng-container', 0],
[HtmlAttrAst, '[ngSwitch]', 'person.gender'],
[HtmlElementAst, 'template', 1],
[HtmlAttrAst, 'ngSwitchCase', '=male'],
[HtmlTextAst, 'm', 2],
[html.Element, 'ng-container', 0],
[html.Attribute, '[ngSwitch]', 'person.gender'],
[html.Element, 'template', 1],
[html.Attribute, 'ngSwitchCase', '=male'],
[html.Text, 'm', 2],
]);
});

View File

@ -0,0 +1,803 @@
/**
* @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 {afterEach, beforeEach, ddescribe, describe, expect, iit, it, xit} from '../../../core/testing/testing_internal';
import {getHtmlTagDefinition} from '../../src/html_parser/html_tags';
import {InterpolationConfig} from '../../src/html_parser/interpolation_config';
import * as lex from '../../src/html_parser/lexer';
import {ParseLocation, ParseSourceFile, ParseSourceSpan} from '../../src/parse_util';
export function main() {
describe('HtmlLexer', () => {
describe('line/column numbers', () => {
it('should work without newlines', () => {
expect(tokenizeAndHumanizeLineColumn('<t>a</t>')).toEqual([
[lex.TokenType.TAG_OPEN_START, '0:0'],
[lex.TokenType.TAG_OPEN_END, '0:2'],
[lex.TokenType.TEXT, '0:3'],
[lex.TokenType.TAG_CLOSE, '0:4'],
[lex.TokenType.EOF, '0:8'],
]);
});
it('should work with one newline', () => {
expect(tokenizeAndHumanizeLineColumn('<t>\na</t>')).toEqual([
[lex.TokenType.TAG_OPEN_START, '0:0'],
[lex.TokenType.TAG_OPEN_END, '0:2'],
[lex.TokenType.TEXT, '0:3'],
[lex.TokenType.TAG_CLOSE, '1:1'],
[lex.TokenType.EOF, '1:5'],
]);
});
it('should work with multiple newlines', () => {
expect(tokenizeAndHumanizeLineColumn('<t\n>\na</t>')).toEqual([
[lex.TokenType.TAG_OPEN_START, '0:0'],
[lex.TokenType.TAG_OPEN_END, '1:0'],
[lex.TokenType.TEXT, '1:1'],
[lex.TokenType.TAG_CLOSE, '2:1'],
[lex.TokenType.EOF, '2:5'],
]);
});
it('should work with CR and LF', () => {
expect(tokenizeAndHumanizeLineColumn('<t\n>\r\na\r</t>')).toEqual([
[lex.TokenType.TAG_OPEN_START, '0:0'],
[lex.TokenType.TAG_OPEN_END, '1:0'],
[lex.TokenType.TEXT, '1:1'],
[lex.TokenType.TAG_CLOSE, '2:1'],
[lex.TokenType.EOF, '2:5'],
]);
});
});
describe('comments', () => {
it('should parse comments', () => {
expect(tokenizeAndHumanizeParts('<!--t\ne\rs\r\nt-->')).toEqual([
[lex.TokenType.COMMENT_START],
[lex.TokenType.RAW_TEXT, 't\ne\ns\nt'],
[lex.TokenType.COMMENT_END],
[lex.TokenType.EOF],
]);
});
it('should store the locations', () => {
expect(tokenizeAndHumanizeSourceSpans('<!--t\ne\rs\r\nt-->')).toEqual([
[lex.TokenType.COMMENT_START, '<!--'],
[lex.TokenType.RAW_TEXT, 't\ne\rs\r\nt'],
[lex.TokenType.COMMENT_END, '-->'],
[lex.TokenType.EOF, ''],
]);
});
it('should report <!- without -', () => {
expect(tokenizeAndHumanizeErrors('<!-a')).toEqual([
[lex.TokenType.COMMENT_START, 'Unexpected character "a"', '0:3']
]);
});
it('should report missing end comment', () => {
expect(tokenizeAndHumanizeErrors('<!--')).toEqual([
[lex.TokenType.RAW_TEXT, 'Unexpected character "EOF"', '0:4']
]);
});
it('should accept comments finishing by too many dashes (even number)', () => {
expect(tokenizeAndHumanizeSourceSpans('<!-- test ---->')).toEqual([
[lex.TokenType.COMMENT_START, '<!--'],
[lex.TokenType.RAW_TEXT, ' test --'],
[lex.TokenType.COMMENT_END, '-->'],
[lex.TokenType.EOF, ''],
]);
});
it('should accept comments finishing by too many dashes (odd number)', () => {
expect(tokenizeAndHumanizeSourceSpans('<!-- test --->')).toEqual([
[lex.TokenType.COMMENT_START, '<!--'],
[lex.TokenType.RAW_TEXT, ' test -'],
[lex.TokenType.COMMENT_END, '-->'],
[lex.TokenType.EOF, ''],
]);
});
});
describe('doctype', () => {
it('should parse doctypes', () => {
expect(tokenizeAndHumanizeParts('<!doctype html>')).toEqual([
[lex.TokenType.DOC_TYPE, 'doctype html'],
[lex.TokenType.EOF],
]);
});
it('should store the locations', () => {
expect(tokenizeAndHumanizeSourceSpans('<!doctype html>')).toEqual([
[lex.TokenType.DOC_TYPE, '<!doctype html>'],
[lex.TokenType.EOF, ''],
]);
});
it('should report missing end doctype', () => {
expect(tokenizeAndHumanizeErrors('<!')).toEqual([
[lex.TokenType.DOC_TYPE, 'Unexpected character "EOF"', '0:2']
]);
});
});
describe('CDATA', () => {
it('should parse CDATA', () => {
expect(tokenizeAndHumanizeParts('<![CDATA[t\ne\rs\r\nt]]>')).toEqual([
[lex.TokenType.CDATA_START],
[lex.TokenType.RAW_TEXT, 't\ne\ns\nt'],
[lex.TokenType.CDATA_END],
[lex.TokenType.EOF],
]);
});
it('should store the locations', () => {
expect(tokenizeAndHumanizeSourceSpans('<![CDATA[t\ne\rs\r\nt]]>')).toEqual([
[lex.TokenType.CDATA_START, '<![CDATA['],
[lex.TokenType.RAW_TEXT, 't\ne\rs\r\nt'],
[lex.TokenType.CDATA_END, ']]>'],
[lex.TokenType.EOF, ''],
]);
});
it('should report <![ without CDATA[', () => {
expect(tokenizeAndHumanizeErrors('<![a')).toEqual([
[lex.TokenType.CDATA_START, 'Unexpected character "a"', '0:3']
]);
});
it('should report missing end cdata', () => {
expect(tokenizeAndHumanizeErrors('<![CDATA[')).toEqual([
[lex.TokenType.RAW_TEXT, 'Unexpected character "EOF"', '0:9']
]);
});
});
describe('open tags', () => {
it('should parse open tags without prefix', () => {
expect(tokenizeAndHumanizeParts('<test>')).toEqual([
[lex.TokenType.TAG_OPEN_START, null, 'test'],
[lex.TokenType.TAG_OPEN_END],
[lex.TokenType.EOF],
]);
});
it('should parse namespace prefix', () => {
expect(tokenizeAndHumanizeParts('<ns1:test>')).toEqual([
[lex.TokenType.TAG_OPEN_START, 'ns1', 'test'],
[lex.TokenType.TAG_OPEN_END],
[lex.TokenType.EOF],
]);
});
it('should parse void tags', () => {
expect(tokenizeAndHumanizeParts('<test/>')).toEqual([
[lex.TokenType.TAG_OPEN_START, null, 'test'],
[lex.TokenType.TAG_OPEN_END_VOID],
[lex.TokenType.EOF],
]);
});
it('should allow whitespace after the tag name', () => {
expect(tokenizeAndHumanizeParts('<test >')).toEqual([
[lex.TokenType.TAG_OPEN_START, null, 'test'],
[lex.TokenType.TAG_OPEN_END],
[lex.TokenType.EOF],
]);
});
it('should store the locations', () => {
expect(tokenizeAndHumanizeSourceSpans('<test>')).toEqual([
[lex.TokenType.TAG_OPEN_START, '<test'],
[lex.TokenType.TAG_OPEN_END, '>'],
[lex.TokenType.EOF, ''],
]);
});
});
describe('attributes', () => {
it('should parse attributes without prefix', () => {
expect(tokenizeAndHumanizeParts('<t a>')).toEqual([
[lex.TokenType.TAG_OPEN_START, null, 't'],
[lex.TokenType.ATTR_NAME, null, 'a'],
[lex.TokenType.TAG_OPEN_END],
[lex.TokenType.EOF],
]);
});
it('should parse attributes with interpolation', () => {
expect(tokenizeAndHumanizeParts('<t a="{{v}}" b="s{{m}}e" c="s{{m//c}}e">')).toEqual([
[lex.TokenType.TAG_OPEN_START, null, 't'],
[lex.TokenType.ATTR_NAME, null, 'a'],
[lex.TokenType.ATTR_VALUE, '{{v}}'],
[lex.TokenType.ATTR_NAME, null, 'b'],
[lex.TokenType.ATTR_VALUE, 's{{m}}e'],
[lex.TokenType.ATTR_NAME, null, 'c'],
[lex.TokenType.ATTR_VALUE, 's{{m//c}}e'],
[lex.TokenType.TAG_OPEN_END],
[lex.TokenType.EOF],
]);
});
it('should parse attributes with prefix', () => {
expect(tokenizeAndHumanizeParts('<t ns1:a>')).toEqual([
[lex.TokenType.TAG_OPEN_START, null, 't'],
[lex.TokenType.ATTR_NAME, 'ns1', 'a'],
[lex.TokenType.TAG_OPEN_END],
[lex.TokenType.EOF],
]);
});
it('should parse attributes whose prefix is not valid', () => {
expect(tokenizeAndHumanizeParts('<t (ns1:a)>')).toEqual([
[lex.TokenType.TAG_OPEN_START, null, 't'],
[lex.TokenType.ATTR_NAME, null, '(ns1:a)'],
[lex.TokenType.TAG_OPEN_END],
[lex.TokenType.EOF],
]);
});
it('should parse attributes with single quote value', () => {
expect(tokenizeAndHumanizeParts('<t a=\'b\'>')).toEqual([
[lex.TokenType.TAG_OPEN_START, null, 't'],
[lex.TokenType.ATTR_NAME, null, 'a'],
[lex.TokenType.ATTR_VALUE, 'b'],
[lex.TokenType.TAG_OPEN_END],
[lex.TokenType.EOF],
]);
});
it('should parse attributes with double quote value', () => {
expect(tokenizeAndHumanizeParts('<t a="b">')).toEqual([
[lex.TokenType.TAG_OPEN_START, null, 't'],
[lex.TokenType.ATTR_NAME, null, 'a'],
[lex.TokenType.ATTR_VALUE, 'b'],
[lex.TokenType.TAG_OPEN_END],
[lex.TokenType.EOF],
]);
});
it('should parse attributes with unquoted value', () => {
expect(tokenizeAndHumanizeParts('<t a=b>')).toEqual([
[lex.TokenType.TAG_OPEN_START, null, 't'],
[lex.TokenType.ATTR_NAME, null, 'a'],
[lex.TokenType.ATTR_VALUE, 'b'],
[lex.TokenType.TAG_OPEN_END],
[lex.TokenType.EOF],
]);
});
it('should allow whitespace', () => {
expect(tokenizeAndHumanizeParts('<t a = b >')).toEqual([
[lex.TokenType.TAG_OPEN_START, null, 't'],
[lex.TokenType.ATTR_NAME, null, 'a'],
[lex.TokenType.ATTR_VALUE, 'b'],
[lex.TokenType.TAG_OPEN_END],
[lex.TokenType.EOF],
]);
});
it('should parse attributes with entities in values', () => {
expect(tokenizeAndHumanizeParts('<t a="&#65;&#x41;">')).toEqual([
[lex.TokenType.TAG_OPEN_START, null, 't'],
[lex.TokenType.ATTR_NAME, null, 'a'],
[lex.TokenType.ATTR_VALUE, 'AA'],
[lex.TokenType.TAG_OPEN_END],
[lex.TokenType.EOF],
]);
});
it('should not decode entities without trailing ";"', () => {
expect(tokenizeAndHumanizeParts('<t a="&amp" b="c&&d">')).toEqual([
[lex.TokenType.TAG_OPEN_START, null, 't'],
[lex.TokenType.ATTR_NAME, null, 'a'],
[lex.TokenType.ATTR_VALUE, '&amp'],
[lex.TokenType.ATTR_NAME, null, 'b'],
[lex.TokenType.ATTR_VALUE, 'c&&d'],
[lex.TokenType.TAG_OPEN_END],
[lex.TokenType.EOF],
]);
});
it('should parse attributes with "&" in values', () => {
expect(tokenizeAndHumanizeParts('<t a="b && c &">')).toEqual([
[lex.TokenType.TAG_OPEN_START, null, 't'],
[lex.TokenType.ATTR_NAME, null, 'a'],
[lex.TokenType.ATTR_VALUE, 'b && c &'],
[lex.TokenType.TAG_OPEN_END],
[lex.TokenType.EOF],
]);
});
it('should parse values with CR and LF', () => {
expect(tokenizeAndHumanizeParts('<t a=\'t\ne\rs\r\nt\'>')).toEqual([
[lex.TokenType.TAG_OPEN_START, null, 't'],
[lex.TokenType.ATTR_NAME, null, 'a'],
[lex.TokenType.ATTR_VALUE, 't\ne\ns\nt'],
[lex.TokenType.TAG_OPEN_END],
[lex.TokenType.EOF],
]);
});
it('should store the locations', () => {
expect(tokenizeAndHumanizeSourceSpans('<t a=b>')).toEqual([
[lex.TokenType.TAG_OPEN_START, '<t'],
[lex.TokenType.ATTR_NAME, 'a'],
[lex.TokenType.ATTR_VALUE, 'b'],
[lex.TokenType.TAG_OPEN_END, '>'],
[lex.TokenType.EOF, ''],
]);
});
});
describe('closing tags', () => {
it('should parse closing tags without prefix', () => {
expect(tokenizeAndHumanizeParts('</test>')).toEqual([
[lex.TokenType.TAG_CLOSE, null, 'test'],
[lex.TokenType.EOF],
]);
});
it('should parse closing tags with prefix', () => {
expect(tokenizeAndHumanizeParts('</ns1:test>')).toEqual([
[lex.TokenType.TAG_CLOSE, 'ns1', 'test'],
[lex.TokenType.EOF],
]);
});
it('should allow whitespace', () => {
expect(tokenizeAndHumanizeParts('</ test >')).toEqual([
[lex.TokenType.TAG_CLOSE, null, 'test'],
[lex.TokenType.EOF],
]);
});
it('should store the locations', () => {
expect(tokenizeAndHumanizeSourceSpans('</test>')).toEqual([
[lex.TokenType.TAG_CLOSE, '</test>'],
[lex.TokenType.EOF, ''],
]);
});
it('should report missing name after </', () => {
expect(tokenizeAndHumanizeErrors('</')).toEqual([
[lex.TokenType.TAG_CLOSE, 'Unexpected character "EOF"', '0:2']
]);
});
it('should report missing >', () => {
expect(tokenizeAndHumanizeErrors('</test')).toEqual([
[lex.TokenType.TAG_CLOSE, 'Unexpected character "EOF"', '0:6']
]);
});
});
describe('entities', () => {
it('should parse named entities', () => {
expect(tokenizeAndHumanizeParts('a&amp;b')).toEqual([
[lex.TokenType.TEXT, 'a&b'],
[lex.TokenType.EOF],
]);
});
it('should parse hexadecimal entities', () => {
expect(tokenizeAndHumanizeParts('&#x41;&#X41;')).toEqual([
[lex.TokenType.TEXT, 'AA'],
[lex.TokenType.EOF],
]);
});
it('should parse decimal entities', () => {
expect(tokenizeAndHumanizeParts('&#65;')).toEqual([
[lex.TokenType.TEXT, 'A'],
[lex.TokenType.EOF],
]);
});
it('should store the locations', () => {
expect(tokenizeAndHumanizeSourceSpans('a&amp;b')).toEqual([
[lex.TokenType.TEXT, 'a&amp;b'],
[lex.TokenType.EOF, ''],
]);
});
it('should report malformed/unknown entities', () => {
expect(tokenizeAndHumanizeErrors('&tbo;')).toEqual([[
lex.TokenType.TEXT,
'Unknown entity "tbo" - use the "&#<decimal>;" or "&#x<hex>;" syntax', '0:0'
]]);
expect(tokenizeAndHumanizeErrors('&#asdf;')).toEqual([
[lex.TokenType.TEXT, 'Unexpected character "s"', '0:3']
]);
expect(tokenizeAndHumanizeErrors('&#xasdf;')).toEqual([
[lex.TokenType.TEXT, 'Unexpected character "s"', '0:4']
]);
expect(tokenizeAndHumanizeErrors('&#xABC')).toEqual([
[lex.TokenType.TEXT, 'Unexpected character "EOF"', '0:6']
]);
});
});
describe('regular text', () => {
it('should parse text', () => {
expect(tokenizeAndHumanizeParts('a')).toEqual([
[lex.TokenType.TEXT, 'a'],
[lex.TokenType.EOF],
]);
});
it('should parse interpolation', () => {
expect(tokenizeAndHumanizeParts('{{ a }}b{{ c // comment }}')).toEqual([
[lex.TokenType.TEXT, '{{ a }}b{{ c // comment }}'],
[lex.TokenType.EOF],
]);
});
it('should parse interpolation with custom markers', () => {
expect(tokenizeAndHumanizeParts('{% a %}', null, {start: '{%', end: '%}'})).toEqual([
[lex.TokenType.TEXT, '{% a %}'],
[lex.TokenType.EOF],
]);
});
it('should handle CR & LF', () => {
expect(tokenizeAndHumanizeParts('t\ne\rs\r\nt')).toEqual([
[lex.TokenType.TEXT, 't\ne\ns\nt'],
[lex.TokenType.EOF],
]);
});
it('should parse entities', () => {
expect(tokenizeAndHumanizeParts('a&amp;b')).toEqual([
[lex.TokenType.TEXT, 'a&b'],
[lex.TokenType.EOF],
]);
});
it('should parse text starting with "&"', () => {
expect(tokenizeAndHumanizeParts('a && b &')).toEqual([
[lex.TokenType.TEXT, 'a && b &'],
[lex.TokenType.EOF],
]);
});
it('should store the locations', () => {
expect(tokenizeAndHumanizeSourceSpans('a')).toEqual([
[lex.TokenType.TEXT, 'a'],
[lex.TokenType.EOF, ''],
]);
});
it('should allow "<" in text nodes', () => {
expect(tokenizeAndHumanizeParts('{{ a < b ? c : d }}')).toEqual([
[lex.TokenType.TEXT, '{{ a < b ? c : d }}'],
[lex.TokenType.EOF],
]);
expect(tokenizeAndHumanizeSourceSpans('<p>a<b</p>')).toEqual([
[lex.TokenType.TAG_OPEN_START, '<p'],
[lex.TokenType.TAG_OPEN_END, '>'],
[lex.TokenType.TEXT, 'a<b'],
[lex.TokenType.TAG_CLOSE, '</p>'],
[lex.TokenType.EOF, ''],
]);
expect(tokenizeAndHumanizeParts('< a>')).toEqual([
[lex.TokenType.TEXT, '< a>'],
[lex.TokenType.EOF],
]);
});
it('should parse valid start tag in interpolation', () => {
expect(tokenizeAndHumanizeParts('{{ a <b && c > d }}')).toEqual([
[lex.TokenType.TEXT, '{{ a '],
[lex.TokenType.TAG_OPEN_START, null, 'b'],
[lex.TokenType.ATTR_NAME, null, '&&'],
[lex.TokenType.ATTR_NAME, null, 'c'],
[lex.TokenType.TAG_OPEN_END],
[lex.TokenType.TEXT, ' d }}'],
[lex.TokenType.EOF],
]);
});
it('should be able to escape {', () => {
expect(tokenizeAndHumanizeParts('{{ "{" }}')).toEqual([
[lex.TokenType.TEXT, '{{ "{" }}'],
[lex.TokenType.EOF],
]);
});
it('should be able to escape {{', () => {
expect(tokenizeAndHumanizeParts('{{ "{{" }}')).toEqual([
[lex.TokenType.TEXT, '{{ "{{" }}'],
[lex.TokenType.EOF],
]);
});
});
describe('raw text', () => {
it('should parse text', () => {
expect(tokenizeAndHumanizeParts(`<script>t\ne\rs\r\nt</script>`)).toEqual([
[lex.TokenType.TAG_OPEN_START, null, 'script'],
[lex.TokenType.TAG_OPEN_END],
[lex.TokenType.RAW_TEXT, 't\ne\ns\nt'],
[lex.TokenType.TAG_CLOSE, null, 'script'],
[lex.TokenType.EOF],
]);
});
it('should not detect entities', () => {
expect(tokenizeAndHumanizeParts(`<script>&amp;</SCRIPT>`)).toEqual([
[lex.TokenType.TAG_OPEN_START, null, 'script'],
[lex.TokenType.TAG_OPEN_END],
[lex.TokenType.RAW_TEXT, '&amp;'],
[lex.TokenType.TAG_CLOSE, null, 'script'],
[lex.TokenType.EOF],
]);
});
it('should ignore other opening tags', () => {
expect(tokenizeAndHumanizeParts(`<script>a<div></script>`)).toEqual([
[lex.TokenType.TAG_OPEN_START, null, 'script'],
[lex.TokenType.TAG_OPEN_END],
[lex.TokenType.RAW_TEXT, 'a<div>'],
[lex.TokenType.TAG_CLOSE, null, 'script'],
[lex.TokenType.EOF],
]);
});
it('should ignore other closing tags', () => {
expect(tokenizeAndHumanizeParts(`<script>a</test></script>`)).toEqual([
[lex.TokenType.TAG_OPEN_START, null, 'script'],
[lex.TokenType.TAG_OPEN_END],
[lex.TokenType.RAW_TEXT, 'a</test>'],
[lex.TokenType.TAG_CLOSE, null, 'script'],
[lex.TokenType.EOF],
]);
});
it('should store the locations', () => {
expect(tokenizeAndHumanizeSourceSpans(`<script>a</script>`)).toEqual([
[lex.TokenType.TAG_OPEN_START, '<script'],
[lex.TokenType.TAG_OPEN_END, '>'],
[lex.TokenType.RAW_TEXT, 'a'],
[lex.TokenType.TAG_CLOSE, '</script>'],
[lex.TokenType.EOF, ''],
]);
});
});
describe('escapable raw text', () => {
it('should parse text', () => {
expect(tokenizeAndHumanizeParts(`<title>t\ne\rs\r\nt</title>`)).toEqual([
[lex.TokenType.TAG_OPEN_START, null, 'title'],
[lex.TokenType.TAG_OPEN_END],
[lex.TokenType.ESCAPABLE_RAW_TEXT, 't\ne\ns\nt'],
[lex.TokenType.TAG_CLOSE, null, 'title'],
[lex.TokenType.EOF],
]);
});
it('should detect entities', () => {
expect(tokenizeAndHumanizeParts(`<title>&amp;</title>`)).toEqual([
[lex.TokenType.TAG_OPEN_START, null, 'title'],
[lex.TokenType.TAG_OPEN_END],
[lex.TokenType.ESCAPABLE_RAW_TEXT, '&'],
[lex.TokenType.TAG_CLOSE, null, 'title'],
[lex.TokenType.EOF],
]);
});
it('should ignore other opening tags', () => {
expect(tokenizeAndHumanizeParts(`<title>a<div></title>`)).toEqual([
[lex.TokenType.TAG_OPEN_START, null, 'title'],
[lex.TokenType.TAG_OPEN_END],
[lex.TokenType.ESCAPABLE_RAW_TEXT, 'a<div>'],
[lex.TokenType.TAG_CLOSE, null, 'title'],
[lex.TokenType.EOF],
]);
});
it('should ignore other closing tags', () => {
expect(tokenizeAndHumanizeParts(`<title>a</test></title>`)).toEqual([
[lex.TokenType.TAG_OPEN_START, null, 'title'],
[lex.TokenType.TAG_OPEN_END],
[lex.TokenType.ESCAPABLE_RAW_TEXT, 'a</test>'],
[lex.TokenType.TAG_CLOSE, null, 'title'],
[lex.TokenType.EOF],
]);
});
it('should store the locations', () => {
expect(tokenizeAndHumanizeSourceSpans(`<title>a</title>`)).toEqual([
[lex.TokenType.TAG_OPEN_START, '<title'],
[lex.TokenType.TAG_OPEN_END, '>'],
[lex.TokenType.ESCAPABLE_RAW_TEXT, 'a'],
[lex.TokenType.TAG_CLOSE, '</title>'],
[lex.TokenType.EOF, ''],
]);
});
});
describe('expansion forms', () => {
it('should parse an expansion form', () => {
expect(tokenizeAndHumanizeParts('{one.two, three, =4 {four} =5 {five} foo {bar} }', true))
.toEqual([
[lex.TokenType.EXPANSION_FORM_START],
[lex.TokenType.RAW_TEXT, 'one.two'],
[lex.TokenType.RAW_TEXT, 'three'],
[lex.TokenType.EXPANSION_CASE_VALUE, '=4'],
[lex.TokenType.EXPANSION_CASE_EXP_START],
[lex.TokenType.TEXT, 'four'],
[lex.TokenType.EXPANSION_CASE_EXP_END],
[lex.TokenType.EXPANSION_CASE_VALUE, '=5'],
[lex.TokenType.EXPANSION_CASE_EXP_START],
[lex.TokenType.TEXT, 'five'],
[lex.TokenType.EXPANSION_CASE_EXP_END],
[lex.TokenType.EXPANSION_CASE_VALUE, 'foo'],
[lex.TokenType.EXPANSION_CASE_EXP_START],
[lex.TokenType.TEXT, 'bar'],
[lex.TokenType.EXPANSION_CASE_EXP_END],
[lex.TokenType.EXPANSION_FORM_END],
[lex.TokenType.EOF],
]);
});
it('should parse an expansion form with text elements surrounding it', () => {
expect(tokenizeAndHumanizeParts('before{one.two, three, =4 {four}}after', true)).toEqual([
[lex.TokenType.TEXT, 'before'],
[lex.TokenType.EXPANSION_FORM_START],
[lex.TokenType.RAW_TEXT, 'one.two'],
[lex.TokenType.RAW_TEXT, 'three'],
[lex.TokenType.EXPANSION_CASE_VALUE, '=4'],
[lex.TokenType.EXPANSION_CASE_EXP_START],
[lex.TokenType.TEXT, 'four'],
[lex.TokenType.EXPANSION_CASE_EXP_END],
[lex.TokenType.EXPANSION_FORM_END],
[lex.TokenType.TEXT, 'after'],
[lex.TokenType.EOF],
]);
});
it('should parse an expansion forms with elements in it', () => {
expect(tokenizeAndHumanizeParts('{one.two, three, =4 {four <b>a</b>}}', true)).toEqual([
[lex.TokenType.EXPANSION_FORM_START],
[lex.TokenType.RAW_TEXT, 'one.two'],
[lex.TokenType.RAW_TEXT, 'three'],
[lex.TokenType.EXPANSION_CASE_VALUE, '=4'],
[lex.TokenType.EXPANSION_CASE_EXP_START],
[lex.TokenType.TEXT, 'four '],
[lex.TokenType.TAG_OPEN_START, null, 'b'],
[lex.TokenType.TAG_OPEN_END],
[lex.TokenType.TEXT, 'a'],
[lex.TokenType.TAG_CLOSE, null, 'b'],
[lex.TokenType.EXPANSION_CASE_EXP_END],
[lex.TokenType.EXPANSION_FORM_END],
[lex.TokenType.EOF],
]);
});
it('should parse an expansion forms containing an interpolation', () => {
expect(tokenizeAndHumanizeParts('{one.two, three, =4 {four {{a}}}}', true)).toEqual([
[lex.TokenType.EXPANSION_FORM_START],
[lex.TokenType.RAW_TEXT, 'one.two'],
[lex.TokenType.RAW_TEXT, 'three'],
[lex.TokenType.EXPANSION_CASE_VALUE, '=4'],
[lex.TokenType.EXPANSION_CASE_EXP_START],
[lex.TokenType.TEXT, 'four {{a}}'],
[lex.TokenType.EXPANSION_CASE_EXP_END],
[lex.TokenType.EXPANSION_FORM_END],
[lex.TokenType.EOF],
]);
});
it('should parse nested expansion forms', () => {
expect(tokenizeAndHumanizeParts(`{one.two, three, =4 { {xx, yy, =x {one}} }}`, true))
.toEqual([
[lex.TokenType.EXPANSION_FORM_START],
[lex.TokenType.RAW_TEXT, 'one.two'],
[lex.TokenType.RAW_TEXT, 'three'],
[lex.TokenType.EXPANSION_CASE_VALUE, '=4'],
[lex.TokenType.EXPANSION_CASE_EXP_START],
[lex.TokenType.EXPANSION_FORM_START],
[lex.TokenType.RAW_TEXT, 'xx'],
[lex.TokenType.RAW_TEXT, 'yy'],
[lex.TokenType.EXPANSION_CASE_VALUE, '=x'],
[lex.TokenType.EXPANSION_CASE_EXP_START],
[lex.TokenType.TEXT, 'one'],
[lex.TokenType.EXPANSION_CASE_EXP_END],
[lex.TokenType.EXPANSION_FORM_END],
[lex.TokenType.TEXT, ' '],
[lex.TokenType.EXPANSION_CASE_EXP_END],
[lex.TokenType.EXPANSION_FORM_END],
[lex.TokenType.EOF],
]);
});
});
describe('errors', () => {
it('should report unescaped "{" on error', () => {
expect(tokenizeAndHumanizeErrors(`<p>before { after</p>`, true)).toEqual([[
lex.TokenType.RAW_TEXT,
`Unexpected character "EOF" (Do you have an unescaped "{" in your template? Use "{{ '{' }}") to escape it.)`,
'0:21',
]]);
});
it('should include 2 lines of context in message', () => {
let src = '111\n222\n333\nE\n444\n555\n666\n';
let file = new ParseSourceFile(src, 'file://');
let location = new ParseLocation(file, 12, 123, 456);
let span = new ParseSourceSpan(location, location);
let error = new lex.TokenError('**ERROR**', null, span);
expect(error.toString())
.toEqual(`**ERROR** ("\n222\n333\n[ERROR ->]E\n444\n555\n"): file://@123:456`);
});
});
describe('unicode characters', () => {
it('should support unicode characters', () => {
expect(tokenizeAndHumanizeSourceSpans(`<p>İ</p>`)).toEqual([
[lex.TokenType.TAG_OPEN_START, '<p'],
[lex.TokenType.TAG_OPEN_END, '>'],
[lex.TokenType.TEXT, 'İ'],
[lex.TokenType.TAG_CLOSE, '</p>'],
[lex.TokenType.EOF, ''],
]);
});
});
});
}
function tokenizeWithoutErrors(
input: string, tokenizeExpansionForms: boolean = false,
interpolationConfig?: InterpolationConfig): lex.Token[] {
var tokenizeResult = lex.tokenize(
input, 'someUrl', getHtmlTagDefinition, tokenizeExpansionForms, interpolationConfig);
if (tokenizeResult.errors.length > 0) {
const errorString = tokenizeResult.errors.join('\n');
throw new Error(`Unexpected parse errors:\n${errorString}`);
}
return tokenizeResult.tokens;
}
function tokenizeAndHumanizeParts(
input: string, tokenizeExpansionForms: boolean = false,
interpolationConfig?: InterpolationConfig): any[] {
return tokenizeWithoutErrors(input, tokenizeExpansionForms, interpolationConfig)
.map(token => [<any>token.type].concat(token.parts));
}
function tokenizeAndHumanizeSourceSpans(input: string): any[] {
return tokenizeWithoutErrors(input).map(token => [<any>token.type, token.sourceSpan.toString()]);
}
function humanizeLineColumn(location: ParseLocation): string {
return `${location.line}:${location.col}`;
}
function tokenizeAndHumanizeLineColumn(input: string): any[] {
return tokenizeWithoutErrors(input).map(
token => [<any>token.type, humanizeLineColumn(token.sourceSpan.start)]);
}
function tokenizeAndHumanizeErrors(input: string, tokenizeExpansionForms: boolean = false): any[] {
return lex.tokenize(input, 'someUrl', getHtmlTagDefinition, tokenizeExpansionForms)
.errors.map(e => [<any>e.tokenType, e.msg, humanizeLineColumn(e.span.start)]);
}

View File

@ -10,261 +10,248 @@ import {ExtractionResult, extractAstMessages} from '@angular/compiler/src/i18n/e
import {beforeEach, ddescribe, describe, expect, iit, inject, it, xdescribe, xit} from '@angular/core/testing/testing_internal';
import {HtmlParser} from '../../src/html_parser/html_parser';
import {serializeAst} from '../html_parser/html_ast_serializer_spec'
import {serializeAst} from '../html_parser/ast_serializer_spec';
export function main() {
ddescribe(
'MessageExtractor',
() => {
describe('elements', () => {
it('should extract from elements', () => {
expect(extract('<div i18n="m|d">text<span>nested</span></div>')).toEqual([
[['text', '<span>nested</span>'], 'm', 'd'],
]);
});
describe('MessageExtractor', () => {
describe('elements', () => {
it('should extract from elements', () => {
expect(extract('<div i18n="m|d">text<span>nested</span></div>')).toEqual([
[['text', '<span>nested</span>'], 'm', 'd'],
]);
});
it('should not create a message for empty elements',
() => { expect(extract('<div i18n="m|d"></div>')).toEqual([]); });
});
it('should not create a message for empty elements',
() => { expect(extract('<div i18n="m|d"></div>')).toEqual([]); });
});
describe('blocks', () => {
it('should extract from blocks', () => {
expect(extract(`<!-- i18n: meaning1|desc1 -->message1<!-- /i18n -->
describe('blocks', () => {
it('should extract from blocks', () => {
expect(extract(`<!-- i18n: meaning1|desc1 -->message1<!-- /i18n -->
<!-- i18n: meaning2 -->message2<!-- /i18n -->
<!-- i18n -->message3<!-- /i18n -->`))
.toEqual([
[['message1'], 'meaning1', 'desc1'],
[['message2'], 'meaning2', ''],
[['message3'], '', ''],
]);
});
it('should extract siblings', () => {
expect(
extract(
`<!-- i18n -->text<p>html<b>nested</b></p>{count, plural, =0 {<span>html</span>}}{{interp}}<!-- /i18n -->`))
.toEqual([
[['{count, plural, =0 {<span>html</span>}}'], '', ''],
[
[
'text', '<p>html<b>nested</b></p>', '{count, plural, =0 {<span>html</span>}}',
'{{interp}}'
],
'', ''
],
]);
});
it('should ignore other comments', () => {
expect(extract(`<!-- i18n: meaning1|desc1 --><!-- other -->message1<!-- /i18n -->`))
.toEqual([
[['message1'], 'meaning1', 'desc1'],
]);
});
it('should not create a message for empty blocks',
() => { expect(extract(`<!-- i18n: meaning1|desc1 --><!-- /i18n -->`)).toEqual([]); });
});
describe('ICU messages', () => {
it('should extract ICU messages from translatable elements', () => {
// single message when ICU is the only children
expect(extract('<div i18n="m|d">{count, plural, =0 {text}}</div>')).toEqual([
[['{count, plural, =0 {text}}'], 'm', 'd'],
.toEqual([
[['message1'], 'meaning1', 'desc1'],
[['message2'], 'meaning2', ''],
[['message3'], '', ''],
]);
});
// one message for the element content and one message for the ICU
expect(extract('<div i18n="m|d">before{count, plural, =0 {text}}after</div>')).toEqual([
[['before', '{count, plural, =0 {text}}', 'after'], 'm', 'd'],
it('should extract siblings', () => {
expect(
extract(
`<!-- i18n -->text<p>html<b>nested</b></p>{count, plural, =0 {<span>html</span>}}{{interp}}<!-- /i18n -->`))
.toEqual([
[['{count, plural, =0 {<span>html</span>}}'], '', ''],
[
[
'text', '<p>html<b>nested</b></p>', '{count, plural, =0 {<span>html</span>}}',
'{{interp}}'
],
'', ''
],
]);
});
it('should ignore other comments', () => {
expect(extract(`<!-- i18n: meaning1|desc1 --><!-- other -->message1<!-- /i18n -->`))
.toEqual([
[['message1'], 'meaning1', 'desc1'],
]);
});
it('should not create a message for empty blocks',
() => { expect(extract(`<!-- i18n: meaning1|desc1 --><!-- /i18n -->`)).toEqual([]); });
});
describe('ICU messages', () => {
it('should extract ICU messages from translatable elements', () => {
// single message when ICU is the only children
expect(extract('<div i18n="m|d">{count, plural, =0 {text}}</div>')).toEqual([
[['{count, plural, =0 {text}}'], 'm', 'd'],
]);
// one message for the element content and one message for the ICU
expect(extract('<div i18n="m|d">before{count, plural, =0 {text}}after</div>')).toEqual([
[['before', '{count, plural, =0 {text}}', 'after'], 'm', 'd'],
[['{count, plural, =0 {text}}'], '', ''],
]);
});
it('should extract ICU messages from translatable block', () => {
// single message when ICU is the only children
expect(extract('<!-- i18n:m|d -->{count, plural, =0 {text}}<!-- /i18n -->')).toEqual([
[['{count, plural, =0 {text}}'], 'm', 'd'],
]);
// one message for the block content and one message for the ICU
expect(extract('<!-- i18n:m|d -->before{count, plural, =0 {text}}after<!-- /i18n -->'))
.toEqual([
[['{count, plural, =0 {text}}'], '', ''],
[['before', '{count, plural, =0 {text}}', 'after'], 'm', 'd'],
]);
});
});
it('should extract ICU messages from translatable block', () => {
// single message when ICU is the only children
expect(extract('<!-- i18n:m|d -->{count, plural, =0 {text}}<!-- /i18n -->')).toEqual([
[['{count, plural, =0 {text}}'], 'm', 'd'],
it('should not extract ICU messages outside of i18n sections',
() => { expect(extract('{count, plural, =0 {text}}')).toEqual([]); });
it('should not extract nested ICU messages', () => {
expect(extract('<div i18n="m|d">{count, plural, =0 { {sex, gender, =m {m}} }}</div>'))
.toEqual([
[['{count, plural, =0 {{sex, gender, =m {m}} }}'], 'm', 'd'],
]);
});
});
// one message for the block content and one message for the ICU
expect(extract('<!-- i18n:m|d -->before{count, plural, =0 {text}}after<!-- /i18n -->'))
.toEqual([
[['{count, plural, =0 {text}}'], '', ''],
[['before', '{count, plural, =0 {text}}', 'after'], 'm', 'd'],
]);
});
describe('attributes', () => {
it('should extract from attributes outside of translatable section', () => {
expect(extract('<div i18n-title="m|d" title="msg"></div>')).toEqual([
[['title="msg"'], 'm', 'd'],
]);
});
it('should not extract ICU messages outside of i18n sections',
() => { expect(extract('{count, plural, =0 {text}}')).toEqual([]); });
it('should extract from attributes in translatable element', () => {
expect(extract('<div i18n><p><b i18n-title="m|d" title="msg"></b></p></div>')).toEqual([
[['<p><b i18n-title="m|d" title="msg"></b></p>'], '', ''],
[['title="msg"'], 'm', 'd'],
]);
});
it('should not extract nested ICU messages', () => {
expect(extract('<div i18n="m|d">{count, plural, =0 { {sex, gender, =m {m}} }}</div>'))
.toEqual([
[['{count, plural, =0 {{sex, gender, =m {m}} }}'], 'm', 'd'],
]);
});
});
describe('attributes', () => {
it('should extract from attributes outside of translatable section', () => {
expect(extract('<div i18n-title="m|d" title="msg"></div>')).toEqual([
it('should extract from attributes in translatable block', () => {
expect(extract('<!-- i18n --><p><b i18n-title="m|d" title="msg"></b></p><!-- /i18n -->'))
.toEqual([
[['title="msg"'], 'm', 'd'],
]);
});
it('should extract from attributes in translatable element', () => {
expect(extract('<div i18n><p><b i18n-title="m|d" title="msg"></b></p></div>')).toEqual([
[['<p><b i18n-title="m|d" title="msg"></b></p>'], '', ''],
]);
});
it('should extract from attributes in translatable ICU', () => {
expect(
extract(
'<!-- i18n -->{count, plural, =0 {<p><b i18n-title="m|d" title="msg"></b></p>}}<!-- /i18n -->'))
.toEqual([
[['title="msg"'], 'm', 'd'],
[['{count, plural, =0 {<p><b i18n-title="m|d" title="msg"></b></p>}}'], '', ''],
]);
});
it('should extract from attributes in non translatable ICU', () => {
expect(extract('{count, plural, =0 {<p><b i18n-title="m|d" title="msg"></b></p>}}'))
.toEqual([
[['title="msg"'], 'm', 'd'],
]);
});
});
it('should extract from attributes in translatable block', () => {
expect(
extract('<!-- i18n --><p><b i18n-title="m|d" title="msg"></b></p><!-- /i18n -->'))
.toEqual([
[['title="msg"'], 'm', 'd'],
[['<p><b i18n-title="m|d" title="msg"></b></p>'], '', ''],
]);
});
it('should not create a message for empty attributes',
() => { expect(extract('<div i18n-title="m|d" title></div>')).toEqual([]); });
});
it('should extract from attributes in translatable ICU', () => {
expect(
extract(
'<!-- i18n -->{count, plural, =0 {<p><b i18n-title="m|d" title="msg"></b></p>}}<!-- /i18n -->'))
.toEqual([
[['title="msg"'], 'm', 'd'],
[['{count, plural, =0 {<p><b i18n-title="m|d" title="msg"></b></p>}}'], '', ''],
]);
});
describe('implicit elements', () => {
it('should extract from implicit elements', () => {
expect(extract('<b>bold</b><i>italic</i>', ['b'])).toEqual([
[['bold'], '', ''],
]);
});
});
it('should extract from attributes in non translatable ICU', () => {
expect(extract('{count, plural, =0 {<p><b i18n-title="m|d" title="msg"></b></p>}}'))
.toEqual([
[['title="msg"'], 'm', 'd'],
]);
});
it('should not create a message for empty attributes',
() => { expect(extract('<div i18n-title="m|d" title></div>')).toEqual([]); });
});
describe('implicit elements', () => {
it('should extract from implicit elements', () => {
expect(extract('<b>bold</b><i>italic</i>', ['b'])).toEqual([
[['bold'], '', ''],
describe('implicit attributes', () => {
it('should extract implicit attributes', () => {
expect(extract('<b title="bb">bold</b><i title="ii">italic</i>', [], {'b': ['title']}))
.toEqual([
[['title="bb"'], '', ''],
]);
});
});
});
describe('errors', () => {
describe('elements', () => {
it('should report nested translatable elements', () => {
expect(extractErrors(`<p i18n><b i18n></b></p>`)).toEqual([
['Could not mark an element as translatable inside a translatable section', '<b i18n>'],
]);
});
describe('implicit attributes', () => {
it('should extract implicit attributes', () => {
expect(extract('<b title="bb">bold</b><i title="ii">italic</i>', [], {'b': ['title']}))
.toEqual([
[['title="bb"'], '', ''],
]);
});
it('should report translatable elements in implicit elements', () => {
expect(extractErrors(`<p><b i18n></b></p>`, ['p'])).toEqual([
['Could not mark an element as translatable inside a translatable section', '<b i18n>'],
]);
});
describe('errors', () => {
describe('elements', () => {
it('should report nested translatable elements', () => {
expect(extractErrors(`<p i18n><b i18n></b></p>`)).toEqual([
[
'Could not mark an element as translatable inside a translatable section',
'<b i18n>'
],
]);
});
it('should report translatable elements in implicit elements', () => {
expect(extractErrors(`<p><b i18n></b></p>`, ['p'])).toEqual([
[
'Could not mark an element as translatable inside a translatable section',
'<b i18n>'
],
]);
});
it('should report translatable elements in translatable blocks', () => {
expect(extractErrors(`<!-- i18n --><b i18n></b><!-- /i18n -->`)).toEqual([
[
'Could not mark an element as translatable inside a translatable section',
'<b i18n>'
],
]);
});
});
describe('blocks', () => {
it('should report nested blocks', () => {
expect(extractErrors(`<!-- i18n --><!-- i18n --><!-- /i18n --><!-- /i18n -->`))
.toEqual([
['Could not start a block inside a translatable section', '<!--'],
['Trying to close an unopened block', '<!--'],
]);
});
it('should report unclosed blocks', () => {
expect(extractErrors(`<!-- i18n -->`)).toEqual([
['Unclosed block', '<!--'],
]);
});
it('should report translatable blocks in translatable elements', () => {
expect(extractErrors(`<p i18n><!-- i18n --><!-- /i18n --></p>`)).toEqual([
['Could not start a block inside a translatable section', '<!--'],
['Trying to close an unopened block', '<!--'],
]);
});
it('should report translatable blocks in implicit elements', () => {
expect(extractErrors(`<p><!-- i18n --><!-- /i18n --></p>`, ['p'])).toEqual([
['Could not start a block inside a translatable section', '<!--'],
['Trying to close an unopened block', '<!--'],
]);
});
it('should report when start and end of a block are not at the same level', () => {
expect(extractErrors(`<!-- i18n --><p><!-- /i18n --></p>`)).toEqual([
['I18N blocks should not cross element boundaries', '<!--'],
['Unclosed block', '<p>'],
]);
expect(extractErrors(`<p><!-- i18n --></p><!-- /i18n -->`)).toEqual([
['I18N blocks should not cross element boundaries', '<!--'],
['Unclosed block', '<!--'],
]);
});
});
describe('implicit elements', () => {
it('should report nested implicit elements', () => {
expect(extractErrors(`<p><b></b></p>`, ['p', 'b'])).toEqual([
['Could not mark an element as translatable inside a translatable section', '<b>'],
]);
});
it('should report implicit element in translatable element', () => {
expect(extractErrors(`<p i18n><b></b></p>`, ['b'])).toEqual([
['Could not mark an element as translatable inside a translatable section', '<b>'],
]);
});
it('should report implicit element in translatable blocks', () => {
expect(extractErrors(`<!-- i18n --><b></b><!-- /i18n -->`, ['b'])).toEqual([
['Could not mark an element as translatable inside a translatable section', '<b>'],
]);
});
});
it('should report translatable elements in translatable blocks', () => {
expect(extractErrors(`<!-- i18n --><b i18n></b><!-- /i18n -->`)).toEqual([
['Could not mark an element as translatable inside a translatable section', '<b i18n>'],
]);
});
});
describe('blocks', () => {
it('should report nested blocks', () => {
expect(extractErrors(`<!-- i18n --><!-- i18n --><!-- /i18n --><!-- /i18n -->`)).toEqual([
['Could not start a block inside a translatable section', '<!--'],
['Trying to close an unopened block', '<!--'],
]);
});
it('should report unclosed blocks', () => {
expect(extractErrors(`<!-- i18n -->`)).toEqual([
['Unclosed block', '<!--'],
]);
});
it('should report translatable blocks in translatable elements', () => {
expect(extractErrors(`<p i18n><!-- i18n --><!-- /i18n --></p>`)).toEqual([
['Could not start a block inside a translatable section', '<!--'],
['Trying to close an unopened block', '<!--'],
]);
});
it('should report translatable blocks in implicit elements', () => {
expect(extractErrors(`<p><!-- i18n --><!-- /i18n --></p>`, ['p'])).toEqual([
['Could not start a block inside a translatable section', '<!--'],
['Trying to close an unopened block', '<!--'],
]);
});
it('should report when start and end of a block are not at the same level', () => {
expect(extractErrors(`<!-- i18n --><p><!-- /i18n --></p>`)).toEqual([
['I18N blocks should not cross element boundaries', '<!--'],
['Unclosed block', '<p>'],
]);
expect(extractErrors(`<p><!-- i18n --></p><!-- /i18n -->`)).toEqual([
['I18N blocks should not cross element boundaries', '<!--'],
['Unclosed block', '<!--'],
]);
});
});
describe('implicit elements', () => {
it('should report nested implicit elements', () => {
expect(extractErrors(`<p><b></b></p>`, ['p', 'b'])).toEqual([
['Could not mark an element as translatable inside a translatable section', '<b>'],
]);
});
it('should report implicit element in translatable element', () => {
expect(extractErrors(`<p i18n><b></b></p>`, ['b'])).toEqual([
['Could not mark an element as translatable inside a translatable section', '<b>'],
]);
});
it('should report implicit element in translatable blocks', () => {
expect(extractErrors(`<!-- i18n --><b></b><!-- /i18n -->`, ['b'])).toEqual([
['Could not mark an element as translatable inside a translatable section', '<b>'],
]);
});
});
});
});
}
function getExtractionResult(
html: string, implicitTags: string[], implicitAttrs:
{[k: string]: string[]}): ExtractionResult {
html: string, implicitTags: string[],
implicitAttrs: {[k: string]: string[]}): ExtractionResult {
const htmlParser = new HtmlParser();
const parseResult = htmlParser.parse(html, 'extractor spec', true);
if (parseResult.errors.length > 1) {
@ -275,8 +262,8 @@ function getExtractionResult(
}
function extract(
html: string, implicitTags: string[] = [], implicitAttrs:
{[k: string]: string[]} = {}): [string[], string, string][] {
html: string, implicitTags: string[] = [],
implicitAttrs: {[k: string]: string[]} = {}): [string[], string, string][] {
const messages = getExtractionResult(html, implicitTags, implicitAttrs).messages;
// clang-format off
@ -287,8 +274,7 @@ function extract(
}
function extractErrors(
html: string, implicitTags: string[] = [], implicitAttrs:
{[k: string]: string[]} = {}): any[] {
html: string, implicitTags: string[] = [], implicitAttrs: {[k: string]: string[]} = {}): any[] {
const errors = getExtractionResult(html, implicitTags, implicitAttrs).errors;
return errors.map((e): [string, string] => [e.msg, e.span.toString()]);

View File

@ -1,298 +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 {Lexer as ExpressionLexer} from '@angular/compiler/src/expression_parser/lexer';
import {Parser as ExpressionParser} from '@angular/compiler/src/expression_parser/parser';
import {I18nHtmlParser} from '@angular/compiler/src/i18n/i18n_html_parser';
import {Message, id} from '@angular/compiler/src/i18n/message';
import {deserializeXmb} from '@angular/compiler/src/i18n/xmb_serializer';
import {ParseError} from '@angular/compiler/src/parse_util';
import {ddescribe, describe, expect, iit, it} from '@angular/core/testing/testing_internal';
import {StringMapWrapper} from '../../src/facade/collection';
import {HtmlAttrAst, HtmlElementAst, HtmlTextAst} from '../../src/html_parser/html_ast';
import {HtmlParseTreeResult, HtmlParser} from '../../src/html_parser/html_parser';
import {InterpolationConfig} from '../../src/html_parser/interpolation_config';
import {humanizeDom} from '../html_parser/html_ast_spec_utils';
export function main() {
describe('I18nHtmlParser', () => {
function parse(
template: string, messages: {[key: string]: string}, implicitTags: string[] = [],
implicitAttrs: {[k: string]: string[]} = {},
interpolation?: InterpolationConfig): HtmlParseTreeResult {
let htmlParser = new HtmlParser();
let msgs = '';
StringMapWrapper.forEach(
messages, (v: string, k: string) => msgs += `<msg id="${k}">${v}</msg>`);
let res = deserializeXmb(`<message-bundle>${msgs}</message-bundle>`, 'someUrl');
const expParser = new ExpressionParser(new ExpressionLexer());
return new I18nHtmlParser(
htmlParser, expParser, res.content, res.messages, implicitTags, implicitAttrs)
.parse(template, 'someurl', true, interpolation);
}
it('should delegate to the provided parser when no i18n', () => {
expect(humanizeDom(parse('<div>a</div>', {}))).toEqual([
[HtmlElementAst, 'div', 0], [HtmlTextAst, 'a', 1]
]);
});
describe('interpolation', () => {
it('should handle interpolation', () => {
let translations: {[key: string]: string} = {};
translations[id(new Message(
'<ph name="INTERPOLATION_0"/> and <ph name="INTERPOLATION_1"/>', null, null))] =
'<ph name="INTERPOLATION_1"/> or <ph name="INTERPOLATION_0"/>';
expect(humanizeDom(parse('<div value=\'{{a}} and {{b}}\' i18n-value></div>', translations)))
.toEqual([[HtmlElementAst, 'div', 0], [HtmlAttrAst, 'value', '{{b}} or {{a}}']]);
});
it('should handle interpolation with config', () => {
let translations: {[key: string]: string} = {};
translations[id(new Message(
'<ph name="INTERPOLATION_0"/> and <ph name="INTERPOLATION_1"/>', null, null))] =
'<ph name="INTERPOLATION_1"/> or <ph name="INTERPOLATION_0"/>';
expect(humanizeDom(parse(
'<div value=\'{%a%} and {%b%}\' i18n-value></div>', translations, [], {},
InterpolationConfig.fromArray(['{%', '%}']))))
.toEqual([
[HtmlElementAst, 'div', 0],
[HtmlAttrAst, 'value', '{%b%} or {%a%}'],
]);
});
it('should handle interpolation with custom placeholder names', () => {
let translations: {[key: string]: string} = {};
translations[id(new Message('<ph name="FIRST"/> and <ph name="SECOND"/>', null, null))] =
'<ph name="SECOND"/> or <ph name="FIRST"/>';
expect(
humanizeDom(parse(
`<div value='{{a //i18n(ph="FIRST")}} and {{b //i18n(ph="SECOND")}}' i18n-value></div>`,
translations)))
.toEqual([
[HtmlElementAst, 'div', 0],
[HtmlAttrAst, 'value', '{{b //i18n(ph="SECOND")}} or {{a //i18n(ph="FIRST")}}']
]);
});
it('should handle interpolation with duplicate placeholder names', () => {
let translations: {[key: string]: string} = {};
translations[id(new Message('<ph name="FIRST"/> and <ph name="FIRST_1"/>', null, null))] =
'<ph name="FIRST_1"/> or <ph name="FIRST"/>';
expect(
humanizeDom(parse(
`<div value='{{a //i18n(ph="FIRST")}} and {{b //i18n(ph="FIRST")}}' i18n-value></div>`,
translations)))
.toEqual([
[HtmlElementAst, 'div', 0],
[HtmlAttrAst, 'value', '{{b //i18n(ph="FIRST")}} or {{a //i18n(ph="FIRST")}}']
]);
});
it('should support interpolation', () => {
let translations: {[key: string]: string} = {};
translations[id(new Message(
'<ph name="e0">a</ph><ph name="e2"><ph name="t3">b<ph name="INTERPOLATION_0"/></ph></ph>',
null, null))] =
'<ph name="e2"><ph name="t3"><ph name="INTERPOLATION_0"/>B</ph></ph><ph name="e0">A</ph>';
expect(humanizeDom(parse('<div i18n><a>a</a><b>b{{i}}</b></div>', translations))).toEqual([
[HtmlElementAst, 'div', 0],
[HtmlElementAst, 'b', 1],
[HtmlTextAst, '{{i}}B', 2],
[HtmlElementAst, 'a', 1],
[HtmlTextAst, 'A', 2],
]);
});
});
describe('html', () => {
it('should handle nested html', () => {
let translations: {[key: string]: string} = {};
translations[id(new Message('<ph name="e0">a</ph><ph name="e2">b</ph>', null, null))] =
'<ph name="e2">B</ph><ph name="e0">A</ph>';
expect(humanizeDom(parse('<div i18n><a>a</a><b>b</b></div>', translations))).toEqual([
[HtmlElementAst, 'div', 0],
[HtmlElementAst, 'b', 1],
[HtmlTextAst, 'B', 2],
[HtmlElementAst, 'a', 1],
[HtmlTextAst, 'A', 2],
]);
});
it('should i18n attributes of placeholder elements', () => {
let translations: {[key: string]: string} = {};
translations[id(new Message('<ph name="e0">a</ph>', null, null))] = '<ph name="e0">A</ph>';
translations[id(new Message('b', null, null))] = 'B';
expect(humanizeDom(parse('<div i18n><a value="b" i18n-value>a</a></div>', translations)))
.toEqual([
[HtmlElementAst, 'div', 0],
[HtmlElementAst, 'a', 1],
[HtmlAttrAst, 'value', 'B'],
[HtmlTextAst, 'A', 2],
]);
});
it('should preserve non-i18n attributes', () => {
let translations: {[key: string]: string} = {};
translations[id(new Message('message', null, null))] = 'another message';
expect(humanizeDom(parse('<div i18n value="b">message</div>', translations))).toEqual([
[HtmlElementAst, 'div', 0], [HtmlAttrAst, 'value', 'b'],
[HtmlTextAst, 'another message', 1]
]);
});
it('should replace attributes', () => {
let translations: {[key: string]: string} = {};
translations[id(new Message('some message', 'meaning', null))] = 'another message';
expect(
humanizeDom(parse(
'<div value=\'some message\' i18n-value=\'meaning|comment\'></div>', translations)))
.toEqual([[HtmlElementAst, 'div', 0], [HtmlAttrAst, 'value', 'another message']]);
});
it('should replace elements with the i18n attr', () => {
let translations: {[key: string]: string} = {};
translations[id(new Message('message', 'meaning', null))] = 'another message';
expect(humanizeDom(parse('<div i18n=\'meaning|desc\'>message</div>', translations)))
.toEqual([[HtmlElementAst, 'div', 0], [HtmlTextAst, 'another message', 1]]);
});
});
it('should extract from partitions', () => {
let translations: {[key: string]: string} = {};
translations[id(new Message('message1', 'meaning1', null))] = 'another message1';
translations[id(new Message('message2', 'meaning2', null))] = 'another message2';
let res = parse(
`<!-- i18n: meaning1|desc1 -->message1<!-- /i18n --><!-- i18n: meaning2|desc2 -->message2<!-- /i18n -->`,
translations);
expect(humanizeDom(res)).toEqual([
[HtmlTextAst, 'another message1', 0],
[HtmlTextAst, 'another message2', 0],
]);
});
it('should preserve original positions', () => {
let translations: {[key: string]: string} = {};
translations[id(new Message('<ph name="e0">a</ph><ph name="e2">b</ph>', null, null))] =
'<ph name="e2">B</ph><ph name="e0">A</ph>';
let res =
(<any>parse('<div i18n><a>a</a><b>b</b></div>', translations).rootNodes[0]).children;
expect(res[0].sourceSpan.start.offset).toEqual(18);
expect(res[1].sourceSpan.start.offset).toEqual(10);
});
describe('errors', () => {
it('should error when giving an invalid template', () => {
expect(humanizeErrors(parse('<a>a</b>', {}).errors)).toEqual([
'Unexpected closing tag "b"'
]);
});
it('should error when no matching message (attr)', () => {
let mid = id(new Message('some message', null, null));
expect(humanizeErrors(parse('<div value=\'some message\' i18n-value></div>', {}).errors))
.toEqual([`Cannot find message for id '${mid}', content 'some message'.`]);
});
it('should error when no matching message (text)', () => {
let mid = id(new Message('some message', null, null));
expect(humanizeErrors(parse('<div i18n>some message</div>', {}).errors)).toEqual([
`Cannot find message for id '${mid}', content 'some message'.`
]);
});
it('should error when a non-placeholder element appears in translation', () => {
let translations: {[key: string]: string} = {};
translations[id(new Message('some message', null, null))] = '<a>a</a>';
expect(humanizeErrors(parse('<div i18n>some message</div>', translations).errors)).toEqual([
`Unexpected tag "a". Only "ph" tags are allowed.`
]);
});
it('should error when a placeholder element does not have the name attribute', () => {
let translations: {[key: string]: string} = {};
translations[id(new Message('some message', null, null))] = '<ph>a</ph>';
expect(humanizeErrors(parse('<div i18n>some message</div>', translations).errors)).toEqual([
`Missing "name" attribute.`
]);
});
it('should error when the translation refers to an invalid expression', () => {
let translations: {[key: string]: string} = {};
translations[id(new Message('hi <ph name="INTERPOLATION_0"/>', null, null))] =
'hi <ph name="INTERPOLATION_99"/>';
expect(
humanizeErrors(parse('<div value=\'hi {{a}}\' i18n-value></div>', translations).errors))
.toEqual(['Invalid interpolation name \'INTERPOLATION_99\'']);
});
});
describe('implicit translation', () => {
it('should support attributes', () => {
let translations: {[key: string]: string} = {};
translations[id(new Message('some message', null, null))] = 'another message';
expect(humanizeDom(parse('<i18n-el value=\'some message\'></i18n-el>', translations, [], {
'i18n-el': ['value']
}))).toEqual([[HtmlElementAst, 'i18n-el', 0], [HtmlAttrAst, 'value', 'another message']]);
});
it('should support attributes with meaning and description', () => {
let translations: {[key: string]: string} = {};
translations[id(new Message('some message', 'meaning', 'description'))] = 'another message';
expect(humanizeDom(parse(
'<i18n-el value=\'some message\' i18n-value=\'meaning|description\'></i18n-el>',
translations, [], {'i18n-el': ['value']})))
.toEqual([[HtmlElementAst, 'i18n-el', 0], [HtmlAttrAst, 'value', 'another message']]);
});
it('should support elements', () => {
let translations: {[key: string]: string} = {};
translations[id(new Message('message', null, null))] = 'another message';
expect(humanizeDom(parse('<i18n-el>message</i18n-el>', translations, ['i18n-el'])))
.toEqual([[HtmlElementAst, 'i18n-el', 0], [HtmlTextAst, 'another message', 1]]);
});
it('should support elements with meaning and description', () => {
let translations: {[key: string]: string} = {};
translations[id(new Message('message', 'meaning', 'description'))] = 'another message';
expect(humanizeDom(parse(
'<i18n-el i18n=\'meaning|description\'>message</i18n-el>', translations,
['i18n-el'])))
.toEqual([[HtmlElementAst, 'i18n-el', 0], [HtmlTextAst, 'another message', 1]]);
});
});
});
}
function humanizeErrors(errors: ParseError[]): string[] {
return errors.map(error => error.msg);
}

View File

@ -6,25 +6,26 @@
* found in the LICENSE file at https://angular.io/license
*/
import {serializeAst} from '@angular/compiler/src/i18n/catalog';
import {Message} from '@angular/compiler/src/i18n/i18n_ast';
import {extractI18nMessages} from '@angular/compiler/src/i18n/i18n_parser';
import {ddescribe, describe, expect, it} from '@angular/core/testing/testing_internal';
import {HtmlParser} from '../../src/html_parser/html_parser';
import {DEFAULT_INTERPOLATION_CONFIG} from '../../src/html_parser/interpolation_config';
import {serializeAst} from '../../src/i18n/message_bundle';
export function main() {
ddescribe('I18nParser', () => {
describe('I18nParser', () => {
describe('elements', () => {
it('should extract from elements', () => {
expect(extract('<div i18n="m|d">text</div>')).toEqual([
expect(_humanizeMessages('<div i18n="m|d">text</div>')).toEqual([
[['text'], 'm', 'd'],
]);
});
it('should extract from nested elements', () => {
expect(extract('<div i18n="m|d">text<span><b>nested</b></span></div>')).toEqual([
expect(_humanizeMessages('<div i18n="m|d">text<span><b>nested</b></span></div>')).toEqual([
[
[
'text',
@ -36,13 +37,13 @@ export function main() {
});
it('should not create a message for empty elements',
() => { expect(extract('<div i18n="m|d"></div>')).toEqual([]); });
() => { expect(_humanizeMessages('<div i18n="m|d"></div>')).toEqual([]); });
it('should not create a message for plain elements',
() => { expect(extract('<div></div>')).toEqual([]); });
() => { expect(_humanizeMessages('<div></div>')).toEqual([]); });
it('should suppoprt void elements', () => {
expect(extract('<div i18n="m|d"><p><br></p></div>')).toEqual([
expect(_humanizeMessages('<div i18n="m|d"><p><br></p></div>')).toEqual([
[
[
'<ph tag name="START_PARAGRAPH"><ph tag name="LINE_BREAK"/></ph name="CLOSE_PARAGRAPH">'
@ -55,25 +56,27 @@ export function main() {
describe('attributes', () => {
it('should extract from attributes outside of translatable section', () => {
expect(extract('<div i18n-title="m|d" title="msg"></div>')).toEqual([
expect(_humanizeMessages('<div i18n-title="m|d" title="msg"></div>')).toEqual([
[['msg'], 'm', 'd'],
]);
});
it('should extract from attributes in translatable element', () => {
expect(extract('<div i18n><p><b i18n-title="m|d" title="msg"></b></p></div>')).toEqual([
[
[
'<ph tag name="START_PARAGRAPH"><ph tag name="START_BOLD_TEXT"></ph name="CLOSE_BOLD_TEXT"></ph name="CLOSE_PARAGRAPH">'
],
'', ''
],
[['msg'], 'm', 'd'],
]);
expect(_humanizeMessages('<div i18n><p><b i18n-title="m|d" title="msg"></b></p></div>'))
.toEqual([
[
[
'<ph tag name="START_PARAGRAPH"><ph tag name="START_BOLD_TEXT"></ph name="CLOSE_BOLD_TEXT"></ph name="CLOSE_PARAGRAPH">'
],
'', ''
],
[['msg'], 'm', 'd'],
]);
});
it('should extract from attributes in translatable block', () => {
expect(extract('<!-- i18n --><p><b i18n-title="m|d" title="msg"></b></p><!-- /i18n -->'))
expect(_humanizeMessages(
'<!-- i18n --><p><b i18n-title="m|d" title="msg"></b></p><!-- /i18n -->'))
.toEqual([
[['msg'], 'm', 'd'],
[
@ -87,7 +90,7 @@ export function main() {
it('should extract from attributes in translatable ICU', () => {
expect(
extract(
_humanizeMessages(
'<!-- i18n -->{count, plural, =0 {<p><b i18n-title="m|d" title="msg"></b></p>}}<!-- /i18n -->'))
.toEqual([
[['msg'], 'm', 'd'],
@ -101,33 +104,35 @@ export function main() {
});
it('should extract from attributes in non translatable ICU', () => {
expect(extract('{count, plural, =0 {<p><b i18n-title="m|d" title="msg"></b></p>}}'))
expect(
_humanizeMessages('{count, plural, =0 {<p><b i18n-title="m|d" title="msg"></b></p>}}'))
.toEqual([
[['msg'], 'm', 'd'],
]);
});
it('should not create a message for empty attributes',
() => { expect(extract('<div i18n-title="m|d" title></div>')).toEqual([]); });
() => { expect(_humanizeMessages('<div i18n-title="m|d" title></div>')).toEqual([]); });
});
describe('interpolation', () => {
it('should replace interpolation with placeholder', () => {
expect(extract('<div i18n="m|d">before{{ exp }}after</div>')).toEqual([
expect(_humanizeMessages('<div i18n="m|d">before{{ exp }}after</div>')).toEqual([
[['[before, <ph name="INTERPOLATION"> exp </ph>, after]'], 'm', 'd'],
]);
});
it('should support named interpolation', () => {
expect(extract('<div i18n="m|d">before{{ exp //i18n(ph="teSt") }}after</div>')).toEqual([
[['[before, <ph name="TEST"> exp //i18n(ph="teSt") </ph>, after]'], 'm', 'd'],
]);
})
expect(_humanizeMessages('<div i18n="m|d">before{{ exp //i18n(ph="teSt") }}after</div>'))
.toEqual([
[['[before, <ph name="TEST"> exp //i18n(ph="teSt") </ph>, after]'], 'm', 'd'],
]);
});
});
describe('blocks', () => {
it('should extract from blocks', () => {
expect(extract(`<!-- i18n: meaning1|desc1 -->message1<!-- /i18n -->
expect(_humanizeMessages(`<!-- i18n: meaning1|desc1 -->message1<!-- /i18n -->
<!-- i18n: meaning2 -->message2<!-- /i18n -->
<!-- i18n -->message3<!-- /i18n -->`))
.toEqual([
@ -138,7 +143,7 @@ export function main() {
});
it('should extract all siblings', () => {
expect(extract(`<!-- i18n -->text<p>html<b>nested</b></p><!-- /i18n -->`)).toEqual([
expect(_humanizeMessages(`<!-- i18n -->text<p>html<b>nested</b></p><!-- /i18n -->`)).toEqual([
[
[
'text',
@ -152,33 +157,36 @@ export function main() {
describe('ICU messages', () => {
it('should extract as ICU when single child of an element', () => {
expect(extract('<div i18n="m|d">{count, plural, =0 {zero}}</div>')).toEqual([
expect(_humanizeMessages('<div i18n="m|d">{count, plural, =0 {zero}}</div>')).toEqual([
[['{count, plural, =0 {[zero]}}'], 'm', 'd'],
]);
});
it('should extract as ICU + ph when not single child of an element', () => {
expect(extract('<div i18n="m|d">b{count, plural, =0 {zero}}a</div>')).toEqual([
expect(_humanizeMessages('<div i18n="m|d">b{count, plural, =0 {zero}}a</div>')).toEqual([
[['b', '<ph icu name="ICU">{count, plural, =0 {[zero]}}</ph>', 'a'], 'm', 'd'],
[['{count, plural, =0 {[zero]}}'], '', ''],
]);
});
it('should extract as ICU when single child of a block', () => {
expect(extract('<!-- i18n:m|d -->{count, plural, =0 {zero}}<!-- /i18n -->')).toEqual([
[['{count, plural, =0 {[zero]}}'], 'm', 'd'],
]);
expect(_humanizeMessages('<!-- i18n:m|d -->{count, plural, =0 {zero}}<!-- /i18n -->'))
.toEqual([
[['{count, plural, =0 {[zero]}}'], 'm', 'd'],
]);
});
it('should extract as ICU + ph when not single child of a block', () => {
expect(extract('<!-- i18n:m|d -->b{count, plural, =0 {zero}}a<!-- /i18n -->')).toEqual([
[['{count, plural, =0 {[zero]}}'], '', ''],
[['b', '<ph icu name="ICU">{count, plural, =0 {[zero]}}</ph>', 'a'], 'm', 'd'],
]);
expect(_humanizeMessages('<!-- i18n:m|d -->b{count, plural, =0 {zero}}a<!-- /i18n -->'))
.toEqual([
[['{count, plural, =0 {[zero]}}'], '', ''],
[['b', '<ph icu name="ICU">{count, plural, =0 {[zero]}}</ph>', 'a'], 'm', 'd'],
]);
});
it('should not extract nested ICU messages', () => {
expect(extract('<div i18n="m|d">b{count, plural, =0 {{sex, gender, =m {m}}}}a</div>'))
expect(_humanizeMessages(
'<div i18n="m|d">b{count, plural, =0 {{sex, gender, =m {m}}}}a</div>'))
.toEqual([
[
[
@ -194,7 +202,7 @@ export function main() {
describe('implicit elements', () => {
it('should extract from implicit elements', () => {
expect(extract('<b>bold</b><i>italic</i>', ['b'])).toEqual([
expect(_humanizeMessages('<b>bold</b><i>italic</i>', ['b'])).toEqual([
[['bold'], '', ''],
]);
});
@ -202,7 +210,8 @@ export function main() {
describe('implicit attributes', () => {
it('should extract implicit attributes', () => {
expect(extract('<b title="bb">bold</b><i title="ii">italic</i>', [], {'b': ['title']}))
expect(_humanizeMessages(
'<b title="bb">bold</b><i title="ii">italic</i>', [], {'b': ['title']}))
.toEqual([
[['bb'], '', ''],
]);
@ -211,7 +220,8 @@ export function main() {
describe('placeholders', () => {
it('should reuse the same placeholder name for tags', () => {
expect(extract('<div i18n="m|d"><p>one</p><p>two</p><p other>three</p></div>')).toEqual([
const html = '<div i18n="m|d"><p>one</p><p>two</p><p other>three</p></div>';
expect(_humanizeMessages(html)).toEqual([
[
[
'<ph tag name="START_PARAGRAPH">one</ph name="CLOSE_PARAGRAPH">',
@ -221,10 +231,16 @@ export function main() {
'm', 'd'
],
]);
expect(_humanizePlaceholders(html)).toEqual([
'START_PARAGRAPH=<p>, CLOSE_PARAGRAPH=</p>, START_PARAGRAPH_1=<p other>',
]);
});
it('should reuse the same placeholder name for interpolations', () => {
expect(extract('<div i18n="m|d">{{ a }}{{ a }}{{ b }}</div>')).toEqual([
const html = '<div i18n="m|d">{{ a }}{{ a }}{{ b }}</div>';
expect(_humanizeMessages(html)).toEqual([
[
[
'[<ph name="INTERPOLATION"> a </ph>, <ph name="INTERPOLATION"> a </ph>, <ph name="INTERPOLATION_1"> b </ph>]'
@ -232,46 +248,70 @@ export function main() {
'm', 'd'
],
]);
expect(_humanizePlaceholders(html)).toEqual([
'INTERPOLATION={{ a }}, INTERPOLATION_1={{ b }}',
]);
});
it('should reuse the same placeholder name for icu messages', () => {
expect(
extract(
'<div i18n="m|d">{count, plural, =0 {0}}{count, plural, =0 {0}}{count, plural, =1 {1}}</div>'))
.toEqual([
[
[
'<ph icu name="ICU">{count, plural, =0 {[0]}}</ph>',
'<ph icu name="ICU">{count, plural, =0 {[0]}}</ph>',
'<ph icu name="ICU_1">{count, plural, =1 {[1]}}</ph>',
],
'm', 'd'
],
[['{count, plural, =0 {[0]}}'], '', ''],
[['{count, plural, =0 {[0]}}'], '', ''],
[['{count, plural, =1 {[1]}}'], '', ''],
]);
});
const html =
'<div i18n="m|d">{count, plural, =0 {0}}{count, plural, =0 {0}}{count, plural, =1 {1}}</div>';
expect(_humanizeMessages(html)).toEqual([
[
[
'<ph icu name="ICU">{count, plural, =0 {[0]}}</ph>',
'<ph icu name="ICU">{count, plural, =0 {[0]}}</ph>',
'<ph icu name="ICU_1">{count, plural, =1 {[1]}}</ph>',
],
'm', 'd'
],
[['{count, plural, =0 {[0]}}'], '', ''],
[['{count, plural, =0 {[0]}}'], '', ''],
[['{count, plural, =1 {[1]}}'], '', ''],
]);
expect(_humanizePlaceholders(html)).toEqual([
'ICU={count, plural, =0 {0}}, ICU_1={count, plural, =1 {1}}',
'',
'',
'',
]);
});
});
});
}
function extract(
function _humanizeMessages(
html: string, implicitTags: string[] = [],
implicitAttrs: {[k: string]: string[]} = {}): [string[], string, string][] {
// clang-format off
// https://github.com/angular/clang-format/issues/35
return _extractMessages(html, implicitTags, implicitAttrs).map(
message => [serializeAst(message.nodes), message.meaning, message.description, ]) as [string[], string, string][];
// clang-format on
}
function _humanizePlaceholders(
html: string, implicitTags: string[] = [],
implicitAttrs: {[k: string]: string[]} = {}): string[] {
// clang-format off
// https://github.com/angular/clang-format/issues/35
return _extractMessages(html, implicitTags, implicitAttrs).map(
msg => Object.getOwnPropertyNames(msg.placeholders).map((name) => `${name}=${msg.placeholders[name]}`).join(', '));
// clang-format on
}
function _extractMessages(
html: string, implicitTags: string[] = [],
implicitAttrs: {[k: string]: string[]} = {}): Message[] {
const htmlParser = new HtmlParser();
const parseResult = htmlParser.parse(html, 'extractor spec', true);
if (parseResult.errors.length > 1) {
throw Error(`unexpected parse errors: ${parseResult.errors.join('\n')}`);
}
const messages = extractI18nMessages(
return extractI18nMessages(
parseResult.rootNodes, DEFAULT_INTERPOLATION_CONFIG, implicitTags, implicitAttrs);
// clang-format off
// https://github.com/angular/clang-format/issues/35
return messages.map(
message => [serializeAst(message.nodes), message.meaning, message.description, ]) as [string[], string, string][];
// clang-format on
}

View File

@ -6,49 +6,40 @@
* found in the LICENSE file at https://angular.io/license
*/
import {Catalog, strHash} from '@angular/compiler/src/i18n/catalog';
import * as i18n from '@angular/compiler/src/i18n/i18n_ast';
import {Serializer} from '@angular/compiler/src/i18n/serializers/serializer';
import {beforeEach, ddescribe, describe, expect, iit, inject, it, xdescribe, xit} from '@angular/core/testing/testing_internal';
import {HtmlParser} from '../../src/html_parser/html_parser';
import {DEFAULT_INTERPOLATION_CONFIG} from '../../src/html_parser/interpolation_config';
import Serializable = webdriver.Serializable;
import {Serializer} from '@angular/compiler/src/i18n/serializers/serializer';
import {serializeAst} from '@angular/compiler/src/i18n/catalog';
import * as i18nAst from '@angular/compiler/src/i18n/i18n_ast';
import {MessageBundle, serializeAst, strHash} from '../../src/i18n/message_bundle';
export function main(): void {
ddescribe('Catalog', () => {
describe('MessageBundle', () => {
describe('Messages', () => {
let messages: MessageBundle;
describe('write', () => {
let catalog: Catalog;
beforeEach(() => { catalog = new Catalog(new HtmlParser, [], {}); });
beforeEach(() => { messages = new MessageBundle(new HtmlParser, [], {}); });
it('should extract the message to the catalog', () => {
catalog.updateFromTemplate(
messages.updateFromTemplate(
'<p i18n="m|d">Translate Me</p>', 'url', DEFAULT_INTERPOLATION_CONFIG);
expect(humanizeCatalog(catalog)).toEqual([
expect(humanizeMessages(messages)).toEqual([
'a486901=Translate Me',
]);
});
it('should extract the same message with different meaning in different entries', () => {
catalog.updateFromTemplate(
messages.updateFromTemplate(
'<p i18n="m|d">Translate Me</p><p i18n>Translate Me</p>', 'url',
DEFAULT_INTERPOLATION_CONFIG);
expect(humanizeCatalog(catalog)).toEqual([
expect(humanizeMessages(messages)).toEqual([
'a486901=Translate Me',
'8475f2cc=Translate Me',
]);
});
});
describe(
'load', () => {
// TODO
});
describe('strHash', () => {
it('should return a hash value', () => {
// https://github.com/google/closure-library/blob/1fb19a857b96b74e6523f3e9d33080baf25be046/closure/goog/string/string_test.js#L1115
@ -66,16 +57,16 @@ export function main(): void {
}
class _TestSerializer implements Serializer {
write(messageMap: {[k: string]: i18nAst.Message}): string {
write(messageMap: {[id: string]: i18n.Message}): string {
return Object.keys(messageMap)
.map(id => `${id}=${serializeAst(messageMap[id].nodes)}`)
.join('//');
}
load(content: string): {[k: string]: i18nAst.Node[]} { return null; }
load(content: string, url: string, placeholders: {}): {} { return null; }
}
function humanizeCatalog(catalog: Catalog): string[] {
function humanizeMessages(catalog: MessageBundle): string[] {
return catalog.write(new _TestSerializer()).split('//');
}

View File

@ -1,275 +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 {Lexer as ExpressionLexer} from '@angular/compiler/src/expression_parser/lexer';
import {Parser as ExpressionParser} from '@angular/compiler/src/expression_parser/parser';
import {Message} from '@angular/compiler/src/i18n/message';
import {MessageExtractor, removeDuplicates} from '@angular/compiler/src/i18n/message_extractor';
import {beforeEach, ddescribe, describe, expect, iit, inject, it, xdescribe, xit} from '@angular/core/testing/testing_internal';
import {HtmlParser} from '../../src/html_parser/html_parser';
export function main() {
describe('MessageExtractor', () => {
let extractor: MessageExtractor;
beforeEach(() => {
const expParser = new ExpressionParser(new ExpressionLexer());
const htmlParser = new HtmlParser();
extractor = new MessageExtractor(htmlParser, expParser, ['i18n-tag'], {'i18n-el': ['trans']});
});
it('should extract from partitions', () => {
let res = extractor.extract(
`
<!-- i18n: meaning1|desc1 -->message1<!-- /i18n -->
<!-- i18n: meaning2 -->message2<!-- /i18n -->
<!-- i18n -->message3<!-- /i18n -->`,
'someUrl');
expect(res.messages).toEqual([
new Message('message1', 'meaning1', 'desc1'),
new Message('message2', 'meaning2'),
new Message('message3', null),
]);
});
it('should ignore other comments', () => {
let res = extractor.extract(
`
<!-- i18n: meaning1|desc1 --><!-- other -->message1<!-- /i18n -->`,
'someUrl');
expect(res.messages).toEqual([new Message('message1', 'meaning1', 'desc1')]);
});
describe('ICU messages', () => {
it('should replace icu messages with placeholders', () => {
let res = extractor.extract('<div i18n>{count, plural, =0 {text} }</div>', 'someurl');
expect(res.messages).toEqual([new Message(
'<ph name="x0">{count, plural =0 {text}}</ph>', null, null)]);
});
it('should replace HTML with placeholders in ICU cases', () => {
let res =
extractor.extract('<div i18n>{count, plural, =0 {<p>html</p>} }</div>', 'someurl');
expect(res.messages).toEqual([new Message(
'<ph name="x0">{count, plural =0 {<ph name="e1">html</ph>}}</ph>', null, null)]);
});
it('should replace interpolation with placeholders in ICU cases', () => {
let res =
extractor.extract('<div i18n>{count, plural, =0 {{{interpolation}}}}</div>', 'someurl');
expect(res.messages).toEqual([new Message(
'<ph name="x0">{count, plural =0 {<ph name="t1"><ph name="INTERPOLATION_0"/></ph>}}</ph>',
null, null)]);
});
it('should not replace nested interpolation with placeholders in ICU cases', () => {
let res = extractor.extract(
'<div i18n>{count, plural, =0 {{sex, gender, =m {{{he}}} =f {<b>she</b>}}}}</div>',
'someurl');
expect(res.messages).toEqual([new Message(
'<ph name="x0">{count, plural =0 {{sex, gender =m {<ph name="t2"><ph name="INTERPOLATION_0"/></ph>} =f {<ph name="e3">she</ph>}}}}</ph>',
null, null)]);
});
});
describe('interpolation', () => {
it('should replace interpolation with placeholders (text nodes)', () => {
let res = extractor.extract('<div i18n>Hi {{one}} and {{two}}</div>', 'someurl');
expect(res.messages).toEqual([new Message(
'<ph name="t0">Hi <ph name="INTERPOLATION_0"/> and <ph name="INTERPOLATION_1"/></ph>',
null, null)]);
});
it('should replace interpolation with placeholders (attributes)', () => {
let res =
extractor.extract('<div title=\'Hi {{one}} and {{two}}\' i18n-title></div>', 'someurl');
expect(res.messages).toEqual([new Message(
'Hi <ph name="INTERPOLATION_0"/> and <ph name="INTERPOLATION_1"/>', null, null)]);
});
it('should replace interpolation with named placeholders if provided (text nodes)', () => {
let res = extractor.extract(
`
<div i18n>Hi {{one //i18n(ph="FIRST")}} and {{two //i18n(ph="SECOND")}}</div>`,
'someurl');
expect(res.messages).toEqual([new Message(
'<ph name="t0">Hi <ph name="FIRST"/> and <ph name="SECOND"/></ph>', null, null)]);
});
it('should replace interpolation with named placeholders if provided (attributes)', () => {
let res = extractor.extract(
`
<div title='Hi {{one //i18n(ph="FIRST")}} and {{two //i18n(ph="SECOND")}}'
i18n-title></div>`,
'someurl');
expect(res.messages).toEqual([new Message(
'Hi <ph name="FIRST"/> and <ph name="SECOND"/>', null, null)]);
});
});
describe('placehoders', () => {
it('should match named placeholders with extra spacing', () => {
let res = extractor.extract(
`
<div title='Hi {{one // i18n ( ph = "FIRST" )}} and {{two // i18n ( ph = "SECOND" )}}'
i18n-title></div>`,
'someurl');
expect(res.messages).toEqual([new Message(
'Hi <ph name="FIRST"/> and <ph name="SECOND"/>', null, null)]);
});
it('should suffix duplicate placeholder names with numbers', () => {
let res = extractor.extract(
`
<div title='Hi {{one //i18n(ph="FIRST")}} and {{two //i18n(ph="FIRST")}} and {{three //i18n(ph="FIRST")}}'
i18n-title></div>`,
'someurl');
expect(res.messages).toEqual([new Message(
'Hi <ph name="FIRST"/> and <ph name="FIRST_1"/> and <ph name="FIRST_2"/>', null,
null)]);
});
});
describe('html', () => {
it('should extract from elements with the i18n attr', () => {
let res = extractor.extract('<div i18n=\'meaning|desc\'>message</div>', 'someurl');
expect(res.messages).toEqual([new Message('message', 'meaning', 'desc')]);
});
it('should extract from elements with the i18n attr without a desc', () => {
let res = extractor.extract('<div i18n=\'meaning\'>message</div>', 'someurl');
expect(res.messages).toEqual([new Message('message', 'meaning', null)]);
});
it('should extract from elements with the i18n attr without a meaning', () => {
let res = extractor.extract('<div i18n>message</div>', 'someurl');
expect(res.messages).toEqual([new Message('message', null, null)]);
});
it('should extract from attributes', () => {
let res = extractor.extract(
`
<div
title1='message1' i18n-title1='meaning1|desc1'
title2='message2' i18n-title2='meaning2|desc2'>
</div>
`,
'someurl');
expect(res.messages).toEqual([
new Message('message1', 'meaning1', 'desc1'), new Message('message2', 'meaning2', 'desc2')
]);
});
it('should handle html content', () => {
let res = extractor.extract(
'<div i18n><div attr="value">zero<div>one</div></div><div>two</div></div>', 'someurl');
expect(res.messages).toEqual([new Message(
'<ph name="e0">zero<ph name="e2">one</ph></ph><ph name="e4">two</ph>', null, null)]);
});
it('should handle html content with interpolation', () => {
let res =
extractor.extract('<div i18n><div>zero{{a}}<div>{{b}}</div></div></div>', 'someurl');
expect(res.messages).toEqual([new Message(
'<ph name="e0"><ph name="t1">zero<ph name="INTERPOLATION_0"/></ph><ph name="e2"><ph name="t3"><ph name="INTERPOLATION_0"/></ph></ph></ph>',
null, null)]);
});
it('should extract from nested elements', () => {
let res = extractor.extract(
'<div title="message1" i18n-title="meaning1|desc1"><div i18n="meaning2|desc2">message2</div></div>',
'someurl');
expect(res.messages).toEqual([
new Message('message2', 'meaning2', 'desc2'), new Message('message1', 'meaning1', 'desc1')
]);
});
it('should extract messages from attributes in i18n blocks', () => {
let res = extractor.extract(
'<div i18n><div attr="value" i18n-attr="meaning|desc">message</div></div>', 'someurl');
expect(res.messages).toEqual([
new Message('<ph name="e0">message</ph>', null, null),
new Message('value', 'meaning', 'desc')
]);
});
});
it('should remove duplicate messages', () => {
let res = extractor.extract(
`
<!-- i18n: meaning|desc1 -->message<!-- /i18n -->
<!-- i18n: meaning|desc2 -->message<!-- /i18n -->`,
'someUrl');
expect(removeDuplicates(res.messages)).toEqual([
new Message('message', 'meaning', 'desc1'),
]);
});
describe('implicit translation', () => {
it('should extract from elements', () => {
let res = extractor.extract('<i18n-tag>message</i18n-tag>', 'someurl');
expect(res.messages).toEqual([new Message('message', null, null)]);
});
it('should extract meaning and description from elements when present', () => {
let res = extractor.extract(
'<i18n-tag i18n=\'meaning|description\'>message</i18n-tag>', 'someurl');
expect(res.messages).toEqual([new Message('message', 'meaning', 'description')]);
});
it('should extract from attributes', () => {
let res = extractor.extract(`<i18n-el trans='message'></i18n-el>`, 'someurl');
expect(res.messages).toEqual([new Message('message', null, null)]);
});
it('should extract meaning and description from attributes when present', () => {
let res = extractor.extract(
`<i18n-el trans='message' i18n-trans="meaning|desc"></i18n-el>`, 'someurl');
expect(res.messages).toEqual([new Message('message', 'meaning', 'desc')]);
});
});
describe('errors', () => {
it('should error on i18n attributes without matching "real" attributes', () => {
let res = extractor.extract(
`
<div
title1='message1' i18n-title1='meaning1|desc1' i18n-title2='meaning2|desc2'>
</div>`,
'someurl');
expect(res.errors.length).toEqual(1);
expect(res.errors[0].msg).toEqual('Missing attribute \'title2\'.');
});
it('should error when i18n comments are unbalanced', () => {
const res = extractor.extract('<!-- i18n -->message1', 'someUrl');
expect(res.errors.length).toEqual(1);
expect(res.errors[0].msg).toEqual('Missing closing \'i18n\' comment.');
});
it('should error when i18n comments are unbalanced', () => {
const res = extractor.extract('<!-- i18n -->', 'someUrl');
expect(res.errors.length).toEqual(1);
expect(res.errors[0].msg).toEqual('Missing closing \'i18n\' comment.');
});
it('should return parse errors when the template is invalid', () => {
let res = extractor.extract('<input&#Besfs', 'someurl');
expect(res.errors.length).toEqual(1);
expect(res.errors[0].msg).toEqual('Unexpected character "s"');
});
});
});
}

View File

@ -1,23 +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 {Message, id} from '@angular/compiler/src/i18n/message';
import {beforeEach, ddescribe, describe, expect, iit, inject, it, xdescribe, xit} from '@angular/core/testing/testing_internal';
export function main() {
describe('Message', () => {
describe('id', () => {
it('should return a different id for messages with and without the meaning', () => {
let m1 = new Message('content', 'meaning', null);
let m2 = new Message('content', null, null);
expect(id(m1)).toEqual(id(m1));
expect(id(m1)).not.toEqual(id(m2));
});
});
});
}

View File

@ -6,14 +6,12 @@
* found in the LICENSE file at https://angular.io/license
*/
import {beforeEach, ddescribe, describe, expect, iit, inject, it, xdescribe, xit} from '@angular/core/testing/testing_internal';
import {PlaceholderRegistry} from '../../../src/i18n/serializers/util';
import {PlaceholderRegistry} from '../../../src/i18n/serializers/placeholder';
export function main(): void {
ddescribe('PlaceholderRegistry', () => {
describe('PlaceholderRegistry', () => {
let reg: PlaceholderRegistry;
beforeEach(() => { reg = new PlaceholderRegistry(); });
@ -34,11 +32,11 @@ export function main(): void {
expect(reg.getStartTagPlaceholderName('p', {}, false)).toEqual('START_PARAGRAPH');
});
it('should be case insensitive for tag name', () => {
it('should be case sensitive for tag name', () => {
expect(reg.getStartTagPlaceholderName('p', {}, false)).toEqual('START_PARAGRAPH');
expect(reg.getStartTagPlaceholderName('P', {}, false)).toEqual('START_PARAGRAPH');
expect(reg.getStartTagPlaceholderName('P', {}, false)).toEqual('START_PARAGRAPH_1');
expect(reg.getCloseTagPlaceholderName('p')).toEqual('CLOSE_PARAGRAPH');
expect(reg.getCloseTagPlaceholderName('P')).toEqual('CLOSE_PARAGRAPH');
expect(reg.getCloseTagPlaceholderName('P')).toEqual('CLOSE_PARAGRAPH_1');
});
it('should generate the same name for the same tag with the same attributes', () => {

View File

@ -6,16 +6,15 @@
* found in the LICENSE file at https://angular.io/license
*/
import {Catalog} from '@angular/compiler/src/i18n/catalog';
import {XmbSerializer} from '@angular/compiler/src/i18n/serializers/xmb';
import {Xmb} from '@angular/compiler/src/i18n/serializers/xmb';
import {beforeEach, ddescribe, describe, expect, iit, inject, it, xdescribe, xit} from '@angular/core/testing/testing_internal';
import {HtmlParser} from '../../../src/html_parser/html_parser';
import {DEFAULT_INTERPOLATION_CONFIG} from '../../../src/html_parser/interpolation_config';
import {MessageBundle} from '../../../src/i18n/message_bundle';
export function main(): void {
ddescribe('XMB serializer', () => {
describe('XMB serializer', () => {
const HTML = `
<p>not translatable</p>
<p i18n>translatable element <b>with placeholders</b> {{ interpolation}}</p>
@ -35,35 +34,18 @@ export function main(): void {
it('should throw when trying to load an xmb file', () => {
expect(() => {
const serializer = new XmbSerializer();
serializer.load(XMB);
const serializer = new Xmb();
serializer.load(XMB, 'url', {});
}).toThrow();
});
});
}
function toXmb(html: string): string {
let catalog = new Catalog(new HtmlParser, [], {});
const serializer = new XmbSerializer();
let catalog = new MessageBundle(new HtmlParser, [], {});
const serializer = new Xmb();
catalog.updateFromTemplate(html, '', DEFAULT_INTERPOLATION_CONFIG);
return catalog.write(serializer);
}
// <? xml version="1.0" encoding="UTF-8" ?><messagebundle><message id="834fa53b">translatable
// element <ph name="START_BOLD_TEXT"><ex>&lt;b&gt;</ex></ph>with placeholders<ph
// name="CLOSE_BOLD_TEXT"><ex>&lt;/b&gt;</ex></ph> <ph name="INTERPOLATION"/></message><message
// id="7a2843db">{ count, plural, =0 {<ph name="START_PARAGRAPH"><ex>&lt;p&gt;</ex></ph>test<ph
// name="CLOSE_PARAGRAPH"><ex>&lt;/p&gt;</ex></ph>}}</message><message id="b45e58a5" description="d"
// meaning="m">foo</message><message id="18ea85bc">{ count, plural, =0 {{ sex, gender, other {<ph
// name="START_PARAGRAPH"><ex>&lt;p&gt;</ex></ph>deeply nested<ph
// name="CLOSE_PARAGRAPH"><ex>&lt;/p&gt;</ex></ph>}} }}</message></messagebundle>
// <? xml version="1.0" encoding="UTF-8" ?><messagebundle><message id="834fa53b">translatable
// element <ph name="START_BOLD_TEXT"><ex>&lt;b&gt;</ex></ph>with placeholders<ph
// name="CLOSE_BOLD_TEXT"><ex>&lt;/b&gt;</ex></ph> <ph name="INTERPOLATION"/></message><message
// id="7a2843db">{ count, plural, =0 {<ph name="START_PARAGRAPH"><ex>&lt;p&gt;</ex></ph>test<ph
// name="CLOSE_PARAGRAPH"><ex>&lt;/p&gt;</ex></ph>}}</message><message id="18ea85bc">{ count,
// plural, =0 {{ sex, gender, other {<ph name="START_PARAGRAPH"><ex>&lt;p&gt;</ex></ph>deeply
// nested<ph name="CLOSE_PARAGRAPH"><ex>&lt;/p&gt;</ex></ph>}} }}</message><message id="b45e58a5"
// description="d" meaning="m">foo</message></messagebundle>
}

View File

@ -11,7 +11,7 @@ import {beforeEach, ddescribe, describe, expect, iit, inject, it, xdescribe, xit
import * as xml from '../../../src/i18n/serializers/xml_helper';
export function main(): void {
ddescribe('XML helper', () => {
describe('XML helper', () => {
it('should serialize XML declaration', () => {
expect(xml.serialize([new xml.Declaration({version: '1.0'})]))
.toEqual('<? xml version="1.0" ?>');

View File

@ -0,0 +1,171 @@
/**
* @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 {Xtb} from '@angular/compiler/src/i18n/serializers/xtb';
import {escapeRegExp} from '@angular/core/src/facade/lang';
import {beforeEach, ddescribe, describe, expect, iit, inject, it, xdescribe, xit} from '@angular/core/testing/testing_internal';
import {HtmlParser} from '../../../src/html_parser/html_parser';
import {DEFAULT_INTERPOLATION_CONFIG} from '../../../src/html_parser/interpolation_config';
import {serializeAst} from '../../html_parser/ast_serializer_spec';
export function main(): void {
describe('XTB serializer', () => {
let serializer: Xtb;
function loadAsText(content: string, placeholders: {[id: string]: {[name: string]: string}}):
{[id: string]: string} {
const asAst = serializer.load(content, 'url', placeholders);
let asText: {[id: string]: string} = {};
Object.getOwnPropertyNames(asAst).forEach(
id => { asText[id] = serializeAst(asAst[id]).join(''); });
return asText;
}
beforeEach(() => { serializer = new Xtb(new HtmlParser(), DEFAULT_INTERPOLATION_CONFIG); });
describe('load', () => {
it('should load XTB files without placeholders', () => {
const XTB = `
<?xml version="1.0" encoding="UTF-8"?>
<translationbundle>
<translation id="foo">bar</translation>
</translationbundle>`;
expect(loadAsText(XTB, {})).toEqual({foo: 'bar'});
});
it('should load XTB files with placeholders', () => {
const XTB = `
<?xml version="1.0" encoding="UTF-8"?>
<translationbundle>
<translation id="foo">bar<ph name="PLACEHOLDER"/><ph name="PLACEHOLDER"/></translation>
</translationbundle>`;
expect(loadAsText(XTB, {foo: {PLACEHOLDER: '!'}})).toEqual({foo: 'bar!!'});
});
it('should load complex XTB files', () => {
const XTB = `
<? xml version="1.0" encoding="UTF-8" ?>
<translationbundle>
<translation id="a">translatable element <ph name="START_BOLD_TEXT"><ex>&lt;b&gt;</ex></ph>with placeholders<ph name="CLOSE_BOLD_TEXT"><ex>&lt;/b&gt;</ex></ph> <ph name="INTERPOLATION"/></translation>
<translation id="b">{ count, plural, =0 {<ph name="START_PARAGRAPH"><ex>&lt;p&gt;</ex></ph>test<ph name="CLOSE_PARAGRAPH"><ex>&lt;/p&gt;</ex></ph>}}</translation>
<translation id="c" desc="d" meaning="m">foo</translation>
<translation id="d">{ count, plural, =0 {{ sex, gender, other {<ph name="START_PARAGRAPH"><ex>&lt;p&gt;</ex></ph>deeply nested<ph name="CLOSE_PARAGRAPH"><ex>&lt;/p&gt;</ex></ph>}} }}</translation>
</translationbundle>`;
const PLACEHOLDERS = {
a: {
START_BOLD_TEXT: '<b>',
CLOSE_BOLD_TEXT: '</b>',
INTERPOLATION: '{{ a + b }}',
},
b: {
START_PARAGRAPH: '<p translated=true>',
CLOSE_PARAGRAPH: '</p>',
},
d: {
START_PARAGRAPH: '<p>',
CLOSE_PARAGRAPH: '</p>',
},
};
expect(loadAsText(XTB, PLACEHOLDERS)).toEqual({
a: 'translatable element <b>with placeholders</b> {{ a + b }}',
b: '{ count, plural, =0 {<p translated="true">test</p>}}',
c: 'foo',
d: '{ count, plural, =0 {{ sex, gender, other {<p>deeply nested</p>}} }}',
});
});
});
describe('errors', () => {
it('should throw on nested <translationbundle>', () => {
const XTB =
'<translationbundle><translationbundle></translationbundle></translationbundle>';
expect(() => {
serializer.load(XTB, 'url', {});
}).toThrowError(/<translationbundle> elements can not be nested/);
});
it('should throw on nested <translation>', () => {
const XTB = `
<translationbundle>
<translation id="outer">
<translation id="inner">
</translation>
</translation>
</translationbundle>`;
expect(() => {
serializer.load(XTB, 'url', {});
}).toThrowError(/<translation> elements can not be nested/);
});
it('should throw when a <translation> has no id attribute', () => {
const XTB = `
<translationbundle>
<translation></translation>
</translationbundle>`;
expect(() => {
serializer.load(XTB, 'url', {});
}).toThrowError(/<translation> misses the "id" attribute/);
});
it('should throw when a placeholder has no name attribute', () => {
const XTB = `
<translationbundle>
<translation id="fail"><ph /></translation>
</translationbundle>`;
expect(() => {
serializer.load(XTB, 'url', {});
}).toThrowError(/<ph> misses the "name" attribute/);
});
it('should throw when a placeholder is not present in the source message', () => {
const XTB = `
<translationbundle>
<translation id="fail"><ph name="UNKNOWN"/></translation>
</translationbundle>`;
expect(() => {
serializer.load(XTB, 'url', {});
}).toThrowError(/The placeholder "UNKNOWN" does not exists in the source message/);
});
});
it('should throw when the translation results in invalid html', () => {
const XTB = `
<translationbundle>
<translation id="fail">foo<ph name="CLOSE_P"/>bar</translation>
</translationbundle>`;
expect(() => {
serializer.load(XTB, 'url', {fail: {CLOSE_P: '</p>'}});
}).toThrowError(/xtb parse errors:\nUnexpected closing tag "p"/);
});
it('should throw on unknown tags', () => {
const XTB = `<what></what>`;
expect(() => {
serializer.load(XTB, 'url', {});
}).toThrowError(new RegExp(escapeRegExp(`Unexpected tag ("[ERROR ->]<what></what>")`)));
});
it('should throw when trying to save an xmb file',
() => { expect(() => { serializer.write({}); }).toThrow(); });
});
}

View File

@ -1,121 +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 {Message, id} from '@angular/compiler/src/i18n/message';
import {deserializeXmb, serializeXmb} from '@angular/compiler/src/i18n/xmb_serializer';
import {ParseError, ParseSourceSpan} from '@angular/compiler/src/parse_util';
import {beforeEach, ddescribe, describe, expect, iit, inject, it, xdescribe, xit} from '@angular/core/testing/testing_internal';
import {HtmlAst} from '../../src/html_parser/html_ast';
export function main() {
describe('Xmb', () => {
describe('Xmb Serialization', () => {
it('should return an empty message bundle for an empty list of messages',
() => { expect(serializeXmb([])).toEqual('<message-bundle></message-bundle>'); });
it('should serialize messages without desc nor meaning', () => {
let m = new Message('content', null, null);
let expected = `<message-bundle><msg id='${id(m)}'>content</msg></message-bundle>`;
expect(serializeXmb([m])).toEqual(expected);
});
it('should serialize messages with desc and meaning', () => {
let m = new Message('content', 'meaning', 'description');
let expected =
`<message-bundle><msg id='${id(m)}' desc='description' meaning='meaning'>content</msg></message-bundle>`;
expect(serializeXmb([m])).toEqual(expected);
});
it('should escape the desc and meaning', () => {
let m = new Message('content', '"\'&<>"\'&<>', '"\'&<>"\'&<>');
let expected =
`<message-bundle><msg id='${id(m)}' desc='&quot;&apos;&amp;&lt;&gt;&quot;&apos;&amp;&lt;&gt;' meaning='&quot;&apos;&amp;&lt;&gt;&quot;&apos;&amp;&lt;&gt;'>content</msg></message-bundle>`;
expect(serializeXmb([m])).toEqual(expected);
});
});
describe('Xmb Deserialization', () => {
it('should parse an empty bundle', () => {
let mb = '<message-bundle></message-bundle>';
expect(deserializeXmb(mb, 'url').messages).toEqual({});
});
it('should parse an non-empty bundle', () => {
let mb = `
<message-bundle>
<msg id="id1" desc="description1">content1</msg>
<msg id="id2">content2</msg>
</message-bundle>
`;
let parsed = deserializeXmb(mb, 'url').messages;
expect(_serialize(parsed['id1'])).toEqual('content1');
expect(_serialize(parsed['id2'])).toEqual('content2');
});
it('should error when cannot parse the content', () => {
let mb = `
<message-bundle>
<msg id="id1" desc="description1">content
</message-bundle>
`;
let res = deserializeXmb(mb, 'url');
expect(_serializeErrors(res.errors)).toEqual(['Unexpected closing tag "message-bundle"']);
});
it('should error when cannot find the id attribute', () => {
let mb = `
<message-bundle>
<msg>content</msg>
</message-bundle>
`;
let res = deserializeXmb(mb, 'url');
expect(_serializeErrors(res.errors)).toEqual(['"id" attribute is missing']);
});
it('should error on empty content', () => {
let mb = ``;
let res = deserializeXmb(mb, 'url');
expect(_serializeErrors(res.errors)).toEqual(['Missing element "message-bundle"']);
});
it('should error on an invalid element', () => {
let mb = `
<message-bundle>
<invalid>content</invalid>
</message-bundle>
`;
let res = deserializeXmb(mb, 'url');
expect(_serializeErrors(res.errors)).toEqual(['Unexpected element "invalid"']);
});
it('should expand \'ph\' elements', () => {
let mb = `
<message-bundle>
<msg id="id1">a<ph name="i0"/></msg>
</message-bundle>
`;
let res = deserializeXmb(mb, 'url').messages['id1'];
expect((<any>res[1]).name).toEqual('ph');
});
});
});
}
function _serialize(nodes: HtmlAst[]): string {
return (<any>nodes[0]).value;
}
function _serializeErrors(errors: ParseError[]): string[] {
return errors.map(e => e.msg);
}

View File

@ -11,7 +11,7 @@ import {CUSTOM_ELEMENTS_SCHEMA, SecurityContext} from '@angular/core';
import {beforeEach, ddescribe, describe, expect, iit, inject, it, xdescribe, xit} from '@angular/core/testing/testing_internal';
import {browserDetection} from '@angular/platform-browser/testing/browser_util';
import {HtmlElementAst} from '../../src/html_parser/html_ast';
import {Element} from '../../src/html_parser/ast';
import {HtmlParser} from '../../src/html_parser/html_parser';
import {extractSchema} from './schema_extractor';
@ -78,7 +78,7 @@ export function main() {
it('should detect properties on namespaced elements', () => {
const htmlAst = new HtmlParser().parse('<svg:style>', 'TestComp');
const nodeName = (<HtmlElementAst>htmlAst.rootNodes[0]).name;
const nodeName = (<Element>htmlAst.rootNodes[0]).name;
expect(registry.hasProperty(nodeName, 'type', [])).toBeTruthy();
});

View File

@ -16,7 +16,6 @@ import {SchemaMetadata, SecurityContext} from '@angular/core';
import {Console} from '@angular/core/src/console';
import {TestBed} from '@angular/core/testing';
import {afterEach, beforeEach, beforeEachProviders, ddescribe, describe, expect, iit, inject, it, xit} from '@angular/core/testing/testing_internal';
import {Identifiers, identifierToken} from '../../src/identifiers';
import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from '../../src/html_parser/interpolation_config';

View File

@ -7,7 +7,7 @@
*/
import {afterEach, beforeEach, beforeEachProviders, ddescribe, describe, expect, iit, inject, it, xdescribe, xit} from '../../../core/testing/testing_internal';
import {HtmlElementAst} from '../../src/html_parser/html_ast';
import {Element} from '../../src/html_parser/ast';
import {HtmlParser} from '../../src/html_parser/html_parser';
import {PreparsedElement, PreparsedElementType, preparseElement} from '../../src/template_parser/template_preparser';
@ -17,7 +17,7 @@ export function main() {
beforeEach(inject([HtmlParser], (_htmlParser: HtmlParser) => { htmlParser = _htmlParser; }));
function preparse(html: string): PreparsedElement {
return preparseElement(htmlParser.parse(html, 'TestComp').rootNodes[0] as HtmlElementAst);
return preparseElement(htmlParser.parse(html, 'TestComp').rootNodes[0] as Element);
}
it('should detect script elements', inject([HtmlParser], (htmlParser: HtmlParser) => {