From 365e2470e84abaaf7e81a378c268ad49af5bb59c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20=C5=BB=C3=B3=C5=82kiewski?= Date: Thu, 20 Nov 2025 11:28:14 +0100 Subject: [PATCH 1/9] feat: add support for inline image on iOS --- example/src/App.tsx | 9 +- ios/EnrichedTextInputView.mm | 41 +++++++-- ios/config/InputConfig.h | 4 + ios/config/InputConfig.mm | 20 +++++ ios/styles/ImageStyle.mm | 128 ++++++++++++++++++++++++++++ ios/utils/ImageData.h | 8 ++ ios/utils/ImageData.mm | 4 + ios/utils/LayoutManagerExtension.mm | 45 ++++++++++ ios/utils/StyleHeaders.h | 4 + 9 files changed, 255 insertions(+), 8 deletions(-) create mode 100644 ios/styles/ImageStyle.mm create mode 100644 ios/utils/ImageData.h create mode 100644 ios/utils/ImageData.mm diff --git a/example/src/App.tsx b/example/src/App.tsx index 4763e3ae2..2d284c69a 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -4,6 +4,7 @@ import { Text, type NativeSyntheticEvent, ScrollView, + Platform, } from 'react-native'; import { EnrichedTextInput, @@ -202,7 +203,13 @@ export default function App() { selectionLimit: 1, }); - const imageUri = response.assets?.[0]?.originalPath; + let imageUri; + if (Platform.OS === 'android') { + imageUri = response.assets?.[0]?.originalPath; + } else { + imageUri = response.assets?.[0]?.uri; + } + if (!imageUri) return; ref.current?.setImage(imageUri); diff --git a/ios/EnrichedTextInputView.mm b/ios/EnrichedTextInputView.mm index 027b5b93f..a9ae45b22 100644 --- a/ios/EnrichedTextInputView.mm +++ b/ios/EnrichedTextInputView.mm @@ -95,7 +95,8 @@ - (void)setDefaults { @([H3Style getStyleType]): [[H3Style alloc] initWithInput:self], @([UnorderedListStyle getStyleType]): [[UnorderedListStyle alloc] initWithInput:self], @([OrderedListStyle getStyleType]): [[OrderedListStyle alloc] initWithInput:self], - @([BlockQuoteStyle getStyleType]): [[BlockQuoteStyle alloc] initWithInput:self] + @([BlockQuoteStyle getStyleType]): [[BlockQuoteStyle alloc] initWithInput:self], + @([ImageStyle getStyleType]): [[ImageStyle alloc] initWithInput:self] }; _conflictingStyles = @{ @@ -103,7 +104,7 @@ - (void)setDefaults { @([ItalicStyle getStyleType]) : @[], @([UnderlineStyle getStyleType]) : @[], @([StrikethroughStyle getStyleType]) : @[], - @([InlineCodeStyle getStyleType]) : @[@([LinkStyle getStyleType]), @([MentionStyle getStyleType])], + @([InlineCodeStyle getStyleType]) : @[@([LinkStyle getStyleType]), @([MentionStyle getStyleType]), @([ImageStyle getStyleType])], @([LinkStyle getStyleType]): @[@([InlineCodeStyle getStyleType]), @([LinkStyle getStyleType]), @([MentionStyle getStyleType])], @([MentionStyle getStyleType]): @[@([InlineCodeStyle getStyleType]), @([LinkStyle getStyleType])], @([H1Style getStyleType]): @[@([H2Style getStyleType]), @([H3Style getStyleType]), @([UnorderedListStyle getStyleType]), @([OrderedListStyle getStyleType]), @([BlockQuoteStyle getStyleType])], @@ -111,7 +112,8 @@ - (void)setDefaults { @([H3Style getStyleType]): @[@([H1Style getStyleType]), @([H2Style getStyleType]), @([UnorderedListStyle getStyleType]), @([OrderedListStyle getStyleType]), @([BlockQuoteStyle getStyleType])], @([UnorderedListStyle getStyleType]): @[@([H1Style getStyleType]), @([H2Style getStyleType]), @([H3Style getStyleType]), @([OrderedListStyle getStyleType]), @([BlockQuoteStyle getStyleType])], @([OrderedListStyle getStyleType]): @[@([H1Style getStyleType]), @([H2Style getStyleType]), @([H3Style getStyleType]), @([UnorderedListStyle getStyleType]), @([BlockQuoteStyle getStyleType])], - @([BlockQuoteStyle getStyleType]): @[@([H1Style getStyleType]), @([H2Style getStyleType]), @([H3Style getStyleType]), @([UnorderedListStyle getStyleType]), @([OrderedListStyle getStyleType])] + @([BlockQuoteStyle getStyleType]): @[@([H1Style getStyleType]), @([H2Style getStyleType]), @([H3Style getStyleType]), @([UnorderedListStyle getStyleType]), @([OrderedListStyle getStyleType])], + @([ImageStyle getStyleType]) : @[] }; _blockingStyles = @{ @@ -119,15 +121,16 @@ - (void)setDefaults { @([ItalicStyle getStyleType]) : @[], @([UnderlineStyle getStyleType]) : @[], @([StrikethroughStyle getStyleType]) : @[], - @([InlineCodeStyle getStyleType]) : @[], - @([LinkStyle getStyleType]): @[], - @([MentionStyle getStyleType]): @[], + @([InlineCodeStyle getStyleType]) : @[@([ImageStyle getStyleType])], + @([LinkStyle getStyleType]): @[@([ImageStyle getStyleType])], + @([MentionStyle getStyleType]): @[@([ImageStyle getStyleType])], @([H1Style getStyleType]): @[], @([H2Style getStyleType]): @[], @([H3Style getStyleType]): @[], @([UnorderedListStyle getStyleType]): @[], @([OrderedListStyle getStyleType]): @[], @([BlockQuoteStyle getStyleType]): @[], + @([ImageStyle getStyleType]) : @[@([InlineCodeStyle getStyleType]), @([LinkStyle getStyleType]), @([MentionStyle getStyleType])] }; parser = [[InputParser alloc] initWithInput:self]; @@ -347,6 +350,16 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const & } } + if(newViewProps.htmlStyle.img.width != oldViewProps.htmlStyle.img.width) { + [newConfig setImageWidth:newViewProps.htmlStyle.img.width]; + stylePropChanged = YES; + } + + if(newViewProps.htmlStyle.img.height != oldViewProps.htmlStyle.img.height) { + [newConfig setImageHeight:newViewProps.htmlStyle.img.height]; + stylePropChanged = YES; + } + if(newViewProps.htmlStyle.a.textDecorationLine != oldViewProps.htmlStyle.a.textDecorationLine) { NSString *objcString = [NSString fromCppString:newViewProps.htmlStyle.a.textDecorationLine]; if([objcString isEqualToString:DecorationUnderline]) { @@ -703,7 +716,7 @@ - (void)tryUpdatingActiveStyles { .isOrderedList = [_activeStyles containsObject: @([OrderedListStyle getStyleType])], .isBlockQuote = [_activeStyles containsObject: @([BlockQuoteStyle getStyleType])], .isCodeBlock = NO, // [_activeStyles containsObject: @([CodeBlockStyle getStyleType])], - .isImage = NO // [_activeStyles containsObject: @([ImageStyle getStyleType]])], + .isImage = [_activeStyles containsObject: @([ImageStyle getStyleType])], }); } } @@ -774,6 +787,9 @@ - (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args { [self toggleParagraphStyle:[OrderedListStyle getStyleType]]; } else if([commandName isEqualToString:@"toggleBlockQuote"]) { [self toggleParagraphStyle:[BlockQuoteStyle getStyleType]]; + } else if([commandName isEqualToString:@"addImage"]) { + NSString *uri = (NSString *)args[0]; + [self addImage:uri]; } } @@ -913,6 +929,17 @@ - (void)addMention:(NSString *)indicator text:(NSString *)text attributes:(NSStr } } +- (void)addImage:(NSString *)uri +{ + ImageStyle *imageStyleClass = (ImageStyle *)stylesDict[@([ImageStyle getStyleType])]; + if(imageStyleClass == nullptr) { return; } + + if([self handleStyleBlocksAndConflicts:[ImageStyle getStyleType] range:textView.selectedRange]) { + [imageStyleClass addImage:uri]; + [self anyTextMayHaveBeenModified]; + } +} + - (void)startMentionWithIndicator:(NSString *)indicator { MentionStyle *mentionStyleClass = (MentionStyle *)stylesDict[@([MentionStyle getStyleType])]; if(mentionStyleClass == nullptr) { return; } diff --git a/ios/config/InputConfig.h b/ios/config/InputConfig.h index cd12910c6..5e2c61bd0 100644 --- a/ios/config/InputConfig.h +++ b/ios/config/InputConfig.h @@ -64,4 +64,8 @@ - (void)setLinkDecorationLine:(TextDecorationLineEnum)newValue; - (void)setMentionStyleProps:(NSDictionary *)newValue; - (MentionStyleProps *)mentionStylePropsForIndicator:(NSString *)indicator; +- (void)setImageWidth:(CGFloat)newValue; +- (CGFloat)imageWidth; +- (void)setImageHeight:(CGFloat)newValue; +- (CGFloat)imageHeight; @end diff --git a/ios/config/InputConfig.mm b/ios/config/InputConfig.mm index 00f0903c1..ccc60509c 100644 --- a/ios/config/InputConfig.mm +++ b/ios/config/InputConfig.mm @@ -36,6 +36,8 @@ @implementation InputConfig { UIColor *_linkColor; TextDecorationLineEnum _linkDecorationLine; NSDictionary *_mentionProperties; + CGFloat _imageWidth; + CGFloat _imageHeight; } - (instancetype) init { @@ -79,6 +81,8 @@ - (id)copyWithZone:(NSZone *)zone { copy->_linkColor = [_linkColor copy]; copy->_linkDecorationLine = [_linkDecorationLine copy]; copy->_mentionProperties = [_mentionProperties mutableCopy]; + copy->_imageWidth = _imageWidth; + copy->_imageHeight = _imageHeight; return copy; } @@ -379,4 +383,20 @@ - (MentionStyleProps *)mentionStylePropsForIndicator:(NSString *)indicator { return fallbackProps; } +- (CGFloat)imageWidth { + return _imageWidth; +} + +- (void)setImageWidth:(CGFloat)newValue { + _imageWidth = newValue; +} + +- (CGFloat)imageHeight { + return _imageHeight; +} + +- (void)setImageHeight:(CGFloat)newValue { + _imageHeight = newValue; +} + @end diff --git a/ios/styles/ImageStyle.mm b/ios/styles/ImageStyle.mm new file mode 100644 index 000000000..2df931a30 --- /dev/null +++ b/ios/styles/ImageStyle.mm @@ -0,0 +1,128 @@ +#import "StyleHeaders.h" +#import "EnrichedTextInputView.h" +#import "ImageData.h" +#import "OccurenceUtils.h" +#import "TextInsertionUtils.h" + +// custom NSAttributedStringKey to differentiate the image +static NSString *const ImageAttributeName = @"ImageAttributeName"; + + +@implementation ImageStyle { + EnrichedTextInputView *_input; +} + ++ (StyleType)getStyleType { return Image; } + +- (instancetype)initWithInput:(id)input { + self = [super init]; + _input = (EnrichedTextInputView *)input; + return self; +} + +- (void)applyStyle:(NSRange)range { + // no-op for image +} + +- (void)addAttributes:(NSRange)range { + // no-op for image +} + +- (void)addTypingAttributes { + // no-op for image +} + +- (void)removeAttributes:(NSRange)range { + [_input->textView.textStorage beginEditing]; + [_input->textView.textStorage removeAttribute:ImageAttributeName range:range]; + [_input->textView.textStorage endEditing]; +} + +- (void)removeTypingAttributes { + NSMutableDictionary *currentAttributes = [_input->textView.typingAttributes mutableCopy]; + [currentAttributes removeObjectForKey:ImageAttributeName]; + _input->textView.typingAttributes = currentAttributes; +} + +- (BOOL)styleCondition:(id _Nullable)value :(NSRange)range { + return [value isKindOfClass:[ImageData class]]; +} + +- (BOOL)anyOccurence:(NSRange)range { + return [OccurenceUtils any:ImageAttributeName withInput:_input inRange:range + withCondition:^BOOL(id _Nullable value, NSRange range) { + return [self styleCondition:value :range]; + } + ]; +} + +- (BOOL)detectStyle:(NSRange)range { + if (range.length >= 1) { + return [OccurenceUtils detect:ImageAttributeName withInput:_input inRange:range + withCondition:^BOOL(id _Nullable value, NSRange range) { + return [self styleCondition:value :range]; + } + ]; + } else { + return [OccurenceUtils detect:ImageAttributeName withInput:_input atIndex:range.location checkPrevious:YES + withCondition:^BOOL(id _Nullable value, NSRange range) { + return [self styleCondition:value :range]; + } + ]; + } +} + +- (NSArray * _Nullable)findAllOccurences:(NSRange)range { + return [OccurenceUtils all:ImageAttributeName withInput:_input inRange:range + withCondition:^BOOL(id _Nullable value, NSRange range) { + return [self styleCondition:value :range]; + } + ]; +} + +- (void)addImage:(NSString *)uri { + ImageData *data = [[ImageData alloc] init]; + data.uri = uri; + + NSMutableDictionary *attributes = [@{ + NSAttachmentAttributeName: [self prepareSpacerAttachement], + ImageAttributeName: data + } mutableCopy]; + + // Use the Object Replacement Character for Image. + // This tells TextKit "something non-text goes here". + NSString *imagePlaceholder = @"\uFFFC"; + + [TextInsertionUtils replaceText:imagePlaceholder + at:_input->textView.selectedRange + additionalAttributes:nil + input:_input + withSelection:YES]; + + NSRange newSelection = _input->textView.selectedRange; + NSRange imageRange = NSMakeRange(newSelection.location - 1, 1); + + [_input->textView.textStorage beginEditing]; + [_input->textView.textStorage addAttributes:attributes range:imageRange]; + + [_input->textView.textStorage endEditing]; +} + +-(NSTextAttachment *)prepareSpacerAttachement +{ + NSTextAttachment *spacerAttachment = [[NSTextAttachment alloc] init]; + spacerAttachment.bounds = CGRectMake(0, 0, [_input->config imageWidth], [_input->config imageHeight]); + spacerAttachment.image = [self prepareTransparentImage]; + + return spacerAttachment; +} + +// Helper to create a 1x1 transparent image +- (UIImage *)prepareTransparentImage { + UIGraphicsBeginImageContextWithOptions(CGSizeMake(1, 1), NO, 0.0); + UIImage *blank = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + return blank; +} + +@end diff --git a/ios/utils/ImageData.h b/ios/utils/ImageData.h new file mode 100644 index 000000000..cfc440026 --- /dev/null +++ b/ios/utils/ImageData.h @@ -0,0 +1,8 @@ +#pragma once +#import + +@interface ImageData : NSObject + +@property NSString *uri; + +@end diff --git a/ios/utils/ImageData.mm b/ios/utils/ImageData.mm new file mode 100644 index 000000000..e5a6d61d2 --- /dev/null +++ b/ios/utils/ImageData.mm @@ -0,0 +1,4 @@ +#import "ImageData.h" + +@implementation ImageData +@end diff --git a/ios/utils/LayoutManagerExtension.mm b/ios/utils/LayoutManagerExtension.mm index 26a9cbcdd..111ccfa48 100644 --- a/ios/utils/LayoutManagerExtension.mm +++ b/ios/utils/LayoutManagerExtension.mm @@ -3,6 +3,7 @@ #import "EnrichedTextInputView.h" #import "StyleHeaders.h" #import "ParagraphsUtils.h" +#import "ImageData.h" @implementation NSLayoutManager (LayoutManagerExtension) @@ -57,6 +58,8 @@ - (void)my_drawBackgroundForGlyphRange:(NSRange)glyphRange atPoint:(CGPoint)orig NSRange inputRange = NSMakeRange(0, typedInput->textView.textStorage.length); + [self drawImages:typedInput origin:origin inputRange:inputRange]; + // it isn't the most performant but we have to check for all the blockquotes each time and redraw them NSArray *allBlockquotes = [bqStyle findAllOccurences:inputRange]; @@ -132,6 +135,48 @@ - (void)my_drawBackgroundForGlyphRange:(NSRange)glyphRange atPoint:(CGPoint)orig } } +-(void)drawImages:(EnrichedTextInputView *)typedInput origin:(CGPoint)origin inputRange:(NSRange)inputRange +{ + ImageStyle *imgStyle = typedInput->stylesDict[@([ImageStyle getStyleType])]; + + if (imgStyle == nullptr) { return; } + NSArray *allImages = [imgStyle findAllOccurences:inputRange]; + + for (StylePair *pair in allImages) { + ImageData *imageData = (ImageData *)pair.styleValue; + NSRange imageCharRange = [pair.rangeValue rangeValue]; + + // Convert Character Range to Glyph Range + NSRange imageGlyphRange = [self glyphRangeForCharacterRange:imageCharRange actualCharacterRange:NULL]; + + // Get the Text Container (needed for rect calculation) + NSTextContainer *textContainer = [self textContainerForGlyphAtIndex:imageGlyphRange.location effectiveRange:NULL]; + + // boundingRectForGlyphRange gives us the calculated space for the \uFFFC character + CGRect imageRect = [self boundingRectForGlyphRange:imageGlyphRange inTextContainer:textContainer]; + + imageRect.origin.x += origin.x; + imageRect.origin.y += origin.y; + imageRect.size.width = [typedInput->config imageWidth]; + imageRect.size.height = [typedInput->config imageHeight]; + + UIImage *imageToDraw = nil; + + if (imageData.uri) { + NSURL *url = [NSURL URLWithString:imageData.uri]; + imageToDraw = [UIImage imageWithContentsOfFile:url.path]; + } + + if (imageToDraw) { + [imageToDraw drawInRect:imageRect]; + } else { + // Draw a placeholder box if image failed to load + [[UIColor lightGrayColor] setFill]; + UIRectFill(imageRect); + } + } +} + - (NSString *)markerForList:(NSTextList *)list charIndex:(NSUInteger)index input:(EnrichedTextInputView *)input { if(list.markerFormat == NSTextListMarkerDecimal) { NSString *fullText = input->textView.textStorage.string; diff --git a/ios/utils/StyleHeaders.h b/ios/utils/StyleHeaders.h index 1142a4a64..4e2a6d95f 100644 --- a/ios/utils/StyleHeaders.h +++ b/ios/utils/StyleHeaders.h @@ -74,3 +74,7 @@ - (BOOL)handleBackspaceInRange:(NSRange)range replacementText:(NSString *)text; - (void)manageBlockquoteColor; @end + +@interface ImageStyle : NSObject +- (void)addImage:(NSString *)uri; +@end From 3f6b1b10776bc40b8c2586704a3bb3872c9342da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20=C5=BB=C3=B3=C5=82kiewski?= Date: Thu, 20 Nov 2025 12:57:36 +0100 Subject: [PATCH 2/9] feat: add handling img inside parser --- ios/inputParser/InputParser.mm | 30 +++++++++++++++++++++++++++++- ios/styles/ImageStyle.mm | 22 +++++++++++++++++++++- ios/utils/StyleHeaders.h | 2 ++ 3 files changed, 52 insertions(+), 2 deletions(-) diff --git a/ios/inputParser/InputParser.mm b/ios/inputParser/InputParser.mm index d0d2002e2..b8e5bd526 100644 --- a/ios/inputParser/InputParser.mm +++ b/ios/inputParser/InputParser.mm @@ -202,7 +202,11 @@ - (NSString *)parseToHtmlFromRange:(NSRange)range { // append opening tags for(NSNumber *style in sortedNewStyles) { NSString *tagContent = [self tagContentForStyle:style openingTag:YES location:currentRange.location]; - [result appendString: [NSString stringWithFormat:@"<%@>", tagContent]]; + if ([style isEqualToNumber: @([ImageStyle getStyleType])]) { + [result appendString: [NSString stringWithFormat:@"<%@/>", tagContent]]; + } else { + [result appendString: [NSString stringWithFormat:@"<%@>", tagContent]]; + } } // append the letter and escape it if needed @@ -223,6 +227,9 @@ - (NSString *)parseToHtmlFromRange:(NSRange)range { // append closing tags for(NSNumber *style in sortedEndedStyles) { + if ([style isEqualToNumber: @([ImageStyle getStyleType])]) { + continue; + } NSString *tagContent = [self tagContentForStyle:style openingTag:NO location:_input->textView.textStorage.string.length - 1]; [result appendString: [NSString stringWithFormat:@"", tagContent]]; } @@ -273,6 +280,19 @@ - (NSString *)tagContentForStyle:(NSNumber *)style openingTag:(BOOL)openingTag l return @"b"; } else if([style isEqualToNumber: @([ItalicStyle getStyleType])]) { return @"i"; + } else if ([style isEqualToNumber: @([ImageStyle getStyleType])]) { + if(openingTag) { + ImageStyle *imageStyle = (ImageStyle *)_input->stylesDict[@([ImageStyle getStyleType])]; + if(imageStyle != nullptr) { + ImageData *data = [imageStyle getImageDataAt:location]; + if(data != nullptr && data.uri != nullptr) { + return [NSString stringWithFormat:@"img src=\"%@\"", data.uri]; + } + } + return @"img"; + } else { + return @""; + } } else if([style isEqualToNumber: @([UnderlineStyle getStyleType])]) { return @"u"; } else if([style isEqualToNumber: @([StrikethroughStyle getStyleType])]) { @@ -603,6 +623,14 @@ - (NSArray *)getTextAndStylesFromHtml:(NSString *)fixedHtml { [styleArr addObject:@([BoldStyle getStyleType])]; } else if([tagName isEqualToString:@"i"]) { [styleArr addObject:@([ItalicStyle getStyleType])]; + } else if([tagName isEqualToString:@"img"]) { + [styleArr addObject:@([ImageStyle getStyleType])]; + // cut only the uri from the src="..." string + NSString *uri = [params substringWithRange:NSMakeRange(5, params.length - 6)]; + ImageData *imageData = [[ImageData alloc] init]; + imageData.uri = uri; + + stylePair.styleValue = imageData; } else if([tagName isEqualToString:@"u"]) { [styleArr addObject:@([UnderlineStyle getStyleType])]; } else if([tagName isEqualToString:@"s"]) { diff --git a/ios/styles/ImageStyle.mm b/ios/styles/ImageStyle.mm index 2df931a30..a1313157a 100644 --- a/ios/styles/ImageStyle.mm +++ b/ios/styles/ImageStyle.mm @@ -1,6 +1,5 @@ #import "StyleHeaders.h" #import "EnrichedTextInputView.h" -#import "ImageData.h" #import "OccurenceUtils.h" #import "TextInsertionUtils.h" @@ -80,6 +79,27 @@ - (BOOL)detectStyle:(NSRange)range { ]; } +- (ImageData *)getImageDataAt:(NSUInteger)location +{ + NSRange imageRange = NSMakeRange(0, 0); + NSRange inputRange = NSMakeRange(0, _input->textView.textStorage.length); + + // don't search at the very end of input + NSUInteger searchLocation = location; + if(searchLocation == _input->textView.textStorage.length) { + return nullptr; + } + + ImageData *imageData = [_input->textView.textStorage + attribute:ImageAttributeName + atIndex:searchLocation + longestEffectiveRange: &imageRange + inRange:inputRange + ]; + + return imageData; +} + - (void)addImage:(NSString *)uri { ImageData *data = [[ImageData alloc] init]; data.uri = uri; diff --git a/ios/utils/StyleHeaders.h b/ios/utils/StyleHeaders.h index 4e2a6d95f..3160425ca 100644 --- a/ios/utils/StyleHeaders.h +++ b/ios/utils/StyleHeaders.h @@ -2,6 +2,7 @@ #import "BaseStyleProtocol.h" #import "LinkData.h" #import "MentionParams.h" +#import "ImageData.h" @interface BoldStyle : NSObject @end @@ -77,4 +78,5 @@ @interface ImageStyle : NSObject - (void)addImage:(NSString *)uri; +- (ImageData *)getImageDataAt:(NSUInteger)location; @end From cffd0a55aa5a4ca9c83499f79e9e6f3c1c0f9571 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20=C5=BB=C3=B3=C5=82kiewski?= Date: Thu, 20 Nov 2025 16:20:40 +0100 Subject: [PATCH 3/9] fix: incorrent font size issue --- ios/EnrichedTextInputView.mm | 4 ++-- ios/inputParser/InputParser.mm | 6 ++++++ ios/styles/ImageStyle.mm | 30 +++++++++++++++++++++++++----- 3 files changed, 33 insertions(+), 7 deletions(-) diff --git a/ios/EnrichedTextInputView.mm b/ios/EnrichedTextInputView.mm index a9ae45b22..c91e90382 100644 --- a/ios/EnrichedTextInputView.mm +++ b/ios/EnrichedTextInputView.mm @@ -113,7 +113,7 @@ - (void)setDefaults { @([UnorderedListStyle getStyleType]): @[@([H1Style getStyleType]), @([H2Style getStyleType]), @([H3Style getStyleType]), @([OrderedListStyle getStyleType]), @([BlockQuoteStyle getStyleType])], @([OrderedListStyle getStyleType]): @[@([H1Style getStyleType]), @([H2Style getStyleType]), @([H3Style getStyleType]), @([UnorderedListStyle getStyleType]), @([BlockQuoteStyle getStyleType])], @([BlockQuoteStyle getStyleType]): @[@([H1Style getStyleType]), @([H2Style getStyleType]), @([H3Style getStyleType]), @([UnorderedListStyle getStyleType]), @([OrderedListStyle getStyleType])], - @([ImageStyle getStyleType]) : @[] + @([ImageStyle getStyleType]) : @[@([LinkStyle getStyleType]), @([MentionStyle getStyleType])] }; _blockingStyles = @{ @@ -130,7 +130,7 @@ - (void)setDefaults { @([UnorderedListStyle getStyleType]): @[], @([OrderedListStyle getStyleType]): @[], @([BlockQuoteStyle getStyleType]): @[], - @([ImageStyle getStyleType]) : @[@([InlineCodeStyle getStyleType]), @([LinkStyle getStyleType]), @([MentionStyle getStyleType])] + @([ImageStyle getStyleType]) : @[@([InlineCodeStyle getStyleType])] }; parser = [[InputParser alloc] initWithInput:self]; diff --git a/ios/inputParser/InputParser.mm b/ios/inputParser/InputParser.mm index b8e5bd526..d3ec82cba 100644 --- a/ios/inputParser/InputParser.mm +++ b/ios/inputParser/InputParser.mm @@ -85,6 +85,9 @@ - (NSString *)parseToHtmlFromRange:(NSRange)range { // append closing tags for(NSNumber *style in sortedEndedStyles) { + if ([style isEqualToNumber: @([ImageStyle getStyleType])]) { + continue; + } NSString *tagContent = [self tagContentForStyle:style openingTag:NO location:currentRange.location]; [result appendString: [NSString stringWithFormat:@"", tagContent]]; } @@ -189,6 +192,9 @@ - (NSString *)parseToHtmlFromRange:(NSRange)range { // append closing tags for(NSNumber *style in sortedEndedStyles) { + if ([style isEqualToNumber: @([ImageStyle getStyleType])]) { + continue; + } NSString *tagContent = [self tagContentForStyle:style openingTag:NO location:currentRange.location]; [result appendString: [NSString stringWithFormat:@"", tagContent]]; } diff --git a/ios/styles/ImageStyle.mm b/ios/styles/ImageStyle.mm index a1313157a..0d1699ee4 100644 --- a/ios/styles/ImageStyle.mm +++ b/ios/styles/ImageStyle.mm @@ -34,13 +34,15 @@ - (void)addTypingAttributes { - (void)removeAttributes:(NSRange)range { [_input->textView.textStorage beginEditing]; [_input->textView.textStorage removeAttribute:ImageAttributeName range:range]; + [_input->textView.textStorage removeAttribute:NSAttachmentAttributeName range:range]; [_input->textView.textStorage endEditing]; } - (void)removeTypingAttributes { - NSMutableDictionary *currentAttributes = [_input->textView.typingAttributes mutableCopy]; - [currentAttributes removeObjectForKey:ImageAttributeName]; - _input->textView.typingAttributes = currentAttributes; + NSMutableDictionary *currentAttributes = [_input->textView.typingAttributes mutableCopy]; + [currentAttributes removeObjectForKey:ImageAttributeName]; + [currentAttributes removeObjectForKey:NSAttachmentAttributeName]; + _input->textView.typingAttributes = currentAttributes; } - (BOOL)styleCondition:(id _Nullable)value :(NSRange)range { @@ -104,9 +106,15 @@ - (void)addImage:(NSString *)uri { ImageData *data = [[ImageData alloc] init]; data.uri = uri; + UIFont *usedFont = _input->textView.typingAttributes[NSFontAttributeName]; + if (!usedFont) { + usedFont = [_input->config primaryFont]; + } + NSMutableDictionary *attributes = [@{ - NSAttachmentAttributeName: [self prepareSpacerAttachement], - ImageAttributeName: data + NSAttachmentAttributeName: [self prepareSpacerAttachement], + ImageAttributeName: data, + NSFontAttributeName: usedFont, } mutableCopy]; // Use the Object Replacement Character for Image. @@ -125,6 +133,18 @@ - (void)addImage:(NSString *)uri { [_input->textView.textStorage beginEditing]; [_input->textView.textStorage addAttributes:attributes range:imageRange]; + // remove typing attributes + NSMutableDictionary *cleanTypingAttrs = [_input->textView.typingAttributes mutableCopy]; + [cleanTypingAttrs removeObjectForKey:ImageAttributeName]; + [cleanTypingAttrs removeObjectForKey:NSAttachmentAttributeName]; + + // Explicitly restore the font in typing attributes. + // Specific Case: When the input is empty, there is no "previous character" to inherit the font from. + // Without this, the cursor falls back to the system default (tiny 12pt), looking broken next to the image. + cleanTypingAttrs[NSFontAttributeName] = usedFont; + + _input->textView.typingAttributes = cleanTypingAttrs; + [_input->textView.textStorage endEditing]; } From 7f512126453cb6cb8d2e521851043a2069a74ddc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20=C5=BB=C3=B3=C5=82kiewski?= Date: Fri, 21 Nov 2025 09:59:39 +0100 Subject: [PATCH 4/9] fix: lists crash with image --- ios/styles/ImageStyle.mm | 21 +-------------------- 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/ios/styles/ImageStyle.mm b/ios/styles/ImageStyle.mm index 0d1699ee4..d78b83a13 100644 --- a/ios/styles/ImageStyle.mm +++ b/ios/styles/ImageStyle.mm @@ -106,15 +106,9 @@ - (void)addImage:(NSString *)uri { ImageData *data = [[ImageData alloc] init]; data.uri = uri; - UIFont *usedFont = _input->textView.typingAttributes[NSFontAttributeName]; - if (!usedFont) { - usedFont = [_input->config primaryFont]; - } - NSMutableDictionary *attributes = [@{ NSAttachmentAttributeName: [self prepareSpacerAttachement], ImageAttributeName: data, - NSFontAttributeName: usedFont, } mutableCopy]; // Use the Object Replacement Character for Image. @@ -123,7 +117,7 @@ - (void)addImage:(NSString *)uri { [TextInsertionUtils replaceText:imagePlaceholder at:_input->textView.selectedRange - additionalAttributes:nil + additionalAttributes:_input->defaultTypingAttributes input:_input withSelection:YES]; @@ -132,19 +126,6 @@ - (void)addImage:(NSString *)uri { [_input->textView.textStorage beginEditing]; [_input->textView.textStorage addAttributes:attributes range:imageRange]; - - // remove typing attributes - NSMutableDictionary *cleanTypingAttrs = [_input->textView.typingAttributes mutableCopy]; - [cleanTypingAttrs removeObjectForKey:ImageAttributeName]; - [cleanTypingAttrs removeObjectForKey:NSAttachmentAttributeName]; - - // Explicitly restore the font in typing attributes. - // Specific Case: When the input is empty, there is no "previous character" to inherit the font from. - // Without this, the cursor falls back to the system default (tiny 12pt), looking broken next to the image. - cleanTypingAttrs[NSFontAttributeName] = usedFont; - - _input->textView.typingAttributes = cleanTypingAttrs; - [_input->textView.textStorage endEditing]; } From db664b6e26cabed7f6a249ddc2c8818b3155bf0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20=C5=BB=C3=B3=C5=82kiewski?= Date: Fri, 21 Nov 2025 10:28:12 +0100 Subject: [PATCH 5/9] docs: update inline image docs --- README.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/README.md b/README.md index 5d4b4b0c5..351093cbc 100644 --- a/README.md +++ b/README.md @@ -212,9 +212,6 @@ You can insert an image into the input using [setImage](docs/API_REFERENCE.md#se The image will be put into a single line in the input and will affect the line's height as well as input's height. Keep in mind, that image will replace currently selected text or insert into the cursor position if there is no text selection. -> [!NOTE] -> The iOS doesn't support inline images just yet, but it's planned in the near future! - ## Style Detection All of the above styles can be detected with the use of [onChangeState](docs/API_REFERENCE.md#onchangestate) event payload. From 3ed3008bea97690f70339c0ec8e5416f655591e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20=C5=BB=C3=B3=C5=82kiewski?= Date: Mon, 24 Nov 2025 13:46:09 +0100 Subject: [PATCH 6/9] fix: use NSTextAttachment --- ios/styles/ImageStyle.mm | 22 ++++++-------- ios/utils/LayoutManagerExtension.mm | 45 ----------------------------- 2 files changed, 9 insertions(+), 58 deletions(-) diff --git a/ios/styles/ImageStyle.mm b/ios/styles/ImageStyle.mm index d78b83a13..63333a32b 100644 --- a/ios/styles/ImageStyle.mm +++ b/ios/styles/ImageStyle.mm @@ -107,7 +107,7 @@ - (void)addImage:(NSString *)uri { data.uri = uri; NSMutableDictionary *attributes = [@{ - NSAttachmentAttributeName: [self prepareSpacerAttachement], + NSAttachmentAttributeName: [self prepareImageAttachement:uri], ImageAttributeName: data, } mutableCopy]; @@ -129,21 +129,17 @@ - (void)addImage:(NSString *)uri { [_input->textView.textStorage endEditing]; } --(NSTextAttachment *)prepareSpacerAttachement +-(NSTextAttachment *)prepareImageAttachement:(NSString *)uri { - NSTextAttachment *spacerAttachment = [[NSTextAttachment alloc] init]; - spacerAttachment.bounds = CGRectMake(0, 0, [_input->config imageWidth], [_input->config imageHeight]); - spacerAttachment.image = [self prepareTransparentImage]; + NSURL *url = [NSURL URLWithString:uri]; + NSData *imgData = [NSData dataWithContentsOfURL:url]; + UIImage *image = [UIImage imageWithData:imgData]; - return spacerAttachment; -} + NSTextAttachment *attachment = [[NSTextAttachment alloc] init]; + attachment.image = image; + attachment.bounds = CGRectMake(0, 0, [_input->config imageWidth], [_input->config imageHeight]); -// Helper to create a 1x1 transparent image -- (UIImage *)prepareTransparentImage { - UIGraphicsBeginImageContextWithOptions(CGSizeMake(1, 1), NO, 0.0); - UIImage *blank = UIGraphicsGetImageFromCurrentImageContext(); - UIGraphicsEndImageContext(); - return blank; + return attachment; } @end diff --git a/ios/utils/LayoutManagerExtension.mm b/ios/utils/LayoutManagerExtension.mm index 111ccfa48..26a9cbcdd 100644 --- a/ios/utils/LayoutManagerExtension.mm +++ b/ios/utils/LayoutManagerExtension.mm @@ -3,7 +3,6 @@ #import "EnrichedTextInputView.h" #import "StyleHeaders.h" #import "ParagraphsUtils.h" -#import "ImageData.h" @implementation NSLayoutManager (LayoutManagerExtension) @@ -58,8 +57,6 @@ - (void)my_drawBackgroundForGlyphRange:(NSRange)glyphRange atPoint:(CGPoint)orig NSRange inputRange = NSMakeRange(0, typedInput->textView.textStorage.length); - [self drawImages:typedInput origin:origin inputRange:inputRange]; - // it isn't the most performant but we have to check for all the blockquotes each time and redraw them NSArray *allBlockquotes = [bqStyle findAllOccurences:inputRange]; @@ -135,48 +132,6 @@ - (void)my_drawBackgroundForGlyphRange:(NSRange)glyphRange atPoint:(CGPoint)orig } } --(void)drawImages:(EnrichedTextInputView *)typedInput origin:(CGPoint)origin inputRange:(NSRange)inputRange -{ - ImageStyle *imgStyle = typedInput->stylesDict[@([ImageStyle getStyleType])]; - - if (imgStyle == nullptr) { return; } - NSArray *allImages = [imgStyle findAllOccurences:inputRange]; - - for (StylePair *pair in allImages) { - ImageData *imageData = (ImageData *)pair.styleValue; - NSRange imageCharRange = [pair.rangeValue rangeValue]; - - // Convert Character Range to Glyph Range - NSRange imageGlyphRange = [self glyphRangeForCharacterRange:imageCharRange actualCharacterRange:NULL]; - - // Get the Text Container (needed for rect calculation) - NSTextContainer *textContainer = [self textContainerForGlyphAtIndex:imageGlyphRange.location effectiveRange:NULL]; - - // boundingRectForGlyphRange gives us the calculated space for the \uFFFC character - CGRect imageRect = [self boundingRectForGlyphRange:imageGlyphRange inTextContainer:textContainer]; - - imageRect.origin.x += origin.x; - imageRect.origin.y += origin.y; - imageRect.size.width = [typedInput->config imageWidth]; - imageRect.size.height = [typedInput->config imageHeight]; - - UIImage *imageToDraw = nil; - - if (imageData.uri) { - NSURL *url = [NSURL URLWithString:imageData.uri]; - imageToDraw = [UIImage imageWithContentsOfFile:url.path]; - } - - if (imageToDraw) { - [imageToDraw drawInRect:imageRect]; - } else { - // Draw a placeholder box if image failed to load - [[UIColor lightGrayColor] setFill]; - UIRectFill(imageRect); - } - } -} - - (NSString *)markerForList:(NSTextList *)list charIndex:(NSUInteger)index input:(EnrichedTextInputView *)input { if(list.markerFormat == NSTextListMarkerDecimal) { NSString *fullText = input->textView.textStorage.string; From 1152ff99f29c5262517f43e81fbce6eea268b235 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20=C5=BB=C3=B3=C5=82kiewski?= Date: Mon, 24 Nov 2025 13:56:18 +0100 Subject: [PATCH 7/9] fix: review changes --- README.md | 3 +++ ios/EnrichedTextInputView.mm | 10 +++++----- ios/inputParser/InputParser.mm | 13 ++++++++++++- ios/styles/ImageStyle.mm | 1 - 4 files changed, 20 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 351093cbc..5d4b4b0c5 100644 --- a/README.md +++ b/README.md @@ -212,6 +212,9 @@ You can insert an image into the input using [setImage](docs/API_REFERENCE.md#se The image will be put into a single line in the input and will affect the line's height as well as input's height. Keep in mind, that image will replace currently selected text or insert into the cursor position if there is no text selection. +> [!NOTE] +> The iOS doesn't support inline images just yet, but it's planned in the near future! + ## Style Detection All of the above styles can be detected with the use of [onChangeState](docs/API_REFERENCE.md#onchangestate) event payload. diff --git a/ios/EnrichedTextInputView.mm b/ios/EnrichedTextInputView.mm index 636c992b9..4bf878928 100644 --- a/ios/EnrichedTextInputView.mm +++ b/ios/EnrichedTextInputView.mm @@ -104,7 +104,7 @@ - (void)setDefaults { @([ItalicStyle getStyleType]) : @[], @([UnderlineStyle getStyleType]) : @[], @([StrikethroughStyle getStyleType]) : @[], - @([InlineCodeStyle getStyleType]) : @[@([LinkStyle getStyleType]), @([MentionStyle getStyleType]), @([ImageStyle getStyleType])], + @([InlineCodeStyle getStyleType]) : @[@([LinkStyle getStyleType]), @([MentionStyle getStyleType])], @([LinkStyle getStyleType]): @[@([InlineCodeStyle getStyleType]), @([LinkStyle getStyleType]), @([MentionStyle getStyleType])], @([MentionStyle getStyleType]): @[@([InlineCodeStyle getStyleType]), @([LinkStyle getStyleType])], @([H1Style getStyleType]): @[@([H2Style getStyleType]), @([H3Style getStyleType]), @([UnorderedListStyle getStyleType]), @([OrderedListStyle getStyleType]), @([BlockQuoteStyle getStyleType])], @@ -117,10 +117,10 @@ - (void)setDefaults { }; _blockingStyles = @{ - @([BoldStyle getStyleType]) : @[], - @([ItalicStyle getStyleType]) : @[], - @([UnderlineStyle getStyleType]) : @[], - @([StrikethroughStyle getStyleType]) : @[], + @([BoldStyle getStyleType]) : @[@[@([ImageStyle getStyleType])]], + @([ItalicStyle getStyleType]) : @[@[@([ImageStyle getStyleType])]], + @([UnderlineStyle getStyleType]) : @[@[@([ImageStyle getStyleType])]], + @([StrikethroughStyle getStyleType]) : @[@[@([ImageStyle getStyleType])]], @([InlineCodeStyle getStyleType]) : @[@([ImageStyle getStyleType])], @([LinkStyle getStyleType]): @[@([ImageStyle getStyleType])], @([MentionStyle getStyleType]): @[@([ImageStyle getStyleType])], diff --git a/ios/inputParser/InputParser.mm b/ios/inputParser/InputParser.mm index 7b289623e..bdf068388 100644 --- a/ios/inputParser/InputParser.mm +++ b/ios/inputParser/InputParser.mm @@ -630,9 +630,20 @@ - (NSArray *)getTextAndStylesFromHtml:(NSString *)fixedHtml { } else if([tagName isEqualToString:@"i"]) { [styleArr addObject:@([ItalicStyle getStyleType])]; } 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 getStyleType])]; // cut only the uri from the src="..." string - NSString *uri = [params substringWithRange:NSMakeRange(5, params.length - 6)]; + NSString *uri = [params substringWithRange:NSMakeRange(srcRange.location + 5, srcRange.length - 6)]; ImageData *imageData = [[ImageData alloc] init]; imageData.uri = uri; diff --git a/ios/styles/ImageStyle.mm b/ios/styles/ImageStyle.mm index 63333a32b..f935b06ce 100644 --- a/ios/styles/ImageStyle.mm +++ b/ios/styles/ImageStyle.mm @@ -6,7 +6,6 @@ // custom NSAttributedStringKey to differentiate the image static NSString *const ImageAttributeName = @"ImageAttributeName"; - @implementation ImageStyle { EnrichedTextInputView *_input; } From 6cb925125f91fca8b5254882aab2acad39106878 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20=C5=BB=C3=B3=C5=82kiewski?= Date: Mon, 24 Nov 2025 15:59:19 +0100 Subject: [PATCH 8/9] fix: displaying image in empty line --- ios/styles/ImageStyle.mm | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/ios/styles/ImageStyle.mm b/ios/styles/ImageStyle.mm index f935b06ce..276014b99 100644 --- a/ios/styles/ImageStyle.mm +++ b/ios/styles/ImageStyle.mm @@ -12,6 +12,8 @@ @implementation ImageStyle { + (StyleType)getStyleType { return Image; } ++ (BOOL)isParagraphStyle { return NO; } + - (instancetype)initWithInput:(id)input { self = [super init]; _input = (EnrichedTextInputView *)input; @@ -114,11 +116,15 @@ - (void)addImage:(NSString *)uri { // This tells TextKit "something non-text goes here". NSString *imagePlaceholder = @"\uFFFC"; - [TextInsertionUtils replaceText:imagePlaceholder - at:_input->textView.selectedRange - additionalAttributes:_input->defaultTypingAttributes - input:_input - withSelection:YES]; + if (_input->textView.selectedRange.length == 0) { + [TextInsertionUtils insertText:imagePlaceholder at:_input->textView.selectedRange.location additionalAttributes:nullptr input:_input withSelection:YES]; + } else { + [TextInsertionUtils replaceText:imagePlaceholder + at:_input->textView.selectedRange + additionalAttributes:nullptr + input:_input + withSelection:YES]; + } NSRange newSelection = _input->textView.selectedRange; NSRange imageRange = NSMakeRange(newSelection.location - 1, 1); From ed005d3f4dbad17d06623165c05699d0e2c6995c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20=C5=BB=C3=B3=C5=82kiewski?= Date: Mon, 24 Nov 2025 16:00:35 +0100 Subject: [PATCH 9/9] fix: use ternary operator --- example/src/App.tsx | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/example/src/App.tsx b/example/src/App.tsx index 2d284c69a..3c29531fe 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -203,12 +203,10 @@ export default function App() { selectionLimit: 1, }); - let imageUri; - if (Platform.OS === 'android') { - imageUri = response.assets?.[0]?.originalPath; - } else { - imageUri = response.assets?.[0]?.uri; - } + const imageUri = + Platform.OS === 'android' + ? response.assets?.[0]?.originalPath + : response.assets?.[0]?.uri; if (!imageUri) return;