From 28a430f0216a6868f27778d2033abdc4e7eabd9a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Kacper=20=C5=BB=C3=B3=C5=82kiewski?=
Date: Wed, 8 Apr 2026 07:46:34 +0200
Subject: [PATCH 01/19] feat: add EnrichedStyleHost
---
ios/EnrichedTextInputView.h | 3 +-
ios/EnrichedTextInputView.mm | 167 +----
ios/extensions/LayoutManagerExtension.mm | 104 ++-
ios/htmlParser/HtmlParser.h | 8 +
ios/htmlParser/HtmlParser.mm | 772 +++++++++++++++++++++++
ios/inputParser/InputParser.mm | 765 +---------------------
ios/interfaces/BaseStyleProtocol.h | 2 +-
ios/interfaces/StyleBase.h | 7 +-
ios/interfaces/StyleBase.mm | 47 +-
ios/interfaces/StyleHeaders.h | 1 +
ios/styles/BlockQuoteStyle.mm | 29 +-
ios/styles/BoldStyle.mm | 4 +-
ios/styles/CheckboxListStyle.mm | 34 +-
ios/styles/CodeBlockStyle.mm | 10 +-
ios/styles/H1Style.mm | 4 +-
ios/styles/H2Style.mm | 4 +-
ios/styles/H3Style.mm | 4 +-
ios/styles/H4Style.mm | 4 +-
ios/styles/H5Style.mm | 4 +-
ios/styles/H6Style.mm | 4 +-
ios/styles/HeadingStyleBase.mm | 15 +-
ios/styles/ImageStyle.mm | 48 +-
ios/styles/InlineCodeStyle.mm | 24 +-
ios/styles/ItalicStyle.mm | 4 +-
ios/styles/LinkStyle.mm | 140 ++--
ios/styles/MentionStyle.mm | 123 ++--
ios/styles/OrderedListStyle.mm | 22 +-
ios/styles/StrikethroughStyle.mm | 4 +-
ios/styles/UnderlineStyle.mm | 6 +-
ios/styles/UnorderedListStyle.mm | 22 +-
ios/utils/AttachmentLayoutUtils.h | 24 +
ios/utils/AttachmentLayoutUtils.mm | 143 +++++
ios/utils/OccurenceUtils.h | 16 +-
ios/utils/OccurenceUtils.mm | 42 +-
34 files changed, 1340 insertions(+), 1270 deletions(-)
create mode 100644 ios/htmlParser/HtmlParser.h
create mode 100644 ios/htmlParser/HtmlParser.mm
create mode 100644 ios/utils/AttachmentLayoutUtils.h
create mode 100644 ios/utils/AttachmentLayoutUtils.mm
diff --git a/ios/EnrichedTextInputView.h b/ios/EnrichedTextInputView.h
index 1ea44ad9f..a107e3ce2 100644
--- a/ios/EnrichedTextInputView.h
+++ b/ios/EnrichedTextInputView.h
@@ -1,6 +1,7 @@
#pragma once
#import "AttributesManager.h"
#import "BaseStyleProtocol.h"
+#import "EnrichedStyleHost.h"
#import "InputConfig.h"
#import "InputParser.h"
#import "InputTextView.h"
@@ -15,7 +16,7 @@
NS_ASSUME_NONNULL_BEGIN
@interface EnrichedTextInputView
- : RCTViewComponentView {
+ : RCTViewComponentView {
@public
InputTextView *textView;
@public
diff --git a/ios/EnrichedTextInputView.mm b/ios/EnrichedTextInputView.mm
index 66c22512b..75a84980f 100644
--- a/ios/EnrichedTextInputView.mm
+++ b/ios/EnrichedTextInputView.mm
@@ -1,4 +1,5 @@
#import "EnrichedTextInputView.h"
+#import "AttachmentLayoutUtils.h"
#import "CoreText/CoreText.h"
#import "DotReplacementUtils.h"
#import "ImageAttachment.h"
@@ -104,29 +105,29 @@ - (void)setDefaults {
[[NSMutableDictionary alloc] init];
stylesDict = @{
- @([BoldStyle getType]) : [[BoldStyle alloc] initWithInput:self],
- @([ItalicStyle getType]) : [[ItalicStyle alloc] initWithInput:self],
- @([UnderlineStyle getType]) : [[UnderlineStyle alloc] initWithInput:self],
+ @([BoldStyle getType]) : [[BoldStyle alloc] initWithHost:self],
+ @([ItalicStyle getType]) : [[ItalicStyle alloc] initWithHost:self],
+ @([UnderlineStyle getType]) : [[UnderlineStyle alloc] initWithHost:self],
@([StrikethroughStyle getType]) :
- [[StrikethroughStyle alloc] initWithInput:self],
- @([InlineCodeStyle getType]) : [[InlineCodeStyle alloc] initWithInput:self],
- @([LinkStyle getType]) : [[LinkStyle alloc] initWithInput:self],
- @([MentionStyle getType]) : [[MentionStyle alloc] initWithInput:self],
- @([H1Style getType]) : [[H1Style alloc] initWithInput:self],
- @([H2Style getType]) : [[H2Style alloc] initWithInput:self],
- @([H3Style getType]) : [[H3Style alloc] initWithInput:self],
- @([H4Style getType]) : [[H4Style alloc] initWithInput:self],
- @([H5Style getType]) : [[H5Style alloc] initWithInput:self],
- @([H6Style getType]) : [[H6Style alloc] initWithInput:self],
+ [[StrikethroughStyle alloc] initWithHost:self],
+ @([InlineCodeStyle getType]) : [[InlineCodeStyle alloc] initWithHost:self],
+ @([LinkStyle getType]) : [[LinkStyle alloc] initWithHost:self],
+ @([MentionStyle getType]) : [[MentionStyle alloc] initWithHost:self],
+ @([H1Style getType]) : [[H1Style alloc] initWithHost:self],
+ @([H2Style getType]) : [[H2Style alloc] initWithHost:self],
+ @([H3Style getType]) : [[H3Style alloc] initWithHost:self],
+ @([H4Style getType]) : [[H4Style alloc] initWithHost:self],
+ @([H5Style getType]) : [[H5Style alloc] initWithHost:self],
+ @([H6Style getType]) : [[H6Style alloc] initWithHost:self],
@([UnorderedListStyle getType]) :
- [[UnorderedListStyle alloc] initWithInput:self],
+ [[UnorderedListStyle alloc] initWithHost:self],
@([OrderedListStyle getType]) :
- [[OrderedListStyle alloc] initWithInput:self],
+ [[OrderedListStyle alloc] initWithHost:self],
@([CheckboxListStyle getType]) :
- [[CheckboxListStyle alloc] initWithInput:self],
- @([BlockQuoteStyle getType]) : [[BlockQuoteStyle alloc] initWithInput:self],
- @([CodeBlockStyle getType]) : [[CodeBlockStyle alloc] initWithInput:self],
- @([ImageStyle getType]) : [[ImageStyle alloc] initWithInput:self]
+ [[CheckboxListStyle alloc] initWithHost:self],
+ @([BlockQuoteStyle getType]) : [[BlockQuoteStyle alloc] initWithHost:self],
+ @([CodeBlockStyle getType]) : [[CodeBlockStyle alloc] initWithHost:self],
+ @([ImageStyle getType]) : [[ImageStyle alloc] initWithHost:self]
};
conflictingStyles = [@{
@@ -2205,129 +2206,19 @@ - (void)textStorage:(NSTextStorage *)textStorage
// MARK: - Media attachments delegate
- (void)mediaAttachmentDidUpdate:(NSTextAttachment *)attachment {
- NSTextStorage *storage = textView.textStorage;
- NSRange fullRange = NSMakeRange(0, storage.length);
-
- __block NSRange foundRange = NSMakeRange(NSNotFound, 0);
-
- [storage enumerateAttribute:NSAttachmentAttributeName
- inRange:fullRange
- options:0
- usingBlock:^(id value, NSRange range, BOOL *stop) {
- if (value == attachment) {
- foundRange = range;
- *stop = YES;
- }
- }];
-
- if (foundRange.location == NSNotFound) {
- return;
- }
-
- [storage edited:NSTextStorageEditedAttributes
- range:foundRange
- changeInLength:0];
-
- dispatch_async(dispatch_get_main_queue(), ^{
- [self layoutAttachments];
- });
+ [AttachmentLayoutUtils handleAttachmentUpdate:attachment
+ textView:textView
+ onLayoutBlock:^{
+ [self layoutAttachments];
+ }];
}
// MARK: - Image/GIF Overlay Management
- (void)layoutAttachments {
- NSTextStorage *storage = textView.textStorage;
- NSMutableDictionary *activeAttachmentViews =
- [NSMutableDictionary dictionary];
-
- // Iterate over the entire text to find ImageAttachments
- [storage enumerateAttribute:NSAttachmentAttributeName
- inRange:NSMakeRange(0, storage.length)
- options:0
- usingBlock:^(id value, NSRange range, BOOL *stop) {
- if ([value isKindOfClass:[ImageAttachment class]]) {
- ImageAttachment *attachment = (ImageAttachment *)value;
-
- CGRect rect = [self frameForAttachment:attachment
- atRange:range];
-
- // Get or Create the UIImageView for this specific
- // attachment key
- NSValue *key =
- [NSValue valueWithNonretainedObject:attachment];
- UIImageView *imgView = _attachmentViews[key];
-
- if (!imgView) {
- // It doesn't exist yet, create it
- imgView = [[UIImageView alloc] initWithFrame:rect];
- imgView.contentMode = UIViewContentModeScaleAspectFit;
- imgView.tintColor = [UIColor labelColor];
-
- // Add it directly to the TextView
- [textView addSubview:imgView];
- }
-
- // Update position (in case text moved/scrolled)
- if (!CGRectEqualToRect(imgView.frame, rect)) {
- imgView.frame = rect;
- }
- UIImage *targetImage =
- attachment.storedAnimatedImage ?: attachment.image;
-
- // Only set if different to avoid resetting the animation
- // loop
- if (imgView.image != targetImage) {
- imgView.image = targetImage;
- }
-
- // Ensure it is visible on top
- imgView.hidden = NO;
- [textView bringSubviewToFront:imgView];
-
- activeAttachmentViews[key] = imgView;
- // Remove from the old map so we know it has been claimed
- [_attachmentViews removeObjectForKey:key];
- }
- }];
-
- // Everything remaining in _attachmentViews is dead or off-screen
- for (UIImageView *danglingView in _attachmentViews.allValues) {
- [danglingView removeFromSuperview];
- }
- _attachmentViews = activeAttachmentViews;
-}
-
-- (CGRect)frameForAttachment:(ImageAttachment *)attachment
- atRange:(NSRange)range {
- NSLayoutManager *layoutManager = textView.layoutManager;
- NSTextContainer *textContainer = textView.textContainer;
- NSTextStorage *storage = textView.textStorage;
-
- NSRange glyphRange = [layoutManager glyphRangeForCharacterRange:range
- actualCharacterRange:NULL];
- CGRect glyphRect = [layoutManager boundingRectForGlyphRange:glyphRange
- inTextContainer:textContainer];
-
- CGRect lineRect =
- [layoutManager lineFragmentRectForGlyphAtIndex:glyphRange.location
- effectiveRange:NULL];
- CGSize attachmentSize = attachment.bounds.size;
-
- UIFont *font = [storage attribute:NSFontAttributeName
- atIndex:range.location
- effectiveRange:NULL];
- if (!font) {
- font = [config primaryFont];
- }
-
- // Calculate (Baseline Alignment)
- CGFloat targetY =
- CGRectGetMaxY(lineRect) + font.descender - attachmentSize.height;
- CGRect rect =
- CGRectMake(glyphRect.origin.x + textView.textContainerInset.left,
- targetY + textView.textContainerInset.top,
- attachmentSize.width, attachmentSize.height);
-
- return CGRectIntegral(rect);
+ _attachmentViews =
+ [AttachmentLayoutUtils layoutAttachmentsInTextView:textView
+ config:config
+ existingViews:_attachmentViews];
}
@end
diff --git a/ios/extensions/LayoutManagerExtension.mm b/ios/extensions/LayoutManagerExtension.mm
index 58f4620a3..efd77d0d9 100644
--- a/ios/extensions/LayoutManagerExtension.mm
+++ b/ios/extensions/LayoutManagerExtension.mm
@@ -1,6 +1,6 @@
#import "LayoutManagerExtension.h"
#import "ColorExtension.h"
-#import "EnrichedTextInputView.h"
+#import "EnrichedStyleHost.h"
#import "RangeUtils.h"
#import "StyleHeaders.h"
#import "WeakBox.h"
@@ -49,28 +49,23 @@ - (void)my_drawBackgroundForGlyphRange:(NSRange)glyphRange
atPoint:(CGPoint)origin {
[self my_drawBackgroundForGlyphRange:glyphRange atPoint:origin];
- EnrichedTextInputView *typedInput = (EnrichedTextInputView *)self.input;
- if (typedInput == nullptr) {
+ id host = self.input;
+ if (host == nullptr) {
return;
}
NSRange visibleCharRange = [self characterRangeForGlyphRange:glyphRange
actualGlyphRange:NULL];
- [self drawBlockQuotes:typedInput
- origin:origin
- visibleCharRange:visibleCharRange];
- [self drawLists:typedInput origin:origin visibleCharRange:visibleCharRange];
- [self drawCodeBlocks:typedInput
- origin:origin
- visibleCharRange:visibleCharRange];
+ [self drawBlockQuotes:host origin:origin visibleCharRange:visibleCharRange];
+ [self drawLists:host origin:origin visibleCharRange:visibleCharRange];
+ [self drawCodeBlocks:host origin:origin visibleCharRange:visibleCharRange];
}
-- (void)drawCodeBlocks:(EnrichedTextInputView *)typedInput
+- (void)drawCodeBlocks:(id)host
origin:(CGPoint)origin
visibleCharRange:(NSRange)visibleCharRange {
- CodeBlockStyle *codeBlockStyle =
- typedInput->stylesDict[@([CodeBlockStyle getType])];
+ CodeBlockStyle *codeBlockStyle = host.stylesDict[@([CodeBlockStyle getType])];
if (codeBlockStyle == nullptr) {
return;
}
@@ -78,9 +73,9 @@ - (void)drawCodeBlocks:(EnrichedTextInputView *)typedInput
NSArray *allCodeBlocks = [codeBlockStyle all:visibleCharRange];
NSArray *mergedCodeBlocks =
[self mergeContiguousStylePairs:allCodeBlocks];
- UIColor *bgColor = [[typedInput->config codeBlockBgColor]
- colorWithAlphaIfNotTransparent:0.4];
- CGFloat radius = [typedInput->config codeBlockBorderRadius];
+ UIColor *bgColor =
+ [[host.config codeBlockBgColor] colorWithAlphaIfNotTransparent:0.4];
+ CGFloat radius = [host.config codeBlockBorderRadius];
[bgColor setFill];
for (StylePair *pair in mergedCodeBlocks) {
@@ -89,7 +84,7 @@ - (void)drawCodeBlocks:(EnrichedTextInputView *)typedInput
continue;
NSArray *paragraphs =
- [RangeUtils getSeparateParagraphsRangesIn:typedInput->textView
+ [RangeUtils getSeparateParagraphsRangesIn:host.textView
range:blockCharacterRange];
if (paragraphs.count == 0)
continue;
@@ -204,11 +199,10 @@ - (void)drawCodeBlocks:(EnrichedTextInputView *)typedInput
return mergedPairs;
}
-- (void)drawBlockQuotes:(EnrichedTextInputView *)typedInput
+- (void)drawBlockQuotes:(id)host
origin:(CGPoint)origin
visibleCharRange:(NSRange)visibleCharRange {
- BlockQuoteStyle *bqStyle =
- typedInput->stylesDict[@([BlockQuoteStyle getType])];
+ BlockQuoteStyle *bqStyle = host.stylesDict[@([BlockQuoteStyle getType])];
if (bqStyle == nullptr) {
return;
}
@@ -216,7 +210,7 @@ - (void)drawBlockQuotes:(EnrichedTextInputView *)typedInput
NSArray *allBlockquotes = [bqStyle all:visibleCharRange];
for (StylePair *pair in allBlockquotes) {
- NSRange paragraphRange = [typedInput->textView.textStorage.string
+ NSRange paragraphRange = [host.textView.textStorage.string
paragraphRangeForRange:[pair.rangeValue rangeValue]];
NSRange paragraphGlyphRange =
[self glyphRangeForCharacterRange:paragraphRange
@@ -232,28 +226,25 @@ - (void)drawBlockQuotes:(EnrichedTextInputView *)typedInput
CGFloat x = paddingLeft;
CGFloat y = paddingTop + rect.origin.y;
CGFloat width =
- [typedInput
- ->config blockquoteBorderWidth];
+ [host.config blockquoteBorderWidth];
CGFloat height = rect.size.height;
CGRect lineRect =
CGRectMake(x, y, width, height);
- [[typedInput->config blockquoteBorderColor]
+ [[host.config blockquoteBorderColor]
setFill];
UIRectFill(lineRect);
}];
}
}
-- (void)drawLists:(EnrichedTextInputView *)typedInput
+- (void)drawLists:(id)host
origin:(CGPoint)origin
visibleCharRange:(NSRange)visibleCharRange {
UnorderedListStyle *ulStyle =
- typedInput->stylesDict[@([UnorderedListStyle getType])];
- OrderedListStyle *olStyle =
- typedInput->stylesDict[@([OrderedListStyle getType])];
- CheckboxListStyle *cbStyle =
- typedInput->stylesDict[@([CheckboxListStyle getType])];
+ host.stylesDict[@([UnorderedListStyle getType])];
+ OrderedListStyle *olStyle = host.stylesDict[@([OrderedListStyle getType])];
+ CheckboxListStyle *cbStyle = host.stylesDict[@([CheckboxListStyle getType])];
if (ulStyle == nullptr || olStyle == nullptr || cbStyle == nullptr) {
return;
}
@@ -267,13 +258,12 @@ - (void)drawLists:(EnrichedTextInputView *)typedInput
for (StylePair *pair in allLists) {
NSParagraphStyle *pStyle = (NSParagraphStyle *)pair.styleValue;
NSDictionary *markerAttributes = @{
- NSFontAttributeName : [typedInput->config orderedListMarkerFont],
- NSForegroundColorAttributeName :
- [typedInput->config orderedListMarkerColor]
+ NSFontAttributeName : [host.config orderedListMarkerFont],
+ NSForegroundColorAttributeName : [host.config orderedListMarkerColor]
};
NSArray *paragraphs =
- [RangeUtils getSeparateParagraphsRangesIn:typedInput->textView
+ [RangeUtils getSeparateParagraphsRangesIn:host.textView
range:[pair.rangeValue rangeValue]];
for (NSValue *paragraph in paragraphs) {
@@ -290,11 +280,10 @@ - (void)drawLists:(EnrichedTextInputView *)typedInput
NSUInteger charIdx =
[self characterIndexForGlyphAtIndex:
lineGlyphRange.location];
- UIFont *font =
- [typedInput->textView.textStorage
- attribute:NSFontAttributeName
- atIndex:charIdx
- effectiveRange:nil];
+ UIFont *font = [host.textView.textStorage
+ attribute:NSFontAttributeName
+ atIndex:charIdx
+ effectiveRange:nil];
CGRect textUsedRect =
[self getTextAlignedUsedRect:usedRect
font:font];
@@ -307,9 +296,9 @@ - (void)drawLists:(EnrichedTextInputView *)typedInput
isEqualToString:
@"EnrichedOrderedList"]) {
NSString *marker = [self
- getDecimalMarkerForList:typedInput
+ getDecimalMarkerForList:host
charIndex:charIdx];
- [self drawDecimal:typedInput
+ [self drawDecimal:host
marker:marker
markerAttributes:markerAttributes
origin:origin
@@ -318,13 +307,13 @@ - (void)drawLists:(EnrichedTextInputView *)typedInput
isEqualToString:
@"EnrichedUnorderedLis"
@"t"]) {
- [self drawBullet:typedInput
+ [self drawBullet:host
origin:origin
usedRect:textUsedRect];
} else if ([markerFormat
hasPrefix:
@"EnrichedCheckbox"]) {
- [self drawCheckbox:typedInput
+ [self drawCheckbox:host
markerFormat:markerFormat
origin:origin
usedRect:textUsedRect];
@@ -337,16 +326,15 @@ - (void)drawLists:(EnrichedTextInputView *)typedInput
}
}
-- (NSString *)getDecimalMarkerForList:(EnrichedTextInputView *)input
+- (NSString *)getDecimalMarkerForList:(id)host
charIndex:(NSUInteger)index {
- NSString *fullText = input->textView.textStorage.string;
+ NSString *fullText = host.textView.textStorage.string;
NSInteger itemNumber = 1;
NSRange currentParagraph =
[fullText paragraphRangeForRange:NSMakeRange(index, 0)];
if (currentParagraph.location > 0) {
- OrderedListStyle *olStyle =
- input->stylesDict[@([OrderedListStyle getType])];
+ OrderedListStyle *olStyle = host.stylesDict[@([OrderedListStyle getType])];
NSInteger prevParagraphsCount = 0;
NSInteger recentParagraphLocation =
@@ -392,16 +380,16 @@ - (CGRect)getTextAlignedUsedRect:(CGRect)usedRect font:(UIFont *)font {
return usedRect;
}
-- (void)drawCheckbox:(EnrichedTextInputView *)typedInput
+- (void)drawCheckbox:(id)host
markerFormat:(NSString *)markerFormat
origin:(CGPoint)origin
usedRect:(CGRect)usedRect {
BOOL isChecked = [markerFormat isEqualToString:@"EnrichedCheckbox1"];
- UIImage *image = isChecked ? typedInput->config.checkboxCheckedImage
- : typedInput->config.checkboxUncheckedImage;
- CGFloat gapWidth = [typedInput->config checkboxListGapWidth];
- CGFloat boxSize = [typedInput->config checkboxListBoxSize];
+ UIImage *image = isChecked ? host.config.checkboxCheckedImage
+ : host.config.checkboxUncheckedImage;
+ CGFloat gapWidth = [host.config checkboxListGapWidth];
+ CGFloat boxSize = [host.config checkboxListBoxSize];
CGFloat centerY = CGRectGetMidY(usedRect) + origin.y;
CGFloat boxX = origin.x + usedRect.origin.x - gapWidth - boxSize;
@@ -410,18 +398,18 @@ - (void)drawCheckbox:(EnrichedTextInputView *)typedInput
[image drawAtPoint:CGPointMake(boxX, boxY)];
}
-- (void)drawBullet:(EnrichedTextInputView *)typedInput
+- (void)drawBullet:(id)host
origin:(CGPoint)origin
usedRect:(CGRect)usedRect {
- CGFloat gapWidth = [typedInput->config unorderedListGapWidth];
- CGFloat bulletSize = [typedInput->config unorderedListBulletSize];
+ CGFloat gapWidth = [host.config unorderedListGapWidth];
+ CGFloat bulletSize = [host.config unorderedListBulletSize];
CGFloat bulletX = origin.x + usedRect.origin.x - gapWidth - bulletSize / 2;
CGFloat centerY = CGRectGetMidY(usedRect) + origin.y;
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSaveGState(context);
{
- [[typedInput->config unorderedListBulletColor] setFill];
+ [[host.config unorderedListBulletColor] setFill];
CGContextAddArc(context, bulletX, centerY, bulletSize / 2, 0, 2 * M_PI,
YES);
CGContextFillPath(context);
@@ -429,12 +417,12 @@ - (void)drawBullet:(EnrichedTextInputView *)typedInput
CGContextRestoreGState(context);
}
-- (void)drawDecimal:(EnrichedTextInputView *)typedInput
+- (void)drawDecimal:(id)host
marker:(NSString *)marker
markerAttributes:(NSDictionary *)markerAttributes
origin:(CGPoint)origin
usedRect:(CGRect)usedRect {
- CGFloat gapWidth = [typedInput->config orderedListGapWidth];
+ CGFloat gapWidth = [host.config orderedListGapWidth];
CGSize markerSize = [marker sizeWithAttributes:markerAttributes];
CGFloat markerX = usedRect.origin.x - gapWidth - markerSize.width / 2;
CGFloat centerY = CGRectGetMidY(usedRect) + origin.y;
diff --git a/ios/htmlParser/HtmlParser.h b/ios/htmlParser/HtmlParser.h
new file mode 100644
index 000000000..7453828c4
--- /dev/null
+++ b/ios/htmlParser/HtmlParser.h
@@ -0,0 +1,8 @@
+#pragma once
+#import
+
+@interface HtmlParser : NSObject
++ (NSString *_Nullable)initiallyProcessHtml:(NSString *_Nonnull)html
+ useHtmlNormalizer:(BOOL)useHtmlNormalizer;
++ (NSArray *_Nonnull)getTextAndStylesFromHtml:(NSString *_Nonnull)fixedHtml;
+@end
diff --git a/ios/htmlParser/HtmlParser.mm b/ios/htmlParser/HtmlParser.mm
new file mode 100644
index 000000000..a315ffffe
--- /dev/null
+++ b/ios/htmlParser/HtmlParser.mm
@@ -0,0 +1,772 @@
+#import "HtmlParser.h"
+#import "ImageData.h"
+#import "LinkData.h"
+#import "MentionParams.h"
+#import "StringExtension.h"
+#import "StyleHeaders.h"
+#import "StylePair.h"
+
+#include "GumboParser.hpp"
+
+@implementation HtmlParser
+
++ (BOOL)isBlockTag:(NSString *)tagName {
+ return [tagName isEqualToString:@"ul"] || [tagName isEqualToString:@"ol"] ||
+ [tagName isEqualToString:@"blockquote"] ||
+ [tagName isEqualToString:@"codeblock"];
+}
+
+/**
+ * Prepares HTML for the parser by stripping extraneous whitespace and newlines
+ * from structural tags, while preserving them within text content.
+ *
+ * APPROACH:
+ * This function treats the HTML as having two distinct states:
+ * 1. Structure Mode (Depth == 0): We are inside or between container tags (like
+ * blockquote, ul, codeblock). In this mode whitespace and newlines are
+ * considered layout artifacts and are REMOVED to prevent the parser from
+ * creating unwanted spaces.
+ * 2. Content Mode (Depth > 0): We are inside a text-containing tag (like p,
+ * b, li). In this mode, all whitespace is PRESERVED exactly as is, ensuring
+ * that sentences and inline formatting remain readable.
+ *
+ * The function iterates character-by-character, using a depth counter to track
+ * nesting levels of the specific tags defined in `textTags`.
+ *
+ * IMPORTANT:
+ * The `textTags` set acts as a whitelist for "Content Mode". If you add support
+ * for a new HTML tag that contains visible text (e.g., h4, h5, h6),
+ * you MUST add it to the `textTags` set below.
+ */
+- (NSString *)stripExtraWhiteSpacesAndNewlines:(NSString *)html {
+ NSSet *textTags = [NSSet setWithObjects:@"p", @"h1", @"h2", @"h3", @"h4",
+ @"h5", @"h6", @"li", @"b", @"a", @"s",
+ @"mention", @"code", @"u", @"i", nil];
+
+ NSMutableString *output = [NSMutableString stringWithCapacity:html.length];
+ NSMutableString *currentTagBuffer = [NSMutableString string];
+ NSCharacterSet *whitespaceAndNewlineSet =
+ [NSCharacterSet whitespaceAndNewlineCharacterSet];
+
+ BOOL isReadingTag = NO;
+ NSInteger textDepth = 0;
+
+ for (NSUInteger i = 0; i < html.length; i++) {
+ unichar c = [html characterAtIndex:i];
+
+ if (c == '<') {
+ isReadingTag = YES;
+ [currentTagBuffer setString:@""];
+ [output appendString:@"<"];
+ } else if (c == '>') {
+ isReadingTag = NO;
+ [output appendString:@">"];
+
+ NSString *fullTag = [currentTagBuffer lowercaseString];
+
+ NSString *cleanName = [fullTag
+ stringByTrimmingCharactersInSet:
+ [NSCharacterSet characterSetWithCharactersInString:@"/"]];
+ NSArray *parts =
+ [cleanName componentsSeparatedByCharactersInSet:
+ [NSCharacterSet whitespaceAndNewlineCharacterSet]];
+ NSString *tagName = parts.firstObject;
+
+ if (![textTags containsObject:tagName]) {
+ continue;
+ }
+
+ if ([fullTag hasPrefix:@"/"]) {
+ textDepth--;
+ if (textDepth < 0)
+ textDepth = 0;
+ } else {
+ // Opening tag (e.g. ) -> Enter Text Mode
+ // (Ignore self-closing tags like
if they happen to be in the
+ // list)
+ if (![fullTag hasSuffix:@"/"]) {
+ textDepth++;
+ }
+ }
+ } else {
+ if (isReadingTag) {
+ [currentTagBuffer appendFormat:@"%C", c];
+ [output appendFormat:@"%C", c];
+ continue;
+ }
+
+ if (textDepth > 0) {
+ [output appendFormat:@"%C", c];
+ } else {
+ if (![whitespaceAndNewlineSet characterIsMember:c]) {
+ [output appendFormat:@"%C", c];
+ }
+ }
+ }
+ }
+
+ return output;
+}
+
+- (NSString *)stringByAddingNewlinesToTag:(NSString *)tag
+ inString:(NSString *)html
+ leading:(BOOL)leading
+ trailing:(BOOL)trailing {
+ NSString *str = [html copy];
+ if (leading) {
+ NSString *formattedTag = [NSString stringWithFormat:@">%@", tag];
+ NSString *formattedNewTag = [NSString stringWithFormat:@">\n%@", tag];
+ str = [str stringByReplacingOccurrencesOfString:formattedTag
+ withString:formattedNewTag];
+ }
+ if (trailing) {
+ NSString *formattedTag = [NSString stringWithFormat:@"%@<", tag];
+ NSString *formattedNewTag = [NSString stringWithFormat:@"%@\n<", tag];
+ str = [str stringByReplacingOccurrencesOfString:formattedTag
+ withString:formattedNewTag];
+ }
+ return str;
+}
+
+#pragma mark - External HTML normalization
+
+/**
+ * Normalizes external HTML (from Google Docs, Word, web pages, etc.) into our
+ * canonical tag subset using the Gumbo-based C++ normalizer.
+ *
+ * Converts: strong → b, em → i, span style="font-weight:bold" → b,
+ * strips unknown tags while preserving text
+ */
++ (NSString *_Nullable)normalizeExternalHtml:(NSString *_Nonnull)html {
+ std::string result =
+ GumboParser::normalizeHtml(std::string([html UTF8String]));
+ if (result.empty())
+ return nil;
+ return [NSString stringWithUTF8String:result.c_str()];
+}
+
++ (void)finalizeTagEntry:(NSMutableString *)tagName
+ ongoingTags:(NSMutableDictionary *)ongoingTags
+ initiallyProcessedTags:(NSMutableArray *)processedTags
+ plainText:(NSMutableString *)plainText
+ precedingImageCount:(NSInteger *)precedingImageCount {
+ NSMutableArray *tagEntry = [[NSMutableArray alloc] init];
+
+ NSArray *tagData = ongoingTags[tagName];
+ NSInteger tagLocation = [((NSNumber *)tagData[0]) intValue];
+
+ // 'tagLocation' is an index based on 'plainText' which currently only holds
+ // raw text.
+ //
+ // Since 'plainText' does not yet contain the special placeholders for images,
+ // the indices for any text following an image are lower than they will be
+ // in the final NSTextStorage.
+ //
+ // We add 'precedingImageCount' to shift the start index forward, aligning
+ // this style's range with the actual position in the final text (where each
+ // image adds 1 character).
+ NSRange tagRange = NSMakeRange(tagLocation + *precedingImageCount,
+ plainText.length - tagLocation);
+
+ [tagEntry addObject:[tagName copy]];
+ [tagEntry addObject:[NSValue valueWithRange:tagRange]];
+ if (tagData.count > 1) {
+ [tagEntry addObject:[(NSString *)tagData[1] copy]];
+ }
+
+ [processedTags addObject:tagEntry];
+ [ongoingTags removeObjectForKey:tagName];
+
+ if ([tagName isEqualToString:@"img"]) {
+ (*precedingImageCount)++;
+ }
+}
+
++ (BOOL)isUlCheckboxList:(NSString *)params {
+ return ([params containsString:@"data-type=\"checkbox\""] ||
+ [params containsString:@"data-type='checkbox'"]);
+}
+
++ (NSDictionary *)prepareCheckboxListStyleValue:(NSValue *)rangeValue
+ checkboxStates:(NSDictionary *)checkboxStates {
+ NSRange range = [rangeValue rangeValue];
+ NSMutableDictionary *statesInRange = [[NSMutableDictionary alloc] init];
+
+ for (NSNumber *key in checkboxStates) {
+ NSUInteger pos = [key unsignedIntegerValue];
+ if (pos >= range.location && pos < range.location + range.length) {
+ [statesInRange setObject:checkboxStates[key] forKey:key];
+ }
+ }
+
+ return statesInRange;
+}
+
++ (NSString *_Nullable)initiallyProcessHtml:(NSString *_Nonnull)html
+ useHtmlNormalizer:(BOOL)useHtmlNormalizer {
+ 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 (_input->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;
+ }
+ }
+
+ // Additionally, try getting the content from between body tags if there are
+ // some:
+
+ // Firstly make sure there are no newlines between them.
+ fixedHtml = [fixedHtml stringByReplacingOccurrencesOfString:@"\n"
+ withString:@""];
+ fixedHtml = [fixedHtml stringByReplacingOccurrencesOfString:@"\n"
+ withString:@""];
+ // Then, if there actually are body tags, use the content between them.
+ NSRange openingBodyRange = [htmlWithoutSpaces rangeOfString:@""];
+ NSRange closingBodyRange = [htmlWithoutSpaces rangeOfString:@""];
+ if (openingBodyRange.length != 0 && closingBodyRange.length != 0) {
+ NSInteger newStart = openingBodyRange.location + 6;
+ NSInteger newEnd = closingBodyRange.location - 1;
+ fixedHtml = [htmlWithoutSpaces
+ substringWithRange:NSMakeRange(newStart, newEnd - newStart + 1)];
+ }
+ }
+
+ // second processing - try fixing htmls with wrong newlines' setup
+ if (fixedHtml != nullptr) {
+ // add
tag wherever needed
+ fixedHtml = [fixedHtml stringByReplacingOccurrencesOfString:@"
"
+ withString:@"
"];
+
+ // remove tags inside of
+ fixedHtml = [fixedHtml stringByReplacingOccurrencesOfString:@""
+ withString:@"
"];
+ fixedHtml = [fixedHtml stringByReplacingOccurrencesOfString:@"
"
+ withString:@""];
+
+ // change
to
+ fixedHtml = [fixedHtml stringByReplacingOccurrencesOfString:@"
"
+ withString:@"
"];
+
+ // remove tags around
+ fixedHtml = [fixedHtml stringByReplacingOccurrencesOfString:@"
"
+ withString:@"
"];
+ fixedHtml = [fixedHtml stringByReplacingOccurrencesOfString:@"
"
+ withString:@"
"];
+
+ // add
tags inside empty blockquote and codeblock tags
+ fixedHtml = [fixedHtml
+ stringByReplacingOccurrencesOfString:@""
+ withString:@"
"
+ @"blockquote>"];
+ fixedHtml = [fixedHtml
+ stringByReplacingOccurrencesOfString:@""
+ withString:@"
"];
+
+ // remove empty ul and ol tags
+ fixedHtml = [fixedHtml stringByReplacingOccurrencesOfString:@""
+ withString:@""];
+ fixedHtml = [fixedHtml
+ stringByReplacingOccurrencesOfString:@""
+ withString:@""];
+ fixedHtml = [fixedHtml stringByReplacingOccurrencesOfString:@"
"
+ withString:@""];
+
+ // tags that have to be in separate lines
+ fixedHtml = [self stringByAddingNewlinesToTag:@"
"
+ inString:fixedHtml
+ leading:YES
+ trailing:YES];
+ fixedHtml = [self stringByAddingNewlinesToTag:@""
+ inString:fixedHtml
+ leading:YES
+ trailing:YES];
+ fixedHtml = [self stringByAddingNewlinesToTag:@"
"
+ inString:fixedHtml
+ leading:YES
+ trailing:YES];
+ fixedHtml = [self stringByAddingNewlinesToTag:@""
+ inString:fixedHtml
+ leading:YES
+ trailing:YES];
+ fixedHtml = [self stringByAddingNewlinesToTag:@"
"
+ inString:fixedHtml
+ leading:YES
+ trailing:YES];
+ fixedHtml = [self stringByAddingNewlinesToTag:@""
+ inString:fixedHtml
+ leading:YES
+ trailing:YES];
+ fixedHtml = [self stringByAddingNewlinesToTag:@"
"
+ inString:fixedHtml
+ leading:YES
+ trailing:YES];
+ fixedHtml = [self stringByAddingNewlinesToTag:@""
+ inString:fixedHtml
+ leading:YES
+ trailing:YES];
+ fixedHtml = [self stringByAddingNewlinesToTag:@""
+ inString:fixedHtml
+ leading:YES
+ trailing:YES];
+
+ // line opening tags
+ fixedHtml = [self stringByAddingNewlinesToTag:@""
+ inString:fixedHtml
+ leading:YES
+ trailing:NO];
+ fixedHtml = [self stringByAddingNewlinesToTag:@"
"
+ inString:fixedHtml
+ leading:YES
+ trailing:NO];
+ fixedHtml = [self stringByAddingNewlinesToTag:@""
+ inString:fixedHtml
+ leading:YES
+ trailing:NO];
+ fixedHtml = [self stringByAddingNewlinesToTag:@""
+ inString:fixedHtml
+ leading:YES
+ trailing:NO];
+ fixedHtml = [self stringByAddingNewlinesToTag:@""
+ inString:fixedHtml
+ leading:YES
+ trailing:NO];
+ fixedHtml = [self stringByAddingNewlinesToTag:@""
+ inString:fixedHtml
+ leading:YES
+ trailing:NO];
+ fixedHtml = [self stringByAddingNewlinesToTag:@""
+ inString:fixedHtml
+ leading:YES
+ trailing:NO];
+ fixedHtml = [self stringByAddingNewlinesToTag:@""
+ inString:fixedHtml
+ leading:YES
+ trailing:NO];
+ fixedHtml = [self stringByAddingNewlinesToTag:@""
+ inString:fixedHtml
+ leading:YES
+ trailing:NO];
+
+ // line closing tags
+ fixedHtml = [self stringByAddingNewlinesToTag:@""
+ inString:fixedHtml
+ leading:NO
+ trailing:YES];
+ fixedHtml = [self stringByAddingNewlinesToTag:@"
"
+ inString:fixedHtml
+ leading:NO
+ trailing:YES];
+ fixedHtml = [self stringByAddingNewlinesToTag:@""
+ inString:fixedHtml
+ leading:NO
+ trailing:YES];
+ fixedHtml = [self stringByAddingNewlinesToTag:@""
+ inString:fixedHtml
+ leading:NO
+ trailing:YES];
+ fixedHtml = [self stringByAddingNewlinesToTag:@""
+ inString:fixedHtml
+ leading:NO
+ trailing:YES];
+ fixedHtml = [self stringByAddingNewlinesToTag:@""
+ inString:fixedHtml
+ leading:NO
+ trailing:YES];
+ fixedHtml = [self stringByAddingNewlinesToTag:@""
+ inString:fixedHtml
+ leading:NO
+ trailing:YES];
+ fixedHtml = [self stringByAddingNewlinesToTag:@""
+ 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"];
+
+ // 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;
+}
+
++ (NSArray *_Nonnull)getTextAndStylesFromHtml:(NSString *_Nonnull)fixedHtml {
+ NSMutableString *plainText = [[NSMutableString alloc] initWithString:@""];
+ NSMutableDictionary *ongoingTags = [[NSMutableDictionary alloc] init];
+ NSMutableArray *initiallyProcessedTags = [[NSMutableArray alloc] init];
+ NSMutableDictionary *checkboxStates = [[NSMutableDictionary alloc] init];
+ BOOL insideCheckboxList = NO;
+ NSInteger precedingImageCount = 0;
+ BOOL insideTag = NO;
+ BOOL gettingTagName = NO;
+ BOOL gettingTagParams = NO;
+ 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++) {
+ NSString *currentCharacterStr =
+ [fixedHtml substringWithRange:NSMakeRange(i, 1)];
+ unichar currentCharacterChar = [fixedHtml characterAtIndex:i];
+
+ if (currentCharacterChar == '<') {
+ // opening the tag, mark that we are inside and getting its name
+ insideTag = YES;
+ gettingTagName = YES;
+ } else if (currentCharacterChar == '>') {
+ // finishing some tag, no longer marked as inside or getting its
+ // name/params
+ insideTag = NO;
+ gettingTagName = NO;
+ gettingTagParams = NO;
+
+ BOOL isSelfClosing = NO;
+
+ // Check if params ended with '/' (e.g.
)
+ if ([currentTagParams hasSuffix:@"/"]) {
+ [currentTagParams
+ deleteCharactersInRange:NSMakeRange(currentTagParams.length - 1,
+ 1)];
+ isSelfClosing = YES;
+ }
+
+ if ([currentTagName isEqualToString:@"p"] ||
+ [currentTagName isEqualToString:@"br"]) {
+ // 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);
+ }
+ } 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 numberWithInteger:plainText.length]];
+ 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;
+ }
+
+ // 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 (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
+ // params into an entry that goes inside initiallyProcessedTags
+
+ // Check if we're closing a checkbox list by looking at the params
+ if ([currentTagName isEqualToString:@"ul"] &&
+ [self isUlCheckboxList:currentTagParams]) {
+ insideCheckboxList = NO;
+ }
+
+ BOOL isBlockTag = [self isBlockTag:currentTagName];
+
+ // 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]]) {
+ plainText = [[plainText
+ substringWithRange:NSMakeRange(0, plainText.length - 1)]
+ mutableCopy];
+ }
+
+ [self finalizeTagEntry:currentTagName
+ ongoingTags:ongoingTags
+ initiallyProcessedTags:initiallyProcessedTags
+ plainText:plainText
+ precedingImageCount:&precedingImageCount];
+ }
+ // post-tag cleanup
+ closingTag = NO;
+ currentTagName = [[NSMutableString alloc] initWithString:@""];
+ currentTagParams = [[NSMutableString alloc] initWithString:@""];
+ } else {
+ if (!insideTag) {
+ // 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 == ' ') {
+ // no longer getting tag name - switch to params
+ gettingTagName = NO;
+ gettingTagParams = YES;
+ } else if (currentCharacterChar == '/') {
+ // mark that the tag is closing
+ closingTag = YES;
+ } else {
+ // append next tag char
+ [currentTagName appendString:currentCharacterStr];
+ }
+ } else if (gettingTagParams) {
+ // append next tag params char
+ [currentTagParams appendString:currentCharacterStr];
+ }
+ }
+ }
+ }
+
+ // process tags into proper StyleType + StylePair values
+ NSMutableArray *processedStyles = [[NSMutableArray alloc] init];
+
+ for (NSArray *arr in initiallyProcessedTags) {
+ NSString *tagName = (NSString *)arr[0];
+ NSValue *tagRangeValue = (NSValue *)arr[1];
+ NSMutableString *params = [[NSMutableString alloc] initWithString:@""];
+ if (arr.count > 2) {
+ [params appendString:(NSString *)arr[2]];
+ }
+
+ NSMutableArray *styleArr = [[NSMutableArray alloc] init];
+ StylePair *stylePair = [[StylePair alloc] init];
+
+ if ([tagName isEqualToString:@"b"]) {
+ [styleArr addObject:@([BoldStyle getType])];
+ } else if ([tagName isEqualToString:@"i"]) {
+ [styleArr addObject:@([ItalicStyle getType])];
+ } else if ([tagName isEqualToString:@"img"]) {
+ NSRegularExpression *srcRegex =
+ [NSRegularExpression regularExpressionWithPattern:@"src=\"([^\"]+)\""
+ options:0
+ error:nullptr];
+ NSTextCheckingResult *match =
+ [srcRegex firstMatchInString:params
+ options:0
+ range:NSMakeRange(0, params.length)];
+
+ if (match == nullptr) {
+ continue;
+ }
+
+ NSRange srcRange = match.range;
+ [styleArr addObject:@([ImageStyle getType])];
+ // cut only the uri from the src="..." string
+ NSString *uri =
+ [params substringWithRange:NSMakeRange(srcRange.location + 5,
+ srcRange.length - 6)];
+ ImageData *imageData = [[ImageData alloc] init];
+ imageData.uri = uri;
+
+ NSRegularExpression *widthRegex = [NSRegularExpression
+ regularExpressionWithPattern:@"width=\"([0-9.]+)\""
+ options:0
+ error:nil];
+ NSTextCheckingResult *widthMatch =
+ [widthRegex firstMatchInString:params
+ options:0
+ range:NSMakeRange(0, params.length)];
+
+ if (widthMatch) {
+ NSString *widthString =
+ [params substringWithRange:[widthMatch rangeAtIndex:1]];
+ imageData.width = [widthString floatValue];
+ }
+
+ NSRegularExpression *heightRegex = [NSRegularExpression
+ regularExpressionWithPattern:@"height=\"([0-9.]+)\""
+ options:0
+ error:nil];
+ NSTextCheckingResult *heightMatch =
+ [heightRegex firstMatchInString:params
+ options:0
+ range:NSMakeRange(0, params.length)];
+
+ if (heightMatch) {
+ NSString *heightString =
+ [params substringWithRange:[heightMatch rangeAtIndex:1]];
+ imageData.height = [heightString floatValue];
+ }
+
+ stylePair.styleValue = imageData;
+ } else if ([tagName isEqualToString:@"u"]) {
+ [styleArr addObject:@([UnderlineStyle getType])];
+ } else if ([tagName isEqualToString:@"s"]) {
+ [styleArr addObject:@([StrikethroughStyle getType])];
+ } else if ([tagName isEqualToString:@"code"]) {
+ [styleArr addObject:@([InlineCodeStyle getType])];
+ } else if ([tagName isEqualToString:@"a"]) {
+ NSRegularExpression *hrefRegex =
+ [NSRegularExpression regularExpressionWithPattern:@"href=\".+\""
+ options:0
+ error:nullptr];
+ NSTextCheckingResult *match =
+ [hrefRegex firstMatchInString:params
+ options:0
+ range:NSMakeRange(0, params.length)];
+
+ if (match == nullptr) {
+ // same as on Android, no href (or empty href) equals no link style
+ continue;
+ }
+
+ NSRange hrefRange = match.range;
+ [styleArr addObject:@([LinkStyle getType])];
+ // cut only the url from the href="..." string
+ NSString *url =
+ [params substringWithRange:NSMakeRange(hrefRange.location + 6,
+ hrefRange.length - 7)];
+ NSString *text = [plainText substringWithRange:tagRangeValue.rangeValue];
+
+ LinkData *linkData = [[LinkData alloc] init];
+ linkData.url = url;
+ linkData.text = text;
+ linkData.isManual = ![text isEqualToString:url];
+
+ stylePair.styleValue = linkData;
+ } else if ([tagName isEqualToString:@"mention"]) {
+ [styleArr addObject:@([MentionStyle getType])];
+ // extract html expression into dict using some regex
+ NSMutableDictionary *paramsDict = [[NSMutableDictionary alloc] init];
+ NSString *pattern = @"(\\w+)=(['\"])(.*?)\\2";
+ NSRegularExpression *regex =
+ [NSRegularExpression regularExpressionWithPattern:pattern
+ options:0
+ error:nil];
+
+ [regex enumerateMatchesInString:params
+ options:0
+ range:NSMakeRange(0, params.length)
+ usingBlock:^(NSTextCheckingResult *_Nullable result,
+ NSMatchingFlags flags,
+ BOOL *_Nonnull stop) {
+ if (result.numberOfRanges == 4) {
+ NSString *key = [params
+ substringWithRange:[result rangeAtIndex:1]];
+ NSString *value = [params
+ substringWithRange:[result rangeAtIndex:3]];
+ paramsDict[key] = value;
+ }
+ }];
+
+ MentionParams *mentionParams = [[MentionParams alloc] init];
+ mentionParams.text = paramsDict[@"text"];
+ mentionParams.indicator = paramsDict[@"indicator"];
+
+ [paramsDict removeObjectsForKeys:@[ @"text", @"indicator" ]];
+ NSError *error;
+ NSData *attrsData = [NSJSONSerialization dataWithJSONObject:paramsDict
+ options:0
+ error:&error];
+ NSString *formattedAttrsString =
+ [[NSString alloc] initWithData:attrsData
+ encoding:NSUTF8StringEncoding];
+ mentionParams.attributes = formattedAttrsString;
+
+ stylePair.styleValue = mentionParams;
+ } else if ([tagName isEqualToString:@"h1"]) {
+ [styleArr addObject:@([H1Style getType])];
+ } else if ([tagName isEqualToString:@"h2"]) {
+ [styleArr addObject:@([H2Style getType])];
+ } else if ([tagName isEqualToString:@"h3"]) {
+ [styleArr addObject:@([H3Style getType])];
+ } else if ([tagName isEqualToString:@"h4"]) {
+ [styleArr addObject:@([H4Style getType])];
+ } else if ([tagName isEqualToString:@"h5"]) {
+ [styleArr addObject:@([H5Style getType])];
+ } else if ([tagName isEqualToString:@"h6"]) {
+ [styleArr addObject:@([H6Style getType])];
+ } else if ([tagName isEqualToString:@"ul"]) {
+ if ([self isUlCheckboxList:params]) {
+ [styleArr addObject:@([CheckboxListStyle getType])];
+ stylePair.styleValue =
+ [self prepareCheckboxListStyleValue:tagRangeValue
+ checkboxStates:checkboxStates];
+ } else {
+ [styleArr addObject:@([UnorderedListStyle getType])];
+ }
+ } else if ([tagName isEqualToString:@"ol"]) {
+ [styleArr addObject:@([OrderedListStyle getType])];
+ } else if ([tagName isEqualToString:@"blockquote"]) {
+ [styleArr addObject:@([BlockQuoteStyle getType])];
+ } else if ([tagName isEqualToString:@"codeblock"]) {
+ [styleArr addObject:@([CodeBlockStyle getType])];
+ } else {
+ // some other external tags like span just don't get put into the
+ // processed styles
+ continue;
+ }
+
+ stylePair.rangeValue = tagRangeValue;
+ [styleArr addObject:stylePair];
+ [processedStyles addObject:styleArr];
+ }
+
+ return @[ plainText, processedStyles ];
+}
+
+@end
diff --git a/ios/inputParser/InputParser.mm b/ios/inputParser/InputParser.mm
index dbe5f9d6c..5cd5bbc64 100644
--- a/ios/inputParser/InputParser.mm
+++ b/ios/inputParser/InputParser.mm
@@ -1,30 +1,20 @@
#import "InputParser.h"
#import "EnrichedTextInputView.h"
+#import "HtmlParser.h"
#import "StringExtension.h"
#import "StyleHeaders.h"
#import "TextInsertionUtils.h"
-#import "UIView+React.h"
-
-#include "GumboParser.hpp"
@implementation InputParser {
EnrichedTextInputView __weak *_input;
- NSInteger _precedingImageCount;
}
- (instancetype)initWithInput:(id)input {
self = [super init];
_input = (EnrichedTextInputView *)input;
- _precedingImageCount = 0;
return self;
}
-- (BOOL)isBlockTag:(NSString *)tagName {
- return [tagName isEqualToString:@"ul"] || [tagName isEqualToString:@"ol"] ||
- [tagName isEqualToString:@"blockquote"] ||
- [tagName isEqualToString:@"codeblock"];
-}
-
- (NSString *)parseToHtmlFromRange:(NSRange)range {
NSInteger offset = range.location;
NSString *text =
@@ -592,7 +582,7 @@ - (NSString *)tagContentForStyle:(NSNumber *)style
}
- (void)replaceWholeFromHtml:(NSString *_Nonnull)html {
- NSArray *processingResult = [self getTextAndStylesFromHtml:html];
+ NSArray *processingResult = [HtmlParser getTextAndStylesFromHtml:html];
NSString *plainText = (NSString *)processingResult[0];
NSArray *stylesInfo = (NSArray *)processingResult[1];
@@ -610,7 +600,7 @@ - (void)replaceWholeFromHtml:(NSString *_Nonnull)html {
}
- (void)replaceFromHtml:(NSString *_Nonnull)html range:(NSRange)range {
- NSArray *processingResult = [self getTextAndStylesFromHtml:html];
+ NSArray *processingResult = [HtmlParser getTextAndStylesFromHtml:html];
NSString *plainText = (NSString *)processingResult[0];
NSArray *stylesInfo = (NSArray *)processingResult[1];
@@ -627,7 +617,7 @@ - (void)replaceFromHtml:(NSString *_Nonnull)html range:(NSRange)range {
}
- (void)insertFromHtml:(NSString *_Nonnull)html location:(NSInteger)location {
- NSArray *processingResult = [self getTextAndStylesFromHtml:html];
+ NSArray *processingResult = [HtmlParser getTextAndStylesFromHtml:html];
NSString *plainText = (NSString *)processingResult[0];
NSArray *stylesInfo = (NSArray *)processingResult[1];
@@ -712,752 +702,9 @@ - (void)applyProcessedStyles:(NSArray *)processedStyles
[_input anyTextMayHaveBeenModified];
}
-#pragma mark - External HTML normalization
-
-/**
- * Normalizes external HTML (from Google Docs, Word, web pages, etc.) into our
- * canonical tag subset using the Gumbo-based C++ normalizer.
- *
- * Converts: strong → b, em → i, span style="font-weight:bold" → b,
- * strips unknown tags while preserving text
- */
-- (NSString *_Nullable)normalizeExternalHtml:(NSString *_Nonnull)html {
- std::string result =
- GumboParser::normalizeHtml(std::string([html UTF8String]));
- if (result.empty())
- return nil;
- return [NSString stringWithUTF8String:result.c_str()];
-}
-
- (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 (_input->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;
- }
- }
-
- // Additionally, try getting the content from between body tags if there are
- // some:
-
- // Firstly make sure there are no newlines between them.
- fixedHtml = [fixedHtml stringByReplacingOccurrencesOfString:@"\n"
- withString:@""];
- fixedHtml = [fixedHtml stringByReplacingOccurrencesOfString:@"\n"
- withString:@""];
- // Then, if there actually are body tags, use the content between them.
- NSRange openingBodyRange = [htmlWithoutSpaces rangeOfString:@""];
- NSRange closingBodyRange = [htmlWithoutSpaces rangeOfString:@""];
- if (openingBodyRange.length != 0 && closingBodyRange.length != 0) {
- NSInteger newStart = openingBodyRange.location + 6;
- NSInteger newEnd = closingBodyRange.location - 1;
- fixedHtml = [htmlWithoutSpaces
- substringWithRange:NSMakeRange(newStart, newEnd - newStart + 1)];
- }
- }
-
- // second processing - try fixing htmls with wrong newlines' setup
- if (fixedHtml != nullptr) {
- // add
tag wherever needed
- fixedHtml = [fixedHtml stringByReplacingOccurrencesOfString:@""
- withString:@"
"];
-
- // remove tags inside of
- fixedHtml = [fixedHtml stringByReplacingOccurrencesOfString:@""
- withString:@"
"];
- fixedHtml = [fixedHtml stringByReplacingOccurrencesOfString:@""
- withString:@""];
-
- // change
to
- fixedHtml = [fixedHtml stringByReplacingOccurrencesOfString:@"
"
- withString:@"
"];
-
- // remove tags around
- fixedHtml = [fixedHtml stringByReplacingOccurrencesOfString:@"
"
- withString:@"
"];
- fixedHtml = [fixedHtml stringByReplacingOccurrencesOfString:@"
"
- withString:@"
"];
-
- // add
tags inside empty blockquote and codeblock tags
- fixedHtml = [fixedHtml
- stringByReplacingOccurrencesOfString:@""
- withString:@"
"
- @"blockquote>"];
- fixedHtml = [fixedHtml
- stringByReplacingOccurrencesOfString:@""
- withString:@"
"];
-
- // remove empty ul and ol tags
- fixedHtml = [fixedHtml stringByReplacingOccurrencesOfString:@""
- withString:@""];
- fixedHtml = [fixedHtml
- stringByReplacingOccurrencesOfString:@""
- withString:@""];
- fixedHtml = [fixedHtml stringByReplacingOccurrencesOfString:@"
"
- withString:@""];
-
- // tags that have to be in separate lines
- fixedHtml = [self stringByAddingNewlinesToTag:@"
"
- inString:fixedHtml
- leading:YES
- trailing:YES];
- fixedHtml = [self stringByAddingNewlinesToTag:@""
- inString:fixedHtml
- leading:YES
- trailing:YES];
- fixedHtml = [self stringByAddingNewlinesToTag:@"
"
- inString:fixedHtml
- leading:YES
- trailing:YES];
- fixedHtml = [self stringByAddingNewlinesToTag:@""
- inString:fixedHtml
- leading:YES
- trailing:YES];
- fixedHtml = [self stringByAddingNewlinesToTag:@"
"
- inString:fixedHtml
- leading:YES
- trailing:YES];
- fixedHtml = [self stringByAddingNewlinesToTag:@""
- inString:fixedHtml
- leading:YES
- trailing:YES];
- fixedHtml = [self stringByAddingNewlinesToTag:@"
"
- inString:fixedHtml
- leading:YES
- trailing:YES];
- fixedHtml = [self stringByAddingNewlinesToTag:@""
- inString:fixedHtml
- leading:YES
- trailing:YES];
- fixedHtml = [self stringByAddingNewlinesToTag:@""
- inString:fixedHtml
- leading:YES
- trailing:YES];
-
- // line opening tags
- fixedHtml = [self stringByAddingNewlinesToTag:@""
- inString:fixedHtml
- leading:YES
- trailing:NO];
- fixedHtml = [self stringByAddingNewlinesToTag:@"
"
- inString:fixedHtml
- leading:YES
- trailing:NO];
- fixedHtml = [self stringByAddingNewlinesToTag:@""
- inString:fixedHtml
- leading:YES
- trailing:NO];
- fixedHtml = [self stringByAddingNewlinesToTag:@""
- inString:fixedHtml
- leading:YES
- trailing:NO];
- fixedHtml = [self stringByAddingNewlinesToTag:@""
- inString:fixedHtml
- leading:YES
- trailing:NO];
- fixedHtml = [self stringByAddingNewlinesToTag:@""
- inString:fixedHtml
- leading:YES
- trailing:NO];
- fixedHtml = [self stringByAddingNewlinesToTag:@""
- inString:fixedHtml
- leading:YES
- trailing:NO];
- fixedHtml = [self stringByAddingNewlinesToTag:@""
- inString:fixedHtml
- leading:YES
- trailing:NO];
- fixedHtml = [self stringByAddingNewlinesToTag:@""
- inString:fixedHtml
- leading:YES
- trailing:NO];
-
- // line closing tags
- fixedHtml = [self stringByAddingNewlinesToTag:@""
- inString:fixedHtml
- leading:NO
- trailing:YES];
- fixedHtml = [self stringByAddingNewlinesToTag:@"
"
- inString:fixedHtml
- leading:NO
- trailing:YES];
- fixedHtml = [self stringByAddingNewlinesToTag:@""
- inString:fixedHtml
- leading:NO
- trailing:YES];
- fixedHtml = [self stringByAddingNewlinesToTag:@""
- inString:fixedHtml
- leading:NO
- trailing:YES];
- fixedHtml = [self stringByAddingNewlinesToTag:@""
- inString:fixedHtml
- leading:NO
- trailing:YES];
- fixedHtml = [self stringByAddingNewlinesToTag:@""
- inString:fixedHtml
- leading:NO
- trailing:YES];
- fixedHtml = [self stringByAddingNewlinesToTag:@""
- inString:fixedHtml
- leading:NO
- trailing:YES];
- fixedHtml = [self stringByAddingNewlinesToTag:@""
- 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"];
-
- // 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;
-}
-
-/**
- * Prepares HTML for the parser by stripping extraneous whitespace and newlines
- * from structural tags, while preserving them within text content.
- *
- * APPROACH:
- * This function treats the HTML as having two distinct states:
- * 1. Structure Mode (Depth == 0): We are inside or between container tags (like
- * blockquote, ul, codeblock). In this mode whitespace and newlines are
- * considered layout artifacts and are REMOVED to prevent the parser from
- * creating unwanted spaces.
- * 2. Content Mode (Depth > 0): We are inside a text-containing tag (like p,
- * b, li). In this mode, all whitespace is PRESERVED exactly as is, ensuring
- * that sentences and inline formatting remain readable.
- *
- * The function iterates character-by-character, using a depth counter to track
- * nesting levels of the specific tags defined in `textTags`.
- *
- * IMPORTANT:
- * The `textTags` set acts as a whitelist for "Content Mode". If you add support
- * for a new HTML tag that contains visible text (e.g., h4, h5, h6),
- * you MUST add it to the `textTags` set below.
- */
-- (NSString *)stripExtraWhiteSpacesAndNewlines:(NSString *)html {
- NSSet *textTags = [NSSet setWithObjects:@"p", @"h1", @"h2", @"h3", @"h4",
- @"h5", @"h6", @"li", @"b", @"a", @"s",
- @"mention", @"code", @"u", @"i", nil];
-
- NSMutableString *output = [NSMutableString stringWithCapacity:html.length];
- NSMutableString *currentTagBuffer = [NSMutableString string];
- NSCharacterSet *whitespaceAndNewlineSet =
- [NSCharacterSet whitespaceAndNewlineCharacterSet];
-
- BOOL isReadingTag = NO;
- NSInteger textDepth = 0;
-
- for (NSUInteger i = 0; i < html.length; i++) {
- unichar c = [html characterAtIndex:i];
-
- if (c == '<') {
- isReadingTag = YES;
- [currentTagBuffer setString:@""];
- [output appendString:@"<"];
- } else if (c == '>') {
- isReadingTag = NO;
- [output appendString:@">"];
-
- NSString *fullTag = [currentTagBuffer lowercaseString];
-
- NSString *cleanName = [fullTag
- stringByTrimmingCharactersInSet:
- [NSCharacterSet characterSetWithCharactersInString:@"/"]];
- NSArray *parts =
- [cleanName componentsSeparatedByCharactersInSet:
- [NSCharacterSet whitespaceAndNewlineCharacterSet]];
- NSString *tagName = parts.firstObject;
-
- if (![textTags containsObject:tagName]) {
- continue;
- }
-
- if ([fullTag hasPrefix:@"/"]) {
- textDepth--;
- if (textDepth < 0)
- textDepth = 0;
- } else {
- // Opening tag (e.g. ) -> Enter Text Mode
- // (Ignore self-closing tags like
if they happen to be in the
- // list)
- if (![fullTag hasSuffix:@"/"]) {
- textDepth++;
- }
- }
- } else {
- if (isReadingTag) {
- [currentTagBuffer appendFormat:@"%C", c];
- [output appendFormat:@"%C", c];
- continue;
- }
-
- if (textDepth > 0) {
- [output appendFormat:@"%C", c];
- } else {
- if (![whitespaceAndNewlineSet characterIsMember:c]) {
- [output appendFormat:@"%C", c];
- }
- }
- }
- }
-
- return output;
-}
-
-- (NSString *)stringByAddingNewlinesToTag:(NSString *)tag
- inString:(NSString *)html
- leading:(BOOL)leading
- trailing:(BOOL)trailing {
- NSString *str = [html copy];
- if (leading) {
- NSString *formattedTag = [NSString stringWithFormat:@">%@", tag];
- NSString *formattedNewTag = [NSString stringWithFormat:@">\n%@", tag];
- str = [str stringByReplacingOccurrencesOfString:formattedTag
- withString:formattedNewTag];
- }
- if (trailing) {
- NSString *formattedTag = [NSString stringWithFormat:@"%@<", tag];
- NSString *formattedNewTag = [NSString stringWithFormat:@"%@\n<", tag];
- str = [str stringByReplacingOccurrencesOfString:formattedTag
- withString:formattedNewTag];
- }
- return str;
-}
-
-- (void)finalizeTagEntry:(NSMutableString *)tagName
- ongoingTags:(NSMutableDictionary *)ongoingTags
- initiallyProcessedTags:(NSMutableArray *)processedTags
- plainText:(NSMutableString *)plainText {
- NSMutableArray *tagEntry = [[NSMutableArray alloc] init];
-
- NSArray *tagData = ongoingTags[tagName];
- NSInteger tagLocation = [((NSNumber *)tagData[0]) intValue];
-
- // 'tagLocation' is an index based on 'plainText' which currently only holds
- // raw text.
- //
- // Since 'plainText' does not yet contain the special placeholders for images,
- // the indices for any text following an image are lower than they will be
- // in the final NSTextStorage.
- //
- // We add '_precedingImageCount' to shift the start index forward, aligning
- // this style's range with the actual position in the final text (where each
- // image adds 1 character).
- NSRange tagRange = NSMakeRange(tagLocation + _precedingImageCount,
- plainText.length - tagLocation);
-
- [tagEntry addObject:[tagName copy]];
- [tagEntry addObject:[NSValue valueWithRange:tagRange]];
- if (tagData.count > 1) {
- [tagEntry addObject:[(NSString *)tagData[1] copy]];
- }
-
- [processedTags addObject:tagEntry];
- [ongoingTags removeObjectForKey:tagName];
-
- if ([tagName isEqualToString:@"img"]) {
- _precedingImageCount++;
- }
-}
-
-- (NSArray *)getTextAndStylesFromHtml:(NSString *)fixedHtml {
- NSMutableString *plainText = [[NSMutableString alloc] initWithString:@""];
- NSMutableDictionary *ongoingTags = [[NSMutableDictionary alloc] init];
- NSMutableArray *initiallyProcessedTags = [[NSMutableArray alloc] init];
- NSMutableDictionary *checkboxStates = [[NSMutableDictionary alloc] init];
- BOOL insideCheckboxList = NO;
- _precedingImageCount = 0;
- BOOL insideTag = NO;
- BOOL gettingTagName = NO;
- BOOL gettingTagParams = NO;
- 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++) {
- NSString *currentCharacterStr =
- [fixedHtml substringWithRange:NSMakeRange(i, 1)];
- unichar currentCharacterChar = [fixedHtml characterAtIndex:i];
-
- if (currentCharacterChar == '<') {
- // opening the tag, mark that we are inside and getting its name
- insideTag = YES;
- gettingTagName = YES;
- } else if (currentCharacterChar == '>') {
- // finishing some tag, no longer marked as inside or getting its
- // name/params
- insideTag = NO;
- gettingTagName = NO;
- gettingTagParams = NO;
-
- BOOL isSelfClosing = NO;
-
- // Check if params ended with '/' (e.g.
)
- if ([currentTagParams hasSuffix:@"/"]) {
- [currentTagParams
- deleteCharactersInRange:NSMakeRange(currentTagParams.length - 1,
- 1)];
- isSelfClosing = YES;
- }
-
- if ([currentTagName isEqualToString:@"p"] ||
- [currentTagName isEqualToString:@"br"]) {
- // 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);
- }
- } 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 numberWithInteger:plainText.length]];
- 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;
- }
-
- // 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 (isSelfClosing) {
- [self finalizeTagEntry:currentTagName
- ongoingTags:ongoingTags
- initiallyProcessedTags:initiallyProcessedTags
- plainText:plainText];
- }
- } else {
- // we finish closing tags - pack tag name, tag range and optionally tag
- // params into an entry that goes inside initiallyProcessedTags
-
- // Check if we're closing a checkbox list by looking at the params
- if ([currentTagName isEqualToString:@"ul"] &&
- [self isUlCheckboxList:currentTagParams]) {
- insideCheckboxList = NO;
- }
-
- BOOL isBlockTag = [self isBlockTag:currentTagName];
-
- // 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]]) {
- plainText = [[plainText
- substringWithRange:NSMakeRange(0, plainText.length - 1)]
- mutableCopy];
- }
-
- [self finalizeTagEntry:currentTagName
- ongoingTags:ongoingTags
- initiallyProcessedTags:initiallyProcessedTags
- plainText:plainText];
- }
- // post-tag cleanup
- closingTag = NO;
- currentTagName = [[NSMutableString alloc] initWithString:@""];
- currentTagParams = [[NSMutableString alloc] initWithString:@""];
- } else {
- if (!insideTag) {
- // 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 == ' ') {
- // no longer getting tag name - switch to params
- gettingTagName = NO;
- gettingTagParams = YES;
- } else if (currentCharacterChar == '/') {
- // mark that the tag is closing
- closingTag = YES;
- } else {
- // append next tag char
- [currentTagName appendString:currentCharacterStr];
- }
- } else if (gettingTagParams) {
- // append next tag params char
- [currentTagParams appendString:currentCharacterStr];
- }
- }
- }
- }
-
- // process tags into proper StyleType + StylePair values
- NSMutableArray *processedStyles = [[NSMutableArray alloc] init];
-
- for (NSArray *arr in initiallyProcessedTags) {
- NSString *tagName = (NSString *)arr[0];
- NSValue *tagRangeValue = (NSValue *)arr[1];
- NSMutableString *params = [[NSMutableString alloc] initWithString:@""];
- if (arr.count > 2) {
- [params appendString:(NSString *)arr[2]];
- }
-
- NSMutableArray *styleArr = [[NSMutableArray alloc] init];
- StylePair *stylePair = [[StylePair alloc] init];
- if ([tagName isEqualToString:@"b"]) {
- [styleArr addObject:@([BoldStyle getType])];
- } else if ([tagName isEqualToString:@"i"]) {
- [styleArr addObject:@([ItalicStyle getType])];
- } else if ([tagName isEqualToString:@"img"]) {
- NSRegularExpression *srcRegex =
- [NSRegularExpression regularExpressionWithPattern:@"src=\"([^\"]+)\""
- options:0
- error:nullptr];
- NSTextCheckingResult *match =
- [srcRegex firstMatchInString:params
- options:0
- range:NSMakeRange(0, params.length)];
-
- if (match == nullptr) {
- continue;
- }
-
- NSRange srcRange = match.range;
- [styleArr addObject:@([ImageStyle getType])];
- // cut only the uri from the src="..." string
- NSString *uri =
- [params substringWithRange:NSMakeRange(srcRange.location + 5,
- srcRange.length - 6)];
- ImageData *imageData = [[ImageData alloc] init];
- imageData.uri = uri;
-
- NSRegularExpression *widthRegex = [NSRegularExpression
- regularExpressionWithPattern:@"width=\"([0-9.]+)\""
- options:0
- error:nil];
- NSTextCheckingResult *widthMatch =
- [widthRegex firstMatchInString:params
- options:0
- range:NSMakeRange(0, params.length)];
-
- if (widthMatch) {
- NSString *widthString =
- [params substringWithRange:[widthMatch rangeAtIndex:1]];
- imageData.width = [widthString floatValue];
- }
-
- NSRegularExpression *heightRegex = [NSRegularExpression
- regularExpressionWithPattern:@"height=\"([0-9.]+)\""
- options:0
- error:nil];
- NSTextCheckingResult *heightMatch =
- [heightRegex firstMatchInString:params
- options:0
- range:NSMakeRange(0, params.length)];
-
- if (heightMatch) {
- NSString *heightString =
- [params substringWithRange:[heightMatch rangeAtIndex:1]];
- imageData.height = [heightString floatValue];
- }
-
- stylePair.styleValue = imageData;
- } else if ([tagName isEqualToString:@"u"]) {
- [styleArr addObject:@([UnderlineStyle getType])];
- } else if ([tagName isEqualToString:@"s"]) {
- [styleArr addObject:@([StrikethroughStyle getType])];
- } else if ([tagName isEqualToString:@"code"]) {
- [styleArr addObject:@([InlineCodeStyle getType])];
- } else if ([tagName isEqualToString:@"a"]) {
- NSRegularExpression *hrefRegex =
- [NSRegularExpression regularExpressionWithPattern:@"href=\".+\""
- options:0
- error:nullptr];
- NSTextCheckingResult *match =
- [hrefRegex firstMatchInString:params
- options:0
- range:NSMakeRange(0, params.length)];
-
- if (match == nullptr) {
- // same as on Android, no href (or empty href) equals no link style
- continue;
- }
-
- NSRange hrefRange = match.range;
- [styleArr addObject:@([LinkStyle getType])];
- // cut only the url from the href="..." string
- NSString *url =
- [params substringWithRange:NSMakeRange(hrefRange.location + 6,
- hrefRange.length - 7)];
- NSString *text = [plainText substringWithRange:tagRangeValue.rangeValue];
-
- LinkData *linkData = [[LinkData alloc] init];
- linkData.url = url;
- linkData.text = text;
- linkData.isManual = ![text isEqualToString:url];
-
- stylePair.styleValue = linkData;
- } else if ([tagName isEqualToString:@"mention"]) {
- [styleArr addObject:@([MentionStyle getType])];
- // extract html expression into dict using some regex
- NSMutableDictionary *paramsDict = [[NSMutableDictionary alloc] init];
- NSString *pattern = @"(\\w+)=(['\"])(.*?)\\2";
- NSRegularExpression *regex =
- [NSRegularExpression regularExpressionWithPattern:pattern
- options:0
- error:nil];
-
- [regex enumerateMatchesInString:params
- options:0
- range:NSMakeRange(0, params.length)
- usingBlock:^(NSTextCheckingResult *_Nullable result,
- NSMatchingFlags flags,
- BOOL *_Nonnull stop) {
- if (result.numberOfRanges == 4) {
- NSString *key = [params
- substringWithRange:[result rangeAtIndex:1]];
- NSString *value = [params
- substringWithRange:[result rangeAtIndex:3]];
- paramsDict[key] = value;
- }
- }];
-
- MentionParams *mentionParams = [[MentionParams alloc] init];
- mentionParams.text = paramsDict[@"text"];
- mentionParams.indicator = paramsDict[@"indicator"];
-
- [paramsDict removeObjectsForKeys:@[ @"text", @"indicator" ]];
- NSError *error;
- NSData *attrsData = [NSJSONSerialization dataWithJSONObject:paramsDict
- options:0
- error:&error];
- NSString *formattedAttrsString =
- [[NSString alloc] initWithData:attrsData
- encoding:NSUTF8StringEncoding];
- mentionParams.attributes = formattedAttrsString;
-
- stylePair.styleValue = mentionParams;
- } else if ([tagName isEqualToString:@"h1"]) {
- [styleArr addObject:@([H1Style getType])];
- } else if ([tagName isEqualToString:@"h2"]) {
- [styleArr addObject:@([H2Style getType])];
- } else if ([tagName isEqualToString:@"h3"]) {
- [styleArr addObject:@([H3Style getType])];
- } else if ([tagName isEqualToString:@"h4"]) {
- [styleArr addObject:@([H4Style getType])];
- } else if ([tagName isEqualToString:@"h5"]) {
- [styleArr addObject:@([H5Style getType])];
- } else if ([tagName isEqualToString:@"h6"]) {
- [styleArr addObject:@([H6Style getType])];
- } else if ([tagName isEqualToString:@"ul"]) {
- if ([self isUlCheckboxList:params]) {
- [styleArr addObject:@([CheckboxListStyle getType])];
- stylePair.styleValue =
- [self prepareCheckboxListStyleValue:tagRangeValue
- checkboxStates:checkboxStates];
- } else {
- [styleArr addObject:@([UnorderedListStyle getType])];
- }
- } else if ([tagName isEqualToString:@"ol"]) {
- [styleArr addObject:@([OrderedListStyle getType])];
- } else if ([tagName isEqualToString:@"blockquote"]) {
- [styleArr addObject:@([BlockQuoteStyle getType])];
- } else if ([tagName isEqualToString:@"codeblock"]) {
- [styleArr addObject:@([CodeBlockStyle getType])];
- } else {
- // some other external tags like span just don't get put into the
- // processed styles
- continue;
- }
-
- stylePair.rangeValue = tagRangeValue;
- [styleArr addObject:stylePair];
- [processedStyles addObject:styleArr];
- }
-
- return @[ plainText, processedStyles ];
-}
-
-- (BOOL)isUlCheckboxList:(NSString *)params {
- return ([params containsString:@"data-type=\"checkbox\""] ||
- [params containsString:@"data-type='checkbox'"]);
-}
-
-- (NSDictionary *)prepareCheckboxListStyleValue:(NSValue *)rangeValue
- checkboxStates:(NSDictionary *)checkboxStates {
- NSRange range = [rangeValue rangeValue];
- NSMutableDictionary *statesInRange = [[NSMutableDictionary alloc] init];
-
- for (NSNumber *key in checkboxStates) {
- NSUInteger pos = [key unsignedIntegerValue];
- if (pos >= range.location && pos < range.location + range.length) {
- [statesInRange setObject:checkboxStates[key] forKey:key];
- }
- }
-
- return statesInRange;
+ return [HtmlParser initiallyProcessHtml:html
+ useHtmlNormalizer:_input->useHtmlNormalizer];
}
@end
diff --git a/ios/interfaces/BaseStyleProtocol.h b/ios/interfaces/BaseStyleProtocol.h
index 5e08e558a..96a3d03ae 100644
--- a/ios/interfaces/BaseStyleProtocol.h
+++ b/ios/interfaces/BaseStyleProtocol.h
@@ -5,7 +5,7 @@
@protocol BaseStyleProtocol
+ (StyleType)getStyleType;
+ (BOOL)isParagraphStyle;
-- (instancetype _Nonnull)initWithInput:(id _Nonnull)input;
+- (instancetype _Nonnull)initWithHost:(id _Nonnull)host;
- (void)applyStyle:(NSRange)range;
- (void)addAttributes:(NSRange)range withTypingAttr:(BOOL)withTypingAttr;
- (void)removeAttributes:(NSRange)range;
diff --git a/ios/interfaces/StyleBase.h b/ios/interfaces/StyleBase.h
index df65437ab..83bbfb0b7 100644
--- a/ios/interfaces/StyleBase.h
+++ b/ios/interfaces/StyleBase.h
@@ -1,19 +1,18 @@
#pragma once
#import "AttributeEntry.h"
+#import "EnrichedStyleHost.h"
#import "StylePair.h"
#import "StyleTypeEnum.h"
#import
-@class EnrichedTextInputView;
-
@interface StyleBase : NSObject
-@property(nonatomic, weak) EnrichedTextInputView *input;
+@property(nonatomic, weak) id host;
+ (StyleType)getType;
- (NSString *)getKey;
- (NSString *)getValue;
- (BOOL)isParagraph;
- (BOOL)needsZWS;
-- (instancetype)initWithInput:(EnrichedTextInputView *)input;
+- (instancetype)initWithHost:(id)host;
- (NSRange)actualUsedRange:(NSRange)range;
- (void)toggle:(NSRange)range;
- (void)add:(NSRange)range
diff --git a/ios/interfaces/StyleBase.mm b/ios/interfaces/StyleBase.mm
index b91d4e685..6c1277fdc 100644
--- a/ios/interfaces/StyleBase.mm
+++ b/ios/interfaces/StyleBase.mm
@@ -1,6 +1,5 @@
#import "StyleBase.h"
#import "AttributeEntry.h"
-#import "EnrichedTextInputView.h"
#import "OccurenceUtils.h"
#import "RangeUtils.h"
#import "ZeroWidthSpaceUtils.h"
@@ -35,9 +34,9 @@ - (BOOL)needsZWS {
return NO;
}
-- (instancetype)initWithInput:(EnrichedTextInputView *)input {
+- (instancetype)initWithHost:(id)host {
self = [super init];
- _input = input;
+ _host = host;
return self;
}
@@ -45,7 +44,7 @@ - (instancetype)initWithInput:(EnrichedTextInputView *)input {
- (NSRange)actualUsedRange:(NSRange)range {
if (![self isParagraph])
return range;
- return [_input->textView.textStorage.string paragraphRangeForRange:range];
+ return [self.host.textView.textStorage.string paragraphRangeForRange:range];
}
- (void)toggle:(NSRange)range {
@@ -76,11 +75,11 @@ - (void)add:(NSRange)range
NSRange actualRange = [self actualUsedRange:range];
if (![self isParagraph]) {
- [_input->textView.textStorage addAttribute:[self getKey]
- value:value
- range:actualRange];
+ [self.host.textView.textStorage addAttribute:[self getKey]
+ value:value
+ range:actualRange];
} else {
- [_input->textView.textStorage
+ [self.host.textView.textStorage
enumerateAttribute:NSParagraphStyleAttributeName
inRange:actualRange
options:0
@@ -93,7 +92,7 @@ - (void)add:(NSRange)range
pStyle.textLists =
@[ [[NSTextList alloc] initWithMarkerFormat:value
options:0] ];
- [_input->textView.textStorage
+ [self.host.textView.textStorage
addAttribute:NSParagraphStyleAttributeName
value:pStyle
range:subRange];
@@ -106,7 +105,7 @@ - (void)add:(NSRange)range
// Notify attributes manager of styling to be re-done if needed.
if (withDirtyRange) {
- [self.input->attributesManager addDirtyRange:actualRange];
+ [self.host.attributesManager addDirtyRange:actualRange];
}
}
@@ -114,10 +113,10 @@ - (void)remove:(NSRange)range withDirtyRange:(BOOL)withDirtyRange {
NSRange actualRange = [self actualUsedRange:range];
if (![self isParagraph]) {
- [_input->textView.textStorage removeAttribute:[self getKey]
- range:actualRange];
+ [self.host.textView.textStorage removeAttribute:[self getKey]
+ range:actualRange];
} else {
- [_input->textView.textStorage
+ [self.host.textView.textStorage
enumerateAttribute:NSParagraphStyleAttributeName
inRange:actualRange
options:0
@@ -128,7 +127,7 @@ - (void)remove:(NSRange)range withDirtyRange:(BOOL)withDirtyRange {
if (pStyle == nullptr)
return;
pStyle.textLists = @[];
- [_input->textView.textStorage
+ [self.host.textView.textStorage
addAttribute:NSParagraphStyleAttributeName
value:pStyle
range:subRange];
@@ -138,13 +137,13 @@ - (void)remove:(NSRange)range withDirtyRange:(BOOL)withDirtyRange {
// Notify attributes manager of styling to be re-done if needed.
if (withDirtyRange) {
- [self.input->attributesManager addDirtyRange:actualRange];
+ [self.host.attributesManager addDirtyRange:actualRange];
}
}
- (void)addTypingWithValue:(NSString *)value {
NSMutableDictionary *newTypingAttrs =
- [_input->textView.typingAttributes mutableCopy];
+ [self.host.textView.typingAttributes mutableCopy];
if (![self isParagraph]) {
newTypingAttrs[[self getKey]] = value;
@@ -156,18 +155,18 @@ - (void)addTypingWithValue:(NSString *)value {
newTypingAttrs[NSParagraphStyleAttributeName] = pStyle;
}
- _input->textView.typingAttributes = newTypingAttrs;
+ self.host.textView.typingAttributes = newTypingAttrs;
}
- (void)removeTyping {
NSMutableDictionary *newTypingAttrs =
- [_input->textView.typingAttributes mutableCopy];
+ [self.host.textView.typingAttributes mutableCopy];
if (![self isParagraph]) {
[newTypingAttrs removeObjectForKey:[self getKey]];
// attributes manager also needs to be notified of custom attributes that
// shouldn't be extended
- [_input->attributesManager didRemoveTypingAttribute:[self getKey]];
+ [self.host.attributesManager didRemoveTypingAttribute:[self getKey]];
} else {
NSMutableParagraphStyle *pStyle =
[newTypingAttrs[NSParagraphStyleAttributeName] mutableCopy];
@@ -175,7 +174,7 @@ - (void)removeTyping {
newTypingAttrs[NSParagraphStyleAttributeName] = pStyle;
}
- _input->textView.typingAttributes = newTypingAttrs;
+ self.host.textView.typingAttributes = newTypingAttrs;
}
// custom styles (e.g. ImageStyle, MentionStyle) will likely need to override
@@ -195,14 +194,14 @@ - (BOOL)styleCondition:(id)value range:(NSRange)range {
- (BOOL)detect:(NSRange)range {
if (range.length >= 1) {
return [OccurenceUtils detect:[self getKey]
- withInput:_input
+ withHost:self.host
inRange:range
withCondition:^BOOL(id _Nullable value, NSRange range) {
return [self styleCondition:value range:range];
}];
} else {
return [OccurenceUtils detect:[self getKey]
- withInput:_input
+ withHost:self.host
atIndex:range.location
checkPrevious:[self isParagraph]
withCondition:^BOOL(id _Nullable value, NSRange range) {
@@ -213,7 +212,7 @@ - (BOOL)detect:(NSRange)range {
- (BOOL)any:(NSRange)range {
return [OccurenceUtils any:[self getKey]
- withInput:_input
+ withHost:self.host
inRange:range
withCondition:^BOOL(id _Nullable value, NSRange range) {
return [self styleCondition:value range:range];
@@ -222,7 +221,7 @@ - (BOOL)any:(NSRange)range {
- (NSArray *)all:(NSRange)range {
return [OccurenceUtils all:[self getKey]
- withInput:_input
+ withHost:self.host
inRange:range
withCondition:^BOOL(id _Nullable value, NSRange range) {
return [self styleCondition:value range:range];
diff --git a/ios/interfaces/StyleHeaders.h b/ios/interfaces/StyleHeaders.h
index 7f1d4c885..247ce2451 100644
--- a/ios/interfaces/StyleHeaders.h
+++ b/ios/interfaces/StyleHeaders.h
@@ -27,6 +27,7 @@
- (NSRange)getFullLinkRangeAt:(NSUInteger)location;
- (void)handleAutomaticLinks:(NSString *)word inRange:(NSRange)wordRange;
- (void)handleManualLinks:(NSString *)word inRange:(NSRange)wordRange;
+- (void)applyLinkMetaWithData:(LinkData *)linkData range:(NSRange)range;
@end
@interface MentionStyle : StyleBase
diff --git a/ios/styles/BlockQuoteStyle.mm b/ios/styles/BlockQuoteStyle.mm
index 98f30587c..796a3cb52 100644
--- a/ios/styles/BlockQuoteStyle.mm
+++ b/ios/styles/BlockQuoteStyle.mm
@@ -21,9 +21,9 @@ - (BOOL)needsZWS {
}
- (void)applyStyling:(NSRange)range {
- CGFloat indent = [self.input->config blockquoteBorderWidth] +
- [self.input->config blockquoteGapWidth];
- [self.input->textView.textStorage
+ CGFloat indent = [self.host.config blockquoteBorderWidth] +
+ [self.host.config blockquoteGapWidth];
+ [self.host.textView.textStorage
enumerateAttribute:NSParagraphStyleAttributeName
inRange:range
options:0
@@ -33,23 +33,22 @@ - (void)applyStyling:(NSRange)range {
[(NSParagraphStyle *)value mutableCopy];
pStyle.headIndent = indent;
pStyle.firstLineHeadIndent = indent;
- [self.input->textView.textStorage
+ [self.host.textView.textStorage
addAttribute:NSParagraphStyleAttributeName
value:pStyle
range:subRange];
}];
- UIColor *bqColor = [self.input->config blockquoteColor];
- [self.input->textView.textStorage addAttribute:NSForegroundColorAttributeName
- value:bqColor
- range:range];
- [self.input->textView.textStorage addAttribute:NSUnderlineColorAttributeName
- value:bqColor
- range:range];
- [self.input->textView.textStorage
- addAttribute:NSStrikethroughColorAttributeName
- value:bqColor
- range:range];
+ UIColor *bqColor = [self.host.config blockquoteColor];
+ [self.host.textView.textStorage addAttribute:NSForegroundColorAttributeName
+ value:bqColor
+ range:range];
+ [self.host.textView.textStorage addAttribute:NSUnderlineColorAttributeName
+ value:bqColor
+ range:range];
+ [self.host.textView.textStorage addAttribute:NSStrikethroughColorAttributeName
+ value:bqColor
+ range:range];
}
@end
diff --git a/ios/styles/BoldStyle.mm b/ios/styles/BoldStyle.mm
index c9f25123d..025866c9b 100644
--- a/ios/styles/BoldStyle.mm
+++ b/ios/styles/BoldStyle.mm
@@ -17,7 +17,7 @@ - (BOOL)isParagraph {
}
- (void)applyStyling:(NSRange)range {
- [self.input->textView.textStorage
+ [self.host.textView.textStorage
enumerateAttribute:NSFontAttributeName
inRange:range
options:0
@@ -26,7 +26,7 @@ - (void)applyStyling:(NSRange)range {
UIFont *font = (UIFont *)value;
if (font != nullptr) {
UIFont *newFont = [font setBold];
- [self.input->textView.textStorage
+ [self.host.textView.textStorage
addAttribute:NSFontAttributeName
value:newFont
range:range];
diff --git a/ios/styles/CheckboxListStyle.mm b/ios/styles/CheckboxListStyle.mm
index 6465db091..6cf8ef32b 100644
--- a/ios/styles/CheckboxListStyle.mm
+++ b/ios/styles/CheckboxListStyle.mm
@@ -22,11 +22,11 @@ - (BOOL)needsZWS {
}
- (void)applyStyling:(NSRange)range {
- CGFloat listHeadIndent = [self.input->config checkboxListMarginLeft] +
- [self.input->config checkboxListGapWidth] +
- [self.input->config checkboxListBoxSize];
+ CGFloat listHeadIndent = [self.host.config checkboxListMarginLeft] +
+ [self.host.config checkboxListGapWidth] +
+ [self.host.config checkboxListBoxSize];
- [self.input->textView.textStorage
+ [self.host.textView.textStorage
enumerateAttribute:NSParagraphStyleAttributeName
inRange:range
options:0
@@ -36,7 +36,7 @@ - (void)applyStyling:(NSRange)range {
[(NSParagraphStyle *)value mutableCopy];
pStyle.headIndent = listHeadIndent;
pStyle.firstLineHeadIndent = listHeadIndent;
- [self.input->textView.textStorage
+ [self.host.textView.textStorage
addAttribute:NSParagraphStyleAttributeName
value:pStyle
range:range];
@@ -88,20 +88,20 @@ - (void)reapplyFromStylePair:(StylePair *)pair {
}
- (void)toggleCheckedAt:(NSUInteger)location {
- if (location >= self.input->textView.textStorage.length) {
+ if (location >= self.host.textView.textStorage.length) {
return;
}
NSParagraphStyle *pStyle =
- [self.input->textView.textStorage attribute:NSParagraphStyleAttributeName
- atIndex:location
- effectiveRange:NULL];
+ [self.host.textView.textStorage attribute:NSParagraphStyleAttributeName
+ atIndex:location
+ effectiveRange:NULL];
NSTextList *list = pStyle.textLists.firstObject;
BOOL isCurrentlyChecked =
[list.markerFormat isEqualToString:@"EnrichedCheckbox1"];
- NSRange paragraphRange = [self.input->textView.textStorage.string
+ NSRange paragraphRange = [self.host.textView.textStorage.string
paragraphRangeForRange:NSMakeRange(location, 0)];
[self addWithChecked:!isCurrentlyChecked
@@ -111,14 +111,14 @@ - (void)toggleCheckedAt:(NSUInteger)location {
}
- (BOOL)getCheckboxStateAt:(NSUInteger)location {
- if (location >= self.input->textView.textStorage.length) {
+ if (location >= self.host.textView.textStorage.length) {
return NO;
}
NSParagraphStyle *style =
- [self.input->textView.textStorage attribute:NSParagraphStyleAttributeName
- atIndex:location
- effectiveRange:NULL];
+ [self.host.textView.textStorage attribute:NSParagraphStyleAttributeName
+ atIndex:location
+ effectiveRange:NULL];
if (style && style.textLists.count > 0) {
NSTextList *list = style.textLists.firstObject;
@@ -131,18 +131,18 @@ - (BOOL)getCheckboxStateAt:(NSUInteger)location {
}
- (BOOL)handleNewlinesInRange:(NSRange)range replacementText:(NSString *)text {
- if ([self detect:self.input->textView.selectedRange] && text.length > 0 &&
+ if ([self detect:self.host.textView.selectedRange] && text.length > 0 &&
[[NSCharacterSet newlineCharacterSet]
characterIsMember:[text characterAtIndex:text.length - 1]]) {
// do the replacement manually
[TextInsertionUtils replaceText:text
at:range
additionalAttributes:nullptr
- input:self.input
+ input:self.host
withSelection:YES];
// apply unchecked checkbox attributes to the new paragraph
[self addWithChecked:NO
- range:self.input->textView.selectedRange
+ range:self.host.textView.selectedRange
withTyping:YES
withDirtyRange:YES];
return YES;
diff --git a/ios/styles/CodeBlockStyle.mm b/ios/styles/CodeBlockStyle.mm
index 65d44af59..74c3dbeb1 100644
--- a/ios/styles/CodeBlockStyle.mm
+++ b/ios/styles/CodeBlockStyle.mm
@@ -21,7 +21,7 @@ - (BOOL)needsZWS {
}
- (void)applyStyling:(NSRange)range {
- [self.input->textView.textStorage
+ [self.host.textView.textStorage
enumerateAttribute:NSFontAttributeName
inRange:range
options:0
@@ -30,19 +30,19 @@ - (void)applyStyling:(NSRange)range {
UIFont *currentFont = (UIFont *)value;
if (currentFont == nullptr)
return;
- UIFont *monoFont = [[[self.input->config monospacedFont]
+ UIFont *monoFont = [[[self.host.config monospacedFont]
withFontTraits:currentFont] setSize:currentFont.pointSize];
if (monoFont != nullptr) {
- [self.input->textView.textStorage
+ [self.host.textView.textStorage
addAttribute:NSFontAttributeName
value:monoFont
range:subRange];
}
}];
- [self.input->textView.textStorage
+ [self.host.textView.textStorage
addAttribute:NSForegroundColorAttributeName
- value:[self.input->config codeBlockFgColor]
+ value:[self.host.config codeBlockFgColor]
range:range];
}
diff --git a/ios/styles/H1Style.mm b/ios/styles/H1Style.mm
index c04c26f3b..c68c46a62 100644
--- a/ios/styles/H1Style.mm
+++ b/ios/styles/H1Style.mm
@@ -12,9 +12,9 @@ - (BOOL)isParagraph {
return YES;
}
- (CGFloat)getHeadingFontSize {
- return [self.input->config h1FontSize];
+ return [self.host.config h1FontSize];
}
- (BOOL)isHeadingBold {
- return [self.input->config h1Bold];
+ return [self.host.config h1Bold];
}
@end
diff --git a/ios/styles/H2Style.mm b/ios/styles/H2Style.mm
index 785b2b72a..98021bad0 100644
--- a/ios/styles/H2Style.mm
+++ b/ios/styles/H2Style.mm
@@ -12,9 +12,9 @@ - (BOOL)isParagraph {
return YES;
}
- (CGFloat)getHeadingFontSize {
- return [self.input->config h2FontSize];
+ return [self.host.config h2FontSize];
}
- (BOOL)isHeadingBold {
- return [self.input->config h2Bold];
+ return [self.host.config h2Bold];
}
@end
diff --git a/ios/styles/H3Style.mm b/ios/styles/H3Style.mm
index 9091f6e3b..41190238b 100644
--- a/ios/styles/H3Style.mm
+++ b/ios/styles/H3Style.mm
@@ -12,9 +12,9 @@ - (BOOL)isParagraph {
return YES;
}
- (CGFloat)getHeadingFontSize {
- return [self.input->config h3FontSize];
+ return [self.host.config h3FontSize];
}
- (BOOL)isHeadingBold {
- return [self.input->config h3Bold];
+ return [self.host.config h3Bold];
}
@end
diff --git a/ios/styles/H4Style.mm b/ios/styles/H4Style.mm
index a641f5ed8..135412af1 100644
--- a/ios/styles/H4Style.mm
+++ b/ios/styles/H4Style.mm
@@ -12,9 +12,9 @@ - (BOOL)isParagraph {
return YES;
}
- (CGFloat)getHeadingFontSize {
- return [self.input->config h4FontSize];
+ return [self.host.config h4FontSize];
}
- (BOOL)isHeadingBold {
- return [self.input->config h4Bold];
+ return [self.host.config h4Bold];
}
@end
diff --git a/ios/styles/H5Style.mm b/ios/styles/H5Style.mm
index 40fab0f28..01f77517c 100644
--- a/ios/styles/H5Style.mm
+++ b/ios/styles/H5Style.mm
@@ -12,9 +12,9 @@ - (BOOL)isParagraph {
return YES;
}
- (CGFloat)getHeadingFontSize {
- return [self.input->config h5FontSize];
+ return [self.host.config h5FontSize];
}
- (BOOL)isHeadingBold {
- return [self.input->config h5Bold];
+ return [self.host.config h5Bold];
}
@end
diff --git a/ios/styles/H6Style.mm b/ios/styles/H6Style.mm
index 2e576bb0a..0f87b37fe 100644
--- a/ios/styles/H6Style.mm
+++ b/ios/styles/H6Style.mm
@@ -12,9 +12,9 @@ - (BOOL)isParagraph {
return YES;
}
- (CGFloat)getHeadingFontSize {
- return [self.input->config h6FontSize];
+ return [self.host.config h6FontSize];
}
- (BOOL)isHeadingBold {
- return [self.input->config h6Bold];
+ return [self.host.config h6Bold];
}
@end
diff --git a/ios/styles/HeadingStyleBase.mm b/ios/styles/HeadingStyleBase.mm
index 6c2e83a2c..15c389d45 100644
--- a/ios/styles/HeadingStyleBase.mm
+++ b/ios/styles/HeadingStyleBase.mm
@@ -23,7 +23,7 @@ - (BOOL)isParagraph {
}
- (void)applyStyling:(NSRange)range {
- [self.input->textView.textStorage
+ [self.host.textView.textStorage
enumerateAttribute:NSFontAttributeName
inRange:range
options:0
@@ -36,27 +36,26 @@ - (void)applyStyling:(NSRange)range {
if ([self isHeadingBold]) {
newFont = [newFont setBold];
}
- [self.input->textView.textStorage
- addAttribute:NSFontAttributeName
- value:newFont
- range:subRange];
+ [self.host.textView.textStorage addAttribute:NSFontAttributeName
+ value:newFont
+ range:subRange];
}];
}
// used to make sure headings dont persist after a newline is placed
- (BOOL)handleNewlinesInRange:(NSRange)range replacementText:(NSString *)text {
// in a heading and a new text ends with a newline
- if ([self detect:self.input->textView.selectedRange] && text.length > 0 &&
+ if ([self detect:self.host.textView.selectedRange] && text.length > 0 &&
[[NSCharacterSet newlineCharacterSet]
characterIsMember:[text characterAtIndex:text.length - 1]]) {
// do the replacement manually
[TextInsertionUtils replaceText:text
at:range
additionalAttributes:nullptr
- input:self.input
+ input:self.host
withSelection:YES];
// remove the attributes at the new selection
- [self remove:self.input->textView.selectedRange withDirtyRange:YES];
+ [self remove:self.host.textView.selectedRange withDirtyRange:YES];
return YES;
}
return NO;
diff --git a/ios/styles/ImageStyle.mm b/ios/styles/ImageStyle.mm
index 4149e30c1..dfc0c58c1 100644
--- a/ios/styles/ImageStyle.mm
+++ b/ios/styles/ImageStyle.mm
@@ -33,13 +33,13 @@ - (void)reapplyFromStylePair:(StylePair *)pair {
ImageAttachment *attachment =
[[ImageAttachment alloc] initWithImageData:imageData];
- attachment.delegate = self.input;
+ attachment.delegate = (id)self.host;
- [self.input->textView.textStorage addAttributes:@{
+ [self.host.textView.textStorage addAttributes:@{
NSAttachmentAttributeName : attachment,
ImageAttributeName : imageData
}
- range:range];
+ range:range];
}
- (AttributeEntry *)getEntryIfPresent:(NSRange)range {
@@ -51,15 +51,15 @@ - (void)toggle:(NSRange)range {
}
- (void)remove:(NSRange)range withDirtyRange:(BOOL)withDirtyRange {
- [self.input->textView.textStorage beginEditing];
- [self.input->textView.textStorage removeAttribute:ImageAttributeName
- range:range];
- [self.input->textView.textStorage removeAttribute:NSAttachmentAttributeName
- range:range];
- [self.input->textView.textStorage endEditing];
+ [self.host.textView.textStorage beginEditing];
+ [self.host.textView.textStorage removeAttribute:ImageAttributeName
+ range:range];
+ [self.host.textView.textStorage removeAttribute:NSAttachmentAttributeName
+ range:range];
+ [self.host.textView.textStorage endEditing];
if (withDirtyRange) {
- [self.input->attributesManager addDirtyRange:range];
+ [self.host.attributesManager addDirtyRange:range];
}
[self removeTyping];
@@ -67,11 +67,11 @@ - (void)remove:(NSRange)range withDirtyRange:(BOOL)withDirtyRange {
- (void)removeTyping {
NSMutableDictionary *currentAttributes =
- [self.input->textView.typingAttributes mutableCopy];
+ [self.host.textView.typingAttributes mutableCopy];
[currentAttributes removeObjectForKey:ImageAttributeName];
[currentAttributes removeObjectForKey:NSAttachmentAttributeName];
- [self.input->attributesManager didRemoveTypingAttribute:ImageAttributeName];
- self.input->textView.typingAttributes = currentAttributes;
+ [self.host.attributesManager didRemoveTypingAttribute:ImageAttributeName];
+ self.host.textView.typingAttributes = currentAttributes;
}
- (BOOL)styleCondition:(id _Nullable)value range:(NSRange)range {
@@ -80,19 +80,19 @@ - (BOOL)styleCondition:(id _Nullable)value range:(NSRange)range {
- (ImageData *)getImageDataAt:(NSUInteger)location {
NSRange imageRange = NSMakeRange(0, 0);
- NSRange inputRange = NSMakeRange(0, self.input->textView.textStorage.length);
+ NSRange inputRange = NSMakeRange(0, self.host.textView.textStorage.length);
// don't search at the very end of input
NSUInteger searchLocation = location;
- if (searchLocation == self.input->textView.textStorage.length) {
+ if (searchLocation == self.host.textView.textStorage.length) {
return nullptr;
}
ImageData *imageData =
- [self.input->textView.textStorage attribute:ImageAttributeName
- atIndex:searchLocation
- longestEffectiveRange:&imageRange
- inRange:inputRange];
+ [self.host.textView.textStorage attribute:ImageAttributeName
+ atIndex:searchLocation
+ longestEffectiveRange:&imageRange
+ inRange:inputRange];
return imageData;
}
@@ -105,7 +105,7 @@ - (void)addImageAtRange:(NSRange)range
ImageAttachment *attachment =
[[ImageAttachment alloc] initWithImageData:imageData];
- attachment.delegate = self.input;
+ attachment.delegate = (id)self.host;
NSDictionary *attributes =
@{NSAttachmentAttributeName : attachment, ImageAttributeName : imageData};
@@ -118,18 +118,18 @@ - (void)addImageAtRange:(NSRange)range
[TextInsertionUtils insertText:placeholderChar
at:range.location
additionalAttributes:attributes
- input:self.input
+ input:self.host
withSelection:withSelection];
} else {
[TextInsertionUtils replaceText:placeholderChar
at:range
additionalAttributes:attributes
- input:self.input
+ input:self.host
withSelection:withSelection];
}
NSRange insertedImageRange = NSMakeRange(range.location, 1);
- [self.input->attributesManager addDirtyRange:insertedImageRange];
+ [self.host.attributesManager addDirtyRange:insertedImageRange];
}
- (void)addImage:(NSString *)uri width:(CGFloat)width height:(CGFloat)height {
@@ -138,7 +138,7 @@ - (void)addImage:(NSString *)uri width:(CGFloat)width height:(CGFloat)height {
data.width = width;
data.height = height;
- [self addImageAtRange:self.input->textView.selectedRange
+ [self addImageAtRange:self.host.textView.selectedRange
imageData:data
withSelection:YES];
}
diff --git a/ios/styles/InlineCodeStyle.mm b/ios/styles/InlineCodeStyle.mm
index 054332a75..4511858ab 100644
--- a/ios/styles/InlineCodeStyle.mm
+++ b/ios/styles/InlineCodeStyle.mm
@@ -21,29 +21,29 @@ - (BOOL)isParagraph {
- (void)applyStyling:(NSRange)range {
// we don't want to apply inline code to newline characters, it looks bad
NSArray *nonNewlineRanges =
- [RangeUtils getNonNewlineRangesIn:self.input->textView range:range];
+ [RangeUtils getNonNewlineRangesIn:self.host.textView range:range];
for (NSValue *value in nonNewlineRanges) {
NSRange subRange = [value rangeValue];
- [self.input->textView.textStorage
+ [self.host.textView.textStorage
addAttribute:NSBackgroundColorAttributeName
- value:[[self.input->config inlineCodeBgColor]
+ value:[[self.host.config inlineCodeBgColor]
colorWithAlphaIfNotTransparent:0.4]
range:subRange];
- [self.input->textView.textStorage
+ [self.host.textView.textStorage
addAttribute:NSForegroundColorAttributeName
- value:[self.input->config inlineCodeFgColor]
+ value:[self.host.config inlineCodeFgColor]
range:subRange];
- [self.input->textView.textStorage
+ [self.host.textView.textStorage
addAttribute:NSUnderlineColorAttributeName
- value:[self.input->config inlineCodeFgColor]
+ value:[self.host.config inlineCodeFgColor]
range:subRange];
- [self.input->textView.textStorage
+ [self.host.textView.textStorage
addAttribute:NSStrikethroughColorAttributeName
- value:[self.input->config inlineCodeFgColor]
+ value:[self.host.config inlineCodeFgColor]
range:subRange];
- [self.input->textView.textStorage
+ [self.host.textView.textStorage
enumerateAttribute:NSFontAttributeName
inRange:subRange
options:0
@@ -51,9 +51,9 @@ - (void)applyStyling:(NSRange)range {
BOOL *_Nonnull stop) {
UIFont *font = (UIFont *)value;
if (font != nullptr) {
- UIFont *newFont = [[[self.input->config monospacedFont]
+ UIFont *newFont = [[[self.host.config monospacedFont]
withFontTraits:font] setSize:font.pointSize];
- [self.input->textView.textStorage
+ [self.host.textView.textStorage
addAttribute:NSFontAttributeName
value:newFont
range:fontRange];
diff --git a/ios/styles/ItalicStyle.mm b/ios/styles/ItalicStyle.mm
index 963a213e6..f2a161141 100644
--- a/ios/styles/ItalicStyle.mm
+++ b/ios/styles/ItalicStyle.mm
@@ -17,7 +17,7 @@ - (BOOL)isParagraph {
}
- (void)applyStyling:(NSRange)range {
- [self.input->textView.textStorage
+ [self.host.textView.textStorage
enumerateAttribute:NSFontAttributeName
inRange:range
options:0
@@ -26,7 +26,7 @@ - (void)applyStyling:(NSRange)range {
UIFont *font = (UIFont *)value;
if (font != nullptr) {
UIFont *newFont = [font setItalic];
- [self.input->textView.textStorage
+ [self.host.textView.textStorage
addAttribute:NSFontAttributeName
value:newFont
range:range];
diff --git a/ios/styles/LinkStyle.mm b/ios/styles/LinkStyle.mm
index 626969e86..3cc00762c 100644
--- a/ios/styles/LinkStyle.mm
+++ b/ios/styles/LinkStyle.mm
@@ -31,13 +31,13 @@ - (void)applyStyling:(NSRange)range {
}
NSMutableDictionary *newAttrs = [[NSMutableDictionary alloc] init];
- newAttrs[NSForegroundColorAttributeName] = [self.input->config linkColor];
- newAttrs[NSUnderlineColorAttributeName] = [self.input->config linkColor];
- newAttrs[NSStrikethroughColorAttributeName] = [self.input->config linkColor];
- if ([self.input->config linkDecorationLine] == DecorationUnderline) {
+ newAttrs[NSForegroundColorAttributeName] = [self.host.config linkColor];
+ newAttrs[NSUnderlineColorAttributeName] = [self.host.config linkColor];
+ newAttrs[NSStrikethroughColorAttributeName] = [self.host.config linkColor];
+ if ([self.host.config linkDecorationLine] == DecorationUnderline) {
newAttrs[NSUnderlineStyleAttributeName] = @(NSUnderlineStyleSingle);
}
- [self.input->textView.textStorage addAttributes:newAttrs range:range];
+ [self.host.textView.textStorage addAttributes:newAttrs range:range];
}
- (void)reapplyFromStylePair:(StylePair *)pair {
@@ -63,34 +63,34 @@ - (void)toggle:(NSRange)range {
// we have to make sure all links in the range get fully removed here
- (void)remove:(NSRange)range withDirtyRange:(BOOL)withDirtyRange {
NSArray *links = [self all:range];
- [self.input->textView.textStorage beginEditing];
+ [self.host.textView.textStorage beginEditing];
for (StylePair *pair in links) {
NSRange linkRange =
[self getFullLinkRangeAt:[pair.rangeValue rangeValue].location];
- [self.input->textView.textStorage removeAttribute:ManualLinkAttributeName
- range:linkRange];
- [self.input->textView.textStorage removeAttribute:AutomaticLinkAttributeName
- range:linkRange];
+ [self.host.textView.textStorage removeAttribute:ManualLinkAttributeName
+ range:linkRange];
+ [self.host.textView.textStorage removeAttribute:AutomaticLinkAttributeName
+ range:linkRange];
if (withDirtyRange) {
- [self.input->attributesManager addDirtyRange:linkRange];
+ [self.host.attributesManager addDirtyRange:linkRange];
}
}
- [self.input->textView.textStorage endEditing];
+ [self.host.textView.textStorage endEditing];
[self removeLinkMetaFromTypingAttributes];
}
// used for conflicts, we have to remove the whole link
- (void)removeTyping {
NSRange linkRange =
- [self getFullLinkRangeAt:self.input->textView.selectedRange.location];
+ [self getFullLinkRangeAt:self.host.textView.selectedRange.location];
if (linkRange.length > 0) {
- [self.input->textView.textStorage beginEditing];
- [self.input->textView.textStorage removeAttribute:ManualLinkAttributeName
- range:linkRange];
- [self.input->textView.textStorage removeAttribute:AutomaticLinkAttributeName
- range:linkRange];
- [self.input->textView.textStorage endEditing];
- [self.input->attributesManager addDirtyRange:linkRange];
+ [self.host.textView.textStorage beginEditing];
+ [self.host.textView.textStorage removeAttribute:ManualLinkAttributeName
+ range:linkRange];
+ [self.host.textView.textStorage removeAttribute:AutomaticLinkAttributeName
+ range:linkRange];
+ [self.host.textView.textStorage endEditing];
+ [self.host.attributesManager addDirtyRange:linkRange];
}
[self removeLinkMetaFromTypingAttributes];
}
@@ -104,7 +104,7 @@ - (BOOL)detect:(NSRange)range {
if (range.length >= 1) {
BOOL onlyLinks = [OccurenceUtils
detectMultiple:@[ ManualLinkAttributeName, AutomaticLinkAttributeName ]
- withInput:self.input
+ withHost:self.host
inRange:range
withCondition:^BOOL(id _Nullable value, NSRange subrange) {
return [self styleCondition:value range:subrange];
@@ -117,7 +117,7 @@ - (BOOL)detect:(NSRange)range {
- (BOOL)any:(NSRange)range {
return [OccurenceUtils
anyMultiple:@[ ManualLinkAttributeName, AutomaticLinkAttributeName ]
- withInput:self.input
+ withHost:self.host
inRange:range
withCondition:^BOOL(id _Nullable value, NSRange subrange) {
return [self styleCondition:value range:subrange];
@@ -127,7 +127,7 @@ - (BOOL)any:(NSRange)range {
- (NSArray *)all:(NSRange)range {
return [OccurenceUtils
allMultiple:@[ ManualLinkAttributeName, AutomaticLinkAttributeName ]
- withInput:self.input
+ withHost:self.host
inRange:range
withCondition:^BOOL(id _Nullable value, NSRange subrange) {
return [self styleCondition:value range:subrange];
@@ -140,21 +140,21 @@ - (void)applyLinkMetaWithData:(LinkData *)linkData range:(NSRange)range {
}
NSString *key =
linkData.isManual ? ManualLinkAttributeName : AutomaticLinkAttributeName;
- [self.input->textView.textStorage addAttribute:key
- value:[linkData copy]
- range:range];
+ [self.host.textView.textStorage addAttribute:key
+ value:[linkData copy]
+ range:range];
}
- (void)removeLinkMetaFromTypingAttributes {
NSMutableDictionary *newTypingAttrs =
- [self.input->textView.typingAttributes mutableCopy];
+ [self.host.textView.typingAttributes mutableCopy];
[newTypingAttrs removeObjectForKey:ManualLinkAttributeName];
[newTypingAttrs removeObjectForKey:AutomaticLinkAttributeName];
- self.input->textView.typingAttributes = newTypingAttrs;
+ self.host.textView.typingAttributes = newTypingAttrs;
- [self.input->attributesManager
+ [self.host.attributesManager
didRemoveTypingAttribute:ManualLinkAttributeName];
- [self.input->attributesManager
+ [self.host.attributesManager
didRemoveTypingAttribute:AutomaticLinkAttributeName];
}
@@ -162,7 +162,7 @@ - (void)addLink:(LinkData *)linkData
range:(NSRange)range
withSelection:(BOOL)withSelection {
NSString *currentText =
- [self.input->textView.textStorage.string substringWithRange:range];
+ [self.host.textView.textStorage.string substringWithRange:range];
NSString *key =
linkData.isManual ? ManualLinkAttributeName : AutomaticLinkAttributeName;
@@ -175,7 +175,7 @@ - (void)addLink:(LinkData *)linkData
[TextInsertionUtils insertText:linkData.text
at:range.location
additionalAttributes:metaAttrs
- input:self.input
+ input:self.host
withSelection:withSelection];
dirtyRange = NSMakeRange(range.location, linkData.text.length);
} else if ([currentText isEqualToString:linkData.text]) {
@@ -185,8 +185,8 @@ - (void)addLink:(LinkData *)linkData
// manually set it behind the link ONLY with manual links, automatic ones
// don't need the selection fix
if (linkData.isManual && withSelection) {
- [self.input->textView reactFocus];
- self.input->textView.selectedRange =
+ [self.host.textView reactFocus];
+ self.host.textView.selectedRange =
NSMakeRange(range.location + linkData.text.length, 0);
}
} else {
@@ -194,18 +194,18 @@ - (void)addLink:(LinkData *)linkData
[TextInsertionUtils replaceText:linkData.text
at:range
additionalAttributes:metaAttrs
- input:self.input
+ input:self.host
withSelection:withSelection];
dirtyRange = NSMakeRange(range.location, linkData.text.length);
}
// add new dirty range
- [self.input->attributesManager addDirtyRange:dirtyRange];
+ [self.host.attributesManager addDirtyRange:dirtyRange];
// mandatory connected links check
NSDictionary *currentWord =
- [WordsUtils getCurrentWord:self.input->textView.textStorage.string
- range:self.input->textView.selectedRange];
+ [WordsUtils getCurrentWord:self.host.textView.textStorage.string
+ range:self.host.textView.selectedRange];
if (currentWord != nullptr) {
// get word properties
NSString *wordText = (NSString *)[currentWord objectForKey:@"word"];
@@ -221,24 +221,24 @@ - (void)addLink:(LinkData *)linkData
- (LinkData *)getLinkDataAt:(NSUInteger)location {
NSRange manualLinkRange = NSMakeRange(0, 0);
NSRange automaticLinkRange = NSMakeRange(0, 0);
- NSRange inputRange = NSMakeRange(0, self.input->textView.textStorage.length);
+ NSRange inputRange = NSMakeRange(0, self.host.textView.textStorage.length);
// don't search at the very end of input
NSUInteger searchLocation = location;
- if (searchLocation == self.input->textView.textStorage.length) {
+ if (searchLocation == self.host.textView.textStorage.length) {
return nullptr;
}
LinkData *manualData =
- [self.input->textView.textStorage attribute:ManualLinkAttributeName
- atIndex:searchLocation
- longestEffectiveRange:&manualLinkRange
- inRange:inputRange];
+ [self.host.textView.textStorage attribute:ManualLinkAttributeName
+ atIndex:searchLocation
+ longestEffectiveRange:&manualLinkRange
+ inRange:inputRange];
LinkData *automaticData =
- [self.input->textView.textStorage attribute:AutomaticLinkAttributeName
- atIndex:searchLocation
- longestEffectiveRange:&automaticLinkRange
- inRange:inputRange];
+ [self.host.textView.textStorage attribute:AutomaticLinkAttributeName
+ atIndex:searchLocation
+ longestEffectiveRange:&automaticLinkRange
+ inRange:inputRange];
if ((manualData == nullptr && automaticData == nullptr) ||
(manualLinkRange.length == 0 && automaticLinkRange.length == 0)) {
@@ -252,11 +252,11 @@ - (LinkData *)getLinkDataAt:(NSUInteger)location {
- (NSRange)getFullLinkRangeAt:(NSUInteger)location {
NSRange manualLinkRange = NSMakeRange(0, 0);
NSRange automaticLinkRange = NSMakeRange(0, 0);
- NSRange inputRange = NSMakeRange(0, self.input->textView.textStorage.length);
+ NSRange inputRange = NSMakeRange(0, self.host.textView.textStorage.length);
// get the previous index if possible when at the very end of input
NSUInteger searchLocation = location;
- if (searchLocation == self.input->textView.textStorage.length) {
+ if (searchLocation == self.host.textView.textStorage.length) {
if (searchLocation == 0) {
return NSMakeRange(0, 0);
}
@@ -264,15 +264,15 @@ - (NSRange)getFullLinkRangeAt:(NSUInteger)location {
}
LinkData *manualData =
- [self.input->textView.textStorage attribute:ManualLinkAttributeName
- atIndex:searchLocation
- longestEffectiveRange:&manualLinkRange
- inRange:inputRange];
+ [self.host.textView.textStorage attribute:ManualLinkAttributeName
+ atIndex:searchLocation
+ longestEffectiveRange:&manualLinkRange
+ inRange:inputRange];
LinkData *automaticData =
- [self.input->textView.textStorage attribute:AutomaticLinkAttributeName
- atIndex:searchLocation
- longestEffectiveRange:&automaticLinkRange
- inRange:inputRange];
+ [self.host.textView.textStorage attribute:AutomaticLinkAttributeName
+ atIndex:searchLocation
+ longestEffectiveRange:&automaticLinkRange
+ inRange:inputRange];
return manualData == nullptr
? automaticData == nullptr ? NSMakeRange(0, 0) : automaticLinkRange
@@ -281,7 +281,7 @@ - (NSRange)getFullLinkRangeAt:(NSUInteger)location {
// handles detecting and removing automatic links
- (void)handleAutomaticLinks:(NSString *)word inRange:(NSRange)wordRange {
- LinkRegexConfig *linkRegexConfig = [self.input->config linkRegexConfig];
+ LinkRegexConfig *linkRegexConfig = [self.host.config linkRegexConfig];
// no automatic links with isDisabled
if (linkRegexConfig.isDisabled) {
@@ -289,11 +289,11 @@ - (void)handleAutomaticLinks:(NSString *)word inRange:(NSRange)wordRange {
}
InlineCodeStyle *inlineCodeStyle =
- [self.input->stylesDict objectForKey:@([InlineCodeStyle getType])];
+ [self.host.stylesDict objectForKey:@([InlineCodeStyle getType])];
MentionStyle *mentionStyle =
- [self.input->stylesDict objectForKey:@([MentionStyle getType])];
+ [self.host.stylesDict objectForKey:@([MentionStyle getType])];
CodeBlockStyle *codeBlockStyle =
- [self.input->stylesDict objectForKey:@([CodeBlockStyle getType])];
+ [self.host.stylesDict objectForKey:@([CodeBlockStyle getType])];
// we don't recognize links along mentions, inline code or codeblocks
if (mentionStyle != nullptr && [mentionStyle any:wordRange]) {
@@ -311,7 +311,7 @@ - (void)handleAutomaticLinks:(NSString *)word inRange:(NSRange)wordRange {
// we don't recognize automatic links along manual ones
__block BOOL manualLinkPresent = NO;
- [self.input->textView.textStorage
+ [self.host.textView.textStorage
enumerateAttribute:ManualLinkAttributeName
inRange:wordRange
options:0
@@ -336,7 +336,7 @@ - (void)handleAutomaticLinks:(NSString *)word inRange:(NSRange)wordRange {
matchRange:matchingRange];
} else {
// use user defined regex if it exists
- NSRegularExpression *userRegex = [self.input->config parsedLinkRegex];
+ NSRegularExpression *userRegex = [self.host.config parsedLinkRegex];
if (userRegex == nullptr) {
// fallback to default regex
@@ -368,7 +368,7 @@ - (void)handleAutomaticLinks:(NSString *)word inRange:(NSRange)wordRange {
[self addLink:newData range:wordRange withSelection:NO];
// emit onLinkDetected if style was added
- [self.input emitOnLinkDetectedEvent:newData range:wordRange];
+ [(id)self.host emitOnLinkDetectedEvent:newData range:wordRange];
}
} else if ([self any:wordRange]) {
// there was some automatic link (because anyOccurence is true and we are
@@ -403,7 +403,7 @@ - (void)handleManualLinks:(NSString *)word inRange:(NSRange)wordRange {
__block NSInteger manualLinkMinIdx = -1;
__block NSInteger manualLinkMaxIdx = -1;
- [self.input->textView.textStorage
+ [self.host.textView.textStorage
enumerateAttribute:ManualLinkAttributeName
inRange:wordRange
options:0
@@ -437,7 +437,7 @@ - (void)handleManualLinks:(NSString *)word inRange:(NSRange)wordRange {
NSRange newRange =
NSMakeRange(manualLinkMinIdx, manualLinkMaxIdx - manualLinkMinIdx + 1);
[self applyLinkMetaWithData:manualLinkMinValue range:newRange];
- [self.input->attributesManager addDirtyRange:newRange];
+ [self.host.attributesManager addDirtyRange:newRange];
}
}
@@ -452,14 +452,14 @@ - (BOOL)isSingleLinkIn:(NSRange)range {
- (void)removeConnectedLinksIfNeeded:(NSString *)word range:(NSRange)wordRange {
BOOL anyAutomatic =
[OccurenceUtils any:AutomaticLinkAttributeName
- withInput:self.input
+ withHost:self.host
inRange:wordRange
withCondition:^BOOL(id _Nullable value, NSRange subrange) {
return [self styleCondition:value range:subrange];
}];
BOOL anyManual =
[OccurenceUtils any:ManualLinkAttributeName
- withInput:self.input
+ withHost:self.host
inRange:wordRange
withCondition:^BOOL(id _Nullable value, NSRange subrange) {
return [self styleCondition:value range:subrange];
@@ -474,7 +474,7 @@ - (void)removeConnectedLinksIfNeeded:(NSString *)word range:(NSRange)wordRange {
// covers the whole word
BOOL onlyLinks = [OccurenceUtils
detectMultiple:@[ ManualLinkAttributeName, AutomaticLinkAttributeName ]
- withInput:self.input
+ withHost:self.host
inRange:wordRange
withCondition:^BOOL(id _Nullable value, NSRange r) {
return [self styleCondition:value range:r];
diff --git a/ios/styles/MentionStyle.mm b/ios/styles/MentionStyle.mm
index bd34971e8..529bc0297 100644
--- a/ios/styles/MentionStyle.mm
+++ b/ios/styles/MentionStyle.mm
@@ -27,8 +27,8 @@ - (BOOL)isParagraph {
return NO;
}
-- (instancetype)initWithInput:(id)input {
- self = [super initWithInput:(EnrichedTextInputView *)input];
+- (instancetype)initWithHost:(id)host {
+ self = [super initWithHost:host];
if (self) {
_activeMentionRange = nullptr;
_activeMentionIndicator = nullptr;
@@ -47,7 +47,7 @@ - (void)applyStyling:(NSRange)range {
}
MentionStyleProps *styleProps =
- [self.input->config mentionStylePropsForIndicator:params.indicator];
+ [self.host.config mentionStylePropsForIndicator:params.indicator];
NSMutableDictionary *newAttrs = [@{
NSForegroundColorAttributeName : styleProps.color,
@@ -61,7 +61,7 @@ - (void)applyStyling:(NSRange)range {
newAttrs[NSUnderlineStyleAttributeName] = @(NSUnderlineStyleSingle);
}
- [self.input->textView.textStorage addAttributes:newAttrs range:range];
+ [self.host.textView.textStorage addAttributes:newAttrs range:range];
}
- (void)reapplyFromStylePair:(StylePair *)pair {
@@ -86,20 +86,20 @@ - (void)toggle:(NSRange)range {
// other styles
- (void)remove:(NSRange)range withDirtyRange:(BOOL)withDirtyRange {
NSArray *mentions = [self all:range];
- [self.input->textView.textStorage beginEditing];
+ [self.host.textView.textStorage beginEditing];
for (StylePair *pair in mentions) {
NSRange mentionRange =
[self getFullMentionRangeAt:[pair.rangeValue rangeValue].location];
if (mentionRange.length == 0) {
continue;
}
- [self.input->textView.textStorage removeAttribute:MentionAttributeName
- range:mentionRange];
+ [self.host.textView.textStorage removeAttribute:MentionAttributeName
+ range:mentionRange];
if (withDirtyRange) {
- [self.input->attributesManager addDirtyRange:mentionRange];
+ [self.host.attributesManager addDirtyRange:mentionRange];
}
}
- [self.input->textView.textStorage endEditing];
+ [self.host.textView.textStorage endEditing];
[super removeTyping];
}
@@ -107,13 +107,13 @@ - (void)remove:(NSRange)range withDirtyRange:(BOOL)withDirtyRange {
// used for conflicts, we have to remove the whole mention
- (void)removeTyping {
NSRange mentionRange =
- [self getFullMentionRangeAt:self.input->textView.selectedRange.location];
+ [self getFullMentionRangeAt:self.host.textView.selectedRange.location];
if (mentionRange.length > 0) {
- [self.input->textView.textStorage beginEditing];
- [self.input->textView.textStorage removeAttribute:MentionAttributeName
- range:mentionRange];
- [self.input->textView.textStorage endEditing];
- [self.input->attributesManager addDirtyRange:mentionRange];
+ [self.host.textView.textStorage beginEditing];
+ [self.host.textView.textStorage removeAttribute:MentionAttributeName
+ range:mentionRange];
+ [self.host.textView.textStorage endEditing];
+ [self.host.attributesManager addDirtyRange:mentionRange];
}
[super removeTyping];
}
@@ -131,9 +131,9 @@ - (BOOL)detect:(NSRange)range {
}
- (void)applyMentionMeta:(MentionParams *)params range:(NSRange)range {
- [self.input->textView.textStorage addAttribute:MentionAttributeName
- value:params
- range:range];
+ [self.host.textView.textStorage addAttribute:MentionAttributeName
+ value:params
+ range:range];
}
// MARK: - Public non-standard methods
@@ -158,13 +158,13 @@ - (void)addMention:(NSString *)indicator
[TextInsertionUtils replaceText:newText
at:rangeToBeReplaced
additionalAttributes:nullptr
- input:self.input
+ input:self.host
withSelection:YES];
// THEN, add the attributes to not apply them on the space
NSRange mentionRange = NSMakeRange(rangeToBeReplaced.location, text.length);
[self applyMentionMeta:params range:mentionRange];
- [self.input->attributesManager addDirtyRange:mentionRange];
+ [self.host.attributesManager addDirtyRange:mentionRange];
// mention editing should finish
[self removeActiveMentionRange];
@@ -175,19 +175,19 @@ - (void)addMentionAtRange:(NSRange)range params:(MentionParams *)params {
_blockMentionEditing = YES;
[self applyMentionMeta:params range:range];
- [self.input->attributesManager addDirtyRange:range];
+ [self.host.attributesManager addDirtyRange:range];
_blockMentionEditing = NO;
}
- (void)startMentionWithIndicator:(NSString *)indicator {
- NSRange currentRange = self.input->textView.selectedRange;
+ NSRange currentRange = self.host.textView.selectedRange;
BOOL addSpaceBefore = NO;
BOOL addSpaceAfter = NO;
if (currentRange.location > 0) {
- unichar charBefore = [self.input->textView.textStorage.string
+ unichar charBefore = [self.host.textView.textStorage.string
characterAtIndex:(currentRange.location - 1)];
if (![[NSCharacterSet whitespaceAndNewlineCharacterSet]
characterIsMember:charBefore]) {
@@ -196,8 +196,8 @@ - (void)startMentionWithIndicator:(NSString *)indicator {
}
if (currentRange.location + currentRange.length <
- self.input->textView.textStorage.string.length) {
- unichar charAfter = [self.input->textView.textStorage.string
+ self.host.textView.textStorage.string.length) {
+ unichar charAfter = [self.host.textView.textStorage.string
characterAtIndex:(currentRange.location + currentRange.length)];
if (![[NSCharacterSet whitespaceAndNewlineCharacterSet]
characterIsMember:charAfter]) {
@@ -216,18 +216,18 @@ - (void)startMentionWithIndicator:(NSString *)indicator {
[TextInsertionUtils insertText:finalString
at:currentRange.location
additionalAttributes:nullptr
- input:self.input
+ input:self.host
withSelection:NO];
} else {
[TextInsertionUtils replaceText:finalString
at:currentRange
additionalAttributes:nullptr
- input:self.input
+ input:self.host
withSelection:NO];
}
- [self.input->textView reactFocus];
- self.input->textView.selectedRange = newSelect;
+ [self.host.textView reactFocus];
+ self.host.textView.selectedRange = newSelect;
}
// handles removing no longer valid mentions
@@ -237,7 +237,7 @@ - (void)handleExistingMentions {
// any number of spaces, which makes one mention any number of words long
NSRange wholeText =
- NSMakeRange(0, self.input->textView.textStorage.string.length);
+ NSMakeRange(0, self.host.textView.textStorage.string.length);
// get mentions in ascending range.location order
NSArray *mentions = [[self all:wholeText]
sortedArrayUsingComparator:^NSComparisonResult(id _Nonnull obj1,
@@ -271,8 +271,8 @@ - (void)handleExistingMentions {
}
// check for text, any modifications to it makes mention invalid
- NSString *existingText = [self.input->textView.textStorage.string
- substringWithRange:currentRange];
+ NSString *existingText =
+ [self.host.textView.textStorage.string substringWithRange:currentRange];
if (![existingText isEqualToString:currentText]) {
[rangesToRemove addObject:[NSValue valueWithRange:currentRange]];
}
@@ -291,7 +291,7 @@ - (void)manageMentionEditing {
}
// we don't take longer selections into consideration
- if (self.input->textView.selectedRange.length > 0) {
+ if (self.host.textView.selectedRange.length > 0) {
[self removeActiveMentionRange];
return;
}
@@ -307,14 +307,14 @@ - (void)manageMentionEditing {
// get style classes that the mention shouldn't be recognized in, together
// with other mentions
- NSArray *conflicts = self.input->conflictingStyles[@([MentionStyle getType])];
- NSArray *blocks = self.input->blockingStyles[@([MentionStyle getType])];
+ NSArray *conflicts = self.host.conflictingStyles[@([MentionStyle getType])];
+ NSArray *blocks = self.host.blockingStyles[@([MentionStyle getType])];
NSArray *allConflicts = [[conflicts arrayByAddingObjectsFromArray:blocks]
arrayByAddingObject:@([MentionStyle getType])];
BOOL conflictingStyle = NO;
for (NSNumber *styleType in allConflicts) {
- StyleBase *styleInst = self.input->stylesDict[styleType];
+ StyleBase *styleInst = self.host.stylesDict[styleType];
if (styleInst != nullptr && [styleInst any:candidateRange]) {
conflictingStyle = YES;
break;
@@ -334,19 +334,19 @@ - (void)manageMentionEditing {
// returns mention params if it exists
- (MentionParams *)getMentionParamsAt:(NSUInteger)location {
NSRange mentionRange = NSMakeRange(0, 0);
- NSRange inputRange = NSMakeRange(0, self.input->textView.textStorage.length);
+ NSRange inputRange = NSMakeRange(0, self.host.textView.textStorage.length);
// don't search at the very end of input
NSUInteger searchLocation = location;
- if (searchLocation == self.input->textView.textStorage.length) {
+ if (searchLocation == self.host.textView.textStorage.length) {
return nullptr;
}
MentionParams *value =
- [self.input->textView.textStorage attribute:MentionAttributeName
- atIndex:searchLocation
- longestEffectiveRange:&mentionRange
- inRange:inputRange];
+ [self.host.textView.textStorage attribute:MentionAttributeName
+ atIndex:searchLocation
+ longestEffectiveRange:&mentionRange
+ inRange:inputRange];
return value;
}
@@ -357,26 +357,26 @@ - (NSValue *)getActiveMentionRange {
// returns full range of a mention at some location
- (NSRange)getFullMentionRangeAt:(NSUInteger)location {
NSRange mentionRange = NSMakeRange(0, 0);
- NSRange inputRange = NSMakeRange(0, self.input->textView.textStorage.length);
+ NSRange inputRange = NSMakeRange(0, self.host.textView.textStorage.length);
// get the previous index if possible when at the very end of input
NSUInteger searchLocation = location;
- if (searchLocation == self.input->textView.textStorage.length) {
+ if (searchLocation == self.host.textView.textStorage.length) {
if (searchLocation == 0) {
return mentionRange;
}
searchLocation = searchLocation - 1;
}
- [self.input->textView.textStorage attribute:MentionAttributeName
- atIndex:searchLocation
- longestEffectiveRange:&mentionRange
- inRange:inputRange];
+ [self.host.textView.textStorage attribute:MentionAttributeName
+ atIndex:searchLocation
+ longestEffectiveRange:&mentionRange
+ inRange:inputRange];
return mentionRange;
}
- (MentionStyleProps *)stylePropsWithParams:(MentionParams *)params {
- return [self.input->config mentionStylePropsForIndicator:params.indicator];
+ return [self.host.config mentionStylePropsForIndicator:params.indicator];
}
// finds if any word/words around current selection are eligible to be edited as
@@ -389,9 +389,8 @@ - (NSArray *)getMentionCandidate {
NSRange finalRange;
// word at the current selection
- currentWord =
- [WordsUtils getCurrentWord:self.input->textView.textStorage.string
- range:self.input->textView.selectedRange];
+ currentWord = [WordsUtils getCurrentWord:self.host.textView.textStorage.string
+ range:self.host.textView.selectedRange];
if (currentWord != nullptr) {
currentWordText = (NSString *)[currentWord objectForKey:@"word"];
currentWordRange = (NSValue *)[currentWord objectForKey:@"range"];
@@ -401,7 +400,7 @@ - (NSArray *)getMentionCandidate {
// current word exists
unichar currentFirstChar = [currentWordText characterAtIndex:0];
- if ([[self.input->config mentionIndicators]
+ if ([[self.host.config mentionIndicators]
containsObject:@(currentFirstChar)]) {
// current word exists and has a mention indicator; no need to check for
// the previous word
@@ -418,7 +417,7 @@ - (NSArray *)getMentionCandidate {
return nullptr;
}
- unichar separatorChar = [self.input->textView.textStorage.string
+ unichar separatorChar = [self.host.textView.textStorage.string
characterAtIndex:previousWordSearchLocation];
if (![[NSCharacterSet whitespaceCharacterSet]
characterIsMember:separatorChar]) {
@@ -428,7 +427,7 @@ - (NSArray *)getMentionCandidate {
}
previousWord = [WordsUtils
- getCurrentWord:self.input->textView.textStorage.string
+ getCurrentWord:self.host.textView.textStorage.string
range:NSMakeRange(previousWordSearchLocation, 0)];
if (previousWord != nullptr) {
@@ -439,7 +438,7 @@ - (NSArray *)getMentionCandidate {
// check for the mention indicators in the previous word
unichar previousFirstChar = [previousWordText characterAtIndex:0];
- if ([[self.input->config mentionIndicators]
+ if ([[self.host.config mentionIndicators]
containsObject:@(previousFirstChar)]) {
// previous word has a proper mention indicator: treat both words as
// an editable mention
@@ -464,13 +463,13 @@ - (NSArray *)getMentionCandidate {
// current word doesn't exist; try getting the previous one
NSInteger previousWordSearchLocation =
- self.input->textView.selectedRange.location - 1;
+ self.host.textView.selectedRange.location - 1;
if (previousWordSearchLocation < 0) {
// previous word can't exist
return nullptr;
}
- unichar separatorChar = [self.input->textView.textStorage.string
+ unichar separatorChar = [self.host.textView.textStorage.string
characterAtIndex:previousWordSearchLocation];
if (![[NSCharacterSet whitespaceCharacterSet]
characterIsMember:separatorChar]) {
@@ -480,7 +479,7 @@ - (NSArray *)getMentionCandidate {
}
previousWord =
- [WordsUtils getCurrentWord:self.input->textView.textStorage.string
+ [WordsUtils getCurrentWord:self.host.textView.textStorage.string
range:NSMakeRange(previousWordSearchLocation, 0)];
if (previousWord != nullptr) {
@@ -491,7 +490,7 @@ - (NSArray *)getMentionCandidate {
// check for the mention indicators in the previous word
unichar previousFirstChar = [previousWordText characterAtIndex:0];
- if ([[self.input->config mentionIndicators]
+ if ([[self.host.config mentionIndicators]
containsObject:@(previousFirstChar)]) {
// previous word has a proper mention indicator; treat previous word + a
// space as a editable mention
@@ -521,7 +520,7 @@ - (void)setActiveMentionRange:(NSRange)range text:(NSString *)text {
[text substringWithRange:NSMakeRange(1, text.length - 1)];
_activeMentionIndicator = indicatorString;
_activeMentionRange = [NSValue valueWithRange:range];
- [self.input emitOnMentionEvent:indicatorString text:textString];
+ [(id)self.host emitOnMentionEvent:indicatorString text:textString];
}
// removes stored mention range + indicator, which means that we no longer edit
@@ -531,7 +530,7 @@ - (void)removeActiveMentionRange {
NSString *indicatorCopy = [_activeMentionIndicator copy];
_activeMentionIndicator = nullptr;
_activeMentionRange = nullptr;
- [self.input emitOnMentionEvent:indicatorCopy text:nullptr];
+ [(id)self.host emitOnMentionEvent:indicatorCopy text:nullptr];
}
}
diff --git a/ios/styles/OrderedListStyle.mm b/ios/styles/OrderedListStyle.mm
index 60a6089a5..291d1a98c 100644
--- a/ios/styles/OrderedListStyle.mm
+++ b/ios/styles/OrderedListStyle.mm
@@ -24,10 +24,10 @@ - (BOOL)needsZWS {
- (void)applyStyling:(NSRange)range {
// lists are drawn manually
// margin before marker + gap between marker and paragraph
- CGFloat listHeadIndent = [self.input->config orderedListMarginLeft] +
- [self.input->config orderedListGapWidth];
+ CGFloat listHeadIndent = [self.host.config orderedListMarginLeft] +
+ [self.host.config orderedListGapWidth];
- [self.input->textView.textStorage
+ [self.host.textView.textStorage
enumerateAttribute:NSParagraphStyleAttributeName
inRange:range
options:0
@@ -37,7 +37,7 @@ - (void)applyStyling:(NSRange)range {
[(NSParagraphStyle *)value mutableCopy];
pStyle.headIndent = listHeadIndent;
pStyle.firstLineHeadIndent = listHeadIndent;
- [self.input->textView.textStorage
+ [self.host.textView.textStorage
addAttribute:NSParagraphStyleAttributeName
value:pStyle
range:range];
@@ -47,28 +47,28 @@ - (void)applyStyling:(NSRange)range {
- (BOOL)tryHandlingListShorcutInRange:(NSRange)range
replacementText:(NSString *)text {
NSRange paragraphRange =
- [self.input->textView.textStorage.string paragraphRangeForRange:range];
+ [self.host.textView.textStorage.string paragraphRangeForRange:range];
// a dot was added - check if we are both at the paragraph beginning + 1
// character (which we want to be a digit '1')
if ([text isEqualToString:@"."] &&
range.location - 1 == paragraphRange.location) {
- unichar charBefore = [self.input->textView.textStorage.string
+ unichar charBefore = [self.host.textView.textStorage.string
characterAtIndex:range.location - 1];
if (charBefore == '1') {
// we got a match - add a list if possible
- if ([self.input handleStyleBlocksAndConflicts:[[self class] getType]
- range:paragraphRange]) {
+ if ([self.host handleStyleBlocksAndConflicts:[[self class] getType]
+ range:paragraphRange]) {
// don't emit during the replacing
- self.input->blockEmitting = YES;
+ self.host.blockEmitting = YES;
// remove the number
[TextInsertionUtils replaceText:@""
at:NSMakeRange(paragraphRange.location, 1)
additionalAttributes:nullptr
- input:self.input
+ input:self.host
withSelection:YES];
- self.input->blockEmitting = NO;
+ self.host.blockEmitting = NO;
// add attributes on the paragraph
[self add:NSMakeRange(paragraphRange.location,
diff --git a/ios/styles/StrikethroughStyle.mm b/ios/styles/StrikethroughStyle.mm
index ad829df31..5b5883389 100644
--- a/ios/styles/StrikethroughStyle.mm
+++ b/ios/styles/StrikethroughStyle.mm
@@ -16,10 +16,10 @@ - (BOOL)isParagraph {
}
- (void)applyStyling:(NSRange)range {
- [self.input->textView.textStorage addAttributes:@{
+ [self.host.textView.textStorage addAttributes:@{
NSStrikethroughStyleAttributeName : @(NSUnderlineStyleSingle)
}
- range:range];
+ range:range];
}
@end
diff --git a/ios/styles/UnderlineStyle.mm b/ios/styles/UnderlineStyle.mm
index 2d30eec58..c09a0fae3 100644
--- a/ios/styles/UnderlineStyle.mm
+++ b/ios/styles/UnderlineStyle.mm
@@ -16,9 +16,9 @@ - (BOOL)isParagraph {
}
- (void)applyStyling:(NSRange)range {
- [self.input->textView.textStorage addAttribute:NSUnderlineStyleAttributeName
- value:@(NSUnderlineStyleSingle)
- range:range];
+ [self.host.textView.textStorage addAttribute:NSUnderlineStyleAttributeName
+ value:@(NSUnderlineStyleSingle)
+ range:range];
}
@end
diff --git a/ios/styles/UnorderedListStyle.mm b/ios/styles/UnorderedListStyle.mm
index 2e0a1a103..8044ab3de 100644
--- a/ios/styles/UnorderedListStyle.mm
+++ b/ios/styles/UnorderedListStyle.mm
@@ -24,10 +24,10 @@ - (BOOL)needsZWS {
- (void)applyStyling:(NSRange)range {
// lists are drawn manually
// margin before bullet + gap between bullet and paragraph
- CGFloat listHeadIndent = [self.input->config unorderedListMarginLeft] +
- [self.input->config unorderedListGapWidth];
+ CGFloat listHeadIndent = [self.host.config unorderedListMarginLeft] +
+ [self.host.config unorderedListGapWidth];
- [self.input->textView.textStorage
+ [self.host.textView.textStorage
enumerateAttribute:NSParagraphStyleAttributeName
inRange:range
options:0
@@ -37,7 +37,7 @@ - (void)applyStyling:(NSRange)range {
[(NSParagraphStyle *)value mutableCopy];
pStyle.headIndent = listHeadIndent;
pStyle.firstLineHeadIndent = listHeadIndent;
- [self.input->textView.textStorage
+ [self.host.textView.textStorage
addAttribute:NSParagraphStyleAttributeName
value:pStyle
range:range];
@@ -47,28 +47,28 @@ - (void)applyStyling:(NSRange)range {
- (BOOL)tryHandlingListShorcutInRange:(NSRange)range
replacementText:(NSString *)text {
NSRange paragraphRange =
- [self.input->textView.textStorage.string paragraphRangeForRange:range];
+ [self.host.textView.textStorage.string paragraphRangeForRange:range];
// space was added - check if we are both at the paragraph beginning + 1
// character (which we want to be a dash)
if ([text isEqualToString:@" "] &&
range.location - 1 == paragraphRange.location) {
- unichar charBefore = [self.input->textView.textStorage.string
+ unichar charBefore = [self.host.textView.textStorage.string
characterAtIndex:range.location - 1];
if (charBefore == '-') {
// we got a match - add a list if possible
- if ([self.input handleStyleBlocksAndConflicts:[[self class] getType]
- range:paragraphRange]) {
+ if ([self.host handleStyleBlocksAndConflicts:[[self class] getType]
+ range:paragraphRange]) {
// don't emit during the replacing
- self.input->blockEmitting = YES;
+ self.host.blockEmitting = YES;
// remove the dash
[TextInsertionUtils replaceText:@""
at:NSMakeRange(paragraphRange.location, 1)
additionalAttributes:nullptr
- input:self.input
+ input:self.host
withSelection:YES];
- self.input->blockEmitting = NO;
+ self.host.blockEmitting = NO;
// add attributes on the dashless paragraph
[self add:NSMakeRange(paragraphRange.location,
diff --git a/ios/utils/AttachmentLayoutUtils.h b/ios/utils/AttachmentLayoutUtils.h
new file mode 100644
index 000000000..79b6515d6
--- /dev/null
+++ b/ios/utils/AttachmentLayoutUtils.h
@@ -0,0 +1,24 @@
+#pragma once
+#import "ImageAttachment.h"
+#import "InputConfig.h"
+#import
+
+@interface AttachmentLayoutUtils : NSObject
+
++ (void)handleAttachmentUpdate:(MediaAttachment *)attachment
+ textView:(UITextView *)textView
+ onLayoutBlock:(dispatch_block_t)layoutBlock;
+
++ (NSMutableDictionary *)
+ layoutAttachmentsInTextView:(UITextView *)textView
+ config:(InputConfig *)config
+ existingViews:
+ (NSMutableDictionary *)
+ attachmentViews;
+
++ (CGRect)frameForAttachment:(ImageAttachment *)attachment
+ atRange:(NSRange)range
+ textView:(UITextView *)textView
+ config:(InputConfig *)config;
+
+@end
diff --git a/ios/utils/AttachmentLayoutUtils.mm b/ios/utils/AttachmentLayoutUtils.mm
new file mode 100644
index 000000000..1b08ad750
--- /dev/null
+++ b/ios/utils/AttachmentLayoutUtils.mm
@@ -0,0 +1,143 @@
+#import "AttachmentLayoutUtils.h"
+
+@implementation AttachmentLayoutUtils
+
++ (void)handleAttachmentUpdate:(MediaAttachment *)attachment
+ textView:(UITextView *)textView
+ onLayoutBlock:(dispatch_block_t)layoutBlock {
+ NSTextStorage *storage = textView.textStorage;
+ NSRange fullRange = NSMakeRange(0, storage.length);
+
+ __block NSRange foundRange = NSMakeRange(NSNotFound, 0);
+
+ [storage enumerateAttribute:NSAttachmentAttributeName
+ inRange:fullRange
+ options:0
+ usingBlock:^(id value, NSRange range, BOOL *stop) {
+ if (value == attachment) {
+ foundRange = range;
+ *stop = YES;
+ }
+ }];
+
+ if (foundRange.location == NSNotFound) {
+ return;
+ }
+
+ [storage edited:NSTextStorageEditedAttributes
+ range:foundRange
+ changeInLength:0];
+
+ dispatch_async(dispatch_get_main_queue(), layoutBlock);
+}
+
++ (NSMutableDictionary *)
+ layoutAttachmentsInTextView:(UITextView *)textView
+ config:(InputConfig *)config
+ existingViews:
+ (NSMutableDictionary *)
+ attachmentViews {
+ NSTextStorage *storage = textView.textStorage;
+ if (storage.length == 0)
+ return attachmentViews;
+
+ NSMutableDictionary *activeAttachmentViews =
+ [NSMutableDictionary dictionary];
+
+ // Iterate over the entire text to find ImageAttachments
+ [storage enumerateAttribute:NSAttachmentAttributeName
+ inRange:NSMakeRange(0, storage.length)
+ options:0
+ usingBlock:^(id value, NSRange range, BOOL *stop) {
+ if ([value isKindOfClass:[ImageAttachment class]]) {
+ ImageAttachment *attachment = (ImageAttachment *)value;
+
+ CGRect rect = [self frameForAttachment:attachment
+ atRange:range
+ textView:textView
+ config:config];
+
+ // Get or Create the UIImageView for this specific
+ // attachment key
+ NSValue *key =
+ [NSValue valueWithNonretainedObject:attachment];
+ UIImageView *imgView = attachmentViews[key];
+
+ if (!imgView) {
+ // It doesn't exist yet, create it
+ imgView = [[UIImageView alloc] initWithFrame:rect];
+ imgView.contentMode = UIViewContentModeScaleAspectFit;
+ imgView.tintColor = [UIColor labelColor];
+
+ // Add it directly to the TextView
+ [textView addSubview:imgView];
+ }
+
+ // Update position (in case text moved/scrolled)
+ if (!CGRectEqualToRect(imgView.frame, rect)) {
+ imgView.frame = rect;
+ }
+ UIImage *targetImage =
+ attachment.storedAnimatedImage ?: attachment.image;
+
+ // Only set if different to avoid resetting the animation
+ // loop
+ if (imgView.image != targetImage) {
+ imgView.image = targetImage;
+ }
+
+ // Ensure it is visible on top
+ imgView.hidden = NO;
+ [textView bringSubviewToFront:imgView];
+
+ activeAttachmentViews[key] = imgView;
+ // Remove from the old map so we know it has been claimed
+ [attachmentViews removeObjectForKey:key];
+ }
+ }];
+
+ // Everything remaining in attachmentViews is dead or off-screen
+ for (UIImageView *danglingView in attachmentViews.allValues) {
+ [danglingView removeFromSuperview];
+ }
+
+ return activeAttachmentViews;
+}
+
++ (CGRect)frameForAttachment:(ImageAttachment *)attachment
+ atRange:(NSRange)range
+ textView:(UITextView *)textView
+ config:(InputConfig *)config {
+ NSLayoutManager *layoutManager = textView.layoutManager;
+ NSTextContainer *textContainer = textView.textContainer;
+ NSTextStorage *storage = textView.textStorage;
+
+ NSRange glyphRange = [layoutManager glyphRangeForCharacterRange:range
+ actualCharacterRange:NULL];
+ CGRect glyphRect = [layoutManager boundingRectForGlyphRange:glyphRange
+ inTextContainer:textContainer];
+
+ CGRect lineRect =
+ [layoutManager lineFragmentRectForGlyphAtIndex:glyphRange.location
+ effectiveRange:NULL];
+ CGSize attachmentSize = attachment.bounds.size;
+
+ UIFont *font = [storage attribute:NSFontAttributeName
+ atIndex:range.location
+ effectiveRange:NULL];
+ if (!font) {
+ font = [config primaryFont];
+ }
+
+ // Calculate (Baseline Alignment)
+ CGFloat targetY =
+ CGRectGetMaxY(lineRect) + font.descender - attachmentSize.height;
+ CGRect rect =
+ CGRectMake(glyphRect.origin.x + textView.textContainerInset.left,
+ targetY + textView.textContainerInset.top,
+ attachmentSize.width, attachmentSize.height);
+
+ return CGRectIntegral(rect);
+}
+
+@end
diff --git a/ios/utils/OccurenceUtils.h b/ios/utils/OccurenceUtils.h
index fd0143d57..a27ce313e 100644
--- a/ios/utils/OccurenceUtils.h
+++ b/ios/utils/OccurenceUtils.h
@@ -1,43 +1,43 @@
#pragma once
-#import "EnrichedTextInputView.h"
+#import "EnrichedStyleHost.h"
#import "StylePair.h"
@interface OccurenceUtils : NSObject
+ (BOOL)detect:(NSAttributedStringKey _Nonnull)key
- withInput:(EnrichedTextInputView *_Nonnull)input
+ withHost:(id _Nonnull)host
inRange:(NSRange)range
withCondition:(BOOL(NS_NOESCAPE ^ _Nonnull)(id _Nullable value,
NSRange range))condition;
+ (BOOL)detect:(NSAttributedStringKey _Nonnull)key
- withInput:(EnrichedTextInputView *_Nonnull)input
+ withHost:(id _Nonnull)host
atIndex:(NSUInteger)index
checkPrevious:(BOOL)checkPrev
withCondition:(BOOL(NS_NOESCAPE ^ _Nonnull)(id _Nullable value,
NSRange range))condition;
+ (BOOL)detectMultiple:(NSArray *_Nonnull)keys
- withInput:(EnrichedTextInputView *_Nonnull)input
+ withHost:(id _Nonnull)host
inRange:(NSRange)range
withCondition:(BOOL(NS_NOESCAPE ^ _Nonnull)(id _Nullable value,
NSRange range))condition;
+ (BOOL)any:(NSAttributedStringKey _Nonnull)key
- withInput:(EnrichedTextInputView *_Nonnull)input
+ withHost:(id _Nonnull)host
inRange:(NSRange)range
withCondition:(BOOL(NS_NOESCAPE ^ _Nonnull)(id _Nullable value,
NSRange range))condition;
+ (BOOL)anyMultiple:(NSArray *_Nonnull)keys
- withInput:(EnrichedTextInputView *_Nonnull)input
+ withHost:(id _Nonnull)host
inRange:(NSRange)range
withCondition:(BOOL(NS_NOESCAPE ^ _Nonnull)(id _Nullable value,
NSRange range))condition;
+ (NSArray *_Nullable)all:(NSAttributedStringKey _Nonnull)key
- withInput:(EnrichedTextInputView *_Nonnull)input
+ withHost:(id _Nonnull)host
inRange:(NSRange)range
withCondition:(BOOL(NS_NOESCAPE ^
_Nonnull)(id _Nullable value,
NSRange range))condition;
+ (NSArray *_Nullable)
allMultiple:(NSArray *_Nonnull)keys
- withInput:(EnrichedTextInputView *_Nonnull)input
+ withHost:(id _Nonnull)host
inRange:(NSRange)range
withCondition:(BOOL(NS_NOESCAPE ^ _Nonnull)(id _Nullable value,
NSRange range))condition;
diff --git a/ios/utils/OccurenceUtils.mm b/ios/utils/OccurenceUtils.mm
index 872783c6c..bab3685a6 100644
--- a/ios/utils/OccurenceUtils.mm
+++ b/ios/utils/OccurenceUtils.mm
@@ -4,12 +4,12 @@
@implementation OccurenceUtils
+ (BOOL)detect:(NSAttributedStringKey _Nonnull)key
- withInput:(EnrichedTextInputView *_Nonnull)input
+ withHost:(id _Nonnull)host
inRange:(NSRange)range
withCondition:(BOOL(NS_NOESCAPE ^ _Nonnull)(id _Nullable value,
NSRange range))condition {
__block NSInteger totalLength = 0;
- [input->textView.textStorage
+ [host.textView.textStorage
enumerateAttribute:key
inRange:range
options:0
@@ -26,24 +26,24 @@ + (BOOL)detect:(NSAttributedStringKey _Nonnull)key
// it means that first character of paragraph will be checked instead if the
// detection is not in input's selected range and at the end of the input
+ (BOOL)detect:(NSAttributedStringKey _Nonnull)key
- withInput:(EnrichedTextInputView *_Nonnull)input
+ withHost:(id _Nonnull)host
atIndex:(NSUInteger)index
checkPrevious:(BOOL)checkPrev
withCondition:(BOOL(NS_NOESCAPE ^ _Nonnull)(id _Nullable value,
NSRange range))condition {
NSRange detectionRange = NSMakeRange(index, 0);
id attrValue;
- if (NSEqualRanges(input->textView.selectedRange, detectionRange)) {
- attrValue = input->textView.typingAttributes[key];
- } else if (index == input->textView.textStorage.string.length) {
+ if (NSEqualRanges(host.textView.selectedRange, detectionRange)) {
+ attrValue = host.textView.typingAttributes[key];
+ } else if (index == host.textView.textStorage.string.length) {
if (checkPrev) {
- NSRange paragraphRange = [input->textView.textStorage.string
+ NSRange paragraphRange = [host.textView.textStorage.string
paragraphRangeForRange:detectionRange];
if (paragraphRange.location == detectionRange.location) {
return NO;
} else {
return [self detect:key
- withInput:input
+ withHost:host
inRange:NSMakeRange(paragraphRange.location, 1)
withCondition:condition];
}
@@ -52,21 +52,21 @@ + (BOOL)detect:(NSAttributedStringKey _Nonnull)key
}
} else {
NSRange attrRange = NSMakeRange(0, 0);
- attrValue = [input->textView.textStorage attribute:key
- atIndex:index
- effectiveRange:&attrRange];
+ attrValue = [host.textView.textStorage attribute:key
+ atIndex:index
+ effectiveRange:&attrRange];
}
return condition(attrValue, detectionRange);
}
+ (BOOL)detectMultiple:(NSArray *_Nonnull)keys
- withInput:(EnrichedTextInputView *_Nonnull)input
+ withHost:(id _Nonnull)host
inRange:(NSRange)range
withCondition:(BOOL(NS_NOESCAPE ^ _Nonnull)(id _Nullable value,
NSRange range))condition {
__block NSInteger totalLength = 0;
for (NSString *key in keys) {
- [input->textView.textStorage
+ [host.textView.textStorage
enumerateAttribute:key
inRange:range
options:0
@@ -81,12 +81,12 @@ + (BOOL)detectMultiple:(NSArray *_Nonnull)keys
}
+ (BOOL)any:(NSAttributedStringKey _Nonnull)key
- withInput:(EnrichedTextInputView *_Nonnull)input
+ withHost:(id _Nonnull)host
inRange:(NSRange)range
withCondition:(BOOL(NS_NOESCAPE ^ _Nonnull)(id _Nullable value,
NSRange range))condition {
__block BOOL found = NO;
- [input->textView.textStorage
+ [host.textView.textStorage
enumerateAttribute:key
inRange:range
options:0
@@ -101,13 +101,13 @@ + (BOOL)any:(NSAttributedStringKey _Nonnull)key
}
+ (BOOL)anyMultiple:(NSArray *_Nonnull)keys
- withInput:(EnrichedTextInputView *_Nonnull)input
+ withHost:(id _Nonnull)host
inRange:(NSRange)range
withCondition:(BOOL(NS_NOESCAPE ^ _Nonnull)(id _Nullable value,
NSRange range))condition {
__block BOOL found = NO;
for (NSString *key in keys) {
- [input->textView.textStorage
+ [host.textView.textStorage
enumerateAttribute:key
inRange:range
options:0
@@ -126,7 +126,7 @@ + (BOOL)anyMultiple:(NSArray *_Nonnull)keys
}
+ (NSArray *_Nullable)all:(NSAttributedStringKey _Nonnull)key
- withInput:(EnrichedTextInputView *_Nonnull)input
+ withHost:(id _Nonnull)host
inRange:(NSRange)range
withCondition:
(BOOL(NS_NOESCAPE ^ _Nonnull)(id _Nullable value,
@@ -134,7 +134,7 @@ + (BOOL)anyMultiple:(NSArray *_Nonnull)keys
condition {
__block NSMutableArray *occurences =
[[NSMutableArray alloc] init];
- [input->textView.textStorage
+ [host.textView.textStorage
enumerateAttribute:key
inRange:range
options:0
@@ -152,14 +152,14 @@ + (BOOL)anyMultiple:(NSArray *_Nonnull)keys
+ (NSArray *_Nullable)
allMultiple:(NSArray *_Nonnull)keys
- withInput:(EnrichedTextInputView *_Nonnull)input
+ withHost:(id _Nonnull)host
inRange:(NSRange)range
withCondition:(BOOL(NS_NOESCAPE ^ _Nonnull)(id _Nullable value,
NSRange range))condition {
__block NSMutableArray *occurences =
[[NSMutableArray alloc] init];
for (NSString *key in keys) {
- [input->textView.textStorage
+ [host.textView.textStorage
enumerateAttribute:key
inRange:range
options:0
From 732fcac98a7cf91c35e1132798907961885f3aed Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Kacper=20=C5=BB=C3=B3=C5=82kiewski?=
Date: Wed, 8 Apr 2026 10:48:49 +0200
Subject: [PATCH 02/19] feat: add EnrichedText shadow nodes
---
.../EnrichedTextComponentDescriptor.h | 19 +++++
ios/internals/EnrichedTextViewShadowNode.h | 34 +++++++++
ios/internals/EnrichedTextViewShadowNode.mm | 72 +++++++++++++++++++
ios/internals/EnrichedTextViewState.cpp | 7 ++
ios/internals/EnrichedTextViewState.h | 18 +++++
5 files changed, 150 insertions(+)
create mode 100644 ios/internals/EnrichedTextComponentDescriptor.h
create mode 100644 ios/internals/EnrichedTextViewShadowNode.h
create mode 100644 ios/internals/EnrichedTextViewShadowNode.mm
create mode 100644 ios/internals/EnrichedTextViewState.cpp
create mode 100644 ios/internals/EnrichedTextViewState.h
diff --git a/ios/internals/EnrichedTextComponentDescriptor.h b/ios/internals/EnrichedTextComponentDescriptor.h
new file mode 100644
index 000000000..996aec1fc
--- /dev/null
+++ b/ios/internals/EnrichedTextComponentDescriptor.h
@@ -0,0 +1,19 @@
+#pragma once
+#include
+#include
+#include
+#include
+
+namespace facebook::react {
+class EnrichedTextComponentDescriptor final
+ : public ConcreteComponentDescriptor {
+public:
+ using ConcreteComponentDescriptor::ConcreteComponentDescriptor;
+ void adopt(ShadowNode &shadowNode) const override {
+ react_native_assert(
+ dynamic_cast(&shadowNode));
+ ConcreteComponentDescriptor::adopt(shadowNode);
+ }
+};
+
+} // namespace facebook::react
diff --git a/ios/internals/EnrichedTextViewShadowNode.h b/ios/internals/EnrichedTextViewShadowNode.h
new file mode 100644
index 000000000..631c8a190
--- /dev/null
+++ b/ios/internals/EnrichedTextViewShadowNode.h
@@ -0,0 +1,34 @@
+#pragma once
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace facebook::react {
+
+JSI_EXPORT extern const char EnrichedTextViewComponentName[];
+
+class EnrichedTextViewShadowNode
+ : public ConcreteViewShadowNode<
+ EnrichedTextViewComponentName, EnrichedTextViewProps,
+ EnrichedTextViewEventEmitter, EnrichedTextViewState> {
+public:
+ using ConcreteViewShadowNode::ConcreteViewShadowNode;
+ Size
+ measureContent(const LayoutContext &layoutContext,
+ const LayoutConstraints &layoutConstraints) const override;
+
+ static ShadowNodeTraits BaseTraits() {
+ auto traits = ConcreteViewShadowNode::BaseTraits();
+ traits.set(ShadowNodeTraits::Trait::LeafYogaNode);
+ traits.set(ShadowNodeTraits::Trait::MeasurableYogaNode);
+ return traits;
+ }
+
+private:
+ id setupMockTextView_() const;
+};
+
+} // namespace facebook::react
diff --git a/ios/internals/EnrichedTextViewShadowNode.mm b/ios/internals/EnrichedTextViewShadowNode.mm
new file mode 100644
index 000000000..ac5bc500c
--- /dev/null
+++ b/ios/internals/EnrichedTextViewShadowNode.mm
@@ -0,0 +1,72 @@
+#import "EnrichedTextViewShadowNode.h"
+#import "CoreText/CoreText.h"
+#import
+#import
+#import
+
+namespace facebook::react {
+
+extern const char EnrichedTextViewComponentName[] = "EnrichedTextView";
+
+id EnrichedTextViewShadowNode::setupMockTextView_() const {
+ const int veryFarAway = 20000;
+ const int mockSize = 1000;
+ EnrichedTextView *mockView = [[EnrichedTextView alloc]
+ initWithFrame:(CGRectMake(veryFarAway, veryFarAway, mockSize, mockSize))];
+ const auto props = this->getProps();
+ [mockView updateProps:props oldProps:nullptr];
+ return mockView;
+}
+
+Size EnrichedTextViewShadowNode::measureContent(
+ const LayoutContext &layoutContext,
+ const LayoutConstraints &layoutConstraints) const {
+ const auto state = this->getStateData();
+ const auto componentRef = state.getComponentViewRef();
+ RCTInternalGenericWeakWrapper *weakWrapper =
+ (RCTInternalGenericWeakWrapper *)unwrapManagedObject(componentRef);
+
+ if (weakWrapper != nullptr) {
+ id componentObject = weakWrapper.object;
+ EnrichedTextView *typedComponentObject =
+ (EnrichedTextView *)componentObject;
+
+ if (typedComponentObject != nullptr) {
+ __block CGSize estimatedSize;
+
+ if ([NSThread isMainThread]) {
+ estimatedSize = [typedComponentObject
+ measureSize:layoutConstraints.maximumSize.width];
+ } else {
+ dispatch_sync(dispatch_get_main_queue(), ^{
+ estimatedSize = [typedComponentObject
+ measureSize:layoutConstraints.maximumSize.width];
+ });
+ }
+
+ return {estimatedSize.width,
+ MIN(estimatedSize.height, layoutConstraints.maximumSize.height)};
+ }
+ } else {
+ __block CGSize estimatedSize;
+
+ if ([NSThread isMainThread]) {
+ EnrichedTextView *mockView = setupMockTextView_();
+ estimatedSize =
+ [mockView measureSize:layoutConstraints.maximumSize.width];
+ } else {
+ dispatch_sync(dispatch_get_main_queue(), ^{
+ EnrichedTextView *mockView = setupMockTextView_();
+ estimatedSize =
+ [mockView measureSize:layoutConstraints.maximumSize.width];
+ });
+ }
+
+ return {estimatedSize.width,
+ MIN(estimatedSize.height, layoutConstraints.maximumSize.height)};
+ }
+
+ return Size();
+}
+
+} // namespace facebook::react
diff --git a/ios/internals/EnrichedTextViewState.cpp b/ios/internals/EnrichedTextViewState.cpp
new file mode 100644
index 000000000..ec988d054
--- /dev/null
+++ b/ios/internals/EnrichedTextViewState.cpp
@@ -0,0 +1,7 @@
+#include "EnrichedTextViewState.h"
+
+namespace facebook::react {
+std::shared_ptr EnrichedTextViewState::getComponentViewRef() const {
+ return componentViewRef_;
+}
+} // namespace facebook::react
diff --git a/ios/internals/EnrichedTextViewState.h b/ios/internals/EnrichedTextViewState.h
new file mode 100644
index 000000000..c32efa0f7
--- /dev/null
+++ b/ios/internals/EnrichedTextViewState.h
@@ -0,0 +1,18 @@
+#pragma once
+#include
+
+namespace facebook::react {
+
+class EnrichedTextViewState {
+public:
+ EnrichedTextViewState() : componentViewRef_(nullptr) {}
+ explicit EnrichedTextViewState(std::shared_ptr ref) {
+ componentViewRef_ = ref;
+ }
+ std::shared_ptr getComponentViewRef() const;
+
+private:
+ std::shared_ptr componentViewRef_{};
+};
+
+} // namespace facebook::react
From d9cf6e880d84000929a8e4ba6c64f96c9b64e6dc Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Kacper=20=C5=BB=C3=B3=C5=82kiewski?=
Date: Thu, 9 Apr 2026 16:24:44 +0200
Subject: [PATCH 03/19] fix: add StyleUtils
---
ios/EnrichedTextInputView.mm | 275 +++-------------------
ios/EnrichedTextViewManager.mm | 13 +
ios/htmlParser/HtmlParser.mm | 6 +-
ios/textStyles/EnrichedTextStyleHeaders.h | 59 +++++
ios/utils/StyleUtils.h | 30 +++
ios/utils/StyleUtils.mm | 268 +++++++++++++++++++++
6 files changed, 405 insertions(+), 246 deletions(-)
create mode 100644 ios/EnrichedTextViewManager.mm
create mode 100644 ios/textStyles/EnrichedTextStyleHeaders.h
create mode 100644 ios/utils/StyleUtils.h
create mode 100644 ios/utils/StyleUtils.mm
diff --git a/ios/EnrichedTextInputView.mm b/ios/EnrichedTextInputView.mm
index 75a84980f..78699dc6a 100644
--- a/ios/EnrichedTextInputView.mm
+++ b/ios/EnrichedTextInputView.mm
@@ -9,6 +9,7 @@
#import "RCTFabricComponentsPlugins.h"
#import "StringExtension.h"
#import "StyleHeaders.h"
+#import "StyleUtils.h"
#import "TextBlockTapGestureRecognizer.h"
#import "UIView+React.h"
#import "WordsUtils.h"
@@ -104,149 +105,9 @@ - (void)setDefaults {
defaultTypingAttributes =
[[NSMutableDictionary alloc] init];
- stylesDict = @{
- @([BoldStyle getType]) : [[BoldStyle alloc] initWithHost:self],
- @([ItalicStyle getType]) : [[ItalicStyle alloc] initWithHost:self],
- @([UnderlineStyle getType]) : [[UnderlineStyle alloc] initWithHost:self],
- @([StrikethroughStyle getType]) :
- [[StrikethroughStyle alloc] initWithHost:self],
- @([InlineCodeStyle getType]) : [[InlineCodeStyle alloc] initWithHost:self],
- @([LinkStyle getType]) : [[LinkStyle alloc] initWithHost:self],
- @([MentionStyle getType]) : [[MentionStyle alloc] initWithHost:self],
- @([H1Style getType]) : [[H1Style alloc] initWithHost:self],
- @([H2Style getType]) : [[H2Style alloc] initWithHost:self],
- @([H3Style getType]) : [[H3Style alloc] initWithHost:self],
- @([H4Style getType]) : [[H4Style alloc] initWithHost:self],
- @([H5Style getType]) : [[H5Style alloc] initWithHost:self],
- @([H6Style getType]) : [[H6Style alloc] initWithHost:self],
- @([UnorderedListStyle getType]) :
- [[UnorderedListStyle alloc] initWithHost:self],
- @([OrderedListStyle getType]) :
- [[OrderedListStyle alloc] initWithHost:self],
- @([CheckboxListStyle getType]) :
- [[CheckboxListStyle alloc] initWithHost:self],
- @([BlockQuoteStyle getType]) : [[BlockQuoteStyle alloc] initWithHost:self],
- @([CodeBlockStyle getType]) : [[CodeBlockStyle alloc] initWithHost:self],
- @([ImageStyle getType]) : [[ImageStyle alloc] initWithHost:self]
- };
-
- conflictingStyles = [@{
- @([BoldStyle getType]) : @[],
- @([ItalicStyle getType]) : @[],
- @([UnderlineStyle getType]) : @[],
- @([StrikethroughStyle getType]) : @[],
- @([InlineCodeStyle getType]) :
- @[ @([LinkStyle getType]), @([MentionStyle getType]) ],
- @([LinkStyle getType]) : @[
- @([InlineCodeStyle getType]), @([LinkStyle getType]),
- @([MentionStyle getType])
- ],
- @([MentionStyle getType]) :
- @[ @([InlineCodeStyle getType]), @([LinkStyle getType]) ],
- @([H1Style getType]) : @[
- @([H2Style getType]), @([H3Style getType]), @([H4Style getType]),
- @([H5Style getType]), @([H6Style getType]),
- @([UnorderedListStyle getType]), @([OrderedListStyle getType]),
- @([BlockQuoteStyle getType]), @([CodeBlockStyle getType]),
- @([CheckboxListStyle getType])
- ],
- @([H2Style getType]) : @[
- @([H1Style getType]), @([H3Style getType]), @([H4Style getType]),
- @([H5Style getType]), @([H6Style getType]),
- @([UnorderedListStyle getType]), @([OrderedListStyle getType]),
- @([BlockQuoteStyle getType]), @([CodeBlockStyle getType]),
- @([CheckboxListStyle getType])
- ],
- @([H3Style getType]) : @[
- @([H1Style getType]), @([H2Style getType]), @([H4Style getType]),
- @([H5Style getType]), @([H6Style getType]),
- @([UnorderedListStyle getType]), @([OrderedListStyle getType]),
- @([BlockQuoteStyle getType]), @([CodeBlockStyle getType]),
- @([CheckboxListStyle getType])
- ],
- @([H4Style getType]) : @[
- @([H1Style getType]), @([H2Style getType]), @([H3Style getType]),
- @([H5Style getType]), @([H6Style getType]),
- @([UnorderedListStyle getType]), @([OrderedListStyle getType]),
- @([BlockQuoteStyle getType]), @([CodeBlockStyle getType]),
- @([CheckboxListStyle getType])
- ],
- @([H5Style getType]) : @[
- @([H1Style getType]), @([H2Style getType]), @([H3Style getType]),
- @([H4Style getType]), @([H6Style getType]),
- @([UnorderedListStyle getType]), @([OrderedListStyle getType]),
- @([BlockQuoteStyle getType]), @([CodeBlockStyle getType]),
- @([CheckboxListStyle getType])
- ],
- @([H6Style getType]) : @[
- @([H1Style getType]), @([H2Style getType]), @([H3Style getType]),
- @([H4Style getType]), @([H5Style getType]),
- @([UnorderedListStyle getType]), @([OrderedListStyle getType]),
- @([BlockQuoteStyle getType]), @([CodeBlockStyle getType]),
- @([CheckboxListStyle getType])
- ],
- @([UnorderedListStyle getType]) : @[
- @([H1Style getType]), @([H2Style getType]), @([H3Style getType]),
- @([H4Style getType]), @([H5Style getType]), @([H6Style getType]),
- @([OrderedListStyle getType]), @([BlockQuoteStyle getType]),
- @([CodeBlockStyle getType]), @([CheckboxListStyle getType])
- ],
- @([OrderedListStyle getType]) : @[
- @([H1Style getType]), @([H2Style getType]), @([H3Style getType]),
- @([H4Style getType]), @([H5Style getType]), @([H6Style getType]),
- @([UnorderedListStyle getType]), @([BlockQuoteStyle getType]),
- @([CodeBlockStyle getType]), @([CheckboxListStyle getType])
- ],
- @([CheckboxListStyle getType]) : @[
- @([H1Style getType]), @([H2Style getType]), @([H3Style getType]),
- @([H4Style getType]), @([H5Style getType]), @([H6Style getType]),
- @([UnorderedListStyle getType]), @([OrderedListStyle getType]),
- @([BlockQuoteStyle getType]), @([CodeBlockStyle getType])
- ],
- @([BlockQuoteStyle getType]) : @[
- @([H1Style getType]), @([H2Style getType]), @([H3Style getType]),
- @([H4Style getType]), @([H5Style getType]), @([H6Style getType]),
- @([UnorderedListStyle getType]), @([OrderedListStyle getType]),
- @([CodeBlockStyle getType]), @([CheckboxListStyle getType])
- ],
- @([CodeBlockStyle getType]) : @[
- @([H1Style getType]), @([H2Style getType]), @([H3Style getType]),
- @([H4Style getType]), @([H5Style getType]), @([H6Style getType]),
- @([BoldStyle getType]), @([UnderlineStyle getType]),
- @([ItalicStyle getType]), @([StrikethroughStyle getType]),
- @([UnorderedListStyle getType]), @([OrderedListStyle getType]),
- @([BlockQuoteStyle getType]), @([InlineCodeStyle getType]),
- @([MentionStyle getType]), @([LinkStyle getType]),
- @([CheckboxListStyle getType])
- ],
- @([ImageStyle getType]) :
- @[ @([LinkStyle getType]), @([MentionStyle getType]) ]
- } mutableCopy];
-
- blockingStyles = [@{
- @([BoldStyle getType]) : @[ @([CodeBlockStyle getType]) ],
- @([ItalicStyle getType]) : @[ @([CodeBlockStyle getType]) ],
- @([UnderlineStyle getType]) : @[ @([CodeBlockStyle getType]) ],
- @([StrikethroughStyle getType]) : @[ @([CodeBlockStyle getType]) ],
- @([InlineCodeStyle getType]) :
- @[ @([CodeBlockStyle getType]), @([ImageStyle getType]) ],
- @([LinkStyle getType]) :
- @[ @([CodeBlockStyle getType]), @([ImageStyle getType]) ],
- @([MentionStyle getType]) :
- @[ @([CodeBlockStyle getType]), @([ImageStyle getType]) ],
- @([H1Style getType]) : @[],
- @([H2Style getType]) : @[],
- @([H3Style getType]) : @[],
- @([H4Style getType]) : @[],
- @([H5Style getType]) : @[],
- @([H6Style getType]) : @[],
- @([UnorderedListStyle getType]) : @[],
- @([OrderedListStyle getType]) : @[],
- @([CheckboxListStyle getType]) : @[],
- @([BlockQuoteStyle getType]) : @[],
- @([CodeBlockStyle getType]) : @[],
- @([ImageStyle getType]) : @[ @([InlineCodeStyle getType]) ]
- } mutableCopy];
+ stylesDict = [StyleUtils stylesDictForHost:self isInput:YES];
+ conflictingStyles = [[StyleUtils conflictMap] mutableCopy];
+ blockingStyles = [[StyleUtils blockingMap] mutableCopy];
parser = [[InputParser alloc] initWithInput:self];
_attachmentViews = [[NSMutableDictionary alloc] init];
@@ -370,11 +231,11 @@ - (void)updateProps:(Props::Shared const &)props
// Update style blocks and conflicts for bold
if (newViewProps.htmlStyle.h1.bold) {
- [self addStyleBlock:H1 to:Bold];
- [self addStyleConflict:Bold to:H1];
+ [StyleUtils addStyleBlock:H1 to:Bold forHost:self];
+ [StyleUtils addStyleConflict:Bold to:H1 forHost:self];
} else {
- [self removeStyleBlock:H1 from:Bold];
- [self removeStyleConflict:Bold from:H1];
+ [StyleUtils removeStyleBlock:H1 from:Bold forHost:self];
+ [StyleUtils removeStyleConflict:Bold from:H1 forHost:self];
}
stylePropChanged = YES;
@@ -391,11 +252,11 @@ - (void)updateProps:(Props::Shared const &)props
// Update style blocks and conflicts for bold
if (newViewProps.htmlStyle.h2.bold) {
- [self addStyleBlock:H2 to:Bold];
- [self addStyleConflict:Bold to:H2];
+ [StyleUtils addStyleBlock:H2 to:Bold forHost:self];
+ [StyleUtils addStyleConflict:Bold to:H2 forHost:self];
} else {
- [self removeStyleBlock:H2 from:Bold];
- [self removeStyleConflict:Bold from:H2];
+ [StyleUtils removeStyleBlock:H2 from:Bold forHost:self];
+ [StyleUtils removeStyleConflict:Bold from:H2 forHost:self];
}
stylePropChanged = YES;
@@ -412,11 +273,11 @@ - (void)updateProps:(Props::Shared const &)props
// Update style blocks and conflicts for bold
if (newViewProps.htmlStyle.h3.bold) {
- [self addStyleBlock:H3 to:Bold];
- [self addStyleConflict:Bold to:H3];
+ [StyleUtils addStyleBlock:H3 to:Bold forHost:self];
+ [StyleUtils addStyleConflict:Bold to:H3 forHost:self];
} else {
- [self removeStyleBlock:H3 from:Bold];
- [self removeStyleConflict:Bold from:H3];
+ [StyleUtils removeStyleBlock:H3 from:Bold forHost:self];
+ [StyleUtils removeStyleConflict:Bold from:H3 forHost:self];
}
stylePropChanged = YES;
@@ -433,11 +294,11 @@ - (void)updateProps:(Props::Shared const &)props
// Update style blocks and conflicts for bold
if (newViewProps.htmlStyle.h4.bold) {
- [self addStyleBlock:H4 to:Bold];
- [self addStyleConflict:Bold to:H4];
+ [StyleUtils addStyleBlock:H4 to:Bold forHost:self];
+ [StyleUtils addStyleConflict:Bold to:H4 forHost:self];
} else {
- [self removeStyleBlock:H4 from:Bold];
- [self removeStyleConflict:Bold from:H4];
+ [StyleUtils removeStyleBlock:H4 from:Bold forHost:self];
+ [StyleUtils removeStyleConflict:Bold from:H4 forHost:self];
}
stylePropChanged = YES;
@@ -454,11 +315,11 @@ - (void)updateProps:(Props::Shared const &)props
// Update style blocks and conflicts for bold
if (newViewProps.htmlStyle.h5.bold) {
- [self addStyleBlock:H5 to:Bold];
- [self addStyleConflict:Bold to:H5];
+ [StyleUtils addStyleBlock:H5 to:Bold forHost:self];
+ [StyleUtils addStyleConflict:Bold to:H5 forHost:self];
} else {
- [self removeStyleBlock:H5 from:Bold];
- [self removeStyleConflict:Bold from:H5];
+ [StyleUtils removeStyleBlock:H5 from:Bold forHost:self];
+ [StyleUtils removeStyleConflict:Bold from:H5 forHost:self];
}
stylePropChanged = YES;
@@ -475,11 +336,11 @@ - (void)updateProps:(Props::Shared const &)props
// Update style blocks and conflicts for bold
if (newViewProps.htmlStyle.h6.bold) {
- [self addStyleBlock:H6 to:Bold];
- [self addStyleConflict:Bold to:H6];
+ [StyleUtils addStyleBlock:H6 to:Bold forHost:self];
+ [StyleUtils addStyleConflict:Bold to:H6 forHost:self];
} else {
- [self removeStyleBlock:H6 from:Bold];
- [self removeStyleConflict:Bold from:H6];
+ [StyleUtils removeStyleBlock:H6 from:Bold forHost:self];
+ [StyleUtils removeStyleConflict:Bold from:H6 forHost:self];
}
stylePropChanged = YES;
@@ -1234,38 +1095,6 @@ - (bool)textInputShouldSubmitOnReturn {
[_submitBehavior isEqualToString:@"submit"];
}
-- (void)addStyleBlock:(StyleType)blocking to:(StyleType)blocked {
- NSMutableArray *blocksArr = [blockingStyles[@(blocked)] mutableCopy];
- if (![blocksArr containsObject:@(blocking)]) {
- [blocksArr addObject:@(blocking)];
- blockingStyles[@(blocked)] = blocksArr;
- }
-}
-
-- (void)removeStyleBlock:(StyleType)blocking from:(StyleType)blocked {
- NSMutableArray *blocksArr = [blockingStyles[@(blocked)] mutableCopy];
- if ([blocksArr containsObject:@(blocking)]) {
- [blocksArr removeObject:@(blocking)];
- blockingStyles[@(blocked)] = blocksArr;
- }
-}
-
-- (void)addStyleConflict:(StyleType)conflicting to:(StyleType)conflicted {
- NSMutableArray *conflictsArr = [conflictingStyles[@(conflicted)] mutableCopy];
- if (![conflictsArr containsObject:@(conflicting)]) {
- [conflictsArr addObject:@(conflicting)];
- conflictingStyles[@(conflicted)] = conflictsArr;
- }
-}
-
-- (void)removeStyleConflict:(StyleType)conflicting from:(StyleType)conflicted {
- NSMutableArray *conflictsArr = [conflictingStyles[@(conflicted)] mutableCopy];
- if ([conflictsArr containsObject:@(conflicting)]) {
- [conflictsArr removeObject:@(conflicting)];
- conflictingStyles[@(conflicted)] = conflictsArr;
- }
-}
-
// MARK: - Native commands and events
- (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args {
@@ -1649,55 +1478,15 @@ - (void)startMentionWithIndicator:(NSString *)indicator {
}
}
-// returns false when style shouldn't be applied and true when it can be
- (BOOL)handleStyleBlocksAndConflicts:(StyleType)type range:(NSRange)range {
- // handle blocking styles: if any is present we do not apply the toggled style
- NSArray *blocking =
- [self getPresentStyleTypesFrom:blockingStyles[@(type)] range:range];
- if (blocking.count != 0) {
- return NO;
- }
-
- // handle conflicting styles: remove styles within the range
- NSArray *conflicting =
- [self getPresentStyleTypesFrom:conflictingStyles[@(type)] range:range];
- if (conflicting.count != 0) {
- for (NSNumber *type in conflicting) {
- StyleBase *style = stylesDict[type];
-
- if ([style isParagraph]) {
- // for paragraph styles we can just call remove since it will pick up
- // proper paragraph range
- [style remove:range withDirtyRange:YES];
- } else {
- // for inline styles we have to differentiate betweeen normal and typing
- // attributes removal
- range.length >= 1 ? [style remove:range withDirtyRange:YES]
- : [style removeTyping];
- }
- }
- }
- return YES;
+ return [StyleUtils handleStyleBlocksAndConflicts:type
+ range:range
+ forHost:self];
}
- (NSArray *)getPresentStyleTypesFrom:(NSArray *)types
range:(NSRange)range {
- NSMutableArray *resultArray =
- [[NSMutableArray alloc] init];
- for (NSNumber *type in types) {
- StyleBase *style = stylesDict[type];
-
- if (range.length >= 1) {
- if ([style any:range]) {
- [resultArray addObject:type];
- }
- } else {
- if ([style detect:range]) {
- [resultArray addObject:type];
- }
- }
- }
- return resultArray;
+ return [StyleUtils getPresentStyleTypesFrom:types range:range forHost:self];
}
- (void)manageSelectionBasedChanges {
diff --git a/ios/EnrichedTextViewManager.mm b/ios/EnrichedTextViewManager.mm
new file mode 100644
index 000000000..2e65c1c30
--- /dev/null
+++ b/ios/EnrichedTextViewManager.mm
@@ -0,0 +1,13 @@
+#import
+#import
+
+@interface EnrichedTextViewManager : RCTViewManager
+@end
+
+@implementation EnrichedTextViewManager
+
+RCT_EXPORT_MODULE(EnrichedTextView)
+
+RCT_EXPORT_VIEW_PROPERTY(text, NSString)
+
+@end
diff --git a/ios/htmlParser/HtmlParser.mm b/ios/htmlParser/HtmlParser.mm
index a315ffffe..994257129 100644
--- a/ios/htmlParser/HtmlParser.mm
+++ b/ios/htmlParser/HtmlParser.mm
@@ -38,7 +38,7 @@ + (BOOL)isBlockTag:(NSString *)tagName {
* for a new HTML tag that contains visible text (e.g., h4, h5, h6),
* you MUST add it to the `textTags` set below.
*/
-- (NSString *)stripExtraWhiteSpacesAndNewlines:(NSString *)html {
++ (NSString *)stripExtraWhiteSpacesAndNewlines:(NSString *)html {
NSSet *textTags = [NSSet setWithObjects:@"p", @"h1", @"h2", @"h3", @"h4",
@"h5", @"h6", @"li", @"b", @"a", @"s",
@"mention", @"code", @"u", @"i", nil];
@@ -108,7 +108,7 @@ - (NSString *)stripExtraWhiteSpacesAndNewlines:(NSString *)html {
return output;
}
-- (NSString *)stringByAddingNewlinesToTag:(NSString *)tag
++ (NSString *)stringByAddingNewlinesToTag:(NSString *)tag
inString:(NSString *)html
leading:(BOOL)leading
trailing:(BOOL)trailing {
@@ -227,7 +227,7 @@ + (NSString *_Nullable)initiallyProcessHtml:(NSString *_Nonnull)html
withString:@""];
fixedHtml = [fixedHtml stringByReplacingOccurrencesOfString:@"