@@ -409,34 +409,6 @@ + (NSString *_Nullable)initiallyProcessHtml:(NSString *_Nonnull)html
409409 inString: fixedHtml
410410 leading: NO
411411 trailing: YES ];
412-
413- // this is more like a hack but for some reason the last <br> in
414- // <blockquote> and <codeblock> are not properly changed into zero width
415- // space so we do that manually here
416- fixedHtml = [fixedHtml
417- stringByReplacingOccurrencesOfString: @" <br>\n </blockquote>"
418- withString: @" <p>\u200B </p>\n </blockquote>" ];
419- fixedHtml = [fixedHtml
420- stringByReplacingOccurrencesOfString: @" <br>\n </codeblock>"
421- withString: @" <p>\u200B </p>\n </codeblock>" ];
422-
423- // The same like above for (blockquote and codeblock) this is more like a
424- // hack but for some reason the last <li></li> in <ul> and <ol> are not
425- // properly changed into zero width space so we do that manually here
426- // TODO: investigate this further, issue is already described here:
427- // https://github.com/software-mansion/react-native-enriched/issues/505
428- fixedHtml = [fixedHtml
429- stringByReplacingOccurrencesOfString: @" <li></li>\n </ul>"
430- withString: @" <li>\u200B </li>\n </ul>" ];
431- fixedHtml = [fixedHtml
432- stringByReplacingOccurrencesOfString: @" <li></li>\n </ol>"
433- withString: @" <li>\u200B </li>\n </ol>" ];
434-
435- // replace "<br>" at the end with "<br>\n" if input is not empty to properly
436- // handle last <br> in html
437- if ([fixedHtml hasSuffix: @" <br>" ] && fixedHtml.length != 4 ) {
438- fixedHtml = [fixedHtml stringByAppendingString: @" \n " ];
439- }
440412 }
441413
442414 return fixedHtml;
@@ -455,6 +427,7 @@ + (NSArray *_Nonnull)getTextAndStylesFromHtml:(NSString *_Nonnull)fixedHtml {
455427 BOOL gettingTagName = NO ;
456428 BOOL gettingTagParams = NO ;
457429 BOOL closingTag = NO ;
430+ BOOL lastTagWasBr = NO ;
458431 NSMutableString *currentTagName =
459432 [[NSMutableString alloc ] initWithString: @" " ];
460433 NSMutableString *currentTagParams =
@@ -490,12 +463,36 @@ + (NSArray *_Nonnull)getTextAndStylesFromHtml:(NSString *_Nonnull)fixedHtml {
490463 }
491464
492465 if ([currentTagName isEqualToString: @" br" ]) {
466+ lastTagWasBr = YES ;
493467 // do nothing, we don't include these tags in styles
494468 } else if ([currentTagName isEqualToString: @" li" ]) {
495- // Only track checkbox state if we're inside a checkbox list
496- if (insideCheckboxList && !closingTag) {
497- BOOL isChecked = [currentTagParams containsString: @" checked" ];
498- checkboxStates[@(plainText.length)] = @(isChecked);
469+ if (!closingTag) {
470+ // Opening tag <li>
471+ // Track checkbox state if we're inside a checkbox list
472+ if (insideCheckboxList) {
473+ BOOL isChecked = [currentTagParams containsString: @" checked" ];
474+ checkboxStates[@(plainText.length)] = @(isChecked);
475+ }
476+ // Record the start location so we can check if it's empty when
477+ // closing
478+ ongoingTags[@" li" ] = @[ @(plainText.length) ];
479+ } else {
480+ // Closing tag </li>
481+ NSArray *tagData = ongoingTags[@" li" ];
482+ if (tagData != nil ) {
483+ NSInteger tagLocation = [((NSNumber *)tagData[0 ]) intValue ];
484+ NSString *innerContent = [plainText substringFromIndex: tagLocation];
485+
486+ // If the li is completely empty (or just contains layout newlines),
487+ // inject ZWS
488+ if ([innerContent
489+ stringByTrimmingCharactersInSet: [NSCharacterSet
490+ newlineCharacterSet ]]
491+ .length == 0 ) {
492+ [plainText appendString: @" \u200B " ];
493+ }
494+ [ongoingTags removeObjectForKey: @" li" ];
495+ }
499496 }
500497 } else if (!closingTag) {
501498 BOOL isPlainParagraph = [currentTagName isEqualToString: @" p" ] &&
@@ -529,6 +526,12 @@ + (NSArray *_Nonnull)getTextAndStylesFromHtml:(NSString *_Nonnull)fixedHtml {
529526 i += 1 ;
530527 }
531528
529+ if ([currentTagName isEqualToString: @" img" ]) {
530+ // Images have no inner text, so we manually break the <br> streak
531+ // here.
532+ lastTagWasBr = NO ;
533+ }
534+
532535 if (isSelfClosing) {
533536 [self finalizeTagEntry: currentTagName
534537 ongoingTags: ongoingTags
@@ -549,12 +552,36 @@ + (NSArray *_Nonnull)getTextAndStylesFromHtml:(NSString *_Nonnull)fixedHtml {
549552
550553 BOOL isBlockTag = [self isBlockTag: currentTagName];
551554
555+ // ZWS logic for blockquote and codeblock
556+ BOOL needsZWS = [currentTagName isEqualToString: @" blockquote" ] ||
557+ [currentTagName isEqualToString: @" codeblock" ];
558+ BOOL isEmptyBlock = NO ;
559+ if (needsZWS) {
560+ NSArray *tagData = ongoingTags[currentTagName];
561+ if (tagData != nil ) {
562+ NSInteger tagLoc = [tagData[0 ] intValue ];
563+ NSString *inner = [plainText substringFromIndex: tagLoc];
564+ if ([inner stringByTrimmingCharactersInSet: [NSCharacterSet
565+ newlineCharacterSet ]]
566+ .length == 0 ) {
567+ isEmptyBlock = YES ;
568+ }
569+ }
570+ }
571+
552572 // skip one newline if it was added before some closing tags that are
553573 // in separate lines
554574 if (isBlockTag && plainText.length > 0 &&
555575 [[NSCharacterSet newlineCharacterSet ]
556576 characterIsMember: [plainText
557577 characterAtIndex: plainText.length - 1 ]]) {
578+
579+ // If the last thing processed was a <br>, or the block is totally
580+ // empty, inject a \u200B before trimming the trailing newline to save
581+ // the empty line.
582+ if (lastTagWasBr || isEmptyBlock) {
583+ [plainText insertString: @" \u200B " atIndex: plainText.length - 1 ];
584+ }
558585 plainText = [[plainText
559586 substringWithRange: NSMakeRange (0 , plainText.length - 1 )]
560587 mutableCopy ];
@@ -589,6 +616,11 @@ + (NSArray *_Nonnull)getTextAndStylesFromHtml:(NSString *_Nonnull)fixedHtml {
589616 i += escaped.length - 1 ;
590617 } else {
591618 [plainText appendString: currentCharacterStr];
619+ // Any typed character that isn't a newline breaks the <br> streak
620+ if (![[NSCharacterSet newlineCharacterSet ]
621+ characterIsMember: currentCharacterChar]) {
622+ lastTagWasBr = NO ;
623+ }
592624 }
593625 } else {
594626 if (gettingTagName) {
0 commit comments