diff --git a/.maestro/enrichedText/flows/empty_list_elements_display.yaml b/.maestro/enrichedText/flows/empty_list_elements_display.yaml
index 6344898c..d0a23a8b 100644
--- a/.maestro/enrichedText/flows/empty_list_elements_display.yaml
+++ b/.maestro/enrichedText/flows/empty_list_elements_display.yaml
@@ -1,6 +1,4 @@
appId: swmansion.enriched.example
-tags:
- - android-only
---
# Validates that lists with empty items are parsed and displayed correctly
- launchApp
diff --git a/.maestro/enrichedText/flows/inline_styles_display.yaml b/.maestro/enrichedText/flows/inline_styles_display.yaml
index 32c21b08..d9046d9e 100644
--- a/.maestro/enrichedText/flows/inline_styles_display.yaml
+++ b/.maestro/enrichedText/flows/inline_styles_display.yaml
@@ -1,6 +1,4 @@
appId: swmansion.enriched.example
-tags:
- - android-only
---
# Validates that inline styles are displayed correctly
- launchApp
diff --git a/.maestro/enrichedText/flows/link_press.yaml b/.maestro/enrichedText/flows/link_press.yaml
index 7b232aad..c3a39e89 100644
--- a/.maestro/enrichedText/flows/link_press.yaml
+++ b/.maestro/enrichedText/flows/link_press.yaml
@@ -1,6 +1,4 @@
appId: swmansion.enriched.example
-tags:
- - android-only
---
# Validates that link press events are triggered correctly
- launchApp
diff --git a/.maestro/enrichedText/flows/mention_press.yaml b/.maestro/enrichedText/flows/mention_press.yaml
index 0133f49c..cc1ec2ea 100644
--- a/.maestro/enrichedText/flows/mention_press.yaml
+++ b/.maestro/enrichedText/flows/mention_press.yaml
@@ -1,6 +1,4 @@
appId: swmansion.enriched.example
-tags:
- - android-only
---
# Validates that mention press events are triggered correctly
- launchApp
diff --git a/.maestro/enrichedText/flows/paragraph_styles_display.yaml b/.maestro/enrichedText/flows/paragraph_styles_display.yaml
index 434e4101..f0d0230f 100644
--- a/.maestro/enrichedText/flows/paragraph_styles_display.yaml
+++ b/.maestro/enrichedText/flows/paragraph_styles_display.yaml
@@ -1,6 +1,4 @@
appId: swmansion.enriched.example
-tags:
- - android-only
---
# Validates that paragraph styles are displayed correctly
- launchApp
diff --git a/.maestro/enrichedText/screenshots/ios/custom_styles_display.png b/.maestro/enrichedText/screenshots/ios/custom_styles_display.png
new file mode 100644
index 00000000..2ac807c3
Binary files /dev/null and b/.maestro/enrichedText/screenshots/ios/custom_styles_display.png differ
diff --git a/.maestro/enrichedText/screenshots/ios/empty_list_elements_display.png b/.maestro/enrichedText/screenshots/ios/empty_list_elements_display.png
new file mode 100644
index 00000000..03dcbd82
Binary files /dev/null and b/.maestro/enrichedText/screenshots/ios/empty_list_elements_display.png differ
diff --git a/.maestro/enrichedText/screenshots/ios/inline_styles_display.png b/.maestro/enrichedText/screenshots/ios/inline_styles_display.png
new file mode 100644
index 00000000..974d833f
Binary files /dev/null and b/.maestro/enrichedText/screenshots/ios/inline_styles_display.png differ
diff --git a/.maestro/enrichedText/screenshots/ios/link_press.png b/.maestro/enrichedText/screenshots/ios/link_press.png
new file mode 100644
index 00000000..4a664d37
Binary files /dev/null and b/.maestro/enrichedText/screenshots/ios/link_press.png differ
diff --git a/.maestro/enrichedText/screenshots/ios/mention_press.png b/.maestro/enrichedText/screenshots/ios/mention_press.png
new file mode 100644
index 00000000..0ec98e5f
Binary files /dev/null and b/.maestro/enrichedText/screenshots/ios/mention_press.png differ
diff --git a/.maestro/enrichedText/screenshots/ios/paragraph_styles_display.png b/.maestro/enrichedText/screenshots/ios/paragraph_styles_display.png
new file mode 100644
index 00000000..a36a2a93
Binary files /dev/null and b/.maestro/enrichedText/screenshots/ios/paragraph_styles_display.png differ
diff --git a/apps/example/src/components/TextRenderer.tsx b/apps/example/src/components/TextRenderer.tsx
index 2cef09cc..74e2e0ab 100644
--- a/apps/example/src/components/TextRenderer.tsx
+++ b/apps/example/src/components/TextRenderer.tsx
@@ -52,7 +52,7 @@ const styles = StyleSheet.create({
borderRadius: 8,
},
text: {
- fontSize: 16,
+ fontSize: 18,
color: 'black',
marginTop: 4,
fontFamily: 'Nunito-Regular',
diff --git a/ios/EnrichedTextInputView.h b/ios/EnrichedTextInputView.h
index 1ea44ad9..432c8c1d 100644
--- a/ios/EnrichedTextInputView.h
+++ b/ios/EnrichedTextInputView.h
@@ -1,7 +1,8 @@
#pragma once
#import "AttributesManager.h"
#import "BaseStyleProtocol.h"
-#import "InputConfig.h"
+#import "EnrichedConfig.h"
+#import "EnrichedViewHost.h"
#import "InputParser.h"
#import "InputTextView.h"
#import "LinkData.h"
@@ -15,11 +16,11 @@
NS_ASSUME_NONNULL_BEGIN
@interface EnrichedTextInputView
- : RCTViewComponentView {
+ : RCTViewComponentView {
@public
InputTextView *textView;
@public
- InputConfig *config;
+ EnrichedConfig *config;
@public
InputParser *parser;
@public
@@ -44,8 +45,7 @@ NS_ASSUME_NONNULL_BEGIN
- (void)anyTextMayHaveBeenModified;
- (void)scheduleRelayoutIfNeeded;
- (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 66c22512..fa8d1fe8 100644
--- a/ios/EnrichedTextInputView.mm
+++ b/ios/EnrichedTextInputView.mm
@@ -1,4 +1,5 @@
#import "EnrichedTextInputView.h"
+#import "AttachmentLayoutUtils.h"
#import "CoreText/CoreText.h"
#import "DotReplacementUtils.h"
#import "ImageAttachment.h"
@@ -8,6 +9,7 @@
#import "RCTFabricComponentsPlugins.h"
#import "StringExtension.h"
#import "StyleHeaders.h"
+#import "StyleUtils.h"
#import "TextBlockTapGestureRecognizer.h"
#import "UIView+React.h"
#import "WordsUtils.h"
@@ -56,6 +58,8 @@ @implementation EnrichedTextInputView {
NSString *_submitBehavior;
}
+@synthesize blockEmitting = blockEmitting;
+
// MARK: - Component utils
+ (ComponentDescriptorProvider)componentDescriptorProvider {
@@ -71,6 +75,36 @@ + (BOOL)shouldBeRecycled {
return NO;
}
+// MARK: - EnrichedViewHost protocol
+
+- (UITextView *)textView {
+ return textView;
+}
+
+- (EnrichedConfig *)config {
+ return config;
+}
+
+- (NSDictionary *)stylesDict {
+ return stylesDict;
+}
+
+- (AttributesManager *)attributesManager {
+ return attributesManager;
+}
+
+- (NSMutableDictionary *> *)conflictingStyles {
+ return conflictingStyles;
+}
+
+- (NSMutableDictionary *> *)blockingStyles {
+ return blockingStyles;
+}
+
+- (NSMutableDictionary *)defaultTypingAttributes {
+ return defaultTypingAttributes;
+}
+
// MARK: - Init
- (instancetype)initWithFrame:(CGRect)frame {
@@ -103,149 +137,9 @@ - (void)setDefaults {
defaultTypingAttributes =
[[NSMutableDictionary alloc] init];
- stylesDict = @{
- @([BoldStyle getType]) : [[BoldStyle alloc] initWithInput:self],
- @([ItalicStyle getType]) : [[ItalicStyle alloc] initWithInput:self],
- @([UnderlineStyle getType]) : [[UnderlineStyle alloc] initWithInput:self],
- @([StrikethroughStyle getType]) :
- [[StrikethroughStyle alloc] initWithInput:self],
- @([InlineCodeStyle getType]) : [[InlineCodeStyle alloc] initWithInput:self],
- @([LinkStyle getType]) : [[LinkStyle alloc] initWithInput:self],
- @([MentionStyle getType]) : [[MentionStyle alloc] initWithInput:self],
- @([H1Style getType]) : [[H1Style alloc] initWithInput:self],
- @([H2Style getType]) : [[H2Style alloc] initWithInput:self],
- @([H3Style getType]) : [[H3Style alloc] initWithInput:self],
- @([H4Style getType]) : [[H4Style alloc] initWithInput:self],
- @([H5Style getType]) : [[H5Style alloc] initWithInput:self],
- @([H6Style getType]) : [[H6Style alloc] initWithInput:self],
- @([UnorderedListStyle getType]) :
- [[UnorderedListStyle alloc] initWithInput:self],
- @([OrderedListStyle getType]) :
- [[OrderedListStyle alloc] initWithInput:self],
- @([CheckboxListStyle getType]) :
- [[CheckboxListStyle alloc] initWithInput:self],
- @([BlockQuoteStyle getType]) : [[BlockQuoteStyle alloc] initWithInput:self],
- @([CodeBlockStyle getType]) : [[CodeBlockStyle alloc] initWithInput:self],
- @([ImageStyle getType]) : [[ImageStyle alloc] initWithInput:self]
- };
-
- conflictingStyles = [@{
- @([BoldStyle getType]) : @[],
- @([ItalicStyle getType]) : @[],
- @([UnderlineStyle getType]) : @[],
- @([StrikethroughStyle getType]) : @[],
- @([InlineCodeStyle getType]) :
- @[ @([LinkStyle getType]), @([MentionStyle getType]) ],
- @([LinkStyle getType]) : @[
- @([InlineCodeStyle getType]), @([LinkStyle getType]),
- @([MentionStyle getType])
- ],
- @([MentionStyle getType]) :
- @[ @([InlineCodeStyle getType]), @([LinkStyle getType]) ],
- @([H1Style getType]) : @[
- @([H2Style getType]), @([H3Style getType]), @([H4Style getType]),
- @([H5Style getType]), @([H6Style getType]),
- @([UnorderedListStyle getType]), @([OrderedListStyle getType]),
- @([BlockQuoteStyle getType]), @([CodeBlockStyle getType]),
- @([CheckboxListStyle getType])
- ],
- @([H2Style getType]) : @[
- @([H1Style getType]), @([H3Style getType]), @([H4Style getType]),
- @([H5Style getType]), @([H6Style getType]),
- @([UnorderedListStyle getType]), @([OrderedListStyle getType]),
- @([BlockQuoteStyle getType]), @([CodeBlockStyle getType]),
- @([CheckboxListStyle getType])
- ],
- @([H3Style getType]) : @[
- @([H1Style getType]), @([H2Style getType]), @([H4Style getType]),
- @([H5Style getType]), @([H6Style getType]),
- @([UnorderedListStyle getType]), @([OrderedListStyle getType]),
- @([BlockQuoteStyle getType]), @([CodeBlockStyle getType]),
- @([CheckboxListStyle getType])
- ],
- @([H4Style getType]) : @[
- @([H1Style getType]), @([H2Style getType]), @([H3Style getType]),
- @([H5Style getType]), @([H6Style getType]),
- @([UnorderedListStyle getType]), @([OrderedListStyle getType]),
- @([BlockQuoteStyle getType]), @([CodeBlockStyle getType]),
- @([CheckboxListStyle getType])
- ],
- @([H5Style getType]) : @[
- @([H1Style getType]), @([H2Style getType]), @([H3Style getType]),
- @([H4Style getType]), @([H6Style getType]),
- @([UnorderedListStyle getType]), @([OrderedListStyle getType]),
- @([BlockQuoteStyle getType]), @([CodeBlockStyle getType]),
- @([CheckboxListStyle getType])
- ],
- @([H6Style getType]) : @[
- @([H1Style getType]), @([H2Style getType]), @([H3Style getType]),
- @([H4Style getType]), @([H5Style getType]),
- @([UnorderedListStyle getType]), @([OrderedListStyle getType]),
- @([BlockQuoteStyle getType]), @([CodeBlockStyle getType]),
- @([CheckboxListStyle getType])
- ],
- @([UnorderedListStyle getType]) : @[
- @([H1Style getType]), @([H2Style getType]), @([H3Style getType]),
- @([H4Style getType]), @([H5Style getType]), @([H6Style getType]),
- @([OrderedListStyle getType]), @([BlockQuoteStyle getType]),
- @([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 getType])
- ],
- @([CheckboxListStyle getType]) : @[
- @([H1Style getType]), @([H2Style getType]), @([H3Style getType]),
- @([H4Style getType]), @([H5Style getType]), @([H6Style getType]),
- @([UnorderedListStyle getType]), @([OrderedListStyle getType]),
- @([BlockQuoteStyle getType]), @([CodeBlockStyle getType])
- ],
- @([BlockQuoteStyle getType]) : @[
- @([H1Style getType]), @([H2Style getType]), @([H3Style getType]),
- @([H4Style getType]), @([H5Style getType]), @([H6Style getType]),
- @([UnorderedListStyle getType]), @([OrderedListStyle getType]),
- @([CodeBlockStyle getType]), @([CheckboxListStyle getType])
- ],
- @([CodeBlockStyle getType]) : @[
- @([H1Style getType]), @([H2Style getType]), @([H3Style getType]),
- @([H4Style getType]), @([H5Style getType]), @([H6Style getType]),
- @([BoldStyle getType]), @([UnderlineStyle getType]),
- @([ItalicStyle getType]), @([StrikethroughStyle getType]),
- @([UnorderedListStyle getType]), @([OrderedListStyle getType]),
- @([BlockQuoteStyle getType]), @([InlineCodeStyle getType]),
- @([MentionStyle getType]), @([LinkStyle getType]),
- @([CheckboxListStyle getType])
- ],
- @([ImageStyle getType]) :
- @[ @([LinkStyle getType]), @([MentionStyle getType]) ]
- } mutableCopy];
-
- blockingStyles = [@{
- @([BoldStyle getType]) : @[ @([CodeBlockStyle getType]) ],
- @([ItalicStyle getType]) : @[ @([CodeBlockStyle getType]) ],
- @([UnderlineStyle getType]) : @[ @([CodeBlockStyle getType]) ],
- @([StrikethroughStyle getType]) : @[ @([CodeBlockStyle getType]) ],
- @([InlineCodeStyle getType]) :
- @[ @([CodeBlockStyle getType]), @([ImageStyle getType]) ],
- @([LinkStyle getType]) :
- @[ @([CodeBlockStyle getType]), @([ImageStyle getType]) ],
- @([MentionStyle getType]) :
- @[ @([CodeBlockStyle getType]), @([ImageStyle getType]) ],
- @([H1Style getType]) : @[],
- @([H2Style getType]) : @[],
- @([H3Style getType]) : @[],
- @([H4Style getType]) : @[],
- @([H5Style getType]) : @[],
- @([H6Style getType]) : @[],
- @([UnorderedListStyle getType]) : @[],
- @([OrderedListStyle getType]) : @[],
- @([CheckboxListStyle getType]) : @[],
- @([BlockQuoteStyle getType]) : @[],
- @([CodeBlockStyle getType]) : @[],
- @([ImageStyle getType]) : @[ @([InlineCodeStyle getType]) ]
- } mutableCopy];
+ stylesDict = [StyleUtils stylesDictForHost:self isInput:YES];
+ conflictingStyles = [[StyleUtils conflictMap] mutableCopy];
+ blockingStyles = [[StyleUtils blockingMap] mutableCopy];
parser = [[InputParser alloc] initWithInput:self];
_attachmentViews = [[NSMutableDictionary alloc] init];
@@ -301,13 +195,13 @@ - (void)updateProps:(Props::Shared const &)props
// initial config
if (config == nullptr) {
isFirstMount = YES;
- config = [[InputConfig alloc] init];
+ config = [[EnrichedConfig alloc] init];
}
// any style prop changes:
// firstly we create the new config for the changes
- InputConfig *newConfig = [config copy];
+ EnrichedConfig *newConfig = [config copy];
if (newViewProps.color != oldViewProps.color) {
if (isColorMeaningful(newViewProps.color)) {
@@ -369,11 +263,11 @@ - (void)updateProps:(Props::Shared const &)props
// Update style blocks and conflicts for bold
if (newViewProps.htmlStyle.h1.bold) {
- [self addStyleBlock:H1 to:Bold];
- [self addStyleConflict:Bold to:H1];
+ [StyleUtils addStyleBlock:H1 to:Bold forHost:self];
+ [StyleUtils addStyleConflict:Bold to:H1 forHost:self];
} else {
- [self removeStyleBlock:H1 from:Bold];
- [self removeStyleConflict:Bold from:H1];
+ [StyleUtils removeStyleBlock:H1 from:Bold forHost:self];
+ [StyleUtils removeStyleConflict:Bold from:H1 forHost:self];
}
stylePropChanged = YES;
@@ -390,11 +284,11 @@ - (void)updateProps:(Props::Shared const &)props
// Update style blocks and conflicts for bold
if (newViewProps.htmlStyle.h2.bold) {
- [self addStyleBlock:H2 to:Bold];
- [self addStyleConflict:Bold to:H2];
+ [StyleUtils addStyleBlock:H2 to:Bold forHost:self];
+ [StyleUtils addStyleConflict:Bold to:H2 forHost:self];
} else {
- [self removeStyleBlock:H2 from:Bold];
- [self removeStyleConflict:Bold from:H2];
+ [StyleUtils removeStyleBlock:H2 from:Bold forHost:self];
+ [StyleUtils removeStyleConflict:Bold from:H2 forHost:self];
}
stylePropChanged = YES;
@@ -411,11 +305,11 @@ - (void)updateProps:(Props::Shared const &)props
// Update style blocks and conflicts for bold
if (newViewProps.htmlStyle.h3.bold) {
- [self addStyleBlock:H3 to:Bold];
- [self addStyleConflict:Bold to:H3];
+ [StyleUtils addStyleBlock:H3 to:Bold forHost:self];
+ [StyleUtils addStyleConflict:Bold to:H3 forHost:self];
} else {
- [self removeStyleBlock:H3 from:Bold];
- [self removeStyleConflict:Bold from:H3];
+ [StyleUtils removeStyleBlock:H3 from:Bold forHost:self];
+ [StyleUtils removeStyleConflict:Bold from:H3 forHost:self];
}
stylePropChanged = YES;
@@ -432,11 +326,11 @@ - (void)updateProps:(Props::Shared const &)props
// Update style blocks and conflicts for bold
if (newViewProps.htmlStyle.h4.bold) {
- [self addStyleBlock:H4 to:Bold];
- [self addStyleConflict:Bold to:H4];
+ [StyleUtils addStyleBlock:H4 to:Bold forHost:self];
+ [StyleUtils addStyleConflict:Bold to:H4 forHost:self];
} else {
- [self removeStyleBlock:H4 from:Bold];
- [self removeStyleConflict:Bold from:H4];
+ [StyleUtils removeStyleBlock:H4 from:Bold forHost:self];
+ [StyleUtils removeStyleConflict:Bold from:H4 forHost:self];
}
stylePropChanged = YES;
@@ -453,11 +347,11 @@ - (void)updateProps:(Props::Shared const &)props
// Update style blocks and conflicts for bold
if (newViewProps.htmlStyle.h5.bold) {
- [self addStyleBlock:H5 to:Bold];
- [self addStyleConflict:Bold to:H5];
+ [StyleUtils addStyleBlock:H5 to:Bold forHost:self];
+ [StyleUtils addStyleConflict:Bold to:H5 forHost:self];
} else {
- [self removeStyleBlock:H5 from:Bold];
- [self removeStyleConflict:Bold from:H5];
+ [StyleUtils removeStyleBlock:H5 from:Bold forHost:self];
+ [StyleUtils removeStyleConflict:Bold from:H5 forHost:self];
}
stylePropChanged = YES;
@@ -474,11 +368,11 @@ - (void)updateProps:(Props::Shared const &)props
// Update style blocks and conflicts for bold
if (newViewProps.htmlStyle.h6.bold) {
- [self addStyleBlock:H6 to:Bold];
- [self addStyleConflict:Bold to:H6];
+ [StyleUtils addStyleBlock:H6 to:Bold forHost:self];
+ [StyleUtils addStyleConflict:Bold to:H6 forHost:self];
} else {
- [self removeStyleBlock:H6 from:Bold];
- [self removeStyleConflict:Bold from:H6];
+ [StyleUtils removeStyleBlock:H6 from:Bold forHost:self];
+ [StyleUtils removeStyleConflict:Bold from:H6 forHost:self];
}
stylePropChanged = YES;
@@ -1233,38 +1127,6 @@ - (bool)textInputShouldSubmitOnReturn {
[_submitBehavior isEqualToString:@"submit"];
}
-- (void)addStyleBlock:(StyleType)blocking to:(StyleType)blocked {
- NSMutableArray *blocksArr = [blockingStyles[@(blocked)] mutableCopy];
- if (![blocksArr containsObject:@(blocking)]) {
- [blocksArr addObject:@(blocking)];
- blockingStyles[@(blocked)] = blocksArr;
- }
-}
-
-- (void)removeStyleBlock:(StyleType)blocking from:(StyleType)blocked {
- NSMutableArray *blocksArr = [blockingStyles[@(blocked)] mutableCopy];
- if ([blocksArr containsObject:@(blocking)]) {
- [blocksArr removeObject:@(blocking)];
- blockingStyles[@(blocked)] = blocksArr;
- }
-}
-
-- (void)addStyleConflict:(StyleType)conflicting to:(StyleType)conflicted {
- NSMutableArray *conflictsArr = [conflictingStyles[@(conflicted)] mutableCopy];
- if (![conflictsArr containsObject:@(conflicting)]) {
- [conflictsArr addObject:@(conflicting)];
- conflictingStyles[@(conflicted)] = conflictsArr;
- }
-}
-
-- (void)removeStyleConflict:(StyleType)conflicting from:(StyleType)conflicted {
- NSMutableArray *conflictsArr = [conflictingStyles[@(conflicted)] mutableCopy];
- if ([conflictsArr containsObject:@(conflicting)]) {
- [conflictsArr removeObject:@(conflicting)];
- conflictingStyles[@(conflicted)] = conflictsArr;
- }
-}
-
// MARK: - Native commands and events
- (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args {
@@ -1363,7 +1225,10 @@ - (void)focus {
- (void)setValue:(NSString *)value {
NSString *initiallyProcessedHtml = [parser initiallyProcessHtml:value];
if (initiallyProcessedHtml == nullptr) {
- // just plain text
+ // reset the text first and reset typing attributes
+ textView.text = @"";
+ textView.typingAttributes = defaultTypingAttributes;
+ // set new text
textView.text = value;
} else {
// we've got some seemingly proper html
@@ -1648,55 +1513,10 @@ - (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];
- if (blocking.count != 0) {
- return NO;
- }
-
- // handle conflicting styles: remove styles within the range
- NSArray *conflicting =
- [self getPresentStyleTypesFrom:conflictingStyles[@(type)] range:range];
- if (conflicting.count != 0) {
- for (NSNumber *type in conflicting) {
- StyleBase *style = stylesDict[type];
-
- if ([style isParagraph]) {
- // for paragraph styles we can just call remove since it will pick up
- // proper paragraph range
- [style remove:range withDirtyRange:YES];
- } else {
- // for inline styles we have to differentiate betweeen normal and typing
- // attributes removal
- range.length >= 1 ? [style remove:range withDirtyRange:YES]
- : [style removeTyping];
- }
- }
- }
- return YES;
-}
-
-- (NSArray *)getPresentStyleTypesFrom:(NSArray *)types
- range:(NSRange)range {
- NSMutableArray *resultArray =
- [[NSMutableArray alloc] init];
- for (NSNumber *type in types) {
- StyleBase *style = stylesDict[type];
-
- if (range.length >= 1) {
- if ([style any:range]) {
- [resultArray addObject:type];
- }
- } else {
- if ([style detect:range]) {
- [resultArray addObject:type];
- }
- }
- }
- return resultArray;
+ return [StyleUtils handleStyleBlocksAndConflicts:type
+ range:range
+ forHost:self];
}
- (void)manageSelectionBasedChanges {
@@ -2149,7 +1969,7 @@ - (void)onTextBlockTap:(TextBlockTapGestureRecognizer *)gr {
if (checkboxStyle) {
NSUInteger charIndex = (NSUInteger)gr.characterIndex;
- [checkboxStyle toggleCheckedAt:charIndex];
+ [checkboxStyle toggleCheckedAt:charIndex withDirtyRange:YES];
[self anyTextMayHaveBeenModified];
NSString *fullText = textView.textStorage.string;
@@ -2205,129 +2025,19 @@ - (void)textStorage:(NSTextStorage *)textStorage
// 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];
-
- dispatch_async(dispatch_get_main_queue(), ^{
- [self layoutAttachments];
- });
+ [AttachmentLayoutUtils handleAttachmentUpdate:attachment
+ textView:textView
+ onLayoutBlock:^{
+ [self layoutAttachments];
+ }];
}
// MARK: - Image/GIF Overlay Management
- (void)layoutAttachments {
- NSTextStorage *storage = textView.textStorage;
- NSMutableDictionary *activeAttachmentViews =
- [NSMutableDictionary dictionary];
-
- // Iterate over the entire text to find ImageAttachments
- [storage enumerateAttribute:NSAttachmentAttributeName
- inRange:NSMakeRange(0, storage.length)
- options:0
- usingBlock:^(id value, NSRange range, BOOL *stop) {
- if ([value isKindOfClass:[ImageAttachment class]]) {
- ImageAttachment *attachment = (ImageAttachment *)value;
-
- CGRect rect = [self frameForAttachment:attachment
- atRange:range];
-
- // Get or Create the UIImageView for this specific
- // attachment key
- NSValue *key =
- [NSValue valueWithNonretainedObject:attachment];
- UIImageView *imgView = _attachmentViews[key];
-
- if (!imgView) {
- // It doesn't exist yet, create it
- imgView = [[UIImageView alloc] initWithFrame:rect];
- imgView.contentMode = UIViewContentModeScaleAspectFit;
- imgView.tintColor = [UIColor labelColor];
-
- // Add it directly to the TextView
- [textView addSubview:imgView];
- }
-
- // Update position (in case text moved/scrolled)
- if (!CGRectEqualToRect(imgView.frame, rect)) {
- imgView.frame = rect;
- }
- UIImage *targetImage =
- attachment.storedAnimatedImage ?: attachment.image;
-
- // Only set if different to avoid resetting the animation
- // loop
- if (imgView.image != targetImage) {
- imgView.image = targetImage;
- }
-
- // Ensure it is visible on top
- imgView.hidden = NO;
- [textView bringSubviewToFront:imgView];
-
- activeAttachmentViews[key] = imgView;
- // Remove from the old map so we know it has been claimed
- [_attachmentViews removeObjectForKey:key];
- }
- }];
-
- // Everything remaining in _attachmentViews is dead or off-screen
- for (UIImageView *danglingView in _attachmentViews.allValues) {
- [danglingView removeFromSuperview];
- }
- _attachmentViews = activeAttachmentViews;
-}
-
-- (CGRect)frameForAttachment:(ImageAttachment *)attachment
- atRange:(NSRange)range {
- NSLayoutManager *layoutManager = textView.layoutManager;
- NSTextContainer *textContainer = textView.textContainer;
- NSTextStorage *storage = textView.textStorage;
-
- NSRange glyphRange = [layoutManager glyphRangeForCharacterRange:range
- actualCharacterRange:NULL];
- CGRect glyphRect = [layoutManager boundingRectForGlyphRange:glyphRange
- inTextContainer:textContainer];
-
- CGRect lineRect =
- [layoutManager lineFragmentRectForGlyphAtIndex:glyphRange.location
- effectiveRange:NULL];
- CGSize attachmentSize = attachment.bounds.size;
-
- UIFont *font = [storage attribute:NSFontAttributeName
- atIndex:range.location
- effectiveRange:NULL];
- if (!font) {
- font = [config primaryFont];
- }
-
- // Calculate (Baseline Alignment)
- CGFloat targetY =
- CGRectGetMaxY(lineRect) + font.descender - attachmentSize.height;
- CGRect rect =
- CGRectMake(glyphRect.origin.x + textView.textContainerInset.left,
- targetY + textView.textContainerInset.top,
- attachmentSize.width, attachmentSize.height);
-
- return CGRectIntegral(rect);
+ _attachmentViews =
+ [AttachmentLayoutUtils layoutAttachmentsInTextView:textView
+ config:config
+ existingViews:_attachmentViews];
}
@end
diff --git a/ios/EnrichedTextView.h b/ios/EnrichedTextView.h
new file mode 100644
index 00000000..6a090fe5
--- /dev/null
+++ b/ios/EnrichedTextView.h
@@ -0,0 +1,33 @@
+#pragma once
+#import "EnrichedConfig.h"
+#import "EnrichedViewHost.h"
+#import "MediaAttachment.h"
+#import "MentionParams.h"
+#import
+#import
+
+#ifndef EnrichedTextViewNativeComponent_h
+#define EnrichedTextViewNativeComponent_h
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface EnrichedTextView
+ : RCTViewComponentView {
+@public
+ UITextView *textView;
+@public
+ EnrichedConfig *config;
+@public
+ NSDictionary *stylesDict;
+ NSMutableDictionary *> *conflictingStyles;
+ NSMutableDictionary *> *blockingStyles;
+ NSMutableDictionary *defaultTypingAttributes;
+}
+- (CGSize)measureSize:(CGFloat)maxWidth;
+- (void)emitOnLinkPressEvent:(NSString *)url;
+- (void)emitOnMentionPressEvent:(MentionParams *)mention;
+@end
+
+NS_ASSUME_NONNULL_END
+
+#endif /* EnrichedTextViewNativeComponent_h */
diff --git a/ios/EnrichedTextView.mm b/ios/EnrichedTextView.mm
new file mode 100644
index 00000000..53ed0202
--- /dev/null
+++ b/ios/EnrichedTextView.mm
@@ -0,0 +1,880 @@
+#import "EnrichedTextView.h"
+#import "AttachmentLayoutUtils.h"
+#import "EnrichedTextStyleHeaders.h"
+#import "EnrichedTextTouchHandler.h"
+#import "EnrichedTouchableTextView.h"
+#import "HtmlParser.h"
+#import "LayoutManagerExtension.h"
+#import "LinkData.h"
+#import "MentionParams.h"
+#import "MentionStyleProps.h"
+#import "RCTFabricComponentsPlugins.h"
+#import "StringExtension.h"
+#import "StyleUtils.h"
+#import "TextDecorationLineEnum.h"
+#import "ZeroWidthSpaceUtils.h"
+#import
+#import
+#import
+#import
+#import
+#import
+
+using namespace facebook::react;
+
+@interface EnrichedTextView ()
+@end
+
+@implementation EnrichedTextView {
+ EnrichedTextViewShadowNode::ConcreteState::Shared _state;
+ NSMutableDictionary *_attachmentViews;
+ EnrichedTextTouchHandler *_touchHandler;
+}
+
+@synthesize blockEmitting = _blockEmitting;
+
+// MARK: - Component utils
+
++ (ComponentDescriptorProvider)componentDescriptorProvider {
+ return concreteComponentDescriptorProvider();
+}
+
+Class EnrichedTextViewCls(void) {
+ return EnrichedTextView.class;
+}
+
++ (BOOL)shouldBeRecycled {
+ return NO;
+}
+
+// MARK: - Init
+
+- (instancetype)initWithFrame:(CGRect)frame {
+ if (self = [super initWithFrame:frame]) {
+ static const auto defaultProps =
+ std::make_shared();
+ _props = defaultProps;
+ _attachmentViews = [[NSMutableDictionary alloc] init];
+ defaultTypingAttributes = [[NSMutableDictionary alloc] init];
+ [self setupTextView];
+ [self setupStyles];
+ self.contentView = textView;
+ }
+ return self;
+}
+
+- (void)setupTextView {
+ EnrichedTouchableTextView *tv = [[EnrichedTouchableTextView alloc] init];
+ _touchHandler = [[EnrichedTextTouchHandler alloc] initWithView:self];
+ tv.touchHandler = _touchHandler;
+ textView = tv;
+
+ textView.backgroundColor = UIColor.clearColor;
+ textView.textContainerInset = UIEdgeInsetsMake(0, 0, 0, 0);
+ textView.textContainer.lineFragmentPadding = 0;
+ textView.editable = NO;
+ textView.scrollEnabled = NO;
+ textView.adjustsFontForContentSizeCategory = YES;
+ textView.layoutManager.input = self;
+}
+
+- (void)setupStyles {
+ stylesDict = [StyleUtils stylesDictForHost:self isInput:NO];
+ conflictingStyles = [[StyleUtils conflictMap] mutableCopy];
+ blockingStyles = [[StyleUtils blockingMap] mutableCopy];
+}
+
+// MARK: - EnrichedViewHost protocol
+
+- (UITextView *)textView {
+ return textView;
+}
+
+- (EnrichedConfig *)config {
+ return config;
+}
+
+- (NSDictionary *)stylesDict {
+ return stylesDict;
+}
+
+- (AttributesManager *)attributesManager {
+ return nil;
+}
+
+- (NSMutableDictionary *> *)conflictingStyles {
+ return conflictingStyles;
+}
+
+- (NSMutableDictionary *> *)blockingStyles {
+ return blockingStyles;
+}
+
+- (NSMutableDictionary *)defaultTypingAttributes {
+ return defaultTypingAttributes;
+}
+
+// MARK: - Props
+
+- (void)updateProps:(Props::Shared const &)props
+ oldProps:(Props::Shared const &)oldProps {
+ const auto &oldViewProps =
+ *std::static_pointer_cast(_props);
+ const auto &newViewProps =
+ *std::static_pointer_cast(props);
+ BOOL isFirstMount = NO;
+ BOOL stylePropChanged = NO;
+ BOOL textChanged = NO;
+
+ if (config == nullptr) {
+ isFirstMount = YES;
+ config = [[EnrichedConfig alloc] init];
+ }
+
+ EnrichedConfig *newConfig = [config copy];
+
+ // color
+ if (newViewProps.color != oldViewProps.color) {
+ if (isColorMeaningful(newViewProps.color)) {
+ UIColor *uiColor = RCTUIColorFromSharedColor(newViewProps.color);
+ [newConfig setPrimaryColor:uiColor];
+ } else {
+ [newConfig setPrimaryColor:nullptr];
+ }
+ stylePropChanged = YES;
+ }
+
+ // fontSize
+ if (newViewProps.fontSize != oldViewProps.fontSize) {
+ if (newViewProps.fontSize) {
+ NSNumber *fontSize = @(newViewProps.fontSize);
+ [newConfig setPrimaryFontSize:fontSize];
+ } else {
+ [newConfig setPrimaryFontSize:nullptr];
+ }
+ stylePropChanged = YES;
+ }
+
+ // fontWeight
+ if (newViewProps.fontWeight != oldViewProps.fontWeight) {
+ if (!newViewProps.fontWeight.empty()) {
+ [newConfig
+ setPrimaryFontWeight:[NSString
+ fromCppString:newViewProps.fontWeight]];
+ } else {
+ [newConfig setPrimaryFontWeight:nullptr];
+ }
+ stylePropChanged = YES;
+ }
+
+ // fontFamily
+ if (newViewProps.fontFamily != oldViewProps.fontFamily) {
+ if (!newViewProps.fontFamily.empty()) {
+ [newConfig
+ setPrimaryFontFamily:[NSString
+ fromCppString:newViewProps.fontFamily]];
+ } else {
+ [newConfig setPrimaryFontFamily:nullptr];
+ }
+ stylePropChanged = YES;
+ }
+
+ // fontStyle
+ if (newViewProps.fontStyle != oldViewProps.fontStyle) {
+ // TODO: Implement fontStyle setter on EnrichedConfig
+ // stylePropChanged = YES;
+ }
+
+ // htmlStyle headings
+ if (newViewProps.htmlStyle.h1.fontSize !=
+ oldViewProps.htmlStyle.h1.fontSize) {
+ [newConfig setH1FontSize:newViewProps.htmlStyle.h1.fontSize];
+ stylePropChanged = YES;
+ }
+
+ if (newViewProps.htmlStyle.h1.bold != oldViewProps.htmlStyle.h1.bold) {
+ [newConfig setH1Bold:newViewProps.htmlStyle.h1.bold];
+
+ // Update style blocks and conflicts for bold
+ if (newViewProps.htmlStyle.h1.bold) {
+ [StyleUtils addStyleBlock:H1 to:Bold forHost:self];
+ [StyleUtils addStyleConflict:Bold to:H1 forHost:self];
+ } else {
+ [StyleUtils removeStyleBlock:H1 from:Bold forHost:self];
+ [StyleUtils removeStyleConflict:Bold from:H1 forHost:self];
+ }
+
+ stylePropChanged = YES;
+ }
+
+ if (newViewProps.htmlStyle.h2.fontSize !=
+ oldViewProps.htmlStyle.h2.fontSize) {
+ [newConfig setH2FontSize:newViewProps.htmlStyle.h2.fontSize];
+ stylePropChanged = YES;
+ }
+
+ if (newViewProps.htmlStyle.h2.bold != oldViewProps.htmlStyle.h2.bold) {
+ [newConfig setH2Bold:newViewProps.htmlStyle.h2.bold];
+
+ // Update style blocks and conflicts for bold
+ if (newViewProps.htmlStyle.h2.bold) {
+ [StyleUtils addStyleBlock:H2 to:Bold forHost:self];
+ [StyleUtils addStyleConflict:Bold to:H2 forHost:self];
+ } else {
+ [StyleUtils removeStyleBlock:H2 from:Bold forHost:self];
+ [StyleUtils removeStyleConflict:Bold from:H2 forHost:self];
+ }
+
+ stylePropChanged = YES;
+ }
+
+ if (newViewProps.htmlStyle.h3.fontSize !=
+ oldViewProps.htmlStyle.h3.fontSize) {
+ [newConfig setH3FontSize:newViewProps.htmlStyle.h3.fontSize];
+ stylePropChanged = YES;
+ }
+
+ if (newViewProps.htmlStyle.h3.bold != oldViewProps.htmlStyle.h3.bold) {
+ [newConfig setH3Bold:newViewProps.htmlStyle.h3.bold];
+
+ // Update style blocks and conflicts for bold
+ if (newViewProps.htmlStyle.h3.bold) {
+ [StyleUtils addStyleBlock:H3 to:Bold forHost:self];
+ [StyleUtils addStyleConflict:Bold to:H3 forHost:self];
+ } else {
+ [StyleUtils removeStyleBlock:H3 from:Bold forHost:self];
+ [StyleUtils removeStyleConflict:Bold from:H3 forHost:self];
+ }
+
+ stylePropChanged = YES;
+ }
+
+ if (newViewProps.htmlStyle.h4.fontSize !=
+ oldViewProps.htmlStyle.h4.fontSize) {
+ [newConfig setH4FontSize:newViewProps.htmlStyle.h4.fontSize];
+ stylePropChanged = YES;
+ }
+
+ if (newViewProps.htmlStyle.h4.bold != oldViewProps.htmlStyle.h4.bold) {
+ [newConfig setH4Bold:newViewProps.htmlStyle.h4.bold];
+
+ // Update style blocks and conflicts for bold
+ if (newViewProps.htmlStyle.h4.bold) {
+ [StyleUtils addStyleBlock:H4 to:Bold forHost:self];
+ [StyleUtils addStyleConflict:Bold to:H4 forHost:self];
+ } else {
+ [StyleUtils removeStyleBlock:H4 from:Bold forHost:self];
+ [StyleUtils removeStyleConflict:Bold from:H4 forHost:self];
+ }
+
+ stylePropChanged = YES;
+ }
+
+ if (newViewProps.htmlStyle.h5.fontSize !=
+ oldViewProps.htmlStyle.h5.fontSize) {
+ [newConfig setH5FontSize:newViewProps.htmlStyle.h5.fontSize];
+ stylePropChanged = YES;
+ }
+
+ if (newViewProps.htmlStyle.h5.bold != oldViewProps.htmlStyle.h5.bold) {
+ [newConfig setH5Bold:newViewProps.htmlStyle.h5.bold];
+
+ // Update style blocks and conflicts for bold
+ if (newViewProps.htmlStyle.h5.bold) {
+ [StyleUtils addStyleBlock:H5 to:Bold forHost:self];
+ [StyleUtils addStyleConflict:Bold to:H5 forHost:self];
+ } else {
+ [StyleUtils removeStyleBlock:H5 from:Bold forHost:self];
+ [StyleUtils removeStyleConflict:Bold from:H5 forHost:self];
+ }
+
+ stylePropChanged = YES;
+ }
+
+ if (newViewProps.htmlStyle.h6.fontSize !=
+ oldViewProps.htmlStyle.h6.fontSize) {
+ [newConfig setH6FontSize:newViewProps.htmlStyle.h6.fontSize];
+ stylePropChanged = YES;
+ }
+
+ if (newViewProps.htmlStyle.h6.bold != oldViewProps.htmlStyle.h6.bold) {
+ [newConfig setH6Bold:newViewProps.htmlStyle.h6.bold];
+
+ // Update style blocks and conflicts for bold
+ if (newViewProps.htmlStyle.h6.bold) {
+ [StyleUtils addStyleBlock:H6 to:Bold forHost:self];
+ [StyleUtils addStyleConflict:Bold to:H6 forHost:self];
+ } else {
+ [StyleUtils removeStyleBlock:H6 from:Bold forHost:self];
+ [StyleUtils removeStyleConflict:Bold from:H6 forHost:self];
+ }
+
+ stylePropChanged = YES;
+ }
+
+ // blockquote
+ if (newViewProps.htmlStyle.blockquote.borderColor !=
+ oldViewProps.htmlStyle.blockquote.borderColor) {
+ if (isColorMeaningful(newViewProps.htmlStyle.blockquote.borderColor)) {
+ [newConfig setBlockquoteBorderColor:RCTUIColorFromSharedColor(
+ newViewProps.htmlStyle.blockquote
+ .borderColor)];
+ stylePropChanged = YES;
+ }
+ }
+ if (newViewProps.htmlStyle.blockquote.borderWidth !=
+ oldViewProps.htmlStyle.blockquote.borderWidth) {
+ [newConfig
+ setBlockquoteBorderWidth:newViewProps.htmlStyle.blockquote.borderWidth];
+ stylePropChanged = YES;
+ }
+ if (newViewProps.htmlStyle.blockquote.gapWidth !=
+ oldViewProps.htmlStyle.blockquote.gapWidth) {
+ [newConfig
+ setBlockquoteGapWidth:newViewProps.htmlStyle.blockquote.gapWidth];
+ stylePropChanged = YES;
+ }
+ if (newViewProps.htmlStyle.blockquote.color !=
+ oldViewProps.htmlStyle.blockquote.color ||
+ isFirstMount) {
+ if (isColorMeaningful(newViewProps.htmlStyle.blockquote.color)) {
+ [newConfig
+ setBlockquoteColor:RCTUIColorFromSharedColor(
+ newViewProps.htmlStyle.blockquote.color)];
+ } else {
+ [newConfig setBlockquoteColor:[newConfig primaryColor]];
+ }
+ stylePropChanged = YES;
+ }
+
+ // inline code
+ if (newViewProps.htmlStyle.code.color != oldViewProps.htmlStyle.code.color) {
+ if (isColorMeaningful(newViewProps.htmlStyle.code.color)) {
+ [newConfig setInlineCodeFgColor:RCTUIColorFromSharedColor(
+ newViewProps.htmlStyle.code.color)];
+ stylePropChanged = YES;
+ }
+ }
+ if (newViewProps.htmlStyle.code.backgroundColor !=
+ oldViewProps.htmlStyle.code.backgroundColor) {
+ if (isColorMeaningful(newViewProps.htmlStyle.code.backgroundColor)) {
+ [newConfig setInlineCodeBgColor:RCTUIColorFromSharedColor(
+ newViewProps.htmlStyle.code
+ .backgroundColor)];
+ stylePropChanged = YES;
+ }
+ }
+
+ // codeblock
+ 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;
+ }
+
+ // ordered list
+ if (newViewProps.htmlStyle.ol.gapWidth !=
+ oldViewProps.htmlStyle.ol.gapWidth) {
+ [newConfig setOrderedListGapWidth:newViewProps.htmlStyle.ol.gapWidth];
+ stylePropChanged = YES;
+ }
+ if (newViewProps.htmlStyle.ol.marginLeft !=
+ oldViewProps.htmlStyle.ol.marginLeft) {
+ [newConfig setOrderedListMarginLeft:newViewProps.htmlStyle.ol.marginLeft];
+ stylePropChanged = YES;
+ }
+ if (newViewProps.htmlStyle.ol.markerFontWeight !=
+ oldViewProps.htmlStyle.ol.markerFontWeight ||
+ isFirstMount) {
+ if (!newViewProps.htmlStyle.ol.markerFontWeight.empty()) {
+ [newConfig
+ setOrderedListMarkerFontWeight:
+ [NSString
+ fromCppString:newViewProps.htmlStyle.ol.markerFontWeight]];
+ } else {
+ [newConfig setOrderedListMarkerFontWeight:[newConfig primaryFontWeight]];
+ }
+ stylePropChanged = YES;
+ }
+ if (newViewProps.htmlStyle.ol.markerColor !=
+ oldViewProps.htmlStyle.ol.markerColor ||
+ isFirstMount) {
+ if (isColorMeaningful(newViewProps.htmlStyle.ol.markerColor)) {
+ [newConfig
+ setOrderedListMarkerColor:RCTUIColorFromSharedColor(
+ newViewProps.htmlStyle.ol.markerColor)];
+ } else {
+ [newConfig setOrderedListMarkerColor:[newConfig primaryColor]];
+ }
+ stylePropChanged = YES;
+ }
+
+ // unordered list
+ if (newViewProps.htmlStyle.ul.bulletColor !=
+ oldViewProps.htmlStyle.ul.bulletColor) {
+ if (isColorMeaningful(newViewProps.htmlStyle.ul.bulletColor)) {
+ [newConfig setUnorderedListBulletColor:RCTUIColorFromSharedColor(
+ newViewProps.htmlStyle.ul
+ .bulletColor)];
+ stylePropChanged = YES;
+ }
+ }
+ if (newViewProps.htmlStyle.ul.bulletSize !=
+ oldViewProps.htmlStyle.ul.bulletSize) {
+ [newConfig setUnorderedListBulletSize:newViewProps.htmlStyle.ul.bulletSize];
+ stylePropChanged = YES;
+ }
+ if (newViewProps.htmlStyle.ul.gapWidth !=
+ oldViewProps.htmlStyle.ul.gapWidth) {
+ [newConfig setUnorderedListGapWidth:newViewProps.htmlStyle.ul.gapWidth];
+ stylePropChanged = YES;
+ }
+ if (newViewProps.htmlStyle.ul.marginLeft !=
+ oldViewProps.htmlStyle.ul.marginLeft) {
+ [newConfig setUnorderedListMarginLeft:newViewProps.htmlStyle.ul.marginLeft];
+ stylePropChanged = YES;
+ }
+
+ // link
+ if (newViewProps.htmlStyle.a.color != oldViewProps.htmlStyle.a.color) {
+ if (isColorMeaningful(newViewProps.htmlStyle.a.color)) {
+ [newConfig setLinkColor:RCTUIColorFromSharedColor(
+ newViewProps.htmlStyle.a.color)];
+ stylePropChanged = YES;
+ }
+ }
+ if (newViewProps.htmlStyle.a.pressColor !=
+ oldViewProps.htmlStyle.a.pressColor) {
+ if (isColorMeaningful(newViewProps.htmlStyle.a.pressColor)) {
+ [newConfig setLinkPressColor:RCTUIColorFromSharedColor(
+ newViewProps.htmlStyle.a.pressColor)];
+ stylePropChanged = YES;
+ }
+ }
+ if (newViewProps.htmlStyle.a.textDecorationLine !=
+ oldViewProps.htmlStyle.a.textDecorationLine) {
+ NSString *objcString =
+ [NSString fromCppString:newViewProps.htmlStyle.a.textDecorationLine];
+ if ([objcString isEqualToString:DecorationUnderline]) {
+ [newConfig setLinkDecorationLine:DecorationUnderline];
+ } else {
+ [newConfig setLinkDecorationLine:DecorationNone];
+ }
+ stylePropChanged = YES;
+ }
+
+ // checkbox list
+ if (newViewProps.htmlStyle.ulCheckbox.boxSize !=
+ oldViewProps.htmlStyle.ulCheckbox.boxSize) {
+ [newConfig
+ setCheckboxListBoxSize:newViewProps.htmlStyle.ulCheckbox.boxSize];
+ stylePropChanged = YES;
+ }
+ if (newViewProps.htmlStyle.ulCheckbox.gapWidth !=
+ oldViewProps.htmlStyle.ulCheckbox.gapWidth) {
+ [newConfig
+ setCheckboxListGapWidth:newViewProps.htmlStyle.ulCheckbox.gapWidth];
+ stylePropChanged = YES;
+ }
+ if (newViewProps.htmlStyle.ulCheckbox.marginLeft !=
+ oldViewProps.htmlStyle.ulCheckbox.marginLeft) {
+ [newConfig
+ setCheckboxListMarginLeft:newViewProps.htmlStyle.ulCheckbox.marginLeft];
+ stylePropChanged = YES;
+ }
+ if (newViewProps.htmlStyle.ulCheckbox.boxColor !=
+ oldViewProps.htmlStyle.ulCheckbox.boxColor) {
+ if (isColorMeaningful(newViewProps.htmlStyle.ulCheckbox.boxColor)) {
+ [newConfig setCheckboxListBoxColor:RCTUIColorFromSharedColor(
+ newViewProps.htmlStyle.ulCheckbox
+ .boxColor)];
+ stylePropChanged = YES;
+ }
+ }
+
+ // mention
+ folly::dynamic oldMentionStyle = oldViewProps.htmlStyle.mention;
+ folly::dynamic newMentionStyle = newViewProps.htmlStyle.mention;
+ if (oldMentionStyle != newMentionStyle) {
+ bool newSingleProps = NO;
+
+ for (const auto &obj : newMentionStyle.items()) {
+ if (obj.second.isInt() || obj.second.isString()) {
+ newSingleProps = YES;
+ break;
+ } else if (obj.second.isObject()) {
+ newSingleProps = NO;
+ break;
+ }
+ }
+
+ if (newSingleProps) {
+ [newConfig setMentionStyleProps:
+ [MentionStyleProps
+ getSinglePropsFromFollyDynamic:newMentionStyle]];
+ } else {
+ [newConfig setMentionStyleProps:
+ [MentionStyleProps
+ getComplexPropsFromFollyDynamic:newMentionStyle]];
+ }
+
+ stylePropChanged = YES;
+ }
+
+ // text prop
+ if (newViewProps.text != oldViewProps.text || isFirstMount) {
+ textChanged = YES;
+ }
+
+ // ellipsizeMode
+ if (newViewProps.ellipsizeMode != oldViewProps.ellipsizeMode ||
+ isFirstMount) {
+ NSString *mode = [NSString fromCppString:newViewProps.ellipsizeMode];
+ if ([mode isEqualToString:@"head"]) {
+ textView.textContainer.lineBreakMode = NSLineBreakByTruncatingHead;
+ } else if ([mode isEqualToString:@"middle"]) {
+ textView.textContainer.lineBreakMode = NSLineBreakByTruncatingMiddle;
+ } else if ([mode isEqualToString:@"clip"]) {
+ textView.textContainer.lineBreakMode = NSLineBreakByClipping;
+ } else {
+ textView.textContainer.lineBreakMode = NSLineBreakByTruncatingTail;
+ }
+ }
+
+ // numberOfLines
+ if (newViewProps.numberOfLines != oldViewProps.numberOfLines ||
+ isFirstMount) {
+ textView.textContainer.maximumNumberOfLines = newViewProps.numberOfLines;
+ }
+
+ // selectable
+ if (newViewProps.selectable != oldViewProps.selectable || isFirstMount) {
+ textView.selectable = newViewProps.selectable;
+ }
+
+ // selectionColor
+ if (newViewProps.selectionColor != oldViewProps.selectionColor) {
+ if (isColorMeaningful(newViewProps.selectionColor)) {
+ textView.tintColor =
+ RCTUIColorFromSharedColor(newViewProps.selectionColor);
+ }
+ }
+
+ if (stylePropChanged) {
+ config = newConfig;
+ }
+
+ [self syncDefaultTypingAttributesFromConfig];
+
+ if (textChanged || stylePropChanged) {
+ [self renderText:[NSString fromCppString:newViewProps.text]];
+ }
+
+ [super updateProps:props oldProps:oldProps];
+ [self tryUpdatingHeight];
+}
+
+- (void)syncDefaultTypingAttributesFromConfig {
+ defaultTypingAttributes[NSForegroundColorAttributeName] =
+ [config primaryColor];
+ defaultTypingAttributes[NSFontAttributeName] = [config primaryFont];
+ defaultTypingAttributes[NSUnderlineColorAttributeName] =
+ [config primaryColor];
+ defaultTypingAttributes[NSStrikethroughColorAttributeName] =
+ [config primaryColor];
+ NSMutableParagraphStyle *pStyle = [[NSMutableParagraphStyle alloc] init];
+ pStyle.minimumLineHeight = [config scaledPrimaryLineHeight];
+ defaultTypingAttributes[NSParagraphStyleAttributeName] = pStyle;
+ textView.typingAttributes = defaultTypingAttributes;
+}
+
+// MARK: - Rendering
+
+- (void)renderText:(NSString *)html {
+ if (html.length == 0) {
+ [textView.textStorage
+ setAttributedString:[[NSAttributedString alloc] initWithString:@""]];
+ return;
+ }
+
+ NSString *normalized = [HtmlParser initiallyProcessHtml:html
+ useHtmlNormalizer:YES];
+ if (normalized == nil) {
+ [textView.textStorage
+ setAttributedString:[[NSAttributedString alloc]
+ initWithString:html
+ attributes:defaultTypingAttributes]];
+ return;
+ }
+
+ NSArray *result = [HtmlParser getTextAndStylesFromHtml:normalized];
+ NSString *plainText = result[0];
+ NSArray *processedStyles = result[1];
+
+ NSMutableAttributedString *body = [[NSMutableAttributedString alloc]
+ initWithString:plainText
+ attributes:defaultTypingAttributes];
+ [textView.textStorage setAttributedString:body];
+ [self applyProcessedStyles:processedStyles];
+ [self layoutAttachments];
+}
+
+- (void)applyProcessedStyles:(NSArray *)processedStyles {
+ // Some paragraph styles (codeblock, blockquote, etc.) insert \u200B
+ // into empty lines, mutating NSTextStorage length. We need to
+ // shift subsequent ranges by this offset.
+ NSInteger zeroWidthSpaceOffset = 0;
+
+ // Inline styles collected during the first pass so their applyStyling: can
+ // be re-run after all paragraph styles have applied their visual attributes.
+ // Each entry is @[style, adjustedRange].
+ NSMutableArray *pendingInlineApply = [NSMutableArray array];
+
+ // Paragraph styles call applyStyling: immediately; inline styles
+ // defer it so that paragraph visual attributes are already in
+ // place when inline styles override them.
+ for (NSArray *arr in processedStyles) {
+ NSNumber *styleType = (NSNumber *)arr[0];
+ StylePair *stylePair = (StylePair *)arr[1];
+ StyleBase *style = stylesDict[styleType];
+ if (style == nullptr)
+ continue;
+
+ NSRange parsedRange = [stylePair.rangeValue rangeValue];
+ NSUInteger textLengthBeforeStyleApplied =
+ textView.textStorage.string.length;
+
+ // Range must be taking zeroWidthSpaceOffset into consideration
+ // because processed styles ranges are relative to only the new text while
+ // we need absolute ranges relative to the whole existing text
+ NSRange styleRange = NSMakeRange(
+ zeroWidthSpaceOffset + parsedRange.location, parsedRange.length);
+
+ if (![StyleUtils handleStyleBlocksAndConflicts:[[style class] getType]
+ range:styleRange
+ forHost:self]) {
+ continue;
+ }
+
+ if ([styleType isEqualToNumber:@([LinkStyle getType])]) {
+ LinkData *linkData = (LinkData *)stylePair.styleValue;
+ [((LinkStyle *)style) applyLinkMetaWithData:linkData range:styleRange];
+ } else if ([styleType isEqualToNumber:@([MentionStyle getType])]) {
+ MentionParams *params = (MentionParams *)stylePair.styleValue;
+ [((MentionStyle *)style) applyMentionMeta:params range:styleRange];
+ } else if ([styleType isEqualToNumber:@([ImageStyle getType])]) {
+ ImageData *imgData = (ImageData *)stylePair.styleValue;
+ [((ImageStyle *)style) addImageAtRange:styleRange
+ imageData:imgData
+ withSelection:NO
+ withDirtyRange:NO];
+ } else if ([styleType isEqualToNumber:@([CheckboxListStyle getType])]) {
+ NSDictionary *checkboxStates = (NSDictionary *)stylePair.styleValue;
+ CheckboxListStyle *cbStyle = (CheckboxListStyle *)style;
+
+ [cbStyle addWithChecked:NO
+ range:styleRange
+ withTyping:NO
+ withDirtyRange:NO];
+
+ if (checkboxStates && checkboxStates.count > 0) {
+ for (NSNumber *key in checkboxStates) {
+ NSUInteger checkboxPosition =
+ zeroWidthSpaceOffset + [key unsignedIntegerValue];
+ BOOL isChecked = [checkboxStates[key] boolValue];
+
+ if (isChecked) {
+ [cbStyle toggleCheckedAt:checkboxPosition withDirtyRange:NO];
+ }
+ }
+ }
+ } else {
+ [style add:styleRange withTyping:NO withDirtyRange:NO];
+ }
+
+ [ZeroWidthSpaceUtils addSpacesIfNeededinInput:self inRange:styleRange];
+
+ NSInteger delta =
+ textView.textStorage.string.length - textLengthBeforeStyleApplied;
+
+ // Use an adjusted range so that applyStyling covers any ZWS characters that
+ // were just inserted by addSpacesIfNeededinInput:inRange:. Without this, a
+ // style applied to an empty range {0,0} would call applyStyling on {0,0}
+ // even after a ZWS was inserted.
+ NSRange adjustedStyleRange = NSMakeRange(
+ styleRange.location, styleRange.length + (NSUInteger)MAX(0LL, delta));
+
+ if ([style isParagraph]) {
+ [style applyStyling:adjustedStyleRange];
+ } else {
+ [pendingInlineApply
+ addObject:@[ style, [NSValue valueWithRange:adjustedStyleRange] ]];
+ }
+
+ // Image shifts are already handled by _precedingImageCount during tag
+ // finalization.
+ if (delta != 0 && ![styleType isEqualToNumber:@([ImageStyle getType])]) {
+ zeroWidthSpaceOffset += delta;
+ }
+ }
+
+ // Apply visual styling for inline styles
+ for (NSArray *entry in pendingInlineApply) {
+ StyleBase *style = entry[0];
+ NSRange adjustedStyleRange = [((NSValue *)entry[1]) rangeValue];
+ [style applyStyling:adjustedStyleRange];
+ }
+}
+
+// MARK: - Measuring and state
+
+- (CGSize)measureSize:(CGFloat)maxWidth {
+ if (textView.textStorage.length == 0) {
+ return CGSizeMake(maxWidth, 0);
+ }
+
+ NSMutableAttributedString *currentStr = [[NSMutableAttributedString alloc]
+ initWithAttributedString:textView.textStorage];
+
+ // edge case: input with only a zero width space should still be of a height
+ // of a single line, so we add a mock "I" character
+ if ([currentStr length] == 1 &&
+ [[currentStr.string substringWithRange:NSMakeRange(0, 1)]
+ isEqualToString:@"\u200B"]) {
+ [currentStr
+ appendAttributedString:[[NSAttributedString alloc]
+ initWithString:@"I"
+ attributes:defaultTypingAttributes]];
+ }
+
+ // edge case: trailing newlines aren't counted towards height calculations, so
+ // we add a mock "I" character
+ if (currentStr.length > 0) {
+ unichar lastChar =
+ [currentStr.string characterAtIndex:currentStr.length - 1];
+ if ([[NSCharacterSet newlineCharacterSet] characterIsMember:lastChar]) {
+ [currentStr
+ appendAttributedString:[[NSAttributedString alloc]
+ initWithString:@"I"
+ attributes:defaultTypingAttributes]];
+ }
+ }
+
+ CGRect boundingBox =
+ [currentStr boundingRectWithSize:CGSizeMake(maxWidth, CGFLOAT_MAX)
+ options:NSStringDrawingUsesLineFragmentOrigin |
+ NSStringDrawingUsesFontLeading
+ context:nullptr];
+
+ return CGSizeMake(maxWidth, ceil(boundingBox.size.height));
+}
+
+- (void)updateState:(State::Shared const &)state
+ oldState:(State::Shared const &)oldState {
+ _state =
+ std::static_pointer_cast(
+ state);
+
+ if (oldState == nullptr) {
+ [self tryUpdatingHeight];
+ }
+}
+
+- (void)tryUpdatingHeight {
+ if (_state == nullptr) {
+ return;
+ }
+ auto selfRef = wrapManagedObjectWeakly(self);
+ _state->updateState(EnrichedTextViewState(selfRef));
+}
+
+- (std::shared_ptr)getEventEmitter {
+ if (_eventEmitter != nullptr) {
+ auto emitter =
+ static_cast(*_eventEmitter);
+ return std::make_shared(emitter);
+ }
+ return nullptr;
+}
+
+- (void)emitOnLinkPressEvent:(NSString *)url {
+ if (!url)
+ return;
+ auto emitter = [self getEventEmitter];
+ if (emitter != nullptr) {
+ emitter->onLinkPress({.url = [url toCppString]});
+ }
+}
+
+- (void)emitOnMentionPressEvent:(MentionParams *)mention {
+ auto emitter = [self getEventEmitter];
+ if (emitter != nullptr) {
+ folly::dynamic attrsObj = folly::dynamic::object;
+ if (mention.attributes != nil) {
+ NSData *data =
+ [mention.attributes dataUsingEncoding:NSUTF8StringEncoding];
+ NSError *error = nil;
+ NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:data
+ options:0
+ error:&error];
+ if (error != nil) {
+ NSLog(@"[EnrichedTextView] Failed to parse mention attributes JSON: %@",
+ error);
+ return;
+ }
+
+ for (NSString *key in dict) {
+ id val = dict[key];
+ if ([val isKindOfClass:[NSString class]]) {
+ attrsObj[[key toCppString]] = [val toCppString];
+ }
+ }
+ }
+
+ emitter->onMentionPress({
+ .text = mention.text ? [mention.text toCppString] : std::string{},
+ .indicator =
+ mention.indicator ? [mention.indicator toCppString] : std::string{},
+ .attributes = attrsObj,
+ });
+ }
+}
+
+// MARK: - Media attachments delegate
+
+- (void)mediaAttachmentDidUpdate:(MediaAttachment *)attachment {
+ [AttachmentLayoutUtils handleAttachmentUpdate:attachment
+ textView:textView
+ onLayoutBlock:^{
+ [self layoutAttachments];
+ [self tryUpdatingHeight];
+ }];
+}
+
+- (void)layoutAttachments {
+ _attachmentViews =
+ [AttachmentLayoutUtils layoutAttachmentsInTextView:textView
+ config:config
+ existingViews:_attachmentViews];
+}
+
+@end
diff --git a/ios/EnrichedTextViewManager.mm b/ios/EnrichedTextViewManager.mm
new file mode 100644
index 00000000..2e65c1c3
--- /dev/null
+++ b/ios/EnrichedTextViewManager.mm
@@ -0,0 +1,13 @@
+#import
+#import
+
+@interface EnrichedTextViewManager : RCTViewManager
+@end
+
+@implementation EnrichedTextViewManager
+
+RCT_EXPORT_MODULE(EnrichedTextView)
+
+RCT_EXPORT_VIEW_PROPERTY(text, NSString)
+
+@end
diff --git a/ios/config/InputConfig.h b/ios/config/EnrichedConfig.h
similarity index 96%
rename from ios/config/InputConfig.h
rename to ios/config/EnrichedConfig.h
index 73685213..44a7c910 100644
--- a/ios/config/InputConfig.h
+++ b/ios/config/EnrichedConfig.h
@@ -4,7 +4,7 @@
#import "TextDecorationLineEnum.h"
#import
-@interface InputConfig : NSObject
+@interface EnrichedConfig : NSObject
- (instancetype)init;
- (UIColor *)primaryColor;
- (void)setPrimaryColor:(UIColor *)newValue;
@@ -101,4 +101,8 @@
- (void)setCheckboxListBoxColor:(UIColor *)newValue;
- (UIImage *)checkboxCheckedImage;
- (UIImage *)checkboxUncheckedImage;
+
+// MARK: - Text only props
+- (UIColor *)linkPressColor;
+- (void)setLinkPressColor:(UIColor *)newValue;
@end
diff --git a/ios/config/InputConfig.mm b/ios/config/EnrichedConfig.mm
similarity index 97%
rename from ios/config/InputConfig.mm
rename to ios/config/EnrichedConfig.mm
index 6bc55d7f..327bddd5 100644
--- a/ios/config/InputConfig.mm
+++ b/ios/config/EnrichedConfig.mm
@@ -1,7 +1,7 @@
-#import
+#import
#import
-@implementation InputConfig {
+@implementation EnrichedConfig {
UIColor *_primaryColor;
NSNumber *_primaryFontSize;
CGFloat _primaryLineHeight;
@@ -54,6 +54,9 @@ @implementation InputConfig {
UIColor *_checkboxListBoxColor;
UIImage *_checkboxCheckedImage;
UIImage *_checkboxUncheckedImage;
+
+ // text only
+ UIColor *_linkPressColor;
}
- (instancetype)init {
@@ -65,7 +68,7 @@ - (instancetype)init {
}
- (id)copyWithZone:(NSZone *)zone {
- InputConfig *copy = [[[self class] allocWithZone:zone] init];
+ EnrichedConfig *copy = [[[self class] allocWithZone:zone] init];
copy->_primaryColor = [_primaryColor copy];
copy->_primaryFontSize = [_primaryFontSize copy];
copy->_primaryLineHeight = _primaryLineHeight;
@@ -115,6 +118,9 @@ - (id)copyWithZone:(NSZone *)zone {
copy->_checkboxListBoxColor = [_checkboxListBoxColor copy];
copy->_checkboxCheckedImage = _checkboxCheckedImage;
copy->_checkboxUncheckedImage = _checkboxUncheckedImage;
+
+ // text only
+ copy->_linkPressColor = [_linkPressColor copy];
return copy;
}
@@ -661,4 +667,14 @@ - (UIImage *)generateCheckboxImage:(BOOL)isChecked {
return result;
}
+// MARK: - Text only props
+
+- (UIColor *)linkPressColor {
+ return _linkPressColor;
+}
+
+- (void)setLinkPressColor:(UIColor *)newValue {
+ _linkPressColor = newValue;
+}
+
@end
diff --git a/ios/extensions/LayoutManagerExtension.mm b/ios/extensions/LayoutManagerExtension.mm
index 58f4620a..5a9b1797 100644
--- a/ios/extensions/LayoutManagerExtension.mm
+++ b/ios/extensions/LayoutManagerExtension.mm
@@ -1,6 +1,6 @@
#import "LayoutManagerExtension.h"
#import "ColorExtension.h"
-#import "EnrichedTextInputView.h"
+#import "EnrichedViewHost.h"
#import "RangeUtils.h"
#import "StyleHeaders.h"
#import "WeakBox.h"
@@ -49,28 +49,23 @@ - (void)my_drawBackgroundForGlyphRange:(NSRange)glyphRange
atPoint:(CGPoint)origin {
[self my_drawBackgroundForGlyphRange:glyphRange atPoint:origin];
- EnrichedTextInputView *typedInput = (EnrichedTextInputView *)self.input;
- if (typedInput == nullptr) {
+ id host = self.input;
+ if (host == nullptr) {
return;
}
NSRange visibleCharRange = [self characterRangeForGlyphRange:glyphRange
actualGlyphRange:NULL];
- [self drawBlockQuotes:typedInput
- origin:origin
- visibleCharRange:visibleCharRange];
- [self drawLists:typedInput origin:origin visibleCharRange:visibleCharRange];
- [self drawCodeBlocks:typedInput
- origin:origin
- visibleCharRange:visibleCharRange];
+ [self drawBlockQuotes:host origin:origin visibleCharRange:visibleCharRange];
+ [self drawLists:host origin:origin visibleCharRange:visibleCharRange];
+ [self drawCodeBlocks:host origin:origin visibleCharRange:visibleCharRange];
}
-- (void)drawCodeBlocks:(EnrichedTextInputView *)typedInput
+- (void)drawCodeBlocks:(id)host
origin:(CGPoint)origin
visibleCharRange:(NSRange)visibleCharRange {
- CodeBlockStyle *codeBlockStyle =
- typedInput->stylesDict[@([CodeBlockStyle getType])];
+ CodeBlockStyle *codeBlockStyle = host.stylesDict[@([CodeBlockStyle getType])];
if (codeBlockStyle == nullptr) {
return;
}
@@ -78,9 +73,9 @@ - (void)drawCodeBlocks:(EnrichedTextInputView *)typedInput
NSArray *allCodeBlocks = [codeBlockStyle all:visibleCharRange];
NSArray *mergedCodeBlocks =
[self mergeContiguousStylePairs:allCodeBlocks];
- UIColor *bgColor = [[typedInput->config codeBlockBgColor]
- colorWithAlphaIfNotTransparent:0.4];
- CGFloat radius = [typedInput->config codeBlockBorderRadius];
+ UIColor *bgColor =
+ [[host.config codeBlockBgColor] colorWithAlphaIfNotTransparent:0.4];
+ CGFloat radius = [host.config codeBlockBorderRadius];
[bgColor setFill];
for (StylePair *pair in mergedCodeBlocks) {
@@ -89,7 +84,7 @@ - (void)drawCodeBlocks:(EnrichedTextInputView *)typedInput
continue;
NSArray *paragraphs =
- [RangeUtils getSeparateParagraphsRangesIn:typedInput->textView
+ [RangeUtils getSeparateParagraphsRangesIn:host.textView
range:blockCharacterRange];
if (paragraphs.count == 0)
continue;
@@ -204,11 +199,10 @@ - (void)drawCodeBlocks:(EnrichedTextInputView *)typedInput
return mergedPairs;
}
-- (void)drawBlockQuotes:(EnrichedTextInputView *)typedInput
+- (void)drawBlockQuotes:(id)host
origin:(CGPoint)origin
visibleCharRange:(NSRange)visibleCharRange {
- BlockQuoteStyle *bqStyle =
- typedInput->stylesDict[@([BlockQuoteStyle getType])];
+ BlockQuoteStyle *bqStyle = host.stylesDict[@([BlockQuoteStyle getType])];
if (bqStyle == nullptr) {
return;
}
@@ -216,7 +210,7 @@ - (void)drawBlockQuotes:(EnrichedTextInputView *)typedInput
NSArray *allBlockquotes = [bqStyle all:visibleCharRange];
for (StylePair *pair in allBlockquotes) {
- NSRange paragraphRange = [typedInput->textView.textStorage.string
+ NSRange paragraphRange = [host.textView.textStorage.string
paragraphRangeForRange:[pair.rangeValue rangeValue]];
NSRange paragraphGlyphRange =
[self glyphRangeForCharacterRange:paragraphRange
@@ -232,48 +226,47 @@ - (void)drawBlockQuotes:(EnrichedTextInputView *)typedInput
CGFloat x = paddingLeft;
CGFloat y = paddingTop + rect.origin.y;
CGFloat width =
- [typedInput
- ->config blockquoteBorderWidth];
+ [host.config blockquoteBorderWidth];
CGFloat height = rect.size.height;
CGRect lineRect =
CGRectMake(x, y, width, height);
- [[typedInput->config blockquoteBorderColor]
+ [[host.config blockquoteBorderColor]
setFill];
UIRectFill(lineRect);
}];
}
}
-- (void)drawLists:(EnrichedTextInputView *)typedInput
+- (void)drawLists:(id)host
origin:(CGPoint)origin
visibleCharRange:(NSRange)visibleCharRange {
UnorderedListStyle *ulStyle =
- typedInput->stylesDict[@([UnorderedListStyle getType])];
- OrderedListStyle *olStyle =
- typedInput->stylesDict[@([OrderedListStyle getType])];
- CheckboxListStyle *cbStyle =
- typedInput->stylesDict[@([CheckboxListStyle getType])];
- if (ulStyle == nullptr || olStyle == nullptr || cbStyle == nullptr) {
- return;
- }
+ host.stylesDict[@([UnorderedListStyle getType])];
+ OrderedListStyle *olStyle = host.stylesDict[@([OrderedListStyle getType])];
+ CheckboxListStyle *cbStyle = host.stylesDict[@([CheckboxListStyle getType])];
NSMutableArray *allLists = [[NSMutableArray alloc] init];
- [allLists addObjectsFromArray:[ulStyle all:visibleCharRange]];
- [allLists addObjectsFromArray:[olStyle all:visibleCharRange]];
- [allLists addObjectsFromArray:[cbStyle all:visibleCharRange]];
+ if (ulStyle != nullptr) {
+ [allLists addObjectsFromArray:[ulStyle all:visibleCharRange]];
+ }
+ if (olStyle != nullptr) {
+ [allLists addObjectsFromArray:[olStyle all:visibleCharRange]];
+ }
+ if (cbStyle != nullptr) {
+ [allLists addObjectsFromArray:[cbStyle all:visibleCharRange]];
+ }
for (StylePair *pair in allLists) {
NSParagraphStyle *pStyle = (NSParagraphStyle *)pair.styleValue;
NSDictionary *markerAttributes = @{
- NSFontAttributeName : [typedInput->config orderedListMarkerFont],
- NSForegroundColorAttributeName :
- [typedInput->config orderedListMarkerColor]
+ NSFontAttributeName : [host.config orderedListMarkerFont],
+ NSForegroundColorAttributeName : [host.config orderedListMarkerColor]
};
NSArray *paragraphs =
- [RangeUtils getSeparateParagraphsRangesIn:typedInput->textView
+ [RangeUtils getSeparateParagraphsRangesIn:host.textView
range:[pair.rangeValue rangeValue]];
for (NSValue *paragraph in paragraphs) {
@@ -290,11 +283,10 @@ - (void)drawLists:(EnrichedTextInputView *)typedInput
NSUInteger charIdx =
[self characterIndexForGlyphAtIndex:
lineGlyphRange.location];
- UIFont *font =
- [typedInput->textView.textStorage
- attribute:NSFontAttributeName
- atIndex:charIdx
- effectiveRange:nil];
+ UIFont *font = [host.textView.textStorage
+ attribute:NSFontAttributeName
+ atIndex:charIdx
+ effectiveRange:nil];
CGRect textUsedRect =
[self getTextAlignedUsedRect:usedRect
font:font];
@@ -307,9 +299,9 @@ - (void)drawLists:(EnrichedTextInputView *)typedInput
isEqualToString:
@"EnrichedOrderedList"]) {
NSString *marker = [self
- getDecimalMarkerForList:typedInput
+ getDecimalMarkerForList:host
charIndex:charIdx];
- [self drawDecimal:typedInput
+ [self drawDecimal:host
marker:marker
markerAttributes:markerAttributes
origin:origin
@@ -318,13 +310,13 @@ - (void)drawLists:(EnrichedTextInputView *)typedInput
isEqualToString:
@"EnrichedUnorderedLis"
@"t"]) {
- [self drawBullet:typedInput
+ [self drawBullet:host
origin:origin
usedRect:textUsedRect];
} else if ([markerFormat
hasPrefix:
@"EnrichedCheckbox"]) {
- [self drawCheckbox:typedInput
+ [self drawCheckbox:host
markerFormat:markerFormat
origin:origin
usedRect:textUsedRect];
@@ -337,16 +329,15 @@ - (void)drawLists:(EnrichedTextInputView *)typedInput
}
}
-- (NSString *)getDecimalMarkerForList:(EnrichedTextInputView *)input
+- (NSString *)getDecimalMarkerForList:(id)host
charIndex:(NSUInteger)index {
- NSString *fullText = input->textView.textStorage.string;
+ NSString *fullText = host.textView.textStorage.string;
NSInteger itemNumber = 1;
NSRange currentParagraph =
[fullText paragraphRangeForRange:NSMakeRange(index, 0)];
if (currentParagraph.location > 0) {
- OrderedListStyle *olStyle =
- input->stylesDict[@([OrderedListStyle getType])];
+ OrderedListStyle *olStyle = host.stylesDict[@([OrderedListStyle getType])];
NSInteger prevParagraphsCount = 0;
NSInteger recentParagraphLocation =
@@ -392,16 +383,16 @@ - (CGRect)getTextAlignedUsedRect:(CGRect)usedRect font:(UIFont *)font {
return usedRect;
}
-- (void)drawCheckbox:(EnrichedTextInputView *)typedInput
+- (void)drawCheckbox:(id)host
markerFormat:(NSString *)markerFormat
origin:(CGPoint)origin
usedRect:(CGRect)usedRect {
BOOL isChecked = [markerFormat isEqualToString:@"EnrichedCheckbox1"];
- UIImage *image = isChecked ? typedInput->config.checkboxCheckedImage
- : typedInput->config.checkboxUncheckedImage;
- CGFloat gapWidth = [typedInput->config checkboxListGapWidth];
- CGFloat boxSize = [typedInput->config checkboxListBoxSize];
+ UIImage *image = isChecked ? host.config.checkboxCheckedImage
+ : host.config.checkboxUncheckedImage;
+ CGFloat gapWidth = [host.config checkboxListGapWidth];
+ CGFloat boxSize = [host.config checkboxListBoxSize];
CGFloat centerY = CGRectGetMidY(usedRect) + origin.y;
CGFloat boxX = origin.x + usedRect.origin.x - gapWidth - boxSize;
@@ -410,18 +401,18 @@ - (void)drawCheckbox:(EnrichedTextInputView *)typedInput
[image drawAtPoint:CGPointMake(boxX, boxY)];
}
-- (void)drawBullet:(EnrichedTextInputView *)typedInput
+- (void)drawBullet:(id)host
origin:(CGPoint)origin
usedRect:(CGRect)usedRect {
- CGFloat gapWidth = [typedInput->config unorderedListGapWidth];
- CGFloat bulletSize = [typedInput->config unorderedListBulletSize];
+ CGFloat gapWidth = [host.config unorderedListGapWidth];
+ CGFloat bulletSize = [host.config unorderedListBulletSize];
CGFloat bulletX = origin.x + usedRect.origin.x - gapWidth - bulletSize / 2;
CGFloat centerY = CGRectGetMidY(usedRect) + origin.y;
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSaveGState(context);
{
- [[typedInput->config unorderedListBulletColor] setFill];
+ [[host.config unorderedListBulletColor] setFill];
CGContextAddArc(context, bulletX, centerY, bulletSize / 2, 0, 2 * M_PI,
YES);
CGContextFillPath(context);
@@ -429,12 +420,12 @@ - (void)drawBullet:(EnrichedTextInputView *)typedInput
CGContextRestoreGState(context);
}
-- (void)drawDecimal:(EnrichedTextInputView *)typedInput
+- (void)drawDecimal:(id)host
marker:(NSString *)marker
markerAttributes:(NSDictionary *)markerAttributes
origin:(CGPoint)origin
usedRect:(CGRect)usedRect {
- CGFloat gapWidth = [typedInput->config orderedListGapWidth];
+ CGFloat gapWidth = [host.config orderedListGapWidth];
CGSize markerSize = [marker sizeWithAttributes:markerAttributes];
CGFloat markerX = usedRect.origin.x - gapWidth - markerSize.width / 2;
CGFloat centerY = CGRectGetMidY(usedRect) + origin.y;
diff --git a/ios/htmlParser/HtmlParser.h b/ios/htmlParser/HtmlParser.h
new file mode 100644
index 00000000..7453828c
--- /dev/null
+++ b/ios/htmlParser/HtmlParser.h
@@ -0,0 +1,8 @@
+#pragma once
+#import
+
+@interface HtmlParser : NSObject
++ (NSString *_Nullable)initiallyProcessHtml:(NSString *_Nonnull)html
+ useHtmlNormalizer:(BOOL)useHtmlNormalizer;
++ (NSArray *_Nonnull)getTextAndStylesFromHtml:(NSString *_Nonnull)fixedHtml;
+@end
diff --git a/ios/htmlParser/HtmlParser.mm b/ios/htmlParser/HtmlParser.mm
new file mode 100644
index 00000000..ee6919e6
--- /dev/null
+++ b/ios/htmlParser/HtmlParser.mm
@@ -0,0 +1,787 @@
+#import "HtmlParser.h"
+#import "ImageData.h"
+#import "LinkData.h"
+#import "MentionParams.h"
+#import "StringExtension.h"
+#import "StyleHeaders.h"
+#import "StylePair.h"
+
+#include "GumboParser.hpp"
+
+@implementation HtmlParser
+
++ (BOOL)isBlockTag:(NSString *)tagName {
+ return [tagName isEqualToString:@"ul"] || [tagName isEqualToString:@"ol"] ||
+ [tagName isEqualToString:@"blockquote"] ||
+ [tagName isEqualToString:@"codeblock"];
+}
+
+/**
+ * Prepares HTML for the parser by stripping extraneous whitespace and newlines
+ * from structural tags, while preserving them within text content.
+ *
+ * APPROACH:
+ * This function treats the HTML as having two distinct states:
+ * 1. Structure Mode (Depth == 0): We are inside or between container tags (like
+ * blockquote, ul, codeblock). In this mode whitespace and newlines are
+ * considered layout artifacts and are REMOVED to prevent the parser from
+ * creating unwanted spaces.
+ * 2. Content Mode (Depth > 0): We are inside a text-containing tag (like p,
+ * b, li). In this mode, all whitespace is PRESERVED exactly as is, ensuring
+ * that sentences and inline formatting remain readable.
+ *
+ * The function iterates character-by-character, using a depth counter to track
+ * nesting levels of the specific tags defined in `textTags`.
+ *
+ * IMPORTANT:
+ * The `textTags` set acts as a whitelist for "Content Mode". If you add support
+ * for a new HTML tag that contains visible text (e.g., h4, h5, h6),
+ * you MUST add it to the `textTags` set below.
+ */
++ (NSString *)stripExtraWhiteSpacesAndNewlines:(NSString *)html {
+ NSSet *textTags = [NSSet setWithObjects:@"p", @"h1", @"h2", @"h3", @"h4",
+ @"h5", @"h6", @"li", @"b", @"a", @"s",
+ @"mention", @"code", @"u", @"i", nil];
+
+ NSMutableString *output = [NSMutableString stringWithCapacity:html.length];
+ NSMutableString *currentTagBuffer = [NSMutableString string];
+ NSCharacterSet *whitespaceAndNewlineSet =
+ [NSCharacterSet whitespaceAndNewlineCharacterSet];
+
+ BOOL isReadingTag = NO;
+ NSInteger textDepth = 0;
+
+ for (NSUInteger i = 0; i < html.length; i++) {
+ unichar c = [html characterAtIndex:i];
+
+ if (c == '<') {
+ isReadingTag = YES;
+ [currentTagBuffer setString:@""];
+ [output appendString:@"<"];
+ } else if (c == '>') {
+ isReadingTag = NO;
+ [output appendString:@">"];
+
+ NSString *fullTag = [currentTagBuffer lowercaseString];
+
+ NSString *cleanName = [fullTag
+ stringByTrimmingCharactersInSet:
+ [NSCharacterSet characterSetWithCharactersInString:@"/"]];
+ NSArray *parts =
+ [cleanName componentsSeparatedByCharactersInSet:
+ [NSCharacterSet whitespaceAndNewlineCharacterSet]];
+ NSString *tagName = parts.firstObject;
+
+ if (![textTags containsObject:tagName]) {
+ continue;
+ }
+
+ if ([fullTag hasPrefix:@"/"]) {
+ textDepth--;
+ if (textDepth < 0)
+ textDepth = 0;
+ } else {
+ // Opening tag (e.g. ) -> Enter Text Mode
+ // (Ignore self-closing tags like
if they happen to be in the
+ // list)
+ if (![fullTag hasSuffix:@"/"]) {
+ textDepth++;
+ }
+ }
+ } else {
+ if (isReadingTag) {
+ [currentTagBuffer appendFormat:@"%C", c];
+ [output appendFormat:@"%C", c];
+ continue;
+ }
+
+ if (textDepth > 0) {
+ [output appendFormat:@"%C", c];
+ } else {
+ if (![whitespaceAndNewlineSet characterIsMember:c]) {
+ [output appendFormat:@"%C", c];
+ }
+ }
+ }
+ }
+
+ return output;
+}
+
++ (NSString *)stringByAddingNewlinesToTag:(NSString *)tag
+ inString:(NSString *)html
+ leading:(BOOL)leading
+ trailing:(BOOL)trailing {
+ NSString *str = [html copy];
+ if (leading) {
+ NSString *formattedTag = [NSString stringWithFormat:@">%@", tag];
+ NSString *formattedNewTag = [NSString stringWithFormat:@">\n%@", tag];
+ str = [str stringByReplacingOccurrencesOfString:formattedTag
+ withString:formattedNewTag];
+ }
+ if (trailing) {
+ NSString *formattedTag = [NSString stringWithFormat:@"%@<", tag];
+ NSString *formattedNewTag = [NSString stringWithFormat:@"%@\n<", tag];
+ str = [str stringByReplacingOccurrencesOfString:formattedTag
+ withString:formattedNewTag];
+ }
+ return str;
+}
+
+#pragma mark - External HTML normalization
+
+/**
+ * Normalizes external HTML (from Google Docs, Word, web pages, etc.) into our
+ * canonical tag subset using the Gumbo-based C++ normalizer.
+ *
+ * Converts: strong → b, em → i, span style="font-weight:bold" → b,
+ * strips unknown tags while preserving text
+ */
++ (NSString *_Nullable)normalizeExternalHtml:(NSString *_Nonnull)html {
+ std::string result =
+ GumboParser::normalizeHtml(std::string([html UTF8String]));
+ if (result.empty())
+ return nil;
+ return [NSString stringWithUTF8String:result.c_str()];
+}
+
++ (void)finalizeTagEntry:(NSMutableString *)tagName
+ ongoingTags:(NSMutableDictionary *)ongoingTags
+ initiallyProcessedTags:(NSMutableArray *)processedTags
+ plainText:(NSMutableString *)plainText
+ precedingImageCount:(NSInteger *)precedingImageCount {
+ NSMutableArray *tagEntry = [[NSMutableArray alloc] init];
+
+ NSArray *tagData = ongoingTags[tagName];
+ NSInteger tagLocation = [((NSNumber *)tagData[0]) intValue];
+ NSInteger openImageCount = [((NSNumber *)tagData[1]) intValue];
+ NSInteger currentImageCount = *precedingImageCount;
+
+ // 'plainText' doesn't contain image placeholders yet, but the final
+ // NSTextStorage will, so each image adds one character that ranges here
+ // must account for. 'openImageCount' (captured when the tag opened) shifts
+ // the start past images finalized BEFORE this tag, while the diff against
+ // 'currentImageCount' extends the length to cover images finalized INSIDE
+ // it.
+ NSRange tagRange = NSMakeRange(tagLocation + openImageCount,
+ (plainText.length - tagLocation) +
+ (currentImageCount - openImageCount));
+
+ [tagEntry addObject:[tagName copy]];
+ [tagEntry addObject:[NSValue valueWithRange:tagRange]];
+ if (tagData.count > 2) {
+ [tagEntry addObject:[(NSString *)tagData[2] copy]];
+ }
+
+ [processedTags addObject:tagEntry];
+ [ongoingTags removeObjectForKey:tagName];
+
+ if ([tagName isEqualToString:@"img"]) {
+ (*precedingImageCount)++;
+ }
+}
+
++ (BOOL)isUlCheckboxList:(NSString *)params {
+ return ([params containsString:@"data-type=\"checkbox\""] ||
+ [params containsString:@"data-type='checkbox'"]);
+}
+
++ (NSDictionary *)prepareCheckboxListStyleValue:(NSValue *)rangeValue
+ checkboxStates:(NSDictionary *)checkboxStates {
+ NSRange range = [rangeValue rangeValue];
+ NSMutableDictionary *statesInRange = [[NSMutableDictionary alloc] init];
+
+ for (NSNumber *key in checkboxStates) {
+ NSUInteger pos = [key unsignedIntegerValue];
+ if (pos >= range.location && pos < range.location + range.length) {
+ [statesInRange setObject:checkboxStates[key] forKey:key];
+ }
+ }
+
+ return statesInRange;
+}
+
++ (NSString *_Nullable)initiallyProcessHtml:(NSString *_Nonnull)html
+ useHtmlNormalizer:(BOOL)useHtmlNormalizer {
+ NSString *htmlWithoutSpaces = [self stripExtraWhiteSpacesAndNewlines:html];
+ NSString *fixedHtml = nullptr;
+
+ if (htmlWithoutSpaces.length >= 13) {
+ NSString *firstSix =
+ [htmlWithoutSpaces substringWithRange:NSMakeRange(0, 6)];
+ NSString *lastSeven = [htmlWithoutSpaces
+ substringWithRange:NSMakeRange(htmlWithoutSpaces.length - 7, 7)];
+
+ if ([firstSix isEqualToString:@""] &&
+ [lastSeven isEqualToString:@""]) {
+ // remove html tags, might be with newlines or without them
+ fixedHtml = [htmlWithoutSpaces copy];
+ // firstly remove newlined html tags if any:
+ fixedHtml = [fixedHtml stringByReplacingOccurrencesOfString:@"\n"
+ withString:@""];
+ fixedHtml = [fixedHtml stringByReplacingOccurrencesOfString:@"\n"
+ withString:@""];
+ // fallback; remove html tags without their newlines
+ fixedHtml = [fixedHtml stringByReplacingOccurrencesOfString:@""
+ withString:@""];
+ fixedHtml = [fixedHtml stringByReplacingOccurrencesOfString:@""
+ withString:@""];
+ } else if (useHtmlNormalizer) {
+ // External HTML (from Google Docs, Word, web pages, etc.)
+ // Run through the Gumbo-based normalizer to convert arbitrary HTML
+ // into our canonical tag subset.
+ NSString *normalized = [self normalizeExternalHtml:html];
+ if (normalized != nil) {
+ fixedHtml = normalized;
+ }
+ }
+
+ // Additionally, try getting the content from between body tags if there are
+ // some:
+
+ // Firstly make sure there are no newlines between them.
+ fixedHtml = [fixedHtml stringByReplacingOccurrencesOfString:@"\n"
+ withString:@""];
+ fixedHtml = [fixedHtml stringByReplacingOccurrencesOfString:@"\n"
+ withString:@""];
+ // Then, if there actually are body tags, use the content between them.
+ NSRange openingBodyRange = [htmlWithoutSpaces rangeOfString:@""];
+ NSRange closingBodyRange = [htmlWithoutSpaces rangeOfString:@""];
+ if (openingBodyRange.length != 0 && closingBodyRange.length != 0) {
+ NSInteger newStart = openingBodyRange.location + 6;
+ NSInteger newEnd = closingBodyRange.location - 1;
+ fixedHtml = [htmlWithoutSpaces
+ substringWithRange:NSMakeRange(newStart, newEnd - newStart + 1)];
+ }
+ }
+
+ // second processing - try fixing htmls with wrong newlines' setup
+ if (fixedHtml != nullptr) {
+ // add
tag wherever needed
+ fixedHtml = [fixedHtml stringByReplacingOccurrencesOfString:@"
"
+ withString:@"
"];
+
+ // remove tags inside of
+ fixedHtml = [fixedHtml stringByReplacingOccurrencesOfString:@""
+ withString:@"
"];
+ fixedHtml = [fixedHtml stringByReplacingOccurrencesOfString:@"
"
+ withString:@""];
+
+ // change
to
+ fixedHtml = [fixedHtml stringByReplacingOccurrencesOfString:@"
"
+ withString:@"
"];
+
+ // remove tags around
+ fixedHtml = [fixedHtml stringByReplacingOccurrencesOfString:@"
"
+ withString:@"
"];
+ fixedHtml = [fixedHtml stringByReplacingOccurrencesOfString:@"
"
+ withString:@"
"];
+
+ // add
tags inside empty blockquote and codeblock tags
+ fixedHtml = [fixedHtml
+ stringByReplacingOccurrencesOfString:@""
+ withString:@"
"
+ @"blockquote>"];
+ fixedHtml = [fixedHtml
+ stringByReplacingOccurrencesOfString:@""
+ withString:@"
"];
+
+ // remove empty ul and ol tags
+ fixedHtml = [fixedHtml stringByReplacingOccurrencesOfString:@""
+ withString:@""];
+ fixedHtml = [fixedHtml
+ stringByReplacingOccurrencesOfString:@""
+ withString:@""];
+ fixedHtml = [fixedHtml stringByReplacingOccurrencesOfString:@"
"
+ withString:@""];
+
+ // tags that have to be in separate lines
+ fixedHtml = [self stringByAddingNewlinesToTag:@"
"
+ inString:fixedHtml
+ leading:YES
+ trailing:YES];
+ fixedHtml = [self stringByAddingNewlinesToTag:@""
+ inString:fixedHtml
+ leading:YES
+ trailing:YES];
+ fixedHtml = [self stringByAddingNewlinesToTag:@"
"
+ inString:fixedHtml
+ leading:YES
+ trailing:YES];
+ fixedHtml = [self stringByAddingNewlinesToTag:@""
+ inString:fixedHtml
+ leading:YES
+ trailing:YES];
+ fixedHtml = [self stringByAddingNewlinesToTag:@"
"
+ inString:fixedHtml
+ leading:YES
+ trailing:YES];
+ fixedHtml = [self stringByAddingNewlinesToTag:@""
+ inString:fixedHtml
+ leading:YES
+ trailing:YES];
+ fixedHtml = [self stringByAddingNewlinesToTag:@"
"
+ inString:fixedHtml
+ leading:YES
+ trailing:YES];
+ fixedHtml = [self stringByAddingNewlinesToTag:@""
+ inString:fixedHtml
+ leading:YES
+ trailing:YES];
+ fixedHtml = [self stringByAddingNewlinesToTag:@""
+ inString:fixedHtml
+ leading:YES
+ trailing:YES];
+
+ // line opening tags
+ fixedHtml = [self stringByAddingNewlinesToTag:@""
+ inString:fixedHtml
+ leading:YES
+ trailing:NO];
+ fixedHtml = [self stringByAddingNewlinesToTag:@"
"
+ inString:fixedHtml
+ leading:YES
+ trailing:NO];
+ fixedHtml = [self stringByAddingNewlinesToTag:@""
+ inString:fixedHtml
+ leading:YES
+ trailing:NO];
+ fixedHtml = [self stringByAddingNewlinesToTag:@""
+ inString:fixedHtml
+ leading:YES
+ trailing:NO];
+ fixedHtml = [self stringByAddingNewlinesToTag:@""
+ inString:fixedHtml
+ leading:YES
+ trailing:NO];
+ fixedHtml = [self stringByAddingNewlinesToTag:@""
+ inString:fixedHtml
+ leading:YES
+ trailing:NO];
+ fixedHtml = [self stringByAddingNewlinesToTag:@""
+ inString:fixedHtml
+ leading:YES
+ trailing:NO];
+ fixedHtml = [self stringByAddingNewlinesToTag:@""
+ inString:fixedHtml
+ leading:YES
+ trailing:NO];
+ fixedHtml = [self stringByAddingNewlinesToTag:@""
+ inString:fixedHtml
+ leading:YES
+ trailing:NO];
+
+ // line closing tags
+ fixedHtml = [self stringByAddingNewlinesToTag:@""
+ inString:fixedHtml
+ leading:NO
+ trailing:YES];
+ fixedHtml = [self stringByAddingNewlinesToTag:@"
"
+ inString:fixedHtml
+ leading:NO
+ trailing:YES];
+ fixedHtml = [self stringByAddingNewlinesToTag:@""
+ inString:fixedHtml
+ leading:NO
+ trailing:YES];
+ fixedHtml = [self stringByAddingNewlinesToTag:@""
+ inString:fixedHtml
+ leading:NO
+ trailing:YES];
+ fixedHtml = [self stringByAddingNewlinesToTag:@""
+ inString:fixedHtml
+ leading:NO
+ trailing:YES];
+ fixedHtml = [self stringByAddingNewlinesToTag:@""
+ inString:fixedHtml
+ leading:NO
+ trailing:YES];
+ fixedHtml = [self stringByAddingNewlinesToTag:@""
+ inString:fixedHtml
+ leading:NO
+ trailing:YES];
+ fixedHtml = [self stringByAddingNewlinesToTag:@""
+ inString:fixedHtml
+ leading:NO
+ trailing:YES];
+
+ // this is more like a hack but for some reason the last
in
+ // and are not properly changed into zero width
+ // space so we do that manually here
+ fixedHtml = [fixedHtml
+ stringByReplacingOccurrencesOfString:@"
\n
"
+ withString:@"\u200B
\n
"];
+ fixedHtml = [fixedHtml
+ stringByReplacingOccurrencesOfString:@"
\n"
+ withString:@"\u200B
\n"];
+
+ // The same like above for (blockquote and codeblock) this is more like a
+ // hack but for some reason the last in and are not
+ // properly changed into zero width space so we do that manually here
+ // TODO: investigate this further, issue is already described here:
+ // https://github.com/software-mansion/react-native-enriched/issues/505
+ fixedHtml = [fixedHtml
+ stringByReplacingOccurrencesOfString:@"\n
"
+ withString:@"\u200B\n"];
+ fixedHtml = [fixedHtml
+ stringByReplacingOccurrencesOfString:@"\n"
+ withString:@"\u200B\n"];
+
+ // replace "
" at the end with "
\n" if input is not empty to properly
+ // handle last
in html
+ if ([fixedHtml hasSuffix:@"
"] && fixedHtml.length != 4) {
+ fixedHtml = [fixedHtml stringByAppendingString:@"\n"];
+ }
+ }
+
+ return fixedHtml;
+}
+
++ (NSArray *_Nonnull)getTextAndStylesFromHtml:(NSString *_Nonnull)fixedHtml {
+ NSMutableString *plainText = [[NSMutableString alloc] initWithString:@""];
+ NSMutableDictionary *ongoingTags = [[NSMutableDictionary alloc] init];
+ NSMutableArray *initiallyProcessedTags = [[NSMutableArray alloc] init];
+ NSMutableDictionary *checkboxStates = [[NSMutableDictionary alloc] init];
+ BOOL insideCheckboxList = NO;
+ NSInteger precedingImageCount = 0;
+ BOOL insideTag = NO;
+ BOOL gettingTagName = NO;
+ BOOL gettingTagParams = NO;
+ BOOL closingTag = NO;
+ NSMutableString *currentTagName =
+ [[NSMutableString alloc] initWithString:@""];
+ NSMutableString *currentTagParams =
+ [[NSMutableString alloc] initWithString:@""];
+ NSDictionary *htmlEntitiesDict =
+ [NSString getEscapedCharactersInfoFrom:fixedHtml];
+
+ // firstly, extract text and initially processed tags
+ for (int i = 0; i < fixedHtml.length; i++) {
+ NSString *currentCharacterStr =
+ [fixedHtml substringWithRange:NSMakeRange(i, 1)];
+ unichar currentCharacterChar = [fixedHtml characterAtIndex:i];
+
+ if (currentCharacterChar == '<') {
+ // opening the tag, mark that we are inside and getting its name
+ insideTag = YES;
+ gettingTagName = YES;
+ } else if (currentCharacterChar == '>') {
+ // finishing some tag, no longer marked as inside or getting its
+ // name/params
+ insideTag = NO;
+ gettingTagName = NO;
+ gettingTagParams = NO;
+
+ BOOL isSelfClosing = NO;
+
+ // Check if params ended with '/' (e.g.
)
+ if ([currentTagParams hasSuffix:@"/"]) {
+ [currentTagParams
+ deleteCharactersInRange:NSMakeRange(currentTagParams.length - 1,
+ 1)];
+ isSelfClosing = YES;
+ }
+
+ if ([currentTagName isEqualToString:@"p"] ||
+ [currentTagName isEqualToString:@"br"]) {
+ // do nothing, we don't include these tags in styles
+ } else if ([currentTagName isEqualToString:@"li"]) {
+ // Only track checkbox state if we're inside a checkbox list
+ if (insideCheckboxList && !closingTag) {
+ BOOL isChecked = [currentTagParams containsString:@"checked"];
+ checkboxStates[@(plainText.length)] = @(isChecked);
+ }
+ } else if (!closingTag) {
+ // we finish opening tag - get its location, the current
+ // precedingImageCount and optionally params and put them under tag name
+ // key in ongoingTags. Storing the open-time image count lets
+ // finalizeTagEntry: correctly shift the start and extend the length
+ // so the range covers any images finalized between open and close.
+ NSMutableArray *tagArr = [[NSMutableArray alloc] init];
+ [tagArr addObject:[NSNumber numberWithInteger:plainText.length]];
+ [tagArr addObject:[NSNumber numberWithInteger:precedingImageCount]];
+ if (currentTagParams.length > 0) {
+ [tagArr addObject:[currentTagParams copy]];
+ }
+ ongoingTags[currentTagName] = tagArr;
+
+ // Check if this is a checkbox list
+ if ([currentTagName isEqualToString:@"ul"] &&
+ [self isUlCheckboxList:currentTagParams]) {
+ insideCheckboxList = YES;
+ }
+
+ // skip one newline if it was added after opening tags that are in
+ // separate lines
+ if ([self isBlockTag:currentTagName] && i + 1 < fixedHtml.length &&
+ [[NSCharacterSet newlineCharacterSet]
+ characterIsMember:[fixedHtml characterAtIndex:i + 1]]) {
+ i += 1;
+ }
+
+ if (isSelfClosing) {
+ [self finalizeTagEntry:currentTagName
+ ongoingTags:ongoingTags
+ initiallyProcessedTags:initiallyProcessedTags
+ plainText:plainText
+ precedingImageCount:&precedingImageCount];
+ }
+ } else {
+ // we finish closing tags - pack tag name, tag range and optionally tag
+ // params into an entry that goes inside initiallyProcessedTags
+
+ // Check if we're closing a checkbox list by looking at the params
+ if ([currentTagName isEqualToString:@"ul"] &&
+ [self isUlCheckboxList:currentTagParams]) {
+ insideCheckboxList = NO;
+ }
+
+ BOOL isBlockTag = [self isBlockTag:currentTagName];
+
+ // skip one newline if it was added before some closing tags that are
+ // in separate lines
+ if (isBlockTag && plainText.length > 0 &&
+ [[NSCharacterSet newlineCharacterSet]
+ characterIsMember:[plainText
+ characterAtIndex:plainText.length - 1]]) {
+ plainText = [[plainText
+ substringWithRange:NSMakeRange(0, plainText.length - 1)]
+ mutableCopy];
+ }
+
+ [self finalizeTagEntry:currentTagName
+ ongoingTags:ongoingTags
+ initiallyProcessedTags:initiallyProcessedTags
+ plainText:plainText
+ precedingImageCount:&precedingImageCount];
+ }
+ // post-tag cleanup
+ closingTag = NO;
+ currentTagName = [[NSMutableString alloc] initWithString:@""];
+ currentTagParams = [[NSMutableString alloc] initWithString:@""];
+ } else {
+ if (!insideTag) {
+ // no tags logic - just append the right text
+
+ // html entity on the index; use unescaped character and forward
+ // iterator accordingly
+ NSArray *entityInfo = htmlEntitiesDict[@(i)];
+ if (entityInfo != nullptr) {
+ NSString *escaped = entityInfo[0];
+ NSString *unescaped = entityInfo[1];
+ [plainText appendString:unescaped];
+ // the iterator will forward by 1 itself
+ i += escaped.length - 1;
+ } else {
+ [plainText appendString:currentCharacterStr];
+ }
+ } else {
+ if (gettingTagName) {
+ if (currentCharacterChar == ' ') {
+ // no longer getting tag name - switch to params
+ gettingTagName = NO;
+ gettingTagParams = YES;
+ } else if (currentCharacterChar == '/') {
+ // mark that the tag is closing
+ closingTag = YES;
+ } else {
+ // append next tag char
+ [currentTagName appendString:currentCharacterStr];
+ }
+ } else if (gettingTagParams) {
+ // append next tag params char
+ [currentTagParams appendString:currentCharacterStr];
+ }
+ }
+ }
+ }
+
+ // process tags into proper StyleType + StylePair values
+ NSMutableArray *processedStyles = [[NSMutableArray alloc] init];
+
+ for (NSArray *arr in initiallyProcessedTags) {
+ NSString *tagName = (NSString *)arr[0];
+ NSValue *tagRangeValue = (NSValue *)arr[1];
+ NSMutableString *params = [[NSMutableString alloc] initWithString:@""];
+ if (arr.count > 2) {
+ [params appendString:(NSString *)arr[2]];
+ }
+
+ NSMutableArray *styleArr = [[NSMutableArray alloc] init];
+ StylePair *stylePair = [[StylePair alloc] init];
+
+ if ([tagName isEqualToString:@"b"]) {
+ [styleArr addObject:@([BoldStyle getType])];
+ } else if ([tagName isEqualToString:@"i"]) {
+ [styleArr addObject:@([ItalicStyle getType])];
+ } else if ([tagName isEqualToString:@"img"]) {
+ NSRegularExpression *srcRegex =
+ [NSRegularExpression regularExpressionWithPattern:@"src=\"([^\"]+)\""
+ options:0
+ error:nullptr];
+ NSTextCheckingResult *match =
+ [srcRegex firstMatchInString:params
+ options:0
+ range:NSMakeRange(0, params.length)];
+
+ if (match == nullptr) {
+ continue;
+ }
+
+ NSRange srcRange = match.range;
+ [styleArr addObject:@([ImageStyle getType])];
+ // cut only the uri from the src="..." string
+ NSString *uri =
+ [params substringWithRange:NSMakeRange(srcRange.location + 5,
+ srcRange.length - 6)];
+ ImageData *imageData = [[ImageData alloc] init];
+ imageData.uri = uri;
+
+ NSRegularExpression *widthRegex = [NSRegularExpression
+ regularExpressionWithPattern:@"width=\"([0-9.]+)\""
+ options:0
+ error:nil];
+ NSTextCheckingResult *widthMatch =
+ [widthRegex firstMatchInString:params
+ options:0
+ range:NSMakeRange(0, params.length)];
+
+ if (widthMatch) {
+ NSString *widthString =
+ [params substringWithRange:[widthMatch rangeAtIndex:1]];
+ imageData.width = [widthString floatValue];
+ }
+
+ NSRegularExpression *heightRegex = [NSRegularExpression
+ regularExpressionWithPattern:@"height=\"([0-9.]+)\""
+ options:0
+ error:nil];
+ NSTextCheckingResult *heightMatch =
+ [heightRegex firstMatchInString:params
+ options:0
+ range:NSMakeRange(0, params.length)];
+
+ if (heightMatch) {
+ NSString *heightString =
+ [params substringWithRange:[heightMatch rangeAtIndex:1]];
+ imageData.height = [heightString floatValue];
+ }
+
+ stylePair.styleValue = imageData;
+ } else if ([tagName isEqualToString:@"u"]) {
+ [styleArr addObject:@([UnderlineStyle getType])];
+ } else if ([tagName isEqualToString:@"s"]) {
+ [styleArr addObject:@([StrikethroughStyle getType])];
+ } else if ([tagName isEqualToString:@"code"]) {
+ [styleArr addObject:@([InlineCodeStyle getType])];
+ } else if ([tagName isEqualToString:@"a"]) {
+ NSRegularExpression *hrefRegex =
+ [NSRegularExpression regularExpressionWithPattern:@"href=\".+\""
+ options:0
+ error:nullptr];
+ NSTextCheckingResult *match =
+ [hrefRegex firstMatchInString:params
+ options:0
+ range:NSMakeRange(0, params.length)];
+
+ if (match == nullptr) {
+ // same as on Android, no href (or empty href) equals no link style
+ continue;
+ }
+
+ NSRange hrefRange = match.range;
+ [styleArr addObject:@([LinkStyle getType])];
+ // cut only the url from the href="..." string
+ NSString *url =
+ [params substringWithRange:NSMakeRange(hrefRange.location + 6,
+ hrefRange.length - 7)];
+ NSString *text = [plainText substringWithRange:tagRangeValue.rangeValue];
+
+ LinkData *linkData = [[LinkData alloc] init];
+ linkData.url = url;
+ linkData.text = text;
+ linkData.isManual = ![text isEqualToString:url];
+
+ stylePair.styleValue = linkData;
+ } else if ([tagName isEqualToString:@"mention"]) {
+ [styleArr addObject:@([MentionStyle getType])];
+ // extract html expression into dict using some regex
+ NSMutableDictionary *paramsDict = [[NSMutableDictionary alloc] init];
+ NSString *pattern = @"(\\w+)=(['\"])(.*?)\\2";
+ NSRegularExpression *regex =
+ [NSRegularExpression regularExpressionWithPattern:pattern
+ options:0
+ error:nil];
+
+ [regex enumerateMatchesInString:params
+ options:0
+ range:NSMakeRange(0, params.length)
+ usingBlock:^(NSTextCheckingResult *_Nullable result,
+ NSMatchingFlags flags,
+ BOOL *_Nonnull stop) {
+ if (result.numberOfRanges == 4) {
+ NSString *key = [params
+ substringWithRange:[result rangeAtIndex:1]];
+ NSString *value = [params
+ substringWithRange:[result rangeAtIndex:3]];
+ paramsDict[key] = value;
+ }
+ }];
+
+ MentionParams *mentionParams = [[MentionParams alloc] init];
+ mentionParams.text = paramsDict[@"text"];
+ mentionParams.indicator = paramsDict[@"indicator"];
+
+ [paramsDict removeObjectsForKeys:@[ @"text", @"indicator" ]];
+ NSError *error;
+ NSData *attrsData = [NSJSONSerialization dataWithJSONObject:paramsDict
+ options:0
+ error:&error];
+ NSString *formattedAttrsString =
+ [[NSString alloc] initWithData:attrsData
+ encoding:NSUTF8StringEncoding];
+ mentionParams.attributes = formattedAttrsString;
+
+ stylePair.styleValue = mentionParams;
+ } else if ([tagName isEqualToString:@"h1"]) {
+ [styleArr addObject:@([H1Style getType])];
+ } else if ([tagName isEqualToString:@"h2"]) {
+ [styleArr addObject:@([H2Style getType])];
+ } else if ([tagName isEqualToString:@"h3"]) {
+ [styleArr addObject:@([H3Style getType])];
+ } else if ([tagName isEqualToString:@"h4"]) {
+ [styleArr addObject:@([H4Style getType])];
+ } else if ([tagName isEqualToString:@"h5"]) {
+ [styleArr addObject:@([H5Style getType])];
+ } else if ([tagName isEqualToString:@"h6"]) {
+ [styleArr addObject:@([H6Style getType])];
+ } else if ([tagName isEqualToString:@"ul"]) {
+ if ([self isUlCheckboxList:params]) {
+ [styleArr addObject:@([CheckboxListStyle getType])];
+ stylePair.styleValue =
+ [self prepareCheckboxListStyleValue:tagRangeValue
+ checkboxStates:checkboxStates];
+ } else {
+ [styleArr addObject:@([UnorderedListStyle getType])];
+ }
+ } else if ([tagName isEqualToString:@"ol"]) {
+ [styleArr addObject:@([OrderedListStyle getType])];
+ } else if ([tagName isEqualToString:@"blockquote"]) {
+ [styleArr addObject:@([BlockQuoteStyle getType])];
+ } else if ([tagName isEqualToString:@"codeblock"]) {
+ [styleArr addObject:@([CodeBlockStyle getType])];
+ } else {
+ // some other external tags like span just don't get put into the
+ // processed styles
+ continue;
+ }
+
+ stylePair.rangeValue = tagRangeValue;
+ [styleArr addObject:stylePair];
+ [processedStyles addObject:styleArr];
+ }
+
+ return @[ plainText, processedStyles ];
+}
+
+@end
diff --git a/ios/inputParser/InputParser.mm b/ios/inputParser/InputParser.mm
index ad08bb9c..1dbc264e 100644
--- a/ios/inputParser/InputParser.mm
+++ b/ios/inputParser/InputParser.mm
@@ -1,30 +1,21 @@
#import "InputParser.h"
#import "EnrichedTextInputView.h"
+#import "HtmlParser.h"
#import "StringExtension.h"
#import "StyleHeaders.h"
#import "TextInsertionUtils.h"
-#import "UIView+React.h"
-
-#include "GumboParser.hpp"
+#import
@implementation InputParser {
EnrichedTextInputView __weak *_input;
- NSInteger _precedingImageCount;
}
- (instancetype)initWithInput:(id)input {
self = [super init];
_input = (EnrichedTextInputView *)input;
- _precedingImageCount = 0;
return self;
}
-- (BOOL)isBlockTag:(NSString *)tagName {
- return [tagName isEqualToString:@"ul"] || [tagName isEqualToString:@"ol"] ||
- [tagName isEqualToString:@"blockquote"] ||
- [tagName isEqualToString:@"codeblock"];
-}
-
- (NSString *)parseToHtmlFromRange:(NSRange)range {
NSInteger offset = range.location;
NSString *text =
@@ -592,12 +583,14 @@ - (NSString *)tagContentForStyle:(NSNumber *)style
}
- (void)replaceWholeFromHtml:(NSString *_Nonnull)html {
+ NSArray *processingResult = [HtmlParser getTextAndStylesFromHtml:html];
+
// reset the text first and reset typing attributes
_input->textView.text = @"";
_input->textView.typingAttributes = _input->defaultTypingAttributes;
@try {
- NSArray *processingResult = [self getTextAndStylesFromHtml:html];
+ NSArray *processingResult = [HtmlParser getTextAndStylesFromHtml:html];
NSString *plainText = (NSString *)processingResult[0];
NSArray *stylesInfo = (NSArray *)processingResult[1];
@@ -620,7 +613,7 @@ - (void)replaceWholeFromHtml:(NSString *_Nonnull)html {
- (void)replaceFromHtml:(NSString *_Nonnull)html range:(NSRange)range {
@try {
- NSArray *processingResult = [self getTextAndStylesFromHtml:html];
+ NSArray *processingResult = [HtmlParser getTextAndStylesFromHtml:html];
NSString *plainText = (NSString *)processingResult[0];
NSArray *stylesInfo = (NSArray *)processingResult[1];
@@ -648,7 +641,7 @@ - (void)replaceFromHtml:(NSString *_Nonnull)html range:(NSRange)range {
- (void)insertFromHtml:(NSString *_Nonnull)html location:(NSInteger)location {
@try {
- NSArray *processingResult = [self getTextAndStylesFromHtml:html];
+ NSArray *processingResult = [HtmlParser getTextAndStylesFromHtml:html];
NSString *plainText = (NSString *)processingResult[0];
NSArray *stylesInfo = (NSArray *)processingResult[1];
@@ -718,7 +711,8 @@ - (void)applyProcessedStyles:(NSArray *)processedStyles
ImageData *imgData = (ImageData *)stylePair.styleValue;
[((ImageStyle *)baseStyle) addImageAtRange:styleRange
imageData:imgData
- withSelection:NO];
+ withSelection:NO
+ withDirtyRange:YES];
} else if ([styleType isEqualToNumber:@([CheckboxListStyle getType])]) {
NSDictionary *checkboxStates = (NSDictionary *)stylePair.styleValue;
CheckboxListStyle *cbLStyle = (CheckboxListStyle *)baseStyle;
@@ -737,7 +731,7 @@ - (void)applyProcessedStyles:(NSArray *)processedStyles
offset + zeroWidthSpaceOffset + [key unsignedIntegerValue];
BOOL isChecked = [checkboxStates[key] boolValue];
if (isChecked) {
- [cbLStyle toggleCheckedAt:checkboxPosition];
+ [cbLStyle toggleCheckedAt:checkboxPosition withDirtyRange:YES];
}
}
}
@@ -759,752 +753,9 @@ - (void)applyProcessedStyles:(NSArray *)processedStyles
[_input anyTextMayHaveBeenModified];
}
-#pragma mark - External HTML normalization
-
-/**
- * Normalizes external HTML (from Google Docs, Word, web pages, etc.) into our
- * canonical tag subset using the Gumbo-based C++ normalizer.
- *
- * Converts: strong → b, em → i, span style="font-weight:bold" → b,
- * strips unknown tags while preserving text
- */
-- (NSString *_Nullable)normalizeExternalHtml:(NSString *_Nonnull)html {
- std::string result =
- GumboParser::normalizeHtml(std::string([html UTF8String]));
- if (result.empty())
- return nil;
- return [NSString stringWithUTF8String:result.c_str()];
-}
-
- (NSString *_Nullable)initiallyProcessHtml:(NSString *_Nonnull)html {
- NSString *htmlWithoutSpaces = [self stripExtraWhiteSpacesAndNewlines:html];
- NSString *fixedHtml = nullptr;
-
- if (htmlWithoutSpaces.length >= 13) {
- NSString *firstSix =
- [htmlWithoutSpaces substringWithRange:NSMakeRange(0, 6)];
- NSString *lastSeven = [htmlWithoutSpaces
- substringWithRange:NSMakeRange(htmlWithoutSpaces.length - 7, 7)];
-
- if ([firstSix isEqualToString:@""] &&
- [lastSeven isEqualToString:@""]) {
- // remove html tags, might be with newlines or without them
- fixedHtml = [htmlWithoutSpaces copy];
- // firstly remove newlined html tags if any:
- fixedHtml = [fixedHtml stringByReplacingOccurrencesOfString:@"\n"
- withString:@""];
- fixedHtml = [fixedHtml stringByReplacingOccurrencesOfString:@"\n"
- withString:@""];
- // fallback; remove html tags without their newlines
- fixedHtml = [fixedHtml stringByReplacingOccurrencesOfString:@""
- withString:@""];
- fixedHtml = [fixedHtml stringByReplacingOccurrencesOfString:@""
- withString:@""];
- } else if (_input->useHtmlNormalizer) {
- // External HTML (from Google Docs, Word, web pages, etc.)
- // Run through the Gumbo-based normalizer to convert arbitrary HTML
- // into our canonical tag subset.
- NSString *normalized = [self normalizeExternalHtml:html];
- if (normalized != nil) {
- fixedHtml = normalized;
- }
- }
-
- // Additionally, try getting the content from between body tags if there are
- // some:
-
- // Firstly make sure there are no newlines between them.
- fixedHtml = [fixedHtml stringByReplacingOccurrencesOfString:@"\n"
- withString:@""];
- fixedHtml = [fixedHtml stringByReplacingOccurrencesOfString:@"\n"
- withString:@""];
- // Then, if there actually are body tags, use the content between them.
- NSRange openingBodyRange = [htmlWithoutSpaces rangeOfString:@""];
- NSRange closingBodyRange = [htmlWithoutSpaces rangeOfString:@""];
- if (openingBodyRange.length != 0 && closingBodyRange.length != 0) {
- NSInteger newStart = openingBodyRange.location + 6;
- NSInteger newEnd = closingBodyRange.location - 1;
- fixedHtml = [htmlWithoutSpaces
- substringWithRange:NSMakeRange(newStart, newEnd - newStart + 1)];
- }
- }
-
- // second processing - try fixing htmls with wrong newlines' setup
- if (fixedHtml != nullptr) {
- // add
tag wherever needed
- fixedHtml = [fixedHtml stringByReplacingOccurrencesOfString:@""
- withString:@"
"];
-
- // remove tags inside of
- fixedHtml = [fixedHtml stringByReplacingOccurrencesOfString:@""
- withString:@"
"];
- fixedHtml = [fixedHtml stringByReplacingOccurrencesOfString:@""
- withString:@""];
-
- // change
to
- fixedHtml = [fixedHtml stringByReplacingOccurrencesOfString:@"
"
- withString:@"
"];
-
- // remove tags around
- fixedHtml = [fixedHtml stringByReplacingOccurrencesOfString:@"
"
- withString:@"
"];
- fixedHtml = [fixedHtml stringByReplacingOccurrencesOfString:@"
"
- withString:@"
"];
-
- // add
tags inside empty blockquote and codeblock tags
- fixedHtml = [fixedHtml
- stringByReplacingOccurrencesOfString:@""
- withString:@"
"
- @"blockquote>"];
- fixedHtml = [fixedHtml
- stringByReplacingOccurrencesOfString:@""
- withString:@"
"];
-
- // remove empty ul and ol tags
- fixedHtml = [fixedHtml stringByReplacingOccurrencesOfString:@""
- withString:@""];
- fixedHtml = [fixedHtml
- stringByReplacingOccurrencesOfString:@""
- withString:@""];
- fixedHtml = [fixedHtml stringByReplacingOccurrencesOfString:@"
"
- withString:@""];
-
- // tags that have to be in separate lines
- fixedHtml = [self stringByAddingNewlinesToTag:@"
"
- inString:fixedHtml
- leading:YES
- trailing:YES];
- fixedHtml = [self stringByAddingNewlinesToTag:@""
- inString:fixedHtml
- leading:YES
- trailing:YES];
- fixedHtml = [self stringByAddingNewlinesToTag:@"
"
- inString:fixedHtml
- leading:YES
- trailing:YES];
- fixedHtml = [self stringByAddingNewlinesToTag:@""
- inString:fixedHtml
- leading:YES
- trailing:YES];
- fixedHtml = [self stringByAddingNewlinesToTag:@"
"
- inString:fixedHtml
- leading:YES
- trailing:YES];
- fixedHtml = [self stringByAddingNewlinesToTag:@""
- inString:fixedHtml
- leading:YES
- trailing:YES];
- fixedHtml = [self stringByAddingNewlinesToTag:@"
"
- inString:fixedHtml
- leading:YES
- trailing:YES];
- fixedHtml = [self stringByAddingNewlinesToTag:@""
- inString:fixedHtml
- leading:YES
- trailing:YES];
- fixedHtml = [self stringByAddingNewlinesToTag:@""
- inString:fixedHtml
- leading:YES
- trailing:YES];
-
- // line opening tags
- fixedHtml = [self stringByAddingNewlinesToTag:@""
- inString:fixedHtml
- leading:YES
- trailing:NO];
- fixedHtml = [self stringByAddingNewlinesToTag:@"
"
- inString:fixedHtml
- leading:YES
- trailing:NO];
- fixedHtml = [self stringByAddingNewlinesToTag:@""
- inString:fixedHtml
- leading:YES
- trailing:NO];
- fixedHtml = [self stringByAddingNewlinesToTag:@""
- inString:fixedHtml
- leading:YES
- trailing:NO];
- fixedHtml = [self stringByAddingNewlinesToTag:@""
- inString:fixedHtml
- leading:YES
- trailing:NO];
- fixedHtml = [self stringByAddingNewlinesToTag:@""
- inString:fixedHtml
- leading:YES
- trailing:NO];
- fixedHtml = [self stringByAddingNewlinesToTag:@""
- inString:fixedHtml
- leading:YES
- trailing:NO];
- fixedHtml = [self stringByAddingNewlinesToTag:@""
- inString:fixedHtml
- leading:YES
- trailing:NO];
- fixedHtml = [self stringByAddingNewlinesToTag:@""
- inString:fixedHtml
- leading:YES
- trailing:NO];
-
- // line closing tags
- fixedHtml = [self stringByAddingNewlinesToTag:@""
- inString:fixedHtml
- leading:NO
- trailing:YES];
- fixedHtml = [self stringByAddingNewlinesToTag:@"
"
- inString:fixedHtml
- leading:NO
- trailing:YES];
- fixedHtml = [self stringByAddingNewlinesToTag:@""
- inString:fixedHtml
- leading:NO
- trailing:YES];
- fixedHtml = [self stringByAddingNewlinesToTag:@""
- inString:fixedHtml
- leading:NO
- trailing:YES];
- fixedHtml = [self stringByAddingNewlinesToTag:@""
- inString:fixedHtml
- leading:NO
- trailing:YES];
- fixedHtml = [self stringByAddingNewlinesToTag:@""
- inString:fixedHtml
- leading:NO
- trailing:YES];
- fixedHtml = [self stringByAddingNewlinesToTag:@""
- inString:fixedHtml
- leading:NO
- trailing:YES];
- fixedHtml = [self stringByAddingNewlinesToTag:@""
- inString:fixedHtml
- leading:NO
- trailing:YES];
-
- // this is more like a hack but for some reason the last
in
- // and are not properly changed into zero width
- // space so we do that manually here
- fixedHtml = [fixedHtml
- stringByReplacingOccurrencesOfString:@"
\n
"
- withString:@"\u200B
\n
"];
- fixedHtml = [fixedHtml
- stringByReplacingOccurrencesOfString:@"
\n"
- withString:@"\u200B
\n"];
-
- // replace "
" at the end with "
\n" if input is not empty to properly
- // handle last
in html
- if ([fixedHtml hasSuffix:@"
"] && fixedHtml.length != 4) {
- fixedHtml = [fixedHtml stringByAppendingString:@"\n"];
- }
- }
-
- return fixedHtml;
-}
-
-/**
- * Prepares HTML for the parser by stripping extraneous whitespace and newlines
- * from structural tags, while preserving them within text content.
- *
- * APPROACH:
- * This function treats the HTML as having two distinct states:
- * 1. Structure Mode (Depth == 0): We are inside or between container tags (like
- * blockquote, ul, codeblock). In this mode whitespace and newlines are
- * considered layout artifacts and are REMOVED to prevent the parser from
- * creating unwanted spaces.
- * 2. Content Mode (Depth > 0): We are inside a text-containing tag (like p,
- * b, li). In this mode, all whitespace is PRESERVED exactly as is, ensuring
- * that sentences and inline formatting remain readable.
- *
- * The function iterates character-by-character, using a depth counter to track
- * nesting levels of the specific tags defined in `textTags`.
- *
- * IMPORTANT:
- * The `textTags` set acts as a whitelist for "Content Mode". If you add support
- * for a new HTML tag that contains visible text (e.g., h4, h5, h6),
- * you MUST add it to the `textTags` set below.
- */
-- (NSString *)stripExtraWhiteSpacesAndNewlines:(NSString *)html {
- NSSet *textTags = [NSSet setWithObjects:@"p", @"h1", @"h2", @"h3", @"h4",
- @"h5", @"h6", @"li", @"b", @"a", @"s",
- @"mention", @"code", @"u", @"i", nil];
-
- NSMutableString *output = [NSMutableString stringWithCapacity:html.length];
- NSMutableString *currentTagBuffer = [NSMutableString string];
- NSCharacterSet *whitespaceAndNewlineSet =
- [NSCharacterSet whitespaceAndNewlineCharacterSet];
-
- BOOL isReadingTag = NO;
- NSInteger textDepth = 0;
-
- for (NSUInteger i = 0; i < html.length; i++) {
- unichar c = [html characterAtIndex:i];
-
- if (c == '<') {
- isReadingTag = YES;
- [currentTagBuffer setString:@""];
- [output appendString:@"<"];
- } else if (c == '>') {
- isReadingTag = NO;
- [output appendString:@">"];
-
- NSString *fullTag = [currentTagBuffer lowercaseString];
-
- NSString *cleanName = [fullTag
- stringByTrimmingCharactersInSet:
- [NSCharacterSet characterSetWithCharactersInString:@"/"]];
- NSArray *parts =
- [cleanName componentsSeparatedByCharactersInSet:
- [NSCharacterSet whitespaceAndNewlineCharacterSet]];
- NSString *tagName = parts.firstObject;
-
- if (![textTags containsObject:tagName]) {
- continue;
- }
-
- if ([fullTag hasPrefix:@"/"]) {
- textDepth--;
- if (textDepth < 0)
- textDepth = 0;
- } else {
- // Opening tag (e.g. ) -> Enter Text Mode
- // (Ignore self-closing tags like
if they happen to be in the
- // list)
- if (![fullTag hasSuffix:@"/"]) {
- textDepth++;
- }
- }
- } else {
- if (isReadingTag) {
- [currentTagBuffer appendFormat:@"%C", c];
- [output appendFormat:@"%C", c];
- continue;
- }
-
- if (textDepth > 0) {
- [output appendFormat:@"%C", c];
- } else {
- if (![whitespaceAndNewlineSet characterIsMember:c]) {
- [output appendFormat:@"%C", c];
- }
- }
- }
- }
-
- return output;
-}
-
-- (NSString *)stringByAddingNewlinesToTag:(NSString *)tag
- inString:(NSString *)html
- leading:(BOOL)leading
- trailing:(BOOL)trailing {
- NSString *str = [html copy];
- if (leading) {
- NSString *formattedTag = [NSString stringWithFormat:@">%@", tag];
- NSString *formattedNewTag = [NSString stringWithFormat:@">\n%@", tag];
- str = [str stringByReplacingOccurrencesOfString:formattedTag
- withString:formattedNewTag];
- }
- if (trailing) {
- NSString *formattedTag = [NSString stringWithFormat:@"%@<", tag];
- NSString *formattedNewTag = [NSString stringWithFormat:@"%@\n<", tag];
- str = [str stringByReplacingOccurrencesOfString:formattedTag
- withString:formattedNewTag];
- }
- return str;
-}
-
-- (void)finalizeTagEntry:(NSMutableString *)tagName
- ongoingTags:(NSMutableDictionary *)ongoingTags
- initiallyProcessedTags:(NSMutableArray *)processedTags
- plainText:(NSMutableString *)plainText {
- NSMutableArray *tagEntry = [[NSMutableArray alloc] init];
-
- NSArray *tagData = ongoingTags[tagName];
- NSInteger tagLocation = [((NSNumber *)tagData[0]) intValue];
-
- // 'tagLocation' is an index based on 'plainText' which currently only holds
- // raw text.
- //
- // Since 'plainText' does not yet contain the special placeholders for images,
- // the indices for any text following an image are lower than they will be
- // in the final NSTextStorage.
- //
- // We add '_precedingImageCount' to shift the start index forward, aligning
- // this style's range with the actual position in the final text (where each
- // image adds 1 character).
- NSRange tagRange = NSMakeRange(tagLocation + _precedingImageCount,
- plainText.length - tagLocation);
-
- [tagEntry addObject:[tagName copy]];
- [tagEntry addObject:[NSValue valueWithRange:tagRange]];
- if (tagData.count > 1) {
- [tagEntry addObject:[(NSString *)tagData[1] copy]];
- }
-
- [processedTags addObject:tagEntry];
- [ongoingTags removeObjectForKey:tagName];
-
- if ([tagName isEqualToString:@"img"]) {
- _precedingImageCount++;
- }
-}
-
-- (NSArray *)getTextAndStylesFromHtml:(NSString *)fixedHtml {
- NSMutableString *plainText = [[NSMutableString alloc] initWithString:@""];
- NSMutableDictionary *ongoingTags = [[NSMutableDictionary alloc] init];
- NSMutableArray *initiallyProcessedTags = [[NSMutableArray alloc] init];
- NSMutableDictionary *checkboxStates = [[NSMutableDictionary alloc] init];
- BOOL insideCheckboxList = NO;
- _precedingImageCount = 0;
- BOOL insideTag = NO;
- BOOL gettingTagName = NO;
- BOOL gettingTagParams = NO;
- BOOL closingTag = NO;
- NSMutableString *currentTagName =
- [[NSMutableString alloc] initWithString:@""];
- NSMutableString *currentTagParams =
- [[NSMutableString alloc] initWithString:@""];
- NSDictionary *htmlEntitiesDict =
- [NSString getEscapedCharactersInfoFrom:fixedHtml];
-
- // firstly, extract text and initially processed tags
- for (int i = 0; i < fixedHtml.length; i++) {
- NSString *currentCharacterStr =
- [fixedHtml substringWithRange:NSMakeRange(i, 1)];
- unichar currentCharacterChar = [fixedHtml characterAtIndex:i];
-
- if (currentCharacterChar == '<') {
- // opening the tag, mark that we are inside and getting its name
- insideTag = YES;
- gettingTagName = YES;
- } else if (currentCharacterChar == '>') {
- // finishing some tag, no longer marked as inside or getting its
- // name/params
- insideTag = NO;
- gettingTagName = NO;
- gettingTagParams = NO;
-
- BOOL isSelfClosing = NO;
-
- // Check if params ended with '/' (e.g.
)
- if ([currentTagParams hasSuffix:@"/"]) {
- [currentTagParams
- deleteCharactersInRange:NSMakeRange(currentTagParams.length - 1,
- 1)];
- isSelfClosing = YES;
- }
-
- if ([currentTagName isEqualToString:@"p"] ||
- [currentTagName isEqualToString:@"br"]) {
- // do nothing, we don't include these tags in styles
- } else if ([currentTagName isEqualToString:@"li"]) {
- // Only track checkbox state if we're inside a checkbox list
- if (insideCheckboxList && !closingTag) {
- BOOL isChecked = [currentTagParams containsString:@"checked"];
- checkboxStates[@(plainText.length)] = @(isChecked);
- }
- } else if (!closingTag) {
- // we finish opening tag - get its location and optionally params and
- // put them under tag name key in ongoingTags
- NSMutableArray *tagArr = [[NSMutableArray alloc] init];
- [tagArr addObject:[NSNumber numberWithInteger:plainText.length]];
- if (currentTagParams.length > 0) {
- [tagArr addObject:[currentTagParams copy]];
- }
- ongoingTags[currentTagName] = tagArr;
-
- // Check if this is a checkbox list
- if ([currentTagName isEqualToString:@"ul"] &&
- [self isUlCheckboxList:currentTagParams]) {
- insideCheckboxList = YES;
- }
-
- // skip one newline if it was added after opening tags that are in
- // separate lines
- if ([self isBlockTag:currentTagName] && i + 1 < fixedHtml.length &&
- [[NSCharacterSet newlineCharacterSet]
- characterIsMember:[fixedHtml characterAtIndex:i + 1]]) {
- i += 1;
- }
-
- if (isSelfClosing) {
- [self finalizeTagEntry:currentTagName
- ongoingTags:ongoingTags
- initiallyProcessedTags:initiallyProcessedTags
- plainText:plainText];
- }
- } else {
- // we finish closing tags - pack tag name, tag range and optionally tag
- // params into an entry that goes inside initiallyProcessedTags
-
- // Check if we're closing a checkbox list by looking at the params
- if ([currentTagName isEqualToString:@"ul"] &&
- [self isUlCheckboxList:currentTagParams]) {
- insideCheckboxList = NO;
- }
-
- BOOL isBlockTag = [self isBlockTag:currentTagName];
-
- // skip one newline if it was added before some closing tags that are
- // in separate lines
- if (isBlockTag && plainText.length > 0 &&
- [[NSCharacterSet newlineCharacterSet]
- characterIsMember:[plainText
- characterAtIndex:plainText.length - 1]]) {
- plainText = [[plainText
- substringWithRange:NSMakeRange(0, plainText.length - 1)]
- mutableCopy];
- }
-
- [self finalizeTagEntry:currentTagName
- ongoingTags:ongoingTags
- initiallyProcessedTags:initiallyProcessedTags
- plainText:plainText];
- }
- // post-tag cleanup
- closingTag = NO;
- currentTagName = [[NSMutableString alloc] initWithString:@""];
- currentTagParams = [[NSMutableString alloc] initWithString:@""];
- } else {
- if (!insideTag) {
- // no tags logic - just append the right text
-
- // html entity on the index; use unescaped character and forward
- // iterator accordingly
- NSArray *entityInfo = htmlEntitiesDict[@(i)];
- if (entityInfo != nullptr) {
- NSString *escaped = entityInfo[0];
- NSString *unescaped = entityInfo[1];
- [plainText appendString:unescaped];
- // the iterator will forward by 1 itself
- i += escaped.length - 1;
- } else {
- [plainText appendString:currentCharacterStr];
- }
- } else {
- if (gettingTagName) {
- if (currentCharacterChar == ' ') {
- // no longer getting tag name - switch to params
- gettingTagName = NO;
- gettingTagParams = YES;
- } else if (currentCharacterChar == '/') {
- // mark that the tag is closing
- closingTag = YES;
- } else {
- // append next tag char
- [currentTagName appendString:currentCharacterStr];
- }
- } else if (gettingTagParams) {
- // append next tag params char
- [currentTagParams appendString:currentCharacterStr];
- }
- }
- }
- }
-
- // process tags into proper StyleType + StylePair values
- NSMutableArray *processedStyles = [[NSMutableArray alloc] init];
-
- for (NSArray *arr in initiallyProcessedTags) {
- NSString *tagName = (NSString *)arr[0];
- NSValue *tagRangeValue = (NSValue *)arr[1];
- NSMutableString *params = [[NSMutableString alloc] initWithString:@""];
- if (arr.count > 2) {
- [params appendString:(NSString *)arr[2]];
- }
-
- NSMutableArray *styleArr = [[NSMutableArray alloc] init];
- StylePair *stylePair = [[StylePair alloc] init];
- if ([tagName isEqualToString:@"b"]) {
- [styleArr addObject:@([BoldStyle getType])];
- } else if ([tagName isEqualToString:@"i"]) {
- [styleArr addObject:@([ItalicStyle getType])];
- } else if ([tagName isEqualToString:@"img"]) {
- NSRegularExpression *srcRegex =
- [NSRegularExpression regularExpressionWithPattern:@"src=\"([^\"]+)\""
- options:0
- error:nullptr];
- NSTextCheckingResult *match =
- [srcRegex firstMatchInString:params
- options:0
- range:NSMakeRange(0, params.length)];
-
- if (match == nullptr) {
- continue;
- }
-
- NSRange srcRange = match.range;
- [styleArr addObject:@([ImageStyle getType])];
- // cut only the uri from the src="..." string
- NSString *uri =
- [params substringWithRange:NSMakeRange(srcRange.location + 5,
- srcRange.length - 6)];
- ImageData *imageData = [[ImageData alloc] init];
- imageData.uri = uri;
-
- NSRegularExpression *widthRegex = [NSRegularExpression
- regularExpressionWithPattern:@"width=\"([0-9.]+)\""
- options:0
- error:nil];
- NSTextCheckingResult *widthMatch =
- [widthRegex firstMatchInString:params
- options:0
- range:NSMakeRange(0, params.length)];
-
- if (widthMatch) {
- NSString *widthString =
- [params substringWithRange:[widthMatch rangeAtIndex:1]];
- imageData.width = [widthString floatValue];
- }
-
- NSRegularExpression *heightRegex = [NSRegularExpression
- regularExpressionWithPattern:@"height=\"([0-9.]+)\""
- options:0
- error:nil];
- NSTextCheckingResult *heightMatch =
- [heightRegex firstMatchInString:params
- options:0
- range:NSMakeRange(0, params.length)];
-
- if (heightMatch) {
- NSString *heightString =
- [params substringWithRange:[heightMatch rangeAtIndex:1]];
- imageData.height = [heightString floatValue];
- }
-
- stylePair.styleValue = imageData;
- } else if ([tagName isEqualToString:@"u"]) {
- [styleArr addObject:@([UnderlineStyle getType])];
- } else if ([tagName isEqualToString:@"s"]) {
- [styleArr addObject:@([StrikethroughStyle getType])];
- } else if ([tagName isEqualToString:@"code"]) {
- [styleArr addObject:@([InlineCodeStyle getType])];
- } else if ([tagName isEqualToString:@"a"]) {
- NSRegularExpression *hrefRegex =
- [NSRegularExpression regularExpressionWithPattern:@"href=\".+\""
- options:0
- error:nullptr];
- NSTextCheckingResult *match =
- [hrefRegex firstMatchInString:params
- options:0
- range:NSMakeRange(0, params.length)];
-
- if (match == nullptr) {
- // same as on Android, no href (or empty href) equals no link style
- continue;
- }
-
- NSRange hrefRange = match.range;
- [styleArr addObject:@([LinkStyle getType])];
- // cut only the url from the href="..." string
- NSString *url =
- [params substringWithRange:NSMakeRange(hrefRange.location + 6,
- hrefRange.length - 7)];
- NSString *text = [plainText substringWithRange:tagRangeValue.rangeValue];
-
- LinkData *linkData = [[LinkData alloc] init];
- linkData.url = url;
- linkData.text = text;
- linkData.isManual = ![text isEqualToString:url];
-
- stylePair.styleValue = linkData;
- } else if ([tagName isEqualToString:@"mention"]) {
- [styleArr addObject:@([MentionStyle getType])];
- // extract html expression into dict using some regex
- NSMutableDictionary *paramsDict = [[NSMutableDictionary alloc] init];
- NSString *pattern = @"(\\w+)=(['\"])(.*?)\\2";
- NSRegularExpression *regex =
- [NSRegularExpression regularExpressionWithPattern:pattern
- options:0
- error:nil];
-
- [regex enumerateMatchesInString:params
- options:0
- range:NSMakeRange(0, params.length)
- usingBlock:^(NSTextCheckingResult *_Nullable result,
- NSMatchingFlags flags,
- BOOL *_Nonnull stop) {
- if (result.numberOfRanges == 4) {
- NSString *key = [params
- substringWithRange:[result rangeAtIndex:1]];
- NSString *value = [params
- substringWithRange:[result rangeAtIndex:3]];
- paramsDict[key] = value;
- }
- }];
-
- MentionParams *mentionParams = [[MentionParams alloc] init];
- mentionParams.text = paramsDict[@"text"];
- mentionParams.indicator = paramsDict[@"indicator"];
-
- [paramsDict removeObjectsForKeys:@[ @"text", @"indicator" ]];
- NSError *error;
- NSData *attrsData = [NSJSONSerialization dataWithJSONObject:paramsDict
- options:0
- error:&error];
- NSString *formattedAttrsString =
- [[NSString alloc] initWithData:attrsData
- encoding:NSUTF8StringEncoding];
- mentionParams.attributes = formattedAttrsString;
-
- stylePair.styleValue = mentionParams;
- } else if ([tagName isEqualToString:@"h1"]) {
- [styleArr addObject:@([H1Style getType])];
- } else if ([tagName isEqualToString:@"h2"]) {
- [styleArr addObject:@([H2Style getType])];
- } else if ([tagName isEqualToString:@"h3"]) {
- [styleArr addObject:@([H3Style getType])];
- } else if ([tagName isEqualToString:@"h4"]) {
- [styleArr addObject:@([H4Style getType])];
- } else if ([tagName isEqualToString:@"h5"]) {
- [styleArr addObject:@([H5Style getType])];
- } else if ([tagName isEqualToString:@"h6"]) {
- [styleArr addObject:@([H6Style getType])];
- } else if ([tagName isEqualToString:@"ul"]) {
- if ([self isUlCheckboxList:params]) {
- [styleArr addObject:@([CheckboxListStyle getType])];
- stylePair.styleValue =
- [self prepareCheckboxListStyleValue:tagRangeValue
- checkboxStates:checkboxStates];
- } else {
- [styleArr addObject:@([UnorderedListStyle getType])];
- }
- } else if ([tagName isEqualToString:@"ol"]) {
- [styleArr addObject:@([OrderedListStyle getType])];
- } else if ([tagName isEqualToString:@"blockquote"]) {
- [styleArr addObject:@([BlockQuoteStyle getType])];
- } else if ([tagName isEqualToString:@"codeblock"]) {
- [styleArr addObject:@([CodeBlockStyle getType])];
- } else {
- // some other external tags like span just don't get put into the
- // processed styles
- continue;
- }
-
- stylePair.rangeValue = tagRangeValue;
- [styleArr addObject:stylePair];
- [processedStyles addObject:styleArr];
- }
-
- return @[ plainText, processedStyles ];
-}
-
-- (BOOL)isUlCheckboxList:(NSString *)params {
- return ([params containsString:@"data-type=\"checkbox\""] ||
- [params containsString:@"data-type='checkbox'"]);
-}
-
-- (NSDictionary *)prepareCheckboxListStyleValue:(NSValue *)rangeValue
- checkboxStates:(NSDictionary *)checkboxStates {
- NSRange range = [rangeValue rangeValue];
- NSMutableDictionary *statesInRange = [[NSMutableDictionary alloc] init];
-
- for (NSNumber *key in checkboxStates) {
- NSUInteger pos = [key unsignedIntegerValue];
- if (pos >= range.location && pos < range.location + range.length) {
- [statesInRange setObject:checkboxStates[key] forKey:key];
- }
- }
-
- return statesInRange;
+ return [HtmlParser initiallyProcessHtml:html
+ useHtmlNormalizer:_input->useHtmlNormalizer];
}
@end
diff --git a/ios/interfaces/BaseStyleProtocol.h b/ios/interfaces/BaseStyleProtocol.h
index 5e08e558..96a3d03a 100644
--- a/ios/interfaces/BaseStyleProtocol.h
+++ b/ios/interfaces/BaseStyleProtocol.h
@@ -5,7 +5,7 @@
@protocol BaseStyleProtocol
+ (StyleType)getStyleType;
+ (BOOL)isParagraphStyle;
-- (instancetype _Nonnull)initWithInput:(id _Nonnull)input;
+- (instancetype _Nonnull)initWithHost:(id _Nonnull)host;
- (void)applyStyle:(NSRange)range;
- (void)addAttributes:(NSRange)range withTypingAttr:(BOOL)withTypingAttr;
- (void)removeAttributes:(NSRange)range;
diff --git a/ios/interfaces/EnrichedTextStyleHeaders.h b/ios/interfaces/EnrichedTextStyleHeaders.h
new file mode 100644
index 00000000..9c717aae
--- /dev/null
+++ b/ios/interfaces/EnrichedTextStyleHeaders.h
@@ -0,0 +1,59 @@
+#pragma once
+#import "StyleHeaders.h"
+
+@interface EnrichedTextBoldStyle : BoldStyle
+@end
+
+@interface EnrichedTextItalicStyle : ItalicStyle
+@end
+
+@interface EnrichedTextUnderlineStyle : UnderlineStyle
+@end
+
+@interface EnrichedTextStrikethroughStyle : StrikethroughStyle
+@end
+
+@interface EnrichedTextInlineCodeStyle : InlineCodeStyle
+@end
+
+@interface EnrichedTextH1Style : H1Style
+@end
+
+@interface EnrichedTextH2Style : H2Style
+@end
+
+@interface EnrichedTextH3Style : H3Style
+@end
+
+@interface EnrichedTextH4Style : H4Style
+@end
+
+@interface EnrichedTextH5Style : H5Style
+@end
+
+@interface EnrichedTextH6Style : H6Style
+@end
+
+@interface EnrichedTextBlockQuoteStyle : BlockQuoteStyle
+@end
+
+@interface EnrichedTextCodeBlockStyle : CodeBlockStyle
+@end
+
+@interface EnrichedTextUnorderedListStyle : UnorderedListStyle
+@end
+
+@interface EnrichedTextOrderedListStyle : OrderedListStyle
+@end
+
+@interface EnrichedTextLinkStyle : LinkStyle
+@end
+
+@interface EnrichedTextMentionStyle : MentionStyle
+@end
+
+@interface EnrichedTextImageStyle : ImageStyle
+@end
+
+@interface EnrichedTextCheckboxListStyle : CheckboxListStyle
+@end
diff --git a/ios/interfaces/EnrichedViewHost.h b/ios/interfaces/EnrichedViewHost.h
new file mode 100644
index 00000000..d56e05c1
--- /dev/null
+++ b/ios/interfaces/EnrichedViewHost.h
@@ -0,0 +1,25 @@
+#pragma once
+#import "AttributesManager.h"
+#import "EnrichedConfig.h"
+#import "StyleTypeEnum.h"
+#import
+
+@protocol EnrichedViewHost
+@required
+@property(nonatomic, readonly) UITextView *textView;
+@property(nonatomic, readonly) EnrichedConfig *config;
+@property(nonatomic, readonly) NSDictionary *stylesDict;
+@property(nonatomic, readonly, nullable) AttributesManager *attributesManager;
+@property(nonatomic, readonly, nullable)
+ NSMutableDictionary *> *conflictingStyles;
+@property(nonatomic, readonly, nullable)
+ NSMutableDictionary *> *blockingStyles;
+@property(nonatomic, readonly, nullable)
+ NSMutableDictionary *defaultTypingAttributes;
+@property(nonatomic) BOOL blockEmitting;
+@optional
+- (BOOL)handleStyleBlocksAndConflicts:(StyleType)type range:(NSRange)range;
+- (void)emitOnLinkDetectedEvent:(id _Nonnull)linkData range:(NSRange)range;
+- (void)emitOnMentionEvent:(NSString *_Nonnull)indicator
+ text:(NSString *_Nullable)text;
+@end
diff --git a/ios/interfaces/MentionStyleProps.h b/ios/interfaces/MentionStyleProps.h
index d0d59dd5..f83d1bc4 100644
--- a/ios/interfaces/MentionStyleProps.h
+++ b/ios/interfaces/MentionStyleProps.h
@@ -10,4 +10,8 @@
@property TextDecorationLineEnum decorationLine;
+ (NSDictionary *)getSinglePropsFromFollyDynamic:(folly::dynamic)folly;
+ (NSDictionary *)getComplexPropsFromFollyDynamic:(folly::dynamic)folly;
+
+// MARK: - Text only props
+@property UIColor *pressColor;
+@property UIColor *pressBackgroundColor;
@end
diff --git a/ios/interfaces/MentionStyleProps.mm b/ios/interfaces/MentionStyleProps.mm
index 5ea23538..b4ca19c1 100644
--- a/ios/interfaces/MentionStyleProps.mm
+++ b/ios/interfaces/MentionStyleProps.mm
@@ -34,6 +34,23 @@ + (MentionStyleProps *)getSingleMentionStylePropsFromFollyDynamic:
nativeProps.decorationLine = DecorationUnderline;
}
+ // text only
+ if (folly["pressColor"].isNumber()) {
+ facebook::react::SharedColor pressColor = facebook::react::SharedColor(
+ facebook::react::Color(int32_t(folly["pressColor"].asInt())));
+ nativeProps.pressColor = RCTUIColorFromSharedColor(pressColor);
+ } else {
+ nativeProps.pressColor = [UIColor blueColor];
+ }
+
+ if (folly["pressBackgroundColor"].isNumber()) {
+ facebook::react::SharedColor bgColor = facebook::react::SharedColor(
+ facebook::react::Color(int32_t(folly["pressBackgroundColor"].asInt())));
+ nativeProps.pressBackgroundColor = RCTUIColorFromSharedColor(bgColor);
+ } else {
+ nativeProps.pressBackgroundColor = [UIColor yellowColor];
+ }
+
return nativeProps;
}
diff --git a/ios/interfaces/StyleBase.h b/ios/interfaces/StyleBase.h
index df65437a..26be1ba4 100644
--- a/ios/interfaces/StyleBase.h
+++ b/ios/interfaces/StyleBase.h
@@ -1,19 +1,18 @@
#pragma once
#import "AttributeEntry.h"
+#import "EnrichedViewHost.h"
#import "StylePair.h"
#import "StyleTypeEnum.h"
#import
-@class EnrichedTextInputView;
-
@interface StyleBase : NSObject
-@property(nonatomic, weak) EnrichedTextInputView *input;
+@property(nonatomic, weak) id host;
+ (StyleType)getType;
- (NSString *)getKey;
- (NSString *)getValue;
- (BOOL)isParagraph;
- (BOOL)needsZWS;
-- (instancetype)initWithInput:(EnrichedTextInputView *)input;
+- (instancetype)initWithHost:(id)host;
- (NSRange)actualUsedRange:(NSRange)range;
- (void)toggle:(NSRange)range;
- (void)add:(NSRange)range
diff --git a/ios/interfaces/StyleBase.mm b/ios/interfaces/StyleBase.mm
index b91d4e68..7754008f 100644
--- a/ios/interfaces/StyleBase.mm
+++ b/ios/interfaces/StyleBase.mm
@@ -1,6 +1,5 @@
#import "StyleBase.h"
#import "AttributeEntry.h"
-#import "EnrichedTextInputView.h"
#import "OccurenceUtils.h"
#import "RangeUtils.h"
#import "ZeroWidthSpaceUtils.h"
@@ -35,9 +34,9 @@ - (BOOL)needsZWS {
return NO;
}
-- (instancetype)initWithInput:(EnrichedTextInputView *)input {
+- (instancetype)initWithHost:(id)host {
self = [super init];
- _input = input;
+ _host = host;
return self;
}
@@ -45,7 +44,7 @@ - (instancetype)initWithInput:(EnrichedTextInputView *)input {
- (NSRange)actualUsedRange:(NSRange)range {
if (![self isParagraph])
return range;
- return [_input->textView.textStorage.string paragraphRangeForRange:range];
+ return [self.host.textView.textStorage.string paragraphRangeForRange:range];
}
- (void)toggle:(NSRange)range {
@@ -76,11 +75,11 @@ - (void)add:(NSRange)range
NSRange actualRange = [self actualUsedRange:range];
if (![self isParagraph]) {
- [_input->textView.textStorage addAttribute:[self getKey]
- value:value
- range:actualRange];
+ [self.host.textView.textStorage addAttribute:[self getKey]
+ value:value
+ range:actualRange];
} else {
- [_input->textView.textStorage
+ [self.host.textView.textStorage
enumerateAttribute:NSParagraphStyleAttributeName
inRange:actualRange
options:0
@@ -93,7 +92,7 @@ - (void)add:(NSRange)range
pStyle.textLists =
@[ [[NSTextList alloc] initWithMarkerFormat:value
options:0] ];
- [_input->textView.textStorage
+ [self.host.textView.textStorage
addAttribute:NSParagraphStyleAttributeName
value:pStyle
range:subRange];
@@ -106,7 +105,7 @@ - (void)add:(NSRange)range
// Notify attributes manager of styling to be re-done if needed.
if (withDirtyRange) {
- [self.input->attributesManager addDirtyRange:actualRange];
+ [self.host.attributesManager addDirtyRange:actualRange];
}
}
@@ -114,10 +113,10 @@ - (void)remove:(NSRange)range withDirtyRange:(BOOL)withDirtyRange {
NSRange actualRange = [self actualUsedRange:range];
if (![self isParagraph]) {
- [_input->textView.textStorage removeAttribute:[self getKey]
- range:actualRange];
+ [self.host.textView.textStorage removeAttribute:[self getKey]
+ range:actualRange];
} else {
- [_input->textView.textStorage
+ [self.host.textView.textStorage
enumerateAttribute:NSParagraphStyleAttributeName
inRange:actualRange
options:0
@@ -128,7 +127,7 @@ - (void)remove:(NSRange)range withDirtyRange:(BOOL)withDirtyRange {
if (pStyle == nullptr)
return;
pStyle.textLists = @[];
- [_input->textView.textStorage
+ [self.host.textView.textStorage
addAttribute:NSParagraphStyleAttributeName
value:pStyle
range:subRange];
@@ -138,13 +137,13 @@ - (void)remove:(NSRange)range withDirtyRange:(BOOL)withDirtyRange {
// Notify attributes manager of styling to be re-done if needed.
if (withDirtyRange) {
- [self.input->attributesManager addDirtyRange:actualRange];
+ [self.host.attributesManager addDirtyRange:actualRange];
}
}
- (void)addTypingWithValue:(NSString *)value {
NSMutableDictionary *newTypingAttrs =
- [_input->textView.typingAttributes mutableCopy];
+ [self.host.textView.typingAttributes mutableCopy];
if (![self isParagraph]) {
newTypingAttrs[[self getKey]] = value;
@@ -156,18 +155,18 @@ - (void)addTypingWithValue:(NSString *)value {
newTypingAttrs[NSParagraphStyleAttributeName] = pStyle;
}
- _input->textView.typingAttributes = newTypingAttrs;
+ self.host.textView.typingAttributes = newTypingAttrs;
}
- (void)removeTyping {
NSMutableDictionary *newTypingAttrs =
- [_input->textView.typingAttributes mutableCopy];
+ [self.host.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]];
+ [self.host.attributesManager didRemoveTypingAttribute:[self getKey]];
} else {
NSMutableParagraphStyle *pStyle =
[newTypingAttrs[NSParagraphStyleAttributeName] mutableCopy];
@@ -175,7 +174,7 @@ - (void)removeTyping {
newTypingAttrs[NSParagraphStyleAttributeName] = pStyle;
}
- _input->textView.typingAttributes = newTypingAttrs;
+ self.host.textView.typingAttributes = newTypingAttrs;
}
// custom styles (e.g. ImageStyle, MentionStyle) will likely need to override
@@ -195,14 +194,14 @@ - (BOOL)styleCondition:(id)value range:(NSRange)range {
- (BOOL)detect:(NSRange)range {
if (range.length >= 1) {
return [OccurenceUtils detect:[self getKey]
- withInput:_input
+ withHost:self.host
inRange:range
withCondition:^BOOL(id _Nullable value, NSRange range) {
return [self styleCondition:value range:range];
}];
} else {
return [OccurenceUtils detect:[self getKey]
- withInput:_input
+ withHost:self.host
atIndex:range.location
checkPrevious:[self isParagraph]
withCondition:^BOOL(id _Nullable value, NSRange range) {
@@ -213,7 +212,7 @@ - (BOOL)detect:(NSRange)range {
- (BOOL)any:(NSRange)range {
return [OccurenceUtils any:[self getKey]
- withInput:_input
+ withHost:self.host
inRange:range
withCondition:^BOOL(id _Nullable value, NSRange range) {
return [self styleCondition:value range:range];
@@ -222,7 +221,7 @@ - (BOOL)any:(NSRange)range {
- (NSArray *)all:(NSRange)range {
return [OccurenceUtils all:[self getKey]
- withInput:_input
+ withHost:self.host
inRange:range
withCondition:^BOOL(id _Nullable value, NSRange range) {
return [self styleCondition:value range:range];
diff --git a/ios/interfaces/StyleHeaders.h b/ios/interfaces/StyleHeaders.h
index 7f1d4c88..e2876936 100644
--- a/ios/interfaces/StyleHeaders.h
+++ b/ios/interfaces/StyleHeaders.h
@@ -27,6 +27,7 @@
- (NSRange)getFullLinkRangeAt:(NSUInteger)location;
- (void)handleAutomaticLinks:(NSString *)word inRange:(NSRange)wordRange;
- (void)handleManualLinks:(NSString *)word inRange:(NSRange)wordRange;
+- (void)applyLinkMetaWithData:(LinkData *)linkData range:(NSRange)range;
@end
@interface MentionStyle : StyleBase
@@ -40,6 +41,7 @@
- (MentionParams *)getMentionParamsAt:(NSUInteger)location;
- (NSRange)getFullMentionRangeAt:(NSUInteger)location;
- (NSValue *)getActiveMentionRange;
+- (void)applyMentionMeta:(MentionParams *)params range:(NSRange)range;
@end
@interface HeadingStyleBase : StyleBase
@@ -82,7 +84,8 @@
range:(NSRange)range
withTyping:(BOOL)withTyping
withDirtyRange:(BOOL)withDirtyRange;
-- (void)toggleCheckedAt:(NSUInteger)location;
+- (void)toggleCheckedAt:(NSUInteger)location
+ withDirtyRange:(BOOL)withDirtyRange;
- (BOOL)getCheckboxStateAt:(NSUInteger)location;
- (BOOL)handleNewlinesInRange:(NSRange)range replacementText:(NSString *)text;
@end
@@ -97,6 +100,7 @@
- (void)addImage:(NSString *)uri width:(CGFloat)width height:(CGFloat)height;
- (void)addImageAtRange:(NSRange)range
imageData:(ImageData *)imageData
- withSelection:(BOOL)withSelection;
+ withSelection:(BOOL)withSelection
+ withDirtyRange:(BOOL)withDirtyRange;
- (ImageData *)getImageDataAt:(NSUInteger)location;
@end
diff --git a/ios/internals/EnrichedTextComponentDescriptor.h b/ios/internals/EnrichedTextComponentDescriptor.h
new file mode 100644
index 00000000..996aec1f
--- /dev/null
+++ b/ios/internals/EnrichedTextComponentDescriptor.h
@@ -0,0 +1,19 @@
+#pragma once
+#include
+#include
+#include
+#include
+
+namespace facebook::react {
+class EnrichedTextComponentDescriptor final
+ : public ConcreteComponentDescriptor {
+public:
+ using ConcreteComponentDescriptor::ConcreteComponentDescriptor;
+ void adopt(ShadowNode &shadowNode) const override {
+ react_native_assert(
+ dynamic_cast(&shadowNode));
+ ConcreteComponentDescriptor::adopt(shadowNode);
+ }
+};
+
+} // namespace facebook::react
diff --git a/ios/internals/EnrichedTextViewShadowNode.h b/ios/internals/EnrichedTextViewShadowNode.h
new file mode 100644
index 00000000..631c8a19
--- /dev/null
+++ b/ios/internals/EnrichedTextViewShadowNode.h
@@ -0,0 +1,34 @@
+#pragma once
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace facebook::react {
+
+JSI_EXPORT extern const char EnrichedTextViewComponentName[];
+
+class EnrichedTextViewShadowNode
+ : public ConcreteViewShadowNode<
+ EnrichedTextViewComponentName, EnrichedTextViewProps,
+ EnrichedTextViewEventEmitter, EnrichedTextViewState> {
+public:
+ using ConcreteViewShadowNode::ConcreteViewShadowNode;
+ Size
+ measureContent(const LayoutContext &layoutContext,
+ const LayoutConstraints &layoutConstraints) const override;
+
+ static ShadowNodeTraits BaseTraits() {
+ auto traits = ConcreteViewShadowNode::BaseTraits();
+ traits.set(ShadowNodeTraits::Trait::LeafYogaNode);
+ traits.set(ShadowNodeTraits::Trait::MeasurableYogaNode);
+ return traits;
+ }
+
+private:
+ id setupMockTextView_() const;
+};
+
+} // namespace facebook::react
diff --git a/ios/internals/EnrichedTextViewShadowNode.mm b/ios/internals/EnrichedTextViewShadowNode.mm
new file mode 100644
index 00000000..ac5bc500
--- /dev/null
+++ b/ios/internals/EnrichedTextViewShadowNode.mm
@@ -0,0 +1,72 @@
+#import "EnrichedTextViewShadowNode.h"
+#import "CoreText/CoreText.h"
+#import
+#import
+#import
+
+namespace facebook::react {
+
+extern const char EnrichedTextViewComponentName[] = "EnrichedTextView";
+
+id EnrichedTextViewShadowNode::setupMockTextView_() const {
+ const int veryFarAway = 20000;
+ const int mockSize = 1000;
+ EnrichedTextView *mockView = [[EnrichedTextView alloc]
+ initWithFrame:(CGRectMake(veryFarAway, veryFarAway, mockSize, mockSize))];
+ const auto props = this->getProps();
+ [mockView updateProps:props oldProps:nullptr];
+ return mockView;
+}
+
+Size EnrichedTextViewShadowNode::measureContent(
+ const LayoutContext &layoutContext,
+ const LayoutConstraints &layoutConstraints) const {
+ const auto state = this->getStateData();
+ const auto componentRef = state.getComponentViewRef();
+ RCTInternalGenericWeakWrapper *weakWrapper =
+ (RCTInternalGenericWeakWrapper *)unwrapManagedObject(componentRef);
+
+ if (weakWrapper != nullptr) {
+ id componentObject = weakWrapper.object;
+ EnrichedTextView *typedComponentObject =
+ (EnrichedTextView *)componentObject;
+
+ if (typedComponentObject != nullptr) {
+ __block CGSize estimatedSize;
+
+ if ([NSThread isMainThread]) {
+ estimatedSize = [typedComponentObject
+ measureSize:layoutConstraints.maximumSize.width];
+ } else {
+ dispatch_sync(dispatch_get_main_queue(), ^{
+ estimatedSize = [typedComponentObject
+ measureSize:layoutConstraints.maximumSize.width];
+ });
+ }
+
+ return {estimatedSize.width,
+ MIN(estimatedSize.height, layoutConstraints.maximumSize.height)};
+ }
+ } else {
+ __block CGSize estimatedSize;
+
+ if ([NSThread isMainThread]) {
+ EnrichedTextView *mockView = setupMockTextView_();
+ estimatedSize =
+ [mockView measureSize:layoutConstraints.maximumSize.width];
+ } else {
+ dispatch_sync(dispatch_get_main_queue(), ^{
+ EnrichedTextView *mockView = setupMockTextView_();
+ estimatedSize =
+ [mockView measureSize:layoutConstraints.maximumSize.width];
+ });
+ }
+
+ return {estimatedSize.width,
+ MIN(estimatedSize.height, layoutConstraints.maximumSize.height)};
+ }
+
+ return Size();
+}
+
+} // namespace facebook::react
diff --git a/ios/internals/EnrichedTextViewState.cpp b/ios/internals/EnrichedTextViewState.cpp
new file mode 100644
index 00000000..ec988d05
--- /dev/null
+++ b/ios/internals/EnrichedTextViewState.cpp
@@ -0,0 +1,7 @@
+#include "EnrichedTextViewState.h"
+
+namespace facebook::react {
+std::shared_ptr EnrichedTextViewState::getComponentViewRef() const {
+ return componentViewRef_;
+}
+} // namespace facebook::react
diff --git a/ios/internals/EnrichedTextViewState.h b/ios/internals/EnrichedTextViewState.h
new file mode 100644
index 00000000..c32efa0f
--- /dev/null
+++ b/ios/internals/EnrichedTextViewState.h
@@ -0,0 +1,18 @@
+#pragma once
+#include
+
+namespace facebook::react {
+
+class EnrichedTextViewState {
+public:
+ EnrichedTextViewState() : componentViewRef_(nullptr) {}
+ explicit EnrichedTextViewState(std::shared_ptr ref) {
+ componentViewRef_ = ref;
+ }
+ std::shared_ptr getComponentViewRef() const;
+
+private:
+ std::shared_ptr componentViewRef_{};
+};
+
+} // namespace facebook::react
diff --git a/ios/styles/BlockQuoteStyle.mm b/ios/styles/BlockQuoteStyle.mm
index 98f30587..796a3cb5 100644
--- a/ios/styles/BlockQuoteStyle.mm
+++ b/ios/styles/BlockQuoteStyle.mm
@@ -21,9 +21,9 @@ - (BOOL)needsZWS {
}
- (void)applyStyling:(NSRange)range {
- CGFloat indent = [self.input->config blockquoteBorderWidth] +
- [self.input->config blockquoteGapWidth];
- [self.input->textView.textStorage
+ CGFloat indent = [self.host.config blockquoteBorderWidth] +
+ [self.host.config blockquoteGapWidth];
+ [self.host.textView.textStorage
enumerateAttribute:NSParagraphStyleAttributeName
inRange:range
options:0
@@ -33,23 +33,22 @@ - (void)applyStyling:(NSRange)range {
[(NSParagraphStyle *)value mutableCopy];
pStyle.headIndent = indent;
pStyle.firstLineHeadIndent = indent;
- [self.input->textView.textStorage
+ [self.host.textView.textStorage
addAttribute:NSParagraphStyleAttributeName
value:pStyle
range:subRange];
}];
- UIColor *bqColor = [self.input->config blockquoteColor];
- [self.input->textView.textStorage addAttribute:NSForegroundColorAttributeName
- value:bqColor
- range:range];
- [self.input->textView.textStorage addAttribute:NSUnderlineColorAttributeName
- value:bqColor
- range:range];
- [self.input->textView.textStorage
- addAttribute:NSStrikethroughColorAttributeName
- value:bqColor
- range:range];
+ UIColor *bqColor = [self.host.config blockquoteColor];
+ [self.host.textView.textStorage addAttribute:NSForegroundColorAttributeName
+ value:bqColor
+ range:range];
+ [self.host.textView.textStorage addAttribute:NSUnderlineColorAttributeName
+ value:bqColor
+ range:range];
+ [self.host.textView.textStorage addAttribute:NSStrikethroughColorAttributeName
+ value:bqColor
+ range:range];
}
@end
diff --git a/ios/styles/BoldStyle.mm b/ios/styles/BoldStyle.mm
index c9f25123..025866c9 100644
--- a/ios/styles/BoldStyle.mm
+++ b/ios/styles/BoldStyle.mm
@@ -17,7 +17,7 @@ - (BOOL)isParagraph {
}
- (void)applyStyling:(NSRange)range {
- [self.input->textView.textStorage
+ [self.host.textView.textStorage
enumerateAttribute:NSFontAttributeName
inRange:range
options:0
@@ -26,7 +26,7 @@ - (void)applyStyling:(NSRange)range {
UIFont *font = (UIFont *)value;
if (font != nullptr) {
UIFont *newFont = [font setBold];
- [self.input->textView.textStorage
+ [self.host.textView.textStorage
addAttribute:NSFontAttributeName
value:newFont
range:range];
diff --git a/ios/styles/CheckboxListStyle.mm b/ios/styles/CheckboxListStyle.mm
index 6465db09..2b0104a0 100644
--- a/ios/styles/CheckboxListStyle.mm
+++ b/ios/styles/CheckboxListStyle.mm
@@ -22,11 +22,11 @@ - (BOOL)needsZWS {
}
- (void)applyStyling:(NSRange)range {
- CGFloat listHeadIndent = [self.input->config checkboxListMarginLeft] +
- [self.input->config checkboxListGapWidth] +
- [self.input->config checkboxListBoxSize];
+ CGFloat listHeadIndent = [self.host.config checkboxListMarginLeft] +
+ [self.host.config checkboxListGapWidth] +
+ [self.host.config checkboxListBoxSize];
- [self.input->textView.textStorage
+ [self.host.textView.textStorage
enumerateAttribute:NSParagraphStyleAttributeName
inRange:range
options:0
@@ -36,7 +36,7 @@ - (void)applyStyling:(NSRange)range {
[(NSParagraphStyle *)value mutableCopy];
pStyle.headIndent = listHeadIndent;
pStyle.firstLineHeadIndent = listHeadIndent;
- [self.input->textView.textStorage
+ [self.host.textView.textStorage
addAttribute:NSParagraphStyleAttributeName
value:pStyle
range:range];
@@ -87,38 +87,39 @@ - (void)reapplyFromStylePair:(StylePair *)pair {
[self addWithChecked:checked range:range withTyping:NO withDirtyRange:NO];
}
-- (void)toggleCheckedAt:(NSUInteger)location {
- if (location >= self.input->textView.textStorage.length) {
+- (void)toggleCheckedAt:(NSUInteger)location
+ withDirtyRange:(BOOL)withDirtyRange {
+ if (location >= self.host.textView.textStorage.length) {
return;
}
NSParagraphStyle *pStyle =
- [self.input->textView.textStorage attribute:NSParagraphStyleAttributeName
- atIndex:location
- effectiveRange:NULL];
+ [self.host.textView.textStorage attribute:NSParagraphStyleAttributeName
+ atIndex:location
+ effectiveRange:NULL];
NSTextList *list = pStyle.textLists.firstObject;
BOOL isCurrentlyChecked =
[list.markerFormat isEqualToString:@"EnrichedCheckbox1"];
- NSRange paragraphRange = [self.input->textView.textStorage.string
+ NSRange paragraphRange = [self.host.textView.textStorage.string
paragraphRangeForRange:NSMakeRange(location, 0)];
[self addWithChecked:!isCurrentlyChecked
range:paragraphRange
withTyping:NO
- withDirtyRange:YES];
+ withDirtyRange:withDirtyRange];
}
- (BOOL)getCheckboxStateAt:(NSUInteger)location {
- if (location >= self.input->textView.textStorage.length) {
+ if (location >= self.host.textView.textStorage.length) {
return NO;
}
NSParagraphStyle *style =
- [self.input->textView.textStorage attribute:NSParagraphStyleAttributeName
- atIndex:location
- effectiveRange:NULL];
+ [self.host.textView.textStorage attribute:NSParagraphStyleAttributeName
+ atIndex:location
+ effectiveRange:NULL];
if (style && style.textLists.count > 0) {
NSTextList *list = style.textLists.firstObject;
@@ -131,18 +132,18 @@ - (BOOL)getCheckboxStateAt:(NSUInteger)location {
}
- (BOOL)handleNewlinesInRange:(NSRange)range replacementText:(NSString *)text {
- if ([self detect:self.input->textView.selectedRange] && text.length > 0 &&
+ if ([self detect:self.host.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
+ input:self.host
withSelection:YES];
// apply unchecked checkbox attributes to the new paragraph
[self addWithChecked:NO
- range:self.input->textView.selectedRange
+ range:self.host.textView.selectedRange
withTyping:YES
withDirtyRange:YES];
return YES;
diff --git a/ios/styles/CodeBlockStyle.mm b/ios/styles/CodeBlockStyle.mm
index 65d44af5..74c3dbeb 100644
--- a/ios/styles/CodeBlockStyle.mm
+++ b/ios/styles/CodeBlockStyle.mm
@@ -21,7 +21,7 @@ - (BOOL)needsZWS {
}
- (void)applyStyling:(NSRange)range {
- [self.input->textView.textStorage
+ [self.host.textView.textStorage
enumerateAttribute:NSFontAttributeName
inRange:range
options:0
@@ -30,19 +30,19 @@ - (void)applyStyling:(NSRange)range {
UIFont *currentFont = (UIFont *)value;
if (currentFont == nullptr)
return;
- UIFont *monoFont = [[[self.input->config monospacedFont]
+ UIFont *monoFont = [[[self.host.config monospacedFont]
withFontTraits:currentFont] setSize:currentFont.pointSize];
if (monoFont != nullptr) {
- [self.input->textView.textStorage
+ [self.host.textView.textStorage
addAttribute:NSFontAttributeName
value:monoFont
range:subRange];
}
}];
- [self.input->textView.textStorage
+ [self.host.textView.textStorage
addAttribute:NSForegroundColorAttributeName
- value:[self.input->config codeBlockFgColor]
+ value:[self.host.config codeBlockFgColor]
range:range];
}
diff --git a/ios/styles/EnrichedTextStyles.mm b/ios/styles/EnrichedTextStyles.mm
new file mode 100644
index 00000000..d4b6cbfb
--- /dev/null
+++ b/ios/styles/EnrichedTextStyles.mm
@@ -0,0 +1,58 @@
+#import "EnrichedTextStyleHeaders.h"
+
+@implementation EnrichedTextBoldStyle
+@end
+
+@implementation EnrichedTextItalicStyle
+@end
+
+@implementation EnrichedTextUnderlineStyle
+@end
+
+@implementation EnrichedTextStrikethroughStyle
+@end
+
+@implementation EnrichedTextInlineCodeStyle
+@end
+
+@implementation EnrichedTextH1Style
+@end
+
+@implementation EnrichedTextH2Style
+@end
+
+@implementation EnrichedTextH3Style
+@end
+
+@implementation EnrichedTextH4Style
+@end
+
+@implementation EnrichedTextH5Style
+@end
+
+@implementation EnrichedTextH6Style
+@end
+
+@implementation EnrichedTextBlockQuoteStyle
+@end
+
+@implementation EnrichedTextCodeBlockStyle
+@end
+
+@implementation EnrichedTextUnorderedListStyle
+@end
+
+@implementation EnrichedTextOrderedListStyle
+@end
+
+@implementation EnrichedTextLinkStyle
+@end
+
+@implementation EnrichedTextMentionStyle
+@end
+
+@implementation EnrichedTextCheckboxListStyle
+@end
+
+@implementation EnrichedTextImageStyle
+@end
diff --git a/ios/styles/H1Style.mm b/ios/styles/H1Style.mm
index c04c26f3..c68c46a6 100644
--- a/ios/styles/H1Style.mm
+++ b/ios/styles/H1Style.mm
@@ -12,9 +12,9 @@ - (BOOL)isParagraph {
return YES;
}
- (CGFloat)getHeadingFontSize {
- return [self.input->config h1FontSize];
+ return [self.host.config h1FontSize];
}
- (BOOL)isHeadingBold {
- return [self.input->config h1Bold];
+ return [self.host.config h1Bold];
}
@end
diff --git a/ios/styles/H2Style.mm b/ios/styles/H2Style.mm
index 785b2b72..98021bad 100644
--- a/ios/styles/H2Style.mm
+++ b/ios/styles/H2Style.mm
@@ -12,9 +12,9 @@ - (BOOL)isParagraph {
return YES;
}
- (CGFloat)getHeadingFontSize {
- return [self.input->config h2FontSize];
+ return [self.host.config h2FontSize];
}
- (BOOL)isHeadingBold {
- return [self.input->config h2Bold];
+ return [self.host.config h2Bold];
}
@end
diff --git a/ios/styles/H3Style.mm b/ios/styles/H3Style.mm
index 9091f6e3..41190238 100644
--- a/ios/styles/H3Style.mm
+++ b/ios/styles/H3Style.mm
@@ -12,9 +12,9 @@ - (BOOL)isParagraph {
return YES;
}
- (CGFloat)getHeadingFontSize {
- return [self.input->config h3FontSize];
+ return [self.host.config h3FontSize];
}
- (BOOL)isHeadingBold {
- return [self.input->config h3Bold];
+ return [self.host.config h3Bold];
}
@end
diff --git a/ios/styles/H4Style.mm b/ios/styles/H4Style.mm
index a641f5ed..135412af 100644
--- a/ios/styles/H4Style.mm
+++ b/ios/styles/H4Style.mm
@@ -12,9 +12,9 @@ - (BOOL)isParagraph {
return YES;
}
- (CGFloat)getHeadingFontSize {
- return [self.input->config h4FontSize];
+ return [self.host.config h4FontSize];
}
- (BOOL)isHeadingBold {
- return [self.input->config h4Bold];
+ return [self.host.config h4Bold];
}
@end
diff --git a/ios/styles/H5Style.mm b/ios/styles/H5Style.mm
index 40fab0f2..01f77517 100644
--- a/ios/styles/H5Style.mm
+++ b/ios/styles/H5Style.mm
@@ -12,9 +12,9 @@ - (BOOL)isParagraph {
return YES;
}
- (CGFloat)getHeadingFontSize {
- return [self.input->config h5FontSize];
+ return [self.host.config h5FontSize];
}
- (BOOL)isHeadingBold {
- return [self.input->config h5Bold];
+ return [self.host.config h5Bold];
}
@end
diff --git a/ios/styles/H6Style.mm b/ios/styles/H6Style.mm
index 2e576bb0..0f87b37f 100644
--- a/ios/styles/H6Style.mm
+++ b/ios/styles/H6Style.mm
@@ -12,9 +12,9 @@ - (BOOL)isParagraph {
return YES;
}
- (CGFloat)getHeadingFontSize {
- return [self.input->config h6FontSize];
+ return [self.host.config h6FontSize];
}
- (BOOL)isHeadingBold {
- return [self.input->config h6Bold];
+ return [self.host.config h6Bold];
}
@end
diff --git a/ios/styles/HeadingStyleBase.mm b/ios/styles/HeadingStyleBase.mm
index 6c2e83a2..15c389d4 100644
--- a/ios/styles/HeadingStyleBase.mm
+++ b/ios/styles/HeadingStyleBase.mm
@@ -23,7 +23,7 @@ - (BOOL)isParagraph {
}
- (void)applyStyling:(NSRange)range {
- [self.input->textView.textStorage
+ [self.host.textView.textStorage
enumerateAttribute:NSFontAttributeName
inRange:range
options:0
@@ -36,27 +36,26 @@ - (void)applyStyling:(NSRange)range {
if ([self isHeadingBold]) {
newFont = [newFont setBold];
}
- [self.input->textView.textStorage
- addAttribute:NSFontAttributeName
- value:newFont
- range:subRange];
+ [self.host.textView.textStorage addAttribute:NSFontAttributeName
+ value:newFont
+ range:subRange];
}];
}
// 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 detect:self.input->textView.selectedRange] && text.length > 0 &&
+ if ([self detect:self.host.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
+ input:self.host
withSelection:YES];
// remove the attributes at the new selection
- [self remove:self.input->textView.selectedRange withDirtyRange:YES];
+ [self remove:self.host.textView.selectedRange withDirtyRange:YES];
return YES;
}
return NO;
diff --git a/ios/styles/ImageStyle.mm b/ios/styles/ImageStyle.mm
index 4149e30c..4dfa1bbd 100644
--- a/ios/styles/ImageStyle.mm
+++ b/ios/styles/ImageStyle.mm
@@ -33,13 +33,13 @@ - (void)reapplyFromStylePair:(StylePair *)pair {
ImageAttachment *attachment =
[[ImageAttachment alloc] initWithImageData:imageData];
- attachment.delegate = self.input;
+ attachment.delegate = (id)self.host;
- [self.input->textView.textStorage addAttributes:@{
+ [self.host.textView.textStorage addAttributes:@{
NSAttachmentAttributeName : attachment,
ImageAttributeName : imageData
}
- range:range];
+ range:range];
}
- (AttributeEntry *)getEntryIfPresent:(NSRange)range {
@@ -51,15 +51,15 @@ - (void)toggle:(NSRange)range {
}
- (void)remove:(NSRange)range withDirtyRange:(BOOL)withDirtyRange {
- [self.input->textView.textStorage beginEditing];
- [self.input->textView.textStorage removeAttribute:ImageAttributeName
- range:range];
- [self.input->textView.textStorage removeAttribute:NSAttachmentAttributeName
- range:range];
- [self.input->textView.textStorage endEditing];
+ [self.host.textView.textStorage beginEditing];
+ [self.host.textView.textStorage removeAttribute:ImageAttributeName
+ range:range];
+ [self.host.textView.textStorage removeAttribute:NSAttachmentAttributeName
+ range:range];
+ [self.host.textView.textStorage endEditing];
if (withDirtyRange) {
- [self.input->attributesManager addDirtyRange:range];
+ [self.host.attributesManager addDirtyRange:range];
}
[self removeTyping];
@@ -67,11 +67,11 @@ - (void)remove:(NSRange)range withDirtyRange:(BOOL)withDirtyRange {
- (void)removeTyping {
NSMutableDictionary *currentAttributes =
- [self.input->textView.typingAttributes mutableCopy];
+ [self.host.textView.typingAttributes mutableCopy];
[currentAttributes removeObjectForKey:ImageAttributeName];
[currentAttributes removeObjectForKey:NSAttachmentAttributeName];
- [self.input->attributesManager didRemoveTypingAttribute:ImageAttributeName];
- self.input->textView.typingAttributes = currentAttributes;
+ [self.host.attributesManager didRemoveTypingAttribute:ImageAttributeName];
+ self.host.textView.typingAttributes = currentAttributes;
}
- (BOOL)styleCondition:(id _Nullable)value range:(NSRange)range {
@@ -80,32 +80,33 @@ - (BOOL)styleCondition:(id _Nullable)value range:(NSRange)range {
- (ImageData *)getImageDataAt:(NSUInteger)location {
NSRange imageRange = NSMakeRange(0, 0);
- NSRange inputRange = NSMakeRange(0, self.input->textView.textStorage.length);
+ NSRange inputRange = NSMakeRange(0, self.host.textView.textStorage.length);
// don't search at the very end of input
NSUInteger searchLocation = location;
- if (searchLocation == self.input->textView.textStorage.length) {
+ if (searchLocation == self.host.textView.textStorage.length) {
return nullptr;
}
ImageData *imageData =
- [self.input->textView.textStorage attribute:ImageAttributeName
- atIndex:searchLocation
- longestEffectiveRange:&imageRange
- inRange:inputRange];
+ [self.host.textView.textStorage attribute:ImageAttributeName
+ atIndex:searchLocation
+ longestEffectiveRange:&imageRange
+ inRange:inputRange];
return imageData;
}
- (void)addImageAtRange:(NSRange)range
imageData:(ImageData *)imageData
- withSelection:(BOOL)withSelection {
+ withSelection:(BOOL)withSelection
+ withDirtyRange:(BOOL)withDirtyRange {
if (!imageData)
return;
ImageAttachment *attachment =
[[ImageAttachment alloc] initWithImageData:imageData];
- attachment.delegate = self.input;
+ attachment.delegate = (id)self.host;
NSDictionary *attributes =
@{NSAttachmentAttributeName : attachment, ImageAttributeName : imageData};
@@ -118,18 +119,20 @@ - (void)addImageAtRange:(NSRange)range
[TextInsertionUtils insertText:placeholderChar
at:range.location
additionalAttributes:attributes
- input:self.input
+ input:self.host
withSelection:withSelection];
} else {
[TextInsertionUtils replaceText:placeholderChar
at:range
additionalAttributes:attributes
- input:self.input
+ input:self.host
withSelection:withSelection];
}
- NSRange insertedImageRange = NSMakeRange(range.location, 1);
- [self.input->attributesManager addDirtyRange:insertedImageRange];
+ if (withDirtyRange) {
+ NSRange insertedImageRange = NSMakeRange(range.location, 1);
+ [self.host.attributesManager addDirtyRange:insertedImageRange];
+ }
}
- (void)addImage:(NSString *)uri width:(CGFloat)width height:(CGFloat)height {
@@ -138,9 +141,10 @@ - (void)addImage:(NSString *)uri width:(CGFloat)width height:(CGFloat)height {
data.width = width;
data.height = height;
- [self addImageAtRange:self.input->textView.selectedRange
+ [self addImageAtRange:self.host.textView.selectedRange
imageData:data
- withSelection:YES];
+ withSelection:YES
+ withDirtyRange:YES];
}
@end
diff --git a/ios/styles/InlineCodeStyle.mm b/ios/styles/InlineCodeStyle.mm
index 054332a7..4511858a 100644
--- a/ios/styles/InlineCodeStyle.mm
+++ b/ios/styles/InlineCodeStyle.mm
@@ -21,29 +21,29 @@ - (BOOL)isParagraph {
- (void)applyStyling:(NSRange)range {
// we don't want to apply inline code to newline characters, it looks bad
NSArray *nonNewlineRanges =
- [RangeUtils getNonNewlineRangesIn:self.input->textView range:range];
+ [RangeUtils getNonNewlineRangesIn:self.host.textView range:range];
for (NSValue *value in nonNewlineRanges) {
NSRange subRange = [value rangeValue];
- [self.input->textView.textStorage
+ [self.host.textView.textStorage
addAttribute:NSBackgroundColorAttributeName
- value:[[self.input->config inlineCodeBgColor]
+ value:[[self.host.config inlineCodeBgColor]
colorWithAlphaIfNotTransparent:0.4]
range:subRange];
- [self.input->textView.textStorage
+ [self.host.textView.textStorage
addAttribute:NSForegroundColorAttributeName
- value:[self.input->config inlineCodeFgColor]
+ value:[self.host.config inlineCodeFgColor]
range:subRange];
- [self.input->textView.textStorage
+ [self.host.textView.textStorage
addAttribute:NSUnderlineColorAttributeName
- value:[self.input->config inlineCodeFgColor]
+ value:[self.host.config inlineCodeFgColor]
range:subRange];
- [self.input->textView.textStorage
+ [self.host.textView.textStorage
addAttribute:NSStrikethroughColorAttributeName
- value:[self.input->config inlineCodeFgColor]
+ value:[self.host.config inlineCodeFgColor]
range:subRange];
- [self.input->textView.textStorage
+ [self.host.textView.textStorage
enumerateAttribute:NSFontAttributeName
inRange:subRange
options:0
@@ -51,9 +51,9 @@ - (void)applyStyling:(NSRange)range {
BOOL *_Nonnull stop) {
UIFont *font = (UIFont *)value;
if (font != nullptr) {
- UIFont *newFont = [[[self.input->config monospacedFont]
+ UIFont *newFont = [[[self.host.config monospacedFont]
withFontTraits:font] setSize:font.pointSize];
- [self.input->textView.textStorage
+ [self.host.textView.textStorage
addAttribute:NSFontAttributeName
value:newFont
range:fontRange];
diff --git a/ios/styles/ItalicStyle.mm b/ios/styles/ItalicStyle.mm
index 963a213e..f2a16114 100644
--- a/ios/styles/ItalicStyle.mm
+++ b/ios/styles/ItalicStyle.mm
@@ -17,7 +17,7 @@ - (BOOL)isParagraph {
}
- (void)applyStyling:(NSRange)range {
- [self.input->textView.textStorage
+ [self.host.textView.textStorage
enumerateAttribute:NSFontAttributeName
inRange:range
options:0
@@ -26,7 +26,7 @@ - (void)applyStyling:(NSRange)range {
UIFont *font = (UIFont *)value;
if (font != nullptr) {
UIFont *newFont = [font setItalic];
- [self.input->textView.textStorage
+ [self.host.textView.textStorage
addAttribute:NSFontAttributeName
value:newFont
range:range];
diff --git a/ios/styles/LinkStyle.mm b/ios/styles/LinkStyle.mm
index 626969e8..3cc00762 100644
--- a/ios/styles/LinkStyle.mm
+++ b/ios/styles/LinkStyle.mm
@@ -31,13 +31,13 @@ - (void)applyStyling:(NSRange)range {
}
NSMutableDictionary *newAttrs = [[NSMutableDictionary alloc] init];
- newAttrs[NSForegroundColorAttributeName] = [self.input->config linkColor];
- newAttrs[NSUnderlineColorAttributeName] = [self.input->config linkColor];
- newAttrs[NSStrikethroughColorAttributeName] = [self.input->config linkColor];
- if ([self.input->config linkDecorationLine] == DecorationUnderline) {
+ newAttrs[NSForegroundColorAttributeName] = [self.host.config linkColor];
+ newAttrs[NSUnderlineColorAttributeName] = [self.host.config linkColor];
+ newAttrs[NSStrikethroughColorAttributeName] = [self.host.config linkColor];
+ if ([self.host.config linkDecorationLine] == DecorationUnderline) {
newAttrs[NSUnderlineStyleAttributeName] = @(NSUnderlineStyleSingle);
}
- [self.input->textView.textStorage addAttributes:newAttrs range:range];
+ [self.host.textView.textStorage addAttributes:newAttrs range:range];
}
- (void)reapplyFromStylePair:(StylePair *)pair {
@@ -63,34 +63,34 @@ - (void)toggle:(NSRange)range {
// we have to make sure all links in the range get fully removed here
- (void)remove:(NSRange)range withDirtyRange:(BOOL)withDirtyRange {
NSArray *links = [self all:range];
- [self.input->textView.textStorage beginEditing];
+ [self.host.textView.textStorage beginEditing];
for (StylePair *pair in links) {
NSRange linkRange =
[self getFullLinkRangeAt:[pair.rangeValue rangeValue].location];
- [self.input->textView.textStorage removeAttribute:ManualLinkAttributeName
- range:linkRange];
- [self.input->textView.textStorage removeAttribute:AutomaticLinkAttributeName
- range:linkRange];
+ [self.host.textView.textStorage removeAttribute:ManualLinkAttributeName
+ range:linkRange];
+ [self.host.textView.textStorage removeAttribute:AutomaticLinkAttributeName
+ range:linkRange];
if (withDirtyRange) {
- [self.input->attributesManager addDirtyRange:linkRange];
+ [self.host.attributesManager addDirtyRange:linkRange];
}
}
- [self.input->textView.textStorage endEditing];
+ [self.host.textView.textStorage endEditing];
[self removeLinkMetaFromTypingAttributes];
}
// used for conflicts, we have to remove the whole link
- (void)removeTyping {
NSRange linkRange =
- [self getFullLinkRangeAt:self.input->textView.selectedRange.location];
+ [self getFullLinkRangeAt:self.host.textView.selectedRange.location];
if (linkRange.length > 0) {
- [self.input->textView.textStorage beginEditing];
- [self.input->textView.textStorage removeAttribute:ManualLinkAttributeName
- range:linkRange];
- [self.input->textView.textStorage removeAttribute:AutomaticLinkAttributeName
- range:linkRange];
- [self.input->textView.textStorage endEditing];
- [self.input->attributesManager addDirtyRange:linkRange];
+ [self.host.textView.textStorage beginEditing];
+ [self.host.textView.textStorage removeAttribute:ManualLinkAttributeName
+ range:linkRange];
+ [self.host.textView.textStorage removeAttribute:AutomaticLinkAttributeName
+ range:linkRange];
+ [self.host.textView.textStorage endEditing];
+ [self.host.attributesManager addDirtyRange:linkRange];
}
[self removeLinkMetaFromTypingAttributes];
}
@@ -104,7 +104,7 @@ - (BOOL)detect:(NSRange)range {
if (range.length >= 1) {
BOOL onlyLinks = [OccurenceUtils
detectMultiple:@[ ManualLinkAttributeName, AutomaticLinkAttributeName ]
- withInput:self.input
+ withHost:self.host
inRange:range
withCondition:^BOOL(id _Nullable value, NSRange subrange) {
return [self styleCondition:value range:subrange];
@@ -117,7 +117,7 @@ - (BOOL)detect:(NSRange)range {
- (BOOL)any:(NSRange)range {
return [OccurenceUtils
anyMultiple:@[ ManualLinkAttributeName, AutomaticLinkAttributeName ]
- withInput:self.input
+ withHost:self.host
inRange:range
withCondition:^BOOL(id _Nullable value, NSRange subrange) {
return [self styleCondition:value range:subrange];
@@ -127,7 +127,7 @@ - (BOOL)any:(NSRange)range {
- (NSArray *)all:(NSRange)range {
return [OccurenceUtils
allMultiple:@[ ManualLinkAttributeName, AutomaticLinkAttributeName ]
- withInput:self.input
+ withHost:self.host
inRange:range
withCondition:^BOOL(id _Nullable value, NSRange subrange) {
return [self styleCondition:value range:subrange];
@@ -140,21 +140,21 @@ - (void)applyLinkMetaWithData:(LinkData *)linkData range:(NSRange)range {
}
NSString *key =
linkData.isManual ? ManualLinkAttributeName : AutomaticLinkAttributeName;
- [self.input->textView.textStorage addAttribute:key
- value:[linkData copy]
- range:range];
+ [self.host.textView.textStorage addAttribute:key
+ value:[linkData copy]
+ range:range];
}
- (void)removeLinkMetaFromTypingAttributes {
NSMutableDictionary *newTypingAttrs =
- [self.input->textView.typingAttributes mutableCopy];
+ [self.host.textView.typingAttributes mutableCopy];
[newTypingAttrs removeObjectForKey:ManualLinkAttributeName];
[newTypingAttrs removeObjectForKey:AutomaticLinkAttributeName];
- self.input->textView.typingAttributes = newTypingAttrs;
+ self.host.textView.typingAttributes = newTypingAttrs;
- [self.input->attributesManager
+ [self.host.attributesManager
didRemoveTypingAttribute:ManualLinkAttributeName];
- [self.input->attributesManager
+ [self.host.attributesManager
didRemoveTypingAttribute:AutomaticLinkAttributeName];
}
@@ -162,7 +162,7 @@ - (void)addLink:(LinkData *)linkData
range:(NSRange)range
withSelection:(BOOL)withSelection {
NSString *currentText =
- [self.input->textView.textStorage.string substringWithRange:range];
+ [self.host.textView.textStorage.string substringWithRange:range];
NSString *key =
linkData.isManual ? ManualLinkAttributeName : AutomaticLinkAttributeName;
@@ -175,7 +175,7 @@ - (void)addLink:(LinkData *)linkData
[TextInsertionUtils insertText:linkData.text
at:range.location
additionalAttributes:metaAttrs
- input:self.input
+ input:self.host
withSelection:withSelection];
dirtyRange = NSMakeRange(range.location, linkData.text.length);
} else if ([currentText isEqualToString:linkData.text]) {
@@ -185,8 +185,8 @@ - (void)addLink:(LinkData *)linkData
// manually set it behind the link ONLY with manual links, automatic ones
// don't need the selection fix
if (linkData.isManual && withSelection) {
- [self.input->textView reactFocus];
- self.input->textView.selectedRange =
+ [self.host.textView reactFocus];
+ self.host.textView.selectedRange =
NSMakeRange(range.location + linkData.text.length, 0);
}
} else {
@@ -194,18 +194,18 @@ - (void)addLink:(LinkData *)linkData
[TextInsertionUtils replaceText:linkData.text
at:range
additionalAttributes:metaAttrs
- input:self.input
+ input:self.host
withSelection:withSelection];
dirtyRange = NSMakeRange(range.location, linkData.text.length);
}
// add new dirty range
- [self.input->attributesManager addDirtyRange:dirtyRange];
+ [self.host.attributesManager addDirtyRange:dirtyRange];
// mandatory connected links check
NSDictionary *currentWord =
- [WordsUtils getCurrentWord:self.input->textView.textStorage.string
- range:self.input->textView.selectedRange];
+ [WordsUtils getCurrentWord:self.host.textView.textStorage.string
+ range:self.host.textView.selectedRange];
if (currentWord != nullptr) {
// get word properties
NSString *wordText = (NSString *)[currentWord objectForKey:@"word"];
@@ -221,24 +221,24 @@ - (void)addLink:(LinkData *)linkData
- (LinkData *)getLinkDataAt:(NSUInteger)location {
NSRange manualLinkRange = NSMakeRange(0, 0);
NSRange automaticLinkRange = NSMakeRange(0, 0);
- NSRange inputRange = NSMakeRange(0, self.input->textView.textStorage.length);
+ NSRange inputRange = NSMakeRange(0, self.host.textView.textStorage.length);
// don't search at the very end of input
NSUInteger searchLocation = location;
- if (searchLocation == self.input->textView.textStorage.length) {
+ if (searchLocation == self.host.textView.textStorage.length) {
return nullptr;
}
LinkData *manualData =
- [self.input->textView.textStorage attribute:ManualLinkAttributeName
- atIndex:searchLocation
- longestEffectiveRange:&manualLinkRange
- inRange:inputRange];
+ [self.host.textView.textStorage attribute:ManualLinkAttributeName
+ atIndex:searchLocation
+ longestEffectiveRange:&manualLinkRange
+ inRange:inputRange];
LinkData *automaticData =
- [self.input->textView.textStorage attribute:AutomaticLinkAttributeName
- atIndex:searchLocation
- longestEffectiveRange:&automaticLinkRange
- inRange:inputRange];
+ [self.host.textView.textStorage attribute:AutomaticLinkAttributeName
+ atIndex:searchLocation
+ longestEffectiveRange:&automaticLinkRange
+ inRange:inputRange];
if ((manualData == nullptr && automaticData == nullptr) ||
(manualLinkRange.length == 0 && automaticLinkRange.length == 0)) {
@@ -252,11 +252,11 @@ - (LinkData *)getLinkDataAt:(NSUInteger)location {
- (NSRange)getFullLinkRangeAt:(NSUInteger)location {
NSRange manualLinkRange = NSMakeRange(0, 0);
NSRange automaticLinkRange = NSMakeRange(0, 0);
- NSRange inputRange = NSMakeRange(0, self.input->textView.textStorage.length);
+ NSRange inputRange = NSMakeRange(0, self.host.textView.textStorage.length);
// get the previous index if possible when at the very end of input
NSUInteger searchLocation = location;
- if (searchLocation == self.input->textView.textStorage.length) {
+ if (searchLocation == self.host.textView.textStorage.length) {
if (searchLocation == 0) {
return NSMakeRange(0, 0);
}
@@ -264,15 +264,15 @@ - (NSRange)getFullLinkRangeAt:(NSUInteger)location {
}
LinkData *manualData =
- [self.input->textView.textStorage attribute:ManualLinkAttributeName
- atIndex:searchLocation
- longestEffectiveRange:&manualLinkRange
- inRange:inputRange];
+ [self.host.textView.textStorage attribute:ManualLinkAttributeName
+ atIndex:searchLocation
+ longestEffectiveRange:&manualLinkRange
+ inRange:inputRange];
LinkData *automaticData =
- [self.input->textView.textStorage attribute:AutomaticLinkAttributeName
- atIndex:searchLocation
- longestEffectiveRange:&automaticLinkRange
- inRange:inputRange];
+ [self.host.textView.textStorage attribute:AutomaticLinkAttributeName
+ atIndex:searchLocation
+ longestEffectiveRange:&automaticLinkRange
+ inRange:inputRange];
return manualData == nullptr
? automaticData == nullptr ? NSMakeRange(0, 0) : automaticLinkRange
@@ -281,7 +281,7 @@ - (NSRange)getFullLinkRangeAt:(NSUInteger)location {
// handles detecting and removing automatic links
- (void)handleAutomaticLinks:(NSString *)word inRange:(NSRange)wordRange {
- LinkRegexConfig *linkRegexConfig = [self.input->config linkRegexConfig];
+ LinkRegexConfig *linkRegexConfig = [self.host.config linkRegexConfig];
// no automatic links with isDisabled
if (linkRegexConfig.isDisabled) {
@@ -289,11 +289,11 @@ - (void)handleAutomaticLinks:(NSString *)word inRange:(NSRange)wordRange {
}
InlineCodeStyle *inlineCodeStyle =
- [self.input->stylesDict objectForKey:@([InlineCodeStyle getType])];
+ [self.host.stylesDict objectForKey:@([InlineCodeStyle getType])];
MentionStyle *mentionStyle =
- [self.input->stylesDict objectForKey:@([MentionStyle getType])];
+ [self.host.stylesDict objectForKey:@([MentionStyle getType])];
CodeBlockStyle *codeBlockStyle =
- [self.input->stylesDict objectForKey:@([CodeBlockStyle getType])];
+ [self.host.stylesDict objectForKey:@([CodeBlockStyle getType])];
// we don't recognize links along mentions, inline code or codeblocks
if (mentionStyle != nullptr && [mentionStyle any:wordRange]) {
@@ -311,7 +311,7 @@ - (void)handleAutomaticLinks:(NSString *)word inRange:(NSRange)wordRange {
// we don't recognize automatic links along manual ones
__block BOOL manualLinkPresent = NO;
- [self.input->textView.textStorage
+ [self.host.textView.textStorage
enumerateAttribute:ManualLinkAttributeName
inRange:wordRange
options:0
@@ -336,7 +336,7 @@ - (void)handleAutomaticLinks:(NSString *)word inRange:(NSRange)wordRange {
matchRange:matchingRange];
} else {
// use user defined regex if it exists
- NSRegularExpression *userRegex = [self.input->config parsedLinkRegex];
+ NSRegularExpression *userRegex = [self.host.config parsedLinkRegex];
if (userRegex == nullptr) {
// fallback to default regex
@@ -368,7 +368,7 @@ - (void)handleAutomaticLinks:(NSString *)word inRange:(NSRange)wordRange {
[self addLink:newData range:wordRange withSelection:NO];
// emit onLinkDetected if style was added
- [self.input emitOnLinkDetectedEvent:newData range:wordRange];
+ [(id)self.host emitOnLinkDetectedEvent:newData range:wordRange];
}
} else if ([self any:wordRange]) {
// there was some automatic link (because anyOccurence is true and we are
@@ -403,7 +403,7 @@ - (void)handleManualLinks:(NSString *)word inRange:(NSRange)wordRange {
__block NSInteger manualLinkMinIdx = -1;
__block NSInteger manualLinkMaxIdx = -1;
- [self.input->textView.textStorage
+ [self.host.textView.textStorage
enumerateAttribute:ManualLinkAttributeName
inRange:wordRange
options:0
@@ -437,7 +437,7 @@ - (void)handleManualLinks:(NSString *)word inRange:(NSRange)wordRange {
NSRange newRange =
NSMakeRange(manualLinkMinIdx, manualLinkMaxIdx - manualLinkMinIdx + 1);
[self applyLinkMetaWithData:manualLinkMinValue range:newRange];
- [self.input->attributesManager addDirtyRange:newRange];
+ [self.host.attributesManager addDirtyRange:newRange];
}
}
@@ -452,14 +452,14 @@ - (BOOL)isSingleLinkIn:(NSRange)range {
- (void)removeConnectedLinksIfNeeded:(NSString *)word range:(NSRange)wordRange {
BOOL anyAutomatic =
[OccurenceUtils any:AutomaticLinkAttributeName
- withInput:self.input
+ withHost:self.host
inRange:wordRange
withCondition:^BOOL(id _Nullable value, NSRange subrange) {
return [self styleCondition:value range:subrange];
}];
BOOL anyManual =
[OccurenceUtils any:ManualLinkAttributeName
- withInput:self.input
+ withHost:self.host
inRange:wordRange
withCondition:^BOOL(id _Nullable value, NSRange subrange) {
return [self styleCondition:value range:subrange];
@@ -474,7 +474,7 @@ - (void)removeConnectedLinksIfNeeded:(NSString *)word range:(NSRange)wordRange {
// covers the whole word
BOOL onlyLinks = [OccurenceUtils
detectMultiple:@[ ManualLinkAttributeName, AutomaticLinkAttributeName ]
- withInput:self.input
+ withHost:self.host
inRange:wordRange
withCondition:^BOOL(id _Nullable value, NSRange r) {
return [self styleCondition:value range:r];
diff --git a/ios/styles/MentionStyle.mm b/ios/styles/MentionStyle.mm
index bd34971e..1201b472 100644
--- a/ios/styles/MentionStyle.mm
+++ b/ios/styles/MentionStyle.mm
@@ -27,8 +27,8 @@ - (BOOL)isParagraph {
return NO;
}
-- (instancetype)initWithInput:(id)input {
- self = [super initWithInput:(EnrichedTextInputView *)input];
+- (instancetype)initWithHost:(id)host {
+ self = [super initWithHost:host];
if (self) {
_activeMentionRange = nullptr;
_activeMentionIndicator = nullptr;
@@ -47,7 +47,7 @@ - (void)applyStyling:(NSRange)range {
}
MentionStyleProps *styleProps =
- [self.input->config mentionStylePropsForIndicator:params.indicator];
+ [self.host.config mentionStylePropsForIndicator:params.indicator];
NSMutableDictionary *newAttrs = [@{
NSForegroundColorAttributeName : styleProps.color,
@@ -61,7 +61,7 @@ - (void)applyStyling:(NSRange)range {
newAttrs[NSUnderlineStyleAttributeName] = @(NSUnderlineStyleSingle);
}
- [self.input->textView.textStorage addAttributes:newAttrs range:range];
+ [self.host.textView.textStorage addAttributes:newAttrs range:range];
}
- (void)reapplyFromStylePair:(StylePair *)pair {
@@ -86,20 +86,20 @@ - (void)toggle:(NSRange)range {
// other styles
- (void)remove:(NSRange)range withDirtyRange:(BOOL)withDirtyRange {
NSArray *mentions = [self all:range];
- [self.input->textView.textStorage beginEditing];
+ [self.host.textView.textStorage beginEditing];
for (StylePair *pair in mentions) {
NSRange mentionRange =
[self getFullMentionRangeAt:[pair.rangeValue rangeValue].location];
if (mentionRange.length == 0) {
continue;
}
- [self.input->textView.textStorage removeAttribute:MentionAttributeName
- range:mentionRange];
+ [self.host.textView.textStorage removeAttribute:MentionAttributeName
+ range:mentionRange];
if (withDirtyRange) {
- [self.input->attributesManager addDirtyRange:mentionRange];
+ [self.host.attributesManager addDirtyRange:mentionRange];
}
}
- [self.input->textView.textStorage endEditing];
+ [self.host.textView.textStorage endEditing];
[super removeTyping];
}
@@ -107,13 +107,13 @@ - (void)remove:(NSRange)range withDirtyRange:(BOOL)withDirtyRange {
// used for conflicts, we have to remove the whole mention
- (void)removeTyping {
NSRange mentionRange =
- [self getFullMentionRangeAt:self.input->textView.selectedRange.location];
+ [self getFullMentionRangeAt:self.host.textView.selectedRange.location];
if (mentionRange.length > 0) {
- [self.input->textView.textStorage beginEditing];
- [self.input->textView.textStorage removeAttribute:MentionAttributeName
- range:mentionRange];
- [self.input->textView.textStorage endEditing];
- [self.input->attributesManager addDirtyRange:mentionRange];
+ [self.host.textView.textStorage beginEditing];
+ [self.host.textView.textStorage removeAttribute:MentionAttributeName
+ range:mentionRange];
+ [self.host.textView.textStorage endEditing];
+ [self.host.attributesManager addDirtyRange:mentionRange];
}
[super removeTyping];
}
@@ -131,9 +131,9 @@ - (BOOL)detect:(NSRange)range {
}
- (void)applyMentionMeta:(MentionParams *)params range:(NSRange)range {
- [self.input->textView.textStorage addAttribute:MentionAttributeName
- value:params
- range:range];
+ [self.host.textView.textStorage addAttribute:MentionAttributeName
+ value:params
+ range:range];
}
// MARK: - Public non-standard methods
@@ -158,13 +158,13 @@ - (void)addMention:(NSString *)indicator
[TextInsertionUtils replaceText:newText
at:rangeToBeReplaced
additionalAttributes:nullptr
- input:self.input
+ input:self.host
withSelection:YES];
// THEN, add the attributes to not apply them on the space
NSRange mentionRange = NSMakeRange(rangeToBeReplaced.location, text.length);
[self applyMentionMeta:params range:mentionRange];
- [self.input->attributesManager addDirtyRange:mentionRange];
+ [self.host.attributesManager addDirtyRange:mentionRange];
// mention editing should finish
[self removeActiveMentionRange];
@@ -175,19 +175,19 @@ - (void)addMentionAtRange:(NSRange)range params:(MentionParams *)params {
_blockMentionEditing = YES;
[self applyMentionMeta:params range:range];
- [self.input->attributesManager addDirtyRange:range];
+ [self.host.attributesManager addDirtyRange:range];
_blockMentionEditing = NO;
}
- (void)startMentionWithIndicator:(NSString *)indicator {
- NSRange currentRange = self.input->textView.selectedRange;
+ NSRange currentRange = self.host.textView.selectedRange;
BOOL addSpaceBefore = NO;
BOOL addSpaceAfter = NO;
if (currentRange.location > 0) {
- unichar charBefore = [self.input->textView.textStorage.string
+ unichar charBefore = [self.host.textView.textStorage.string
characterAtIndex:(currentRange.location - 1)];
if (![[NSCharacterSet whitespaceAndNewlineCharacterSet]
characterIsMember:charBefore]) {
@@ -196,8 +196,8 @@ - (void)startMentionWithIndicator:(NSString *)indicator {
}
if (currentRange.location + currentRange.length <
- self.input->textView.textStorage.string.length) {
- unichar charAfter = [self.input->textView.textStorage.string
+ self.host.textView.textStorage.string.length) {
+ unichar charAfter = [self.host.textView.textStorage.string
characterAtIndex:(currentRange.location + currentRange.length)];
if (![[NSCharacterSet whitespaceAndNewlineCharacterSet]
characterIsMember:charAfter]) {
@@ -216,18 +216,18 @@ - (void)startMentionWithIndicator:(NSString *)indicator {
[TextInsertionUtils insertText:finalString
at:currentRange.location
additionalAttributes:nullptr
- input:self.input
+ input:self.host
withSelection:NO];
} else {
[TextInsertionUtils replaceText:finalString
at:currentRange
additionalAttributes:nullptr
- input:self.input
+ input:self.host
withSelection:NO];
}
- [self.input->textView reactFocus];
- self.input->textView.selectedRange = newSelect;
+ [self.host.textView reactFocus];
+ self.host.textView.selectedRange = newSelect;
}
// handles removing no longer valid mentions
@@ -237,7 +237,7 @@ - (void)handleExistingMentions {
// any number of spaces, which makes one mention any number of words long
NSRange wholeText =
- NSMakeRange(0, self.input->textView.textStorage.string.length);
+ NSMakeRange(0, self.host.textView.textStorage.string.length);
// get mentions in ascending range.location order
NSArray *mentions = [[self all:wholeText]
sortedArrayUsingComparator:^NSComparisonResult(id _Nonnull obj1,
@@ -271,8 +271,8 @@ - (void)handleExistingMentions {
}
// check for text, any modifications to it makes mention invalid
- NSString *existingText = [self.input->textView.textStorage.string
- substringWithRange:currentRange];
+ NSString *existingText =
+ [self.host.textView.textStorage.string substringWithRange:currentRange];
if (![existingText isEqualToString:currentText]) {
[rangesToRemove addObject:[NSValue valueWithRange:currentRange]];
}
@@ -291,7 +291,7 @@ - (void)manageMentionEditing {
}
// we don't take longer selections into consideration
- if (self.input->textView.selectedRange.length > 0) {
+ if (self.host.textView.selectedRange.length > 0) {
[self removeActiveMentionRange];
return;
}
@@ -307,14 +307,14 @@ - (void)manageMentionEditing {
// get style classes that the mention shouldn't be recognized in, together
// with other mentions
- NSArray *conflicts = self.input->conflictingStyles[@([MentionStyle getType])];
- NSArray *blocks = self.input->blockingStyles[@([MentionStyle getType])];
+ NSArray *conflicts = self.host.conflictingStyles[@([MentionStyle getType])];
+ NSArray *blocks = self.host.blockingStyles[@([MentionStyle getType])];
NSArray *allConflicts = [[conflicts arrayByAddingObjectsFromArray:blocks]
arrayByAddingObject:@([MentionStyle getType])];
BOOL conflictingStyle = NO;
for (NSNumber *styleType in allConflicts) {
- StyleBase *styleInst = self.input->stylesDict[styleType];
+ StyleBase *styleInst = self.host.stylesDict[styleType];
if (styleInst != nullptr && [styleInst any:candidateRange]) {
conflictingStyle = YES;
break;
@@ -334,19 +334,19 @@ - (void)manageMentionEditing {
// returns mention params if it exists
- (MentionParams *)getMentionParamsAt:(NSUInteger)location {
NSRange mentionRange = NSMakeRange(0, 0);
- NSRange inputRange = NSMakeRange(0, self.input->textView.textStorage.length);
+ NSRange inputRange = NSMakeRange(0, self.host.textView.textStorage.length);
// don't search at the very end of input
NSUInteger searchLocation = location;
- if (searchLocation == self.input->textView.textStorage.length) {
+ if (searchLocation == self.host.textView.textStorage.length) {
return nullptr;
}
MentionParams *value =
- [self.input->textView.textStorage attribute:MentionAttributeName
- atIndex:searchLocation
- longestEffectiveRange:&mentionRange
- inRange:inputRange];
+ [self.host.textView.textStorage attribute:MentionAttributeName
+ atIndex:searchLocation
+ longestEffectiveRange:&mentionRange
+ inRange:inputRange];
return value;
}
@@ -357,26 +357,26 @@ - (NSValue *)getActiveMentionRange {
// returns full range of a mention at some location
- (NSRange)getFullMentionRangeAt:(NSUInteger)location {
NSRange mentionRange = NSMakeRange(0, 0);
- NSRange inputRange = NSMakeRange(0, self.input->textView.textStorage.length);
+ NSRange inputRange = NSMakeRange(0, self.host.textView.textStorage.length);
// get the previous index if possible when at the very end of input
NSUInteger searchLocation = location;
- if (searchLocation == self.input->textView.textStorage.length) {
+ if (searchLocation == self.host.textView.textStorage.length) {
if (searchLocation == 0) {
return mentionRange;
}
searchLocation = searchLocation - 1;
}
- [self.input->textView.textStorage attribute:MentionAttributeName
- atIndex:searchLocation
- longestEffectiveRange:&mentionRange
- inRange:inputRange];
+ [self.host.textView.textStorage attribute:MentionAttributeName
+ atIndex:searchLocation
+ longestEffectiveRange:&mentionRange
+ inRange:inputRange];
return mentionRange;
}
- (MentionStyleProps *)stylePropsWithParams:(MentionParams *)params {
- return [self.input->config mentionStylePropsForIndicator:params.indicator];
+ return [self.host.config mentionStylePropsForIndicator:params.indicator];
}
// finds if any word/words around current selection are eligible to be edited as
@@ -389,9 +389,8 @@ - (NSArray *)getMentionCandidate {
NSRange finalRange;
// word at the current selection
- currentWord =
- [WordsUtils getCurrentWord:self.input->textView.textStorage.string
- range:self.input->textView.selectedRange];
+ currentWord = [WordsUtils getCurrentWord:self.host.textView.textStorage.string
+ range:self.host.textView.selectedRange];
if (currentWord != nullptr) {
currentWordText = (NSString *)[currentWord objectForKey:@"word"];
currentWordRange = (NSValue *)[currentWord objectForKey:@"range"];
@@ -401,7 +400,7 @@ - (NSArray *)getMentionCandidate {
// current word exists
unichar currentFirstChar = [currentWordText characterAtIndex:0];
- if ([[self.input->config mentionIndicators]
+ if ([[self.host.config mentionIndicators]
containsObject:@(currentFirstChar)]) {
// current word exists and has a mention indicator; no need to check for
// the previous word
@@ -418,7 +417,7 @@ - (NSArray *)getMentionCandidate {
return nullptr;
}
- unichar separatorChar = [self.input->textView.textStorage.string
+ unichar separatorChar = [self.host.textView.textStorage.string
characterAtIndex:previousWordSearchLocation];
if (![[NSCharacterSet whitespaceCharacterSet]
characterIsMember:separatorChar]) {
@@ -428,7 +427,7 @@ - (NSArray *)getMentionCandidate {
}
previousWord = [WordsUtils
- getCurrentWord:self.input->textView.textStorage.string
+ getCurrentWord:self.host.textView.textStorage.string
range:NSMakeRange(previousWordSearchLocation, 0)];
if (previousWord != nullptr) {
@@ -439,7 +438,7 @@ - (NSArray *)getMentionCandidate {
// check for the mention indicators in the previous word
unichar previousFirstChar = [previousWordText characterAtIndex:0];
- if ([[self.input->config mentionIndicators]
+ if ([[self.host.config mentionIndicators]
containsObject:@(previousFirstChar)]) {
// previous word has a proper mention indicator: treat both words as
// an editable mention
@@ -464,13 +463,13 @@ - (NSArray *)getMentionCandidate {
// current word doesn't exist; try getting the previous one
NSInteger previousWordSearchLocation =
- self.input->textView.selectedRange.location - 1;
+ self.host.textView.selectedRange.location - 1;
if (previousWordSearchLocation < 0) {
// previous word can't exist
return nullptr;
}
- unichar separatorChar = [self.input->textView.textStorage.string
+ unichar separatorChar = [self.host.textView.textStorage.string
characterAtIndex:previousWordSearchLocation];
if (![[NSCharacterSet whitespaceCharacterSet]
characterIsMember:separatorChar]) {
@@ -480,7 +479,7 @@ - (NSArray *)getMentionCandidate {
}
previousWord =
- [WordsUtils getCurrentWord:self.input->textView.textStorage.string
+ [WordsUtils getCurrentWord:self.host.textView.textStorage.string
range:NSMakeRange(previousWordSearchLocation, 0)];
if (previousWord != nullptr) {
@@ -491,7 +490,7 @@ - (NSArray *)getMentionCandidate {
// check for the mention indicators in the previous word
unichar previousFirstChar = [previousWordText characterAtIndex:0];
- if ([[self.input->config mentionIndicators]
+ if ([[self.host.config mentionIndicators]
containsObject:@(previousFirstChar)]) {
// previous word has a proper mention indicator; treat previous word + a
// space as a editable mention
@@ -521,7 +520,7 @@ - (void)setActiveMentionRange:(NSRange)range text:(NSString *)text {
[text substringWithRange:NSMakeRange(1, text.length - 1)];
_activeMentionIndicator = indicatorString;
_activeMentionRange = [NSValue valueWithRange:range];
- [self.input emitOnMentionEvent:indicatorString text:textString];
+ [self.host emitOnMentionEvent:indicatorString text:textString];
}
// removes stored mention range + indicator, which means that we no longer edit
@@ -531,7 +530,7 @@ - (void)removeActiveMentionRange {
NSString *indicatorCopy = [_activeMentionIndicator copy];
_activeMentionIndicator = nullptr;
_activeMentionRange = nullptr;
- [self.input emitOnMentionEvent:indicatorCopy text:nullptr];
+ [self.host emitOnMentionEvent:indicatorCopy text:nullptr];
}
}
diff --git a/ios/styles/OrderedListStyle.mm b/ios/styles/OrderedListStyle.mm
index 60a6089a..291d1a98 100644
--- a/ios/styles/OrderedListStyle.mm
+++ b/ios/styles/OrderedListStyle.mm
@@ -24,10 +24,10 @@ - (BOOL)needsZWS {
- (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];
+ CGFloat listHeadIndent = [self.host.config orderedListMarginLeft] +
+ [self.host.config orderedListGapWidth];
- [self.input->textView.textStorage
+ [self.host.textView.textStorage
enumerateAttribute:NSParagraphStyleAttributeName
inRange:range
options:0
@@ -37,7 +37,7 @@ - (void)applyStyling:(NSRange)range {
[(NSParagraphStyle *)value mutableCopy];
pStyle.headIndent = listHeadIndent;
pStyle.firstLineHeadIndent = listHeadIndent;
- [self.input->textView.textStorage
+ [self.host.textView.textStorage
addAttribute:NSParagraphStyleAttributeName
value:pStyle
range:range];
@@ -47,28 +47,28 @@ - (void)applyStyling:(NSRange)range {
- (BOOL)tryHandlingListShorcutInRange:(NSRange)range
replacementText:(NSString *)text {
NSRange paragraphRange =
- [self.input->textView.textStorage.string paragraphRangeForRange:range];
+ [self.host.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
+ unichar charBefore = [self.host.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]) {
+ if ([self.host handleStyleBlocksAndConflicts:[[self class] getType]
+ range:paragraphRange]) {
// don't emit during the replacing
- self.input->blockEmitting = YES;
+ self.host.blockEmitting = YES;
// remove the number
[TextInsertionUtils replaceText:@""
at:NSMakeRange(paragraphRange.location, 1)
additionalAttributes:nullptr
- input:self.input
+ input:self.host
withSelection:YES];
- self.input->blockEmitting = NO;
+ self.host.blockEmitting = NO;
// add attributes on the paragraph
[self add:NSMakeRange(paragraphRange.location,
diff --git a/ios/styles/StrikethroughStyle.mm b/ios/styles/StrikethroughStyle.mm
index ad829df3..5b588338 100644
--- a/ios/styles/StrikethroughStyle.mm
+++ b/ios/styles/StrikethroughStyle.mm
@@ -16,10 +16,10 @@ - (BOOL)isParagraph {
}
- (void)applyStyling:(NSRange)range {
- [self.input->textView.textStorage addAttributes:@{
+ [self.host.textView.textStorage addAttributes:@{
NSStrikethroughStyleAttributeName : @(NSUnderlineStyleSingle)
}
- range:range];
+ range:range];
}
@end
diff --git a/ios/styles/UnderlineStyle.mm b/ios/styles/UnderlineStyle.mm
index 2d30eec5..c09a0fae 100644
--- a/ios/styles/UnderlineStyle.mm
+++ b/ios/styles/UnderlineStyle.mm
@@ -16,9 +16,9 @@ - (BOOL)isParagraph {
}
- (void)applyStyling:(NSRange)range {
- [self.input->textView.textStorage addAttribute:NSUnderlineStyleAttributeName
- value:@(NSUnderlineStyleSingle)
- range:range];
+ [self.host.textView.textStorage addAttribute:NSUnderlineStyleAttributeName
+ value:@(NSUnderlineStyleSingle)
+ range:range];
}
@end
diff --git a/ios/styles/UnorderedListStyle.mm b/ios/styles/UnorderedListStyle.mm
index 2e0a1a10..8044ab3d 100644
--- a/ios/styles/UnorderedListStyle.mm
+++ b/ios/styles/UnorderedListStyle.mm
@@ -24,10 +24,10 @@ - (BOOL)needsZWS {
- (void)applyStyling:(NSRange)range {
// lists are drawn manually
// margin before bullet + gap between bullet and paragraph
- CGFloat listHeadIndent = [self.input->config unorderedListMarginLeft] +
- [self.input->config unorderedListGapWidth];
+ CGFloat listHeadIndent = [self.host.config unorderedListMarginLeft] +
+ [self.host.config unorderedListGapWidth];
- [self.input->textView.textStorage
+ [self.host.textView.textStorage
enumerateAttribute:NSParagraphStyleAttributeName
inRange:range
options:0
@@ -37,7 +37,7 @@ - (void)applyStyling:(NSRange)range {
[(NSParagraphStyle *)value mutableCopy];
pStyle.headIndent = listHeadIndent;
pStyle.firstLineHeadIndent = listHeadIndent;
- [self.input->textView.textStorage
+ [self.host.textView.textStorage
addAttribute:NSParagraphStyleAttributeName
value:pStyle
range:range];
@@ -47,28 +47,28 @@ - (void)applyStyling:(NSRange)range {
- (BOOL)tryHandlingListShorcutInRange:(NSRange)range
replacementText:(NSString *)text {
NSRange paragraphRange =
- [self.input->textView.textStorage.string paragraphRangeForRange:range];
+ [self.host.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
+ unichar charBefore = [self.host.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]) {
+ if ([self.host handleStyleBlocksAndConflicts:[[self class] getType]
+ range:paragraphRange]) {
// don't emit during the replacing
- self.input->blockEmitting = YES;
+ self.host.blockEmitting = YES;
// remove the dash
[TextInsertionUtils replaceText:@""
at:NSMakeRange(paragraphRange.location, 1)
additionalAttributes:nullptr
- input:self.input
+ input:self.host
withSelection:YES];
- self.input->blockEmitting = NO;
+ self.host.blockEmitting = NO;
// add attributes on the dashless paragraph
[self add:NSMakeRange(paragraphRange.location,
diff --git a/ios/utils/AttachmentLayoutUtils.h b/ios/utils/AttachmentLayoutUtils.h
new file mode 100644
index 00000000..78a37dad
--- /dev/null
+++ b/ios/utils/AttachmentLayoutUtils.h
@@ -0,0 +1,24 @@
+#pragma once
+#import "EnrichedConfig.h"
+#import "ImageAttachment.h"
+#import
+
+@interface AttachmentLayoutUtils : NSObject
+
++ (void)handleAttachmentUpdate:(MediaAttachment *)attachment
+ textView:(UITextView *)textView
+ onLayoutBlock:(dispatch_block_t)layoutBlock;
+
++ (NSMutableDictionary *)
+ layoutAttachmentsInTextView:(UITextView *)textView
+ config:(EnrichedConfig *)config
+ existingViews:
+ (NSMutableDictionary *)
+ attachmentViews;
+
++ (CGRect)frameForAttachment:(ImageAttachment *)attachment
+ atRange:(NSRange)range
+ textView:(UITextView *)textView
+ config:(EnrichedConfig *)config;
+
+@end
diff --git a/ios/utils/AttachmentLayoutUtils.mm b/ios/utils/AttachmentLayoutUtils.mm
new file mode 100644
index 00000000..2cd39052
--- /dev/null
+++ b/ios/utils/AttachmentLayoutUtils.mm
@@ -0,0 +1,143 @@
+#import "AttachmentLayoutUtils.h"
+
+@implementation AttachmentLayoutUtils
+
++ (void)handleAttachmentUpdate:(MediaAttachment *)attachment
+ textView:(UITextView *)textView
+ onLayoutBlock:(dispatch_block_t)layoutBlock {
+ 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];
+
+ dispatch_async(dispatch_get_main_queue(), layoutBlock);
+}
+
++ (NSMutableDictionary *)
+ layoutAttachmentsInTextView:(UITextView *)textView
+ config:(EnrichedConfig *)config
+ existingViews:
+ (NSMutableDictionary *)
+ attachmentViews {
+ NSTextStorage *storage = textView.textStorage;
+ if (storage.length == 0)
+ return attachmentViews;
+
+ NSMutableDictionary *activeAttachmentViews =
+ [NSMutableDictionary dictionary];
+
+ // Iterate over the entire text to find ImageAttachments
+ [storage enumerateAttribute:NSAttachmentAttributeName
+ inRange:NSMakeRange(0, storage.length)
+ options:0
+ usingBlock:^(id value, NSRange range, BOOL *stop) {
+ if ([value isKindOfClass:[ImageAttachment class]]) {
+ ImageAttachment *attachment = (ImageAttachment *)value;
+
+ CGRect rect = [self frameForAttachment:attachment
+ atRange:range
+ textView:textView
+ config:config];
+
+ // Get or Create the UIImageView for this specific
+ // attachment key
+ NSValue *key =
+ [NSValue valueWithNonretainedObject:attachment];
+ UIImageView *imgView = attachmentViews[key];
+
+ if (!imgView) {
+ // It doesn't exist yet, create it
+ imgView = [[UIImageView alloc] initWithFrame:rect];
+ imgView.contentMode = UIViewContentModeScaleAspectFit;
+ imgView.tintColor = [UIColor labelColor];
+
+ // Add it directly to the TextView
+ [textView addSubview:imgView];
+ }
+
+ // Update position (in case text moved/scrolled)
+ if (!CGRectEqualToRect(imgView.frame, rect)) {
+ imgView.frame = rect;
+ }
+ UIImage *targetImage =
+ attachment.storedAnimatedImage ?: attachment.image;
+
+ // Only set if different to avoid resetting the animation
+ // loop
+ if (imgView.image != targetImage) {
+ imgView.image = targetImage;
+ }
+
+ // Ensure it is visible on top
+ imgView.hidden = NO;
+ [textView bringSubviewToFront:imgView];
+
+ activeAttachmentViews[key] = imgView;
+ // Remove from the old map so we know it has been claimed
+ [attachmentViews removeObjectForKey:key];
+ }
+ }];
+
+ // Everything remaining in attachmentViews is dead or off-screen
+ for (UIImageView *danglingView in attachmentViews.allValues) {
+ [danglingView removeFromSuperview];
+ }
+
+ return activeAttachmentViews;
+}
+
++ (CGRect)frameForAttachment:(ImageAttachment *)attachment
+ atRange:(NSRange)range
+ textView:(UITextView *)textView
+ config:(EnrichedConfig *)config {
+ NSLayoutManager *layoutManager = textView.layoutManager;
+ NSTextContainer *textContainer = textView.textContainer;
+ NSTextStorage *storage = textView.textStorage;
+
+ NSRange glyphRange = [layoutManager glyphRangeForCharacterRange:range
+ actualCharacterRange:NULL];
+ CGRect glyphRect = [layoutManager boundingRectForGlyphRange:glyphRange
+ inTextContainer:textContainer];
+
+ CGRect lineRect =
+ [layoutManager lineFragmentRectForGlyphAtIndex:glyphRange.location
+ effectiveRange:NULL];
+ CGSize attachmentSize = attachment.bounds.size;
+
+ UIFont *font = [storage attribute:NSFontAttributeName
+ atIndex:range.location
+ effectiveRange:NULL];
+ if (!font) {
+ font = [config primaryFont];
+ }
+
+ // Calculate (Baseline Alignment)
+ CGFloat targetY =
+ CGRectGetMaxY(lineRect) + font.descender - attachmentSize.height;
+ CGRect rect =
+ CGRectMake(glyphRect.origin.x + textView.textContainerInset.left,
+ targetY + textView.textContainerInset.top,
+ attachmentSize.width, attachmentSize.height);
+
+ return CGRectIntegral(rect);
+}
+
+@end
diff --git a/ios/utils/CheckboxHitTestUtils.mm b/ios/utils/CheckboxHitTestUtils.mm
index 851d48b3..4055b8b2 100644
--- a/ios/utils/CheckboxHitTestUtils.mm
+++ b/ios/utils/CheckboxHitTestUtils.mm
@@ -1,6 +1,6 @@
#import "CheckboxHitTestUtils.h"
+#import "EnrichedConfig.h"
#import "EnrichedTextInputView.h"
-#import "InputConfig.h"
#import "StyleHeaders.h"
static const CGFloat kCheckboxHitSlopLeft = 8.0;
@@ -56,7 +56,7 @@ + (CGRect)checkboxRectForGlyphIndex:(NSUInteger)glyphIndex
inInput:(EnrichedTextInputView *)input {
UITextView *textView = input->textView;
NSLayoutManager *layoutManager = textView.layoutManager;
- InputConfig *config = input->config;
+ EnrichedConfig *config = input->config;
if (!config) {
return CGRectNull;
diff --git a/ios/utils/EnrichedTextTouchHandler.h b/ios/utils/EnrichedTextTouchHandler.h
new file mode 100644
index 00000000..8f59bf2f
--- /dev/null
+++ b/ios/utils/EnrichedTextTouchHandler.h
@@ -0,0 +1,14 @@
+#import
+
+@class EnrichedTextView;
+
+@interface EnrichedTextTouchHandler : NSObject
+
+@property(nonatomic, weak) EnrichedTextView *view;
+
+- (instancetype)initWithView:(EnrichedTextView *)view;
+- (void)handleTouchBeganAtPoint:(CGPoint)point;
+- (void)handleTouchEndedAtPoint:(CGPoint)point;
+- (void)handleTouchCancelled;
+
+@end
diff --git a/ios/utils/EnrichedTextTouchHandler.mm b/ios/utils/EnrichedTextTouchHandler.mm
new file mode 100644
index 00000000..15be60d2
--- /dev/null
+++ b/ios/utils/EnrichedTextTouchHandler.mm
@@ -0,0 +1,150 @@
+#import "EnrichedTextTouchHandler.h"
+#import "EnrichedTextView.h"
+#import "LinkData.h"
+#import "MentionParams.h"
+#import "MentionStyleProps.h"
+#import "StyleBase.h"
+
+@implementation EnrichedTextTouchHandler {
+ NSRange _activeRange;
+ NSString *_activeAttrKey;
+ id _activeValue;
+}
+
+- (instancetype)initWithView:(EnrichedTextView *)view {
+ if (self = [super init]) {
+ _view = view;
+ }
+ return self;
+}
+
+- (void)handleTouchBeganAtPoint:(CGPoint)point {
+ NSUInteger charIndex = [self characterIndexAtPoint:point];
+ NSRange range;
+ NSString *key;
+ id value = [self findClickableAt:charIndex range:&range key:&key];
+ if (value) {
+ _activeRange = range;
+ _activeAttrKey = key;
+ _activeValue = value;
+ [self updateVisualsPressed:YES];
+ }
+}
+
+- (void)handleTouchEndedAtPoint:(CGPoint)point {
+ if (!_activeValue) {
+ return;
+ }
+
+ NSUInteger charIndex = [self characterIndexAtPoint:point];
+ if (NSLocationInRange(charIndex, _activeRange)) {
+ [self dispatchEvent];
+ }
+
+ [self updateVisualsPressed:NO];
+ [self reset];
+}
+
+- (void)handleTouchCancelled {
+ [self updateVisualsPressed:NO];
+ [self reset];
+}
+
+- (NSUInteger)characterIndexAtPoint:(CGPoint)point {
+ UITextView *tv = self.view.textView;
+ NSLayoutManager *lm = tv.layoutManager;
+ NSTextContainer *tc = tv.textContainer;
+
+ CGPoint textOffset =
+ CGPointMake(tv.textContainerInset.left, tv.textContainerInset.top);
+ CGPoint locationInContainer =
+ CGPointMake(point.x - textOffset.x, point.y - textOffset.y);
+
+ NSUInteger glyphIndex = [lm glyphIndexForPoint:locationInContainer
+ inTextContainer:tc];
+ CGRect glyphRect = [lm boundingRectForGlyphRange:NSMakeRange(glyphIndex, 1)
+ inTextContainer:tc];
+
+ if (!CGRectContainsPoint(glyphRect, locationInContainer)) {
+ return NSNotFound;
+ }
+ return [lm characterIndexForGlyphAtIndex:glyphIndex];
+}
+
+- (id)findClickableAt:(NSUInteger)idx
+ range:(NSRangePointer)range
+ key:(NSString **)key {
+ if (idx == NSNotFound || idx >= self.view.textView.textStorage.length)
+ return nil;
+
+ NSArray *keys =
+ @[ @"EnrichedManualLink", @"EnrichedAutomaticLink", @"EnrichedMention" ];
+ for (NSString *k in keys) {
+ id val = [self.view.textView.textStorage attribute:k
+ atIndex:idx
+ effectiveRange:range];
+ if (val) {
+ *key = k;
+ return val;
+ }
+ }
+ return nil;
+}
+
+- (void)updateVisualsPressed:(BOOL)pressed {
+ if (pressed) {
+ UIColor *color = nil;
+ UIColor *bgColor = nil;
+
+ if ([_activeAttrKey isEqualToString:@"EnrichedMention"]) {
+ MentionParams *m = (MentionParams *)_activeValue;
+ MentionStyleProps *mProps =
+ [self.view.config mentionStylePropsForIndicator:m.indicator];
+ color = [mProps pressColor];
+ bgColor = [mProps pressBackgroundColor];
+ } else {
+ color = [self.view.config linkPressColor];
+ }
+
+ NSMutableDictionary *newAttrs = [[NSMutableDictionary alloc] init];
+ if (color) {
+ newAttrs[NSForegroundColorAttributeName] = color;
+ newAttrs[NSUnderlineColorAttributeName] = color;
+ newAttrs[NSStrikethroughColorAttributeName] = color;
+ }
+ if (bgColor) {
+ newAttrs[NSBackgroundColorAttributeName] = bgColor;
+ }
+
+ [self.view.textView.textStorage addAttributes:newAttrs range:_activeRange];
+ } else {
+ // REVERT using the Style Engine in the View
+ for (StyleBase *style in self.view.stylesDict.allValues) {
+ NSString *styleKey = [style getKey];
+ // If the style key matches exactly (Mentions)
+ // OR if both the style and the active key are Link-related
+ BOOL isMatch = [styleKey isEqualToString:_activeAttrKey];
+ BOOL isLinkMatch = ([_activeAttrKey containsString:@"Link"] &&
+ [styleKey containsString:@"Link"]);
+
+ if (isMatch || isLinkMatch) {
+ [style applyStyling:_activeRange];
+ }
+ }
+ }
+}
+
+- (void)dispatchEvent {
+ if ([_activeAttrKey containsString:@"Link"]) {
+ [self.view emitOnLinkPressEvent:((LinkData *)_activeValue).url];
+ } else if ([_activeAttrKey isEqualToString:@"EnrichedMention"]) {
+ [self.view emitOnMentionPressEvent:(MentionParams *)_activeValue];
+ }
+}
+
+- (void)reset {
+ _activeValue = nil;
+ _activeAttrKey = nil;
+ _activeRange = NSMakeRange(0, 0);
+}
+@end
diff --git a/ios/utils/EnrichedTouchableTextView.h b/ios/utils/EnrichedTouchableTextView.h
new file mode 100644
index 00000000..6b98def8
--- /dev/null
+++ b/ios/utils/EnrichedTouchableTextView.h
@@ -0,0 +1,14 @@
+#pragma once
+
+#import
+
+@class EnrichedTextTouchHandler;
+
+// Forwards single-finger touches to `EnrichedTextTouchHandler` before `super`
+// so link/mention pressed styling is not delayed by `UITextView` gesture
+// arbitration.
+@interface EnrichedTouchableTextView : UITextView
+
+@property(nonatomic, weak) EnrichedTextTouchHandler *touchHandler;
+
+@end
diff --git a/ios/utils/EnrichedTouchableTextView.mm b/ios/utils/EnrichedTouchableTextView.mm
new file mode 100644
index 00000000..b1aab2cb
--- /dev/null
+++ b/ios/utils/EnrichedTouchableTextView.mm
@@ -0,0 +1,30 @@
+#import "EnrichedTouchableTextView.h"
+#import "EnrichedTextTouchHandler.h"
+
+@implementation EnrichedTouchableTextView
+
+- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
+ if (touches.count == 1) {
+ UITouch *touch = touches.anyObject;
+ CGPoint point = [touch locationInView:self];
+ [self.touchHandler handleTouchBeganAtPoint:point];
+ }
+ [super touchesBegan:touches withEvent:event];
+}
+
+- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
+ if (touches.count == 1) {
+ UITouch *touch = touches.anyObject;
+ CGPoint point = [touch locationInView:self];
+ [self.touchHandler handleTouchEndedAtPoint:point];
+ }
+ [super touchesEnded:touches withEvent:event];
+}
+
+- (void)touchesCancelled:(NSSet *)touches
+ withEvent:(UIEvent *)event {
+ [self.touchHandler handleTouchCancelled];
+ [super touchesCancelled:touches withEvent:event];
+}
+
+@end
diff --git a/ios/utils/OccurenceUtils.h b/ios/utils/OccurenceUtils.h
index fd0143d5..a355399b 100644
--- a/ios/utils/OccurenceUtils.h
+++ b/ios/utils/OccurenceUtils.h
@@ -1,43 +1,43 @@
#pragma once
-#import "EnrichedTextInputView.h"
+#import "EnrichedViewHost.h"
#import "StylePair.h"
@interface OccurenceUtils : NSObject
+ (BOOL)detect:(NSAttributedStringKey _Nonnull)key
- withInput:(EnrichedTextInputView *_Nonnull)input
+ withHost:(id _Nonnull)host
inRange:(NSRange)range
withCondition:(BOOL(NS_NOESCAPE ^ _Nonnull)(id _Nullable value,
NSRange range))condition;
+ (BOOL)detect:(NSAttributedStringKey _Nonnull)key
- withInput:(EnrichedTextInputView *_Nonnull)input
+ withHost:(id _Nonnull)host
atIndex:(NSUInteger)index
checkPrevious:(BOOL)checkPrev
withCondition:(BOOL(NS_NOESCAPE ^ _Nonnull)(id _Nullable value,
NSRange range))condition;
+ (BOOL)detectMultiple:(NSArray *_Nonnull)keys
- withInput:(EnrichedTextInputView *_Nonnull)input
+ withHost:(id _Nonnull)host
inRange:(NSRange)range
withCondition:(BOOL(NS_NOESCAPE ^ _Nonnull)(id _Nullable value,
NSRange range))condition;
+ (BOOL)any:(NSAttributedStringKey _Nonnull)key
- withInput:(EnrichedTextInputView *_Nonnull)input
+ withHost:(id _Nonnull)host
inRange:(NSRange)range
withCondition:(BOOL(NS_NOESCAPE ^ _Nonnull)(id _Nullable value,
NSRange range))condition;
+ (BOOL)anyMultiple:(NSArray *_Nonnull)keys
- withInput:(EnrichedTextInputView *_Nonnull)input
+ withHost:(id _Nonnull)host
inRange:(NSRange)range
withCondition:(BOOL(NS_NOESCAPE ^ _Nonnull)(id _Nullable value,
NSRange range))condition;
+ (NSArray *_Nullable)all:(NSAttributedStringKey _Nonnull)key
- withInput:(EnrichedTextInputView *_Nonnull)input
+ withHost:(id _Nonnull)host
inRange:(NSRange)range
withCondition:(BOOL(NS_NOESCAPE ^
_Nonnull)(id _Nullable value,
NSRange range))condition;
+ (NSArray *_Nullable)
allMultiple:(NSArray *_Nonnull)keys
- withInput:(EnrichedTextInputView *_Nonnull)input
+ withHost:(id _Nonnull)host
inRange:(NSRange)range
withCondition:(BOOL(NS_NOESCAPE ^ _Nonnull)(id _Nullable value,
NSRange range))condition;
diff --git a/ios/utils/OccurenceUtils.mm b/ios/utils/OccurenceUtils.mm
index 872783c6..1cf092c2 100644
--- a/ios/utils/OccurenceUtils.mm
+++ b/ios/utils/OccurenceUtils.mm
@@ -4,12 +4,12 @@
@implementation OccurenceUtils
+ (BOOL)detect:(NSAttributedStringKey _Nonnull)key
- withInput:(EnrichedTextInputView *_Nonnull)input
+ withHost:(id _Nonnull)host
inRange:(NSRange)range
withCondition:(BOOL(NS_NOESCAPE ^ _Nonnull)(id _Nullable value,
NSRange range))condition {
__block NSInteger totalLength = 0;
- [input->textView.textStorage
+ [host.textView.textStorage
enumerateAttribute:key
inRange:range
options:0
@@ -26,24 +26,31 @@ + (BOOL)detect:(NSAttributedStringKey _Nonnull)key
// it means that first character of paragraph will be checked instead if the
// detection is not in input's selected range and at the end of the input
+ (BOOL)detect:(NSAttributedStringKey _Nonnull)key
- withInput:(EnrichedTextInputView *_Nonnull)input
+ withHost:(id _Nonnull)host
atIndex:(NSUInteger)index
checkPrevious:(BOOL)checkPrev
withCondition:(BOOL(NS_NOESCAPE ^ _Nonnull)(id _Nullable value,
NSRange range))condition {
NSRange detectionRange = NSMakeRange(index, 0);
id attrValue;
- if (NSEqualRanges(input->textView.selectedRange, detectionRange)) {
- attrValue = input->textView.typingAttributes[key];
- } else if (index == input->textView.textStorage.string.length) {
+ // Only trust typingAttributes when the textView is actually editable.
+ // Non-editable hosts (e.g. EnrichedTextView) keep selectedRange pinned at
+ // (0, 0), so without this gate every detection at index 0 would match the
+ // selection and read stale/default typingAttributes instead of the real
+ // attribute at that position in textStorage.
+ if (host.textView.isEditable &&
+ NSEqualRanges(host.textView.selectedRange, detectionRange)) {
+ NSLog(@"[OccurenceUtils] isEditable srodek: %d", host.textView.isEditable);
+ attrValue = host.textView.typingAttributes[key];
+ } else if (index == host.textView.textStorage.string.length) {
if (checkPrev) {
- NSRange paragraphRange = [input->textView.textStorage.string
+ NSRange paragraphRange = [host.textView.textStorage.string
paragraphRangeForRange:detectionRange];
if (paragraphRange.location == detectionRange.location) {
return NO;
} else {
return [self detect:key
- withInput:input
+ withHost:host
inRange:NSMakeRange(paragraphRange.location, 1)
withCondition:condition];
}
@@ -52,21 +59,21 @@ + (BOOL)detect:(NSAttributedStringKey _Nonnull)key
}
} else {
NSRange attrRange = NSMakeRange(0, 0);
- attrValue = [input->textView.textStorage attribute:key
- atIndex:index
- effectiveRange:&attrRange];
+ attrValue = [host.textView.textStorage attribute:key
+ atIndex:index
+ effectiveRange:&attrRange];
}
return condition(attrValue, detectionRange);
}
+ (BOOL)detectMultiple:(NSArray *_Nonnull)keys
- withInput:(EnrichedTextInputView *_Nonnull)input
+ withHost:(id _Nonnull)host
inRange:(NSRange)range
withCondition:(BOOL(NS_NOESCAPE ^ _Nonnull)(id _Nullable value,
NSRange range))condition {
__block NSInteger totalLength = 0;
for (NSString *key in keys) {
- [input->textView.textStorage
+ [host.textView.textStorage
enumerateAttribute:key
inRange:range
options:0
@@ -81,12 +88,12 @@ + (BOOL)detectMultiple:(NSArray *_Nonnull)keys
}
+ (BOOL)any:(NSAttributedStringKey _Nonnull)key
- withInput:(EnrichedTextInputView *_Nonnull)input
+ withHost:(id _Nonnull)host
inRange:(NSRange)range
withCondition:(BOOL(NS_NOESCAPE ^ _Nonnull)(id _Nullable value,
NSRange range))condition {
__block BOOL found = NO;
- [input->textView.textStorage
+ [host.textView.textStorage
enumerateAttribute:key
inRange:range
options:0
@@ -101,13 +108,13 @@ + (BOOL)any:(NSAttributedStringKey _Nonnull)key
}
+ (BOOL)anyMultiple:(NSArray *_Nonnull)keys
- withInput:(EnrichedTextInputView *_Nonnull)input
+ withHost:(id _Nonnull)host
inRange:(NSRange)range
withCondition:(BOOL(NS_NOESCAPE ^ _Nonnull)(id _Nullable value,
NSRange range))condition {
__block BOOL found = NO;
for (NSString *key in keys) {
- [input->textView.textStorage
+ [host.textView.textStorage
enumerateAttribute:key
inRange:range
options:0
@@ -126,7 +133,7 @@ + (BOOL)anyMultiple:(NSArray *_Nonnull)keys
}
+ (NSArray *_Nullable)all:(NSAttributedStringKey _Nonnull)key
- withInput:(EnrichedTextInputView *_Nonnull)input
+ withHost:(id _Nonnull)host
inRange:(NSRange)range
withCondition:
(BOOL(NS_NOESCAPE ^ _Nonnull)(id _Nullable value,
@@ -134,7 +141,7 @@ + (BOOL)anyMultiple:(NSArray *_Nonnull)keys
condition {
__block NSMutableArray *occurences =
[[NSMutableArray alloc] init];
- [input->textView.textStorage
+ [host.textView.textStorage
enumerateAttribute:key
inRange:range
options:0
@@ -152,14 +159,14 @@ + (BOOL)anyMultiple:(NSArray *_Nonnull)keys
+ (NSArray *_Nullable)
allMultiple:(NSArray *_Nonnull)keys
- withInput:(EnrichedTextInputView *_Nonnull)input
+ withHost:(id _Nonnull)host
inRange:(NSRange)range
withCondition:(BOOL(NS_NOESCAPE ^ _Nonnull)(id _Nullable value,
NSRange range))condition {
__block NSMutableArray *occurences =
[[NSMutableArray alloc] init];
for (NSString *key in keys) {
- [input->textView.textStorage
+ [host.textView.textStorage
enumerateAttribute:key
inRange:range
options:0
diff --git a/ios/utils/ParagraphAttributesUtils.mm b/ios/utils/ParagraphAttributesUtils.mm
index 0f4a4a19..95efd8f7 100644
--- a/ios/utils/ParagraphAttributesUtils.mm
+++ b/ios/utils/ParagraphAttributesUtils.mm
@@ -2,6 +2,7 @@
#import "EnrichedTextInputView.h"
#import "RangeUtils.h"
#import "StyleHeaders.h"
+#import "StyleUtils.h"
#import "TextInsertionUtils.h"
@implementation ParagraphAttributesUtils
@@ -159,12 +160,14 @@ + (BOOL)handleParagraphStylesMergeOnBackspace:(NSRange)range
StyleType type = [[leftParagraphStyle class] getType];
- NSArray *conflictingStyles = [typedInput
+ NSArray *conflictingStyles = [StyleUtils
getPresentStyleTypesFrom:typedInput->conflictingStyles[@(type)]
- range:rightRange];
+ range:rightRange
+ forHost:typedInput];
NSArray *blockingStyles =
- [typedInput getPresentStyleTypesFrom:typedInput->blockingStyles[@(type)]
- range:rightRange];
+ [StyleUtils getPresentStyleTypesFrom:typedInput->blockingStyles[@(type)]
+ range:rightRange
+ forHost:typedInput];
NSArray *allToBeRemoved =
[conflictingStyles arrayByAddingObjectsFromArray:blockingStyles];
diff --git a/ios/utils/StyleUtils.h b/ios/utils/StyleUtils.h
new file mode 100644
index 00000000..d0850ddb
--- /dev/null
+++ b/ios/utils/StyleUtils.h
@@ -0,0 +1,30 @@
+#import "EnrichedTextStyleHeaders.h"
+#import "StyleHeaders.h"
+
+@interface StyleUtils : NSObject
++ (NSDictionary *> *)conflictMap;
++ (NSDictionary *> *)blockingMap;
++ (NSDictionary *)stylesDictForHost:
+ (id)host
+ isInput:(BOOL)isInput;
+
++ (BOOL)handleStyleBlocksAndConflicts:(StyleType)type
+ range:(NSRange)range
+ forHost:(id)host;
++ (NSArray *)getPresentStyleTypesFrom:(NSArray *)types
+ range:(NSRange)range
+ forHost:(id)host;
++ (void)addStyleBlock:(StyleType)blocking
+ to:(StyleType)blocked
+ forHost:(id)host;
++ (void)removeStyleBlock:(StyleType)blocking
+ from:(StyleType)blocked
+ forHost:(id)host;
+
++ (void)addStyleConflict:(StyleType)conflicting
+ to:(StyleType)conflicted
+ forHost:(id)host;
++ (void)removeStyleConflict:(StyleType)conflicting
+ from:(StyleType)conflicted
+ forHost:(id)host;
+@end
diff --git a/ios/utils/StyleUtils.mm b/ios/utils/StyleUtils.mm
new file mode 100644
index 00000000..5372e8cd
--- /dev/null
+++ b/ios/utils/StyleUtils.mm
@@ -0,0 +1,268 @@
+#import "StyleUtils.h"
+
+@implementation StyleUtils
+
++ (NSDictionary *)conflictMap {
+ return @{
+ @([BoldStyle getType]) : @[],
+ @([ItalicStyle getType]) : @[],
+ @([UnderlineStyle getType]) : @[],
+ @([StrikethroughStyle getType]) : @[],
+ @([InlineCodeStyle getType]) :
+ @[ @([LinkStyle getType]), @([MentionStyle getType]) ],
+ @([LinkStyle getType]) : @[
+ @([InlineCodeStyle getType]), @([LinkStyle getType]),
+ @([MentionStyle getType])
+ ],
+ @([MentionStyle getType]) :
+ @[ @([InlineCodeStyle getType]), @([LinkStyle getType]) ],
+ @([H1Style getType]) : @[
+ @([H2Style getType]), @([H3Style getType]), @([H4Style getType]),
+ @([H5Style getType]), @([H6Style getType]),
+ @([UnorderedListStyle getType]), @([OrderedListStyle getType]),
+ @([BlockQuoteStyle getType]), @([CodeBlockStyle getType]),
+ @([CheckboxListStyle getType])
+ ],
+ @([H2Style getType]) : @[
+ @([H1Style getType]), @([H3Style getType]), @([H4Style getType]),
+ @([H5Style getType]), @([H6Style getType]),
+ @([UnorderedListStyle getType]), @([OrderedListStyle getType]),
+ @([BlockQuoteStyle getType]), @([CodeBlockStyle getType]),
+ @([CheckboxListStyle getType])
+ ],
+ @([H3Style getType]) : @[
+ @([H1Style getType]), @([H2Style getType]), @([H4Style getType]),
+ @([H5Style getType]), @([H6Style getType]),
+ @([UnorderedListStyle getType]), @([OrderedListStyle getType]),
+ @([BlockQuoteStyle getType]), @([CodeBlockStyle getType]),
+ @([CheckboxListStyle getType])
+ ],
+ @([H4Style getType]) : @[
+ @([H1Style getType]), @([H2Style getType]), @([H3Style getType]),
+ @([H5Style getType]), @([H6Style getType]),
+ @([UnorderedListStyle getType]), @([OrderedListStyle getType]),
+ @([BlockQuoteStyle getType]), @([CodeBlockStyle getType]),
+ @([CheckboxListStyle getType])
+ ],
+ @([H5Style getType]) : @[
+ @([H1Style getType]), @([H2Style getType]), @([H3Style getType]),
+ @([H4Style getType]), @([H6Style getType]),
+ @([UnorderedListStyle getType]), @([OrderedListStyle getType]),
+ @([BlockQuoteStyle getType]), @([CodeBlockStyle getType]),
+ @([CheckboxListStyle getType])
+ ],
+ @([H6Style getType]) : @[
+ @([H1Style getType]), @([H2Style getType]), @([H3Style getType]),
+ @([H4Style getType]), @([H5Style getType]),
+ @([UnorderedListStyle getType]), @([OrderedListStyle getType]),
+ @([BlockQuoteStyle getType]), @([CodeBlockStyle getType]),
+ @([CheckboxListStyle getType])
+ ],
+ @([UnorderedListStyle getType]) : @[
+ @([H1Style getType]), @([H2Style getType]), @([H3Style getType]),
+ @([H4Style getType]), @([H5Style getType]), @([H6Style getType]),
+ @([OrderedListStyle getType]), @([BlockQuoteStyle getType]),
+ @([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 getType])
+ ],
+ @([CheckboxListStyle getType]) : @[
+ @([H1Style getType]), @([H2Style getType]), @([H3Style getType]),
+ @([H4Style getType]), @([H5Style getType]), @([H6Style getType]),
+ @([UnorderedListStyle getType]), @([OrderedListStyle getType]),
+ @([BlockQuoteStyle getType]), @([CodeBlockStyle getType])
+ ],
+ @([BlockQuoteStyle getType]) : @[
+ @([H1Style getType]), @([H2Style getType]), @([H3Style getType]),
+ @([H4Style getType]), @([H5Style getType]), @([H6Style getType]),
+ @([UnorderedListStyle getType]), @([OrderedListStyle getType]),
+ @([CodeBlockStyle getType]), @([CheckboxListStyle getType])
+ ],
+ @([CodeBlockStyle getType]) : @[
+ @([H1Style getType]), @([H2Style getType]), @([H3Style getType]),
+ @([H4Style getType]), @([H5Style getType]), @([H6Style getType]),
+ @([BoldStyle getType]), @([UnderlineStyle getType]),
+ @([ItalicStyle getType]), @([StrikethroughStyle getType]),
+ @([UnorderedListStyle getType]), @([OrderedListStyle getType]),
+ @([BlockQuoteStyle getType]), @([InlineCodeStyle getType]),
+ @([MentionStyle getType]), @([LinkStyle getType]),
+ @([CheckboxListStyle getType])
+ ],
+ @([ImageStyle getType]) :
+ @[ @([LinkStyle getType]), @([MentionStyle getType]) ]
+ };
+}
+
++ (NSDictionary *)blockingMap {
+ return @{
+ @([BoldStyle getType]) : @[ @([CodeBlockStyle getType]) ],
+ @([ItalicStyle getType]) : @[ @([CodeBlockStyle getType]) ],
+ @([UnderlineStyle getType]) : @[ @([CodeBlockStyle getType]) ],
+ @([StrikethroughStyle getType]) : @[ @([CodeBlockStyle getType]) ],
+ @([InlineCodeStyle getType]) :
+ @[ @([CodeBlockStyle getType]), @([ImageStyle getType]) ],
+ @([LinkStyle getType]) :
+ @[ @([CodeBlockStyle getType]), @([ImageStyle getType]) ],
+ @([MentionStyle getType]) :
+ @[ @([CodeBlockStyle getType]), @([ImageStyle getType]) ],
+ @([H1Style getType]) : @[],
+ @([H2Style getType]) : @[],
+ @([H3Style getType]) : @[],
+ @([H4Style getType]) : @[],
+ @([H5Style getType]) : @[],
+ @([H6Style getType]) : @[],
+ @([UnorderedListStyle getType]) : @[],
+ @([OrderedListStyle getType]) : @[],
+ @([CheckboxListStyle getType]) : @[],
+ @([BlockQuoteStyle getType]) : @[],
+ @([CodeBlockStyle getType]) : @[],
+ @([ImageStyle getType]) : @[ @([InlineCodeStyle getType]) ]
+ };
+}
+
++ (NSDictionary *)stylesDictForHost:(id)host
+ isInput:(BOOL)isInput {
+ NSArray *baseClasses = @[
+ [BoldStyle class], [ItalicStyle class], [UnderlineStyle class],
+ [StrikethroughStyle class], [InlineCodeStyle class], [LinkStyle class],
+ [MentionStyle class], [H1Style class], [H2Style class], [H3Style class],
+ [H4Style class], [H5Style class], [H6Style class],
+ [UnorderedListStyle class], [OrderedListStyle class],
+ [CheckboxListStyle class], [BlockQuoteStyle class], [CodeBlockStyle class],
+ [ImageStyle class]
+ ];
+
+ NSArray *viewerClasses = @[
+ [EnrichedTextBoldStyle class], [EnrichedTextItalicStyle class],
+ [EnrichedTextUnderlineStyle class], [EnrichedTextStrikethroughStyle class],
+ [EnrichedTextInlineCodeStyle class], [EnrichedTextLinkStyle class],
+ [EnrichedTextMentionStyle class], [EnrichedTextH1Style class],
+ [EnrichedTextH2Style class], [EnrichedTextH3Style class],
+ [EnrichedTextH4Style class], [EnrichedTextH5Style class],
+ [EnrichedTextH6Style class], [EnrichedTextUnorderedListStyle class],
+ [EnrichedTextOrderedListStyle class], [EnrichedTextCheckboxListStyle class],
+ [EnrichedTextBlockQuoteStyle class], [EnrichedTextCodeBlockStyle class],
+ [EnrichedTextImageStyle class]
+ ];
+
+ NSMutableDictionary *dict = [NSMutableDictionary new];
+
+ for (NSUInteger i = 0; i < baseClasses.count; i++) {
+ // Choose the class based on the context
+ Class targetClass = isInput ? baseClasses[i] : viewerClasses[i];
+
+ // Instantiate and add to dictionary
+ // We use [baseClasses[i] getType] for the key to ensure the
+ // conflict maps (which use base types) always match.
+ StyleBase *instance = [[targetClass alloc] initWithHost:host];
+ dict[@([baseClasses[i] getType])] = instance;
+ }
+
+ return [dict copy];
+}
+
+// returns false when style shouldn't be applied and true when it can be
++ (BOOL)handleStyleBlocksAndConflicts:(StyleType)type
+ range:(NSRange)range
+ forHost:(id)host {
+ // handle blocking styles: if any is present we do not apply the toggled style
+ NSArray *blocking =
+ [self getPresentStyleTypesFrom:host.blockingStyles[@(type)]
+ range:range
+ forHost:host];
+ if (blocking.count != 0) {
+ return NO;
+ }
+
+ // handle conflicting styles: remove styles within the range
+ NSArray *conflicting =
+ [self getPresentStyleTypesFrom:host.conflictingStyles[@(type)]
+ range:range
+ forHost:host];
+ if (conflicting.count != 0) {
+ for (NSNumber *type in conflicting) {
+ StyleBase *style = host.stylesDict[type];
+
+ if ([style isParagraph]) {
+ // for paragraph styles we can just call remove since it will pick up
+ // proper paragraph range
+ [style remove:range withDirtyRange:YES];
+ } else {
+ // for inline styles we have to differentiate betweeen normal and typing
+ // attributes removal
+ range.length >= 1 ? [style remove:range withDirtyRange:YES]
+ : [style removeTyping];
+ }
+ }
+ }
+ return YES;
+}
+
++ (NSArray *)getPresentStyleTypesFrom:(NSArray *)types
+ range:(NSRange)range
+ forHost:(id)host {
+ NSMutableArray *resultArray =
+ [[NSMutableArray alloc] init];
+ for (NSNumber *type in types) {
+ StyleBase *style = host.stylesDict[type];
+
+ if (range.length >= 1) {
+ if ([style any:range]) {
+ [resultArray addObject:type];
+ }
+ } else {
+ if ([style detect:range]) {
+ [resultArray addObject:type];
+ }
+ }
+ }
+ return resultArray;
+}
+
++ (void)addStyleBlock:(StyleType)blocking
+ to:(StyleType)blocked
+ forHost:(id)host {
+ NSMutableArray *blocksArr = [host.blockingStyles[@(blocked)] mutableCopy];
+ if (![blocksArr containsObject:@(blocking)]) {
+ [blocksArr addObject:@(blocking)];
+ host.blockingStyles[@(blocked)] = blocksArr;
+ }
+}
+
++ (void)removeStyleBlock:(StyleType)blocking
+ from:(StyleType)blocked
+ forHost:(id)host {
+ NSMutableArray *blocksArr = [host.blockingStyles[@(blocked)] mutableCopy];
+ if ([blocksArr containsObject:@(blocking)]) {
+ [blocksArr removeObject:@(blocking)];
+ host.blockingStyles[@(blocked)] = blocksArr;
+ }
+}
+
++ (void)addStyleConflict:(StyleType)conflicting
+ to:(StyleType)conflicted
+ forHost:(id)host {
+ NSMutableArray *conflictsArr =
+ [host.conflictingStyles[@(conflicted)] mutableCopy];
+ if (![conflictsArr containsObject:@(conflicting)]) {
+ [conflictsArr addObject:@(conflicting)];
+ host.conflictingStyles[@(conflicted)] = conflictsArr;
+ }
+}
+
++ (void)removeStyleConflict:(StyleType)conflicting
+ from:(StyleType)conflicted
+ forHost:(id)host {
+ NSMutableArray *conflictsArr =
+ [host.conflictingStyles[@(conflicted)] mutableCopy];
+ if ([conflictsArr containsObject:@(conflicting)]) {
+ [conflictsArr removeObject:@(conflicting)];
+ host.conflictingStyles[@(conflicted)] = conflictsArr;
+ }
+}
+
+@end
diff --git a/ios/utils/TextInsertionUtils.h b/ios/utils/TextInsertionUtils.h
index 1012f383..79e11bd8 100644
--- a/ios/utils/TextInsertionUtils.h
+++ b/ios/utils/TextInsertionUtils.h
@@ -1,3 +1,4 @@
+#import "EnrichedViewHost.h"
#import
@interface TextInsertionUtils : NSObject
@@ -5,13 +6,12 @@
at:(NSInteger)index
additionalAttributes:
(NSDictionary *)additionalAttrs
- input:(id)input
+ input:(id)host
withSelection:(BOOL)withSelection;
+ (void)replaceText:(NSString *)text
at:(NSRange)range
additionalAttributes:
(NSDictionary *)additionalAttrs
- input:(id)input
+ input:(id)host
withSelection:(BOOL)withSelection;
-;
@end
diff --git a/ios/utils/TextInsertionUtils.mm b/ios/utils/TextInsertionUtils.mm
index 20927ba0..70bf31ac 100644
--- a/ios/utils/TextInsertionUtils.mm
+++ b/ios/utils/TextInsertionUtils.mm
@@ -1,5 +1,4 @@
#import "TextInsertionUtils.h"
-#import "EnrichedTextInputView.h"
#import "UIView+React.h"
@implementation TextInsertionUtils
@@ -7,14 +6,13 @@ + (void)insertText:(NSString *)text
at:(NSInteger)index
additionalAttributes:
(NSDictionary *)additionalAttrs
- input:(id)input
+ input:(id)host
withSelection:(BOOL)withSelection {
- EnrichedTextInputView *typedInput = (EnrichedTextInputView *)input;
- if (typedInput == nullptr) {
+ if (host == nullptr) {
return;
}
- UITextView *textView = typedInput->textView;
+ UITextView *textView = host.textView;
NSMutableDictionary *copiedAttrs =
[textView.typingAttributes mutableCopy];
@@ -38,15 +36,13 @@ + (void)replaceText:(NSString *)text
at:(NSRange)range
additionalAttributes:
(NSDictionary *)additionalAttrs
- input:(id)input
+ input:(id)host
withSelection:(BOOL)withSelection {
- EnrichedTextInputView *typedInput = (EnrichedTextInputView *)input;
- if (typedInput == nullptr) {
+ if (host == nullptr) {
return;
}
- UITextView *textView = typedInput->textView;
-
+ UITextView *textView = host.textView;
[textView.textStorage replaceCharactersInRange:range withString:text];
if (additionalAttrs != nullptr) {
[textView.textStorage
diff --git a/ios/utils/ZeroWidthSpaceUtils.h b/ios/utils/ZeroWidthSpaceUtils.h
index 7678749d..dc8bca25 100644
--- a/ios/utils/ZeroWidthSpaceUtils.h
+++ b/ios/utils/ZeroWidthSpaceUtils.h
@@ -1,9 +1,12 @@
+#import "EnrichedViewHost.h"
#import
#pragma once
@interface ZeroWidthSpaceUtils : NSObject
-+ (void)handleZeroWidthSpacesInInput:(id)input;
++ (void)handleZeroWidthSpacesInInput:(id)host;
++ (void)addSpacesIfNeededinInput:(id)host
+ inRange:(NSRange)range;
+ (BOOL)handleBackspaceInRange:(NSRange)range
replacementText:(NSString *)text
- input:(id)input;
+ input:(id)host;
@end
diff --git a/ios/utils/ZeroWidthSpaceUtils.mm b/ios/utils/ZeroWidthSpaceUtils.mm
index bebb21e9..ed9025ff 100644
--- a/ios/utils/ZeroWidthSpaceUtils.mm
+++ b/ios/utils/ZeroWidthSpaceUtils.mm
@@ -5,26 +5,28 @@
#import "UIView+React.h"
@implementation ZeroWidthSpaceUtils
-+ (void)handleZeroWidthSpacesInInput:(id)input {
- EnrichedTextInputView *typedInput = (EnrichedTextInputView *)input;
- if (typedInput == nullptr) {
++ (void)handleZeroWidthSpacesInInput:(id)host {
+ if (host == nullptr) {
return;
}
- [self removeSpacesIfNeededinInput:typedInput];
- [self addSpacesIfNeededinInput:typedInput];
+ [self removeSpacesIfNeededinInput:host];
+ [self
+ addSpacesIfNeededinInput:host
+ inRange:NSMakeRange(
+ 0, host.textView.textStorage.string.length)];
}
-+ (void)removeSpacesIfNeededinInput:(EnrichedTextInputView *)input {
++ (void)removeSpacesIfNeededinInput:(id)host {
NSMutableArray *indexesToBeRemoved = [[NSMutableArray alloc] init];
- NSRange preRemoveSelection = input->textView.selectedRange;
+ NSRange preRemoveSelection = host.textView.selectedRange;
- for (int i = 0; i < input->textView.textStorage.string.length; i++) {
- unichar character = [input->textView.textStorage.string characterAtIndex:i];
+ for (int i = 0; i < host.textView.textStorage.string.length; i++) {
+ unichar character = [host.textView.textStorage.string characterAtIndex:i];
if (character == 0x200B) {
NSRange characterRange = NSMakeRange(i, 1);
- NSRange paragraphRange = [input->textView.textStorage.string
+ NSRange paragraphRange = [host.textView.textStorage.string
paragraphRangeForRange:characterRange];
// having paragraph longer than 1 character means someone most likely
// added something and we probably can remove the space
@@ -33,7 +35,7 @@ + (void)removeSpacesIfNeededinInput:(EnrichedTextInputView *)input {
// here, we still need zero width space to keep the empty list items
if (paragraphRange.length == 2 && paragraphRange.location == i &&
[[NSCharacterSet newlineCharacterSet]
- characterIsMember:[input->textView.textStorage.string
+ characterIsMember:[host.textView.textStorage.string
characterAtIndex:i + 1]]) {
removeSpace = NO;
}
@@ -44,7 +46,7 @@ + (void)removeSpacesIfNeededinInput:(EnrichedTextInputView *)input {
}
// zero width spaces with no needsZWS style on them get removed
- if (![self anyZWSStylePresentInRange:characterRange input:input]) {
+ if (![self anyZWSStylePresentInRange:characterRange input:host]) {
[indexesToBeRemoved addObject:@(characterRange.location)];
}
}
@@ -59,7 +61,7 @@ + (void)removeSpacesIfNeededinInput:(EnrichedTextInputView *)input {
[TextInsertionUtils replaceText:@""
at:replaceRange
additionalAttributes:nullptr
- input:input
+ input:host
withSelection:NO];
offset -= 1;
if ([index integerValue] < preRemoveSelection.location) {
@@ -72,8 +74,8 @@ + (void)removeSpacesIfNeededinInput:(EnrichedTextInputView *)input {
}
// fix the selection if needed
- if ([input->textView isFirstResponder]) {
- input->textView.selectedRange =
+ if ([host.textView isFirstResponder]) {
+ host.textView.selectedRange =
NSMakeRange(preRemoveSelection.location + postRemoveLocationOffset,
preRemoveSelection.length + postRemoveLengthOffset);
}
@@ -83,13 +85,13 @@ + (void)removeSpacesIfNeededinInput:(EnrichedTextInputView *)input {
// dictionary so that ZWS characters carry the same meta-attributes that are
// currently active in the typing attributes. Only within the currently selected
// range!
-+ (NSDictionary *)inlineMetaAttributesForInput:(EnrichedTextInputView *)input {
++ (NSDictionary *)inlineMetaAttributesForInput:(id)host {
NSMutableDictionary *metaAttrs = [NSMutableDictionary new];
- for (NSNumber *type in input->stylesDict) {
- StyleBase *style = input->stylesDict[type];
+ for (NSNumber *type in host.stylesDict) {
+ StyleBase *style = host.stylesDict[type];
if (![style isParagraph]) {
AttributeEntry *entry =
- [style getEntryIfPresent:input->textView.selectedRange];
+ [style getEntryIfPresent:host.textView.selectedRange];
if (entry) {
metaAttrs[entry.key] = entry.value;
}
@@ -98,20 +100,27 @@ + (NSDictionary *)inlineMetaAttributesForInput:(EnrichedTextInputView *)input {
return metaAttrs.count > 0 ? metaAttrs : nullptr;
}
-+ (void)addSpacesIfNeededinInput:(EnrichedTextInputView *)input {
++ (void)addSpacesIfNeededinInput:(id)host
+ inRange:(NSRange)range {
NSMutableArray *indexesToBeInserted = [[NSMutableArray alloc] init];
- NSRange preAddSelection = input->textView.selectedRange;
+ NSRange preAddSelection = host.textView.selectedRange;
- for (NSUInteger i = 0; i < input->textView.textStorage.string.length; i++) {
- unichar character = [input->textView.textStorage.string characterAtIndex:i];
+ // Expand to paragraph boundaries so callers can pass any style range
+ // without worrying about missing the terminating newline of an empty
+ // paragraph that starts before range.location.
+ NSRange scanRange =
+ [host.textView.textStorage.string paragraphRangeForRange:range];
+
+ for (NSUInteger i = scanRange.location; i < NSMaxRange(scanRange); i++) {
+ unichar character = [host.textView.textStorage.string characterAtIndex:i];
if ([[NSCharacterSet newlineCharacterSet] characterIsMember:character]) {
NSRange characterRange = NSMakeRange(i, 1);
- NSRange paragraphRange = [input->textView.textStorage.string
+ NSRange paragraphRange = [host.textView.textStorage.string
paragraphRangeForRange:characterRange];
if (paragraphRange.length == 1) {
- if ([self anyZWSStylePresentInRange:characterRange input:input]) {
+ if ([self anyZWSStylePresentInRange:characterRange input:host]) {
// we have an empty list or quote item with no space: add it!
[indexesToBeInserted addObject:@(paragraphRange.location)];
}
@@ -119,7 +128,7 @@ + (void)addSpacesIfNeededinInput:(EnrichedTextInputView *)input {
}
}
- NSDictionary *metaAttrs = [self inlineMetaAttributesForInput:input];
+ NSDictionary *metaAttrs = [self inlineMetaAttributesForInput:host];
// do the replacing
NSInteger offset = 0;
@@ -130,7 +139,7 @@ + (void)addSpacesIfNeededinInput:(EnrichedTextInputView *)input {
[TextInsertionUtils replaceText:@"\u200B\n"
at:replaceRange
additionalAttributes:metaAttrs
- input:input
+ input:host
withSelection:NO];
offset += 1;
if ([index integerValue] < preAddSelection.location) {
@@ -142,22 +151,25 @@ + (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 &&
- [self anyZWSStylePresentInRange:lastRange input:input]) {
- [TextInsertionUtils insertText:@"\u200B"
- at:lastRange.location
- additionalAttributes:metaAttrs
- input:input
- withSelection:NO];
+ // additional check for last index of the input - only when the caller's
+ // range actually reaches the end of the input
+ if (NSMaxRange(scanRange) == host.textView.textStorage.string.length) {
+ NSRange lastRange = NSMakeRange(host.textView.textStorage.string.length, 0);
+ NSRange lastParagraphRange =
+ [host.textView.textStorage.string paragraphRangeForRange:lastRange];
+ if (lastParagraphRange.length == 0 &&
+ [self anyZWSStylePresentInRange:lastRange input:host]) {
+ [TextInsertionUtils insertText:@"\u200B"
+ at:lastRange.location
+ additionalAttributes:metaAttrs
+ input:host
+ withSelection:NO];
+ }
}
// fix the selection if needed
- if ([input->textView isFirstResponder]) {
- input->textView.selectedRange =
+ if ([host.textView isFirstResponder]) {
+ host.textView.selectedRange =
NSMakeRange(preAddSelection.location + postAddLocationOffset,
preAddSelection.length + postAddLengthOffset);
}
@@ -165,12 +177,11 @@ + (void)addSpacesIfNeededinInput:(EnrichedTextInputView *)input {
+ (BOOL)handleBackspaceInRange:(NSRange)range
replacementText:(NSString *)text
- input:(id)input {
+ input:(id)host {
if (![text isEqualToString:@""]) {
return NO;
}
- EnrichedTextInputView *typedInput = (EnrichedTextInputView *)input;
- if (typedInput == nullptr) {
+ if (host == nullptr) {
return NO;
}
@@ -178,9 +189,9 @@ + (BOOL)handleBackspaceInRange:(NSRange)range
// 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
+ NSRange firstParagraphRange = [host.textView.textStorage.string
paragraphRangeForRange:NSMakeRange(0, 0)];
- if ([self removeZWSStyleInRange:firstParagraphRange input:typedInput]) {
+ if ([self removeZWSStyleInRange:firstParagraphRange input:host]) {
return YES;
}
return NO;
@@ -191,19 +202,19 @@ + (BOOL)handleBackspaceInRange:(NSRange)range
}
unichar character =
- [typedInput->textView.textStorage.string characterAtIndex:range.location];
+ [host.textView.textStorage.string characterAtIndex:range.location];
// zero-width space got backspaced
if (character == 0x200B) {
// in such case: remove the whole line without the endline if there is one
NSRange paragraphRange =
- [typedInput->textView.textStorage.string paragraphRangeForRange:range];
+ [host.textView.textStorage.string paragraphRangeForRange:range];
NSRange removalRange = paragraphRange;
// if whole paragraph gets removed then 0 length for style removal
NSRange styleRemovalRange = NSMakeRange(paragraphRange.location, 0);
if ([[NSCharacterSet newlineCharacterSet]
- characterIsMember:[typedInput->textView.textStorage.string
+ characterIsMember:[host.textView.textStorage.string
characterAtIndex:NSMaxRange(paragraphRange) -
1]]) {
// if endline is there, don't remove it
@@ -217,11 +228,11 @@ + (BOOL)handleBackspaceInRange:(NSRange)range
[TextInsertionUtils replaceText:@""
at:removalRange
additionalAttributes:nullptr
- input:typedInput
+ input:host
withSelection:YES];
// and then remove associated styling
- [self removeZWSStyleInRange:styleRemovalRange input:typedInput];
+ [self removeZWSStyleInRange:styleRemovalRange input:host];
return YES;
}
@@ -232,10 +243,10 @@ + (BOOL)handleBackspaceInRange:(NSRange)range
// 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
+ if (nextParaStart < host.textView.textStorage.string.length) {
+ NSRange nextParagraphRange = [host.textView.textStorage.string
paragraphRangeForRange:NSMakeRange(nextParaStart, 0)];
- if ([self removeZWSStyleInRange:nextParagraphRange input:typedInput]) {
+ if ([self removeZWSStyleInRange:nextParagraphRange input:host]) {
return YES;
}
}
@@ -245,9 +256,9 @@ + (BOOL)handleBackspaceInRange:(NSRange)range
}
+ (BOOL)anyZWSStylePresentInRange:(NSRange)range
- input:(EnrichedTextInputView *)input {
- for (NSNumber *type in input->stylesDict) {
- StyleBase *style = input->stylesDict[type];
+ input:(id)host {
+ for (NSNumber *type in host.stylesDict) {
+ StyleBase *style = host.stylesDict[type];
if ([style needsZWS] && [style detect:range]) {
return YES;
}
@@ -255,10 +266,9 @@ + (BOOL)anyZWSStylePresentInRange:(NSRange)range
return NO;
}
-+ (BOOL)removeZWSStyleInRange:(NSRange)range
- input:(EnrichedTextInputView *)input {
- for (NSNumber *type in input->stylesDict) {
- StyleBase *style = input->stylesDict[type];
++ (BOOL)removeZWSStyleInRange:(NSRange)range input:(id)host {
+ for (NSNumber *type in host.stylesDict) {
+ StyleBase *style = host.stylesDict[type];
if ([style needsZWS] && [style detect:range]) {
[style remove:range withDirtyRange:YES];
return YES;
diff --git a/package.json b/package.json
index c7637eb4..f8b7a8fd 100644
--- a/package.json
+++ b/package.json
@@ -218,6 +218,7 @@
},
"ios": {
"componentProvider": {
+ "EnrichedTextView": "EnrichedTextView",
"EnrichedTextInputView": "EnrichedTextInputView"
}
},