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