From a88535519220bb6b0edaeba3d99234ca81653006 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20=C5=BB=C3=B3=C5=82kiewski?= Date: Fri, 29 May 2026 11:10:09 +0200 Subject: [PATCH] fix: remove hacks for last element parsing --- .../flows/empty_element_parsing.yaml | 2 - .../screenshots/ios/empty_element_parsing.png | Bin 0 -> 12102 bytes ios/htmlParser/HtmlParser.mm | 160 +++++++++++------- 3 files changed, 95 insertions(+), 67 deletions(-) create mode 100644 .maestro/enrichedInput/screenshots/ios/empty_element_parsing.png diff --git a/.maestro/enrichedInput/flows/empty_element_parsing.yaml b/.maestro/enrichedInput/flows/empty_element_parsing.yaml index 4429bf8e9..981cd4dae 100644 --- a/.maestro/enrichedInput/flows/empty_element_parsing.yaml +++ b/.maestro/enrichedInput/flows/empty_element_parsing.yaml @@ -1,6 +1,4 @@ appId: swmansion.enriched.example -tags: - - android-only --- # PR #284 - fix: parsing empty elements - launchApp diff --git a/.maestro/enrichedInput/screenshots/ios/empty_element_parsing.png b/.maestro/enrichedInput/screenshots/ios/empty_element_parsing.png new file mode 100644 index 0000000000000000000000000000000000000000..ea9dd3310a6d6e7dc64ba2501604fbb49a9d5de7 GIT binary patch literal 12102 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 Lzh|Di`RM-vOvjG_ literal 0 HcmV?d00001 diff --git a/ios/htmlParser/HtmlParser.mm b/ios/htmlParser/HtmlParser.mm index 242d964c6..c64a8d363 100644 --- a/ios/htmlParser/HtmlParser.mm +++ b/ios/htmlParser/HtmlParser.mm @@ -406,34 +406,6 @@ + (NSString *_Nullable)initiallyProcessHtml:(NSString *_Nonnull)html inString:fixedHtml leading:NO trailing:YES]; - - // this is more like a hack but for some reason the last
in - //
and are not properly changed into zero width - // space so we do that manually here - fixedHtml = [fixedHtml - stringByReplacingOccurrencesOfString:@"
\n
" - withString:@"

\u200B

\n"]; - fixedHtml = [fixedHtml - stringByReplacingOccurrencesOfString:@"
\n" - withString:@"

\u200B

