diff --git a/.maestro/enrichedInput/screenshots/android/checkbox_toggle.png b/.maestro/enrichedInput/screenshots/android/checkbox_toggle.png index b3b661a85..2ebc3fd4b 100644 Binary files a/.maestro/enrichedInput/screenshots/android/checkbox_toggle.png and b/.maestro/enrichedInput/screenshots/android/checkbox_toggle.png differ diff --git a/.maestro/enrichedInput/screenshots/android/mention_popup_closing_on_cursor_travel_1.png b/.maestro/enrichedInput/screenshots/android/mention_popup_closing_on_cursor_travel_1.png index 74947bcfd..8fa6291ad 100644 Binary files a/.maestro/enrichedInput/screenshots/android/mention_popup_closing_on_cursor_travel_1.png and b/.maestro/enrichedInput/screenshots/android/mention_popup_closing_on_cursor_travel_1.png differ diff --git a/.maestro/enrichedInput/screenshots/android/mention_popup_closing_on_cursor_travel_2.png b/.maestro/enrichedInput/screenshots/android/mention_popup_closing_on_cursor_travel_2.png index 7a053e6c5..c092fb52d 100644 Binary files a/.maestro/enrichedInput/screenshots/android/mention_popup_closing_on_cursor_travel_2.png and b/.maestro/enrichedInput/screenshots/android/mention_popup_closing_on_cursor_travel_2.png differ diff --git a/.maestro/enrichedInput/screenshots/android/mention_popup_closing_on_cursor_travel_3.png b/.maestro/enrichedInput/screenshots/android/mention_popup_closing_on_cursor_travel_3.png index d80b9a756..cb34c00b1 100644 Binary files a/.maestro/enrichedInput/screenshots/android/mention_popup_closing_on_cursor_travel_3.png and b/.maestro/enrichedInput/screenshots/android/mention_popup_closing_on_cursor_travel_3.png differ diff --git a/.maestro/enrichedInput/screenshots/android/paragraph_styles_lists.png b/.maestro/enrichedInput/screenshots/android/paragraph_styles_lists.png index 7bb6e38db..4a01cdb95 100644 Binary files a/.maestro/enrichedInput/screenshots/android/paragraph_styles_lists.png and b/.maestro/enrichedInput/screenshots/android/paragraph_styles_lists.png differ diff --git a/.maestro/enrichedInput/screenshots/android/paragraph_styles_no_crash.png b/.maestro/enrichedInput/screenshots/android/paragraph_styles_no_crash.png index 3278d7428..ffeb45b81 100644 Binary files a/.maestro/enrichedInput/screenshots/android/paragraph_styles_no_crash.png and b/.maestro/enrichedInput/screenshots/android/paragraph_styles_no_crash.png differ diff --git a/.maestro/enrichedText/screenshots/android/custom_styles_display.png b/.maestro/enrichedText/screenshots/android/custom_styles_display.png index 97f2e6a81..22db51667 100644 Binary files a/.maestro/enrichedText/screenshots/android/custom_styles_display.png and b/.maestro/enrichedText/screenshots/android/custom_styles_display.png differ diff --git a/android/src/main/java/com/swmansion/enriched/common/parser/HtmlWrapperExtractor.kt b/android/src/main/java/com/swmansion/enriched/common/parser/HtmlWrapperExtractor.kt new file mode 100644 index 000000000..13ba5ac5e --- /dev/null +++ b/android/src/main/java/com/swmansion/enriched/common/parser/HtmlWrapperExtractor.kt @@ -0,0 +1,28 @@ +package com.swmansion.enriched.common.parser + +object HtmlWrapperExtractor { + private const val HTML_OPEN = "" + private const val HTML_CLOSE = "" + private const val BODY_OPEN = "
" + private const val BODY_CLOSE = "" + + /** + * Strips the tags and, if there is a pair inside, keeps only its content + * (anything outside of it is dropped). + */ + fun extract(html: String): String { + val trimmed = html.trim() + if (!trimmed.startsWith(HTML_OPEN) || !trimmed.endsWith(HTML_CLOSE)) return html + + val inner = trimmed.substring(HTML_OPEN.length, trimmed.length - HTML_CLOSE.length) + val bodyStart = inner.indexOf(BODY_OPEN) + if (bodyStart == -1) return inner + + val contentStart = bodyStart + BODY_OPEN.length + // only look for the closing tag after the opening one + val bodyEnd = inner.indexOf(BODY_CLOSE, contentStart) + if (bodyEnd == -1) return inner + + return inner.substring(contentStart, bodyEnd) + } +} diff --git a/android/src/main/java/com/swmansion/enriched/text/EnrichedTextView.kt b/android/src/main/java/com/swmansion/enriched/text/EnrichedTextView.kt index 357016da3..021af0083 100644 --- a/android/src/main/java/com/swmansion/enriched/text/EnrichedTextView.kt +++ b/android/src/main/java/com/swmansion/enriched/text/EnrichedTextView.kt @@ -26,6 +26,7 @@ import com.swmansion.enriched.common.EnrichedConstants import com.swmansion.enriched.common.EnrichedSpanFlags import com.swmansion.enriched.common.GumboNormalizer import com.swmansion.enriched.common.parser.EnrichedParser +import com.swmansion.enriched.common.parser.HtmlWrapperExtractor import com.swmansion.enriched.common.pixelFromSpOrDp import com.swmansion.enriched.text.spans.EnrichedTextImageSpan import com.swmansion.enriched.text.spans.interfaces.EnrichedTextClickableSpan @@ -195,44 +196,20 @@ class EnrichedTextView : AppCompatTextView { text: String, style: EnrichedTextStyle, ): CharSequence? { - val isInternalHtml = text.startsWith("") && text.endsWith("") - - if (isInternalHtml) { - try { - val parsed = EnrichedParser.fromHtml(text, style, spannableFactory) - return parsed.trimEnd('\n') - } catch (e: Exception) { - Log.e(TAG, "Error parsing HTML: ${e.message}") - return normalizeHtmlIfNeeded(text, style) - } - } - - return normalizeHtmlIfNeeded(text, style) - } - - private fun normalizeHtmlIfNeeded( - text: String, - style: EnrichedTextStyle, - ): CharSequence? { - if (!useHtmlNormalizer) return null - return parseNormalizedHtml(text, style) - } + var html = HtmlWrapperExtractor.extract(text) + if (useHtmlNormalizer) html = normalizeHtml(html) - private fun parseNormalizedHtml( - text: String, - style: EnrichedTextStyle, - ): CharSequence? { - val normalized = GumboNormalizer.normalizeHtml(text) ?: return null - - return try { - val parsed: Spanned = EnrichedParser.fromHtml(normalized, style, spannableFactory) - parsed.trimEnd('\n') + try { + val parsed = EnrichedParser.fromHtml(html, style, spannableFactory) + return parsed.trimEnd('\n') } catch (e: Exception) { - Log.e(TAG, "Error parsing normalized HTML: ${e.message}") - null + Log.e(TAG, "Error parsing HTML: ${e.message}") + return null } } + private fun normalizeHtml(text: String): String = GumboNormalizer.normalizeHtml(text) ?: text + private fun observeAsyncImages() { val spanned = parsedText as? Spanned ?: return val spans = spanned.getSpans(0, spanned.length, EnrichedTextImageSpan::class.java) diff --git a/android/src/main/java/com/swmansion/enriched/textinput/EnrichedTextInputView.kt b/android/src/main/java/com/swmansion/enriched/textinput/EnrichedTextInputView.kt index e61435f35..a82188280 100644 --- a/android/src/main/java/com/swmansion/enriched/textinput/EnrichedTextInputView.kt +++ b/android/src/main/java/com/swmansion/enriched/textinput/EnrichedTextInputView.kt @@ -42,6 +42,7 @@ import com.swmansion.enriched.common.EnrichedConstants import com.swmansion.enriched.common.EnrichedSpanFlags import com.swmansion.enriched.common.GumboNormalizer import com.swmansion.enriched.common.parser.EnrichedParser +import com.swmansion.enriched.common.parser.HtmlWrapperExtractor import com.swmansion.enriched.common.pixelFromSpOrDp import com.swmansion.enriched.textinput.events.MentionHandler import com.swmansion.enriched.textinput.events.OnContextMenuItemPressEvent @@ -428,33 +429,19 @@ class EnrichedTextInputView : setSelection(selection?.start ?: text?.length ?: 0) } - private fun normalizeHtmlIfNeeded(text: CharSequence): CharSequence { - if (!useHtmlNormalizer) return text - val normalized = GumboNormalizer.normalizeHtml(text.toString()) ?: return text - - return try { - val parsed = EnrichedParser.fromHtml(normalized, htmlStyle, spannableFactory, linkRegex) - parsed.trimEnd('\n') - } catch (e: Exception) { - Log.e(TAG, "Error parsing normalized HTML: ${e.message}") - text - } - } + private fun normalizeHtml(text: String): String = GumboNormalizer.normalizeHtml(text) ?: text private fun parseText(text: CharSequence): CharSequence { - val isInternalHtml = text.startsWith("") && text.endsWith("") + var html = HtmlWrapperExtractor.extract(text.toString()) + if (useHtmlNormalizer) html = normalizeHtml(html) - if (isInternalHtml) { - try { - val parsed = EnrichedParser.fromHtml(text.toString(), htmlStyle, spannableFactory, linkRegex) - return parsed.trimEnd('\n') - } catch (e: Exception) { - Log.e(TAG, "Error parsing HTML: ${e.message}") - return normalizeHtmlIfNeeded(text) - } + try { + val parsed = EnrichedParser.fromHtml(html, htmlStyle, spannableFactory, linkRegex) + return parsed.trimEnd('\n') + } catch (e: Exception) { + Log.e(TAG, "Error parsing HTML: ${e.message}") + return text } - - return normalizeHtmlIfNeeded(text) } fun setValue( diff --git a/cpp/parser/GumboNormalizer.c b/cpp/parser/GumboNormalizer.c index 5a6210f15..368d37eb1 100644 --- a/cpp/parser/GumboNormalizer.c +++ b/cpp/parser/GumboNormalizer.c @@ -985,7 +985,7 @@ static void walk_node(GumboNode *node, buffer_t *out) { /* */
if (strcmp(out_name, "codeblock") == 0) {
- bool wrap = is_purely_inline(node);
+ bool wrap = is_purely_inline(node) && el->children.length > 0;
buffer_append_str(out, " ");
diff --git a/ios/EnrichedTextInputView.mm b/ios/EnrichedTextInputView.mm
index c44d4d9dd..458dfda21 100644
--- a/ios/EnrichedTextInputView.mm
+++ b/ios/EnrichedTextInputView.mm
@@ -2097,7 +2097,9 @@ - (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection {
parseToHtmlFromRange:NSMakeRange(0, textView.textStorage.string.length)
host:self];
NSString *initiallyProcessedHtml = [parser initiallyProcessHtml:currentHtml];
- [parser replaceWholeFromHtml:initiallyProcessedHtml];
+ if (initiallyProcessedHtml != nullptr) {
+ [parser replaceWholeFromHtml:initiallyProcessedHtml];
+ }
textView.selectedRange = prevSelectedRange;
[self anyTextMayHaveBeenModified];
diff --git a/ios/htmlParser/HtmlParser.mm b/ios/htmlParser/HtmlParser.mm
index f710a7411..0d6f84882 100644
--- a/ios/htmlParser/HtmlParser.mm
+++ b/ios/htmlParser/HtmlParser.mm
@@ -212,35 +212,20 @@ + (NSString *_Nullable)initiallyProcessHtml:(NSString *_Nonnull)html
NSString *htmlWithoutSpaces = [self stripExtraWhiteSpacesAndNewlines:html];
NSString *fixedHtml = nullptr;
- if (htmlWithoutSpaces.length >= 13) {
- NSString *firstSix =
- [htmlWithoutSpaces substringWithRange:NSMakeRange(0, 6)];
- NSString *lastSeven = [htmlWithoutSpaces
- substringWithRange:NSMakeRange(htmlWithoutSpaces.length - 7, 7)];
-
- if ([firstSix isEqualToString:@""] &&
- [lastSeven isEqualToString:@""]) {
- // remove html tags, might be with newlines or without them
- fixedHtml = [htmlWithoutSpaces copy];
- // firstly remove newlined html tags if any:
- fixedHtml = [fixedHtml stringByReplacingOccurrencesOfString:@"\n"
- withString:@""];
- fixedHtml = [fixedHtml stringByReplacingOccurrencesOfString:@"\n"
- withString:@""];
- // fallback; remove html tags without their newlines
- fixedHtml = [fixedHtml stringByReplacingOccurrencesOfString:@""
- withString:@""];
- fixedHtml = [fixedHtml stringByReplacingOccurrencesOfString:@""
- withString:@""];
- } else if (useHtmlNormalizer) {
- // External HTML (from Google Docs, Word, web pages, etc.)
- // Run through the Gumbo-based normalizer to convert arbitrary HTML
- // into our canonical tag subset.
- NSString *normalized = [self normalizeExternalHtml:html];
- if (normalized != nil) {
- fixedHtml = normalized;
- }
- }
+ if ([htmlWithoutSpaces hasPrefix:@""] &&
+ [htmlWithoutSpaces hasSuffix:@""]) {
+ // remove html tags, might be with newlines or without them
+ fixedHtml = [htmlWithoutSpaces copy];
+ // firstly remove newlined html tags if any:
+ fixedHtml = [fixedHtml stringByReplacingOccurrencesOfString:@"\n"
+ withString:@""];
+ fixedHtml = [fixedHtml stringByReplacingOccurrencesOfString:@"\n"
+ withString:@""];
+ // fallback; remove html tags without their newlines
+ fixedHtml = [fixedHtml stringByReplacingOccurrencesOfString:@""
+ withString:@""];
+ fixedHtml = [fixedHtml stringByReplacingOccurrencesOfString:@""
+ withString:@""];
// Additionally, try getting the content from between body tags if there are
// some:
@@ -251,13 +236,30 @@ + (NSString *_Nullable)initiallyProcessHtml:(NSString *_Nonnull)html
fixedHtml = [fixedHtml stringByReplacingOccurrencesOfString:@"\n"
withString:@""];
// Then, if there actually are body tags, use the content between them.
- NSRange openingBodyRange = [htmlWithoutSpaces rangeOfString:@"