From a5f20049caa6641c59f82a412e13eb4b7bcb13a2 Mon Sep 17 00:00:00 2001 From: szydlovsky <9szydlowski9@gmail.com> Date: Mon, 17 Nov 2025 13:38:54 +0100 Subject: [PATCH 1/3] feat: html entities handling on iOS --- ios/inputParser/InputParser.mm | 23 ++++++++++++++++++----- ios/inputTextView/InputTextView.mm | 8 +++----- ios/utils/StringExtension.h | 2 +- ios/utils/StringExtension.mm | 26 +++++++++++++++++++++----- 4 files changed, 43 insertions(+), 16 deletions(-) diff --git a/ios/inputParser/InputParser.mm b/ios/inputParser/InputParser.mm index f27cecda5..d0d2002e2 100644 --- a/ios/inputParser/InputParser.mm +++ b/ios/inputParser/InputParser.mm @@ -3,6 +3,7 @@ #import "StyleHeaders.h" #import "UIView+React.h" #import "TextInsertionUtils.h" +#import "StringExtension.h" @implementation InputParser { EnrichedTextInputView *_input; @@ -204,8 +205,8 @@ - (NSString *)parseToHtmlFromRange:(NSRange)range { [result appendString: [NSString stringWithFormat:@"<%@>", tagContent]]; } - // append the letter - [result appendString:currentCharacterStr]; + // append the letter and escape it if needed + [result appendString: [NSString stringByEscapingHtml:currentCharacterStr]]; // save current styles for next character's checks previousActiveStyles = currentActiveStyles; @@ -490,6 +491,7 @@ - (NSArray *)getTextAndStylesFromHtml:(NSString *)fixedHtml { BOOL closingTag = NO; NSMutableString *currentTagName = [[NSMutableString alloc] initWithString:@""]; NSMutableString *currentTagParams = [[NSMutableString alloc] initWithString:@""]; + NSDictionary *htmlEntitiesDict = [NSString getEscapedCharactersInfoFrom:fixedHtml]; // firstly, extract text and initially processed tags for(int i = 0; i < fixedHtml.length; i++) { @@ -511,7 +513,7 @@ - (NSArray *)getTextAndStylesFromHtml:(NSString *)fixedHtml { } else if(!closingTag) { // we finish opening tag - get its location and optionally params and put them under tag name key in ongoingTags NSMutableArray *tagArr = [[NSMutableArray alloc] init]; - [tagArr addObject:[NSNumber numberWithInt:plainText.length]]; + [tagArr addObject:[NSNumber numberWithInteger:plainText.length]]; if(currentTagParams.length > 0) { [tagArr addObject:[currentTagParams copy]]; } @@ -550,8 +552,19 @@ - (NSArray *)getTextAndStylesFromHtml:(NSString *)fixedHtml { currentTagParams = [[NSMutableString alloc] initWithString:@""]; } else { if(!insideTag) { - // no tags logic - just append text - [plainText appendString:currentCharacterStr]; + // no tags logic - just append the right text + + // html entity on the index; use unescaped character and forward iterator accordingly + NSArray *entityInfo = htmlEntitiesDict[@(i)]; + if(entityInfo != nullptr) { + NSString *escaped = entityInfo[0]; + NSString *unescaped = entityInfo[1]; + [plainText appendString:unescaped]; + // the iterator will forward by 1 itself + i += escaped.length - 1; + } else { + [plainText appendString:currentCharacterStr]; + } } else { if(gettingTagName) { if(currentCharacterChar == ' ') { diff --git a/ios/inputTextView/InputTextView.mm b/ios/inputTextView/InputTextView.mm index 33767c8d2..73115fa22 100644 --- a/ios/inputTextView/InputTextView.mm +++ b/ios/inputTextView/InputTextView.mm @@ -14,7 +14,7 @@ - (void)copy:(id)sender { NSString *plainText = [typedInput->textView.textStorage.string substringWithRange:typedInput->textView.selectedRange]; NSString *fixedPlainText = [plainText stringByReplacingOccurrencesOfString:@"\u200B" withString:@""]; - NSString *escapedHtml = [NSString stringByEscapingHtml:[typedInput->parser parseToHtmlFromRange:typedInput->textView.selectedRange]]; + NSString *parsedHtml = [typedInput->parser parseToHtmlFromRange:typedInput->textView.selectedRange]; NSMutableAttributedString *attrStr = [[typedInput->textView.textStorage attributedSubstringFromRange:typedInput->textView.selectedRange] mutableCopy]; NSRange fullAttrStrRange = NSMakeRange(0, attrStr.length); @@ -28,7 +28,7 @@ - (void)copy:(id)sender { UIPasteboard *pasteboard = [UIPasteboard generalPasteboard]; [pasteboard setItems:@[@{ UTTypeUTF8PlainText.identifier : fixedPlainText, - UTTypeHTML.identifier : escapedHtml, + UTTypeHTML.identifier : parsedHtml, UTTypeRTF.identifier : rtfData }]]; } @@ -53,9 +53,7 @@ - (void)paste:(id)sender { htmlString = htmlValue; } - // unescape the html - htmlString = [NSString stringByUnescapingHtml:htmlString]; - // validate it + // validate the html NSString *initiallyProcessedHtml = [typedInput->parser initiallyProcessHtml:htmlString]; if(initiallyProcessedHtml != nullptr) { diff --git a/ios/utils/StringExtension.h b/ios/utils/StringExtension.h index fa4d00b90..f13d65da2 100644 --- a/ios/utils/StringExtension.h +++ b/ios/utils/StringExtension.h @@ -6,7 +6,7 @@ - (std::string)toCppString; + (NSString *)fromCppString:(std::string)string; + (NSString *)stringByEscapingHtml:(NSString *)html; -+ (NSString *)stringByUnescapingHtml:(NSString *)html; ++ (NSDictionary *)getEscapedCharactersInfoFrom:(NSString *)text; @end @interface NSMutableString (StringExtension) diff --git a/ios/utils/StringExtension.mm b/ios/utils/StringExtension.mm index cc2a18108..d08a73b6a 100644 --- a/ios/utils/StringExtension.mm +++ b/ios/utils/StringExtension.mm @@ -17,7 +17,8 @@ + (NSString *)stringByEscapingHtml:(NSString *)html { @"<": @"<", @">": @">", @"\"": @""", - @"'": @"'" + @"'": @"'", + @"/": @"/", }; for(NSString *key in escapeMap) { @@ -26,20 +27,35 @@ + (NSString *)stringByEscapingHtml:(NSString *)html { return escaped; } -+ (NSString *)stringByUnescapingHtml:(NSString *)html { - NSMutableString *unescaped = [html mutableCopy]; ++ (NSDictionary *)getEscapedCharactersInfoFrom:(NSString *)text { NSDictionary *unescapeMap = @{ @"&": @"&", @"<": @"<", @">": @">", @""": @"\"", @"'": @"'", + @"/": @"/", }; + NSMutableDictionary *results = [[NSMutableDictionary alloc] init]; + for(NSString *key in unescapeMap) { - [unescaped replaceOccurrencesOfString:key withString:unescapeMap[key] options:NSLiteralSearch range:NSMakeRange(0, unescaped.length)]; + NSRange searchRange = NSMakeRange(0, text.length); + NSRange foundRange; + + while(searchRange.location < text.length) { + foundRange = [text rangeOfString:key options:0 range:searchRange]; + if(foundRange.location != NSNotFound) { + results[@(foundRange.location)] = @[key, unescapeMap[key]]; + searchRange.location = foundRange.location + foundRange.length; + searchRange.length = text.length - searchRange.location; + } else { + break; + } + } } - return unescaped; + + return results; } @end From c95447247fb3da7d24e27738dd0e543d554e99d8 Mon Sep 17 00:00:00 2001 From: szydlovsky <9szydlowski9@gmail.com> Date: Mon, 17 Nov 2025 15:28:16 +0100 Subject: [PATCH 2/3] fix: remove unneeded characters that Android doesn't handle anyway --- ios/utils/StringExtension.mm | 6 ------ 1 file changed, 6 deletions(-) diff --git a/ios/utils/StringExtension.mm b/ios/utils/StringExtension.mm index d08a73b6a..2e80197f8 100644 --- a/ios/utils/StringExtension.mm +++ b/ios/utils/StringExtension.mm @@ -16,9 +16,6 @@ + (NSString *)stringByEscapingHtml:(NSString *)html { @"&": @"&", @"<": @"<", @">": @">", - @"\"": @""", - @"'": @"'", - @"/": @"/", }; for(NSString *key in escapeMap) { @@ -32,9 +29,6 @@ + (NSDictionary *)getEscapedCharactersInfoFrom:(NSString *)text { @"&": @"&", @"<": @"<", @">": @">", - @""": @"\"", - @"'": @"'", - @"/": @"/", }; NSMutableDictionary *results = [[NSMutableDictionary alloc] init]; From 7b1f592b27dbc4a2e49282176dd10e1e2b4b07eb Mon Sep 17 00:00:00 2001 From: szydlovsky <9szydlowski9@gmail.com> Date: Mon, 17 Nov 2025 18:09:27 +0100 Subject: [PATCH 3/3] chore: code review cosmetic changes --- ios/utils/StringExtension.mm | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/ios/utils/StringExtension.mm b/ios/utils/StringExtension.mm index 2e80197f8..95519debf 100644 --- a/ios/utils/StringExtension.mm +++ b/ios/utils/StringExtension.mm @@ -39,13 +39,12 @@ + (NSDictionary *)getEscapedCharactersInfoFrom:(NSString *)text { while(searchRange.location < text.length) { foundRange = [text rangeOfString:key options:0 range:searchRange]; - if(foundRange.location != NSNotFound) { - results[@(foundRange.location)] = @[key, unescapeMap[key]]; - searchRange.location = foundRange.location + foundRange.length; - searchRange.length = text.length - searchRange.location; - } else { + if(foundRange.location == NSNotFound) { break; } + results[@(foundRange.location)] = @[key, unescapeMap[key]]; + searchRange.location = foundRange.location + foundRange.length; + searchRange.length = text.length - searchRange.location; } }