feat(i18n): xtb serializer
This commit is contained in:
parent
1b77604ee2
commit
0eee1d5de3
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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';
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -10,9 +10,9 @@ import {AUTO_STYLE} from '@angular/core';
|
|||
|
||||
import {ANY_STATE, DEFAULT_STATE, EMPTY_STATE} from '../../core_private';
|
||||
import {CompileDirectiveMetadata} from '../compile_metadata';
|
||||
import {ListWrapper, Map, StringMapWrapper} from '../facade/collection';
|
||||
import {StringMapWrapper} from '../facade/collection';
|
||||
import {BaseException} from '../facade/exceptions';
|
||||
import {isArray, isBlank, isPresent} from '../facade/lang';
|
||||
import {isBlank, isPresent} from '../facade/lang';
|
||||
import {Identifiers} from '../identifiers';
|
||||
import * as o from '../output/output_ast';
|
||||
import * as t from '../template_parser/template_ast';
|
||||
|
|
|
@ -8,10 +8,10 @@
|
|||
|
||||
import {ChangeDetectionStrategy, SchemaMetadata, ViewEncapsulation} from '@angular/core';
|
||||
|
||||
import {CHANGE_DETECTION_STRATEGY_VALUES, LIFECYCLE_HOOKS_VALUES, LifecycleHooks, VIEW_ENCAPSULATION_VALUES, reflector} from '../core_private';
|
||||
import {ListWrapper, StringMapWrapper} from '../src/facade/collection';
|
||||
import {BaseException, unimplemented} from '../src/facade/exceptions';
|
||||
import {NumberWrapper, RegExpWrapper, Type, isArray, isBlank, isBoolean, isNumber, isPresent, isString, isStringMap, normalizeBlank, normalizeBool, serializeEnum} from '../src/facade/lang';
|
||||
import {LifecycleHooks, reflector} from '../core_private';
|
||||
import {ListWrapper, StringMapWrapper} from './facade/collection';
|
||||
import {BaseException, unimplemented} from './facade/exceptions';
|
||||
import {RegExpWrapper, Type, isBlank, isPresent, isStringMap, normalizeBlank, normalizeBool} from './facade/lang';
|
||||
|
||||
import {CssSelector} from './selector';
|
||||
import {getUrlScheme} from './url_resolver';
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {Compiler, CompilerFactory, CompilerOptions, Component, ComponentResolver, Inject, Injectable, NgModule, PLATFORM_DIRECTIVES, PLATFORM_INITIALIZER, PLATFORM_PIPES, PlatformRef, ReflectiveInjector, Type, ViewEncapsulation, createPlatformFactory, disposePlatform, isDevMode, platformCore} from '@angular/core';
|
||||
import {Compiler, CompilerFactory, CompilerOptions, Component, Inject, Injectable, PLATFORM_DIRECTIVES, PLATFORM_INITIALIZER, PLATFORM_PIPES, PlatformRef, ReflectiveInjector, Type, ViewEncapsulation, createPlatformFactory, isDevMode, platformCore} from '@angular/core';
|
||||
|
||||
export * from './template_parser/template_ast';
|
||||
export {TEMPLATE_TRANSFORMS} from './template_parser/template_parser';
|
||||
|
|
|
@ -9,8 +9,8 @@
|
|||
import {OnInit, OnDestroy, DoCheck, OnChanges, AfterContentInit, AfterContentChecked, AfterViewInit, AfterViewChecked,} from '@angular/core';
|
||||
import {reflector, LifecycleHooks} from '../core_private';
|
||||
|
||||
import {Type} from '../src/facade/lang';
|
||||
import {MapWrapper} from '../src/facade/collection';
|
||||
import {Type} from './facade/lang';
|
||||
import {MapWrapper} from './facade/collection';
|
||||
|
||||
const LIFECYCLE_INTERFACES: Map<any, Type> = MapWrapper.createFromPairs([
|
||||
[LifecycleHooks.OnInit, OnInit],
|
||||
|
|
|
@ -7,12 +7,13 @@
|
|||
*/
|
||||
|
||||
import {Injectable, ViewEncapsulation} from '@angular/core';
|
||||
import {MapWrapper} from '../src/facade/collection';
|
||||
import {BaseException} from '../src/facade/exceptions';
|
||||
import {isBlank, isPresent} from '../src/facade/lang';
|
||||
|
||||
import {CompileDirectiveMetadata, CompileStylesheetMetadata, CompileTemplateMetadata, CompileTypeMetadata} from './compile_metadata';
|
||||
import {CompilerConfig} from './config';
|
||||
import {HtmlAstVisitor, HtmlAttrAst, HtmlCommentAst, HtmlElementAst, HtmlExpansionAst, HtmlExpansionCaseAst, HtmlTextAst, htmlVisitAll} from './html_parser/html_ast';
|
||||
import {MapWrapper} from './facade/collection';
|
||||
import {BaseException} from './facade/exceptions';
|
||||
import {isBlank, isPresent} from './facade/lang';
|
||||
import * as html from './html_parser/ast';
|
||||
import {HtmlParser} from './html_parser/html_parser';
|
||||
import {InterpolationConfig} from './html_parser/interpolation_config';
|
||||
import {extractStyleUrls, isStyleUrlResolvable} from './style_url_resolver';
|
||||
|
@ -111,7 +112,7 @@ export class DirectiveNormalizer {
|
|||
}));
|
||||
|
||||
const visitor = new TemplatePreparseVisitor();
|
||||
htmlVisitAll(visitor, rootNodesAndErrors.rootNodes);
|
||||
html.visitAll(visitor, rootNodesAndErrors.rootNodes);
|
||||
const templateStyles = this.normalizeStylesheet(new CompileStylesheetMetadata(
|
||||
{styles: visitor.styles, styleUrls: visitor.styleUrls, moduleUrl: templateAbsUrl}));
|
||||
|
||||
|
@ -187,13 +188,13 @@ export class DirectiveNormalizer {
|
|||
}
|
||||
}
|
||||
|
||||
class TemplatePreparseVisitor implements HtmlAstVisitor {
|
||||
class TemplatePreparseVisitor implements html.Visitor {
|
||||
ngContentSelectors: string[] = [];
|
||||
styles: string[] = [];
|
||||
styleUrls: string[] = [];
|
||||
ngNonBindableStackCount: number = 0;
|
||||
|
||||
visitElement(ast: HtmlElementAst, context: any): any {
|
||||
visitElement(ast: html.Element, context: any): any {
|
||||
var preparsedElement = preparseElement(ast);
|
||||
switch (preparsedElement.type) {
|
||||
case PreparsedElementType.NG_CONTENT:
|
||||
|
@ -204,7 +205,7 @@ class TemplatePreparseVisitor implements HtmlAstVisitor {
|
|||
case PreparsedElementType.STYLE:
|
||||
var textContent = '';
|
||||
ast.children.forEach(child => {
|
||||
if (child instanceof HtmlTextAst) {
|
||||
if (child instanceof html.Text) {
|
||||
textContent += child.value;
|
||||
}
|
||||
});
|
||||
|
@ -221,18 +222,18 @@ class TemplatePreparseVisitor implements HtmlAstVisitor {
|
|||
if (preparsedElement.nonBindable) {
|
||||
this.ngNonBindableStackCount++;
|
||||
}
|
||||
htmlVisitAll(this, ast.children);
|
||||
html.visitAll(this, ast.children);
|
||||
if (preparsedElement.nonBindable) {
|
||||
this.ngNonBindableStackCount--;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
visitComment(ast: HtmlCommentAst, context: any): any { return null; }
|
||||
visitAttr(ast: HtmlAttrAst, context: any): any { return null; }
|
||||
visitText(ast: HtmlTextAst, context: any): any { return null; }
|
||||
visitExpansion(ast: HtmlExpansionAst, context: any): any { return null; }
|
||||
visitComment(ast: html.Comment, context: any): any { return null; }
|
||||
visitAttribute(ast: html.Attribute, context: any): any { return null; }
|
||||
visitText(ast: html.Text, context: any): any { return null; }
|
||||
visitExpansion(ast: html.Expansion, context: any): any { return null; }
|
||||
|
||||
visitExpansionCase(ast: HtmlExpansionCaseAst, context: any): any { return null; }
|
||||
visitExpansionCase(ast: html.ExpansionCase, context: any): any { return null; }
|
||||
}
|
||||
|
||||
function _cloneDirectiveWithTemplate(
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {ParseSourceSpan} from '../parse_util';
|
||||
|
||||
export interface Node {
|
||||
sourceSpan: ParseSourceSpan;
|
||||
visit(visitor: Visitor, context: any): any;
|
||||
}
|
||||
|
||||
export class Text implements Node {
|
||||
constructor(public value: string, public sourceSpan: ParseSourceSpan) {}
|
||||
visit(visitor: Visitor, context: any): any { return visitor.visitText(this, context); }
|
||||
}
|
||||
|
||||
export class Expansion implements Node {
|
||||
constructor(
|
||||
public switchValue: string, public type: string, public cases: ExpansionCase[],
|
||||
public sourceSpan: ParseSourceSpan, public switchValueSourceSpan: ParseSourceSpan) {}
|
||||
visit(visitor: Visitor, context: any): any { return visitor.visitExpansion(this, context); }
|
||||
}
|
||||
|
||||
export class ExpansionCase implements Node {
|
||||
constructor(
|
||||
public value: string, public expression: Node[], public sourceSpan: ParseSourceSpan,
|
||||
public valueSourceSpan: ParseSourceSpan, public expSourceSpan: ParseSourceSpan) {}
|
||||
|
||||
visit(visitor: Visitor, context: any): any { return visitor.visitExpansionCase(this, context); }
|
||||
}
|
||||
|
||||
export class Attribute implements Node {
|
||||
constructor(public name: string, public value: string, public sourceSpan: ParseSourceSpan) {}
|
||||
visit(visitor: Visitor, context: any): any { return visitor.visitAttribute(this, context); }
|
||||
}
|
||||
|
||||
export class Element implements Node {
|
||||
constructor(
|
||||
public name: string, public attrs: Attribute[], public children: Node[],
|
||||
public sourceSpan: ParseSourceSpan, public startSourceSpan: ParseSourceSpan,
|
||||
public endSourceSpan: ParseSourceSpan) {}
|
||||
visit(visitor: Visitor, context: any): any { return visitor.visitElement(this, context); }
|
||||
}
|
||||
|
||||
export class Comment implements Node {
|
||||
constructor(public value: string, public sourceSpan: ParseSourceSpan) {}
|
||||
visit(visitor: Visitor, context: any): any { return visitor.visitComment(this, context); }
|
||||
}
|
||||
|
||||
export interface Visitor {
|
||||
visitElement(element: Element, context: any): any;
|
||||
visitAttribute(attribute: Attribute, context: any): any;
|
||||
visitText(text: Text, context: any): any;
|
||||
visitComment(comment: Comment, context: any): any;
|
||||
visitExpansion(expansion: Expansion, context: any): any;
|
||||
visitExpansionCase(expansionCase: ExpansionCase, context: any): any;
|
||||
}
|
||||
|
||||
export function visitAll(visitor: Visitor, nodes: Node[], context: any = null): any[] {
|
||||
let result: any[] = [];
|
||||
nodes.forEach(ast => {
|
||||
const astResult = ast.visit(visitor, context);
|
||||
if (astResult) {
|
||||
result.push(astResult);
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
|
@ -1,78 +0,0 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {isPresent} from '../facade/lang';
|
||||
|
||||
import {ParseSourceSpan} from '../parse_util';
|
||||
|
||||
export interface HtmlAst {
|
||||
sourceSpan: ParseSourceSpan;
|
||||
visit(visitor: HtmlAstVisitor, context: any): any;
|
||||
}
|
||||
|
||||
export class HtmlTextAst implements HtmlAst {
|
||||
constructor(public value: string, public sourceSpan: ParseSourceSpan) {}
|
||||
visit(visitor: HtmlAstVisitor, context: any): any { return visitor.visitText(this, context); }
|
||||
}
|
||||
|
||||
export class HtmlExpansionAst implements HtmlAst {
|
||||
constructor(
|
||||
public switchValue: string, public type: string, public cases: HtmlExpansionCaseAst[],
|
||||
public sourceSpan: ParseSourceSpan, public switchValueSourceSpan: ParseSourceSpan) {}
|
||||
visit(visitor: HtmlAstVisitor, context: any): any {
|
||||
return visitor.visitExpansion(this, context);
|
||||
}
|
||||
}
|
||||
|
||||
export class HtmlExpansionCaseAst implements HtmlAst {
|
||||
constructor(
|
||||
public value: string, public expression: HtmlAst[], public sourceSpan: ParseSourceSpan,
|
||||
public valueSourceSpan: ParseSourceSpan, public expSourceSpan: ParseSourceSpan) {}
|
||||
|
||||
visit(visitor: HtmlAstVisitor, context: any): any {
|
||||
return visitor.visitExpansionCase(this, context);
|
||||
}
|
||||
}
|
||||
|
||||
export class HtmlAttrAst implements HtmlAst {
|
||||
constructor(public name: string, public value: string, public sourceSpan: ParseSourceSpan) {}
|
||||
visit(visitor: HtmlAstVisitor, context: any): any { return visitor.visitAttr(this, context); }
|
||||
}
|
||||
|
||||
export class HtmlElementAst implements HtmlAst {
|
||||
constructor(
|
||||
public name: string, public attrs: HtmlAttrAst[], public children: HtmlAst[],
|
||||
public sourceSpan: ParseSourceSpan, public startSourceSpan: ParseSourceSpan,
|
||||
public endSourceSpan: ParseSourceSpan) {}
|
||||
visit(visitor: HtmlAstVisitor, context: any): any { return visitor.visitElement(this, context); }
|
||||
}
|
||||
|
||||
export class HtmlCommentAst implements HtmlAst {
|
||||
constructor(public value: string, public sourceSpan: ParseSourceSpan) {}
|
||||
visit(visitor: HtmlAstVisitor, context: any): any { return visitor.visitComment(this, context); }
|
||||
}
|
||||
|
||||
export interface HtmlAstVisitor {
|
||||
visitElement(ast: HtmlElementAst, context: any): any;
|
||||
visitAttr(ast: HtmlAttrAst, context: any): any;
|
||||
visitText(ast: HtmlTextAst, context: any): any;
|
||||
visitComment(ast: HtmlCommentAst, context: any): any;
|
||||
visitExpansion(ast: HtmlExpansionAst, context: any): any;
|
||||
visitExpansionCase(ast: HtmlExpansionCaseAst, context: any): any;
|
||||
}
|
||||
|
||||
export function htmlVisitAll(visitor: HtmlAstVisitor, asts: HtmlAst[], context: any = null): any[] {
|
||||
var result: any[] = [];
|
||||
asts.forEach(ast => {
|
||||
var astResult = ast.visit(visitor, context);
|
||||
if (isPresent(astResult)) {
|
||||
result.push(astResult);
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
|
@ -6,403 +6,21 @@
|
|||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {Injectable} from '../../../core/index';
|
||||
import {isPresent, isBlank,} from '../facade/lang';
|
||||
import {ListWrapper} from '../facade/collection';
|
||||
import {HtmlAst, HtmlAttrAst, HtmlTextAst, HtmlCommentAst, HtmlElementAst, HtmlExpansionAst, HtmlExpansionCaseAst} from './html_ast';
|
||||
import {HtmlToken, HtmlTokenType, tokenizeHtml} from './html_lexer';
|
||||
import {ParseError, ParseSourceSpan} from '../parse_util';
|
||||
import {getHtmlTagDefinition, getNsPrefix, mergeNsAndName} from './html_tags';
|
||||
import {Injectable} from '@angular/core';
|
||||
|
||||
import {getHtmlTagDefinition} from './html_tags';
|
||||
import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from './interpolation_config';
|
||||
import {ParseTreeResult, Parser} from './parser';
|
||||
|
||||
export class HtmlTreeError extends ParseError {
|
||||
static create(elementName: string, span: ParseSourceSpan, msg: string): HtmlTreeError {
|
||||
return new HtmlTreeError(elementName, span, msg);
|
||||
}
|
||||
|
||||
constructor(public elementName: string, span: ParseSourceSpan, msg: string) { super(span, msg); }
|
||||
}
|
||||
|
||||
export class HtmlParseTreeResult {
|
||||
constructor(public rootNodes: HtmlAst[], public errors: ParseError[]) {}
|
||||
}
|
||||
export {ParseTreeResult, TreeError} from './parser';
|
||||
|
||||
@Injectable()
|
||||
export class HtmlParser {
|
||||
export class HtmlParser extends Parser {
|
||||
constructor() { super(getHtmlTagDefinition); }
|
||||
|
||||
parse(
|
||||
sourceContent: string, sourceUrl: string, parseExpansionForms: boolean = false,
|
||||
interpolationConfig: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG):
|
||||
HtmlParseTreeResult {
|
||||
const tokensAndErrors =
|
||||
tokenizeHtml(sourceContent, sourceUrl, parseExpansionForms, interpolationConfig);
|
||||
const treeAndErrors = new TreeBuilder(tokensAndErrors.tokens).build();
|
||||
return new HtmlParseTreeResult(
|
||||
treeAndErrors.rootNodes,
|
||||
(<ParseError[]>tokensAndErrors.errors).concat(treeAndErrors.errors));
|
||||
source: string, url: string, parseExpansionForms: boolean = false,
|
||||
interpolationConfig: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG): ParseTreeResult {
|
||||
return super.parse(source, url, parseExpansionForms, interpolationConfig);
|
||||
}
|
||||
}
|
||||
|
||||
class TreeBuilder {
|
||||
private index: number = -1;
|
||||
private peek: HtmlToken;
|
||||
|
||||
private rootNodes: HtmlAst[] = [];
|
||||
private errors: HtmlTreeError[] = [];
|
||||
|
||||
private elementStack: HtmlElementAst[] = [];
|
||||
|
||||
constructor(private tokens: HtmlToken[]) { this._advance(); }
|
||||
|
||||
build(): HtmlParseTreeResult {
|
||||
while (this.peek.type !== HtmlTokenType.EOF) {
|
||||
if (this.peek.type === HtmlTokenType.TAG_OPEN_START) {
|
||||
this._consumeStartTag(this._advance());
|
||||
} else if (this.peek.type === HtmlTokenType.TAG_CLOSE) {
|
||||
this._consumeEndTag(this._advance());
|
||||
} else if (this.peek.type === HtmlTokenType.CDATA_START) {
|
||||
this._closeVoidElement();
|
||||
this._consumeCdata(this._advance());
|
||||
} else if (this.peek.type === HtmlTokenType.COMMENT_START) {
|
||||
this._closeVoidElement();
|
||||
this._consumeComment(this._advance());
|
||||
} else if (
|
||||
this.peek.type === HtmlTokenType.TEXT || this.peek.type === HtmlTokenType.RAW_TEXT ||
|
||||
this.peek.type === HtmlTokenType.ESCAPABLE_RAW_TEXT) {
|
||||
this._closeVoidElement();
|
||||
this._consumeText(this._advance());
|
||||
} else if (this.peek.type === HtmlTokenType.EXPANSION_FORM_START) {
|
||||
this._consumeExpansion(this._advance());
|
||||
} else {
|
||||
// Skip all other tokens...
|
||||
this._advance();
|
||||
}
|
||||
}
|
||||
return new HtmlParseTreeResult(this.rootNodes, this.errors);
|
||||
}
|
||||
|
||||
private _advance(): HtmlToken {
|
||||
const prev = this.peek;
|
||||
if (this.index < this.tokens.length - 1) {
|
||||
// Note: there is always an EOF token at the end
|
||||
this.index++;
|
||||
}
|
||||
this.peek = this.tokens[this.index];
|
||||
return prev;
|
||||
}
|
||||
|
||||
private _advanceIf(type: HtmlTokenType): HtmlToken {
|
||||
if (this.peek.type === type) {
|
||||
return this._advance();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private _consumeCdata(startToken: HtmlToken) {
|
||||
this._consumeText(this._advance());
|
||||
this._advanceIf(HtmlTokenType.CDATA_END);
|
||||
}
|
||||
|
||||
private _consumeComment(token: HtmlToken) {
|
||||
const text = this._advanceIf(HtmlTokenType.RAW_TEXT);
|
||||
this._advanceIf(HtmlTokenType.COMMENT_END);
|
||||
const value = isPresent(text) ? text.parts[0].trim() : null;
|
||||
this._addToParent(new HtmlCommentAst(value, token.sourceSpan));
|
||||
}
|
||||
|
||||
private _consumeExpansion(token: HtmlToken) {
|
||||
const switchValue = this._advance();
|
||||
|
||||
const type = this._advance();
|
||||
const cases: HtmlExpansionCaseAst[] = [];
|
||||
|
||||
// read =
|
||||
while (this.peek.type === HtmlTokenType.EXPANSION_CASE_VALUE) {
|
||||
let expCase = this._parseExpansionCase();
|
||||
if (isBlank(expCase)) return; // error
|
||||
cases.push(expCase);
|
||||
}
|
||||
|
||||
// read the final }
|
||||
if (this.peek.type !== HtmlTokenType.EXPANSION_FORM_END) {
|
||||
this.errors.push(
|
||||
HtmlTreeError.create(null, this.peek.sourceSpan, `Invalid ICU message. Missing '}'.`));
|
||||
return;
|
||||
}
|
||||
this._advance();
|
||||
|
||||
const mainSourceSpan = new ParseSourceSpan(token.sourceSpan.start, this.peek.sourceSpan.end);
|
||||
this._addToParent(new HtmlExpansionAst(
|
||||
switchValue.parts[0], type.parts[0], cases, mainSourceSpan, switchValue.sourceSpan));
|
||||
}
|
||||
|
||||
private _parseExpansionCase(): HtmlExpansionCaseAst {
|
||||
const value = this._advance();
|
||||
|
||||
// read {
|
||||
if (this.peek.type !== HtmlTokenType.EXPANSION_CASE_EXP_START) {
|
||||
this.errors.push(
|
||||
HtmlTreeError.create(null, this.peek.sourceSpan, `Invalid ICU message. Missing '{'.`));
|
||||
return null;
|
||||
}
|
||||
|
||||
// read until }
|
||||
const start = this._advance();
|
||||
|
||||
const exp = this._collectExpansionExpTokens(start);
|
||||
if (isBlank(exp)) return null;
|
||||
|
||||
const end = this._advance();
|
||||
exp.push(new HtmlToken(HtmlTokenType.EOF, [], end.sourceSpan));
|
||||
|
||||
// parse everything in between { and }
|
||||
const parsedExp = new TreeBuilder(exp).build();
|
||||
if (parsedExp.errors.length > 0) {
|
||||
this.errors = this.errors.concat(<HtmlTreeError[]>parsedExp.errors);
|
||||
return null;
|
||||
}
|
||||
|
||||
const sourceSpan = new ParseSourceSpan(value.sourceSpan.start, end.sourceSpan.end);
|
||||
const expSourceSpan = new ParseSourceSpan(start.sourceSpan.start, end.sourceSpan.end);
|
||||
return new HtmlExpansionCaseAst(
|
||||
value.parts[0], parsedExp.rootNodes, sourceSpan, value.sourceSpan, expSourceSpan);
|
||||
}
|
||||
|
||||
private _collectExpansionExpTokens(start: HtmlToken): HtmlToken[] {
|
||||
const exp: HtmlToken[] = [];
|
||||
const expansionFormStack = [HtmlTokenType.EXPANSION_CASE_EXP_START];
|
||||
|
||||
while (true) {
|
||||
if (this.peek.type === HtmlTokenType.EXPANSION_FORM_START ||
|
||||
this.peek.type === HtmlTokenType.EXPANSION_CASE_EXP_START) {
|
||||
expansionFormStack.push(this.peek.type);
|
||||
}
|
||||
|
||||
if (this.peek.type === HtmlTokenType.EXPANSION_CASE_EXP_END) {
|
||||
if (lastOnStack(expansionFormStack, HtmlTokenType.EXPANSION_CASE_EXP_START)) {
|
||||
expansionFormStack.pop();
|
||||
if (expansionFormStack.length == 0) return exp;
|
||||
|
||||
} else {
|
||||
this.errors.push(
|
||||
HtmlTreeError.create(null, start.sourceSpan, `Invalid ICU message. Missing '}'.`));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.peek.type === HtmlTokenType.EXPANSION_FORM_END) {
|
||||
if (lastOnStack(expansionFormStack, HtmlTokenType.EXPANSION_FORM_START)) {
|
||||
expansionFormStack.pop();
|
||||
} else {
|
||||
this.errors.push(
|
||||
HtmlTreeError.create(null, start.sourceSpan, `Invalid ICU message. Missing '}'.`));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.peek.type === HtmlTokenType.EOF) {
|
||||
this.errors.push(
|
||||
HtmlTreeError.create(null, start.sourceSpan, `Invalid ICU message. Missing '}'.`));
|
||||
return null;
|
||||
}
|
||||
|
||||
exp.push(this._advance());
|
||||
}
|
||||
}
|
||||
|
||||
private _consumeText(token: HtmlToken) {
|
||||
let text = token.parts[0];
|
||||
if (text.length > 0 && text[0] == '\n') {
|
||||
const parent = this._getParentElement();
|
||||
if (isPresent(parent) && parent.children.length == 0 &&
|
||||
getHtmlTagDefinition(parent.name).ignoreFirstLf) {
|
||||
text = text.substring(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (text.length > 0) {
|
||||
this._addToParent(new HtmlTextAst(text, token.sourceSpan));
|
||||
}
|
||||
}
|
||||
|
||||
private _closeVoidElement(): void {
|
||||
if (this.elementStack.length > 0) {
|
||||
const el = ListWrapper.last(this.elementStack);
|
||||
|
||||
if (getHtmlTagDefinition(el.name).isVoid) {
|
||||
this.elementStack.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _consumeStartTag(startTagToken: HtmlToken) {
|
||||
const prefix = startTagToken.parts[0];
|
||||
const name = startTagToken.parts[1];
|
||||
const attrs: HtmlAttrAst[] = [];
|
||||
while (this.peek.type === HtmlTokenType.ATTR_NAME) {
|
||||
attrs.push(this._consumeAttr(this._advance()));
|
||||
}
|
||||
const fullName = getElementFullName(prefix, name, this._getParentElement());
|
||||
let selfClosing = false;
|
||||
// Note: There could have been a tokenizer error
|
||||
// so that we don't get a token for the end tag...
|
||||
if (this.peek.type === HtmlTokenType.TAG_OPEN_END_VOID) {
|
||||
this._advance();
|
||||
selfClosing = true;
|
||||
if (getNsPrefix(fullName) == null && !getHtmlTagDefinition(fullName).isVoid) {
|
||||
this.errors.push(HtmlTreeError.create(
|
||||
fullName, startTagToken.sourceSpan,
|
||||
`Only void and foreign elements can be self closed "${startTagToken.parts[1]}"`));
|
||||
}
|
||||
} else if (this.peek.type === HtmlTokenType.TAG_OPEN_END) {
|
||||
this._advance();
|
||||
selfClosing = false;
|
||||
}
|
||||
const end = this.peek.sourceSpan.start;
|
||||
const span = new ParseSourceSpan(startTagToken.sourceSpan.start, end);
|
||||
const el = new HtmlElementAst(fullName, attrs, [], span, span, null);
|
||||
this._pushElement(el);
|
||||
if (selfClosing) {
|
||||
this._popElement(fullName);
|
||||
el.endSourceSpan = span;
|
||||
}
|
||||
}
|
||||
|
||||
private _pushElement(el: HtmlElementAst) {
|
||||
if (this.elementStack.length > 0) {
|
||||
const parentEl = ListWrapper.last(this.elementStack);
|
||||
if (getHtmlTagDefinition(parentEl.name).isClosedByChild(el.name)) {
|
||||
this.elementStack.pop();
|
||||
}
|
||||
}
|
||||
|
||||
const tagDef = getHtmlTagDefinition(el.name);
|
||||
const {parent, container} = this._getParentElementSkippingContainers();
|
||||
|
||||
if (isPresent(parent) && tagDef.requireExtraParent(parent.name)) {
|
||||
const newParent = new HtmlElementAst(
|
||||
tagDef.parentToAdd, [], [], el.sourceSpan, el.startSourceSpan, el.endSourceSpan);
|
||||
this._insertBeforeContainer(parent, container, newParent);
|
||||
}
|
||||
|
||||
this._addToParent(el);
|
||||
this.elementStack.push(el);
|
||||
}
|
||||
|
||||
private _consumeEndTag(endTagToken: HtmlToken) {
|
||||
const fullName =
|
||||
getElementFullName(endTagToken.parts[0], endTagToken.parts[1], this._getParentElement());
|
||||
|
||||
if (this._getParentElement()) {
|
||||
this._getParentElement().endSourceSpan = endTagToken.sourceSpan;
|
||||
}
|
||||
|
||||
if (getHtmlTagDefinition(fullName).isVoid) {
|
||||
this.errors.push(HtmlTreeError.create(
|
||||
fullName, endTagToken.sourceSpan,
|
||||
`Void elements do not have end tags "${endTagToken.parts[1]}"`));
|
||||
} else if (!this._popElement(fullName)) {
|
||||
this.errors.push(HtmlTreeError.create(
|
||||
fullName, endTagToken.sourceSpan, `Unexpected closing tag "${endTagToken.parts[1]}"`));
|
||||
}
|
||||
}
|
||||
|
||||
private _popElement(fullName: string): boolean {
|
||||
for (let stackIndex = this.elementStack.length - 1; stackIndex >= 0; stackIndex--) {
|
||||
const el = this.elementStack[stackIndex];
|
||||
if (el.name == fullName) {
|
||||
ListWrapper.splice(this.elementStack, stackIndex, this.elementStack.length - stackIndex);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!getHtmlTagDefinition(el.name).closedByParent) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private _consumeAttr(attrName: HtmlToken): HtmlAttrAst {
|
||||
const fullName = mergeNsAndName(attrName.parts[0], attrName.parts[1]);
|
||||
let end = attrName.sourceSpan.end;
|
||||
let value = '';
|
||||
if (this.peek.type === HtmlTokenType.ATTR_VALUE) {
|
||||
const valueToken = this._advance();
|
||||
value = valueToken.parts[0];
|
||||
end = valueToken.sourceSpan.end;
|
||||
}
|
||||
return new HtmlAttrAst(fullName, value, new ParseSourceSpan(attrName.sourceSpan.start, end));
|
||||
}
|
||||
|
||||
private _getParentElement(): HtmlElementAst {
|
||||
return this.elementStack.length > 0 ? ListWrapper.last(this.elementStack) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the parent in the DOM and the container.
|
||||
*
|
||||
* `<ng-container>` elements are skipped as they are not rendered as DOM element.
|
||||
*/
|
||||
private _getParentElementSkippingContainers():
|
||||
{parent: HtmlElementAst, container: HtmlElementAst} {
|
||||
let container: HtmlElementAst = null;
|
||||
|
||||
for (let i = this.elementStack.length - 1; i >= 0; i--) {
|
||||
if (this.elementStack[i].name !== 'ng-container') {
|
||||
return {parent: this.elementStack[i], container};
|
||||
}
|
||||
container = this.elementStack[i];
|
||||
}
|
||||
|
||||
return {parent: ListWrapper.last(this.elementStack), container};
|
||||
}
|
||||
|
||||
private _addToParent(node: HtmlAst) {
|
||||
const parent = this._getParentElement();
|
||||
if (isPresent(parent)) {
|
||||
parent.children.push(node);
|
||||
} else {
|
||||
this.rootNodes.push(node);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert a node between the parent and the container.
|
||||
* When no container is given, the node is appended as a child of the parent.
|
||||
* Also updates the element stack accordingly.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
private _insertBeforeContainer(
|
||||
parent: HtmlElementAst, container: HtmlElementAst, node: HtmlElementAst) {
|
||||
if (!container) {
|
||||
this._addToParent(node);
|
||||
this.elementStack.push(node);
|
||||
} else {
|
||||
if (parent) {
|
||||
// replace the container with the new node in the children
|
||||
const index = parent.children.indexOf(container);
|
||||
parent.children[index] = node;
|
||||
} else {
|
||||
this.rootNodes.push(node);
|
||||
}
|
||||
node.children.push(container);
|
||||
this.elementStack.splice(this.elementStack.indexOf(container), 0, node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getElementFullName(
|
||||
prefix: string, localName: string, parentElement: HtmlElementAst): string {
|
||||
if (isBlank(prefix)) {
|
||||
prefix = getHtmlTagDefinition(localName).implicitNamespacePrefix;
|
||||
if (isBlank(prefix) && isPresent(parentElement)) {
|
||||
prefix = getNsPrefix(parentElement.name);
|
||||
}
|
||||
}
|
||||
|
||||
return mergeNsAndName(prefix, localName);
|
||||
}
|
||||
|
||||
function lastOnStack(stack: any[], element: any): boolean {
|
||||
return stack.length > 0 && stack[stack.length - 1] === element;
|
||||
}
|
||||
|
|
|
@ -6,299 +6,37 @@
|
|||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {normalizeBool, RegExpWrapper,} from '../facade/lang';
|
||||
import {TagContentType, TagDefinition} from './tags';
|
||||
|
||||
// see http://www.w3.org/TR/html51/syntax.html#named-character-references
|
||||
// see https://html.spec.whatwg.org/multipage/entities.json
|
||||
// This list is not exhaustive to keep the compiler footprint low.
|
||||
// The `{` / `ƫ` syntax should be used when the named character reference does not exist.
|
||||
export const NAMED_ENTITIES = /*@ts2dart_const*/ {
|
||||
'Aacute': '\u00C1',
|
||||
'aacute': '\u00E1',
|
||||
'Acirc': '\u00C2',
|
||||
'acirc': '\u00E2',
|
||||
'acute': '\u00B4',
|
||||
'AElig': '\u00C6',
|
||||
'aelig': '\u00E6',
|
||||
'Agrave': '\u00C0',
|
||||
'agrave': '\u00E0',
|
||||
'alefsym': '\u2135',
|
||||
'Alpha': '\u0391',
|
||||
'alpha': '\u03B1',
|
||||
'amp': '&',
|
||||
'and': '\u2227',
|
||||
'ang': '\u2220',
|
||||
'apos': '\u0027',
|
||||
'Aring': '\u00C5',
|
||||
'aring': '\u00E5',
|
||||
'asymp': '\u2248',
|
||||
'Atilde': '\u00C3',
|
||||
'atilde': '\u00E3',
|
||||
'Auml': '\u00C4',
|
||||
'auml': '\u00E4',
|
||||
'bdquo': '\u201E',
|
||||
'Beta': '\u0392',
|
||||
'beta': '\u03B2',
|
||||
'brvbar': '\u00A6',
|
||||
'bull': '\u2022',
|
||||
'cap': '\u2229',
|
||||
'Ccedil': '\u00C7',
|
||||
'ccedil': '\u00E7',
|
||||
'cedil': '\u00B8',
|
||||
'cent': '\u00A2',
|
||||
'Chi': '\u03A7',
|
||||
'chi': '\u03C7',
|
||||
'circ': '\u02C6',
|
||||
'clubs': '\u2663',
|
||||
'cong': '\u2245',
|
||||
'copy': '\u00A9',
|
||||
'crarr': '\u21B5',
|
||||
'cup': '\u222A',
|
||||
'curren': '\u00A4',
|
||||
'dagger': '\u2020',
|
||||
'Dagger': '\u2021',
|
||||
'darr': '\u2193',
|
||||
'dArr': '\u21D3',
|
||||
'deg': '\u00B0',
|
||||
'Delta': '\u0394',
|
||||
'delta': '\u03B4',
|
||||
'diams': '\u2666',
|
||||
'divide': '\u00F7',
|
||||
'Eacute': '\u00C9',
|
||||
'eacute': '\u00E9',
|
||||
'Ecirc': '\u00CA',
|
||||
'ecirc': '\u00EA',
|
||||
'Egrave': '\u00C8',
|
||||
'egrave': '\u00E8',
|
||||
'empty': '\u2205',
|
||||
'emsp': '\u2003',
|
||||
'ensp': '\u2002',
|
||||
'Epsilon': '\u0395',
|
||||
'epsilon': '\u03B5',
|
||||
'equiv': '\u2261',
|
||||
'Eta': '\u0397',
|
||||
'eta': '\u03B7',
|
||||
'ETH': '\u00D0',
|
||||
'eth': '\u00F0',
|
||||
'Euml': '\u00CB',
|
||||
'euml': '\u00EB',
|
||||
'euro': '\u20AC',
|
||||
'exist': '\u2203',
|
||||
'fnof': '\u0192',
|
||||
'forall': '\u2200',
|
||||
'frac12': '\u00BD',
|
||||
'frac14': '\u00BC',
|
||||
'frac34': '\u00BE',
|
||||
'frasl': '\u2044',
|
||||
'Gamma': '\u0393',
|
||||
'gamma': '\u03B3',
|
||||
'ge': '\u2265',
|
||||
'gt': '>',
|
||||
'harr': '\u2194',
|
||||
'hArr': '\u21D4',
|
||||
'hearts': '\u2665',
|
||||
'hellip': '\u2026',
|
||||
'Iacute': '\u00CD',
|
||||
'iacute': '\u00ED',
|
||||
'Icirc': '\u00CE',
|
||||
'icirc': '\u00EE',
|
||||
'iexcl': '\u00A1',
|
||||
'Igrave': '\u00CC',
|
||||
'igrave': '\u00EC',
|
||||
'image': '\u2111',
|
||||
'infin': '\u221E',
|
||||
'int': '\u222B',
|
||||
'Iota': '\u0399',
|
||||
'iota': '\u03B9',
|
||||
'iquest': '\u00BF',
|
||||
'isin': '\u2208',
|
||||
'Iuml': '\u00CF',
|
||||
'iuml': '\u00EF',
|
||||
'Kappa': '\u039A',
|
||||
'kappa': '\u03BA',
|
||||
'Lambda': '\u039B',
|
||||
'lambda': '\u03BB',
|
||||
'lang': '\u27E8',
|
||||
'laquo': '\u00AB',
|
||||
'larr': '\u2190',
|
||||
'lArr': '\u21D0',
|
||||
'lceil': '\u2308',
|
||||
'ldquo': '\u201C',
|
||||
'le': '\u2264',
|
||||
'lfloor': '\u230A',
|
||||
'lowast': '\u2217',
|
||||
'loz': '\u25CA',
|
||||
'lrm': '\u200E',
|
||||
'lsaquo': '\u2039',
|
||||
'lsquo': '\u2018',
|
||||
'lt': '<',
|
||||
'macr': '\u00AF',
|
||||
'mdash': '\u2014',
|
||||
'micro': '\u00B5',
|
||||
'middot': '\u00B7',
|
||||
'minus': '\u2212',
|
||||
'Mu': '\u039C',
|
||||
'mu': '\u03BC',
|
||||
'nabla': '\u2207',
|
||||
'nbsp': '\u00A0',
|
||||
'ndash': '\u2013',
|
||||
'ne': '\u2260',
|
||||
'ni': '\u220B',
|
||||
'not': '\u00AC',
|
||||
'notin': '\u2209',
|
||||
'nsub': '\u2284',
|
||||
'Ntilde': '\u00D1',
|
||||
'ntilde': '\u00F1',
|
||||
'Nu': '\u039D',
|
||||
'nu': '\u03BD',
|
||||
'Oacute': '\u00D3',
|
||||
'oacute': '\u00F3',
|
||||
'Ocirc': '\u00D4',
|
||||
'ocirc': '\u00F4',
|
||||
'OElig': '\u0152',
|
||||
'oelig': '\u0153',
|
||||
'Ograve': '\u00D2',
|
||||
'ograve': '\u00F2',
|
||||
'oline': '\u203E',
|
||||
'Omega': '\u03A9',
|
||||
'omega': '\u03C9',
|
||||
'Omicron': '\u039F',
|
||||
'omicron': '\u03BF',
|
||||
'oplus': '\u2295',
|
||||
'or': '\u2228',
|
||||
'ordf': '\u00AA',
|
||||
'ordm': '\u00BA',
|
||||
'Oslash': '\u00D8',
|
||||
'oslash': '\u00F8',
|
||||
'Otilde': '\u00D5',
|
||||
'otilde': '\u00F5',
|
||||
'otimes': '\u2297',
|
||||
'Ouml': '\u00D6',
|
||||
'ouml': '\u00F6',
|
||||
'para': '\u00B6',
|
||||
'permil': '\u2030',
|
||||
'perp': '\u22A5',
|
||||
'Phi': '\u03A6',
|
||||
'phi': '\u03C6',
|
||||
'Pi': '\u03A0',
|
||||
'pi': '\u03C0',
|
||||
'piv': '\u03D6',
|
||||
'plusmn': '\u00B1',
|
||||
'pound': '\u00A3',
|
||||
'prime': '\u2032',
|
||||
'Prime': '\u2033',
|
||||
'prod': '\u220F',
|
||||
'prop': '\u221D',
|
||||
'Psi': '\u03A8',
|
||||
'psi': '\u03C8',
|
||||
'quot': '\u0022',
|
||||
'radic': '\u221A',
|
||||
'rang': '\u27E9',
|
||||
'raquo': '\u00BB',
|
||||
'rarr': '\u2192',
|
||||
'rArr': '\u21D2',
|
||||
'rceil': '\u2309',
|
||||
'rdquo': '\u201D',
|
||||
'real': '\u211C',
|
||||
'reg': '\u00AE',
|
||||
'rfloor': '\u230B',
|
||||
'Rho': '\u03A1',
|
||||
'rho': '\u03C1',
|
||||
'rlm': '\u200F',
|
||||
'rsaquo': '\u203A',
|
||||
'rsquo': '\u2019',
|
||||
'sbquo': '\u201A',
|
||||
'Scaron': '\u0160',
|
||||
'scaron': '\u0161',
|
||||
'sdot': '\u22C5',
|
||||
'sect': '\u00A7',
|
||||
'shy': '\u00AD',
|
||||
'Sigma': '\u03A3',
|
||||
'sigma': '\u03C3',
|
||||
'sigmaf': '\u03C2',
|
||||
'sim': '\u223C',
|
||||
'spades': '\u2660',
|
||||
'sub': '\u2282',
|
||||
'sube': '\u2286',
|
||||
'sum': '\u2211',
|
||||
'sup': '\u2283',
|
||||
'sup1': '\u00B9',
|
||||
'sup2': '\u00B2',
|
||||
'sup3': '\u00B3',
|
||||
'supe': '\u2287',
|
||||
'szlig': '\u00DF',
|
||||
'Tau': '\u03A4',
|
||||
'tau': '\u03C4',
|
||||
'there4': '\u2234',
|
||||
'Theta': '\u0398',
|
||||
'theta': '\u03B8',
|
||||
'thetasym': '\u03D1',
|
||||
'thinsp': '\u2009',
|
||||
'THORN': '\u00DE',
|
||||
'thorn': '\u00FE',
|
||||
'tilde': '\u02DC',
|
||||
'times': '\u00D7',
|
||||
'trade': '\u2122',
|
||||
'Uacute': '\u00DA',
|
||||
'uacute': '\u00FA',
|
||||
'uarr': '\u2191',
|
||||
'uArr': '\u21D1',
|
||||
'Ucirc': '\u00DB',
|
||||
'ucirc': '\u00FB',
|
||||
'Ugrave': '\u00D9',
|
||||
'ugrave': '\u00F9',
|
||||
'uml': '\u00A8',
|
||||
'upsih': '\u03D2',
|
||||
'Upsilon': '\u03A5',
|
||||
'upsilon': '\u03C5',
|
||||
'Uuml': '\u00DC',
|
||||
'uuml': '\u00FC',
|
||||
'weierp': '\u2118',
|
||||
'Xi': '\u039E',
|
||||
'xi': '\u03BE',
|
||||
'Yacute': '\u00DD',
|
||||
'yacute': '\u00FD',
|
||||
'yen': '\u00A5',
|
||||
'yuml': '\u00FF',
|
||||
'Yuml': '\u0178',
|
||||
'Zeta': '\u0396',
|
||||
'zeta': '\u03B6',
|
||||
'zwj': '\u200D',
|
||||
'zwnj': '\u200C',
|
||||
};
|
||||
|
||||
export enum HtmlTagContentType {
|
||||
RAW_TEXT,
|
||||
ESCAPABLE_RAW_TEXT,
|
||||
PARSABLE_DATA
|
||||
}
|
||||
|
||||
export class HtmlTagDefinition {
|
||||
export class HtmlTagDefinition implements TagDefinition {
|
||||
private closedByChildren: {[key: string]: boolean} = {};
|
||||
public closedByParent: boolean = false;
|
||||
public requiredParents: {[key: string]: boolean};
|
||||
public parentToAdd: string;
|
||||
public implicitNamespacePrefix: string;
|
||||
public contentType: HtmlTagContentType;
|
||||
public isVoid: boolean;
|
||||
public ignoreFirstLf: boolean;
|
||||
|
||||
closedByParent: boolean = false;
|
||||
requiredParents: {[key: string]: boolean};
|
||||
parentToAdd: string;
|
||||
implicitNamespacePrefix: string;
|
||||
contentType: TagContentType;
|
||||
isVoid: boolean;
|
||||
ignoreFirstLf: boolean;
|
||||
canSelfClose: boolean = false;
|
||||
|
||||
constructor(
|
||||
{closedByChildren, requiredParents, implicitNamespacePrefix, contentType, closedByParent,
|
||||
isVoid, ignoreFirstLf}: {
|
||||
{closedByChildren, requiredParents, implicitNamespacePrefix,
|
||||
contentType = TagContentType.PARSABLE_DATA, closedByParent = false, isVoid = false,
|
||||
ignoreFirstLf = false}: {
|
||||
closedByChildren?: string[],
|
||||
closedByParent?: boolean,
|
||||
requiredParents?: string[],
|
||||
implicitNamespacePrefix?: string,
|
||||
contentType?: HtmlTagContentType,
|
||||
contentType?: TagContentType,
|
||||
isVoid?: boolean,
|
||||
ignoreFirstLf?: boolean
|
||||
} = {}) {
|
||||
if (closedByChildren && closedByChildren.length > 0) {
|
||||
closedByChildren.forEach(tagName => this.closedByChildren[tagName] = true);
|
||||
}
|
||||
this.isVoid = normalizeBool(isVoid);
|
||||
this.closedByParent = normalizeBool(closedByParent) || this.isVoid;
|
||||
this.isVoid = isVoid;
|
||||
this.closedByParent = closedByParent || isVoid;
|
||||
if (requiredParents && requiredParents.length > 0) {
|
||||
this.requiredParents = {};
|
||||
// The first parent is the list is automatically when none of the listed parents are present
|
||||
|
@ -306,8 +44,8 @@ export class HtmlTagDefinition {
|
|||
requiredParents.forEach(tagName => this.requiredParents[tagName] = true);
|
||||
}
|
||||
this.implicitNamespacePrefix = implicitNamespacePrefix;
|
||||
this.contentType = contentType || HtmlTagContentType.PARSABLE_DATA;
|
||||
this.ignoreFirstLf = normalizeBool(ignoreFirstLf);
|
||||
this.contentType = contentType;
|
||||
this.ignoreFirstLf = ignoreFirstLf;
|
||||
}
|
||||
|
||||
requireExtraParent(currentParent: string): boolean {
|
||||
|
@ -324,13 +62,13 @@ export class HtmlTagDefinition {
|
|||
}
|
||||
|
||||
isClosedByChild(name: string): boolean {
|
||||
return this.isVoid || normalizeBool(this.closedByChildren[name.toLowerCase()]);
|
||||
return this.isVoid || name.toLowerCase() in this.closedByChildren;
|
||||
}
|
||||
}
|
||||
|
||||
// see http://www.w3.org/TR/html51/syntax.html#optional-tags
|
||||
// This implementation does not fully conform to the HTML5 spec.
|
||||
var TAG_DEFINITIONS: {[key: string]: HtmlTagDefinition} = {
|
||||
const TAG_DEFINITIONS: {[key: string]: HtmlTagDefinition} = {
|
||||
'base': new HtmlTagDefinition({isVoid: true}),
|
||||
'meta': new HtmlTagDefinition({isVoid: true}),
|
||||
'area': new HtmlTagDefinition({isVoid: true}),
|
||||
|
@ -376,11 +114,11 @@ var TAG_DEFINITIONS: {[key: string]: HtmlTagDefinition} = {
|
|||
'option': new HtmlTagDefinition({closedByChildren: ['option', 'optgroup'], closedByParent: true}),
|
||||
'pre': new HtmlTagDefinition({ignoreFirstLf: true}),
|
||||
'listing': new HtmlTagDefinition({ignoreFirstLf: true}),
|
||||
'style': new HtmlTagDefinition({contentType: HtmlTagContentType.RAW_TEXT}),
|
||||
'script': new HtmlTagDefinition({contentType: HtmlTagContentType.RAW_TEXT}),
|
||||
'title': new HtmlTagDefinition({contentType: HtmlTagContentType.ESCAPABLE_RAW_TEXT}),
|
||||
'textarea': new HtmlTagDefinition(
|
||||
{contentType: HtmlTagContentType.ESCAPABLE_RAW_TEXT, ignoreFirstLf: true}),
|
||||
'style': new HtmlTagDefinition({contentType: TagContentType.RAW_TEXT}),
|
||||
'script': new HtmlTagDefinition({contentType: TagContentType.RAW_TEXT}),
|
||||
'title': new HtmlTagDefinition({contentType: TagContentType.ESCAPABLE_RAW_TEXT}),
|
||||
'textarea':
|
||||
new HtmlTagDefinition({contentType: TagContentType.ESCAPABLE_RAW_TEXT, ignoreFirstLf: true}),
|
||||
};
|
||||
|
||||
const _DEFAULT_TAG_DEFINITION = new HtmlTagDefinition();
|
||||
|
@ -388,21 +126,3 @@ const _DEFAULT_TAG_DEFINITION = new HtmlTagDefinition();
|
|||
export function getHtmlTagDefinition(tagName: string): HtmlTagDefinition {
|
||||
return TAG_DEFINITIONS[tagName.toLowerCase()] || _DEFAULT_TAG_DEFINITION;
|
||||
}
|
||||
|
||||
const _NS_PREFIX_RE = /^:([^:]+):(.+)/g;
|
||||
|
||||
export function splitNsName(elementName: string): [string, string] {
|
||||
if (elementName[0] != ':') {
|
||||
return [null, elementName];
|
||||
}
|
||||
const match = RegExpWrapper.firstMatch(_NS_PREFIX_RE, elementName);
|
||||
return [match[1], match[2]];
|
||||
}
|
||||
|
||||
export function getNsPrefix(elementName: string): string {
|
||||
return splitNsName(elementName)[0];
|
||||
}
|
||||
|
||||
export function mergeNsAndName(prefix: string, localName: string): string {
|
||||
return prefix ? `:${prefix}:${localName}` : localName;
|
||||
}
|
||||
|
|
|
@ -8,8 +8,7 @@
|
|||
|
||||
import {ParseError, ParseSourceSpan} from '../parse_util';
|
||||
|
||||
import {HtmlAst, HtmlAstVisitor, HtmlAttrAst, HtmlCommentAst, HtmlElementAst, HtmlExpansionAst, HtmlExpansionCaseAst, HtmlTextAst, htmlVisitAll} from './html_ast';
|
||||
|
||||
import * as html from './ast';
|
||||
|
||||
// http://cldr.unicode.org/index/cldr-spec/plural-rules
|
||||
const PLURAL_CASES: string[] = ['zero', 'one', 'two', 'few', 'many', 'other'];
|
||||
|
@ -37,13 +36,13 @@ const PLURAL_CASES: string[] = ['zero', 'one', 'two', 'few', 'many', 'other'];
|
|||
* </ng-container>
|
||||
* ```
|
||||
*/
|
||||
export function expandNodes(nodes: HtmlAst[]): ExpansionResult {
|
||||
export function expandNodes(nodes: html.Node[]): ExpansionResult {
|
||||
const expander = new _Expander();
|
||||
return new ExpansionResult(htmlVisitAll(expander, nodes), expander.isExpanded, expander.errors);
|
||||
return new ExpansionResult(html.visitAll(expander, nodes), expander.isExpanded, expander.errors);
|
||||
}
|
||||
|
||||
export class ExpansionResult {
|
||||
constructor(public nodes: HtmlAst[], public expanded: boolean, public errors: ParseError[]) {}
|
||||
constructor(public nodes: html.Node[], public expanded: boolean, public errors: ParseError[]) {}
|
||||
}
|
||||
|
||||
export class ExpansionError extends ParseError {
|
||||
|
@ -55,34 +54,34 @@ export class ExpansionError extends ParseError {
|
|||
*
|
||||
* @internal
|
||||
*/
|
||||
class _Expander implements HtmlAstVisitor {
|
||||
class _Expander implements html.Visitor {
|
||||
isExpanded: boolean = false;
|
||||
errors: ParseError[] = [];
|
||||
|
||||
visitElement(ast: HtmlElementAst, context: any): any {
|
||||
return new HtmlElementAst(
|
||||
ast.name, ast.attrs, htmlVisitAll(this, ast.children), ast.sourceSpan, ast.startSourceSpan,
|
||||
ast.endSourceSpan);
|
||||
visitElement(element: html.Element, context: any): any {
|
||||
return new html.Element(
|
||||
element.name, element.attrs, html.visitAll(this, element.children), element.sourceSpan,
|
||||
element.startSourceSpan, element.endSourceSpan);
|
||||
}
|
||||
|
||||
visitAttr(ast: HtmlAttrAst, context: any): any { return ast; }
|
||||
visitAttribute(attribute: html.Attribute, context: any): any { return attribute; }
|
||||
|
||||
visitText(ast: HtmlTextAst, context: any): any { return ast; }
|
||||
visitText(text: html.Text, context: any): any { return text; }
|
||||
|
||||
visitComment(ast: HtmlCommentAst, context: any): any { return ast; }
|
||||
visitComment(comment: html.Comment, context: any): any { return comment; }
|
||||
|
||||
visitExpansion(ast: HtmlExpansionAst, context: any): any {
|
||||
visitExpansion(icu: html.Expansion, context: any): any {
|
||||
this.isExpanded = true;
|
||||
return ast.type == 'plural' ? _expandPluralForm(ast, this.errors) :
|
||||
_expandDefaultForm(ast, this.errors);
|
||||
return icu.type == 'plural' ? _expandPluralForm(icu, this.errors) :
|
||||
_expandDefaultForm(icu, this.errors);
|
||||
}
|
||||
|
||||
visitExpansionCase(ast: HtmlExpansionCaseAst, context: any): any {
|
||||
visitExpansionCase(icuCase: html.ExpansionCase, context: any): any {
|
||||
throw new Error('Should not be reached');
|
||||
}
|
||||
}
|
||||
|
||||
function _expandPluralForm(ast: HtmlExpansionAst, errors: ParseError[]): HtmlElementAst {
|
||||
function _expandPluralForm(ast: html.Expansion, errors: ParseError[]): html.Element {
|
||||
const children = ast.cases.map(c => {
|
||||
if (PLURAL_CASES.indexOf(c.value) == -1 && !c.value.match(/^=\d+$/)) {
|
||||
errors.push(new ExpansionError(
|
||||
|
@ -93,25 +92,25 @@ function _expandPluralForm(ast: HtmlExpansionAst, errors: ParseError[]): HtmlEle
|
|||
const expansionResult = expandNodes(c.expression);
|
||||
errors.push(...expansionResult.errors);
|
||||
|
||||
return new HtmlElementAst(
|
||||
`template`, [new HtmlAttrAst('ngPluralCase', `${c.value}`, c.valueSourceSpan)],
|
||||
return new html.Element(
|
||||
`template`, [new html.Attribute('ngPluralCase', `${c.value}`, c.valueSourceSpan)],
|
||||
expansionResult.nodes, c.sourceSpan, c.sourceSpan, c.sourceSpan);
|
||||
});
|
||||
const switchAttr = new HtmlAttrAst('[ngPlural]', ast.switchValue, ast.switchValueSourceSpan);
|
||||
return new HtmlElementAst(
|
||||
const switchAttr = new html.Attribute('[ngPlural]', ast.switchValue, ast.switchValueSourceSpan);
|
||||
return new html.Element(
|
||||
'ng-container', [switchAttr], children, ast.sourceSpan, ast.sourceSpan, ast.sourceSpan);
|
||||
}
|
||||
|
||||
function _expandDefaultForm(ast: HtmlExpansionAst, errors: ParseError[]): HtmlElementAst {
|
||||
function _expandDefaultForm(ast: html.Expansion, errors: ParseError[]): html.Element {
|
||||
let children = ast.cases.map(c => {
|
||||
const expansionResult = expandNodes(c.expression);
|
||||
errors.push(...expansionResult.errors);
|
||||
|
||||
return new HtmlElementAst(
|
||||
`template`, [new HtmlAttrAst('ngSwitchCase', `${c.value}`, c.valueSourceSpan)],
|
||||
return new html.Element(
|
||||
`template`, [new html.Attribute('ngSwitchCase', `${c.value}`, c.valueSourceSpan)],
|
||||
expansionResult.nodes, c.sourceSpan, c.sourceSpan, c.sourceSpan);
|
||||
});
|
||||
const switchAttr = new HtmlAttrAst('[ngSwitch]', ast.switchValue, ast.switchValueSourceSpan);
|
||||
return new HtmlElementAst(
|
||||
const switchAttr = new html.Attribute('[ngSwitch]', ast.switchValue, ast.switchValueSourceSpan);
|
||||
return new html.Element(
|
||||
'ng-container', [switchAttr], children, ast.sourceSpan, ast.sourceSpan, ast.sourceSpan);
|
||||
}
|
|
@ -7,13 +7,12 @@
|
|||
*/
|
||||
|
||||
import * as chars from '../chars';
|
||||
import {isBlank, isPresent} from '../facade/lang';
|
||||
import {ParseError, ParseLocation, ParseSourceFile, ParseSourceSpan} from '../parse_util';
|
||||
|
||||
import {HtmlTagContentType, NAMED_ENTITIES, getHtmlTagDefinition} from './html_tags';
|
||||
import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from './interpolation_config';
|
||||
import {NAMED_ENTITIES, TagContentType, TagDefinition} from './tags';
|
||||
|
||||
export enum HtmlTokenType {
|
||||
export enum TokenType {
|
||||
TAG_OPEN_START,
|
||||
TAG_OPEN_END,
|
||||
TAG_OPEN_END_VOID,
|
||||
|
@ -36,26 +35,26 @@ export enum HtmlTokenType {
|
|||
EOF
|
||||
}
|
||||
|
||||
export class HtmlToken {
|
||||
constructor(
|
||||
public type: HtmlTokenType, public parts: string[], public sourceSpan: ParseSourceSpan) {}
|
||||
export class Token {
|
||||
constructor(public type: TokenType, public parts: string[], public sourceSpan: ParseSourceSpan) {}
|
||||
}
|
||||
|
||||
export class HtmlTokenError extends ParseError {
|
||||
constructor(errorMsg: string, public tokenType: HtmlTokenType, span: ParseSourceSpan) {
|
||||
export class TokenError extends ParseError {
|
||||
constructor(errorMsg: string, public tokenType: TokenType, span: ParseSourceSpan) {
|
||||
super(span, errorMsg);
|
||||
}
|
||||
}
|
||||
|
||||
export class HtmlTokenizeResult {
|
||||
constructor(public tokens: HtmlToken[], public errors: HtmlTokenError[]) {}
|
||||
export class TokenizeResult {
|
||||
constructor(public tokens: Token[], public errors: TokenError[]) {}
|
||||
}
|
||||
|
||||
export function tokenizeHtml(
|
||||
sourceContent: string, sourceUrl: string, tokenizeExpansionForms: boolean = false,
|
||||
interpolationConfig: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG): HtmlTokenizeResult {
|
||||
return new _HtmlTokenizer(
|
||||
new ParseSourceFile(sourceContent, sourceUrl), tokenizeExpansionForms,
|
||||
export function tokenize(
|
||||
source: string, url: string, getTagDefinition: (tagName: string) => TagDefinition,
|
||||
tokenizeExpansionForms: boolean = false,
|
||||
interpolationConfig: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG): TokenizeResult {
|
||||
return new _Tokenizer(
|
||||
new ParseSourceFile(source, url), getTagDefinition, tokenizeExpansionForms,
|
||||
interpolationConfig)
|
||||
.tokenize();
|
||||
}
|
||||
|
@ -72,11 +71,11 @@ function _unknownEntityErrorMsg(entitySrc: string): string {
|
|||
}
|
||||
|
||||
class _ControlFlowError {
|
||||
constructor(public error: HtmlTokenError) {}
|
||||
constructor(public error: TokenError) {}
|
||||
}
|
||||
|
||||
// See http://www.w3.org/TR/html51/syntax.html#writing
|
||||
class _HtmlTokenizer {
|
||||
class _Tokenizer {
|
||||
private _input: string;
|
||||
private _length: number;
|
||||
// Note: this is always lowercase!
|
||||
|
@ -86,20 +85,22 @@ class _HtmlTokenizer {
|
|||
private _line: number = 0;
|
||||
private _column: number = -1;
|
||||
private _currentTokenStart: ParseLocation;
|
||||
private _currentTokenType: HtmlTokenType;
|
||||
private _expansionCaseStack: HtmlTokenType[] = [];
|
||||
private _currentTokenType: TokenType;
|
||||
private _expansionCaseStack: TokenType[] = [];
|
||||
private _inInterpolation: boolean = false;
|
||||
|
||||
tokens: HtmlToken[] = [];
|
||||
errors: HtmlTokenError[] = [];
|
||||
tokens: Token[] = [];
|
||||
errors: TokenError[] = [];
|
||||
|
||||
/**
|
||||
* @param _file The html source
|
||||
* @param _getTagDefinition
|
||||
* @param _tokenizeIcu Whether to tokenize ICU messages (considered as text nodes when false)
|
||||
* @param _interpolationConfig
|
||||
*/
|
||||
constructor(
|
||||
private _file: ParseSourceFile, private _tokenizeIcu: boolean,
|
||||
private _file: ParseSourceFile, private _getTagDefinition: (tagName: string) => TagDefinition,
|
||||
private _tokenizeIcu: boolean,
|
||||
private _interpolationConfig: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG) {
|
||||
this._input = _file.content;
|
||||
this._length = _file.content.length;
|
||||
|
@ -114,7 +115,7 @@ class _HtmlTokenizer {
|
|||
return content.replace(_CR_OR_CRLF_REGEXP, '\n');
|
||||
}
|
||||
|
||||
tokenize(): HtmlTokenizeResult {
|
||||
tokenize(): TokenizeResult {
|
||||
while (this._peek !== chars.$EOF) {
|
||||
const start = this._getLocation();
|
||||
try {
|
||||
|
@ -143,9 +144,9 @@ class _HtmlTokenizer {
|
|||
}
|
||||
}
|
||||
}
|
||||
this._beginToken(HtmlTokenType.EOF);
|
||||
this._beginToken(TokenType.EOF);
|
||||
this._endToken([]);
|
||||
return new HtmlTokenizeResult(mergeTextTokens(this.tokens), this.errors);
|
||||
return new TokenizeResult(mergeTextTokens(this.tokens), this.errors);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -188,14 +189,14 @@ class _HtmlTokenizer {
|
|||
return new ParseSourceSpan(start, end);
|
||||
}
|
||||
|
||||
private _beginToken(type: HtmlTokenType, start: ParseLocation = this._getLocation()) {
|
||||
private _beginToken(type: TokenType, start: ParseLocation = this._getLocation()) {
|
||||
this._currentTokenStart = start;
|
||||
this._currentTokenType = type;
|
||||
}
|
||||
|
||||
private _endToken(parts: string[], end: ParseLocation = this._getLocation()): HtmlToken {
|
||||
const token = new HtmlToken(
|
||||
this._currentTokenType, parts, new ParseSourceSpan(this._currentTokenStart, end));
|
||||
private _endToken(parts: string[], end: ParseLocation = this._getLocation()): Token {
|
||||
const token =
|
||||
new Token(this._currentTokenType, parts, new ParseSourceSpan(this._currentTokenStart, end));
|
||||
this.tokens.push(token);
|
||||
this._currentTokenStart = null;
|
||||
this._currentTokenType = null;
|
||||
|
@ -204,9 +205,9 @@ class _HtmlTokenizer {
|
|||
|
||||
private _createError(msg: string, span: ParseSourceSpan): _ControlFlowError {
|
||||
if (this._isInExpansionForm()) {
|
||||
msg += ' (Do you have an unescaped "{" in your template?).';
|
||||
msg += ` (Do you have an unescaped "{" in your template? Use "{{ '{' }}") to escape it.)`;
|
||||
}
|
||||
const error = new HtmlTokenError(msg, this._currentTokenType, span);
|
||||
const error = new TokenError(msg, this._currentTokenType, span);
|
||||
this._currentTokenStart = null;
|
||||
this._currentTokenType = null;
|
||||
return new _ControlFlowError(error);
|
||||
|
@ -343,9 +344,9 @@ class _HtmlTokenizer {
|
|||
return '&';
|
||||
}
|
||||
this._advance();
|
||||
let name = this._input.substring(start.offset + 1, this._index - 1);
|
||||
let char = (NAMED_ENTITIES as any)[name];
|
||||
if (isBlank(char)) {
|
||||
const name = this._input.substring(start.offset + 1, this._index - 1);
|
||||
const char = NAMED_ENTITIES[name];
|
||||
if (!char) {
|
||||
throw this._createError(_unknownEntityErrorMsg(name), this._getSpan(start));
|
||||
}
|
||||
return char;
|
||||
|
@ -353,11 +354,10 @@ class _HtmlTokenizer {
|
|||
}
|
||||
|
||||
private _consumeRawText(
|
||||
decodeEntities: boolean, firstCharOfEnd: number, attemptEndRest: () => boolean): HtmlToken {
|
||||
decodeEntities: boolean, firstCharOfEnd: number, attemptEndRest: () => boolean): Token {
|
||||
let tagCloseStart: ParseLocation;
|
||||
const textStart = this._getLocation();
|
||||
this._beginToken(
|
||||
decodeEntities ? HtmlTokenType.ESCAPABLE_RAW_TEXT : HtmlTokenType.RAW_TEXT, textStart);
|
||||
this._beginToken(decodeEntities ? TokenType.ESCAPABLE_RAW_TEXT : TokenType.RAW_TEXT, textStart);
|
||||
const parts: string[] = [];
|
||||
while (true) {
|
||||
tagCloseStart = this._getLocation();
|
||||
|
@ -376,25 +376,25 @@ class _HtmlTokenizer {
|
|||
}
|
||||
|
||||
private _consumeComment(start: ParseLocation) {
|
||||
this._beginToken(HtmlTokenType.COMMENT_START, start);
|
||||
this._beginToken(TokenType.COMMENT_START, start);
|
||||
this._requireCharCode(chars.$MINUS);
|
||||
this._endToken([]);
|
||||
const textToken = this._consumeRawText(false, chars.$MINUS, () => this._attemptStr('->'));
|
||||
this._beginToken(HtmlTokenType.COMMENT_END, textToken.sourceSpan.end);
|
||||
this._beginToken(TokenType.COMMENT_END, textToken.sourceSpan.end);
|
||||
this._endToken([]);
|
||||
}
|
||||
|
||||
private _consumeCdata(start: ParseLocation) {
|
||||
this._beginToken(HtmlTokenType.CDATA_START, start);
|
||||
this._beginToken(TokenType.CDATA_START, start);
|
||||
this._requireStr('CDATA[');
|
||||
this._endToken([]);
|
||||
const textToken = this._consumeRawText(false, chars.$RBRACKET, () => this._attemptStr(']>'));
|
||||
this._beginToken(HtmlTokenType.CDATA_END, textToken.sourceSpan.end);
|
||||
this._beginToken(TokenType.CDATA_END, textToken.sourceSpan.end);
|
||||
this._endToken([]);
|
||||
}
|
||||
|
||||
private _consumeDocType(start: ParseLocation) {
|
||||
this._beginToken(HtmlTokenType.DOC_TYPE, start);
|
||||
this._beginToken(TokenType.DOC_TYPE, start);
|
||||
this._attemptUntilChar(chars.$GT);
|
||||
this._advance();
|
||||
this._endToken([this._input.substring(start.offset + 2, this._index - 1)]);
|
||||
|
@ -421,6 +421,7 @@ class _HtmlTokenizer {
|
|||
|
||||
private _consumeTagOpen(start: ParseLocation) {
|
||||
let savedPos = this._savePosition();
|
||||
let tagName: string;
|
||||
let lowercaseTagName: string;
|
||||
try {
|
||||
if (!chars.isAsciiLetter(this._peek)) {
|
||||
|
@ -428,7 +429,8 @@ class _HtmlTokenizer {
|
|||
}
|
||||
const nameStart = this._index;
|
||||
this._consumeTagOpenStart(start);
|
||||
lowercaseTagName = this._input.substring(nameStart, this._index).toLowerCase();
|
||||
tagName = this._input.substring(nameStart, this._index);
|
||||
lowercaseTagName = tagName.toLowerCase();
|
||||
this._attemptCharCodeUntilFn(isNotWhitespace);
|
||||
while (this._peek !== chars.$SLASH && this._peek !== chars.$GT) {
|
||||
this._consumeAttributeName();
|
||||
|
@ -445,7 +447,7 @@ class _HtmlTokenizer {
|
|||
// When the start tag is invalid, assume we want a "<"
|
||||
this._restorePosition(savedPos);
|
||||
// Back to back text tokens are merged at the end
|
||||
this._beginToken(HtmlTokenType.TEXT, start);
|
||||
this._beginToken(TokenType.TEXT, start);
|
||||
this._endToken(['<']);
|
||||
return;
|
||||
}
|
||||
|
@ -453,10 +455,11 @@ class _HtmlTokenizer {
|
|||
throw e;
|
||||
}
|
||||
|
||||
const contentTokenType = getHtmlTagDefinition(lowercaseTagName).contentType;
|
||||
if (contentTokenType === HtmlTagContentType.RAW_TEXT) {
|
||||
const contentTokenType = this._getTagDefinition(tagName).contentType;
|
||||
|
||||
if (contentTokenType === TagContentType.RAW_TEXT) {
|
||||
this._consumeRawTextWithTagClose(lowercaseTagName, false);
|
||||
} else if (contentTokenType === HtmlTagContentType.ESCAPABLE_RAW_TEXT) {
|
||||
} else if (contentTokenType === TagContentType.ESCAPABLE_RAW_TEXT) {
|
||||
this._consumeRawTextWithTagClose(lowercaseTagName, true);
|
||||
}
|
||||
}
|
||||
|
@ -469,24 +472,24 @@ class _HtmlTokenizer {
|
|||
this._attemptCharCodeUntilFn(isNotWhitespace);
|
||||
return this._attemptCharCode(chars.$GT);
|
||||
});
|
||||
this._beginToken(HtmlTokenType.TAG_CLOSE, textToken.sourceSpan.end);
|
||||
this._beginToken(TokenType.TAG_CLOSE, textToken.sourceSpan.end);
|
||||
this._endToken([null, lowercaseTagName]);
|
||||
}
|
||||
|
||||
private _consumeTagOpenStart(start: ParseLocation) {
|
||||
this._beginToken(HtmlTokenType.TAG_OPEN_START, start);
|
||||
this._beginToken(TokenType.TAG_OPEN_START, start);
|
||||
const parts = this._consumePrefixAndName();
|
||||
this._endToken(parts);
|
||||
}
|
||||
|
||||
private _consumeAttributeName() {
|
||||
this._beginToken(HtmlTokenType.ATTR_NAME);
|
||||
this._beginToken(TokenType.ATTR_NAME);
|
||||
const prefixAndName = this._consumePrefixAndName();
|
||||
this._endToken(prefixAndName);
|
||||
}
|
||||
|
||||
private _consumeAttributeValue() {
|
||||
this._beginToken(HtmlTokenType.ATTR_VALUE);
|
||||
this._beginToken(TokenType.ATTR_VALUE);
|
||||
var value: string;
|
||||
if (this._peek === chars.$SQ || this._peek === chars.$DQ) {
|
||||
var quoteChar = this._peek;
|
||||
|
@ -506,15 +509,15 @@ class _HtmlTokenizer {
|
|||
}
|
||||
|
||||
private _consumeTagOpenEnd() {
|
||||
const tokenType = this._attemptCharCode(chars.$SLASH) ? HtmlTokenType.TAG_OPEN_END_VOID :
|
||||
HtmlTokenType.TAG_OPEN_END;
|
||||
const tokenType =
|
||||
this._attemptCharCode(chars.$SLASH) ? TokenType.TAG_OPEN_END_VOID : TokenType.TAG_OPEN_END;
|
||||
this._beginToken(tokenType);
|
||||
this._requireCharCode(chars.$GT);
|
||||
this._endToken([]);
|
||||
}
|
||||
|
||||
private _consumeTagClose(start: ParseLocation) {
|
||||
this._beginToken(HtmlTokenType.TAG_CLOSE, start);
|
||||
this._beginToken(TokenType.TAG_CLOSE, start);
|
||||
this._attemptCharCodeUntilFn(isNotWhitespace);
|
||||
let prefixAndName = this._consumePrefixAndName();
|
||||
this._attemptCharCodeUntilFn(isNotWhitespace);
|
||||
|
@ -523,19 +526,19 @@ class _HtmlTokenizer {
|
|||
}
|
||||
|
||||
private _consumeExpansionFormStart() {
|
||||
this._beginToken(HtmlTokenType.EXPANSION_FORM_START, this._getLocation());
|
||||
this._beginToken(TokenType.EXPANSION_FORM_START, this._getLocation());
|
||||
this._requireCharCode(chars.$LBRACE);
|
||||
this._endToken([]);
|
||||
|
||||
this._expansionCaseStack.push(HtmlTokenType.EXPANSION_FORM_START);
|
||||
this._expansionCaseStack.push(TokenType.EXPANSION_FORM_START);
|
||||
|
||||
this._beginToken(HtmlTokenType.RAW_TEXT, this._getLocation());
|
||||
this._beginToken(TokenType.RAW_TEXT, this._getLocation());
|
||||
const condition = this._readUntil(chars.$COMMA);
|
||||
this._endToken([condition], this._getLocation());
|
||||
this._requireCharCode(chars.$COMMA);
|
||||
this._attemptCharCodeUntilFn(isNotWhitespace);
|
||||
|
||||
this._beginToken(HtmlTokenType.RAW_TEXT, this._getLocation());
|
||||
this._beginToken(TokenType.RAW_TEXT, this._getLocation());
|
||||
let type = this._readUntil(chars.$COMMA);
|
||||
this._endToken([type], this._getLocation());
|
||||
this._requireCharCode(chars.$COMMA);
|
||||
|
@ -543,21 +546,21 @@ class _HtmlTokenizer {
|
|||
}
|
||||
|
||||
private _consumeExpansionCaseStart() {
|
||||
this._beginToken(HtmlTokenType.EXPANSION_CASE_VALUE, this._getLocation());
|
||||
this._beginToken(TokenType.EXPANSION_CASE_VALUE, this._getLocation());
|
||||
const value = this._readUntil(chars.$LBRACE).trim();
|
||||
this._endToken([value], this._getLocation());
|
||||
this._attemptCharCodeUntilFn(isNotWhitespace);
|
||||
|
||||
this._beginToken(HtmlTokenType.EXPANSION_CASE_EXP_START, this._getLocation());
|
||||
this._beginToken(TokenType.EXPANSION_CASE_EXP_START, this._getLocation());
|
||||
this._requireCharCode(chars.$LBRACE);
|
||||
this._endToken([], this._getLocation());
|
||||
this._attemptCharCodeUntilFn(isNotWhitespace);
|
||||
|
||||
this._expansionCaseStack.push(HtmlTokenType.EXPANSION_CASE_EXP_START);
|
||||
this._expansionCaseStack.push(TokenType.EXPANSION_CASE_EXP_START);
|
||||
}
|
||||
|
||||
private _consumeExpansionCaseEnd() {
|
||||
this._beginToken(HtmlTokenType.EXPANSION_CASE_EXP_END, this._getLocation());
|
||||
this._beginToken(TokenType.EXPANSION_CASE_EXP_END, this._getLocation());
|
||||
this._requireCharCode(chars.$RBRACE);
|
||||
this._endToken([], this._getLocation());
|
||||
this._attemptCharCodeUntilFn(isNotWhitespace);
|
||||
|
@ -566,7 +569,7 @@ class _HtmlTokenizer {
|
|||
}
|
||||
|
||||
private _consumeExpansionFormEnd() {
|
||||
this._beginToken(HtmlTokenType.EXPANSION_FORM_END, this._getLocation());
|
||||
this._beginToken(TokenType.EXPANSION_FORM_END, this._getLocation());
|
||||
this._requireCharCode(chars.$RBRACE);
|
||||
this._endToken([]);
|
||||
|
||||
|
@ -575,14 +578,16 @@ class _HtmlTokenizer {
|
|||
|
||||
private _consumeText() {
|
||||
const start = this._getLocation();
|
||||
this._beginToken(HtmlTokenType.TEXT, start);
|
||||
this._beginToken(TokenType.TEXT, start);
|
||||
const parts: string[] = [];
|
||||
|
||||
do {
|
||||
if (this._attemptStr(this._interpolationConfig.start)) {
|
||||
if (this._interpolationConfig && this._attemptStr(this._interpolationConfig.start)) {
|
||||
parts.push(this._interpolationConfig.start);
|
||||
this._inInterpolation = true;
|
||||
} else if (this._attemptStr(this._interpolationConfig.end) && this._inInterpolation) {
|
||||
} else if (
|
||||
this._interpolationConfig && this._attemptStr(this._interpolationConfig.end) &&
|
||||
this._inInterpolation) {
|
||||
parts.push(this._interpolationConfig.end);
|
||||
this._inInterpolation = false;
|
||||
} else {
|
||||
|
@ -638,13 +643,13 @@ class _HtmlTokenizer {
|
|||
private _isInExpansionCase(): boolean {
|
||||
return this._expansionCaseStack.length > 0 &&
|
||||
this._expansionCaseStack[this._expansionCaseStack.length - 1] ===
|
||||
HtmlTokenType.EXPANSION_CASE_EXP_START;
|
||||
TokenType.EXPANSION_CASE_EXP_START;
|
||||
}
|
||||
|
||||
private _isInExpansionForm(): boolean {
|
||||
return this._expansionCaseStack.length > 0 &&
|
||||
this._expansionCaseStack[this._expansionCaseStack.length - 1] ===
|
||||
HtmlTokenType.EXPANSION_FORM_START;
|
||||
TokenType.EXPANSION_FORM_START;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -672,8 +677,10 @@ function isNamedEntityEnd(code: number): boolean {
|
|||
|
||||
function isExpansionFormStart(
|
||||
input: string, offset: number, interpolationConfig: InterpolationConfig): boolean {
|
||||
return input.charCodeAt(offset) == chars.$LBRACE &&
|
||||
input.indexOf(interpolationConfig.start, offset) != offset;
|
||||
const isInterpolationStart =
|
||||
interpolationConfig ? input.indexOf(interpolationConfig.start, offset) == offset : false;
|
||||
|
||||
return input.charCodeAt(offset) == chars.$LBRACE && !isInterpolationStart;
|
||||
}
|
||||
|
||||
function isExpansionCaseStart(peek: number): boolean {
|
||||
|
@ -688,13 +695,12 @@ function toUpperCaseCharCode(code: number): number {
|
|||
return code >= chars.$a && code <= chars.$z ? code - chars.$a + chars.$A : code;
|
||||
}
|
||||
|
||||
function mergeTextTokens(srcTokens: HtmlToken[]): HtmlToken[] {
|
||||
let dstTokens: HtmlToken[] = [];
|
||||
let lastDstToken: HtmlToken;
|
||||
function mergeTextTokens(srcTokens: Token[]): Token[] {
|
||||
let dstTokens: Token[] = [];
|
||||
let lastDstToken: Token;
|
||||
for (let i = 0; i < srcTokens.length; i++) {
|
||||
let token = srcTokens[i];
|
||||
if (isPresent(lastDstToken) && lastDstToken.type == HtmlTokenType.TEXT &&
|
||||
token.type == HtmlTokenType.TEXT) {
|
||||
if (lastDstToken && lastDstToken.type == TokenType.TEXT && token.type == TokenType.TEXT) {
|
||||
lastDstToken.parts[0] += token.parts[0];
|
||||
lastDstToken.sourceSpan.end = token.sourceSpan.end;
|
||||
} else {
|
|
@ -0,0 +1,412 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {isPresent, isBlank,} from '../facade/lang';
|
||||
import {ListWrapper} from '../facade/collection';
|
||||
import * as html from './ast';
|
||||
import * as lex from './lexer';
|
||||
import {ParseSourceSpan, ParseError} from '../parse_util';
|
||||
import {TagDefinition, getNsPrefix, mergeNsAndName} from './tags';
|
||||
import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from './interpolation_config';
|
||||
|
||||
export class TreeError extends ParseError {
|
||||
static create(elementName: string, span: ParseSourceSpan, msg: string): TreeError {
|
||||
return new TreeError(elementName, span, msg);
|
||||
}
|
||||
|
||||
constructor(public elementName: string, span: ParseSourceSpan, msg: string) { super(span, msg); }
|
||||
}
|
||||
|
||||
export class ParseTreeResult {
|
||||
constructor(public rootNodes: html.Node[], public errors: ParseError[]) {}
|
||||
}
|
||||
|
||||
export class Parser {
|
||||
constructor(private _getTagDefinition: (tagName: string) => TagDefinition) {}
|
||||
|
||||
parse(
|
||||
source: string, url: string, parseExpansionForms: boolean = false,
|
||||
interpolationConfig: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG): ParseTreeResult {
|
||||
const tokensAndErrors =
|
||||
lex.tokenize(source, url, this._getTagDefinition, parseExpansionForms, interpolationConfig);
|
||||
|
||||
const treeAndErrors = new _TreeBuilder(tokensAndErrors.tokens, this._getTagDefinition).build();
|
||||
|
||||
return new ParseTreeResult(
|
||||
treeAndErrors.rootNodes,
|
||||
(<ParseError[]>tokensAndErrors.errors).concat(treeAndErrors.errors));
|
||||
}
|
||||
}
|
||||
|
||||
class _TreeBuilder {
|
||||
private _index: number = -1;
|
||||
private _peek: lex.Token;
|
||||
|
||||
private _rootNodes: html.Node[] = [];
|
||||
private _errors: TreeError[] = [];
|
||||
|
||||
private _elementStack: html.Element[] = [];
|
||||
|
||||
constructor(
|
||||
private tokens: lex.Token[], private getTagDefinition: (tagName: string) => TagDefinition) {
|
||||
this._advance();
|
||||
}
|
||||
|
||||
build(): ParseTreeResult {
|
||||
while (this._peek.type !== lex.TokenType.EOF) {
|
||||
if (this._peek.type === lex.TokenType.TAG_OPEN_START) {
|
||||
this._consumeStartTag(this._advance());
|
||||
} else if (this._peek.type === lex.TokenType.TAG_CLOSE) {
|
||||
this._consumeEndTag(this._advance());
|
||||
} else if (this._peek.type === lex.TokenType.CDATA_START) {
|
||||
this._closeVoidElement();
|
||||
this._consumeCdata(this._advance());
|
||||
} else if (this._peek.type === lex.TokenType.COMMENT_START) {
|
||||
this._closeVoidElement();
|
||||
this._consumeComment(this._advance());
|
||||
} else if (
|
||||
this._peek.type === lex.TokenType.TEXT || this._peek.type === lex.TokenType.RAW_TEXT ||
|
||||
this._peek.type === lex.TokenType.ESCAPABLE_RAW_TEXT) {
|
||||
this._closeVoidElement();
|
||||
this._consumeText(this._advance());
|
||||
} else if (this._peek.type === lex.TokenType.EXPANSION_FORM_START) {
|
||||
this._consumeExpansion(this._advance());
|
||||
} else {
|
||||
// Skip all other tokens...
|
||||
this._advance();
|
||||
}
|
||||
}
|
||||
return new ParseTreeResult(this._rootNodes, this._errors);
|
||||
}
|
||||
|
||||
private _advance(): lex.Token {
|
||||
const prev = this._peek;
|
||||
if (this._index < this.tokens.length - 1) {
|
||||
// Note: there is always an EOF token at the end
|
||||
this._index++;
|
||||
}
|
||||
this._peek = this.tokens[this._index];
|
||||
return prev;
|
||||
}
|
||||
|
||||
private _advanceIf(type: lex.TokenType): lex.Token {
|
||||
if (this._peek.type === type) {
|
||||
return this._advance();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private _consumeCdata(startToken: lex.Token) {
|
||||
this._consumeText(this._advance());
|
||||
this._advanceIf(lex.TokenType.CDATA_END);
|
||||
}
|
||||
|
||||
private _consumeComment(token: lex.Token) {
|
||||
const text = this._advanceIf(lex.TokenType.RAW_TEXT);
|
||||
this._advanceIf(lex.TokenType.COMMENT_END);
|
||||
const value = isPresent(text) ? text.parts[0].trim() : null;
|
||||
this._addToParent(new html.Comment(value, token.sourceSpan));
|
||||
}
|
||||
|
||||
private _consumeExpansion(token: lex.Token) {
|
||||
const switchValue = this._advance();
|
||||
|
||||
const type = this._advance();
|
||||
const cases: html.ExpansionCase[] = [];
|
||||
|
||||
// read =
|
||||
while (this._peek.type === lex.TokenType.EXPANSION_CASE_VALUE) {
|
||||
let expCase = this._parseExpansionCase();
|
||||
if (isBlank(expCase)) return; // error
|
||||
cases.push(expCase);
|
||||
}
|
||||
|
||||
// read the final }
|
||||
if (this._peek.type !== lex.TokenType.EXPANSION_FORM_END) {
|
||||
this._errors.push(
|
||||
TreeError.create(null, this._peek.sourceSpan, `Invalid ICU message. Missing '}'.`));
|
||||
return;
|
||||
}
|
||||
const sourceSpan = new ParseSourceSpan(token.sourceSpan.start, this._peek.sourceSpan.end);
|
||||
this._addToParent(new html.Expansion(
|
||||
switchValue.parts[0], type.parts[0], cases, sourceSpan, switchValue.sourceSpan));
|
||||
|
||||
this._advance();
|
||||
}
|
||||
|
||||
private _parseExpansionCase(): html.ExpansionCase {
|
||||
const value = this._advance();
|
||||
|
||||
// read {
|
||||
if (this._peek.type !== lex.TokenType.EXPANSION_CASE_EXP_START) {
|
||||
this._errors.push(
|
||||
TreeError.create(null, this._peek.sourceSpan, `Invalid ICU message. Missing '{'.`));
|
||||
return null;
|
||||
}
|
||||
|
||||
// read until }
|
||||
const start = this._advance();
|
||||
|
||||
const exp = this._collectExpansionExpTokens(start);
|
||||
if (isBlank(exp)) return null;
|
||||
|
||||
const end = this._advance();
|
||||
exp.push(new lex.Token(lex.TokenType.EOF, [], end.sourceSpan));
|
||||
|
||||
// parse everything in between { and }
|
||||
const parsedExp = new _TreeBuilder(exp, this.getTagDefinition).build();
|
||||
if (parsedExp.errors.length > 0) {
|
||||
this._errors = this._errors.concat(<TreeError[]>parsedExp.errors);
|
||||
return null;
|
||||
}
|
||||
|
||||
const sourceSpan = new ParseSourceSpan(value.sourceSpan.start, end.sourceSpan.end);
|
||||
const expSourceSpan = new ParseSourceSpan(start.sourceSpan.start, end.sourceSpan.end);
|
||||
return new html.ExpansionCase(
|
||||
value.parts[0], parsedExp.rootNodes, sourceSpan, value.sourceSpan, expSourceSpan);
|
||||
}
|
||||
|
||||
private _collectExpansionExpTokens(start: lex.Token): lex.Token[] {
|
||||
const exp: lex.Token[] = [];
|
||||
const expansionFormStack = [lex.TokenType.EXPANSION_CASE_EXP_START];
|
||||
|
||||
while (true) {
|
||||
if (this._peek.type === lex.TokenType.EXPANSION_FORM_START ||
|
||||
this._peek.type === lex.TokenType.EXPANSION_CASE_EXP_START) {
|
||||
expansionFormStack.push(this._peek.type);
|
||||
}
|
||||
|
||||
if (this._peek.type === lex.TokenType.EXPANSION_CASE_EXP_END) {
|
||||
if (lastOnStack(expansionFormStack, lex.TokenType.EXPANSION_CASE_EXP_START)) {
|
||||
expansionFormStack.pop();
|
||||
if (expansionFormStack.length == 0) return exp;
|
||||
|
||||
} else {
|
||||
this._errors.push(
|
||||
TreeError.create(null, start.sourceSpan, `Invalid ICU message. Missing '}'.`));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (this._peek.type === lex.TokenType.EXPANSION_FORM_END) {
|
||||
if (lastOnStack(expansionFormStack, lex.TokenType.EXPANSION_FORM_START)) {
|
||||
expansionFormStack.pop();
|
||||
} else {
|
||||
this._errors.push(
|
||||
TreeError.create(null, start.sourceSpan, `Invalid ICU message. Missing '}'.`));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (this._peek.type === lex.TokenType.EOF) {
|
||||
this._errors.push(
|
||||
TreeError.create(null, start.sourceSpan, `Invalid ICU message. Missing '}'.`));
|
||||
return null;
|
||||
}
|
||||
|
||||
exp.push(this._advance());
|
||||
}
|
||||
}
|
||||
|
||||
private _consumeText(token: lex.Token) {
|
||||
let text = token.parts[0];
|
||||
if (text.length > 0 && text[0] == '\n') {
|
||||
const parent = this._getParentElement();
|
||||
if (isPresent(parent) && parent.children.length == 0 &&
|
||||
this.getTagDefinition(parent.name).ignoreFirstLf) {
|
||||
text = text.substring(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (text.length > 0) {
|
||||
this._addToParent(new html.Text(text, token.sourceSpan));
|
||||
}
|
||||
}
|
||||
|
||||
private _closeVoidElement(): void {
|
||||
if (this._elementStack.length > 0) {
|
||||
const el = ListWrapper.last(this._elementStack);
|
||||
|
||||
if (this.getTagDefinition(el.name).isVoid) {
|
||||
this._elementStack.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _consumeStartTag(startTagToken: lex.Token) {
|
||||
const prefix = startTagToken.parts[0];
|
||||
const name = startTagToken.parts[1];
|
||||
const attrs: html.Attribute[] = [];
|
||||
while (this._peek.type === lex.TokenType.ATTR_NAME) {
|
||||
attrs.push(this._consumeAttr(this._advance()));
|
||||
}
|
||||
const fullName = this._getElementFullName(prefix, name, this._getParentElement());
|
||||
let selfClosing = false;
|
||||
// Note: There could have been a tokenizer error
|
||||
// so that we don't get a token for the end tag...
|
||||
if (this._peek.type === lex.TokenType.TAG_OPEN_END_VOID) {
|
||||
this._advance();
|
||||
selfClosing = true;
|
||||
const tagDef = this.getTagDefinition(fullName);
|
||||
if (!(tagDef.canSelfClose || getNsPrefix(fullName) !== null || tagDef.isVoid)) {
|
||||
this._errors.push(TreeError.create(
|
||||
fullName, startTagToken.sourceSpan,
|
||||
`Only void and foreign elements can be self closed "${startTagToken.parts[1]}"`));
|
||||
}
|
||||
} else if (this._peek.type === lex.TokenType.TAG_OPEN_END) {
|
||||
this._advance();
|
||||
selfClosing = false;
|
||||
}
|
||||
const end = this._peek.sourceSpan.start;
|
||||
const span = new ParseSourceSpan(startTagToken.sourceSpan.start, end);
|
||||
const el = new html.Element(fullName, attrs, [], span, span, null);
|
||||
this._pushElement(el);
|
||||
if (selfClosing) {
|
||||
this._popElement(fullName);
|
||||
el.endSourceSpan = span;
|
||||
}
|
||||
}
|
||||
|
||||
private _pushElement(el: html.Element) {
|
||||
if (this._elementStack.length > 0) {
|
||||
const parentEl = ListWrapper.last(this._elementStack);
|
||||
if (this.getTagDefinition(parentEl.name).isClosedByChild(el.name)) {
|
||||
this._elementStack.pop();
|
||||
}
|
||||
}
|
||||
|
||||
const tagDef = this.getTagDefinition(el.name);
|
||||
const {parent, container} = this._getParentElementSkippingContainers();
|
||||
|
||||
if (isPresent(parent) && tagDef.requireExtraParent(parent.name)) {
|
||||
const newParent = new html.Element(
|
||||
tagDef.parentToAdd, [], [], el.sourceSpan, el.startSourceSpan, el.endSourceSpan);
|
||||
this._insertBeforeContainer(parent, container, newParent);
|
||||
}
|
||||
|
||||
this._addToParent(el);
|
||||
this._elementStack.push(el);
|
||||
}
|
||||
|
||||
private _consumeEndTag(endTagToken: lex.Token) {
|
||||
const fullName = this._getElementFullName(
|
||||
endTagToken.parts[0], endTagToken.parts[1], this._getParentElement());
|
||||
|
||||
if (this._getParentElement()) {
|
||||
this._getParentElement().endSourceSpan = endTagToken.sourceSpan;
|
||||
}
|
||||
|
||||
if (this.getTagDefinition(fullName).isVoid) {
|
||||
this._errors.push(TreeError.create(
|
||||
fullName, endTagToken.sourceSpan,
|
||||
`Void elements do not have end tags "${endTagToken.parts[1]}"`));
|
||||
} else if (!this._popElement(fullName)) {
|
||||
this._errors.push(TreeError.create(
|
||||
fullName, endTagToken.sourceSpan, `Unexpected closing tag "${endTagToken.parts[1]}"`));
|
||||
}
|
||||
}
|
||||
|
||||
private _popElement(fullName: string): boolean {
|
||||
for (let stackIndex = this._elementStack.length - 1; stackIndex >= 0; stackIndex--) {
|
||||
const el = this._elementStack[stackIndex];
|
||||
if (el.name == fullName) {
|
||||
ListWrapper.splice(this._elementStack, stackIndex, this._elementStack.length - stackIndex);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!this.getTagDefinition(el.name).closedByParent) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private _consumeAttr(attrName: lex.Token): html.Attribute {
|
||||
const fullName = mergeNsAndName(attrName.parts[0], attrName.parts[1]);
|
||||
let end = attrName.sourceSpan.end;
|
||||
let value = '';
|
||||
if (this._peek.type === lex.TokenType.ATTR_VALUE) {
|
||||
const valueToken = this._advance();
|
||||
value = valueToken.parts[0];
|
||||
end = valueToken.sourceSpan.end;
|
||||
}
|
||||
return new html.Attribute(fullName, value, new ParseSourceSpan(attrName.sourceSpan.start, end));
|
||||
}
|
||||
|
||||
private _getParentElement(): html.Element {
|
||||
return this._elementStack.length > 0 ? ListWrapper.last(this._elementStack) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the parent in the DOM and the container.
|
||||
*
|
||||
* `<ng-container>` elements are skipped as they are not rendered as DOM element.
|
||||
*/
|
||||
private _getParentElementSkippingContainers(): {parent: html.Element, container: html.Element} {
|
||||
let container: html.Element = null;
|
||||
|
||||
for (let i = this._elementStack.length - 1; i >= 0; i--) {
|
||||
if (this._elementStack[i].name !== 'ng-container') {
|
||||
return {parent: this._elementStack[i], container};
|
||||
}
|
||||
container = this._elementStack[i];
|
||||
}
|
||||
|
||||
return {parent: ListWrapper.last(this._elementStack), container};
|
||||
}
|
||||
|
||||
private _addToParent(node: html.Node) {
|
||||
const parent = this._getParentElement();
|
||||
if (isPresent(parent)) {
|
||||
parent.children.push(node);
|
||||
} else {
|
||||
this._rootNodes.push(node);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert a node between the parent and the container.
|
||||
* When no container is given, the node is appended as a child of the parent.
|
||||
* Also updates the element stack accordingly.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
private _insertBeforeContainer(
|
||||
parent: html.Element, container: html.Element, node: html.Element) {
|
||||
if (!container) {
|
||||
this._addToParent(node);
|
||||
this._elementStack.push(node);
|
||||
} else {
|
||||
if (parent) {
|
||||
// replace the container with the new node in the children
|
||||
const index = parent.children.indexOf(container);
|
||||
parent.children[index] = node;
|
||||
} else {
|
||||
this._rootNodes.push(node);
|
||||
}
|
||||
node.children.push(container);
|
||||
this._elementStack.splice(this._elementStack.indexOf(container), 0, node);
|
||||
}
|
||||
}
|
||||
|
||||
private _getElementFullName(prefix: string, localName: string, parentElement: html.Element):
|
||||
string {
|
||||
if (isBlank(prefix)) {
|
||||
prefix = this.getTagDefinition(localName).implicitNamespacePrefix;
|
||||
if (isBlank(prefix) && isPresent(parentElement)) {
|
||||
prefix = getNsPrefix(parentElement.name);
|
||||
}
|
||||
}
|
||||
|
||||
return mergeNsAndName(prefix, localName);
|
||||
}
|
||||
}
|
||||
|
||||
function lastOnStack(stack: any[], element: any): boolean {
|
||||
return stack.length > 0 && stack[stack.length - 1] === element;
|
||||
}
|
|
@ -0,0 +1,310 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
export enum TagContentType {
|
||||
RAW_TEXT,
|
||||
ESCAPABLE_RAW_TEXT,
|
||||
PARSABLE_DATA
|
||||
}
|
||||
|
||||
// TODO(vicb): read-only when TS supports it
|
||||
export interface TagDefinition {
|
||||
closedByParent: boolean;
|
||||
requiredParents: {[key: string]: boolean};
|
||||
parentToAdd: string;
|
||||
implicitNamespacePrefix: string;
|
||||
contentType: TagContentType;
|
||||
isVoid: boolean;
|
||||
ignoreFirstLf: boolean;
|
||||
canSelfClose: boolean;
|
||||
|
||||
requireExtraParent(currentParent: string): boolean;
|
||||
|
||||
isClosedByChild(name: string): boolean;
|
||||
}
|
||||
|
||||
export function splitNsName(elementName: string): [string, string] {
|
||||
if (elementName[0] != ':') {
|
||||
return [null, elementName];
|
||||
}
|
||||
|
||||
const parts = elementName.substring(1).split(':', 2);
|
||||
|
||||
if (parts.length != 2) {
|
||||
throw new Error(`Unsupported format "${elementName}" expecting ":namespace:name"`);
|
||||
}
|
||||
|
||||
return parts as[string, string];
|
||||
}
|
||||
|
||||
export function getNsPrefix(fullName: string): string {
|
||||
return fullName === null ? null : splitNsName(fullName)[0];
|
||||
}
|
||||
|
||||
export function mergeNsAndName(prefix: string, localName: string): string {
|
||||
return prefix ? `:${prefix}:${localName}` : localName;
|
||||
}
|
||||
|
||||
// see http://www.w3.org/TR/html51/syntax.html#named-character-references
|
||||
// see https://html.spec.whatwg.org/multipage/entities.json
|
||||
// This list is not exhaustive to keep the compiler footprint low.
|
||||
// The `{` / `ƫ` syntax should be used when the named character reference does not exist.
|
||||
export const NAMED_ENTITIES: {[k: string]: string} = {
|
||||
'Aacute': '\u00C1',
|
||||
'aacute': '\u00E1',
|
||||
'Acirc': '\u00C2',
|
||||
'acirc': '\u00E2',
|
||||
'acute': '\u00B4',
|
||||
'AElig': '\u00C6',
|
||||
'aelig': '\u00E6',
|
||||
'Agrave': '\u00C0',
|
||||
'agrave': '\u00E0',
|
||||
'alefsym': '\u2135',
|
||||
'Alpha': '\u0391',
|
||||
'alpha': '\u03B1',
|
||||
'amp': '&',
|
||||
'and': '\u2227',
|
||||
'ang': '\u2220',
|
||||
'apos': '\u0027',
|
||||
'Aring': '\u00C5',
|
||||
'aring': '\u00E5',
|
||||
'asymp': '\u2248',
|
||||
'Atilde': '\u00C3',
|
||||
'atilde': '\u00E3',
|
||||
'Auml': '\u00C4',
|
||||
'auml': '\u00E4',
|
||||
'bdquo': '\u201E',
|
||||
'Beta': '\u0392',
|
||||
'beta': '\u03B2',
|
||||
'brvbar': '\u00A6',
|
||||
'bull': '\u2022',
|
||||
'cap': '\u2229',
|
||||
'Ccedil': '\u00C7',
|
||||
'ccedil': '\u00E7',
|
||||
'cedil': '\u00B8',
|
||||
'cent': '\u00A2',
|
||||
'Chi': '\u03A7',
|
||||
'chi': '\u03C7',
|
||||
'circ': '\u02C6',
|
||||
'clubs': '\u2663',
|
||||
'cong': '\u2245',
|
||||
'copy': '\u00A9',
|
||||
'crarr': '\u21B5',
|
||||
'cup': '\u222A',
|
||||
'curren': '\u00A4',
|
||||
'dagger': '\u2020',
|
||||
'Dagger': '\u2021',
|
||||
'darr': '\u2193',
|
||||
'dArr': '\u21D3',
|
||||
'deg': '\u00B0',
|
||||
'Delta': '\u0394',
|
||||
'delta': '\u03B4',
|
||||
'diams': '\u2666',
|
||||
'divide': '\u00F7',
|
||||
'Eacute': '\u00C9',
|
||||
'eacute': '\u00E9',
|
||||
'Ecirc': '\u00CA',
|
||||
'ecirc': '\u00EA',
|
||||
'Egrave': '\u00C8',
|
||||
'egrave': '\u00E8',
|
||||
'empty': '\u2205',
|
||||
'emsp': '\u2003',
|
||||
'ensp': '\u2002',
|
||||
'Epsilon': '\u0395',
|
||||
'epsilon': '\u03B5',
|
||||
'equiv': '\u2261',
|
||||
'Eta': '\u0397',
|
||||
'eta': '\u03B7',
|
||||
'ETH': '\u00D0',
|
||||
'eth': '\u00F0',
|
||||
'Euml': '\u00CB',
|
||||
'euml': '\u00EB',
|
||||
'euro': '\u20AC',
|
||||
'exist': '\u2203',
|
||||
'fnof': '\u0192',
|
||||
'forall': '\u2200',
|
||||
'frac12': '\u00BD',
|
||||
'frac14': '\u00BC',
|
||||
'frac34': '\u00BE',
|
||||
'frasl': '\u2044',
|
||||
'Gamma': '\u0393',
|
||||
'gamma': '\u03B3',
|
||||
'ge': '\u2265',
|
||||
'gt': '>',
|
||||
'harr': '\u2194',
|
||||
'hArr': '\u21D4',
|
||||
'hearts': '\u2665',
|
||||
'hellip': '\u2026',
|
||||
'Iacute': '\u00CD',
|
||||
'iacute': '\u00ED',
|
||||
'Icirc': '\u00CE',
|
||||
'icirc': '\u00EE',
|
||||
'iexcl': '\u00A1',
|
||||
'Igrave': '\u00CC',
|
||||
'igrave': '\u00EC',
|
||||
'image': '\u2111',
|
||||
'infin': '\u221E',
|
||||
'int': '\u222B',
|
||||
'Iota': '\u0399',
|
||||
'iota': '\u03B9',
|
||||
'iquest': '\u00BF',
|
||||
'isin': '\u2208',
|
||||
'Iuml': '\u00CF',
|
||||
'iuml': '\u00EF',
|
||||
'Kappa': '\u039A',
|
||||
'kappa': '\u03BA',
|
||||
'Lambda': '\u039B',
|
||||
'lambda': '\u03BB',
|
||||
'lang': '\u27E8',
|
||||
'laquo': '\u00AB',
|
||||
'larr': '\u2190',
|
||||
'lArr': '\u21D0',
|
||||
'lceil': '\u2308',
|
||||
'ldquo': '\u201C',
|
||||
'le': '\u2264',
|
||||
'lfloor': '\u230A',
|
||||
'lowast': '\u2217',
|
||||
'loz': '\u25CA',
|
||||
'lrm': '\u200E',
|
||||
'lsaquo': '\u2039',
|
||||
'lsquo': '\u2018',
|
||||
'lt': '<',
|
||||
'macr': '\u00AF',
|
||||
'mdash': '\u2014',
|
||||
'micro': '\u00B5',
|
||||
'middot': '\u00B7',
|
||||
'minus': '\u2212',
|
||||
'Mu': '\u039C',
|
||||
'mu': '\u03BC',
|
||||
'nabla': '\u2207',
|
||||
'nbsp': '\u00A0',
|
||||
'ndash': '\u2013',
|
||||
'ne': '\u2260',
|
||||
'ni': '\u220B',
|
||||
'not': '\u00AC',
|
||||
'notin': '\u2209',
|
||||
'nsub': '\u2284',
|
||||
'Ntilde': '\u00D1',
|
||||
'ntilde': '\u00F1',
|
||||
'Nu': '\u039D',
|
||||
'nu': '\u03BD',
|
||||
'Oacute': '\u00D3',
|
||||
'oacute': '\u00F3',
|
||||
'Ocirc': '\u00D4',
|
||||
'ocirc': '\u00F4',
|
||||
'OElig': '\u0152',
|
||||
'oelig': '\u0153',
|
||||
'Ograve': '\u00D2',
|
||||
'ograve': '\u00F2',
|
||||
'oline': '\u203E',
|
||||
'Omega': '\u03A9',
|
||||
'omega': '\u03C9',
|
||||
'Omicron': '\u039F',
|
||||
'omicron': '\u03BF',
|
||||
'oplus': '\u2295',
|
||||
'or': '\u2228',
|
||||
'ordf': '\u00AA',
|
||||
'ordm': '\u00BA',
|
||||
'Oslash': '\u00D8',
|
||||
'oslash': '\u00F8',
|
||||
'Otilde': '\u00D5',
|
||||
'otilde': '\u00F5',
|
||||
'otimes': '\u2297',
|
||||
'Ouml': '\u00D6',
|
||||
'ouml': '\u00F6',
|
||||
'para': '\u00B6',
|
||||
'permil': '\u2030',
|
||||
'perp': '\u22A5',
|
||||
'Phi': '\u03A6',
|
||||
'phi': '\u03C6',
|
||||
'Pi': '\u03A0',
|
||||
'pi': '\u03C0',
|
||||
'piv': '\u03D6',
|
||||
'plusmn': '\u00B1',
|
||||
'pound': '\u00A3',
|
||||
'prime': '\u2032',
|
||||
'Prime': '\u2033',
|
||||
'prod': '\u220F',
|
||||
'prop': '\u221D',
|
||||
'Psi': '\u03A8',
|
||||
'psi': '\u03C8',
|
||||
'quot': '\u0022',
|
||||
'radic': '\u221A',
|
||||
'rang': '\u27E9',
|
||||
'raquo': '\u00BB',
|
||||
'rarr': '\u2192',
|
||||
'rArr': '\u21D2',
|
||||
'rceil': '\u2309',
|
||||
'rdquo': '\u201D',
|
||||
'real': '\u211C',
|
||||
'reg': '\u00AE',
|
||||
'rfloor': '\u230B',
|
||||
'Rho': '\u03A1',
|
||||
'rho': '\u03C1',
|
||||
'rlm': '\u200F',
|
||||
'rsaquo': '\u203A',
|
||||
'rsquo': '\u2019',
|
||||
'sbquo': '\u201A',
|
||||
'Scaron': '\u0160',
|
||||
'scaron': '\u0161',
|
||||
'sdot': '\u22C5',
|
||||
'sect': '\u00A7',
|
||||
'shy': '\u00AD',
|
||||
'Sigma': '\u03A3',
|
||||
'sigma': '\u03C3',
|
||||
'sigmaf': '\u03C2',
|
||||
'sim': '\u223C',
|
||||
'spades': '\u2660',
|
||||
'sub': '\u2282',
|
||||
'sube': '\u2286',
|
||||
'sum': '\u2211',
|
||||
'sup': '\u2283',
|
||||
'sup1': '\u00B9',
|
||||
'sup2': '\u00B2',
|
||||
'sup3': '\u00B3',
|
||||
'supe': '\u2287',
|
||||
'szlig': '\u00DF',
|
||||
'Tau': '\u03A4',
|
||||
'tau': '\u03C4',
|
||||
'there4': '\u2234',
|
||||
'Theta': '\u0398',
|
||||
'theta': '\u03B8',
|
||||
'thetasym': '\u03D1',
|
||||
'thinsp': '\u2009',
|
||||
'THORN': '\u00DE',
|
||||
'thorn': '\u00FE',
|
||||
'tilde': '\u02DC',
|
||||
'times': '\u00D7',
|
||||
'trade': '\u2122',
|
||||
'Uacute': '\u00DA',
|
||||
'uacute': '\u00FA',
|
||||
'uarr': '\u2191',
|
||||
'uArr': '\u21D1',
|
||||
'Ucirc': '\u00DB',
|
||||
'ucirc': '\u00FB',
|
||||
'Ugrave': '\u00D9',
|
||||
'ugrave': '\u00F9',
|
||||
'uml': '\u00A8',
|
||||
'upsih': '\u03D2',
|
||||
'Upsilon': '\u03A5',
|
||||
'upsilon': '\u03C5',
|
||||
'Uuml': '\u00DC',
|
||||
'uuml': '\u00FC',
|
||||
'weierp': '\u2118',
|
||||
'Xi': '\u039E',
|
||||
'xi': '\u03BE',
|
||||
'Yacute': '\u00DD',
|
||||
'yacute': '\u00FD',
|
||||
'yen': '\u00A5',
|
||||
'yuml': '\u00FF',
|
||||
'Yuml': '\u0178',
|
||||
'Zeta': '\u0396',
|
||||
'zeta': '\u03B6',
|
||||
'zwj': '\u200D',
|
||||
'zwnj': '\u200C',
|
||||
};
|
|
@ -0,0 +1,20 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {ParseTreeResult, Parser} from './parser';
|
||||
import {getXmlTagDefinition} from './xml_tags';
|
||||
|
||||
export {ParseTreeResult, TreeError} from './parser';
|
||||
|
||||
export class XmlParser extends Parser {
|
||||
constructor() { super(getXmlTagDefinition); }
|
||||
|
||||
parse(source: string, url: string, parseExpansionForms: boolean = false): ParseTreeResult {
|
||||
return super.parse(source, url, parseExpansionForms, null);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {TagContentType, TagDefinition} from './tags';
|
||||
|
||||
export class XmlTagDefinition implements TagDefinition {
|
||||
closedByParent: boolean = false;
|
||||
requiredParents: {[key: string]: boolean};
|
||||
parentToAdd: string;
|
||||
implicitNamespacePrefix: string;
|
||||
contentType: TagContentType = TagContentType.PARSABLE_DATA;
|
||||
isVoid: boolean = false;
|
||||
ignoreFirstLf: boolean = false;
|
||||
canSelfClose: boolean = true;
|
||||
|
||||
requireExtraParent(currentParent: string): boolean { return false; }
|
||||
|
||||
isClosedByChild(name: string): boolean { return false; }
|
||||
}
|
||||
|
||||
const _TAG_DEFINITION = new XmlTagDefinition();
|
||||
|
||||
export function getXmlTagDefinition(tagName: string): XmlTagDefinition {
|
||||
return _TAG_DEFINITION;
|
||||
}
|
|
@ -6,29 +6,28 @@
|
|||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {HtmlAst, HtmlAstVisitor, HtmlAttrAst, HtmlCommentAst, HtmlElementAst, HtmlExpansionAst, HtmlExpansionCaseAst, HtmlTextAst} from '../html_parser/html_ast';
|
||||
import * as html from '../html_parser/ast';
|
||||
import {I18nError, I18N_ATTR_PREFIX, getI18nAttr, meaning, description, isOpeningComment, isClosingComment,} from './shared';
|
||||
import {htmlVisitAll} from '../html_parser/html_ast';
|
||||
|
||||
export function extractAstMessages(
|
||||
sourceAst: HtmlAst[], implicitTags: string[],
|
||||
sourceAst: html.Node[], implicitTags: string[],
|
||||
implicitAttrs: {[k: string]: string[]}): ExtractionResult {
|
||||
const visitor = new _ExtractVisitor(implicitTags, implicitAttrs);
|
||||
return visitor.extract(sourceAst);
|
||||
}
|
||||
|
||||
export class ExtractionResult {
|
||||
constructor(public messages: AstMessage[], public errors: I18nError[]) {}
|
||||
constructor(public messages: Message[], public errors: I18nError[]) {}
|
||||
}
|
||||
|
||||
class _ExtractVisitor implements HtmlAstVisitor {
|
||||
class _ExtractVisitor implements html.Visitor {
|
||||
// <el i18n>...</el>
|
||||
private _inI18nNode = false;
|
||||
private _depth: number = 0;
|
||||
|
||||
// <!--i18n-->...<!--/i18n-->
|
||||
private _blockMeaningAndDesc: string;
|
||||
private _blockChildren: HtmlAst[];
|
||||
private _blockChildren: html.Node[];
|
||||
private _blockStartDepth: number;
|
||||
private _inI18nBlock: boolean;
|
||||
|
||||
|
@ -40,8 +39,8 @@ class _ExtractVisitor implements HtmlAstVisitor {
|
|||
|
||||
constructor(private _implicitTags: string[], private _implicitAttrs: {[k: string]: string[]}) {}
|
||||
|
||||
extract(source: HtmlAst[]): ExtractionResult {
|
||||
const messages: AstMessage[] = [];
|
||||
extract(nodes: html.Node[]): ExtractionResult {
|
||||
const messages: Message[] = [];
|
||||
this._inI18nBlock = false;
|
||||
this._inI18nNode = false;
|
||||
this._depth = 0;
|
||||
|
@ -49,20 +48,20 @@ class _ExtractVisitor implements HtmlAstVisitor {
|
|||
this._sectionStartIndex = void 0;
|
||||
this._errors = [];
|
||||
|
||||
source.forEach(node => node.visit(this, messages));
|
||||
nodes.forEach(node => node.visit(this, messages));
|
||||
|
||||
if (this._inI18nBlock) {
|
||||
this._reportError(source[source.length - 1], 'Unclosed block');
|
||||
this._reportError(nodes[nodes.length - 1], 'Unclosed block');
|
||||
}
|
||||
|
||||
return new ExtractionResult(messages, this._errors);
|
||||
}
|
||||
|
||||
visitExpansionCase(part: HtmlExpansionCaseAst, messages: AstMessage[]): any {
|
||||
htmlVisitAll(this, part.expression, messages);
|
||||
visitExpansionCase(icuCase: html.ExpansionCase, messages: Message[]): any {
|
||||
html.visitAll(this, icuCase.expression, messages);
|
||||
}
|
||||
|
||||
visitExpansion(icu: HtmlExpansionAst, messages: AstMessage[]): any {
|
||||
visitExpansion(icu: html.Expansion, messages: Message[]): any {
|
||||
this._mayBeAddBlockChildren(icu);
|
||||
|
||||
const wasInIcu = this._inIcu;
|
||||
|
@ -74,12 +73,12 @@ class _ExtractVisitor implements HtmlAstVisitor {
|
|||
this._inIcu = true;
|
||||
}
|
||||
|
||||
htmlVisitAll(this, icu.cases, messages);
|
||||
html.visitAll(this, icu.cases, messages);
|
||||
|
||||
this._inIcu = wasInIcu;
|
||||
}
|
||||
|
||||
visitComment(comment: HtmlCommentAst, messages: AstMessage[]): any {
|
||||
visitComment(comment: html.Comment, messages: Message[]): any {
|
||||
const isOpening = isOpeningComment(comment);
|
||||
|
||||
if (isOpening && (this._inI18nBlock || this._inI18nNode)) {
|
||||
|
@ -118,9 +117,9 @@ class _ExtractVisitor implements HtmlAstVisitor {
|
|||
}
|
||||
}
|
||||
|
||||
visitText(text: HtmlTextAst, messages: AstMessage[]): any { this._mayBeAddBlockChildren(text); }
|
||||
visitText(text: html.Text, messages: Message[]): any { this._mayBeAddBlockChildren(text); }
|
||||
|
||||
visitElement(el: HtmlElementAst, messages: AstMessage[]): any {
|
||||
visitElement(el: html.Element, messages: Message[]): any {
|
||||
this._mayBeAddBlockChildren(el);
|
||||
this._depth++;
|
||||
const wasInI18nNode = this._inI18nNode;
|
||||
|
@ -152,19 +151,21 @@ class _ExtractVisitor implements HtmlAstVisitor {
|
|||
|
||||
if (useSection) {
|
||||
this._startSection(messages);
|
||||
htmlVisitAll(this, el.children, messages);
|
||||
html.visitAll(this, el.children, messages);
|
||||
this._endSection(messages, el.children);
|
||||
} else {
|
||||
htmlVisitAll(this, el.children, messages);
|
||||
html.visitAll(this, el.children, messages);
|
||||
}
|
||||
|
||||
this._depth--;
|
||||
this._inI18nNode = wasInI18nNode;
|
||||
}
|
||||
|
||||
visitAttr(ast: HtmlAttrAst, messages: AstMessage[]): any { throw new Error('unreachable code'); }
|
||||
visitAttribute(attribute: html.Attribute, messages: Message[]): any {
|
||||
throw new Error('unreachable code');
|
||||
}
|
||||
|
||||
private _extractFromAttributes(el: HtmlElementAst, messages: AstMessage[]): void {
|
||||
private _extractFromAttributes(el: html.Element, messages: Message[]): void {
|
||||
const explicitAttrNameToValue: Map<string, string> = new Map();
|
||||
const implicitAttrNames: string[] = this._implicitAttrs[el.name] || [];
|
||||
|
||||
|
@ -182,13 +183,13 @@ class _ExtractVisitor implements HtmlAstVisitor {
|
|||
});
|
||||
}
|
||||
|
||||
private _addMessage(messages: AstMessage[], ast: HtmlAst[], meaningAndDesc?: string): void {
|
||||
private _addMessage(messages: Message[], ast: html.Node[], meaningAndDesc?: string): void {
|
||||
if (ast.length == 0 ||
|
||||
ast.length == 1 && ast[0] instanceof HtmlAttrAst && !(<HtmlAttrAst>ast[0]).value) {
|
||||
ast.length == 1 && ast[0] instanceof html.Attribute && !(<html.Attribute>ast[0]).value) {
|
||||
// Do not create empty messages
|
||||
return;
|
||||
}
|
||||
messages.push(new AstMessage(ast, meaning(meaningAndDesc), description(meaningAndDesc)));
|
||||
messages.push(new Message(ast, meaning(meaningAndDesc), description(meaningAndDesc)));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -197,16 +198,16 @@ class _ExtractVisitor implements HtmlAstVisitor {
|
|||
* - we are not inside a ICU message (those are handled separately),
|
||||
* - the node is a "direct child" of the block
|
||||
*/
|
||||
private _mayBeAddBlockChildren(ast: HtmlAst): void {
|
||||
private _mayBeAddBlockChildren(node: html.Node): void {
|
||||
if (this._inI18nBlock && !this._inIcu && this._depth == this._blockStartDepth) {
|
||||
this._blockChildren.push(ast);
|
||||
this._blockChildren.push(node);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the start of a section, see `_endSection`
|
||||
*/
|
||||
private _startSection(messages: AstMessage[]): void {
|
||||
private _startSection(messages: Message[]): void {
|
||||
if (this._sectionStartIndex !== void 0) {
|
||||
throw new Error('Unexpected section start');
|
||||
}
|
||||
|
@ -231,20 +232,20 @@ class _ExtractVisitor implements HtmlAstVisitor {
|
|||
* Note that we should still keep messages extracted from attributes inside the section (ie in the
|
||||
* ICU message here)
|
||||
*/
|
||||
private _endSection(messages: AstMessage[], directChildren: HtmlAst[]): void {
|
||||
private _endSection(messages: Message[], directChildren: html.Node[]): void {
|
||||
if (this._sectionStartIndex === void 0) {
|
||||
throw new Error('Unexpected section end');
|
||||
}
|
||||
|
||||
const startIndex = this._sectionStartIndex;
|
||||
const significantChildren: number = directChildren.reduce(
|
||||
(count: number, node: HtmlAst): number => count + (node instanceof HtmlCommentAst ? 0 : 1),
|
||||
(count: number, node: html.Node): number => count + (node instanceof html.Comment ? 0 : 1),
|
||||
0);
|
||||
|
||||
if (significantChildren == 1) {
|
||||
for (let i = startIndex; i < messages.length; i++) {
|
||||
let ast = messages[i].nodes;
|
||||
if (!(ast.length == 1 && ast[0] instanceof HtmlAttrAst)) {
|
||||
if (!(ast.length == 1 && ast[0] instanceof html.Attribute)) {
|
||||
messages.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
|
@ -254,11 +255,14 @@ class _ExtractVisitor implements HtmlAstVisitor {
|
|||
this._sectionStartIndex = void 0;
|
||||
}
|
||||
|
||||
private _reportError(astNode: HtmlAst, msg: string): void {
|
||||
this._errors.push(new I18nError(astNode.sourceSpan, msg));
|
||||
private _reportError(node: html.Node, msg: string): void {
|
||||
this._errors.push(new I18nError(node.sourceSpan, msg));
|
||||
}
|
||||
}
|
||||
|
||||
export class AstMessage {
|
||||
constructor(public nodes: HtmlAst[], public meaning: string, public description: string) {}
|
||||
/**
|
||||
* A Message contain a fragment (= a subtree) of the source html AST.
|
||||
*/
|
||||
export class Message {
|
||||
constructor(public nodes: html.Node[], public meaning: string, public description: string) {}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,9 @@
|
|||
import {ParseSourceSpan} from '../parse_util';
|
||||
|
||||
export class Message {
|
||||
constructor(public nodes: Node[], public meaning: string, public description: string) {}
|
||||
constructor(
|
||||
public nodes: Node[], public placeholders: {[name: string]: string}, public meaning: string,
|
||||
public description: string) {}
|
||||
}
|
||||
|
||||
export interface Node { visit(visitor: Visitor, context?: any): any; }
|
||||
|
|
|
@ -1,340 +0,0 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {Parser as ExpressionParser} from '../expression_parser/parser';
|
||||
import {ListWrapper, StringMapWrapper} from '../facade/collection';
|
||||
import {BaseException} from '../facade/exceptions';
|
||||
import {NumberWrapper, RegExpWrapper, isPresent} from '../facade/lang';
|
||||
import {HtmlAst, HtmlAstVisitor, HtmlAttrAst, HtmlCommentAst, HtmlElementAst, HtmlExpansionAst, HtmlExpansionCaseAst, HtmlTextAst, htmlVisitAll} from '../html_parser/html_ast';
|
||||
import {HtmlParseTreeResult, HtmlParser} from '../html_parser/html_parser';
|
||||
import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from '../html_parser/interpolation_config';
|
||||
import {ParseError, ParseSourceSpan} from '../parse_util';
|
||||
import {Message, id} from './message';
|
||||
import {I18N_ATTR, I18N_ATTR_PREFIX, I18nError, Part, dedupePhName, extractPhNameFromInterpolation, messageFromAttribute, messageFromI18nAttribute, partition} from './shared';
|
||||
|
||||
const _PLACEHOLDER_ELEMENT = 'ph';
|
||||
const _NAME_ATTR = 'name';
|
||||
const _PLACEHOLDER_EXPANDED_REGEXP = /<ph\s+name="(\w+)"><\/ph>/gi;
|
||||
|
||||
/**
|
||||
* Creates an i18n-ed version of the parsed template.
|
||||
*
|
||||
* Algorithm:
|
||||
*
|
||||
* See `message_extractor.ts` for details on the partitioning algorithm.
|
||||
*
|
||||
* This is how the merging works:
|
||||
*
|
||||
* 1. Use the stringify function to get the message id. Look up the message in the map.
|
||||
* 2. Get the translated message. At this point we have two trees: the original tree
|
||||
* and the translated tree, where all the elements are replaced with placeholders.
|
||||
* 3. Use the original tree to create a mapping Index:number -> HtmlAst.
|
||||
* 4. Walk the translated tree.
|
||||
* 5. If we encounter a placeholder element, get its name property.
|
||||
* 6. Get the type and the index of the node using the name property.
|
||||
* 7. If the type is 'e', which means element, then:
|
||||
* - translate the attributes of the original element
|
||||
* - recurse to merge the children
|
||||
* - create a new element using the original element name, original position,
|
||||
* and translated children and attributes
|
||||
* 8. If the type if 't', which means text, then:
|
||||
* - get the list of expressions from the original node.
|
||||
* - get the string version of the interpolation subtree
|
||||
* - find all the placeholders in the translated message, and replace them with the
|
||||
* corresponding original expressions
|
||||
*/
|
||||
export class I18nHtmlParser implements HtmlParser {
|
||||
private _errors: ParseError[];
|
||||
private _interpolationConfig: InterpolationConfig;
|
||||
|
||||
constructor(
|
||||
private _htmlParser: HtmlParser, public _expressionParser: ExpressionParser,
|
||||
private _messagesContent: string, private _messages: {[key: string]: HtmlAst[]},
|
||||
private _implicitTags: string[], private _implicitAttrs: {[k: string]: string[]}) {}
|
||||
|
||||
parse(
|
||||
sourceContent: string, sourceUrl: string, parseExpansionForms: boolean = false,
|
||||
interpolationConfig: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG):
|
||||
HtmlParseTreeResult {
|
||||
this._errors = [];
|
||||
this._interpolationConfig = interpolationConfig;
|
||||
|
||||
let res = this._htmlParser.parse(sourceContent, sourceUrl, true, interpolationConfig);
|
||||
|
||||
if (res.errors.length > 0) {
|
||||
return res;
|
||||
}
|
||||
|
||||
const nodes = this._recurse(res.rootNodes);
|
||||
|
||||
return this._errors.length > 0 ? new HtmlParseTreeResult([], this._errors) :
|
||||
new HtmlParseTreeResult(nodes, []);
|
||||
}
|
||||
|
||||
// Merge the translation recursively
|
||||
private _processI18nPart(part: Part): HtmlAst[] {
|
||||
try {
|
||||
return part.hasI18n ? this._mergeI18Part(part) : this._recurseIntoI18nPart(part);
|
||||
} catch (e) {
|
||||
if (e instanceof I18nError) {
|
||||
this._errors.push(e);
|
||||
return [];
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _recurseIntoI18nPart(p: Part): HtmlAst[] {
|
||||
// we found an element without an i18n attribute
|
||||
// we need to recurse in case its children may have i18n set
|
||||
// we also need to translate its attributes
|
||||
if (isPresent(p.rootElement)) {
|
||||
const root = p.rootElement;
|
||||
const children = this._recurse(p.children);
|
||||
const attrs = this._i18nAttributes(root);
|
||||
return [new HtmlElementAst(
|
||||
root.name, attrs, children, root.sourceSpan, root.startSourceSpan, root.endSourceSpan)];
|
||||
}
|
||||
|
||||
if (isPresent(p.rootTextNode)) {
|
||||
// a text node without i18n or interpolation, nothing to do
|
||||
return [p.rootTextNode];
|
||||
}
|
||||
|
||||
return this._recurse(p.children);
|
||||
}
|
||||
|
||||
private _recurse(nodes: HtmlAst[]): HtmlAst[] {
|
||||
let parts = partition(nodes, this._errors, this._implicitTags);
|
||||
return ListWrapper.flatten(parts.map(p => this._processI18nPart(p)));
|
||||
}
|
||||
|
||||
// Look for the translated message and merge it back to the tree
|
||||
private _mergeI18Part(part: Part): HtmlAst[] {
|
||||
let messages = part.createMessages(this._expressionParser, this._interpolationConfig);
|
||||
// TODO - dirty smoke fix
|
||||
let message = messages[0];
|
||||
|
||||
let messageId = id(message);
|
||||
|
||||
if (!StringMapWrapper.contains(this._messages, messageId)) {
|
||||
throw new I18nError(
|
||||
part.sourceSpan,
|
||||
`Cannot find message for id '${messageId}', content '${message.content}'.`);
|
||||
}
|
||||
|
||||
const translation = this._messages[messageId];
|
||||
return this._mergeTrees(part, translation);
|
||||
}
|
||||
|
||||
|
||||
private _mergeTrees(part: Part, translation: HtmlAst[]): HtmlAst[] {
|
||||
if (isPresent(part.rootTextNode)) {
|
||||
// this should never happen with a part. Parts that have root text node should not be merged.
|
||||
throw new BaseException('should not be reached');
|
||||
}
|
||||
|
||||
const visitor = new _NodeMappingVisitor();
|
||||
htmlVisitAll(visitor, part.children);
|
||||
|
||||
// merge the translated tree with the original tree.
|
||||
// we do it by preserving the source code position of the original tree
|
||||
const translatedAst = this._expandPlaceholders(translation, visitor.mapping);
|
||||
|
||||
// if the root element is present, we need to create a new root element with its attributes
|
||||
// translated
|
||||
if (part.rootElement) {
|
||||
const root = part.rootElement;
|
||||
const attrs = this._i18nAttributes(root);
|
||||
return [new HtmlElementAst(
|
||||
root.name, attrs, translatedAst, root.sourceSpan, root.startSourceSpan,
|
||||
root.endSourceSpan)];
|
||||
}
|
||||
|
||||
return translatedAst;
|
||||
}
|
||||
|
||||
/**
|
||||
* The translation AST is composed on text nodes and placeholder elements
|
||||
*/
|
||||
private _expandPlaceholders(translation: HtmlAst[], mapping: HtmlAst[]): HtmlAst[] {
|
||||
return translation.map(node => {
|
||||
if (node instanceof HtmlElementAst) {
|
||||
// This node is a placeholder, replace with the original content
|
||||
return this._expandPlaceholdersInNode(node, mapping);
|
||||
}
|
||||
|
||||
if (node instanceof HtmlTextAst) {
|
||||
return node;
|
||||
}
|
||||
|
||||
throw new BaseException('should not be reached');
|
||||
});
|
||||
}
|
||||
|
||||
private _expandPlaceholdersInNode(node: HtmlElementAst, mapping: HtmlAst[]): HtmlAst {
|
||||
let name = this._getName(node);
|
||||
let index = NumberWrapper.parseInt(name.substring(1), 10);
|
||||
let originalNode = mapping[index];
|
||||
|
||||
if (originalNode instanceof HtmlTextAst) {
|
||||
return this._mergeTextInterpolation(node, originalNode);
|
||||
}
|
||||
|
||||
if (originalNode instanceof HtmlElementAst) {
|
||||
return this._mergeElement(node, originalNode, mapping);
|
||||
}
|
||||
|
||||
throw new BaseException('should not be reached');
|
||||
}
|
||||
|
||||
// Extract the value of a <ph> name attribute
|
||||
private _getName(node: HtmlElementAst): string {
|
||||
if (node.name != _PLACEHOLDER_ELEMENT) {
|
||||
throw new I18nError(
|
||||
node.sourceSpan,
|
||||
`Unexpected tag "${node.name}". Only "${_PLACEHOLDER_ELEMENT}" tags are allowed.`);
|
||||
}
|
||||
|
||||
const nameAttr = node.attrs.find(a => a.name == _NAME_ATTR);
|
||||
|
||||
if (nameAttr) {
|
||||
return nameAttr.value;
|
||||
}
|
||||
|
||||
throw new I18nError(node.sourceSpan, `Missing "${_NAME_ATTR}" attribute.`);
|
||||
}
|
||||
|
||||
private _mergeTextInterpolation(node: HtmlElementAst, originalNode: HtmlTextAst): HtmlTextAst {
|
||||
const split = this._expressionParser.splitInterpolation(
|
||||
originalNode.value, originalNode.sourceSpan.toString(), this._interpolationConfig);
|
||||
|
||||
const exps = split ? split.expressions : [];
|
||||
|
||||
const messageSubstring = this._messagesContent.substring(
|
||||
node.startSourceSpan.end.offset, node.endSourceSpan.start.offset);
|
||||
|
||||
let translated = this._replacePlaceholdersWithInterpolations(
|
||||
messageSubstring, exps, originalNode.sourceSpan);
|
||||
|
||||
return new HtmlTextAst(translated, originalNode.sourceSpan);
|
||||
}
|
||||
|
||||
private _mergeElement(node: HtmlElementAst, originalNode: HtmlElementAst, mapping: HtmlAst[]):
|
||||
HtmlElementAst {
|
||||
const children = this._expandPlaceholders(node.children, mapping);
|
||||
|
||||
return new HtmlElementAst(
|
||||
originalNode.name, this._i18nAttributes(originalNode), children, originalNode.sourceSpan,
|
||||
originalNode.startSourceSpan, originalNode.endSourceSpan);
|
||||
}
|
||||
|
||||
private _i18nAttributes(el: HtmlElementAst): HtmlAttrAst[] {
|
||||
let res: HtmlAttrAst[] = [];
|
||||
let implicitAttrs: string[] =
|
||||
isPresent(this._implicitAttrs[el.name]) ? this._implicitAttrs[el.name] : [];
|
||||
|
||||
el.attrs.forEach(attr => {
|
||||
if (attr.name.startsWith(I18N_ATTR_PREFIX) || attr.name == I18N_ATTR) return;
|
||||
|
||||
let message: Message;
|
||||
|
||||
let i18nAttr = el.attrs.find(a => a.name == `${I18N_ATTR_PREFIX}${attr.name}`);
|
||||
|
||||
if (!i18nAttr) {
|
||||
if (implicitAttrs.indexOf(attr.name) == -1) {
|
||||
res.push(attr);
|
||||
return;
|
||||
}
|
||||
message = messageFromAttribute(this._expressionParser, this._interpolationConfig, attr);
|
||||
} else {
|
||||
message = messageFromI18nAttribute(
|
||||
this._expressionParser, this._interpolationConfig, el, i18nAttr);
|
||||
}
|
||||
|
||||
let messageId = id(message);
|
||||
|
||||
if (StringMapWrapper.contains(this._messages, messageId)) {
|
||||
const updatedMessage = this._replaceInterpolationInAttr(attr, this._messages[messageId]);
|
||||
res.push(new HtmlAttrAst(attr.name, updatedMessage, attr.sourceSpan));
|
||||
|
||||
} else {
|
||||
throw new I18nError(
|
||||
attr.sourceSpan,
|
||||
`Cannot find message for id '${messageId}', content '${message.content}'.`);
|
||||
}
|
||||
});
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
private _replaceInterpolationInAttr(attr: HtmlAttrAst, msg: HtmlAst[]): string {
|
||||
const split = this._expressionParser.splitInterpolation(
|
||||
attr.value, attr.sourceSpan.toString(), this._interpolationConfig);
|
||||
const exps = isPresent(split) ? split.expressions : [];
|
||||
|
||||
const first = msg[0];
|
||||
const last = msg[msg.length - 1];
|
||||
|
||||
const start = first.sourceSpan.start.offset;
|
||||
const end =
|
||||
last instanceof HtmlElementAst ? last.endSourceSpan.end.offset : last.sourceSpan.end.offset;
|
||||
const messageSubstring = this._messagesContent.substring(start, end);
|
||||
|
||||
return this._replacePlaceholdersWithInterpolations(messageSubstring, exps, attr.sourceSpan);
|
||||
};
|
||||
|
||||
private _replacePlaceholdersWithInterpolations(
|
||||
message: string, exps: string[], sourceSpan: ParseSourceSpan): string {
|
||||
const expMap = this._buildExprMap(exps);
|
||||
|
||||
return message.replace(
|
||||
_PLACEHOLDER_EXPANDED_REGEXP,
|
||||
(_: string, name: string) => this._convertIntoExpression(name, expMap, sourceSpan));
|
||||
}
|
||||
|
||||
private _buildExprMap(exps: string[]): Map<string, string> {
|
||||
const expMap = new Map<string, string>();
|
||||
const usedNames = new Map<string, number>();
|
||||
|
||||
for (let i = 0; i < exps.length; i++) {
|
||||
const phName = extractPhNameFromInterpolation(exps[i], i);
|
||||
expMap.set(dedupePhName(usedNames, phName), exps[i]);
|
||||
}
|
||||
|
||||
return expMap;
|
||||
}
|
||||
|
||||
private _convertIntoExpression(
|
||||
name: string, expMap: Map<string, string>, sourceSpan: ParseSourceSpan) {
|
||||
if (expMap.has(name)) {
|
||||
return `${this._interpolationConfig.start}${expMap.get(name)}${this._interpolationConfig.end}`;
|
||||
}
|
||||
|
||||
throw new I18nError(sourceSpan, `Invalid interpolation name '${name}'`);
|
||||
}
|
||||
}
|
||||
|
||||
// Creates a list of elements and text nodes in the AST
|
||||
// The indexes match the placeholders indexes
|
||||
class _NodeMappingVisitor implements HtmlAstVisitor {
|
||||
mapping: HtmlAst[] = [];
|
||||
|
||||
visitElement(ast: HtmlElementAst, context: any): any {
|
||||
this.mapping.push(ast);
|
||||
htmlVisitAll(this, ast.children);
|
||||
}
|
||||
|
||||
visitText(ast: HtmlTextAst, context: any): any { this.mapping.push(ast); }
|
||||
|
||||
visitAttr(ast: HtmlAttrAst, context: any): any {}
|
||||
visitExpansion(ast: HtmlExpansionAst, context: any): any {}
|
||||
visitExpansionCase(ast: HtmlExpansionCaseAst, context: any): any {}
|
||||
visitComment(ast: HtmlCommentAst, context: any): any {}
|
||||
}
|
|
@ -8,44 +8,58 @@
|
|||
|
||||
import {Lexer as ExpressionLexer} from '../expression_parser/lexer';
|
||||
import {Parser as ExpressionParser} from '../expression_parser/parser';
|
||||
import * as hAst from '../html_parser/html_ast';
|
||||
import * as html from '../html_parser/ast';
|
||||
import {getHtmlTagDefinition} from '../html_parser/html_tags';
|
||||
import {InterpolationConfig} from '../html_parser/interpolation_config';
|
||||
import {ParseSourceSpan} from '../parse_util';
|
||||
|
||||
import {extractAstMessages} from './extractor';
|
||||
import * as i18nAst from './i18n_ast';
|
||||
import {PlaceholderRegistry} from './serializers/util';
|
||||
import * as i18n from './i18n_ast';
|
||||
import {PlaceholderRegistry} from './serializers/placeholder';
|
||||
import {extractPlaceholderName} from './shared';
|
||||
|
||||
/**
|
||||
* Extract all the i18n messages from a component template.
|
||||
*/
|
||||
export function extractI18nMessages(
|
||||
sourceAst: hAst.HtmlAst[], interpolationConfig: InterpolationConfig, implicitTags: string[],
|
||||
implicitAttrs: {[k: string]: string[]}): i18nAst.Message[] {
|
||||
sourceAst: html.Node[], interpolationConfig: InterpolationConfig, implicitTags: string[],
|
||||
implicitAttrs: {[k: string]: string[]}): i18n.Message[] {
|
||||
const extractionResult = extractAstMessages(sourceAst, implicitTags, implicitAttrs);
|
||||
|
||||
if (extractionResult.errors.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const visitor =
|
||||
new _I18nVisitor(new ExpressionParser(new ExpressionLexer()), interpolationConfig);
|
||||
const expParser = new ExpressionParser(new ExpressionLexer());
|
||||
const visitor = new _I18nVisitor(expParser, interpolationConfig);
|
||||
|
||||
return extractionResult.messages.map((msg): i18nAst.Message => {
|
||||
return new i18nAst.Message(visitor.convertToI18nAst(msg.nodes), msg.meaning, msg.description);
|
||||
});
|
||||
return extractionResult.messages.map(
|
||||
(msg) => visitor.toI18nMessage(msg.nodes, msg.meaning, msg.description));
|
||||
}
|
||||
|
||||
class _I18nVisitor implements hAst.HtmlAstVisitor {
|
||||
class _I18nVisitor implements html.Visitor {
|
||||
private _isIcu: boolean;
|
||||
private _icuDepth: number;
|
||||
private _placeholderRegistry: PlaceholderRegistry;
|
||||
private _placeholderToContent: {[name: string]: string};
|
||||
|
||||
constructor(
|
||||
private _expressionParser: ExpressionParser,
|
||||
private _interpolationConfig: InterpolationConfig) {}
|
||||
|
||||
visitElement(el: hAst.HtmlElementAst, context: any): i18nAst.Node {
|
||||
const children = hAst.htmlVisitAll(this, el.children);
|
||||
public toI18nMessage(nodes: html.Node[], meaning: string, description: string): i18n.Message {
|
||||
this._isIcu = nodes.length == 1 && nodes[0] instanceof html.Expansion;
|
||||
this._icuDepth = 0;
|
||||
this._placeholderRegistry = new PlaceholderRegistry();
|
||||
this._placeholderToContent = {};
|
||||
|
||||
const i18nodes: i18n.Node[] = html.visitAll(this, nodes, {});
|
||||
|
||||
return new i18n.Message(i18nodes, this._placeholderToContent, meaning, description);
|
||||
}
|
||||
|
||||
visitElement(el: html.Element, context: any): i18n.Node {
|
||||
const children = html.visitAll(this, el.children);
|
||||
const attrs: {[k: string]: string} = {};
|
||||
el.attrs.forEach(attr => {
|
||||
// Do not visit the attributes, translatable ones are top-level ASTs
|
||||
|
@ -55,66 +69,67 @@ class _I18nVisitor implements hAst.HtmlAstVisitor {
|
|||
const isVoid: boolean = getHtmlTagDefinition(el.name).isVoid;
|
||||
const startPhName =
|
||||
this._placeholderRegistry.getStartTagPlaceholderName(el.name, attrs, isVoid);
|
||||
const closePhName = isVoid ? '' : this._placeholderRegistry.getCloseTagPlaceholderName(el.name);
|
||||
this._placeholderToContent[startPhName] = el.sourceSpan.toString();
|
||||
|
||||
return new i18nAst.TagPlaceholder(
|
||||
let closePhName = '';
|
||||
|
||||
if (!isVoid) {
|
||||
closePhName = this._placeholderRegistry.getCloseTagPlaceholderName(el.name);
|
||||
this._placeholderToContent[closePhName] = `</${el.name}>`;
|
||||
}
|
||||
|
||||
return new i18n.TagPlaceholder(
|
||||
el.name, attrs, startPhName, closePhName, children, isVoid, el.sourceSpan);
|
||||
}
|
||||
|
||||
visitAttr(attr: hAst.HtmlAttrAst, context: any): i18nAst.Node {
|
||||
return this._visitTextWithInterpolation(attr.value, attr.sourceSpan);
|
||||
visitAttribute(attribute: html.Attribute, context: any): i18n.Node {
|
||||
return this._visitTextWithInterpolation(attribute.value, attribute.sourceSpan);
|
||||
}
|
||||
|
||||
visitText(text: hAst.HtmlTextAst, context: any): i18nAst.Node {
|
||||
visitText(text: html.Text, context: any): i18n.Node {
|
||||
return this._visitTextWithInterpolation(text.value, text.sourceSpan);
|
||||
}
|
||||
|
||||
visitComment(comment: hAst.HtmlCommentAst, context: any): i18nAst.Node { return null; }
|
||||
visitComment(comment: html.Comment, context: any): i18n.Node { return null; }
|
||||
|
||||
visitExpansion(icu: hAst.HtmlExpansionAst, context: any): i18nAst.Node {
|
||||
visitExpansion(icu: html.Expansion, context: any): i18n.Node {
|
||||
this._icuDepth++;
|
||||
const i18nIcuCases: {[k: string]: i18nAst.Node} = {};
|
||||
const i18nIcu = new i18nAst.Icu(icu.switchValue, icu.type, i18nIcuCases, icu.sourceSpan);
|
||||
const i18nIcuCases: {[k: string]: i18n.Node} = {};
|
||||
const i18nIcu = new i18n.Icu(icu.switchValue, icu.type, i18nIcuCases, icu.sourceSpan);
|
||||
icu.cases.forEach((caze): void => {
|
||||
i18nIcuCases[caze.value] = new i18nAst.Container(
|
||||
caze.expression.map((hAst) => hAst.visit(this, {})), caze.expSourceSpan);
|
||||
i18nIcuCases[caze.value] = new i18n.Container(
|
||||
caze.expression.map((node) => node.visit(this, {})), caze.expSourceSpan);
|
||||
});
|
||||
this._icuDepth--;
|
||||
|
||||
if (this._isIcu || this._icuDepth > 0) {
|
||||
// If the message (vs a part of the message) is an ICU message return its
|
||||
// If the message (vs a part of the message) is an ICU message returns it
|
||||
return i18nIcu;
|
||||
}
|
||||
|
||||
// else returns a placeholder
|
||||
const phName = this._placeholderRegistry.getPlaceholderName('ICU', icu.sourceSpan.toString());
|
||||
return new i18nAst.IcuPlaceholder(i18nIcu, phName, icu.sourceSpan);
|
||||
this._placeholderToContent[phName] = icu.sourceSpan.toString();
|
||||
return new i18n.IcuPlaceholder(i18nIcu, phName, icu.sourceSpan);
|
||||
}
|
||||
|
||||
visitExpansionCase(icuCase: hAst.HtmlExpansionCaseAst, context: any): i18nAst.Node {
|
||||
visitExpansionCase(icuCase: html.ExpansionCase, context: any): i18n.Node {
|
||||
throw new Error('Unreachable code');
|
||||
}
|
||||
|
||||
public convertToI18nAst(htmlAsts: hAst.HtmlAst[]): i18nAst.Node[] {
|
||||
this._isIcu = htmlAsts.length == 1 && htmlAsts[0] instanceof hAst.HtmlExpansionAst;
|
||||
this._icuDepth = 0;
|
||||
this._placeholderRegistry = new PlaceholderRegistry();
|
||||
|
||||
return hAst.htmlVisitAll(this, htmlAsts, {});
|
||||
}
|
||||
|
||||
private _visitTextWithInterpolation(text: string, sourceSpan: ParseSourceSpan): i18nAst.Node {
|
||||
private _visitTextWithInterpolation(text: string, sourceSpan: ParseSourceSpan): i18n.Node {
|
||||
const splitInterpolation = this._expressionParser.splitInterpolation(
|
||||
text, sourceSpan.start.toString(), this._interpolationConfig);
|
||||
|
||||
if (!splitInterpolation) {
|
||||
// No expression, return a single text
|
||||
return new i18nAst.Text(text, sourceSpan);
|
||||
return new i18n.Text(text, sourceSpan);
|
||||
}
|
||||
|
||||
// Return a group of text + expressions
|
||||
const nodes: i18nAst.Node[] = [];
|
||||
const container = new i18nAst.Container(nodes, sourceSpan);
|
||||
const nodes: i18n.Node[] = [];
|
||||
const container = new i18n.Container(nodes, sourceSpan);
|
||||
const {start: sDelimiter, end: eDelimiter} = this._interpolationConfig;
|
||||
|
||||
for (let i = 0; i < splitInterpolation.strings.length - 1; i++) {
|
||||
const expression = splitInterpolation.expressions[i];
|
||||
|
@ -122,16 +137,18 @@ class _I18nVisitor implements hAst.HtmlAstVisitor {
|
|||
const phName = this._placeholderRegistry.getPlaceholderName(baseName, expression);
|
||||
|
||||
if (splitInterpolation.strings[i].length) {
|
||||
nodes.push(new i18nAst.Text(splitInterpolation.strings[i], sourceSpan));
|
||||
// No need to add empty strings
|
||||
nodes.push(new i18n.Text(splitInterpolation.strings[i], sourceSpan));
|
||||
}
|
||||
|
||||
nodes.push(new i18nAst.Placeholder(expression, phName, sourceSpan));
|
||||
nodes.push(new i18n.Placeholder(expression, phName, sourceSpan));
|
||||
this._placeholderToContent[phName] = sDelimiter + expression + eDelimiter;
|
||||
}
|
||||
|
||||
// The last index contains no expression
|
||||
const lastStringIdx = splitInterpolation.strings.length - 1;
|
||||
if (splitInterpolation.strings[lastStringIdx].length) {
|
||||
nodes.push(new i18nAst.Text(splitInterpolation.strings[lastStringIdx], sourceSpan));
|
||||
nodes.push(new i18n.Text(splitInterpolation.strings[lastStringIdx], sourceSpan));
|
||||
}
|
||||
return container;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
export {MessageBundle} from './message_bundle';
|
||||
export {Serializer} from './serializers/serializer';
|
||||
export {Xmb} from './serializers/xmb';
|
||||
export {Xtb} from './serializers/xtb';
|
|
@ -1,30 +0,0 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {escape, isPresent} from '../facade/lang';
|
||||
|
||||
|
||||
/**
|
||||
* A message extracted from a template.
|
||||
*
|
||||
* The identity of a message is comprised of `content` and `meaning`.
|
||||
*
|
||||
* `description` is additional information provided to the translator.
|
||||
*/
|
||||
export class Message {
|
||||
constructor(public content: string, public meaning: string, public description: string = null) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the id of a message
|
||||
*/
|
||||
export function id(m: Message): string {
|
||||
let meaning = isPresent(m.meaning) ? m.meaning : '';
|
||||
let content = isPresent(m.content) ? m.content : '';
|
||||
return escape(`$ng|${meaning}|${content}`);
|
||||
}
|
|
@ -8,24 +8,29 @@
|
|||
|
||||
import {HtmlParser} from '../html_parser/html_parser';
|
||||
import {InterpolationConfig} from '../html_parser/interpolation_config';
|
||||
import {ParseError} from '../parse_util';
|
||||
|
||||
import * as i18nAst from './i18n_ast';
|
||||
import * as i18n from './i18n_ast';
|
||||
import {extractI18nMessages} from './i18n_parser';
|
||||
import {Serializer} from './serializers/serializer';
|
||||
|
||||
export class Catalog {
|
||||
private _messageMap: {[k: string]: i18nAst.Message} = {};
|
||||
|
||||
/**
|
||||
* A container for message extracted from the templates.
|
||||
*/
|
||||
export class MessageBundle {
|
||||
private _messageMap: {[id: string]: i18n.Message} = {};
|
||||
|
||||
constructor(
|
||||
private _htmlParser: HtmlParser, private _implicitTags: string[],
|
||||
private _implicitAttrs: {[k: string]: string[]}) {}
|
||||
|
||||
public updateFromTemplate(html: string, url: string, interpolationConfig: InterpolationConfig):
|
||||
void {
|
||||
updateFromTemplate(html: string, url: string, interpolationConfig: InterpolationConfig):
|
||||
ParseError[] {
|
||||
const htmlParserResult = this._htmlParser.parse(html, url, true, interpolationConfig);
|
||||
|
||||
if (htmlParserResult.errors.length) {
|
||||
throw new Error();
|
||||
return htmlParserResult.errors;
|
||||
}
|
||||
|
||||
const messages = extractI18nMessages(
|
||||
|
@ -37,18 +42,9 @@ export class Catalog {
|
|||
});
|
||||
}
|
||||
|
||||
public load(content: string, serializer: Serializer): void {
|
||||
const nodeMap = serializer.load(content);
|
||||
this._messageMap = {};
|
||||
|
||||
Object.getOwnPropertyNames(nodeMap).forEach(
|
||||
(id) => { this._messageMap[id] = new i18nAst.Message(nodeMap[id], '', ''); });
|
||||
}
|
||||
|
||||
public write(serializer: Serializer): string { return serializer.write(this._messageMap); }
|
||||
write(serializer: Serializer): string { return serializer.write(this._messageMap); }
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* String hash function similar to java.lang.String.hashCode().
|
||||
* The hash code for a string is computed as
|
||||
|
@ -78,35 +74,35 @@ export function strHash(str: string): string {
|
|||
*
|
||||
* @internal
|
||||
*/
|
||||
class _SerializerVisitor implements i18nAst.Visitor {
|
||||
visitText(text: i18nAst.Text, context: any): any { return text.value; }
|
||||
class _SerializerVisitor implements i18n.Visitor {
|
||||
visitText(text: i18n.Text, context: any): any { return text.value; }
|
||||
|
||||
visitContainer(container: i18nAst.Container, context: any): any {
|
||||
visitContainer(container: i18n.Container, context: any): any {
|
||||
return `[${container.children.map(child => child.visit(this)).join(', ')}]`;
|
||||
}
|
||||
|
||||
visitIcu(icu: i18nAst.Icu, context: any): any {
|
||||
visitIcu(icu: i18n.Icu, context: any): any {
|
||||
let strCases = Object.keys(icu.cases).map((k: string) => `${k} {${icu.cases[k].visit(this)}}`);
|
||||
return `{${icu.expression}, ${icu.type}, ${strCases.join(', ')}}`;
|
||||
}
|
||||
|
||||
visitTagPlaceholder(ph: i18nAst.TagPlaceholder, context: any): any {
|
||||
visitTagPlaceholder(ph: i18n.TagPlaceholder, context: any): any {
|
||||
return ph.isVoid ?
|
||||
`<ph tag name="${ph.startName}"/>` :
|
||||
`<ph tag name="${ph.startName}">${ph.children.map(child => child.visit(this)).join(', ')}</ph name="${ph.closeName}">`;
|
||||
}
|
||||
|
||||
visitPlaceholder(ph: i18nAst.Placeholder, context: any): any {
|
||||
visitPlaceholder(ph: i18n.Placeholder, context: any): any {
|
||||
return `<ph name="${ph.name}">${ph.value}</ph>`;
|
||||
}
|
||||
|
||||
visitIcuPlaceholder(ph: i18nAst.IcuPlaceholder, context?: any): any {
|
||||
visitIcuPlaceholder(ph: i18n.IcuPlaceholder, context?: any): any {
|
||||
return `<ph icu name="${ph.name}">${ph.value.visit(this)}</ph>`;
|
||||
}
|
||||
}
|
||||
|
||||
const serializerVisitor = new _SerializerVisitor();
|
||||
|
||||
export function serializeAst(ast: i18nAst.Node[]): string[] {
|
||||
export function serializeAst(ast: i18n.Node[]): string[] {
|
||||
return ast.map(a => a.visit(serializerVisitor, null));
|
||||
}
|
|
@ -1,183 +0,0 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {Parser as ExpressionParser} from '../expression_parser/parser';
|
||||
import {StringMapWrapper} from '../facade/collection';
|
||||
import {isPresent} from '../facade/lang';
|
||||
import {HtmlAst, HtmlElementAst} from '../html_parser/html_ast';
|
||||
import {HtmlParser} from '../html_parser/html_parser';
|
||||
import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from '../html_parser/interpolation_config';
|
||||
import {ParseError} from '../parse_util';
|
||||
|
||||
import {Message, id} from './message';
|
||||
import {I18N_ATTR_PREFIX, I18nError, Part, messageFromAttribute, messageFromI18nAttribute, partition} from './shared';
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* All messages extracted from a template.
|
||||
*/
|
||||
export class ExtractionResult {
|
||||
constructor(public messages: Message[], public errors: ParseError[]) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes duplicate messages.
|
||||
*/
|
||||
export function removeDuplicates(messages: Message[]): Message[] {
|
||||
let uniq: {[key: string]: Message} = {};
|
||||
messages.forEach(m => {
|
||||
if (!StringMapWrapper.contains(uniq, id(m))) {
|
||||
uniq[id(m)] = m;
|
||||
}
|
||||
});
|
||||
return StringMapWrapper.values(uniq);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts all messages from a template.
|
||||
*
|
||||
* Algorithm:
|
||||
*
|
||||
* To understand the algorithm, you need to know how partitioning works.
|
||||
* Partitioning is required as we can use two i18n comments to group node siblings together.
|
||||
* That is why we cannot just use nodes.
|
||||
*
|
||||
* Partitioning transforms an array of HtmlAst into an array of Part.
|
||||
* A part can optionally contain a root element or a root text node. And it can also contain
|
||||
* children.
|
||||
* A part can contain i18n property, in which case it needs to be extracted.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* The following array of nodes will be split into four parts:
|
||||
*
|
||||
* ```
|
||||
* <a>A</a>
|
||||
* <b i18n>B</b>
|
||||
* <!-- i18n -->
|
||||
* <c>C</c>
|
||||
* D
|
||||
* <!-- /i18n -->
|
||||
* E
|
||||
* ```
|
||||
*
|
||||
* Part 1 containing the a tag. It should not be translated.
|
||||
* Part 2 containing the b tag. It should be translated.
|
||||
* Part 3 containing the c tag and the D text node. It should be translated.
|
||||
* Part 4 containing the E text node. It should not be translated..
|
||||
*
|
||||
* It is also important to understand how we stringify nodes to create a message.
|
||||
*
|
||||
* We walk the tree and replace every element node with a placeholder. We also replace
|
||||
* all expressions in interpolation with placeholders. We also insert a placeholder element
|
||||
* to wrap a text node containing interpolation.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* The following tree:
|
||||
*
|
||||
* ```
|
||||
* <a>A{{I}}</a><b>B</b>
|
||||
* ```
|
||||
*
|
||||
* will be stringified into:
|
||||
* ```
|
||||
* <ph name="e0"><ph name="t1">A<ph name="0"/></ph></ph><ph name="e2">B</ph>
|
||||
* ```
|
||||
*
|
||||
* This is what the algorithm does:
|
||||
*
|
||||
* 1. Use the provided html parser to get the html AST of the template.
|
||||
* 2. Partition the root nodes, and process each part separately.
|
||||
* 3. If a part does not have the i18n attribute, recurse to process children and attributes.
|
||||
* 4. If a part has the i18n attribute, stringify the nodes to create a Message.
|
||||
*/
|
||||
export class MessageExtractor {
|
||||
private _messages: Message[];
|
||||
private _errors: ParseError[];
|
||||
|
||||
constructor(
|
||||
private _htmlParser: HtmlParser, private _expressionParser: ExpressionParser,
|
||||
private _implicitTags: string[], private _implicitAttrs: {[k: string]: string[]}) {}
|
||||
|
||||
extract(
|
||||
template: string, sourceUrl: string,
|
||||
interpolationConfig: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG): ExtractionResult {
|
||||
this._messages = [];
|
||||
this._errors = [];
|
||||
|
||||
const res = this._htmlParser.parse(template, sourceUrl, true, interpolationConfig);
|
||||
|
||||
if (res.errors.length == 0) {
|
||||
this._recurse(res.rootNodes, interpolationConfig);
|
||||
}
|
||||
|
||||
return new ExtractionResult(this._messages, this._errors.concat(res.errors));
|
||||
}
|
||||
|
||||
private _extractMessagesFromPart(part: Part, interpolationConfig: InterpolationConfig): void {
|
||||
if (part.hasI18n) {
|
||||
this._messages.push(...part.createMessages(this._expressionParser, interpolationConfig));
|
||||
this._recurseToExtractMessagesFromAttributes(part.children, interpolationConfig);
|
||||
} else {
|
||||
this._recurse(part.children, interpolationConfig);
|
||||
}
|
||||
|
||||
if (isPresent(part.rootElement)) {
|
||||
this._extractMessagesFromAttributes(part.rootElement, interpolationConfig);
|
||||
}
|
||||
}
|
||||
|
||||
private _recurse(nodes: HtmlAst[], interpolationConfig: InterpolationConfig): void {
|
||||
if (isPresent(nodes)) {
|
||||
let parts = partition(nodes, this._errors, this._implicitTags);
|
||||
parts.forEach(part => this._extractMessagesFromPart(part, interpolationConfig));
|
||||
}
|
||||
}
|
||||
|
||||
private _recurseToExtractMessagesFromAttributes(
|
||||
nodes: HtmlAst[], interpolationConfig: InterpolationConfig): void {
|
||||
nodes.forEach(n => {
|
||||
if (n instanceof HtmlElementAst) {
|
||||
this._extractMessagesFromAttributes(n, interpolationConfig);
|
||||
this._recurseToExtractMessagesFromAttributes(n.children, interpolationConfig);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private _extractMessagesFromAttributes(
|
||||
p: HtmlElementAst, interpolationConfig: InterpolationConfig): void {
|
||||
let transAttrs: string[] =
|
||||
isPresent(this._implicitAttrs[p.name]) ? this._implicitAttrs[p.name] : [];
|
||||
let explicitAttrs: string[] = [];
|
||||
|
||||
// `i18n-` prefixed attributes should be translated
|
||||
p.attrs.filter(attr => attr.name.startsWith(I18N_ATTR_PREFIX)).forEach(attr => {
|
||||
try {
|
||||
explicitAttrs.push(attr.name.substring(I18N_ATTR_PREFIX.length));
|
||||
this._messages.push(
|
||||
messageFromI18nAttribute(this._expressionParser, interpolationConfig, p, attr));
|
||||
} catch (e) {
|
||||
if (e instanceof I18nError) {
|
||||
this._errors.push(e);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// implicit attributes should also be translated
|
||||
p.attrs.filter(attr => !attr.name.startsWith(I18N_ATTR_PREFIX))
|
||||
.filter(attr => explicitAttrs.indexOf(attr.name) == -1)
|
||||
.filter(attr => transAttrs.indexOf(attr.name) > -1)
|
||||
.forEach(
|
||||
attr => this._messages.push(
|
||||
messageFromAttribute(this._expressionParser, interpolationConfig, attr)));
|
||||
}
|
||||
}
|
|
@ -45,7 +45,9 @@ const TAG_TO_PLACEHOLDER_NAMES: {[k: string]: string} = {
|
|||
* @internal
|
||||
*/
|
||||
export class PlaceholderRegistry {
|
||||
// Count the occurrence of the base name top generate a unique name
|
||||
private _placeHolderNameCounts: {[k: string]: number} = {};
|
||||
// Maps signature to placeholder names
|
||||
private _signatureToName: {[k: string]: string} = {};
|
||||
|
||||
getStartTagPlaceholderName(tag: string, attrs: {[k: string]: string}, isVoid: boolean): string {
|
||||
|
@ -91,18 +93,17 @@ export class PlaceholderRegistry {
|
|||
return uniqueName;
|
||||
}
|
||||
|
||||
// Generate a hash for a tag - does not take attribute order into account
|
||||
private _hashTag(tag: string, attrs: {[k: string]: string}, isVoid: boolean): string {
|
||||
const start = `<${tag.toUpperCase()}`;
|
||||
const start = `<${tag}`;
|
||||
const strAttrs =
|
||||
Object.getOwnPropertyNames(attrs).sort().map((name) => ` ${name}=${attrs[name]}`).join('');
|
||||
const end = isVoid ? '/>' : `></${tag.toUpperCase()}>`;
|
||||
const end = isVoid ? '/>' : `></${tag}>`;
|
||||
|
||||
return start + strAttrs + end;
|
||||
}
|
||||
|
||||
private _hashClosingTag(tag: string): string {
|
||||
return this._hashTag(`/${tag.toUpperCase()}`, {}, false);
|
||||
}
|
||||
private _hashClosingTag(tag: string): string { return this._hashTag(`/${tag}`, {}, false); }
|
||||
|
||||
private _generateUniqueName(base: string): string {
|
||||
let name = base;
|
|
@ -6,10 +6,12 @@
|
|||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import * as i18nAst from '../i18n_ast';
|
||||
import * as html from '../../html_parser/ast';
|
||||
import * as i18n from '../i18n_ast';
|
||||
|
||||
export interface Serializer {
|
||||
write(messageMap: {[k: string]: i18nAst.Message}): string;
|
||||
write(messageMap: {[id: string]: i18n.Message}): string;
|
||||
|
||||
load(content: string): {[k: string]: i18nAst.Node[]};
|
||||
load(content: string, url: string, placeholders: {[id: string]: {[name: string]: string}}):
|
||||
{[id: string]: html.Node[]};
|
||||
}
|
|
@ -7,7 +7,8 @@
|
|||
*/
|
||||
|
||||
import {ListWrapper} from '../../facade/collection';
|
||||
import * as i18nAst from '../i18n_ast';
|
||||
import * as html from '../../html_parser/ast';
|
||||
import * as i18n from '../i18n_ast';
|
||||
|
||||
import {Serializer} from './serializer';
|
||||
import * as xml from './xml_helper';
|
||||
|
@ -17,9 +18,9 @@ const _MESSAGE_TAG = 'msg';
|
|||
const _PLACEHOLDER_TAG = 'ph';
|
||||
const _EXEMPLE_TAG = 'ex';
|
||||
|
||||
export class XmbSerializer implements Serializer {
|
||||
export class Xmb implements Serializer {
|
||||
// TODO(vicb): DOCTYPE
|
||||
write(messageMap: {[k: string]: i18nAst.Message}): string {
|
||||
write(messageMap: {[k: string]: i18n.Message}): string {
|
||||
const visitor = new _Visitor();
|
||||
const declaration = new xml.Declaration({version: '1.0', encoding: 'UTF-8'});
|
||||
let rootNode = new xml.Tag(_MESSAGES_TAG);
|
||||
|
@ -49,19 +50,22 @@ export class XmbSerializer implements Serializer {
|
|||
]);
|
||||
}
|
||||
|
||||
load(content: string): {[k: string]: i18nAst.Node[]} { throw new Error('Unsupported'); }
|
||||
load(content: string, url: string, placeholders: {[id: string]: {[name: string]: string}}):
|
||||
{[id: string]: html.Node[]} {
|
||||
throw new Error('Unsupported');
|
||||
}
|
||||
}
|
||||
|
||||
class _Visitor implements i18nAst.Visitor {
|
||||
visitText(text: i18nAst.Text, context?: any): xml.Node[] { return [new xml.Text(text.value)]; }
|
||||
class _Visitor implements i18n.Visitor {
|
||||
visitText(text: i18n.Text, context?: any): xml.Node[] { return [new xml.Text(text.value)]; }
|
||||
|
||||
visitContainer(container: i18nAst.Container, context?: any): xml.Node[] {
|
||||
visitContainer(container: i18n.Container, context?: any): xml.Node[] {
|
||||
const nodes: xml.Node[] = [];
|
||||
container.children.forEach((node: i18nAst.Node) => nodes.push(...node.visit(this)));
|
||||
container.children.forEach((node: i18n.Node) => nodes.push(...node.visit(this)));
|
||||
return nodes;
|
||||
}
|
||||
|
||||
visitIcu(icu: i18nAst.Icu, context?: any): xml.Node[] {
|
||||
visitIcu(icu: i18n.Icu, context?: any): xml.Node[] {
|
||||
const nodes = [new xml.Text(`{${icu.expression}, ${icu.type}, `)];
|
||||
|
||||
Object.getOwnPropertyNames(icu.cases).forEach((c: string) => {
|
||||
|
@ -73,7 +77,7 @@ class _Visitor implements i18nAst.Visitor {
|
|||
return nodes;
|
||||
}
|
||||
|
||||
visitTagPlaceholder(ph: i18nAst.TagPlaceholder, context?: any): xml.Node[] {
|
||||
visitTagPlaceholder(ph: i18n.TagPlaceholder, context?: any): xml.Node[] {
|
||||
const startEx = new xml.Tag(_EXEMPLE_TAG, {}, [new xml.Text(`<${ph.tag}>`)]);
|
||||
const startTagPh = new xml.Tag(_PLACEHOLDER_TAG, {name: ph.startName}, [startEx]);
|
||||
if (ph.isVoid) {
|
||||
|
@ -87,15 +91,15 @@ class _Visitor implements i18nAst.Visitor {
|
|||
return [startTagPh, ...this.serialize(ph.children), closeTagPh];
|
||||
}
|
||||
|
||||
visitPlaceholder(ph: i18nAst.Placeholder, context?: any): xml.Node[] {
|
||||
visitPlaceholder(ph: i18n.Placeholder, context?: any): xml.Node[] {
|
||||
return [new xml.Tag(_PLACEHOLDER_TAG, {name: ph.name})];
|
||||
}
|
||||
|
||||
visitIcuPlaceholder(ph: i18nAst.IcuPlaceholder, context?: any): xml.Node[] {
|
||||
visitIcuPlaceholder(ph: i18n.IcuPlaceholder, context?: any): xml.Node[] {
|
||||
return [new xml.Tag(_PLACEHOLDER_TAG, {name: ph.name})];
|
||||
}
|
||||
|
||||
serialize(nodes: i18nAst.Node[]): xml.Node[] {
|
||||
serialize(nodes: i18n.Node[]): xml.Node[] {
|
||||
return ListWrapper.flatten(nodes.map(node => node.visit(this)));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,149 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import * as xml from '../../html_parser/ast';
|
||||
import {HtmlParser} from '../../html_parser/html_parser';
|
||||
import {InterpolationConfig} from '../../html_parser/interpolation_config';
|
||||
import {XmlParser} from '../../html_parser/xml_parser';
|
||||
import {ParseError} from '../../parse_util';
|
||||
import * as i18n from '../i18n_ast';
|
||||
import {I18nError} from '../shared';
|
||||
|
||||
import {Serializer} from './serializer';
|
||||
|
||||
const _TRANSLATIONS_TAG = 'translationbundle';
|
||||
const _TRANSLATION_TAG = 'translation';
|
||||
const _PLACEHOLDER_TAG = 'ph';
|
||||
|
||||
export class Xtb implements Serializer {
|
||||
constructor(private _htmlParser: HtmlParser, private _interpolationConfig: InterpolationConfig) {}
|
||||
|
||||
write(messageMap: {[id: string]: i18n.Message}): string { throw new Error('Unsupported'); }
|
||||
|
||||
load(content: string, url: string, placeholders: {[id: string]: {[name: string]: string}}):
|
||||
{[id: string]: xml.Node[]} {
|
||||
// Parse the xtb file into xml nodes
|
||||
const result = new XmlParser().parse(content, url);
|
||||
|
||||
if (result.errors.length) {
|
||||
throw new Error(`xtb parse errors:\n${result.errors.join('\n')}`);
|
||||
}
|
||||
|
||||
// Replace the placeholders, messages are now string
|
||||
const {messages, errors} = new _Serializer().parse(result.rootNodes, placeholders);
|
||||
|
||||
if (errors.length) {
|
||||
throw new Error(`xtb parse errors:\n${errors.join('\n')}`);
|
||||
}
|
||||
|
||||
// Convert the string messages to html ast
|
||||
// TODO(vicb): map error message back to the original message in xtb
|
||||
let messageMap: {[id: string]: xml.Node[]} = {};
|
||||
let parseErrors: ParseError[] = [];
|
||||
|
||||
Object.getOwnPropertyNames(messages).forEach((id) => {
|
||||
const res = this._htmlParser.parse(messages[id], url, true, this._interpolationConfig);
|
||||
parseErrors.push(...res.errors);
|
||||
messageMap[id] = res.rootNodes;
|
||||
});
|
||||
|
||||
if (parseErrors.length) {
|
||||
throw new Error(`xtb parse errors:\n${parseErrors.join('\n')}`);
|
||||
}
|
||||
|
||||
return messageMap;
|
||||
}
|
||||
}
|
||||
|
||||
class _Serializer implements xml.Visitor {
|
||||
private _messages: {[id: string]: string};
|
||||
private _bundleDepth: number;
|
||||
private _translationDepth: number;
|
||||
private _errors: I18nError[];
|
||||
private _placeholders: {[id: string]: {[name: string]: string}};
|
||||
private _currentPlaceholders: {[name: string]: string};
|
||||
|
||||
parse(nodes: xml.Node[], _placeholders: {[id: string]: {[name: string]: string}}):
|
||||
{messages: {[k: string]: string}, errors: I18nError[]} {
|
||||
this._messages = {};
|
||||
this._bundleDepth = 0;
|
||||
this._translationDepth = 0;
|
||||
this._errors = [];
|
||||
this._placeholders = _placeholders;
|
||||
|
||||
xml.visitAll(this, nodes, null);
|
||||
|
||||
return {messages: this._messages, errors: this._errors};
|
||||
}
|
||||
|
||||
visitElement(element: xml.Element, context: any): any {
|
||||
switch (element.name) {
|
||||
case _TRANSLATIONS_TAG:
|
||||
this._bundleDepth++;
|
||||
if (this._bundleDepth > 1) {
|
||||
this._addError(element, `<${_TRANSLATIONS_TAG}> elements can not be nested`);
|
||||
}
|
||||
xml.visitAll(this, element.children, null);
|
||||
this._bundleDepth--;
|
||||
break;
|
||||
|
||||
case _TRANSLATION_TAG:
|
||||
this._translationDepth++;
|
||||
if (this._translationDepth > 1) {
|
||||
this._addError(element, `<${_TRANSLATION_TAG}> elements can not be nested`);
|
||||
}
|
||||
const idAttr = element.attrs.find((attr) => attr.name === 'id');
|
||||
if (!idAttr) {
|
||||
this._addError(element, `<${_TRANSLATION_TAG}> misses the "id" attribute`);
|
||||
} else {
|
||||
this._currentPlaceholders = this._placeholders[idAttr.value] || {};
|
||||
this._messages[idAttr.value] = xml.visitAll(this, element.children).join('');
|
||||
}
|
||||
this._translationDepth--;
|
||||
break;
|
||||
|
||||
case _PLACEHOLDER_TAG:
|
||||
const nameAttr = element.attrs.find((attr) => attr.name === 'name');
|
||||
if (!nameAttr) {
|
||||
this._addError(element, `<${_PLACEHOLDER_TAG}> misses the "name" attribute`);
|
||||
} else {
|
||||
if (this._currentPlaceholders.hasOwnProperty(nameAttr.value)) {
|
||||
return this._currentPlaceholders[nameAttr.value];
|
||||
}
|
||||
this._addError(
|
||||
element, `The placeholder "${nameAttr.value}" does not exists in the source message`);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
this._addError(element, 'Unexpected tag');
|
||||
}
|
||||
}
|
||||
|
||||
visitAttribute(attribute: xml.Attribute, context: any): any {
|
||||
throw new Error('unreachable code');
|
||||
}
|
||||
|
||||
visitText(text: xml.Text, context: any): any { return text.value; }
|
||||
|
||||
visitComment(comment: xml.Comment, context: any): any { return ''; }
|
||||
|
||||
visitExpansion(expansion: xml.Expansion, context: any): any {
|
||||
const strCases = expansion.cases.map(c => c.visit(this, null));
|
||||
|
||||
return `{${expansion.switchValue}, ${expansion.type}, strCases.join(' ')}`;
|
||||
}
|
||||
|
||||
visitExpansionCase(expansionCase: xml.ExpansionCase, context: any): any {
|
||||
return `${expansionCase.value} {${xml.visitAll(this, expansionCase.expression, null)}}`;
|
||||
}
|
||||
|
||||
private _addError(node: xml.Node, message: string): void {
|
||||
this._errors.push(new I18nError(node.sourceSpan, message));
|
||||
}
|
||||
}
|
|
@ -6,14 +6,10 @@
|
|||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {normalizeBlank} from '../../../router-deprecated/src/facade/lang';
|
||||
import {Parser as ExpressionParser} from '../expression_parser/parser';
|
||||
import {StringWrapper, isBlank, isPresent} from '../facade/lang';
|
||||
import {HtmlAst, HtmlAstVisitor, HtmlAttrAst, HtmlCommentAst, HtmlElementAst, HtmlExpansionAst, HtmlExpansionCaseAst, HtmlTextAst, htmlVisitAll} from '../html_parser/html_ast';
|
||||
import {InterpolationConfig} from '../html_parser/interpolation_config';
|
||||
import {StringWrapper, isBlank, isPresent, normalizeBlank} from '../facade/lang';
|
||||
import * as html from '../html_parser/ast';
|
||||
import {ParseError, ParseSourceSpan} from '../parse_util';
|
||||
|
||||
import {Message} from './message';
|
||||
|
||||
export const I18N_ATTR = 'i18n';
|
||||
export const I18N_ATTR_PREFIX = 'i18n-';
|
||||
|
@ -26,74 +22,15 @@ export class I18nError extends ParseError {
|
|||
constructor(span: ParseSourceSpan, msg: string) { super(span, msg); }
|
||||
}
|
||||
|
||||
export function partition(nodes: HtmlAst[], errors: ParseError[], implicitTags: string[]): Part[] {
|
||||
let parts: Part[] = [];
|
||||
|
||||
for (let i = 0; i < nodes.length; ++i) {
|
||||
let node = nodes[i];
|
||||
let msgNodes: HtmlAst[] = [];
|
||||
// Nodes between `<!-- i18n -->` and `<!-- /i18n -->`
|
||||
if (isOpeningComment(node)) {
|
||||
let i18n = (<HtmlCommentAst>node).value.replace(/^i18n:?/, '').trim();
|
||||
|
||||
while (++i < nodes.length && !isClosingComment(nodes[i])) {
|
||||
msgNodes.push(nodes[i]);
|
||||
}
|
||||
|
||||
if (i === nodes.length) {
|
||||
errors.push(new I18nError(node.sourceSpan, 'Missing closing \'i18n\' comment.'));
|
||||
break;
|
||||
}
|
||||
|
||||
parts.push(new Part(null, null, msgNodes, i18n, true));
|
||||
} else if (node instanceof HtmlElementAst) {
|
||||
// Node with an `i18n` attribute
|
||||
let i18n = getI18nAttr(node);
|
||||
let hasI18n: boolean = isPresent(i18n) || implicitTags.indexOf(node.name) > -1;
|
||||
parts.push(new Part(node, null, node.children, isPresent(i18n) ? i18n.value : null, hasI18n));
|
||||
} else if (node instanceof HtmlTextAst) {
|
||||
parts.push(new Part(null, node, null, null, false));
|
||||
}
|
||||
}
|
||||
|
||||
return parts;
|
||||
export function isOpeningComment(n: html.Node): boolean {
|
||||
return n instanceof html.Comment && isPresent(n.value) && n.value.startsWith('i18n');
|
||||
}
|
||||
|
||||
export class Part {
|
||||
constructor(
|
||||
public rootElement: HtmlElementAst, public rootTextNode: HtmlTextAst,
|
||||
public children: HtmlAst[], public i18n: string, public hasI18n: boolean) {}
|
||||
|
||||
get sourceSpan(): ParseSourceSpan {
|
||||
if (isPresent(this.rootElement)) {
|
||||
return this.rootElement.sourceSpan;
|
||||
}
|
||||
if (isPresent(this.rootTextNode)) {
|
||||
return this.rootTextNode.sourceSpan;
|
||||
}
|
||||
|
||||
return new ParseSourceSpan(
|
||||
this.children[0].sourceSpan.start, this.children[this.children.length - 1].sourceSpan.end);
|
||||
}
|
||||
|
||||
createMessages(parser: ExpressionParser, interpolationConfig: InterpolationConfig): Message[] {
|
||||
let {message, icuMessages} = stringifyNodes(this.children, parser, interpolationConfig);
|
||||
return [
|
||||
new Message(message, meaning(this.i18n), description(this.i18n)),
|
||||
...icuMessages.map(icu => new Message(icu, null))
|
||||
];
|
||||
}
|
||||
export function isClosingComment(n: html.Node): boolean {
|
||||
return n instanceof html.Comment && isPresent(n.value) && n.value === '/i18n';
|
||||
}
|
||||
|
||||
export function isOpeningComment(n: HtmlAst): boolean {
|
||||
return n instanceof HtmlCommentAst && isPresent(n.value) && n.value.startsWith('i18n');
|
||||
}
|
||||
|
||||
export function isClosingComment(n: HtmlAst): boolean {
|
||||
return n instanceof HtmlCommentAst && isPresent(n.value) && n.value === '/i18n';
|
||||
}
|
||||
|
||||
export function getI18nAttr(p: HtmlElementAst): HtmlAttrAst {
|
||||
export function getI18nAttr(p: html.Element): html.Attribute {
|
||||
return normalizeBlank(p.attrs.find(attr => attr.name === I18N_ATTR));
|
||||
}
|
||||
|
||||
|
@ -108,148 +45,6 @@ export function description(i18n: string): string {
|
|||
return parts.length > 1 ? parts[1] : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract a translation string given an `i18n-` prefixed attribute.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export function messageFromI18nAttribute(
|
||||
parser: ExpressionParser, interpolationConfig: InterpolationConfig, p: HtmlElementAst,
|
||||
i18nAttr: HtmlAttrAst): Message {
|
||||
const expectedName = i18nAttr.name.substring(5);
|
||||
const attr = p.attrs.find(a => a.name == expectedName);
|
||||
|
||||
if (attr) {
|
||||
return messageFromAttribute(
|
||||
parser, interpolationConfig, attr, meaning(i18nAttr.value), description(i18nAttr.value));
|
||||
}
|
||||
|
||||
throw new I18nError(p.sourceSpan, `Missing attribute '${expectedName}'.`);
|
||||
}
|
||||
|
||||
export function messageFromAttribute(
|
||||
parser: ExpressionParser, interpolationConfig: InterpolationConfig, attr: HtmlAttrAst,
|
||||
meaning: string = null, description: string = null): Message {
|
||||
const value = removeInterpolation(attr.value, attr.sourceSpan, parser, interpolationConfig);
|
||||
return new Message(value, meaning, description);
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace interpolation in the `value` string with placeholders
|
||||
*/
|
||||
export function removeInterpolation(
|
||||
value: string, source: ParseSourceSpan, expressionParser: ExpressionParser,
|
||||
interpolationConfig: InterpolationConfig): string {
|
||||
try {
|
||||
const parsed =
|
||||
expressionParser.splitInterpolation(value, source.toString(), interpolationConfig);
|
||||
const usedNames = new Map<string, number>();
|
||||
if (isPresent(parsed)) {
|
||||
let res = '';
|
||||
for (let i = 0; i < parsed.strings.length; ++i) {
|
||||
res += parsed.strings[i];
|
||||
if (i != parsed.strings.length - 1) {
|
||||
let customPhName = extractPhNameFromInterpolation(parsed.expressions[i], i);
|
||||
customPhName = dedupePhName(usedNames, customPhName);
|
||||
res += `<ph name="${customPhName}"/>`;
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
return value;
|
||||
} catch (e) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the placeholder name from the interpolation.
|
||||
*
|
||||
* Use a custom name when specified (ie: `{{<expression> //i18n(ph="FIRST")}}`) otherwise generate a
|
||||
* unique name.
|
||||
*/
|
||||
export function extractPhNameFromInterpolation(input: string, index: number): string {
|
||||
let customPhMatch = StringWrapper.split(input, _CUSTOM_PH_EXP);
|
||||
return customPhMatch.length > 1 ? customPhMatch[1] : `INTERPOLATION_${index}`;
|
||||
}
|
||||
|
||||
export function extractPlaceholderName(input: string): string {
|
||||
return StringWrapper.split(input, _CUSTOM_PH_EXP)[1];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return a unique placeholder name based on the given name
|
||||
*/
|
||||
export function dedupePhName(usedNames: Map<string, number>, name: string): string {
|
||||
const duplicateNameCount = usedNames.get(name);
|
||||
|
||||
if (duplicateNameCount) {
|
||||
usedNames.set(name, duplicateNameCount + 1);
|
||||
return `${name}_${duplicateNameCount}`;
|
||||
}
|
||||
|
||||
usedNames.set(name, 1);
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a list of nodes to a string message.
|
||||
*
|
||||
*/
|
||||
export function stringifyNodes(
|
||||
nodes: HtmlAst[], expressionParser: ExpressionParser,
|
||||
interpolationConfig: InterpolationConfig): {message: string, icuMessages: string[]} {
|
||||
const visitor = new _StringifyVisitor(expressionParser, interpolationConfig);
|
||||
const icuMessages: string[] = [];
|
||||
const message = htmlVisitAll(visitor, nodes, icuMessages).join('');
|
||||
return {message, icuMessages};
|
||||
}
|
||||
|
||||
class _StringifyVisitor implements HtmlAstVisitor {
|
||||
private _index: number = 0;
|
||||
private _nestedExpansion = 0;
|
||||
|
||||
constructor(
|
||||
private _expressionParser: ExpressionParser,
|
||||
private _interpolationConfig: InterpolationConfig) {}
|
||||
|
||||
visitElement(ast: HtmlElementAst, context: any): any {
|
||||
const index = this._index++;
|
||||
const children = this._join(htmlVisitAll(this, ast.children), '');
|
||||
return `<ph name="e${index}">${children}</ph>`;
|
||||
}
|
||||
|
||||
visitAttr(ast: HtmlAttrAst, context: any): any { return null; }
|
||||
|
||||
visitText(ast: HtmlTextAst, context: any): any {
|
||||
const index = this._index++;
|
||||
const noInterpolation = removeInterpolation(
|
||||
ast.value, ast.sourceSpan, this._expressionParser, this._interpolationConfig);
|
||||
if (noInterpolation != ast.value) {
|
||||
return `<ph name="t${index}">${noInterpolation}</ph>`;
|
||||
}
|
||||
return ast.value;
|
||||
}
|
||||
|
||||
visitComment(ast: HtmlCommentAst, context: any): any { return ''; }
|
||||
|
||||
visitExpansion(ast: HtmlExpansionAst, context: any): any {
|
||||
const index = this._index++;
|
||||
this._nestedExpansion++;
|
||||
const content = `{${ast.switchValue}, ${ast.type}${htmlVisitAll(this, ast.cases).join('')}}`;
|
||||
this._nestedExpansion--;
|
||||
|
||||
return this._nestedExpansion == 0 ? `<ph name="x${index}">${content}</ph>` : content;
|
||||
}
|
||||
|
||||
visitExpansionCase(ast: HtmlExpansionCaseAst, context: any): any {
|
||||
const expStr = htmlVisitAll(this, ast.expression).join('');
|
||||
return ` ${ast.value} {${expStr}}`;
|
||||
}
|
||||
|
||||
private _join(strs: string[], str: string): string {
|
||||
return strs.filter(s => s.length > 0).join(str);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import * as html from '../html_parser/ast';
|
||||
import {Serializer} from './serializers/serializer';
|
||||
|
||||
|
||||
/**
|
||||
* A container for translated messages
|
||||
*/
|
||||
export class TranslationBundle {
|
||||
constructor(private _messageMap: {[id: string]: html.Node[]} = {}) {}
|
||||
|
||||
static load(
|
||||
content: string, url: string, placeholders: {[id: string]: {[name: string]: string}},
|
||||
serializer: Serializer): TranslationBundle {
|
||||
return new TranslationBundle(serializer.load(content, 'url', placeholders));
|
||||
}
|
||||
}
|
|
@ -1,112 +0,0 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {RegExpWrapper, isPresent} from '../facade/lang';
|
||||
import {HtmlAst, HtmlElementAst} from '../html_parser/html_ast';
|
||||
import {HtmlParser} from '../html_parser/html_parser';
|
||||
import {ParseError, ParseSourceSpan} from '../parse_util';
|
||||
|
||||
import {Message, id} from './message';
|
||||
|
||||
let _PLACEHOLDER_REGEXP = RegExpWrapper.create(`\\<ph(\\s)+name=("(\\w)+")\\/\\>`);
|
||||
const _ID_ATTR = 'id';
|
||||
const _MSG_ELEMENT = 'msg';
|
||||
const _BUNDLE_ELEMENT = 'message-bundle';
|
||||
|
||||
export function serializeXmb(messages: Message[]): string {
|
||||
let ms = messages.map((m) => _serializeMessage(m)).join('');
|
||||
return `<message-bundle>${ms}</message-bundle>`;
|
||||
}
|
||||
|
||||
export class XmbDeserializationResult {
|
||||
constructor(
|
||||
public content: string, public messages: {[key: string]: HtmlAst[]},
|
||||
public errors: ParseError[]) {}
|
||||
}
|
||||
|
||||
export class XmbDeserializationError extends ParseError {
|
||||
constructor(span: ParseSourceSpan, msg: string) { super(span, msg); }
|
||||
}
|
||||
|
||||
export function deserializeXmb(content: string, url: string): XmbDeserializationResult {
|
||||
const normalizedContent = _expandPlaceholder(content.trim());
|
||||
const parsed = new HtmlParser().parse(normalizedContent, url);
|
||||
|
||||
if (parsed.errors.length > 0) {
|
||||
return new XmbDeserializationResult(null, {}, parsed.errors);
|
||||
}
|
||||
|
||||
if (_checkRootElement(parsed.rootNodes)) {
|
||||
return new XmbDeserializationResult(
|
||||
null, {}, [new XmbDeserializationError(null, `Missing element "${_BUNDLE_ELEMENT}"`)]);
|
||||
}
|
||||
|
||||
const bundleEl = <HtmlElementAst>parsed.rootNodes[0]; // test this
|
||||
const errors: ParseError[] = [];
|
||||
const messages: {[key: string]: HtmlAst[]} = {};
|
||||
|
||||
_createMessages(bundleEl.children, messages, errors);
|
||||
|
||||
return (errors.length == 0) ?
|
||||
new XmbDeserializationResult(normalizedContent, messages, []) :
|
||||
new XmbDeserializationResult(null, <{[key: string]: HtmlAst[]}>{}, errors);
|
||||
}
|
||||
|
||||
function _checkRootElement(nodes: HtmlAst[]): boolean {
|
||||
return nodes.length < 1 || !(nodes[0] instanceof HtmlElementAst) ||
|
||||
(<HtmlElementAst>nodes[0]).name != _BUNDLE_ELEMENT;
|
||||
}
|
||||
|
||||
function _createMessages(
|
||||
nodes: HtmlAst[], messages: {[key: string]: HtmlAst[]}, errors: ParseError[]): void {
|
||||
nodes.forEach((node) => {
|
||||
if (node instanceof HtmlElementAst) {
|
||||
let msg = <HtmlElementAst>node;
|
||||
|
||||
if (msg.name != _MSG_ELEMENT) {
|
||||
errors.push(
|
||||
new XmbDeserializationError(node.sourceSpan, `Unexpected element "${msg.name}"`));
|
||||
return;
|
||||
}
|
||||
|
||||
let idAttr = msg.attrs.find(a => a.name == _ID_ATTR);
|
||||
|
||||
if (idAttr) {
|
||||
messages[idAttr.value] = msg.children;
|
||||
} else {
|
||||
errors.push(
|
||||
new XmbDeserializationError(node.sourceSpan, `"${_ID_ATTR}" attribute is missing`));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function _serializeMessage(m: Message): string {
|
||||
const desc = isPresent(m.description) ? ` desc='${_escapeXml(m.description)}'` : '';
|
||||
const meaning = isPresent(m.meaning) ? ` meaning='${_escapeXml(m.meaning)}'` : '';
|
||||
return `<msg id='${id(m)}'${desc}${meaning}>${m.content}</msg>`;
|
||||
}
|
||||
|
||||
function _expandPlaceholder(input: string): string {
|
||||
return RegExpWrapper.replaceAll(_PLACEHOLDER_REGEXP, input, (match: string[]) => {
|
||||
let nameWithQuotes = match[2];
|
||||
return `<ph name=${nameWithQuotes}></ph>`;
|
||||
});
|
||||
}
|
||||
|
||||
const _XML_ESCAPED_CHARS: [RegExp, string][] = [
|
||||
[/&/g, '&'],
|
||||
[/"/g, '"'],
|
||||
[/'/g, '''],
|
||||
[/</g, '<'],
|
||||
[/>/g, '>'],
|
||||
];
|
||||
|
||||
function _escapeXml(value: string): string {
|
||||
return _XML_ESCAPED_CHARS.reduce((value, escape) => value.replace(escape[0], escape[1]), value);
|
||||
}
|
|
@ -9,15 +9,15 @@
|
|||
import {AnimationAnimateMetadata, AnimationEntryMetadata, AnimationGroupMetadata, AnimationKeyframesSequenceMetadata, AnimationMetadata, AnimationStateDeclarationMetadata, AnimationStateMetadata, AnimationStateTransitionMetadata, AnimationStyleMetadata, AnimationWithStepsMetadata, AttributeMetadata, ChangeDetectionStrategy, ComponentMetadata, HostMetadata, Inject, InjectMetadata, Injectable, ModuleWithProviders, NgModule, NgModuleMetadata, Optional, OptionalMetadata, Provider, QueryMetadata, SchemaMetadata, SelfMetadata, SkipSelfMetadata, ViewMetadata, ViewQueryMetadata, resolveForwardRef} from '@angular/core';
|
||||
|
||||
import {Console, LIFECYCLE_HOOKS_VALUES, ReflectorReader, createProvider, isProviderLiteral, reflector} from '../core_private';
|
||||
import {MapWrapper, StringMapWrapper} from '../src/facade/collection';
|
||||
import {BaseException} from '../src/facade/exceptions';
|
||||
import {Type, isArray, isBlank, isPresent, isString, stringify} from '../src/facade/lang';
|
||||
import {StringMapWrapper} from '../src/facade/collection';
|
||||
|
||||
import {assertArrayOfStrings, assertInterpolationSymbols} from './assertions';
|
||||
import * as cpl from './compile_metadata';
|
||||
import {CompilerConfig} from './config';
|
||||
import {hasLifecycleHook} from './directive_lifecycle_reflector';
|
||||
import {DirectiveResolver} from './directive_resolver';
|
||||
import {BaseException} from './facade/exceptions';
|
||||
import {Type, isArray, isBlank, isPresent, isString, stringify} from './facade/lang';
|
||||
import {Identifiers, identifierToken} from './identifiers';
|
||||
import {NgModuleResolver} from './ng_module_resolver';
|
||||
import {PipeResolver} from './pipe_resolver';
|
||||
|
|
|
@ -10,7 +10,7 @@ import {Injectable, NgModuleMetadata} from '@angular/core';
|
|||
|
||||
import {ReflectorReader, reflector} from '../core_private';
|
||||
import {BaseException} from '../src/facade/exceptions';
|
||||
import {Type, isBlank, isPresent, stringify} from '../src/facade/lang';
|
||||
import {Type, isPresent, stringify} from './facade/lang';
|
||||
|
||||
function _isNgModuleMetadata(obj: any): obj is NgModuleMetadata {
|
||||
return obj instanceof NgModuleMetadata;
|
||||
|
|
|
@ -70,7 +70,7 @@ export class OfflineCompiler {
|
|||
return Promise
|
||||
.all(components.map((compType) => {
|
||||
const compMeta = this._metadataResolver.getDirectiveMetadata(<any>compType);
|
||||
let ngModule = ngModulesSummary.ngModuleByComponent.get(compType);
|
||||
const ngModule = ngModulesSummary.ngModuleByComponent.get(compType);
|
||||
if (!ngModule) {
|
||||
throw new BaseException(
|
||||
`Cannot determine the module for component ${compMeta.type.name}!`);
|
||||
|
|
|
@ -9,8 +9,8 @@
|
|||
import {Injectable, PipeMetadata, resolveForwardRef} from '@angular/core';
|
||||
|
||||
import {ReflectorReader, reflector} from '../core_private';
|
||||
import {BaseException} from '../src/facade/exceptions';
|
||||
import {Type, isPresent, stringify} from '../src/facade/lang';
|
||||
import {BaseException} from './facade/exceptions';
|
||||
import {Type, isPresent, stringify} from './facade/lang';
|
||||
|
||||
function _isPipeMetadata(type: any): boolean {
|
||||
return type instanceof PipeMetadata;
|
||||
|
|
|
@ -6,11 +6,10 @@
|
|||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {ListWrapper} from '../src/facade/collection';
|
||||
import {BaseException} from '../src/facade/exceptions';
|
||||
import {isArray, isBlank, isPresent, normalizeBlank} from '../src/facade/lang';
|
||||
|
||||
import {CompileDiDependencyMetadata, CompileDirectiveMetadata, CompileIdentifierMap, CompileNgModuleMetadata, CompileProviderMetadata, CompileQueryMetadata, CompileTokenMetadata, CompileTypeMetadata} from './compile_metadata';
|
||||
import {ListWrapper} from './facade/collection';
|
||||
import {BaseException} from './facade/exceptions';
|
||||
import {isArray, isBlank, isPresent, normalizeBlank} from './facade/lang';
|
||||
import {Identifiers, identifierToken} from './identifiers';
|
||||
import {ParseError, ParseSourceSpan} from './parse_util';
|
||||
import {AttrAst, DirectiveAst, ProviderAst, ProviderAstType, ReferenceAst, VariableAst} from './template_parser/template_ast';
|
||||
|
|
|
@ -9,22 +9,24 @@
|
|||
import {Compiler, ComponentFactory, ComponentResolver, ComponentStillLoadingError, Injectable, Injector, ModuleWithComponentFactories, NgModule, NgModuleFactory, NgModuleMetadata, OptionalMetadata, Provider, SchemaMetadata, SkipSelfMetadata} from '@angular/core';
|
||||
|
||||
import {Console} from '../core_private';
|
||||
import {BaseException} from '../src/facade/exceptions';
|
||||
import {ConcreteType, IS_DART, Type, isBlank, isString, stringify} from '../src/facade/lang';
|
||||
|
||||
import {PromiseWrapper} from '../src/facade/async';
|
||||
import {createHostComponentMeta, CompileDirectiveMetadata, CompilePipeMetadata, CompileIdentifierMetadata, CompileNgModuleMetadata} from './compile_metadata';
|
||||
import {StyleCompiler, CompiledStylesheet} from './style_compiler';
|
||||
import {ViewCompiler, ViewFactoryDependency, ComponentFactoryDependency} from './view_compiler/view_compiler';
|
||||
import {NgModuleCompiler} from './ng_module_compiler';
|
||||
import {TemplateParser} from './template_parser/template_parser';
|
||||
import {DirectiveNormalizer} from './directive_normalizer';
|
||||
import {CompileMetadataResolver} from './metadata_resolver';
|
||||
import {CompileDirectiveMetadata, CompileIdentifierMetadata, CompileNgModuleMetadata, CompilePipeMetadata, createHostComponentMeta} from './compile_metadata';
|
||||
import {CompilerConfig} from './config';
|
||||
import {DirectiveNormalizer} from './directive_normalizer';
|
||||
import {PromiseWrapper} from './facade/async';
|
||||
import {BaseException} from './facade/exceptions';
|
||||
import {ConcreteType, IS_DART, Type, isBlank, isString, stringify} from './facade/lang';
|
||||
import {CompileMetadataResolver} from './metadata_resolver';
|
||||
import {NgModuleCompiler} from './ng_module_compiler';
|
||||
import * as ir from './output/output_ast';
|
||||
import {jitStatements} from './output/output_jit';
|
||||
import {interpretStatements} from './output/output_interpreter';
|
||||
import {jitStatements} from './output/output_jit';
|
||||
import {CompiledStylesheet, StyleCompiler} from './style_compiler';
|
||||
import {TemplateParser} from './template_parser/template_parser';
|
||||
import {SyncAsyncResult} from './util';
|
||||
import {ComponentFactoryDependency, ViewCompiler, ViewFactoryDependency} from './view_compiler/view_compiler';
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* An internal module of the Angular compiler that begins with component types,
|
||||
|
|
|
@ -6,9 +6,9 @@
|
|||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {ListWrapper} from '../src/facade/collection';
|
||||
import {BaseException} from '../src/facade/exceptions';
|
||||
import {RegExpMatcherWrapper, RegExpWrapper, StringWrapper, isBlank, isPresent} from '../src/facade/lang';
|
||||
import {ListWrapper} from './facade/collection';
|
||||
import {BaseException} from './facade/exceptions';
|
||||
import {RegExpMatcherWrapper, RegExpWrapper, StringWrapper, isBlank, isPresent} from './facade/lang';
|
||||
|
||||
const _EMPTY_ATTR_VALUE = /*@ts2dart_const*/ '';
|
||||
|
||||
|
|
|
@ -6,8 +6,8 @@
|
|||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {ListWrapper} from '../src/facade/collection';
|
||||
import {RegExpMatcherWrapper, RegExpWrapper, StringWrapper, isBlank, isPresent} from '../src/facade/lang';
|
||||
import {ListWrapper} from './facade/collection';
|
||||
import {RegExpMatcherWrapper, RegExpWrapper, StringWrapper, isBlank, isPresent} from './facade/lang';
|
||||
|
||||
/**
|
||||
* This file is a port of shadowCSS from webcomponents.js to TypeScript.
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
// Some of the code comes from WebComponents.JS
|
||||
// https://github.com/webcomponents/webcomponentsjs/blob/master/src/HTMLImports/path.js
|
||||
|
||||
import {RegExpWrapper, StringWrapper, isBlank, isPresent} from '../src/facade/lang';
|
||||
import {RegExpWrapper, StringWrapper, isBlank, isPresent} from './facade/lang';
|
||||
|
||||
import {UrlResolver} from './url_resolver';
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ import {isPresent} from '../facade/lang';
|
|||
|
||||
import {CompileDirectiveMetadata, CompileTokenMetadata, CompileProviderMetadata,} from '../compile_metadata';
|
||||
import {ParseSourceSpan} from '../parse_util';
|
||||
import {SecurityContext} from '../../../core/index';
|
||||
import {SecurityContext} from '@angular/core';
|
||||
|
||||
/**
|
||||
* An Abstract Syntax Tree node representing part of a parsed Angular template.
|
||||
|
|
|
@ -6,30 +6,28 @@
|
|||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {Inject, Injectable, OpaqueToken, Optional, SecurityContext, SchemaMetadata} from '../../../core/index';
|
||||
import {Inject, Injectable, OpaqueToken, Optional, SchemaMetadata, SecurityContext} from '@angular/core';
|
||||
import {Console, MAX_INTERPOLATION_VALUES} from '../../core_private';
|
||||
|
||||
import {ListWrapper, StringMapWrapper, SetWrapper,} from '../facade/collection';
|
||||
import {RegExpWrapper, isPresent, StringWrapper, isBlank} from '../facade/lang';
|
||||
import {RegExpWrapper, isPresent, isBlank} from '../facade/lang';
|
||||
import {BaseException} from '../facade/exceptions';
|
||||
import {AST, Interpolation, ASTWithSource, TemplateBinding, RecursiveAstVisitor, BindingPipe, ParserError} from '../expression_parser/ast';
|
||||
import {Parser} from '../expression_parser/parser';
|
||||
import {
|
||||
CompileDirectiveMetadata, CompilePipeMetadata, CompileTokenMetadata,
|
||||
removeIdentifierDuplicates,
|
||||
} from '../compile_metadata';
|
||||
import {HtmlParser, HtmlParseTreeResult} from '../html_parser/html_parser';
|
||||
import {splitNsName, mergeNsAndName} from '../html_parser/html_tags';
|
||||
import {CompileDirectiveMetadata, CompilePipeMetadata, CompileTokenMetadata, removeIdentifierDuplicates,} from '../compile_metadata';
|
||||
import {HtmlParser, ParseTreeResult} from '../html_parser/html_parser';
|
||||
import {splitNsName, mergeNsAndName} from '../html_parser/tags';
|
||||
import {ParseSourceSpan, ParseError, ParseErrorLevel} from '../parse_util';
|
||||
import {InterpolationConfig} from '../html_parser/interpolation_config';
|
||||
import {ElementAst, BoundElementPropertyAst, BoundEventAst, ReferenceAst, TemplateAst, TemplateAstVisitor, templateVisitAll, TextAst, BoundTextAst, EmbeddedTemplateAst, AttrAst, NgContentAst, PropertyBindingType, DirectiveAst, BoundDirectivePropertyAst, ProviderAst, ProviderAstType, VariableAst} from './template_ast';
|
||||
import {ElementAst, BoundElementPropertyAst, BoundEventAst, ReferenceAst, TemplateAst, TemplateAstVisitor, templateVisitAll, TextAst, BoundTextAst, EmbeddedTemplateAst, AttrAst, NgContentAst, PropertyBindingType, DirectiveAst, BoundDirectivePropertyAst, VariableAst} from './template_ast';
|
||||
import {CssSelector, SelectorMatcher} from '../selector';
|
||||
import {ElementSchemaRegistry} from '../schema/element_schema_registry';
|
||||
import {preparseElement, PreparsedElementType} from './template_preparser';
|
||||
import {isStyleUrlResolvable} from '../style_url_resolver';
|
||||
import {HtmlAstVisitor, HtmlElementAst, HtmlAttrAst, HtmlTextAst, HtmlCommentAst, HtmlExpansionAst, HtmlExpansionCaseAst, htmlVisitAll} from '../html_parser/html_ast';
|
||||
import * as html from '../html_parser/ast';
|
||||
import {splitAtColon} from '../util';
|
||||
import {identifierToken, Identifiers} from '../identifiers';
|
||||
import {expandNodes} from '../html_parser/expander';
|
||||
import {expandNodes} from '../html_parser/icu_ast_expander';
|
||||
import {ProviderElementContext, ProviderViewContext} from '../provider_analyzer';
|
||||
|
||||
// Group 1 = "bind-"
|
||||
|
@ -118,7 +116,7 @@ export class TemplateParser {
|
|||
// Transform ICU messages to angular directives
|
||||
const expandedHtmlAst = expandNodes(htmlAstWithErrors.rootNodes);
|
||||
errors.push(...expandedHtmlAst.errors);
|
||||
htmlAstWithErrors = new HtmlParseTreeResult(expandedHtmlAst.nodes, errors);
|
||||
htmlAstWithErrors = new ParseTreeResult(expandedHtmlAst.nodes, errors);
|
||||
}
|
||||
|
||||
if (htmlAstWithErrors.rootNodes.length > 0) {
|
||||
|
@ -130,7 +128,7 @@ export class TemplateParser {
|
|||
providerViewContext, uniqDirectives, uniqPipes, schemas, this._exprParser,
|
||||
this._schemaRegistry);
|
||||
|
||||
result = htmlVisitAll(parseVisitor, htmlAstWithErrors.rootNodes, EMPTY_ELEMENT_CONTEXT);
|
||||
result = html.visitAll(parseVisitor, htmlAstWithErrors.rootNodes, EMPTY_ELEMENT_CONTEXT);
|
||||
errors.push(...parseVisitor.errors, ...providerViewContext.errors);
|
||||
} else {
|
||||
result = [];
|
||||
|
@ -170,7 +168,7 @@ export class TemplateParser {
|
|||
}
|
||||
}
|
||||
|
||||
class TemplateParseVisitor implements HtmlAstVisitor {
|
||||
class TemplateParseVisitor implements html.Visitor {
|
||||
selectorMatcher: SelectorMatcher;
|
||||
errors: TemplateParseError[] = [];
|
||||
directivesIndex = new Map<CompileDirectiveMetadata, number>();
|
||||
|
@ -291,27 +289,27 @@ class TemplateParseVisitor implements HtmlAstVisitor {
|
|||
}
|
||||
}
|
||||
|
||||
visitExpansion(ast: HtmlExpansionAst, context: any): any { return null; }
|
||||
visitExpansion(expansion: html.Expansion, context: any): any { return null; }
|
||||
|
||||
visitExpansionCase(ast: HtmlExpansionCaseAst, context: any): any { return null; }
|
||||
visitExpansionCase(expansionCase: html.ExpansionCase, context: any): any { return null; }
|
||||
|
||||
visitText(ast: HtmlTextAst, parent: ElementContext): any {
|
||||
visitText(text: html.Text, parent: ElementContext): any {
|
||||
const ngContentIndex = parent.findNgContentIndex(TEXT_CSS_SELECTOR);
|
||||
const expr = this._parseInterpolation(ast.value, ast.sourceSpan);
|
||||
const expr = this._parseInterpolation(text.value, text.sourceSpan);
|
||||
if (isPresent(expr)) {
|
||||
return new BoundTextAst(expr, ngContentIndex, ast.sourceSpan);
|
||||
return new BoundTextAst(expr, ngContentIndex, text.sourceSpan);
|
||||
} else {
|
||||
return new TextAst(ast.value, ngContentIndex, ast.sourceSpan);
|
||||
return new TextAst(text.value, ngContentIndex, text.sourceSpan);
|
||||
}
|
||||
}
|
||||
|
||||
visitAttr(ast: HtmlAttrAst, contex: any): any {
|
||||
return new AttrAst(ast.name, ast.value, ast.sourceSpan);
|
||||
visitAttribute(attribute: html.Attribute, contex: any): any {
|
||||
return new AttrAst(attribute.name, attribute.value, attribute.sourceSpan);
|
||||
}
|
||||
|
||||
visitComment(ast: HtmlCommentAst, context: any): any { return null; }
|
||||
visitComment(comment: html.Comment, context: any): any { return null; }
|
||||
|
||||
visitElement(element: HtmlElementAst, parent: ElementContext): any {
|
||||
visitElement(element: html.Element, parent: ElementContext): any {
|
||||
const nodeName = element.name;
|
||||
const preparsedElement = preparseElement(element);
|
||||
if (preparsedElement.type === PreparsedElementType.SCRIPT ||
|
||||
|
@ -359,7 +357,7 @@ class TemplateParseVisitor implements HtmlAstVisitor {
|
|||
|
||||
if (!hasBinding && !hasTemplateBinding) {
|
||||
// don't include the bindings as attributes as well in the AST
|
||||
attrs.push(this.visitAttr(attr, null));
|
||||
attrs.push(this.visitAttribute(attr, null));
|
||||
matchableAttrs.push([attr.name, attr.value]);
|
||||
}
|
||||
if (hasTemplateBinding) {
|
||||
|
@ -380,7 +378,7 @@ class TemplateParseVisitor implements HtmlAstVisitor {
|
|||
const providerContext = new ProviderElementContext(
|
||||
this.providerViewContext, parent.providerContext, isViewRoot, directiveAsts, attrs,
|
||||
references, element.sourceSpan);
|
||||
const children = htmlVisitAll(
|
||||
const children = html.visitAll(
|
||||
preparsedElement.nonBindable ? NON_BINDABLE_VISITOR : this, element.children,
|
||||
ElementContext.create(
|
||||
isTemplateElement, directiveAsts,
|
||||
|
@ -448,7 +446,7 @@ class TemplateParseVisitor implements HtmlAstVisitor {
|
|||
}
|
||||
|
||||
private _parseInlineTemplateBinding(
|
||||
attr: HtmlAttrAst, targetMatchableAttrs: string[][],
|
||||
attr: html.Attribute, targetMatchableAttrs: string[][],
|
||||
targetProps: BoundElementOrDirectiveProperty[], targetVars: VariableAst[]): boolean {
|
||||
let templateBindingsSource: string = null;
|
||||
if (this._normalizeAttributeName(attr.name) == TEMPLATE_ATTR) {
|
||||
|
@ -477,7 +475,7 @@ class TemplateParseVisitor implements HtmlAstVisitor {
|
|||
}
|
||||
|
||||
private _parseAttr(
|
||||
isTemplateElement: boolean, attr: HtmlAttrAst, targetMatchableAttrs: string[][],
|
||||
isTemplateElement: boolean, attr: html.Attribute, targetMatchableAttrs: string[][],
|
||||
targetProps: BoundElementOrDirectiveProperty[],
|
||||
targetAnimationProps: BoundElementPropertyAst[], targetEvents: BoundEventAst[],
|
||||
targetRefs: ElementOrDirectiveRef[], targetVars: VariableAst[]): boolean {
|
||||
|
@ -910,8 +908,8 @@ class TemplateParseVisitor implements HtmlAstVisitor {
|
|||
}
|
||||
}
|
||||
|
||||
class NonBindableVisitor implements HtmlAstVisitor {
|
||||
visitElement(ast: HtmlElementAst, parent: ElementContext): ElementAst {
|
||||
class NonBindableVisitor implements html.Visitor {
|
||||
visitElement(ast: html.Element, parent: ElementContext): ElementAst {
|
||||
const preparsedElement = preparseElement(ast);
|
||||
if (preparsedElement.type === PreparsedElementType.SCRIPT ||
|
||||
preparsedElement.type === PreparsedElementType.STYLE ||
|
||||
|
@ -925,21 +923,25 @@ class NonBindableVisitor implements HtmlAstVisitor {
|
|||
const attrNameAndValues = ast.attrs.map(attrAst => [attrAst.name, attrAst.value]);
|
||||
const selector = createElementCssSelector(ast.name, attrNameAndValues);
|
||||
const ngContentIndex = parent.findNgContentIndex(selector);
|
||||
const children = htmlVisitAll(this, ast.children, EMPTY_ELEMENT_CONTEXT);
|
||||
const children = html.visitAll(this, ast.children, EMPTY_ELEMENT_CONTEXT);
|
||||
return new ElementAst(
|
||||
ast.name, htmlVisitAll(this, ast.attrs), [], [], [], [], [], false, children,
|
||||
ast.name, html.visitAll(this, ast.attrs), [], [], [], [], [], false, children,
|
||||
ngContentIndex, ast.sourceSpan);
|
||||
}
|
||||
visitComment(ast: HtmlCommentAst, context: any): any { return null; }
|
||||
visitAttr(ast: HtmlAttrAst, context: any): AttrAst {
|
||||
return new AttrAst(ast.name, ast.value, ast.sourceSpan);
|
||||
visitComment(comment: html.Comment, context: any): any { return null; }
|
||||
|
||||
visitAttribute(attribute: html.Attribute, context: any): AttrAst {
|
||||
return new AttrAst(attribute.name, attribute.value, attribute.sourceSpan);
|
||||
}
|
||||
visitText(ast: HtmlTextAst, parent: ElementContext): TextAst {
|
||||
|
||||
visitText(text: html.Text, parent: ElementContext): TextAst {
|
||||
const ngContentIndex = parent.findNgContentIndex(TEXT_CSS_SELECTOR);
|
||||
return new TextAst(ast.value, ngContentIndex, ast.sourceSpan);
|
||||
return new TextAst(text.value, ngContentIndex, text.sourceSpan);
|
||||
}
|
||||
visitExpansion(ast: HtmlExpansionAst, context: any): any { return ast; }
|
||||
visitExpansionCase(ast: HtmlExpansionCaseAst, context: any): any { return ast; }
|
||||
|
||||
visitExpansion(expansion: html.Expansion, context: any): any { return expansion; }
|
||||
|
||||
visitExpansionCase(expansionCase: html.ExpansionCase, context: any): any { return expansionCase; }
|
||||
}
|
||||
|
||||
class BoundElementOrDirectiveProperty {
|
||||
|
@ -953,7 +955,7 @@ class ElementOrDirectiveRef {
|
|||
}
|
||||
|
||||
export function splitClasses(classAttrValue: string): string[] {
|
||||
return StringWrapper.split(classAttrValue.trim(), /\s+/g);
|
||||
return classAttrValue.trim().split(/\s+/g);
|
||||
}
|
||||
|
||||
class ElementContext {
|
||||
|
@ -967,7 +969,7 @@ class ElementContext {
|
|||
const ngContentSelectors = component.directive.template.ngContentSelectors;
|
||||
for (let i = 0; i < ngContentSelectors.length; i++) {
|
||||
const selector = ngContentSelectors[i];
|
||||
if (StringWrapper.equals(selector, '*')) {
|
||||
if (selector === '*') {
|
||||
wildcardNgContentIndex = i;
|
||||
} else {
|
||||
matcher.addSelectables(CssSelector.parse(ngContentSelectors[i]), i);
|
||||
|
|
|
@ -6,10 +6,8 @@
|
|||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {isBlank} from '../facade/lang';
|
||||
|
||||
import {HtmlElementAst} from '../html_parser/html_ast';
|
||||
import {splitNsName} from '../html_parser/html_tags';
|
||||
import * as html from '../html_parser/ast';
|
||||
import {splitNsName} from '../html_parser/tags';
|
||||
|
||||
const NG_CONTENT_SELECT_ATTR = 'select';
|
||||
const NG_CONTENT_ELEMENT = 'ng-content';
|
||||
|
@ -22,7 +20,7 @@ const SCRIPT_ELEMENT = 'script';
|
|||
const NG_NON_BINDABLE_ATTR = 'ngNonBindable';
|
||||
const NG_PROJECT_AS = 'ngProjectAs';
|
||||
|
||||
export function preparseElement(ast: HtmlElementAst): PreparsedElement {
|
||||
export function preparseElement(ast: html.Element): PreparsedElement {
|
||||
var selectAttr: string = null;
|
||||
var hrefAttr: string = null;
|
||||
var relAttr: string = null;
|
||||
|
@ -75,7 +73,7 @@ export class PreparsedElement {
|
|||
|
||||
|
||||
function normalizeNgContentSelect(selectAttr: string): string {
|
||||
if (isBlank(selectAttr) || selectAttr.length === 0) {
|
||||
if (selectAttr === null || selectAttr.length === 0) {
|
||||
return '*';
|
||||
}
|
||||
return selectAttr;
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
import {Inject, Injectable, PACKAGE_ROOT_URL} from '@angular/core';
|
||||
|
||||
import {StringWrapper, isPresent, isBlank, RegExpWrapper,} from '../src/facade/lang';
|
||||
import {StringWrapper, isPresent, isBlank, RegExpWrapper,} from './facade/lang';
|
||||
|
||||
|
||||
const _ASSET_SCHEME = 'asset:';
|
||||
|
|
|
@ -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));
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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="AA">')).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="&" b="c&&d">')).toEqual([
|
||||
[HtmlTokenType.TAG_OPEN_START, null, 't'],
|
||||
[HtmlTokenType.ATTR_NAME, null, 'a'],
|
||||
[HtmlTokenType.ATTR_VALUE, '&'],
|
||||
[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&b')).toEqual([
|
||||
[HtmlTokenType.TEXT, 'a&b'],
|
||||
[HtmlTokenType.EOF],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should parse hexadecimal entities', () => {
|
||||
expect(tokenizeAndHumanizeParts('AA')).toEqual([
|
||||
[HtmlTokenType.TEXT, 'AA'],
|
||||
[HtmlTokenType.EOF],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should parse decimal entities', () => {
|
||||
expect(tokenizeAndHumanizeParts('A')).toEqual([
|
||||
[HtmlTokenType.TEXT, 'A'],
|
||||
[HtmlTokenType.EOF],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should store the locations', () => {
|
||||
expect(tokenizeAndHumanizeSourceSpans('a&b')).toEqual([
|
||||
[HtmlTokenType.TEXT, 'a&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('
sdf;')).toEqual([
|
||||
[HtmlTokenType.TEXT, 'Unexpected character "s"', '0:4']
|
||||
]);
|
||||
|
||||
expect(tokenizeAndHumanizeErrors('઼')).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&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>&</SCRIPT>`)).toEqual([
|
||||
[HtmlTokenType.TAG_OPEN_START, null, 'script'],
|
||||
[HtmlTokenType.TAG_OPEN_END],
|
||||
[HtmlTokenType.RAW_TEXT, '&'],
|
||||
[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>&</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)]);
|
||||
}
|
|
@ -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)];
|
||||
}
|
||||
|
|
|
@ -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],
|
||||
]);
|
||||
});
|
||||
|
|
@ -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="AA">')).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="&" b="c&&d">')).toEqual([
|
||||
[lex.TokenType.TAG_OPEN_START, null, 't'],
|
||||
[lex.TokenType.ATTR_NAME, null, 'a'],
|
||||
[lex.TokenType.ATTR_VALUE, '&'],
|
||||
[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&b')).toEqual([
|
||||
[lex.TokenType.TEXT, 'a&b'],
|
||||
[lex.TokenType.EOF],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should parse hexadecimal entities', () => {
|
||||
expect(tokenizeAndHumanizeParts('AA')).toEqual([
|
||||
[lex.TokenType.TEXT, 'AA'],
|
||||
[lex.TokenType.EOF],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should parse decimal entities', () => {
|
||||
expect(tokenizeAndHumanizeParts('A')).toEqual([
|
||||
[lex.TokenType.TEXT, 'A'],
|
||||
[lex.TokenType.EOF],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should store the locations', () => {
|
||||
expect(tokenizeAndHumanizeSourceSpans('a&b')).toEqual([
|
||||
[lex.TokenType.TEXT, 'a&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('
sdf;')).toEqual([
|
||||
[lex.TokenType.TEXT, 'Unexpected character "s"', '0:4']
|
||||
]);
|
||||
|
||||
expect(tokenizeAndHumanizeErrors('઼')).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&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>&</SCRIPT>`)).toEqual([
|
||||
[lex.TokenType.TAG_OPEN_START, null, 'script'],
|
||||
[lex.TokenType.TAG_OPEN_END],
|
||||
[lex.TokenType.RAW_TEXT, '&'],
|
||||
[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>&</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)]);
|
||||
}
|
|
@ -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()]);
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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('//');
|
||||
}
|
||||
|
|
@ -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"');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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));
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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', () => {
|
|
@ -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><b></ex></ph>with placeholders<ph
|
||||
// name="CLOSE_BOLD_TEXT"><ex></b></ex></ph> <ph name="INTERPOLATION"/></message><message
|
||||
// id="7a2843db">{ count, plural, =0 {<ph name="START_PARAGRAPH"><ex><p></ex></ph>test<ph
|
||||
// name="CLOSE_PARAGRAPH"><ex></p></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><p></ex></ph>deeply nested<ph
|
||||
// name="CLOSE_PARAGRAPH"><ex></p></ex></ph>}} }}</message></messagebundle>
|
||||
// <? xml version="1.0" encoding="UTF-8" ?><messagebundle><message id="834fa53b">translatable
|
||||
// element <ph name="START_BOLD_TEXT"><ex><b></ex></ph>with placeholders<ph
|
||||
// name="CLOSE_BOLD_TEXT"><ex></b></ex></ph> <ph name="INTERPOLATION"/></message><message
|
||||
// id="7a2843db">{ count, plural, =0 {<ph name="START_PARAGRAPH"><ex><p></ex></ph>test<ph
|
||||
// name="CLOSE_PARAGRAPH"><ex></p></ex></ph>}}</message><message id="18ea85bc">{ count,
|
||||
// plural, =0 {{ sex, gender, other {<ph name="START_PARAGRAPH"><ex><p></ex></ph>deeply
|
||||
// nested<ph name="CLOSE_PARAGRAPH"><ex></p></ex></ph>}} }}</message><message id="b45e58a5"
|
||||
// description="d" meaning="m">foo</message></messagebundle>
|
||||
}
|
|
@ -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" ?>');
|
||||
|
|
|
@ -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><b></ex></ph>with placeholders<ph name="CLOSE_BOLD_TEXT"><ex></b></ex></ph> <ph name="INTERPOLATION"/></translation>
|
||||
<translation id="b">{ count, plural, =0 {<ph name="START_PARAGRAPH"><ex><p></ex></ph>test<ph name="CLOSE_PARAGRAPH"><ex></p></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><p></ex></ph>deeply nested<ph name="CLOSE_PARAGRAPH"><ex></p></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(); });
|
||||
});
|
||||
}
|
|
@ -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='"'&<>"'&<>' meaning='"'&<>"'&<>'>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);
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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) => {
|
||||
|
|
Loading…
Reference in New Issue