diff --git a/ios/EnrichedTextInputView.h b/ios/EnrichedTextInputView.h index 962cd2f9..d75b4adc 100644 --- a/ios/EnrichedTextInputView.h +++ b/ios/EnrichedTextInputView.h @@ -18,6 +18,8 @@ NS_ASSUME_NONNULL_BEGIN @public InputParser *parser; @public NSMutableDictionary *defaultTypingAttributes; @public NSDictionary> *stylesDict; + NSDictionary *> *conflictingStyles; + NSDictionary *> *blockingStyles; @public BOOL blockEmitting; } - (CGSize)measureSize:(CGFloat)maxWidth; @@ -25,6 +27,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)emitOnMentionEvent:(NSString *)indicator text:(nullable NSString *)text; - (void)anyTextMayHaveBeenModified; - (BOOL)handleStyleBlocksAndConflicts:(StyleType)type range:(NSRange)range; +- (NSArray *)getPresentStyleTypesFrom:(NSArray *)types range:(NSRange)range; @end NS_ASSUME_NONNULL_END diff --git a/ios/EnrichedTextInputView.mm b/ios/EnrichedTextInputView.mm index d588ab0f..3573f328 100644 --- a/ios/EnrichedTextInputView.mm +++ b/ios/EnrichedTextInputView.mm @@ -26,8 +26,6 @@ @implementation EnrichedTextInputView { EnrichedTextInputViewShadowNode::ConcreteState::Shared _state; int _componentViewHeightUpdateCounter; NSMutableSet *_activeStyles; - NSDictionary *> *_conflictingStyles; - NSDictionary *> *_blockingStyles; LinkData *_recentlyActiveLinkData; NSRange _recentlyActiveLinkRange; NSString *_recentlyEmittedString; @@ -95,10 +93,11 @@ - (void)setDefaults { @([H3Style getStyleType]): [[H3Style alloc] initWithInput:self], @([UnorderedListStyle getStyleType]): [[UnorderedListStyle alloc] initWithInput:self], @([OrderedListStyle getStyleType]): [[OrderedListStyle alloc] initWithInput:self], - @([BlockQuoteStyle getStyleType]): [[BlockQuoteStyle alloc] initWithInput:self] + @([BlockQuoteStyle getStyleType]): [[BlockQuoteStyle alloc] initWithInput:self], + @([CodeBlockStyle getStyleType]): [[CodeBlockStyle alloc] initWithInput:self] }; - _conflictingStyles = @{ + conflictingStyles = @{ @([BoldStyle getStyleType]) : @[], @([ItalicStyle getStyleType]) : @[], @([UnderlineStyle getStyleType]) : @[], @@ -106,28 +105,31 @@ - (void)setDefaults { @([InlineCodeStyle getStyleType]) : @[@([LinkStyle getStyleType]), @([MentionStyle getStyleType])], @([LinkStyle getStyleType]): @[@([InlineCodeStyle getStyleType]), @([LinkStyle getStyleType]), @([MentionStyle getStyleType])], @([MentionStyle getStyleType]): @[@([InlineCodeStyle getStyleType]), @([LinkStyle getStyleType])], - @([H1Style getStyleType]): @[@([H2Style getStyleType]), @([H3Style getStyleType]), @([UnorderedListStyle getStyleType]), @([OrderedListStyle getStyleType]), @([BlockQuoteStyle getStyleType])], - @([H2Style getStyleType]): @[@([H1Style getStyleType]), @([H3Style getStyleType]), @([UnorderedListStyle getStyleType]), @([OrderedListStyle getStyleType]), @([BlockQuoteStyle getStyleType])], - @([H3Style getStyleType]): @[@([H1Style getStyleType]), @([H2Style getStyleType]), @([UnorderedListStyle getStyleType]), @([OrderedListStyle getStyleType]), @([BlockQuoteStyle getStyleType])], - @([UnorderedListStyle getStyleType]): @[@([H1Style getStyleType]), @([H2Style getStyleType]), @([H3Style getStyleType]), @([OrderedListStyle getStyleType]), @([BlockQuoteStyle getStyleType])], - @([OrderedListStyle getStyleType]): @[@([H1Style getStyleType]), @([H2Style getStyleType]), @([H3Style getStyleType]), @([UnorderedListStyle getStyleType]), @([BlockQuoteStyle getStyleType])], - @([BlockQuoteStyle getStyleType]): @[@([H1Style getStyleType]), @([H2Style getStyleType]), @([H3Style getStyleType]), @([UnorderedListStyle getStyleType]), @([OrderedListStyle getStyleType])] + @([H1Style getStyleType]): @[@([H2Style getStyleType]), @([H3Style getStyleType]), @([UnorderedListStyle getStyleType]), @([OrderedListStyle getStyleType]), @([BlockQuoteStyle getStyleType]), @([CodeBlockStyle getStyleType])], + @([H2Style getStyleType]): @[@([H1Style getStyleType]), @([H3Style getStyleType]), @([UnorderedListStyle getStyleType]), @([OrderedListStyle getStyleType]), @([BlockQuoteStyle getStyleType]), @([CodeBlockStyle getStyleType])], + @([H3Style getStyleType]): @[@([H1Style getStyleType]), @([H2Style getStyleType]), @([UnorderedListStyle getStyleType]), @([OrderedListStyle getStyleType]), @([BlockQuoteStyle getStyleType]), @([CodeBlockStyle getStyleType])], + @([UnorderedListStyle getStyleType]): @[@([H1Style getStyleType]), @([H2Style getStyleType]), @([H3Style getStyleType]), @([OrderedListStyle getStyleType]), @([BlockQuoteStyle getStyleType]), @([CodeBlockStyle getStyleType])], + @([OrderedListStyle getStyleType]): @[@([H1Style getStyleType]), @([H2Style getStyleType]), @([H3Style getStyleType]), @([UnorderedListStyle getStyleType]), @([BlockQuoteStyle getStyleType]), @([CodeBlockStyle getStyleType])], + @([BlockQuoteStyle getStyleType]): @[@([H1Style getStyleType]), @([H2Style getStyleType]), @([H3Style getStyleType]), @([UnorderedListStyle getStyleType]), @([OrderedListStyle getStyleType]), @([CodeBlockStyle getStyleType])], + @([CodeBlockStyle getStyleType]): @[@([H1Style getStyleType]), @([H2Style getStyleType]), @([H3Style getStyleType]), + @([BoldStyle getStyleType]), @([ItalicStyle getStyleType]), @([UnderlineStyle getStyleType]), @([StrikethroughStyle getStyleType]), @([UnorderedListStyle getStyleType]), @([OrderedListStyle getStyleType]), @([BlockQuoteStyle getStyleType]), @([InlineCodeStyle getStyleType]), @([MentionStyle getStyleType]), @([LinkStyle getStyleType])] }; - _blockingStyles = @{ - @([BoldStyle getStyleType]) : @[], - @([ItalicStyle getStyleType]) : @[], - @([UnderlineStyle getStyleType]) : @[], - @([StrikethroughStyle getStyleType]) : @[], - @([InlineCodeStyle 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])], + @([LinkStyle getStyleType]): @[@([CodeBlockStyle getStyleType])], + @([MentionStyle getStyleType]): @[@([CodeBlockStyle getStyleType])], @([H1Style getStyleType]): @[], @([H2Style getStyleType]): @[], @([H3Style getStyleType]): @[], @([UnorderedListStyle getStyleType]): @[], @([OrderedListStyle getStyleType]): @[], @([BlockQuoteStyle getStyleType]): @[], + @([CodeBlockStyle getStyleType]): @[], }; parser = [[InputParser alloc] initWithInput:self]; @@ -347,6 +349,25 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const & } } + if(newViewProps.htmlStyle.codeblock.color != oldViewProps.htmlStyle.codeblock.color) { + if(isColorMeaningful(newViewProps.htmlStyle.codeblock.color)) { + [newConfig setCodeBlockFgColor:RCTUIColorFromSharedColor(newViewProps.htmlStyle.codeblock.color)]; + stylePropChanged = YES; + } + } + + if(newViewProps.htmlStyle.codeblock.backgroundColor != oldViewProps.htmlStyle.codeblock.backgroundColor) { + if(isColorMeaningful(newViewProps.htmlStyle.codeblock.backgroundColor)) { + [newConfig setCodeBlockBgColor:RCTUIColorFromSharedColor(newViewProps.htmlStyle.codeblock.backgroundColor)]; + stylePropChanged = YES; + } + } + + if(newViewProps.htmlStyle.codeblock.borderRadius != oldViewProps.htmlStyle.codeblock.borderRadius) { + [newConfig setCodeBlockBorderRadius:newViewProps.htmlStyle.codeblock.borderRadius]; + stylePropChanged = YES; + } + if(newViewProps.htmlStyle.a.textDecorationLine != oldViewProps.htmlStyle.a.textDecorationLine) { NSString *objcString = [NSString fromCppString:newViewProps.htmlStyle.a.textDecorationLine]; if([objcString isEqualToString:DecorationUnderline]) { @@ -512,9 +533,8 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const & _emitHtml = newViewProps.isOnChangeHtmlSet; [super updateProps:props oldProps:oldProps]; - // mandatory text and height checks + // run the changes callback [self anyTextMayHaveBeenModified]; - [self tryUpdatingHeight]; // autofocus - needs to be done at the very end if(isFirstMount && newViewProps.autoFocus) { @@ -702,7 +722,7 @@ - (void)tryUpdatingActiveStyles { .isUnorderedList = [_activeStyles containsObject: @([UnorderedListStyle getStyleType])], .isOrderedList = [_activeStyles containsObject: @([OrderedListStyle getStyleType])], .isBlockQuote = [_activeStyles containsObject: @([BlockQuoteStyle getStyleType])], - .isCodeBlock = NO, // [_activeStyles containsObject: @([CodeBlockStyle getStyleType])], + .isCodeBlock = [_activeStyles containsObject: @([CodeBlockStyle getStyleType])], .isImage = NO // [_activeStyles containsObject: @([ImageStyle getStyleType]])], }); } @@ -771,6 +791,8 @@ - (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args { [self toggleParagraphStyle:[OrderedListStyle getStyleType]]; } else if([commandName isEqualToString:@"toggleBlockQuote"]) { [self toggleParagraphStyle:[BlockQuoteStyle getStyleType]]; + } else if([commandName isEqualToString:@"toggleCodeBlock"]) { + [self toggleParagraphStyle:[CodeBlockStyle getStyleType]]; } } @@ -930,13 +952,13 @@ - (void)startMentionWithIndicator:(NSString *)indicator { // returns false when style shouldn't be applied and true when it can be - (BOOL)handleStyleBlocksAndConflicts:(StyleType)type range:(NSRange)range { // handle blocking styles: if any is present we do not apply the toggled style - NSArray *blocking = [self getPresentStyleTypesFrom: _blockingStyles[@(type)] range:range]; + NSArray *blocking = [self getPresentStyleTypesFrom: blockingStyles[@(type)] range:range]; if(blocking.count != 0) { return NO; } // handle conflicting styles: all of their occurences have to be removed - NSArray *conflicting = [self getPresentStyleTypesFrom: _conflictingStyles[@(type)] range:range]; + NSArray *conflicting = [self getPresentStyleTypesFrom: conflictingStyles[@(type)] range:range]; if(conflicting.count != 0) { for(NSNumber *style in conflicting) { id styleClass = stylesDict[style]; @@ -1012,6 +1034,7 @@ - (void)manageSelectionBasedChanges { - (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]; @@ -1046,6 +1069,12 @@ - (void)anyTextMayHaveBeenModified { [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])]; @@ -1171,6 +1200,7 @@ - (bool)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range r UnorderedListStyle *uStyle = stylesDict[@([UnorderedListStyle getStyleType])]; OrderedListStyle *oStyle = stylesDict[@([OrderedListStyle 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])]; @@ -1186,13 +1216,19 @@ - (bool)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range r [oStyle handleBackspaceInRange:range replacementText:text] || [oStyle tryHandlingListShorcutInRange: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] || [ZeroWidthSpaceUtils handleBackspaceInRange:range replacementText:text input:self] || - [ParagraphAttributesUtils handleBackspaceInRange:range replacementText:text input:self] + [ParagraphAttributesUtils handleBackspaceInRange: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 handleNewlineBackspaceInRange:range replacementText:text input:self] ) { [self anyTextMayHaveBeenModified]; return NO; diff --git a/ios/config/InputConfig.h b/ios/config/InputConfig.h index cd12910c..4778807c 100644 --- a/ios/config/InputConfig.h +++ b/ios/config/InputConfig.h @@ -64,4 +64,10 @@ - (void)setLinkDecorationLine:(TextDecorationLineEnum)newValue; - (void)setMentionStyleProps:(NSDictionary *)newValue; - (MentionStyleProps *)mentionStylePropsForIndicator:(NSString *)indicator; +- (UIColor *)codeBlockFgColor; +- (void)setCodeBlockFgColor:(UIColor *)newValue; +- (UIColor *)codeBlockBgColor; +- (void)setCodeBlockBgColor:(UIColor *)newValue; +- (CGFloat)codeBlockBorderRadius; +- (void)setCodeBlockBorderRadius:(CGFloat)newValue; @end diff --git a/ios/config/InputConfig.mm b/ios/config/InputConfig.mm index 00f0903c..10dbff10 100644 --- a/ios/config/InputConfig.mm +++ b/ios/config/InputConfig.mm @@ -36,6 +36,9 @@ @implementation InputConfig { UIColor *_linkColor; TextDecorationLineEnum _linkDecorationLine; NSDictionary *_mentionProperties; + UIColor *_codeBlockFgColor; + CGFloat _codeBlockBorderRadius; + UIColor *_codeBlockBgColor; } - (instancetype) init { @@ -79,6 +82,9 @@ - (id)copyWithZone:(NSZone *)zone { copy->_linkColor = [_linkColor copy]; copy->_linkDecorationLine = [_linkDecorationLine copy]; copy->_mentionProperties = [_mentionProperties mutableCopy]; + copy->_codeBlockFgColor = [_codeBlockFgColor copy]; + copy->_codeBlockBgColor = [_codeBlockBgColor copy]; + copy->_codeBlockBorderRadius = _codeBlockBorderRadius; return copy; } @@ -379,4 +385,28 @@ - (MentionStyleProps *)mentionStylePropsForIndicator:(NSString *)indicator { return fallbackProps; } +- (UIColor *)codeBlockFgColor { + return _codeBlockFgColor; +} + +- (void)setCodeBlockFgColor:(UIColor *)newValue { + _codeBlockFgColor = newValue; +} + +- (UIColor *)codeBlockBgColor { + return _codeBlockBgColor; +} + +- (void)setCodeBlockBgColor:(UIColor *)newValue { + _codeBlockBgColor = newValue; +} + +- (CGFloat)codeBlockBorderRadius { + return _codeBlockBorderRadius; +} + +- (void)setCodeBlockBorderRadius:(CGFloat)newValue { + _codeBlockBorderRadius = newValue; +} + @end diff --git a/ios/inputParser/InputParser.mm b/ios/inputParser/InputParser.mm index 17b06e8d..ce2c3b0c 100644 --- a/ios/inputParser/InputParser.mm +++ b/ios/inputParser/InputParser.mm @@ -29,6 +29,7 @@ - (NSString *)parseToHtmlFromRange:(NSRange)range { BOOL inUnorderedList = NO; BOOL inOrderedList = NO; BOOL inBlockQuote = NO; + BOOL inCodeBlock = NO; unichar lastCharacter = 0; for(int i = 0; i < text.length; i++) { @@ -95,7 +96,8 @@ - (NSString *)parseToHtmlFromRange:(NSRange)range { [previousActiveStyles containsObject:@([H1Style getStyleType])] || [previousActiveStyles containsObject:@([H2Style getStyleType])] || [previousActiveStyles containsObject:@([H3Style getStyleType])] || - [previousActiveStyles containsObject:@([BlockQuoteStyle getStyleType])] + [previousActiveStyles containsObject:@([BlockQuoteStyle getStyleType])] || + [previousActiveStyles containsObject:@([CodeBlockStyle getStyleType])] ) { // do nothing, proper closing paragraph tags have been already appended } else { @@ -128,6 +130,11 @@ - (NSString *)parseToHtmlFromRange:(NSRange)range { inBlockQuote = NO; [result appendString:@"\n"]; } + // handle ending codeblock + if(inCodeBlock && ![currentActiveStyles containsObject:@([CodeBlockStyle getStyleType])]) { + inCodeBlock = NO; + [result appendString:@"\n"]; + } // handle starting unordered list if(!inUnorderedList && [currentActiveStyles containsObject:@([UnorderedListStyle getStyleType])]) { @@ -144,6 +151,11 @@ - (NSString *)parseToHtmlFromRange:(NSRange)range { inBlockQuote = YES; [result appendString:@"\n
"]; } + // handle starting codeblock + if(!inCodeBlock && [currentActiveStyles containsObject:@([CodeBlockStyle getStyleType])]) { + inCodeBlock = YES; + [result appendString:@"\n"]; + } // don't add the

tag if some paragraph styles are present if([currentActiveStyles containsObject:@([UnorderedListStyle getStyleType])] || @@ -151,7 +163,8 @@ - (NSString *)parseToHtmlFromRange:(NSRange)range { [currentActiveStyles containsObject:@([H1Style getStyleType])] || [currentActiveStyles containsObject:@([H2Style getStyleType])] || [currentActiveStyles containsObject:@([H3Style getStyleType])] || - [currentActiveStyles containsObject:@([BlockQuoteStyle getStyleType])] + [currentActiveStyles containsObject:@([BlockQuoteStyle getStyleType])] || + [currentActiveStyles containsObject:@([CodeBlockStyle getStyleType])] ) { [result appendString:@"\n"]; } else { @@ -235,6 +248,8 @@ - (NSString *)parseToHtmlFromRange:(NSRange)range { [result appendString:@"\n"]; } else if([previousActiveStyles containsObject:@([BlockQuoteStyle getStyleType])]) { [result appendString:@"\n

"]; + } else if([previousActiveStyles containsObject:@([CodeBlockStyle getStyleType])]) { + [result appendString:@"\n"]; } else if( [previousActiveStyles containsObject:@([H1Style getStyleType])] || [previousActiveStyles containsObject:@([H2Style getStyleType])] || @@ -258,6 +273,10 @@ - (NSString *)parseToHtmlFromRange:(NSRange)range { inBlockQuote = NO; [result appendString:@"\n"]; } + if(inCodeBlock) { + inCodeBlock = NO; + [result appendString:@"\n"]; + } } [result appendString: @"\n"]; @@ -328,8 +347,8 @@ - (NSString *)tagContentForStyle:(NSNumber *)style openingTag:(BOOL)openingTag l return @"h3"; } else if([style isEqualToNumber:@([UnorderedListStyle getStyleType])] || [style isEqualToNumber:@([OrderedListStyle getStyleType])]) { return @"li"; - } else if([style isEqualToNumber:@([BlockQuoteStyle getStyleType])]) { - // blockquotes use

tags the same way lists use

  • + } else if([style isEqualToNumber:@([BlockQuoteStyle getStyleType])] || [style isEqualToNumber:@([CodeBlockStyle getStyleType])]) { + // blockquotes and codeblock use

    tags the same way lists use

  • return @"p"; } return @""; @@ -447,6 +466,8 @@ - (NSString * _Nullable)initiallyProcessHtml:(NSString * _Nonnull)html { fixedHtml = [self stringByAddingNewlinesToTag:@"" inString:fixedHtml leading:YES trailing:YES]; fixedHtml = [self stringByAddingNewlinesToTag:@"
    " inString:fixedHtml leading:YES trailing:YES]; fixedHtml = [self stringByAddingNewlinesToTag:@"
    " inString:fixedHtml leading:YES trailing:YES]; + fixedHtml = [self stringByAddingNewlinesToTag:@"" inString:fixedHtml leading:YES trailing:YES]; + fixedHtml = [self stringByAddingNewlinesToTag:@"" inString:fixedHtml leading:YES trailing:YES]; // line opening tags fixedHtml = [self stringByAddingNewlinesToTag:@"

    " inString:fixedHtml leading:YES trailing:NO]; @@ -520,14 +541,14 @@ - (NSArray *)getTextAndStylesFromHtml:(NSString *)fixedHtml { ongoingTags[currentTagName] = tagArr; // skip one newline after opening tags that are in separate lines intentionally - if([currentTagName isEqualToString:@"ul"] || [currentTagName isEqualToString:@"ol"] || [currentTagName isEqualToString:@"blockquote"]) { + if([currentTagName isEqualToString:@"ul"] || [currentTagName isEqualToString:@"ol"] || [currentTagName isEqualToString:@"blockquote"] || [currentTagName isEqualToString:@"codeblock"]) { i += 1; } } else { // we finish closing tags - pack tag name, tag range and optionally tag params into an entry that goes inside initiallyProcessedTags // skip one newline that was added before some closing tags that are in separate lines - if([currentTagName isEqualToString:@"ul"] || [currentTagName isEqualToString:@"ol"] || [currentTagName isEqualToString:@"blockquote"]) { + if([currentTagName isEqualToString:@"ul"] || [currentTagName isEqualToString:@"ol"] || [currentTagName isEqualToString:@"blockquote"] || [currentTagName isEqualToString:@"codeblock"]) { plainText = [[plainText substringWithRange: NSMakeRange(0, plainText.length - 1)] mutableCopy]; } @@ -668,6 +689,8 @@ - (NSArray *)getTextAndStylesFromHtml:(NSString *)fixedHtml { [styleArr addObject:@([OrderedListStyle getStyleType])]; } else if([tagName isEqualToString:@"blockquote"]) { [styleArr addObject:@([BlockQuoteStyle getStyleType])]; + } else if([tagName isEqualToString:@"codeblock"]) { + [styleArr addObject:@([CodeBlockStyle getStyleType])]; } else { // some other external tags like span just don't get put into the processed styles continue; diff --git a/ios/styles/BlockQuoteStyle.mm b/ios/styles/BlockQuoteStyle.mm index 4c04cbb1..48f33036 100644 --- a/ios/styles/BlockQuoteStyle.mm +++ b/ios/styles/BlockQuoteStyle.mm @@ -7,13 +7,17 @@ @implementation BlockQuoteStyle { EnrichedTextInputView *_input; + NSArray *_stylesToExclude; } + (StyleType)getStyleType { return BlockQuote; } ++ (BOOL)isParagraphStyle { return YES; } + - (instancetype)initWithInput:(id)input { self = [super init]; _input = (EnrichedTextInputView *)input; + _stylesToExclude = @[ @(InlineCode), @(Mention), @(Link) ]; return self; } @@ -175,31 +179,6 @@ - (BOOL)anyOccurence:(NSRange)range { ]; } -// gets ranges that aren't link, mention or inline code -- (NSArray *)getProperColorRangesIn:(NSRange)range { - LinkStyle *linkStyle = _input->stylesDict[@([LinkStyle getStyleType])]; - MentionStyle *mentionStyle = _input->stylesDict[@([MentionStyle getStyleType])]; - InlineCodeStyle *codeStyle = _input->stylesDict[@([InlineCodeStyle getStyleType])]; - - NSMutableArray *newRanges = [[NSMutableArray alloc] init]; - int lastRangeLocation = range.location; - - for(int i = range.location; i < range.location + range.length; i++) { - NSRange currentRange = NSMakeRange(i, 1); - if([linkStyle detectStyle:currentRange] || [mentionStyle detectStyle:currentRange] || [codeStyle detectStyle:currentRange]) { - if(i - lastRangeLocation > 0) { - [newRanges addObject:[NSValue valueWithRange:NSMakeRange(lastRangeLocation, i - lastRangeLocation)]]; - } - lastRangeLocation = i+1; - } - } - if(lastRangeLocation < range.location + range.length) { - [newRanges addObject:[NSValue valueWithRange:NSMakeRange(lastRangeLocation, range.location + range.length - lastRangeLocation)]]; - } - - return newRanges; -} - // general checkup correcting blockquote color // since links, mentions and inline code affects coloring, the checkup gets done only outside of them - (void)manageBlockquoteColor { @@ -212,7 +191,7 @@ - (void)manageBlockquoteColor { NSArray *paragraphs = [ParagraphsUtils getSeparateParagraphsRangesIn:_input->textView range:wholeRange]; for(NSValue *pValue in paragraphs) { NSRange paragraphRange = [pValue rangeValue]; - NSArray *properRanges = [self getProperColorRangesIn:paragraphRange]; + NSArray *properRanges = [OccurenceUtils getRangesWithout:_stylesToExclude withInput:_input inRange:paragraphRange]; for(NSValue *value in properRanges) { NSRange currRange = [value rangeValue]; diff --git a/ios/styles/BoldStyle.mm b/ios/styles/BoldStyle.mm index 2dd50739..78b80b4a 100644 --- a/ios/styles/BoldStyle.mm +++ b/ios/styles/BoldStyle.mm @@ -9,6 +9,8 @@ @implementation BoldStyle { + (StyleType)getStyleType { return Bold; } ++ (BOOL)isParagraphStyle { return NO; } + - (instancetype)initWithInput:(id)input { self = [super init]; _input = (EnrichedTextInputView *)input; diff --git a/ios/styles/CodeBlockStyle.mm b/ios/styles/CodeBlockStyle.mm new file mode 100644 index 00000000..cf1969d7 --- /dev/null +++ b/ios/styles/CodeBlockStyle.mm @@ -0,0 +1,228 @@ +#import "StyleHeaders.h" +#import "EnrichedTextInputView.h" +#import "FontExtension.h" +#import "OccurenceUtils.h" +#import "ParagraphsUtils.h" +#import "TextInsertionUtils.h" +#import "ColorExtension.h" + +@implementation CodeBlockStyle { + EnrichedTextInputView *_input; + NSArray *_stylesToExclude; +} + ++ (StyleType)getStyleType { 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]; + } else { + isStylePresent ? [self removeTypingAttributes] : [self addTypingAttributes]; + } +} + +- (void)addAttributes:(NSRange)range { + NSTextList *codeBlockList = [[NSTextList alloc] initWithMarkerFormat:@"codeblock" options:0]; + NSArray *paragraphs = [ParagraphsUtils getSeparateParagraphsRangesIn:_input->textView range:range]; + // if we fill empty lines with zero width spaces, we need to offset later ranges + NSInteger offset = 0; + NSRange preModificationRange = _input->textView.selectedRange; + + // to not emit any space filling selection/text changes + _input->blockEmitting = YES; + + for (NSValue *value in paragraphs) { + NSRange pRange = NSMakeRange([value rangeValue].location + offset, [value rangeValue].length); + // length 0 with first line, length 1 and newline with some empty lines in the middle + if(pRange.length == 0 || + (pRange.length == 1 && + [[NSCharacterSet newlineCharacterSet] characterIsMember: [_input->textView.textStorage.string characterAtIndex:pRange.location]]) + ) { + [TextInsertionUtils insertText:@"\u200B" at:pRange.location additionalAttributes:nullptr input:_input withSelection:NO]; + pRange = NSMakeRange(pRange.location, pRange.length + 1); + offset += 1; + } + + [_input->textView.textStorage enumerateAttribute:NSParagraphStyleAttributeName inRange:pRange options:0 + usingBlock:^(id _Nullable value, NSRange range, BOOL * _Nonnull stop) { + NSMutableParagraphStyle *pStyle = [(NSParagraphStyle *)value mutableCopy]; + pStyle.textLists = @[codeBlockList]; + [_input->textView.textStorage addAttribute:NSParagraphStyleAttributeName value:pStyle range:range]; + } + ]; + } + + // back to emitting + _input->blockEmitting = NO; + + if(preModificationRange.length == 0) { + // fix selection if only one line was possibly made a list and filled with a space + _input->textView.selectedRange = preModificationRange; + } else { + // in other cases, fix the selection with newly made offsets + _input->textView.selectedRange = NSMakeRange(preModificationRange.location, preModificationRange.length + offset); + } + + // also add typing attributes + 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]; +} + +- (void)removeAttributes:(NSRange)range { + NSArray *paragraphs = [ParagraphsUtils getSeparateParagraphsRangesIn:_input->textView range:range]; + + [_input->textView.textStorage beginEditing]; + + for(NSValue *value in paragraphs) { + NSRange pRange = [value rangeValue]; + + [_input->textView.textStorage enumerateAttribute:NSParagraphStyleAttributeName inRange:pRange options:0 + usingBlock:^(id _Nullable value, NSRange range, BOOL * _Nonnull stop) { + NSMutableParagraphStyle *pStyle = [(NSParagraphStyle *)value mutableCopy]; + pStyle.textLists = @[]; + [_input->textView.textStorage addAttribute:NSParagraphStyleAttributeName value:pStyle range:range]; + } + ]; + } + + [_input->textView.textStorage endEditing]; + + // also remove typing attributes + NSMutableDictionary *typingAttrs = [_input->textView.typingAttributes mutableCopy]; + NSMutableParagraphStyle *pStyle = [typingAttrs[NSParagraphStyleAttributeName] mutableCopy]; + pStyle.textLists = @[]; + + typingAttrs[NSParagraphStyleAttributeName] = pStyle; + + _input->textView.typingAttributes = typingAttrs; +} + +- (void)removeTypingAttributes { + [self removeAttributes:_input->textView.selectedRange]; +} + +- (BOOL)handleBackspaceInRange:(NSRange)range replacementText:(NSString *)text { + if([self detectStyle:_input->textView.selectedRange] && text.length == 0) { + // backspace while the style is active + + NSRange paragraphRange = [_input->textView.textStorage.string paragraphRangeForRange:_input->textView.selectedRange]; + + if(NSEqualRanges(_input->textView.selectedRange, NSMakeRange(0, 0))) { + // a backspace on the very first input's line quote + // it doesn't run textVieDidChange so we need to manually remove attributes + [self removeAttributes:paragraphRange]; + return YES; + } + } + return NO; +} + +- (BOOL)styleCondition:(id _Nullable)value :(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]; + } + ]; + } else { + return [OccurenceUtils detect:NSParagraphStyleAttributeName withInput:_input atIndex:range.location checkPrevious:YES + withCondition:^BOOL(id _Nullable value, NSRange range) { + return [self styleCondition:value :range]; + } + ]; + } +} + +- (BOOL)anyOccurence:(NSRange)range { + return [OccurenceUtils any:NSParagraphStyleAttributeName withInput:_input inRange:range + withCondition:^BOOL(id _Nullable value, NSRange range) { + return [self styleCondition:value :range]; + } + ]; +} + +- (NSArray *_Nullable)findAllOccurences:(NSRange)range { + return [OccurenceUtils all:NSParagraphStyleAttributeName withInput:_input inRange:range + withCondition:^BOOL(id _Nullable value, NSRange range) { + return [self styleCondition:value :range]; + } + ]; +} + +- (void)manageCodeBlockFontAndColor { + if([[_input->config codeBlockFgColor] isEqualToColor:[_input->config primaryColor]]) { + return; + } + + NSRange wholeRange = NSMakeRange(0, _input->textView.textStorage.string.length); + NSArray *paragraphs = [ParagraphsUtils getSeparateParagraphsRangesIn:_input->textView range:wholeRange]; + + for(NSValue *pValue in paragraphs) { + NSRange paragraphRange = [pValue rangeValue]; + NSArray *properRanges = [OccurenceUtils getRangesWithout:_stylesToExclude withInput:_input inRange:paragraphRange]; + + for(NSValue *value in properRanges) { + NSRange currRange = [value rangeValue]; + BOOL selfDetected = [self detectStyle:currRange]; + + [_input->textView.textStorage enumerateAttribute:NSFontAttributeName inRange:currRange options:0 + usingBlock:^(id _Nullable value, NSRange range, BOOL * _Nonnull stop) { + UIFont *currentFont = (UIFont *)value; + UIFont *newFont = nullptr; + + BOOL isCodeFont = [[currentFont familyName] isEqualToString:[[_input->config monospacedFont] familyName]]; + + if (isCodeFont && !selfDetected) { + newFont = [[[_input->config primaryFont] withFontTraits:currentFont] setSize:currentFont.pointSize]; + } else if (!isCodeFont && selfDetected) { + newFont = [[[_input->config monospacedFont] withFontTraits:currentFont] setSize:currentFont.pointSize]; + } + + if (newFont != nullptr) { + [_input->textView.textStorage addAttribute:NSFontAttributeName value:newFont range:range]; + } + }]; + + [_input->textView.textStorage enumerateAttribute:NSForegroundColorAttributeName inRange:currRange options:0 + usingBlock:^(id _Nullable value, NSRange range, BOOL * _Nonnull stop) { + UIColor *newColor = nullptr; + BOOL colorApplied = [(UIColor *)value isEqualToColor:[_input->config codeBlockFgColor]]; + + if(colorApplied && !selfDetected) { + newColor = [_input->config primaryColor]; + } else if(!colorApplied && selfDetected) { + newColor = [_input->config codeBlockFgColor]; + } + + if(newColor != nullptr) { + [_input->textView.textStorage addAttribute:NSForegroundColorAttributeName value:newColor range:range]; + } + }]; + } + } +} + +@end diff --git a/ios/styles/H1Style.mm b/ios/styles/H1Style.mm index 9a8c4ded..0b5add38 100644 --- a/ios/styles/H1Style.mm +++ b/ios/styles/H1Style.mm @@ -3,6 +3,7 @@ @implementation H1Style + (StyleType)getStyleType { return H1; } ++ (BOOL)isParagraphStyle { return YES; } - (CGFloat)getHeadingFontSize { return [((EnrichedTextInputView *)input)->config h1FontSize]; } - (BOOL)isHeadingBold { return [((EnrichedTextInputView *)input)->config h1Bold]; diff --git a/ios/styles/H2Style.mm b/ios/styles/H2Style.mm index 1913c2f3..158634b4 100644 --- a/ios/styles/H2Style.mm +++ b/ios/styles/H2Style.mm @@ -3,6 +3,7 @@ @implementation H2Style + (StyleType)getStyleType { return H2; } ++ (BOOL)isParagraphStyle { return YES; } - (CGFloat)getHeadingFontSize { return [((EnrichedTextInputView *)input)->config h2FontSize]; } - (BOOL)isHeadingBold { return [((EnrichedTextInputView *)input)->config h2Bold]; diff --git a/ios/styles/H3Style.mm b/ios/styles/H3Style.mm index f7b55bc7..49ffefb1 100644 --- a/ios/styles/H3Style.mm +++ b/ios/styles/H3Style.mm @@ -3,6 +3,7 @@ @implementation H3Style + (StyleType)getStyleType { return H3; } ++ (BOOL)isParagraphStyle { return YES; } - (CGFloat)getHeadingFontSize { return [((EnrichedTextInputView *)input)->config h3FontSize]; } - (BOOL)isHeadingBold { return [((EnrichedTextInputView *)input)->config h3Bold]; diff --git a/ios/styles/InlineCodeStyle.mm b/ios/styles/InlineCodeStyle.mm index 94d8ad00..cb6a0b59 100644 --- a/ios/styles/InlineCodeStyle.mm +++ b/ios/styles/InlineCodeStyle.mm @@ -11,6 +11,8 @@ @implementation InlineCodeStyle { + (StyleType)getStyleType { return InlineCode; } ++ (BOOL)isParagraphStyle { return NO; } + - (instancetype)initWithInput:(id)input { self = [super init]; _input = (EnrichedTextInputView *)input; diff --git a/ios/styles/ItalicStyle.mm b/ios/styles/ItalicStyle.mm index 6ec4debc..0640689d 100644 --- a/ios/styles/ItalicStyle.mm +++ b/ios/styles/ItalicStyle.mm @@ -9,6 +9,8 @@ @implementation ItalicStyle { + (StyleType)getStyleType { return Italic; } ++ (BOOL)isParagraphStyle { return NO; } + - (instancetype)initWithInput:(id)input { self = [super init]; _input = (EnrichedTextInputView *)input; diff --git a/ios/styles/LinkStyle.mm b/ios/styles/LinkStyle.mm index 69e8a2d2..208911f5 100644 --- a/ios/styles/LinkStyle.mm +++ b/ios/styles/LinkStyle.mm @@ -15,6 +15,8 @@ @implementation LinkStyle { + (StyleType)getStyleType { return Link; } ++ (BOOL)isParagraphStyle { return NO; } + - (instancetype)initWithInput:(id)input { self = [super init]; _input = (EnrichedTextInputView *)input; @@ -280,6 +282,7 @@ - (void)manageLinkTypingAttributes { - (void)handleAutomaticLinks:(NSString *)word inRange:(NSRange)wordRange { InlineCodeStyle *inlineCodeStyle = [_input->stylesDict objectForKey:@([InlineCodeStyle getStyleType])]; MentionStyle *mentionStyle = [_input->stylesDict objectForKey:@([MentionStyle getStyleType])]; + CodeBlockStyle *codeBlockStyle = [_input->stylesDict objectForKey:@([CodeBlockStyle getStyleType])]; if (inlineCodeStyle == nullptr || mentionStyle == nullptr) { return; @@ -295,6 +298,11 @@ - (void)handleAutomaticLinks:(NSString *)word inRange:(NSRange)wordRange { return; } + // we don't recognize links in codeblocks + if ([codeBlockStyle anyOccurence:wordRange]) { + return; + } + // remove connected different links [self removeConnectedLinksIfNeeded:word range:wordRange]; diff --git a/ios/styles/MentionStyle.mm b/ios/styles/MentionStyle.mm index f0ec496a..906e3ebf 100644 --- a/ios/styles/MentionStyle.mm +++ b/ios/styles/MentionStyle.mm @@ -18,6 +18,8 @@ @implementation MentionStyle { + (StyleType)getStyleType { return Mention; } ++ (BOOL)isParagraphStyle { return NO; } + - (instancetype)initWithInput:(id)input { self = [super init]; _input = (EnrichedTextInputView *)input; @@ -327,16 +329,22 @@ - (void)manageMentionEditing { return; } - // get conflicting style classes - LinkStyle* linkStyle = [_input->stylesDict objectForKey:@([LinkStyle getStyleType])]; - InlineCodeStyle* inlineCodeStyle = [_input->stylesDict objectForKey:@([InlineCodeStyle getStyleType])]; - if(linkStyle == nullptr || inlineCodeStyle == nullptr) { - [self removeActiveMentionRange]; - return; + // get style classes that the mention shouldn't be recognized in + NSArray *conflicts = _input->conflictingStyles[@([MentionStyle getStyleType])]; + NSArray *blocks = _input->blockingStyles[@([MentionStyle getStyleType])]; + NSArray *allConflicts = [conflicts arrayByAddingObjectsFromArray:blocks]; + BOOL conflictingStyle = NO; + + for(NSNumber *styleType in allConflicts) { + id styleClass = _input->stylesDict[styleType]; + if(styleClass != nullptr && [styleClass anyOccurence:wordRange]) { + conflictingStyle = YES; + break; + } } - // if there is any sign of conflicting style classes, stop editing a mention - if([linkStyle anyOccurence:wordRange] || [inlineCodeStyle anyOccurence:wordRange]) { + // if any of the conflicting styles were present, don't edit the mention + if(conflictingStyle) { [self removeActiveMentionRange]; return; } diff --git a/ios/styles/OrderedListStyle.mm b/ios/styles/OrderedListStyle.mm index c4234a45..63e0fbf5 100644 --- a/ios/styles/OrderedListStyle.mm +++ b/ios/styles/OrderedListStyle.mm @@ -11,6 +11,8 @@ @implementation OrderedListStyle { + (StyleType)getStyleType { return OrderedList; } ++ (BOOL)isParagraphStyle { return YES; } + - (CGFloat)getHeadIndent { // lists are drawn manually // margin before marker + gap between marker and paragraph diff --git a/ios/styles/StrikethroughStyle.mm b/ios/styles/StrikethroughStyle.mm index 82932384..8ff74fd2 100644 --- a/ios/styles/StrikethroughStyle.mm +++ b/ios/styles/StrikethroughStyle.mm @@ -8,6 +8,8 @@ @implementation StrikethroughStyle { + (StyleType)getStyleType { return Strikethrough; } ++ (BOOL)isParagraphStyle { return NO; } + - (instancetype)initWithInput:(id)input { self = [super init]; _input = (EnrichedTextInputView *)input; diff --git a/ios/styles/UnderlineStyle.mm b/ios/styles/UnderlineStyle.mm index f36aa089..933ff897 100644 --- a/ios/styles/UnderlineStyle.mm +++ b/ios/styles/UnderlineStyle.mm @@ -8,6 +8,8 @@ @implementation UnderlineStyle { + (StyleType)getStyleType { return Underline; } ++ (BOOL)isParagraphStyle { return NO; } + - (instancetype)initWithInput:(id)input { self = [super init]; _input = (EnrichedTextInputView *)input; diff --git a/ios/styles/UnorderedListStyle.mm b/ios/styles/UnorderedListStyle.mm index c550b5e4..10991a85 100644 --- a/ios/styles/UnorderedListStyle.mm +++ b/ios/styles/UnorderedListStyle.mm @@ -11,6 +11,8 @@ @implementation UnorderedListStyle { + (StyleType)getStyleType { return UnorderedList; } ++ (BOOL)isParagraphStyle { return YES; } + - (CGFloat)getHeadIndent { // lists are drawn manually // margin before bullet + gap between bullet and paragraph diff --git a/ios/utils/BaseStyleProtocol.h b/ios/utils/BaseStyleProtocol.h index 82e59886..1685773b 100644 --- a/ios/utils/BaseStyleProtocol.h +++ b/ios/utils/BaseStyleProtocol.h @@ -4,6 +4,7 @@ @protocol BaseStyleProtocol + (StyleType)getStyleType; ++ (BOOL)isParagraphStyle; - (instancetype _Nonnull)initWithInput:(id _Nonnull)input; - (void)applyStyle:(NSRange)range; - (void)addAttributes:(NSRange)range; diff --git a/ios/utils/LayoutManagerExtension.mm b/ios/utils/LayoutManagerExtension.mm index 26a9cbcd..2218dc53 100644 --- a/ios/utils/LayoutManagerExtension.mm +++ b/ios/utils/LayoutManagerExtension.mm @@ -3,6 +3,7 @@ #import "EnrichedTextInputView.h" #import "StyleHeaders.h" #import "ParagraphsUtils.h" +#import "ColorExtension.h" @implementation NSLayoutManager (LayoutManagerExtension) @@ -52,11 +53,122 @@ - (void)my_drawBackgroundForGlyphRange:(NSRange)glyphRange atPoint:(CGPoint)orig EnrichedTextInputView *typedInput = (EnrichedTextInputView *)self.input; if(typedInput == nullptr) { return; } + NSRange inputRange = NSMakeRange(0, typedInput->textView.textStorage.length); + + [self drawBlockQuotes:typedInput origin:origin inputRange:inputRange]; + [self drawLists:typedInput origin:origin inputRange:inputRange]; + [self drawCodeBlocks:typedInput origin:origin inputRange:inputRange]; +} + +- (void)drawCodeBlocks:(EnrichedTextInputView *)typedInput origin:(CGPoint)origin inputRange:(NSRange)inputRange +{ + CodeBlockStyle *codeBlockStyle = typedInput->stylesDict[@([CodeBlockStyle getStyleType])]; + if(codeBlockStyle == nullptr) { return; } + + NSArray *allCodeBlocks = [codeBlockStyle findAllOccurences:inputRange]; + NSArray *mergedCodeBlocks = [self mergeContiguousStylePairs:allCodeBlocks]; + UIColor *bgColor = [[typedInput->config codeBlockBgColor] colorWithAlphaIfNotTransparent:0.4]; + CGFloat radius = [typedInput->config codeBlockBorderRadius]; + [bgColor setFill]; + + for (StylePair *pair in mergedCodeBlocks) { + NSRange blockCharacterRange = [pair.rangeValue rangeValue]; + if (blockCharacterRange.length == 0) continue; + + NSArray *paragraphs = [ParagraphsUtils getSeparateParagraphsRangesIn:typedInput->textView range:blockCharacterRange]; + if (paragraphs.count == 0) continue; + + NSRange firstParagraphRange = [((NSValue *)[paragraphs firstObject]) rangeValue]; + NSRange lastParagraphRange = [((NSValue *)[paragraphs lastObject]) rangeValue]; + + for (NSValue *paragraphValue in paragraphs) { + NSRange paragraphCharacterRange = [paragraphValue rangeValue]; + + BOOL isFirstParagraph = NSEqualRanges(paragraphCharacterRange, firstParagraphRange); + BOOL isLastParagraph = NSEqualRanges(paragraphCharacterRange, lastParagraphRange); + + NSRange paragraphGlyphRange = [self glyphRangeForCharacterRange:paragraphCharacterRange actualCharacterRange:NULL]; + + __block BOOL isFirstLineOfParagraph = YES; + + [self enumerateLineFragmentsForGlyphRange:paragraphGlyphRange + usingBlock:^(CGRect rect, CGRect usedRect, NSTextContainer * _Nonnull textContainer, NSRange glyphRange, BOOL * _Nonnull stop) { + + CGRect lineBgRect = rect; + lineBgRect.origin.x = origin.x; + lineBgRect.origin.y += origin.y; + lineBgRect.size.width = textContainer.size.width; + + UIRectCorner cornersForThisLine = 0; + + if (isFirstParagraph && isFirstLineOfParagraph) { + cornersForThisLine = UIRectCornerTopLeft | UIRectCornerTopRight; + } + + BOOL isLastLineOfParagraph = (NSMaxRange(glyphRange) >= NSMaxRange(paragraphGlyphRange)); + + if (isLastParagraph && isLastLineOfParagraph) { + cornersForThisLine = cornersForThisLine | UIRectCornerBottomLeft | UIRectCornerBottomRight; + } + + UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:lineBgRect + byRoundingCorners:cornersForThisLine + cornerRadii:CGSizeMake(radius, radius)]; + [path fill]; + + isFirstLineOfParagraph = NO; + }]; + } + } +} + +- (NSArray *)mergeContiguousStylePairs:(NSArray *)pairs +{ + if (pairs.count == 0) { + return @[]; + } + + NSMutableArray *mergedPairs = [[NSMutableArray alloc] init]; + StylePair *currentPair = pairs[0]; + NSRange currentRange = [currentPair.rangeValue rangeValue]; + for (NSUInteger i = 1; i < pairs.count; i++) { + StylePair *nextPair = pairs[i]; + NSRange nextRange = [nextPair.rangeValue rangeValue]; + + // The Gap Check: + // NSMaxRange(currentRange) is where the current block ends. + // nextRange.location is where the next block starts. + if (NSMaxRange(currentRange) == nextRange.location) { + // They touch perfectly (no gap). Merge them. + currentRange.length += nextRange.length; + } else { + // There is a gap (indices don't match). + // 1. Save the finished block. + StylePair *mergedPair = [[StylePair alloc] init]; + mergedPair.rangeValue = [NSValue valueWithRange:currentRange]; + mergedPair.styleValue = currentPair.styleValue; + [mergedPairs addObject:mergedPair]; + + // 2. Start a brand new block. + currentPair = nextPair; + currentRange = nextRange; + } + } + + // Add the final block + StylePair *lastPair = [[StylePair alloc] init]; + lastPair.rangeValue = [NSValue valueWithRange:currentRange]; + lastPair.styleValue = currentPair.styleValue; + [mergedPairs addObject:lastPair]; + + return mergedPairs; +} + +- (void)drawBlockQuotes:(EnrichedTextInputView *)typedInput origin:(CGPoint)origin inputRange:(NSRange)inputRange +{ BlockQuoteStyle *bqStyle = typedInput->stylesDict[@([BlockQuoteStyle getStyleType])]; if(bqStyle == nullptr) { return; } - NSRange inputRange = NSMakeRange(0, typedInput->textView.textStorage.length); - // it isn't the most performant but we have to check for all the blockquotes each time and redraw them NSArray *allBlockquotes = [bqStyle findAllOccurences:inputRange]; @@ -78,7 +190,10 @@ - (void)my_drawBackgroundForGlyphRange:(NSRange)glyphRange atPoint:(CGPoint)orig } ]; } - +} + +- (void)drawLists:(EnrichedTextInputView *)typedInput origin:(CGPoint)origin inputRange:(NSRange)inputRange +{ UnorderedListStyle *ulStyle = typedInput->stylesDict[@([UnorderedListStyle getStyleType])]; OrderedListStyle *olStyle = typedInput->stylesDict[@([OrderedListStyle getStyleType])]; if(ulStyle == nullptr || olStyle == nullptr) { return; } diff --git a/ios/utils/OccurenceUtils.h b/ios/utils/OccurenceUtils.h index 885cb212..3c320a2d 100644 --- a/ios/utils/OccurenceUtils.h +++ b/ios/utils/OccurenceUtils.h @@ -40,4 +40,8 @@ withInput:(EnrichedTextInputView* _Nonnull)input inRange:(NSRange)range withCondition:(BOOL (NS_NOESCAPE ^_Nonnull)(id _Nullable value, NSRange range))condition; ++ (NSArray *_Nonnull)getRangesWithout + :(NSArray *_Nonnull)types + withInput:(EnrichedTextInputView* _Nonnull)input + inRange:(NSRange)range; @end diff --git a/ios/utils/OccurenceUtils.mm b/ios/utils/OccurenceUtils.mm index 796282eb..e575f15c 100644 --- a/ios/utils/OccurenceUtils.mm +++ b/ios/utils/OccurenceUtils.mm @@ -152,4 +152,51 @@ + (BOOL)anyMultiple return occurences; } ++ (NSArray *_Nonnull)getRangesWithout + :(NSArray *_Nonnull)types + withInput:(EnrichedTextInputView* _Nonnull)input + inRange:(NSRange)range +{ + NSMutableArray *activeStyleObjects = [[NSMutableArray alloc] init]; + for(NSNumber *type in types) { + id styleClass = input->stylesDict[type]; + [activeStyleObjects addObject:styleClass]; + } + + if (activeStyleObjects.count == 0) { + return @[[NSValue valueWithRange:range]]; + } + + NSMutableArray *newRanges = [[NSMutableArray alloc] init]; + NSUInteger lastRangeLocation = range.location; + NSUInteger endLocation = range.location + range.length; + + for (NSUInteger i = range.location; i < endLocation; i++) { + NSRange currentRange = NSMakeRange(i, 1); + BOOL forbiddenStyleFound = NO; + + for (id style in activeStyleObjects) { + if ([style detectStyle:currentRange]) { + forbiddenStyleFound = YES; + break; + } + } + + if (forbiddenStyleFound) { + if (i > lastRangeLocation) { + NSRange cleanRange = NSMakeRange(lastRangeLocation, i - lastRangeLocation); + [newRanges addObject:[NSValue valueWithRange:cleanRange]]; + } + lastRangeLocation = i + 1; + } + } + + if (lastRangeLocation < endLocation) { + NSRange remainingRange = NSMakeRange(lastRangeLocation, endLocation - lastRangeLocation); + [newRanges addObject:[NSValue valueWithRange:remainingRange]]; + } + + return newRanges; +} + @end diff --git a/ios/utils/ParagraphAttributesUtils.h b/ios/utils/ParagraphAttributesUtils.h index 0a1fd3a3..f9f07d8d 100644 --- a/ios/utils/ParagraphAttributesUtils.h +++ b/ios/utils/ParagraphAttributesUtils.h @@ -3,4 +3,5 @@ @interface ParagraphAttributesUtils : NSObject + (BOOL)handleBackspaceInRange:(NSRange)range replacementText:(NSString *)text input:(id)input; ++ (BOOL)handleNewlineBackspaceInRange:(NSRange)range replacementText:(NSString *)text input:(id)input; @end diff --git a/ios/utils/ParagraphAttributesUtils.mm b/ios/utils/ParagraphAttributesUtils.mm index 4cf63840..c2754e45 100644 --- a/ios/utils/ParagraphAttributesUtils.mm +++ b/ios/utils/ParagraphAttributesUtils.mm @@ -14,6 +14,7 @@ + (BOOL)handleBackspaceInRange:(NSRange)range replacementText:(NSString *)text i UnorderedListStyle *ulStyle = typedInput->stylesDict[@([UnorderedListStyle getStyleType])]; OrderedListStyle *olStyle = typedInput->stylesDict[@([OrderedListStyle getStyleType])]; BlockQuoteStyle *bqStyle = typedInput->stylesDict[@([BlockQuoteStyle getStyleType])]; + CodeBlockStyle *cbStyle = typedInput->stylesDict[@([CodeBlockStyle getStyleType])]; if(typedInput == nullptr) { return NO; @@ -36,22 +37,14 @@ + (BOOL)handleBackspaceInRange:(NSRange)range replacementText:(NSString *)text i // if the backspace removes the whole content of a paragraph (possibly more but has to start where the paragraph starts), we remove the typing attributes if(range.location == nonNewlineRange.location && range.length >= nonNewlineRange.length) { - // for lists and quotes we want to remove the characters but keep attribtues so that a zero width space appears here + // for lists, quotes and codeblocks we want to remove the characters but keep the attributes so that a zero width space appears here // so we do the removing manually and reapply attributes - if([ulStyle detectStyle:nonNewlineRange]) { - [TextInsertionUtils replaceText:text at:range additionalAttributes:nullptr input:typedInput withSelection:YES]; - [ulStyle addAttributes:NSMakeRange(range.location, 0)]; - return YES; - } - if([olStyle detectStyle:nonNewlineRange]) { - [TextInsertionUtils replaceText:text at:range additionalAttributes:nullptr input:typedInput withSelection:YES]; - [olStyle addAttributes:NSMakeRange(range.location, 0)]; - return YES; - } - if([bqStyle detectStyle:nonNewlineRange]) { - [TextInsertionUtils replaceText:text at:range additionalAttributes:nullptr input:typedInput withSelection:YES]; - [bqStyle addAttributes:NSMakeRange(range.location, 0)]; - return YES; + NSArray *handledStyles = @[ulStyle, olStyle, bqStyle, cbStyle]; + for(id style in handledStyles) { + if([style detectStyle:nonNewlineRange]) { + [TextInsertionUtils replaceText:text at:range additionalAttributes:nullptr input:typedInput withSelection:YES]; + return YES; + } } // do the replacement manually @@ -64,4 +57,74 @@ + (BOOL)handleBackspaceInRange:(NSRange)range replacementText:(NSString *)text i return NO; } +/** + * Handles the specific case of backspacing a newline character, which results in merging two paragraphs. + * + * THE PROBLEM: + * When merging a bottom paragraph (Source) into a top paragraph (Destination), the bottom paragraph + * normally brings all its attributes with it. If the top paragraph is a restrictive style (like a CodeBlock), + * and the bottom paragraph contains a conflicting style (like an H1 Header), a standard merge would + * create an invalid state (e.g., a CodeBlock that is also a Header). + * + * THE SOLUTION: + * 1. Identifies the dominant style of the paragraph ABOVE the deleted newline (`leftParagraphStyle`). + * 2. Checks the paragraph BELOW the newline (`rightRange`) for any styles that conflict with or are blocked by the top style. + * 3. Explicitly removes those forbidden styles from the bottom paragraph *before* the merge occurs. + * 4. Performs the merge (deletes the newline). + * + * @return YES if the newline backspace was handled and sanitized; NO otherwise. + */ ++ (BOOL)handleNewlineBackspaceInRange:(NSRange)range replacementText:(NSString *)text input:(id)input { + EnrichedTextInputView *typedInput = (EnrichedTextInputView *)input; + if(typedInput == nullptr) { + return NO; + } + + if(text.length == 0 && range.length == 1 && + [[NSCharacterSet newlineCharacterSet] characterIsMember:[typedInput->textView.textStorage.string characterAtIndex:range.location]]) { + NSRange leftRange = [typedInput->textView.textStorage.string paragraphRangeForRange:range]; + + id leftParagraphStyle = nullptr; + for (NSNumber *key in typedInput->stylesDict) { + id style = typedInput->stylesDict[key]; + if([[style class] isParagraphStyle] && [style detectStyle:leftRange]) { + leftParagraphStyle = style; + } + } + + if(leftParagraphStyle == nullptr) { + return NO; + } + + // index out of bounds + if(range.location + 1 >= typedInput->textView.textStorage.string.length) { + return NO; + } + + NSRange rightRange = [typedInput->textView.textStorage.string paragraphRangeForRange:NSMakeRange(range.location + 1, 1)]; + + StyleType type = [[leftParagraphStyle class] getStyleType]; + + NSArray *conflictingStyles = [typedInput getPresentStyleTypesFrom:typedInput->conflictingStyles[@(type)] range:rightRange]; + NSArray *blockingStyles = [typedInput getPresentStyleTypesFrom:typedInput->blockingStyles[@(type)] range:rightRange]; + NSArray *allToBeRemoved = [conflictingStyles arrayByAddingObjectsFromArray:blockingStyles]; + + for(NSNumber *style in allToBeRemoved) { + id styleClass = typedInput->stylesDict[style]; + + // for ranges, we need to remove each occurence + NSArray *allOccurences = [styleClass findAllOccurences:rightRange]; + + for(StylePair* pair in allOccurences) { + [styleClass removeAttributes: [pair.rangeValue rangeValue]]; + } + } + + [TextInsertionUtils replaceText:text at:range additionalAttributes:nullptr input:typedInput withSelection:YES]; + return YES; + } + + return NO; +} + @end diff --git a/ios/utils/StyleHeaders.h b/ios/utils/StyleHeaders.h index 1142a4a6..a4da5118 100644 --- a/ios/utils/StyleHeaders.h +++ b/ios/utils/StyleHeaders.h @@ -74,3 +74,8 @@ - (BOOL)handleBackspaceInRange:(NSRange)range replacementText:(NSString *)text; - (void)manageBlockquoteColor; @end + +@interface CodeBlockStyle : NSObject +- (void)manageCodeBlockFontAndColor; +- (BOOL)handleBackspaceInRange:(NSRange)range replacementText:(NSString *)text; +@end diff --git a/ios/utils/ZeroWidthSpaceUtils.mm b/ios/utils/ZeroWidthSpaceUtils.mm index 63f76a4a..e467289f 100644 --- a/ios/utils/ZeroWidthSpaceUtils.mm +++ b/ios/utils/ZeroWidthSpaceUtils.mm @@ -42,9 +42,10 @@ + (void)removeSpacesIfNeededinInput:(EnrichedTextInputView *)input { UnorderedListStyle *ulStyle = input->stylesDict[@([UnorderedListStyle getStyleType])]; OrderedListStyle *olStyle = input->stylesDict[@([OrderedListStyle getStyleType])]; BlockQuoteStyle *bqStyle = input->stylesDict[@([BlockQuoteStyle getStyleType])]; + CodeBlockStyle *cbStyle = input->stylesDict[@([CodeBlockStyle getStyleType])]; - // zero width spaces with no lists/blockquote styles on them get removed - if(![ulStyle detectStyle:characterRange] && ![olStyle detectStyle:characterRange] && ![bqStyle detectStyle:characterRange]) { + // zero width spaces with no lists/blockquotes/codeblocks on them get removed + if(![ulStyle detectStyle:characterRange] && ![olStyle detectStyle:characterRange] && ![bqStyle detectStyle:characterRange] && ![cbStyle detectStyle:characterRange]) { [indexesToBeRemoved addObject:@(characterRange.location)]; } } @@ -76,6 +77,7 @@ + (void)addSpacesIfNeededinInput:(EnrichedTextInputView *)input { UnorderedListStyle *ulStyle = input->stylesDict[@([UnorderedListStyle getStyleType])]; OrderedListStyle *olStyle = input->stylesDict[@([OrderedListStyle getStyleType])]; BlockQuoteStyle *bqStyle = input->stylesDict[@([BlockQuoteStyle getStyleType])]; + CodeBlockStyle *cbStyle = input->stylesDict[@([CodeBlockStyle getStyleType])]; NSMutableArray *indexesToBeInserted = [[NSMutableArray alloc] init]; NSRange preAddSelection = input->textView.selectedRange; @@ -87,7 +89,7 @@ + (void)addSpacesIfNeededinInput:(EnrichedTextInputView *)input { NSRange paragraphRange = [input->textView.textStorage.string paragraphRangeForRange:characterRange]; if(paragraphRange.length == 1) { - if([ulStyle detectStyle:characterRange] || [olStyle detectStyle:characterRange] || [bqStyle detectStyle:characterRange]) { + if([ulStyle detectStyle:characterRange] || [olStyle detectStyle:characterRange] || [bqStyle detectStyle:characterRange] || [cbStyle detectStyle:characterRange]) { // we have an empty list or quote item with no space: add it! [indexesToBeInserted addObject:@(paragraphRange.location)]; } @@ -114,7 +116,7 @@ + (void)addSpacesIfNeededinInput:(EnrichedTextInputView *)input { // additional check for last index of the input NSRange lastRange = NSMakeRange(input->textView.textStorage.string.length, 0); NSRange lastParagraphRange = [input->textView.textStorage.string paragraphRangeForRange:lastRange]; - if(lastParagraphRange.length == 0 && ([ulStyle detectStyle:lastRange] || [olStyle detectStyle:lastRange] || [bqStyle detectStyle:lastRange])) { + if(lastParagraphRange.length == 0 && ([ulStyle detectStyle:lastRange] || [olStyle detectStyle:lastRange] || [bqStyle detectStyle:lastRange] || [cbStyle detectStyle:lastRange])) { [TextInsertionUtils insertText:@"\u200B" at:lastRange.location additionalAttributes:nullptr input:input withSelection:NO]; } @@ -146,19 +148,29 @@ + (BOOL)handleBackspaceInRange:(NSRange)range replacementText:(NSString *)text i styleRemovalRange = NSMakeRange(paragraphRange.location, 1); } - [TextInsertionUtils replaceText:@"" at:removalRange additionalAttributes:nullptr input:typedInput withSelection:YES]; - // and then remove associated styling UnorderedListStyle *ulStyle = typedInput->stylesDict[@([UnorderedListStyle getStyleType])]; OrderedListStyle *olStyle = typedInput->stylesDict[@([OrderedListStyle getStyleType])]; BlockQuoteStyle *bqStyle = typedInput->stylesDict[@([BlockQuoteStyle getStyleType])]; + CodeBlockStyle *cbStyle = typedInput->stylesDict[@([CodeBlockStyle getStyleType])]; + + 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; + } - if([ulStyle detectStyle:styleRemovalRange]) { + [TextInsertionUtils replaceText:@"" at:removalRange additionalAttributes:nullptr input:typedInput withSelection:YES]; + + if ([ulStyle detectStyle:styleRemovalRange]) { [ulStyle removeAttributes:styleRemovalRange]; - } else if([olStyle detectStyle:styleRemovalRange]) { + } else if ([olStyle detectStyle:styleRemovalRange]) { [olStyle removeAttributes:styleRemovalRange]; - } else if([bqStyle detectStyle:styleRemovalRange]) { + } else if ([bqStyle detectStyle:styleRemovalRange]) { [bqStyle removeAttributes:styleRemovalRange]; } @@ -166,5 +178,5 @@ + (BOOL)handleBackspaceInRange:(NSRange)range replacementText:(NSString *)text i } return NO; } -@end +@end