diff --git a/ios/EnrichedTextInputView.h b/ios/EnrichedTextInputView.h
index ec49f3f3c..9bf2c3dd8 100644
--- a/ios/EnrichedTextInputView.h
+++ b/ios/EnrichedTextInputView.h
@@ -3,6 +3,7 @@
#import "InputConfig.h"
#import "InputParser.h"
#import "InputTextView.h"
+#import "MediaAttachment.h"
#import
#import
@@ -11,7 +12,8 @@
NS_ASSUME_NONNULL_BEGIN
-@interface EnrichedTextInputView : RCTViewComponentView {
+@interface EnrichedTextInputView
+ : RCTViewComponentView {
@public
InputTextView *textView;
@public
diff --git a/ios/EnrichedTextInputView.mm b/ios/EnrichedTextInputView.mm
index c84dd532f..1647cd3e0 100644
--- a/ios/EnrichedTextInputView.mm
+++ b/ios/EnrichedTextInputView.mm
@@ -37,6 +37,7 @@ @implementation EnrichedTextInputView {
UILabel *_placeholderLabel;
UIColor *_placeholderColor;
BOOL _emitFocusBlur;
+ BOOL _didRunInitialMount;
}
// MARK: - Component utils
@@ -218,6 +219,31 @@ - (void)setupPlaceholderLabel {
_placeholderLabel.hidden = YES;
}
+- (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];
+}
+
// MARK: - Props
- (void)updateProps:(Props::Shared const &)props
@@ -529,32 +555,38 @@ - (void)updateProps:(Props::Shared const &)props
stylePropChanged = YES;
}
- if (stylePropChanged) {
- // all the text needs to be rebuilt
- // we get the current html using old config, then switch to new config and
- // replace text using the html this way, the newest config attributes are
- // being used!
-
- // the html needs to be generated using the old config
- NSString *currentHtml = [parser
- parseToHtmlFromRange:NSMakeRange(0,
- textView.textStorage.string.length)];
+ BOOL defaultValueChanged =
+ newViewProps.defaultValue != oldViewProps.defaultValue;
+ if (stylePropChanged) {
// now set the new config
config = newConfig;
- // no emitting during styles reload
- blockEmitting = YES;
+ // we already applied html with styles in default value
+ if (!defaultValueChanged) {
+ // all the text needs to be rebuilt
+ // we get the current html using old config, then switch to new config and
+ // replace text using the html this way, the newest config attributes are
+ // being used!
+
+ // the html needs to be generated using the old config
+ NSString *currentHtml = [parser
+ parseToHtmlFromRange:NSMakeRange(0,
+ textView.textStorage.string.length)];
+ // no emitting during styles reload
+ blockEmitting = YES;
+
+ // make sure everything is sound in the html
+ NSString *initiallyProcessedHtml =
+ [parser initiallyProcessHtml:currentHtml];
+ if (initiallyProcessedHtml != nullptr) {
+ [parser replaceWholeFromHtml:initiallyProcessedHtml
+ notifyAnyTextMayHaveBeenModified:!isFirstMount];
+ }
- // make sure everything is sound in the html
- NSString *initiallyProcessedHtml =
- [parser initiallyProcessHtml:currentHtml];
- if (initiallyProcessedHtml != nullptr) {
- [parser replaceWholeFromHtml:initiallyProcessedHtml];
+ blockEmitting = NO;
}
- blockEmitting = NO;
-
// fill the typing attributes with style props
defaultTypingAttributes[NSForegroundColorAttributeName] =
[config primaryColor];
@@ -578,7 +610,7 @@ - (void)updateProps:(Props::Shared const &)props
// default value - must be set before placeholder to make sure it correctly
// shows on first mount
- if (newViewProps.defaultValue != oldViewProps.defaultValue) {
+ if (defaultValueChanged) {
NSString *newDefaultValue =
[NSString fromCppString:newViewProps.defaultValue];
@@ -589,7 +621,8 @@ - (void)updateProps:(Props::Shared const &)props
textView.text = newDefaultValue;
} else {
// we've got some seemingly proper html
- [parser replaceWholeFromHtml:initiallyProcessedHtml];
+ [parser replaceWholeFromHtml:initiallyProcessedHtml
+ notifyAnyTextMayHaveBeenModified:!isFirstMount];
}
}
@@ -669,8 +702,13 @@ - (void)updateProps:(Props::Shared const &)props
_emitHtml = newViewProps.isOnChangeHtmlSet;
[super updateProps:props oldProps:oldProps];
- // run the changes callback
- [self anyTextMayHaveBeenModified];
+
+ // if default value changed it will be fired in default value update
+ // if this is initial mount it will be called in didMoveToWindow
+ if (!defaultValueChanged && !isFirstMount) {
+ // run the changes callback
+ [self anyTextMayHaveBeenModified];
+ }
// autofocus - needs to be done at the very end
if (isFirstMount && newViewProps.autoFocus) {
@@ -1002,7 +1040,8 @@ - (void)setValue:(NSString *)value {
textView.text = value;
} else {
// we've got some seemingly proper html
- [parser replaceWholeFromHtml:initiallyProcessedHtml];
+ [parser replaceWholeFromHtml:initiallyProcessedHtml
+ notifyAnyTextMayHaveBeenModified:YES];
}
// set recentlyChangedRange and check for changes
@@ -1418,8 +1457,13 @@ - (void)_performRelayout {
- (void)didMoveToWindow {
[super didMoveToWindow];
- // used to run all lifecycle callbacks
- [self anyTextMayHaveBeenModified];
+
+ if (self.window && !_didRunInitialMount) {
+ _didRunInitialMount = YES;
+ [self layoutIfNeeded];
+ // Ideally we should remove this to match RN's uncontrolled inputs behaviour
+ [self anyTextMayHaveBeenModified];
+ }
}
// MARK: - UITextView delegate methods
diff --git a/ios/inputParser/AttributedStringBuilder.h b/ios/inputParser/AttributedStringBuilder.h
new file mode 100644
index 000000000..0d9a733df
--- /dev/null
+++ b/ios/inputParser/AttributedStringBuilder.h
@@ -0,0 +1,14 @@
+#import
+#import
+
+@interface AttributedStringBuilder : NSObject
+
+@property(nonatomic, weak) NSDictionary *stylesDict;
+
+- (void)apply:(NSArray *)processedStyles
+ toAttributedString:(NSMutableAttributedString *)attributedString
+ offsetFromBeginning:(NSInteger)offset
+ conflictingStyles:
+ (NSDictionary *> *)conflictingStyles;
+
+@end
diff --git a/ios/inputParser/AttributedStringBuilder.mm b/ios/inputParser/AttributedStringBuilder.mm
new file mode 100644
index 000000000..9e44398dc
--- /dev/null
+++ b/ios/inputParser/AttributedStringBuilder.mm
@@ -0,0 +1,82 @@
+#import "AttributedStringBuilder.h"
+#import "StyleHeaders.h"
+#import "StylePair.h"
+
+@implementation AttributedStringBuilder
+
+- (void)apply:(NSArray *)processedStyles
+ toAttributedString:(NSMutableAttributedString *)attributedString
+ offsetFromBeginning:(NSInteger)offset
+ conflictingStyles:
+ (NSDictionary *> *)conflictingStyles {
+ [attributedString beginEditing];
+
+ for (NSArray *processedStylePair in processedStyles) {
+ NSNumber *type = processedStylePair[0];
+ StylePair *pair = processedStylePair[1];
+
+ NSRange pairRange = [pair.rangeValue rangeValue];
+ NSRange range = NSMakeRange(offset + pairRange.location, pairRange.length);
+ if (![self canApplyStyle:type
+ range:range
+ attributedString:attributedString
+ conflictingMap:conflictingStyles
+ stylesDict:self.stylesDict]) {
+ continue;
+ }
+
+ id style = self.stylesDict[type];
+
+ if ([type isEqualToNumber:@([LinkStyle getStyleType])]) {
+ NSString *text = [attributedString.string substringWithRange:range];
+ NSString *url = pair.styleValue;
+ BOOL isManual = [text isEqualToString:url];
+ [(LinkStyle *)style addLinkInAttributedString:attributedString
+ range:range
+ text:text
+ url:url
+ manual:isManual];
+
+ } else if ([type isEqualToNumber:@([MentionStyle getStyleType])]) {
+ [(MentionStyle *)style addMentionInAttributedString:attributedString
+ range:range
+ params:pair.styleValue];
+
+ } else if ([type isEqualToNumber:@([ImageStyle getStyleType])]) {
+ [(ImageStyle *)style addImageInAttributedString:attributedString
+ range:range
+ imageData:pair.styleValue];
+ } else {
+ [style addAttributesInAttributedString:attributedString range:range];
+ }
+ }
+
+ [attributedString endEditing];
+}
+
+- (BOOL)canApplyStyle:(NSNumber *)type
+ range:(NSRange)range
+ attributedString:(NSAttributedString *)string
+ conflictingMap:(NSDictionary *> *)confMap
+ stylesDict:
+ (NSDictionary> *)stylesDict {
+ NSArray *conflicts = confMap[type];
+ if (!conflicts || conflicts.count == 0)
+ return YES;
+
+ for (NSNumber *conflictType in conflicts) {
+ id conflictStyle = stylesDict[conflictType];
+ if (!conflictStyle)
+ continue;
+
+ NSArray *occurrences =
+ [conflictStyle findAllOccurencesInAttributedString:string range:range];
+
+ if (occurrences.count > 0) {
+ return NO;
+ }
+ }
+
+ return YES;
+}
+@end
diff --git a/ios/inputParser/ConvertHtmlToPlainTextAndStylesResult.h b/ios/inputParser/ConvertHtmlToPlainTextAndStylesResult.h
new file mode 100644
index 000000000..ea805215a
--- /dev/null
+++ b/ios/inputParser/ConvertHtmlToPlainTextAndStylesResult.h
@@ -0,0 +1,7 @@
+#import
+
+@interface ConvertHtmlToPlainTextAndStylesResult : NSObject
+@property(nonatomic, strong) NSString *text;
+@property(nonatomic, strong) NSArray *styles;
+- (instancetype)initWithData:(NSString *)text styles:(NSArray *)styles;
+@end
diff --git a/ios/inputParser/ConvertHtmlToPlainTextAndStylesResult.mm b/ios/inputParser/ConvertHtmlToPlainTextAndStylesResult.mm
new file mode 100644
index 000000000..08ee8cdd2
--- /dev/null
+++ b/ios/inputParser/ConvertHtmlToPlainTextAndStylesResult.mm
@@ -0,0 +1,10 @@
+#import "ConvertHtmlToPlainTextAndStylesResult.h"
+
+@implementation ConvertHtmlToPlainTextAndStylesResult
+- (instancetype)initWithData:(NSString *)text styles:(NSArray *)styles {
+ self = [super init];
+ _text = text;
+ _styles = styles;
+ return self;
+}
+@end
diff --git a/ios/inputParser/HtmlBuilder.h b/ios/inputParser/HtmlBuilder.h
new file mode 100644
index 000000000..1ed77406d
--- /dev/null
+++ b/ios/inputParser/HtmlBuilder.h
@@ -0,0 +1,12 @@
+#import "EnrichedTextInputView.h"
+#import
+#import
+
+@interface HtmlBuilder : NSObject
+
+@property(nonatomic, weak) NSDictionary *stylesDict;
+@property(nonatomic, weak) EnrichedTextInputView *input;
+
+- (NSString *)htmlFromRange:(NSRange)range;
+
+@end
diff --git a/ios/inputParser/HtmlBuilder.mm b/ios/inputParser/HtmlBuilder.mm
new file mode 100644
index 000000000..e4608f028
--- /dev/null
+++ b/ios/inputParser/HtmlBuilder.mm
@@ -0,0 +1,484 @@
+#import "HtmlBuilder.h"
+#import "StringExtension.h"
+#import "StyleHeaders.h"
+
+@implementation HtmlBuilder
+
+- (NSString *)htmlFromRange:(NSRange)range {
+ NSInteger offset = range.location;
+ NSString *text =
+ [_input->textView.textStorage.string substringWithRange:range];
+
+ if (text.length == 0) {
+ return @"\n\n";
+ }
+
+ NSMutableString *result = [[NSMutableString alloc] initWithString:@""];
+ NSSet *previousActiveStyles = [[NSSet alloc] init];
+ BOOL newLine = YES;
+ BOOL inUnorderedList = NO;
+ BOOL inOrderedList = NO;
+ BOOL inBlockQuote = NO;
+ BOOL inCodeBlock = NO;
+ unichar lastCharacter = 0;
+
+ for (int i = 0; i < text.length; i++) {
+ NSRange currentRange = NSMakeRange(offset + i, 1);
+ NSMutableSet *currentActiveStyles =
+ [[NSMutableSet alloc] init];
+ NSMutableDictionary *currentActiveStylesBeginning =
+ [[NSMutableDictionary alloc] init];
+
+ // check each existing style existence
+ for (NSNumber *type in _input->stylesDict) {
+ id style = _input->stylesDict[type];
+ if ([style detectStyle:currentRange]) {
+ [currentActiveStyles addObject:type];
+
+ if (![previousActiveStyles member:type]) {
+ currentActiveStylesBeginning[type] = [NSNumber numberWithInt:i];
+ }
+ } else if ([previousActiveStyles member:type]) {
+ [currentActiveStylesBeginning removeObjectForKey:type];
+ }
+ }
+
+ NSString *currentCharacterStr =
+ [_input->textView.textStorage.string substringWithRange:currentRange];
+ unichar currentCharacterChar = [_input->textView.textStorage.string
+ characterAtIndex:currentRange.location];
+
+ if ([[NSCharacterSet newlineCharacterSet]
+ characterIsMember:currentCharacterChar]) {
+ if (newLine) {
+ // we can either have an empty list item OR need to close the list and
+ // put a BR in such a situation the existence of the list must be
+ // checked on 0 length range, not on the newline character
+ if (inOrderedList) {
+ OrderedListStyle *oStyle = _input->stylesDict[@(OrderedList)];
+ BOOL detected =
+ [oStyle detectStyle:NSMakeRange(currentRange.location, 0)];
+ if (detected) {
+ [result appendString:@"\n"];
+ } else {
+ [result appendString:@"\n\n
"];
+ inOrderedList = NO;
+ }
+ } else if (inUnorderedList) {
+ UnorderedListStyle *uStyle = _input->stylesDict[@(UnorderedList)];
+ BOOL detected =
+ [uStyle detectStyle:NSMakeRange(currentRange.location, 0)];
+ if (detected) {
+ [result appendString:@"\n"];
+ } else {
+ [result appendString:@"\n\n
"];
+ inUnorderedList = NO;
+ }
+ } else {
+ [result appendString:@"\n
"];
+ }
+ } else {
+ // newline finishes a paragraph and all style tags need to be closed
+ // we use previous styles
+ NSArray *sortedEndedStyles = [previousActiveStyles
+ sortedArrayUsingDescriptors:@[ [NSSortDescriptor
+ sortDescriptorWithKey:@"intValue"
+ ascending:NO] ]];
+
+ // 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]];
+ }
+
+ // append closing paragraph tag
+ if ([previousActiveStyles
+ containsObject:@([UnorderedListStyle getStyleType])] ||
+ [previousActiveStyles
+ containsObject:@([OrderedListStyle getStyleType])] ||
+ [previousActiveStyles containsObject:@([H1Style getStyleType])] ||
+ [previousActiveStyles containsObject:@([H2Style getStyleType])] ||
+ [previousActiveStyles containsObject:@([H3Style getStyleType])] ||
+ [previousActiveStyles
+ containsObject:@([BlockQuoteStyle getStyleType])] ||
+ [previousActiveStyles
+ containsObject:@([CodeBlockStyle getStyleType])]) {
+ // do nothing, proper closing paragraph tags have been already
+ // appended
+ } else {
+ [result appendString:@"
"];
+ }
+ }
+
+ // clear the previous styles
+ previousActiveStyles = [[NSSet alloc] init];
+
+ // next character opens new paragraph
+ newLine = YES;
+ } else {
+ // new line - open the paragraph
+ if (newLine) {
+ newLine = NO;
+
+ // handle ending unordered list
+ if (inUnorderedList &&
+ ![currentActiveStyles
+ containsObject:@([UnorderedListStyle getStyleType])]) {
+ inUnorderedList = NO;
+ [result appendString:@"\n"];
+ }
+ // handle ending ordered list
+ if (inOrderedList &&
+ ![currentActiveStyles
+ containsObject:@([OrderedListStyle getStyleType])]) {
+ inOrderedList = NO;
+ [result appendString:@"\n"];
+ }
+ // handle ending blockquotes
+ if (inBlockQuote &&
+ ![currentActiveStyles
+ containsObject:@([BlockQuoteStyle getStyleType])]) {
+ inBlockQuote = NO;
+ [result appendString:@"\n"];
+ }
+ // handle ending codeblock
+ if (inCodeBlock &&
+ ![currentActiveStyles
+ containsObject:@([CodeBlockStyle getStyleType])]) {
+ inCodeBlock = NO;
+ [result appendString:@"\n"];
+ }
+
+ // handle starting unordered list
+ if (!inUnorderedList &&
+ [currentActiveStyles
+ containsObject:@([UnorderedListStyle getStyleType])]) {
+ inUnorderedList = YES;
+ [result appendString:@"\n"];
+ }
+ // handle starting ordered list
+ if (!inOrderedList &&
+ [currentActiveStyles
+ containsObject:@([OrderedListStyle getStyleType])]) {
+ inOrderedList = YES;
+ [result appendString:@"\n"];
+ }
+ // handle starting blockquotes
+ if (!inBlockQuote &&
+ [currentActiveStyles
+ containsObject:@([BlockQuoteStyle getStyleType])]) {
+ inBlockQuote = YES;
+ [result appendString:@"\n"];
+ }
+ // handle starting codeblock
+ if (!inCodeBlock &&
+ [currentActiveStyles
+ containsObject:@([CodeBlockStyle getStyleType])]) {
+ inCodeBlock = YES;
+ [result appendString:@"\n"];
+ }
+
+ // don't add the tag if some paragraph styles are present
+ if ([currentActiveStyles
+ containsObject:@([UnorderedListStyle getStyleType])] ||
+ [currentActiveStyles
+ containsObject:@([OrderedListStyle getStyleType])] ||
+ [currentActiveStyles containsObject:@([H1Style getStyleType])] ||
+ [currentActiveStyles containsObject:@([H2Style getStyleType])] ||
+ [currentActiveStyles containsObject:@([H3Style getStyleType])] ||
+ [currentActiveStyles
+ containsObject:@([BlockQuoteStyle getStyleType])] ||
+ [currentActiveStyles
+ containsObject:@([CodeBlockStyle getStyleType])]) {
+ [result appendString:@"\n"];
+ } else {
+ [result appendString:@"\n
"];
+ }
+ }
+
+ // get styles that have ended
+ NSMutableSet *endedStyles =
+ [previousActiveStyles mutableCopy];
+ [endedStyles minusSet:currentActiveStyles];
+
+ // also finish styles that should be ended becasue they are nested in a
+ // style that ended
+ NSMutableSet *fixedEndedStyles = [endedStyles mutableCopy];
+ NSMutableSet *stylesToBeReAdded = [[NSMutableSet alloc] init];
+
+ for (NSNumber *style in endedStyles) {
+ NSInteger styleBeginning =
+ [currentActiveStylesBeginning[style] integerValue];
+
+ for (NSNumber *activeStyle in currentActiveStyles) {
+ NSInteger activeStyleBeginning =
+ [currentActiveStylesBeginning[activeStyle] integerValue];
+
+ // we end the styles that began after the currently ended style but
+ // not at the "i" (cause the old style ended at exactly "i-1" also the
+ // ones that began in the exact same place but are "inner" in relation
+ // to them due to StyleTypeEnum integer values
+
+ if ((activeStyleBeginning > styleBeginning &&
+ activeStyleBeginning < i) ||
+ (activeStyleBeginning == styleBeginning &&
+ activeStyleBeginning<
+ i && [activeStyle integerValue]>[style integerValue])) {
+ [fixedEndedStyles addObject:activeStyle];
+ [stylesToBeReAdded addObject:activeStyle];
+ }
+ }
+ }
+
+ // if a style begins but there is a style inner to it that is (and was
+ // previously) active, it also should be closed and readded
+
+ // newly added styles
+ NSMutableSet *newStyles = [currentActiveStyles mutableCopy];
+ [newStyles minusSet:previousActiveStyles];
+ // styles that were and still are active
+ NSMutableSet *stillActiveStyles = [previousActiveStyles mutableCopy];
+ [stillActiveStyles intersectSet:currentActiveStyles];
+
+ for (NSNumber *style in newStyles) {
+ for (NSNumber *ongoingStyle in stillActiveStyles) {
+ if ([ongoingStyle integerValue] > [style integerValue]) {
+ // the prev style is inner; needs to be closed and re-added later
+ [fixedEndedStyles addObject:ongoingStyle];
+ [stylesToBeReAdded addObject:ongoingStyle];
+ }
+ }
+ }
+
+ // they are sorted in a descending order
+ NSArray *sortedEndedStyles = [fixedEndedStyles
+ sortedArrayUsingDescriptors:@[ [NSSortDescriptor
+ sortDescriptorWithKey:@"intValue"
+ ascending:NO] ]];
+
+ // 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]];
+ }
+
+ // all styles that have begun: new styles + the ones that need to be
+ // re-added they are sorted in a ascending manner to properly keep tags'
+ // FILO order
+ [newStyles unionSet:stylesToBeReAdded];
+ NSArray *sortedNewStyles = [newStyles
+ sortedArrayUsingDescriptors:@[ [NSSortDescriptor
+ sortDescriptorWithKey:@"intValue"
+ ascending:YES] ]];
+
+ // append opening tags
+ for (NSNumber *style in sortedNewStyles) {
+ NSString *tagContent = [self tagContentForStyle:style
+ openingTag:YES
+ location:currentRange.location];
+ if ([style isEqualToNumber:@([ImageStyle getStyleType])]) {
+ [result
+ appendString:[NSString stringWithFormat:@"<%@/>", tagContent]];
+ [currentActiveStyles removeObject:@([ImageStyle getStyleType])];
+ } else {
+ [result appendString:[NSString stringWithFormat:@"<%@>", tagContent]];
+ }
+ }
+
+ // append the letter and escape it if needed
+ [result appendString:[NSString stringByEscapingHtml:currentCharacterStr]];
+
+ // save current styles for next character's checks
+ previousActiveStyles = currentActiveStyles;
+ }
+
+ // set last character
+ lastCharacter = currentCharacterChar;
+ }
+
+ if (![[NSCharacterSet newlineCharacterSet] characterIsMember:lastCharacter]) {
+ // not-newline character was last - finish the paragraph
+ // close all pending tags
+ NSArray *sortedEndedStyles = [previousActiveStyles
+ sortedArrayUsingDescriptors:@[ [NSSortDescriptor
+ sortDescriptorWithKey:@"intValue"
+ ascending:NO] ]];
+
+ // 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]];
+ }
+
+ // finish the paragraph
+ // handle ending of some paragraph styles
+ if ([previousActiveStyles
+ containsObject:@([UnorderedListStyle getStyleType])]) {
+ [result appendString:@"\n
"];
+ } else if ([previousActiveStyles
+ containsObject:@([OrderedListStyle getStyleType])]) {
+ [result appendString:@"\n"];
+ } else if ([previousActiveStyles
+ containsObject:@([BlockQuoteStyle getStyleType])]) {
+ [result appendString:@"\n"];
+ } else if ([previousActiveStyles
+ containsObject:@([CodeBlockStyle getStyleType])]) {
+ [result appendString:@"\n"];
+ } else if ([previousActiveStyles
+ containsObject:@([H1Style getStyleType])] ||
+ [previousActiveStyles
+ containsObject:@([H2Style getStyleType])] ||
+ [previousActiveStyles
+ containsObject:@([H3Style getStyleType])]) {
+ // do nothing, heading closing tag has already ben appended
+ } else {
+ [result appendString:@""];
+ }
+ } else {
+ // newline character was last - some paragraph styles need to be closed
+ if (inUnorderedList) {
+ inUnorderedList = NO;
+ [result appendString:@"\n"];
+ }
+ if (inOrderedList) {
+ inOrderedList = NO;
+ [result appendString:@"\n"];
+ }
+ if (inBlockQuote) {
+ inBlockQuote = NO;
+ [result appendString:@"\n"];
+ }
+ if (inCodeBlock) {
+ inCodeBlock = NO;
+ [result appendString:@"\n"];
+ }
+ }
+
+ [result appendString:@"\n"];
+
+ // remove zero width spaces in the very end
+ NSRange resultRange = NSMakeRange(0, result.length);
+ [result replaceOccurrencesOfString:@"\u200B"
+ withString:@""
+ options:0
+ range:resultRange];
+ return result;
+}
+
+- (NSString *)tagContentForStyle:(NSNumber *)style
+ openingTag:(BOOL)openingTag
+ location:(NSInteger)location {
+ if ([style isEqualToNumber:@([BoldStyle getStyleType])]) {
+ 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=\"%@\" width=\"%f\" height=\"%f\"",
+ data.uri, data.width, data.height];
+ }
+ }
+ return @"img";
+ } else {
+ return @"";
+ }
+ } else if ([style isEqualToNumber:@([UnderlineStyle getStyleType])]) {
+ return @"u";
+ } else if ([style isEqualToNumber:@([StrikethroughStyle getStyleType])]) {
+ return @"s";
+ } else if ([style isEqualToNumber:@([InlineCodeStyle getStyleType])]) {
+ return @"code";
+ } else if ([style isEqualToNumber:@([LinkStyle getStyleType])]) {
+ if (openingTag) {
+ LinkStyle *linkStyle =
+ (LinkStyle *)_input->stylesDict[@([LinkStyle getStyleType])];
+ if (linkStyle != nullptr) {
+ LinkData *data = [linkStyle getLinkDataAt:location];
+ if (data != nullptr && data.url != nullptr) {
+ return [NSString stringWithFormat:@"a href=\"%@\"", data.url];
+ }
+ }
+ return @"a";
+ } else {
+ return @"a";
+ }
+ } else if ([style isEqualToNumber:@([MentionStyle getStyleType])]) {
+ if (openingTag) {
+ MentionStyle *mentionStyle =
+ (MentionStyle *)_input->stylesDict[@([MentionStyle getStyleType])];
+ if (mentionStyle != nullptr) {
+ MentionParams *params = [mentionStyle getMentionParamsAt:location];
+ // attributes can theoretically be nullptr
+ if (params != nullptr && params.indicator != nullptr &&
+ params.text != nullptr) {
+ NSMutableString *attrsStr =
+ [[NSMutableString alloc] initWithString:@""];
+ if (params.attributes != nullptr) {
+ // turn attributes to Data and then into dict
+ NSData *attrsData =
+ [params.attributes dataUsingEncoding:NSUTF8StringEncoding];
+ NSError *jsonError;
+ NSDictionary *json =
+ [NSJSONSerialization JSONObjectWithData:attrsData
+ options:0
+ error:&jsonError];
+ // format dict keys and values into string
+ [json enumerateKeysAndObjectsUsingBlock:^(
+ id _Nonnull key, id _Nonnull obj, BOOL *_Nonnull stop) {
+ [attrsStr
+ appendString:[NSString stringWithFormat:@" %@=\"%@\"",
+ (NSString *)key,
+ (NSString *)obj]];
+ }];
+ }
+ return [NSString
+ stringWithFormat:@"mention text=\"%@\" indicator=\"%@\"%@",
+ params.text, params.indicator, attrsStr];
+ }
+ }
+ return @"mention";
+ } else {
+ return @"mention";
+ }
+ } else if ([style isEqualToNumber:@([H1Style getStyleType])]) {
+ return @"h1";
+ } else if ([style isEqualToNumber:@([H2Style getStyleType])]) {
+ return @"h2";
+ } else if ([style isEqualToNumber:@([H3Style getStyleType])]) {
+ return @"h3";
+ } else if ([style isEqualToNumber:@([UnorderedListStyle getStyleType])] ||
+ [style isEqualToNumber:@([OrderedListStyle getStyleType])]) {
+ return @"li";
+ } else if ([style isEqualToNumber:@([BlockQuoteStyle getStyleType])] ||
+ [style isEqualToNumber:@([CodeBlockStyle getStyleType])]) {
+ // blockquotes and codeblock use tags the same way lists use
+ return @"p";
+ }
+ return @"";
+}
+
+@end
diff --git a/ios/inputParser/HtmlHandler.h b/ios/inputParser/HtmlHandler.h
new file mode 100644
index 000000000..35908d3c0
--- /dev/null
+++ b/ios/inputParser/HtmlHandler.h
@@ -0,0 +1,10 @@
+#import
+
+@class ConvertHtmlToPlainTextAndStylesResult;
+@class HtmlTokenizationResult;
+
+@interface HtmlHandler : NSObject
+- (NSString *)initiallyProcessHtml:(NSString *)html;
+- (ConvertHtmlToPlainTextAndStylesResult *)getTextAndStylesFromHtml:
+ (NSString *)fixedHtml;
+@end
diff --git a/ios/inputParser/HtmlHandler.mm b/ios/inputParser/HtmlHandler.mm
new file mode 100644
index 000000000..b39af4a96
--- /dev/null
+++ b/ios/inputParser/HtmlHandler.mm
@@ -0,0 +1,386 @@
+#import "HtmlHandler.h"
+#import "ConvertHtmlToPlainTextAndStylesResult.h"
+#import "HtmlTokenizationResult.h"
+#import "StringExtension.h"
+#import "StyleHeaders.h"
+#import "TagHandlersFactory.h"
+
+static const int MIN_HTML_SIZE = 13;
+
+@implementation HtmlHandler
+
+static NSDictionary *TagHandlers;
+
++ (void)initialize {
+ if (self != [HtmlHandler class])
+ return;
+ TagHandlers = MakeTagHandlers();
+}
+
+- (NSMutableArray *)convertTagsToStyles:(NSArray *)initiallyProcessedTags {
+ NSMutableArray *processedStyles = [NSMutableArray array];
+
+ for (NSArray *arr in initiallyProcessedTags) {
+ NSString *tagName = arr[0];
+ NSValue *tagRangeValue = arr[1];
+ NSString *params = arr.count > 2 ? arr[2] : @"";
+
+ TagHandler tagHandler = TagHandlers[tagName];
+ if (!tagHandler)
+ continue;
+
+ StylePair *pair = [StylePair new];
+ pair.rangeValue = tagRangeValue;
+
+ NSMutableArray *styleArr = [NSMutableArray array];
+ tagHandler(params, pair, styleArr);
+
+ if (styleArr.count == 0)
+ continue;
+
+ [styleArr addObject:pair];
+ [processedStyles addObject:styleArr];
+ }
+
+ return processedStyles;
+}
+
+- (NSString *_Nullable)initiallyProcessHtml:(NSString *_Nonnull)html {
+ NSString *fixedHtml = nullptr;
+
+ if (html.length >= 13) {
+ NSString *firstSix = [html substringWithRange:NSMakeRange(0, 6)];
+ NSString *lastSeven =
+ [html substringWithRange:NSMakeRange(html.length - 7, 7)];
+
+ if ([firstSix isEqualToString:@""] &&
+ [lastSeven isEqualToString:@""]) {
+ // remove html tags, might be with newlines or without them
+ fixedHtml = [html 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 {
+ // in other case we are most likely working with some external html - try
+ // getting the styles from between body tags
+ NSRange openingBodyRange = [html rangeOfString:@""];
+ NSRange closingBodyRange = [html rangeOfString:@""];
+
+ if (openingBodyRange.length != 0 && closingBodyRange.length != 0) {
+ NSInteger newStart = openingBodyRange.location + 7;
+ NSInteger newEnd = closingBodyRange.location - 1;
+ fixedHtml = [html
+ 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:@""];
+
+ // 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];
+
+ // 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];
+ }
+
+ return fixedHtml;
+}
+
+- (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;
+}
+
+- (HtmlTokenizationResult *)tokenize:(NSString *)fixedHtml {
+ NSMutableString *plainText = [[NSMutableString alloc] initWithString:@""];
+ NSMutableDictionary *ongoingTags = [[NSMutableDictionary alloc] init];
+ NSMutableArray *initiallyProcessedTags = [[NSMutableArray alloc] init];
+ 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"] ||
+ [currentTagName isEqualToString:@"li"]) {
+ // do nothing, we don't include these tags in styles
+ } 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;
+
+ // skip one newline after opening tags that are in separate lines
+ // intentionally
+ if ([currentTagName isEqualToString:@"ul"] ||
+ [currentTagName isEqualToString:@"ol"] ||
+ [currentTagName isEqualToString:@"blockquote"] ||
+ [currentTagName isEqualToString:@"codeblock"]) {
+ i += 1;
+ }
+
+ if (isSelfClosing) {
+ [self finalizeTag: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
+
+ // skip one newline that was added before some closing tags that are in
+ // separate lines
+ if ([currentTagName isEqualToString:@"ul"] ||
+ [currentTagName isEqualToString:@"ol"] ||
+ [currentTagName isEqualToString:@"blockquote"] ||
+ [currentTagName isEqualToString:@"codeblock"]) {
+ plainText = [[plainText
+ substringWithRange:NSMakeRange(0, plainText.length - 1)]
+ mutableCopy];
+ }
+
+ [self finalizeTag: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];
+ }
+ }
+ }
+ }
+
+ return [[HtmlTokenizationResult alloc] initWithData:plainText
+ tags:initiallyProcessedTags];
+}
+
+- (void)finalizeTag:(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];
+ NSRange tagRange = NSMakeRange(tagLocation, 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];
+}
+
+- (ConvertHtmlToPlainTextAndStylesResult *)getTextAndStylesFromHtml:
+ (NSString *)fixedHtml {
+ HtmlTokenizationResult *tagTokens = [self tokenize:fixedHtml];
+ NSMutableArray *processed = [self convertTagsToStyles:tagTokens.tags];
+
+ NSArray *sorted = [processed sortedArrayUsingComparator:^NSComparisonResult(
+ NSArray *firstArray, NSArray *secondArray) {
+ StylePair *firstStylePair = firstArray[1];
+ StylePair *secondStylePair = secondArray[1];
+
+ NSRange firstStyleRange = [firstStylePair.rangeValue rangeValue];
+ NSRange secondStyleRange = [secondStylePair.rangeValue rangeValue];
+ NSInteger firstStyleLocation = firstStyleRange.location;
+ NSInteger secondStyleLocation = secondStyleRange.location;
+
+ if (firstStyleLocation < secondStyleLocation)
+ return NSOrderedDescending;
+ if (firstStyleLocation > secondStyleLocation)
+ return NSOrderedAscending;
+ return NSOrderedSame;
+ }];
+
+ return
+ [[ConvertHtmlToPlainTextAndStylesResult alloc] initWithData:tagTokens.text
+ styles:sorted];
+}
+
+@end
diff --git a/ios/inputParser/HtmlTokenizationResult.h b/ios/inputParser/HtmlTokenizationResult.h
new file mode 100644
index 000000000..1546c363a
--- /dev/null
+++ b/ios/inputParser/HtmlTokenizationResult.h
@@ -0,0 +1,8 @@
+#import
+
+@interface HtmlTokenizationResult : NSObject
+@property(nonatomic, strong) NSString *text;
+@property(nonatomic, strong) NSMutableArray *tags;
+- (instancetype)initWithData:(NSString *_Nonnull)text
+ tags:(NSMutableArray *_Nonnull)tags;
+@end
diff --git a/ios/inputParser/HtmlTokenizationResult.mm b/ios/inputParser/HtmlTokenizationResult.mm
new file mode 100644
index 000000000..9de04d43c
--- /dev/null
+++ b/ios/inputParser/HtmlTokenizationResult.mm
@@ -0,0 +1,11 @@
+#import "HtmlTokenizationResult.h"
+
+@implementation HtmlTokenizationResult
+- (instancetype)initWithData:(NSString *)text
+ tags:(NSMutableArray *_Nonnull)tags {
+ self = [super init];
+ _text = text;
+ _tags = tags;
+ return self;
+}
+@end
diff --git a/ios/inputParser/InputParser.h b/ios/inputParser/InputParser.h
index b54ace32e..7d04ea9c5 100644
--- a/ios/inputParser/InputParser.h
+++ b/ios/inputParser/InputParser.h
@@ -1,10 +1,13 @@
#pragma once
#import
+@class ConvertHtmlToPlainTextAndStylesResult;
+
@interface InputParser : NSObject
- (instancetype _Nonnull)initWithInput:(id _Nonnull)input;
- (NSString *_Nonnull)parseToHtmlFromRange:(NSRange)range;
-- (void)replaceWholeFromHtml:(NSString *_Nonnull)html;
+- (void)replaceWholeFromHtml:(NSString *_Nonnull)html
+ notifyAnyTextMayHaveBeenModified:(BOOL)notifyAnyTextMayHaveBeenModified;
- (void)replaceFromHtml:(NSString *_Nonnull)html range:(NSRange)range;
- (void)insertFromHtml:(NSString *_Nonnull)html location:(NSInteger)location;
- (NSString *_Nullable)initiallyProcessHtml:(NSString *_Nonnull)html;
diff --git a/ios/inputParser/InputParser.mm b/ios/inputParser/InputParser.mm
index 7ad395ede..0d21959bf 100644
--- a/ios/inputParser/InputParser.mm
+++ b/ios/inputParser/InputParser.mm
@@ -1,1071 +1,130 @@
#import "InputParser.h"
#import "EnrichedTextInputView.h"
-#import "StringExtension.h"
+
+#import "AttributedStringBuilder.h"
+#import "ConvertHtmlToPlainTextAndStylesResult.h"
+#import "HtmlBuilder.h"
+#import "HtmlHandler.h"
+
#import "StyleHeaders.h"
-#import "TextInsertionUtils.h"
-#import "UIView+React.h"
@implementation InputParser {
EnrichedTextInputView *_input;
+ AttributedStringBuilder *_builder;
+ HtmlBuilder *_htmlBuilder;
+ HtmlHandler *_htmlHandler;
}
- (instancetype)initWithInput:(id)input {
self = [super init];
_input = (EnrichedTextInputView *)input;
- return self;
-}
-
-- (NSString *)parseToHtmlFromRange:(NSRange)range {
- NSInteger offset = range.location;
- NSString *text =
- [_input->textView.textStorage.string substringWithRange:range];
-
- if (text.length == 0) {
- return @"\n\n";
- }
-
- NSMutableString *result = [[NSMutableString alloc] initWithString:@""];
- NSSet *previousActiveStyles = [[NSSet alloc] init];
- BOOL newLine = YES;
- BOOL inUnorderedList = NO;
- BOOL inOrderedList = NO;
- BOOL inBlockQuote = NO;
- BOOL inCodeBlock = NO;
- unichar lastCharacter = 0;
-
- for (int i = 0; i < text.length; i++) {
- NSRange currentRange = NSMakeRange(offset + i, 1);
- NSMutableSet *currentActiveStyles =
- [[NSMutableSet alloc] init];
- NSMutableDictionary *currentActiveStylesBeginning =
- [[NSMutableDictionary alloc] init];
-
- // check each existing style existence
- for (NSNumber *type in _input->stylesDict) {
- id style = _input->stylesDict[type];
- if ([style detectStyle:currentRange]) {
- [currentActiveStyles addObject:type];
-
- if (![previousActiveStyles member:type]) {
- currentActiveStylesBeginning[type] = [NSNumber numberWithInt:i];
- }
- } else if ([previousActiveStyles member:type]) {
- [currentActiveStylesBeginning removeObjectForKey:type];
- }
- }
-
- NSString *currentCharacterStr =
- [_input->textView.textStorage.string substringWithRange:currentRange];
- unichar currentCharacterChar = [_input->textView.textStorage.string
- characterAtIndex:currentRange.location];
-
- if ([[NSCharacterSet newlineCharacterSet]
- characterIsMember:currentCharacterChar]) {
- if (newLine) {
- // we can either have an empty list item OR need to close the list and
- // put a BR in such a situation the existence of the list must be
- // checked on 0 length range, not on the newline character
- if (inOrderedList) {
- OrderedListStyle *oStyle = _input->stylesDict[@(OrderedList)];
- BOOL detected =
- [oStyle detectStyle:NSMakeRange(currentRange.location, 0)];
- if (detected) {
- [result appendString:@"\n"];
- } else {
- [result appendString:@"\n\n
"];
- inOrderedList = NO;
- }
- } else if (inUnorderedList) {
- UnorderedListStyle *uStyle = _input->stylesDict[@(UnorderedList)];
- BOOL detected =
- [uStyle detectStyle:NSMakeRange(currentRange.location, 0)];
- if (detected) {
- [result appendString:@"\n"];
- } else {
- [result appendString:@"\n\n
"];
- inUnorderedList = NO;
- }
- } else {
- [result appendString:@"\n
"];
- }
- } else {
- // newline finishes a paragraph and all style tags need to be closed
- // we use previous styles
- NSArray *sortedEndedStyles = [previousActiveStyles
- sortedArrayUsingDescriptors:@[ [NSSortDescriptor
- sortDescriptorWithKey:@"intValue"
- ascending:NO] ]];
-
- // 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]];
- }
-
- // append closing paragraph tag
- if ([previousActiveStyles
- containsObject:@([UnorderedListStyle getStyleType])] ||
- [previousActiveStyles
- containsObject:@([OrderedListStyle getStyleType])] ||
- [previousActiveStyles containsObject:@([H1Style getStyleType])] ||
- [previousActiveStyles containsObject:@([H2Style getStyleType])] ||
- [previousActiveStyles containsObject:@([H3Style getStyleType])] ||
- [previousActiveStyles
- containsObject:@([BlockQuoteStyle getStyleType])] ||
- [previousActiveStyles
- containsObject:@([CodeBlockStyle getStyleType])]) {
- // do nothing, proper closing paragraph tags have been already
- // appended
- } else {
- [result appendString:@""];
- }
- }
-
- // clear the previous styles
- previousActiveStyles = [[NSSet alloc] init];
-
- // next character opens new paragraph
- newLine = YES;
- } else {
- // new line - open the paragraph
- if (newLine) {
- newLine = NO;
-
- // handle ending unordered list
- if (inUnorderedList &&
- ![currentActiveStyles
- containsObject:@([UnorderedListStyle getStyleType])]) {
- inUnorderedList = NO;
- [result appendString:@"\n"];
- }
- // handle ending ordered list
- if (inOrderedList &&
- ![currentActiveStyles
- containsObject:@([OrderedListStyle getStyleType])]) {
- inOrderedList = NO;
- [result appendString:@"\n"];
- }
- // handle ending blockquotes
- if (inBlockQuote &&
- ![currentActiveStyles
- containsObject:@([BlockQuoteStyle getStyleType])]) {
- inBlockQuote = NO;
- [result appendString:@"\n"];
- }
- // handle ending codeblock
- if (inCodeBlock &&
- ![currentActiveStyles
- containsObject:@([CodeBlockStyle getStyleType])]) {
- inCodeBlock = NO;
- [result appendString:@"\n"];
- }
-
- // handle starting unordered list
- if (!inUnorderedList &&
- [currentActiveStyles
- containsObject:@([UnorderedListStyle getStyleType])]) {
- inUnorderedList = YES;
- [result appendString:@"\n"];
- }
- // handle starting ordered list
- if (!inOrderedList &&
- [currentActiveStyles
- containsObject:@([OrderedListStyle getStyleType])]) {
- inOrderedList = YES;
- [result appendString:@"\n"];
- }
- // handle starting blockquotes
- if (!inBlockQuote &&
- [currentActiveStyles
- containsObject:@([BlockQuoteStyle getStyleType])]) {
- inBlockQuote = YES;
- [result appendString:@"\n"];
- }
- // handle starting codeblock
- if (!inCodeBlock &&
- [currentActiveStyles
- containsObject:@([CodeBlockStyle getStyleType])]) {
- inCodeBlock = YES;
- [result appendString:@"\n"];
- }
-
- // don't add the tag if some paragraph styles are present
- if ([currentActiveStyles
- containsObject:@([UnorderedListStyle getStyleType])] ||
- [currentActiveStyles
- containsObject:@([OrderedListStyle getStyleType])] ||
- [currentActiveStyles containsObject:@([H1Style getStyleType])] ||
- [currentActiveStyles containsObject:@([H2Style getStyleType])] ||
- [currentActiveStyles containsObject:@([H3Style getStyleType])] ||
- [currentActiveStyles
- containsObject:@([BlockQuoteStyle getStyleType])] ||
- [currentActiveStyles
- containsObject:@([CodeBlockStyle getStyleType])]) {
- [result appendString:@"\n"];
- } else {
- [result appendString:@"\n
"];
- }
- }
-
- // get styles that have ended
- NSMutableSet *endedStyles =
- [previousActiveStyles mutableCopy];
- [endedStyles minusSet:currentActiveStyles];
-
- // also finish styles that should be ended becasue they are nested in a
- // style that ended
- NSMutableSet *fixedEndedStyles = [endedStyles mutableCopy];
- NSMutableSet *stylesToBeReAdded = [[NSMutableSet alloc] init];
-
- for (NSNumber *style in endedStyles) {
- NSInteger styleBeginning =
- [currentActiveStylesBeginning[style] integerValue];
- for (NSNumber *activeStyle in currentActiveStyles) {
- NSInteger activeStyleBeginning =
- [currentActiveStylesBeginning[activeStyle] integerValue];
+ _builder = [AttributedStringBuilder new];
+ _builder.stylesDict = _input->stylesDict;
- // we end the styles that began after the currently ended style but
- // not at the "i" (cause the old style ended at exactly "i-1" also the
- // ones that began in the exact same place but are "inner" in relation
- // to them due to StyleTypeEnum integer values
+ _htmlBuilder = [HtmlBuilder new];
+ _htmlBuilder.stylesDict = _input->stylesDict;
+ _htmlBuilder.input = _input;
- if ((activeStyleBeginning > styleBeginning &&
- activeStyleBeginning < i) ||
- (activeStyleBeginning == styleBeginning &&
- activeStyleBeginning<
- i && [activeStyle integerValue]>[style integerValue])) {
- [fixedEndedStyles addObject:activeStyle];
- [stylesToBeReAdded addObject:activeStyle];
- }
- }
- }
+ _htmlHandler = [HtmlHandler new];
- // if a style begins but there is a style inner to it that is (and was
- // previously) active, it also should be closed and readded
-
- // newly added styles
- NSMutableSet *newStyles = [currentActiveStyles mutableCopy];
- [newStyles minusSet:previousActiveStyles];
- // styles that were and still are active
- NSMutableSet *stillActiveStyles = [previousActiveStyles mutableCopy];
- [stillActiveStyles intersectSet:currentActiveStyles];
-
- for (NSNumber *style in newStyles) {
- for (NSNumber *ongoingStyle in stillActiveStyles) {
- if ([ongoingStyle integerValue] > [style integerValue]) {
- // the prev style is inner; needs to be closed and re-added later
- [fixedEndedStyles addObject:ongoingStyle];
- [stylesToBeReAdded addObject:ongoingStyle];
- }
- }
- }
-
- // they are sorted in a descending order
- NSArray *sortedEndedStyles = [fixedEndedStyles
- sortedArrayUsingDescriptors:@[ [NSSortDescriptor
- sortDescriptorWithKey:@"intValue"
- ascending:NO] ]];
+ return self;
+}
- // 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]];
- }
+- (void)replaceWholeFromHtml:(NSString *)html
+ notifyAnyTextMayHaveBeenModified:(BOOL)notifyAnyTextMayHaveBeenModified {
+ ConvertHtmlToPlainTextAndStylesResult *plainTextAndStyles =
+ [_htmlHandler getTextAndStylesFromHtml:html];
- // all styles that have begun: new styles + the ones that need to be
- // re-added they are sorted in a ascending manner to properly keep tags'
- // FILO order
- [newStyles unionSet:stylesToBeReAdded];
- NSArray *sortedNewStyles = [newStyles
- sortedArrayUsingDescriptors:@[ [NSSortDescriptor
- sortDescriptorWithKey:@"intValue"
- ascending:YES] ]];
+ if (plainTextAndStyles.text == nil) {
+ _input->textView.text = @"";
+ return;
+ }
- // append opening tags
- for (NSNumber *style in sortedNewStyles) {
- NSString *tagContent = [self tagContentForStyle:style
- openingTag:YES
- location:currentRange.location];
- if ([style isEqualToNumber:@([ImageStyle getStyleType])]) {
- [result
- appendString:[NSString stringWithFormat:@"<%@/>", tagContent]];
- [currentActiveStyles removeObject:@([ImageStyle getStyleType])];
- } else {
- [result appendString:[NSString stringWithFormat:@"<%@>", tagContent]];
- }
- }
+ NSMutableAttributedString *attributedString =
+ [[NSMutableAttributedString alloc]
+ initWithString:plainTextAndStyles.text
+ attributes:_input->defaultTypingAttributes];
- // append the letter and escape it if needed
- [result appendString:[NSString stringByEscapingHtml:currentCharacterStr]];
+ [_builder apply:plainTextAndStyles.styles
+ toAttributedString:attributedString
+ offsetFromBeginning:0
+ conflictingStyles:_input->conflictingStyles];
- // save current styles for next character's checks
- previousActiveStyles = currentActiveStyles;
- }
+ NSTextStorage *storage = _input->textView.textStorage;
+ [storage setAttributedString:attributedString];
- // set last character
- lastCharacter = currentCharacterChar;
+ _input->textView.typingAttributes = _input->defaultTypingAttributes;
+ if (notifyAnyTextMayHaveBeenModified) {
+ [_input anyTextMayHaveBeenModified];
}
+}
- if (![[NSCharacterSet newlineCharacterSet] characterIsMember:lastCharacter]) {
- // not-newline character was last - finish the paragraph
- // close all pending tags
- NSArray *sortedEndedStyles = [previousActiveStyles
- sortedArrayUsingDescriptors:@[ [NSSortDescriptor
- sortDescriptorWithKey:@"intValue"
- ascending:NO] ]];
-
- // 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]];
- }
+- (void)replaceFromHtml:(NSString *)html range:(NSRange)range {
+ NSTextStorage *storage = _input->textView.textStorage;
+ ConvertHtmlToPlainTextAndStylesResult *plainTextAndStyles =
+ [_htmlHandler getTextAndStylesFromHtml:html];
- // finish the paragraph
- // handle ending of some paragraph styles
- if ([previousActiveStyles
- containsObject:@([UnorderedListStyle getStyleType])]) {
- [result appendString:@"\n
"];
- } else if ([previousActiveStyles
- containsObject:@([OrderedListStyle getStyleType])]) {
- [result appendString:@"\n"];
- } else if ([previousActiveStyles
- containsObject:@([BlockQuoteStyle getStyleType])]) {
- [result appendString:@"\n"];
- } else if ([previousActiveStyles
- containsObject:@([CodeBlockStyle getStyleType])]) {
- [result appendString:@"\n"];
- } else if ([previousActiveStyles
- containsObject:@([H1Style getStyleType])] ||
- [previousActiveStyles
- containsObject:@([H2Style getStyleType])] ||
- [previousActiveStyles
- containsObject:@([H3Style getStyleType])]) {
- // do nothing, heading closing tag has already ben appended
- } else {
- [result appendString:@""];
- }
- } else {
- // newline character was last - some paragraph styles need to be closed
- if (inUnorderedList) {
- inUnorderedList = NO;
- [result appendString:@"\n"];
- }
- if (inOrderedList) {
- inOrderedList = NO;
- [result appendString:@"\n"];
- }
- if (inBlockQuote) {
- inBlockQuote = NO;
- [result appendString:@"\n"];
- }
- if (inCodeBlock) {
- inCodeBlock = NO;
- [result appendString:@"\n"];
- }
+ if (plainTextAndStyles.text == nil) {
+ [storage replaceCharactersInRange:range withString:@""];
+ return;
}
- [result appendString:@"\n"];
+ NSMutableAttributedString *inserted = [[NSMutableAttributedString alloc]
+ initWithString:plainTextAndStyles.text
+ attributes:_input->defaultTypingAttributes];
- // remove zero width spaces in the very end
- NSRange resultRange = NSMakeRange(0, result.length);
- [result replaceOccurrencesOfString:@"\u200B"
- withString:@""
- options:0
- range:resultRange];
- return result;
-}
+ [_builder apply:plainTextAndStyles.styles
+ toAttributedString:inserted
+ offsetFromBeginning:0
+ conflictingStyles:_input->conflictingStyles];
-- (NSString *)tagContentForStyle:(NSNumber *)style
- openingTag:(BOOL)openingTag
- location:(NSInteger)location {
- if ([style isEqualToNumber:@([BoldStyle getStyleType])]) {
- 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=\"%@\" width=\"%f\" height=\"%f\"",
- data.uri, data.width, data.height];
- }
- }
- return @"img";
- } else {
- return @"";
- }
- } else if ([style isEqualToNumber:@([UnderlineStyle getStyleType])]) {
- return @"u";
- } else if ([style isEqualToNumber:@([StrikethroughStyle getStyleType])]) {
- return @"s";
- } else if ([style isEqualToNumber:@([InlineCodeStyle getStyleType])]) {
- return @"code";
- } else if ([style isEqualToNumber:@([LinkStyle getStyleType])]) {
- if (openingTag) {
- LinkStyle *linkStyle =
- (LinkStyle *)_input->stylesDict[@([LinkStyle getStyleType])];
- if (linkStyle != nullptr) {
- LinkData *data = [linkStyle getLinkDataAt:location];
- if (data != nullptr && data.url != nullptr) {
- return [NSString stringWithFormat:@"a href=\"%@\"", data.url];
- }
- }
- return @"a";
- } else {
- return @"a";
- }
- } else if ([style isEqualToNumber:@([MentionStyle getStyleType])]) {
- if (openingTag) {
- MentionStyle *mentionStyle =
- (MentionStyle *)_input->stylesDict[@([MentionStyle getStyleType])];
- if (mentionStyle != nullptr) {
- MentionParams *params = [mentionStyle getMentionParamsAt:location];
- // attributes can theoretically be nullptr
- if (params != nullptr && params.indicator != nullptr &&
- params.text != nullptr) {
- NSMutableString *attrsStr =
- [[NSMutableString alloc] initWithString:@""];
- if (params.attributes != nullptr) {
- // turn attributes to Data and then into dict
- NSData *attrsData =
- [params.attributes dataUsingEncoding:NSUTF8StringEncoding];
- NSError *jsonError;
- NSDictionary *json =
- [NSJSONSerialization JSONObjectWithData:attrsData
- options:0
- error:&jsonError];
- // format dict keys and values into string
- [json enumerateKeysAndObjectsUsingBlock:^(
- id _Nonnull key, id _Nonnull obj, BOOL *_Nonnull stop) {
- [attrsStr
- appendString:[NSString stringWithFormat:@" %@=\"%@\"",
- (NSString *)key,
- (NSString *)obj]];
- }];
- }
- return [NSString
- stringWithFormat:@"mention text=\"%@\" indicator=\"%@\"%@",
- params.text, params.indicator, attrsStr];
- }
- }
- return @"mention";
- } else {
- return @"mention";
- }
- } else if ([style isEqualToNumber:@([H1Style getStyleType])]) {
- return @"h1";
- } else if ([style isEqualToNumber:@([H2Style getStyleType])]) {
- return @"h2";
- } else if ([style isEqualToNumber:@([H3Style getStyleType])]) {
- return @"h3";
- } else if ([style isEqualToNumber:@([UnorderedListStyle getStyleType])] ||
- [style isEqualToNumber:@([OrderedListStyle getStyleType])]) {
- return @"li";
- } else if ([style isEqualToNumber:@([BlockQuoteStyle getStyleType])] ||
- [style isEqualToNumber:@([CodeBlockStyle getStyleType])]) {
- // blockquotes and codeblock use tags the same way lists use
- return @"p";
+ if (range.location > storage.length)
+ range.location = storage.length;
+ if (NSMaxRange(range) > storage.length) {
+ range.length = storage.length - range.location;
}
- return @"";
-}
-- (void)replaceWholeFromHtml:(NSString *_Nonnull)html {
- NSArray *processingResult = [self getTextAndStylesFromHtml:html];
- NSString *plainText = (NSString *)processingResult[0];
- NSArray *stylesInfo = (NSArray *)processingResult[1];
+ [storage beginEditing];
+ [storage replaceCharactersInRange:range withAttributedString:inserted];
+ [storage endEditing];
- // reset the text first and reset typing attributes
- _input->textView.text = @"";
+ _input->textView.selectedRange =
+ NSMakeRange(range.location + inserted.length, 0);
_input->textView.typingAttributes = _input->defaultTypingAttributes;
-
- // set new text
- _input->textView.text = plainText;
-
- // re-apply the styles
- [self applyProcessedStyles:stylesInfo
- offsetFromBeginning:0
- plainTextLength:plainText.length];
-}
-
-- (void)replaceFromHtml:(NSString *_Nonnull)html range:(NSRange)range {
- NSArray *processingResult = [self getTextAndStylesFromHtml:html];
- NSString *plainText = (NSString *)processingResult[0];
- NSArray *stylesInfo = (NSArray *)processingResult[1];
-
- // we can use ready replace util
- [TextInsertionUtils replaceText:plainText
- at:range
- additionalAttributes:nil
- input:_input
- withSelection:YES];
-
- [self applyProcessedStyles:stylesInfo
- offsetFromBeginning:range.location
- plainTextLength:plainText.length];
-}
-
-- (void)insertFromHtml:(NSString *_Nonnull)html location:(NSInteger)location {
- NSArray *processingResult = [self getTextAndStylesFromHtml:html];
- NSString *plainText = (NSString *)processingResult[0];
- NSArray *stylesInfo = (NSArray *)processingResult[1];
-
- // same here, insertion utils got our back
- [TextInsertionUtils insertText:plainText
- at:location
- additionalAttributes:nil
- input:_input
- withSelection:YES];
-
- [self applyProcessedStyles:stylesInfo
- offsetFromBeginning:location
- plainTextLength:plainText.length];
-}
-
-- (void)applyProcessedStyles:(NSArray *)processedStyles
- offsetFromBeginning:(NSInteger)offset
- plainTextLength:(NSUInteger)plainTextLength {
- for (NSArray *arr in processedStyles) {
- // unwrap all info from processed style
- NSNumber *styleType = (NSNumber *)arr[0];
- StylePair *stylePair = (StylePair *)arr[1];
- id baseStyle = _input->stylesDict[styleType];
- // range must be taking offest into consideration because processed styles'
- // ranges are relative to only the new text while we need absolute ranges
- // relative to the whole existing text
- NSRange styleRange =
- NSMakeRange(offset + [stylePair.rangeValue rangeValue].location,
- [stylePair.rangeValue rangeValue].length);
-
- // of course any changes here need to take blocks and conflicts into
- // consideration
- if ([_input handleStyleBlocksAndConflicts:[[baseStyle class] getStyleType]
- range:styleRange]) {
- if ([styleType isEqualToNumber:@([LinkStyle getStyleType])]) {
- NSString *text =
- [_input->textView.textStorage.string substringWithRange:styleRange];
- NSString *url = (NSString *)stylePair.styleValue;
- BOOL isManual = ![text isEqualToString:url];
- [((LinkStyle *)baseStyle) addLink:text
- url:url
- range:styleRange
- manual:isManual];
- } else if ([styleType isEqualToNumber:@([MentionStyle getStyleType])]) {
- MentionParams *params = (MentionParams *)stylePair.styleValue;
- [((MentionStyle *)baseStyle) addMentionAtRange:styleRange
- params:params];
- } else if ([styleType isEqualToNumber:@([ImageStyle getStyleType])]) {
- ImageData *imgData = (ImageData *)stylePair.styleValue;
- [((ImageStyle *)baseStyle) addImageAtRange:styleRange
- imageData:imgData
- withSelection:NO];
- } else {
- BOOL shouldAddTypingAttr =
- styleRange.location + styleRange.length == plainTextLength;
- [baseStyle addAttributes:styleRange withTypingAttr:shouldAddTypingAttr];
- }
- }
- }
[_input anyTextMayHaveBeenModified];
}
-- (NSString *_Nullable)initiallyProcessHtml:(NSString *_Nonnull)html {
- NSString *fixedHtml = nullptr;
-
- if (html.length >= 13) {
- NSString *firstSix = [html substringWithRange:NSMakeRange(0, 6)];
- NSString *lastSeven =
- [html substringWithRange:NSMakeRange(html.length - 7, 7)];
-
- if ([firstSix isEqualToString:@""] &&
- [lastSeven isEqualToString:@""]) {
- // remove html tags, might be with newlines or without them
- fixedHtml = [html 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 {
- // in other case we are most likely working with some external html - try
- // getting the styles from between body tags
- NSRange openingBodyRange = [html rangeOfString:@""];
- NSRange closingBodyRange = [html rangeOfString:@""];
-
- if (openingBodyRange.length != 0 && closingBodyRange.length != 0) {
- NSInteger newStart = openingBodyRange.location + 7;
- NSInteger newEnd = closingBodyRange.location - 1;
- fixedHtml = [html
- 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:@"
"];
+- (void)insertFromHtml:(NSString *)html location:(NSInteger)location {
- // remove tags inside of
- fixedHtml = [fixedHtml stringByReplacingOccurrencesOfString:@""
- withString:@"
"];
- fixedHtml = [fixedHtml stringByReplacingOccurrencesOfString:@""
- withString:@""];
+ ConvertHtmlToPlainTextAndStylesResult *plainTextAndStyles =
+ [_htmlHandler getTextAndStylesFromHtml:html];
- // 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];
+ NSMutableAttributedString *inserted = [[NSMutableAttributedString alloc]
+ initWithString:plainTextAndStyles.text
+ attributes:_input->defaultTypingAttributes];
- // 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];
+ [_builder apply:plainTextAndStyles.styles
+ toAttributedString:inserted
+ offsetFromBeginning:0
+ conflictingStyles:_input->conflictingStyles];
- // 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];
- }
+ [_input->textView.textStorage beginEditing];
+ [_input->textView.textStorage insertAttributedString:inserted
+ atIndex:location];
+ [_input->textView.textStorage endEditing];
- return fixedHtml;
+ _input->textView.selectedRange = NSMakeRange(location + inserted.length, 0);
}
-- (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;
+- (NSString *)initiallyProcessHtml:(NSString *)html {
+ return [_htmlHandler initiallyProcessHtml:html];
}
-- (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];
- NSRange tagRange = NSMakeRange(tagLocation, 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];
-}
-
-- (NSArray *)getTextAndStylesFromHtml:(NSString *)fixedHtml {
- NSMutableString *plainText = [[NSMutableString alloc] initWithString:@""];
- NSMutableDictionary *ongoingTags = [[NSMutableDictionary alloc] init];
- NSMutableArray *initiallyProcessedTags = [[NSMutableArray alloc] init];
- 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"] ||
- [currentTagName isEqualToString:@"li"]) {
- // do nothing, we don't include these tags in styles
- } 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;
-
- // skip one newline after opening tags that are in separate lines
- // intentionally
- if ([currentTagName isEqualToString:@"ul"] ||
- [currentTagName isEqualToString:@"ol"] ||
- [currentTagName isEqualToString:@"blockquote"] ||
- [currentTagName isEqualToString:@"codeblock"]) {
- 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
+#pragma mark - NSAttributedString → HTML
- // skip one newline that was added before some closing tags that are in
- // separate lines
- if ([currentTagName isEqualToString:@"ul"] ||
- [currentTagName isEqualToString:@"ol"] ||
- [currentTagName isEqualToString:@"blockquote"] ||
- [currentTagName isEqualToString:@"codeblock"]) {
- 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 getStyleType])];
- } 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(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 getStyleType])];
- } else if ([tagName isEqualToString:@"s"]) {
- [styleArr addObject:@([StrikethroughStyle getStyleType])];
- } else if ([tagName isEqualToString:@"code"]) {
- [styleArr addObject:@([InlineCodeStyle getStyleType])];
- } 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 getStyleType])];
- // cut only the url from the href="..." string
- NSString *url =
- [params substringWithRange:NSMakeRange(hrefRange.location + 6,
- hrefRange.length - 7)];
- stylePair.styleValue = url;
- } else if ([tagName isEqualToString:@"mention"]) {
- [styleArr addObject:@([MentionStyle getStyleType])];
- // extract html expression into dict using some regex
- NSMutableDictionary *paramsDict = [[NSMutableDictionary alloc] init];
- NSString *pattern = @"(\\w+)=\"([^\"]*)\"";
- 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 == 3) {
- NSString *key = [params
- substringWithRange:[result rangeAtIndex:1]];
- NSString *value = [params
- substringWithRange:[result rangeAtIndex:2]];
- 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 substringWithRange:NSMakeRange(0, 1)]
- isEqualToString:@"h"]) {
- if ([tagName isEqualToString:@"h1"]) {
- [styleArr addObject:@([H1Style getStyleType])];
- } else if ([tagName isEqualToString:@"h2"]) {
- [styleArr addObject:@([H2Style getStyleType])];
- } else if ([tagName isEqualToString:@"h3"]) {
- [styleArr addObject:@([H3Style getStyleType])];
- }
- } else if ([tagName isEqualToString:@"ul"]) {
- [styleArr addObject:@([UnorderedListStyle getStyleType])];
- } else if ([tagName isEqualToString:@"ol"]) {
- [styleArr addObject:@([OrderedListStyle getStyleType])];
- } else if ([tagName isEqualToString:@"blockquote"]) {
- [styleArr addObject:@([BlockQuoteStyle getStyleType])];
- } else if ([tagName isEqualToString:@"codeblock"]) {
- [styleArr addObject:@([CodeBlockStyle getStyleType])];
- } 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 ];
+- (NSString *)parseToHtmlFromRange:(NSRange)range {
+ return [_htmlBuilder htmlFromRange:range];
}
@end
diff --git a/ios/inputParser/TagHandlersFactory.h b/ios/inputParser/TagHandlersFactory.h
new file mode 100644
index 000000000..c6475e8fd
--- /dev/null
+++ b/ios/inputParser/TagHandlersFactory.h
@@ -0,0 +1,8 @@
+#import
+
+@class StylePair;
+
+typedef void (^TagHandler)(NSString *params, StylePair *pair,
+ NSMutableArray *styleArr);
+
+FOUNDATION_EXPORT NSDictionary *MakeTagHandlers(void);
diff --git a/ios/inputParser/TagHandlersFactory.mm b/ios/inputParser/TagHandlersFactory.mm
new file mode 100644
index 000000000..ab212a17f
--- /dev/null
+++ b/ios/inputParser/TagHandlersFactory.mm
@@ -0,0 +1,163 @@
+#import "TagHandlersFactory.h"
+
+#import "ImageData.h"
+#import "MentionParams.h"
+#import "StyleHeaders.h"
+#import "StylePair.h"
+
+NSDictionary *MakeTagHandlers(void) {
+ static NSDictionary *taghandlers;
+ static dispatch_once_t onceToken;
+
+ dispatch_once(&onceToken, ^{
+ taghandlers =
+ @{@"b" : ^(NSString *params, StylePair *pair, NSMutableArray *styleArr){
+ [styleArr addObject:@([BoldStyle getStyleType])];
+ },
+ @"strong": ^(NSString *params, StylePair *pair, NSMutableArray *styleArr) { // alias
+ [styleArr addObject:@([BoldStyle getStyleType])];
+ },
+ @"i": ^(NSString *params, StylePair *pair, NSMutableArray *styleArr) {
+ [styleArr addObject:@([ItalicStyle getStyleType])];
+ },
+
+ @"em": ^(NSString *params, StylePair *pair, NSMutableArray *styleArr) { // alias
+ [styleArr addObject:@([ItalicStyle getStyleType])];
+ },
+
+ @"u": ^(NSString *params, StylePair *pair, NSMutableArray *styleArr) {
+ [styleArr addObject:@([UnderlineStyle getStyleType])];
+ },
+
+ @"s": ^(NSString *params, StylePair *pair, NSMutableArray *styleArr) {
+ [styleArr addObject:@([StrikethroughStyle getStyleType])];
+ },
+
+ @"code": ^(NSString *params, StylePair *pair, NSMutableArray *styleArr) {
+ [styleArr addObject:@([InlineCodeStyle getStyleType])];
+ },
+ @"img": ^(NSString *params, StylePair *pair, NSMutableArray *styleArr) {
+ ImageData *img = [ImageData new];
+
+ NSRegularExpression *srcRegex =
+ [NSRegularExpression regularExpressionWithPattern:@"src=\"([^\"]+)\""
+ options:0
+ error:nil];
+ NSTextCheckingResult *sercMatch =
+ [srcRegex firstMatchInString:params
+ options:0
+ range:NSMakeRange(0, params.length)];
+ if (!sercMatch)
+ return;
+
+ img.uri = [params substringWithRange:[sercMatch rangeAtIndex:1]];
+
+ 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) {
+ img.width =
+ [[params substringWithRange:[widthMatch rangeAtIndex:1]] 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) {
+ img.height =
+ [[params substringWithRange:[heightMatch rangeAtIndex:1]] floatValue];
+ }
+
+ pair.styleValue = img;
+ [styleArr addObject:@([ImageStyle getStyleType])];
+ },
+ @"a": ^(NSString *params, StylePair *pair, NSMutableArray *styleArr) {
+ NSRegularExpression *hrefRegex =
+ [NSRegularExpression regularExpressionWithPattern:@"href=\"([^\"]*)\""
+ options:0
+ error:nil];
+
+ NSTextCheckingResult *match =
+ [hrefRegex firstMatchInString:params
+ options:0
+ range:NSMakeRange(0, params.length)];
+
+ if (!match)
+ return;
+ NSString *url = [params substringWithRange:[match rangeAtIndex:1]];
+
+ pair.styleValue = url;
+ [styleArr addObject:@([LinkStyle getStyleType])];
+ },
+ @"mention": ^(NSString *params, StylePair *pair, NSMutableArray *styleArr) {
+ NSMutableDictionary *dict = [NSMutableDictionary dictionary];
+
+ NSRegularExpression *re =
+ [NSRegularExpression regularExpressionWithPattern:@"(\\w+)=\"([^\"]*)\""
+ options:0
+ error:nil];
+
+ [re enumerateMatchesInString:params
+ options:0
+ range:NSMakeRange(0, params.length)
+ usingBlock:^(NSTextCheckingResult *res,
+ NSMatchingFlags flags, BOOL *stop) {
+ if (res.numberOfRanges == 3) {
+ NSString *k =
+ [params substringWithRange:[res rangeAtIndex:1]];
+ NSString *v =
+ [params substringWithRange:[res rangeAtIndex:2]];
+ dict[k] = v;
+ }
+ }];
+
+ MentionParams *mp = [MentionParams new];
+ mp.text = dict[@"text"];
+ mp.indicator = dict[@"indicator"];
+
+ [dict removeObjectsForKeys:@[ @"text", @"indicator" ]];
+ NSData *json = [NSJSONSerialization dataWithJSONObject:dict
+ options:0
+ error:nil];
+ mp.attributes = [[NSString alloc] initWithData:json
+ encoding:NSUTF8StringEncoding];
+
+ pair.styleValue = mp;
+ [styleArr addObject:@([MentionStyle getStyleType])];
+ },
+ @"h1": ^(NSString *params, StylePair *pair, NSMutableArray *styleArr) {
+ [styleArr addObject:@([H1Style getStyleType])];
+ },
+ @"h2": ^(NSString *params, StylePair *pair, NSMutableArray *styleArr) {
+ [styleArr addObject:@([H2Style getStyleType])];
+ },
+ @"h3": ^(NSString *params, StylePair *pair, NSMutableArray *styleArr) {
+ [styleArr addObject:@([H3Style getStyleType])];
+ },
+ @"ul": ^(NSString *params, StylePair *pair, NSMutableArray *styleArr) {
+ [styleArr addObject:@([UnorderedListStyle getStyleType])];
+ },
+ @"ol": ^(NSString *params, StylePair *pair, NSMutableArray *styleArr) {
+ [styleArr addObject:@([OrderedListStyle getStyleType])];
+ },
+ @"blockquote": ^(NSString *params, StylePair *pair, NSMutableArray *styleArr) {
+ [styleArr addObject:@([BlockQuoteStyle getStyleType])];
+ },
+ @"codeblock": ^(NSString *params, StylePair *pair, NSMutableArray *styleArr) {
+ [styleArr addObject:@([CodeBlockStyle getStyleType])];
+ },
+};
+});
+
+return taghandlers;
+}
diff --git a/ios/styles/BlockQuoteStyle.mm b/ios/styles/BlockQuoteStyle.mm
index f8d039cd9..aa459ad32 100644
--- a/ios/styles/BlockQuoteStyle.mm
+++ b/ios/styles/BlockQuoteStyle.mm
@@ -35,14 +35,63 @@ - (CGFloat)getHeadIndent {
- (void)applyStyle:(NSRange)range {
BOOL isStylePresent = [self detectStyle:range];
if (range.length >= 1) {
- isStylePresent ? [self removeAttributes:range]
- : [self addAttributes:range withTypingAttr:YES];
+ isStylePresent ? [self removeAttributes:range] : [self addAttributes:range];
} else {
isStylePresent ? [self removeTypingAttributes] : [self addTypingAttributes];
}
}
-- (void)addAttributes:(NSRange)range withTypingAttr:(BOOL)withTypingAttr {
+- (void)addAttributesInAttributedString:
+ (NSMutableAttributedString *)attributedString
+ range:(NSRange)range {
+ NSArray *paragraphs = [ParagraphsUtils
+ getSeparateParagraphsRangesInAttributedString:attributedString
+ range:range];
+ // if we fill empty lines with zero width spaces, we need to offset later
+ // ranges
+ NSInteger offset = 0;
+
+ for (NSValue *value in paragraphs) {
+ NSRange pRange = NSMakeRange([value rangeValue].location + offset,
+ [value rangeValue].length);
+
+ // length 0 with first line, length 1 and newline with some empty lines in
+ // the middle
+ if (pRange.length == 0 ||
+ (pRange.length == 1 &&
+ [[NSCharacterSet newlineCharacterSet]
+ characterIsMember:[attributedString.string
+ characterAtIndex:pRange.location]])) {
+ [TextInsertionUtils insertTextInAttributedString:@"\u200B"
+ at:pRange.location
+ additionalAttributes:nullptr
+ attributedString:attributedString];
+ pRange = NSMakeRange(pRange.location, pRange.length + 1);
+ offset += 1;
+ }
+
+ [attributedString
+ enumerateAttribute:NSParagraphStyleAttributeName
+ inRange:pRange
+ options:0
+ usingBlock:^(id _Nullable value, NSRange range,
+ BOOL *_Nonnull stop) {
+ NSMutableParagraphStyle *pStyle =
+ value ? [(NSParagraphStyle *)value mutableCopy]
+ : [NSMutableParagraphStyle new];
+ pStyle.headIndent = [self getHeadIndent];
+ pStyle.firstLineHeadIndent = [self getHeadIndent];
+ NSMutableDictionary *typingAttrs =
+ [_input->textView.typingAttributes mutableCopy];
+ typingAttrs[NSParagraphStyleAttributeName] = pStyle;
+ [attributedString addAttribute:NSParagraphStyleAttributeName
+ value:pStyle
+ range:range];
+ }];
+ }
+}
+
+- (void)addAttributes:(NSRange)range {
NSArray *paragraphs =
[ParagraphsUtils getSeparateParagraphsRangesIn:_input->textView
range:range];
@@ -105,46 +154,51 @@ - (void)addAttributes:(NSRange)range withTypingAttr:(BOOL)withTypingAttr {
}
// also add typing attributes
- if (withTypingAttr) {
- NSMutableDictionary *typingAttrs =
- [_input->textView.typingAttributes mutableCopy];
- NSMutableParagraphStyle *pStyle =
- [typingAttrs[NSParagraphStyleAttributeName] mutableCopy];
- pStyle.headIndent = [self getHeadIndent];
- pStyle.firstLineHeadIndent = [self getHeadIndent];
- typingAttrs[NSParagraphStyleAttributeName] = pStyle;
- _input->textView.typingAttributes = typingAttrs;
- }
+ NSMutableDictionary *typingAttrs =
+ [_input->textView.typingAttributes mutableCopy];
+ NSMutableParagraphStyle *pStyle =
+ [typingAttrs[NSParagraphStyleAttributeName] mutableCopy];
+ pStyle.headIndent = [self getHeadIndent];
+ pStyle.firstLineHeadIndent = [self getHeadIndent];
+ typingAttrs[NSParagraphStyleAttributeName] = pStyle;
+ _input->textView.typingAttributes = typingAttrs;
}
// does pretty much the same as addAttributes
- (void)addTypingAttributes {
- [self addAttributes:_input->textView.selectedRange withTypingAttr:YES];
+ [self addAttributes:_input->textView.selectedRange];
}
-- (void)removeAttributes:(NSRange)range {
+- (void)removeAttributesInAttributedString:
+ (NSMutableAttributedString *)attributedString
+ range:(NSRange)range {
NSArray *paragraphs =
- [ParagraphsUtils getSeparateParagraphsRangesIn:_input->textView
- range:range];
+ [ParagraphsUtils getNonNewlineRangesInAttributedString:attributedString
+ range:range];
for (NSValue *value in paragraphs) {
NSRange pRange = [value rangeValue];
- [_input->textView.textStorage
+ [attributedString
enumerateAttribute:NSParagraphStyleAttributeName
inRange:pRange
options:0
usingBlock:^(id _Nullable value, NSRange range,
BOOL *_Nonnull stop) {
NSMutableParagraphStyle *pStyle =
- [(NSParagraphStyle *)value mutableCopy];
+ value ? [(NSParagraphStyle *)value mutableCopy]
+ : [NSMutableParagraphStyle new];
pStyle.headIndent = 0;
pStyle.firstLineHeadIndent = 0;
- [_input->textView.textStorage
- addAttribute:NSParagraphStyleAttributeName
- value:pStyle
- range:range];
+ [attributedString addAttribute:NSParagraphStyleAttributeName
+ value:pStyle
+ range:range];
}];
}
+}
+
+- (void)removeAttributes:(NSRange)range {
+ [self removeAttributesInAttributedString:_input->textView.textStorage
+ range:range];
// also remove typing attributes
NSMutableDictionary *typingAttrs =
@@ -193,14 +247,21 @@ - (BOOL)styleCondition:(id _Nullable)value:(NSRange)range {
pStyle.textLists.count == 0;
}
+- (BOOL)detectStyleInAttributedString:
+ (NSMutableAttributedString *)attributedString
+ range:(NSRange)range {
+ return [OccurenceUtils detect:NSParagraphStyleAttributeName
+ inString:attributedString
+ 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:NSParagraphStyleAttributeName
- withInput:_input
- inRange:range
- withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
- }];
+ return [self detectStyleInAttributedString:_input->textView.textStorage
+ range:range];
} else {
return [OccurenceUtils detect:NSParagraphStyleAttributeName
withInput:_input
@@ -230,6 +291,17 @@ - (BOOL)anyOccurence:(NSRange)range {
}];
}
+- (NSArray *_Nullable)
+ findAllOccurencesInAttributedString:(NSAttributedString *)attributedString
+ range:(NSRange)range {
+ return [OccurenceUtils all:NSParagraphStyleAttributeName
+ inString:attributedString
+ inRange:range
+ withCondition:^BOOL(id _Nullable value, NSRange range) {
+ return [self styleCondition:value:range];
+ }];
+}
+
// general checkup correcting blockquote color
// since links, mentions and inline code affects coloring, the checkup gets done
// only outside of them
diff --git a/ios/styles/BoldStyle.mm b/ios/styles/BoldStyle.mm
index aae3b400b..7d70e3e54 100644
--- a/ios/styles/BoldStyle.mm
+++ b/ios/styles/BoldStyle.mm
@@ -24,29 +24,32 @@ - (instancetype)initWithInput:(id)input {
- (void)applyStyle:(NSRange)range {
BOOL isStylePresent = [self detectStyle:range];
if (range.length >= 1) {
- isStylePresent ? [self removeAttributes:range]
- : [self addAttributes:range withTypingAttr:YES];
+ isStylePresent ? [self removeAttributes:range] : [self addAttributes:range];
} else {
isStylePresent ? [self removeTypingAttributes] : [self addTypingAttributes];
}
}
-- (void)addAttributes:(NSRange)range withTypingAttr:(BOOL)withTypingAttr {
+- (void)addAttributesInAttributedString:(NSMutableAttributedString *)attr
+ range:(NSRange)range {
+ [attr enumerateAttribute:NSFontAttributeName
+ inRange:range
+ options:0
+ usingBlock:^(id value, NSRange subrange, BOOL *stop) {
+ UIFont *font = (UIFont *)value;
+ if (font != nil) {
+ UIFont *newFont = [font setBold];
+ [attr addAttribute:NSFontAttributeName
+ value:newFont
+ range:subrange];
+ }
+ }];
+}
+
+- (void)addAttributes:(NSRange)range {
[_input->textView.textStorage beginEditing];
- [_input->textView.textStorage
- enumerateAttribute:NSFontAttributeName
- inRange:range
- options:0
- usingBlock:^(id _Nullable value, NSRange range,
- BOOL *_Nonnull stop) {
- UIFont *font = (UIFont *)value;
- if (font != nullptr) {
- UIFont *newFont = [font setBold];
- [_input->textView.textStorage addAttribute:NSFontAttributeName
- value:newFont
- range:range];
- }
- }];
+ [self addAttributesInAttributedString:_input->textView.textStorage
+ range:range];
[_input->textView.textStorage endEditing];
}
@@ -61,22 +64,28 @@ - (void)addTypingAttributes {
}
}
-- (void)removeAttributes:(NSRange)range {
- [_input->textView.textStorage beginEditing];
- [_input->textView.textStorage
+- (void)removeAttributesInAttributedString:
+ (NSMutableAttributedString *)attributedString
+ range:(NSRange)range {
+ [attributedString
enumerateAttribute:NSFontAttributeName
inRange:range
options:0
- usingBlock:^(id _Nullable value, NSRange range,
- BOOL *_Nonnull stop) {
+ usingBlock:^(id value, NSRange subrange, BOOL *stop) {
UIFont *font = (UIFont *)value;
- if (font != nullptr) {
+ if (font != nil) {
UIFont *newFont = [font removeBold];
- [_input->textView.textStorage addAttribute:NSFontAttributeName
- value:newFont
- range:range];
+ [attributedString addAttribute:NSFontAttributeName
+ value:newFont
+ range:subrange];
}
}];
+}
+
+- (void)removeAttributes:(NSRange)range {
+ [_input->textView.textStorage beginEditing];
+ [self removeAttributesInAttributedString:_input->textView.textStorage
+ range:range];
[_input->textView.textStorage endEditing];
}
@@ -119,14 +128,21 @@ - (BOOL)styleCondition:(id _Nullable)value:(NSRange)range {
![self boldHeadingConflictsInRange:range type:H3];
}
+- (BOOL)detectStyleInAttributedString:
+ (NSMutableAttributedString *)attributedString
+ range:(NSRange)range {
+ return [OccurenceUtils detect:NSFontAttributeName
+ inString:attributedString
+ 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:NSFontAttributeName
- withInput:_input
- inRange:range
- withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
- }];
+ return [self detectStyleInAttributedString:_input->textView.textStorage
+ range:range];
} else {
return [OccurenceUtils detect:NSFontAttributeName
withInput:_input
@@ -156,4 +172,15 @@ - (BOOL)anyOccurence:(NSRange)range {
}];
}
+- (NSArray *_Nullable)
+ findAllOccurencesInAttributedString:(NSAttributedString *)attributedString
+ range:(NSRange)range {
+ return [OccurenceUtils all:NSFontAttributeName
+ inString:attributedString
+ inRange:range
+ withCondition:^BOOL(id _Nullable value, NSRange range) {
+ return [self styleCondition:value:range];
+ }];
+}
+
@end
diff --git a/ios/styles/CodeBlockStyle.mm b/ios/styles/CodeBlockStyle.mm
index 1f5aa9976..65c5ab9cc 100644
--- a/ios/styles/CodeBlockStyle.mm
+++ b/ios/styles/CodeBlockStyle.mm
@@ -29,14 +29,64 @@ - (instancetype)initWithInput:(id)input {
- (void)applyStyle:(NSRange)range {
BOOL isStylePresent = [self detectStyle:range];
if (range.length >= 1) {
- isStylePresent ? [self removeAttributes:range]
- : [self addAttributes:range withTypingAttr:YES];
+ isStylePresent ? [self removeAttributes:range] : [self addAttributes:range];
} else {
isStylePresent ? [self removeTypingAttributes] : [self addTypingAttributes];
}
}
-- (void)addAttributes:(NSRange)range withTypingAttr:(BOOL)withTypingAttr {
+- (void)addAttributesInAttributedString:
+ (NSMutableAttributedString *)attributedString
+ range:(NSRange)range {
+ NSTextList *codeBlockList =
+ [[NSTextList alloc] initWithMarkerFormat:@"codeblock" options:0];
+ NSArray *paragraphs = [ParagraphsUtils
+ getSeparateParagraphsRangesInAttributedString:attributedString
+ range:range];
+ // if we fill empty lines with zero width spaces, we need to offset later
+ // ranges
+ NSInteger offset = 0;
+
+ for (NSValue *value in paragraphs) {
+ NSRange pRange = NSMakeRange([value rangeValue].location + offset,
+ [value rangeValue].length);
+
+ // length 0 with first line, length 1 and newline with some empty lines in
+ // the middle
+ if (pRange.length == 0 ||
+ (pRange.length == 1 &&
+ [[NSCharacterSet newlineCharacterSet]
+ characterIsMember:[attributedString.string
+ characterAtIndex:pRange.location]])) {
+ [TextInsertionUtils insertTextInAttributedString:@"\u200B"
+ at:pRange.location
+ additionalAttributes:nullptr
+ attributedString:attributedString];
+ pRange = NSMakeRange(pRange.location, pRange.length + 1);
+ offset += 1;
+ }
+
+ [attributedString
+ enumerateAttribute:NSParagraphStyleAttributeName
+ inRange:pRange
+ options:0
+ usingBlock:^(id _Nullable value, NSRange range,
+ BOOL *_Nonnull stop) {
+ NSMutableParagraphStyle *pStyle =
+ value ? [(NSParagraphStyle *)value mutableCopy]
+ : [NSMutableParagraphStyle new];
+ NSMutableDictionary *typingAttrs =
+ [_input->textView.typingAttributes mutableCopy];
+ pStyle.textLists = @[ codeBlockList ];
+ typingAttrs[NSParagraphStyleAttributeName] = pStyle;
+ [attributedString addAttribute:NSParagraphStyleAttributeName
+ value:pStyle
+ range:range];
+ }];
+ }
+}
+
+- (void)addAttributes:(NSRange)range {
NSTextList *codeBlockList =
[[NSTextList alloc] initWithMarkerFormat:@"codeblock" options:0];
NSArray *paragraphs =
@@ -99,48 +149,51 @@ - (void)addAttributes:(NSRange)range withTypingAttr:(BOOL)withTypingAttr {
}
// also add typing attributes
- if (withTypingAttr) {
- NSMutableDictionary *typingAttrs =
- [_input->textView.typingAttributes mutableCopy];
- NSMutableParagraphStyle *pStyle =
- [typingAttrs[NSParagraphStyleAttributeName] mutableCopy];
- pStyle.textLists = @[ codeBlockList ];
- typingAttrs[NSParagraphStyleAttributeName] = pStyle;
-
- _input->textView.typingAttributes = typingAttrs;
- }
+ NSMutableDictionary *typingAttrs =
+ [_input->textView.typingAttributes mutableCopy];
+ NSMutableParagraphStyle *pStyle =
+ [typingAttrs[NSParagraphStyleAttributeName] mutableCopy];
+ pStyle.textLists = @[ codeBlockList ];
+ typingAttrs[NSParagraphStyleAttributeName] = pStyle;
+
+ _input->textView.typingAttributes = typingAttrs;
}
- (void)addTypingAttributes {
- [self addAttributes:_input->textView.selectedRange withTypingAttr:YES];
+ [self addAttributes:_input->textView.selectedRange];
}
-- (void)removeAttributes:(NSRange)range {
+- (void)removeAttributesInAttributedString:
+ (NSMutableAttributedString *)attributedString
+ range:(NSRange)range {
NSArray *paragraphs =
- [ParagraphsUtils getSeparateParagraphsRangesIn:_input->textView
- range:range];
-
- [_input->textView.textStorage beginEditing];
+ [ParagraphsUtils getNonNewlineRangesInAttributedString:attributedString
+ range:range];
for (NSValue *value in paragraphs) {
NSRange pRange = [value rangeValue];
- [_input->textView.textStorage
- enumerateAttribute:NSParagraphStyleAttributeName
- inRange:pRange
- options:0
- usingBlock:^(id _Nullable value, NSRange range,
- BOOL *_Nonnull stop) {
- NSMutableParagraphStyle *pStyle =
- [(NSParagraphStyle *)value mutableCopy];
- pStyle.textLists = @[];
- [_input->textView.textStorage
- addAttribute:NSParagraphStyleAttributeName
- value:pStyle
- range:range];
- }];
+ [attributedString enumerateAttribute:NSParagraphStyleAttributeName
+ inRange:pRange
+ options:0
+ usingBlock:^(id _Nullable value, NSRange range,
+ BOOL *_Nonnull stop) {
+ NSMutableParagraphStyle *pStyle =
+ [(NSParagraphStyle *)value mutableCopy];
+
+ pStyle.textLists = @[];
+ [attributedString
+ addAttribute:NSParagraphStyleAttributeName
+ value:pStyle
+ range:range];
+ }];
}
+}
+- (void)removeAttributes:(NSRange)range {
+ [_input->textView.textStorage beginEditing];
+ [self removeAttributesInAttributedString:_input->textView.textStorage
+ range:range];
[_input->textView.textStorage endEditing];
// also remove typing attributes
@@ -184,14 +237,21 @@ - (BOOL)styleCondition:(id _Nullable)value:(NSRange)range {
isEqualToString:@"codeblock"];
}
+- (BOOL)detectStyleInAttributedString:
+ (NSMutableAttributedString *)attributedString
+ range:(NSRange)range {
+ return [OccurenceUtils detect:NSParagraphStyleAttributeName
+ inString:attributedString
+ 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:NSParagraphStyleAttributeName
- withInput:_input
- inRange:range
- withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
- }];
+ return [self detectStyleInAttributedString:_input->textView.textStorage
+ range:range];
} else {
return [OccurenceUtils detect:NSParagraphStyleAttributeName
withInput:_input
@@ -221,6 +281,17 @@ - (BOOL)anyOccurence:(NSRange)range {
}];
}
+- (NSArray *_Nullable)
+ findAllOccurencesInAttributedString:(NSAttributedString *)attributedString
+ range:(NSRange)range {
+ return [OccurenceUtils all:NSParagraphStyleAttributeName
+ inString:attributedString
+ inRange:range
+ withCondition:^BOOL(id _Nullable value, NSRange range) {
+ return [self styleCondition:value:range];
+ }];
+}
+
- (void)manageCodeBlockFontAndColor {
if ([[_input->config codeBlockFgColor]
isEqualToColor:[_input->config primaryColor]]) {
diff --git a/ios/styles/HeadingStyleBase.mm b/ios/styles/HeadingStyleBase.mm
index 6e86672f3..f1c6a524c 100644
--- a/ios/styles/HeadingStyleBase.mm
+++ b/ios/styles/HeadingStyleBase.mm
@@ -32,17 +32,27 @@ - (instancetype)initWithInput:(id)input {
- (void)applyStyle:(NSRange)range {
BOOL isStylePresent = [self detectStyle:range];
if (range.length >= 1) {
- isStylePresent ? [self removeAttributes:range]
- : [self addAttributes:range withTypingAttr:YES];
+ isStylePresent ? [self removeAttributes:range] : [self addAttributes:range];
} else {
isStylePresent ? [self removeTypingAttributes] : [self addTypingAttributes];
}
}
// the range will already be the proper full paragraph/s range
-- (void)addAttributes:(NSRange)range withTypingAttr:(BOOL)withTypingAttr {
+- (void)addAttributes:(NSRange)range {
[[self typedInput]->textView.textStorage beginEditing];
- [[self typedInput]->textView.textStorage
+ [self addAttributesInAttributedString:[self typedInput]->textView.textStorage
+ range:range];
+ [[self typedInput]->textView.textStorage endEditing];
+
+ // also toggle typing attributes
+ [self addTypingAttributes];
+}
+
+- (void)addAttributesInAttributedString:
+ (NSMutableAttributedString *)attributedString
+ range:(NSRange)range {
+ [attributedString
enumerateAttribute:NSFontAttributeName
inRange:range
options:0
@@ -54,18 +64,11 @@ - (void)addAttributes:(NSRange)range withTypingAttr:(BOOL)withTypingAttr {
if ([self isHeadingBold]) {
newFont = [newFont setBold];
}
- [[self typedInput]->textView.textStorage
- addAttribute:NSFontAttributeName
- value:newFont
- range:range];
+ [attributedString addAttribute:NSFontAttributeName
+ value:newFont
+ range:range];
}
}];
- [[self typedInput]->textView.textStorage endEditing];
-
- // also toggle typing attributes
- if (withTypingAttr) {
- [self addTypingAttributes];
- }
}
// will always be called on empty paragraphs so only typing attributes can be
@@ -86,6 +89,31 @@ - (void)addTypingAttributes {
}
}
+- (void)removeAttributesInAttributedString:
+ (NSMutableAttributedString *)attributedString
+ range:(NSRange)range {
+ NSRange paragraphRange =
+ [attributedString.string paragraphRangeForRange:range];
+ [attributedString
+ enumerateAttribute:NSFontAttributeName
+ inRange:paragraphRange
+ options:0
+ usingBlock:^(id _Nullable value, NSRange range,
+ BOOL *_Nonnull stop) {
+ if ([self styleCondition:value:range]) {
+ UIFont *newFont = [(UIFont *)value
+ setSize:[[[self typedInput]->config primaryFontSize]
+ floatValue]];
+ if ([self isHeadingBold]) {
+ newFont = [newFont removeBold];
+ }
+ [attributedString addAttribute:NSFontAttributeName
+ value:newFont
+ range:range];
+ }
+ }];
+}
+
// we need to remove the style from the whole paragraph
- (void)removeAttributes:(NSRange)range {
NSRange paragraphRange = [[self typedInput]->textView.textStorage.string
@@ -142,14 +170,22 @@ - (BOOL)styleCondition:(id _Nullable)value:(NSRange)range {
return font != nullptr && font.pointSize == [self getHeadingFontSize];
}
+- (BOOL)detectStyleInAttributedString:
+ (NSMutableAttributedString *)attributedString
+ range:(NSRange)range {
+ return [OccurenceUtils detect:NSFontAttributeName
+ inString:attributedString
+ 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:NSFontAttributeName
- withInput:[self typedInput]
- inRange:range
- withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
- }];
+ return [self
+ detectStyleInAttributedString:[self typedInput]->textView.textStorage
+ range:range];
} else {
return [OccurenceUtils detect:NSFontAttributeName
withInput:[self typedInput]
@@ -179,6 +215,17 @@ - (BOOL)anyOccurence:(NSRange)range {
}];
}
+- (NSArray *_Nullable)
+ findAllOccurencesInAttributedString:(NSAttributedString *)attributedString
+ range:(NSRange)range {
+ return [OccurenceUtils all:NSFontAttributeName
+ inString:attributedString
+ inRange:range
+ withCondition:^BOOL(id _Nullable value, NSRange range) {
+ return [self styleCondition:value:range];
+ }];
+}
+
// 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
@@ -213,7 +260,7 @@ - (void)handleImproperHeadings {
if (!NSEqualRanges(occurenceRange, paragraphRange)) {
// we have a heading but it does not span its whole paragraph - let's fix
// it
- [self addAttributes:paragraphRange withTypingAttr:NO];
+ [self addAttributes:paragraphRange];
}
}
}
diff --git a/ios/styles/ImageAttachment.h b/ios/styles/ImageAttachment.h
new file mode 100644
index 000000000..98681f490
--- /dev/null
+++ b/ios/styles/ImageAttachment.h
@@ -0,0 +1,10 @@
+#import "ImageData.h"
+#import "MediaAttachment.h"
+
+@interface ImageAttachment : MediaAttachment
+
+@property(nonatomic, strong) ImageData *imageData;
+
+- (instancetype)initWithImageData:(ImageData *)data;
+
+@end
diff --git a/ios/styles/ImageAttachment.mm b/ios/styles/ImageAttachment.mm
new file mode 100644
index 000000000..de6974e6b
--- /dev/null
+++ b/ios/styles/ImageAttachment.mm
@@ -0,0 +1,34 @@
+#import "ImageAttachment.h"
+
+@implementation ImageAttachment
+
+- (instancetype)initWithImageData:(ImageData *)data {
+ self = [super initWithURI:data.uri width:data.width height:data.height];
+ if (!self)
+ return nil;
+
+ _imageData = data;
+
+ [self loadAsync];
+ return self;
+}
+
+- (void)loadAsync {
+ NSURL *url = [NSURL URLWithString:self.uri];
+ if (!url)
+ return;
+
+ dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
+ NSData *bytes = [NSData dataWithContentsOfURL:url];
+ UIImage *img = bytes ? [UIImage imageWithData:bytes] : nil;
+ if (!img)
+ return;
+
+ dispatch_async(dispatch_get_main_queue(), ^{
+ self.image = img;
+ [self notifyUpdate];
+ });
+ });
+}
+
+@end
diff --git a/ios/styles/ImageStyle.mm b/ios/styles/ImageStyle.mm
index 2470d826d..287788a1d 100644
--- a/ios/styles/ImageStyle.mm
+++ b/ios/styles/ImageStyle.mm
@@ -1,4 +1,5 @@
#import "EnrichedTextInputView.h"
+#import "ImageAttachment.h"
#import "OccurenceUtils.h"
#import "StyleHeaders.h"
#import "TextInsertionUtils.h"
@@ -28,7 +29,13 @@ - (void)applyStyle:(NSRange)range {
// no-op for image
}
-- (void)addAttributes:(NSRange)range withTypingAttr:(BOOL)withTypingAttr {
+- (void)addAttributes:(NSRange)range {
+ // no-op for image
+}
+
+- (void)addAttributesInAttributedString:
+ (NSMutableAttributedString *)attributedString
+ range:(NSRange)range {
// no-op for image
}
@@ -36,11 +43,17 @@ - (void)addTypingAttributes {
// no-op for image
}
+- (void)removeAttributesInAttributedString:
+ (NSMutableAttributedString *)attributedString
+ range:(NSRange)range {
+ [attributedString removeAttribute:ImageAttributeName range:range];
+ [attributedString removeAttribute:NSAttachmentAttributeName range:range];
+}
+
- (void)removeAttributes:(NSRange)range {
[_input->textView.textStorage beginEditing];
- [_input->textView.textStorage removeAttribute:ImageAttributeName range:range];
- [_input->textView.textStorage removeAttribute:NSAttachmentAttributeName
- range:range];
+ [self removeAttributesInAttributedString:_input->textView.textStorage
+ range:range];
[_input->textView.textStorage endEditing];
}
@@ -65,14 +78,21 @@ - (BOOL)anyOccurence:(NSRange)range {
}];
}
+- (BOOL)detectStyleInAttributedString:
+ (NSMutableAttributedString *)attributedString
+ range:(NSRange)range {
+ return [OccurenceUtils detect:ImageAttributeName
+ inString:attributedString
+ 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];
- }];
+ return [self detectStyleInAttributedString:_input->textView.textStorage
+ range:range];
} else {
return [OccurenceUtils detect:ImageAttributeName
withInput:_input
@@ -93,6 +113,17 @@ - (BOOL)detectStyle:(NSRange)range {
}];
}
+- (NSArray *_Nullable)
+ findAllOccurencesInAttributedString:(NSAttributedString *)attributedString
+ range:(NSRange)range {
+ return [OccurenceUtils all:ImageAttributeName
+ inString:attributedString
+ inRange:range
+ withCondition:^BOOL(id _Nullable value, NSRange range) {
+ return [self styleCondition:value:range];
+ }];
+}
+
- (ImageData *)getImageDataAt:(NSUInteger)location {
NSRange imageRange = NSMakeRange(0, 0);
NSRange inputRange = NSMakeRange(0, _input->textView.textStorage.length);
@@ -115,29 +146,28 @@ - (ImageData *)getImageDataAt:(NSUInteger)location {
- (void)addImageAtRange:(NSRange)range
imageData:(ImageData *)imageData
withSelection:(BOOL)withSelection {
- UIImage *img = [self prepareImageFromUri:imageData.uri];
+ if (!imageData)
+ return;
+
+ ImageAttachment *attachment =
+ [[ImageAttachment alloc] initWithImageData:imageData];
+ attachment.delegate = _input;
- NSDictionary *attributes = [@{
- NSAttachmentAttributeName : [self prepareImageAttachement:img
- width:imageData.width
- height:imageData.height],
- ImageAttributeName : imageData,
- } mutableCopy];
+ NSDictionary *attrs =
+ @{NSAttachmentAttributeName : attachment, ImageAttributeName : imageData};
- // Use the Object Replacement Character for Image.
- // This tells TextKit "something non-text goes here".
- NSString *imagePlaceholder = @"\uFFFC";
+ NSString *placeholderChar = @"\uFFFC";
if (range.length == 0) {
- [TextInsertionUtils insertText:imagePlaceholder
+ [TextInsertionUtils insertText:placeholderChar
at:range.location
- additionalAttributes:attributes
+ additionalAttributes:attrs
input:_input
withSelection:withSelection];
} else {
- [TextInsertionUtils replaceText:imagePlaceholder
+ [TextInsertionUtils replaceText:placeholderChar
at:range
- additionalAttributes:attributes
+ additionalAttributes:attrs
input:_input
withSelection:withSelection];
}
@@ -154,23 +184,28 @@ - (void)addImage:(NSString *)uri width:(CGFloat)width height:(CGFloat)height {
withSelection:YES];
}
-- (NSTextAttachment *)prepareImageAttachement:(UIImage *)image
- width:(CGFloat)width
- height:(CGFloat)height {
+- (void)addImageInAttributedString:(NSMutableAttributedString *)string
+ range:(NSRange)range
+ imageData:(ImageData *)imageData {
+ if (!imageData)
+ return;
- NSTextAttachment *attachment = [[NSTextAttachment alloc] init];
- attachment.image = image;
- attachment.bounds = CGRectMake(0, 0, width, height);
+ ImageAttachment *attachment =
+ [[ImageAttachment alloc] initWithImageData:imageData];
+ attachment.delegate = _input;
- return attachment;
-}
+ NSMutableDictionary *attrs = [_input->defaultTypingAttributes mutableCopy];
+ attrs[NSAttachmentAttributeName] = attachment;
+ attrs[ImageAttributeName] = imageData;
-- (UIImage *)prepareImageFromUri:(NSString *)uri {
- NSURL *url = [NSURL URLWithString:uri];
- NSData *imgData = [NSData dataWithContentsOfURL:url];
- UIImage *image = [UIImage imageWithData:imgData];
+ NSAttributedString *imgString =
+ [[NSAttributedString alloc] initWithString:@"\uFFFC" attributes:attrs];
- return image;
+ if (range.length == 0) {
+ [string insertAttributedString:imgString atIndex:range.location];
+ } else {
+ [string replaceCharactersInRange:range withAttributedString:imgString];
+ }
}
@end
diff --git a/ios/styles/InlineCodeStyle.mm b/ios/styles/InlineCodeStyle.mm
index 3bc7d0ab8..df980d1ed 100644
--- a/ios/styles/InlineCodeStyle.mm
+++ b/ios/styles/InlineCodeStyle.mm
@@ -26,14 +26,46 @@ - (instancetype)initWithInput:(id)input {
- (void)applyStyle:(NSRange)range {
BOOL isStylePresent = [self detectStyle:range];
if (range.length >= 1) {
- isStylePresent ? [self removeAttributes:range]
- : [self addAttributes:range withTypingAttr:YES];
+ isStylePresent ? [self removeAttributes:range] : [self addAttributes:range];
} else {
isStylePresent ? [self removeTypingAttributes] : [self addTypingAttributes];
}
}
-- (void)addAttributes:(NSRange)range withTypingAttr:(BOOL)withTypingAttr {
+- (void)addAttributesInAttributedString:
+ (NSMutableAttributedString *)attributedString
+ range:(NSRange)currentRange {
+ [attributedString addAttribute:NSBackgroundColorAttributeName
+ value:[[_input->config inlineCodeBgColor]
+ colorWithAlphaIfNotTransparent:0.4]
+ range:currentRange];
+ [attributedString addAttribute:NSForegroundColorAttributeName
+ value:[_input->config inlineCodeFgColor]
+ range:currentRange];
+ [attributedString addAttribute:NSUnderlineColorAttributeName
+ value:[_input->config inlineCodeFgColor]
+ range:currentRange];
+ [attributedString addAttribute:NSStrikethroughColorAttributeName
+ value:[_input->config inlineCodeFgColor]
+ range:currentRange];
+ [attributedString
+ enumerateAttribute:NSFontAttributeName
+ inRange:currentRange
+ options:0
+ usingBlock:^(id _Nullable value, NSRange range,
+ BOOL *_Nonnull stop) {
+ UIFont *font = (UIFont *)value;
+ if (font != nullptr) {
+ UIFont *newFont = [[[_input->config monospacedFont]
+ withFontTraits:font] setSize:font.pointSize];
+ [attributedString addAttribute:NSFontAttributeName
+ value:newFont
+ range:range];
+ }
+ }];
+}
+
+- (void)addAttributes:(NSRange)range {
// we don't want to apply inline code to newline characters, it looks bad
NSArray *nonNewlineRanges =
[ParagraphsUtils getNonNewlineRangesIn:_input->textView range:range];
@@ -41,41 +73,8 @@ - (void)addAttributes:(NSRange)range withTypingAttr:(BOOL)withTypingAttr {
for (NSValue *value in nonNewlineRanges) {
NSRange currentRange = [value rangeValue];
[_input->textView.textStorage beginEditing];
-
- [_input->textView.textStorage
- addAttribute:NSBackgroundColorAttributeName
- value:[[_input->config inlineCodeBgColor]
- colorWithAlphaIfNotTransparent:0.4]
- range:currentRange];
- [_input->textView.textStorage
- addAttribute:NSForegroundColorAttributeName
- value:[_input->config inlineCodeFgColor]
- range:currentRange];
- [_input->textView.textStorage
- addAttribute:NSUnderlineColorAttributeName
- value:[_input->config inlineCodeFgColor]
- range:currentRange];
- [_input->textView.textStorage
- addAttribute:NSStrikethroughColorAttributeName
- value:[_input->config inlineCodeFgColor]
- range:currentRange];
- [_input->textView.textStorage
- enumerateAttribute:NSFontAttributeName
- inRange:currentRange
- options:0
- usingBlock:^(id _Nullable value, NSRange range,
- BOOL *_Nonnull stop) {
- UIFont *font = (UIFont *)value;
- if (font != nullptr) {
- UIFont *newFont = [[[_input->config monospacedFont]
- withFontTraits:font] setSize:font.pointSize];
- [_input->textView.textStorage
- addAttribute:NSFontAttributeName
- value:newFont
- range:range];
- }
- }];
-
+ [self addAttributesInAttributedString:_input->textView.textStorage
+ range:currentRange];
[_input->textView.textStorage endEditing];
}
}
@@ -99,21 +98,20 @@ - (void)addTypingAttributes {
_input->textView.typingAttributes = newTypingAttrs;
}
-- (void)removeAttributes:(NSRange)range {
- [_input->textView.textStorage beginEditing];
-
- [_input->textView.textStorage removeAttribute:NSBackgroundColorAttributeName
- range:range];
- [_input->textView.textStorage addAttribute:NSForegroundColorAttributeName
- value:[_input->config primaryColor]
- range:range];
- [_input->textView.textStorage addAttribute:NSUnderlineColorAttributeName
- value:[_input->config primaryColor]
- range:range];
- [_input->textView.textStorage addAttribute:NSStrikethroughColorAttributeName
- value:[_input->config primaryColor]
- range:range];
- [_input->textView.textStorage
+- (void)removeAttributesInAttributedString:
+ (NSMutableAttributedString *)attributedString
+ range:(NSRange)range {
+ [attributedString removeAttribute:NSBackgroundColorAttributeName range:range];
+ [attributedString addAttribute:NSForegroundColorAttributeName
+ value:[_input->config primaryColor]
+ range:range];
+ [attributedString addAttribute:NSUnderlineColorAttributeName
+ value:[_input->config primaryColor]
+ range:range];
+ [attributedString addAttribute:NSStrikethroughColorAttributeName
+ value:[_input->config primaryColor]
+ range:range];
+ [attributedString
enumerateAttribute:NSFontAttributeName
inRange:range
options:0
@@ -123,12 +121,17 @@ - (void)removeAttributes:(NSRange)range {
if (font != nullptr) {
UIFont *newFont = [[[_input->config primaryFont]
withFontTraits:font] setSize:font.pointSize];
- [_input->textView.textStorage addAttribute:NSFontAttributeName
- value:newFont
- range:range];
+ [attributedString addAttribute:NSFontAttributeName
+ value:newFont
+ range:range];
}
}];
+}
+- (void)removeAttributes:(NSRange)range {
+ [_input->textView.textStorage beginEditing];
+ [self removeAttributesInAttributedString:_input->textView.textStorage
+ range:range];
[_input->textView.textStorage endEditing];
}
@@ -177,29 +180,36 @@ - (BOOL)styleCondition:(id _Nullable)value:(NSRange)range {
return bgColor != nullptr && mStyle != nullptr && ![mStyle detectStyle:range];
}
-- (BOOL)detectStyle:(NSRange)range {
- if (range.length >= 1) {
- // detect only in non-newline characters
- NSArray *nonNewlineRanges =
- [ParagraphsUtils getNonNewlineRangesIn:_input->textView range:range];
- if (nonNewlineRanges.count == 0) {
- return NO;
- }
+- (BOOL)detectStyleInAttributedString:
+ (NSMutableAttributedString *)attributedString
+ range:(NSRange)range {
+ // detect only in non-newline characters
+ NSArray *nonNewlineRanges =
+ [ParagraphsUtils getNonNewlineRangesIn:_input->textView range:range];
+ if (nonNewlineRanges.count == 0) {
+ return NO;
+ }
- BOOL detected = YES;
- for (NSValue *value in nonNewlineRanges) {
- NSRange currentRange = [value rangeValue];
- BOOL currentDetected =
- [OccurenceUtils detect:NSBackgroundColorAttributeName
- withInput:_input
- inRange:currentRange
- withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
- }];
- detected = detected && currentDetected;
- }
+ BOOL detected = YES;
+ for (NSValue *value in nonNewlineRanges) {
+ NSRange currentRange = [value rangeValue];
+ BOOL currentDetected =
+ [OccurenceUtils detect:NSBackgroundColorAttributeName
+ inString:attributedString
+ inRange:currentRange
+ withCondition:^BOOL(id _Nullable value, NSRange range) {
+ return [self styleCondition:value:range];
+ }];
+ detected = detected && currentDetected;
+ }
+
+ return detected;
+}
- return detected;
+- (BOOL)detectStyle:(NSRange)range {
+ if (range.length >= 1) {
+ return [self detectStyleInAttributedString:_input->textView.textStorage
+ range:range];
} else {
return [OccurenceUtils detect:NSBackgroundColorAttributeName
withInput:_input
@@ -229,4 +239,15 @@ - (BOOL)anyOccurence:(NSRange)range {
}];
}
+- (NSArray *_Nullable)
+ findAllOccurencesInAttributedString:(NSAttributedString *)attributedString
+ range:(NSRange)range {
+ return [OccurenceUtils all:NSBackgroundColorAttributeName
+ inString:attributedString
+ inRange:range
+ withCondition:^BOOL(id _Nullable value, NSRange range) {
+ return [self styleCondition:value:range];
+ }];
+}
+
@end
diff --git a/ios/styles/ItalicStyle.mm b/ios/styles/ItalicStyle.mm
index 82ddf7072..feeb6fcb8 100644
--- a/ios/styles/ItalicStyle.mm
+++ b/ios/styles/ItalicStyle.mm
@@ -24,29 +24,35 @@ - (instancetype)initWithInput:(id)input {
- (void)applyStyle:(NSRange)range {
BOOL isStylePresent = [self detectStyle:range];
if (range.length >= 1) {
- isStylePresent ? [self removeAttributes:range]
- : [self addAttributes:range withTypingAttr:YES];
+ isStylePresent ? [self removeAttributes:range] : [self addAttributes:range];
} else {
isStylePresent ? [self removeTypingAttributes] : [self addTypingAttributes];
}
}
-- (void)addAttributes:(NSRange)range withTypingAttr:(BOOL)withTypingAttr {
+- (void)addAttributesInAttributedString:
+ (NSMutableAttributedString *)attributedString
+ range:(NSRange)range {
+ [attributedString enumerateAttribute:NSFontAttributeName
+ inRange:range
+ options:0
+ usingBlock:^(id _Nullable value, NSRange range,
+ BOOL *_Nonnull stop) {
+ UIFont *font = (UIFont *)value;
+ if (font != nullptr) {
+ UIFont *newFont = [font setItalic];
+ [attributedString
+ addAttribute:NSFontAttributeName
+ value:newFont
+ range:range];
+ }
+ }];
+}
+
+- (void)addAttributes:(NSRange)range {
[_input->textView.textStorage beginEditing];
- [_input->textView.textStorage
- enumerateAttribute:NSFontAttributeName
- inRange:range
- options:0
- usingBlock:^(id _Nullable value, NSRange range,
- BOOL *_Nonnull stop) {
- UIFont *font = (UIFont *)value;
- if (font != nullptr) {
- UIFont *newFont = [font setItalic];
- [_input->textView.textStorage addAttribute:NSFontAttributeName
- value:newFont
- range:range];
- }
- }];
+ [self addAttributesInAttributedString:_input->textView.textStorage
+ range:range];
[_input->textView.textStorage endEditing];
}
@@ -61,22 +67,29 @@ - (void)addTypingAttributes {
}
}
+- (void)removeAttributesInAttributedString:
+ (NSMutableAttributedString *)attributedString
+ range:(NSRange)range {
+ [attributedString enumerateAttribute:NSFontAttributeName
+ inRange:range
+ options:0
+ usingBlock:^(id _Nullable value, NSRange range,
+ BOOL *_Nonnull stop) {
+ UIFont *font = (UIFont *)value;
+ if (font != nullptr) {
+ UIFont *newFont = [font removeItalic];
+ [attributedString
+ addAttribute:NSFontAttributeName
+ value:newFont
+ range:range];
+ }
+ }];
+}
+
- (void)removeAttributes:(NSRange)range {
[_input->textView.textStorage beginEditing];
- [_input->textView.textStorage
- enumerateAttribute:NSFontAttributeName
- inRange:range
- options:0
- usingBlock:^(id _Nullable value, NSRange range,
- BOOL *_Nonnull stop) {
- UIFont *font = (UIFont *)value;
- if (font != nullptr) {
- UIFont *newFont = [font removeItalic];
- [_input->textView.textStorage addAttribute:NSFontAttributeName
- value:newFont
- range:range];
- }
- }];
+ [self removeAttributesInAttributedString:_input->textView.textStorage
+ range:range];
[_input->textView.textStorage endEditing];
}
@@ -96,14 +109,21 @@ - (BOOL)styleCondition:(id _Nullable)value:(NSRange)range {
return font != nullptr && [font isItalic];
}
+- (BOOL)detectStyleInAttributedString:
+ (NSMutableAttributedString *)attributedString
+ range:(NSRange)range {
+ return [OccurenceUtils detect:NSFontAttributeName
+ inString:attributedString
+ 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:NSFontAttributeName
- withInput:_input
- inRange:range
- withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
- }];
+ return [self detectStyleInAttributedString:_input->textView.textStorage
+ range:range];
} else {
return [OccurenceUtils detect:NSFontAttributeName
withInput:_input
@@ -133,4 +153,15 @@ - (BOOL)anyOccurence:(NSRange)range {
}];
}
+- (NSArray *_Nullable)
+ findAllOccurencesInAttributedString:(NSAttributedString *)attributedString
+ range:(NSRange)range {
+ return [OccurenceUtils all:NSFontAttributeName
+ inString:attributedString
+ inRange:range
+ withCondition:^BOOL(id _Nullable value, NSRange range) {
+ return [self styleCondition:value:range];
+ }];
+}
+
@end
diff --git a/ios/styles/LinkStyle.mm b/ios/styles/LinkStyle.mm
index c9da3fc54..8846a5ebe 100644
--- a/ios/styles/LinkStyle.mm
+++ b/ios/styles/LinkStyle.mm
@@ -33,7 +33,13 @@ - (void)applyStyle:(NSRange)range {
// no-op for links
}
-- (void)addAttributes:(NSRange)range withTypingAttr:(BOOL)withTypingAttr {
+- (void)addAttributes:(NSRange)range {
+ // no-op for links
+}
+
+- (void)addAttributesInAttributedString:
+ (NSMutableAttributedString *)attributedString
+ range:(NSRange)range {
// no-op for links
}
@@ -41,32 +47,47 @@ - (void)addTypingAttributes {
// no-op for links
}
-// we have to make sure all links in the range get fully removed here
-- (void)removeAttributes:(NSRange)range {
- NSArray *links = [self findAllOccurences:range];
- [_input->textView.textStorage beginEditing];
+- (void)removeAttributesInAttributedString:(NSMutableAttributedString *)attr
+ range:(NSRange)range {
+ NSArray *links = [OccurenceUtils
+ allMultiple:@[ ManualLinkAttributeName, AutomaticLinkAttributeName ]
+ inString:attr
+ inRange:range
+ withCondition:^BOOL(id value, NSRange r) {
+ return [self styleCondition:value:r];
+ }];
+
for (StylePair *pair in links) {
- NSRange linkRange =
- [self getFullLinkRangeAt:[pair.rangeValue rangeValue].location];
- [_input->textView.textStorage removeAttribute:ManualLinkAttributeName
- range:linkRange];
- [_input->textView.textStorage removeAttribute:AutomaticLinkAttributeName
- range:linkRange];
- [_input->textView.textStorage addAttribute:NSForegroundColorAttributeName
- value:[_input->config primaryColor]
- range:linkRange];
- [_input->textView.textStorage addAttribute:NSUnderlineColorAttributeName
- value:[_input->config primaryColor]
- range:linkRange];
- [_input->textView.textStorage addAttribute:NSStrikethroughColorAttributeName
- value:[_input->config primaryColor]
- range:linkRange];
+ NSRange fullRange = [self
+ offline_fullLinkRangeInAttributedString:attr
+ atIndex:[pair.rangeValue rangeValue]
+ .location];
+
+ [attr removeAttribute:ManualLinkAttributeName range:fullRange];
+ [attr removeAttribute:AutomaticLinkAttributeName range:fullRange];
+
+ UIColor *primary = [_input->config primaryColor];
+ [attr addAttribute:NSForegroundColorAttributeName
+ value:primary
+ range:fullRange];
+ [attr addAttribute:NSUnderlineColorAttributeName
+ value:primary
+ range:fullRange];
+ [attr addAttribute:NSStrikethroughColorAttributeName
+ value:primary
+ range:fullRange];
+
if ([_input->config linkDecorationLine] == DecorationUnderline) {
- [_input->textView.textStorage
- removeAttribute:NSUnderlineStyleAttributeName
- range:linkRange];
+ [attr removeAttribute:NSUnderlineStyleAttributeName range:fullRange];
}
}
+}
+
+// we have to make sure all links in the range get fully removed here
+- (void)removeAttributes:(NSRange)range {
+ [_input->textView.textStorage beginEditing];
+ [self removeAttributesInAttributedString:_input->textView.textStorage
+ range:range];
[_input->textView.textStorage endEditing];
// adjust typing attributes as well
@@ -126,6 +147,21 @@ - (BOOL)styleCondition:(id _Nullable)value:(NSRange)range {
return linkValue != nullptr;
}
+- (BOOL)detectStyleInAttributedString:(NSAttributedString *)attrString
+ range:(NSRange)range {
+ BOOL onlyLinks = [OccurenceUtils
+ detectMultiple:@[ ManualLinkAttributeName, AutomaticLinkAttributeName ]
+ inString:attrString
+ inRange:range
+ withCondition:^BOOL(id value, NSRange r) {
+ return [self styleCondition:value:r];
+ }];
+
+ if (!onlyLinks)
+ return NO;
+ return [self offline_isSingleLinkIn:attrString range:range];
+}
+
- (BOOL)detectStyle:(NSRange)range {
if (range.length >= 1) {
BOOL onlyLinks = [OccurenceUtils
@@ -161,6 +197,18 @@ - (BOOL)anyOccurence:(NSRange)range {
}];
}
+- (NSArray *_Nullable)
+ findAllOccurencesInAttributedString:(NSAttributedString *)attributedString
+ range:(NSRange)range {
+ return [OccurenceUtils
+ allMultiple:@[ ManualLinkAttributeName, AutomaticLinkAttributeName ]
+ inString:attributedString
+ inRange:range
+ withCondition:^BOOL(id _Nullable value, NSRange range) {
+ return [self styleCondition:value:range];
+ }];
+}
+
// MARK: - Public non-standard methods
- (void)addLink:(NSString *)text
@@ -584,4 +632,77 @@ - (void)removeConnectedLinksIfNeeded:(NSString *)word range:(NSRange)wordRange {
}
}
+- (void)addLinkInAttributedString:(NSMutableAttributedString *)attr
+ range:(NSRange)range
+ text:(NSString *)text
+ url:(NSString *)url
+ manual:(BOOL)manual {
+ if (!text || !url)
+ return;
+
+ NSDictionary *attrs = [self offline_linkAttributesForURL:url manual:manual];
+ [attr addAttributes:attrs range:range];
+}
+
+- (NSMutableDictionary *)offline_linkAttributesForURL:(NSString *)url
+ manual:(BOOL)manual {
+ NSMutableDictionary *attrs = [_input->defaultTypingAttributes mutableCopy];
+
+ attrs[NSForegroundColorAttributeName] = [_input->config linkColor];
+ attrs[NSUnderlineColorAttributeName] = [_input->config linkColor];
+ attrs[NSStrikethroughColorAttributeName] = [_input->config linkColor];
+
+ if ([_input->config linkDecorationLine] == DecorationUnderline) {
+ attrs[NSUnderlineStyleAttributeName] = @(NSUnderlineStyleSingle);
+ }
+ if (manual) {
+ attrs[ManualLinkAttributeName] = url;
+ } else {
+ attrs[AutomaticLinkAttributeName] = url;
+ }
+
+ return attrs;
+}
+
+- (NSRange)offline_fullLinkRangeInAttributedString:(NSAttributedString *)attr
+ atIndex:(NSUInteger)location {
+ NSRange fullManual = NSMakeRange(0, 0);
+ NSRange fullAuto = NSMakeRange(0, 0);
+
+ NSRange bounds = NSMakeRange(0, attr.length);
+
+ if (location >= attr.length && attr.length > 0) {
+ location = attr.length - 1;
+ }
+
+ NSString *manual = [attr attribute:ManualLinkAttributeName
+ atIndex:location
+ longestEffectiveRange:&fullManual
+ inRange:bounds];
+
+ NSString *autoUrl = [attr attribute:AutomaticLinkAttributeName
+ atIndex:location
+ longestEffectiveRange:&fullAuto
+ inRange:bounds];
+
+ if (manual != nil)
+ return fullManual;
+ if (autoUrl != nil)
+ return fullAuto;
+
+ return NSMakeRange(0, 0);
+}
+
+- (BOOL)offline_isSingleLinkIn:(NSAttributedString *)attr range:(NSRange)range {
+ NSArray *pairs = [OccurenceUtils
+ allMultiple:@[ ManualLinkAttributeName, AutomaticLinkAttributeName ]
+ inString:attr
+ inRange:range
+ withCondition:^BOOL(id value, NSRange r) {
+ return [self styleCondition:value:r];
+ }];
+
+ return pairs.count == 1;
+}
+
@end
diff --git a/ios/styles/MediaAttachment.h b/ios/styles/MediaAttachment.h
new file mode 100644
index 000000000..050a993f8
--- /dev/null
+++ b/ios/styles/MediaAttachment.h
@@ -0,0 +1,23 @@
+#import
+
+@class MediaAttachment;
+
+@protocol MediaAttachmentDelegate
+- (void)mediaAttachmentDidUpdate:(MediaAttachment *)attachment;
+@end
+
+@interface MediaAttachment : NSTextAttachment
+
+@property(nonatomic, weak) id delegate;
+@property(nonatomic, strong) NSString *uri;
+@property(nonatomic, assign) CGFloat width;
+@property(nonatomic, assign) CGFloat height;
+
+- (instancetype)initWithURI:(NSString *)uri
+ width:(CGFloat)width
+ height:(CGFloat)height;
+
+- (void)loadAsync;
+- (void)notifyUpdate;
+
+@end
diff --git a/ios/styles/MediaAttachment.mm b/ios/styles/MediaAttachment.mm
new file mode 100644
index 000000000..7eed179e8
--- /dev/null
+++ b/ios/styles/MediaAttachment.mm
@@ -0,0 +1,31 @@
+#import "MediaAttachment.h"
+
+@implementation MediaAttachment
+
+- (instancetype)initWithURI:(NSString *)uri
+ width:(CGFloat)width
+ height:(CGFloat)height {
+ self = [super init];
+ if (!self)
+ return nil;
+
+ _uri = uri;
+ _width = width;
+ _height = height;
+
+ self.bounds = CGRectMake(0, 0, width, height);
+
+ return self;
+}
+
+- (void)loadAsync {
+ // no-op for base
+}
+
+- (void)notifyUpdate {
+ if ([self.delegate respondsToSelector:@selector(mediaAttachmentDidUpdate:)]) {
+ [self.delegate mediaAttachmentDidUpdate:self];
+ }
+}
+
+@end
diff --git a/ios/styles/MentionStyle.mm b/ios/styles/MentionStyle.mm
index a60b687fb..4acaa326b 100644
--- a/ios/styles/MentionStyle.mm
+++ b/ios/styles/MentionStyle.mm
@@ -37,7 +37,13 @@ - (void)applyStyle:(NSRange)range {
// no-op for mentions
}
-- (void)addAttributes:(NSRange)range withTypingAttr:(BOOL)withTypingAttr {
+- (void)addAttributes:(NSRange)range {
+ // no-op for mentions
+}
+
+- (void)addAttributesInAttributedString:
+ (NSMutableAttributedString *)attributedString
+ range:(NSRange)range {
// no-op for mentions
}
@@ -45,6 +51,41 @@ - (void)addTypingAttributes {
// no-op for mentions
}
+- (void)removeAttributesInAttributedString:
+ (NSMutableAttributedString *)attributedString
+ range:(NSRange)range {
+ NSArray *mentions = [self findAllOccurences:range];
+
+ for (StylePair *pair in mentions) {
+ NSRange fullRange =
+ [self getFullMentionRangeInAttributedString:attributedString
+ atIndex:[pair.rangeValue rangeValue]
+ .location];
+
+ [attributedString removeAttribute:MentionAttributeName range:fullRange];
+
+ // restore normal coloring
+ UIColor *primary = [_input->config primaryColor];
+ [attributedString addAttribute:NSForegroundColorAttributeName
+ value:primary
+ range:fullRange];
+ [attributedString addAttribute:NSUnderlineColorAttributeName
+ value:primary
+ range:fullRange];
+ [attributedString addAttribute:NSStrikethroughColorAttributeName
+ value:primary
+ range:fullRange];
+ [attributedString removeAttribute:NSBackgroundColorAttributeName
+ range:fullRange];
+
+ MentionStyleProps *props = [self stylePropsWithParams:pair.styleValue];
+ if (props.decorationLine == DecorationUnderline) {
+ [attributedString removeAttribute:NSUnderlineStyleAttributeName
+ range:fullRange];
+ }
+ }
+}
+
// we have to make sure all mentions get removed properly
- (void)removeAttributes:(NSRange)range {
BOOL someMentionHadUnderline = NO;
@@ -141,6 +182,17 @@ - (BOOL)styleCondition:(id _Nullable)value:(NSRange)range {
return params != nullptr;
}
+- (BOOL)detectStyleInAttributedString:
+ (NSMutableAttributedString *)attributedString
+ range:(NSRange)range {
+ return [OccurenceUtils detect:MentionAttributeName
+ inString:attributedString
+ inRange:range
+ withCondition:^BOOL(id value, NSRange r) {
+ return [self styleCondition:value:r];
+ }];
+}
+
- (BOOL)detectStyle:(NSRange)range {
if (range.length >= 1) {
return [OccurenceUtils detect:MentionAttributeName
@@ -172,6 +224,17 @@ - (BOOL)anyOccurence:(NSRange)range {
}];
}
+- (NSArray *_Nullable)
+ findAllOccurencesInAttributedString:(NSAttributedString *)attributedString
+ range:(NSRange)range {
+ return [OccurenceUtils all:MentionAttributeName
+ inString:attributedString
+ inRange:range
+ withCondition:^BOOL(id _Nullable value, NSRange range) {
+ return [self styleCondition:value:range];
+ }];
+}
+
// MARK: - Public non-standard methods
- (void)addMention:(NSString *)indicator
@@ -688,4 +751,58 @@ - (void)removeActiveMentionRange {
}
}
+- (NSRange)getFullMentionRangeInAttributedString:
+ (NSMutableAttributedString *)attrString
+ atIndex:(NSUInteger)location {
+ NSRange full = NSMakeRange(0, 0);
+ NSRange bounds = NSMakeRange(0, attrString.length);
+
+ if (location >= attrString.length && attrString.length > 0) {
+ location = attrString.length - 1;
+ }
+
+ [attrString attribute:MentionAttributeName
+ atIndex:location
+ longestEffectiveRange:&full
+ inRange:bounds];
+
+ return full;
+}
+
+- (void)addMentionInAttributedString:(NSMutableAttributedString *)string
+ range:(NSRange)range
+ params:(MentionParams *)params {
+ if (!string || !params)
+ return;
+
+ MentionStyleProps *props =
+ [_input->config mentionStylePropsForIndicator:params.indicator];
+
+ NSMutableDictionary *attrs =
+ [_input->textView.typingAttributes mutableCopy];
+
+ attrs[MentionAttributeName] = params;
+ attrs[NSForegroundColorAttributeName] = props.color;
+ attrs[NSUnderlineColorAttributeName] = props.color;
+ attrs[NSStrikethroughColorAttributeName] = props.color;
+ attrs[NSBackgroundColorAttributeName] =
+ [props.backgroundColor colorWithAlphaIfNotTransparent:0.4];
+
+ if (props.decorationLine == DecorationUnderline) {
+ attrs[NSUnderlineStyleAttributeName] = @(NSUnderlineStyleSingle);
+ } else {
+ [attrs removeObjectForKey:NSUnderlineStyleAttributeName];
+ }
+
+ NSString *mentionText = params.text ?: @"";
+ NSAttributedString *mention =
+ [[NSAttributedString alloc] initWithString:mentionText attributes:attrs];
+
+ if (range.length == 0) {
+ [string insertAttributedString:mention atIndex:range.location];
+ } else {
+ [string replaceCharactersInRange:range withAttributedString:mention];
+ }
+}
+
@end
diff --git a/ios/styles/OrderedListStyle.mm b/ios/styles/OrderedListStyle.mm
index d9c918efa..76cc137b9 100644
--- a/ios/styles/OrderedListStyle.mm
+++ b/ios/styles/OrderedListStyle.mm
@@ -33,15 +33,68 @@ - (instancetype)initWithInput:(id)input {
- (void)applyStyle:(NSRange)range {
BOOL isStylePresent = [self detectStyle:range];
if (range.length >= 1) {
- isStylePresent ? [self removeAttributes:range]
- : [self addAttributes:range withTypingAttr:YES];
+ isStylePresent ? [self removeAttributes:range] : [self addAttributes:range];
} else {
isStylePresent ? [self removeTypingAttributes] : [self addTypingAttributes];
}
}
+- (void)addAttributesInAttributedString:
+ (NSMutableAttributedString *)attributedString
+ range:(NSRange)range {
+ NSTextList *numberBullet =
+ [[NSTextList alloc] initWithMarkerFormat:NSTextListMarkerDecimal
+ options:0];
+ NSArray *paragraphs = [ParagraphsUtils
+ getSeparateParagraphsRangesInAttributedString:attributedString
+ range:range];
+
+ NSInteger offset = 0;
+
+ for (NSValue *val in paragraphs) {
+ NSRange p = val.rangeValue;
+ NSRange fixedRange = NSMakeRange(p.location + offset, p.length);
+
+ if (fixedRange.length == 0 ||
+ (fixedRange.length == 1 &&
+ [[NSCharacterSet newlineCharacterSet]
+ characterIsMember:[attributedString.string
+ characterAtIndex:fixedRange.location]])) {
+
+ [TextInsertionUtils insertTextInAttributedString:@"\u200B"
+ at:fixedRange.location
+ additionalAttributes:nil
+ attributedString:attributedString];
+
+ fixedRange = NSMakeRange(fixedRange.location, fixedRange.length + 1);
+ offset += 1;
+ }
+
+ [attributedString
+ enumerateAttribute:NSParagraphStyleAttributeName
+ inRange:fixedRange
+ options:0
+ usingBlock:^(id _Nullable value, NSRange subRange,
+ BOOL *_Nonnull stop) {
+ NSMutableParagraphStyle *pStyle =
+ value ? [(NSParagraphStyle *)value mutableCopy]
+ : [NSMutableParagraphStyle new];
+
+ pStyle.textLists = @[ numberBullet ];
+ pStyle.headIndent = [self getHeadIndent];
+ pStyle.firstLineHeadIndent = [self getHeadIndent];
+ NSMutableDictionary *typingAttrs =
+ [_input->textView.typingAttributes mutableCopy];
+ typingAttrs[NSParagraphStyleAttributeName] = pStyle;
+ [attributedString addAttribute:NSParagraphStyleAttributeName
+ value:pStyle
+ range:subRange];
+ }];
+ }
+}
+
// we assume correct paragraph range is already given
-- (void)addAttributes:(NSRange)range withTypingAttr:(BOOL)withTypingAttr {
+- (void)addAttributes:(NSRange)range {
NSTextList *numberBullet =
[[NSTextList alloc] initWithMarkerFormat:NSTextListMarkerDecimal
options:0];
@@ -110,50 +163,54 @@ - (void)addAttributes:(NSRange)range withTypingAttr:(BOOL)withTypingAttr {
}
// also add typing attributes
- if (withTypingAttr) {
- NSMutableDictionary *typingAttrs =
- [_input->textView.typingAttributes mutableCopy];
- NSMutableParagraphStyle *pStyle =
- [typingAttrs[NSParagraphStyleAttributeName] mutableCopy];
- pStyle.textLists = @[ numberBullet ];
- pStyle.headIndent = [self getHeadIndent];
- pStyle.firstLineHeadIndent = [self getHeadIndent];
- typingAttrs[NSParagraphStyleAttributeName] = pStyle;
- _input->textView.typingAttributes = typingAttrs;
- }
+ NSMutableDictionary *typingAttrs =
+ [_input->textView.typingAttributes mutableCopy];
+ NSMutableParagraphStyle *pStyle =
+ [typingAttrs[NSParagraphStyleAttributeName] mutableCopy];
+ pStyle.textLists = @[ numberBullet ];
+ pStyle.headIndent = [self getHeadIndent];
+ pStyle.firstLineHeadIndent = [self getHeadIndent];
+ typingAttrs[NSParagraphStyleAttributeName] = pStyle;
+ _input->textView.typingAttributes = typingAttrs;
}
// does pretty much the same as normal addAttributes, just need to get the range
- (void)addTypingAttributes {
- [self addAttributes:_input->textView.selectedRange withTypingAttr:YES];
+ [self addAttributes:_input->textView.selectedRange];
}
-- (void)removeAttributes:(NSRange)range {
- NSArray *paragraphs =
- [ParagraphsUtils getSeparateParagraphsRangesIn:_input->textView
- range:range];
-
- [_input->textView.textStorage beginEditing];
+- (void)removeAttributesInAttributedString:
+ (NSMutableAttributedString *)attributedString
+ range:(NSRange)range {
+ NSArray *paragraphs = [ParagraphsUtils
+ getSeparateParagraphsRangesInAttributedString:attributedString
+ range:range];
for (NSValue *value in paragraphs) {
NSRange range = [value rangeValue];
- [_input->textView.textStorage
- enumerateAttribute:NSParagraphStyleAttributeName
- inRange:range
- options:0
- usingBlock:^(id _Nullable value, NSRange range,
- BOOL *_Nonnull stop) {
- NSMutableParagraphStyle *pStyle =
- [(NSParagraphStyle *)value mutableCopy];
- pStyle.textLists = @[];
- pStyle.headIndent = 0;
- pStyle.firstLineHeadIndent = 0;
- [_input->textView.textStorage
- addAttribute:NSParagraphStyleAttributeName
- value:pStyle
- range:range];
- }];
+ [attributedString enumerateAttribute:NSParagraphStyleAttributeName
+ inRange:range
+ options:0
+ usingBlock:^(id _Nullable value, NSRange range,
+ BOOL *_Nonnull stop) {
+ NSMutableParagraphStyle *pStyle =
+ [(NSParagraphStyle *)value mutableCopy];
+ pStyle.textLists = @[];
+ pStyle.headIndent = 0;
+ pStyle.firstLineHeadIndent = 0;
+ [attributedString
+ addAttribute:NSParagraphStyleAttributeName
+ value:pStyle
+ range:range];
+ }];
}
+}
+
+- (void)removeAttributes:(NSRange)range {
+ [_input->textView.textStorage beginEditing];
+
+ [self removeAttributesInAttributedString:_input->textView.textStorage
+ range:range];
[_input->textView.textStorage endEditing];
@@ -226,8 +283,7 @@ - (BOOL)tryHandlingListShorcutInRange:(NSRange)range
// add attributes on the paragraph
[self addAttributes:NSMakeRange(paragraphRange.location,
- paragraphRange.length - 1)
- withTypingAttr:YES];
+ paragraphRange.length - 1)];
return YES;
}
}
@@ -242,14 +298,21 @@ - (BOOL)styleCondition:(id _Nullable)value:(NSRange)range {
NSTextListMarkerDecimal;
}
+- (BOOL)detectStyleInAttributedString:
+ (NSMutableAttributedString *)attributedString
+ range:(NSRange)range {
+ return [OccurenceUtils detect:NSParagraphStyleAttributeName
+ inString:attributedString
+ 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:NSParagraphStyleAttributeName
- withInput:_input
- inRange:range
- withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
- }];
+ return [self detectStyleInAttributedString:_input->textView.textStorage
+ range:range];
} else {
return [OccurenceUtils detect:NSParagraphStyleAttributeName
withInput:_input
@@ -279,4 +342,15 @@ - (BOOL)anyOccurence:(NSRange)range {
}];
}
+- (NSArray *_Nullable)
+ findAllOccurencesInAttributedString:(NSAttributedString *)attributedString
+ range:(NSRange)range {
+ return [OccurenceUtils all:NSParagraphStyleAttributeName
+ inString:attributedString
+ inRange:range
+ withCondition:^BOOL(id _Nullable value, NSRange range) {
+ return [self styleCondition:value:range];
+ }];
+}
+
@end
diff --git a/ios/styles/StrikethroughStyle.mm b/ios/styles/StrikethroughStyle.mm
index 9cf96d34a..516737049 100644
--- a/ios/styles/StrikethroughStyle.mm
+++ b/ios/styles/StrikethroughStyle.mm
@@ -23,17 +23,23 @@ - (instancetype)initWithInput:(id)input {
- (void)applyStyle:(NSRange)range {
BOOL isStylePresent = [self detectStyle:range];
if (range.length >= 1) {
- isStylePresent ? [self removeAttributes:range]
- : [self addAttributes:range withTypingAttr:YES];
+ isStylePresent ? [self removeAttributes:range] : [self addAttributes:range];
} else {
isStylePresent ? [self removeTypingAttributes] : [self addTypingAttributes];
}
}
-- (void)addAttributes:(NSRange)range withTypingAttr:(BOOL)withTypingAttr {
- [_input->textView.textStorage addAttribute:NSStrikethroughStyleAttributeName
- value:@(NSUnderlineStyleSingle)
- range:range];
+- (void)addAttributesInAttributedString:
+ (NSMutableAttributedString *)attributedString
+ range:(NSRange)range {
+ [attributedString addAttribute:NSStrikethroughStyleAttributeName
+ value:@(NSUnderlineStyleSingle)
+ range:range];
+}
+
+- (void)addAttributes:(NSRange)range {
+ [self addAttributesInAttributedString:_input->textView.textStorage
+ range:range];
}
- (void)addTypingAttributes {
@@ -43,10 +49,16 @@ - (void)addTypingAttributes {
_input->textView.typingAttributes = newTypingAttrs;
}
+- (void)removeAttributesInAttributedString:
+ (NSMutableAttributedString *)attributedString
+ range:(NSRange)range {
+ [attributedString removeAttribute:NSStrikethroughStyleAttributeName
+ range:range];
+}
+
- (void)removeAttributes:(NSRange)range {
- [_input->textView.textStorage
- removeAttribute:NSStrikethroughStyleAttributeName
- range:range];
+ [self removeAttributesInAttributedString:_input->textView.textStorage
+ range:range];
}
- (void)removeTypingAttributes {
@@ -62,14 +74,21 @@ - (BOOL)styleCondition:(id _Nullable)value:(NSRange)range {
[strikethroughStyle intValue] != NSUnderlineStyleNone;
}
+- (BOOL)detectStyleInAttributedString:
+ (NSMutableAttributedString *)attributedString
+ range:(NSRange)range {
+ return [OccurenceUtils detect:NSStrikethroughStyleAttributeName
+ inString:attributedString
+ 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:NSStrikethroughStyleAttributeName
- withInput:_input
- inRange:range
- withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
- }];
+ return [self detectStyleInAttributedString:_input->textView.textStorage
+ range:range];
} else {
return [OccurenceUtils detect:NSStrikethroughStyleAttributeName
withInput:_input
@@ -99,4 +118,15 @@ - (BOOL)anyOccurence:(NSRange)range {
}];
}
+- (NSArray *_Nullable)
+ findAllOccurencesInAttributedString:(NSAttributedString *)attributedString
+ range:(NSRange)range {
+ return [OccurenceUtils all:NSStrikethroughStyleAttributeName
+ inString:attributedString
+ inRange:range
+ withCondition:^BOOL(id _Nullable value, NSRange range) {
+ return [self styleCondition:value:range];
+ }];
+}
+
@end
diff --git a/ios/styles/UnderlineStyle.mm b/ios/styles/UnderlineStyle.mm
index 98050475d..ffe085347 100644
--- a/ios/styles/UnderlineStyle.mm
+++ b/ios/styles/UnderlineStyle.mm
@@ -23,17 +23,23 @@ - (instancetype)initWithInput:(id)input {
- (void)applyStyle:(NSRange)range {
BOOL isStylePresent = [self detectStyle:range];
if (range.length >= 1) {
- isStylePresent ? [self removeAttributes:range]
- : [self addAttributes:range withTypingAttr:YES];
+ isStylePresent ? [self removeAttributes:range] : [self addAttributes:range];
} else {
isStylePresent ? [self removeTypingAttributes] : [self addTypingAttributes];
}
}
-- (void)addAttributes:(NSRange)range withTypingAttr:(BOOL)withTypingAttr {
- [_input->textView.textStorage addAttribute:NSUnderlineStyleAttributeName
- value:@(NSUnderlineStyleSingle)
- range:range];
+- (void)addAttributesInAttributedString:
+ (NSMutableAttributedString *)attributedString
+ range:(NSRange)range {
+ [attributedString addAttribute:NSUnderlineStyleAttributeName
+ value:@(NSUnderlineStyleSingle)
+ range:range];
+}
+
+- (void)addAttributes:(NSRange)range {
+ [self addAttributesInAttributedString:_input->textView.textStorage
+ range:range];
}
- (void)addTypingAttributes {
@@ -43,9 +49,15 @@ - (void)addTypingAttributes {
_input->textView.typingAttributes = newTypingAttrs;
}
+- (void)removeAttributesInAttributedString:
+ (NSMutableAttributedString *)attributedString
+ range:(NSRange)range {
+ [attributedString removeAttribute:NSUnderlineStyleAttributeName range:range];
+}
+
- (void)removeAttributes:(NSRange)range {
- [_input->textView.textStorage removeAttribute:NSUnderlineStyleAttributeName
- range:range];
+ [self removeAttributesInAttributedString:_input->textView.textStorage
+ range:range];
}
- (void)removeTypingAttributes {
@@ -99,14 +111,21 @@ - (BOOL)styleCondition:(id _Nullable)value:(NSRange)range {
![self underlinedMentionConflictsInRange:range];
}
+- (BOOL)detectStyleInAttributedString:
+ (NSMutableAttributedString *)attributedString
+ range:(NSRange)range {
+ return [OccurenceUtils detect:NSStrikethroughStyleAttributeName
+ inString:attributedString
+ 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:NSUnderlineStyleAttributeName
- withInput:_input
- inRange:range
- withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
- }];
+ return [self detectStyleInAttributedString:_input->textView.textStorage
+ range:range];
} else {
return [OccurenceUtils detect:NSUnderlineStyleAttributeName
withInput:_input
@@ -136,4 +155,15 @@ - (BOOL)anyOccurence:(NSRange)range {
}];
}
+- (NSArray *_Nullable)
+ findAllOccurencesInAttributedString:(NSAttributedString *)attributedString
+ range:(NSRange)range {
+ return [OccurenceUtils all:NSUnderlineStyleAttributeName
+ inString:attributedString
+ inRange:range
+ withCondition:^BOOL(id _Nullable value, NSRange range) {
+ return [self styleCondition:value:range];
+ }];
+}
+
@end
diff --git a/ios/styles/UnorderedListStyle.mm b/ios/styles/UnorderedListStyle.mm
index 9fb2f7b24..3dd744ee6 100644
--- a/ios/styles/UnorderedListStyle.mm
+++ b/ios/styles/UnorderedListStyle.mm
@@ -33,15 +33,68 @@ - (instancetype)initWithInput:(id)input {
- (void)applyStyle:(NSRange)range {
BOOL isStylePresent = [self detectStyle:range];
if (range.length >= 1) {
- isStylePresent ? [self removeAttributes:range]
- : [self addAttributes:range withTypingAttr:YES];
+ isStylePresent ? [self removeAttributes:range] : [self addAttributes:range];
} else {
isStylePresent ? [self removeTypingAttributes] : [self addTypingAttributes];
}
}
+- (void)addAttributesInAttributedString:
+ (NSMutableAttributedString *)attributedString
+ range:(NSRange)range {
+ NSTextList *bullet =
+ [[NSTextList alloc] initWithMarkerFormat:NSTextListMarkerDisc options:0];
+ NSArray *paragraphs = [ParagraphsUtils
+ getSeparateParagraphsRangesInAttributedString:attributedString
+ range:range];
+ // if we fill empty lines with zero width spaces, we need to offset later
+ // ranges
+ NSInteger offset = 0;
+
+ for (NSValue *value in paragraphs) {
+ // take previous offsets into consideration
+ NSRange fixedRange = NSMakeRange([value rangeValue].location + offset,
+ [value rangeValue].length);
+
+ // length 0 with first line, length 1 and newline with some empty lines in
+ // the middle
+ if (fixedRange.length == 0 ||
+ (fixedRange.length == 1 &&
+ [[NSCharacterSet newlineCharacterSet]
+ characterIsMember:[attributedString.string
+ characterAtIndex:fixedRange.location]])) {
+ [TextInsertionUtils insertTextInAttributedString:@"\u200B"
+ at:fixedRange.location
+ additionalAttributes:nullptr
+ attributedString:attributedString];
+ fixedRange = NSMakeRange(fixedRange.location, fixedRange.length + 1);
+ offset += 1;
+ }
+
+ [attributedString
+ enumerateAttribute:NSParagraphStyleAttributeName
+ inRange:fixedRange
+ options:0
+ usingBlock:^(id _Nullable value, NSRange range,
+ BOOL *_Nonnull stop) {
+ NSMutableParagraphStyle *pStyle =
+ value ? [(NSParagraphStyle *)value mutableCopy]
+ : [NSMutableParagraphStyle new];
+ pStyle.textLists = @[ bullet ];
+ pStyle.headIndent = [self getHeadIndent];
+ pStyle.firstLineHeadIndent = [self getHeadIndent];
+ NSMutableDictionary *typingAttrs =
+ [_input->textView.typingAttributes mutableCopy];
+ typingAttrs[NSParagraphStyleAttributeName] = pStyle;
+ [attributedString addAttribute:NSParagraphStyleAttributeName
+ value:pStyle
+ range:range];
+ }];
+ }
+}
+
// we assume correct paragraph range is already given
-- (void)addAttributes:(NSRange)range withTypingAttr:(BOOL)withTypingAttr {
+- (void)addAttributes:(NSRange)range {
NSTextList *bullet =
[[NSTextList alloc] initWithMarkerFormat:NSTextListMarkerDisc options:0];
NSArray *paragraphs =
@@ -109,51 +162,53 @@ - (void)addAttributes:(NSRange)range withTypingAttr:(BOOL)withTypingAttr {
}
// also add typing attributes
- if (withTypingAttr) {
- NSMutableDictionary *typingAttrs =
- [_input->textView.typingAttributes mutableCopy];
- NSMutableParagraphStyle *pStyle =
- [typingAttrs[NSParagraphStyleAttributeName] mutableCopy];
- pStyle.textLists = @[ bullet ];
- pStyle.headIndent = [self getHeadIndent];
- pStyle.firstLineHeadIndent = [self getHeadIndent];
- typingAttrs[NSParagraphStyleAttributeName] = pStyle;
- _input->textView.typingAttributes = typingAttrs;
- }
+ NSMutableDictionary *typingAttrs =
+ [_input->textView.typingAttributes mutableCopy];
+ NSMutableParagraphStyle *pStyle =
+ [typingAttrs[NSParagraphStyleAttributeName] mutableCopy];
+ pStyle.textLists = @[ bullet ];
+ pStyle.headIndent = [self getHeadIndent];
+ pStyle.firstLineHeadIndent = [self getHeadIndent];
+ typingAttrs[NSParagraphStyleAttributeName] = pStyle;
+ _input->textView.typingAttributes = typingAttrs;
}
// does pretty much the same as normal addAttributes, just need to get the range
- (void)addTypingAttributes {
- [self addAttributes:_input->textView.selectedRange withTypingAttr:YES];
+ [self addAttributes:_input->textView.selectedRange];
}
-- (void)removeAttributes:(NSRange)range {
- NSArray *paragraphs =
- [ParagraphsUtils getSeparateParagraphsRangesIn:_input->textView
- range:range];
-
- [_input->textView.textStorage beginEditing];
+- (void)removeAttributesInAttributedString:
+ (NSMutableAttributedString *)attributedString
+ range:(NSRange)range {
+ NSArray *paragraphs = [ParagraphsUtils
+ getSeparateParagraphsRangesInAttributedString:attributedString
+ range:range];
for (NSValue *value in paragraphs) {
NSRange range = [value rangeValue];
- [_input->textView.textStorage
- enumerateAttribute:NSParagraphStyleAttributeName
- inRange:range
- options:0
- usingBlock:^(id _Nullable value, NSRange range,
- BOOL *_Nonnull stop) {
- NSMutableParagraphStyle *pStyle =
- [(NSParagraphStyle *)value mutableCopy];
- pStyle.textLists = @[];
- pStyle.headIndent = 0;
- pStyle.firstLineHeadIndent = 0;
- [_input->textView.textStorage
- addAttribute:NSParagraphStyleAttributeName
- value:pStyle
- range:range];
- }];
+ [attributedString enumerateAttribute:NSParagraphStyleAttributeName
+ inRange:range
+ options:0
+ usingBlock:^(id _Nullable value, NSRange range,
+ BOOL *_Nonnull stop) {
+ NSMutableParagraphStyle *pStyle =
+ [(NSParagraphStyle *)value mutableCopy];
+ pStyle.textLists = @[];
+ pStyle.headIndent = 0;
+ pStyle.firstLineHeadIndent = 0;
+ [attributedString
+ addAttribute:NSParagraphStyleAttributeName
+ value:pStyle
+ range:range];
+ }];
}
+}
+- (void)removeAttributes:(NSRange)range {
+ [_input->textView.textStorage beginEditing];
+ [self removeAttributesInAttributedString:_input->textView.textStorage
+ range:range];
[_input->textView.textStorage endEditing];
// also remove typing attributes
@@ -225,8 +280,7 @@ - (BOOL)tryHandlingListShorcutInRange:(NSRange)range
// add attributes on the dashless paragraph
[self addAttributes:NSMakeRange(paragraphRange.location,
- paragraphRange.length - 1)
- withTypingAttr:YES];
+ paragraphRange.length - 1)];
return YES;
}
}
@@ -240,14 +294,21 @@ - (BOOL)styleCondition:(id _Nullable)value:(NSRange)range {
paragraph.textLists.firstObject.markerFormat == NSTextListMarkerDisc;
}
+- (BOOL)detectStyleInAttributedString:
+ (NSMutableAttributedString *)attributedString
+ range:(NSRange)range {
+ return [OccurenceUtils detect:NSParagraphStyleAttributeName
+ inString:attributedString
+ 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:NSParagraphStyleAttributeName
- withInput:_input
- inRange:range
- withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
- }];
+ return [self detectStyleInAttributedString:_input->textView.textStorage
+ range:range];
} else {
return [OccurenceUtils detect:NSParagraphStyleAttributeName
withInput:_input
@@ -277,4 +338,15 @@ - (BOOL)anyOccurence:(NSRange)range {
}];
}
+- (NSArray *_Nullable)
+ findAllOccurencesInAttributedString:(NSAttributedString *)attributedString
+ range:(NSRange)range {
+ return [OccurenceUtils all:NSParagraphStyleAttributeName
+ inString:attributedString
+ inRange:range
+ withCondition:^BOOL(id _Nullable value, NSRange range) {
+ return [self styleCondition:value:range];
+ }];
+}
+
@end
diff --git a/ios/utils/BaseStyleProtocol.h b/ios/utils/BaseStyleProtocol.h
index 5e08e558a..d7a4b6eb7 100644
--- a/ios/utils/BaseStyleProtocol.h
+++ b/ios/utils/BaseStyleProtocol.h
@@ -7,11 +7,24 @@
+ (BOOL)isParagraphStyle;
- (instancetype _Nonnull)initWithInput:(id _Nonnull)input;
- (void)applyStyle:(NSRange)range;
-- (void)addAttributes:(NSRange)range withTypingAttr:(BOOL)withTypingAttr;
+- (void)addAttributes:(NSRange)range;
+- (void)addAttributesInAttributedString:
+ (NSMutableAttributedString *_Nonnull)attributedString
+ range:(NSRange)range;
+- (void)removeAttributesInAttributedString:
+ (NSMutableAttributedString *_Nonnull)attributedString
+ range:(NSRange)range;
+- (BOOL)detectStyleInAttributedString:
+ (NSMutableAttributedString *_Nonnull)attributedString
+ range:(NSRange)range;
- (void)removeAttributes:(NSRange)range;
- (void)addTypingAttributes;
- (void)removeTypingAttributes;
- (BOOL)detectStyle:(NSRange)range;
- (BOOL)anyOccurence:(NSRange)range;
- (NSArray *_Nullable)findAllOccurences:(NSRange)range;
+- (NSArray *_Nullable)
+ findAllOccurencesInAttributedString:
+ (NSAttributedString *_Nonnull)attributedString
+ range:(NSRange)range;
@end
diff --git a/ios/utils/OccurenceUtils.h b/ios/utils/OccurenceUtils.h
index 83cf12a65..4596b7746 100644
--- a/ios/utils/OccurenceUtils.h
+++ b/ios/utils/OccurenceUtils.h
@@ -1,47 +1,93 @@
#pragma once
#import "EnrichedTextInputView.h"
#import "StylePair.h"
+#import
@interface OccurenceUtils : NSObject
++ (BOOL)detect:(NSAttributedStringKey _Nonnull)key
+ inString:(NSAttributedString *_Nonnull)string
+ inRange:(NSRange)range
+ withCondition:(BOOL(NS_NOESCAPE ^ _Nonnull)(id _Nullable value,
+ NSRange range))condition;
+
++ (BOOL)detectMultiple:(NSArray *_Nonnull)keys
+ inString:(NSAttributedString *_Nonnull)string
+ inRange:(NSRange)range
+ withCondition:(BOOL(NS_NOESCAPE ^ _Nonnull)(id _Nullable value,
+ NSRange range))condition;
+
++ (BOOL)any:(NSAttributedStringKey _Nonnull)key
+ inString:(NSAttributedString *_Nonnull)string
+ inRange:(NSRange)range
+ withCondition:(BOOL(NS_NOESCAPE ^ _Nonnull)(id _Nullable value,
+ NSRange range))condition;
+
++ (BOOL)anyMultiple:(NSArray *_Nonnull)keys
+ inString:(NSAttributedString *_Nonnull)string
+ inRange:(NSRange)range
+ withCondition:(BOOL(NS_NOESCAPE ^ _Nonnull)(id _Nullable value,
+ NSRange range))condition;
+
++ (NSArray *_Nonnull)all:(NSAttributedStringKey _Nonnull)key
+ inString:(NSAttributedString *_Nonnull)string
+ inRange:(NSRange)range
+ withCondition:(BOOL(NS_NOESCAPE ^
+ _Nonnull)(id _Nullable value,
+ NSRange range))condition;
+
++ (NSArray *_Nonnull)
+ allMultiple:(NSArray *_Nonnull)keys
+ inString:(NSAttributedString *_Nonnull)string
+ inRange:(NSRange)range
+ withCondition:(BOOL(NS_NOESCAPE ^ _Nonnull)(id _Nullable value,
+ NSRange range))condition;
+ (BOOL)detect:(NSAttributedStringKey _Nonnull)key
withInput:(EnrichedTextInputView *_Nonnull)input
inRange:(NSRange)range
withCondition:(BOOL(NS_NOESCAPE ^ _Nonnull)(id _Nullable value,
NSRange range))condition;
+
+ (BOOL)detect:(NSAttributedStringKey _Nonnull)key
withInput:(EnrichedTextInputView *_Nonnull)input
atIndex:(NSUInteger)index
- checkPrevious:(BOOL)check
+ checkPrevious:(BOOL)checkPrev
withCondition:(BOOL(NS_NOESCAPE ^ _Nonnull)(id _Nullable value,
NSRange range))condition;
+
+ (BOOL)detectMultiple:(NSArray *_Nonnull)keys
withInput:(EnrichedTextInputView *_Nonnull)input
inRange:(NSRange)range
withCondition:(BOOL(NS_NOESCAPE ^ _Nonnull)(id _Nullable value,
NSRange range))condition;
+
+ (BOOL)any:(NSAttributedStringKey _Nonnull)key
withInput:(EnrichedTextInputView *_Nonnull)input
inRange:(NSRange)range
withCondition:(BOOL(NS_NOESCAPE ^ _Nonnull)(id _Nullable value,
NSRange range))condition;
+
+ (BOOL)anyMultiple:(NSArray *_Nonnull)keys
withInput:(EnrichedTextInputView *_Nonnull)input
inRange:(NSRange)range
withCondition:(BOOL(NS_NOESCAPE ^ _Nonnull)(id _Nullable value,
NSRange range))condition;
-+ (NSArray *_Nullable)all:(NSAttributedStringKey _Nonnull)key
- withInput:(EnrichedTextInputView *_Nonnull)input
- inRange:(NSRange)range
- withCondition:(BOOL(NS_NOESCAPE ^
- _Nonnull)(id _Nullable value,
- NSRange range))condition;
-+ (NSArray *_Nullable)
+
++ (NSArray *_Nonnull)all:(NSAttributedStringKey _Nonnull)key
+ withInput:(EnrichedTextInputView *_Nonnull)input
+ inRange:(NSRange)range
+ withCondition:(BOOL(NS_NOESCAPE ^
+ _Nonnull)(id _Nullable value,
+ NSRange range))condition;
+
++ (NSArray *_Nonnull)
allMultiple:(NSArray *_Nonnull)keys
withInput:(EnrichedTextInputView *_Nonnull)input
inRange:(NSRange)range
withCondition:(BOOL(NS_NOESCAPE ^ _Nonnull)(id _Nullable value,
NSRange range))condition;
+
+ (NSArray *_Nonnull)getRangesWithout:(NSArray *_Nonnull)types
withInput:(EnrichedTextInputView *_Nonnull)input
inRange:(NSRange)range;
+
@end
diff --git a/ios/utils/OccurenceUtils.mm b/ios/utils/OccurenceUtils.mm
index 43d58403c..19d53f34f 100644
--- a/ios/utils/OccurenceUtils.mm
+++ b/ios/utils/OccurenceUtils.mm
@@ -1,222 +1,314 @@
#import "OccurenceUtils.h"
+@interface OccurenceUtils ()
++ (void)enumerateAttributes:(NSArray *)keys
+ inString:(NSAttributedString *)string
+ inRange:(NSRange)range
+ withBlock:(void(NS_NOESCAPE ^)(NSAttributedStringKey key,
+ id value, NSRange range,
+ BOOL *stop))block;
+
++ (NSArray *)
+ collectAttributes:(NSArray *)keys
+ inString:(NSAttributedString *)string
+ inRange:(NSRange)range
+ withCondition:(BOOL(NS_NOESCAPE ^)(id value, NSRange range))condition;
+@end
+
@implementation OccurenceUtils
-+ (BOOL)detect:(NSAttributedStringKey _Nonnull)key
- withInput:(EnrichedTextInputView *_Nonnull)input
++ (void)enumerateAttributes:(NSArray *)keys
+ inString:(NSAttributedString *)string
+ inRange:(NSRange)range
+ withBlock:(void(NS_NOESCAPE ^)(NSAttributedStringKey key,
+ id value, NSRange range,
+ BOOL *stop))block {
+ __block BOOL outerStop = NO;
+
+ for (NSAttributedStringKey key in keys) {
+
+ [string enumerateAttribute:key
+ inRange:range
+ options:0
+ usingBlock:^(id value, NSRange subRange, BOOL *innerStop) {
+ block(key, value, subRange, &outerStop);
+ if (outerStop) {
+ *innerStop = YES;
+ }
+ }];
+
+ if (outerStop)
+ break;
+ }
+}
+
++ (NSArray *)
+ collectAttributes:(NSArray *)keys
+ inString:(NSAttributedString *)string
+ inRange:(NSRange)range
+ withCondition:(BOOL(NS_NOESCAPE ^)(id value, NSRange range))condition {
+ NSMutableArray *result = [NSMutableArray array];
+
+ [self enumerateAttributes:keys
+ inString:string
+ inRange:range
+ withBlock:^(NSAttributedStringKey key, id value,
+ NSRange attrRange, BOOL *stop) {
+ if (condition(value, attrRange)) {
+ StylePair *pair = [StylePair new];
+ pair.rangeValue = [NSValue valueWithRange:attrRange];
+ pair.styleValue = value;
+ [result addObject:pair];
+ }
+ }];
+
+ return result;
+}
+
++ (BOOL)detect:(NSAttributedStringKey)key
+ inString:(NSAttributedString *)string
+ inRange:(NSRange)range
+ withCondition:(BOOL(NS_NOESCAPE ^)(id value, NSRange range))condition {
+ __block NSInteger total = 0;
+
+ [self enumerateAttributes:@[ key ]
+ inString:string
+ inRange:range
+ withBlock:^(NSAttributedStringKey key, id value, NSRange r,
+ BOOL *stop) {
+ if (condition(value, r)) {
+ total += r.length;
+ }
+ }];
+
+ return total == range.length;
+}
+
++ (BOOL)detectMultiple:(NSArray *)keys
+ inString:(NSAttributedString *)string
+ inRange:(NSRange)range
+ withCondition:(BOOL(NS_NOESCAPE ^)(id value, NSRange range))condition {
+ __block NSInteger total = 0;
+
+ [self enumerateAttributes:keys
+ inString:string
+ inRange:range
+ withBlock:^(NSAttributedStringKey key, id value, NSRange r,
+ BOOL *stop) {
+ if (condition(value, r)) {
+ total += r.length;
+ }
+ }];
+
+ return total == range.length;
+}
+
++ (BOOL)any:(NSAttributedStringKey)key
+ inString:(NSAttributedString *)string
+ inRange:(NSRange)range
+ withCondition:(BOOL(NS_NOESCAPE ^)(id value, NSRange range))condition {
+ __block BOOL found = NO;
+
+ [self enumerateAttributes:@[ key ]
+ inString:string
+ inRange:range
+ withBlock:^(NSAttributedStringKey key, id value, NSRange r,
+ BOOL *stop) {
+ if (condition(value, r)) {
+ found = YES;
+ *stop = YES;
+ }
+ }];
+
+ return found;
+}
+
++ (BOOL)anyMultiple:(NSArray *)keys
+ inString:(NSAttributedString *)string
+ inRange:(NSRange)range
+ withCondition:(BOOL(NS_NOESCAPE ^)(id value, NSRange range))condition {
+ __block BOOL found = NO;
+
+ [self enumerateAttributes:keys
+ inString:string
+ inRange:range
+ withBlock:^(NSAttributedStringKey key, id value, NSRange r,
+ BOOL *stop) {
+ if (condition(value, r)) {
+ found = YES;
+ *stop = YES;
+ }
+ }];
+
+ return found;
+}
+
++ (NSArray *)all:(NSAttributedStringKey)key
+ inString:(NSAttributedString *)string
+ inRange:(NSRange)range
+ withCondition:(BOOL(NS_NOESCAPE ^)(id value, NSRange range))
+ condition {
+ return [self collectAttributes:@[ key ]
+ inString:string
+ inRange:range
+ withCondition:condition];
+}
+
++ (NSArray *)allMultiple:(NSArray *)keys
+ inString:(NSAttributedString *)string
+ inRange:(NSRange)range
+ withCondition:
+ (BOOL(NS_NOESCAPE ^)(id value, NSRange range))
+ condition {
+ return [self collectAttributes:keys
+ inString:string
+ inRange:range
+ withCondition:condition];
+}
+
++ (BOOL)detect:(NSAttributedStringKey)key
+ withInput:(EnrichedTextInputView *)input
inRange:(NSRange)range
- withCondition:(BOOL(NS_NOESCAPE ^ _Nonnull)(id _Nullable value,
- NSRange range))condition {
- __block NSInteger totalLength = 0;
- [input->textView.textStorage
- enumerateAttribute:key
- inRange:range
- options:0
- usingBlock:^(id _Nullable value, NSRange range,
- BOOL *_Nonnull stop) {
- if (condition(value, range)) {
- totalLength += range.length;
- }
- }];
- return totalLength == range.length;
-}
-
-// checkPrevious flag is used for styles like lists or blockquotes
-// 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
+ withCondition:(BOOL(NS_NOESCAPE ^)(id value, NSRange range))condition {
+ return [self detect:key
+ inString:input->textView.textStorage
+ inRange:range
+ withCondition:condition];
+}
+
++ (BOOL)detect:(NSAttributedStringKey)key
+ withInput:(EnrichedTextInputView *)input
atIndex:(NSUInteger)index
checkPrevious:(BOOL)checkPrev
- withCondition:(BOOL(NS_NOESCAPE ^ _Nonnull)(id _Nullable value,
- NSRange range))condition {
+ withCondition:(BOOL(NS_NOESCAPE ^)(id 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 (checkPrev) {
- NSRange paragraphRange = [input->textView.textStorage.string
+ NSRange paragraph = [input->textView.textStorage.string
paragraphRangeForRange:detectionRange];
- if (paragraphRange.location == detectionRange.location) {
+ if (paragraph.location == detectionRange.location) {
return NO;
} else {
return [self detect:key
withInput:input
- inRange:NSMakeRange(paragraphRange.location, 1)
+ inRange:NSMakeRange(paragraph.location, 1)
withCondition:condition];
}
} else {
return NO;
}
+
} else {
- NSRange attrRange = NSMakeRange(0, 0);
+ NSRange eff;
attrValue = [input->textView.textStorage attribute:key
atIndex:index
- effectiveRange:&attrRange];
+ effectiveRange:&eff];
}
+
return condition(attrValue, detectionRange);
}
-+ (BOOL)detectMultiple:(NSArray *_Nonnull)keys
- withInput:(EnrichedTextInputView *_Nonnull)input
++ (BOOL)detectMultiple:(NSArray *)keys
+ withInput:(EnrichedTextInputView *)input
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
- enumerateAttribute:key
- inRange:range
- options:0
- usingBlock:^(id _Nullable value, NSRange range,
- BOOL *_Nonnull stop) {
- if (condition(value, range)) {
- totalLength += range.length;
- }
- }];
- }
- return totalLength == range.length;
+ withCondition:(BOOL(NS_NOESCAPE ^)(id value, NSRange range))condition {
+ return [self detectMultiple:keys
+ inString:input->textView.textStorage
+ inRange:range
+ withCondition:condition];
}
-+ (BOOL)any:(NSAttributedStringKey _Nonnull)key
- withInput:(EnrichedTextInputView *_Nonnull)input
++ (BOOL)any:(NSAttributedStringKey)key
+ withInput:(EnrichedTextInputView *)input
inRange:(NSRange)range
- withCondition:(BOOL(NS_NOESCAPE ^ _Nonnull)(id _Nullable value,
- NSRange range))condition {
- __block BOOL found = NO;
- [input->textView.textStorage
- enumerateAttribute:key
- inRange:range
- options:0
- usingBlock:^(id _Nullable value, NSRange range,
- BOOL *_Nonnull stop) {
- if (condition(value, range)) {
- found = YES;
- *stop = YES;
- }
- }];
- return found;
+ withCondition:(BOOL(NS_NOESCAPE ^)(id value, NSRange range))condition {
+ return [self any:key
+ inString:input->textView.textStorage
+ inRange:range
+ withCondition:condition];
}
-+ (BOOL)anyMultiple:(NSArray *_Nonnull)keys
- withInput:(EnrichedTextInputView *_Nonnull)input
++ (BOOL)anyMultiple:(NSArray *)keys
+ withInput:(EnrichedTextInputView *)input
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
- enumerateAttribute:key
+ withCondition:(BOOL(NS_NOESCAPE ^)(id value, NSRange range))condition {
+ return [self anyMultiple:keys
+ inString:input->textView.textStorage
inRange:range
- options:0
- usingBlock:^(id _Nullable value, NSRange range,
- BOOL *_Nonnull stop) {
- if (condition(value, range)) {
- found = YES;
- *stop = YES;
- }
- }];
- if (found) {
- return YES;
- }
- }
- return NO;
-}
-
-+ (NSArray *_Nullable)all:(NSAttributedStringKey _Nonnull)key
- withInput:(EnrichedTextInputView *_Nonnull)input
- inRange:(NSRange)range
- withCondition:
- (BOOL(NS_NOESCAPE ^ _Nonnull)(id _Nullable value,
- NSRange range))
- condition {
- __block NSMutableArray *occurences =
- [[NSMutableArray alloc] init];
- [input->textView.textStorage
- enumerateAttribute:key
- inRange:range
- options:0
- usingBlock:^(id _Nullable value, NSRange range,
- BOOL *_Nonnull stop) {
- if (condition(value, range)) {
- StylePair *pair = [[StylePair alloc] init];
- pair.rangeValue = [NSValue valueWithRange:range];
- pair.styleValue = value;
- [occurences addObject:pair];
- }
- }];
- return occurences;
-}
-
-+ (NSArray *_Nullable)
- allMultiple:(NSArray *_Nonnull)keys
- withInput:(EnrichedTextInputView *_Nonnull)input
- 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
- enumerateAttribute:key
+ withCondition:condition];
+}
+
++ (NSArray *)all:(NSAttributedStringKey)key
+ withInput:(EnrichedTextInputView *)input
+ inRange:(NSRange)range
+ withCondition:(BOOL(NS_NOESCAPE ^)(id value, NSRange range))
+ condition {
+ return [self all:key
+ inString:input->textView.textStorage
+ inRange:range
+ withCondition:condition];
+}
+
++ (NSArray *)allMultiple:(NSArray *)keys
+ withInput:(EnrichedTextInputView *)input
+ inRange:(NSRange)range
+ withCondition:
+ (BOOL(NS_NOESCAPE ^)(id value, NSRange range))
+ condition {
+ return [self allMultiple:keys
+ inString:input->textView.textStorage
inRange:range
- options:0
- usingBlock:^(id _Nullable value, NSRange range,
- BOOL *_Nonnull stop) {
- if (condition(value, range)) {
- StylePair *pair = [[StylePair alloc] init];
- pair.rangeValue = [NSValue valueWithRange:range];
- pair.styleValue = value;
- [occurences addObject:pair];
- }
- }];
- }
- return occurences;
+ withCondition:condition];
}
-+ (NSArray *_Nonnull)getRangesWithout:(NSArray *_Nonnull)types
- withInput:(EnrichedTextInputView *_Nonnull)input
- inRange:(NSRange)range {
- NSMutableArray *activeStyleObjects = [[NSMutableArray alloc] init];
++ (NSArray *)getRangesWithout:(NSArray *)types
+ withInput:(EnrichedTextInputView *)input
+ inRange:(NSRange)range {
+ NSMutableArray *activeStyles = [NSMutableArray array];
+
for (NSNumber *type in types) {
- id styleClass = input->stylesDict[type];
- [activeStyleObjects addObject:styleClass];
+ id style = input->stylesDict[type];
+ [activeStyles addObject:style];
}
- if (activeStyleObjects.count == 0) {
+ if (activeStyles.count == 0) {
return @[ [NSValue valueWithRange:range] ];
}
- NSMutableArray *newRanges = [[NSMutableArray alloc] init];
- NSUInteger lastRangeLocation = range.location;
- NSUInteger endLocation = range.location + range.length;
+ NSMutableArray *newRanges = [NSMutableArray array];
+ NSUInteger lastLocation = range.location;
+ NSUInteger end = range.location + range.length;
- for (NSUInteger i = range.location; i < endLocation; i++) {
- NSRange currentRange = NSMakeRange(i, 1);
- BOOL forbiddenStyleFound = NO;
+ for (NSUInteger i = range.location; i < end; i++) {
- for (id style in activeStyleObjects) {
- if ([style detectStyle:currentRange]) {
- forbiddenStyleFound = YES;
+ BOOL forbidden = NO;
+ for (id style in activeStyles) {
+ if ([style detectStyle:NSMakeRange(i, 1)]) {
+ forbidden = YES;
break;
}
}
- if (forbiddenStyleFound) {
- if (i > lastRangeLocation) {
- NSRange cleanRange =
- NSMakeRange(lastRangeLocation, i - lastRangeLocation);
- [newRanges addObject:[NSValue valueWithRange:cleanRange]];
+ if (forbidden) {
+ if (i > lastLocation) {
+ [newRanges
+ addObject:[NSValue valueWithRange:NSMakeRange(lastLocation,
+ i - lastLocation)]];
}
- lastRangeLocation = i + 1;
+ lastLocation = i + 1;
}
}
- if (lastRangeLocation < endLocation) {
- NSRange remainingRange =
- NSMakeRange(lastRangeLocation, endLocation - lastRangeLocation);
- [newRanges addObject:[NSValue valueWithRange:remainingRange]];
+ if (lastLocation < end) {
+ [newRanges
+ addObject:[NSValue valueWithRange:NSMakeRange(lastLocation,
+ end - lastLocation)]];
}
return newRanges;
diff --git a/ios/utils/ParagraphAttributesUtils.mm b/ios/utils/ParagraphAttributesUtils.mm
index ee8f9994e..9ac7e735a 100644
--- a/ios/utils/ParagraphAttributesUtils.mm
+++ b/ios/utils/ParagraphAttributesUtils.mm
@@ -67,7 +67,7 @@ + (BOOL)handleBackspaceInRange:(NSRange)range
withSelection:YES];
typedInput->textView.typingAttributes =
typedInput->defaultTypingAttributes;
- [style addAttributes:NSMakeRange(range.location, 0) withTypingAttr:YES];
+ [style addAttributes:NSMakeRange(range.location, 0)];
return YES;
}
}
diff --git a/ios/utils/ParagraphsUtils.h b/ios/utils/ParagraphsUtils.h
index a6a1cfabf..0bd14c81f 100644
--- a/ios/utils/ParagraphsUtils.h
+++ b/ios/utils/ParagraphsUtils.h
@@ -5,4 +5,10 @@
+ (NSArray *)getSeparateParagraphsRangesIn:(UITextView *)textView
range:(NSRange)range;
+ (NSArray *)getNonNewlineRangesIn:(UITextView *)textView range:(NSRange)range;
++ (NSArray *)getSeparateParagraphsRangesInAttributedString:
+ (NSAttributedString *)attributedString
+ range:(NSRange)range;
++ (NSArray *)getNonNewlineRangesInAttributedString:
+ (NSAttributedString *)attributedString
+ range:(NSRange)range;
@end
diff --git a/ios/utils/ParagraphsUtils.mm b/ios/utils/ParagraphsUtils.mm
index aa3c116a4..277ef9df7 100644
--- a/ios/utils/ParagraphsUtils.mm
+++ b/ios/utils/ParagraphsUtils.mm
@@ -65,4 +65,69 @@ + (NSArray *)getNonNewlineRangesIn:(UITextView *)textView range:(NSRange)range {
return nonNewlineRanges;
}
++ (NSArray *)getSeparateParagraphsRangesInAttributedString:
+ (NSAttributedString *)attributedString
+ range:(NSRange)range {
+ // just in case, get full paragraphs range
+ NSRange fullRange = [attributedString.string paragraphRangeForRange:range];
+
+ // we are in an empty paragraph
+ if (fullRange.length == 0) {
+ return @[ [NSValue valueWithRange:fullRange] ];
+ }
+
+ NSMutableArray *results = [[NSMutableArray alloc] init];
+
+ NSInteger lastStart = fullRange.location;
+ for (int i = fullRange.location; i < fullRange.location + fullRange.length;
+ i++) {
+ unichar currentChar = [attributedString.string characterAtIndex:i];
+ if ([[NSCharacterSet newlineCharacterSet] characterIsMember:currentChar]) {
+ NSRange paragraphRange = [attributedString.string
+ paragraphRangeForRange:NSMakeRange(lastStart, i - lastStart)];
+ [results addObject:[NSValue valueWithRange:paragraphRange]];
+ lastStart = i + 1;
+ }
+ }
+
+ if (lastStart < fullRange.location + fullRange.length) {
+ NSRange paragraphRange = [attributedString.string
+ paragraphRangeForRange:NSMakeRange(lastStart, fullRange.location +
+ fullRange.length -
+ lastStart)];
+ [results addObject:[NSValue valueWithRange:paragraphRange]];
+ }
+
+ return results;
+}
+
++ (NSArray *)getNonNewlineRangesInAttributedString:
+ (NSAttributedString *)attributedString
+ range:(NSRange)range {
+ NSMutableArray *nonNewlineRanges = [[NSMutableArray alloc] init];
+ int lastRangeLocation = range.location;
+
+ for (int i = range.location; i < range.location + range.length; i++) {
+ unichar currentChar = [attributedString.string characterAtIndex:i];
+ if ([[NSCharacterSet newlineCharacterSet] characterIsMember:currentChar]) {
+ if (i - lastRangeLocation > 0) {
+ [nonNewlineRanges
+ addObject:[NSValue
+ valueWithRange:NSMakeRange(lastRangeLocation,
+ i - lastRangeLocation)]];
+ }
+ lastRangeLocation = i + 1;
+ }
+ }
+ if (lastRangeLocation < range.location + range.length) {
+ [nonNewlineRanges
+ addObject:[NSValue
+ valueWithRange:NSMakeRange(lastRangeLocation,
+ range.location + range.length -
+ lastRangeLocation)]];
+ }
+
+ return nonNewlineRanges;
+}
+
@end
diff --git a/ios/utils/StyleHeaders.h b/ios/utils/StyleHeaders.h
index 4c57321ab..baad6e024 100644
--- a/ios/utils/StyleHeaders.h
+++ b/ios/utils/StyleHeaders.h
@@ -32,6 +32,11 @@
- (void)handleManualLinks:(NSString *)word inRange:(NSRange)wordRange;
- (BOOL)handleLeadingLinkReplacement:(NSRange)range
replacementText:(NSString *)text;
+- (void)addLinkInAttributedString:(NSMutableAttributedString *)attr
+ range:(NSRange)range
+ text:(NSString *)text
+ url:(NSString *)url
+ manual:(BOOL)manual;
@end
@interface MentionStyle : NSObject
@@ -48,6 +53,9 @@
- (MentionParams *)getMentionParamsAt:(NSUInteger)location;
- (NSRange)getFullMentionRangeAt:(NSUInteger)location;
- (NSValue *)getActiveMentionRange;
+- (void)addMentionInAttributedString:(NSMutableAttributedString *)string
+ range:(NSRange)range
+ params:(MentionParams *)params;
@end
@interface HeadingStyleBase : NSObject {
@@ -96,4 +104,7 @@
imageData:(ImageData *)imageData
withSelection:(BOOL)withSelection;
- (ImageData *)getImageDataAt:(NSUInteger)location;
+- (void)addImageInAttributedString:(NSMutableAttributedString *)attributedString
+ range:(NSRange)range
+ imageData:(ImageData *)imageData;
@end
diff --git a/ios/utils/TextInsertionUtils.h b/ios/utils/TextInsertionUtils.h
index 1012f3837..1f7366747 100644
--- a/ios/utils/TextInsertionUtils.h
+++ b/ios/utils/TextInsertionUtils.h
@@ -14,4 +14,10 @@
input:(id)input
withSelection:(BOOL)withSelection;
;
++ (void)insertTextInAttributedString:(NSString *)text
+ at:(NSInteger)index
+ additionalAttributes:
+ (NSDictionary *)additionalAttrs
+ attributedString:
+ (NSMutableAttributedString *)attributedString;
@end
diff --git a/ios/utils/TextInsertionUtils.mm b/ios/utils/TextInsertionUtils.mm
index 6afa0b876..4c67ef638 100644
--- a/ios/utils/TextInsertionUtils.mm
+++ b/ios/utils/TextInsertionUtils.mm
@@ -63,4 +63,28 @@ + (void)replaceText:(NSString *)text
}
typedInput->recentlyChangedRange = NSMakeRange(range.location, text.length);
}
+
++ (void)insertTextInAttributedString:(NSString *)text
+ at:(NSInteger)index
+ additionalAttributes:
+ (NSDictionary *)additionalAttrs
+ attributedString:
+ (NSMutableAttributedString *)attributedString {
+ NSDictionary *baseAttributes = @{};
+ if (attributedString.length > 0 && index < attributedString.length) {
+ baseAttributes = [attributedString attributesAtIndex:index
+ effectiveRange:nil];
+ }
+
+ NSMutableDictionary *attrs = [baseAttributes mutableCopy];
+ if (additionalAttrs) {
+ [attrs addEntriesFromDictionary:additionalAttrs];
+ }
+
+ NSAttributedString *newAttrString =
+ [[NSAttributedString alloc] initWithString:text attributes:attrs];
+
+ [attributedString insertAttributedString:newAttrString atIndex:index];
+}
+
@end