diff --git a/packages/compiler/src/render3/r3_identifiers.ts b/packages/compiler/src/render3/r3_identifiers.ts index b9bf96b0d3..6cdcdc8404 100644 --- a/packages/compiler/src/render3/r3_identifiers.ts +++ b/packages/compiler/src/render3/r3_identifiers.ts @@ -17,6 +17,12 @@ export class Identifiers { static PATCH_DEPS = 'patchedDeps'; /* Instructions */ + static namespaceHTML: o.ExternalReference = {name: 'ɵNH', moduleName: CORE}; + + static namespaceMathML: o.ExternalReference = {name: 'ɵNM', moduleName: CORE}; + + static namespaceSVG: o.ExternalReference = {name: 'ɵNS', moduleName: CORE}; + static createElement: o.ExternalReference = {name: 'ɵE', moduleName: CORE}; static elementEnd: o.ExternalReference = {name: 'ɵe', moduleName: CORE}; diff --git a/packages/compiler/src/render3/view/compiler.ts b/packages/compiler/src/render3/view/compiler.ts index a057d83826..d830630418 100644 --- a/packages/compiler/src/render3/view/compiler.ts +++ b/packages/compiler/src/render3/view/compiler.ts @@ -136,7 +136,8 @@ export function compileComponentFromMetadata( const templateFunctionExpression = new TemplateDefinitionBuilder( constantPool, CONTEXT_NAME, BindingScope.ROOT_SCOPE, 0, templateTypeName, templateName, - meta.viewQueries, directiveMatcher, directivesUsed, meta.pipes, pipesUsed) + meta.viewQueries, directiveMatcher, directivesUsed, meta.pipes, pipesUsed, + R3.namespaceHTML) .buildTemplateFunction( template.nodes, [], template.hasNgContent, template.ngContentSelectors); @@ -443,4 +444,4 @@ function typeMapToExpressionMap( const entries = Array.from(map).map( ([key, type]): [string, o.Expression] => [key, outputCtx.importExpr(type)]); return new Map(entries); -} \ No newline at end of file +} diff --git a/packages/compiler/src/render3/view/template.ts b/packages/compiler/src/render3/view/template.ts index b0d21a9a11..a9f535f848 100644 --- a/packages/compiler/src/render3/view/template.ts +++ b/packages/compiler/src/render3/view/template.ts @@ -18,6 +18,7 @@ import * as html from '../../ml_parser/ast'; import {HtmlParser} from '../../ml_parser/html_parser'; import {WhitespaceVisitor} from '../../ml_parser/html_whitespaces'; import {DEFAULT_INTERPOLATION_CONFIG} from '../../ml_parser/interpolation_config'; +import {splitNsName} from '../../ml_parser/tags'; import * as o from '../../output/output_ast'; import {ParseError, ParseSourceSpan} from '../../parse_util'; import {DomElementSchemaRegistry} from '../../schema/dom_element_schema_registry'; @@ -66,7 +67,8 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver parentBindingScope: BindingScope, private level = 0, private contextName: string|null, private templateName: string|null, private viewQueries: R3QueryMetadata[], private directiveMatcher: SelectorMatcher|null, private directives: Set, - private pipeTypeByName: Map, private pipes: Set) { + private pipeTypeByName: Map, private pipes: Set, + private _namespace: o.ExternalReference) { this._bindingScope = parentBindingScope.nestedScope((lhsVar: o.ReadVarExpr, expression: o.Expression) => { this._bindingCode.push( @@ -89,6 +91,10 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver buildTemplateFunction( nodes: t.Node[], variables: t.Variable[], hasNgContent: boolean = false, ngContentSelectors: string[] = []): o.FunctionExpr { + if (this._namespace !== R3.namespaceHTML) { + this.instruction(this._creationCode, null, this._namespace); + } + // Create variable bindings for (const variable of variables) { const variableName = variable.name; @@ -220,6 +226,23 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver this.instruction(this._creationCode, ngContent.sourceSpan, R3.projection, ...parameters); } + + getNamespaceInstruction(namespaceKey: string|null) { + switch (namespaceKey) { + case 'math': + return R3.namespaceMathML; + case 'svg': + return R3.namespaceSVG; + default: + return R3.namespaceHTML; + } + } + + addNamespaceInstruction(nsInstruction: o.ExternalReference, element: t.Element) { + this._namespace = nsInstruction; + this.instruction(this._creationCode, element.sourceSpan, nsInstruction); + } + visitElement(element: t.Element) { const elementIndex = this.allocateDataSlot(); const referenceDataSlots = new Map(); @@ -229,6 +252,8 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver const attrI18nMetas: {[name: string]: string} = {}; let i18nMeta: string = ''; + const [namespaceKey, elementName] = splitNsName(element.name); + // Elements inside i18n sections are replaced with placeholders // TODO(vicb): nested elements are a WIP in this phase if (this._inI18nSection) { @@ -269,7 +294,7 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver // Element creation mode const parameters: o.Expression[] = [ o.literal(elementIndex), - o.literal(element.name), + o.literal(elementName), ]; // Add the attributes @@ -314,6 +339,16 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver if (i18nMessages.length > 0) { this._creationCode.push(...i18nMessages); } + + const wasInNamespace = this._namespace; + const currentNamespace = this.getNamespaceInstruction(namespaceKey); + + // If the namespace is changing now, include an instruction to change it + // during element creation. + if (currentNamespace !== wasInNamespace) { + this.addNamespaceInstruction(currentNamespace, element); + } + this.instruction( this._creationCode, element.sourceSpan, R3.createElement, ...trimTrailingNulls(parameters)); @@ -433,7 +468,8 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver // Create the template function const templateVisitor = new TemplateDefinitionBuilder( this.constantPool, templateContext, this._bindingScope, this.level + 1, contextName, - templateName, [], this.directiveMatcher, this.directives, this.pipeTypeByName, this.pipes); + templateName, [], this.directiveMatcher, this.directives, this.pipeTypeByName, this.pipes, + this._namespace); const templateFunctionExpr = templateVisitor.buildTemplateFunction(template.children, template.variables); this._postfixCode.push(templateFunctionExpr.toDeclStmt(templateName, null)); diff --git a/packages/compiler/test/render3/r3_compiler_compliance_spec.ts b/packages/compiler/test/render3/r3_compiler_compliance_spec.ts index a60ccb687c..f0371076b3 100644 --- a/packages/compiler/test/render3/r3_compiler_compliance_spec.ts +++ b/packages/compiler/test/render3/r3_compiler_compliance_spec.ts @@ -21,6 +21,105 @@ describe('compiler compliance', () => { }); describe('elements', () => { + it('should handle SVG', () => { + const files = { + app: { + 'spec.ts': ` + import {Component, NgModule} from '@angular/core'; + + @Component({ + selector: 'my-component', + template: \`

test

\` + }) + export class MyComponent {} + + @NgModule({declarations: [MyComponent]}) + export class MyModule {} + ` + } + }; + + // The factory should look like this: + const factory = 'factory: function MyComponent_Factory() { return new MyComponent(); }'; + + // The template should look like this (where IDENT is a wild card for an identifier): + const template = ` + const $c1$ = ['class', 'my-app', 'title', 'Hello']; + const $c2$ = ['cx', '20', 'cy', '30', 'r', '50']; + … + template: function MyComponent_Template(rf: IDENT, ctx: IDENT) { + if (rf & 1) { + $r3$.ɵE(0, 'div', $c1$); + $r3$.ɵNS(); + $r3$.ɵE(1, 'svg'); + $r3$.ɵE(2, 'circle', $c2$); + $r3$.ɵe(); + $r3$.ɵe(); + $r3$.ɵNH(); + $r3$.ɵE(3, 'p'); + $r3$.ɵT(4, 'test'); + $r3$.ɵe(); + $r3$.ɵe(); + } + } + `; + + + const result = compile(files, angularFiles); + + expectEmit(result.source, factory, 'Incorrect factory'); + expectEmit(result.source, template, 'Incorrect template'); + }); + + it('should handle MathML', () => { + const files = { + app: { + 'spec.ts': ` + import {Component, NgModule} from '@angular/core'; + + @Component({ + selector: 'my-component', + template: \`

test

\` + }) + export class MyComponent {} + + @NgModule({declarations: [MyComponent]}) + export class MyModule {} + ` + } + }; + + // The factory should look like this: + const factory = 'factory: function MyComponent_Factory() { return new MyComponent(); }'; + + // The template should look like this (where IDENT is a wild card for an identifier): + const template = ` + const $c1$ = ['class', 'my-app', 'title', 'Hello']; + … + template: function MyComponent_Template(rf: IDENT, ctx: IDENT) { + if (rf & 1) { + $r3$.ɵE(0, 'div', $c1$); + $r3$.ɵNM(); + $r3$.ɵE(1, 'math'); + $r3$.ɵE(2, 'infinity'); + $r3$.ɵe(); + $r3$.ɵe(); + $r3$.ɵNH(); + $r3$.ɵE(3, 'p'); + $r3$.ɵT(4, 'test'); + $r3$.ɵe(); + $r3$.ɵe(); + } + } + `; + + + const result = compile(files, angularFiles); + + expectEmit(result.source, factory, 'Incorrect factory'); + expectEmit(result.source, template, 'Incorrect template'); + }); + it('should translate DOM structure', () => { const files = { app: { @@ -1164,6 +1263,80 @@ describe('compiler compliance', () => { } }; + it('should support embedded views in the SVG namespace', () => { + const files = { + app: { + ...shared, + 'spec.ts': ` + import {Component, NgModule} from '@angular/core'; + import {ForOfDirective} from './shared/for_of'; + + @Component({ + selector: 'my-component', + template: \`\` + }) + export class MyComponent { + items = [{ data: 42 }, { data: 42 }]; + } + + @NgModule({ + declarations: [MyComponent, ForOfDirective] + }) + export class MyModule {} + ` + } + }; + + // TODO(benlesh): Enforce this when the directives are specified + const ForDirectiveDefinition = ` + static ngDirectiveDef = $r3$.ɵdefineDirective({ + type: ForOfDirective, + selectors: [['', 'forOf', '']], + factory: function ForOfDirective_Factory() { + return new ForOfDirective($r3$.ɵinjectViewContainerRef(), $r3$.ɵinjectTemplateRef()); + }, + features: [$r3$.ɵNgOnChangesFeature(NgForOf)], + inputs: {forOf: 'forOf'} + }); + `; + + const MyComponentDefinition = ` + const $_c0$ = ['for','','forOf','']; + … + static ngComponentDef = $r3$.ɵdefineComponent({ + type: MyComponent, + selectors: [['my-component']], + factory: function MyComponent_Factory() { return new MyComponent(); }, + template: function MyComponent_Template(rf:IDENT,ctx:IDENT){ + if (rf & 1) { + $r3$.ɵNS(); + $r3$.ɵE(0,'svg'); + $r3$.ɵC(1,MyComponent__svg_g_Template_1,null,$_c0$); + $r3$.ɵe(); + } + if (rf & 2) { $r3$.ɵp(1,'forOf',$r3$.ɵb(ctx.items)); } + function MyComponent__svg_g_Template_1(rf:IDENT,ctx0:IDENT) { + if (rf & 1) { + $r3$.ɵNS(); + $r3$.ɵE(0,'g'); + $r3$.ɵE(1,'circle'); + $r3$.ɵe(); + $r3$.ɵe(); + } + } + }, + directives: [ForOfDirective] + }); + `; + + const result = compile(files, angularFiles); + const source = result.source; + + // TODO(benlesh): Enforce this when the directives are specified + // expectEmit(source, ForDirectiveDefinition, 'Invalid directive definition'); + expectEmit(source, MyComponentDefinition, 'Invalid component definition'); + }); + it('should support a let variable and reference', () => { const files = { app: { diff --git a/packages/core/src/core_render3_private_export.ts b/packages/core/src/core_render3_private_export.ts index 3998173ec9..60df2a44cd 100644 --- a/packages/core/src/core_render3_private_export.ts +++ b/packages/core/src/core_render3_private_export.ts @@ -30,6 +30,9 @@ export { NC as ɵNC, C as ɵC, E as ɵE, + NH as ɵNH, + NM as ɵNM, + NS as ɵNS, L as ɵL, T as ɵT, V as ɵV, diff --git a/packages/core/src/render3/STATUS.md b/packages/core/src/render3/STATUS.md index 786f0eac08..c0a0daae8a 100644 --- a/packages/core/src/render3/STATUS.md +++ b/packages/core/src/render3/STATUS.md @@ -150,7 +150,7 @@ The goal is for the `@Component` (and friends) to be the compiler of template. S | `{{ ['literal', exp ] }}` | ✅ | ✅ | ✅ | | `{{ { a: 'literal', b: exp } }}` | ✅ | ✅ | ✅ | | `{{ exp \| pipe: arg }}` | ✅ | ✅ | ✅ | -| `` | ❌ | ❌ | ❌ | +| `` | ✅ | ✅ | ✅ | | `` sanitization | ❌ | ❌ | ❌ | | `
` | ❌ | ❌ | ❌ | | `
` | ❌ | ❌ | ❌ | diff --git a/packages/core/src/render3/index.ts b/packages/core/src/render3/index.ts index 9d3c28b344..e57520dc1c 100644 --- a/packages/core/src/render3/index.ts +++ b/packages/core/src/render3/index.ts @@ -58,6 +58,10 @@ export { load as ld, loadDirective as d, + namespaceHTML as NH, + namespaceMathML as NM, + namespaceSVG as NS, + projection as P, projectionDef as pD, diff --git a/packages/core/src/render3/instructions.ts b/packages/core/src/render3/instructions.ts index e14ee63070..442d94ae15 100644 --- a/packages/core/src/render3/instructions.ts +++ b/packages/core/src/render3/instructions.ts @@ -495,6 +495,7 @@ export function renderEmbeddedTemplate( rf = RenderFlags.Create; } oldView = enterView(viewNode.data, viewNode); + namespaceHTML(); tView.template !(rf, context); if (rf & RenderFlags.Update) { refreshView(); @@ -520,6 +521,7 @@ export function renderComponentOrTemplate( rendererFactory.begin(); } if (template) { + namespaceHTML(); template(getRenderFlags(hostView), componentOrContext !); refreshView(); } else { @@ -552,6 +554,24 @@ function getRenderFlags(view: LView): RenderFlags { RenderFlags.Update; } +////////////////////////// +//// Namespace +////////////////////////// + +let _currentNamespace: string|null = null; + +export function namespaceSVG() { + _currentNamespace = 'http://www.w3.org/2000/svg/'; +} + +export function namespaceMathML() { + _currentNamespace = 'http://www.w3.org/1998/MathML/'; +} + +export function namespaceHTML() { + _currentNamespace = null; +} + ////////////////////////// //// Element ////////////////////////// @@ -575,7 +595,19 @@ export function elementStart( assertEqual(currentView.bindingIndex, -1, 'elements should be created before any bindings'); ngDevMode && ngDevMode.rendererCreateElement++; - const native: RElement = renderer.createElement(name); + + let native: RElement; + + if (isProceduralRenderer(renderer)) { + native = renderer.createElement(name, _currentNamespace); + } else { + if (_currentNamespace === null) { + native = renderer.createElement(name); + } else { + native = renderer.createElementNS(_currentNamespace, name); + } + } + ngDevMode && assertDataInRange(index - 1); const node: LElementNode = @@ -2130,6 +2162,7 @@ export function detectChangesInternal(hostView: LView, hostNode: LElementNode const template = hostView.tView.template !; try { + namespaceHTML(); template(getRenderFlags(hostView), component); refreshView(); } finally { diff --git a/packages/core/src/render3/interfaces/renderer.ts b/packages/core/src/render3/interfaces/renderer.ts index 54983d0a3b..2bc0775a00 100644 --- a/packages/core/src/render3/interfaces/renderer.ts +++ b/packages/core/src/render3/interfaces/renderer.ts @@ -36,6 +36,7 @@ export type Renderer3 = ObjectOrientedRenderer3 | ProceduralRenderer3; * */ export interface ObjectOrientedRenderer3 { createElement(tagName: string): RElement; + createElementNS(namespace: string, tagName: string): RElement; createTextNode(data: string): RText; querySelector(selectors: string): RElement|null; diff --git a/packages/core/src/render3/jit/environment.ts b/packages/core/src/render3/jit/environment.ts index 4714697558..0496c64a8b 100644 --- a/packages/core/src/render3/jit/environment.ts +++ b/packages/core/src/render3/jit/environment.ts @@ -36,6 +36,9 @@ export const angularCoreEnv: {[name: string]: Function} = { 'ɵcR': r3.cR, 'ɵcr': r3.cr, 'ɵd': r3.d, + 'ɵNH': r3.NH, + 'ɵNM': r3.NM, + 'ɵNS': r3.NS, 'ɵE': r3.E, 'ɵe': r3.e, 'ɵf0': r3.f0, diff --git a/packages/core/test/bundling/todo/bundle.golden_symbols.json b/packages/core/test/bundling/todo/bundle.golden_symbols.json index 865432ac91..e1929f512a 100644 --- a/packages/core/test/bundling/todo/bundle.golden_symbols.json +++ b/packages/core/test/bundling/todo/bundle.golden_symbols.json @@ -176,6 +176,9 @@ { "name": "_currentInjector" }, + { + "name": "_currentNamespace" + }, { "name": "_devMode" }, @@ -545,6 +548,9 @@ { "name": "markViewDirty" }, + { + "name": "namespaceHTML" + }, { "name": "notImplemented" },