From 91b007e58ffb91f7c396cbc0333a91d18f02bd27 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Thu, 2 May 2024 12:29:30 +0200 Subject: [PATCH] fix(compiler): add math elements to schema (#55631) Fixes that we didn't have the MathML elements in the schema. Note that we can't discover which tag names are available by looking at globally-available classes, because all MathML elements are `MathMLElement` rather than something like `SVGCircleElement`. As such, I ended up having to hardcode the currently-available tags. Fixes #55608. PR Close #55631 --- .../test/ngtsc/template_typecheck_spec.ts | 26 ++++++++++++++++ .../src/schema/dom_element_schema_registry.ts | 30 +++++++++++++++++++ .../compiler/test/schema/schema_extractor.ts | 30 +++++++++++++++++-- 3 files changed, 84 insertions(+), 2 deletions(-) diff --git a/packages/compiler-cli/test/ngtsc/template_typecheck_spec.ts b/packages/compiler-cli/test/ngtsc/template_typecheck_spec.ts index 60a76d9567c6a..ce86852dca7d7 100644 --- a/packages/compiler-cli/test/ngtsc/template_typecheck_spec.ts +++ b/packages/compiler-cli/test/ngtsc/template_typecheck_spec.ts @@ -3507,6 +3507,32 @@ runInEachFileSystem(() => { 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 allow math elements', () => { + env.write( + 'test.ts', + ` + import {Component} from '@angular/core'; + @Component({ + template: \` + + + 1 + + 2 + + + + \`, + standalone: true, + }) + export class MathCmp {} + `, + ); + + const diags = env.driveDiagnostics(); + expect(diags.length).toBe(0); + }); }); // Test both sync and async compilations, see https://github.com/angular/angular/issues/32538 diff --git a/packages/compiler/src/schema/dom_element_schema_registry.ts b/packages/compiler/src/schema/dom_element_schema_registry.ts index e5b98ef1cbf81..e5ac5b9abf8f5 100644 --- a/packages/compiler/src/schema/dom_element_schema_registry.ts +++ b/packages/compiler/src/schema/dom_element_schema_registry.ts @@ -229,6 +229,36 @@ const SCHEMA: string[] = [ 'summary^[HTMLElement]|', 'time^[HTMLElement]|dateTime', ':svg:cursor^:svg:|', + ':math:^[HTMLElement]|!autofocus,nonce,*abort,*animationend,*animationiteration,*animationstart,*auxclick,*beforeinput,*beforematch,*beforetoggle,*beforexrselect,*blur,*cancel,*canplay,*canplaythrough,*change,*click,*close,*contentvisibilityautostatechange,*contextlost,*contextmenu,*contextrestored,*copy,*cuechange,*cut,*dblclick,*drag,*dragend,*dragenter,*dragleave,*dragover,*dragstart,*drop,*durationchange,*emptied,*ended,*error,*focus,*formdata,*gotpointercapture,*input,*invalid,*keydown,*keypress,*keyup,*load,*loadeddata,*loadedmetadata,*loadstart,*lostpointercapture,*mousedown,*mouseenter,*mouseleave,*mousemove,*mouseout,*mouseover,*mouseup,*mousewheel,*paste,*pause,*play,*playing,*pointercancel,*pointerdown,*pointerenter,*pointerleave,*pointermove,*pointerout,*pointerover,*pointerrawupdate,*pointerup,*progress,*ratechange,*reset,*resize,*scroll,*scrollend,*securitypolicyviolation,*seeked,*seeking,*select,*selectionchange,*selectstart,*slotchange,*stalled,*submit,*suspend,*timeupdate,*toggle,*transitioncancel,*transitionend,*transitionrun,*transitionstart,*volumechange,*waiting,*webkitanimationend,*webkitanimationiteration,*webkitanimationstart,*webkittransitionend,*wheel,%style,#tabIndex', + ':math:math^:math:|', + ':math:maction^:math:|', + ':math:menclose^:math:|', + ':math:merror^:math:|', + ':math:mfenced^:math:|', + ':math:mfrac^:math:|', + ':math:mi^:math:|', + ':math:mmultiscripts^:math:|', + ':math:mn^:math:|', + ':math:mo^:math:|', + ':math:mover^:math:|', + ':math:mpadded^:math:|', + ':math:mphantom^:math:|', + ':math:mroot^:math:|', + ':math:mrow^:math:|', + ':math:ms^:math:|', + ':math:mspace^:math:|', + ':math:msqrt^:math:|', + ':math:mstyle^:math:|', + ':math:msub^:math:|', + ':math:msubsup^:math:|', + ':math:msup^:math:|', + ':math:mtable^:math:|', + ':math:mtd^:math:|', + ':math:mtext^:math:|', + ':math:mtr^:math:|', + ':math:munder^:math:|', + ':math:munderover^:math:|', + ':math:semantics^:math:|', ]; const _ATTR_TO_PROP = new Map( diff --git a/packages/compiler/test/schema/schema_extractor.ts b/packages/compiler/test/schema/schema_extractor.ts index bd73d415ebce7..e5d4d7f164275 100644 --- a/packages/compiler/test/schema/schema_extractor.ts +++ b/packages/compiler/test/schema/schema_extractor.ts @@ -7,6 +7,7 @@ */ const SVG_PREFIX = ':svg:'; +const MATH_PREFIX = ':math:'; // Element | Node interfaces // see https://developer.mozilla.org/en-US/docs/Web/API/Element @@ -25,6 +26,10 @@ const ALL_HTML_TAGS = // https://html.spec.whatwg.org/ 'details,summary,menu,menuitem'; +// Via https://developer.mozilla.org/en-US/docs/Web/MathML +const ALL_MATH_TAGS = + 'math,maction,menclose,merror,mfenced,mfrac,mi,mmultiscripts,mn,mo,mover,mpadded,mphantom,mroot,mrow,ms,mspace,msqrt,mstyle,msub,msubsup,msup,mtable,mtd,mtext,mtr,munder,munderover,semantics'; + // Elements missing from Chrome (HtmlUnknownElement), to be manually added const MISSING_FROM_CHROME: {[el: string]: string[]} = { 'data^[HTMLElement]': ['value'], @@ -81,8 +86,8 @@ export function extractSchema(): Map | null { const SVGGradientElement = _G['SVGGradientElement']; const SVGTextContentElement = _G['SVGTextContentElement']; const SVGTextPositioningElement = _G['SVGTextPositioningElement']; - extractProperties(SVGElement, svgText, visited, descMap, SVG_PREFIX, HTMLELEMENT_IF); + extractProperties( SVGGraphicsElement, svgText, @@ -154,6 +159,22 @@ export function extractSchema(): Map | null { descMap.set(elHierarchy, MISSING_FROM_CHROME[elHierarchy]); }); + // Needed because we're running tests against some older Android versions. + if (typeof MathMLElement !== 'undefined') { + // Math top level + const math = document.createElementNS('http://www.w3.org/1998/Math/MathML', 'math'); + extractProperties(MathMLElement, math, visited, descMap, MATH_PREFIX, HTMLELEMENT_IF); + + // This script is written under the assumption that each tag has a corresponding class name, e.g. + // `` -> `SVGCircleElement` however this doesn't hold for Math elements which are all + // `MathMLElement`. Furthermore, they don't have special property names, but rather are + // configured exclusively via attributes. Register them as plain elements that inherit from + // the top-level `:math` namespace. + ALL_MATH_TAGS.split(',').forEach((tag) => + descMap.set(`${MATH_PREFIX}${tag}^${MATH_PREFIX}`, []), + ); + } + assertNoMissingTags(descMap); return descMap; @@ -166,7 +187,12 @@ function assertNoMissingTags(descMap: Map): void { extractedTags.push(...key.split('|')[0].split('^')[0].split(',')); }); - const missingTags = ALL_HTML_TAGS.split(',').filter((tag) => extractedTags.indexOf(tag) == -1); + const missingTags = [ + ...ALL_HTML_TAGS.split(','), + ...(typeof MathMLElement === 'undefined' + ? [] + : ALL_MATH_TAGS.split(',').map((tag) => MATH_PREFIX + tag)), + ].filter((tag) => !extractedTags.includes(tag)); if (missingTags.length) { throw new Error(`DOM schema misses tags: ${missingTags.join(',')}`);