fix(compiler): incorrectly inferring content type of SVG-specific title tag (#40259)
The parser has a list of tag definitions that it uses when parsing the template. Each tag has a `contentType` which tells the parser what kind of content the tag should contain. The problem is that the browser has two separate `title` tags (`HTMLTitleElement` and `SVGTitleElement`) and each of them has to have a different `contentType`, otherwise the parser will throw an error further down the pipeline. These changes update the tag definitions so that each tag name can have multiple content types associated with it and the correct one can be returned based on the element's prefix. Fixes #31503. PR Close #40259
This commit is contained in:
parent
184d0e568f
commit
66c27ffdfc
|
@ -4312,6 +4312,29 @@ runInEachFileSystem(os => {
|
|||
expect(jsContents).toMatch(setClassMetadataRegExp('type: i1.Other'));
|
||||
});
|
||||
|
||||
it('should not throw when using an SVG-specific `title` tag', () => {
|
||||
env.write('test.ts', `
|
||||
import {Component, NgModule} from '@angular/core';
|
||||
@Component({
|
||||
template: \`
|
||||
<svg>
|
||||
<rect>
|
||||
<svg:title>I'm a title tag</svg:title>
|
||||
</rect>
|
||||
</svg>
|
||||
\`,
|
||||
})
|
||||
export class SvgCmp {}
|
||||
@NgModule({
|
||||
declarations: [SvgCmp],
|
||||
})
|
||||
export class SvgModule {}
|
||||
`);
|
||||
|
||||
const diags = env.driveDiagnostics();
|
||||
expect(diags.length).toBe(0);
|
||||
});
|
||||
|
||||
describe('namespace support', () => {
|
||||
it('should generate correct imports for type references to namespaced symbols using a namespace import',
|
||||
() => {
|
||||
|
|
|
@ -10,10 +10,11 @@ import {TagContentType, TagDefinition} from './tags';
|
|||
|
||||
export class HtmlTagDefinition implements TagDefinition {
|
||||
private closedByChildren: {[key: string]: boolean} = {};
|
||||
private contentType: TagContentType|
|
||||
{default: TagContentType, [namespace: string]: TagContentType};
|
||||
|
||||
closedByParent: boolean = false;
|
||||
implicitNamespacePrefix: string|null;
|
||||
contentType: TagContentType;
|
||||
isVoid: boolean;
|
||||
ignoreFirstLf: boolean;
|
||||
canSelfClose: boolean = false;
|
||||
|
@ -31,7 +32,7 @@ export class HtmlTagDefinition implements TagDefinition {
|
|||
closedByChildren?: string[],
|
||||
closedByParent?: boolean,
|
||||
implicitNamespacePrefix?: string,
|
||||
contentType?: TagContentType,
|
||||
contentType?: TagContentType|{default: TagContentType, [namespace: string]: TagContentType},
|
||||
isVoid?: boolean,
|
||||
ignoreFirstLf?: boolean,
|
||||
preventNamespaceInheritance?: boolean
|
||||
|
@ -50,6 +51,14 @@ export class HtmlTagDefinition implements TagDefinition {
|
|||
isClosedByChild(name: string): boolean {
|
||||
return this.isVoid || name.toLowerCase() in this.closedByChildren;
|
||||
}
|
||||
|
||||
getContentType(prefix?: string): TagContentType {
|
||||
if (typeof this.contentType === 'object') {
|
||||
const overrideType = prefix == null ? undefined : this.contentType[prefix];
|
||||
return overrideType ?? this.contentType.default;
|
||||
}
|
||||
return this.contentType;
|
||||
}
|
||||
}
|
||||
|
||||
let _DEFAULT_TAG_DEFINITION!: HtmlTagDefinition;
|
||||
|
@ -121,7 +130,11 @@ export function getHtmlTagDefinition(tagName: string): HtmlTagDefinition {
|
|||
'listing': new HtmlTagDefinition({ignoreFirstLf: true}),
|
||||
'style': new HtmlTagDefinition({contentType: TagContentType.RAW_TEXT}),
|
||||
'script': new HtmlTagDefinition({contentType: TagContentType.RAW_TEXT}),
|
||||
'title': new HtmlTagDefinition({contentType: TagContentType.ESCAPABLE_RAW_TEXT}),
|
||||
'title': new HtmlTagDefinition({
|
||||
// The browser supports two separate `title` tags which have to use
|
||||
// a different content type: `HTMLTitleElement` and `SVGTitleElement`
|
||||
contentType: {default: TagContentType.ESCAPABLE_RAW_TEXT, svg: TagContentType.PARSABLE_DATA}
|
||||
}),
|
||||
'textarea': new HtmlTagDefinition(
|
||||
{contentType: TagContentType.ESCAPABLE_RAW_TEXT, ignoreFirstLf: true}),
|
||||
};
|
||||
|
|
|
@ -550,7 +550,7 @@ class _Tokenizer {
|
|||
throw e;
|
||||
}
|
||||
|
||||
const contentTokenType = this._getTagDefinition(tagName).contentType;
|
||||
const contentTokenType = this._getTagDefinition(tagName).getContentType(prefix);
|
||||
|
||||
if (contentTokenType === TagContentType.RAW_TEXT) {
|
||||
this._consumeRawTextWithTagClose(prefix, tagName, false);
|
||||
|
@ -560,7 +560,7 @@ class _Tokenizer {
|
|||
}
|
||||
|
||||
private _consumeRawTextWithTagClose(prefix: string, tagName: string, decodeEntities: boolean) {
|
||||
const textToken = this._consumeRawText(decodeEntities, () => {
|
||||
this._consumeRawText(decodeEntities, () => {
|
||||
if (!this._attemptCharCode(chars.$LT)) return false;
|
||||
if (!this._attemptCharCode(chars.$SLASH)) return false;
|
||||
this._attemptCharCodeUntilFn(isNotWhitespace);
|
||||
|
|
|
@ -15,13 +15,13 @@ export enum TagContentType {
|
|||
export interface TagDefinition {
|
||||
closedByParent: boolean;
|
||||
implicitNamespacePrefix: string|null;
|
||||
contentType: TagContentType;
|
||||
isVoid: boolean;
|
||||
ignoreFirstLf: boolean;
|
||||
canSelfClose: boolean;
|
||||
preventNamespaceInheritance: boolean;
|
||||
|
||||
isClosedByChild(name: string): boolean;
|
||||
getContentType(prefix?: string): TagContentType;
|
||||
}
|
||||
|
||||
export function splitNsName(elementName: string): [string|null, string] {
|
||||
|
|
|
@ -16,7 +16,6 @@ export class XmlTagDefinition implements TagDefinition {
|
|||
parentToAdd!: string;
|
||||
// TODO(issue/24571): remove '!'.
|
||||
implicitNamespacePrefix!: string;
|
||||
contentType: TagContentType = TagContentType.PARSABLE_DATA;
|
||||
isVoid: boolean = false;
|
||||
ignoreFirstLf: boolean = false;
|
||||
canSelfClose: boolean = true;
|
||||
|
@ -29,6 +28,10 @@ export class XmlTagDefinition implements TagDefinition {
|
|||
isClosedByChild(name: string): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
getContentType(): TagContentType {
|
||||
return TagContentType.PARSABLE_DATA;
|
||||
}
|
||||
}
|
||||
|
||||
const _TAG_DEFINITION = new XmlTagDefinition();
|
||||
|
|
|
@ -789,6 +789,31 @@ import {ParseLocation, ParseSourceFile, ParseSourceSpan} from '../../src/parse_u
|
|||
});
|
||||
});
|
||||
|
||||
describe('parsable data', () => {
|
||||
it('should parse an SVG <title> tag', () => {
|
||||
expect(tokenizeAndHumanizeParts(`<svg:title>test</svg:title>`)).toEqual([
|
||||
[lex.TokenType.TAG_OPEN_START, 'svg', 'title'],
|
||||
[lex.TokenType.TAG_OPEN_END],
|
||||
[lex.TokenType.TEXT, 'test'],
|
||||
[lex.TokenType.TAG_CLOSE, 'svg', 'title'],
|
||||
[lex.TokenType.EOF],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should parse an SVG <title> tag with children', () => {
|
||||
expect(tokenizeAndHumanizeParts(`<svg:title><f>test</f></svg:title>`)).toEqual([
|
||||
[lex.TokenType.TAG_OPEN_START, 'svg', 'title'],
|
||||
[lex.TokenType.TAG_OPEN_END],
|
||||
[lex.TokenType.TAG_OPEN_START, '', 'f'],
|
||||
[lex.TokenType.TAG_OPEN_END],
|
||||
[lex.TokenType.TEXT, 'test'],
|
||||
[lex.TokenType.TAG_CLOSE, '', 'f'],
|
||||
[lex.TokenType.TAG_CLOSE, 'svg', 'title'],
|
||||
[lex.TokenType.EOF],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('expansion forms', () => {
|
||||
it('should parse an expansion form', () => {
|
||||
expect(
|
||||
|
|
|
@ -196,7 +196,7 @@ class HtmlVisitor implements Visitor {
|
|||
// any test cases for it.
|
||||
const element = this.htmlPath.first(Element);
|
||||
if (element &&
|
||||
getHtmlTagDefinition(element.name).contentType !== TagContentType.PARSABLE_DATA) {
|
||||
getHtmlTagDefinition(element.name).getContentType() !== TagContentType.PARSABLE_DATA) {
|
||||
return [];
|
||||
}
|
||||
// This is to account for cases like <h1> <a> text | </h1> where the
|
||||
|
|
Loading…
Reference in New Issue