\n"]; - - // The same like above for (blockquote and codeblock) this is more like a - // hack but for some reason the last
  • in
      and
        are not - // properly changed into zero width space so we do that manually here - // TODO: investigate this further, issue is already described here: - // https://github.com/software-mansion/react-native-enriched/issues/505 - fixedHtml = [fixedHtml - stringByReplacingOccurrencesOfString:@"
      1. \n
    " - withString:@"
  • \u200B
  • \n"]; - fixedHtml = [fixedHtml - stringByReplacingOccurrencesOfString:@"
  • \n" - withString:@"
  • \u200B
  • \n"]; - - // replace "
    " at the end with "
    \n" if input is not empty to properly - // handle last
    in html - if ([fixedHtml hasSuffix:@"
    "] && fixedHtml.length != 4) { - fixedHtml = [fixedHtml stringByAppendingString:@"\n"]; - } } return fixedHtml; @@ -452,6 +424,7 @@ + (NSArray *_Nonnull)getTextAndStylesFromHtml:(NSString *_Nonnull)fixedHtml { BOOL gettingTagName = NO; BOOL gettingTagParams = NO; BOOL closingTag = NO; + BOOL lastTagWasBr = NO; NSMutableString *currentTagName = [[NSMutableString alloc] initWithString:@""]; NSMutableString *currentTagParams = @@ -487,54 +460,82 @@ + (NSArray *_Nonnull)getTextAndStylesFromHtml:(NSString *_Nonnull)fixedHtml { } if ([currentTagName isEqualToString:@"br"]) { + lastTagWasBr = YES; // do nothing, we don't include these tags in styles } else if ([currentTagName isEqualToString:@"li"]) { - // Only track checkbox state if we're inside a checkbox list - if (insideCheckboxList && !closingTag) { - BOOL isChecked = [currentTagParams containsString:@"checked"]; - checkboxStates[@(plainText.length)] = @(isChecked); + if (!closingTag) { + // Opening tag
  • + if (insideCheckboxList) { + BOOL isChecked = [currentTagParams containsString:@"checked"]; + checkboxStates[@(plainText.length)] = @(isChecked); + } + // Record the start location so we can check if it's empty when + // closing + ongoingTags[@"li"] = @[ @(plainText.length) ]; + } else { + // Closing tag
  • + NSArray *tagData = ongoingTags[@"li"]; + if (tagData != nil) { + NSInteger tagLocation = [((NSNumber *)tagData[0]) intValue]; + NSString *innerContent = [plainText substringFromIndex:tagLocation]; + + // If the li is completely empty (or just contains layout newlines), + // inject ZWS + if ([innerContent + stringByTrimmingCharactersInSet:[NSCharacterSet + newlineCharacterSet]] + .length == 0) { + [plainText appendString:@"\u200B"]; + } + [ongoingTags removeObjectForKey:@"li"]; + } } } else if (!closingTag) { BOOL isPlainParagraph = [currentTagName isEqualToString:@"p"] && (!currentTagParams || [currentTagParams length] == 0); - if (isPlainParagraph) { - continue; - } - // we finish opening tag - get its location, the current - // precedingImageCount and optionally params and put them under tag name - // key in ongoingTags. Storing the open-time image count lets - // finalizeTagEntry: correctly shift the start and extend the length - // so the range covers any images finalized between open and close. - NSMutableArray *tagArr = [[NSMutableArray alloc] init]; - [tagArr addObject:[NSNumber numberWithInteger:plainText.length]]; - [tagArr addObject:[NSNumber numberWithInteger:precedingImageCount]]; - if (currentTagParams.length > 0) { - [tagArr addObject:[currentTagParams copy]]; - } - ongoingTags[currentTagName] = tagArr; + if (!isPlainParagraph) { + // we finish opening tag - get its location, the current + // precedingImageCount and optionally params and put them under tag + // name key in ongoingTags. Storing the open-time image count lets + // finalizeTagEntry: correctly shift the start and extend the length + // so the range covers any images finalized between open and close. + NSMutableArray *tagArr = [[NSMutableArray alloc] init]; + [tagArr addObject:[NSNumber numberWithInteger:plainText.length]]; + [tagArr addObject:[NSNumber numberWithInteger:precedingImageCount]]; + if (currentTagParams.length > 0) { + [tagArr addObject:[currentTagParams copy]]; + } + ongoingTags[currentTagName] = tagArr; - // Check if this is a checkbox list - if ([currentTagName isEqualToString:@"ul"] && - [self isUlCheckboxList:currentTagParams]) { - insideCheckboxList = YES; - } + // Check if this is a checkbox list + if ([currentTagName isEqualToString:@"ul"] && + [self isUlCheckboxList:currentTagParams]) { + insideCheckboxList = YES; + } - // skip one newline if it was added after opening tags that are in - // separate lines - if ([self isBlockTag:currentTagName] && i + 1 < fixedHtml.length && - [[NSCharacterSet newlineCharacterSet] - characterIsMember:[fixedHtml characterAtIndex:i + 1]]) { - i += 1; - } + // skip one newline if it was added after opening tags that are in + // separate lines + if ([self isBlockTag:currentTagName] && i + 1 < fixedHtml.length && + [[NSCharacterSet newlineCharacterSet] + characterIsMember:[fixedHtml characterAtIndex:i + 1]]) { + i += 1; + } + + if ([currentTagName isEqualToString:@"img"]) { + // Images have no inner text, so we manually break the
    streak + // here. + lastTagWasBr = NO; + } - if (isSelfClosing) { - [self finalizeTagEntry:currentTagName - ongoingTags:ongoingTags - initiallyProcessedTags:initiallyProcessedTags - plainText:plainText - precedingImageCount:&precedingImageCount]; + if (isSelfClosing) { + [self finalizeTagEntry:currentTagName + ongoingTags:ongoingTags + initiallyProcessedTags:initiallyProcessedTags + plainText:plainText + precedingImageCount:&precedingImageCount]; + } } } else { // we finish closing tags - pack tag name, tag range and optionally tag @@ -548,12 +549,36 @@ + (NSArray *_Nonnull)getTextAndStylesFromHtml:(NSString *_Nonnull)fixedHtml { BOOL isBlockTag = [self isBlockTag:currentTagName]; + // ZWS logic for blockquote and codeblock + BOOL needsZWS = [currentTagName isEqualToString:@"blockquote"] || + [currentTagName isEqualToString:@"codeblock"]; + BOOL isEmptyBlock = NO; + if (needsZWS) { + NSArray *tagData = ongoingTags[currentTagName]; + if (tagData != nil) { + NSInteger tagLoc = [tagData[0] intValue]; + NSString *inner = [plainText substringFromIndex:tagLoc]; + if ([inner stringByTrimmingCharactersInSet:[NSCharacterSet + newlineCharacterSet]] + .length == 0) { + isEmptyBlock = YES; + } + } + } + // skip one newline if it was added before some closing tags that are // in separate lines if (isBlockTag && plainText.length > 0 && [[NSCharacterSet newlineCharacterSet] characterIsMember:[plainText characterAtIndex:plainText.length - 1]]) { + + // If the last thing processed was a
    , or the block is totally + // empty, inject a \u200B before trimming the trailing newline to save + // the empty line. + if (lastTagWasBr || isEmptyBlock) { + [plainText insertString:@"\u200B" atIndex:plainText.length - 1]; + } plainText = [[plainText substringWithRange:NSMakeRange(0, plainText.length - 1)] mutableCopy]; @@ -588,6 +613,11 @@ + (NSArray *_Nonnull)getTextAndStylesFromHtml:(NSString *_Nonnull)fixedHtml { i += escaped.length - 1; } else { [plainText appendString:currentCharacterStr]; + // Any typed character that isn't a newline breaks the
    streak + if (![[NSCharacterSet newlineCharacterSet] + characterIsMember:currentCharacterChar]) { + lastTagWasBr = NO; + } } } else { if (gettingTagName) {