diff --git a/src/core/xml_parser.js b/src/core/xml_parser.js
index 3d3ffb983..95d1614b6 100644
--- a/src/core/xml_parser.js
+++ b/src/core/xml_parser.js
@@ -53,12 +53,12 @@ class XMLParserBase {
}
_resolveEntities(s) {
- return s.replaceAll(XMLParserBase._entityRegex, (_, hex, dec, entity) => {
- if (hex) {
- return String.fromCodePoint(parseInt(hex, 16));
- }
- if (dec) {
- return String.fromCodePoint(parseInt(dec, 10));
+ return s.replaceAll(XMLParserBase._entityRegex, (all, hex, dec, entity) => {
+ if (hex || dec) {
+ const code = hex ? parseInt(hex, 16) : parseInt(dec, 10);
+ // An out-of-range or unparsable code point is kept as-is, since
+ // `String.fromCodePoint` would throw on it.
+ return code >= 0 && code <= 0x10ffff ? String.fromCodePoint(code) : all;
}
switch (entity) {
case "lt":
diff --git a/test/unit/xml_spec.js b/test/unit/xml_spec.js
index 14a32e67e..d64f8bccd 100644
--- a/test/unit/xml_spec.js
+++ b/test/unit/xml_spec.js
@@ -109,6 +109,27 @@ describe("XML", function () {
});
});
+ describe("character references", function () {
+ const parseText = xml =>
+ new SimpleXMLParser({}).parseFromString(xml).documentElement.textContent;
+
+ it("should resolve the valid ones", function () {
+ expect(parseText("AB😂")).toEqual(
+ "AB\u{1F602}\0\u{10FFFF}"
+ );
+ });
+
+ it("should keep the invalid ones as-is", function () {
+ // These must not throw: `String.fromCodePoint` rejects anything which
+ // isn't a code point.
+ expect(parseText("ZZ;")).toEqual("ZZ;");
+ expect(parseText("zz;")).toEqual("zz;");
+ expect(parseText("")).toEqual("");
+ expect(parseText("")).toEqual("");
+ expect(parseText("-1;")).toEqual("-1;");
+ });
+ });
+
it("should parse processing instructions", function () {
const xml = `