From 7cb9ff6f830879385e35268d9188abbd39dff56b Mon Sep 17 00:00:00 2001 From: simpleqt <2015900818@qq.com> Date: Thu, 5 Nov 2026 01:21:40 +0800 Subject: [PATCH] Replace numeric character references to surrogates with U+FFFD A numeric character reference to a code point in the surrogate range, such as ``, produced a lone surrogate in the text. The spec says that invalid Unicode code points are replaced by the REPLACEMENT CHARACTER, and a surrogate is not a valid Unicode code point; cmark and commonmark.js both replace the whole range. The resulting string is not valid UTF-16 and cannot be encoded, so it turned into "?" when the rendered HTML was written as UTF-8. Html5Entities.entityToString only special-cased 0 and relied on Character.toChars throwing for values above U+10FFFF, but Character.toChars accepts the surrogate range. --- .../org/commonmark/internal/util/Html5Entities.java | 6 +++++- .../java/org/commonmark/test/SpecialInputTest.java | 13 +++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/commonmark/src/main/java/org/commonmark/internal/util/Html5Entities.java b/commonmark/src/main/java/org/commonmark/internal/util/Html5Entities.java index 2e92c9be8..2e25289ec 100644 --- a/commonmark/src/main/java/org/commonmark/internal/util/Html5Entities.java +++ b/commonmark/src/main/java/org/commonmark/internal/util/Html5Entities.java @@ -30,7 +30,11 @@ public static String entityToString(String input) { try { int codePoint = Integer.parseInt(value, base); - if (codePoint == 0) { + // The spec says that invalid Unicode code points are replaced by the REPLACEMENT + // CHARACTER. The surrogate range is not a set of valid code points. + if (codePoint == 0 + || (codePoint >= Character.MIN_SURROGATE + && codePoint <= Character.MAX_SURROGATE)) { return "\uFFFD"; } return new String(Character.toChars(codePoint)); diff --git a/commonmark/src/test/java/org/commonmark/test/SpecialInputTest.java b/commonmark/src/test/java/org/commonmark/test/SpecialInputTest.java index 59382b28f..8de5e209d 100644 --- a/commonmark/src/test/java/org/commonmark/test/SpecialInputTest.java +++ b/commonmark/src/test/java/org/commonmark/test/SpecialInputTest.java @@ -19,6 +19,19 @@ public void nullCharacterEntityShouldBeReplaced() { assertRendering("foobar", "
foo\uFFFDbar
\n"); } + @Test + public void surrogateCharacterReferenceShouldBeReplaced() { + // The reference is well-formed, but a surrogate is not a valid Unicode code point, so it + // must be replaced by U+FFFD (same as cmark and commonmark.js) + assertRendering("", "\uFFFD
\n"); + assertRendering("", "\uFFFD
\n"); + assertRendering("", "\uFFFD
\n"); + assertRendering("beforeafter", "before\uFFFDafter
\n"); + // Code points outside the surrogate range are not affected + assertRendering("😀", "\uD83D\uDE00
\n"); + assertRendering("", "\uDBFF\uDFFF
\n"); + } + @Test public void crLfAsLineSeparatorShouldBeParsed() { assertRendering("foo\r\nbar", "foo\nbar
\n");