diff --git a/apps/example/ios/Podfile.lock b/apps/example/ios/Podfile.lock index 2abfd0c6b..0caf0c630 100644 --- a/apps/example/ios/Podfile.lock +++ b/apps/example/ios/Podfile.lock @@ -2763,7 +2763,7 @@ SPEC CHECKSUMS: FBLazyVector: a293a88992c4c33f0aee184acab0b64a08ff9458 fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd glog: 5683914934d5b6e4240e497e0f4a3b42d1854183 - hermes-engine: 0552e8a1c46c773b6888e9fe198c2272cc097193 + hermes-engine: b45e3b4b9d7f8227a6c11c8342f81742829b8af8 RCT-Folly: 59ec0ac1f2f39672a0c6e6cecdd39383b764646f RCTDeprecation: 2b70c6e3abe00396cefd8913efbf6a2db01a2b36 RCTRequired: f3540eee8094231581d40c5c6d41b0f170237a81 diff --git a/ios/EnrichedTextInputView.h b/ios/EnrichedTextInputView.h index 346f1d36c..4beb8ff94 100644 --- a/ios/EnrichedTextInputView.h +++ b/ios/EnrichedTextInputView.h @@ -1,4 +1,5 @@ #pragma once +#import "AttributesManager.h" #import "BaseStyleProtocol.h" #import "InputConfig.h" #import "InputParser.h" @@ -22,14 +23,18 @@ NS_ASSUME_NONNULL_BEGIN InputConfig *config; @public InputParser *parser; +@public + AttributesManager *attributesManager; @public NSMutableDictionary *defaultTypingAttributes; @public - NSDictionary> *stylesDict; + NSDictionary *stylesDict; NSDictionary *> *conflictingStyles; NSMutableDictionary *> *blockingStyles; @public BOOL blockEmitting; +@public + NSValue *dotReplacementRange; } - (void)emitOnLinkDetectedEvent:(NSString *)text url:(NSString *)url diff --git a/ios/EnrichedTextInputView.mm b/ios/EnrichedTextInputView.mm index 6601ce1da..682e26804 100644 --- a/ios/EnrichedTextInputView.mm +++ b/ios/EnrichedTextInputView.mm @@ -1,5 +1,6 @@ #import "EnrichedTextInputView.h" #import "CoreText/CoreText.h" +#import "DotReplacementUtils.h" #import "LayoutManagerExtension.h" #import "ParagraphAttributesUtils.h" #import "RCTFabricComponentsPlugins.h" @@ -26,9 +27,9 @@ using namespace facebook::react; -@interface EnrichedTextInputView () +@interface EnrichedTextInputView () < + RCTEnrichedTextInputViewViewProtocol, UITextViewDelegate, + UIGestureRecognizerDelegate, NSTextStorageDelegate, NSObject> @end @@ -92,165 +93,164 @@ - (void)setDefaults { blockEmitting = NO; _emitFocusBlur = YES; _emitTextChange = NO; + dotReplacementRange = nullptr; defaultTypingAttributes = [[NSMutableDictionary alloc] init]; stylesDict = @{ - @([BoldStyle getStyleType]) : [[BoldStyle alloc] initWithInput:self], - @([ItalicStyle getStyleType]) : [[ItalicStyle alloc] initWithInput:self], - @([UnderlineStyle getStyleType]) : - [[UnderlineStyle alloc] initWithInput:self], - @([StrikethroughStyle getStyleType]) : + @([BoldStyle getType]) : [[BoldStyle alloc] initWithInput:self], + @([ItalicStyle getType]) : [[ItalicStyle alloc] initWithInput:self], + @([UnderlineStyle getType]) : [[UnderlineStyle alloc] initWithInput:self], + @([StrikethroughStyle getType]) : [[StrikethroughStyle alloc] initWithInput:self], - @([InlineCodeStyle getStyleType]) : - [[InlineCodeStyle alloc] initWithInput:self], - @([LinkStyle getStyleType]) : [[LinkStyle alloc] initWithInput:self], - @([MentionStyle getStyleType]) : [[MentionStyle alloc] initWithInput:self], - @([H1Style getStyleType]) : [[H1Style alloc] initWithInput:self], - @([H2Style getStyleType]) : [[H2Style alloc] initWithInput:self], - @([H3Style getStyleType]) : [[H3Style alloc] initWithInput:self], - @([H4Style getStyleType]) : [[H4Style alloc] initWithInput:self], - @([H5Style getStyleType]) : [[H5Style alloc] initWithInput:self], - @([H6Style getStyleType]) : [[H6Style alloc] initWithInput:self], - @([UnorderedListStyle getStyleType]) : + @([InlineCodeStyle getType]) : [[InlineCodeStyle alloc] initWithInput:self], + // @([LinkStyle getStyleType]) : [[LinkStyle alloc] initWithInput:self], + // @([MentionStyle getStyleType]) : [[MentionStyle alloc] + // initWithInput:self], + @([H1Style getType]) : [[H1Style alloc] initWithInput:self], + @([H2Style getType]) : [[H2Style alloc] initWithInput:self], + @([H3Style getType]) : [[H3Style alloc] initWithInput:self], + @([H4Style getType]) : [[H4Style alloc] initWithInput:self], + @([H5Style getType]) : [[H5Style alloc] initWithInput:self], + @([H6Style getType]) : [[H6Style alloc] initWithInput:self], + @([UnorderedListStyle getType]) : [[UnorderedListStyle alloc] initWithInput:self], - @([OrderedListStyle getStyleType]) : + @([OrderedListStyle getType]) : [[OrderedListStyle alloc] initWithInput:self], - @([CheckboxListStyle getStyleType]) : + @([CheckboxListStyle getType]) : [[CheckboxListStyle alloc] initWithInput:self], - @([BlockQuoteStyle getStyleType]) : - [[BlockQuoteStyle alloc] initWithInput:self], - @([CodeBlockStyle getStyleType]) : - [[CodeBlockStyle alloc] initWithInput:self], - @([ImageStyle getStyleType]) : [[ImageStyle alloc] initWithInput:self] + @([BlockQuoteStyle getType]) : [[BlockQuoteStyle alloc] initWithInput:self], + @([CodeBlockStyle getType]) : [[CodeBlockStyle alloc] initWithInput:self], + // @([ImageStyle getStyleType]) : [[ImageStyle alloc] initWithInput:self] }; conflictingStyles = @{ - @([BoldStyle getStyleType]) : @[], - @([ItalicStyle getStyleType]) : @[], - @([UnderlineStyle getStyleType]) : @[], - @([StrikethroughStyle getStyleType]) : @[], - @([InlineCodeStyle getStyleType]) : - @[ @([LinkStyle getStyleType]), @([MentionStyle getStyleType]) ], - @([LinkStyle getStyleType]) : @[ - @([InlineCodeStyle getStyleType]), @([LinkStyle getStyleType]), - @([MentionStyle getStyleType]) + @([BoldStyle getType]) : @[], + @([ItalicStyle getType]) : @[], + @([UnderlineStyle getType]) : @[], + @([StrikethroughStyle getType]) : @[], + @([InlineCodeStyle getType]) : @[ + // @([LinkStyle getStyleType]), @([MentionStyle getStyleType]) ], - @([MentionStyle getStyleType]) : - @[ @([InlineCodeStyle getStyleType]), @([LinkStyle getStyleType]) ], - @([H1Style getStyleType]) : @[ - @([H2Style getStyleType]), @([H3Style getStyleType]), - @([H4Style getStyleType]), @([H5Style getStyleType]), - @([H6Style getStyleType]), @([UnorderedListStyle getStyleType]), - @([OrderedListStyle getStyleType]), @([BlockQuoteStyle getStyleType]), - @([CodeBlockStyle getStyleType]), @([CheckboxListStyle getStyleType]) + // @([LinkStyle getStyleType]) : @[ + // @([InlineCodeStyle getStyleType]), @([LinkStyle getStyleType]), + // @([MentionStyle getStyleType]) + // ], + // @([MentionStyle getStyleType]) : + // @[ @([InlineCodeStyle getStyleType]), @([LinkStyle getStyleType]) + // ], + @([H1Style getType]) : @[ + @([H2Style getType]), @([H3Style getType]), @([H4Style getType]), + @([H5Style getType]), @([H6Style getType]), + @([UnorderedListStyle getType]), @([OrderedListStyle getType]), + @([BlockQuoteStyle getType]), @([CodeBlockStyle getType]), + @([CheckboxListStyle getType]) ], - @([H2Style getStyleType]) : @[ - @([H1Style getStyleType]), @([H3Style getStyleType]), - @([H4Style getStyleType]), @([H5Style getStyleType]), - @([H6Style getStyleType]), @([UnorderedListStyle getStyleType]), - @([OrderedListStyle getStyleType]), @([BlockQuoteStyle getStyleType]), - @([CodeBlockStyle getStyleType]), @([CheckboxListStyle getStyleType]) + @([H2Style getType]) : @[ + @([H1Style getType]), @([H3Style getType]), @([H4Style getType]), + @([H5Style getType]), @([H6Style getType]), + @([UnorderedListStyle getType]), @([OrderedListStyle getType]), + @([BlockQuoteStyle getType]), @([CodeBlockStyle getType]), + @([CheckboxListStyle getType]) ], - @([H3Style getStyleType]) : @[ - @([H1Style getStyleType]), @([H2Style getStyleType]), - @([H4Style getStyleType]), @([H5Style getStyleType]), - @([H6Style getStyleType]), @([UnorderedListStyle getStyleType]), - @([OrderedListStyle getStyleType]), @([BlockQuoteStyle getStyleType]), - @([CodeBlockStyle getStyleType]), @([CheckboxListStyle getStyleType]) + @([H3Style getType]) : @[ + @([H1Style getType]), @([H2Style getType]), @([H4Style getType]), + @([H5Style getType]), @([H6Style getType]), + @([UnorderedListStyle getType]), @([OrderedListStyle getType]), + @([BlockQuoteStyle getType]), @([CodeBlockStyle getType]), + @([CheckboxListStyle getType]) ], - @([H4Style getStyleType]) : @[ - @([H1Style getStyleType]), @([H2Style getStyleType]), - @([H3Style getStyleType]), @([H5Style getStyleType]), - @([H6Style getStyleType]), @([UnorderedListStyle getStyleType]), - @([OrderedListStyle getStyleType]), @([BlockQuoteStyle getStyleType]), - @([CodeBlockStyle getStyleType]), @([CheckboxListStyle getStyleType]) + @([H4Style getType]) : @[ + @([H1Style getType]), @([H2Style getType]), @([H3Style getType]), + @([H5Style getType]), @([H6Style getType]), + @([UnorderedListStyle getType]), @([OrderedListStyle getType]), + @([BlockQuoteStyle getType]), @([CodeBlockStyle getType]), + @([CheckboxListStyle getType]) ], - @([H5Style getStyleType]) : @[ - @([H1Style getStyleType]), @([H2Style getStyleType]), - @([H3Style getStyleType]), @([H4Style getStyleType]), - @([H6Style getStyleType]), @([UnorderedListStyle getStyleType]), - @([OrderedListStyle getStyleType]), @([BlockQuoteStyle getStyleType]), - @([CodeBlockStyle getStyleType]), @([CheckboxListStyle getStyleType]) + @([H5Style getType]) : @[ + @([H1Style getType]), @([H2Style getType]), @([H3Style getType]), + @([H4Style getType]), @([H6Style getType]), + @([UnorderedListStyle getType]), @([OrderedListStyle getType]), + @([BlockQuoteStyle getType]), @([CodeBlockStyle getType]), + @([CheckboxListStyle getType]) ], - @([H6Style getStyleType]) : @[ - @([H1Style getStyleType]), @([H2Style getStyleType]), - @([H3Style getStyleType]), @([H4Style getStyleType]), - @([H5Style getStyleType]), @([UnorderedListStyle getStyleType]), - @([OrderedListStyle getStyleType]), @([BlockQuoteStyle getStyleType]), - @([CodeBlockStyle getStyleType]), @([CheckboxListStyle getStyleType]) + @([H6Style getType]) : @[ + @([H1Style getType]), @([H2Style getType]), @([H3Style getType]), + @([H4Style getType]), @([H5Style getType]), + @([UnorderedListStyle getType]), @([OrderedListStyle getType]), + @([BlockQuoteStyle getType]), @([CodeBlockStyle getType]), + @([CheckboxListStyle getType]) ], - @([UnorderedListStyle getStyleType]) : @[ - @([H1Style getStyleType]), @([H2Style getStyleType]), - @([H3Style getStyleType]), @([H4Style getStyleType]), - @([H5Style getStyleType]), @([H6Style getStyleType]), - @([OrderedListStyle getStyleType]), @([BlockQuoteStyle getStyleType]), - @([CodeBlockStyle getStyleType]), @([CheckboxListStyle getStyleType]) + @([UnorderedListStyle getType]) : @[ + @([H1Style getType]), @([H2Style getType]), @([H3Style getType]), + @([H4Style getType]), @([H5Style getType]), @([H6Style getType]), + @([OrderedListStyle getType]), @([BlockQuoteStyle getType]), + @([CodeBlockStyle getType]), @([CheckboxListStyle getType]) ], - @([OrderedListStyle getStyleType]) : @[ - @([H1Style getStyleType]), @([H2Style getStyleType]), - @([H3Style getStyleType]), @([H4Style getStyleType]), - @([H5Style getStyleType]), @([H6Style getStyleType]), - @([UnorderedListStyle getStyleType]), @([BlockQuoteStyle getStyleType]), - @([CodeBlockStyle getStyleType]), @([CheckboxListStyle getStyleType]) + @([OrderedListStyle getType]) : @[ + @([H1Style getType]), @([H2Style getType]), @([H3Style getType]), + @([H4Style getType]), @([H5Style getType]), @([H6Style getType]), + @([UnorderedListStyle getType]), @([BlockQuoteStyle getType]), + @([CodeBlockStyle getType]), @([CheckboxListStyle getType]) ], - @([CheckboxListStyle getStyleType]) : @[ - @([H1Style getStyleType]), @([H2Style getStyleType]), - @([H3Style getStyleType]), @([H4Style getStyleType]), - @([H5Style getStyleType]), @([H6Style getStyleType]), - @([UnorderedListStyle getStyleType]), @([OrderedListStyle getStyleType]), - @([BlockQuoteStyle getStyleType]), @([CodeBlockStyle getStyleType]) + @([CheckboxListStyle getType]) : @[ + @([H1Style getType]), @([H2Style getType]), @([H3Style getType]), + @([H4Style getType]), @([H5Style getType]), @([H6Style getType]), + @([UnorderedListStyle getType]), @([OrderedListStyle getType]), + @([BlockQuoteStyle getType]), @([CodeBlockStyle getType]) ], - @([BlockQuoteStyle getStyleType]) : @[ - @([H1Style getStyleType]), @([H2Style getStyleType]), - @([H3Style getStyleType]), @([H4Style getStyleType]), - @([H5Style getStyleType]), @([H6Style getStyleType]), - @([UnorderedListStyle getStyleType]), @([OrderedListStyle getStyleType]), - @([CodeBlockStyle getStyleType]), @([CheckboxListStyle getStyleType]) + @([BlockQuoteStyle getType]) : @[ + @([H1Style getType]), @([H2Style getType]), @([H3Style getType]), + @([H4Style getType]), @([H5Style getType]), @([H6Style getType]), + @([UnorderedListStyle getType]), @([OrderedListStyle getType]), + @([CodeBlockStyle getType]), @([CheckboxListStyle getType]) ], - @([CodeBlockStyle getStyleType]) : @[ - @([H1Style getStyleType]), @([H2Style getStyleType]), - @([H3Style getStyleType]), @([H4Style getStyleType]), - @([H5Style getStyleType]), @([H6Style getStyleType]), - @([BoldStyle getStyleType]), @([ItalicStyle getStyleType]), - @([UnderlineStyle getStyleType]), @([StrikethroughStyle getStyleType]), - @([UnorderedListStyle getStyleType]), @([OrderedListStyle getStyleType]), - @([BlockQuoteStyle getStyleType]), @([InlineCodeStyle getStyleType]), - @([MentionStyle getStyleType]), @([LinkStyle getStyleType]), - @([CheckboxListStyle getStyleType]) + @([CodeBlockStyle getType]) : @[ + @([H1Style getType]), @([H2Style getType]), @([H3Style getType]), + @([H4Style getType]), @([H5Style getType]), @([H6Style getType]), + @([BoldStyle getType]), @([UnderlineStyle getType]), + @([ItalicStyle getType]), @([StrikethroughStyle getType]), + @([UnorderedListStyle getType]), @([OrderedListStyle getType]), + @([BlockQuoteStyle getType]), @([InlineCodeStyle getType]), + // @([MentionStyle getStyleType]), @([LinkStyle getStyleType]), + @([CheckboxListStyle getType]) ], - @([ImageStyle getStyleType]) : - @[ @([LinkStyle getStyleType]), @([MentionStyle getStyleType]) ] + // @([ImageStyle getStyleType]) : + // @[ @([LinkStyle getStyleType]), @([MentionStyle getStyleType]) ] }; blockingStyles = [@{ - @([BoldStyle getStyleType]) : @[ @([CodeBlockStyle getStyleType]) ], - @([ItalicStyle getStyleType]) : @[ @([CodeBlockStyle getStyleType]) ], - @([UnderlineStyle getStyleType]) : @[ @([CodeBlockStyle getStyleType]) ], - @([StrikethroughStyle getStyleType]) : - @[ @([CodeBlockStyle getStyleType]) ], - @([InlineCodeStyle getStyleType]) : - @[ @([CodeBlockStyle getStyleType]), @([ImageStyle getStyleType]) ], - @([LinkStyle getStyleType]) : - @[ @([CodeBlockStyle getStyleType]), @([ImageStyle getStyleType]) ], - @([MentionStyle getStyleType]) : - @[ @([CodeBlockStyle getStyleType]), @([ImageStyle getStyleType]) ], - @([H1Style getStyleType]) : @[], - @([H2Style getStyleType]) : @[], - @([H3Style getStyleType]) : @[], - @([H4Style getStyleType]) : @[], - @([H5Style getStyleType]) : @[], - @([H6Style getStyleType]) : @[], - @([UnorderedListStyle getStyleType]) : @[], - @([OrderedListStyle getStyleType]) : @[], - @([CheckboxListStyle getStyleType]) : @[], - @([BlockQuoteStyle getStyleType]) : @[], - @([CodeBlockStyle getStyleType]) : @[], - @([ImageStyle getStyleType]) : @[ @([InlineCodeStyle getStyleType]) ] + @([BoldStyle getType]) : @[ @([CodeBlockStyle getType]) ], + @([ItalicStyle getType]) : @[ @([CodeBlockStyle getType]) ], + @([UnderlineStyle getType]) : @[ @([CodeBlockStyle getType]) ], + @([StrikethroughStyle getType]) : @[ @([CodeBlockStyle getType]) ], + @([InlineCodeStyle getType]) : @[ + @([CodeBlockStyle getType]), + // @([ImageStyle getStyleType]) + ], + // @([LinkStyle getStyleType]) : + // @[ @([CodeBlockStyle getStyleType]), @([ImageStyle getStyleType]) + // ], + // @([MentionStyle getStyleType]) : + // @[ @([CodeBlockStyle getStyleType]), @([ImageStyle getStyleType]) + // ], + @([H1Style getType]) : @[], + @([H2Style getType]) : @[], + @([H3Style getType]) : @[], + @([H4Style getType]) : @[], + @([H5Style getType]) : @[], + @([H6Style getType]) : @[], + @([UnorderedListStyle getType]) : @[], + @([OrderedListStyle getType]) : @[], + @([CheckboxListStyle getType]) : @[], + @([BlockQuoteStyle getType]) : @[], + @([CodeBlockStyle getType]) : @[], + // @([ImageStyle getStyleType]) : @[ @([InlineCodeStyle getStyleType]) ] } mutableCopy]; parser = [[InputParser alloc] initWithInput:self]; + attributesManager = [[AttributesManager alloc] initWithInput:self]; } - (void)setupTextView { @@ -261,6 +261,8 @@ - (void)setupTextView { textView.delegate = self; textView.input = self; textView.layoutManager.input = self; + textView.textStorage.delegate = self; + textView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; textView.adjustsFontForContentSizeCategory = YES; @@ -901,19 +903,19 @@ - (void)tryUpdatingActiveStyles { // separately NSMutableSet *newBlockedStyles = [_blockedStyles mutableCopy]; - // data for onLinkDetected event - LinkData *detectedLinkData; - NSRange detectedLinkRange = NSMakeRange(0, 0); - - // data for onMentionDetected event - MentionParams *detectedMentionParams; - NSRange detectedMentionRange = NSMakeRange(0, 0); + // // data for onLinkDetected event + // LinkData *detectedLinkData; + // NSRange detectedLinkRange = NSMakeRange(0, 0); + // + // // data for onMentionDetected event + // MentionParams *detectedMentionParams; + // NSRange detectedMentionRange = NSMakeRange(0, 0); for (NSNumber *type in stylesDict) { - id style = stylesDict[type]; + StyleBase *style = stylesDict[type]; BOOL wasActive = [newActiveStyles containsObject:type]; - BOOL isActive = [style detectStyle:textView.selectedRange]; + BOOL isActive = [style detect:textView.selectedRange]; BOOL wasBlocked = [newBlockedStyles containsObject:type]; BOOL isBlocked = [self isStyle:(StyleType)[type integerValue] @@ -938,65 +940,69 @@ - (void)tryUpdatingActiveStyles { } } - // onLinkDetected event - if (isActive && [type intValue] == [LinkStyle getStyleType]) { - // get the link data - LinkData *candidateLinkData; - NSRange candidateLinkRange = NSMakeRange(0, 0); - LinkStyle *linkStyleClass = - (LinkStyle *)stylesDict[@([LinkStyle getStyleType])]; - if (linkStyleClass != nullptr) { - candidateLinkData = - [linkStyleClass getLinkDataAt:textView.selectedRange.location]; - candidateLinkRange = - [linkStyleClass getFullLinkRangeAt:textView.selectedRange.location]; - } - - if (wasActive == NO) { - // we changed selection from non-link to a link - detectedLinkData = candidateLinkData; - detectedLinkRange = candidateLinkRange; - } else if (![_recentlyActiveLinkData.url - isEqualToString:candidateLinkData.url] || - ![_recentlyActiveLinkData.text - isEqualToString:candidateLinkData.text] || - !NSEqualRanges(_recentlyActiveLinkRange, candidateLinkRange)) { - // we changed selection from one link to the other or modified current - // link's text - detectedLinkData = candidateLinkData; - detectedLinkRange = candidateLinkRange; - } - } - - // onMentionDetected event - if (isActive && [type intValue] == [MentionStyle getStyleType]) { - // get mention data - MentionParams *candidateMentionParams; - NSRange candidateMentionRange = NSMakeRange(0, 0); - MentionStyle *mentionStyleClass = - (MentionStyle *)stylesDict[@([MentionStyle getStyleType])]; - if (mentionStyleClass != nullptr) { - candidateMentionParams = [mentionStyleClass - getMentionParamsAt:textView.selectedRange.location]; - candidateMentionRange = [mentionStyleClass - getFullMentionRangeAt:textView.selectedRange.location]; - } - - if (wasActive == NO) { - // selection was changed from a non-mention to a mention - detectedMentionParams = candidateMentionParams; - detectedMentionRange = candidateMentionRange; - } else if (![_recentlyActiveMentionParams.text - isEqualToString:candidateMentionParams.text] || - ![_recentlyActiveMentionParams.attributes - isEqualToString:candidateMentionParams.attributes] || - !NSEqualRanges(_recentlyActiveMentionRange, - candidateMentionRange)) { - // selection changed from one mention to another - detectedMentionParams = candidateMentionParams; - detectedMentionRange = candidateMentionRange; - } - } + // // onLinkDetected event + // if (isActive && [type intValue] == [LinkStyle getStyleType]) { + // // get the link data + // LinkData *candidateLinkData; + // NSRange candidateLinkRange = NSMakeRange(0, 0); + // LinkStyle *linkStyleClass = + // (LinkStyle *)stylesDict[@([LinkStyle getStyleType])]; + // if (linkStyleClass != nullptr) { + // candidateLinkData = + // [linkStyleClass + // getLinkDataAt:textView.selectedRange.location]; + // candidateLinkRange = + // [linkStyleClass + // getFullLinkRangeAt:textView.selectedRange.location]; + // } + // + // if (wasActive == NO) { + // // we changed selection from non-link to a link + // detectedLinkData = candidateLinkData; + // detectedLinkRange = candidateLinkRange; + // } else if (![_recentlyActiveLinkData.url + // isEqualToString:candidateLinkData.url] || + // ![_recentlyActiveLinkData.text + // isEqualToString:candidateLinkData.text] || + // !NSEqualRanges(_recentlyActiveLinkRange, + // candidateLinkRange)) { + // // we changed selection from one link to the other or modified + // current + // // link's text + // detectedLinkData = candidateLinkData; + // detectedLinkRange = candidateLinkRange; + // } + // } + // + // // onMentionDetected event + // if (isActive && [type intValue] == [MentionStyle getStyleType]) { + // // get mention data + // MentionParams *candidateMentionParams; + // NSRange candidateMentionRange = NSMakeRange(0, 0); + // MentionStyle *mentionStyleClass = + // (MentionStyle *)stylesDict[@([MentionStyle getStyleType])]; + // if (mentionStyleClass != nullptr) { + // candidateMentionParams = [mentionStyleClass + // getMentionParamsAt:textView.selectedRange.location]; + // candidateMentionRange = [mentionStyleClass + // getFullMentionRangeAt:textView.selectedRange.location]; + // } + // + // if (wasActive == NO) { + // // selection was changed from a non-mention to a mention + // detectedMentionParams = candidateMentionParams; + // detectedMentionRange = candidateMentionRange; + // } else if (![_recentlyActiveMentionParams.text + // isEqualToString:candidateMentionParams.text] || + // ![_recentlyActiveMentionParams.attributes + // isEqualToString:candidateMentionParams.attributes] || + // !NSEqualRanges(_recentlyActiveMentionRange, + // candidateMentionRange)) { + // // selection changed from one mention to another + // detectedMentionParams = candidateMentionParams; + // detectedMentionRange = candidateMentionRange; + // } + // } } if (updateNeeded) { @@ -1007,71 +1013,69 @@ - (void)tryUpdatingActiveStyles { _blockedStyles = newBlockedStyles; emitter->onChangeStateDeprecated( - {.isBold = [self isStyleActive:[BoldStyle getStyleType]], - .isItalic = [self isStyleActive:[ItalicStyle getStyleType]], - .isUnderline = [self isStyleActive:[UnderlineStyle getStyleType]], - .isStrikeThrough = - [self isStyleActive:[StrikethroughStyle getStyleType]], - .isInlineCode = [self isStyleActive:[InlineCodeStyle getStyleType]], - .isLink = [self isStyleActive:[LinkStyle getStyleType]], - .isMention = [self isStyleActive:[MentionStyle getStyleType]], - .isH1 = [self isStyleActive:[H1Style getStyleType]], - .isH2 = [self isStyleActive:[H2Style getStyleType]], - .isH3 = [self isStyleActive:[H3Style getStyleType]], - .isH4 = [self isStyleActive:[H4Style getStyleType]], - .isH5 = [self isStyleActive:[H5Style getStyleType]], - .isH6 = [self isStyleActive:[H6Style getStyleType]], - .isUnorderedList = - [self isStyleActive:[UnorderedListStyle getStyleType]], - .isOrderedList = - [self isStyleActive:[OrderedListStyle getStyleType]], - .isBlockQuote = [self isStyleActive:[BlockQuoteStyle getStyleType]], - .isCodeBlock = [self isStyleActive:[CodeBlockStyle getStyleType]], - .isImage = [self isStyleActive:[ImageStyle getStyleType]], - .isCheckboxList = - [self isStyleActive:[CheckboxListStyle getStyleType]]}); + {.isBold = [self isStyleActive:[BoldStyle getType]], + .isItalic = [self isStyleActive:[ItalicStyle getType]], + .isUnderline = [self isStyleActive:[UnderlineStyle getType]], + .isStrikeThrough = [self isStyleActive:[StrikethroughStyle getType]], + .isInlineCode = [self isStyleActive:[InlineCodeStyle getType]], + // .isLink = [self isStyleActive:[LinkStyle getStyleType]], + // .isMention = [self isStyleActive:[MentionStyle + // getStyleType]], + .isH1 = [self isStyleActive:[H1Style getType]], + .isH2 = [self isStyleActive:[H2Style getType]], + .isH3 = [self isStyleActive:[H3Style getType]], + .isH4 = [self isStyleActive:[H4Style getType]], + .isH5 = [self isStyleActive:[H5Style getType]], + .isH6 = [self isStyleActive:[H6Style getType]], + .isUnorderedList = [self isStyleActive:[UnorderedListStyle getType]], + .isOrderedList = [self isStyleActive:[OrderedListStyle getType]], + .isBlockQuote = [self isStyleActive:[BlockQuoteStyle getType]], + .isCodeBlock = [self isStyleActive:[CodeBlockStyle getType]], + // .isImage = [self isStyleActive:[ImageStyle + // getStyleType]], + .isCheckboxList = [self isStyleActive:[CheckboxListStyle getType]]}); emitter->onChangeState( - {.bold = GET_STYLE_STATE([BoldStyle getStyleType]), - .italic = GET_STYLE_STATE([ItalicStyle getStyleType]), - .underline = GET_STYLE_STATE([UnderlineStyle getStyleType]), - .strikeThrough = GET_STYLE_STATE([StrikethroughStyle getStyleType]), - .inlineCode = GET_STYLE_STATE([InlineCodeStyle getStyleType]), - .link = GET_STYLE_STATE([LinkStyle getStyleType]), - .mention = GET_STYLE_STATE([MentionStyle getStyleType]), - .h1 = GET_STYLE_STATE([H1Style getStyleType]), - .h2 = GET_STYLE_STATE([H2Style getStyleType]), - .h3 = GET_STYLE_STATE([H3Style getStyleType]), - .h4 = GET_STYLE_STATE([H4Style getStyleType]), - .h5 = GET_STYLE_STATE([H5Style getStyleType]), - .h6 = GET_STYLE_STATE([H6Style getStyleType]), - .unorderedList = GET_STYLE_STATE([UnorderedListStyle getStyleType]), - .orderedList = GET_STYLE_STATE([OrderedListStyle getStyleType]), - .blockQuote = GET_STYLE_STATE([BlockQuoteStyle getStyleType]), - .codeBlock = GET_STYLE_STATE([CodeBlockStyle getStyleType]), - .image = GET_STYLE_STATE([ImageStyle getStyleType]), - .checkboxList = GET_STYLE_STATE([CheckboxListStyle getStyleType])}); + {.bold = GET_STYLE_STATE([BoldStyle getType]), + .italic = GET_STYLE_STATE([ItalicStyle getType]), + .underline = GET_STYLE_STATE([UnderlineStyle getType]), + .strikeThrough = GET_STYLE_STATE([StrikethroughStyle getType]), + .inlineCode = GET_STYLE_STATE([InlineCodeStyle getType]), + // .link = GET_STYLE_STATE([LinkStyle getStyleType]), + // .mention = GET_STYLE_STATE([MentionStyle getStyleType]), + .h1 = GET_STYLE_STATE([H1Style getType]), + .h2 = GET_STYLE_STATE([H2Style getType]), + .h3 = GET_STYLE_STATE([H3Style getType]), + .h4 = GET_STYLE_STATE([H4Style getType]), + .h5 = GET_STYLE_STATE([H5Style getType]), + .h6 = GET_STYLE_STATE([H6Style getType]), + .unorderedList = GET_STYLE_STATE([UnorderedListStyle getType]), + .orderedList = GET_STYLE_STATE([OrderedListStyle getType]), + .blockQuote = GET_STYLE_STATE([BlockQuoteStyle getType]), + .codeBlock = GET_STYLE_STATE([CodeBlockStyle getType]), + // .image = GET_STYLE_STATE([ImageStyle getStyleType]), + .checkboxList = GET_STYLE_STATE([CheckboxListStyle getType])}); } } - if (detectedLinkData != nullptr) { - // emit onLinkeDetected event - [self emitOnLinkDetectedEvent:detectedLinkData.text - url:detectedLinkData.url - range:detectedLinkRange]; - } - - if (detectedMentionParams != nullptr) { - // emit onMentionDetected event - [self emitOnMentionDetectedEvent:detectedMentionParams.text - indicator:detectedMentionParams.indicator - attributes:detectedMentionParams.attributes]; - - _recentlyActiveMentionParams = detectedMentionParams; - _recentlyActiveMentionRange = detectedMentionRange; - } - - // emit onChangeHtml event if needed - [self tryEmittingOnChangeHtmlEvent]; + // if (detectedLinkData != nullptr) { + // // emit onLinkeDetected event + // [self emitOnLinkDetectedEvent:detectedLinkData.text + // url:detectedLinkData.url + // range:detectedLinkRange]; + // } + // + // if (detectedMentionParams != nullptr) { + // // emit onMentionDetected event + // [self emitOnMentionDetectedEvent:detectedMentionParams.text + // indicator:detectedMentionParams.indicator + // attributes:detectedMentionParams.attributes]; + // + // _recentlyActiveMentionParams = detectedMentionParams; + // _recentlyActiveMentionRange = detectedMentionRange; + // } + // + // // emit onChangeHtml event if needed + // [self tryEmittingOnChangeHtmlEvent]; } - (bool)isStyleActive:(StyleType)type { @@ -1117,70 +1121,76 @@ - (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args { [self focus]; } else if ([commandName isEqualToString:@"blur"]) { [self blur]; - } else if ([commandName isEqualToString:@"setValue"]) { - NSString *value = (NSString *)args[0]; - [self setValue:value]; - } else if ([commandName isEqualToString:@"toggleBold"]) { - [self toggleRegularStyle:[BoldStyle getStyleType]]; + } + // else if ([commandName isEqualToString:@"setValue"]) { + // NSString *value = (NSString *)args[0]; + // [self setValue:value]; + // } + else if ([commandName isEqualToString:@"toggleBold"]) { + [self toggleRegularStyle:[BoldStyle getType]]; } else if ([commandName isEqualToString:@"toggleItalic"]) { - [self toggleRegularStyle:[ItalicStyle getStyleType]]; + [self toggleRegularStyle:[ItalicStyle getType]]; } else if ([commandName isEqualToString:@"toggleUnderline"]) { - [self toggleRegularStyle:[UnderlineStyle getStyleType]]; + [self toggleRegularStyle:[UnderlineStyle getType]]; } else if ([commandName isEqualToString:@"toggleStrikeThrough"]) { - [self toggleRegularStyle:[StrikethroughStyle getStyleType]]; + [self toggleRegularStyle:[StrikethroughStyle getType]]; } else if ([commandName isEqualToString:@"toggleInlineCode"]) { - [self toggleRegularStyle:[InlineCodeStyle getStyleType]]; - } else if ([commandName isEqualToString:@"addLink"]) { - NSInteger start = [((NSNumber *)args[0]) integerValue]; - NSInteger end = [((NSNumber *)args[1]) integerValue]; - NSString *text = (NSString *)args[2]; - NSString *url = (NSString *)args[3]; - [self addLinkAt:start end:end text:text url:url]; - } else if ([commandName isEqualToString:@"addMention"]) { - NSString *indicator = (NSString *)args[0]; - NSString *text = (NSString *)args[1]; - NSString *attributes = (NSString *)args[2]; - [self addMention:indicator text:text attributes:attributes]; - } else if ([commandName isEqualToString:@"startMention"]) { - NSString *indicator = (NSString *)args[0]; - [self startMentionWithIndicator:indicator]; - } else if ([commandName isEqualToString:@"toggleH1"]) { - [self toggleParagraphStyle:[H1Style getStyleType]]; + [self toggleRegularStyle:[InlineCodeStyle getType]]; + } + // else if ([commandName isEqualToString:@"addLink"]) { + // NSInteger start = [((NSNumber *)args[0]) integerValue]; + // NSInteger end = [((NSNumber *)args[1]) integerValue]; + // NSString *text = (NSString *)args[2]; + // NSString *url = (NSString *)args[3]; + // [self addLinkAt:start end:end text:text url:url]; + // } else if ([commandName isEqualToString:@"addMention"]) { + // NSString *indicator = (NSString *)args[0]; + // NSString *text = (NSString *)args[1]; + // NSString *attributes = (NSString *)args[2]; + // [self addMention:indicator text:text attributes:attributes]; + // } else if ([commandName isEqualToString:@"startMention"]) { + // NSString *indicator = (NSString *)args[0]; + // [self startMentionWithIndicator:indicator]; + else if ([commandName isEqualToString:@"toggleH1"]) { + [self toggleRegularStyle:[H1Style getType]]; } else if ([commandName isEqualToString:@"toggleH2"]) { - [self toggleParagraphStyle:[H2Style getStyleType]]; + [self toggleRegularStyle:[H2Style getType]]; } else if ([commandName isEqualToString:@"toggleH3"]) { - [self toggleParagraphStyle:[H3Style getStyleType]]; + [self toggleRegularStyle:[H3Style getType]]; } else if ([commandName isEqualToString:@"toggleH4"]) { - [self toggleParagraphStyle:[H4Style getStyleType]]; + [self toggleRegularStyle:[H4Style getType]]; } else if ([commandName isEqualToString:@"toggleH5"]) { - [self toggleParagraphStyle:[H5Style getStyleType]]; + [self toggleRegularStyle:[H5Style getType]]; } else if ([commandName isEqualToString:@"toggleH6"]) { - [self toggleParagraphStyle:[H6Style getStyleType]]; + [self toggleRegularStyle:[H6Style getType]]; } else if ([commandName isEqualToString:@"toggleUnorderedList"]) { - [self toggleParagraphStyle:[UnorderedListStyle getStyleType]]; + [self toggleRegularStyle:[UnorderedListStyle getType]]; } else if ([commandName isEqualToString:@"toggleOrderedList"]) { - [self toggleParagraphStyle:[OrderedListStyle getStyleType]]; + [self toggleRegularStyle:[OrderedListStyle getType]]; } else if ([commandName isEqualToString:@"toggleCheckboxList"]) { BOOL checked = [args[0] boolValue]; [self toggleCheckboxList:checked]; } else if ([commandName isEqualToString:@"toggleBlockQuote"]) { - [self toggleParagraphStyle:[BlockQuoteStyle getStyleType]]; + [self toggleRegularStyle:[BlockQuoteStyle getType]]; } else if ([commandName isEqualToString:@"toggleCodeBlock"]) { - [self toggleParagraphStyle:[CodeBlockStyle getStyleType]]; - } else if ([commandName isEqualToString:@"addImage"]) { - NSString *uri = (NSString *)args[0]; - CGFloat imgWidth = [(NSNumber *)args[1] floatValue]; - CGFloat imgHeight = [(NSNumber *)args[2] floatValue]; - - [self addImage:uri width:imgWidth height:imgHeight]; - } else if ([commandName isEqualToString:@"setSelection"]) { + [self toggleRegularStyle:[CodeBlockStyle getType]]; + } + // else if ([commandName isEqualToString:@"addImage"]) { + // NSString *uri = (NSString *)args[0]; + // CGFloat imgWidth = [(NSNumber *)args[1] floatValue]; + // CGFloat imgHeight = [(NSNumber *)args[2] floatValue]; + // + // [self addImage:uri width:imgWidth height:imgHeight]; + // } + else if ([commandName isEqualToString:@"setSelection"]) { NSInteger start = [((NSNumber *)args[0]) integerValue]; NSInteger end = [((NSNumber *)args[1]) integerValue]; [self setCustomSelection:start end:end]; - } else if ([commandName isEqualToString:@"requestHTML"]) { - NSInteger requestId = [((NSNumber *)args[0]) integerValue]; - [self requestHTML:requestId]; } + // else if ([commandName isEqualToString:@"requestHTML"]) { + // NSInteger requestId = [((NSNumber *)args[0]) integerValue]; + // [self requestHTML:requestId]; + // } } - (std::shared_ptr)getEventEmitter { @@ -1201,21 +1211,21 @@ - (void)focus { [textView reactFocus]; } -- (void)setValue:(NSString *)value { - NSString *initiallyProcessedHtml = [parser initiallyProcessHtml:value]; - if (initiallyProcessedHtml == nullptr) { - // just plain text - textView.text = value; - } else { - // we've got some seemingly proper html - [parser replaceWholeFromHtml:initiallyProcessedHtml]; - } - - // set recentlyChangedRange and check for changes - recentlyChangedRange = NSMakeRange(0, textView.textStorage.string.length); - textView.selectedRange = NSRange(textView.textStorage.string.length, 0); - [self anyTextMayHaveBeenModified]; -} +//- (void)setValue:(NSString *)value { +// NSString *initiallyProcessedHtml = [parser initiallyProcessHtml:value]; +// if (initiallyProcessedHtml == nullptr) { +// // just plain text +// textView.text = value; +// } else { +// // we've got some seemingly proper html +// [parser replaceWholeFromHtml:initiallyProcessedHtml]; +// } +// +// // set recentlyChangedRange and check for changes +// recentlyChangedRange = NSMakeRange(0, textView.textStorage.string.length); +// textView.selectedRange = NSRange(textView.textStorage.string.length, 0); +// [self anyTextMayHaveBeenModified]; +//} - (void)setCustomSelection:(NSInteger)visibleStart end:(NSInteger)visibleEnd { NSString *text = textView.textStorage.string; @@ -1249,83 +1259,84 @@ - (NSUInteger)getActualIndex:(NSInteger)visibleIndex text:(NSString *)text { return actualIndex; } -- (void)emitOnLinkDetectedEvent:(NSString *)text - url:(NSString *)url - range:(NSRange)range { - auto emitter = [self getEventEmitter]; - if (emitter != nullptr) { - // update recently active link info - LinkData *newLinkData = [[LinkData alloc] init]; - newLinkData.text = text; - newLinkData.url = url; - _recentlyActiveLinkData = newLinkData; - _recentlyActiveLinkRange = range; - - emitter->onLinkDetected({ - .text = [text toCppString], - .url = [url toCppString], - .start = static_cast(range.location), - .end = static_cast(range.location + range.length), - }); - } -} - -- (void)emitOnMentionDetectedEvent:(NSString *)text - indicator:(NSString *)indicator - attributes:(NSString *)attributes { - auto emitter = [self getEventEmitter]; - if (emitter != nullptr) { - emitter->onMentionDetected({.text = [text toCppString], - .indicator = [indicator toCppString], - .payload = [attributes toCppString]}); - } -} - -- (void)emitOnMentionEvent:(NSString *)indicator text:(NSString *)text { - auto emitter = [self getEventEmitter]; - if (emitter != nullptr) { - if (text != nullptr) { - folly::dynamic fdStr = [text toCppString]; - emitter->onMention({.indicator = [indicator toCppString], .text = fdStr}); - } else { - folly::dynamic nul = nullptr; - emitter->onMention({.indicator = [indicator toCppString], .text = nul}); - } - } -} - -- (void)tryEmittingOnChangeHtmlEvent { - if (!_emitHtml || textView.markedTextRange != nullptr) { - return; - } - auto emitter = [self getEventEmitter]; - if (emitter != nullptr) { - NSString *htmlOutput = [parser - parseToHtmlFromRange:NSMakeRange(0, - textView.textStorage.string.length)]; - // make sure html really changed - if (![htmlOutput isEqualToString:_recentlyEmittedHtml]) { - _recentlyEmittedHtml = htmlOutput; - emitter->onChangeHtml({.value = [htmlOutput toCppString]}); - } - } -} - -- (void)requestHTML:(NSInteger)requestId { - auto emitter = [self getEventEmitter]; - if (emitter != nullptr) { - @try { - NSString *htmlOutput = [parser - parseToHtmlFromRange:NSMakeRange(0, - textView.textStorage.string.length)]; - emitter->onRequestHtmlResult({.requestId = static_cast(requestId), - .html = [htmlOutput toCppString]}); - } @catch (NSException *exception) { - emitter->onRequestHtmlResult({.requestId = static_cast(requestId), - .html = folly::dynamic(nullptr)}); - } - } -} +//- (void)emitOnLinkDetectedEvent:(NSString *)text +// url:(NSString *)url +// range:(NSRange)range { +// auto emitter = [self getEventEmitter]; +// if (emitter != nullptr) { +// // update recently active link info +// LinkData *newLinkData = [[LinkData alloc] init]; +// newLinkData.text = text; +// newLinkData.url = url; +// _recentlyActiveLinkData = newLinkData; +// _recentlyActiveLinkRange = range; +// +// emitter->onLinkDetected({ +// .text = [text toCppString], +// .url = [url toCppString], +// .start = static_cast(range.location), +// .end = static_cast(range.location + range.length), +// }); +// } +//} +// +//- (void)emitOnMentionDetectedEvent:(NSString *)text +// indicator:(NSString *)indicator +// attributes:(NSString *)attributes { +// auto emitter = [self getEventEmitter]; +// if (emitter != nullptr) { +// emitter->onMentionDetected({.text = [text toCppString], +// .indicator = [indicator toCppString], +// .payload = [attributes toCppString]}); +// } +//} +// +//- (void)emitOnMentionEvent:(NSString *)indicator text:(NSString *)text { +// auto emitter = [self getEventEmitter]; +// if (emitter != nullptr) { +// if (text != nullptr) { +// folly::dynamic fdStr = [text toCppString]; +// emitter->onMention({.indicator = [indicator toCppString], .text = +// fdStr}); +// } else { +// folly::dynamic nul = nullptr; +// emitter->onMention({.indicator = [indicator toCppString], .text = nul}); +// } +// } +//} + +//- (void)tryEmittingOnChangeHtmlEvent { +// if (!_emitHtml || textView.markedTextRange != nullptr) { +// return; +// } +// auto emitter = [self getEventEmitter]; +// if (emitter != nullptr) { +// NSString *htmlOutput = [parser +// parseToHtmlFromRange:NSMakeRange(0, +// textView.textStorage.string.length)]; +// // make sure html really changed +// if (![htmlOutput isEqualToString:_recentlyEmittedHtml]) { +// _recentlyEmittedHtml = htmlOutput; +// emitter->onChangeHtml({.value = [htmlOutput toCppString]}); +// } +// } +//} + +//- (void)requestHTML:(NSInteger)requestId { +// auto emitter = [self getEventEmitter]; +// if (emitter != nullptr) { +// @try { +// NSString *htmlOutput = [parser +// parseToHtmlFromRange:NSMakeRange(0, +// textView.textStorage.string.length)]; +// emitter->onRequestHtmlResult({.requestId = static_cast(requestId), +// .html = [htmlOutput toCppString]}); +// } @catch (NSException *exception) { +// emitter->onRequestHtmlResult({.requestId = static_cast(requestId), +// .html = folly::dynamic(nullptr)}); +// } +// } +//} - (void)emitOnKeyPressEvent:(NSString *)key { auto emitter = [self getEventEmitter]; @@ -1337,115 +1348,115 @@ - (void)emitOnKeyPressEvent:(NSString *)key { // MARK: - Styles manipulation - (void)toggleRegularStyle:(StyleType)type { - id styleClass = stylesDict[@(type)]; - - if ([self handleStyleBlocksAndConflicts:type range:textView.selectedRange]) { - [styleClass applyStyle:textView.selectedRange]; - [self anyTextMayHaveBeenModified]; + StyleBase *style = stylesDict[@(type)]; + NSRange range = textView.selectedRange; + if ([style isParagraph]) { + range = [textView.textStorage.string paragraphRangeForRange:range]; } -} - -- (void)toggleParagraphStyle:(StyleType)type { - id styleClass = stylesDict[@(type)]; - // we always pass whole paragraph/s range to these styles - NSRange paragraphRange = [textView.textStorage.string - paragraphRangeForRange:textView.selectedRange]; - - if ([self handleStyleBlocksAndConflicts:type range:paragraphRange]) { - [styleClass applyStyle:paragraphRange]; + if ([self handleStyleBlocksAndConflicts:type range:range]) { + [style toggle:range]; [self anyTextMayHaveBeenModified]; } } +//- (void)toggleParagraphStyle:(StyleType)type { +// id styleClass = stylesDict[@(type)]; +// // we always pass whole paragraph/s range to these styles +// NSRange paragraphRange = [textView.textStorage.string +// paragraphRangeForRange:textView.selectedRange]; +// +// if ([self handleStyleBlocksAndConflicts:type range:paragraphRange]) { +// [styleClass applyStyle:paragraphRange]; +// [self anyTextMayHaveBeenModified]; +// } +//} + - (void)toggleCheckboxList:(BOOL)checked { - CheckboxListStyle *checkboxListStyleClass = - (CheckboxListStyle *)stylesDict[@([CheckboxListStyle getStyleType])]; - if (checkboxListStyleClass == nullptr) { + CheckboxListStyle *style = + (CheckboxListStyle *)stylesDict[@([CheckboxListStyle getType])]; + if (style == nullptr) { return; } - // we always pass whole paragraph/s range to these styles - NSRange paragraphRange = [textView.textStorage.string + NSRange range = [textView.textStorage.string paragraphRangeForRange:textView.selectedRange]; - - if ([self handleStyleBlocksAndConflicts:[CheckboxListStyle getStyleType] - range:paragraphRange]) { - [checkboxListStyleClass applyStyleWithCheckedValue:checked - inRange:paragraphRange]; - [self anyTextMayHaveBeenModified]; - } -} - -- (void)addLinkAt:(NSInteger)start - end:(NSInteger)end - text:(NSString *)text - url:(NSString *)url { - LinkStyle *linkStyleClass = - (LinkStyle *)stylesDict[@([LinkStyle getStyleType])]; - if (linkStyleClass == nullptr) { - return; - } - - // translate the output start-end notation to range - NSRange linkRange = NSMakeRange(start, end - start); - if ([self handleStyleBlocksAndConflicts:[LinkStyle getStyleType] - range:linkRange]) { - [linkStyleClass addLink:text - url:url - range:linkRange - manual:YES - withSelection:YES]; - [self anyTextMayHaveBeenModified]; - } -} - -- (void)addMention:(NSString *)indicator - text:(NSString *)text - attributes:(NSString *)attributes { - MentionStyle *mentionStyleClass = - (MentionStyle *)stylesDict[@([MentionStyle getStyleType])]; - if (mentionStyleClass == nullptr) { - return; - } - if ([mentionStyleClass getActiveMentionRange] == nullptr) { - return; - } - - if ([self handleStyleBlocksAndConflicts:[MentionStyle getStyleType] - range:[[mentionStyleClass - getActiveMentionRange] - rangeValue]]) { - [mentionStyleClass addMention:indicator text:text attributes:attributes]; + if ([self handleStyleBlocksAndConflicts:[CheckboxListStyle getType] + range:range]) { + [style toggleWithChecked:checked range:range]; [self anyTextMayHaveBeenModified]; } } -- (void)addImage:(NSString *)uri width:(float)width height:(float)height { - ImageStyle *imageStyleClass = - (ImageStyle *)stylesDict[@([ImageStyle getStyleType])]; - if (imageStyleClass == nullptr) { - return; - } - - if ([self handleStyleBlocksAndConflicts:[ImageStyle getStyleType] - range:textView.selectedRange]) { - [imageStyleClass addImage:uri width:width height:height]; - [self anyTextMayHaveBeenModified]; - } -} - -- (void)startMentionWithIndicator:(NSString *)indicator { - MentionStyle *mentionStyleClass = - (MentionStyle *)stylesDict[@([MentionStyle getStyleType])]; - if (mentionStyleClass == nullptr) { - return; - } - - if ([self handleStyleBlocksAndConflicts:[MentionStyle getStyleType] - range:textView.selectedRange]) { - [mentionStyleClass startMentionWithIndicator:indicator]; - [self anyTextMayHaveBeenModified]; - } -} +// - (void)addLinkAt:(NSInteger)start +// end:(NSInteger)end +// text:(NSString *)text +// url:(NSString *)url { +// LinkStyle *linkStyleClass = +// (LinkStyle *)stylesDict[@([LinkStyle getStyleType])]; +// if (linkStyleClass == nullptr) { +// return; +// } + +// // translate the output start-end notation to range +// NSRange linkRange = NSMakeRange(start, end - start); +// if ([self handleStyleBlocksAndConflicts:[LinkStyle getStyleType] +// range:linkRange]) { +// [linkStyleClass addLink:text +// url:url +// range:linkRange +// manual:YES +// withSelection:YES]; +// [self anyTextMayHaveBeenModified]; +// } +// } + +// - (void)addMention:(NSString *)indicator +// text:(NSString *)text +// attributes:(NSString *)attributes { +// MentionStyle *mentionStyleClass = +// (MentionStyle *)stylesDict[@([MentionStyle getStyleType])]; +// if (mentionStyleClass == nullptr) { +// return; +// } +// if ([mentionStyleClass getActiveMentionRange] == nullptr) { +// return; +// } + +// if ([self handleStyleBlocksAndConflicts:[MentionStyle getStyleType] +// range:[[mentionStyleClass +// getActiveMentionRange] +// rangeValue]]) { +// [mentionStyleClass addMention:indicator text:text attributes:attributes]; +// [self anyTextMayHaveBeenModified]; +// } +// } + +// - (void)addImage:(NSString *)uri width:(float)width height:(float)height { +// ImageStyle *imageStyleClass = +// (ImageStyle *)stylesDict[@([ImageStyle getStyleType])]; +// if (imageStyleClass == nullptr) { +// return; +// } + +// if ([self handleStyleBlocksAndConflicts:[ImageStyle getStyleType] +// range:textView.selectedRange]) { +// [imageStyleClass addImage:uri width:width height:height]; +// [self anyTextMayHaveBeenModified]; +// } +// } + +// - (void)startMentionWithIndicator:(NSString *)indicator { +// MentionStyle *mentionStyleClass = +// (MentionStyle *)stylesDict[@([MentionStyle getStyleType])]; +// if (mentionStyleClass == nullptr) { +// return; +// } + +// if ([self handleStyleBlocksAndConflicts:[MentionStyle getStyleType] +// range:textView.selectedRange]) { +// [mentionStyleClass startMentionWithIndicator:indicator]; +// [self anyTextMayHaveBeenModified]; +// } +// } // returns false when style shouldn't be applied and true when it can be - (BOOL)handleStyleBlocksAndConflicts:(StyleType)type range:(NSRange)range { @@ -1456,25 +1467,22 @@ - (BOOL)handleStyleBlocksAndConflicts:(StyleType)type range:(NSRange)range { return NO; } - // handle conflicting styles: all of their occurences have to be removed + // handle conflicting styles: remove styles within the range NSArray *conflicting = [self getPresentStyleTypesFrom:conflictingStyles[@(type)] range:range]; if (conflicting.count != 0) { - for (NSNumber *style in conflicting) { - id styleClass = stylesDict[style]; - - if (range.length >= 1) { - // for ranges, we need to remove each occurence - NSArray *allOccurences = - [styleClass findAllOccurences:range]; + for (NSNumber *type in conflicting) { + StyleBase *style = stylesDict[type]; - for (StylePair *pair in allOccurences) { - [styleClass removeAttributes:[pair.rangeValue rangeValue]]; - } + if ([style isParagraph]) { + // for paragraph styles we can just call remove since it will pick up + // proper paragraph range + [style remove:range withDirtyRange:YES]; } else { - // with in-place selection, we just remove the adequate typing - // attributes - [styleClass removeTypingAttributes]; + // for inline styles we have to differentiate betweeen normal and typing + // attributes removal + range.length >= 1 ? [style remove:range withDirtyRange:YES] + : [style removeTyping]; } } } @@ -1486,14 +1494,14 @@ - (BOOL)handleStyleBlocksAndConflicts:(StyleType)type range:(NSRange)range { NSMutableArray *resultArray = [[NSMutableArray alloc] init]; for (NSNumber *type in types) { - id styleClass = stylesDict[type]; + StyleBase *style = stylesDict[type]; if (range.length >= 1) { - if ([styleClass anyOccurence:range]) { + if ([style any:range]) { [resultArray addObject:type]; } } else { - if ([styleClass detectStyle:range]) { + if ([style detect:range]) { [resultArray addObject:type]; } } @@ -1502,62 +1510,57 @@ - (BOOL)handleStyleBlocksAndConflicts:(StyleType)type range:(NSRange)range { } - (void)manageSelectionBasedChanges { - // link typing attributes fix - LinkStyle *linkStyleClass = - (LinkStyle *)stylesDict[@([LinkStyle getStyleType])]; - if (linkStyleClass != nullptr) { - [linkStyleClass manageLinkTypingAttributes]; - } + // // link typing attributes fix + // LinkStyle *linkStyleClass = + // (LinkStyle *)stylesDict[@([LinkStyle getStyleType])]; + // if (linkStyleClass != nullptr) { + // [linkStyleClass manageLinkTypingAttributes]; + // } NSString *currentString = [textView.textStorage.string copy]; - // mention typing attribtues fix and active editing - MentionStyle *mentionStyleClass = - (MentionStyle *)stylesDict[@([MentionStyle getStyleType])]; - if (mentionStyleClass != nullptr) { - [mentionStyleClass manageMentionTypingAttributes]; - - // mention editing runs if only a selection was done (no text change) - // otherwise we would double-emit with a second call in the - // anyTextMayHaveBeenModified method - if ([_recentInputString isEqualToString:currentString]) { - [mentionStyleClass manageMentionEditing]; - } - } - - // typing attributes for empty lines selection reset - if (textView.selectedRange.length == 0 && - [_recentInputString isEqualToString:currentString]) { - // no string change means only a selection changed with no character changes - NSRange paragraphRange = [textView.textStorage.string - paragraphRangeForRange:textView.selectedRange]; - if (paragraphRange.length == 0 || - (paragraphRange.length == 1 && - [[NSCharacterSet newlineCharacterSet] - characterIsMember:[textView.textStorage.string - characterAtIndex:paragraphRange - .location]])) { - // user changed selection to an empty line (or empty line with a newline) - // typing attributes need to be reset - textView.typingAttributes = defaultTypingAttributes; - } - } - - // update active styles as well + // // mention typing attribtues fix and active editing + // MentionStyle *mentionStyleClass = + // (MentionStyle *)stylesDict[@([MentionStyle getStyleType])]; + // if (mentionStyleClass != nullptr) { + // [mentionStyleClass manageMentionTypingAttributes]; + // + // // mention editing runs if only a selection was done (no text change) + // // otherwise we would double-emit with a second call in the + // // anyTextMayHaveBeenModified method + // if ([_recentInputString isEqualToString:currentString]) { + // [mentionStyleClass manageMentionEditing]; + // } + // } + + // attributes manager handles proper typingAttributes at all times to properly + // extend meta-attributes + BOOL onlySelectionChanged = + textView.selectedRange.length == 0 && + [_recentInputString isEqualToString:currentString]; + // We want to remember which attributes were removed as long as we stay at the + // same position. This prevents a removed attribute from being re-applied from + // the preceding character right after we toggled it off + [attributesManager clearRemovedTypingAttributes]; + [attributesManager + manageTypingAttributesWithOnlySelection:onlySelectionChanged]; + + // always update active styles [self tryUpdatingActiveStyles]; } -- (void)handleWordModificationBasedChanges:(NSString *)word - inRange:(NSRange)range { - // manual links refreshing and automatic links detection handling - LinkStyle *linkStyle = [stylesDict objectForKey:@([LinkStyle getStyleType])]; - - if (linkStyle != nullptr) { - // manual links need to be handled first because they can block automatic - // links after being refreshed - [linkStyle handleManualLinks:word inRange:range]; - [linkStyle handleAutomaticLinks:word inRange:range]; - } -} +//- (void)handleWordModificationBasedChanges:(NSString *)word +// inRange:(NSRange)range { +// // manual links refreshing and automatic links detection handling +// LinkStyle *linkStyle = [stylesDict objectForKey:@([LinkStyle +// getStyleType])]; +// +// if (linkStyle != nullptr) { +// // manual links need to be handled first because they can block automatic +// // links after being refreshed +// [linkStyle handleManualLinks:word inRange:range]; +// [linkStyle handleAutomaticLinks:word inRange:range]; +// } +//} - (void)anyTextMayHaveBeenModified { // we don't do no text changes when working with iOS marked text @@ -1575,74 +1578,74 @@ - (void)anyTextMayHaveBeenModified { textView.typingAttributes = defaultTypingAttributes; } - // inline code on newlines fix - InlineCodeStyle *codeStyle = stylesDict[@([InlineCodeStyle getStyleType])]; - if (codeStyle != nullptr) { - [codeStyle handleNewlines]; - } - - // blockquote colors management - BlockQuoteStyle *bqStyle = stylesDict[@([BlockQuoteStyle getStyleType])]; - if (bqStyle != nullptr) { - [bqStyle manageBlockquoteColor]; - } - - // codeblock font and color management - CodeBlockStyle *codeBlockStyle = stylesDict[@([CodeBlockStyle getStyleType])]; - if (codeBlockStyle != nullptr) { - [codeBlockStyle manageCodeBlockFontAndColor]; - } - - // improper headings fix - H1Style *h1Style = stylesDict[@([H1Style getStyleType])]; - H2Style *h2Style = stylesDict[@([H2Style getStyleType])]; - H3Style *h3Style = stylesDict[@([H3Style getStyleType])]; - H4Style *h4Style = stylesDict[@([H4Style getStyleType])]; - H5Style *h5Style = stylesDict[@([H5Style getStyleType])]; - H6Style *h6Style = stylesDict[@([H6Style getStyleType])]; - - bool headingStylesDefined = h1Style != nullptr && h2Style != nullptr && - h3Style != nullptr && h4Style != nullptr && - h5Style != nullptr && h6Style != nullptr; - - if (headingStylesDefined) { - [h1Style handleImproperHeadings]; - [h2Style handleImproperHeadings]; - [h3Style handleImproperHeadings]; - [h4Style handleImproperHeadings]; - [h5Style handleImproperHeadings]; - [h6Style handleImproperHeadings]; - } - - // mentions management: removal and editing - MentionStyle *mentionStyleClass = - (MentionStyle *)stylesDict[@([MentionStyle getStyleType])]; - if (mentionStyleClass != nullptr) { - [mentionStyleClass handleExistingMentions]; - [mentionStyleClass manageMentionEditing]; - } + // // inline code on newlines fix + // InlineCodeStyle *codeStyle = stylesDict[@([InlineCodeStyle + // getStyleType])]; if (codeStyle != nullptr) { + // [codeStyle handleNewlines]; + // } + // + // // blockquote colors management + // BlockQuoteStyle *bqStyle = stylesDict[@([BlockQuoteStyle getStyleType])]; + // if (bqStyle != nullptr) { + // [bqStyle manageBlockquoteColor]; + // } + // + // // codeblock font and color management + // CodeBlockStyle *codeBlockStyle = stylesDict[@([CodeBlockStyle + // getStyleType])]; if (codeBlockStyle != nullptr) { + // [codeBlockStyle manageCodeBlockFontAndColor]; + // } + // + // // improper headings fix + // H1Style *h1Style = stylesDict[@([H1Style getStyleType])]; + // H2Style *h2Style = stylesDict[@([H2Style getStyleType])]; + // H3Style *h3Style = stylesDict[@([H3Style getStyleType])]; + // H4Style *h4Style = stylesDict[@([H4Style getStyleType])]; + // H5Style *h5Style = stylesDict[@([H5Style getStyleType])]; + // H6Style *h6Style = stylesDict[@([H6Style getStyleType])]; + // + // bool headingStylesDefined = h1Style != nullptr && h2Style != nullptr && + // h3Style != nullptr && h4Style != nullptr && + // h5Style != nullptr && h6Style != nullptr; + // + // if (headingStylesDefined) { + // [h1Style handleImproperHeadings]; + // [h2Style handleImproperHeadings]; + // [h3Style handleImproperHeadings]; + // [h4Style handleImproperHeadings]; + // [h5Style handleImproperHeadings]; + // [h6Style handleImproperHeadings]; + // } + // + // // mentions management: removal and editing + // MentionStyle *mentionStyleClass = + // (MentionStyle *)stylesDict[@([MentionStyle getStyleType])]; + // if (mentionStyleClass != nullptr) { + // [mentionStyleClass handleExistingMentions]; + // [mentionStyleClass manageMentionEditing]; + // } // placholder management [textView updatePlaceholderVisibility]; if (![textView.textStorage.string isEqualToString:_recentInputString]) { - // modified words handling - NSArray *modifiedWords = - [WordsUtils getAffectedWordsFromText:textView.textStorage.string - modificationRange:recentlyChangedRange]; - if (modifiedWords != nullptr) { - for (NSDictionary *wordDict in modifiedWords) { - NSString *wordText = (NSString *)[wordDict objectForKey:@"word"]; - NSValue *wordRange = (NSValue *)[wordDict objectForKey:@"range"]; - - if (wordText == nullptr || wordRange == nullptr) { - continue; - } - - [self handleWordModificationBasedChanges:wordText - inRange:[wordRange rangeValue]]; - } - } + // // modified words handling + // NSArray *modifiedWords = + // [WordsUtils getAffectedWordsFromText:textView.textStorage.string + // modificationRange:recentlyChangedRange]; + // if (modifiedWords != nullptr) { + // for (NSDictionary *wordDict in modifiedWords) { + // NSString *wordText = (NSString *)[wordDict objectForKey:@"word"]; + // NSValue *wordRange = (NSValue *)[wordDict objectForKey:@"range"]; + // + // if (wordText == nullptr || wordRange == nullptr) { + // continue; + // } + // + // [self handleWordModificationBasedChanges:wordText + // inRange:[wordRange rangeValue]]; + // } + // } // emit onChangeText event auto emitter = [self getEventEmitter]; @@ -1658,12 +1661,14 @@ - (void)anyTextMayHaveBeenModified { emitter->onChangeText({.value = [stringToBeEmitted toCppString]}); } } - + // all the visible (not meta) attributes handling in the ranges that could + // have changed + [attributesManager handleDirtyRangesStyling]; // update active styles as well [self tryUpdatingActiveStyles]; } -// MARK: - UITextView delegate methods +// MARK: - Delegate methods - (void)textViewDidBeginEditing:(UITextView *)textView { auto emitter = [self getEventEmitter]; @@ -1719,58 +1724,74 @@ - (bool)textView:(UITextView *)textView recentlyChangedRange = NSMakeRange(range.location, text.length); [self handleKeyPressInRange:text range:range]; - UnorderedListStyle *uStyle = stylesDict[@([UnorderedListStyle getStyleType])]; - OrderedListStyle *oStyle = stylesDict[@([OrderedListStyle getStyleType])]; - CheckboxListStyle *cbLStyle = stylesDict[@([CheckboxListStyle getStyleType])]; - BlockQuoteStyle *bqStyle = stylesDict[@([BlockQuoteStyle getStyleType])]; - CodeBlockStyle *cbStyle = stylesDict[@([CodeBlockStyle getStyleType])]; - LinkStyle *linkStyle = stylesDict[@([LinkStyle getStyleType])]; - MentionStyle *mentionStyle = stylesDict[@([MentionStyle getStyleType])]; - H1Style *h1Style = stylesDict[@([H1Style getStyleType])]; - H2Style *h2Style = stylesDict[@([H2Style getStyleType])]; - H3Style *h3Style = stylesDict[@([H3Style getStyleType])]; - H4Style *h4Style = stylesDict[@([H4Style getStyleType])]; - H5Style *h5Style = stylesDict[@([H5Style getStyleType])]; - H6Style *h6Style = stylesDict[@([H6Style getStyleType])]; + UnorderedListStyle *uStyle = stylesDict[@([UnorderedListStyle getType])]; + OrderedListStyle *oStyle = stylesDict[@([OrderedListStyle getType])]; + CheckboxListStyle *cbLStyle = + (CheckboxListStyle *)stylesDict[@([CheckboxListStyle getType])]; + // BlockQuoteStyle *bqStyle = stylesDict[@([BlockQuoteStyle + // getStyleType])]; CodeBlockStyle *cbStyle = stylesDict[@([CodeBlockStyle + // getStyleType])]; LinkStyle *linkStyle = stylesDict[@([LinkStyle + // getStyleType])]; MentionStyle *mentionStyle = stylesDict[@([MentionStyle + // getStyleType])]; H1Style *h1Style = stylesDict[@([H1Style getStyleType])]; + // H2Style *h2Style = stylesDict[@([H2Style getStyleType])]; + // H3Style *h3Style = stylesDict[@([H3Style getStyleType])]; + // H4Style *h4Style = stylesDict[@([H4Style getStyleType])]; + // H5Style *h5Style = stylesDict[@([H5Style getStyleType])]; + // H6Style *h6Style = stylesDict[@([H6Style getStyleType])]; // some of the changes these checks do could interfere with later checks and // cause a crash so here I rely on short circuiting evaluation of the logical // expression either way it's not possible to have two of them come off at the // same time - if ([uStyle handleBackspaceInRange:range replacementText:text] || - [uStyle tryHandlingListShorcutInRange:range replacementText:text] || - [oStyle handleBackspaceInRange:range replacementText:text] || - [oStyle tryHandlingListShorcutInRange:range replacementText:text] || - [cbLStyle handleBackspaceInRange:range replacementText:text] || - [cbLStyle handleNewlinesInRange:range replacementText:text] || - [bqStyle handleBackspaceInRange:range replacementText:text] || - [cbStyle handleBackspaceInRange:range replacementText:text] || - [linkStyle handleLeadingLinkReplacement:range replacementText:text] || - [mentionStyle handleLeadingMentionReplacement:range - replacementText:text] || - [h1Style handleNewlinesInRange:range replacementText:text] || - [h2Style handleNewlinesInRange:range replacementText:text] || - [h3Style handleNewlinesInRange:range replacementText:text] || - [h4Style handleNewlinesInRange:range replacementText:text] || - [h5Style handleNewlinesInRange:range replacementText:text] || - [h6Style handleNewlinesInRange:range replacementText:text] || + if ( + // ZWS backspace handling for paragraph styles [ZeroWidthSpaceUtils handleBackspaceInRange:range replacementText:text input:self] || + [uStyle tryHandlingListShorcutInRange:range replacementText:text] || + [oStyle tryHandlingListShorcutInRange:range replacementText:text] || + [cbLStyle handleNewlinesInRange:range replacementText:text] + // || [bqStyle handleBackspaceInRange:range replacementText:text] + // || [cbStyle handleBackspaceInRange:range replacementText:text] + // || [linkStyle handleLeadingLinkReplacement:range + // replacementText:text] || [mentionStyle + // handleLeadingMentionReplacement:range + // replacementText:text] + || [(HeadingStyleBase *)stylesDict[@([H1Style getType])] + handleNewlinesInRange:range + replacementText:text] || + [(HeadingStyleBase *)stylesDict[@([H2Style getType])] + handleNewlinesInRange:range + replacementText:text] || + [(HeadingStyleBase *)stylesDict[@([H3Style getType])] + handleNewlinesInRange:range + replacementText:text] || + [(HeadingStyleBase *)stylesDict[@([H4Style getType])] + handleNewlinesInRange:range + replacementText:text] || + [(HeadingStyleBase *)stylesDict[@([H5Style getType])] + handleNewlinesInRange:range + replacementText:text] || + [(HeadingStyleBase *)stylesDict[@([H6Style getType])] + handleNewlinesInRange:range + replacementText:text] || [ParagraphAttributesUtils handleBackspaceInRange:range replacementText:text input:self] || [ParagraphAttributesUtils handleResetTypingAttributesOnBackspace:range replacementText:text - input:self] || + input:self] + // || // CRITICAL: This callback HAS TO be always evaluated last. // // This function is the "Generic Fallback": if no specific style claims // the backspace action to change its state, only then do we proceed to // physically delete the newline and merge paragraphs. - [ParagraphAttributesUtils handleParagraphStylesMergeOnBackspace:range - replacementText:text - input:self]) { + // || [ParagraphAttributesUtils + // handleParagraphStylesMergeOnBackspace:range + // replacementText:text + // input:self] + ) { [self anyTextMayHaveBeenModified]; return NO; } @@ -1857,7 +1878,7 @@ - (void)onTextBlockTap:(TextBlockTapGestureRecognizer *)gr { case TextBlockTapKindCheckbox: { CheckboxListStyle *checkboxStyle = - (CheckboxListStyle *)stylesDict[@([CheckboxListStyle getStyleType])]; + (CheckboxListStyle *)stylesDict[@([CheckboxListStyle getType])]; if (checkboxStyle) { NSUInteger charIndex = (NSUInteger)gr.characterIndex; @@ -1891,31 +1912,54 @@ - (void)onTextBlockTap:(TextBlockTapGestureRecognizer *)gr { } } -// MARK: - Media attachments delegate - -- (void)mediaAttachmentDidUpdate:(NSTextAttachment *)attachment { - NSTextStorage *storage = textView.textStorage; - NSRange fullRange = NSMakeRange(0, storage.length); - - __block NSRange foundRange = NSMakeRange(NSNotFound, 0); - - [storage enumerateAttribute:NSAttachmentAttributeName - inRange:fullRange - options:0 - usingBlock:^(id value, NSRange range, BOOL *stop) { - if (value == attachment) { - foundRange = range; - *stop = YES; - } - }]; - - if (foundRange.location == NSNotFound) { - return; +- (void)textStorage:(NSTextStorage *)textStorage + didProcessEditing:(NSTextStorageEditActions)editedMask + range:(NSRange)editedRange + changeInLength:(NSInteger)delta { + // iOS replacing quick double space with ". " attributes fix. + [DotReplacementUtils handleDotReplacement:self + textStorage:textStorage + editedMask:editedMask + editedRange:editedRange + delta:delta]; + + // Needed dirty ranges adjustments happen on every character edition. + if ((editedMask & NSTextStorageEditedCharacters) != 0) { + // Always try shifting dirty ranges (happens only with delta != 0). + [attributesManager shiftDirtyRangesWithEditedRange:editedRange + changeInLength:delta]; + + // Always try adding new dirty range (happens only with editedRange.length > + // 0). + [attributesManager addDirtyRange:editedRange]; } - - [storage edited:NSTextStorageEditedAttributes - range:foundRange - changeInLength:0]; } +// MARK: - Media attachments delegate + +//- (void)mediaAttachmentDidUpdate:(NSTextAttachment *)attachment { +// NSTextStorage *storage = textView.textStorage; +// NSRange fullRange = NSMakeRange(0, storage.length); +// +// __block NSRange foundRange = NSMakeRange(NSNotFound, 0); +// +// [storage enumerateAttribute:NSAttachmentAttributeName +// inRange:fullRange +// options:0 +// usingBlock:^(id value, NSRange range, BOOL *stop) { +// if (value == attachment) { +// foundRange = range; +// *stop = YES; +// } +// }]; +// +// if (foundRange.location == NSNotFound) { +// return; +// } +// +// [storage edited:NSTextStorageEditedAttributes +// range:foundRange +// changeInLength:0]; +//} + @end diff --git a/ios/attributesManager/AttributesManager.h b/ios/attributesManager/AttributesManager.h new file mode 100644 index 000000000..dc87e4199 --- /dev/null +++ b/ios/attributesManager/AttributesManager.h @@ -0,0 +1,16 @@ +#pragma once +#import + +@class EnrichedTextInputView; + +@interface AttributesManager : NSObject +@property(nonatomic, weak) EnrichedTextInputView *input; +- (instancetype)initWithInput:(EnrichedTextInputView *)input; +- (void)addDirtyRange:(NSRange)range; +- (void)shiftDirtyRangesWithEditedRange:(NSRange)editedRange + changeInLength:(NSInteger)delta; +- (void)didRemoveTypingAttribute:(NSString *)key; +- (void)clearRemovedTypingAttributes; +- (void)manageTypingAttributesWithOnlySelection:(BOOL)onlySelectionChanged; +- (void)handleDirtyRangesStyling; +@end diff --git a/ios/attributesManager/AttributesManager.mm b/ios/attributesManager/AttributesManager.mm new file mode 100644 index 000000000..4e233a07c --- /dev/null +++ b/ios/attributesManager/AttributesManager.mm @@ -0,0 +1,187 @@ +#import "AttributesManager.h" +#import "AttributeEntry.h" +#import "EnrichedTextInputView.h" +#import "RangeUtils.h" +#import "StyleHeaders.h" + +@implementation AttributesManager { + NSMutableArray *_dirtyRanges; + NSSet *_customAttributesKeys; + NSMutableSet *_removedTypingAttributes; +} + +- (instancetype)initWithInput:(EnrichedTextInputView *)input { + self = [super init]; + _input = input; + _dirtyRanges = [[NSMutableArray alloc] init]; + _removedTypingAttributes = [[NSMutableSet alloc] init]; + + // setup customAttributes + NSMutableSet *_customAttrsSet = [[NSMutableSet alloc] init]; + for (StyleBase *style in _input->stylesDict.allValues) { + [_customAttrsSet addObject:[style getKey]]; + } + _customAttributesKeys = _customAttrsSet; + + return self; +} + +- (void)addDirtyRange:(NSRange)range { + if (range.length == 0) { + return; + } + [_dirtyRanges addObject:[NSValue valueWithRange:range]]; + _dirtyRanges = [[RangeUtils connectAndDedupeRanges:_dirtyRanges] mutableCopy]; +} + +- (void)shiftDirtyRangesWithEditedRange:(NSRange)editedRange + changeInLength:(NSInteger)delta { + if (delta == 0) { + return; + } + NSArray *shiftedRanges = [RangeUtils shiftRanges:_dirtyRanges + withEditedRange:editedRange + changeInLength:delta]; + _dirtyRanges = + [[RangeUtils connectAndDedupeRanges:shiftedRanges] mutableCopy]; +} + +- (void)didRemoveTypingAttribute:(NSString *)key { + [_removedTypingAttributes addObject:key]; +} + +- (void)clearRemovedTypingAttributes { + [_removedTypingAttributes removeAllObjects]; +} + +- (void)handleDirtyRangesStyling { + for (NSValue *rangeObj in _dirtyRanges) { + NSRange dirtyRange = [rangeObj rangeValue]; + + // dirty range can sometimes be wrong because of apple doing some changes + // behind the scenes + if (dirtyRange.location + dirtyRange.length > + _input->textView.textStorage.string.length) + continue; + + // firstly, get all styles' occurences in that dirty range + NSMutableDictionary *presentStyles = [[NSMutableDictionary alloc] init]; + for (StyleBase *style in _input->stylesDict.allValues) { + // the dict has keys of StyleType NSNumber and values of an array of all + // occurences + presentStyles[@([[style class] getType])] = [style all:dirtyRange]; + } + + // now reset the attributes to default ones + [_input->textView.textStorage setAttributes:_input->defaultTypingAttributes + range:dirtyRange]; + + // Sort style types so paragraph styles come first. Their broad visual + // attributes (e.g. foreground color, font) are laid down before inline + // styles override them on their specific sub-ranges. + NSArray *sortedStyleTypes = [presentStyles.allKeys + sortedArrayUsingComparator:^NSComparisonResult(NSNumber *a, + NSNumber *b) { + BOOL aPara = [_input->stylesDict[a] isParagraph]; + BOOL bPara = [_input->stylesDict[b] isParagraph]; + if (aPara == bPara) + return NSOrderedSame; + return aPara ? NSOrderedAscending : NSOrderedDescending; + }]; + + // Apply visual styling and re-apply meta-attributes following the saved + // occurences. + for (NSNumber *styleType in sortedStyleTypes) { + StyleBase *style = _input->stylesDict[styleType]; + if (style == nullptr) + continue; + + for (StylePair *stylePair in presentStyles[styleType]) { + NSRange occurenceRange = [stylePair.rangeValue rangeValue]; + [style applyStyling:occurenceRange]; + [style reapplyAttributesFromStylePair:stylePair]; + } + } + } + // do the typing attributes management, with no selection + [self manageTypingAttributesWithOnlySelection:NO]; + + [_dirtyRanges removeAllObjects]; +} + +- (void)manageTypingAttributesWithOnlySelection:(BOOL)onlySelectionChanged { + InputTextView *textView = _input->textView; + NSRange selectedRange = textView.selectedRange; + + // Typing attributes get reset when only selection changed to an empty line + // (or empty line with newline). + if (onlySelectionChanged) { + NSRange paragraphRange = + [textView.textStorage.string paragraphRangeForRange:selectedRange]; + // User changed selection to an empty line (or empty line with a newline). + if (paragraphRange.length == 0 || + (paragraphRange.length == 1 && + [[NSCharacterSet newlineCharacterSet] + characterIsMember:[textView.textStorage.string + characterAtIndex:paragraphRange + .location]])) { + textView.typingAttributes = _input->defaultTypingAttributes; + return; + } + } + + // General typing attributes management. + + // Firstly, we make sure only default + custom + paragraph typing attribtues + // are left. + NSMutableDictionary *newAttrs = [_input->defaultTypingAttributes mutableCopy]; + + for (NSString *key in _input->textView.typingAttributes.allKeys) { + if ([_customAttributesKeys containsObject:key]) { + if ([key isEqualToString:NSParagraphStyleAttributeName]) { + // NSParagraphStyle for paragraph styles -> only keep the textLists + // property + NSParagraphStyle *pStyle = + (NSParagraphStyle *)_input->textView + .typingAttributes[NSParagraphStyleAttributeName]; + if (pStyle != nullptr && pStyle.textLists.count == 1) { + NSMutableParagraphStyle *newPStyle = + [[NSMutableParagraphStyle alloc] init]; + newPStyle.textLists = pStyle.textLists; + newAttrs[NSParagraphStyleAttributeName] = newPStyle; + } + } else { + // Inline styles -> keep the key/value as a whole + newAttrs[key] = _input->textView.typingAttributes[key]; + } + } + } + + // Then, we add typingAttributes from present inline styles. + // We check for the previous character to naturally extend typing attributes. + // getEntryIfPresent properly returns nullptr for styles that we don't want to + // extend this way. Attributes from _removedTypingAttributes aren't added + // because they were just removed. + for (StyleBase *style in _input->stylesDict.allValues) { + if ([style isParagraph]) + continue; + if ([_removedTypingAttributes containsObject:[style getKey]]) + continue; + + AttributeEntry *entry = nullptr; + + if (selectedRange.location > 0) { + entry = + [style getEntryIfPresent:NSMakeRange(selectedRange.location - 1, 1)]; + } + + if (entry == nullptr) + continue; + + newAttrs[entry.key] = entry.value; + } + + textView.typingAttributes = newAttrs; +} + +@end diff --git a/ios/extensions/LayoutManagerExtension.mm b/ios/extensions/LayoutManagerExtension.mm index 4de25c12d..9d647faf5 100644 --- a/ios/extensions/LayoutManagerExtension.mm +++ b/ios/extensions/LayoutManagerExtension.mm @@ -1,7 +1,7 @@ #import "LayoutManagerExtension.h" #import "ColorExtension.h" #import "EnrichedTextInputView.h" -#import "ParagraphsUtils.h" +#import "RangeUtils.h" #import "StyleHeaders.h" #import @@ -66,13 +66,12 @@ - (void)drawCodeBlocks:(EnrichedTextInputView *)typedInput origin:(CGPoint)origin visibleCharRange:(NSRange)visibleCharRange { CodeBlockStyle *codeBlockStyle = - typedInput->stylesDict[@([CodeBlockStyle getStyleType])]; + typedInput->stylesDict[@([CodeBlockStyle getType])]; if (codeBlockStyle == nullptr) { return; } - NSArray *allCodeBlocks = - [codeBlockStyle findAllOccurences:visibleCharRange]; + NSArray *allCodeBlocks = [codeBlockStyle all:visibleCharRange]; NSArray *mergedCodeBlocks = [self mergeContiguousStylePairs:allCodeBlocks]; UIColor *bgColor = [[typedInput->config codeBlockBgColor] @@ -86,8 +85,8 @@ - (void)drawCodeBlocks:(EnrichedTextInputView *)typedInput continue; NSArray *paragraphs = - [ParagraphsUtils getSeparateParagraphsRangesIn:typedInput->textView - range:blockCharacterRange]; + [RangeUtils getSeparateParagraphsRangesIn:typedInput->textView + range:blockCharacterRange]; if (paragraphs.count == 0) continue; @@ -205,12 +204,12 @@ - (void)drawBlockQuotes:(EnrichedTextInputView *)typedInput origin:(CGPoint)origin visibleCharRange:(NSRange)visibleCharRange { BlockQuoteStyle *bqStyle = - typedInput->stylesDict[@([BlockQuoteStyle getStyleType])]; + typedInput->stylesDict[@([BlockQuoteStyle getType])]; if (bqStyle == nullptr) { return; } - NSArray *allBlockquotes = [bqStyle findAllOccurences:visibleCharRange]; + NSArray *allBlockquotes = [bqStyle all:visibleCharRange]; for (StylePair *pair in allBlockquotes) { NSRange paragraphRange = [typedInput->textView.textStorage.string @@ -246,19 +245,20 @@ - (void)drawLists:(EnrichedTextInputView *)typedInput origin:(CGPoint)origin visibleCharRange:(NSRange)visibleCharRange { UnorderedListStyle *ulStyle = - typedInput->stylesDict[@([UnorderedListStyle getStyleType])]; + typedInput->stylesDict[@([UnorderedListStyle getType])]; OrderedListStyle *olStyle = - typedInput->stylesDict[@([OrderedListStyle getStyleType])]; + typedInput->stylesDict[@([OrderedListStyle getType])]; CheckboxListStyle *cbStyle = - typedInput->stylesDict[@([CheckboxListStyle getStyleType])]; + typedInput->stylesDict[@([CheckboxListStyle getType])]; if (ulStyle == nullptr || olStyle == nullptr || cbStyle == nullptr) { return; } NSMutableArray *allLists = [[NSMutableArray alloc] init]; - [allLists addObjectsFromArray:[ulStyle findAllOccurences:visibleCharRange]]; - [allLists addObjectsFromArray:[olStyle findAllOccurences:visibleCharRange]]; - [allLists addObjectsFromArray:[cbStyle findAllOccurences:visibleCharRange]]; + + [allLists addObjectsFromArray:[ulStyle all:visibleCharRange]]; + [allLists addObjectsFromArray:[olStyle all:visibleCharRange]]; + [allLists addObjectsFromArray:[cbStyle all:visibleCharRange]]; for (StylePair *pair in allLists) { NSParagraphStyle *pStyle = (NSParagraphStyle *)pair.styleValue; @@ -268,9 +268,9 @@ - (void)drawLists:(EnrichedTextInputView *)typedInput [typedInput->config orderedListMarkerColor] }; - NSArray *paragraphs = [ParagraphsUtils - getSeparateParagraphsRangesIn:typedInput->textView - range:[pair.rangeValue rangeValue]]; + NSArray *paragraphs = + [RangeUtils getSeparateParagraphsRangesIn:typedInput->textView + range:[pair.rangeValue rangeValue]]; for (NSValue *paragraph in paragraphs) { NSRange paragraphGlyphRange = @@ -287,8 +287,9 @@ - (void)drawLists:(EnrichedTextInputView *)typedInput pStyle.textLists.firstObject .markerFormat; - if (markerFormat == - NSTextListMarkerDecimal) { + if ([markerFormat + isEqualToString: + @"EnrichedOrderedList"]) { NSString *marker = [self getDecimalMarkerForList:typedInput charIndex: @@ -301,8 +302,10 @@ - (void)drawLists:(EnrichedTextInputView *)typedInput markerAttributes:markerAttributes origin:origin usedRect:usedRect]; - } else if (markerFormat == - NSTextListMarkerDisc) { + } else if ([markerFormat + isEqualToString: + @"EnrichedUnorderedLis" + @"t"]) { [self drawBullet:typedInput origin:origin usedRect:usedRect]; @@ -329,7 +332,7 @@ - (NSString *)getDecimalMarkerForList:(EnrichedTextInputView *)input [fullText paragraphRangeForRange:NSMakeRange(index, 0)]; if (currentParagraph.location > 0) { OrderedListStyle *olStyle = - input->stylesDict[@([OrderedListStyle getStyleType])]; + input->stylesDict[@([OrderedListStyle getType])]; NSInteger prevParagraphsCount = 0; NSInteger recentParagraphLocation = @@ -339,7 +342,7 @@ - (NSString *)getDecimalMarkerForList:(EnrichedTextInputView *)input // seek for previous lists while (true) { - if ([olStyle detectStyle:NSMakeRange(recentParagraphLocation, 0)]) { + if ([olStyle detect:NSMakeRange(recentParagraphLocation, 0)]) { prevParagraphsCount += 1; if (recentParagraphLocation > 0) { @@ -366,7 +369,7 @@ - (void)drawCheckbox:(EnrichedTextInputView *)typedInput markerFormat:(NSString *)markerFormat origin:(CGPoint)origin usedRect:(CGRect)usedRect { - BOOL isChecked = [markerFormat isEqualToString:@"{checkbox:1}"]; + BOOL isChecked = [markerFormat isEqualToString:@"EnrichedCheckbox1"]; UIImage *image = isChecked ? typedInput->config.checkboxCheckedImage : typedInput->config.checkboxUncheckedImage; @@ -406,7 +409,7 @@ - (void)drawDecimal:(EnrichedTextInputView *)typedInput usedRect:(CGRect)usedRect { CGFloat gapWidth = [typedInput->config orderedListGapWidth]; CGFloat markerWidth = [marker sizeWithAttributes:markerAttributes].width; - CGFloat markerX = usedRect.origin.x - gapWidth - markerWidth / 2; + CGFloat markerX = origin.x + usedRect.origin.x - gapWidth - markerWidth / 2; [marker drawAtPoint:CGPointMake(markerX, usedRect.origin.y + origin.y) withAttributes:markerAttributes]; diff --git a/ios/inputParser/InputParser.mm b/ios/inputParser/InputParser.mm index a2558c190..3806037e5 100644 --- a/ios/inputParser/InputParser.mm +++ b/ios/inputParser/InputParser.mm @@ -45,8 +45,8 @@ - (NSString *)parseToHtmlFromRange:(NSRange)range { // check each existing style existence for (NSNumber *type in _input->stylesDict) { - id style = _input->stylesDict[type]; - if ([style detectStyle:currentRange]) { + StyleBase *style = _input->stylesDict[type]; + if ([style detect:currentRange]) { [currentActiveStyles addObject:type]; if (![previousActiveStyles member:type]) { @@ -70,8 +70,7 @@ - (NSString *)parseToHtmlFromRange:(NSRange)range { // 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)]; + BOOL detected = [oStyle detect:NSMakeRange(currentRange.location, 0)]; if (detected) { [result appendString:@"\n
  • "]; } else { @@ -80,8 +79,7 @@ - (NSString *)parseToHtmlFromRange:(NSRange)range { } } else if (inUnorderedList) { UnorderedListStyle *uStyle = _input->stylesDict[@(UnorderedList)]; - BOOL detected = - [uStyle detectStyle:NSMakeRange(currentRange.location, 0)]; + BOOL detected = [uStyle detect:NSMakeRange(currentRange.location, 0)]; if (detected) { [result appendString:@"\n
  • "]; } else { @@ -91,7 +89,7 @@ - (NSString *)parseToHtmlFromRange:(NSRange)range { } else if (inBlockQuote) { BlockQuoteStyle *bqStyle = _input->stylesDict[@(BlockQuote)]; BOOL detected = - [bqStyle detectStyle:NSMakeRange(currentRange.location, 0)]; + [bqStyle detect:NSMakeRange(currentRange.location, 0)]; if (detected) { [result appendString:@"\n
    "]; } else { @@ -101,7 +99,7 @@ - (NSString *)parseToHtmlFromRange:(NSRange)range { } else if (inCodeBlock) { CodeBlockStyle *cbStyle = _input->stylesDict[@(CodeBlock)]; BOOL detected = - [cbStyle detectStyle:NSMakeRange(currentRange.location, 0)]; + [cbStyle detect:NSMakeRange(currentRange.location, 0)]; if (detected) { [result appendString:@"\n
    "]; } else { @@ -111,7 +109,7 @@ - (NSString *)parseToHtmlFromRange:(NSRange)range { } else if (inCheckboxList) { CheckboxListStyle *cbLStyle = _input->stylesDict[@(CheckboxList)]; BOOL detected = - [cbLStyle detectStyle:NSMakeRange(currentRange.location, 0)]; + [cbLStyle detect:NSMakeRange(currentRange.location, 0)]; if (detected) { BOOL checked = [cbLStyle getCheckboxStateAt:currentRange.location]; if (checked) { @@ -149,21 +147,20 @@ - (NSString *)parseToHtmlFromRange:(NSRange)range { // 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:@([H4Style getStyleType])] || - [previousActiveStyles containsObject:@([H5Style getStyleType])] || - [previousActiveStyles containsObject:@([H6Style getStyleType])] || + containsObject:@([UnorderedListStyle getType])] || [previousActiveStyles - containsObject:@([BlockQuoteStyle getStyleType])] || + containsObject:@([OrderedListStyle getType])] || + [previousActiveStyles containsObject:@([H1Style getType])] || + [previousActiveStyles containsObject:@([H2Style getType])] || + [previousActiveStyles containsObject:@([H3Style getType])] || + [previousActiveStyles containsObject:@([H4Style getType])] || + [previousActiveStyles containsObject:@([H5Style getType])] || + [previousActiveStyles containsObject:@([H6Style getType])] || [previousActiveStyles - containsObject:@([CodeBlockStyle getStyleType])] || + containsObject:@([BlockQuoteStyle getType])] || + [previousActiveStyles containsObject:@([CodeBlockStyle getType])] || [previousActiveStyles - containsObject:@([CheckboxListStyle getStyleType])]) { + containsObject:@([CheckboxListStyle getType])]) { // do nothing, proper closing paragraph tags have been already // appended } else { @@ -184,35 +181,33 @@ - (NSString *)parseToHtmlFromRange:(NSRange)range { // handle ending unordered list if (inUnorderedList && ![currentActiveStyles - containsObject:@([UnorderedListStyle getStyleType])]) { + containsObject:@([UnorderedListStyle getType])]) { inUnorderedList = NO; [result appendString:@"\n"]; } // handle ending ordered list if (inOrderedList && ![currentActiveStyles - containsObject:@([OrderedListStyle getStyleType])]) { + containsObject:@([OrderedListStyle getType])]) { inOrderedList = NO; [result appendString:@"\n"]; } // handle ending blockquotes - if (inBlockQuote && - ![currentActiveStyles - containsObject:@([BlockQuoteStyle getStyleType])]) { + if (inBlockQuote && ![currentActiveStyles + containsObject:@([BlockQuoteStyle getType])]) { inBlockQuote = NO; [result appendString:@"\n"]; } // handle ending codeblock if (inCodeBlock && - ![currentActiveStyles - containsObject:@([CodeBlockStyle getStyleType])]) { + ![currentActiveStyles containsObject:@([CodeBlockStyle getType])]) { inCodeBlock = NO; [result appendString:@"\n"]; } // handle ending checkbox list if (inCheckboxList && ![currentActiveStyles - containsObject:@([CheckboxListStyle getStyleType])]) { + containsObject:@([CheckboxListStyle getType])]) { inCheckboxList = NO; [result appendString:@"\n"]; } @@ -220,56 +215,52 @@ - (NSString *)parseToHtmlFromRange:(NSRange)range { // handle starting unordered list if (!inUnorderedList && [currentActiveStyles - containsObject:@([UnorderedListStyle getStyleType])]) { + containsObject:@([UnorderedListStyle getType])]) { inUnorderedList = YES; [result appendString:@"\n
      "]; } // handle starting ordered list if (!inOrderedList && [currentActiveStyles - containsObject:@([OrderedListStyle getStyleType])]) { + containsObject:@([OrderedListStyle getType])]) { inOrderedList = YES; [result appendString:@"\n
        "]; } // handle starting blockquotes if (!inBlockQuote && - [currentActiveStyles - containsObject:@([BlockQuoteStyle getStyleType])]) { + [currentActiveStyles containsObject:@([BlockQuoteStyle getType])]) { inBlockQuote = YES; [result appendString:@"\n
        "]; } // handle starting codeblock if (!inCodeBlock && - [currentActiveStyles - containsObject:@([CodeBlockStyle getStyleType])]) { + [currentActiveStyles containsObject:@([CodeBlockStyle getType])]) { inCodeBlock = YES; [result appendString:@"\n"]; } // handle starting checkbox list if (!inCheckboxList && [currentActiveStyles - containsObject:@([CheckboxListStyle getStyleType])]) { + containsObject:@([CheckboxListStyle getType])]) { inCheckboxList = 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:@([H4Style getStyleType])] || - [currentActiveStyles containsObject:@([H5Style getStyleType])] || - [currentActiveStyles containsObject:@([H6Style getStyleType])] || + containsObject:@([UnorderedListStyle getType])] || [currentActiveStyles - containsObject:@([BlockQuoteStyle getStyleType])] || + containsObject:@([OrderedListStyle getType])] || + [currentActiveStyles containsObject:@([H1Style getType])] || + [currentActiveStyles containsObject:@([H2Style getType])] || + [currentActiveStyles containsObject:@([H3Style getType])] || + [currentActiveStyles containsObject:@([H4Style getType])] || + [currentActiveStyles containsObject:@([H5Style getType])] || + [currentActiveStyles containsObject:@([H6Style getType])] || + [currentActiveStyles containsObject:@([BlockQuoteStyle getType])] || + [currentActiveStyles containsObject:@([CodeBlockStyle getType])] || [currentActiveStyles - containsObject:@([CodeBlockStyle getStyleType])] || - [currentActiveStyles - containsObject:@([CheckboxListStyle getStyleType])]) { + containsObject:@([CheckboxListStyle getType])]) { [result appendString:@"\n"]; } else { [result appendString:@"\n

          "]; @@ -403,33 +394,26 @@ - (NSString *)parseToHtmlFromRange:(NSRange)range { // finish the paragraph // handle ending of some paragraph styles - if ([previousActiveStyles - containsObject:@([UnorderedListStyle getStyleType])]) { + if ([previousActiveStyles containsObject:@([UnorderedListStyle getType])]) { [result appendString:@"\n

        "]; } else if ([previousActiveStyles - containsObject:@([OrderedListStyle getStyleType])]) { + containsObject:@([OrderedListStyle getType])]) { [result appendString:@"\n
      "]; } else if ([previousActiveStyles - containsObject:@([BlockQuoteStyle getStyleType])]) { + containsObject:@([BlockQuoteStyle getType])]) { [result appendString:@"\n"]; } else if ([previousActiveStyles - containsObject:@([CodeBlockStyle getStyleType])]) { + containsObject:@([CodeBlockStyle getType])]) { [result appendString:@"\n"]; } else if ([previousActiveStyles - containsObject:@([CheckboxListStyle getStyleType])]) { + containsObject:@([CheckboxListStyle getType])]) { [result appendString:@"\n
    "]; - } else if ([previousActiveStyles - containsObject:@([H1Style getStyleType])] || - [previousActiveStyles - containsObject:@([H2Style getStyleType])] || - [previousActiveStyles - containsObject:@([H3Style getStyleType])] || - [previousActiveStyles - containsObject:@([H4Style getStyleType])] || - [previousActiveStyles - containsObject:@([H5Style getStyleType])] || - [previousActiveStyles - containsObject:@([H6Style getStyleType])]) { + } else if ([previousActiveStyles containsObject:@([H1Style getType])] || + [previousActiveStyles containsObject:@([H2Style getType])] || + [previousActiveStyles containsObject:@([H3Style getType])] || + [previousActiveStyles containsObject:@([H4Style getType])] || + [previousActiveStyles containsObject:@([H5Style getType])] || + [previousActiveStyles containsObject:@([H6Style getType])]) { // do nothing, heading closing tag has already been appended } else { [result appendString:@"

    "]; @@ -484,9 +468,9 @@ - (NSString *)parseToHtmlFromRange:(NSRange)range { - (NSString *)tagContentForStyle:(NSNumber *)style openingTag:(BOOL)openingTag location:(NSInteger)location { - if ([style isEqualToNumber:@([BoldStyle getStyleType])]) { + if ([style isEqualToNumber:@([BoldStyle getType])]) { return @"b"; - } else if ([style isEqualToNumber:@([ItalicStyle getStyleType])]) { + } else if ([style isEqualToNumber:@([ItalicStyle getType])]) { return @"i"; } else if ([style isEqualToNumber:@([ImageStyle getStyleType])]) { if (openingTag) { @@ -504,11 +488,11 @@ - (NSString *)tagContentForStyle:(NSNumber *)style } else { return @""; } - } else if ([style isEqualToNumber:@([UnderlineStyle getStyleType])]) { + } else if ([style isEqualToNumber:@([UnderlineStyle getType])]) { return @"u"; - } else if ([style isEqualToNumber:@([StrikethroughStyle getStyleType])]) { + } else if ([style isEqualToNumber:@([StrikethroughStyle getType])]) { return @"s"; - } else if ([style isEqualToNumber:@([InlineCodeStyle getStyleType])]) { + } else if ([style isEqualToNumber:@([InlineCodeStyle getType])]) { return @"code"; } else if ([style isEqualToNumber:@([LinkStyle getStyleType])]) { if (openingTag) { @@ -562,26 +546,26 @@ - (NSString *)tagContentForStyle:(NSNumber *)style } else { return @"mention"; } - } else if ([style isEqualToNumber:@([H1Style getStyleType])]) { + } else if ([style isEqualToNumber:@([H1Style getType])]) { return @"h1"; - } else if ([style isEqualToNumber:@([H2Style getStyleType])]) { + } else if ([style isEqualToNumber:@([H2Style getType])]) { return @"h2"; - } else if ([style isEqualToNumber:@([H3Style getStyleType])]) { + } else if ([style isEqualToNumber:@([H3Style getType])]) { return @"h3"; - } else if ([style isEqualToNumber:@([H4Style getStyleType])]) { + } else if ([style isEqualToNumber:@([H4Style getType])]) { return @"h4"; - } else if ([style isEqualToNumber:@([H5Style getStyleType])]) { + } else if ([style isEqualToNumber:@([H5Style getType])]) { return @"h5"; - } else if ([style isEqualToNumber:@([H6Style getStyleType])]) { + } else if ([style isEqualToNumber:@([H6Style getType])]) { return @"h6"; - } else if ([style isEqualToNumber:@([UnorderedListStyle getStyleType])] || - [style isEqualToNumber:@([OrderedListStyle getStyleType])]) { + } else if ([style isEqualToNumber:@([UnorderedListStyle getType])] || + [style isEqualToNumber:@([OrderedListStyle getType])]) { return @"li"; - } else if ([style isEqualToNumber:@([CheckboxListStyle getStyleType])]) { + } else if ([style isEqualToNumber:@([CheckboxListStyle getType])]) { if (openingTag) { CheckboxListStyle *checkboxListStyleClass = (CheckboxListStyle *) - _input->stylesDict[@([CheckboxListStyle getStyleType])]; + _input->stylesDict[@([CheckboxListStyle getType])]; BOOL checked = [checkboxListStyleClass getCheckboxStateAt:location]; if (checked) { @@ -591,8 +575,8 @@ - (NSString *)tagContentForStyle:(NSNumber *)style } else { return @"li"; } - } else if ([style isEqualToNumber:@([BlockQuoteStyle getStyleType])] || - [style isEqualToNumber:@([CodeBlockStyle getStyleType])]) { + } else if ([style isEqualToNumber:@([BlockQuoteStyle getType])] || + [style isEqualToNumber:@([CodeBlockStyle getType])]) { // blockquotes and codeblock use

    tags the same way lists use

  • return @"p"; } @@ -689,8 +673,7 @@ - (void)applyProcessedStyles:(NSArray *)processedStyles [((ImageStyle *)baseStyle) addImageAtRange:styleRange imageData:imgData withSelection:NO]; - } else if ([styleType - isEqualToNumber:@([CheckboxListStyle getStyleType])]) { + } else if ([styleType isEqualToNumber:@([CheckboxListStyle getType])]) { NSDictionary *checkboxStates = (NSDictionary *)stylePair.styleValue; CheckboxListStyle *cbLStyle = (CheckboxListStyle *)baseStyle; @@ -698,7 +681,10 @@ - (void)applyProcessedStyles:(NSArray *)processedStyles // unchecked value BOOL shouldAddTypingAttr = styleRange.location + styleRange.length == plainTextLength; - [cbLStyle addAttributes:styleRange withTypingAttr:shouldAddTypingAttr]; + [cbLStyle addWithChecked:NO + range:styleRange + withTyping:shouldAddTypingAttr + withDirtyRange:YES]; if (!checkboxStates && checkboxStates.count == 0) { continue; @@ -1226,9 +1212,9 @@ - (NSArray *)getTextAndStylesFromHtml:(NSString *)fixedHtml { NSMutableArray *styleArr = [[NSMutableArray alloc] init]; StylePair *stylePair = [[StylePair alloc] init]; if ([tagName isEqualToString:@"b"]) { - [styleArr addObject:@([BoldStyle getStyleType])]; + [styleArr addObject:@([BoldStyle getType])]; } else if ([tagName isEqualToString:@"i"]) { - [styleArr addObject:@([ItalicStyle getStyleType])]; + [styleArr addObject:@([ItalicStyle getType])]; } else if ([tagName isEqualToString:@"img"]) { NSRegularExpression *srcRegex = [NSRegularExpression regularExpressionWithPattern:@"src=\"([^\"]+)\"" @@ -1284,11 +1270,11 @@ - (NSArray *)getTextAndStylesFromHtml:(NSString *)fixedHtml { stylePair.styleValue = imageData; } else if ([tagName isEqualToString:@"u"]) { - [styleArr addObject:@([UnderlineStyle getStyleType])]; + [styleArr addObject:@([UnderlineStyle getType])]; } else if ([tagName isEqualToString:@"s"]) { - [styleArr addObject:@([StrikethroughStyle getStyleType])]; + [styleArr addObject:@([StrikethroughStyle getType])]; } else if ([tagName isEqualToString:@"code"]) { - [styleArr addObject:@([InlineCodeStyle getStyleType])]; + [styleArr addObject:@([InlineCodeStyle getType])]; } else if ([tagName isEqualToString:@"a"]) { NSRegularExpression *hrefRegex = [NSRegularExpression regularExpressionWithPattern:@"href=\".+\"" @@ -1354,33 +1340,33 @@ - (NSArray *)getTextAndStylesFromHtml:(NSString *)fixedHtml { } else if ([[tagName substringWithRange:NSMakeRange(0, 1)] isEqualToString:@"h"]) { if ([tagName isEqualToString:@"h1"]) { - [styleArr addObject:@([H1Style getStyleType])]; + [styleArr addObject:@([H1Style getType])]; } else if ([tagName isEqualToString:@"h2"]) { - [styleArr addObject:@([H2Style getStyleType])]; + [styleArr addObject:@([H2Style getType])]; } else if ([tagName isEqualToString:@"h3"]) { - [styleArr addObject:@([H3Style getStyleType])]; + [styleArr addObject:@([H3Style getType])]; } else if ([tagName isEqualToString:@"h4"]) { - [styleArr addObject:@([H4Style getStyleType])]; + [styleArr addObject:@([H4Style getType])]; } else if ([tagName isEqualToString:@"h5"]) { - [styleArr addObject:@([H5Style getStyleType])]; + [styleArr addObject:@([H5Style getType])]; } else if ([tagName isEqualToString:@"h6"]) { - [styleArr addObject:@([H6Style getStyleType])]; + [styleArr addObject:@([H6Style getType])]; } } else if ([tagName isEqualToString:@"ul"]) { if ([self isUlCheckboxList:params]) { - [styleArr addObject:@([CheckboxListStyle getStyleType])]; + [styleArr addObject:@([CheckboxListStyle getType])]; stylePair.styleValue = [self prepareCheckboxListStyleValue:tagRangeValue checkboxStates:checkboxStates]; } else { - [styleArr addObject:@([UnorderedListStyle getStyleType])]; + [styleArr addObject:@([UnorderedListStyle getType])]; } } else if ([tagName isEqualToString:@"ol"]) { - [styleArr addObject:@([OrderedListStyle getStyleType])]; + [styleArr addObject:@([OrderedListStyle getType])]; } else if ([tagName isEqualToString:@"blockquote"]) { - [styleArr addObject:@([BlockQuoteStyle getStyleType])]; + [styleArr addObject:@([BlockQuoteStyle getType])]; } else if ([tagName isEqualToString:@"codeblock"]) { - [styleArr addObject:@([CodeBlockStyle getStyleType])]; + [styleArr addObject:@([CodeBlockStyle getType])]; } else { // some other external tags like span just don't get put into the // processed styles diff --git a/ios/interfaces/AttributeEntry.h b/ios/interfaces/AttributeEntry.h new file mode 100644 index 000000000..798823e7a --- /dev/null +++ b/ios/interfaces/AttributeEntry.h @@ -0,0 +1,9 @@ +#pragma once +#import + +@interface AttributeEntry : NSObject + +@property NSString *key; +@property id value; + +@end diff --git a/ios/interfaces/AttributeEntry.mm b/ios/interfaces/AttributeEntry.mm new file mode 100644 index 000000000..66ab1fadb --- /dev/null +++ b/ios/interfaces/AttributeEntry.mm @@ -0,0 +1,4 @@ +#import "AttributeEntry.h" + +@implementation AttributeEntry +@end diff --git a/ios/interfaces/StyleBase.h b/ios/interfaces/StyleBase.h new file mode 100644 index 000000000..a404b9f93 --- /dev/null +++ b/ios/interfaces/StyleBase.h @@ -0,0 +1,36 @@ +#pragma once +#import "AttributeEntry.h" +#import "StylePair.h" +#import "StyleTypeEnum.h" +#import + +@class EnrichedTextInputView; + +@interface StyleBase : NSObject +@property(nonatomic, weak) EnrichedTextInputView *input; ++ (StyleType)getType; +- (NSString *)getKey; +- (NSString *)getValue; +- (BOOL)isParagraph; +- (BOOL)needsZWS; +- (instancetype)initWithInput:(EnrichedTextInputView *)input; +- (NSRange)actualUsedRange:(NSRange)range; +- (void)toggle:(NSRange)range; +- (void)add:(NSRange)range + withTyping:(BOOL)withTyping + withDirtyRange:(BOOL)withDirtyRange; +- (void)add:(NSRange)range + withValue:(NSString *)value + withTyping:(BOOL)withTyping + withDirtyRange:(BOOL)withDirtyRange; +- (void)remove:(NSRange)range withDirtyRange:(BOOL)withDirtyRange; +- (void)addTypingWithValue:(NSString *)value; +- (void)removeTyping; +- (BOOL)styleCondition:(id)value range:(NSRange)range; +- (BOOL)detect:(NSRange)range; +- (BOOL)any:(NSRange)range; +- (NSArray *)all:(NSRange)range; +- (void)applyStyling:(NSRange)range; +- (void)reapplyAttributesFromStylePair:(StylePair *)pair; +- (AttributeEntry *)getEntryIfPresent:(NSRange)range; +@end diff --git a/ios/interfaces/StyleBase.mm b/ios/interfaces/StyleBase.mm new file mode 100644 index 000000000..0b1e1ec3e --- /dev/null +++ b/ios/interfaces/StyleBase.mm @@ -0,0 +1,257 @@ +#import "StyleBase.h" +#import "AttributeEntry.h" +#import "EnrichedTextInputView.h" +#import "OccurenceUtils.h" +#import "RangeUtils.h" +#import "ZeroWidthSpaceUtils.h" + +@implementation StyleBase + +// This method gets overridden ++ (StyleType)getType { + return None; +} + +// This method gets overridden +- (NSString *)getKey { + if ([self isParagraph]) { + return NSParagraphStyleAttributeName; + } + return @"NoneAttribute"; +} + +// Basic inline styles will use this default value, paragraph styles will +// override it and parametrised ones completely don't use it +- (NSString *)getValue { + return @"AnyValue"; +} + +// This method gets overridden +- (BOOL)isParagraph { + return false; +} + +- (BOOL)needsZWS { + return NO; +} + +- (instancetype)initWithInput:(EnrichedTextInputView *)input { + self = [super init]; + _input = input; + return self; +} + +// aligns range to whole paragraph for the paragraph stlyes +- (NSRange)actualUsedRange:(NSRange)range { + if (![self isParagraph]) + return range; + return [_input->textView.textStorage.string paragraphRangeForRange:range]; +} + +- (void)toggle:(NSRange)range { + NSRange actualRange = [self actualUsedRange:range]; + + BOOL isPresent = [self detect:actualRange]; + if (actualRange.length >= 1) { + isPresent ? [self remove:actualRange withDirtyRange:YES] + : [self add:actualRange withTyping:YES withDirtyRange:YES]; + } else { + isPresent ? [self removeTyping] : [self addTypingWithValue:[self getValue]]; + } +} + +- (void)add:(NSRange)range + withTyping:(BOOL)withTyping + withDirtyRange:(BOOL)withDirtyRange { + [self add:range + withValue:[self getValue] + withTyping:withTyping + withDirtyRange:withDirtyRange]; +} + +- (void)add:(NSRange)range + withValue:(NSString *)value + withTyping:(BOOL)withTyping + withDirtyRange:(BOOL)withDirtyRange { + NSRange actualRange = [self actualUsedRange:range]; + + if (![self isParagraph]) { + [_input->textView.textStorage addAttribute:[self getKey] + value:value + range:actualRange]; + } else { + [_input->textView.textStorage + enumerateAttribute:NSParagraphStyleAttributeName + inRange:actualRange + options:0 + usingBlock:^(id _Nullable existingValue, NSRange subRange, + BOOL *_Nonnull stop) { + NSMutableParagraphStyle *pStyle = + [(NSParagraphStyle *)existingValue mutableCopy]; + if (pStyle == nullptr) + return; + pStyle.textLists = + @[ [[NSTextList alloc] initWithMarkerFormat:value + options:0] ]; + [_input->textView.textStorage + addAttribute:NSParagraphStyleAttributeName + value:pStyle + range:subRange]; + }]; + + // Only paragraph styles need additional typing attributes when toggling. + if (withTyping) { + [self addTypingWithValue:value]; + } + } + + // Notify attributes manager of styling to be re-done if needed. + if (withDirtyRange) { + [self.input->attributesManager addDirtyRange:actualRange]; + } +} + +- (void)remove:(NSRange)range withDirtyRange:(BOOL)withDirtyRange { + NSRange actualRange = [self actualUsedRange:range]; + + if (![self isParagraph]) { + [_input->textView.textStorage removeAttribute:[self getKey] + range:actualRange]; + } else { + [_input->textView.textStorage + enumerateAttribute:NSParagraphStyleAttributeName + inRange:actualRange + options:0 + usingBlock:^(id _Nullable existingValue, NSRange subRange, + BOOL *_Nonnull stop) { + NSMutableParagraphStyle *pStyle = + [(NSParagraphStyle *)existingValue mutableCopy]; + if (pStyle == nullptr) + return; + pStyle.textLists = @[]; + [_input->textView.textStorage + addAttribute:NSParagraphStyleAttributeName + value:pStyle + range:subRange]; + }]; + + // Paragraph styles also need typing attributes removal. + [self removeTyping]; + } + + // Notify attributes manager of styling to be re-done if needed. + if (withDirtyRange) { + [self.input->attributesManager addDirtyRange:actualRange]; + } +} + +- (void)addTypingWithValue:(NSString *)value { + NSMutableDictionary *newTypingAttrs = + [_input->textView.typingAttributes mutableCopy]; + + if (![self isParagraph]) { + newTypingAttrs[[self getKey]] = value; + } else { + NSMutableParagraphStyle *pStyle = + [newTypingAttrs[NSParagraphStyleAttributeName] mutableCopy]; + pStyle.textLists = @[ [[NSTextList alloc] initWithMarkerFormat:value + options:0] ]; + newTypingAttrs[NSParagraphStyleAttributeName] = pStyle; + } + + _input->textView.typingAttributes = newTypingAttrs; +} + +- (void)removeTyping { + NSMutableDictionary *newTypingAttrs = + [_input->textView.typingAttributes mutableCopy]; + + if (![self isParagraph]) { + [newTypingAttrs removeObjectForKey:[self getKey]]; + // attributes manager also needs to be notified of custom attributes that + // shouldn't be extended + [_input->attributesManager didRemoveTypingAttribute:[self getKey]]; + } else { + NSMutableParagraphStyle *pStyle = + [newTypingAttrs[NSParagraphStyleAttributeName] mutableCopy]; + pStyle.textLists = @[]; + newTypingAttrs[NSParagraphStyleAttributeName] = pStyle; + } + + _input->textView.typingAttributes = newTypingAttrs; +} + +- (BOOL)styleCondition:(id)value range:(NSRange)range { + if (![self isParagraph]) { + NSString *valueString = (NSString *)value; + return valueString != nullptr && + [valueString isEqualToString:[self getValue]]; + } else { + NSParagraphStyle *pStyle = (NSParagraphStyle *)value; + return pStyle != nullptr && [pStyle.textLists.firstObject.markerFormat + isEqualToString:[self getValue]]; + } +} + +- (BOOL)detect:(NSRange)range { + if (range.length >= 1) { + return [OccurenceUtils detect:[self getKey] + withInput:_input + inRange:range + withCondition:^BOOL(id _Nullable value, NSRange range) { + return [self styleCondition:value range:range]; + }]; + } else { + return [OccurenceUtils detect:[self getKey] + withInput:_input + atIndex:range.location + checkPrevious:[self isParagraph] + withCondition:^BOOL(id _Nullable value, NSRange range) { + return [self styleCondition:value range:range]; + }]; + } +} + +- (BOOL)any:(NSRange)range { + return [OccurenceUtils any:[self getKey] + withInput:_input + inRange:range + withCondition:^BOOL(id _Nullable value, NSRange range) { + return [self styleCondition:value range:range]; + }]; +} + +- (NSArray *)all:(NSRange)range { + return [OccurenceUtils all:[self getKey] + withInput:_input + inRange:range + withCondition:^BOOL(id _Nullable value, NSRange range) { + return [self styleCondition:value range:range]; + }]; +} + +// This method gets overridden +- (void)applyStyling:(NSRange)range { +} + +// Called during dirty range re-application to restore a style from a saved +// StylePair +- (void)reapplyAttributesFromStylePair:(StylePair *)pair { + NSRange range = [pair.rangeValue rangeValue]; + [self add:range withTyping:NO withDirtyRange:NO]; +} + +// Gets a custom attribtue entry for the typingAttributes. +// Only used with inline styles. +- (AttributeEntry *)getEntryIfPresent:(NSRange)range { + if (![self detect:range]) { + return nullptr; + } + + AttributeEntry *entry = [[AttributeEntry alloc] init]; + entry.key = [self getKey]; + entry.value = [self getValue]; + return entry; +} + +@end diff --git a/ios/interfaces/StyleHeaders.h b/ios/interfaces/StyleHeaders.h index 3adeee7b5..5cb1e3ded 100644 --- a/ios/interfaces/StyleHeaders.h +++ b/ios/interfaces/StyleHeaders.h @@ -3,21 +3,21 @@ #import "ImageData.h" #import "LinkData.h" #import "MentionParams.h" +#import "StyleBase.h" -@interface BoldStyle : NSObject +@interface BoldStyle : StyleBase @end -@interface ItalicStyle : NSObject +@interface ItalicStyle : StyleBase @end -@interface UnderlineStyle : NSObject +@interface UnderlineStyle : StyleBase @end -@interface StrikethroughStyle : NSObject +@interface StrikethroughStyle : StyleBase @end -@interface InlineCodeStyle : NSObject -- (void)handleNewlines; +@interface InlineCodeStyle : StyleBase @end @interface LinkStyle : NSObject @@ -51,14 +51,10 @@ - (NSValue *)getActiveMentionRange; @end -@interface HeadingStyleBase : NSObject { - id input; -} +@interface HeadingStyleBase : StyleBase - (CGFloat)getHeadingFontSize; - (BOOL)isHeadingBold; - (BOOL)handleNewlinesInRange:(NSRange)range replacementText:(NSString *)text; -- (void)handleImproperHeadings; -@property(nonatomic, assign) CGFloat lastAppliedFontSize; @end @interface H1Style : HeadingStyleBase @@ -79,37 +75,31 @@ @interface H6Style : HeadingStyleBase @end -@interface UnorderedListStyle : NSObject -- (BOOL)handleBackspaceInRange:(NSRange)range replacementText:(NSString *)text; +@interface UnorderedListStyle : StyleBase - (BOOL)tryHandlingListShorcutInRange:(NSRange)range replacementText:(NSString *)text; @end -@interface OrderedListStyle : NSObject -- (BOOL)handleBackspaceInRange:(NSRange)range replacementText:(NSString *)text; +@interface OrderedListStyle : StyleBase - (BOOL)tryHandlingListShorcutInRange:(NSRange)range replacementText:(NSString *)text; @end -@interface CheckboxListStyle : NSObject -- (void)applyStyleWithCheckedValue:(BOOL)checked inRange:(NSRange)range; -- (BOOL)handleBackspaceInRange:(NSRange)range replacementText:(NSString *)text; -- (BOOL)getCheckboxStateAt:(NSUInteger)location; +@interface CheckboxListStyle : StyleBase +- (void)toggleWithChecked:(BOOL)checked range:(NSRange)range; +- (void)addWithChecked:(BOOL)checked + range:(NSRange)range + withTyping:(BOOL)withTyping + withDirtyRange:(BOOL)withDirtyRange; - (void)toggleCheckedAt:(NSUInteger)location; +- (BOOL)getCheckboxStateAt:(NSUInteger)location; - (BOOL)handleNewlinesInRange:(NSRange)range replacementText:(NSString *)text; -- (void)addAttributesWithCheckedValue:(BOOL)checked - inRange:(NSRange)range - withTypingAttr:(BOOL)withTypingAttr; @end -@interface BlockQuoteStyle : NSObject -- (BOOL)handleBackspaceInRange:(NSRange)range replacementText:(NSString *)text; -- (void)manageBlockquoteColor; +@interface BlockQuoteStyle : StyleBase @end -@interface CodeBlockStyle : NSObject -- (void)manageCodeBlockFontAndColor; -- (BOOL)handleBackspaceInRange:(NSRange)range replacementText:(NSString *)text; +@interface CodeBlockStyle : StyleBase @end @interface ImageStyle : NSObject diff --git a/ios/styles/BlockQuoteStyle.mm b/ios/styles/BlockQuoteStyle.mm index 0f1a6fb67..fc3b02896 100644 --- a/ios/styles/BlockQuoteStyle.mm +++ b/ios/styles/BlockQuoteStyle.mm @@ -1,293 +1,58 @@ #import "ColorExtension.h" #import "EnrichedTextInputView.h" -#import "OccurenceUtils.h" -#import "ParagraphsUtils.h" #import "StyleHeaders.h" -#import "TextInsertionUtils.h" -@implementation BlockQuoteStyle { - EnrichedTextInputView *_input; - NSArray *_stylesToExclude; -} +@implementation BlockQuoteStyle -+ (StyleType)getStyleType { ++ (StyleType)getType { return BlockQuote; } -+ (BOOL)isParagraphStyle { - return YES; -} - -- (instancetype)initWithInput:(id)input { - self = [super init]; - _input = (EnrichedTextInputView *)input; - _stylesToExclude = @[ @(InlineCode), @(Mention), @(Link) ]; - return self; -} - -- (CGFloat)getHeadIndent { - // rectangle width + gap - return [_input->config blockquoteBorderWidth] + - [_input->config blockquoteGapWidth]; +- (NSString *)getValue { + return @"EnrichedBlockQuote"; } -// the range will already be the full paragraph/s range -- (void)applyStyle:(NSRange)range { - BOOL isStylePresent = [self detectStyle:range]; - if (range.length >= 1) { - isStylePresent ? [self removeAttributes:range] - : [self addAttributes:range withTypingAttr:YES]; - } else { - isStylePresent ? [self removeTypingAttributes] : [self addTypingAttributes]; - } -} - -- (void)addAttributes:(NSRange)range withTypingAttr:(BOOL)withTypingAttr { - NSArray *paragraphs = - [ParagraphsUtils getSeparateParagraphsRangesIn:_input->textView - range:range]; - // if we fill empty lines with zero width spaces, we need to offset later - // ranges - NSInteger offset = 0; - NSRange preModificationRange = _input->textView.selectedRange; - - // to not emit any space filling selection/text changes - _input->blockEmitting = YES; - - 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:[_input->textView.textStorage.string - characterAtIndex:pRange.location]])) { - [TextInsertionUtils insertText:@"\u200B" - at:pRange.location - additionalAttributes:nullptr - input:_input - withSelection:NO]; - pRange = NSMakeRange(pRange.location, pRange.length + 1); - offset += 1; - } - - [_input->textView.textStorage - enumerateAttribute:NSParagraphStyleAttributeName - inRange:pRange - options:0 - usingBlock:^(id _Nullable value, NSRange range, - BOOL *_Nonnull stop) { - NSMutableParagraphStyle *pStyle = - [(NSParagraphStyle *)value mutableCopy]; - pStyle.headIndent = [self getHeadIndent]; - pStyle.firstLineHeadIndent = [self getHeadIndent]; - [_input->textView.textStorage - addAttribute:NSParagraphStyleAttributeName - value:pStyle - range:range]; - }]; - } - - // back to emitting - _input->blockEmitting = NO; - - if (preModificationRange.length == 0) { - // fix selection if only one line was possibly made a list and filled with a - // space - _input->textView.selectedRange = preModificationRange; - } else { - // in other cases, fix the selection with newly made offsets - _input->textView.selectedRange = NSMakeRange( - preModificationRange.location, preModificationRange.length + offset); - } - - // 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; - } -} - -// does pretty much the same as addAttributes -- (void)addTypingAttributes { - [self addAttributes:_input->textView.selectedRange withTypingAttr:YES]; -} - -- (void)removeAttributes:(NSRange)range { - NSArray *paragraphs = - [ParagraphsUtils getSeparateParagraphsRangesIn:_input->textView - 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.headIndent = 0; - pStyle.firstLineHeadIndent = 0; - [_input->textView.textStorage - addAttribute:NSParagraphStyleAttributeName - value:pStyle - range:range]; - }]; - } - - // also remove typing attributes - NSMutableDictionary *typingAttrs = - [_input->textView.typingAttributes mutableCopy]; - NSMutableParagraphStyle *pStyle = - [typingAttrs[NSParagraphStyleAttributeName] mutableCopy]; - pStyle.headIndent = 0; - pStyle.firstLineHeadIndent = 0; - typingAttrs[NSParagraphStyleAttributeName] = pStyle; - _input->textView.typingAttributes = typingAttrs; -} - -// needed for the sake of style conflicts, needs to do exactly the same as -// removeAttribtues -- (void)removeTypingAttributes { - [self removeAttributes:_input->textView.selectedRange]; -} - -- (BOOL)handleBackspaceInRange:(NSRange)range replacementText:(NSString *)text { - if ([self detectStyle:_input->textView.selectedRange] && text.length == 0) { - // backspace while the style is active - - NSRange paragraphRange = [_input->textView.textStorage.string - paragraphRangeForRange:_input->textView.selectedRange]; - - if (NSEqualRanges(_input->textView.selectedRange, NSMakeRange(0, 0))) { - // a backspace on the very first input's line quote - // it doesn't run textVieDidChange so we need to manually remove - // attributes - [self removeAttributes:paragraphRange]; - return YES; - } else if (range.location == paragraphRange.location - 1) { - // same case in other lines; here, the removed range location will be - // exactly 1 less than paragraph range location - [self removeAttributes:paragraphRange]; - return YES; - } - } - return NO; -} - -- (BOOL)styleCondition:(id _Nullable)value range:(NSRange)range { - NSParagraphStyle *pStyle = (NSParagraphStyle *)value; - return pStyle != nullptr && pStyle.headIndent == [self getHeadIndent] && - pStyle.firstLineHeadIndent == [self getHeadIndent] && - pStyle.textLists.count == 0; -} - -- (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:range]; - }]; - } else { - return [OccurenceUtils detect:NSParagraphStyleAttributeName - withInput:_input - atIndex:range.location - checkPrevious:YES - withCondition:^BOOL(id _Nullable value, NSRange range) { - return [self styleCondition:value range:range]; - }]; - } -} - -- (BOOL)anyOccurence:(NSRange)range { - return [OccurenceUtils any:NSParagraphStyleAttributeName - withInput:_input - inRange:range - withCondition:^BOOL(id _Nullable value, NSRange range) { - return [self styleCondition:value range:range]; - }]; +- (BOOL)isParagraph { + return YES; } -- (NSArray *_Nullable)findAllOccurences:(NSRange)range { - return [OccurenceUtils all:NSParagraphStyleAttributeName - withInput:_input - inRange:range - withCondition:^BOOL(id _Nullable value, NSRange range) { - return [self styleCondition:value range:range]; - }]; +- (BOOL)needsZWS { + return YES; } -// general checkup correcting blockquote color -// since links, mentions and inline code affects coloring, the checkup gets done -// only outside of them -- (void)manageBlockquoteColor { - if ([[_input->config blockquoteColor] - isEqualToColor:[_input->config primaryColor]]) { - return; - } - - NSRange wholeRange = - NSMakeRange(0, _input->textView.textStorage.string.length); - - NSArray *paragraphs = - [ParagraphsUtils getSeparateParagraphsRangesIn:_input->textView - range:wholeRange]; - for (NSValue *pValue in paragraphs) { - NSRange paragraphRange = [pValue rangeValue]; - NSArray *properRanges = [OccurenceUtils getRangesWithout:_stylesToExclude - withInput:_input - inRange:paragraphRange]; - - for (NSValue *value in properRanges) { - NSRange currRange = [value rangeValue]; - BOOL selfDetected = [self detectStyle:currRange]; - - [_input->textView.textStorage - enumerateAttribute:NSForegroundColorAttributeName - inRange:currRange - options:0 - usingBlock:^(id _Nullable value, NSRange range, - BOOL *_Nonnull stop) { - UIColor *newColor = nullptr; - BOOL colorApplied = [(UIColor *)value - isEqualToColor:[_input->config blockquoteColor]]; - - if (colorApplied && !selfDetected) { - newColor = [_input->config primaryColor]; - } else if (!colorApplied && selfDetected) { - newColor = [_input->config blockquoteColor]; - } - - if (newColor != nullptr) { - [_input->textView.textStorage - addAttribute:NSForegroundColorAttributeName - value:newColor - range:currRange]; - [_input->textView.textStorage - addAttribute:NSUnderlineColorAttributeName - value:newColor - range:currRange]; - [_input->textView.textStorage - addAttribute:NSStrikethroughColorAttributeName - value:newColor - range:currRange]; - } - }]; - } - } +- (void)applyStyling:(NSRange)range { + NSRange fullRange = + [self.input->textView.textStorage.string paragraphRangeForRange:range]; + + CGFloat indent = [self.input->config blockquoteBorderWidth] + + [self.input->config blockquoteGapWidth]; + [self.input->textView.textStorage + enumerateAttribute:NSParagraphStyleAttributeName + inRange:fullRange + options:0 + usingBlock:^(id _Nullable value, NSRange subRange, + BOOL *_Nonnull stop) { + NSMutableParagraphStyle *pStyle = + [(NSParagraphStyle *)value mutableCopy]; + pStyle.headIndent = indent; + pStyle.firstLineHeadIndent = indent; + [self.input->textView.textStorage + addAttribute:NSParagraphStyleAttributeName + value:pStyle + range:subRange]; + }]; + + UIColor *bqColor = [self.input->config blockquoteColor]; + [self.input->textView.textStorage addAttribute:NSForegroundColorAttributeName + value:bqColor + range:range]; + [self.input->textView.textStorage addAttribute:NSUnderlineColorAttributeName + value:bqColor + range:range]; + [self.input->textView.textStorage + addAttribute:NSStrikethroughColorAttributeName + value:bqColor + range:range]; } @end diff --git a/ios/styles/BoldStyle.mm b/ios/styles/BoldStyle.mm index 0f8a37432..c9f25123d 100644 --- a/ios/styles/BoldStyle.mm +++ b/ios/styles/BoldStyle.mm @@ -1,39 +1,23 @@ #import "EnrichedTextInputView.h" #import "FontExtension.h" -#import "OccurenceUtils.h" #import "StyleHeaders.h" -@implementation BoldStyle { - EnrichedTextInputView *_input; -} +@implementation BoldStyle : StyleBase -+ (StyleType)getStyleType { ++ (StyleType)getType { return Bold; } -+ (BOOL)isParagraphStyle { - return NO; -} - -- (instancetype)initWithInput:(id)input { - self = [super init]; - _input = (EnrichedTextInputView *)input; - return self; +- (NSString *)getKey { + return @"EnrichedBold"; } -- (void)applyStyle:(NSRange)range { - BOOL isStylePresent = [self detectStyle:range]; - if (range.length >= 1) { - isStylePresent ? [self removeAttributes:range] - : [self addAttributes:range withTypingAttr:YES]; - } else { - isStylePresent ? [self removeTypingAttributes] : [self addTypingAttributes]; - } +- (BOOL)isParagraph { + return NO; } -- (void)addAttributes:(NSRange)range withTypingAttr:(BOOL)withTypingAttr { - [_input->textView.textStorage beginEditing]; - [_input->textView.textStorage +- (void)applyStyling:(NSRange)range { + [self.input->textView.textStorage enumerateAttribute:NSFontAttributeName inRange:range options:0 @@ -42,133 +26,12 @@ - (void)addAttributes:(NSRange)range withTypingAttr:(BOOL)withTypingAttr { UIFont *font = (UIFont *)value; if (font != nullptr) { UIFont *newFont = [font setBold]; - [_input->textView.textStorage addAttribute:NSFontAttributeName - value:newFont - range:range]; + [self.input->textView.textStorage + addAttribute:NSFontAttributeName + value:newFont + range:range]; } }]; - [_input->textView.textStorage endEditing]; -} - -- (void)addTypingAttributes { - UIFont *currentFontAttr = - (UIFont *)_input->textView.typingAttributes[NSFontAttributeName]; - if (currentFontAttr != nullptr) { - NSMutableDictionary *newTypingAttrs = - [_input->textView.typingAttributes mutableCopy]; - newTypingAttrs[NSFontAttributeName] = [currentFontAttr setBold]; - _input->textView.typingAttributes = newTypingAttrs; - } -} - -- (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 removeBold]; - [_input->textView.textStorage addAttribute:NSFontAttributeName - value:newFont - range:range]; - } - }]; - [_input->textView.textStorage endEditing]; -} - -- (void)removeTypingAttributes { - UIFont *currentFontAttr = - (UIFont *)_input->textView.typingAttributes[NSFontAttributeName]; - if (currentFontAttr != nullptr) { - NSMutableDictionary *newTypingAttrs = - [_input->textView.typingAttributes mutableCopy]; - newTypingAttrs[NSFontAttributeName] = [currentFontAttr removeBold]; - _input->textView.typingAttributes = newTypingAttrs; - } -} - -- (BOOL)boldHeadingConflictsInRange:(NSRange)range type:(StyleType)type { - if (type == H1) { - if (![_input->config h1Bold]) { - return NO; - } - } else if (type == H2) { - if (![_input->config h2Bold]) { - return NO; - } - } else if (type == H3) { - if (![_input->config h3Bold]) { - return NO; - } - } else if (type == H4) { - if (![_input->config h4Bold]) { - return NO; - } - } else if (type == H5) { - if (![_input->config h5Bold]) { - return NO; - } - } else if (type == H6) { - if (![_input->config h6Bold]) { - return NO; - } - } - - id headingStyle = _input->stylesDict[@(type)]; - return range.length > 0 ? [headingStyle anyOccurence:range] - : [headingStyle detectStyle:range]; -} - -- (BOOL)styleCondition:(id _Nullable)value range:(NSRange)range { - UIFont *font = (UIFont *)value; - return font != nullptr && [font isBold] && - ![self boldHeadingConflictsInRange:range type:H1] && - ![self boldHeadingConflictsInRange:range type:H2] && - ![self boldHeadingConflictsInRange:range type:H3] && - ![self boldHeadingConflictsInRange:range type:H4] && - ![self boldHeadingConflictsInRange:range type:H5] && - ![self boldHeadingConflictsInRange:range type:H6]; -} - -- (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:range]; - }]; - } else { - return [OccurenceUtils detect:NSFontAttributeName - withInput:_input - atIndex:range.location - checkPrevious:NO - withCondition:^BOOL(id _Nullable value, NSRange range) { - return [self styleCondition:value range:range]; - }]; - } -} - -- (BOOL)anyOccurence:(NSRange)range { - return [OccurenceUtils any:NSFontAttributeName - withInput:_input - inRange:range - withCondition:^BOOL(id _Nullable value, NSRange range) { - return [self styleCondition:value range:range]; - }]; -} - -- (NSArray *_Nullable)findAllOccurences:(NSRange)range { - return [OccurenceUtils all:NSFontAttributeName - withInput:_input - inRange:range - withCondition:^BOOL(id _Nullable value, NSRange range) { - return [self styleCondition:value range:range]; - }]; } @end diff --git a/ios/styles/CheckboxListStyle.mm b/ios/styles/CheckboxListStyle.mm index b6a2b8a6b..ae7f740a4 100644 --- a/ios/styles/CheckboxListStyle.mm +++ b/ios/styles/CheckboxListStyle.mm @@ -1,321 +1,153 @@ #import "EnrichedTextInputView.h" -#import "FontExtension.h" -#import "OccurenceUtils.h" -#import "ParagraphsUtils.h" +#import "RangeUtils.h" #import "StyleHeaders.h" #import "TextInsertionUtils.h" -@implementation CheckboxListStyle { - EnrichedTextInputView *_input; -} +@implementation CheckboxListStyle -+ (StyleType)getStyleType { ++ (StyleType)getType { return CheckboxList; } -+ (BOOL)isParagraphStyle { - return YES; +- (NSString *)getValue { + return @"EnrichedCheckbox0"; } -- (CGFloat)getHeadIndent { - return [_input->config checkboxListMarginLeft] + - [_input->config checkboxListGapWidth] + - [_input->config checkboxListBoxSize]; -} - -- (instancetype)initWithInput:(id)input { - self = [super init]; - _input = (EnrichedTextInputView *)input; - return self; +- (BOOL)isParagraph { + return YES; } -- (void)applyStyle:(NSRange)range { - // Applying a checkbox list style requires a checked value, - // which is why this method is not implemented. - // Use 'applyStyleWithCheckedValue' and provide the checked value instead. +- (BOOL)needsZWS { + return YES; } -- (void)applyStyleWithCheckedValue:(BOOL)checked inRange:(NSRange)range { - BOOL isStylePresent = [self detectStyle:range]; - if (range.length >= 1) { - isStylePresent ? [self removeAttributes:range] - : [self addAttributesWithCheckedValue:checked - inRange:range - withTypingAttr:YES]; +- (void)applyStyling:(NSRange)range { + CGFloat listHeadIndent = [self.input->config checkboxListMarginLeft] + + [self.input->config checkboxListGapWidth] + + [self.input->config checkboxListBoxSize]; + + [self.input->textView.textStorage + enumerateAttribute:NSParagraphStyleAttributeName + inRange:range + options:0 + usingBlock:^(id _Nullable value, NSRange range, + BOOL *_Nonnull stop) { + NSMutableParagraphStyle *pStyle = + [(NSParagraphStyle *)value mutableCopy]; + pStyle.headIndent = listHeadIndent; + pStyle.firstLineHeadIndent = listHeadIndent; + [self.input->textView.textStorage + addAttribute:NSParagraphStyleAttributeName + value:pStyle + range:range]; + }]; +} + +- (BOOL)styleCondition:(id)value range:(NSRange)range { + NSParagraphStyle *pStyle = (NSParagraphStyle *)value; + return pStyle != nullptr && pStyle.textLists.count == 1 && + [pStyle.textLists.firstObject.markerFormat + hasPrefix:@"EnrichedCheckbox"]; +} + +- (void)toggleWithChecked:(BOOL)checked range:(NSRange)range { + NSRange actualRange = [self actualUsedRange:range]; + BOOL isPresent = [self detect:actualRange]; + + if (isPresent) { + [self remove:actualRange withDirtyRange:YES]; } else { - isStylePresent - ? [self removeTypingAttributes] - : [self addAttributesWithCheckedValue:checked - inRange:_input->textView.selectedRange - withTypingAttr:YES]; - ; + [self addWithChecked:checked + range:actualRange + withTyping:YES + withDirtyRange:YES]; } } -- (void)addAttributes:(NSRange)range withTypingAttr:(BOOL)withTypingAttr { - [self addAttributesWithCheckedValue:NO - inRange:range - withTypingAttr:withTypingAttr]; +- (void)addWithChecked:(BOOL)checked + range:(NSRange)range + withTyping:(BOOL)withTyping + withDirtyRange:(BOOL)withDirtyRange { + NSString *value = checked ? @"EnrichedCheckbox1" : @"EnrichedCheckbox0"; + [self add:range + withValue:value + withTyping:withTyping + withDirtyRange:withDirtyRange]; } -- (void)addTypingAttributes { +// During dirty range re-application the default add: would use getValue +// (EnrichedCheckbox0) and lose the checked state. Instead, read the original +// marker format from the saved StylePair +- (void)reapplyAttributesFromStylePair:(StylePair *)pair { + NSRange range = [pair.rangeValue rangeValue]; + NSParagraphStyle *savedPStyle = (NSParagraphStyle *)pair.styleValue; + BOOL checked = + savedPStyle != nullptr && [savedPStyle.textLists.firstObject.markerFormat + isEqualToString:@"EnrichedCheckbox1"]; + [self addWithChecked:checked range:range withTyping:NO withDirtyRange:NO]; } -- (void)removeAttributes:(NSRange)range { - NSArray *paragraphs = - [ParagraphsUtils getSeparateParagraphsRangesIn:_input->textView - range:range]; - - [_input->textView.textStorage beginEditing]; - - 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]; - }]; +- (void)toggleCheckedAt:(NSUInteger)location { + if (location >= self.input->textView.textStorage.length) { + return; } - [_input->textView.textStorage endEditing]; + NSParagraphStyle *pStyle = + [self.input->textView.textStorage attribute:NSParagraphStyleAttributeName + atIndex:location + effectiveRange:NULL]; + NSTextList *list = pStyle.textLists.firstObject; - // also remove typing attributes - NSMutableDictionary *typingAttrs = - [_input->textView.typingAttributes mutableCopy]; - NSMutableParagraphStyle *pStyle = - [typingAttrs[NSParagraphStyleAttributeName] mutableCopy]; - pStyle.textLists = @[]; - pStyle.headIndent = 0; - pStyle.firstLineHeadIndent = 0; - typingAttrs[NSParagraphStyleAttributeName] = pStyle; - _input->textView.typingAttributes = typingAttrs; -} + BOOL isCurrentlyChecked = + [list.markerFormat isEqualToString:@"EnrichedCheckbox1"]; -- (void)removeTypingAttributes { - [self removeAttributes:_input->textView.selectedRange]; + NSRange paragraphRange = [self.input->textView.textStorage.string + paragraphRangeForRange:NSMakeRange(location, 0)]; + + [self addWithChecked:!isCurrentlyChecked + range:paragraphRange + withTyping:YES + withDirtyRange:YES]; } -- (BOOL)handleBackspaceInRange:(NSRange)range replacementText:(NSString *)text { - if ([self detectStyle:_input->textView.selectedRange] && text.length == 0) { - // backspace while the style is active +- (BOOL)getCheckboxStateAt:(NSUInteger)location { + if (location >= self.input->textView.textStorage.length) { + return NO; + } - NSRange paragraphRange = [_input->textView.textStorage.string - paragraphRangeForRange:_input->textView.selectedRange]; + NSParagraphStyle *style = + [self.input->textView.textStorage attribute:NSParagraphStyleAttributeName + atIndex:location + effectiveRange:NULL]; - if (NSEqualRanges(_input->textView.selectedRange, NSMakeRange(0, 0))) { - // a backspace on the very first input's line list point - // it doesn't run textVieDidChange so we need to manually remove - // attributes - [self removeAttributes:paragraphRange]; - return YES; - } else if (range.location == paragraphRange.location - 1) { - // same case in other lines; here, the removed range location will be - // exactly 1 less than paragraph range location - [self removeAttributes:paragraphRange]; + if (style && style.textLists.count > 0) { + NSTextList *list = style.textLists.firstObject; + if ([list.markerFormat isEqualToString:@"EnrichedCheckbox1"]) { return YES; } } + return NO; } -// used to make sure checkbox marker is correct when a newline is placed - (BOOL)handleNewlinesInRange:(NSRange)range replacementText:(NSString *)text { - // in a checkbox list and a new text ends with a newline - if ([self detectStyle:_input->textView.selectedRange] && text.length > 0 && + if ([self detect:self.input->textView.selectedRange] && text.length > 0 && [[NSCharacterSet newlineCharacterSet] characterIsMember:[text characterAtIndex:text.length - 1]]) { // do the replacement manually [TextInsertionUtils replaceText:text at:range additionalAttributes:nullptr - input:_input + input:self.input withSelection:YES]; - // apply checkbox attributes to the new paragraph - [self addAttributes:_input->textView.selectedRange withTypingAttr:YES]; + // apply unchecked checkbox attributes to the new paragraph + [self addWithChecked:NO + range:self.input->textView.selectedRange + withTyping:YES + withDirtyRange:YES]; return YES; } return NO; } -- (void)toggleCheckedAt:(NSUInteger)location { - if (location >= _input->textView.textStorage.length) { - return; - } - - NSParagraphStyle *pStyle = - [_input->textView.textStorage attribute:NSParagraphStyleAttributeName - atIndex:location - effectiveRange:NULL]; - NSTextList *list = pStyle.textLists.firstObject; - - BOOL isCurrentlyChecked = [list.markerFormat isEqualToString:@"{checkbox:1}"]; - - NSString *fullText = _input->textView.textStorage.string; - NSRange paragraphRange = - [fullText paragraphRangeForRange:NSMakeRange(location, 0)]; - - [self addAttributesWithCheckedValue:!isCurrentlyChecked - inRange:paragraphRange - withTypingAttr:YES]; -} - -- (void)addAttributesWithCheckedValue:(BOOL)checked - inRange:(NSRange)range - withTypingAttr:(BOOL)withTypingAttr { - NSString *markerFormat = checked ? @"{checkbox:1}" : @"{checkbox:0}"; - NSTextList *checkboxMarker = - [[NSTextList alloc] initWithMarkerFormat:markerFormat options:0]; - NSArray *paragraphs = - [ParagraphsUtils getSeparateParagraphsRangesIn:_input->textView - range:range]; - // if we fill empty lines with zero width spaces, we need to offset later - // ranges - NSInteger offset = 0; - // needed for range adjustments - NSRange preModificationRange = _input->textView.selectedRange; - - // let's not emit some weird selection changes or text/html changes - _input->blockEmitting = YES; - - 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:[_input->textView.textStorage.string - characterAtIndex:fixedRange.location]])) { - [TextInsertionUtils insertText:@"\u200B" - at:fixedRange.location - additionalAttributes:nullptr - input:_input - withSelection:NO]; - fixedRange = NSMakeRange(fixedRange.location, fixedRange.length + 1); - offset += 1; - } - - [_input->textView.textStorage - enumerateAttribute:NSParagraphStyleAttributeName - inRange:fixedRange - options:0 - usingBlock:^(id _Nullable value, NSRange range, - BOOL *_Nonnull stop) { - NSMutableParagraphStyle *pStyle = - [(NSParagraphStyle *)value mutableCopy]; - pStyle.textLists = @[ checkboxMarker ]; - pStyle.headIndent = [self getHeadIndent]; - pStyle.firstLineHeadIndent = [self getHeadIndent]; - [_input->textView.textStorage - addAttribute:NSParagraphStyleAttributeName - value:pStyle - range:range]; - }]; - } - - // back to emitting - _input->blockEmitting = NO; - - if (preModificationRange.length == 0) { - // fix selection if only one line was possibly made a list and filled with a - // space - _input->textView.selectedRange = preModificationRange; - } else { - // in other cases, fix the selection with newly made offsets - _input->textView.selectedRange = NSMakeRange( - preModificationRange.location, preModificationRange.length + offset); - } - - // also add typing attributes - if (withTypingAttr) { - NSMutableDictionary *typingAttrs = - [_input->textView.typingAttributes mutableCopy]; - NSMutableParagraphStyle *pStyle = - [typingAttrs[NSParagraphStyleAttributeName] mutableCopy]; - pStyle.textLists = @[ checkboxMarker ]; - pStyle.headIndent = [self getHeadIndent]; - pStyle.firstLineHeadIndent = [self getHeadIndent]; - typingAttrs[NSParagraphStyleAttributeName] = pStyle; - _input->textView.typingAttributes = typingAttrs; - } -} - -- (BOOL)styleCondition:(id _Nullable)value:(NSRange)range { - NSParagraphStyle *paragraph = (NSParagraphStyle *)value; - return paragraph != nullptr && paragraph.textLists.count == 1 && - [paragraph.textLists.firstObject.markerFormat hasPrefix:@"{checkbox"]; -} - -- (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]; - }]; - } else { - return [OccurenceUtils detect:NSParagraphStyleAttributeName - withInput:_input - atIndex:range.location - checkPrevious:YES - withCondition:^BOOL(id _Nullable value, NSRange range) { - return [self styleCondition:value:range]; - }]; - } -} - -- (BOOL)anyOccurence:(NSRange)range { - return [OccurenceUtils any:NSParagraphStyleAttributeName - withInput:_input - inRange:range - withCondition:^BOOL(id _Nullable value, NSRange range) { - return [self styleCondition:value:range]; - }]; -} - -- (NSArray *_Nullable)findAllOccurences:(NSRange)range { - return [OccurenceUtils all:NSParagraphStyleAttributeName - withInput:_input - inRange:range - withCondition:^BOOL(id _Nullable value, NSRange range) { - return [self styleCondition:value:range]; - }]; -} - -- (BOOL)getCheckboxStateAt:(NSUInteger)location { - if (location >= _input->textView.textStorage.length) { - return NO; - } - - NSParagraphStyle *style = - [_input->textView.textStorage attribute:NSParagraphStyleAttributeName - atIndex:location - effectiveRange:NULL]; - - if (style && style.textLists.count > 0) { - NSTextList *list = style.textLists.firstObject; - - if ([list.markerFormat isEqualToString:@"{checkbox:1}"]) { - return YES; - } - } - - return NO; -} - @end diff --git a/ios/styles/CodeBlockStyle.mm b/ios/styles/CodeBlockStyle.mm index 3495a1ddb..f4576b22d 100644 --- a/ios/styles/CodeBlockStyle.mm +++ b/ios/styles/CodeBlockStyle.mm @@ -1,304 +1,52 @@ -#import "ColorExtension.h" #import "EnrichedTextInputView.h" #import "FontExtension.h" -#import "OccurenceUtils.h" -#import "ParagraphsUtils.h" #import "StyleHeaders.h" -#import "TextInsertionUtils.h" -@implementation CodeBlockStyle { - EnrichedTextInputView *_input; - NSArray *_stylesToExclude; -} +@implementation CodeBlockStyle -+ (StyleType)getStyleType { ++ (StyleType)getType { return CodeBlock; } -+ (BOOL)isParagraphStyle { - return YES; -} - -- (instancetype)initWithInput:(id)input { - self = [super init]; - _input = (EnrichedTextInputView *)input; - _stylesToExclude = @[ @(InlineCode), @(Mention), @(Link) ]; - return self; -} - -- (void)applyStyle:(NSRange)range { - BOOL isStylePresent = [self detectStyle:range]; - if (range.length >= 1) { - isStylePresent ? [self removeAttributes:range] - : [self addAttributes:range withTypingAttr:YES]; - } else { - isStylePresent ? [self removeTypingAttributes] : [self addTypingAttributes]; - } -} - -- (void)addAttributes:(NSRange)range withTypingAttr:(BOOL)withTypingAttr { - NSTextList *codeBlockList = - [[NSTextList alloc] initWithMarkerFormat:@"codeblock" options:0]; - NSArray *paragraphs = - [ParagraphsUtils getSeparateParagraphsRangesIn:_input->textView - range:range]; - // if we fill empty lines with zero width spaces, we need to offset later - // ranges - NSInteger offset = 0; - NSRange preModificationRange = _input->textView.selectedRange; - - // to not emit any space filling selection/text changes - _input->blockEmitting = YES; - - 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:[_input->textView.textStorage.string - characterAtIndex:pRange.location]])) { - [TextInsertionUtils insertText:@"\u200B" - at:pRange.location - additionalAttributes:nullptr - input:_input - withSelection:NO]; - pRange = NSMakeRange(pRange.location, pRange.length + 1); - offset += 1; - } - - [_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 = @[ codeBlockList ]; - [_input->textView.textStorage - addAttribute:NSParagraphStyleAttributeName - value:pStyle - range:range]; - }]; - } - - // back to emitting - _input->blockEmitting = NO; - - if (preModificationRange.length == 0) { - // fix selection if only one line was possibly made a list and filled with a - // space - _input->textView.selectedRange = preModificationRange; - } else { - // in other cases, fix the selection with newly made offsets - _input->textView.selectedRange = NSMakeRange( - preModificationRange.location, preModificationRange.length + offset); - } - - // 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; - } -} - -- (void)addTypingAttributes { - [self addAttributes:_input->textView.selectedRange withTypingAttr:YES]; -} - -- (void)removeAttributes:(NSRange)range { - NSArray *paragraphs = - [ParagraphsUtils getSeparateParagraphsRangesIn:_input->textView - range:range]; - - [_input->textView.textStorage beginEditing]; - - 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]; - }]; - } - - [_input->textView.textStorage endEditing]; - - // also remove typing attributes - NSMutableDictionary *typingAttrs = - [_input->textView.typingAttributes mutableCopy]; - NSMutableParagraphStyle *pStyle = - [typingAttrs[NSParagraphStyleAttributeName] mutableCopy]; - pStyle.textLists = @[]; - - typingAttrs[NSParagraphStyleAttributeName] = pStyle; - - _input->textView.typingAttributes = typingAttrs; -} - -- (void)removeTypingAttributes { - [self removeAttributes:_input->textView.selectedRange]; -} - -- (BOOL)handleBackspaceInRange:(NSRange)range replacementText:(NSString *)text { - if ([self detectStyle:_input->textView.selectedRange] && text.length == 0) { - // backspace while the style is active - - NSRange paragraphRange = [_input->textView.textStorage.string - paragraphRangeForRange:_input->textView.selectedRange]; - - if (NSEqualRanges(_input->textView.selectedRange, NSMakeRange(0, 0))) { - // a backspace on the very first input's line quote - // it doesn't run textVieDidChange so we need to manually remove - // attributes - [self removeAttributes:paragraphRange]; - return YES; - } - } - return NO; -} - -- (BOOL)styleCondition:(id _Nullable)value range:(NSRange)range { - NSParagraphStyle *paragraph = (NSParagraphStyle *)value; - return paragraph != nullptr && paragraph.textLists.count == 1 && - [paragraph.textLists.firstObject.markerFormat - isEqualToString:@"codeblock"]; -} - -- (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:range]; - }]; - } else { - return [OccurenceUtils detect:NSParagraphStyleAttributeName - withInput:_input - atIndex:range.location - checkPrevious:YES - withCondition:^BOOL(id _Nullable value, NSRange range) { - return [self styleCondition:value range:range]; - }]; - } -} - -- (BOOL)anyOccurence:(NSRange)range { - return [OccurenceUtils any:NSParagraphStyleAttributeName - withInput:_input - inRange:range - withCondition:^BOOL(id _Nullable value, NSRange range) { - return [self styleCondition:value range:range]; - }]; +- (NSString *)getValue { + return @"EnrichedCodeBlock"; } -- (NSArray *_Nullable)findAllOccurences:(NSRange)range { - return [OccurenceUtils all:NSParagraphStyleAttributeName - withInput:_input - inRange:range - withCondition:^BOOL(id _Nullable value, NSRange range) { - return [self styleCondition:value range:range]; - }]; +- (BOOL)isParagraph { + return YES; } -- (void)manageCodeBlockFontAndColor { - if ([[_input->config codeBlockFgColor] - isEqualToColor:[_input->config primaryColor]]) { - return; - } - - NSRange wholeRange = - NSMakeRange(0, _input->textView.textStorage.string.length); - NSArray *paragraphs = - [ParagraphsUtils getSeparateParagraphsRangesIn:_input->textView - range:wholeRange]; - - for (NSValue *pValue in paragraphs) { - NSRange paragraphRange = [pValue rangeValue]; - NSArray *properRanges = [OccurenceUtils getRangesWithout:_stylesToExclude - withInput:_input - inRange:paragraphRange]; - - for (NSValue *value in properRanges) { - NSRange currRange = [value rangeValue]; - BOOL selfDetected = [self detectStyle:currRange]; - - [_input->textView.textStorage - enumerateAttribute:NSFontAttributeName - inRange:currRange - options:0 - usingBlock:^(id _Nullable value, NSRange range, - BOOL *_Nonnull stop) { - UIFont *currentFont = (UIFont *)value; - UIFont *newFont = nullptr; - - BOOL isCodeFont = [[currentFont familyName] - isEqualToString:[[_input->config monospacedFont] - familyName]]; - - if (isCodeFont && !selfDetected) { - newFont = [[[_input->config primaryFont] - withFontTraits:currentFont] - setSize:currentFont.pointSize]; - } else if (!isCodeFont && selfDetected) { - newFont = [[[_input->config monospacedFont] - withFontTraits:currentFont] - setSize:currentFont.pointSize]; - } - - if (newFont != nullptr) { - [_input->textView.textStorage - addAttribute:NSFontAttributeName - value:newFont - range:range]; - } - }]; - - [_input->textView.textStorage - enumerateAttribute:NSForegroundColorAttributeName - inRange:currRange - options:0 - usingBlock:^(id _Nullable value, NSRange range, - BOOL *_Nonnull stop) { - UIColor *newColor = nullptr; - BOOL colorApplied = [(UIColor *)value - isEqualToColor:[_input->config codeBlockFgColor]]; - - if (colorApplied && !selfDetected) { - newColor = [_input->config primaryColor]; - } else if (!colorApplied && selfDetected) { - newColor = [_input->config codeBlockFgColor]; - } - - if (newColor != nullptr) { - [_input->textView.textStorage - addAttribute:NSForegroundColorAttributeName - value:newColor - range:range]; - } - }]; - } - } +- (BOOL)needsZWS { + return YES; } -@end +- (void)applyStyling:(NSRange)range { + NSRange fullRange = + [self.input->textView.textStorage.string paragraphRangeForRange:range]; + + [self.input->textView.textStorage + enumerateAttribute:NSFontAttributeName + inRange:fullRange + options:0 + usingBlock:^(id _Nullable value, NSRange subRange, + BOOL *_Nonnull stop) { + UIFont *currentFont = (UIFont *)value; + if (currentFont == nullptr) + return; + UIFont *monoFont = [[[self.input->config monospacedFont] + withFontTraits:currentFont] setSize:currentFont.pointSize]; + if (monoFont != nullptr) { + [self.input->textView.textStorage + addAttribute:NSFontAttributeName + value:monoFont + range:subRange]; + } + }]; + + [self.input->textView.textStorage + addAttribute:NSForegroundColorAttributeName + value:[self.input->config codeBlockFgColor] + range:range]; +} + +@end \ No newline at end of file diff --git a/ios/styles/H1Style.mm b/ios/styles/H1Style.mm index ca22c1154..c04c26f3b 100644 --- a/ios/styles/H1Style.mm +++ b/ios/styles/H1Style.mm @@ -2,16 +2,19 @@ #import "StyleHeaders.h" @implementation H1Style -+ (StyleType)getStyleType { ++ (StyleType)getType { return H1; } -+ (BOOL)isParagraphStyle { +- (NSString *)getValue { + return @"EnrichedH1"; +} +- (BOOL)isParagraph { return YES; } - (CGFloat)getHeadingFontSize { - return [((EnrichedTextInputView *)input)->config h1FontSize]; + return [self.input->config h1FontSize]; } - (BOOL)isHeadingBold { - return [((EnrichedTextInputView *)input)->config h1Bold]; + return [self.input->config h1Bold]; } @end diff --git a/ios/styles/H2Style.mm b/ios/styles/H2Style.mm index a943b96a7..785b2b72a 100644 --- a/ios/styles/H2Style.mm +++ b/ios/styles/H2Style.mm @@ -2,16 +2,19 @@ #import "StyleHeaders.h" @implementation H2Style -+ (StyleType)getStyleType { ++ (StyleType)getType { return H2; } -+ (BOOL)isParagraphStyle { +- (NSString *)getValue { + return @"EnrichedH2"; +} +- (BOOL)isParagraph { return YES; } - (CGFloat)getHeadingFontSize { - return [((EnrichedTextInputView *)input)->config h2FontSize]; + return [self.input->config h2FontSize]; } - (BOOL)isHeadingBold { - return [((EnrichedTextInputView *)input)->config h2Bold]; + return [self.input->config h2Bold]; } @end diff --git a/ios/styles/H3Style.mm b/ios/styles/H3Style.mm index e6c5765b3..9091f6e3b 100644 --- a/ios/styles/H3Style.mm +++ b/ios/styles/H3Style.mm @@ -2,16 +2,19 @@ #import "StyleHeaders.h" @implementation H3Style -+ (StyleType)getStyleType { ++ (StyleType)getType { return H3; } -+ (BOOL)isParagraphStyle { +- (NSString *)getValue { + return @"EnrichedH3"; +} +- (BOOL)isParagraph { return YES; } - (CGFloat)getHeadingFontSize { - return [((EnrichedTextInputView *)input)->config h3FontSize]; + return [self.input->config h3FontSize]; } - (BOOL)isHeadingBold { - return [((EnrichedTextInputView *)input)->config h3Bold]; + return [self.input->config h3Bold]; } @end diff --git a/ios/styles/H4Style.mm b/ios/styles/H4Style.mm index 0fef5cc7d..a641f5ed8 100644 --- a/ios/styles/H4Style.mm +++ b/ios/styles/H4Style.mm @@ -2,16 +2,19 @@ #import "StyleHeaders.h" @implementation H4Style -+ (StyleType)getStyleType { ++ (StyleType)getType { return H4; } -+ (BOOL)isParagraphStyle { +- (NSString *)getValue { + return @"EnrichedH4"; +} +- (BOOL)isParagraph { return YES; } - (CGFloat)getHeadingFontSize { - return [((EnrichedTextInputView *)input)->config h4FontSize]; + return [self.input->config h4FontSize]; } - (BOOL)isHeadingBold { - return [((EnrichedTextInputView *)input)->config h4Bold]; + return [self.input->config h4Bold]; } @end diff --git a/ios/styles/H5Style.mm b/ios/styles/H5Style.mm index 60a06abaf..40fab0f28 100644 --- a/ios/styles/H5Style.mm +++ b/ios/styles/H5Style.mm @@ -2,16 +2,19 @@ #import "StyleHeaders.h" @implementation H5Style -+ (StyleType)getStyleType { ++ (StyleType)getType { return H5; } -+ (BOOL)isParagraphStyle { +- (NSString *)getValue { + return @"EnrichedH5"; +} +- (BOOL)isParagraph { return YES; } - (CGFloat)getHeadingFontSize { - return [((EnrichedTextInputView *)input)->config h5FontSize]; + return [self.input->config h5FontSize]; } - (BOOL)isHeadingBold { - return [((EnrichedTextInputView *)input)->config h5Bold]; + return [self.input->config h5Bold]; } @end diff --git a/ios/styles/H6Style.mm b/ios/styles/H6Style.mm index 80b46f810..2e576bb0a 100644 --- a/ios/styles/H6Style.mm +++ b/ios/styles/H6Style.mm @@ -2,16 +2,19 @@ #import "StyleHeaders.h" @implementation H6Style -+ (StyleType)getStyleType { ++ (StyleType)getType { return H6; } -+ (BOOL)isParagraphStyle { +- (NSString *)getValue { + return @"EnrichedH6"; +} +- (BOOL)isParagraph { return YES; } - (CGFloat)getHeadingFontSize { - return [((EnrichedTextInputView *)input)->config h6FontSize]; + return [self.input->config h6FontSize]; } - (BOOL)isHeadingBold { - return [((EnrichedTextInputView *)input)->config h6Bold]; + return [self.input->config h6Bold]; } @end diff --git a/ios/styles/HeadingStyleBase.mm b/ios/styles/HeadingStyleBase.mm index c32314e69..d689c4581 100644 --- a/ios/styles/HeadingStyleBase.mm +++ b/ios/styles/HeadingStyleBase.mm @@ -1,238 +1,64 @@ #import "EnrichedTextInputView.h" #import "FontExtension.h" -#import "OccurenceUtils.h" #import "StyleHeaders.h" #import "TextInsertionUtils.h" @implementation HeadingStyleBase -// mock values since H1/2/3/4/5/6Style classes anyway are used -+ (StyleType)getStyleType { +// mock values since H1/2/3/4/5/6Style classes are used ++ (StyleType)getType { return None; } + - (CGFloat)getHeadingFontSize { return 0; } + - (BOOL)isHeadingBold { - return false; -} -+ (BOOL)isParagraphStyle { - return true; + return NO; } -- (EnrichedTextInputView *)typedInput { - return (EnrichedTextInputView *)input; +- (BOOL)isParagraph { + return YES; } -- (instancetype)initWithInput:(id)input { - self = [super init]; - self->input = input; - _lastAppliedFontSize = 0.0; - return self; -} +- (void)applyStyling:(NSRange)range { + NSRange fullRange = + [self.input->textView.textStorage.string paragraphRangeForRange:range]; -// the range will already be the full paragraph/s range -// but if the paragraph is empty it still is of length 0 -- (void)applyStyle:(NSRange)range { - BOOL isStylePresent = [self detectStyle:range]; - if (range.length >= 1) { - isStylePresent ? [self removeAttributes:range] - : [self addAttributes:range withTypingAttr:YES]; - } else { - isStylePresent ? [self removeTypingAttributes] : [self addTypingAttributes]; - } - _lastAppliedFontSize = [self getHeadingFontSize]; -} - -// the range will already be the proper full paragraph/s range -- (void)addAttributes:(NSRange)range withTypingAttr:(BOOL)withTypingAttr { - [[self typedInput]->textView.textStorage beginEditing]; - [[self typedInput]->textView.textStorage + [self.input->textView.textStorage enumerateAttribute:NSFontAttributeName - inRange:range + inRange:fullRange options:0 - usingBlock:^(id _Nullable value, NSRange range, + usingBlock:^(id _Nullable value, NSRange subRange, BOOL *_Nonnull stop) { UIFont *font = (UIFont *)value; - if (font != nullptr) { - UIFont *newFont = [font setSize:[self getHeadingFontSize]]; - if ([self isHeadingBold]) { - newFont = [newFont setBold]; - } - [[self typedInput]->textView.textStorage - addAttribute:NSFontAttributeName - value:newFont - range:range]; + if (font == nullptr) + return; + UIFont *newFont = [font setSize:[self getHeadingFontSize]]; + if ([self isHeadingBold]) { + newFont = [newFont setBold]; } + [self.input->textView.textStorage + addAttribute:NSFontAttributeName + value:newFont + range:subRange]; }]; - [[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 -// changed -- (void)addTypingAttributes { - UIFont *currentFontAttr = - (UIFont *)[self typedInput] - ->textView.typingAttributes[NSFontAttributeName]; - if (currentFontAttr != nullptr) { - NSMutableDictionary *newTypingAttrs = - [[self typedInput]->textView.typingAttributes mutableCopy]; - UIFont *newFont = [currentFontAttr setSize:[self getHeadingFontSize]]; - if ([self isHeadingBold]) { - newFont = [newFont setBold]; - } - newTypingAttrs[NSFontAttributeName] = newFont; - [self typedInput]->textView.typingAttributes = newTypingAttrs; - } -} - -// we need to remove the style from the whole paragraph -- (void)removeAttributes:(NSRange)range { - NSRange paragraphRange = [[self typedInput]->textView.textStorage.string - paragraphRangeForRange:range]; - - [[self typedInput]->textView.textStorage beginEditing]; - [[self typedInput]->textView.textStorage - enumerateAttribute:NSFontAttributeName - inRange:paragraphRange - options:0 - usingBlock:^(id _Nullable value, NSRange range, - BOOL *_Nonnull stop) { - if ([self styleCondition:value range:range]) { - UIFont *newFont = [(UIFont *)value - setSize:[[[self typedInput]->config scaledPrimaryFontSize] - floatValue]]; - if ([self isHeadingBold]) { - newFont = [newFont removeBold]; - } - [[self typedInput]->textView.textStorage - addAttribute:NSFontAttributeName - value:newFont - range:range]; - } - }]; - [[self typedInput]->textView.textStorage endEditing]; - - // typing attributes still need to be removed - UIFont *currentFontAttr = - (UIFont *)[self typedInput] - ->textView.typingAttributes[NSFontAttributeName]; - if (currentFontAttr != nullptr) { - NSMutableDictionary *newTypingAttrs = - [[self typedInput]->textView.typingAttributes mutableCopy]; - UIFont *newFont = [currentFontAttr - setSize:[[[self typedInput]->config scaledPrimaryFontSize] floatValue]]; - if ([self isHeadingBold]) { - newFont = [newFont removeBold]; - } - newTypingAttrs[NSFontAttributeName] = newFont; - [self typedInput]->textView.typingAttributes = newTypingAttrs; - } -} - -- (void)removeTypingAttributes { - // all the heading still needs to be removed because this function may be - // called in conflicting styles logic typing attributes already get removed in - // there as well - [self removeAttributes:[self typedInput]->textView.selectedRange]; -} - -// when the traits already change, the getHeadginFontSize will return the new -// font size and no headings would be properly detected, so that's why we have -// to use the latest applied font size rather than that value. -- (BOOL)styleCondition:(id _Nullable)value range:(NSRange)range { - UIFont *font = (UIFont *)value; - if (font == nullptr) { - return NO; - } - - if (self.lastAppliedFontSize > 0.0) { - return font.pointSize == self.lastAppliedFontSize; - } - - return font.pointSize == [self getHeadingFontSize]; -} - -- (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:range]; - }]; - } else { - return [OccurenceUtils detect:NSFontAttributeName - withInput:[self typedInput] - atIndex:range.location - checkPrevious:YES - withCondition:^BOOL(id _Nullable value, NSRange range) { - return [self styleCondition:value range:range]; - }]; - } -} - -- (BOOL)anyOccurence:(NSRange)range { - return [OccurenceUtils any:NSFontAttributeName - withInput:[self typedInput] - inRange:range - withCondition:^BOOL(id _Nullable value, NSRange range) { - return [self styleCondition:value range:range]; - }]; -} - -- (NSArray *_Nullable)findAllOccurences:(NSRange)range { - return [OccurenceUtils all:NSFontAttributeName - withInput:[self typedInput] - inRange:range - withCondition:^BOOL(id _Nullable value, NSRange range) { - return [self styleCondition:value range: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 - if ([self detectStyle:[self typedInput]->textView.selectedRange] && - text.length > 0 && + if ([self detect:self.input->textView.selectedRange] && text.length > 0 && [[NSCharacterSet newlineCharacterSet] characterIsMember:[text characterAtIndex:text.length - 1]]) { - // do the replacement manually [TextInsertionUtils replaceText:text at:range additionalAttributes:nullptr - input:[self typedInput] + input:self.input withSelection:YES]; - // remove the attribtues at the new selection - [self removeAttributes:[self typedInput]->textView.selectedRange]; + [self remove:self.input->textView.selectedRange withDirtyRange:YES]; return YES; } return NO; } -// backspacing a line after a heading "into" a heading will not result in the -// text attaining heading attributes so, we do it manually -- (void)handleImproperHeadings { - NSArray *occurences = [self - findAllOccurences:NSMakeRange(0, - [self typedInput] - ->textView.textStorage.string.length)]; - for (StylePair *pair in occurences) { - NSRange occurenceRange = [pair.rangeValue rangeValue]; - NSRange paragraphRange = [[self typedInput]->textView.textStorage.string - paragraphRangeForRange:occurenceRange]; - 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]; - } - } - _lastAppliedFontSize = [self getHeadingFontSize]; -} - @end diff --git a/ios/styles/InlineCodeStyle.mm b/ios/styles/InlineCodeStyle.mm index ed5880240..054332a75 100644 --- a/ios/styles/InlineCodeStyle.mm +++ b/ios/styles/InlineCodeStyle.mm @@ -1,243 +1,65 @@ #import "ColorExtension.h" #import "EnrichedTextInputView.h" #import "FontExtension.h" -#import "OccurenceUtils.h" -#import "ParagraphsUtils.h" +#import "RangeUtils.h" #import "StyleHeaders.h" -@implementation InlineCodeStyle { - EnrichedTextInputView *_input; -} +@implementation InlineCodeStyle -+ (StyleType)getStyleType { ++ (StyleType)getType { return InlineCode; } -+ (BOOL)isParagraphStyle { - return NO; +- (NSString *)getKey { + return @"EnrichedInlineCode"; } -- (instancetype)initWithInput:(id)input { - self = [super init]; - _input = (EnrichedTextInputView *)input; - return self; -} - -- (void)applyStyle:(NSRange)range { - BOOL isStylePresent = [self detectStyle:range]; - if (range.length >= 1) { - isStylePresent ? [self removeAttributes:range] - : [self addAttributes:range withTypingAttr:YES]; - } else { - isStylePresent ? [self removeTypingAttributes] : [self addTypingAttributes]; - } +- (BOOL)isParagraph { + return NO; } -- (void)addAttributes:(NSRange)range withTypingAttr:(BOOL)withTypingAttr { +- (void)applyStyling:(NSRange)range { // we don't want to apply inline code to newline characters, it looks bad NSArray *nonNewlineRanges = - [ParagraphsUtils getNonNewlineRangesIn:_input->textView range:range]; + [RangeUtils getNonNewlineRangesIn:self.input->textView range:range]; for (NSValue *value in nonNewlineRanges) { - NSRange currentRange = [value rangeValue]; - [_input->textView.textStorage beginEditing]; + NSRange subRange = [value rangeValue]; - [_input->textView.textStorage + [self.input->textView.textStorage addAttribute:NSBackgroundColorAttributeName - value:[[_input->config inlineCodeBgColor] + value:[[self.input->config inlineCodeBgColor] colorWithAlphaIfNotTransparent:0.4] - range:currentRange]; - [_input->textView.textStorage + range:subRange]; + [self.input->textView.textStorage addAttribute:NSForegroundColorAttributeName - value:[_input->config inlineCodeFgColor] - range:currentRange]; - [_input->textView.textStorage + value:[self.input->config inlineCodeFgColor] + range:subRange]; + [self.input->textView.textStorage addAttribute:NSUnderlineColorAttributeName - value:[_input->config inlineCodeFgColor] - range:currentRange]; - [_input->textView.textStorage + value:[self.input->config inlineCodeFgColor] + range:subRange]; + [self.input->textView.textStorage addAttribute:NSStrikethroughColorAttributeName - value:[_input->config inlineCodeFgColor] - range:currentRange]; - [_input->textView.textStorage + value:[self.input->config inlineCodeFgColor] + range:subRange]; + [self.input->textView.textStorage enumerateAttribute:NSFontAttributeName - inRange:currentRange + inRange:subRange options:0 - usingBlock:^(id _Nullable value, NSRange range, + usingBlock:^(id _Nullable value, NSRange fontRange, BOOL *_Nonnull stop) { UIFont *font = (UIFont *)value; if (font != nullptr) { - UIFont *newFont = [[[_input->config monospacedFont] + UIFont *newFont = [[[self.input->config monospacedFont] withFontTraits:font] setSize:font.pointSize]; - [_input->textView.textStorage + [self.input->textView.textStorage addAttribute:NSFontAttributeName value:newFont - range:range]; + range:fontRange]; } }]; - - [_input->textView.textStorage endEditing]; - } -} - -- (void)addTypingAttributes { - NSMutableDictionary *newTypingAttrs = - [_input->textView.typingAttributes mutableCopy]; - newTypingAttrs[NSBackgroundColorAttributeName] = - [[_input->config inlineCodeBgColor] colorWithAlphaIfNotTransparent:0.4]; - newTypingAttrs[NSForegroundColorAttributeName] = - [_input->config inlineCodeFgColor]; - newTypingAttrs[NSUnderlineColorAttributeName] = - [_input->config inlineCodeFgColor]; - newTypingAttrs[NSStrikethroughColorAttributeName] = - [_input->config inlineCodeFgColor]; - UIFont *currentFont = (UIFont *)newTypingAttrs[NSFontAttributeName]; - if (currentFont != nullptr) { - newTypingAttrs[NSFontAttributeName] = [[[_input->config monospacedFont] - withFontTraits:currentFont] setSize:currentFont.pointSize]; } - _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 - enumerateAttribute:NSFontAttributeName - inRange:range - options:0 - usingBlock:^(id _Nullable value, NSRange range, - BOOL *_Nonnull stop) { - UIFont *font = (UIFont *)value; - if (font != nullptr) { - UIFont *newFont = [[[_input->config primaryFont] - withFontTraits:font] setSize:font.pointSize]; - [_input->textView.textStorage addAttribute:NSFontAttributeName - value:newFont - range:range]; - } - }]; - - [_input->textView.textStorage endEditing]; -} - -- (void)removeTypingAttributes { - NSMutableDictionary *newTypingAttrs = - [_input->textView.typingAttributes mutableCopy]; - [newTypingAttrs removeObjectForKey:NSBackgroundColorAttributeName]; - newTypingAttrs[NSForegroundColorAttributeName] = - [_input->config primaryColor]; - newTypingAttrs[NSUnderlineColorAttributeName] = [_input->config primaryColor]; - newTypingAttrs[NSStrikethroughColorAttributeName] = - [_input->config primaryColor]; - UIFont *currentFont = (UIFont *)newTypingAttrs[NSFontAttributeName]; - if (currentFont != nullptr) { - newTypingAttrs[NSFontAttributeName] = [[[_input->config primaryFont] - withFontTraits:currentFont] setSize:currentFont.pointSize]; - } - _input->textView.typingAttributes = newTypingAttrs; -} - -// making sure no newlines get inline code style, it looks bad -- (void)handleNewlines { - NSTextStorage *storage = _input->textView.textStorage; - NSString *string = storage.string; - NSUInteger length = string.length; - - if (length == 0) - return; - - CFStringInlineBuffer buffer; - CFStringInitInlineBuffer((CFStringRef)string, &buffer, - CFRangeMake(0, length)); - - for (NSUInteger index = 0; index < length; index++) { - unichar ch = CFStringGetCharacterFromInlineBuffer(&buffer, index); - // check new lines only - if (![[NSCharacterSet newlineCharacterSet] characterIsMember:ch]) - continue; - - NSRange newlineRange = NSMakeRange(index, 1); - - UIColor *bgColor = [storage attribute:NSBackgroundColorAttributeName - atIndex:index - effectiveRange:nil]; - - if (bgColor != nil && [self styleCondition:bgColor range:newlineRange]) { - [self removeAttributes:newlineRange]; - } - } -} - -// emojis don't retain monospace font attribute so we check for the background -// color if there is no mention -- (BOOL)styleCondition:(id _Nullable)value range:(NSRange)range { - UIColor *bgColor = (UIColor *)value; - MentionStyle *mStyle = _input->stylesDict[@([MentionStyle getStyleType])]; - 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 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:range]; - }]; - detected = detected && currentDetected; - } - - return detected; - } else { - return [OccurenceUtils detect:NSBackgroundColorAttributeName - withInput:_input - atIndex:range.location - checkPrevious:NO - withCondition:^BOOL(id _Nullable value, NSRange range) { - return [self styleCondition:value range:range]; - }]; - } -} - -- (BOOL)anyOccurence:(NSRange)range { - return [OccurenceUtils any:NSBackgroundColorAttributeName - withInput:_input - inRange:range - withCondition:^BOOL(id _Nullable value, NSRange range) { - return [self styleCondition:value range:range]; - }]; -} - -- (NSArray *_Nullable)findAllOccurences:(NSRange)range { - return [OccurenceUtils all:NSBackgroundColorAttributeName - withInput:_input - inRange:range - withCondition:^BOOL(id _Nullable value, NSRange range) { - return [self styleCondition:value range:range]; - }]; } @end diff --git a/ios/styles/ItalicStyle.mm b/ios/styles/ItalicStyle.mm index 052c88f12..963a213e6 100644 --- a/ios/styles/ItalicStyle.mm +++ b/ios/styles/ItalicStyle.mm @@ -1,39 +1,23 @@ #import "EnrichedTextInputView.h" #import "FontExtension.h" -#import "OccurenceUtils.h" #import "StyleHeaders.h" -@implementation ItalicStyle { - EnrichedTextInputView *_input; -} +@implementation ItalicStyle : StyleBase -+ (StyleType)getStyleType { ++ (StyleType)getType { return Italic; } -+ (BOOL)isParagraphStyle { - return NO; -} - -- (instancetype)initWithInput:(id)input { - self = [super init]; - _input = (EnrichedTextInputView *)input; - return self; +- (NSString *)getKey { + return @"EnrichedItalic"; } -- (void)applyStyle:(NSRange)range { - BOOL isStylePresent = [self detectStyle:range]; - if (range.length >= 1) { - isStylePresent ? [self removeAttributes:range] - : [self addAttributes:range withTypingAttr:YES]; - } else { - isStylePresent ? [self removeTypingAttributes] : [self addTypingAttributes]; - } +- (BOOL)isParagraph { + return NO; } -- (void)addAttributes:(NSRange)range withTypingAttr:(BOOL)withTypingAttr { - [_input->textView.textStorage beginEditing]; - [_input->textView.textStorage +- (void)applyStyling:(NSRange)range { + [self.input->textView.textStorage enumerateAttribute:NSFontAttributeName inRange:range options:0 @@ -42,95 +26,12 @@ - (void)addAttributes:(NSRange)range withTypingAttr:(BOOL)withTypingAttr { UIFont *font = (UIFont *)value; if (font != nullptr) { UIFont *newFont = [font setItalic]; - [_input->textView.textStorage addAttribute:NSFontAttributeName - value:newFont - range:range]; - } - }]; - [_input->textView.textStorage endEditing]; -} - -- (void)addTypingAttributes { - UIFont *currentFontAttr = - (UIFont *)_input->textView.typingAttributes[NSFontAttributeName]; - if (currentFontAttr != nullptr) { - NSMutableDictionary *newTypingAttrs = - [_input->textView.typingAttributes mutableCopy]; - newTypingAttrs[NSFontAttributeName] = [currentFontAttr setItalic]; - _input->textView.typingAttributes = newTypingAttrs; - } -} - -- (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.input->textView.textStorage + addAttribute:NSFontAttributeName + value:newFont + range:range]; } }]; - [_input->textView.textStorage endEditing]; -} - -- (void)removeTypingAttributes { - UIFont *currentFontAttr = - (UIFont *)_input->textView.typingAttributes[NSFontAttributeName]; - if (currentFontAttr != nullptr) { - NSMutableDictionary *newTypingAttrs = - [_input->textView.typingAttributes mutableCopy]; - newTypingAttrs[NSFontAttributeName] = [currentFontAttr removeItalic]; - _input->textView.typingAttributes = newTypingAttrs; - } -} - -- (BOOL)styleCondition:(id _Nullable)value range:(NSRange)range { - UIFont *font = (UIFont *)value; - return font != nullptr && [font isItalic]; -} - -- (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:range]; - }]; - } else { - return [OccurenceUtils detect:NSFontAttributeName - withInput:_input - atIndex:range.location - checkPrevious:NO - withCondition:^BOOL(id _Nullable value, NSRange range) { - return [self styleCondition:value range:range]; - }]; - } -} - -- (BOOL)anyOccurence:(NSRange)range { - return [OccurenceUtils any:NSFontAttributeName - withInput:_input - inRange:range - withCondition:^BOOL(id _Nullable value, NSRange range) { - return [self styleCondition:value range:range]; - }]; -} - -- (NSArray *_Nullable)findAllOccurences:(NSRange)range { - return [OccurenceUtils all:NSFontAttributeName - withInput:_input - inRange:range - withCondition:^BOOL(id _Nullable value, NSRange range) { - return [self styleCondition:value range:range]; - }]; } @end diff --git a/ios/styles/LinkStyle.mm b/ios/styles/LinkStyle.mm index ac23f19fb..db0467dd2 100644 --- a/ios/styles/LinkStyle.mm +++ b/ios/styles/LinkStyle.mm @@ -397,11 +397,11 @@ - (void)handleAutomaticLinks:(NSString *)word inRange:(NSRange)wordRange { } InlineCodeStyle *inlineCodeStyle = - [_input->stylesDict objectForKey:@([InlineCodeStyle getStyleType])]; + [_input->stylesDict objectForKey:@([InlineCodeStyle getType])]; MentionStyle *mentionStyle = [_input->stylesDict objectForKey:@([MentionStyle getStyleType])]; CodeBlockStyle *codeBlockStyle = - [_input->stylesDict objectForKey:@([CodeBlockStyle getStyleType])]; + [_input->stylesDict objectForKey:@([CodeBlockStyle getType])]; if (inlineCodeStyle == nullptr || mentionStyle == nullptr) { return; @@ -413,12 +413,12 @@ - (void)handleAutomaticLinks:(NSString *)word inRange:(NSRange)wordRange { } // we don't recognize links among inline code - if ([inlineCodeStyle anyOccurence:wordRange]) { + if ([inlineCodeStyle any:wordRange]) { return; } // we don't recognize links in codeblocks - if ([codeBlockStyle anyOccurence:wordRange]) { + if ([codeBlockStyle any:wordRange]) { return; } diff --git a/ios/styles/OrderedListStyle.mm b/ios/styles/OrderedListStyle.mm index 933b57e11..60a6089a5 100644 --- a/ios/styles/OrderedListStyle.mm +++ b/ios/styles/OrderedListStyle.mm @@ -1,233 +1,81 @@ #import "EnrichedTextInputView.h" -#import "FontExtension.h" -#import "OccurenceUtils.h" -#import "ParagraphsUtils.h" +#import "RangeUtils.h" #import "StyleHeaders.h" #import "TextInsertionUtils.h" -@implementation OrderedListStyle { - EnrichedTextInputView *_input; -} +@implementation OrderedListStyle -+ (StyleType)getStyleType { ++ (StyleType)getType { return OrderedList; } -+ (BOOL)isParagraphStyle { - return YES; -} - -- (CGFloat)getHeadIndent { - // lists are drawn manually - // margin before marker + gap between marker and paragraph - return [_input->config orderedListMarginLeft] + - [_input->config orderedListGapWidth]; -} - -- (instancetype)initWithInput:(id)input { - self = [super init]; - _input = (EnrichedTextInputView *)input; - return self; -} - -- (void)applyStyle:(NSRange)range { - BOOL isStylePresent = [self detectStyle:range]; - if (range.length >= 1) { - isStylePresent ? [self removeAttributes:range] - : [self addAttributes:range withTypingAttr:YES]; - } else { - isStylePresent ? [self removeTypingAttributes] : [self addTypingAttributes]; - } -} - -// we assume correct paragraph range is already given -- (void)addAttributes:(NSRange)range withTypingAttr:(BOOL)withTypingAttr { - NSTextList *numberBullet = - [[NSTextList alloc] initWithMarkerFormat:NSTextListMarkerDecimal - options:0]; - NSArray *paragraphs = - [ParagraphsUtils getSeparateParagraphsRangesIn:_input->textView - range:range]; - // if we fill empty lines with zero width spaces, we need to offset later - // ranges - NSInteger offset = 0; - // needed for range adjustments - NSRange preModificationRange = _input->textView.selectedRange; - - // let's not emit some weird selection changes or text/html changes - _input->blockEmitting = YES; - - 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:[_input->textView.textStorage.string - characterAtIndex:fixedRange.location]])) { - [TextInsertionUtils insertText:@"\u200B" - at:fixedRange.location - additionalAttributes:nullptr - input:_input - withSelection:NO]; - fixedRange = NSMakeRange(fixedRange.location, fixedRange.length + 1); - offset += 1; - } - - [_input->textView.textStorage - enumerateAttribute:NSParagraphStyleAttributeName - inRange:fixedRange - options:0 - usingBlock:^(id _Nullable value, NSRange range, - BOOL *_Nonnull stop) { - NSMutableParagraphStyle *pStyle = - [(NSParagraphStyle *)value mutableCopy]; - pStyle.textLists = @[ numberBullet ]; - pStyle.headIndent = [self getHeadIndent]; - pStyle.firstLineHeadIndent = [self getHeadIndent]; - [_input->textView.textStorage - addAttribute:NSParagraphStyleAttributeName - value:pStyle - range:range]; - }]; - } - - // back to emitting - _input->blockEmitting = NO; - - if (preModificationRange.length == 0) { - // fix selection if only one line was possibly made a list and filled with a - // space - _input->textView.selectedRange = preModificationRange; - } else { - // in other cases, fix the selection with newly made offsets - _input->textView.selectedRange = NSMakeRange( - preModificationRange.location, preModificationRange.length + offset); - } - - // 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; - } -} - -// does pretty much the same as normal addAttributes, just need to get the range -- (void)addTypingAttributes { - [self addAttributes:_input->textView.selectedRange withTypingAttr:YES]; +- (NSString *)getValue { + return @"EnrichedOrderedList"; } -- (void)removeAttributes:(NSRange)range { - NSArray *paragraphs = - [ParagraphsUtils getSeparateParagraphsRangesIn:_input->textView - range:range]; - - [_input->textView.textStorage beginEditing]; - - 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]; - }]; - } - - [_input->textView.textStorage endEditing]; - - // also remove typing attributes - NSMutableDictionary *typingAttrs = - [_input->textView.typingAttributes mutableCopy]; - NSMutableParagraphStyle *pStyle = - [typingAttrs[NSParagraphStyleAttributeName] mutableCopy]; - pStyle.textLists = @[]; - pStyle.headIndent = 0; - pStyle.firstLineHeadIndent = 0; - typingAttrs[NSParagraphStyleAttributeName] = pStyle; - _input->textView.typingAttributes = typingAttrs; +- (BOOL)isParagraph { + return YES; } -// needed for the sake of style conflicts, needs to do exactly the same as -// removeAttribtues -- (void)removeTypingAttributes { - [self removeAttributes:_input->textView.selectedRange]; +- (BOOL)needsZWS { + return YES; } -- (BOOL)handleBackspaceInRange:(NSRange)range replacementText:(NSString *)text { - if ([self detectStyle:_input->textView.selectedRange] && text.length == 0) { - // backspace while the style is active - - NSRange paragraphRange = [_input->textView.textStorage.string - paragraphRangeForRange:_input->textView.selectedRange]; - - if (NSEqualRanges(_input->textView.selectedRange, NSMakeRange(0, 0))) { - // a backspace on the very first input's line list point - // it doesn't run textVieDidChange so we need to manually remove - // attributes - [self removeAttributes:paragraphRange]; - return YES; - } else if (range.location == paragraphRange.location - 1) { - // same case in other lines; here, the removed range location will be - // exactly 1 less than paragraph range location - [self removeAttributes:paragraphRange]; - return YES; - } - } - return NO; +- (void)applyStyling:(NSRange)range { + // lists are drawn manually + // margin before marker + gap between marker and paragraph + CGFloat listHeadIndent = [self.input->config orderedListMarginLeft] + + [self.input->config orderedListGapWidth]; + + [self.input->textView.textStorage + enumerateAttribute:NSParagraphStyleAttributeName + inRange:range + options:0 + usingBlock:^(id _Nullable value, NSRange range, + BOOL *_Nonnull stop) { + NSMutableParagraphStyle *pStyle = + [(NSParagraphStyle *)value mutableCopy]; + pStyle.headIndent = listHeadIndent; + pStyle.firstLineHeadIndent = listHeadIndent; + [self.input->textView.textStorage + addAttribute:NSParagraphStyleAttributeName + value:pStyle + range:range]; + }]; } - (BOOL)tryHandlingListShorcutInRange:(NSRange)range replacementText:(NSString *)text { NSRange paragraphRange = - [_input->textView.textStorage.string paragraphRangeForRange:range]; + [self.input->textView.textStorage.string paragraphRangeForRange:range]; // a dot was added - check if we are both at the paragraph beginning + 1 - // character (which we want to be a dash) + // character (which we want to be a digit '1') if ([text isEqualToString:@"."] && range.location - 1 == paragraphRange.location) { - unichar charBefore = [_input->textView.textStorage.string + unichar charBefore = [self.input->textView.textStorage.string characterAtIndex:range.location - 1]; if (charBefore == '1') { // we got a match - add a list if possible - if ([_input handleStyleBlocksAndConflicts:[[self class] getStyleType] - range:paragraphRange]) { + if ([self.input handleStyleBlocksAndConflicts:[[self class] getType] + range:paragraphRange]) { // don't emit during the replacing - _input->blockEmitting = YES; + self.input->blockEmitting = YES; // remove the number [TextInsertionUtils replaceText:@"" at:NSMakeRange(paragraphRange.location, 1) additionalAttributes:nullptr - input:_input + input:self.input withSelection:YES]; - _input->blockEmitting = NO; + self.input->blockEmitting = NO; // add attributes on the paragraph - [self addAttributes:NSMakeRange(paragraphRange.location, - paragraphRange.length - 1) - withTypingAttr:YES]; + [self add:NSMakeRange(paragraphRange.location, + paragraphRange.length - 1) + withTyping:YES + withDirtyRange:YES]; + return YES; } } @@ -235,48 +83,4 @@ - (BOOL)tryHandlingListShorcutInRange:(NSRange)range return NO; } -- (BOOL)styleCondition:(id _Nullable)value range:(NSRange)range { - NSParagraphStyle *paragraph = (NSParagraphStyle *)value; - return paragraph != nullptr && paragraph.textLists.count == 1 && - paragraph.textLists.firstObject.markerFormat == - NSTextListMarkerDecimal; -} - -- (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:range]; - }]; - } else { - return [OccurenceUtils detect:NSParagraphStyleAttributeName - withInput:_input - atIndex:range.location - checkPrevious:YES - withCondition:^BOOL(id _Nullable value, NSRange range) { - return [self styleCondition:value range:range]; - }]; - } -} - -- (BOOL)anyOccurence:(NSRange)range { - return [OccurenceUtils any:NSParagraphStyleAttributeName - withInput:_input - inRange:range - withCondition:^BOOL(id _Nullable value, NSRange range) { - return [self styleCondition:value range:range]; - }]; -} - -- (NSArray *_Nullable)findAllOccurences:(NSRange)range { - return [OccurenceUtils all:NSParagraphStyleAttributeName - withInput:_input - inRange:range - withCondition:^BOOL(id _Nullable value, NSRange range) { - return [self styleCondition:value range:range]; - }]; -} - @end diff --git a/ios/styles/StrikethroughStyle.mm b/ios/styles/StrikethroughStyle.mm index f695efcb3..050b226ae 100644 --- a/ios/styles/StrikethroughStyle.mm +++ b/ios/styles/StrikethroughStyle.mm @@ -1,102 +1,24 @@ #import "EnrichedTextInputView.h" -#import "OccurenceUtils.h" #import "StyleHeaders.h" -@implementation StrikethroughStyle { - EnrichedTextInputView *_input; -} +@implementation StrikethroughStyle : StyleBase -+ (StyleType)getStyleType { ++ (StyleType)getType { return Strikethrough; } -+ (BOOL)isParagraphStyle { - return NO; -} - -- (instancetype)initWithInput:(id)input { - self = [super init]; - _input = (EnrichedTextInputView *)input; - return self; -} - -- (void)applyStyle:(NSRange)range { - BOOL isStylePresent = [self detectStyle:range]; - if (range.length >= 1) { - isStylePresent ? [self removeAttributes:range] - : [self addAttributes:range withTypingAttr:YES]; - } else { - isStylePresent ? [self removeTypingAttributes] : [self addTypingAttributes]; - } -} - -- (void)addAttributes:(NSRange)range withTypingAttr:(BOOL)withTypingAttr { - [_input->textView.textStorage addAttribute:NSStrikethroughStyleAttributeName - value:@(NSUnderlineStyleSingle) - range:range]; -} - -- (void)addTypingAttributes { - NSMutableDictionary *newTypingAttrs = - [_input->textView.typingAttributes mutableCopy]; - newTypingAttrs[NSStrikethroughStyleAttributeName] = @(NSUnderlineStyleSingle); - _input->textView.typingAttributes = newTypingAttrs; +- (NSString *)getKey { + return @"EnrichedStrikethrough"; } -- (void)removeAttributes:(NSRange)range { - [_input->textView.textStorage - removeAttribute:NSStrikethroughStyleAttributeName - range:range]; -} - -- (void)removeTypingAttributes { - NSMutableDictionary *newTypingAttrs = - [_input->textView.typingAttributes mutableCopy]; - [newTypingAttrs removeObjectForKey:NSStrikethroughStyleAttributeName]; - _input->textView.typingAttributes = newTypingAttrs; -} - -- (BOOL)styleCondition:(id _Nullable)value range:(NSRange)range { - NSNumber *strikethroughStyle = (NSNumber *)value; - return strikethroughStyle != nullptr && - [strikethroughStyle intValue] != NSUnderlineStyleNone; -} - -- (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:range]; - }]; - } else { - return [OccurenceUtils detect:NSStrikethroughStyleAttributeName - withInput:_input - atIndex:range.location - checkPrevious:NO - withCondition:^BOOL(id _Nullable value, NSRange range) { - return [self styleCondition:value range:range]; - }]; - } -} - -- (BOOL)anyOccurence:(NSRange)range { - return [OccurenceUtils any:NSStrikethroughStyleAttributeName - withInput:_input - inRange:range - withCondition:^BOOL(id _Nullable value, NSRange range) { - return [self styleCondition:value range:range]; - }]; +- (BOOL)isParagraph { + return NO; } -- (NSArray *_Nullable)findAllOccurences:(NSRange)range { - return [OccurenceUtils all:NSStrikethroughStyleAttributeName - withInput:_input - inRange:range - withCondition:^BOOL(id _Nullable value, NSRange range) { - return [self styleCondition:value range:range]; - }]; +- (void)applyStyling:(NSRange)range { + NSDictionary *styles = + @{NSStrikethroughStyleAttributeName : @(NSUnderlineStyleSingle)}; + [self.input->textView.textStorage addAttributes:styles range:range]; } @end diff --git a/ios/styles/UnderlineStyle.mm b/ios/styles/UnderlineStyle.mm index ae2ecfeb7..2d30eec58 100644 --- a/ios/styles/UnderlineStyle.mm +++ b/ios/styles/UnderlineStyle.mm @@ -1,139 +1,24 @@ #import "EnrichedTextInputView.h" -#import "OccurenceUtils.h" #import "StyleHeaders.h" -@implementation UnderlineStyle { - EnrichedTextInputView *_input; -} +@implementation UnderlineStyle -+ (StyleType)getStyleType { ++ (StyleType)getType { return Underline; } -+ (BOOL)isParagraphStyle { - return NO; -} - -- (instancetype)initWithInput:(id)input { - self = [super init]; - _input = (EnrichedTextInputView *)input; - return self; -} - -- (void)applyStyle:(NSRange)range { - BOOL isStylePresent = [self detectStyle:range]; - if (range.length >= 1) { - isStylePresent ? [self removeAttributes:range] - : [self addAttributes:range withTypingAttr:YES]; - } else { - isStylePresent ? [self removeTypingAttributes] : [self addTypingAttributes]; - } -} - -- (void)addAttributes:(NSRange)range withTypingAttr:(BOOL)withTypingAttr { - [_input->textView.textStorage addAttribute:NSUnderlineStyleAttributeName - value:@(NSUnderlineStyleSingle) - range:range]; -} - -- (void)addTypingAttributes { - NSMutableDictionary *newTypingAttrs = - [_input->textView.typingAttributes mutableCopy]; - newTypingAttrs[NSUnderlineStyleAttributeName] = @(NSUnderlineStyleSingle); - _input->textView.typingAttributes = newTypingAttrs; -} - -- (void)removeAttributes:(NSRange)range { - [_input->textView.textStorage removeAttribute:NSUnderlineStyleAttributeName - range:range]; +- (NSString *)getKey { + return @"EnrichedUnderline"; } -- (void)removeTypingAttributes { - NSMutableDictionary *newTypingAttrs = - [_input->textView.typingAttributes mutableCopy]; - [newTypingAttrs removeObjectForKey:NSUnderlineStyleAttributeName]; - _input->textView.typingAttributes = newTypingAttrs; -} - -- (BOOL)underlinedLinkConflictsInRange:(NSRange)range { - BOOL conflicted = NO; - if ([_input->config linkDecorationLine] == DecorationUnderline) { - LinkStyle *linkStyle = _input->stylesDict[@([LinkStyle getStyleType])]; - conflicted = range.length > 0 ? [linkStyle anyOccurence:range] - : [linkStyle detectStyle:range]; - } - return conflicted; -} - -- (BOOL)underlinedMentionConflictsInRange:(NSRange)range { - BOOL conflicted = NO; - MentionStyle *mentionStyle = - _input->stylesDict[@([MentionStyle getStyleType])]; - if (range.length == 0) { - if ([mentionStyle detectStyle:range]) { - MentionParams *params = [mentionStyle getMentionParamsAt:range.location]; - conflicted = - [_input->config mentionStylePropsForIndicator:params.indicator] - .decorationLine == DecorationUnderline; - } - } else { - NSArray *occurences = [mentionStyle findAllOccurences:range]; - for (StylePair *pair in occurences) { - MentionParams *params = [mentionStyle - getMentionParamsAt:[pair.rangeValue rangeValue].location]; - if ([_input->config mentionStylePropsForIndicator:params.indicator] - .decorationLine == DecorationUnderline) { - conflicted = YES; - break; - } - } - } - return conflicted; -} - -- (BOOL)styleCondition:(id _Nullable)value range:(NSRange)range { - NSNumber *underlineStyle = (NSNumber *)value; - return underlineStyle != nullptr && - [underlineStyle intValue] != NSUnderlineStyleNone && - ![self underlinedLinkConflictsInRange:range] && - ![self underlinedMentionConflictsInRange: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:range]; - }]; - } else { - return [OccurenceUtils detect:NSUnderlineStyleAttributeName - withInput:_input - atIndex:range.location - checkPrevious:NO - withCondition:^BOOL(id _Nullable value, NSRange range) { - return [self styleCondition:value range:range]; - }]; - } -} - -- (BOOL)anyOccurence:(NSRange)range { - return [OccurenceUtils any:NSUnderlineStyleAttributeName - withInput:_input - inRange:range - withCondition:^BOOL(id _Nullable value, NSRange range) { - return [self styleCondition:value range:range]; - }]; +- (BOOL)isParagraph { + return NO; } -- (NSArray *_Nullable)findAllOccurences:(NSRange)range { - return [OccurenceUtils all:NSUnderlineStyleAttributeName - withInput:_input - inRange:range - withCondition:^BOOL(id _Nullable value, NSRange range) { - return [self styleCondition:value range:range]; - }]; +- (void)applyStyling:(NSRange)range { + [self.input->textView.textStorage addAttribute:NSUnderlineStyleAttributeName + value:@(NSUnderlineStyleSingle) + range:range]; } @end diff --git a/ios/styles/UnorderedListStyle.mm b/ios/styles/UnorderedListStyle.mm index d2aa15099..2e0a1a103 100644 --- a/ios/styles/UnorderedListStyle.mm +++ b/ios/styles/UnorderedListStyle.mm @@ -1,232 +1,81 @@ #import "EnrichedTextInputView.h" -#import "FontExtension.h" -#import "OccurenceUtils.h" -#import "ParagraphsUtils.h" +#import "RangeUtils.h" #import "StyleHeaders.h" #import "TextInsertionUtils.h" -@implementation UnorderedListStyle { - EnrichedTextInputView *_input; -} +@implementation UnorderedListStyle -+ (StyleType)getStyleType { ++ (StyleType)getType { return UnorderedList; } -+ (BOOL)isParagraphStyle { - return YES; -} - -- (CGFloat)getHeadIndent { - // lists are drawn manually - // margin before bullet + gap between bullet and paragraph - return [_input->config unorderedListMarginLeft] + - [_input->config unorderedListGapWidth]; -} - -- (instancetype)initWithInput:(id)input { - self = [super init]; - _input = (EnrichedTextInputView *)input; - return self; -} - -- (void)applyStyle:(NSRange)range { - BOOL isStylePresent = [self detectStyle:range]; - if (range.length >= 1) { - isStylePresent ? [self removeAttributes:range] - : [self addAttributes:range withTypingAttr:YES]; - } else { - isStylePresent ? [self removeTypingAttributes] : [self addTypingAttributes]; - } -} - -// we assume correct paragraph range is already given -- (void)addAttributes:(NSRange)range withTypingAttr:(BOOL)withTypingAttr { - NSTextList *bullet = - [[NSTextList alloc] initWithMarkerFormat:NSTextListMarkerDisc options:0]; - NSArray *paragraphs = - [ParagraphsUtils getSeparateParagraphsRangesIn:_input->textView - range:range]; - // if we fill empty lines with zero width spaces, we need to offset later - // ranges - NSInteger offset = 0; - // needed for range adjustments - NSRange preModificationRange = _input->textView.selectedRange; - - // let's not emit some weird selection changes or text/html changes - _input->blockEmitting = YES; - - 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:[_input->textView.textStorage.string - characterAtIndex:fixedRange.location]])) { - [TextInsertionUtils insertText:@"\u200B" - at:fixedRange.location - additionalAttributes:nullptr - input:_input - withSelection:NO]; - fixedRange = NSMakeRange(fixedRange.location, fixedRange.length + 1); - offset += 1; - } - - [_input->textView.textStorage - enumerateAttribute:NSParagraphStyleAttributeName - inRange:fixedRange - options:0 - usingBlock:^(id _Nullable value, NSRange range, - BOOL *_Nonnull stop) { - NSMutableParagraphStyle *pStyle = - [(NSParagraphStyle *)value mutableCopy]; - pStyle.textLists = @[ bullet ]; - pStyle.headIndent = [self getHeadIndent]; - pStyle.firstLineHeadIndent = [self getHeadIndent]; - [_input->textView.textStorage - addAttribute:NSParagraphStyleAttributeName - value:pStyle - range:range]; - }]; - } - - // back to emitting - _input->blockEmitting = NO; - - if (preModificationRange.length == 0) { - // fix selection if only one line was possibly made a list and filled with a - // space - _input->textView.selectedRange = preModificationRange; - } else { - // in other cases, fix the selection with newly made offsets - _input->textView.selectedRange = NSMakeRange( - preModificationRange.location, preModificationRange.length + offset); - } - - // 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; - } -} - -// does pretty much the same as normal addAttributes, just need to get the range -- (void)addTypingAttributes { - [self addAttributes:_input->textView.selectedRange withTypingAttr:YES]; +- (NSString *)getValue { + return @"EnrichedUnorderedList"; } -- (void)removeAttributes:(NSRange)range { - NSArray *paragraphs = - [ParagraphsUtils getSeparateParagraphsRangesIn:_input->textView - range:range]; - - [_input->textView.textStorage beginEditing]; - - 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]; - }]; - } - - [_input->textView.textStorage endEditing]; - - // also remove typing attributes - NSMutableDictionary *typingAttrs = - [_input->textView.typingAttributes mutableCopy]; - NSMutableParagraphStyle *pStyle = - [typingAttrs[NSParagraphStyleAttributeName] mutableCopy]; - pStyle.textLists = @[]; - pStyle.headIndent = 0; - pStyle.firstLineHeadIndent = 0; - typingAttrs[NSParagraphStyleAttributeName] = pStyle; - _input->textView.typingAttributes = typingAttrs; +- (BOOL)isParagraph { + return YES; } -// needed for the sake of style conflicts, needs to do exactly the same as -// removeAttribtues -- (void)removeTypingAttributes { - [self removeAttributes:_input->textView.selectedRange]; +- (BOOL)needsZWS { + return YES; } -- (BOOL)handleBackspaceInRange:(NSRange)range replacementText:(NSString *)text { - if ([self detectStyle:_input->textView.selectedRange] && text.length == 0) { - // backspace while the style is active - - NSRange paragraphRange = [_input->textView.textStorage.string - paragraphRangeForRange:_input->textView.selectedRange]; - - if (NSEqualRanges(_input->textView.selectedRange, NSMakeRange(0, 0))) { - // a backspace on the very first input's line list point - // it doesn't run textVieDidChange so we need to manually remove - // attributes - [self removeAttributes:paragraphRange]; - return YES; - } else if (range.location == paragraphRange.location - 1) { - // same case in other lines; here, the removed range location will be - // exactly 1 less than paragraph range location - [self removeAttributes:paragraphRange]; - return YES; - } - } - return NO; +- (void)applyStyling:(NSRange)range { + // lists are drawn manually + // margin before bullet + gap between bullet and paragraph + CGFloat listHeadIndent = [self.input->config unorderedListMarginLeft] + + [self.input->config unorderedListGapWidth]; + + [self.input->textView.textStorage + enumerateAttribute:NSParagraphStyleAttributeName + inRange:range + options:0 + usingBlock:^(id _Nullable value, NSRange range, + BOOL *_Nonnull stop) { + NSMutableParagraphStyle *pStyle = + [(NSParagraphStyle *)value mutableCopy]; + pStyle.headIndent = listHeadIndent; + pStyle.firstLineHeadIndent = listHeadIndent; + [self.input->textView.textStorage + addAttribute:NSParagraphStyleAttributeName + value:pStyle + range:range]; + }]; } - (BOOL)tryHandlingListShorcutInRange:(NSRange)range replacementText:(NSString *)text { NSRange paragraphRange = - [_input->textView.textStorage.string paragraphRangeForRange:range]; + [self.input->textView.textStorage.string paragraphRangeForRange:range]; // space was added - check if we are both at the paragraph beginning + 1 // character (which we want to be a dash) if ([text isEqualToString:@" "] && range.location - 1 == paragraphRange.location) { - unichar charBefore = [_input->textView.textStorage.string + unichar charBefore = [self.input->textView.textStorage.string characterAtIndex:range.location - 1]; if (charBefore == '-') { // we got a match - add a list if possible - if ([_input handleStyleBlocksAndConflicts:[[self class] getStyleType] - range:paragraphRange]) { + if ([self.input handleStyleBlocksAndConflicts:[[self class] getType] + range:paragraphRange]) { // don't emit during the replacing - _input->blockEmitting = YES; + self.input->blockEmitting = YES; // remove the dash [TextInsertionUtils replaceText:@"" at:NSMakeRange(paragraphRange.location, 1) additionalAttributes:nullptr - input:_input + input:self.input withSelection:YES]; - _input->blockEmitting = NO; + self.input->blockEmitting = NO; // add attributes on the dashless paragraph - [self addAttributes:NSMakeRange(paragraphRange.location, - paragraphRange.length - 1) - withTypingAttr:YES]; + [self add:NSMakeRange(paragraphRange.location, + paragraphRange.length - 1) + withTyping:YES + withDirtyRange:YES]; + return YES; } } @@ -234,47 +83,4 @@ - (BOOL)tryHandlingListShorcutInRange:(NSRange)range return NO; } -- (BOOL)styleCondition:(id _Nullable)value range:(NSRange)range { - NSParagraphStyle *paragraph = (NSParagraphStyle *)value; - return paragraph != nullptr && paragraph.textLists.count == 1 && - paragraph.textLists.firstObject.markerFormat == NSTextListMarkerDisc; -} - -- (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:range]; - }]; - } else { - return [OccurenceUtils detect:NSParagraphStyleAttributeName - withInput:_input - atIndex:range.location - checkPrevious:YES - withCondition:^BOOL(id _Nullable value, NSRange range) { - return [self styleCondition:value range:range]; - }]; - } -} - -- (BOOL)anyOccurence:(NSRange)range { - return [OccurenceUtils any:NSParagraphStyleAttributeName - withInput:_input - inRange:range - withCondition:^BOOL(id _Nullable value, NSRange range) { - return [self styleCondition:value range:range]; - }]; -} - -- (NSArray *_Nullable)findAllOccurences:(NSRange)range { - return [OccurenceUtils all:NSParagraphStyleAttributeName - withInput:_input - inRange:range - withCondition:^BOOL(id _Nullable value, NSRange range) { - return [self styleCondition:value range:range]; - }]; -} - @end diff --git a/ios/utils/CheckboxHitTestUtils.mm b/ios/utils/CheckboxHitTestUtils.mm index e020fca6f..935229876 100644 --- a/ios/utils/CheckboxHitTestUtils.mm +++ b/ios/utils/CheckboxHitTestUtils.mm @@ -42,14 +42,13 @@ + (BOOL)isCheckboxGlyph:(NSUInteger)glyphIndex } CheckboxListStyle *checkboxListStyle = - (CheckboxListStyle *) - input->stylesDict[@([CheckboxListStyle getStyleType])]; + (CheckboxListStyle *)input->stylesDict[@([CheckboxListStyle getType])]; if (!checkboxListStyle) { return NO; } - return [checkboxListStyle detectStyle:NSMakeRange(charIndex, 0)]; + return [checkboxListStyle detect:NSMakeRange(charIndex, 0)]; } // MARK: - Checkbox rect diff --git a/ios/utils/DotReplacementUtils.h b/ios/utils/DotReplacementUtils.h new file mode 100644 index 000000000..5e4fc8225 --- /dev/null +++ b/ios/utils/DotReplacementUtils.h @@ -0,0 +1,10 @@ +#import +#pragma once + +@interface DotReplacementUtils : NSObject ++ (void)handleDotReplacement:(id)input + textStorage:(NSTextStorage *)textStorage + editedMask:(NSTextStorageEditActions)editedMask + editedRange:(NSRange)editedRange + delta:(NSInteger)delta; +@end diff --git a/ios/utils/DotReplacementUtils.mm b/ios/utils/DotReplacementUtils.mm new file mode 100644 index 000000000..3a9abd596 --- /dev/null +++ b/ios/utils/DotReplacementUtils.mm @@ -0,0 +1,68 @@ +#import "DotReplacementUtils.h" +#import "EnrichedTextInputView.h" + +@implementation DotReplacementUtils + +// This is a fix for iOS replacing a space with a dot when two spaces are +// quickly inputted That operation doesn't properly extend our custom attributes +// and we do it here manually ++ (void)handleDotReplacement:(id)input + textStorage:(NSTextStorage *)textStorage + editedMask:(NSTextStorageEditActions)editedMask + editedRange:(NSRange)editedRange + delta:(NSInteger)delta { + EnrichedTextInputView *typedInput = (EnrichedTextInputView *)input; + if (typedInput == nullptr) { + return; + } + + // Conditions for the dot attributes fix: + // - character edition was done + // - it edited one character + // - new character is a dot + // - delta=0, meaning a replacement was done + // - there is something before the edited range to get attributes from + if ((editedMask & NSTextStorageEditedCharacters) != 0 && + editedRange.length == 1 && + [[textStorage.string substringWithRange:editedRange] + isEqualToString:@"."] && + delta == 0 && editedRange.location > 0) { + // If all of the above are true, we are sure some dot replacement has been + // done So we manually need to apply the preceeding attribtues to the dot + NSDictionary *prevAttrs = + [textStorage attributesAtIndex:editedRange.location - 1 + effectiveRange:nullptr]; + [textStorage addAttributes:prevAttrs range:editedRange]; + typedInput->dotReplacementRange = [NSValue valueWithRange:editedRange]; + return; + } + + // Space after the dot added by iOS comes in a separate, second callback. + // Checking its conditions: + // - dotReplacementRange defined + // - dotReplacementRange was exactly before the new edited range + // - character edition was done + // - it edited one character + // - edited character is a space + // - delta=1, meaning addition was done + if (typedInput->dotReplacementRange != nullptr && + [typedInput->dotReplacementRange rangeValue].location + 1 == + editedRange.location && + (editedMask & NSTextStorageEditedCharacters) != 0 && + editedRange.length == 1 && + [[textStorage.string substringWithRange:editedRange] + isEqualToString:@" "] && + delta == 1) { + // If all of the above are true, we are now sure it was the iOS dot + // replacement Only then do we also fix attribtues of the space added + // afterwards + NSDictionary *prevAttrs = + [textStorage attributesAtIndex:editedRange.location - 1 + effectiveRange:nullptr]; + [textStorage addAttributes:prevAttrs range:editedRange]; + } + // always reset the replacement range after any processing + typedInput->dotReplacementRange = nullptr; +} + +@end diff --git a/ios/utils/OccurenceUtils.h b/ios/utils/OccurenceUtils.h index 83cf12a65..8f54f1d1b 100644 --- a/ios/utils/OccurenceUtils.h +++ b/ios/utils/OccurenceUtils.h @@ -11,7 +11,7 @@ + (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 diff --git a/ios/utils/ParagraphAttributesUtils.mm b/ios/utils/ParagraphAttributesUtils.mm index fee5503f5..09093bfaa 100644 --- a/ios/utils/ParagraphAttributesUtils.mm +++ b/ios/utils/ParagraphAttributesUtils.mm @@ -1,6 +1,6 @@ #import "ParagraphAttributesUtils.h" #import "EnrichedTextInputView.h" -#import "ParagraphsUtils.h" +#import "RangeUtils.h" #import "StyleHeaders.h" #import "TextInsertionUtils.h" @@ -14,17 +14,6 @@ + (BOOL)handleBackspaceInRange:(NSRange)range replacementText:(NSString *)text input:(id)input { EnrichedTextInputView *typedInput = (EnrichedTextInputView *)input; - UnorderedListStyle *ulStyle = - typedInput->stylesDict[@([UnorderedListStyle getStyleType])]; - OrderedListStyle *olStyle = - typedInput->stylesDict[@([OrderedListStyle getStyleType])]; - BlockQuoteStyle *bqStyle = - typedInput->stylesDict[@([BlockQuoteStyle getStyleType])]; - CodeBlockStyle *cbStyle = - typedInput->stylesDict[@([CodeBlockStyle getStyleType])]; - CheckboxListStyle *cbLStyle = - typedInput->stylesDict[@([CheckboxListStyle getStyleType])]; - if (typedInput == nullptr) { return NO; } @@ -39,9 +28,8 @@ + (BOOL)handleBackspaceInRange:(NSRange)range NSRange paragraphRange = [typedInput->textView.textStorage.string paragraphRangeForRange:range]; - NSArray *paragraphs = - [ParagraphsUtils getNonNewlineRangesIn:typedInput->textView - range:paragraphRange]; + NSArray *paragraphs = [RangeUtils getNonNewlineRangesIn:typedInput->textView + range:paragraphRange]; if (paragraphs.count == 0) { return NO; } @@ -53,41 +41,25 @@ + (BOOL)handleBackspaceInRange:(NSRange)range if (range.location == nonNewlineRange.location && range.length >= nonNewlineRange.length) { - // for lists, quotes and codeblocks present we do the following: + // for styles that need ZWS (lists, quotes, etc.) we do the following: // - manually do the removing // - reset typing attribtues so that the previous line styles don't get // applied // - reapply the paragraph style that was present so that a zero width space // appears here - NSArray *handledStyles = @[ ulStyle, olStyle, bqStyle, cbStyle, cbLStyle ]; - for (id style in handledStyles) { - if ([style detectStyle:nonNewlineRange]) { - // For checkbox lists, preserve the current checked state - if (style == cbLStyle) { - BOOL isCurrentlyChecked = - [cbLStyle getCheckboxStateAt:range.location]; - [TextInsertionUtils replaceText:text - at:range - additionalAttributes:nullptr - input:typedInput - withSelection:YES]; - typedInput->textView.typingAttributes = - typedInput->defaultTypingAttributes; - [cbLStyle addAttributesWithCheckedValue:isCurrentlyChecked - inRange:NSMakeRange(range.location, 0) - withTypingAttr:YES]; - } else { - [TextInsertionUtils replaceText:text - at:range - additionalAttributes:nullptr - input:typedInput - withSelection:YES]; - typedInput->textView.typingAttributes = - typedInput->defaultTypingAttributes; - [style addAttributes:NSMakeRange(range.location, 0) - withTypingAttr:YES]; - } - + for (NSNumber *type in typedInput->stylesDict) { + StyleBase *style = typedInput->stylesDict[type]; + if ([style needsZWS] && [style detect:nonNewlineRange]) { + [TextInsertionUtils replaceText:text + at:range + additionalAttributes:nullptr + input:typedInput + withSelection:YES]; + typedInput->textView.typingAttributes = + typedInput->defaultTypingAttributes; + [style add:NSMakeRange(range.location, 0) + withTyping:YES + withDirtyRange:YES]; return YES; } } diff --git a/ios/utils/ParagraphsUtils.mm b/ios/utils/ParagraphsUtils.mm deleted file mode 100644 index 342d7334e..000000000 --- a/ios/utils/ParagraphsUtils.mm +++ /dev/null @@ -1,68 +0,0 @@ -#import "ParagraphsUtils.h" - -@implementation ParagraphsUtils - -+ (NSArray *)getSeparateParagraphsRangesIn:(UITextView *)textView - range:(NSRange)range { - // just in case, get full paragraphs range - NSRange fullRange = - [textView.textStorage.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 = int(fullRange.location); - i < fullRange.location + fullRange.length; i++) { - unichar currentChar = [textView.textStorage.string characterAtIndex:i]; - if ([[NSCharacterSet newlineCharacterSet] characterIsMember:currentChar]) { - NSRange paragraphRange = [textView.textStorage.string - paragraphRangeForRange:NSMakeRange(lastStart, i - lastStart)]; - [results addObject:[NSValue valueWithRange:paragraphRange]]; - lastStart = i + 1; - } - } - - if (lastStart < fullRange.location + fullRange.length) { - NSRange paragraphRange = [textView.textStorage.string - paragraphRangeForRange:NSMakeRange(lastStart, fullRange.location + - fullRange.length - - lastStart)]; - [results addObject:[NSValue valueWithRange:paragraphRange]]; - } - - return results; -} - -+ (NSArray *)getNonNewlineRangesIn:(UITextView *)textView range:(NSRange)range { - NSMutableArray *nonNewlineRanges = [[NSMutableArray alloc] init]; - int lastRangeLocation = int(range.location); - - for (int i = int(range.location); i < range.location + range.length; i++) { - unichar currentChar = [textView.textStorage.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/ParagraphsUtils.h b/ios/utils/RangeUtils.h similarity index 52% rename from ios/utils/ParagraphsUtils.h rename to ios/utils/RangeUtils.h index a6a1cfabf..7d5211d09 100644 --- a/ios/utils/ParagraphsUtils.h +++ b/ios/utils/RangeUtils.h @@ -1,8 +1,12 @@ #pragma once #import -@interface ParagraphsUtils : NSObject +@interface RangeUtils : NSObject + (NSArray *)getSeparateParagraphsRangesIn:(UITextView *)textView range:(NSRange)range; + (NSArray *)getNonNewlineRangesIn:(UITextView *)textView range:(NSRange)range; ++ (NSArray *)connectAndDedupeRanges:(NSArray *)ranges; ++ (NSArray *)shiftRanges:(NSArray *)ranges + withEditedRange:(NSRange)editedRange + changeInLength:(NSInteger)delta; @end diff --git a/ios/utils/RangeUtils.mm b/ios/utils/RangeUtils.mm new file mode 100644 index 000000000..1a4d79825 --- /dev/null +++ b/ios/utils/RangeUtils.mm @@ -0,0 +1,183 @@ +#import "RangeUtils.h" + +@implementation RangeUtils + ++ (NSArray *)getSeparateParagraphsRangesIn:(UITextView *)textView + range:(NSRange)range { + // just in case, get full paragraphs range + NSRange fullRange = + [textView.textStorage.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 = int(fullRange.location); + i < fullRange.location + fullRange.length; i++) { + unichar currentChar = [textView.textStorage.string characterAtIndex:i]; + if ([[NSCharacterSet newlineCharacterSet] characterIsMember:currentChar]) { + NSRange paragraphRange = [textView.textStorage.string + paragraphRangeForRange:NSMakeRange(lastStart, i - lastStart)]; + [results addObject:[NSValue valueWithRange:paragraphRange]]; + lastStart = i + 1; + } + } + + if (lastStart < fullRange.location + fullRange.length) { + NSRange paragraphRange = [textView.textStorage.string + paragraphRangeForRange:NSMakeRange(lastStart, fullRange.location + + fullRange.length - + lastStart)]; + [results addObject:[NSValue valueWithRange:paragraphRange]]; + } + + return results; +} + ++ (NSArray *)getNonNewlineRangesIn:(UITextView *)textView range:(NSRange)range { + NSMutableArray *nonNewlineRanges = [[NSMutableArray alloc] init]; + int lastRangeLocation = int(range.location); + + for (int i = int(range.location); i < range.location + range.length; i++) { + unichar currentChar = [textView.textStorage.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; +} + +// Condenses an array of NSRange to make sure the overlapping ones are connected +// + sorted based on NSRange.location ++ (NSArray *)connectAndDedupeRanges:(NSArray *)ranges { + if (ranges.count == 0) { + return @[]; + } + + // We sort primarily by location. If locations match, shorter length goes + // first + NSArray *sortedRanges = + [ranges sortedArrayUsingComparator:^NSComparisonResult(NSValue *obj1, + NSValue *obj2) { + NSRange range1 = obj1.rangeValue; + NSRange range2 = obj2.rangeValue; + + if (range1.location < range2.location) + return NSOrderedAscending; + if (range1.location > range2.location) + return NSOrderedDescending; + + if (range1.length < range2.length) + return NSOrderedAscending; + if (range1.length > range2.length) + return NSOrderedDescending; + + return NSOrderedSame; + }]; + + NSMutableArray *mergedRanges = [[NSMutableArray alloc] init]; + + // We work by comparing each two ranges. + // If we connected some ranges, the newly created one is still compared with + // the next ranges from the sorted list. + NSRange currentRange = sortedRanges[0].rangeValue; + + for (NSUInteger i = 1; i < sortedRanges.count; i++) { + NSRange nextRange = sortedRanges[i].rangeValue; + + // Calculate the end points + NSUInteger currentMax = currentRange.location + currentRange.length; + NSUInteger nextMax = nextRange.location + nextRange.length; + + // If next range starts before (or exactly when) the current one ends. + if (nextRange.location <= currentMax) { + // Merge them; the new end is the maximum of the two ends + NSUInteger newMax = MAX(currentMax, nextMax); + currentRange.length = newMax - currentRange.location; + } else { + // No overlap; push the current range and start a new one + [mergedRanges addObject:[NSValue valueWithRange:currentRange]]; + currentRange = nextRange; + } + } + + // Add the final range + [mergedRanges addObject:[NSValue valueWithRange:currentRange]]; + + return [mergedRanges copy]; +} + +// Updates a list of NSRanges based on a text change, so that they are still +// pointing to the same characters. editedRange is the post-change range of the +// edited fragment. delta tells what is the length change of the edited +// fragment. While the ranges outside of the change are being just shifted, the +// ones intersecting with it are just merging with the change. ++ (NSArray *)shiftRanges:(NSArray *)ranges + withEditedRange:(NSRange)editedRange + changeInLength:(NSInteger)delta { + NSMutableArray *result = [[NSMutableArray alloc] init]; + + // Calculate what the changed range was like before being edited + NSUInteger oldEditLength = editedRange.length - delta; + NSUInteger oldEditEnd = editedRange.location + oldEditLength; + + NSUInteger newEditEnd = editedRange.location + editedRange.length; + + for (NSValue *value in ranges) { + NSRange range = [value rangeValue]; + NSUInteger rangeEnd = range.location + range.length; + + if (rangeEnd <= editedRange.location) { + // Range was strictly before the old edit range. + // Do nothing. + [result addObject:value]; + } else if (range.location >= oldEditEnd) { + // Range was strictly after the old edit range. + // Shift it by the delta. + [result + addObject:[NSValue valueWithRange:NSMakeRange(range.location + delta, + range.length)]]; + } else { + // Range overlaps the old edit range in some way. + // Our best bet is to merge it with the edit range. + + NSUInteger newStart = MIN(range.location, editedRange.location); + + NSUInteger newEnd; + if (rangeEnd <= oldEditEnd) { + // The range was inside the editedRange before. + // So we use the newer editRange as the end here. + newEnd = newEditEnd; + } else { + // The range sticked outside of the editedRange before. + // It is safe to shift its end and use it. + newEnd = rangeEnd + delta; + } + + NSRange adjustedRange = NSMakeRange(newStart, newEnd - newStart); + [result addObject:[NSValue valueWithRange:adjustedRange]]; + } + } + + return result; +} + +@end diff --git a/ios/utils/ZeroWidthSpaceUtils.mm b/ios/utils/ZeroWidthSpaceUtils.mm index 4df7d10b5..2ab5769ed 100644 --- a/ios/utils/ZeroWidthSpaceUtils.mm +++ b/ios/utils/ZeroWidthSpaceUtils.mm @@ -43,24 +43,16 @@ + (void)removeSpacesIfNeededinInput:(EnrichedTextInputView *)input { continue; } - UnorderedListStyle *ulStyle = - input->stylesDict[@([UnorderedListStyle getStyleType])]; - OrderedListStyle *olStyle = - input->stylesDict[@([OrderedListStyle getStyleType])]; - BlockQuoteStyle *bqStyle = - input->stylesDict[@([BlockQuoteStyle getStyleType])]; - CodeBlockStyle *cbStyle = - input->stylesDict[@([CodeBlockStyle getStyleType])]; - CheckboxListStyle *cbLStyle = - input->stylesDict[@([CheckboxListStyle getStyleType])]; - - // zero width spaces with no lists/blockquotes/codeblocks on them get - // removed - if (![ulStyle detectStyle:characterRange] && - ![olStyle detectStyle:characterRange] && - ![bqStyle detectStyle:characterRange] && - ![cbStyle detectStyle:characterRange] && - ![cbLStyle detectStyle:characterRange]) { + // zero width spaces with no needsZWS style on them get removed + BOOL anyZWSStylePresent = NO; + for (NSNumber *type in input->stylesDict) { + StyleBase *style = input->stylesDict[type]; + if ([style needsZWS] && [style detect:characterRange]) { + anyZWSStylePresent = YES; + break; + } + } + if (!anyZWSStylePresent) { [indexesToBeRemoved addObject:@(characterRange.location)]; } } @@ -95,20 +87,29 @@ + (void)removeSpacesIfNeededinInput:(EnrichedTextInputView *)input { } } +// Collects active inline (non-paragraph) meta-attributes from the style +// dictionary so that ZWS characters carry the same meta-attributes that are +// currently active in the typing attributes. ++ (NSDictionary *)inlineMetaAttributesForInput:(EnrichedTextInputView *)input { + NSMutableDictionary *metaAttrs = [NSMutableDictionary new]; + for (NSNumber *type in input->stylesDict) { + StyleBase *style = input->stylesDict[type]; + if (![style isParagraph]) { + AttributeEntry *entry = + [style getEntryIfPresent:input->textView.selectedRange]; + if (entry) { + metaAttrs[entry.key] = entry.value; + } + } + } + return metaAttrs.count > 0 ? metaAttrs : nullptr; +} + + (void)addSpacesIfNeededinInput:(EnrichedTextInputView *)input { - UnorderedListStyle *ulStyle = - input->stylesDict[@([UnorderedListStyle getStyleType])]; - OrderedListStyle *olStyle = - input->stylesDict[@([OrderedListStyle getStyleType])]; - BlockQuoteStyle *bqStyle = - input->stylesDict[@([BlockQuoteStyle getStyleType])]; - CodeBlockStyle *cbStyle = input->stylesDict[@([CodeBlockStyle getStyleType])]; - CheckboxListStyle *cbLStyle = - input->stylesDict[@([CheckboxListStyle getStyleType])]; NSMutableArray *indexesToBeInserted = [[NSMutableArray alloc] init]; NSRange preAddSelection = input->textView.selectedRange; - for (int i = 0; i < input->textView.textStorage.string.length; i++) { + for (NSUInteger i = 0; i < input->textView.textStorage.string.length; i++) { unichar character = [input->textView.textStorage.string characterAtIndex:i]; if ([[NSCharacterSet newlineCharacterSet] characterIsMember:character]) { @@ -117,11 +118,15 @@ + (void)addSpacesIfNeededinInput:(EnrichedTextInputView *)input { paragraphRangeForRange:characterRange]; if (paragraphRange.length == 1) { - if ([ulStyle detectStyle:characterRange] || - [olStyle detectStyle:characterRange] || - [bqStyle detectStyle:characterRange] || - [cbStyle detectStyle:characterRange] || - [cbLStyle detectStyle:characterRange]) { + BOOL anyZWSStylePresent = NO; + for (NSNumber *type in input->stylesDict) { + StyleBase *style = input->stylesDict[type]; + if ([style needsZWS] && [style detect:characterRange]) { + anyZWSStylePresent = YES; + break; + } + } + if (anyZWSStylePresent) { // we have an empty list or quote item with no space: add it! [indexesToBeInserted addObject:@(paragraphRange.location)]; } @@ -129,6 +134,8 @@ + (void)addSpacesIfNeededinInput:(EnrichedTextInputView *)input { } } + NSDictionary *metaAttrs = [self inlineMetaAttributesForInput:input]; + // do the replacing NSInteger offset = 0; NSInteger postAddLocationOffset = 0; @@ -137,7 +144,7 @@ + (void)addSpacesIfNeededinInput:(EnrichedTextInputView *)input { NSRange replaceRange = NSMakeRange([index integerValue] + offset, 1); [TextInsertionUtils replaceText:@"\u200B\n" at:replaceRange - additionalAttributes:nullptr + additionalAttributes:metaAttrs input:input withSelection:NO]; offset += 1; @@ -154,15 +161,22 @@ + (void)addSpacesIfNeededinInput:(EnrichedTextInputView *)input { NSRange lastRange = NSMakeRange(input->textView.textStorage.string.length, 0); NSRange lastParagraphRange = [input->textView.textStorage.string paragraphRangeForRange:lastRange]; - if (lastParagraphRange.length == 0 && - ([ulStyle detectStyle:lastRange] || [olStyle detectStyle:lastRange] || - [bqStyle detectStyle:lastRange] || [cbStyle detectStyle:lastRange] || - [cbLStyle detectStyle:lastRange])) { - [TextInsertionUtils insertText:@"\u200B" - at:lastRange.location - additionalAttributes:nullptr - input:input - withSelection:NO]; + if (lastParagraphRange.length == 0) { + BOOL anyZWSStylePresent = NO; + for (NSNumber *type in input->stylesDict) { + StyleBase *style = input->stylesDict[type]; + if ([style needsZWS] && [style detect:lastRange]) { + anyZWSStylePresent = YES; + break; + } + } + if (anyZWSStylePresent) { + [TextInsertionUtils insertText:@"\u200B" + at:lastRange.location + additionalAttributes:metaAttrs + input:input + withSelection:NO]; + } } // fix the selection if needed @@ -176,7 +190,7 @@ + (void)addSpacesIfNeededinInput:(EnrichedTextInputView *)input { + (BOOL)handleBackspaceInRange:(NSRange)range replacementText:(NSString *)text input:(id)input { - if (range.length != 1 || ![text isEqualToString:@""]) { + if (![text isEqualToString:@""]) { return NO; } EnrichedTextInputView *typedInput = (EnrichedTextInputView *)input; @@ -184,6 +198,26 @@ + (BOOL)handleBackspaceInRange:(NSRange)range return NO; } + // Backspace at the very beginning of the input ({0, 0}). + // Nothing to delete, but if the first paragraph has a needsZWS style, + // remove it. + if (range.length == 0 && range.location == 0) { + NSRange firstParagraphRange = [typedInput->textView.textStorage.string + paragraphRangeForRange:NSMakeRange(0, 0)]; + for (NSNumber *type in typedInput->stylesDict) { + StyleBase *style = typedInput->stylesDict[type]; + if ([style needsZWS] && [style detect:firstParagraphRange]) { + [style remove:firstParagraphRange withDirtyRange:YES]; + return YES; + } + } + return NO; + } + + if (range.length != 1) { + return NO; + } + unichar character = [typedInput->textView.textStorage.string characterAtIndex:range.location]; // zero-width space got backspaced @@ -207,52 +241,44 @@ + (BOOL)handleBackspaceInRange:(NSRange)range styleRemovalRange = NSMakeRange(paragraphRange.location, 1); } - // and then remove associated styling - - UnorderedListStyle *ulStyle = - typedInput->stylesDict[@([UnorderedListStyle getStyleType])]; - OrderedListStyle *olStyle = - typedInput->stylesDict[@([OrderedListStyle getStyleType])]; - BlockQuoteStyle *bqStyle = - typedInput->stylesDict[@([BlockQuoteStyle getStyleType])]; - CodeBlockStyle *cbStyle = - typedInput->stylesDict[@([CodeBlockStyle getStyleType])]; - CheckboxListStyle *cbLStyle = - typedInput->stylesDict[@([CheckboxListStyle getStyleType])]; - - if ([cbStyle detectStyle:removalRange]) { - // code blocks are being handled differently; we want to remove previous - // newline if there is a one - if (range.location > 0) { - removalRange = - NSMakeRange(removalRange.location - 1, removalRange.length + 1); - } - [TextInsertionUtils replaceText:@"" - at:removalRange - additionalAttributes:nullptr - input:typedInput - withSelection:YES]; - return YES; - } - + // remove the ZWS (keep the newline if present) [TextInsertionUtils replaceText:@"" at:removalRange additionalAttributes:nullptr input:typedInput withSelection:YES]; - if ([ulStyle detectStyle:styleRemovalRange]) { - [ulStyle removeAttributes:styleRemovalRange]; - } else if ([olStyle detectStyle:styleRemovalRange]) { - [olStyle removeAttributes:styleRemovalRange]; - } else if ([bqStyle detectStyle:styleRemovalRange]) { - [bqStyle removeAttributes:styleRemovalRange]; - } else if ([cbLStyle detectStyle:styleRemovalRange]) { - [cbLStyle removeAttributes:styleRemovalRange]; + // and then remove associated styling + for (NSNumber *type in typedInput->stylesDict) { + StyleBase *style = typedInput->stylesDict[type]; + if ([style needsZWS] && [style detect:styleRemovalRange]) { + [style remove:styleRemovalRange withDirtyRange:YES]; + break; + } } return YES; } + + // Backspace at the start of a paragraph that has a ZWS-needing style. + // The character being deleted is the newline at the end of the previous + // paragraph. Instead of letting iOS merge the two lines, just remove the + // style from the current paragraph. + if ([[NSCharacterSet newlineCharacterSet] characterIsMember:character]) { + NSUInteger nextParaStart = NSMaxRange(range); + if (nextParaStart < typedInput->textView.textStorage.string.length) { + NSRange nextParagraphRange = [typedInput->textView.textStorage.string + paragraphRangeForRange:NSMakeRange(nextParaStart, 0)]; + for (NSNumber *type in typedInput->stylesDict) { + StyleBase *style = typedInput->stylesDict[type]; + if ([style needsZWS] && [style detect:nextParagraphRange]) { + [style remove:nextParagraphRange withDirtyRange:YES]; + return YES; + } + } + } + } + return NO; }