Skip to content

Commit 7f4607e

Browse files
fix(iOS): rendering numeric entities (#638)
# Summary Fixes: #636 This PR adds support for numeric character references (both decimal and hexadecimal), for example: 用 and 用. ## Test Plan Run reproduction steps from issue: #636 Issue should be resolved. ## Screenshots / Videos https://github.com/user-attachments/assets/f6be0e7d-df27-4794-a972-9b5c910b1a3f ## Compatibility | OS | Implemented | | ------- | :---------: | | iOS | ✅ | | Android | ❌ | ## Checklist - [x] E2E tests are passing - [ ] Required E2E tests have been added (if applicable) ---------
1 parent 6409223 commit 7f4607e

1 file changed

Lines changed: 75 additions & 0 deletions

File tree

ios/extensions/StringExtension.mm

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,81 @@ + (NSDictionary *)getEscapedCharactersInfoFrom:(NSString *)text {
5151
}
5252
}
5353

54+
// Numeric character references: &#NNNN; (decimal) and &#xHHHH; (hex)
55+
NSRegularExpression *numericEntityRegex = [NSRegularExpression
56+
regularExpressionWithPattern:@"&#([xX][0-9a-fA-F]+|[0-9]+);"
57+
options:0
58+
error:nil];
59+
60+
[numericEntityRegex
61+
enumerateMatchesInString:text
62+
options:0
63+
range:NSMakeRange(0, text.length)
64+
usingBlock:^(NSTextCheckingResult *match,
65+
NSMatchingFlags flags, BOOL *stop) {
66+
if (match == nil) {
67+
return;
68+
}
69+
NSRange fullRange = [match range];
70+
NSString *entityStr = [text substringWithRange:fullRange];
71+
NSString *valueStr =
72+
[text substringWithRange:[match rangeAtIndex:1]];
73+
74+
// Convert the matched string into a raw integer (UTF32
75+
// Code Point)
76+
UTF32Char codePoint = 0;
77+
if ([valueStr hasPrefix:@"x"] ||
78+
[valueStr hasPrefix:@"X"]) {
79+
// Parse Hexadecimal (base 16)
80+
const char *hexStr =
81+
[[valueStr substringFromIndex:1] UTF8String];
82+
codePoint = (UTF32Char)strtoul(hexStr, NULL, 16);
83+
} else {
84+
// Parse Decimal (base 10)
85+
const char *decStr = [valueStr UTF8String];
86+
codePoint = (UTF32Char)strtoul(decStr, NULL, 10);
87+
}
88+
89+
// Safety check: HTML numeric character references should
90+
// map to valid Unicode scalar values (0x0..0x10FFFF),
91+
// excluding surrogate code points (0xD800-0xDFFF). Per
92+
// HTML5, code point 0 is treated as U+FFFD. Replace
93+
// invalid values with U+FFFD (Replacement Character) to
94+
// avoid crashes/truncation.
95+
if (codePoint == 0 || codePoint > 0x10FFFF ||
96+
(codePoint >= 0xD800 && codePoint <= 0xDFFF)) {
97+
codePoint = 0xFFFD;
98+
}
99+
100+
NSString *decoded;
101+
if (codePoint <= 0xFFFF) {
102+
// STANDARD CHARACTER: Fits perfectly in one 16-bit
103+
// unichar.
104+
unichar ch = (unichar)codePoint;
105+
decoded = [NSString stringWithCharacters:&ch length:1];
106+
} else {
107+
// LARGE CHARACTER: Too big for 16 bits.
108+
// We must split the code point into two 16-bit halves
109+
// (a "Surrogate Pair") so NSString can store it
110+
// properly in UTF-16.
111+
UniChar surrogate[2];
112+
113+
// Calculate the "High" surrogate half
114+
surrogate[0] =
115+
(UniChar)(0xD800 + ((codePoint - 0x10000) >> 10));
116+
117+
// Calculate the "Low" surrogate half
118+
surrogate[1] =
119+
(UniChar)(0xDC00 + ((codePoint - 0x10000) & 0x3FF));
120+
121+
// Create the string using both 16-bit pieces
122+
decoded = [NSString stringWithCharacters:surrogate
123+
length:2];
124+
}
125+
126+
results[@(fullRange.location)] = @[ entityStr, decoded ];
127+
}];
128+
54129
return results;
55130
}
56131

0 commit comments

Comments
 (0)