diff --git a/packages/happy-dom/src/custom-element/CustomElementRegistry.ts b/packages/happy-dom/src/custom-element/CustomElementRegistry.ts index 53382a7c0..496e93c51 100644 --- a/packages/happy-dom/src/custom-element/CustomElementRegistry.ts +++ b/packages/happy-dom/src/custom-element/CustomElementRegistry.ts @@ -9,6 +9,37 @@ export default class CustomElementRegistry { public _registry: { [k: string]: { elementClass: typeof HTMLElement; extends: string } } = {}; public _callbacks: { [k: string]: (() => void)[] } = {}; + /** + * Validates the correctness of custom element tag names. + * + * @param tagName custom element tag name. + * @returns boolean True, if tag name is standard compliant. + */ + private isValidCustomElementName(tagName: string): boolean { + // Validation criteria based on: + // https://html.spec.whatwg.org/multipage/custom-elements.html#valid-custom-element-name + const PCENChar = + '[-_.]|[0-9]|[a-z]|\u{B7}|[\u{C0}-\u{D6}]|[\u{D8}-\u{F6}]' + + '|[\u{F8}-\u{37D}]|[\u{37F}-\u{1FFF}]' + + '|[\u{200C}-\u{200D}]|[\u{203F}-\u{2040}]|[\u{2070}-\u{218F}]' + + '|[\u{2C00}-\u{2FEF}]|[\u{3001}-\u{D7FF}]' + + '|[\u{F900}-\u{FDCF}]|[\u{FDF0}-\u{FFFD}]|[\u{10000}-\u{EFFFF}]'; + + const PCEN = new RegExp(`^[a-z](${PCENChar})*-(${PCENChar})*$`, 'u'); + + const forbiddenNames = [ + 'annotation-xml', + 'color-profile', + 'font-face', + 'font-face-src', + 'font-face-uri', + 'font-face-format', + 'font-face-name', + 'missing-glyph' + ]; + return PCEN.test(tagName) && !forbiddenNames.includes(tagName); + } + /** * Defines a custom element class. * diff --git a/packages/happy-dom/test/custom-element/CustomElementRegistry.test.ts b/packages/happy-dom/test/custom-element/CustomElementRegistry.test.ts index 51637a923..50e96fda0 100644 --- a/packages/happy-dom/test/custom-element/CustomElementRegistry.test.ts +++ b/packages/happy-dom/test/custom-element/CustomElementRegistry.test.ts @@ -10,6 +10,20 @@ describe('CustomElementRegistry', () => { CustomElement.observedAttributesCallCount = 0; }); + describe('isValidCustomElementName()', () => { + it('Validate custom elements tag name.', () => { + expect(customElements.isValidCustomElementName('a-b')).toBe(true); + expect(customElements.isValidCustomElementName('2a-b')).toBe(false); + expect(customElements.isValidCustomElementName('a2-b')).toBe(true); + expect(customElements.isValidCustomElementName('A-B')).toBe(false); + expect(customElements.isValidCustomElementName('aB-c')).toBe(false); + expect(customElements.isValidCustomElementName('ab')).toBe(false); + expect(customElements.isValidCustomElementName('a-\u00d9')).toBe(true); + expect(customElements.isValidCustomElementName('a_b.c-d')).toBe(true); + expect(customElements.isValidCustomElementName('font-face')).toBe(false); + }); + }); + describe('define()', () => { it('Defines an HTML element and returns it with get().', () => { customElements.define('custom-element', CustomElement); @@ -43,7 +57,7 @@ describe('CustomElementRegistry', () => { }); describe('get()', () => { - it('Returns element class if the tag name has been defined..', () => { + it('Returns element class if the tag name has been defined.', () => { customElements.define('custom-element', CustomElement); expect(customElements.get('custom-element')).toBe(CustomElement); });