From 2c2077aa82f4768a3555aaa229916fd918412662 Mon Sep 17 00:00:00 2001 From: szydlovsky <9szydlowski9@gmail.com> Date: Mon, 2 Feb 2026 11:14:13 +0100 Subject: [PATCH 01/17] feat: init attribtues remake; strike, italic, ul --- apps/example/ios/Podfile.lock | 2 +- ios/EnrichedTextInputView.h | 7 +- ios/EnrichedTextInputView.mm | 1535 +++++++++-------- ios/attributesManager/AttributesManager.h | 15 + ios/attributesManager/AttributesManager.mm | 175 ++ ios/extensions/LayoutManagerExtension.mm | 21 +- ios/inputParser/InputParser.mm | 30 +- ios/interfaces/AttributeEntry.h | 9 + ios/interfaces/AttributeEntry.mm | 4 + ios/interfaces/StyleBase.h | 28 + ios/interfaces/StyleBase.mm | 230 +++ ios/interfaces/StyleHeaders.h | 14 +- ios/styles/BlockQuoteStyle.mm | 12 +- ios/styles/CheckboxListStyle.mm | 18 +- ios/styles/CodeBlockStyle.mm | 12 +- ios/styles/InlineCodeStyle.mm | 8 +- ios/styles/ItalicStyle.mm | 123 +- ios/styles/OrderedListStyle.mm | 8 +- ios/styles/StrikethroughStyle.mm | 98 +- ios/styles/UnorderedListStyle.mm | 355 +--- ios/utils/DotReplacementUtils.h | 10 + ios/utils/DotReplacementUtils.mm | 68 + ios/utils/OccurenceUtils.h | 2 +- ios/utils/ParagraphAttributesUtils.mm | 9 +- ios/utils/ParagraphsUtils.mm | 68 - ios/utils/{ParagraphsUtils.h => RangeUtils.h} | 6 +- ios/utils/RangeUtils.mm | 183 ++ ios/utils/ZeroWidthSpaceUtils.mm | 16 +- 28 files changed, 1720 insertions(+), 1346 deletions(-) create mode 100644 ios/attributesManager/AttributesManager.h create mode 100644 ios/attributesManager/AttributesManager.mm create mode 100644 ios/interfaces/AttributeEntry.h create mode 100644 ios/interfaces/AttributeEntry.mm create mode 100644 ios/interfaces/StyleBase.h create mode 100644 ios/interfaces/StyleBase.mm create mode 100644 ios/utils/DotReplacementUtils.h create mode 100644 ios/utils/DotReplacementUtils.mm delete mode 100644 ios/utils/ParagraphsUtils.mm rename ios/utils/{ParagraphsUtils.h => RangeUtils.h} (52%) create mode 100644 ios/utils/RangeUtils.mm 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..1060aa53d 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,188 @@ - (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 getStyleType]) : [[BoldStyle alloc] initWithInput:self], + @([ItalicStyle getType]) : [[ItalicStyle alloc] initWithInput:self], + // @([UnderlineStyle getStyleType]) : + // [[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 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 getType]) : [[UnorderedListStyle alloc] initWithInput:self], - @([OrderedListStyle getStyleType]) : - [[OrderedListStyle alloc] initWithInput:self], - @([CheckboxListStyle getStyleType]) : - [[CheckboxListStyle alloc] initWithInput:self], - @([BlockQuoteStyle getStyleType]) : - [[BlockQuoteStyle alloc] initWithInput:self], - @([CodeBlockStyle getStyleType]) : - [[CodeBlockStyle alloc] initWithInput:self], - @([ImageStyle getStyleType]) : [[ImageStyle alloc] initWithInput:self] + // @([OrderedListStyle getStyleType]) : + // [[OrderedListStyle alloc] initWithInput:self], + // @([CheckboxListStyle getStyleType]) : + // [[CheckboxListStyle alloc] initWithInput:self], + // @([BlockQuoteStyle getStyleType]) : + // [[BlockQuoteStyle alloc] initWithInput:self], + // @([CodeBlockStyle getStyleType]) : + // [[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 getStyleType]) : @[], + @([ItalicStyle getType]) : @[], + // @([UnderlineStyle getStyleType]) : @[], + @([StrikethroughStyle getType]) : @[], + // @([InlineCodeStyle getStyleType]) : + // @[ @([LinkStyle getStyleType]), @([MentionStyle getStyleType]) ], + // @([LinkStyle getStyleType]) : @[ + // @([InlineCodeStyle getStyleType]), @([LinkStyle getStyleType]), + // @([MentionStyle getStyleType]) + // ], + // @([MentionStyle getStyleType]) : + // @[ @([InlineCodeStyle getStyleType]), @([LinkStyle getStyleType]) + // ], + // @([H1Style getStyleType]) : @[ + // @([H2Style getStyleType]), @([H3Style getStyleType]), + // @([H4Style getStyleType]), @([H5Style getStyleType]), + // @([H6Style getStyleType]), @([UnorderedListStyle getStyleType]), + // @([OrderedListStyle getStyleType]), @([BlockQuoteStyle + // getStyleType]), + // @([CodeBlockStyle getStyleType]) + // ], + // @([H2Style getStyleType]) : @[ + // @([H1Style getStyleType]), @([H3Style getStyleType]), + // @([H4Style getStyleType]), @([H5Style getStyleType]), + // @([H6Style getStyleType]), @([UnorderedListStyle getStyleType]), + // @([OrderedListStyle getStyleType]), @([BlockQuoteStyle + // getStyleType]), + // @([CodeBlockStyle getStyleType]) + // ], + // @([H3Style getStyleType]) : @[ + // @([H1Style getStyleType]), @([H2Style getStyleType]), + // @([H4Style getStyleType]), @([H5Style getStyleType]), + // @([H6Style getStyleType]), @([UnorderedListStyle getStyleType]), + // @([OrderedListStyle getStyleType]), @([BlockQuoteStyle + // getStyleType]), + // @([CodeBlockStyle getStyleType]) + // ], + // @([H4Style getStyleType]) : @[ + // @([H1Style getStyleType]), @([H2Style getStyleType]), + // @([H3Style getStyleType]), @([H5Style getStyleType]), + // @([H6Style getStyleType]), @([UnorderedListStyle getStyleType]), + // @([OrderedListStyle getStyleType]), @([BlockQuoteStyle + // getStyleType]), + // @([CodeBlockStyle getStyleType]) + // ], + // @([H5Style getStyleType]) : @[ + // @([H1Style getStyleType]), @([H2Style getStyleType]), + // @([H3Style getStyleType]), @([H4Style getStyleType]), + // @([H6Style getStyleType]), @([UnorderedListStyle getStyleType]), + // @([OrderedListStyle getStyleType]), @([BlockQuoteStyle + // getStyleType]), + // @([CodeBlockStyle getStyleType]) + // ], + // @([H6Style getStyleType]) : @[ + // @([H1Style getStyleType]), @([H2Style getStyleType]), + // @([H3Style getStyleType]), @([H4Style getStyleType]), + // @([H5Style getStyleType]), @([UnorderedListStyle getStyleType]), + // @([OrderedListStyle getStyleType]), @([BlockQuoteStyle + // getStyleType]), + // @([CodeBlockStyle getStyleType]) + // ], + @([UnorderedListStyle getType]) : @[ + // @([H1Style getStyleType]), @([H2Style getStyleType]), + // @([H3Style getStyleType]), @([H4Style getStyleType]), + // @([H5Style getStyleType]), @([H6Style getStyleType]), + // @([OrderedListStyle getStyleType]), @([BlockQuoteStyle + // getStyleType]), + // @([CodeBlockStyle 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]) - ], - @([H2Style getStyleType]) : @[ - @([H1Style getStyleType]), @([H3Style getStyleType]), - @([H4Style getStyleType]), @([H5Style getStyleType]), - @([H6Style getStyleType]), @([UnorderedListStyle getStyleType]), - @([OrderedListStyle getStyleType]), @([BlockQuoteStyle getStyleType]), - @([CodeBlockStyle getStyleType]), @([CheckboxListStyle getStyleType]) - ], - @([H3Style getStyleType]) : @[ - @([H1Style getStyleType]), @([H2Style getStyleType]), - @([H4Style getStyleType]), @([H5Style getStyleType]), - @([H6Style getStyleType]), @([UnorderedListStyle getStyleType]), - @([OrderedListStyle getStyleType]), @([BlockQuoteStyle getStyleType]), - @([CodeBlockStyle getStyleType]), @([CheckboxListStyle getStyleType]) - ], - @([H4Style getStyleType]) : @[ - @([H1Style getStyleType]), @([H2Style getStyleType]), - @([H3Style getStyleType]), @([H5Style getStyleType]), - @([H6Style getStyleType]), @([UnorderedListStyle getStyleType]), - @([OrderedListStyle getStyleType]), @([BlockQuoteStyle getStyleType]), - @([CodeBlockStyle getStyleType]), @([CheckboxListStyle getStyleType]) - ], - @([H5Style getStyleType]) : @[ - @([H1Style getStyleType]), @([H2Style getStyleType]), - @([H3Style getStyleType]), @([H4Style getStyleType]), - @([H6Style getStyleType]), @([UnorderedListStyle getStyleType]), - @([OrderedListStyle getStyleType]), @([BlockQuoteStyle getStyleType]), - @([CodeBlockStyle getStyleType]), @([CheckboxListStyle getStyleType]) - ], - @([H6Style getStyleType]) : @[ - @([H1Style getStyleType]), @([H2Style getStyleType]), - @([H3Style getStyleType]), @([H4Style getStyleType]), - @([H5Style getStyleType]), @([UnorderedListStyle getStyleType]), - @([OrderedListStyle getStyleType]), @([BlockQuoteStyle getStyleType]), - @([CodeBlockStyle getStyleType]), @([CheckboxListStyle getStyleType]) - ], - @([UnorderedListStyle getStyleType]) : @[ - @([H1Style getStyleType]), @([H2Style getStyleType]), - @([H3Style getStyleType]), @([H4Style getStyleType]), - @([H5Style getStyleType]), @([H6Style getStyleType]), - @([OrderedListStyle getStyleType]), @([BlockQuoteStyle getStyleType]), - @([CodeBlockStyle getStyleType]), @([CheckboxListStyle getStyleType]) - ], - @([OrderedListStyle getStyleType]) : @[ - @([H1Style getStyleType]), @([H2Style getStyleType]), - @([H3Style getStyleType]), @([H4Style getStyleType]), - @([H5Style getStyleType]), @([H6Style getStyleType]), - @([UnorderedListStyle getStyleType]), @([BlockQuoteStyle getStyleType]), - @([CodeBlockStyle getStyleType]), @([CheckboxListStyle getStyleType]) - ], - @([CheckboxListStyle getStyleType]) : @[ - @([H1Style getStyleType]), @([H2Style getStyleType]), - @([H3Style getStyleType]), @([H4Style getStyleType]), - @([H5Style getStyleType]), @([H6Style getStyleType]), - @([UnorderedListStyle getStyleType]), @([OrderedListStyle getStyleType]), - @([BlockQuoteStyle getStyleType]), @([CodeBlockStyle getStyleType]) - ], - @([BlockQuoteStyle getStyleType]) : @[ - @([H1Style getStyleType]), @([H2Style getStyleType]), - @([H3Style getStyleType]), @([H4Style getStyleType]), - @([H5Style getStyleType]), @([H6Style getStyleType]), - @([UnorderedListStyle getStyleType]), @([OrderedListStyle getStyleType]), - @([CodeBlockStyle getStyleType]), @([CheckboxListStyle getStyleType]) - ], - @([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]) - ], - @([ImageStyle getStyleType]) : - @[ @([LinkStyle getStyleType]), @([MentionStyle getStyleType]) ] + // @([OrderedListStyle getStyleType]) : @[ + // @([H1Style getStyleType]), @([H2Style getStyleType]), + // @([H3Style getStyleType]), @([H4Style getStyleType]), + // @([H5Style getStyleType]), @([H6Style getStyleType]), + // @([UnorderedListStyle getStyleType]), @([BlockQuoteStyle + // getStyleType]), + // @([CodeBlockStyle getStyleType]), @([CheckboxListStyle + // getStyleType]) + // ], + // @([CheckboxListStyle getStyleType]) : @[ + // @([H1Style getStyleType]), @([H2Style getStyleType]), + // @([H3Style getStyleType]), @([H4Style getStyleType]), + // @([H5Style getStyleType]), @([H6Style getStyleType]), + // @([UnorderedListStyle getStyleType]), @([OrderedListStyle + // getStyleType]), + // @([BlockQuoteStyle getStyleType]), @([CodeBlockStyle getStyleType]) + // ], + // @([BlockQuoteStyle getStyleType]) : @[ + // @([H1Style getStyleType]), @([H2Style getStyleType]), + // @([H3Style getStyleType]), @([H4Style getStyleType]), + // @([H5Style getStyleType]), @([H6Style getStyleType]), + // @([UnorderedListStyle getStyleType]), @([OrderedListStyle + // getStyleType]), + // @([CodeBlockStyle getStyleType]), @([CheckboxListStyle + // getStyleType]) + // ], + // @([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]) + // ], + // @([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 getStyleType]) : @[ @([CodeBlockStyle getStyleType]) ], + @([ItalicStyle getType]) : @[ /*@([CodeBlockStyle getStyleType]) */ ], + // @([UnderlineStyle getStyleType]) : @[ @([CodeBlockStyle getStyleType]) + // ], + @([StrikethroughStyle getType]) : + @[ /*@([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]) ] } mutableCopy]; parser = [[InputParser alloc] initWithInput:self]; + attributesManager = [[AttributesManager alloc] initWithInput:self]; } - (void)setupTextView { @@ -261,6 +285,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 +927,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 +964,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) { @@ -1006,72 +1036,84 @@ - (void)tryUpdatingActiveStyles { _activeStyles = newActiveStyles; _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]]}); - 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])}); + emitter->onChangeStateDeprecated({ + // .isBold = [self isStyleActive:[BoldStyle getStyleType]], + .isItalic = [self isStyleActive:[ItalicStyle getType]], + // .isUnderline = [self isStyleActive:[UnderlineStyle + // getStyleType]], + .isStrikeThrough = [self isStyleActive:[StrikethroughStyle getType]], + // .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 getType]], + // .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]] + }); + emitter->onChangeState({ + // .bold = GET_STYLE_STATE([BoldStyle getStyleType]), + .italic = GET_STYLE_STATE([ItalicStyle getType]), + // .underline = GET_STYLE_STATE([UnderlineStyle + // getStyleType]), + .strikeThrough = GET_STYLE_STATE([StrikethroughStyle getType]), + // .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 getType]), + // .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]) + }); } } - 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 +1159,80 @@ - (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:@"toggleItalic"]) { - [self toggleRegularStyle:[ItalicStyle getStyleType]]; - } else if ([commandName isEqualToString:@"toggleUnderline"]) { - [self toggleRegularStyle:[UnderlineStyle getStyleType]]; - } else if ([commandName isEqualToString:@"toggleStrikeThrough"]) { - [self toggleRegularStyle:[StrikethroughStyle getStyleType]]; - } 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]]; - } else if ([commandName isEqualToString:@"toggleH2"]) { - [self toggleParagraphStyle:[H2Style getStyleType]]; - } else if ([commandName isEqualToString:@"toggleH3"]) { - [self toggleParagraphStyle:[H3Style getStyleType]]; - } else if ([commandName isEqualToString:@"toggleH4"]) { - [self toggleParagraphStyle:[H4Style getStyleType]]; - } else if ([commandName isEqualToString:@"toggleH5"]) { - [self toggleParagraphStyle:[H5Style getStyleType]]; - } else if ([commandName isEqualToString:@"toggleH6"]) { - [self toggleParagraphStyle:[H6Style getStyleType]]; - } else if ([commandName isEqualToString:@"toggleUnorderedList"]) { - [self toggleParagraphStyle:[UnorderedListStyle getStyleType]]; - } else if ([commandName isEqualToString:@"toggleOrderedList"]) { - [self toggleParagraphStyle:[OrderedListStyle getStyleType]]; - } else if ([commandName isEqualToString:@"toggleCheckboxList"]) { - BOOL checked = [args[0] boolValue]; - [self toggleCheckboxList:checked]; - } else if ([commandName isEqualToString:@"toggleBlockQuote"]) { - [self toggleParagraphStyle:[BlockQuoteStyle getStyleType]]; - } 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"]) { + } + // 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:@"toggleItalic"]) { + [self toggleRegularStyle:[ItalicStyle getType]]; + } + // else if ([commandName isEqualToString:@"toggleUnderline"]) { + // [self toggleRegularStyle:[UnderlineStyle getStyleType]]; + // } + else if ([commandName isEqualToString:@"toggleStrikeThrough"]) { + [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]]; + // } else if ([commandName isEqualToString:@"toggleH2"]) { + // [self toggleParagraphStyle:[H2Style getStyleType]]; + // } else if ([commandName isEqualToString:@"toggleH3"]) { + // [self toggleParagraphStyle:[H3Style getStyleType]]; + // } else if ([commandName isEqualToString:@"toggleH4"]) { + // [self toggleParagraphStyle:[H4Style getStyleType]]; + // } else if ([commandName isEqualToString:@"toggleH5"]) { + // [self toggleParagraphStyle:[H5Style getStyleType]]; + // } else if ([commandName isEqualToString:@"toggleH6"]) { + // [self toggleParagraphStyle:[H6Style getStyleType]]; + // } + else if ([commandName isEqualToString:@"toggleUnorderedList"]) { + [self toggleRegularStyle:[UnorderedListStyle getType]]; + } + // else if ([commandName isEqualToString:@"toggleOrderedList"]) { + // [self toggleParagraphStyle:[OrderedListStyle getStyleType]]; + // } else if ([commandName isEqualToString:@"toggleCheckboxList"]) { + // BOOL checked = [args[0] boolValue]; + // [self toggleCheckboxList:checked]; + // } else if ([commandName isEqualToString:@"toggleBlockQuote"]) { + // [self toggleParagraphStyle:[BlockQuoteStyle getStyleType]]; + // } 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"]) { 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 +1253,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 +1301,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 +1390,115 @@ - (void)emitOnKeyPressEvent:(NSString *)key { // MARK: - Styles manipulation - (void)toggleRegularStyle:(StyleType)type { - id styleClass = stylesDict[@(type)]; + StyleBase *style = stylesDict[@(type)]; if ([self handleStyleBlocksAndConflicts:type range:textView.selectedRange]) { - [styleClass applyStyle:textView.selectedRange]; - [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) { - return; - } - // we always pass whole paragraph/s range to these styles - NSRange paragraphRange = [textView.textStorage.string - paragraphRangeForRange:textView.selectedRange]; - - if ([self handleStyleBlocksAndConflicts:[CheckboxListStyle getStyleType] - range:paragraphRange]) { - [checkboxListStyleClass applyStyleWithCheckedValue:checked - inRange:paragraphRange]; + [style toggle:textView.selectedRange]; [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]; - } -} +//- (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) { +// return; +// } +// // we always pass whole paragraph/s range to these styles +// NSRange paragraphRange = [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]; +// [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 +1509,21 @@ - (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]; + for (NSNumber *type in conflicting) { + StyleBase *style = stylesDict[type]; - if (range.length >= 1) { - // for ranges, we need to remove each occurence - NSArray *allOccurences = - [styleClass findAllOccurences:range]; - - 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]; } 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] : [style removeTyping]; } } } @@ -1486,14 +1535,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 +1551,53 @@ - (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]; + [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 @@ -1565,8 +1605,8 @@ - (void)anyTextMayHaveBeenModified { return; } - // zero width space adding or removal - [ZeroWidthSpaceUtils handleZeroWidthSpacesInInput:self]; + // // zero width space adding or removal + // [ZeroWidthSpaceUtils handleZeroWidthSpacesInInput:self]; // emptying input typing attributes management if (textView.textStorage.string.length == 0 && @@ -1575,74 +1615,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 +1698,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 +1761,64 @@ - (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 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])]; // 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] || - [ZeroWidthSpaceUtils handleBackspaceInRange:range - replacementText:text - input:self] || - [ParagraphAttributesUtils handleBackspaceInRange:range - replacementText:text - input:self] || - [ParagraphAttributesUtils handleResetTypingAttributesOnBackspace:range - replacementText:text - input:self] || + if (false + // [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] || + // [ZeroWidthSpaceUtils handleBackspaceInRange:range + // replacementText:text + // input:self] || + // [ParagraphAttributesUtils handleBackspaceInRange:range + // replacementText:text + // input:self] || + // [ParagraphAttributesUtils + // handleResetTypingAttributesOnBackspace:range + // replacementText:text + // 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; } @@ -1891,31 +1939,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..33350ec81 --- /dev/null +++ b/ios/attributesManager/AttributesManager.h @@ -0,0 +1,15 @@ +#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)manageTypingAttributesWithOnlySelection:(BOOL)onlySelectionChanged; +- (void)handleDirtyRangesStyling; +@end diff --git a/ios/attributesManager/AttributesManager.mm b/ios/attributesManager/AttributesManager.mm new file mode 100644 index 000000000..905e9e3d2 --- /dev/null +++ b/ios/attributesManager/AttributesManager.mm @@ -0,0 +1,175 @@ +#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)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]; + + // then apply styling and re-apply meta-attribtues following the saved + // occurences + for (NSNumber *styleType in presentStyles) { + StyleBase *style = _input->stylesDict[styleType]; + if (style == nullptr) + continue; + + for (StylePair *stylePair in presentStyles[styleType]) { + NSRange occurenceRange = [stylePair.rangeValue rangeValue]; + [style applyStyling:occurenceRange]; + [style add:occurenceRange withTyping:NO]; + } + } + } + // 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 (![style isParagraph] && selectedRange.location > 0) { + entry = + [style getEntryIfPresent:NSMakeRange(selectedRange.location - 1, 1)]; + } else if ([style isParagraph]) { + NSRange paragraphRange = [_input->textView.textStorage.string + paragraphRangeForRange:selectedRange]; + entry = [style getEntryIfPresent:paragraphRange]; + } + + if (entry == nullptr) + continue; + + newAttrs[entry.key] = entry.value; + } + + [_removedTypingAttributes removeAllObjects]; + textView.typingAttributes = newAttrs; +} + +@end diff --git a/ios/extensions/LayoutManagerExtension.mm b/ios/extensions/LayoutManagerExtension.mm index 4de25c12d..c27532801 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 @@ -86,8 +86,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; @@ -246,7 +246,7 @@ - (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])]; CheckboxListStyle *cbStyle = @@ -256,7 +256,7 @@ - (void)drawLists:(EnrichedTextInputView *)typedInput } NSMutableArray *allLists = [[NSMutableArray alloc] init]; - [allLists addObjectsFromArray:[ulStyle findAllOccurences:visibleCharRange]]; + [allLists addObjectsFromArray:[ulStyle all:visibleCharRange]]; [allLists addObjectsFromArray:[olStyle findAllOccurences:visibleCharRange]]; [allLists addObjectsFromArray:[cbStyle findAllOccurences:visibleCharRange]]; @@ -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 = @@ -301,8 +301,9 @@ - (void)drawLists:(EnrichedTextInputView *)typedInput markerAttributes:markerAttributes origin:origin usedRect:usedRect]; - } else if (markerFormat == - NSTextListMarkerDisc) { + } else if ([markerFormat + isEqualToString: + @"UnorderedList"]) { [self drawBullet:typedInput origin:origin usedRect:usedRect]; diff --git a/ios/inputParser/InputParser.mm b/ios/inputParser/InputParser.mm index a2558c190..5634d0733 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]) { @@ -80,8 +80,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 { @@ -149,7 +148,7 @@ - (NSString *)parseToHtmlFromRange:(NSRange)range { // append closing paragraph tag if ([previousActiveStyles - containsObject:@([UnorderedListStyle getStyleType])] || + containsObject:@([UnorderedListStyle getType])] || [previousActiveStyles containsObject:@([OrderedListStyle getStyleType])] || [previousActiveStyles containsObject:@([H1Style getStyleType])] || @@ -184,7 +183,7 @@ - (NSString *)parseToHtmlFromRange:(NSRange)range { // handle ending unordered list if (inUnorderedList && ![currentActiveStyles - containsObject:@([UnorderedListStyle getStyleType])]) { + containsObject:@([UnorderedListStyle getType])]) { inUnorderedList = NO; [result appendString:@"\n"]; } @@ -220,7 +219,7 @@ - (NSString *)parseToHtmlFromRange:(NSRange)range { // handle starting unordered list if (!inUnorderedList && [currentActiveStyles - containsObject:@([UnorderedListStyle getStyleType])]) { + containsObject:@([UnorderedListStyle getType])]) { inUnorderedList = YES; [result appendString:@"\n
      "]; } @@ -255,7 +254,7 @@ - (NSString *)parseToHtmlFromRange:(NSRange)range { // don't add the

      tag if some paragraph styles are present if ([currentActiveStyles - containsObject:@([UnorderedListStyle getStyleType])] || + containsObject:@([UnorderedListStyle getType])] || [currentActiveStyles containsObject:@([OrderedListStyle getStyleType])] || [currentActiveStyles containsObject:@([H1Style getStyleType])] || @@ -403,8 +402,7 @@ - (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])]) { @@ -486,7 +484,7 @@ - (NSString *)tagContentForStyle:(NSNumber *)style location:(NSInteger)location { if ([style isEqualToNumber:@([BoldStyle getStyleType])]) { return @"b"; - } else if ([style isEqualToNumber:@([ItalicStyle getStyleType])]) { + } else if ([style isEqualToNumber:@([ItalicStyle getType])]) { return @"i"; } else if ([style isEqualToNumber:@([ImageStyle getStyleType])]) { if (openingTag) { @@ -506,7 +504,7 @@ - (NSString *)tagContentForStyle:(NSNumber *)style } } else if ([style isEqualToNumber:@([UnderlineStyle getStyleType])]) { return @"u"; - } else if ([style isEqualToNumber:@([StrikethroughStyle getStyleType])]) { + } else if ([style isEqualToNumber:@([StrikethroughStyle getType])]) { return @"s"; } else if ([style isEqualToNumber:@([InlineCodeStyle getStyleType])]) { return @"code"; @@ -574,7 +572,7 @@ - (NSString *)tagContentForStyle:(NSNumber *)style return @"h5"; } else if ([style isEqualToNumber:@([H6Style getStyleType])]) { return @"h6"; - } else if ([style isEqualToNumber:@([UnorderedListStyle getStyleType])] || + } else if ([style isEqualToNumber:@([UnorderedListStyle getType])] || [style isEqualToNumber:@([OrderedListStyle getStyleType])]) { return @"li"; } else if ([style isEqualToNumber:@([CheckboxListStyle getStyleType])]) { @@ -1228,7 +1226,7 @@ - (NSArray *)getTextAndStylesFromHtml:(NSString *)fixedHtml { if ([tagName isEqualToString:@"b"]) { [styleArr addObject:@([BoldStyle getStyleType])]; } else if ([tagName isEqualToString:@"i"]) { - [styleArr addObject:@([ItalicStyle getStyleType])]; + [styleArr addObject:@([ItalicStyle getType])]; } else if ([tagName isEqualToString:@"img"]) { NSRegularExpression *srcRegex = [NSRegularExpression regularExpressionWithPattern:@"src=\"([^\"]+)\"" @@ -1286,7 +1284,7 @@ - (NSArray *)getTextAndStylesFromHtml:(NSString *)fixedHtml { } else if ([tagName isEqualToString:@"u"]) { [styleArr addObject:@([UnderlineStyle getStyleType])]; } else if ([tagName isEqualToString:@"s"]) { - [styleArr addObject:@([StrikethroughStyle getStyleType])]; + [styleArr addObject:@([StrikethroughStyle getType])]; } else if ([tagName isEqualToString:@"code"]) { [styleArr addObject:@([InlineCodeStyle getStyleType])]; } else if ([tagName isEqualToString:@"a"]) { @@ -1373,7 +1371,7 @@ - (NSArray *)getTextAndStylesFromHtml:(NSString *)fixedHtml { [self prepareCheckboxListStyleValue:tagRangeValue checkboxStates:checkboxStates]; } else { - [styleArr addObject:@([UnorderedListStyle getStyleType])]; + [styleArr addObject:@([UnorderedListStyle getType])]; } } else if ([tagName isEqualToString:@"ol"]) { [styleArr addObject:@([OrderedListStyle getStyleType])]; 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..93a0e321e --- /dev/null +++ b/ios/interfaces/StyleBase.h @@ -0,0 +1,28 @@ +#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; +- (instancetype)initWithInput:(EnrichedTextInputView *)input; +- (NSRange)actualUsedRange:(NSRange)range; +- (void)toggle:(NSRange)range; +- (void)add:(NSRange)range withTyping:(BOOL)withTyping; +- (void)remove:(NSRange)range; +- (void)addTyping; +- (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; +- (AttributeEntry *)getEntryIfPresent:(NSRange)range; +@end diff --git a/ios/interfaces/StyleBase.mm b/ios/interfaces/StyleBase.mm new file mode 100644 index 000000000..253ad7c92 --- /dev/null +++ b/ios/interfaces/StyleBase.mm @@ -0,0 +1,230 @@ +#import "StyleBase.h" +#import "AttributeEntry.h" +#import "EnrichedTextInputView.h" +#import "OccurenceUtils.h" +#import "RangeUtils.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; +} + +- (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] + : [self add:actualRange withTyping:YES]; + } else { + isPresent ? [self removeTyping] : [self addTyping]; + } +} + +- (void)add:(NSRange)range withTyping:(BOOL)withTyping { + NSRange actualRange = [self actualUsedRange:range]; + + if (![self isParagraph]) { + [_input->textView.textStorage addAttribute:[self getKey] + value:[self getValue] + range:actualRange]; + } else { + [_input->textView.textStorage + enumerateAttribute:NSParagraphStyleAttributeName + inRange:actualRange + options:0 + usingBlock:^(id _Nullable value, NSRange range, + BOOL *_Nonnull stop) { + NSMutableParagraphStyle *pStyle = + [(NSParagraphStyle *)value mutableCopy]; + if (pStyle == nullptr) + return; + pStyle.textLists = @[ [[NSTextList alloc] + initWithMarkerFormat:[self getValue] + options:0] ]; + [_input->textView.textStorage + addAttribute:NSParagraphStyleAttributeName + value:pStyle + range:range]; + }]; + + // Only paragraph styles need additional typing attributes when toggling. + if (withTyping) { + [self addTyping]; + } + } + + // Notify attributes manager of styling to be re-done. + [self.input->attributesManager addDirtyRange:actualRange]; +} + +- (void)remove:(NSRange)range { + 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 value, NSRange range, + BOOL *_Nonnull stop) { + NSMutableParagraphStyle *pStyle = + [(NSParagraphStyle *)value mutableCopy]; + if (pStyle == nullptr) + return; + pStyle.textLists = @[]; + [_input->textView.textStorage + addAttribute:NSParagraphStyleAttributeName + value:pStyle + range:range]; + }]; + + // Paragraph styles also need typing attributes removal. + [self removeTyping]; + } + + // Notify attributes manager of styling to be re-done. + [self.input->attributesManager addDirtyRange:actualRange]; +} + +- (void)addTyping { + NSMutableDictionary *newTypingAttrs = + [_input->textView.typingAttributes mutableCopy]; + + if (![self isParagraph]) { + newTypingAttrs[[self getKey]] = [self getValue]; + } else { + NSMutableParagraphStyle *pStyle = + [newTypingAttrs[NSParagraphStyleAttributeName] mutableCopy]; + pStyle.textLists = + @[ [[NSTextList alloc] initWithMarkerFormat:[self getValue] + 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 { +} + +// 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..0eade8f73 100644 --- a/ios/interfaces/StyleHeaders.h +++ b/ios/interfaces/StyleHeaders.h @@ -3,17 +3,18 @@ #import "ImageData.h" #import "LinkData.h" #import "MentionParams.h" +#import "StyleBase.h" @interface BoldStyle : NSObject @end -@interface ItalicStyle : NSObject +@interface ItalicStyle : StyleBase @end @interface UnderlineStyle : NSObject @end -@interface StrikethroughStyle : NSObject +@interface StrikethroughStyle : StyleBase @end @interface InlineCodeStyle : NSObject @@ -79,10 +80,11 @@ @interface H6Style : HeadingStyleBase @end -@interface UnorderedListStyle : NSObject -- (BOOL)handleBackspaceInRange:(NSRange)range replacementText:(NSString *)text; -- (BOOL)tryHandlingListShorcutInRange:(NSRange)range - replacementText:(NSString *)text; +@interface UnorderedListStyle : StyleBase +//- (BOOL)handleBackspaceInRange:(NSRange)range replacementText:(NSString +//*)text; +//- (BOOL)tryHandlingListShorcutInRange:(NSRange)range +// replacementText:(NSString *)text; @end @interface OrderedListStyle : NSObject diff --git a/ios/styles/BlockQuoteStyle.mm b/ios/styles/BlockQuoteStyle.mm index 0f1a6fb67..d5c9f9918 100644 --- a/ios/styles/BlockQuoteStyle.mm +++ b/ios/styles/BlockQuoteStyle.mm @@ -1,7 +1,7 @@ #import "ColorExtension.h" #import "EnrichedTextInputView.h" #import "OccurenceUtils.h" -#import "ParagraphsUtils.h" +#import "RangeUtils.h" #import "StyleHeaders.h" #import "TextInsertionUtils.h" @@ -44,8 +44,7 @@ - (void)applyStyle:(NSRange)range { - (void)addAttributes:(NSRange)range withTypingAttr:(BOOL)withTypingAttr { NSArray *paragraphs = - [ParagraphsUtils getSeparateParagraphsRangesIn:_input->textView - range:range]; + [RangeUtils getSeparateParagraphsRangesIn:_input->textView range:range]; // if we fill empty lines with zero width spaces, we need to offset later // ranges NSInteger offset = 0; @@ -124,8 +123,7 @@ - (void)addTypingAttributes { - (void)removeAttributes:(NSRange)range { NSArray *paragraphs = - [ParagraphsUtils getSeparateParagraphsRangesIn:_input->textView - range:range]; + [RangeUtils getSeparateParagraphsRangesIn:_input->textView range:range]; for (NSValue *value in paragraphs) { NSRange pRange = [value rangeValue]; @@ -243,8 +241,8 @@ - (void)manageBlockquoteColor { NSMakeRange(0, _input->textView.textStorage.string.length); NSArray *paragraphs = - [ParagraphsUtils getSeparateParagraphsRangesIn:_input->textView - range:wholeRange]; + [RangeUtils getSeparateParagraphsRangesIn:_input->textView + range:wholeRange]; for (NSValue *pValue in paragraphs) { NSRange paragraphRange = [pValue rangeValue]; NSArray *properRanges = [OccurenceUtils getRangesWithout:_stylesToExclude diff --git a/ios/styles/CheckboxListStyle.mm b/ios/styles/CheckboxListStyle.mm index b6a2b8a6b..d9709dc12 100644 --- a/ios/styles/CheckboxListStyle.mm +++ b/ios/styles/CheckboxListStyle.mm @@ -1,7 +1,7 @@ #import "EnrichedTextInputView.h" #import "FontExtension.h" #import "OccurenceUtils.h" -#import "ParagraphsUtils.h" +#import "RangeUtils.h" #import "StyleHeaders.h" #import "TextInsertionUtils.h" @@ -63,8 +63,7 @@ - (void)addTypingAttributes { - (void)removeAttributes:(NSRange)range { NSArray *paragraphs = - [ParagraphsUtils getSeparateParagraphsRangesIn:_input->textView - range:range]; + [RangeUtils getSeparateParagraphsRangesIn:_input->textView range:range]; [_input->textView.textStorage beginEditing]; @@ -177,8 +176,7 @@ - (void)addAttributesWithCheckedValue:(BOOL)checked NSTextList *checkboxMarker = [[NSTextList alloc] initWithMarkerFormat:markerFormat options:0]; NSArray *paragraphs = - [ParagraphsUtils getSeparateParagraphsRangesIn:_input->textView - range:range]; + [RangeUtils getSeparateParagraphsRangesIn:_input->textView range:range]; // if we fill empty lines with zero width spaces, we need to offset later // ranges NSInteger offset = 0; @@ -254,7 +252,7 @@ - (void)addAttributesWithCheckedValue:(BOOL)checked } } -- (BOOL)styleCondition:(id _Nullable)value:(NSRange)range { +- (BOOL)styleCondition:(id _Nullable)value range:(NSRange)range { NSParagraphStyle *paragraph = (NSParagraphStyle *)value; return paragraph != nullptr && paragraph.textLists.count == 1 && [paragraph.textLists.firstObject.markerFormat hasPrefix:@"{checkbox"]; @@ -266,7 +264,7 @@ - (BOOL)detectStyle:(NSRange)range { withInput:_input inRange:range withCondition:^BOOL(id _Nullable value, NSRange range) { - return [self styleCondition:value:range]; + return [self styleCondition:value range:range]; }]; } else { return [OccurenceUtils detect:NSParagraphStyleAttributeName @@ -274,7 +272,7 @@ - (BOOL)detectStyle:(NSRange)range { atIndex:range.location checkPrevious:YES withCondition:^BOOL(id _Nullable value, NSRange range) { - return [self styleCondition:value:range]; + return [self styleCondition:value range:range]; }]; } } @@ -284,7 +282,7 @@ - (BOOL)anyOccurence:(NSRange)range { withInput:_input inRange:range withCondition:^BOOL(id _Nullable value, NSRange range) { - return [self styleCondition:value:range]; + return [self styleCondition:value range:range]; }]; } @@ -293,7 +291,7 @@ - (BOOL)anyOccurence:(NSRange)range { withInput:_input inRange:range withCondition:^BOOL(id _Nullable value, NSRange range) { - return [self styleCondition:value:range]; + return [self styleCondition:value range:range]; }]; } diff --git a/ios/styles/CodeBlockStyle.mm b/ios/styles/CodeBlockStyle.mm index 3495a1ddb..9ce79f27d 100644 --- a/ios/styles/CodeBlockStyle.mm +++ b/ios/styles/CodeBlockStyle.mm @@ -2,7 +2,7 @@ #import "EnrichedTextInputView.h" #import "FontExtension.h" #import "OccurenceUtils.h" -#import "ParagraphsUtils.h" +#import "RangeUtils.h" #import "StyleHeaders.h" #import "TextInsertionUtils.h" @@ -40,8 +40,7 @@ - (void)addAttributes:(NSRange)range withTypingAttr:(BOOL)withTypingAttr { NSTextList *codeBlockList = [[NSTextList alloc] initWithMarkerFormat:@"codeblock" options:0]; NSArray *paragraphs = - [ParagraphsUtils getSeparateParagraphsRangesIn:_input->textView - range:range]; + [RangeUtils getSeparateParagraphsRangesIn:_input->textView range:range]; // if we fill empty lines with zero width spaces, we need to offset later // ranges NSInteger offset = 0; @@ -117,8 +116,7 @@ - (void)addTypingAttributes { - (void)removeAttributes:(NSRange)range { NSArray *paragraphs = - [ParagraphsUtils getSeparateParagraphsRangesIn:_input->textView - range:range]; + [RangeUtils getSeparateParagraphsRangesIn:_input->textView range:range]; [_input->textView.textStorage beginEditing]; @@ -230,8 +228,8 @@ - (void)manageCodeBlockFontAndColor { NSRange wholeRange = NSMakeRange(0, _input->textView.textStorage.string.length); NSArray *paragraphs = - [ParagraphsUtils getSeparateParagraphsRangesIn:_input->textView - range:wholeRange]; + [RangeUtils getSeparateParagraphsRangesIn:_input->textView + range:wholeRange]; for (NSValue *pValue in paragraphs) { NSRange paragraphRange = [pValue rangeValue]; diff --git a/ios/styles/InlineCodeStyle.mm b/ios/styles/InlineCodeStyle.mm index ed5880240..a04c9741f 100644 --- a/ios/styles/InlineCodeStyle.mm +++ b/ios/styles/InlineCodeStyle.mm @@ -2,7 +2,7 @@ #import "EnrichedTextInputView.h" #import "FontExtension.h" #import "OccurenceUtils.h" -#import "ParagraphsUtils.h" +#import "RangeUtils.h" #import "StyleHeaders.h" @implementation InlineCodeStyle { @@ -35,8 +35,8 @@ - (void)applyStyle:(NSRange)range { - (void)addAttributes:(NSRange)range withTypingAttr:(BOOL)withTypingAttr { // we don't want to apply inline code to newline characters, it looks bad - NSArray *nonNewlineRanges = - [ParagraphsUtils getNonNewlineRangesIn:_input->textView range:range]; + NSArray *nonNewlineRanges = [RangeUtils getNonNewlineRangesIn:_input->textView + range:range]; for (NSValue *value in nonNewlineRanges) { NSRange currentRange = [value rangeValue]; @@ -192,7 +192,7 @@ - (BOOL)detectStyle:(NSRange)range { if (range.length >= 1) { // detect only in non-newline characters NSArray *nonNewlineRanges = - [ParagraphsUtils getNonNewlineRangesIn:_input->textView range:range]; + [RangeUtils getNonNewlineRangesIn:_input->textView range:range]; if (nonNewlineRanges.count == 0) { return NO; } 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/OrderedListStyle.mm b/ios/styles/OrderedListStyle.mm index 933b57e11..8d92590a1 100644 --- a/ios/styles/OrderedListStyle.mm +++ b/ios/styles/OrderedListStyle.mm @@ -1,7 +1,7 @@ #import "EnrichedTextInputView.h" #import "FontExtension.h" #import "OccurenceUtils.h" -#import "ParagraphsUtils.h" +#import "RangeUtils.h" #import "StyleHeaders.h" #import "TextInsertionUtils.h" @@ -46,8 +46,7 @@ - (void)addAttributes:(NSRange)range withTypingAttr:(BOOL)withTypingAttr { [[NSTextList alloc] initWithMarkerFormat:NSTextListMarkerDecimal options:0]; NSArray *paragraphs = - [ParagraphsUtils getSeparateParagraphsRangesIn:_input->textView - range:range]; + [RangeUtils getSeparateParagraphsRangesIn:_input->textView range:range]; // if we fill empty lines with zero width spaces, we need to offset later // ranges NSInteger offset = 0; @@ -130,8 +129,7 @@ - (void)addTypingAttributes { - (void)removeAttributes:(NSRange)range { NSArray *paragraphs = - [ParagraphsUtils getSeparateParagraphsRangesIn:_input->textView - range:range]; + [RangeUtils getSeparateParagraphsRangesIn:_input->textView range:range]; [_input->textView.textStorage beginEditing]; 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/UnorderedListStyle.mm b/ios/styles/UnorderedListStyle.mm index d2aa15099..efca56b9b 100644 --- a/ios/styles/UnorderedListStyle.mm +++ b/ios/styles/UnorderedListStyle.mm @@ -1,280 +1,105 @@ #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 { +- (NSString *)getValue { + return @"UnorderedList"; +} + +- (BOOL)isParagraph { return YES; } -- (CGFloat)getHeadIndent { +- (void)applyStyling:(NSRange)range { // 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]; -} - -- (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; -} - -// 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 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; -} - -- (BOOL)tryHandlingListShorcutInRange:(NSRange)range - replacementText:(NSString *)text { - NSRange paragraphRange = - [_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 - characterAtIndex:range.location - 1]; - if (charBefore == '-') { - // we got a match - add a list if possible - if ([_input handleStyleBlocksAndConflicts:[[self class] getStyleType] - range:paragraphRange]) { - // don't emit during the replacing - _input->blockEmitting = YES; - - // remove the dash - [TextInsertionUtils replaceText:@"" - at:NSMakeRange(paragraphRange.location, 1) - additionalAttributes:nullptr - input:_input - withSelection:YES]; - - _input->blockEmitting = NO; - - // add attributes on the dashless paragraph - [self addAttributes:NSMakeRange(paragraphRange.location, - paragraphRange.length - 1) - withTypingAttr:YES]; - 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 == 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]; - }]; -} + 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)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; +//} +// +//- (BOOL)tryHandlingListShorcutInRange:(NSRange)range +// replacementText:(NSString *)text { +// NSRange paragraphRange = +// [_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 +// characterAtIndex:range.location - 1]; +// if (charBefore == '-') { +// // we got a match - add a list if possible +// if ([_input handleStyleBlocksAndConflicts:[[self class] getStyleType] +// range:paragraphRange]) { +// // don't emit during the replacing +// _input->blockEmitting = YES; +// +// // remove the dash +// [TextInsertionUtils replaceText:@"" +// at:NSMakeRange(paragraphRange.location, +// 1) +// additionalAttributes:nullptr +// input:_input +// withSelection:YES]; +// +// _input->blockEmitting = NO; +// +// // add attributes on the dashless paragraph +// [self addAttributes:NSMakeRange(paragraphRange.location, +// paragraphRange.length - 1) +// withTypingAttr:YES]; +// return YES; +// } +// } +// } +// return NO; +//} @end 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..86080c0a6 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" @@ -15,7 +15,7 @@ + (BOOL)handleBackspaceInRange:(NSRange)range input:(id)input { EnrichedTextInputView *typedInput = (EnrichedTextInputView *)input; UnorderedListStyle *ulStyle = - typedInput->stylesDict[@([UnorderedListStyle getStyleType])]; + typedInput->stylesDict[@([UnorderedListStyle getType])]; OrderedListStyle *olStyle = typedInput->stylesDict[@([OrderedListStyle getStyleType])]; BlockQuoteStyle *bqStyle = @@ -39,9 +39,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; } 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..562073e5a 100644 --- a/ios/utils/ZeroWidthSpaceUtils.mm +++ b/ios/utils/ZeroWidthSpaceUtils.mm @@ -44,7 +44,7 @@ + (void)removeSpacesIfNeededinInput:(EnrichedTextInputView *)input { } UnorderedListStyle *ulStyle = - input->stylesDict[@([UnorderedListStyle getStyleType])]; + input->stylesDict[@([UnorderedListStyle getType])]; OrderedListStyle *olStyle = input->stylesDict[@([OrderedListStyle getStyleType])]; BlockQuoteStyle *bqStyle = @@ -56,7 +56,7 @@ + (void)removeSpacesIfNeededinInput:(EnrichedTextInputView *)input { // zero width spaces with no lists/blockquotes/codeblocks on them get // removed - if (![ulStyle detectStyle:characterRange] && + if (![ulStyle detect:characterRange] && ![olStyle detectStyle:characterRange] && ![bqStyle detectStyle:characterRange] && ![cbStyle detectStyle:characterRange] && @@ -97,7 +97,7 @@ + (void)removeSpacesIfNeededinInput:(EnrichedTextInputView *)input { + (void)addSpacesIfNeededinInput:(EnrichedTextInputView *)input { UnorderedListStyle *ulStyle = - input->stylesDict[@([UnorderedListStyle getStyleType])]; + input->stylesDict[@([UnorderedListStyle getType])]; OrderedListStyle *olStyle = input->stylesDict[@([OrderedListStyle getStyleType])]; BlockQuoteStyle *bqStyle = @@ -117,7 +117,7 @@ + (void)addSpacesIfNeededinInput:(EnrichedTextInputView *)input { paragraphRangeForRange:characterRange]; if (paragraphRange.length == 1) { - if ([ulStyle detectStyle:characterRange] || + if ([ulStyle detect:characterRange] || [olStyle detectStyle:characterRange] || [bqStyle detectStyle:characterRange] || [cbStyle detectStyle:characterRange] || @@ -155,7 +155,7 @@ + (void)addSpacesIfNeededinInput:(EnrichedTextInputView *)input { NSRange lastParagraphRange = [input->textView.textStorage.string paragraphRangeForRange:lastRange]; if (lastParagraphRange.length == 0 && - ([ulStyle detectStyle:lastRange] || [olStyle detectStyle:lastRange] || + ([ulStyle detect:lastRange] || [olStyle detectStyle:lastRange] || [bqStyle detectStyle:lastRange] || [cbStyle detectStyle:lastRange] || [cbLStyle detectStyle:lastRange])) { [TextInsertionUtils insertText:@"\u200B" @@ -210,7 +210,7 @@ + (BOOL)handleBackspaceInRange:(NSRange)range // and then remove associated styling UnorderedListStyle *ulStyle = - typedInput->stylesDict[@([UnorderedListStyle getStyleType])]; + typedInput->stylesDict[@([UnorderedListStyle getType])]; OrderedListStyle *olStyle = typedInput->stylesDict[@([OrderedListStyle getStyleType])]; BlockQuoteStyle *bqStyle = @@ -241,8 +241,8 @@ + (BOOL)handleBackspaceInRange:(NSRange)range input:typedInput withSelection:YES]; - if ([ulStyle detectStyle:styleRemovalRange]) { - [ulStyle removeAttributes:styleRemovalRange]; + if ([ulStyle detect:styleRemovalRange]) { + [ulStyle remove:styleRemovalRange]; } else if ([olStyle detectStyle:styleRemovalRange]) { [olStyle removeAttributes:styleRemovalRange]; } else if ([bqStyle detectStyle:styleRemovalRange]) { From f34556d783c9c122181b18d5befc931f3a179eb5 Mon Sep 17 00:00:00 2001 From: szydlovsky <9szydlowski9@gmail.com> Date: Wed, 11 Mar 2026 13:26:00 +0100 Subject: [PATCH 02/17] fix: some minor improvements --- ios/EnrichedTextInputView.mm | 5 +++-- ios/attributesManager/AttributesManager.mm | 8 ++------ ios/interfaces/StyleBase.h | 6 ++++-- ios/interfaces/StyleBase.mm | 22 ++++++++++++++-------- ios/utils/ZeroWidthSpaceUtils.mm | 2 +- 5 files changed, 24 insertions(+), 19 deletions(-) diff --git a/ios/EnrichedTextInputView.mm b/ios/EnrichedTextInputView.mm index 1060aa53d..dba8711e1 100644 --- a/ios/EnrichedTextInputView.mm +++ b/ios/EnrichedTextInputView.mm @@ -1519,11 +1519,12 @@ - (BOOL)handleStyleBlocksAndConflicts:(StyleType)type range:(NSRange)range { if ([style isParagraph]) { // for paragraph styles we can just call remove since it will pick up // proper paragraph range - [style remove:range]; + [style remove:range withDirtyRange:YES]; } else { // for inline styles we have to differentiate betweeen normal and typing // attributes removal - range.length >= 1 ? [style remove:range] : [style removeTyping]; + range.length >= 1 ? [style remove:range withDirtyRange:YES] + : [style removeTyping]; } } } diff --git a/ios/attributesManager/AttributesManager.mm b/ios/attributesManager/AttributesManager.mm index 905e9e3d2..3dc52f9f0 100644 --- a/ios/attributesManager/AttributesManager.mm +++ b/ios/attributesManager/AttributesManager.mm @@ -82,7 +82,7 @@ - (void)handleDirtyRangesStyling { for (StylePair *stylePair in presentStyles[styleType]) { NSRange occurenceRange = [stylePair.rangeValue rangeValue]; [style applyStyling:occurenceRange]; - [style add:occurenceRange withTyping:NO]; + [style add:occurenceRange withTyping:NO withDirtyRange:NO]; } } } @@ -153,13 +153,9 @@ - (void)manageTypingAttributesWithOnlySelection:(BOOL)onlySelectionChanged { AttributeEntry *entry = nullptr; - if (![style isParagraph] && selectedRange.location > 0) { + if (selectedRange.location > 0) { entry = [style getEntryIfPresent:NSMakeRange(selectedRange.location - 1, 1)]; - } else if ([style isParagraph]) { - NSRange paragraphRange = [_input->textView.textStorage.string - paragraphRangeForRange:selectedRange]; - entry = [style getEntryIfPresent:paragraphRange]; } if (entry == nullptr) diff --git a/ios/interfaces/StyleBase.h b/ios/interfaces/StyleBase.h index 93a0e321e..67642e0c5 100644 --- a/ios/interfaces/StyleBase.h +++ b/ios/interfaces/StyleBase.h @@ -15,8 +15,10 @@ - (instancetype)initWithInput:(EnrichedTextInputView *)input; - (NSRange)actualUsedRange:(NSRange)range; - (void)toggle:(NSRange)range; -- (void)add:(NSRange)range withTyping:(BOOL)withTyping; -- (void)remove:(NSRange)range; +- (void)add:(NSRange)range + withTyping:(BOOL)withTyping + withDirtyRange:(BOOL)withDirtyRange; +- (void)remove:(NSRange)range withDirtyRange:(BOOL)withDirtyRange; - (void)addTyping; - (void)removeTyping; - (BOOL)styleCondition:(id)value range:(NSRange)range; diff --git a/ios/interfaces/StyleBase.mm b/ios/interfaces/StyleBase.mm index 253ad7c92..816fbb81a 100644 --- a/ios/interfaces/StyleBase.mm +++ b/ios/interfaces/StyleBase.mm @@ -48,14 +48,16 @@ - (void)toggle:(NSRange)range { BOOL isPresent = [self detect:actualRange]; if (actualRange.length >= 1) { - isPresent ? [self remove:actualRange] - : [self add:actualRange withTyping:YES]; + isPresent ? [self remove:actualRange withDirtyRange:YES] + : [self add:actualRange withTyping:YES withDirtyRange:YES]; } else { isPresent ? [self removeTyping] : [self addTyping]; } } -- (void)add:(NSRange)range withTyping:(BOOL)withTyping { +- (void)add:(NSRange)range + withTyping:(BOOL)withTyping + withDirtyRange:(BOOL)withDirtyRange { NSRange actualRange = [self actualUsedRange:range]; if (![self isParagraph]) { @@ -88,11 +90,13 @@ - (void)add:(NSRange)range withTyping:(BOOL)withTyping { } } - // Notify attributes manager of styling to be re-done. - [self.input->attributesManager addDirtyRange:actualRange]; + // Notify attributes manager of styling to be re-done if needed. + if (withDirtyRange) { + [self.input->attributesManager addDirtyRange:actualRange]; + } } -- (void)remove:(NSRange)range { +- (void)remove:(NSRange)range withDirtyRange:(BOOL)withDirtyRange { NSRange actualRange = [self actualUsedRange:range]; if (![self isParagraph]) { @@ -120,8 +124,10 @@ - (void)remove:(NSRange)range { [self removeTyping]; } - // Notify attributes manager of styling to be re-done. - [self.input->attributesManager addDirtyRange:actualRange]; + // Notify attributes manager of styling to be re-done if needed. + if (withDirtyRange) { + [self.input->attributesManager addDirtyRange:actualRange]; + } } - (void)addTyping { diff --git a/ios/utils/ZeroWidthSpaceUtils.mm b/ios/utils/ZeroWidthSpaceUtils.mm index 562073e5a..aa8bd31f7 100644 --- a/ios/utils/ZeroWidthSpaceUtils.mm +++ b/ios/utils/ZeroWidthSpaceUtils.mm @@ -242,7 +242,7 @@ + (BOOL)handleBackspaceInRange:(NSRange)range withSelection:YES]; if ([ulStyle detect:styleRemovalRange]) { - [ulStyle remove:styleRemovalRange]; + [ulStyle remove:styleRemovalRange withDirtyRange:YES]; } else if ([olStyle detectStyle:styleRemovalRange]) { [olStyle removeAttributes:styleRemovalRange]; } else if ([bqStyle detectStyle:styleRemovalRange]) { From 8cbe8952fa9e4572ee4782ca0cc953b89b236690 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20=C5=BB=C3=B3=C5=82kiewski?= Date: Wed, 18 Mar 2026 15:37:47 +0100 Subject: [PATCH 03/17] fix: make zws inside ul works --- ios/EnrichedTextInputView.mm | 57 ++++---- ios/extensions/LayoutManagerExtension.mm | 7 +- ios/interfaces/StyleBase.h | 1 + ios/interfaces/StyleBase.mm | 5 + ios/styles/UnorderedListStyle.mm | 4 + ios/utils/ParagraphAttributesUtils.mm | 55 ++------ ios/utils/ZeroWidthSpaceUtils.mm | 164 ++++++++++++----------- 7 files changed, 143 insertions(+), 150 deletions(-) diff --git a/ios/EnrichedTextInputView.mm b/ios/EnrichedTextInputView.mm index dba8711e1..186ce02b0 100644 --- a/ios/EnrichedTextInputView.mm +++ b/ios/EnrichedTextInputView.mm @@ -1606,8 +1606,8 @@ - (void)anyTextMayHaveBeenModified { return; } - // // zero width space adding or removal - // [ZeroWidthSpaceUtils handleZeroWidthSpacesInInput:self]; + // zero width space adding or removal + [ZeroWidthSpaceUtils handleZeroWidthSpacesInInput:self]; // emptying input typing attributes management if (textView.textStorage.string.length == 0 && @@ -1780,32 +1780,35 @@ - (bool)textView:(UITextView *)textView // 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 (false - // [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 + if ( + // ZWS backspace handling for paragraph styles + [ZeroWidthSpaceUtils handleBackspaceInRange:range + replacementText:text + input:self] + // || [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] || - // [ZeroWidthSpaceUtils handleBackspaceInRange:range - // replacementText:text - // input:self] || - // [ParagraphAttributesUtils handleBackspaceInRange:range - // replacementText:text - // input:self] || - // [ParagraphAttributesUtils + // 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] + || [ParagraphAttributesUtils handleBackspaceInRange:range + replacementText:text + input:self] + // || [ParagraphAttributesUtils // handleResetTypingAttributesOnBackspace:range // replacementText:text // input:self] @@ -1815,7 +1818,7 @@ - (bool)textView:(UITextView *)textView // 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 + // || [ParagraphAttributesUtils // handleParagraphStylesMergeOnBackspace:range // replacementText:text // input:self] diff --git a/ios/extensions/LayoutManagerExtension.mm b/ios/extensions/LayoutManagerExtension.mm index c27532801..d62869e8c 100644 --- a/ios/extensions/LayoutManagerExtension.mm +++ b/ios/extensions/LayoutManagerExtension.mm @@ -251,14 +251,15 @@ - (void)drawLists:(EnrichedTextInputView *)typedInput typedInput->stylesDict[@([OrderedListStyle getStyleType])]; CheckboxListStyle *cbStyle = typedInput->stylesDict[@([CheckboxListStyle getStyleType])]; - if (ulStyle == nullptr || olStyle == nullptr || cbStyle == nullptr) { + if (ulStyle == nullptr) { return; } NSMutableArray *allLists = [[NSMutableArray alloc] init]; [allLists addObjectsFromArray:[ulStyle all:visibleCharRange]]; - [allLists addObjectsFromArray:[olStyle findAllOccurences:visibleCharRange]]; - [allLists addObjectsFromArray:[cbStyle findAllOccurences:visibleCharRange]]; + // [allLists addObjectsFromArray:[olStyle + // findAllOccurences:visibleCharRange]]; [allLists + // addObjectsFromArray:[cbStyle findAllOccurences:visibleCharRange]]; for (StylePair *pair in allLists) { NSParagraphStyle *pStyle = (NSParagraphStyle *)pair.styleValue; diff --git a/ios/interfaces/StyleBase.h b/ios/interfaces/StyleBase.h index 67642e0c5..53e55498c 100644 --- a/ios/interfaces/StyleBase.h +++ b/ios/interfaces/StyleBase.h @@ -12,6 +12,7 @@ - (NSString *)getKey; - (NSString *)getValue; - (BOOL)isParagraph; +- (BOOL)needsZWS; - (instancetype)initWithInput:(EnrichedTextInputView *)input; - (NSRange)actualUsedRange:(NSRange)range; - (void)toggle:(NSRange)range; diff --git a/ios/interfaces/StyleBase.mm b/ios/interfaces/StyleBase.mm index 816fbb81a..5a4686e31 100644 --- a/ios/interfaces/StyleBase.mm +++ b/ios/interfaces/StyleBase.mm @@ -3,6 +3,7 @@ #import "EnrichedTextInputView.h" #import "OccurenceUtils.h" #import "RangeUtils.h" +#import "ZeroWidthSpaceUtils.h" @implementation StyleBase @@ -30,6 +31,10 @@ - (BOOL)isParagraph { return false; } +- (BOOL)needsZWS { + return NO; +} + - (instancetype)initWithInput:(EnrichedTextInputView *)input { self = [super init]; _input = input; diff --git a/ios/styles/UnorderedListStyle.mm b/ios/styles/UnorderedListStyle.mm index efca56b9b..ffd489125 100644 --- a/ios/styles/UnorderedListStyle.mm +++ b/ios/styles/UnorderedListStyle.mm @@ -17,6 +17,10 @@ - (BOOL)isParagraph { return YES; } +- (BOOL)needsZWS { + return YES; +} + - (void)applyStyling:(NSRange)range { // lists are drawn manually // margin before bullet + gap between bullet and paragraph diff --git a/ios/utils/ParagraphAttributesUtils.mm b/ios/utils/ParagraphAttributesUtils.mm index 86080c0a6..09093bfaa 100644 --- a/ios/utils/ParagraphAttributesUtils.mm +++ b/ios/utils/ParagraphAttributesUtils.mm @@ -14,17 +14,6 @@ + (BOOL)handleBackspaceInRange:(NSRange)range replacementText:(NSString *)text input:(id)input { EnrichedTextInputView *typedInput = (EnrichedTextInputView *)input; - UnorderedListStyle *ulStyle = - typedInput->stylesDict[@([UnorderedListStyle getType])]; - 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; } @@ -52,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/ZeroWidthSpaceUtils.mm b/ios/utils/ZeroWidthSpaceUtils.mm index aa8bd31f7..ddbb91b1f 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 getType])]; - 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 detect: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 getType])]; - 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 detect: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 detect: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 @@ -207,52 +221,44 @@ + (BOOL)handleBackspaceInRange:(NSRange)range styleRemovalRange = NSMakeRange(paragraphRange.location, 1); } - // and then remove associated styling - - UnorderedListStyle *ulStyle = - typedInput->stylesDict[@([UnorderedListStyle getType])]; - 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 detect:styleRemovalRange]) { - [ulStyle remove:styleRemovalRange withDirtyRange:YES]; - } 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; } From 7ea8ea5ac66b7fcdcc4f3400eb1e0191e5f9484d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20=C5=BB=C3=B3=C5=82kiewski?= Date: Thu, 19 Mar 2026 10:15:35 +0100 Subject: [PATCH 04/17] fix: inheriting list styles edge cases --- ios/EnrichedTextInputView.mm | 9 ++++----- ios/utils/ZeroWidthSpaceUtils.mm | 22 +++++++++++++++++++++- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/ios/EnrichedTextInputView.mm b/ios/EnrichedTextInputView.mm index 186ce02b0..66b1a7a8a 100644 --- a/ios/EnrichedTextInputView.mm +++ b/ios/EnrichedTextInputView.mm @@ -1807,11 +1807,10 @@ - (bool)textView:(UITextView *)textView // || [h6Style handleNewlinesInRange:range replacementText:text] || [ParagraphAttributesUtils handleBackspaceInRange:range replacementText:text - input:self] - // || [ParagraphAttributesUtils - // handleResetTypingAttributesOnBackspace:range - // replacementText:text - // input:self] + input:self] || + [ParagraphAttributesUtils handleResetTypingAttributesOnBackspace:range + replacementText:text + input:self] // || // CRITICAL: This callback HAS TO be always evaluated last. // diff --git a/ios/utils/ZeroWidthSpaceUtils.mm b/ios/utils/ZeroWidthSpaceUtils.mm index ddbb91b1f..2ab5769ed 100644 --- a/ios/utils/ZeroWidthSpaceUtils.mm +++ b/ios/utils/ZeroWidthSpaceUtils.mm @@ -190,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; @@ -198,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 From 65d16d47f6f18af340b2e440c61cfa3080a76c4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20=C5=BB=C3=B3=C5=82kiewski?= Date: Thu, 19 Mar 2026 11:29:17 +0100 Subject: [PATCH 05/17] feat: refactor ordered list --- ios/EnrichedTextInputView.mm | 38 ++- ios/extensions/LayoutManagerExtension.mm | 31 ++- ios/inputParser/InputParser.mm | 17 +- ios/interfaces/StyleHeaders.h | 9 +- ios/styles/OrderedListStyle.mm | 331 +++++------------------ ios/styles/UnorderedListStyle.mm | 24 -- 6 files changed, 115 insertions(+), 335 deletions(-) diff --git a/ios/EnrichedTextInputView.mm b/ios/EnrichedTextInputView.mm index 66b1a7a8a..88be559a8 100644 --- a/ios/EnrichedTextInputView.mm +++ b/ios/EnrichedTextInputView.mm @@ -118,8 +118,8 @@ - (void)setDefaults { // @([H6Style getStyleType]) : [[H6Style alloc] initWithInput:self], @([UnorderedListStyle getType]) : [[UnorderedListStyle alloc] initWithInput:self], - // @([OrderedListStyle getStyleType]) : - // [[OrderedListStyle alloc] initWithInput:self], + @([OrderedListStyle getType]) : + [[OrderedListStyle alloc] initWithInput:self], // @([CheckboxListStyle getStyleType]) : // [[CheckboxListStyle alloc] initWithInput:self], // @([BlockQuoteStyle getStyleType]) : @@ -195,19 +195,19 @@ - (void)setDefaults { // @([H1Style getStyleType]), @([H2Style getStyleType]), // @([H3Style getStyleType]), @([H4Style getStyleType]), // @([H5Style getStyleType]), @([H6Style getStyleType]), - // @([OrderedListStyle getStyleType]), @([BlockQuoteStyle - // getStyleType]), + @([OrderedListStyle getType]), + // @([BlockQuoteStyle getStyleType]), // @([CodeBlockStyle getStyleType]) ], - // @([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 getStyleType]), @([H2Style getStyleType]), + // @([H3Style getStyleType]), @([H4Style getStyleType]), + // @([H5Style getStyleType]), @([H6Style getStyleType]), + @([UnorderedListStyle getType]), + // @([BlockQuoteStyle getStyleType]), + // @([CodeBlockStyle getStyleType]), + // @([CheckboxListStyle getStyleType]) + ], // @([CheckboxListStyle getStyleType]) : @[ // @([H1Style getStyleType]), @([H2Style getStyleType]), // @([H3Style getStyleType]), @([H4Style getStyleType]), @@ -1054,8 +1054,7 @@ - (void)tryUpdatingActiveStyles { // .isH5 = [self isStyleActive:[H5Style getStyleType]], // .isH6 = [self isStyleActive:[H6Style getStyleType]], .isUnorderedList = [self isStyleActive:[UnorderedListStyle getType]], - // .isOrderedList = - // [self isStyleActive:[OrderedListStyle getStyleType]], + .isOrderedList = [self isStyleActive:[OrderedListStyle getType]], // .isBlockQuote = [self isStyleActive:[BlockQuoteStyle // getStyleType]], // .isCodeBlock = [self isStyleActive:[CodeBlockStyle @@ -1082,8 +1081,7 @@ - (void)tryUpdatingActiveStyles { // .h5 = GET_STYLE_STATE([H5Style getStyleType]), // .h6 = GET_STYLE_STATE([H6Style getStyleType]), .unorderedList = GET_STYLE_STATE([UnorderedListStyle getType]), - // .orderedList = GET_STYLE_STATE([OrderedListStyle - // getStyleType]), + .orderedList = GET_STYLE_STATE([OrderedListStyle getType]), // .blockQuote = GET_STYLE_STATE([BlockQuoteStyle // getStyleType]), // .codeBlock = GET_STYLE_STATE([CodeBlockStyle @@ -1207,10 +1205,10 @@ - (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args { // } else if ([commandName isEqualToString:@"toggleUnorderedList"]) { [self toggleRegularStyle:[UnorderedListStyle getType]]; + } else if ([commandName isEqualToString:@"toggleOrderedList"]) { + [self toggleRegularStyle:[OrderedListStyle getType]]; } - // else if ([commandName isEqualToString:@"toggleOrderedList"]) { - // [self toggleParagraphStyle:[OrderedListStyle getStyleType]]; - // } else if ([commandName isEqualToString:@"toggleCheckboxList"]) { + // else if ([commandName isEqualToString:@"toggleCheckboxList"]) { // BOOL checked = [args[0] boolValue]; // [self toggleCheckboxList:checked]; // } else if ([commandName isEqualToString:@"toggleBlockQuote"]) { diff --git a/ios/extensions/LayoutManagerExtension.mm b/ios/extensions/LayoutManagerExtension.mm index d62869e8c..d18c32242 100644 --- a/ios/extensions/LayoutManagerExtension.mm +++ b/ios/extensions/LayoutManagerExtension.mm @@ -248,18 +248,22 @@ - (void)drawLists:(EnrichedTextInputView *)typedInput UnorderedListStyle *ulStyle = typedInput->stylesDict[@([UnorderedListStyle getType])]; OrderedListStyle *olStyle = - typedInput->stylesDict[@([OrderedListStyle getStyleType])]; - CheckboxListStyle *cbStyle = - typedInput->stylesDict[@([CheckboxListStyle getStyleType])]; - if (ulStyle == nullptr) { + typedInput->stylesDict[@([OrderedListStyle getType])]; + // CheckboxListStyle *cbStyle = + // typedInput->stylesDict[@([CheckboxListStyle getStyleType])]; + if (ulStyle == nullptr && olStyle == nullptr) { return; } NSMutableArray *allLists = [[NSMutableArray alloc] init]; - [allLists addObjectsFromArray:[ulStyle all:visibleCharRange]]; - // [allLists addObjectsFromArray:[olStyle - // findAllOccurences:visibleCharRange]]; [allLists - // addObjectsFromArray:[cbStyle findAllOccurences:visibleCharRange]]; + if (ulStyle != nullptr) { + [allLists addObjectsFromArray:[ulStyle all:visibleCharRange]]; + } + if (olStyle != nullptr) { + [allLists addObjectsFromArray:[olStyle all:visibleCharRange]]; + } + // [allLists addObjectsFromArray:[cbStyle + // findAllOccurences:visibleCharRange]]; for (StylePair *pair in allLists) { NSParagraphStyle *pStyle = (NSParagraphStyle *)pair.styleValue; @@ -288,8 +292,9 @@ - (void)drawLists:(EnrichedTextInputView *)typedInput pStyle.textLists.firstObject .markerFormat; - if (markerFormat == - NSTextListMarkerDecimal) { + if ([markerFormat + isEqualToString: + NSTextListMarkerDecimal]) { NSString *marker = [self getDecimalMarkerForList:typedInput charIndex: @@ -331,7 +336,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 = @@ -341,7 +346,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) { @@ -408,7 +413,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 5634d0733..c8afa82ab 100644 --- a/ios/inputParser/InputParser.mm +++ b/ios/inputParser/InputParser.mm @@ -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 { @@ -150,7 +149,7 @@ - (NSString *)parseToHtmlFromRange:(NSRange)range { if ([previousActiveStyles containsObject:@([UnorderedListStyle getType])] || [previousActiveStyles - containsObject:@([OrderedListStyle getStyleType])] || + containsObject:@([OrderedListStyle getType])] || [previousActiveStyles containsObject:@([H1Style getStyleType])] || [previousActiveStyles containsObject:@([H2Style getStyleType])] || [previousActiveStyles containsObject:@([H3Style getStyleType])] || @@ -190,7 +189,7 @@ - (NSString *)parseToHtmlFromRange:(NSRange)range { // handle ending ordered list if (inOrderedList && ![currentActiveStyles - containsObject:@([OrderedListStyle getStyleType])]) { + containsObject:@([OrderedListStyle getType])]) { inOrderedList = NO; [result appendString:@"\n"]; } @@ -226,7 +225,7 @@ - (NSString *)parseToHtmlFromRange:(NSRange)range { // handle starting ordered list if (!inOrderedList && [currentActiveStyles - containsObject:@([OrderedListStyle getStyleType])]) { + containsObject:@([OrderedListStyle getType])]) { inOrderedList = YES; [result appendString:@"\n
      "]; } @@ -256,7 +255,7 @@ - (NSString *)parseToHtmlFromRange:(NSRange)range { if ([currentActiveStyles containsObject:@([UnorderedListStyle getType])] || [currentActiveStyles - containsObject:@([OrderedListStyle getStyleType])] || + containsObject:@([OrderedListStyle getType])] || [currentActiveStyles containsObject:@([H1Style getStyleType])] || [currentActiveStyles containsObject:@([H2Style getStyleType])] || [currentActiveStyles containsObject:@([H3Style getStyleType])] || @@ -405,7 +404,7 @@ - (NSString *)parseToHtmlFromRange:(NSRange)range { 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])]) { @@ -573,7 +572,7 @@ - (NSString *)tagContentForStyle:(NSNumber *)style } else if ([style isEqualToNumber:@([H6Style getStyleType])]) { return @"h6"; } else if ([style isEqualToNumber:@([UnorderedListStyle getType])] || - [style isEqualToNumber:@([OrderedListStyle getStyleType])]) { + [style isEqualToNumber:@([OrderedListStyle getType])]) { return @"li"; } else if ([style isEqualToNumber:@([CheckboxListStyle getStyleType])]) { if (openingTag) { @@ -1374,7 +1373,7 @@ - (NSArray *)getTextAndStylesFromHtml:(NSString *)fixedHtml { [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])]; } else if ([tagName isEqualToString:@"codeblock"]) { diff --git a/ios/interfaces/StyleHeaders.h b/ios/interfaces/StyleHeaders.h index 0eade8f73..1844aaef0 100644 --- a/ios/interfaces/StyleHeaders.h +++ b/ios/interfaces/StyleHeaders.h @@ -81,16 +81,13 @@ @end @interface UnorderedListStyle : StyleBase -//- (BOOL)handleBackspaceInRange:(NSRange)range replacementText:(NSString -//*)text; //- (BOOL)tryHandlingListShorcutInRange:(NSRange)range // replacementText:(NSString *)text; @end -@interface OrderedListStyle : NSObject -- (BOOL)handleBackspaceInRange:(NSRange)range replacementText:(NSString *)text; -- (BOOL)tryHandlingListShorcutInRange:(NSRange)range - replacementText:(NSString *)text; +@interface OrderedListStyle : StyleBase +//- (BOOL)tryHandlingListShorcutInRange:(NSRange)range +// replacementText:(NSString *)text; @end @interface CheckboxListStyle : NSObject diff --git a/ios/styles/OrderedListStyle.mm b/ios/styles/OrderedListStyle.mm index 8d92590a1..55c9afe9f 100644 --- a/ios/styles/OrderedListStyle.mm +++ b/ios/styles/OrderedListStyle.mm @@ -1,280 +1,85 @@ #import "EnrichedTextInputView.h" -#import "FontExtension.h" -#import "OccurenceUtils.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]; - } +- (NSString *)getValue { + return NSTextListMarkerDecimal; } -// 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 = - [RangeUtils 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]; -} - -- (void)removeAttributes:(NSRange)range { - NSArray *paragraphs = - [RangeUtils 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; -} - -// 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 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; -} - -- (BOOL)tryHandlingListShorcutInRange:(NSRange)range - replacementText:(NSString *)text { - NSRange paragraphRange = - [_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) - if ([text isEqualToString:@"."] && - range.location - 1 == paragraphRange.location) { - unichar charBefore = [_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]) { - // don't emit during the replacing - _input->blockEmitting = YES; - - // remove the number - [TextInsertionUtils replaceText:@"" - at:NSMakeRange(paragraphRange.location, 1) - additionalAttributes:nullptr - input:_input - withSelection:YES]; - - _input->blockEmitting = NO; - - // add attributes on the paragraph - [self addAttributes:NSMakeRange(paragraphRange.location, - paragraphRange.length - 1) - withTypingAttr:YES]; - 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 == - 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)isParagraph { + return YES; } -- (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)needsZWS { + 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]; - }]; -} +- (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]; +// // a dot 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 +// characterAtIndex:range.location - 1]; +// if (charBefore == '1') { +// // we got a match - add a list if possible +// if ([_input handleStyleBlocksAndConflicts:[[self class] getStyleType] +// range:paragraphRange]) { +// // don't emit during the replacing +// _input->blockEmitting = YES; + +// // remove the number +// [TextInsertionUtils replaceText:@"" +// at:NSMakeRange(paragraphRange.location, +// 1) +// additionalAttributes:nullptr +// input:_input +// withSelection:YES]; + +// _input->blockEmitting = NO; + +// // add attributes on the paragraph +// [self addAttributes:NSMakeRange(paragraphRange.location, +// paragraphRange.length - 1) +// withTypingAttr:YES]; +// return YES; +// } +// } +// } +// return NO; +// } @end diff --git a/ios/styles/UnorderedListStyle.mm b/ios/styles/UnorderedListStyle.mm index ffd489125..2dfcb0dec 100644 --- a/ios/styles/UnorderedListStyle.mm +++ b/ios/styles/UnorderedListStyle.mm @@ -44,30 +44,6 @@ - (void)applyStyling:(NSRange)range { }]; } -//- (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; -//} -// //- (BOOL)tryHandlingListShorcutInRange:(NSRange)range // replacementText:(NSString *)text { // NSRange paragraphRange = From 9a98fa0b8a587e8203401e49c8195e30b9f45956 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20=C5=BB=C3=B3=C5=82kiewski?= Date: Thu, 19 Mar 2026 12:24:56 +0100 Subject: [PATCH 06/17] feat: refactor codeblock style --- ios/EnrichedTextInputView.mm | 60 ++--- ios/extensions/LayoutManagerExtension.mm | 5 +- ios/inputParser/InputParser.mm | 20 +- ios/interfaces/StyleHeaders.h | 4 +- ios/styles/CodeBlockStyle.mm | 326 +++-------------------- ios/styles/LinkStyle.mm | 4 +- 6 files changed, 80 insertions(+), 339 deletions(-) diff --git a/ios/EnrichedTextInputView.mm b/ios/EnrichedTextInputView.mm index 88be559a8..0296867da 100644 --- a/ios/EnrichedTextInputView.mm +++ b/ios/EnrichedTextInputView.mm @@ -124,8 +124,7 @@ - (void)setDefaults { // [[CheckboxListStyle alloc] initWithInput:self], // @([BlockQuoteStyle getStyleType]) : // [[BlockQuoteStyle alloc] initWithInput:self], - // @([CodeBlockStyle getStyleType]) : - // [[CodeBlockStyle alloc] initWithInput:self], + @([CodeBlockStyle getType]) : [[CodeBlockStyle alloc] initWithInput:self], // @([ImageStyle getStyleType]) : [[ImageStyle alloc] initWithInput:self] }; @@ -197,7 +196,7 @@ - (void)setDefaults { // @([H5Style getStyleType]), @([H6Style getStyleType]), @([OrderedListStyle getType]), // @([BlockQuoteStyle getStyleType]), - // @([CodeBlockStyle getStyleType]) + @([CodeBlockStyle getType]), ], @([OrderedListStyle getType]) : @[ // @([H1Style getStyleType]), @([H2Style getStyleType]), @@ -205,7 +204,7 @@ - (void)setDefaults { // @([H5Style getStyleType]), @([H6Style getStyleType]), @([UnorderedListStyle getType]), // @([BlockQuoteStyle getStyleType]), - // @([CodeBlockStyle getStyleType]), + @([CodeBlockStyle getType]), // @([CheckboxListStyle getStyleType]) ], // @([CheckboxListStyle getStyleType]) : @[ @@ -225,31 +224,28 @@ - (void)setDefaults { // @([CodeBlockStyle getStyleType]), @([CheckboxListStyle // getStyleType]) // ], - // @([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 getStyleType]), @([H2Style getStyleType]), + // @([H3Style getStyleType]), @([H4Style getStyleType]), + // @([H5Style getStyleType]), @([H6Style getStyleType]), + // @([BoldStyle getStyleType]), @([UnderlineStyle getStyleType]), + @([ItalicStyle getType]), @([StrikethroughStyle getType]), + @([UnorderedListStyle getType]), @([OrderedListStyle getType]), + // @([BlockQuoteStyle getStyleType]), @([InlineCodeStyle + // getStyleType]), + // @([MentionStyle getStyleType]), @([LinkStyle getStyleType]), + // @([CheckboxListStyle getStyleType]) + ], // @([ImageStyle getStyleType]) : // @[ @([LinkStyle getStyleType]), @([MentionStyle getStyleType]) ] }; blockingStyles = [@{ // @([BoldStyle getStyleType]) : @[ @([CodeBlockStyle getStyleType]) ], - @([ItalicStyle getType]) : @[ /*@([CodeBlockStyle getStyleType]) */ ], + @([ItalicStyle getType]) : @[ @([CodeBlockStyle getType]) ], // @([UnderlineStyle getStyleType]) : @[ @([CodeBlockStyle getStyleType]) // ], - @([StrikethroughStyle getType]) : - @[ /*@([CodeBlockStyle getStyleType]) */ ], + @([StrikethroughStyle getType]) : @[ @([CodeBlockStyle getType]) ], // @([InlineCodeStyle getStyleType]) : // @[ @([CodeBlockStyle getStyleType]), @([ImageStyle getStyleType]) // ], @@ -1057,8 +1053,7 @@ - (void)tryUpdatingActiveStyles { .isOrderedList = [self isStyleActive:[OrderedListStyle getType]], // .isBlockQuote = [self isStyleActive:[BlockQuoteStyle // getStyleType]], - // .isCodeBlock = [self isStyleActive:[CodeBlockStyle - // getStyleType]], + .isCodeBlock = [self isStyleActive:[CodeBlockStyle getType]], // .isImage = [self isStyleActive:[ImageStyle // getStyleType]], // .isCheckboxList = @@ -1084,8 +1079,7 @@ - (void)tryUpdatingActiveStyles { .orderedList = GET_STYLE_STATE([OrderedListStyle getType]), // .blockQuote = GET_STYLE_STATE([BlockQuoteStyle // getStyleType]), - // .codeBlock = GET_STYLE_STATE([CodeBlockStyle - // getStyleType]), + .codeBlock = GET_STYLE_STATE([CodeBlockStyle getType]), // .image = GET_STYLE_STATE([ImageStyle getStyleType]), // .checkboxList = GET_STYLE_STATE([CheckboxListStyle // getStyleType]) @@ -1213,9 +1207,10 @@ - (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args { // [self toggleCheckboxList:checked]; // } else if ([commandName isEqualToString:@"toggleBlockQuote"]) { // [self toggleParagraphStyle:[BlockQuoteStyle getStyleType]]; - // } else if ([commandName isEqualToString:@"toggleCodeBlock"]) { - // [self toggleParagraphStyle:[CodeBlockStyle getStyleType]]; - // } else if ([commandName isEqualToString:@"addImage"]) { + else if ([commandName isEqualToString:@"toggleCodeBlock"]) { + [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]; @@ -1389,9 +1384,12 @@ - (void)emitOnKeyPressEvent:(NSString *)key { - (void)toggleRegularStyle:(StyleType)type { StyleBase *style = stylesDict[@(type)]; - - if ([self handleStyleBlocksAndConflicts:type range:textView.selectedRange]) { - [style toggle:textView.selectedRange]; + NSRange range = textView.selectedRange; + if ([style isParagraph]) { + range = [textView.textStorage.string paragraphRangeForRange:range]; + } + if ([self handleStyleBlocksAndConflicts:type range:range]) { + [style toggle:range]; [self anyTextMayHaveBeenModified]; } } diff --git a/ios/extensions/LayoutManagerExtension.mm b/ios/extensions/LayoutManagerExtension.mm index d18c32242..5bed18a75 100644 --- a/ios/extensions/LayoutManagerExtension.mm +++ b/ios/extensions/LayoutManagerExtension.mm @@ -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] diff --git a/ios/inputParser/InputParser.mm b/ios/inputParser/InputParser.mm index c8afa82ab..bb8feee51 100644 --- a/ios/inputParser/InputParser.mm +++ b/ios/inputParser/InputParser.mm @@ -99,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 { @@ -158,8 +158,7 @@ - (NSString *)parseToHtmlFromRange:(NSRange)range { [previousActiveStyles containsObject:@([H6Style getStyleType])] || [previousActiveStyles containsObject:@([BlockQuoteStyle getStyleType])] || - [previousActiveStyles - containsObject:@([CodeBlockStyle getStyleType])] || + [previousActiveStyles containsObject:@([CodeBlockStyle getType])] || [previousActiveStyles containsObject:@([CheckboxListStyle getStyleType])]) { // do nothing, proper closing paragraph tags have been already @@ -202,8 +201,7 @@ - (NSString *)parseToHtmlFromRange:(NSRange)range { } // handle ending codeblock if (inCodeBlock && - ![currentActiveStyles - containsObject:@([CodeBlockStyle getStyleType])]) { + ![currentActiveStyles containsObject:@([CodeBlockStyle getType])]) { inCodeBlock = NO; [result appendString:@"\n"]; } @@ -238,8 +236,7 @@ - (NSString *)parseToHtmlFromRange:(NSRange)range { } // handle starting codeblock if (!inCodeBlock && - [currentActiveStyles - containsObject:@([CodeBlockStyle getStyleType])]) { + [currentActiveStyles containsObject:@([CodeBlockStyle getType])]) { inCodeBlock = YES; [result appendString:@"\n"]; } @@ -264,8 +261,7 @@ - (NSString *)parseToHtmlFromRange:(NSRange)range { [currentActiveStyles containsObject:@([H6Style getStyleType])] || [currentActiveStyles containsObject:@([BlockQuoteStyle getStyleType])] || - [currentActiveStyles - containsObject:@([CodeBlockStyle getStyleType])] || + [currentActiveStyles containsObject:@([CodeBlockStyle getType])] || [currentActiveStyles containsObject:@([CheckboxListStyle getStyleType])]) { [result appendString:@"\n"]; @@ -410,7 +406,7 @@ - (NSString *)parseToHtmlFromRange:(NSRange)range { containsObject:@([BlockQuoteStyle getStyleType])]) { [result appendString:@"\n"]; } else if ([previousActiveStyles - containsObject:@([CodeBlockStyle getStyleType])]) { + containsObject:@([CodeBlockStyle getType])]) { [result appendString:@"\n"]; } else if ([previousActiveStyles containsObject:@([CheckboxListStyle getStyleType])]) { @@ -589,7 +585,7 @@ - (NSString *)tagContentForStyle:(NSNumber *)style return @"li"; } } else if ([style isEqualToNumber:@([BlockQuoteStyle getStyleType])] || - [style isEqualToNumber:@([CodeBlockStyle getStyleType])]) { + [style isEqualToNumber:@([CodeBlockStyle getType])]) { // blockquotes and codeblock use

    tags the same way lists use

  • return @"p"; } @@ -1377,7 +1373,7 @@ - (NSArray *)getTextAndStylesFromHtml:(NSString *)fixedHtml { } else if ([tagName isEqualToString:@"blockquote"]) { [styleArr addObject:@([BlockQuoteStyle getStyleType])]; } 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/StyleHeaders.h b/ios/interfaces/StyleHeaders.h index 1844aaef0..7057cc3c4 100644 --- a/ios/interfaces/StyleHeaders.h +++ b/ios/interfaces/StyleHeaders.h @@ -106,9 +106,7 @@ - (void)manageBlockquoteColor; @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/CodeBlockStyle.mm b/ios/styles/CodeBlockStyle.mm index 9ce79f27d..da2a40a42 100644 --- a/ios/styles/CodeBlockStyle.mm +++ b/ios/styles/CodeBlockStyle.mm @@ -1,302 +1,52 @@ -#import "ColorExtension.h" #import "EnrichedTextInputView.h" #import "FontExtension.h" -#import "OccurenceUtils.h" -#import "RangeUtils.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 = - [RangeUtils 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 = - [RangeUtils 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 @"codeblock"; } -- (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 = - [RangeUtils 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:fullRange]; +} + +@end \ No newline at end of file diff --git a/ios/styles/LinkStyle.mm b/ios/styles/LinkStyle.mm index ac23f19fb..c44f2a540 100644 --- a/ios/styles/LinkStyle.mm +++ b/ios/styles/LinkStyle.mm @@ -401,7 +401,7 @@ - (void)handleAutomaticLinks:(NSString *)word inRange:(NSRange)wordRange { 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; @@ -418,7 +418,7 @@ - (void)handleAutomaticLinks:(NSString *)word inRange:(NSRange)wordRange { } // we don't recognize links in codeblocks - if ([codeBlockStyle anyOccurence:wordRange]) { + if ([codeBlockStyle any:wordRange]) { return; } From 223a3f6c6d6ae89814bf5f97b983f390ce4df3fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20=C5=BB=C3=B3=C5=82kiewski?= Date: Thu, 19 Mar 2026 12:54:45 +0100 Subject: [PATCH 07/17] feat: refactor blockquote style --- ios/EnrichedTextInputView.mm | 41 ++- ios/extensions/LayoutManagerExtension.mm | 4 +- ios/inputParser/InputParser.mm | 21 +- ios/interfaces/StyleHeaders.h | 4 +- ios/styles/BlockQuoteStyle.mm | 315 +++-------------------- 5 files changed, 71 insertions(+), 314 deletions(-) diff --git a/ios/EnrichedTextInputView.mm b/ios/EnrichedTextInputView.mm index 0296867da..8ca799683 100644 --- a/ios/EnrichedTextInputView.mm +++ b/ios/EnrichedTextInputView.mm @@ -122,8 +122,7 @@ - (void)setDefaults { [[OrderedListStyle alloc] initWithInput:self], // @([CheckboxListStyle getStyleType]) : // [[CheckboxListStyle alloc] initWithInput:self], - // @([BlockQuoteStyle getStyleType]) : - // [[BlockQuoteStyle alloc] initWithInput:self], + @([BlockQuoteStyle getType]) : [[BlockQuoteStyle alloc] initWithInput:self], @([CodeBlockStyle getType]) : [[CodeBlockStyle alloc] initWithInput:self], // @([ImageStyle getStyleType]) : [[ImageStyle alloc] initWithInput:self] }; @@ -195,15 +194,14 @@ - (void)setDefaults { // @([H3Style getStyleType]), @([H4Style getStyleType]), // @([H5Style getStyleType]), @([H6Style getStyleType]), @([OrderedListStyle getType]), - // @([BlockQuoteStyle getStyleType]), + @([BlockQuoteStyle getType]), @([CodeBlockStyle getType]), ], @([OrderedListStyle getType]) : @[ // @([H1Style getStyleType]), @([H2Style getStyleType]), // @([H3Style getStyleType]), @([H4Style getStyleType]), // @([H5Style getStyleType]), @([H6Style getStyleType]), - @([UnorderedListStyle getType]), - // @([BlockQuoteStyle getStyleType]), + @([UnorderedListStyle getType]), @([BlockQuoteStyle getType]), @([CodeBlockStyle getType]), // @([CheckboxListStyle getStyleType]) ], @@ -215,15 +213,14 @@ - (void)setDefaults { // getStyleType]), // @([BlockQuoteStyle getStyleType]), @([CodeBlockStyle getStyleType]) // ], - // @([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 getStyleType]), @([H2Style getStyleType]), + // @([H3Style getStyleType]), @([H4Style getStyleType]), + // @([H5Style getStyleType]), @([H6Style getStyleType]), + @([UnorderedListStyle getType]), @([OrderedListStyle getType]), + @([CodeBlockStyle getType]), + // @([CheckboxListStyle getStyleType]) + ], @([CodeBlockStyle getType]) : @[ // @([H1Style getStyleType]), @([H2Style getStyleType]), // @([H3Style getStyleType]), @([H4Style getStyleType]), @@ -231,8 +228,8 @@ - (void)setDefaults { // @([BoldStyle getStyleType]), @([UnderlineStyle getStyleType]), @([ItalicStyle getType]), @([StrikethroughStyle getType]), @([UnorderedListStyle getType]), @([OrderedListStyle getType]), - // @([BlockQuoteStyle getStyleType]), @([InlineCodeStyle - // getStyleType]), + @([BlockQuoteStyle getType]), + // @([InlineCodeStyle getStyleType]), // @([MentionStyle getStyleType]), @([LinkStyle getStyleType]), // @([CheckboxListStyle getStyleType]) ], @@ -1051,8 +1048,7 @@ - (void)tryUpdatingActiveStyles { // .isH6 = [self isStyleActive:[H6Style getStyleType]], .isUnorderedList = [self isStyleActive:[UnorderedListStyle getType]], .isOrderedList = [self isStyleActive:[OrderedListStyle getType]], - // .isBlockQuote = [self isStyleActive:[BlockQuoteStyle - // getStyleType]], + .isBlockQuote = [self isStyleActive:[BlockQuoteStyle getType]], .isCodeBlock = [self isStyleActive:[CodeBlockStyle getType]], // .isImage = [self isStyleActive:[ImageStyle // getStyleType]], @@ -1077,8 +1073,7 @@ - (void)tryUpdatingActiveStyles { // .h6 = GET_STYLE_STATE([H6Style getStyleType]), .unorderedList = GET_STYLE_STATE([UnorderedListStyle getType]), .orderedList = GET_STYLE_STATE([OrderedListStyle getType]), - // .blockQuote = GET_STYLE_STATE([BlockQuoteStyle - // getStyleType]), + .blockQuote = GET_STYLE_STATE([BlockQuoteStyle getType]), .codeBlock = GET_STYLE_STATE([CodeBlockStyle getType]), // .image = GET_STYLE_STATE([ImageStyle getStyleType]), // .checkboxList = GET_STYLE_STATE([CheckboxListStyle @@ -1205,9 +1200,9 @@ - (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args { // else if ([commandName isEqualToString:@"toggleCheckboxList"]) { // BOOL checked = [args[0] boolValue]; // [self toggleCheckboxList:checked]; - // } else if ([commandName isEqualToString:@"toggleBlockQuote"]) { - // [self toggleParagraphStyle:[BlockQuoteStyle getStyleType]]; - else if ([commandName isEqualToString:@"toggleCodeBlock"]) { + else if ([commandName isEqualToString:@"toggleBlockQuote"]) { + [self toggleRegularStyle:[BlockQuoteStyle getType]]; + } else if ([commandName isEqualToString:@"toggleCodeBlock"]) { [self toggleRegularStyle:[CodeBlockStyle getType]]; } // else if ([commandName isEqualToString:@"addImage"]) { diff --git a/ios/extensions/LayoutManagerExtension.mm b/ios/extensions/LayoutManagerExtension.mm index 5bed18a75..7cce0509a 100644 --- a/ios/extensions/LayoutManagerExtension.mm +++ b/ios/extensions/LayoutManagerExtension.mm @@ -204,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 diff --git a/ios/inputParser/InputParser.mm b/ios/inputParser/InputParser.mm index bb8feee51..4f140308a 100644 --- a/ios/inputParser/InputParser.mm +++ b/ios/inputParser/InputParser.mm @@ -89,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 { @@ -157,7 +157,7 @@ - (NSString *)parseToHtmlFromRange:(NSRange)range { [previousActiveStyles containsObject:@([H5Style getStyleType])] || [previousActiveStyles containsObject:@([H6Style getStyleType])] || [previousActiveStyles - containsObject:@([BlockQuoteStyle getStyleType])] || + containsObject:@([BlockQuoteStyle getType])] || [previousActiveStyles containsObject:@([CodeBlockStyle getType])] || [previousActiveStyles containsObject:@([CheckboxListStyle getStyleType])]) { @@ -193,9 +193,8 @@ - (NSString *)parseToHtmlFromRange:(NSRange)range { [result appendString:@"\n"]; } // handle ending blockquotes - if (inBlockQuote && - ![currentActiveStyles - containsObject:@([BlockQuoteStyle getStyleType])]) { + if (inBlockQuote && ![currentActiveStyles + containsObject:@([BlockQuoteStyle getType])]) { inBlockQuote = NO; [result appendString:@"\n"]; } @@ -229,8 +228,7 @@ - (NSString *)parseToHtmlFromRange:(NSRange)range { } // handle starting blockquotes if (!inBlockQuote && - [currentActiveStyles - containsObject:@([BlockQuoteStyle getStyleType])]) { + [currentActiveStyles containsObject:@([BlockQuoteStyle getType])]) { inBlockQuote = YES; [result appendString:@"\n
    "]; } @@ -259,8 +257,7 @@ - (NSString *)parseToHtmlFromRange:(NSRange)range { [currentActiveStyles containsObject:@([H4Style getStyleType])] || [currentActiveStyles containsObject:@([H5Style getStyleType])] || [currentActiveStyles containsObject:@([H6Style getStyleType])] || - [currentActiveStyles - containsObject:@([BlockQuoteStyle getStyleType])] || + [currentActiveStyles containsObject:@([BlockQuoteStyle getType])] || [currentActiveStyles containsObject:@([CodeBlockStyle getType])] || [currentActiveStyles containsObject:@([CheckboxListStyle getStyleType])]) { @@ -403,7 +400,7 @@ - (NSString *)parseToHtmlFromRange:(NSRange)range { containsObject:@([OrderedListStyle getType])]) { [result appendString:@"\n"]; } else if ([previousActiveStyles - containsObject:@([BlockQuoteStyle getStyleType])]) { + containsObject:@([BlockQuoteStyle getType])]) { [result appendString:@"\n
    "]; } else if ([previousActiveStyles containsObject:@([CodeBlockStyle getType])]) { @@ -584,7 +581,7 @@ - (NSString *)tagContentForStyle:(NSNumber *)style } else { return @"li"; } - } else if ([style isEqualToNumber:@([BlockQuoteStyle getStyleType])] || + } else if ([style isEqualToNumber:@([BlockQuoteStyle getType])] || [style isEqualToNumber:@([CodeBlockStyle getType])]) { // blockquotes and codeblock use

    tags the same way lists use

  • return @"p"; @@ -1371,7 +1368,7 @@ - (NSArray *)getTextAndStylesFromHtml:(NSString *)fixedHtml { } else if ([tagName isEqualToString:@"ol"]) { [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 getType])]; } else { diff --git a/ios/interfaces/StyleHeaders.h b/ios/interfaces/StyleHeaders.h index 7057cc3c4..e5598d27e 100644 --- a/ios/interfaces/StyleHeaders.h +++ b/ios/interfaces/StyleHeaders.h @@ -101,9 +101,7 @@ withTypingAttr:(BOOL)withTypingAttr; @end -@interface BlockQuoteStyle : NSObject -- (BOOL)handleBackspaceInRange:(NSRange)range replacementText:(NSString *)text; -- (void)manageBlockquoteColor; +@interface BlockQuoteStyle : StyleBase @end @interface CodeBlockStyle : StyleBase diff --git a/ios/styles/BlockQuoteStyle.mm b/ios/styles/BlockQuoteStyle.mm index d5c9f9918..06f4dfa50 100644 --- a/ios/styles/BlockQuoteStyle.mm +++ b/ios/styles/BlockQuoteStyle.mm @@ -1,291 +1,58 @@ #import "ColorExtension.h" #import "EnrichedTextInputView.h" -#import "OccurenceUtils.h" -#import "RangeUtils.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 @"blockquote"; } -// 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 = - [RangeUtils 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 = - [RangeUtils 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 = - [RangeUtils 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:fullRange]; + [self.input->textView.textStorage addAttribute:NSUnderlineColorAttributeName + value:bqColor + range:fullRange]; + [self.input->textView.textStorage + addAttribute:NSStrikethroughColorAttributeName + value:bqColor + range:fullRange]; } @end From 5802b3bd2bc93f03d30a04c81b12ed6753c4f6ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20=C5=BB=C3=B3=C5=82kiewski?= Date: Thu, 19 Mar 2026 14:58:03 +0100 Subject: [PATCH 08/17] feat: refactor headings style --- ios/EnrichedTextInputView.mm | 213 +++++++++++++++---------------- ios/inputParser/InputParser.mm | 66 +++++----- ios/interfaces/StyleHeaders.h | 6 +- ios/styles/H1Style.mm | 11 +- ios/styles/H2Style.mm | 11 +- ios/styles/H3Style.mm | 11 +- ios/styles/H4Style.mm | 11 +- ios/styles/H5Style.mm | 11 +- ios/styles/H6Style.mm | 11 +- ios/styles/HeadingStyleBase.mm | 224 ++++----------------------------- 10 files changed, 205 insertions(+), 370 deletions(-) diff --git a/ios/EnrichedTextInputView.mm b/ios/EnrichedTextInputView.mm index 8ca799683..87b09c812 100644 --- a/ios/EnrichedTextInputView.mm +++ b/ios/EnrichedTextInputView.mm @@ -110,12 +110,12 @@ - (void)setDefaults { // @([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], + @([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 getType]) : @@ -141,90 +141,80 @@ - (void)setDefaults { // @([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]) - // ], - // @([H2Style getStyleType]) : @[ - // @([H1Style getStyleType]), @([H3Style getStyleType]), - // @([H4Style getStyleType]), @([H5Style getStyleType]), - // @([H6Style getStyleType]), @([UnorderedListStyle getStyleType]), - // @([OrderedListStyle getStyleType]), @([BlockQuoteStyle - // getStyleType]), - // @([CodeBlockStyle getStyleType]) - // ], - // @([H3Style getStyleType]) : @[ - // @([H1Style getStyleType]), @([H2Style getStyleType]), - // @([H4Style getStyleType]), @([H5Style getStyleType]), - // @([H6Style getStyleType]), @([UnorderedListStyle getStyleType]), - // @([OrderedListStyle getStyleType]), @([BlockQuoteStyle - // getStyleType]), - // @([CodeBlockStyle getStyleType]) - // ], - // @([H4Style getStyleType]) : @[ - // @([H1Style getStyleType]), @([H2Style getStyleType]), - // @([H3Style getStyleType]), @([H5Style getStyleType]), - // @([H6Style getStyleType]), @([UnorderedListStyle getStyleType]), - // @([OrderedListStyle getStyleType]), @([BlockQuoteStyle - // getStyleType]), - // @([CodeBlockStyle getStyleType]) - // ], - // @([H5Style getStyleType]) : @[ - // @([H1Style getStyleType]), @([H2Style getStyleType]), - // @([H3Style getStyleType]), @([H4Style getStyleType]), - // @([H6Style getStyleType]), @([UnorderedListStyle getStyleType]), - // @([OrderedListStyle getStyleType]), @([BlockQuoteStyle - // getStyleType]), - // @([CodeBlockStyle getStyleType]) - // ], - // @([H6Style getStyleType]) : @[ - // @([H1Style getStyleType]), @([H2Style getStyleType]), - // @([H3Style getStyleType]), @([H4Style getStyleType]), - // @([H5Style getStyleType]), @([UnorderedListStyle getStyleType]), - // @([OrderedListStyle getStyleType]), @([BlockQuoteStyle - // getStyleType]), - // @([CodeBlockStyle getStyleType]) - // ], + @([H1Style getType]) : @[ + @([H2Style getType]), @([H3Style getType]), @([H4Style getType]), + @([H5Style getType]), @([H6Style getType]), + @([UnorderedListStyle getType]), @([OrderedListStyle getType]), + @([BlockQuoteStyle getType]), @([CodeBlockStyle getType]), + // @([CheckboxListStyle getStyleType]) + ], + @([H2Style getType]) : @[ + @([H1Style getType]), @([H3Style getType]), @([H4Style getType]), + @([H5Style getType]), @([H6Style getType]), + @([UnorderedListStyle getType]), @([OrderedListStyle getType]), + @([BlockQuoteStyle getType]), @([CodeBlockStyle getType]), + // @([CheckboxListStyle getStyleType]) + ], + @([H3Style getType]) : @[ + @([H1Style getType]), @([H2Style getType]), @([H4Style getType]), + @([H5Style getType]), @([H6Style getType]), + @([UnorderedListStyle getType]), @([OrderedListStyle getType]), + @([BlockQuoteStyle getType]), @([CodeBlockStyle getType]), + // @([CheckboxListStyle getStyleType]) + ], + @([H4Style getType]) : @[ + @([H1Style getType]), @([H2Style getType]), @([H3Style getType]), + @([H5Style getType]), @([H6Style getType]), + @([UnorderedListStyle getType]), @([OrderedListStyle getType]), + @([BlockQuoteStyle getType]), @([CodeBlockStyle getType]), + // @([CheckboxListStyle getStyleType]) + ], + @([H5Style getType]) : @[ + @([H1Style getType]), @([H2Style getType]), @([H3Style getType]), + @([H4Style getType]), @([H6Style getType]), + @([UnorderedListStyle getType]), @([OrderedListStyle getType]), + @([BlockQuoteStyle getType]), @([CodeBlockStyle getType]), + // @([CheckboxListStyle getStyleType]) + ], + @([H6Style getType]) : @[ + @([H1Style getType]), @([H2Style getType]), @([H3Style getType]), + @([H4Style getType]), @([H5Style getType]), + @([UnorderedListStyle getType]), @([OrderedListStyle getType]), + @([BlockQuoteStyle getType]), @([CodeBlockStyle getType]), + // @([CheckboxListStyle getStyleType]) + ], @([UnorderedListStyle getType]) : @[ - // @([H1Style getStyleType]), @([H2Style getStyleType]), - // @([H3Style getStyleType]), @([H4Style getStyleType]), - // @([H5Style getStyleType]), @([H6Style getStyleType]), - @([OrderedListStyle getType]), - @([BlockQuoteStyle getType]), + @([H1Style getType]), @([H2Style getType]), @([H3Style getType]), + @([H4Style getType]), @([H5Style getType]), @([H6Style getType]), + @([OrderedListStyle getType]), @([BlockQuoteStyle getType]), @([CodeBlockStyle getType]), + // @([CheckboxListStyle getStyleType]) ], @([OrderedListStyle getType]) : @[ - // @([H1Style getStyleType]), @([H2Style getStyleType]), - // @([H3Style getStyleType]), @([H4Style getStyleType]), - // @([H5Style getStyleType]), @([H6Style getStyleType]), + @([H1Style getType]), @([H2Style getType]), @([H3Style getType]), + @([H4Style getType]), @([H5Style getType]), @([H6Style getType]), @([UnorderedListStyle getType]), @([BlockQuoteStyle getType]), @([CodeBlockStyle getType]), // @([CheckboxListStyle getStyleType]) ], // @([CheckboxListStyle getStyleType]) : @[ - // @([H1Style getStyleType]), @([H2Style getStyleType]), - // @([H3Style getStyleType]), @([H4Style getStyleType]), - // @([H5Style getStyleType]), @([H6Style getStyleType]), + // @([H1Style getType]), @([H2Style getType]), + // @([H3Style getType]), @([H4Style getType]), + // @([H5Style getType]), @([H6Style getType]), // @([UnorderedListStyle getStyleType]), @([OrderedListStyle // getStyleType]), // @([BlockQuoteStyle getStyleType]), @([CodeBlockStyle getStyleType]) // ], @([BlockQuoteStyle getType]) : @[ - // @([H1Style getStyleType]), @([H2Style getStyleType]), - // @([H3Style getStyleType]), @([H4Style getStyleType]), - // @([H5Style getStyleType]), @([H6Style getStyleType]), + @([H1Style getType]), @([H2Style getType]), @([H3Style getType]), + @([H4Style getType]), @([H5Style getType]), @([H6Style getType]), @([UnorderedListStyle getType]), @([OrderedListStyle getType]), @([CodeBlockStyle getType]), // @([CheckboxListStyle getStyleType]) ], @([CodeBlockStyle getType]) : @[ - // @([H1Style getStyleType]), @([H2Style getStyleType]), - // @([H3Style getStyleType]), @([H4Style getStyleType]), - // @([H5Style getStyleType]), @([H6Style getStyleType]), + @([H1Style getType]), @([H2Style getType]), @([H3Style getType]), + @([H4Style getType]), @([H5Style getType]), @([H6Style getType]), // @([BoldStyle getStyleType]), @([UnderlineStyle getStyleType]), @([ItalicStyle getType]), @([StrikethroughStyle getType]), @([UnorderedListStyle getType]), @([OrderedListStyle getType]), @@ -1040,12 +1030,12 @@ - (void)tryUpdatingActiveStyles { // .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]], + .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]], @@ -1065,12 +1055,12 @@ - (void)tryUpdatingActiveStyles { // 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]), + .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]), @@ -1179,20 +1169,19 @@ - (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args { // } else if ([commandName isEqualToString:@"startMention"]) { // NSString *indicator = (NSString *)args[0]; // [self startMentionWithIndicator:indicator]; - // } else if ([commandName isEqualToString:@"toggleH1"]) { - // [self toggleParagraphStyle:[H1Style getStyleType]]; - // } else if ([commandName isEqualToString:@"toggleH2"]) { - // [self toggleParagraphStyle:[H2Style getStyleType]]; - // } else if ([commandName isEqualToString:@"toggleH3"]) { - // [self toggleParagraphStyle:[H3Style getStyleType]]; - // } else if ([commandName isEqualToString:@"toggleH4"]) { - // [self toggleParagraphStyle:[H4Style getStyleType]]; - // } else if ([commandName isEqualToString:@"toggleH5"]) { - // [self toggleParagraphStyle:[H5Style getStyleType]]; - // } else if ([commandName isEqualToString:@"toggleH6"]) { - // [self toggleParagraphStyle:[H6Style getStyleType]]; - // } - else if ([commandName isEqualToString:@"toggleUnorderedList"]) { + else if ([commandName isEqualToString:@"toggleH1"]) { + [self toggleRegularStyle:[H1Style getType]]; + } else if ([commandName isEqualToString:@"toggleH2"]) { + [self toggleRegularStyle:[H2Style getType]]; + } else if ([commandName isEqualToString:@"toggleH3"]) { + [self toggleRegularStyle:[H3Style getType]]; + } else if ([commandName isEqualToString:@"toggleH4"]) { + [self toggleRegularStyle:[H4Style getType]]; + } else if ([commandName isEqualToString:@"toggleH5"]) { + [self toggleRegularStyle:[H5Style getType]]; + } else if ([commandName isEqualToString:@"toggleH6"]) { + [self toggleRegularStyle:[H6Style getType]]; + } else if ([commandName isEqualToString:@"toggleUnorderedList"]) { [self toggleRegularStyle:[UnorderedListStyle getType]]; } else if ([commandName isEqualToString:@"toggleOrderedList"]) { [self toggleRegularStyle:[OrderedListStyle getType]]; @@ -1790,15 +1779,27 @@ - (bool)textView:(UITextView *)textView // 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] - || [ParagraphAttributesUtils handleBackspaceInRange:range - replacementText:text - input:self] || + || [(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] diff --git a/ios/inputParser/InputParser.mm b/ios/inputParser/InputParser.mm index 4f140308a..5a2d9839d 100644 --- a/ios/inputParser/InputParser.mm +++ b/ios/inputParser/InputParser.mm @@ -150,12 +150,12 @@ - (NSString *)parseToHtmlFromRange:(NSRange)range { containsObject:@([UnorderedListStyle getType])] || [previousActiveStyles containsObject:@([OrderedListStyle getType])] || - [previousActiveStyles containsObject:@([H1Style getStyleType])] || - [previousActiveStyles containsObject:@([H2Style getStyleType])] || - [previousActiveStyles containsObject:@([H3Style getStyleType])] || - [previousActiveStyles containsObject:@([H4Style getStyleType])] || - [previousActiveStyles containsObject:@([H5Style getStyleType])] || - [previousActiveStyles containsObject:@([H6Style getStyleType])] || + [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:@([BlockQuoteStyle getType])] || [previousActiveStyles containsObject:@([CodeBlockStyle getType])] || @@ -251,12 +251,12 @@ - (NSString *)parseToHtmlFromRange:(NSRange)range { containsObject:@([UnorderedListStyle getType])] || [currentActiveStyles containsObject:@([OrderedListStyle getType])] || - [currentActiveStyles containsObject:@([H1Style getStyleType])] || - [currentActiveStyles containsObject:@([H2Style getStyleType])] || - [currentActiveStyles containsObject:@([H3Style getStyleType])] || - [currentActiveStyles containsObject:@([H4Style getStyleType])] || - [currentActiveStyles containsObject:@([H5Style getStyleType])] || - [currentActiveStyles containsObject:@([H6Style getStyleType])] || + [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 @@ -408,18 +408,12 @@ - (NSString *)parseToHtmlFromRange:(NSRange)range { } else if ([previousActiveStyles containsObject:@([CheckboxListStyle getStyleType])]) { [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:@"

    "]; @@ -552,17 +546,17 @@ - (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 getType])] || [style isEqualToNumber:@([OrderedListStyle getType])]) { @@ -1344,17 +1338,17 @@ - (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]) { diff --git a/ios/interfaces/StyleHeaders.h b/ios/interfaces/StyleHeaders.h index e5598d27e..08f050513 100644 --- a/ios/interfaces/StyleHeaders.h +++ b/ios/interfaces/StyleHeaders.h @@ -52,14 +52,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 diff --git a/ios/styles/H1Style.mm b/ios/styles/H1Style.mm index ca22c1154..f0e72394a 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 @"h1"; +} +- (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..dffc83f94 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 @"h2"; +} +- (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..bb0b3ea4a 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 @"h3"; +} +- (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..ef1ddf701 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 @"h4"; +} +- (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..8954106ac 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 @"h5"; +} +- (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..daaa5e610 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 @"h6"; +} +- (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 From 408e1eee98dcc56375cb570ade830acae3ff9fa8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20=C5=BB=C3=B3=C5=82kiewski?= Date: Thu, 19 Mar 2026 15:18:11 +0100 Subject: [PATCH 09/17] feat: refactor inline code style --- ios/EnrichedTextInputView.mm | 50 ++++--- ios/inputParser/InputParser.mm | 4 +- ios/interfaces/StyleHeaders.h | 3 +- ios/styles/InlineCodeStyle.mm | 234 ++++----------------------------- ios/styles/LinkStyle.mm | 4 +- 5 files changed, 57 insertions(+), 238 deletions(-) diff --git a/ios/EnrichedTextInputView.mm b/ios/EnrichedTextInputView.mm index 87b09c812..3256e3f02 100644 --- a/ios/EnrichedTextInputView.mm +++ b/ios/EnrichedTextInputView.mm @@ -105,8 +105,7 @@ - (void)setDefaults { // [[UnderlineStyle alloc] initWithInput:self], @([StrikethroughStyle getType]) : [[StrikethroughStyle alloc] initWithInput:self], - // @([InlineCodeStyle getStyleType]) : - // [[InlineCodeStyle alloc] initWithInput:self], + @([InlineCodeStyle getType]) : [[InlineCodeStyle alloc] initWithInput:self], // @([LinkStyle getStyleType]) : [[LinkStyle alloc] initWithInput:self], // @([MentionStyle getStyleType]) : [[MentionStyle alloc] // initWithInput:self], @@ -132,8 +131,9 @@ - (void)setDefaults { @([ItalicStyle getType]) : @[], // @([UnderlineStyle getStyleType]) : @[], @([StrikethroughStyle getType]) : @[], - // @([InlineCodeStyle getStyleType]) : - // @[ @([LinkStyle getStyleType]), @([MentionStyle getStyleType]) ], + @([InlineCodeStyle getType]) : @[ + // @([LinkStyle getStyleType]), @([MentionStyle getStyleType]) + ], // @([LinkStyle getStyleType]) : @[ // @([InlineCodeStyle getStyleType]), @([LinkStyle getStyleType]), // @([MentionStyle getStyleType]) @@ -218,8 +218,7 @@ - (void)setDefaults { // @([BoldStyle getStyleType]), @([UnderlineStyle getStyleType]), @([ItalicStyle getType]), @([StrikethroughStyle getType]), @([UnorderedListStyle getType]), @([OrderedListStyle getType]), - @([BlockQuoteStyle getType]), - // @([InlineCodeStyle getStyleType]), + @([BlockQuoteStyle getType]), @([InlineCodeStyle getType]), // @([MentionStyle getStyleType]), @([LinkStyle getStyleType]), // @([CheckboxListStyle getStyleType]) ], @@ -233,26 +232,27 @@ - (void)setDefaults { // @([UnderlineStyle getStyleType]) : @[ @([CodeBlockStyle getStyleType]) // ], @([StrikethroughStyle getType]) : @[ @([CodeBlockStyle getType]) ], - // @([InlineCodeStyle getStyleType]) : - // @[ @([CodeBlockStyle getStyleType]), @([ImageStyle getStyleType]) - // ], + @([InlineCodeStyle getType]) : @[ + @([CodeBlockStyle getType]), + // @([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]) : @[], + @([H1Style getType]) : @[], + @([H2Style getType]) : @[], + @([H3Style getType]) : @[], + @([H4Style getType]) : @[], + @([H5Style getType]) : @[], + @([H6Style getType]) : @[], + @([UnorderedListStyle getType]) : @[], + @([OrderedListStyle getType]) : @[], // @([CheckboxListStyle getStyleType]) : @[], - // @([BlockQuoteStyle getStyleType]) : @[], - // @([CodeBlockStyle getStyleType]) : @[], + @([BlockQuoteStyle getType]) : @[], + @([CodeBlockStyle getType]) : @[], // @([ImageStyle getStyleType]) : @[ @([InlineCodeStyle getStyleType]) ] } mutableCopy]; @@ -1025,8 +1025,7 @@ - (void)tryUpdatingActiveStyles { // .isUnderline = [self isStyleActive:[UnderlineStyle // getStyleType]], .isStrikeThrough = [self isStyleActive:[StrikethroughStyle getType]], - // .isInlineCode = [self isStyleActive:[InlineCodeStyle - // getStyleType]], + .isInlineCode = [self isStyleActive:[InlineCodeStyle getType]], // .isLink = [self isStyleActive:[LinkStyle getStyleType]], // .isMention = [self isStyleActive:[MentionStyle // getStyleType]], @@ -1051,8 +1050,7 @@ - (void)tryUpdatingActiveStyles { // .underline = GET_STYLE_STATE([UnderlineStyle // getStyleType]), .strikeThrough = GET_STYLE_STATE([StrikethroughStyle getType]), - // .inlineCode = GET_STYLE_STATE([InlineCodeStyle - // getStyleType]), + .inlineCode = GET_STYLE_STATE([InlineCodeStyle getType]), // .link = GET_STYLE_STATE([LinkStyle getStyleType]), // .mention = GET_STYLE_STATE([MentionStyle getStyleType]), .h1 = GET_STYLE_STATE([H1Style getType]), @@ -1152,10 +1150,10 @@ - (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args { // } else if ([commandName isEqualToString:@"toggleStrikeThrough"]) { [self toggleRegularStyle:[StrikethroughStyle getType]]; + } else if ([commandName isEqualToString:@"toggleInlineCode"]) { + [self toggleRegularStyle:[InlineCodeStyle getType]]; } - // else if ([commandName isEqualToString:@"toggleInlineCode"]) { - // [self toggleRegularStyle:[InlineCodeStyle getStyleType]]; - // } else if ([commandName isEqualToString:@"addLink"]) { + // else if ([commandName isEqualToString:@"addLink"]) { // NSInteger start = [((NSNumber *)args[0]) integerValue]; // NSInteger end = [((NSNumber *)args[1]) integerValue]; // NSString *text = (NSString *)args[2]; diff --git a/ios/inputParser/InputParser.mm b/ios/inputParser/InputParser.mm index 5a2d9839d..7a626be0a 100644 --- a/ios/inputParser/InputParser.mm +++ b/ios/inputParser/InputParser.mm @@ -492,7 +492,7 @@ - (NSString *)tagContentForStyle:(NSNumber *)style return @"u"; } 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) { @@ -1272,7 +1272,7 @@ - (NSArray *)getTextAndStylesFromHtml:(NSString *)fixedHtml { } else if ([tagName isEqualToString:@"s"]) { [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=\".+\"" diff --git a/ios/interfaces/StyleHeaders.h b/ios/interfaces/StyleHeaders.h index 08f050513..e18473d95 100644 --- a/ios/interfaces/StyleHeaders.h +++ b/ios/interfaces/StyleHeaders.h @@ -17,8 +17,7 @@ @interface StrikethroughStyle : StyleBase @end -@interface InlineCodeStyle : NSObject -- (void)handleNewlines; +@interface InlineCodeStyle : StyleBase @end @interface LinkStyle : NSObject diff --git a/ios/styles/InlineCodeStyle.mm b/ios/styles/InlineCodeStyle.mm index a04c9741f..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 "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 = [RangeUtils getNonNewlineRangesIn:_input->textView - range:range]; + NSArray *nonNewlineRanges = + [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 = - [RangeUtils 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/LinkStyle.mm b/ios/styles/LinkStyle.mm index c44f2a540..db0467dd2 100644 --- a/ios/styles/LinkStyle.mm +++ b/ios/styles/LinkStyle.mm @@ -397,7 +397,7 @@ - (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 = @@ -413,7 +413,7 @@ - (void)handleAutomaticLinks:(NSString *)word inRange:(NSRange)wordRange { } // we don't recognize links among inline code - if ([inlineCodeStyle anyOccurence:wordRange]) { + if ([inlineCodeStyle any:wordRange]) { return; } From e6bc666b0f63c41eeef1446d77259c660877cd60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20=C5=BB=C3=B3=C5=82kiewski?= Date: Thu, 19 Mar 2026 15:37:44 +0100 Subject: [PATCH 10/17] feat: refactor underline style --- ios/EnrichedTextInputView.mm | 31 ++++---- ios/inputParser/InputParser.mm | 4 +- ios/interfaces/StyleHeaders.h | 2 +- ios/styles/UnderlineStyle.mm | 135 +++------------------------------ 4 files changed, 26 insertions(+), 146 deletions(-) diff --git a/ios/EnrichedTextInputView.mm b/ios/EnrichedTextInputView.mm index 3256e3f02..e86834574 100644 --- a/ios/EnrichedTextInputView.mm +++ b/ios/EnrichedTextInputView.mm @@ -101,8 +101,7 @@ - (void)setDefaults { stylesDict = @{ // @([BoldStyle getStyleType]) : [[BoldStyle alloc] initWithInput:self], @([ItalicStyle getType]) : [[ItalicStyle alloc] initWithInput:self], - // @([UnderlineStyle getStyleType]) : - // [[UnderlineStyle alloc] initWithInput:self], + @([UnderlineStyle getType]) : [[UnderlineStyle alloc] initWithInput:self], @([StrikethroughStyle getType]) : [[StrikethroughStyle alloc] initWithInput:self], @([InlineCodeStyle getType]) : [[InlineCodeStyle alloc] initWithInput:self], @@ -129,7 +128,7 @@ - (void)setDefaults { conflictingStyles = @{ // @([BoldStyle getStyleType]) : @[], @([ItalicStyle getType]) : @[], - // @([UnderlineStyle getStyleType]) : @[], + @([UnderlineStyle getType]) : @[], @([StrikethroughStyle getType]) : @[], @([InlineCodeStyle getType]) : @[ // @([LinkStyle getStyleType]), @([MentionStyle getStyleType]) @@ -215,10 +214,11 @@ - (void)setDefaults { @([CodeBlockStyle getType]) : @[ @([H1Style getType]), @([H2Style getType]), @([H3Style getType]), @([H4Style getType]), @([H5Style getType]), @([H6Style getType]), - // @([BoldStyle getStyleType]), @([UnderlineStyle getStyleType]), - @([ItalicStyle getType]), @([StrikethroughStyle getType]), - @([UnorderedListStyle getType]), @([OrderedListStyle getType]), - @([BlockQuoteStyle getType]), @([InlineCodeStyle getType]), + // @([BoldStyle getStyleType]), + @([UnderlineStyle getType]), @([ItalicStyle getType]), + @([StrikethroughStyle getType]), @([UnorderedListStyle getType]), + @([OrderedListStyle getType]), @([BlockQuoteStyle getType]), + @([InlineCodeStyle getType]), // @([MentionStyle getStyleType]), @([LinkStyle getStyleType]), // @([CheckboxListStyle getStyleType]) ], @@ -229,8 +229,7 @@ - (void)setDefaults { blockingStyles = [@{ // @([BoldStyle getStyleType]) : @[ @([CodeBlockStyle getStyleType]) ], @([ItalicStyle getType]) : @[ @([CodeBlockStyle getType]) ], - // @([UnderlineStyle getStyleType]) : @[ @([CodeBlockStyle getStyleType]) - // ], + @([UnderlineStyle getType]) : @[ @([CodeBlockStyle getType]) ], @([StrikethroughStyle getType]) : @[ @([CodeBlockStyle getType]) ], @([InlineCodeStyle getType]) : @[ @([CodeBlockStyle getType]), @@ -1022,8 +1021,7 @@ - (void)tryUpdatingActiveStyles { emitter->onChangeStateDeprecated({ // .isBold = [self isStyleActive:[BoldStyle getStyleType]], .isItalic = [self isStyleActive:[ItalicStyle getType]], - // .isUnderline = [self isStyleActive:[UnderlineStyle - // getStyleType]], + .isUnderline = [self isStyleActive:[UnderlineStyle getType]], .isStrikeThrough = [self isStyleActive:[StrikethroughStyle getType]], .isInlineCode = [self isStyleActive:[InlineCodeStyle getType]], // .isLink = [self isStyleActive:[LinkStyle getStyleType]], @@ -1047,8 +1045,7 @@ - (void)tryUpdatingActiveStyles { emitter->onChangeState({ // .bold = GET_STYLE_STATE([BoldStyle getStyleType]), .italic = GET_STYLE_STATE([ItalicStyle getType]), - // .underline = GET_STYLE_STATE([UnderlineStyle - // getStyleType]), + .underline = GET_STYLE_STATE([UnderlineStyle getType]), .strikeThrough = GET_STYLE_STATE([StrikethroughStyle getType]), .inlineCode = GET_STYLE_STATE([InlineCodeStyle getType]), // .link = GET_STYLE_STATE([LinkStyle getStyleType]), @@ -1144,11 +1141,9 @@ - (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args { // } else if ([commandName isEqualToString:@"toggleItalic"]) { [self toggleRegularStyle:[ItalicStyle getType]]; - } - // else if ([commandName isEqualToString:@"toggleUnderline"]) { - // [self toggleRegularStyle:[UnderlineStyle getStyleType]]; - // } - else if ([commandName isEqualToString:@"toggleStrikeThrough"]) { + } else if ([commandName isEqualToString:@"toggleUnderline"]) { + [self toggleRegularStyle:[UnderlineStyle getType]]; + } else if ([commandName isEqualToString:@"toggleStrikeThrough"]) { [self toggleRegularStyle:[StrikethroughStyle getType]]; } else if ([commandName isEqualToString:@"toggleInlineCode"]) { [self toggleRegularStyle:[InlineCodeStyle getType]]; diff --git a/ios/inputParser/InputParser.mm b/ios/inputParser/InputParser.mm index 7a626be0a..15c05d972 100644 --- a/ios/inputParser/InputParser.mm +++ b/ios/inputParser/InputParser.mm @@ -488,7 +488,7 @@ - (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 getType])]) { return @"s"; @@ -1268,7 +1268,7 @@ - (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 getType])]; } else if ([tagName isEqualToString:@"code"]) { diff --git a/ios/interfaces/StyleHeaders.h b/ios/interfaces/StyleHeaders.h index e18473d95..c83c3c84b 100644 --- a/ios/interfaces/StyleHeaders.h +++ b/ios/interfaces/StyleHeaders.h @@ -11,7 +11,7 @@ @interface ItalicStyle : StyleBase @end -@interface UnderlineStyle : NSObject +@interface UnderlineStyle : StyleBase @end @interface StrikethroughStyle : StyleBase 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 From df2aff1cb4e3017a56372355a05f6aab1a19465e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20=C5=BB=C3=B3=C5=82kiewski?= Date: Thu, 19 Mar 2026 16:34:03 +0100 Subject: [PATCH 11/17] fix: clearing removed typing attributes --- ios/EnrichedTextInputView.mm | 1 + ios/attributesManager/AttributesManager.h | 1 + ios/attributesManager/AttributesManager.mm | 5 ++++- 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/ios/EnrichedTextInputView.mm b/ios/EnrichedTextInputView.mm index e86834574..ebf0a1f81 100644 --- a/ios/EnrichedTextInputView.mm +++ b/ios/EnrichedTextInputView.mm @@ -1552,6 +1552,7 @@ - (void)manageSelectionBasedChanges { BOOL onlySelectionChanged = textView.selectedRange.length == 0 && [_recentInputString isEqualToString:currentString]; + [attributesManager clearRemovedTypingAttributes]; [attributesManager manageTypingAttributesWithOnlySelection:onlySelectionChanged]; diff --git a/ios/attributesManager/AttributesManager.h b/ios/attributesManager/AttributesManager.h index 33350ec81..dc87e4199 100644 --- a/ios/attributesManager/AttributesManager.h +++ b/ios/attributesManager/AttributesManager.h @@ -10,6 +10,7 @@ - (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 index 3dc52f9f0..f96883cea 100644 --- a/ios/attributesManager/AttributesManager.mm +++ b/ios/attributesManager/AttributesManager.mm @@ -50,6 +50,10 @@ - (void)didRemoveTypingAttribute:(NSString *)key { [_removedTypingAttributes addObject:key]; } +- (void)clearRemovedTypingAttributes { + [_removedTypingAttributes removeAllObjects]; +} + - (void)handleDirtyRangesStyling { for (NSValue *rangeObj in _dirtyRanges) { NSRange dirtyRange = [rangeObj rangeValue]; @@ -164,7 +168,6 @@ - (void)manageTypingAttributesWithOnlySelection:(BOOL)onlySelectionChanged { newAttrs[entry.key] = entry.value; } - [_removedTypingAttributes removeAllObjects]; textView.typingAttributes = newAttrs; } From b0ed778deb32290fc7c3f7a68ded182472b23304 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20=C5=BB=C3=B3=C5=82kiewski?= Date: Thu, 19 Mar 2026 17:05:58 +0100 Subject: [PATCH 12/17] feat: refactor bold style --- ios/EnrichedTextInputView.mm | 26 +++--- ios/inputParser/InputParser.mm | 4 +- ios/interfaces/StyleHeaders.h | 2 +- ios/styles/BoldStyle.mm | 161 +++------------------------------ 4 files changed, 27 insertions(+), 166 deletions(-) diff --git a/ios/EnrichedTextInputView.mm b/ios/EnrichedTextInputView.mm index ebf0a1f81..85c88ca52 100644 --- a/ios/EnrichedTextInputView.mm +++ b/ios/EnrichedTextInputView.mm @@ -99,7 +99,7 @@ - (void)setDefaults { [[NSMutableDictionary alloc] init]; stylesDict = @{ - // @([BoldStyle getStyleType]) : [[BoldStyle alloc] initWithInput:self], + @([BoldStyle getType]) : [[BoldStyle alloc] initWithInput:self], @([ItalicStyle getType]) : [[ItalicStyle alloc] initWithInput:self], @([UnderlineStyle getType]) : [[UnderlineStyle alloc] initWithInput:self], @([StrikethroughStyle getType]) : @@ -126,7 +126,7 @@ - (void)setDefaults { }; conflictingStyles = @{ - // @([BoldStyle getStyleType]) : @[], + @([BoldStyle getType]) : @[], @([ItalicStyle getType]) : @[], @([UnderlineStyle getType]) : @[], @([StrikethroughStyle getType]) : @[], @@ -214,11 +214,10 @@ - (void)setDefaults { @([CodeBlockStyle getType]) : @[ @([H1Style getType]), @([H2Style getType]), @([H3Style getType]), @([H4Style getType]), @([H5Style getType]), @([H6Style getType]), - // @([BoldStyle getStyleType]), - @([UnderlineStyle getType]), @([ItalicStyle getType]), - @([StrikethroughStyle getType]), @([UnorderedListStyle getType]), - @([OrderedListStyle getType]), @([BlockQuoteStyle getType]), - @([InlineCodeStyle getType]), + @([BoldStyle getType]), @([UnderlineStyle getType]), + @([ItalicStyle getType]), @([StrikethroughStyle getType]), + @([UnorderedListStyle getType]), @([OrderedListStyle getType]), + @([BlockQuoteStyle getType]), @([InlineCodeStyle getType]), // @([MentionStyle getStyleType]), @([LinkStyle getStyleType]), // @([CheckboxListStyle getStyleType]) ], @@ -227,7 +226,7 @@ - (void)setDefaults { }; blockingStyles = [@{ - // @([BoldStyle getStyleType]) : @[ @([CodeBlockStyle getStyleType]) ], + @([BoldStyle getType]) : @[ @([CodeBlockStyle getType]) ], @([ItalicStyle getType]) : @[ @([CodeBlockStyle getType]) ], @([UnderlineStyle getType]) : @[ @([CodeBlockStyle getType]) ], @([StrikethroughStyle getType]) : @[ @([CodeBlockStyle getType]) ], @@ -1019,7 +1018,7 @@ - (void)tryUpdatingActiveStyles { _blockedStyles = newBlockedStyles; emitter->onChangeStateDeprecated({ - // .isBold = [self isStyleActive:[BoldStyle getStyleType]], + .isBold = [self isStyleActive:[BoldStyle getType]], .isItalic = [self isStyleActive:[ItalicStyle getType]], .isUnderline = [self isStyleActive:[UnderlineStyle getType]], .isStrikeThrough = [self isStyleActive:[StrikethroughStyle getType]], @@ -1043,7 +1042,7 @@ - (void)tryUpdatingActiveStyles { // [self isStyleActive:[CheckboxListStyle getStyleType]] }); emitter->onChangeState({ - // .bold = GET_STYLE_STATE([BoldStyle 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]), @@ -1136,10 +1135,9 @@ - (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args { // NSString *value = (NSString *)args[0]; // [self setValue:value]; // } - // else if ([commandName isEqualToString:@"toggleBold"]) { - // [self toggleRegularStyle:[BoldStyle getStyleType]]; - // } - else if ([commandName isEqualToString:@"toggleItalic"]) { + else if ([commandName isEqualToString:@"toggleBold"]) { + [self toggleRegularStyle:[BoldStyle getType]]; + } else if ([commandName isEqualToString:@"toggleItalic"]) { [self toggleRegularStyle:[ItalicStyle getType]]; } else if ([commandName isEqualToString:@"toggleUnderline"]) { [self toggleRegularStyle:[UnderlineStyle getType]]; diff --git a/ios/inputParser/InputParser.mm b/ios/inputParser/InputParser.mm index 15c05d972..81bb1ca5b 100644 --- a/ios/inputParser/InputParser.mm +++ b/ios/inputParser/InputParser.mm @@ -468,7 +468,7 @@ - (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 getType])]) { return @"i"; @@ -1210,7 +1210,7 @@ - (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 getType])]; } else if ([tagName isEqualToString:@"img"]) { diff --git a/ios/interfaces/StyleHeaders.h b/ios/interfaces/StyleHeaders.h index c83c3c84b..80c2118a4 100644 --- a/ios/interfaces/StyleHeaders.h +++ b/ios/interfaces/StyleHeaders.h @@ -5,7 +5,7 @@ #import "MentionParams.h" #import "StyleBase.h" -@interface BoldStyle : NSObject +@interface BoldStyle : StyleBase @end @interface ItalicStyle : StyleBase 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 From a6906de1ec324e2da0befc082311ff93c7062db8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20=C5=BB=C3=B3=C5=82kiewski?= Date: Thu, 19 Mar 2026 17:13:55 +0100 Subject: [PATCH 13/17] fix: naming of styles --- ios/extensions/LayoutManagerExtension.mm | 5 +++-- ios/styles/BlockQuoteStyle.mm | 2 +- ios/styles/CodeBlockStyle.mm | 2 +- ios/styles/H1Style.mm | 2 +- ios/styles/H2Style.mm | 2 +- ios/styles/H3Style.mm | 2 +- ios/styles/H4Style.mm | 2 +- ios/styles/H5Style.mm | 2 +- ios/styles/H6Style.mm | 2 +- ios/styles/OrderedListStyle.mm | 2 +- ios/styles/UnorderedListStyle.mm | 2 +- 11 files changed, 13 insertions(+), 12 deletions(-) diff --git a/ios/extensions/LayoutManagerExtension.mm b/ios/extensions/LayoutManagerExtension.mm index 7cce0509a..0480fe6f6 100644 --- a/ios/extensions/LayoutManagerExtension.mm +++ b/ios/extensions/LayoutManagerExtension.mm @@ -293,7 +293,7 @@ - (void)drawLists:(EnrichedTextInputView *)typedInput if ([markerFormat isEqualToString: - NSTextListMarkerDecimal]) { + @"EnrichedOrderedList"]) { NSString *marker = [self getDecimalMarkerForList:typedInput charIndex: @@ -308,7 +308,8 @@ - (void)drawLists:(EnrichedTextInputView *)typedInput usedRect:usedRect]; } else if ([markerFormat isEqualToString: - @"UnorderedList"]) { + @"EnrichedUnorderedLis" + @"t"]) { [self drawBullet:typedInput origin:origin usedRect:usedRect]; diff --git a/ios/styles/BlockQuoteStyle.mm b/ios/styles/BlockQuoteStyle.mm index 06f4dfa50..ea28bbd0b 100644 --- a/ios/styles/BlockQuoteStyle.mm +++ b/ios/styles/BlockQuoteStyle.mm @@ -9,7 +9,7 @@ + (StyleType)getType { } - (NSString *)getValue { - return @"blockquote"; + return @"EnrichedBlockQuote"; } - (BOOL)isParagraph { diff --git a/ios/styles/CodeBlockStyle.mm b/ios/styles/CodeBlockStyle.mm index da2a40a42..b1cf91884 100644 --- a/ios/styles/CodeBlockStyle.mm +++ b/ios/styles/CodeBlockStyle.mm @@ -9,7 +9,7 @@ + (StyleType)getType { } - (NSString *)getValue { - return @"codeblock"; + return @"EnrichedCodeBlock"; } - (BOOL)isParagraph { diff --git a/ios/styles/H1Style.mm b/ios/styles/H1Style.mm index f0e72394a..c04c26f3b 100644 --- a/ios/styles/H1Style.mm +++ b/ios/styles/H1Style.mm @@ -6,7 +6,7 @@ + (StyleType)getType { return H1; } - (NSString *)getValue { - return @"h1"; + return @"EnrichedH1"; } - (BOOL)isParagraph { return YES; diff --git a/ios/styles/H2Style.mm b/ios/styles/H2Style.mm index dffc83f94..785b2b72a 100644 --- a/ios/styles/H2Style.mm +++ b/ios/styles/H2Style.mm @@ -6,7 +6,7 @@ + (StyleType)getType { return H2; } - (NSString *)getValue { - return @"h2"; + return @"EnrichedH2"; } - (BOOL)isParagraph { return YES; diff --git a/ios/styles/H3Style.mm b/ios/styles/H3Style.mm index bb0b3ea4a..9091f6e3b 100644 --- a/ios/styles/H3Style.mm +++ b/ios/styles/H3Style.mm @@ -6,7 +6,7 @@ + (StyleType)getType { return H3; } - (NSString *)getValue { - return @"h3"; + return @"EnrichedH3"; } - (BOOL)isParagraph { return YES; diff --git a/ios/styles/H4Style.mm b/ios/styles/H4Style.mm index ef1ddf701..a641f5ed8 100644 --- a/ios/styles/H4Style.mm +++ b/ios/styles/H4Style.mm @@ -6,7 +6,7 @@ + (StyleType)getType { return H4; } - (NSString *)getValue { - return @"h4"; + return @"EnrichedH4"; } - (BOOL)isParagraph { return YES; diff --git a/ios/styles/H5Style.mm b/ios/styles/H5Style.mm index 8954106ac..40fab0f28 100644 --- a/ios/styles/H5Style.mm +++ b/ios/styles/H5Style.mm @@ -6,7 +6,7 @@ + (StyleType)getType { return H5; } - (NSString *)getValue { - return @"h5"; + return @"EnrichedH5"; } - (BOOL)isParagraph { return YES; diff --git a/ios/styles/H6Style.mm b/ios/styles/H6Style.mm index daaa5e610..2e576bb0a 100644 --- a/ios/styles/H6Style.mm +++ b/ios/styles/H6Style.mm @@ -6,7 +6,7 @@ + (StyleType)getType { return H6; } - (NSString *)getValue { - return @"h6"; + return @"EnrichedH6"; } - (BOOL)isParagraph { return YES; diff --git a/ios/styles/OrderedListStyle.mm b/ios/styles/OrderedListStyle.mm index 55c9afe9f..3f81f53e8 100644 --- a/ios/styles/OrderedListStyle.mm +++ b/ios/styles/OrderedListStyle.mm @@ -10,7 +10,7 @@ + (StyleType)getType { } - (NSString *)getValue { - return NSTextListMarkerDecimal; + return @"EnrichedOrderedList"; } - (BOOL)isParagraph { diff --git a/ios/styles/UnorderedListStyle.mm b/ios/styles/UnorderedListStyle.mm index 2dfcb0dec..d69bd1f72 100644 --- a/ios/styles/UnorderedListStyle.mm +++ b/ios/styles/UnorderedListStyle.mm @@ -10,7 +10,7 @@ + (StyleType)getType { } - (NSString *)getValue { - return @"UnorderedList"; + return @"EnrichedUnorderedList"; } - (BOOL)isParagraph { From 2d27d51186cfbc477be161f3c22ea396b09cb85a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20=C5=BB=C3=B3=C5=82kiewski?= Date: Thu, 19 Mar 2026 17:15:37 +0100 Subject: [PATCH 14/17] fix: add comment --- ios/EnrichedTextInputView.mm | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ios/EnrichedTextInputView.mm b/ios/EnrichedTextInputView.mm index 85c88ca52..b142da937 100644 --- a/ios/EnrichedTextInputView.mm +++ b/ios/EnrichedTextInputView.mm @@ -1550,6 +1550,9 @@ - (void)manageSelectionBasedChanges { 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]; From 09c920bf16d59b549a1ffcd05ed138ea3ab23194 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20=C5=BB=C3=B3=C5=82kiewski?= Date: Fri, 20 Mar 2026 08:45:03 +0100 Subject: [PATCH 15/17] fix: make list shortcuts work --- ios/EnrichedTextInputView.mm | 12 ++--- ios/interfaces/StyleHeaders.h | 8 ++-- ios/styles/OrderedListStyle.mm | 69 ++++++++++++++--------------- ios/styles/UnorderedListStyle.mm | 75 ++++++++++++++++---------------- 4 files changed, 81 insertions(+), 83 deletions(-) diff --git a/ios/EnrichedTextInputView.mm b/ios/EnrichedTextInputView.mm index b142da937..097f0d00f 100644 --- a/ios/EnrichedTextInputView.mm +++ b/ios/EnrichedTextInputView.mm @@ -1738,7 +1738,7 @@ - (bool)textView:(UITextView *)textView [self handleKeyPressInRange:text range:range]; UnorderedListStyle *uStyle = stylesDict[@([UnorderedListStyle getType])]; - // OrderedListStyle *oStyle = stylesDict[@([OrderedListStyle getStyleType])]; + OrderedListStyle *oStyle = stylesDict[@([OrderedListStyle getType])]; // CheckboxListStyle *cbLStyle = stylesDict[@([CheckboxListStyle // getStyleType])]; BlockQuoteStyle *bqStyle = stylesDict[@([BlockQuoteStyle // getStyleType])]; CodeBlockStyle *cbStyle = stylesDict[@([CodeBlockStyle @@ -1759,13 +1759,9 @@ - (bool)textView:(UITextView *)textView // ZWS backspace handling for paragraph styles [ZeroWidthSpaceUtils handleBackspaceInRange:range replacementText:text - input:self] - // || [uStyle handleBackspaceInRange:range replacementText:text] - // || [uStyle tryHandlingListShorcutInRange:range - // replacementText:text] - // || [oStyle handleBackspaceInRange:range replacementText:text] - // || [oStyle tryHandlingListShorcutInRange:range - // replacementText:text] + input:self] || + [uStyle tryHandlingListShorcutInRange:range replacementText:text] || + [oStyle tryHandlingListShorcutInRange:range replacementText:text] // || [cbLStyle handleBackspaceInRange:range replacementText:text] // || [cbLStyle handleNewlinesInRange:range replacementText:text] // || [bqStyle handleBackspaceInRange:range replacementText:text] diff --git a/ios/interfaces/StyleHeaders.h b/ios/interfaces/StyleHeaders.h index 80c2118a4..85f3d7524 100644 --- a/ios/interfaces/StyleHeaders.h +++ b/ios/interfaces/StyleHeaders.h @@ -76,13 +76,13 @@ @end @interface UnorderedListStyle : StyleBase -//- (BOOL)tryHandlingListShorcutInRange:(NSRange)range -// replacementText:(NSString *)text; +- (BOOL)tryHandlingListShorcutInRange:(NSRange)range + replacementText:(NSString *)text; @end @interface OrderedListStyle : StyleBase -//- (BOOL)tryHandlingListShorcutInRange:(NSRange)range -// replacementText:(NSString *)text; +- (BOOL)tryHandlingListShorcutInRange:(NSRange)range + replacementText:(NSString *)text; @end @interface CheckboxListStyle : NSObject diff --git a/ios/styles/OrderedListStyle.mm b/ios/styles/OrderedListStyle.mm index 3f81f53e8..60a6089a5 100644 --- a/ios/styles/OrderedListStyle.mm +++ b/ios/styles/OrderedListStyle.mm @@ -44,42 +44,43 @@ - (void)applyStyling:(NSRange)range { }]; } -// - (BOOL)tryHandlingListShorcutInRange:(NSRange)range -// replacementText:(NSString *)text { -// NSRange paragraphRange = -// [_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) -// if ([text isEqualToString:@"."] && -// range.location - 1 == paragraphRange.location) { -// unichar charBefore = [_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]) { -// // don't emit during the replacing -// _input->blockEmitting = YES; +- (BOOL)tryHandlingListShorcutInRange:(NSRange)range + replacementText:(NSString *)text { + NSRange paragraphRange = + [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 digit '1') + if ([text isEqualToString:@"."] && + range.location - 1 == paragraphRange.location) { + unichar charBefore = [self.input->textView.textStorage.string + characterAtIndex:range.location - 1]; + if (charBefore == '1') { + // we got a match - add a list if possible + if ([self.input handleStyleBlocksAndConflicts:[[self class] getType] + range:paragraphRange]) { + // don't emit during the replacing + self.input->blockEmitting = YES; -// // remove the number -// [TextInsertionUtils replaceText:@"" -// at:NSMakeRange(paragraphRange.location, -// 1) -// additionalAttributes:nullptr -// input:_input -// withSelection:YES]; + // remove the number + [TextInsertionUtils replaceText:@"" + at:NSMakeRange(paragraphRange.location, 1) + additionalAttributes:nullptr + 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]; -// return YES; -// } -// } -// } -// return NO; -// } + // add attributes on the paragraph + [self add:NSMakeRange(paragraphRange.location, + paragraphRange.length - 1) + withTyping:YES + withDirtyRange:YES]; + + return YES; + } + } + } + return NO; +} @end diff --git a/ios/styles/UnorderedListStyle.mm b/ios/styles/UnorderedListStyle.mm index d69bd1f72..2e0a1a103 100644 --- a/ios/styles/UnorderedListStyle.mm +++ b/ios/styles/UnorderedListStyle.mm @@ -44,42 +44,43 @@ - (void)applyStyling:(NSRange)range { }]; } -//- (BOOL)tryHandlingListShorcutInRange:(NSRange)range -// replacementText:(NSString *)text { -// NSRange paragraphRange = -// [_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 -// characterAtIndex:range.location - 1]; -// if (charBefore == '-') { -// // we got a match - add a list if possible -// if ([_input handleStyleBlocksAndConflicts:[[self class] getStyleType] -// range:paragraphRange]) { -// // don't emit during the replacing -// _input->blockEmitting = YES; -// -// // remove the dash -// [TextInsertionUtils replaceText:@"" -// at:NSMakeRange(paragraphRange.location, -// 1) -// additionalAttributes:nullptr -// input:_input -// withSelection:YES]; -// -// _input->blockEmitting = NO; -// -// // add attributes on the dashless paragraph -// [self addAttributes:NSMakeRange(paragraphRange.location, -// paragraphRange.length - 1) -// withTypingAttr:YES]; -// return YES; -// } -// } -// } -// return NO; -//} +- (BOOL)tryHandlingListShorcutInRange:(NSRange)range + replacementText:(NSString *)text { + NSRange paragraphRange = + [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 = [self.input->textView.textStorage.string + characterAtIndex:range.location - 1]; + if (charBefore == '-') { + // we got a match - add a list if possible + if ([self.input handleStyleBlocksAndConflicts:[[self class] getType] + range:paragraphRange]) { + // don't emit during the replacing + self.input->blockEmitting = YES; + + // remove the dash + [TextInsertionUtils replaceText:@"" + at:NSMakeRange(paragraphRange.location, 1) + additionalAttributes:nullptr + input:self.input + withSelection:YES]; + + self.input->blockEmitting = NO; + + // add attributes on the dashless paragraph + [self add:NSMakeRange(paragraphRange.location, + paragraphRange.length - 1) + withTyping:YES + withDirtyRange:YES]; + + return YES; + } + } + } + return NO; +} @end From 6c1bc0b569ecaaba90559e2ece17b1e2bee7a83f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20=C5=BB=C3=B3=C5=82kiewski?= Date: Fri, 20 Mar 2026 11:17:44 +0100 Subject: [PATCH 16/17] feat: refactor checkbox list style --- ios/EnrichedTextInputView.mm | 183 +++++----- ios/attributesManager/AttributesManager.mm | 2 +- ios/extensions/LayoutManagerExtension.mm | 20 +- ios/inputParser/InputParser.mm | 26 +- ios/interfaces/StyleBase.h | 1 + ios/interfaces/StyleBase.mm | 7 + ios/interfaces/StyleHeaders.h | 14 +- ios/styles/CheckboxListStyle.mm | 395 +++++++-------------- ios/utils/CheckboxHitTestUtils.mm | 5 +- 9 files changed, 255 insertions(+), 398 deletions(-) diff --git a/ios/EnrichedTextInputView.mm b/ios/EnrichedTextInputView.mm index 097f0d00f..682e26804 100644 --- a/ios/EnrichedTextInputView.mm +++ b/ios/EnrichedTextInputView.mm @@ -118,8 +118,8 @@ - (void)setDefaults { [[UnorderedListStyle alloc] initWithInput:self], @([OrderedListStyle getType]) : [[OrderedListStyle alloc] initWithInput:self], - // @([CheckboxListStyle getStyleType]) : - // [[CheckboxListStyle alloc] initWithInput:self], + @([CheckboxListStyle getType]) : + [[CheckboxListStyle alloc] initWithInput:self], @([BlockQuoteStyle getType]) : [[BlockQuoteStyle alloc] initWithInput:self], @([CodeBlockStyle getType]) : [[CodeBlockStyle alloc] initWithInput:self], // @([ImageStyle getStyleType]) : [[ImageStyle alloc] initWithInput:self] @@ -145,71 +145,66 @@ - (void)setDefaults { @([H5Style getType]), @([H6Style getType]), @([UnorderedListStyle getType]), @([OrderedListStyle getType]), @([BlockQuoteStyle getType]), @([CodeBlockStyle getType]), - // @([CheckboxListStyle getStyleType]) + @([CheckboxListStyle getType]) ], @([H2Style getType]) : @[ @([H1Style getType]), @([H3Style getType]), @([H4Style getType]), @([H5Style getType]), @([H6Style getType]), @([UnorderedListStyle getType]), @([OrderedListStyle getType]), @([BlockQuoteStyle getType]), @([CodeBlockStyle getType]), - // @([CheckboxListStyle getStyleType]) + @([CheckboxListStyle getType]) ], @([H3Style getType]) : @[ @([H1Style getType]), @([H2Style getType]), @([H4Style getType]), @([H5Style getType]), @([H6Style getType]), @([UnorderedListStyle getType]), @([OrderedListStyle getType]), @([BlockQuoteStyle getType]), @([CodeBlockStyle getType]), - // @([CheckboxListStyle getStyleType]) + @([CheckboxListStyle getType]) ], @([H4Style getType]) : @[ @([H1Style getType]), @([H2Style getType]), @([H3Style getType]), @([H5Style getType]), @([H6Style getType]), @([UnorderedListStyle getType]), @([OrderedListStyle getType]), @([BlockQuoteStyle getType]), @([CodeBlockStyle getType]), - // @([CheckboxListStyle getStyleType]) + @([CheckboxListStyle getType]) ], @([H5Style getType]) : @[ @([H1Style getType]), @([H2Style getType]), @([H3Style getType]), @([H4Style getType]), @([H6Style getType]), @([UnorderedListStyle getType]), @([OrderedListStyle getType]), @([BlockQuoteStyle getType]), @([CodeBlockStyle getType]), - // @([CheckboxListStyle getStyleType]) + @([CheckboxListStyle getType]) ], @([H6Style getType]) : @[ @([H1Style getType]), @([H2Style getType]), @([H3Style getType]), @([H4Style getType]), @([H5Style getType]), @([UnorderedListStyle getType]), @([OrderedListStyle getType]), @([BlockQuoteStyle getType]), @([CodeBlockStyle getType]), - // @([CheckboxListStyle getStyleType]) + @([CheckboxListStyle getType]) ], @([UnorderedListStyle getType]) : @[ @([H1Style getType]), @([H2Style getType]), @([H3Style getType]), @([H4Style getType]), @([H5Style getType]), @([H6Style getType]), @([OrderedListStyle getType]), @([BlockQuoteStyle getType]), - @([CodeBlockStyle getType]), - // @([CheckboxListStyle getStyleType]) + @([CodeBlockStyle getType]), @([CheckboxListStyle getType]) ], @([OrderedListStyle getType]) : @[ @([H1Style getType]), @([H2Style getType]), @([H3Style getType]), @([H4Style getType]), @([H5Style getType]), @([H6Style getType]), @([UnorderedListStyle getType]), @([BlockQuoteStyle getType]), - @([CodeBlockStyle getType]), - // @([CheckboxListStyle getStyleType]) + @([CodeBlockStyle getType]), @([CheckboxListStyle getType]) + ], + @([CheckboxListStyle getType]) : @[ + @([H1Style getType]), @([H2Style getType]), @([H3Style getType]), + @([H4Style getType]), @([H5Style getType]), @([H6Style getType]), + @([UnorderedListStyle getType]), @([OrderedListStyle getType]), + @([BlockQuoteStyle getType]), @([CodeBlockStyle getType]) ], - // @([CheckboxListStyle getStyleType]) : @[ - // @([H1Style getType]), @([H2Style getType]), - // @([H3Style getType]), @([H4Style getType]), - // @([H5Style getType]), @([H6Style getType]), - // @([UnorderedListStyle getStyleType]), @([OrderedListStyle - // getStyleType]), - // @([BlockQuoteStyle getStyleType]), @([CodeBlockStyle getStyleType]) - // ], @([BlockQuoteStyle getType]) : @[ @([H1Style getType]), @([H2Style getType]), @([H3Style getType]), @([H4Style getType]), @([H5Style getType]), @([H6Style getType]), @([UnorderedListStyle getType]), @([OrderedListStyle getType]), - @([CodeBlockStyle getType]), - // @([CheckboxListStyle getStyleType]) + @([CodeBlockStyle getType]), @([CheckboxListStyle getType]) ], @([CodeBlockStyle getType]) : @[ @([H1Style getType]), @([H2Style getType]), @([H3Style getType]), @@ -219,7 +214,7 @@ - (void)setDefaults { @([UnorderedListStyle getType]), @([OrderedListStyle getType]), @([BlockQuoteStyle getType]), @([InlineCodeStyle getType]), // @([MentionStyle getStyleType]), @([LinkStyle getStyleType]), - // @([CheckboxListStyle getStyleType]) + @([CheckboxListStyle getType]) ], // @([ImageStyle getStyleType]) : // @[ @([LinkStyle getStyleType]), @([MentionStyle getStyleType]) ] @@ -248,7 +243,7 @@ - (void)setDefaults { @([H6Style getType]) : @[], @([UnorderedListStyle getType]) : @[], @([OrderedListStyle getType]) : @[], - // @([CheckboxListStyle getStyleType]) : @[], + @([CheckboxListStyle getType]) : @[], @([BlockQuoteStyle getType]) : @[], @([CodeBlockStyle getType]) : @[], // @([ImageStyle getStyleType]) : @[ @([InlineCodeStyle getStyleType]) ] @@ -1017,52 +1012,48 @@ - (void)tryUpdatingActiveStyles { _activeStyles = newActiveStyles; _blockedStyles = newBlockedStyles; - emitter->onChangeStateDeprecated({ - .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 getStyleType]] - }); - emitter->onChangeState({ - .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 - // getStyleType]) - }); + emitter->onChangeStateDeprecated( + {.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 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])}); } } @@ -1176,11 +1167,10 @@ - (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args { [self toggleRegularStyle:[UnorderedListStyle getType]]; } else if ([commandName isEqualToString:@"toggleOrderedList"]) { [self toggleRegularStyle:[OrderedListStyle getType]]; - } - // else if ([commandName isEqualToString:@"toggleCheckboxList"]) { - // BOOL checked = [args[0] boolValue]; - // [self toggleCheckboxList:checked]; - else if ([commandName isEqualToString:@"toggleBlockQuote"]) { + } else if ([commandName isEqualToString:@"toggleCheckboxList"]) { + BOOL checked = [args[0] boolValue]; + [self toggleCheckboxList:checked]; + } else if ([commandName isEqualToString:@"toggleBlockQuote"]) { [self toggleRegularStyle:[BlockQuoteStyle getType]]; } else if ([commandName isEqualToString:@"toggleCodeBlock"]) { [self toggleRegularStyle:[CodeBlockStyle getType]]; @@ -1381,23 +1371,20 @@ - (void)toggleRegularStyle:(StyleType)type { // } //} -// - (void)toggleCheckboxList:(BOOL)checked { -// CheckboxListStyle *checkboxListStyleClass = -// (CheckboxListStyle *)stylesDict[@([CheckboxListStyle getStyleType])]; -// if (checkboxListStyleClass == nullptr) { -// return; -// } -// // we always pass whole paragraph/s range to these styles -// NSRange paragraphRange = [textView.textStorage.string -// paragraphRangeForRange:textView.selectedRange]; - -// if ([self handleStyleBlocksAndConflicts:[CheckboxListStyle getStyleType] -// range:paragraphRange]) { -// [checkboxListStyleClass applyStyleWithCheckedValue:checked -// inRange:paragraphRange]; -// [self anyTextMayHaveBeenModified]; -// } -// } +- (void)toggleCheckboxList:(BOOL)checked { + CheckboxListStyle *style = + (CheckboxListStyle *)stylesDict[@([CheckboxListStyle getType])]; + if (style == nullptr) { + return; + } + NSRange range = [textView.textStorage.string + paragraphRangeForRange:textView.selectedRange]; + if ([self handleStyleBlocksAndConflicts:[CheckboxListStyle getType] + range:range]) { + [style toggleWithChecked:checked range:range]; + [self anyTextMayHaveBeenModified]; + } +} // - (void)addLinkAt:(NSInteger)start // end:(NSInteger)end @@ -1739,8 +1726,9 @@ - (bool)textView:(UITextView *)textView UnorderedListStyle *uStyle = stylesDict[@([UnorderedListStyle getType])]; OrderedListStyle *oStyle = stylesDict[@([OrderedListStyle getType])]; - // CheckboxListStyle *cbLStyle = stylesDict[@([CheckboxListStyle - // getStyleType])]; BlockQuoteStyle *bqStyle = stylesDict[@([BlockQuoteStyle + 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 @@ -1761,9 +1749,8 @@ - (bool)textView:(UITextView *)textView replacementText:text input:self] || [uStyle tryHandlingListShorcutInRange:range replacementText:text] || - [oStyle tryHandlingListShorcutInRange:range replacementText:text] - // || [cbLStyle handleBackspaceInRange:range replacementText:text] - // || [cbLStyle handleNewlinesInRange: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 @@ -1891,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; diff --git a/ios/attributesManager/AttributesManager.mm b/ios/attributesManager/AttributesManager.mm index f96883cea..46d39090f 100644 --- a/ios/attributesManager/AttributesManager.mm +++ b/ios/attributesManager/AttributesManager.mm @@ -86,7 +86,7 @@ - (void)handleDirtyRangesStyling { for (StylePair *stylePair in presentStyles[styleType]) { NSRange occurenceRange = [stylePair.rangeValue rangeValue]; [style applyStyling:occurenceRange]; - [style add:occurenceRange withTyping:NO withDirtyRange:NO]; + [style reapplyAttributesFromStylePair:stylePair]; } } } diff --git a/ios/extensions/LayoutManagerExtension.mm b/ios/extensions/LayoutManagerExtension.mm index 0480fe6f6..9d647faf5 100644 --- a/ios/extensions/LayoutManagerExtension.mm +++ b/ios/extensions/LayoutManagerExtension.mm @@ -248,21 +248,17 @@ - (void)drawLists:(EnrichedTextInputView *)typedInput typedInput->stylesDict[@([UnorderedListStyle getType])]; OrderedListStyle *olStyle = typedInput->stylesDict[@([OrderedListStyle getType])]; - // CheckboxListStyle *cbStyle = - // typedInput->stylesDict[@([CheckboxListStyle getStyleType])]; - if (ulStyle == nullptr && olStyle == nullptr) { + CheckboxListStyle *cbStyle = + typedInput->stylesDict[@([CheckboxListStyle getType])]; + if (ulStyle == nullptr || olStyle == nullptr || cbStyle == nullptr) { return; } NSMutableArray *allLists = [[NSMutableArray alloc] init]; - if (ulStyle != nullptr) { - [allLists addObjectsFromArray:[ulStyle all:visibleCharRange]]; - } - if (olStyle != nullptr) { - [allLists addObjectsFromArray:[olStyle all: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; @@ -373,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; diff --git a/ios/inputParser/InputParser.mm b/ios/inputParser/InputParser.mm index 81bb1ca5b..3806037e5 100644 --- a/ios/inputParser/InputParser.mm +++ b/ios/inputParser/InputParser.mm @@ -109,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) { @@ -160,7 +160,7 @@ - (NSString *)parseToHtmlFromRange:(NSRange)range { 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 { @@ -207,7 +207,7 @@ - (NSString *)parseToHtmlFromRange:(NSRange)range { // handle ending checkbox list if (inCheckboxList && ![currentActiveStyles - containsObject:@([CheckboxListStyle getStyleType])]) { + containsObject:@([CheckboxListStyle getType])]) { inCheckboxList = NO; [result appendString:@"\n"]; } @@ -241,7 +241,7 @@ - (NSString *)parseToHtmlFromRange:(NSRange)range { // handle starting checkbox list if (!inCheckboxList && [currentActiveStyles - containsObject:@([CheckboxListStyle getStyleType])]) { + containsObject:@([CheckboxListStyle getType])]) { inCheckboxList = YES; [result appendString:@"\n
      "]; } @@ -260,7 +260,7 @@ - (NSString *)parseToHtmlFromRange:(NSRange)range { [currentActiveStyles containsObject:@([BlockQuoteStyle getType])] || [currentActiveStyles containsObject:@([CodeBlockStyle getType])] || [currentActiveStyles - containsObject:@([CheckboxListStyle getStyleType])]) { + containsObject:@([CheckboxListStyle getType])]) { [result appendString:@"\n"]; } else { [result appendString:@"\n

      "]; @@ -406,7 +406,7 @@ - (NSString *)parseToHtmlFromRange:(NSRange)range { containsObject:@([CodeBlockStyle getType])]) { [result appendString:@"\n"]; } else if ([previousActiveStyles - containsObject:@([CheckboxListStyle getStyleType])]) { + containsObject:@([CheckboxListStyle getType])]) { [result appendString:@"\n

    "]; } else if ([previousActiveStyles containsObject:@([H1Style getType])] || [previousActiveStyles containsObject:@([H2Style getType])] || @@ -561,11 +561,11 @@ - (NSString *)tagContentForStyle:(NSNumber *)style } 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) { @@ -673,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; @@ -682,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; @@ -1352,7 +1354,7 @@ - (NSArray *)getTextAndStylesFromHtml:(NSString *)fixedHtml { } } else if ([tagName isEqualToString:@"ul"]) { if ([self isUlCheckboxList:params]) { - [styleArr addObject:@([CheckboxListStyle getStyleType])]; + [styleArr addObject:@([CheckboxListStyle getType])]; stylePair.styleValue = [self prepareCheckboxListStyleValue:tagRangeValue checkboxStates:checkboxStates]; diff --git a/ios/interfaces/StyleBase.h b/ios/interfaces/StyleBase.h index 53e55498c..a6b7b3ea0 100644 --- a/ios/interfaces/StyleBase.h +++ b/ios/interfaces/StyleBase.h @@ -27,5 +27,6 @@ - (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 index 5a4686e31..16e4d70da 100644 --- a/ios/interfaces/StyleBase.mm +++ b/ios/interfaces/StyleBase.mm @@ -225,6 +225,13 @@ - (BOOL)any:(NSRange)range { - (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 { diff --git a/ios/interfaces/StyleHeaders.h b/ios/interfaces/StyleHeaders.h index 85f3d7524..5cb1e3ded 100644 --- a/ios/interfaces/StyleHeaders.h +++ b/ios/interfaces/StyleHeaders.h @@ -85,15 +85,15 @@ 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 : StyleBase diff --git a/ios/styles/CheckboxListStyle.mm b/ios/styles/CheckboxListStyle.mm index d9709dc12..f9caec0c4 100644 --- a/ios/styles/CheckboxListStyle.mm +++ b/ios/styles/CheckboxListStyle.mm @@ -1,314 +1,159 @@ #import "EnrichedTextInputView.h" -#import "FontExtension.h" -#import "OccurenceUtils.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; -} - -- (CGFloat)getHeadIndent { - return [_input->config checkboxListMarginLeft] + - [_input->config checkboxListGapWidth] + - [_input->config checkboxListBoxSize]; +- (NSString *)getValue { + return @"EnrichedCheckbox0"; } -- (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)addTypingAttributes { -} - -- (void)removeAttributes:(NSRange)range { - NSArray *paragraphs = - [RangeUtils getSeparateParagraphsRangesIn:_input->textView range:range]; - - [_input->textView.textStorage beginEditing]; +- (void)addWithChecked:(BOOL)checked + range:(NSRange)range + withTyping:(BOOL)withTyping + withDirtyRange:(BOOL)withDirtyRange { + NSRange actualRange = [self actualUsedRange:range]; + NSString *markerFormat = + checked ? @"EnrichedCheckbox1" : @"EnrichedCheckbox0"; + NSTextList *checkboxMarker = + [[NSTextList alloc] initWithMarkerFormat:markerFormat options:0]; - 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]; - }]; + [self.input->textView.textStorage + enumerateAttribute:NSParagraphStyleAttributeName + inRange:actualRange + options:0 + usingBlock:^(id _Nullable value, NSRange range, + BOOL *_Nonnull stop) { + NSMutableParagraphStyle *pStyle = + [(NSParagraphStyle *)value mutableCopy]; + if (pStyle == nullptr) + return; + pStyle.textLists = @[ checkboxMarker ]; + [self.input->textView.textStorage + addAttribute:NSParagraphStyleAttributeName + value:pStyle + range:range]; + }]; + + if (withTyping) { + NSMutableDictionary *typingAttrs = + [self.input->textView.typingAttributes mutableCopy]; + NSMutableParagraphStyle *pStyle = + [typingAttrs[NSParagraphStyleAttributeName] mutableCopy]; + pStyle.textLists = @[ checkboxMarker ]; + typingAttrs[NSParagraphStyleAttributeName] = pStyle; + self.input->textView.typingAttributes = typingAttrs; } - [_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; -} - -- (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 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; - } + if (withDirtyRange) { + [self.input->attributesManager addDirtyRange:actualRange]; } - 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 && - [[NSCharacterSet newlineCharacterSet] - characterIsMember:[text characterAtIndex:text.length - 1]]) { - // do the replacement manually - [TextInsertionUtils replaceText:text - at:range - additionalAttributes:nullptr - input:_input - withSelection:YES]; - // apply checkbox attributes to the new paragraph - [self addAttributes:_input->textView.selectedRange withTypingAttr:YES]; - return YES; - } - return NO; +// 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)toggleCheckedAt:(NSUInteger)location { - if (location >= _input->textView.textStorage.length) { + if (location >= self.input->textView.textStorage.length) { return; } NSParagraphStyle *pStyle = - [_input->textView.textStorage attribute:NSParagraphStyleAttributeName - atIndex:location - effectiveRange:NULL]; + [self.input->textView.textStorage attribute:NSParagraphStyleAttributeName + atIndex:location + effectiveRange:NULL]; NSTextList *list = pStyle.textLists.firstObject; - BOOL isCurrentlyChecked = [list.markerFormat isEqualToString:@"{checkbox:1}"]; + BOOL isCurrentlyChecked = + [list.markerFormat isEqualToString:@"EnrichedCheckbox1"]; - NSString *fullText = _input->textView.textStorage.string; - NSRange paragraphRange = - [fullText paragraphRangeForRange:NSMakeRange(location, 0)]; + NSRange paragraphRange = [self.input->textView.textStorage.string + 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 = - [RangeUtils 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 range:(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: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]; - }]; + [self addWithChecked:!isCurrentlyChecked + range:paragraphRange + withTyping:YES + withDirtyRange:YES]; } - (BOOL)getCheckboxStateAt:(NSUInteger)location { - if (location >= _input->textView.textStorage.length) { + if (location >= self.input->textView.textStorage.length) { return NO; } NSParagraphStyle *style = - [_input->textView.textStorage attribute:NSParagraphStyleAttributeName - atIndex:location - effectiveRange:NULL]; + [self.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}"]) { + if ([list.markerFormat isEqualToString:@"EnrichedCheckbox1"]) { return YES; } } @@ -316,4 +161,24 @@ - (BOOL)getCheckboxStateAt:(NSUInteger)location { return NO; } +- (BOOL)handleNewlinesInRange:(NSRange)range replacementText:(NSString *)text { + 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.input + withSelection: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; +} + @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 From bdce71ac600c488fdaf3beda8653725804d91fb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20=C5=BB=C3=B3=C5=82kiewski?= Date: Fri, 20 Mar 2026 16:49:00 +0100 Subject: [PATCH 17/17] fix: applying inline styles inside paragraph styles --- ios/attributesManager/AttributesManager.mm | 19 ++++++++-- ios/interfaces/StyleBase.h | 6 ++- ios/interfaces/StyleBase.mm | 43 +++++++++++++--------- ios/styles/BlockQuoteStyle.mm | 6 +-- ios/styles/CheckboxListStyle.mm | 41 +++------------------ ios/styles/CodeBlockStyle.mm | 2 +- 6 files changed, 56 insertions(+), 61 deletions(-) diff --git a/ios/attributesManager/AttributesManager.mm b/ios/attributesManager/AttributesManager.mm index 46d39090f..4e233a07c 100644 --- a/ios/attributesManager/AttributesManager.mm +++ b/ios/attributesManager/AttributesManager.mm @@ -76,9 +76,22 @@ - (void)handleDirtyRangesStyling { [_input->textView.textStorage setAttributes:_input->defaultTypingAttributes range:dirtyRange]; - // then apply styling and re-apply meta-attribtues following the saved - // occurences - for (NSNumber *styleType in presentStyles) { + // 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; diff --git a/ios/interfaces/StyleBase.h b/ios/interfaces/StyleBase.h index a6b7b3ea0..a404b9f93 100644 --- a/ios/interfaces/StyleBase.h +++ b/ios/interfaces/StyleBase.h @@ -19,8 +19,12 @@ - (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)addTyping; +- (void)addTypingWithValue:(NSString *)value; - (void)removeTyping; - (BOOL)styleCondition:(id)value range:(NSRange)range; - (BOOL)detect:(NSRange)range; diff --git a/ios/interfaces/StyleBase.mm b/ios/interfaces/StyleBase.mm index 16e4d70da..0b1e1ec3e 100644 --- a/ios/interfaces/StyleBase.mm +++ b/ios/interfaces/StyleBase.mm @@ -56,42 +56,52 @@ - (void)toggle:(NSRange)range { isPresent ? [self remove:actualRange withDirtyRange:YES] : [self add:actualRange withTyping:YES withDirtyRange:YES]; } else { - isPresent ? [self removeTyping] : [self addTyping]; + 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:[self getValue] + value:value range:actualRange]; } else { [_input->textView.textStorage enumerateAttribute:NSParagraphStyleAttributeName inRange:actualRange options:0 - usingBlock:^(id _Nullable value, NSRange range, + usingBlock:^(id _Nullable existingValue, NSRange subRange, BOOL *_Nonnull stop) { NSMutableParagraphStyle *pStyle = - [(NSParagraphStyle *)value mutableCopy]; + [(NSParagraphStyle *)existingValue mutableCopy]; if (pStyle == nullptr) return; - pStyle.textLists = @[ [[NSTextList alloc] - initWithMarkerFormat:[self getValue] - options:0] ]; + pStyle.textLists = + @[ [[NSTextList alloc] initWithMarkerFormat:value + options:0] ]; [_input->textView.textStorage addAttribute:NSParagraphStyleAttributeName value:pStyle - range:range]; + range:subRange]; }]; // Only paragraph styles need additional typing attributes when toggling. if (withTyping) { - [self addTyping]; + [self addTypingWithValue:value]; } } @@ -112,17 +122,17 @@ - (void)remove:(NSRange)range withDirtyRange:(BOOL)withDirtyRange { enumerateAttribute:NSParagraphStyleAttributeName inRange:actualRange options:0 - usingBlock:^(id _Nullable value, NSRange range, + usingBlock:^(id _Nullable existingValue, NSRange subRange, BOOL *_Nonnull stop) { NSMutableParagraphStyle *pStyle = - [(NSParagraphStyle *)value mutableCopy]; + [(NSParagraphStyle *)existingValue mutableCopy]; if (pStyle == nullptr) return; pStyle.textLists = @[]; [_input->textView.textStorage addAttribute:NSParagraphStyleAttributeName value:pStyle - range:range]; + range:subRange]; }]; // Paragraph styles also need typing attributes removal. @@ -135,18 +145,17 @@ - (void)remove:(NSRange)range withDirtyRange:(BOOL)withDirtyRange { } } -- (void)addTyping { +- (void)addTypingWithValue:(NSString *)value { NSMutableDictionary *newTypingAttrs = [_input->textView.typingAttributes mutableCopy]; if (![self isParagraph]) { - newTypingAttrs[[self getKey]] = [self getValue]; + newTypingAttrs[[self getKey]] = value; } else { NSMutableParagraphStyle *pStyle = [newTypingAttrs[NSParagraphStyleAttributeName] mutableCopy]; - pStyle.textLists = - @[ [[NSTextList alloc] initWithMarkerFormat:[self getValue] - options:0] ]; + pStyle.textLists = @[ [[NSTextList alloc] initWithMarkerFormat:value + options:0] ]; newTypingAttrs[NSParagraphStyleAttributeName] = pStyle; } diff --git a/ios/styles/BlockQuoteStyle.mm b/ios/styles/BlockQuoteStyle.mm index ea28bbd0b..fc3b02896 100644 --- a/ios/styles/BlockQuoteStyle.mm +++ b/ios/styles/BlockQuoteStyle.mm @@ -45,14 +45,14 @@ - (void)applyStyling:(NSRange)range { UIColor *bqColor = [self.input->config blockquoteColor]; [self.input->textView.textStorage addAttribute:NSForegroundColorAttributeName value:bqColor - range:fullRange]; + range:range]; [self.input->textView.textStorage addAttribute:NSUnderlineColorAttributeName value:bqColor - range:fullRange]; + range:range]; [self.input->textView.textStorage addAttribute:NSStrikethroughColorAttributeName value:bqColor - range:fullRange]; + range:range]; } @end diff --git a/ios/styles/CheckboxListStyle.mm b/ios/styles/CheckboxListStyle.mm index f9caec0c4..ae7f740a4 100644 --- a/ios/styles/CheckboxListStyle.mm +++ b/ios/styles/CheckboxListStyle.mm @@ -68,42 +68,11 @@ - (void)addWithChecked:(BOOL)checked range:(NSRange)range withTyping:(BOOL)withTyping withDirtyRange:(BOOL)withDirtyRange { - NSRange actualRange = [self actualUsedRange:range]; - NSString *markerFormat = - checked ? @"EnrichedCheckbox1" : @"EnrichedCheckbox0"; - NSTextList *checkboxMarker = - [[NSTextList alloc] initWithMarkerFormat:markerFormat options:0]; - - [self.input->textView.textStorage - enumerateAttribute:NSParagraphStyleAttributeName - inRange:actualRange - options:0 - usingBlock:^(id _Nullable value, NSRange range, - BOOL *_Nonnull stop) { - NSMutableParagraphStyle *pStyle = - [(NSParagraphStyle *)value mutableCopy]; - if (pStyle == nullptr) - return; - pStyle.textLists = @[ checkboxMarker ]; - [self.input->textView.textStorage - addAttribute:NSParagraphStyleAttributeName - value:pStyle - range:range]; - }]; - - if (withTyping) { - NSMutableDictionary *typingAttrs = - [self.input->textView.typingAttributes mutableCopy]; - NSMutableParagraphStyle *pStyle = - [typingAttrs[NSParagraphStyleAttributeName] mutableCopy]; - pStyle.textLists = @[ checkboxMarker ]; - typingAttrs[NSParagraphStyleAttributeName] = pStyle; - self.input->textView.typingAttributes = typingAttrs; - } - - if (withDirtyRange) { - [self.input->attributesManager addDirtyRange:actualRange]; - } + NSString *value = checked ? @"EnrichedCheckbox1" : @"EnrichedCheckbox0"; + [self add:range + withValue:value + withTyping:withTyping + withDirtyRange:withDirtyRange]; } // During dirty range re-application the default add: would use getValue diff --git a/ios/styles/CodeBlockStyle.mm b/ios/styles/CodeBlockStyle.mm index b1cf91884..f4576b22d 100644 --- a/ios/styles/CodeBlockStyle.mm +++ b/ios/styles/CodeBlockStyle.mm @@ -46,7 +46,7 @@ - (void)applyStyling:(NSRange)range { [self.input->textView.textStorage addAttribute:NSForegroundColorAttributeName value:[self.input->config codeBlockFgColor] - range:fullRange]; + range:range]; } @end \ No newline at end of file