diff --git a/packages/compiler-cli/test/ngtsc/template_typecheck_spec.ts b/packages/compiler-cli/test/ngtsc/template_typecheck_spec.ts index 63c6a168eb052..26212e3bc1e64 100644 --- a/packages/compiler-cli/test/ngtsc/template_typecheck_spec.ts +++ b/packages/compiler-cli/test/ngtsc/template_typecheck_spec.ts @@ -2017,6 +2017,28 @@ export declare class AnimationEvent { expect(diags.length).toBe(0); }); + it('should allow HTML elements without explicit namespace inside SVG foreignObject', () => { + env.write('test.ts', ` + import {Component, NgModule} from '@angular/core'; + @Component({ + template: \` + + +
Hello
+
+
+ \`, + }) + export class FooCmp {} + @NgModule({ + declarations: [FooCmp], + }) + export class FooModule {} + `); + const diags = env.driveDiagnostics(); + expect(diags.length).toBe(0); + }); + it('should check for unknown elements inside an SVG foreignObject', () => { env.write('test.ts', ` import {Component, NgModule} from '@angular/core'; @@ -2042,6 +2064,33 @@ export declare class AnimationEvent { 1. If 'foo' is an Angular component, then verify that it is part of this module. 2. To allow any element add 'NO_ERRORS_SCHEMA' to the '@NgModule.schemas' of this component.`); }); + + it('should check for unknown elements without explicit namespace inside an SVG foreignObject', + () => { + env.write('test.ts', ` + import {Component, NgModule} from '@angular/core'; + @Component({ + selector: 'blah', + template: \` + + + Hello + + + \`, + }) + export class FooCmp {} + @NgModule({ + declarations: [FooCmp], + }) + export class FooModule {} + `); + const diags = env.driveDiagnostics(); + expect(diags.length).toBe(1); + expect(diags[0].messageText).toBe(`'foo' is not a known element: +1. If 'foo' is an Angular component, then verify that it is part of this module. +2. To allow any element add 'NO_ERRORS_SCHEMA' to the '@NgModule.schemas' of this component.`); + }); }); // Test both sync and async compilations, see https://github.com/angular/angular/issues/32538 diff --git a/packages/compiler/src/ml_parser/html_tags.ts b/packages/compiler/src/ml_parser/html_tags.ts index 969fb0f95fac2..e83d7aa670ab4 100644 --- a/packages/compiler/src/ml_parser/html_tags.ts +++ b/packages/compiler/src/ml_parser/html_tags.ts @@ -17,6 +17,7 @@ export class HtmlTagDefinition implements TagDefinition { isVoid: boolean; ignoreFirstLf: boolean; canSelfClose: boolean = false; + preventNamespaceInheritance: boolean; constructor({ closedByChildren, @@ -24,14 +25,16 @@ export class HtmlTagDefinition implements TagDefinition { contentType = TagContentType.PARSABLE_DATA, closedByParent = false, isVoid = false, - ignoreFirstLf = false + ignoreFirstLf = false, + preventNamespaceInheritance = false }: { closedByChildren?: string[], closedByParent?: boolean, implicitNamespacePrefix?: string, contentType?: TagContentType, isVoid?: boolean, - ignoreFirstLf?: boolean + ignoreFirstLf?: boolean, + preventNamespaceInheritance?: boolean } = {}) { if (closedByChildren && closedByChildren.length > 0) { closedByChildren.forEach(tagName => this.closedByChildren[tagName] = true); @@ -41,6 +44,7 @@ export class HtmlTagDefinition implements TagDefinition { this.implicitNamespacePrefix = implicitNamespacePrefix || null; this.contentType = contentType; this.ignoreFirstLf = ignoreFirstLf; + this.preventNamespaceInheritance = preventNamespaceInheritance; } isClosedByChild(name: string): boolean { @@ -88,6 +92,17 @@ export function getHtmlTagDefinition(tagName: string): HtmlTagDefinition { 'th': new HtmlTagDefinition({closedByChildren: ['td', 'th'], closedByParent: true}), 'col': new HtmlTagDefinition({isVoid: true}), 'svg': new HtmlTagDefinition({implicitNamespacePrefix: 'svg'}), + 'foreignObject': new HtmlTagDefinition({ + // Usually the implicit namespace here would be redundant since it will be inherited from + // the parent `svg`, but we have to do it for `foreignObject`, because the way the parser + // works is that the parent node of an end tag is its own start tag which means that + // the `preventNamespaceInheritance` on `foreignObject` would have it default to the + // implicit namespace which is `html`, unless specified otherwise. + implicitNamespacePrefix: 'svg', + // We want to prevent children of foreignObject from inheriting its namespace, because + // the point of the element is to allow nodes from other namespaces to be inserted. + preventNamespaceInheritance: true, + }), 'math': new HtmlTagDefinition({implicitNamespacePrefix: 'math'}), 'li': new HtmlTagDefinition({closedByChildren: ['li'], closedByParent: true}), 'dt': new HtmlTagDefinition({closedByChildren: ['dt', 'dd']}), @@ -111,5 +126,8 @@ export function getHtmlTagDefinition(tagName: string): HtmlTagDefinition { {contentType: TagContentType.ESCAPABLE_RAW_TEXT, ignoreFirstLf: true}), }; } - return TAG_DEFINITIONS[tagName.toLowerCase()] || _DEFAULT_TAG_DEFINITION; + // We have to make both a case-sensitive and a case-insesitive lookup, because + // HTML tag names are case insensitive, whereas some SVG tags are case sensitive. + return TAG_DEFINITIONS[tagName] ?? TAG_DEFINITIONS[tagName.toLowerCase()] ?? + _DEFAULT_TAG_DEFINITION; } diff --git a/packages/compiler/src/ml_parser/parser.ts b/packages/compiler/src/ml_parser/parser.ts index b31db0c25cfc0..3d3131989cdc7 100644 --- a/packages/compiler/src/ml_parser/parser.ts +++ b/packages/compiler/src/ml_parser/parser.ts @@ -10,7 +10,7 @@ import {ParseError, ParseSourceSpan} from '../parse_util'; import * as html from './ast'; import * as lex from './lexer'; -import {getNsPrefix, isNgContainer, mergeNsAndName, TagDefinition} from './tags'; +import {getNsPrefix, mergeNsAndName, splitNsName, TagDefinition} from './tags'; export class TreeError extends ParseError { static create(elementName: string|null, span: ParseSourceSpan, msg: string): TreeError { @@ -353,7 +353,11 @@ class _TreeBuilder { if (prefix === '') { prefix = this.getTagDefinition(localName).implicitNamespacePrefix || ''; if (prefix === '' && parentElement != null) { - prefix = getNsPrefix(parentElement.name); + const parentTagName = splitNsName(parentElement.name)[1]; + const parentTagDefinition = this.getTagDefinition(parentTagName); + if (!parentTagDefinition.preventNamespaceInheritance) { + prefix = getNsPrefix(parentElement.name); + } } } diff --git a/packages/compiler/src/ml_parser/tags.ts b/packages/compiler/src/ml_parser/tags.ts index 279ab0aacb78e..32b3aab2af624 100644 --- a/packages/compiler/src/ml_parser/tags.ts +++ b/packages/compiler/src/ml_parser/tags.ts @@ -19,6 +19,7 @@ export interface TagDefinition { isVoid: boolean; ignoreFirstLf: boolean; canSelfClose: boolean; + preventNamespaceInheritance: boolean; isClosedByChild(name: string): boolean; } diff --git a/packages/compiler/src/ml_parser/xml_tags.ts b/packages/compiler/src/ml_parser/xml_tags.ts index 81af50d6898e3..e4a76ed2456b0 100644 --- a/packages/compiler/src/ml_parser/xml_tags.ts +++ b/packages/compiler/src/ml_parser/xml_tags.ts @@ -20,6 +20,7 @@ export class XmlTagDefinition implements TagDefinition { isVoid: boolean = false; ignoreFirstLf: boolean = false; canSelfClose: boolean = true; + preventNamespaceInheritance: boolean = false; requireExtraParent(currentParent: string): boolean { return false;