From a631c2446996e8b203132773ca5efa1733fb2321 Mon Sep 17 00:00:00 2001 From: Yarchik Date: Mon, 22 Jun 2026 15:27:16 +0100 Subject: [PATCH] fix: decode numeric references to surrogate code points as U+FFFD `decode('�')`, and any numeric character reference in the surrogate range U+D800..U+DFFF, returned a lone surrogate instead of the replacement character. The WHATWG numeric character reference end state requires a surrogate code point to be set to 0xFFFD; emitting a lone surrogate produces strings that are not well-formed UTF-16 and break round-trips through UTF-8 and JSON.stringify. The numeric branch passed code points <= 0xFFFF straight to fromCharCode, which yields the lone surrogate. Add the surrogate range to the existing out-of-bounds check. Non-surrogate references (C1 Windows-1252 mappings, astral code points, named references) are unaffected. --- src/index.ts | 3 ++- test/index.test.ts | 7 +++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 197413e..26bbf63 100644 --- a/src/index.ts +++ b/src/index.ts @@ -125,7 +125,8 @@ function getDecodedEntity( : parseInt(entity.substr(2)); decodeResult = - decodeCode >= 0x10ffff + decodeCode >= 0x10ffff || + (decodeCode >= 0xd800 && decodeCode <= 0xdfff) ? outOfBoundsChar : decodeCode > 65535 ? fromCodePoint(decodeCode) diff --git a/test/index.test.ts b/test/index.test.ts index 37d7311..d6470f7 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -143,6 +143,13 @@ describe('lib', () => { it('should handle invalid numeric entities', () => { expect(decode('�')).toEqual(String.fromCharCode(65533)); }); + it('should replace surrogate numeric entities with the replacement character', () => { + // Per the WHATWG spec, a numeric character reference to a surrogate + // code point (U+D800..U+DFFF) must become U+FFFD, not a lone surrogate. + expect(decode('�')).toEqual(String.fromCharCode(65533)); + expect(decode('�')).toEqual(String.fromCharCode(65533)); + expect(decode('�')).toEqual(String.fromCharCode(65533)); + }); it('should decode numeric entities without semicolon', () => { expect(decode('"C"')).toEqual('"C"'); });