diff --git a/packages/compiler/src/ml_parser/lexer.ts b/packages/compiler/src/ml_parser/lexer.ts index de96aa74fba..c58f0bc6d45 100644 --- a/packages/compiler/src/ml_parser/lexer.ts +++ b/packages/compiler/src/ml_parser/lexer.ts @@ -692,7 +692,7 @@ class _Tokenizer { this._cursor.advance(); try { const charCode = parseInt(strNum, isHex ? 16 : 10); - this._endToken([String.fromCharCode(charCode), this._cursor.getChars(start)]); + this._endToken([String.fromCodePoint(charCode), this._cursor.getChars(start)]); } catch { throw this._createError( _unknownEntityErrorMsg(this._cursor.getChars(start)), diff --git a/packages/compiler/test/ml_parser/html_parser_spec.ts b/packages/compiler/test/ml_parser/html_parser_spec.ts index fa70619e00e..332082a17c6 100644 --- a/packages/compiler/test/ml_parser/html_parser_spec.ts +++ b/packages/compiler/test/ml_parser/html_parser_spec.ts @@ -52,6 +52,22 @@ describe('HtmlParser', () => { ]); }); + it('should parse text nodes with HTML entities (5+ hex digits)', () => { + // Test with 🛈 (U+1F6C8 - Circled Information Source) + expect(humanizeDom(parser.parse('
🛈
', 'TestComp'))).toEqual([ + [html.Element, 'div', 0], + [html.Text, '\u{1F6C8}', 1, [''], ['\u{1F6C8}', '🛈'], ['']], + ]); + }); + + it('should parse text nodes with decimal HTML entities (5+ digits)', () => { + // Test with 🛈 (U+1F6C8 - Circled Information Source) as decimal 128712 + expect(humanizeDom(parser.parse('
🛈
', 'TestComp'))).toEqual([ + [html.Element, 'div', 0], + [html.Text, '\u{1F6C8}', 1, [''], ['\u{1F6C8}', '🛈'], ['']], + ]); + }); + it('should normalize line endings within CDATA', () => { const parsed = parser.parse('', 'TestComp'); expect(humanizeDom(parsed)).toEqual([ @@ -326,6 +342,22 @@ describe('HtmlParser', () => { ]); }); + it('should parse attributes containing encoded entities (5+ hex digits)', () => { + // Test with 🛈 (U+1F6C8 - Circled Information Source) + expect(humanizeDom(parser.parse('
', 'TestComp'))).toEqual([ + [html.Element, 'div', 0], + [html.Attribute, 'foo', '\u{1F6C8}', [''], ['\u{1F6C8}', '🛈'], ['']], + ]); + }); + + it('should parse attributes containing encoded decimal entities (5+ digits)', () => { + // Test with 🛈 (U+1F6C8 - Circled Information Source) as decimal 128712 + expect(humanizeDom(parser.parse('
', 'TestComp'))).toEqual([ + [html.Element, 'div', 0], + [html.Attribute, 'foo', '\u{1F6C8}', [''], ['\u{1F6C8}', '🛈'], ['']], + ]); + }); + it('should parse attributes containing unquoted interpolation', () => { expect(humanizeDom(parser.parse('
', 'TestComp'))).toEqual([ [html.Element, 'div', 0], @@ -1632,6 +1664,25 @@ describe('HtmlParser', () => { ]); }); + it('should decode HTML entities with 5+ hex digits in interpolations', () => { + // Test with 🛈 (U+1F6C8 - Circled Information Source) + expect( + humanizeDomSourceSpans(parser.parse('{{🛈}}' + '{{🛈}}', 'TestComp')), + ).toEqual([ + [ + html.Text, + '{{\u{1F6C8}}}' + '{{\u{1F6C8}}}', + 0, + [''], + ['{{', '🛈', '}}'], + [''], + ['{{', '🛈', '}}'], + [''], + '{{🛈}}' + '{{🛈}}', + ], + ]); + }); + it('should support interpolations in text', () => { expect( humanizeDomSourceSpans(parser.parse('
pre {{ value }} post
', 'TestComp')), diff --git a/packages/compiler/test/ml_parser/lexer_spec.ts b/packages/compiler/test/ml_parser/lexer_spec.ts index 6587fc799cd..bc57f2d8c61 100644 --- a/packages/compiler/test/ml_parser/lexer_spec.ts +++ b/packages/compiler/test/ml_parser/lexer_spec.ts @@ -2136,6 +2136,26 @@ describe('HtmlLexer', () => { ]); }); + it('should parse entities with more than 4 hex digits', () => { + // Test 5 hex digit entity: 🛈 (🛈 - Circled Information Source) + expect(tokenizeAndHumanizeParts('🛈')).toEqual([ + [TokenType.TEXT, ''], + [TokenType.ENCODED_ENTITY, '\u{1F6C8}', '🛈'], + [TokenType.TEXT, ''], + [TokenType.EOF], + ]); + }); + + it('should parse entities with more than 4 decimal digits', () => { + // Test decimal entity: 🛈 (🛈 - Circled Information Source) + expect(tokenizeAndHumanizeParts('🛈')).toEqual([ + [TokenType.TEXT, ''], + [TokenType.ENCODED_ENTITY, '\u{1F6C8}', '🛈'], + [TokenType.TEXT, ''], + [TokenType.EOF], + ]); + }); + it('should store the locations', () => { expect(tokenizeAndHumanizeSourceSpans('a&b')).toEqual([ [TokenType.TEXT, 'a'],