From cfbffef5d37892d9750b661e39ccab6c88be3fa7 Mon Sep 17 00:00:00 2001
From: IvanIhnatsiuk
Date: Sat, 13 Dec 2025 22:52:05 +0100
Subject: [PATCH 1/8] feat: fast html parser
---
ios/inputParser/EnrichedHTMLParser.h | 12 +
ios/inputParser/EnrichedHTMLParser.mm | 258 +++++++++
ios/inputParser/EnrichedHTMLTagUtils.h | 128 +++++
ios/inputParser/HtmlNode.h | 14 +
ios/inputParser/HtmlNode.mm | 16 +
ios/inputParser/InputParser.mm | 743 +------------------------
ios/styles/BlockQuoteStyle.mm | 26 +-
ios/styles/BoldStyle.mm | 26 +-
ios/styles/CodeBlockStyle.mm | 33 +-
ios/styles/H1Style.mm | 3 +
ios/styles/H2Style.mm | 3 +
ios/styles/H3Style.mm | 3 +
ios/styles/HeadingStyleBase.mm | 24 +-
ios/styles/ImageStyle.mm | 38 +-
ios/styles/InlineCodeStyle.mm | 28 +-
ios/styles/ItalicStyle.mm | 26 +-
ios/styles/LinkStyle.mm | 47 +-
ios/styles/MentionStyle.mm | 46 +-
ios/styles/OrderedListStyle.mm | 26 +-
ios/styles/StrikethroughStyle.mm | 31 +-
ios/styles/UnderlineStyle.mm | 26 +-
ios/styles/UnorderedListStyle.mm | 26 +-
ios/utils/BaseStyleProtocol.h | 5 +
ios/utils/ParameterizedStyleProtocol.h | 5 +
ios/utils/StyleHeaders.h | 14 +-
25 files changed, 791 insertions(+), 816 deletions(-)
create mode 100644 ios/inputParser/EnrichedHTMLParser.h
create mode 100644 ios/inputParser/EnrichedHTMLParser.mm
create mode 100644 ios/inputParser/EnrichedHTMLTagUtils.h
create mode 100644 ios/inputParser/HtmlNode.h
create mode 100644 ios/inputParser/HtmlNode.mm
create mode 100644 ios/utils/ParameterizedStyleProtocol.h
diff --git a/ios/inputParser/EnrichedHTMLParser.h b/ios/inputParser/EnrichedHTMLParser.h
new file mode 100644
index 000000000..53d6c2a76
--- /dev/null
+++ b/ios/inputParser/EnrichedHTMLParser.h
@@ -0,0 +1,12 @@
+#import
+
+@class HTMLNode;
+@class HTMLTextNode;
+
+#pragma mark - Public Builder
+
+@interface EnrichedHTMLParser : NSObject
+- (instancetype)initWithStyles:(NSDictionary *)stylesDict;
+- (NSString *)buildHtmlFromAttributedString:(NSAttributedString *)text
+ pretify:(BOOL)pretify;
+@end
diff --git a/ios/inputParser/EnrichedHTMLParser.mm b/ios/inputParser/EnrichedHTMLParser.mm
new file mode 100644
index 000000000..9a0076d29
--- /dev/null
+++ b/ios/inputParser/EnrichedHTMLParser.mm
@@ -0,0 +1,258 @@
+#import "EnrichedHtmlParser.h"
+#import "EnrichedHTMLTagUtils.h"
+#import "HtmlNode.h"
+#import "StyleHeaders.h"
+
+@implementation EnrichedHTMLParser {
+ NSDictionary> *_styles;
+ NSArray *_inlineOrder;
+ NSArray *_paragraphOrder;
+}
+
+#pragma mark - Init
+
+- (instancetype)initWithStyles:(NSDictionary *)stylesDict {
+ self = [super init];
+ if (!self)
+ return nil;
+
+ _styles = stylesDict ?: @{};
+
+ NSMutableArray *inlineArr = [NSMutableArray array];
+ NSMutableArray *paragraphArr = [NSMutableArray array];
+
+ for (NSNumber *type in _styles) {
+ id style = _styles[type];
+ Class cls = style.class;
+
+ BOOL isParagraph = ([cls respondsToSelector:@selector(isParagraphStyle)] &&
+ [cls isParagraphStyle]);
+
+ if (isParagraph)
+ [paragraphArr addObject:type];
+ else
+ [inlineArr addObject:type];
+ }
+
+ [inlineArr sortUsingSelector:@selector(compare:)];
+ [paragraphArr sortUsingSelector:@selector(compare:)];
+
+ _inlineOrder = inlineArr.copy;
+ _paragraphOrder = paragraphArr.copy;
+
+ return self;
+}
+
+#pragma mark - Public API
+
+- (NSString *)buildHtmlFromAttributedString:(NSAttributedString *)text
+ pretify:(BOOL)pretify {
+
+ if (text.length == 0)
+ return @"\n\n";
+
+ HTMLElement *root = [self buildRootNodeFromAttributedString:text];
+
+ NSMutableData *buf = [NSMutableData data];
+ [self createHtmlFromNode:root into:buf pretify:pretify];
+
+ return [[NSString alloc] initWithData:buf encoding:NSUTF8StringEncoding];
+}
+
+- (HTMLElement *)buildRootNodeFromAttributedString:(NSAttributedString *)text {
+ NSString *plain = text.string;
+
+ HTMLElement *root = [HTMLElement new];
+ root.tag = "html";
+
+ HTMLElement *br = [HTMLElement new];
+ br.tag = "br";
+ br.selfClosing = YES;
+
+ __block NSNumber *previousParagraphType = nil;
+ __block HTMLElement *previousNode = nil;
+
+ [plain
+ enumerateSubstringsInRange:NSMakeRange(0, plain.length)
+ options:NSStringEnumerationByParagraphs
+ usingBlock:^(NSString *_Nullable substring,
+ NSRange paragraphRange,
+ NSRange __unused enclosingRange,
+ BOOL *__unused stop) {
+ if (paragraphRange.length == 0) {
+ [root.children addObject:br];
+ previousParagraphType = nil;
+ previousNode = nil;
+ return;
+ }
+ NSDictionary *attrsAtStart =
+ [text attributesAtIndex:paragraphRange.location
+ effectiveRange:nil];
+
+ NSNumber *ptype = nil;
+ for (NSNumber *sty in self->_paragraphOrder) {
+ id s = self->_styles[sty];
+ NSString *key = [s.class attributeKey];
+ id val = attrsAtStart[key];
+ if (val && [s styleCondition:val
+ range:paragraphRange]) {
+ ptype = sty;
+ break;
+ }
+ }
+
+ HTMLElement *container =
+ [self containerForBlock:ptype
+ reuseLastOf:previousParagraphType
+ previousNode:previousNode
+ rootNode:root];
+
+ previousParagraphType = ptype;
+ previousNode = container;
+
+ HTMLElement *target = [self getNextContainer:ptype
+ currentContainer:container];
+
+ [text
+ enumerateAttributesInRange:paragraphRange
+ options:0
+ usingBlock:^(
+ NSDictionary *attrs,
+ NSRange runRange,
+ BOOL *__unused stopRun) {
+ HTMLNode *node = [self
+ getInlineStyleNodes:text
+ range:runRange
+ attrs:attrs
+ plain:plain];
+ [target.children addObject:node];
+ }];
+ }];
+
+ return root;
+}
+
+#pragma mark - Block container helpers
+
+- (HTMLElement *)containerForBlock:(NSNumber *)currentParagraphType
+ reuseLastOf:(NSNumber *)previousParagraphType
+ previousNode:(HTMLElement *)previousNode
+ rootNode:(HTMLElement *)rootNode {
+ if (!currentParagraphType) {
+ HTMLElement *outer = [HTMLElement new];
+ outer.tag = "p";
+ [rootNode.children addObject:outer];
+ return outer;
+ }
+
+ BOOL isTheSameBlock = currentParagraphType == previousParagraphType;
+ id styleObject = _styles[currentParagraphType];
+ Class styleClass = styleObject.class;
+
+ BOOL hasSubTags = [styleClass subTagName] != NULL;
+
+ if (isTheSameBlock && hasSubTags)
+ return previousNode;
+
+ HTMLElement *outer = [HTMLElement new];
+
+ outer.tag = [styleClass tagName];
+
+ [rootNode.children addObject:outer];
+ return outer;
+}
+
+- (HTMLElement *)getNextContainer:(NSNumber *)blockType
+ currentContainer:(HTMLElement *)container {
+
+ if (!blockType)
+ return container;
+
+ id style = _styles[blockType];
+
+ const char *subTagName = [style.class subTagName];
+
+ if (subTagName) {
+ HTMLElement *inner = [HTMLElement new];
+ inner.tag = subTagName;
+ [container.children addObject:inner];
+ return inner;
+ }
+
+ return container;
+}
+
+#pragma mark - Inline styling
+
+- (HTMLNode *)getInlineStyleNodes:(NSAttributedString *)text
+ range:(NSRange)range
+ attrs:(NSDictionary *)attrs
+ plain:(NSString *)plain {
+ HTMLTextNode *textNode = [HTMLTextNode new];
+ textNode.source = plain;
+ textNode.range = range;
+ HTMLNode *currentNode = textNode;
+
+ for (NSNumber *sty in _inlineOrder) {
+
+ id obj = _styles[sty];
+ Class cls = obj.class;
+
+ NSString *key = [cls attributeKey];
+ id v = attrs[key];
+
+ if (!v || ![obj styleCondition:v range:range])
+ continue;
+
+ HTMLElement *wrap = [HTMLElement new];
+ const char *tag = [cls tagName];
+
+ wrap.tag = tag;
+ wrap.attributes =
+ [cls respondsToSelector:@selector(getParametersFromValue:)]
+ ? [cls getParametersFromValue:v]
+ : nullptr;
+ wrap.selfClosing = [cls isSelfClosing];
+ [wrap.children addObject:currentNode];
+ currentNode = wrap;
+ }
+
+ return currentNode;
+}
+
+#pragma mark - Rendering
+
+- (void)createHtmlFromNode:(HTMLNode *)node
+ into:(NSMutableData *)buf
+ pretify:(BOOL)pretify {
+ if ([node isKindOfClass:[HTMLTextNode class]]) {
+ HTMLTextNode *t = (HTMLTextNode *)node;
+ appendEscapedRange(buf, t.source, t.range);
+ return;
+ }
+
+ if (![node isKindOfClass:[HTMLElement class]])
+ return;
+
+ HTMLElement *el = (HTMLElement *)node;
+
+ BOOL addNewLineBefore = pretify && isBlockTag(el.tag);
+ BOOL addNewLineAfter = pretify && needsNewLineAfter(el.tag);
+
+ if (el.selfClosing) {
+ appendSelfClosingTagC(buf, el.tag, el.attributes, addNewLineBefore);
+ return;
+ }
+
+ appendOpenTagC(buf, el.tag, el.attributes ?: nullptr, addNewLineBefore);
+
+ for (HTMLNode *child in el.children)
+ [self createHtmlFromNode:child into:buf pretify:pretify];
+
+ if (addNewLineAfter)
+ appendC(buf, "\n");
+
+ appendCloseTagC(buf, el.tag);
+}
+
+@end
diff --git a/ios/inputParser/EnrichedHTMLTagUtils.h b/ios/inputParser/EnrichedHTMLTagUtils.h
new file mode 100644
index 000000000..e32d26cc7
--- /dev/null
+++ b/ios/inputParser/EnrichedHTMLTagUtils.h
@@ -0,0 +1,128 @@
+#pragma once
+
+#import
+
+static inline void appendC(NSMutableData *buf, const char *c) {
+ if (!c)
+ return;
+ [buf appendBytes:c length:strlen(c)];
+}
+
+static inline void appendEscapedRange(NSMutableData *buf, NSString *src,
+ NSRange r) {
+ NSUInteger len = r.length;
+ unichar *tmp = (unichar *)alloca(len * sizeof(unichar));
+ [src getCharacters:tmp range:r];
+
+ for (NSUInteger i = 0; i < len; i++) {
+ unichar c = tmp[i];
+ if (c == 0x200B)
+ continue;
+
+ switch (c) {
+ case '<':
+ appendC(buf, "<");
+ break;
+ case '>':
+ appendC(buf, ">");
+ break;
+ case '&':
+ appendC(buf, "&");
+ break;
+
+ default: {
+ char out[4];
+ int n = 0;
+ if (c < 0x80) {
+ out[0] = (char)c;
+ n = 1;
+ } else if (c < 0x800) {
+ out[0] = 0xC0 | (c >> 6);
+ out[1] = 0x80 | (c & 0x3F);
+ n = 2;
+ } else {
+ out[0] = 0xE0 | (c >> 12);
+ out[1] = 0x80 | ((c >> 6) & 0x3F);
+ out[2] = 0x80 | (c & 0x3F);
+ n = 3;
+ }
+ [buf appendBytes:out length:n];
+ }
+ }
+ }
+}
+
+static inline void appendKeyVal(NSMutableData *buf, NSString *key,
+ NSString *val) {
+ appendC(buf, " ");
+ const char *k = key.UTF8String;
+ appendC(buf, k);
+ appendC(buf, "=\"");
+ appendEscapedRange(buf, val, NSMakeRange(0, val.length));
+ appendC(buf, "\"");
+}
+
+static inline BOOL isBlockTag(const char *t) {
+ if (!t)
+ return NO;
+ switch (t[0]) {
+ case 'p':
+ return t[1] == 0;
+ case 'h':
+ return (t[2] == 0 && (t[1] == '1' || t[1] == '2' || t[1] == '3'));
+ case 'u':
+ return strcmp(t, "ul") == 0;
+ case 'o':
+ return strcmp(t, "ol") == 0;
+ case 'l':
+ return strcmp(t, "li") == 0;
+ case 'b':
+ return strcmp(t, "br") == 0 || strcmp(t, "blockquote") == 0;
+ case 'c':
+ return strcmp(t, "codeblock") == 0;
+ default:
+ return NO;
+ }
+}
+
+static inline BOOL needsNewLineAfter(const char *t) {
+ if (!t)
+ return NO;
+ return (strcmp(t, "ul") == 0 || strcmp(t, "ol") == 0 ||
+ strcmp(t, "blockquote") == 0 || strcmp(t, "codeblock") == 0 ||
+ strcmp(t, "html") == 0);
+}
+
+static inline void appendOpenTagC(NSMutableData *buf, const char *t,
+ NSDictionary *attrs, BOOL block) {
+ if (block)
+ appendC(buf, "\n<");
+ else
+ appendC(buf, "<");
+
+ appendC(buf, t);
+ for (NSString *key in attrs)
+ appendKeyVal(buf, key, attrs[key]);
+
+ appendC(buf, ">");
+}
+
+static inline void appendSelfClosingTagC(NSMutableData *buf, const char *t,
+ NSDictionary *attrs, BOOL block) {
+ if (block)
+ appendC(buf, "\n<");
+ else
+ appendC(buf, "<");
+
+ appendC(buf, t);
+ for (NSString *key in attrs)
+ appendKeyVal(buf, key, attrs[key]);
+
+ appendC(buf, "/>");
+}
+
+static inline void appendCloseTagC(NSMutableData *buf, const char *t) {
+ appendC(buf, "");
+ appendC(buf, t);
+ appendC(buf, ">");
+}
diff --git a/ios/inputParser/HtmlNode.h b/ios/inputParser/HtmlNode.h
new file mode 100644
index 000000000..1e3c525c3
--- /dev/null
+++ b/ios/inputParser/HtmlNode.h
@@ -0,0 +1,14 @@
+@interface HTMLNode : NSObject
+@property(nonatomic, strong) NSMutableArray *children;
+@end
+
+@interface HTMLElement : HTMLNode
+@property(nonatomic) const char *tag;
+@property(nonatomic, strong) NSDictionary *attributes;
+@property(nonatomic) BOOL selfClosing;
+@end
+
+@interface HTMLTextNode : HTMLNode
+@property(nonatomic) NSString *source;
+@property(nonatomic) NSRange range;
+@end
diff --git a/ios/inputParser/HtmlNode.mm b/ios/inputParser/HtmlNode.mm
new file mode 100644
index 000000000..3516a01fa
--- /dev/null
+++ b/ios/inputParser/HtmlNode.mm
@@ -0,0 +1,16 @@
+#import "HtmlNode.h"
+
+@implementation HTMLNode
+- (instancetype)init {
+ if ((self = [super init])) {
+ _children = [NSMutableArray array];
+ }
+ return self;
+}
+@end
+
+@implementation HTMLElement
+@end
+
+@implementation HTMLTextNode
+@end
diff --git a/ios/inputParser/InputParser.mm b/ios/inputParser/InputParser.mm
index 9a6aa9264..e7cb3e3dc 100644
--- a/ios/inputParser/InputParser.mm
+++ b/ios/inputParser/InputParser.mm
@@ -1,5 +1,5 @@
#import "InputParser.h"
-#import "ColorExtension.h"
+#import "EnrichedHTMLParser.h"
#import "EnrichedTextInputView.h"
#import "StringExtension.h"
#import "StyleHeaders.h"
@@ -8,752 +8,21 @@
@implementation InputParser {
EnrichedTextInputView *_input;
+ EnrichedHTMLParser *_htmlParser;
}
- (instancetype)initWithInput:(id)input {
self = [super init];
_input = (EnrichedTextInputView *)input;
+ _htmlParser = [[EnrichedHTMLParser alloc] initWithStyles:_input->stylesDict];
return self;
}
- (NSString *)parseToHtmlFromRange:(NSRange)range {
- NSInteger offset = range.location;
- NSString *text =
- [_input->textView.textStorage.string substringWithRange:range];
+ NSAttributedString *sub =
+ [_input->textView.textStorage attributedSubstringFromRange:range];
- if (text.length == 0) {
- return @"\n\n";
- }
-
- NSMutableString *result = [[NSMutableString alloc] initWithString:@""];
- NSSet *previousActiveStyles = [[NSSet alloc] init];
- BOOL newLine = YES;
- BOOL inUnorderedList = NO;
- BOOL inOrderedList = NO;
- BOOL inBlockQuote = NO;
- BOOL inCodeBlock = NO;
- BOOL isDivider = NO;
- unichar lastCharacter = 0;
- UIColor *previousColor = nil;
-
- for (int i = 0; i < text.length; i++) {
- NSRange currentRange = NSMakeRange(offset + i, 1);
- NSMutableSet *currentActiveStyles =
- [[NSMutableSet alloc] init];
- NSMutableDictionary *currentActiveStylesBeginning =
- [[NSMutableDictionary alloc] init];
-
- // check each existing style existence
- for (NSNumber *type in _input->stylesDict) {
- id style = _input->stylesDict[type];
- if ([style detectStyle:currentRange]) {
- [currentActiveStyles addObject:type];
-
- if (![previousActiveStyles member:type]) {
- currentActiveStylesBeginning[type] = [NSNumber numberWithInt:i];
- }
- } else if ([previousActiveStyles member:type]) {
- [currentActiveStylesBeginning removeObjectForKey:type];
- }
- }
-
- UIColor *currentColor = nil;
-
- if ([currentActiveStyles containsObject:@(Colored)]) {
- ColorStyle *colorStyle = _input->stylesDict[@(Colored)];
- currentColor = [colorStyle getColorAt:currentRange.location];
- if (previousColor && ![currentColor isEqual:previousColor]) {
- NSString *closeTag = [self tagContentForStyle:@(Colored)
- openingTag:NO
- location:currentRange.location];
- [result appendFormat:@"%@>", closeTag];
- NSString *openTag = [self tagContentForStyle:@(Colored)
- openingTag:YES
- location:currentRange.location];
- [result appendFormat:@"<%@>", openTag];
- NSString *currentCharacterStr = [_input->textView.textStorage.string
- substringWithRange:currentRange];
- [result appendString:currentCharacterStr];
- previousColor = currentColor;
- [currentActiveStyles addObject:@(Colored)];
- continue;
- }
- }
-
- NSString *currentCharacterStr =
- [_input->textView.textStorage.string substringWithRange:currentRange];
- unichar currentCharacterChar = [_input->textView.textStorage.string
- characterAtIndex:currentRange.location];
-
- if ([[NSCharacterSet newlineCharacterSet]
- characterIsMember:currentCharacterChar]) {
- if (newLine) {
- // we can either have an empty list item OR need to close the list and
- // put a BR in such a situation the existence of the list must be
- // checked on 0 length range, not on the newline character
- if (inOrderedList) {
- OrderedListStyle *oStyle = _input->stylesDict[@(OrderedList)];
- BOOL detected =
- [oStyle detectStyle:NSMakeRange(currentRange.location, 0)];
- if (detected) {
- [result appendString:@"\n"];
- } else {
- [result appendString:@"\n\n
"];
- inOrderedList = NO;
- }
- } else if (inUnorderedList) {
- UnorderedListStyle *uStyle = _input->stylesDict[@(UnorderedList)];
- BOOL detected =
- [uStyle detectStyle:NSMakeRange(currentRange.location, 0)];
- if (detected) {
- [result appendString:@"\n"];
- } else {
- [result appendString:@"\n\n
"];
- inUnorderedList = NO;
- }
- } else {
- [result appendString:@"\n
"];
- }
- } else {
- // newline finishes a paragraph and all style tags need to be closed
- // we use previous styles
- NSArray *sortedEndedStyles = [previousActiveStyles
- sortedArrayUsingDescriptors:@[ [NSSortDescriptor
- sortDescriptorWithKey:@"intValue"
- ascending:NO] ]];
-
- // append closing tags
- for (NSNumber *style in sortedEndedStyles) {
- if ([style isEqualToNumber:@([ImageStyle getStyleType])] ||
- [style isEqualToNumber:@([DividerStyle getStyleType])] ||
- [style isEqualToNumber:@([ContentStyle getStyleType])]) {
- continue;
- }
- NSString *tagContent =
- [self tagContentForStyle:style
- openingTag:NO
- location:currentRange.location];
- [result
- appendString:[NSString stringWithFormat:@"%@>", tagContent]];
- }
-
- // append closing paragraph tag
- if ([previousActiveStyles
- containsObject:@([UnorderedListStyle getStyleType])] ||
- [previousActiveStyles
- containsObject:@([OrderedListStyle getStyleType])] ||
- [previousActiveStyles containsObject:@([H1Style getStyleType])] ||
- [previousActiveStyles containsObject:@([H2Style getStyleType])] ||
- [previousActiveStyles containsObject:@([H3Style getStyleType])] ||
- [previousActiveStyles
- containsObject:@([BlockQuoteStyle getStyleType])] ||
- [previousActiveStyles
- containsObject:@([CodeBlockStyle getStyleType])] ||
- [previousActiveStyles
- containsObject:@([CheckBoxStyle getStyleType])]) {
- // do nothing, proper closing paragraph tags have been already
- // appended
- } else {
- [result appendString:@"
"];
- }
- }
-
- // clear the previous styles and valued trackers
- previousActiveStyles = [[NSSet alloc] init];
- previousColor = nil;
- isDivider = NO;
-
- // next character opens new paragraph
- newLine = YES;
- } else {
- // new line - open the paragraph
- if (newLine) {
- newLine = NO;
-
- // handle ending unordered list
- if (inUnorderedList &&
- ![currentActiveStyles
- containsObject:@([UnorderedListStyle getStyleType])]) {
- inUnorderedList = NO;
- [result appendString:@"\n"];
- }
- // handle ending ordered list
- if (inOrderedList &&
- ![currentActiveStyles
- containsObject:@([OrderedListStyle getStyleType])]) {
- inOrderedList = NO;
- [result appendString:@"\n"];
- }
- // handle ending blockquotes
- if (inBlockQuote &&
- ![currentActiveStyles
- containsObject:@([BlockQuoteStyle getStyleType])]) {
- inBlockQuote = NO;
- [result appendString:@"\n"];
- }
- // handle ending codeblock
- if (inCodeBlock &&
- ![currentActiveStyles
- containsObject:@([CodeBlockStyle getStyleType])]) {
- inCodeBlock = NO;
- [result appendString:@"\n"];
- }
-
- // handle starting unordered list
- if (!inUnorderedList &&
- [currentActiveStyles
- containsObject:@([UnorderedListStyle getStyleType])]) {
- inUnorderedList = YES;
- [result appendString:@"\n"];
- }
- // handle starting ordered list
- if (!inOrderedList &&
- [currentActiveStyles
- containsObject:@([OrderedListStyle getStyleType])]) {
- inOrderedList = YES;
- [result appendString:@"\n"];
- }
- // handle starting blockquotes
- if (!inBlockQuote &&
- [currentActiveStyles
- containsObject:@([BlockQuoteStyle getStyleType])]) {
- inBlockQuote = YES;
- [result appendString:@"\n"];
- }
- // handle starting codeblock
- if (!inCodeBlock &&
- [currentActiveStyles
- containsObject:@([CodeBlockStyle getStyleType])]) {
- inCodeBlock = YES;
- [result appendString:@"\n"];
- }
-
- if (isDivider && ![currentActiveStyles
- containsObject:@([DividerStyle getStyleType])]) {
- [result appendString:@"\n"];
- }
-
- if (!isDivider && [currentActiveStyles
- containsObject:@([DividerStyle getStyleType])]) {
- isDivider = YES;
- }
-
- // don't add the tag if some paragraph styles are present
- if ([currentActiveStyles
- containsObject:@([UnorderedListStyle getStyleType])] ||
- [currentActiveStyles
- containsObject:@([OrderedListStyle getStyleType])] ||
- [currentActiveStyles containsObject:@([H1Style getStyleType])] ||
- [currentActiveStyles containsObject:@([H2Style getStyleType])] ||
- [currentActiveStyles containsObject:@([H3Style getStyleType])] ||
- [currentActiveStyles containsObject:@([H4Style getStyleType])] ||
- [currentActiveStyles containsObject:@([H5Style getStyleType])] ||
- [currentActiveStyles containsObject:@([H6Style getStyleType])] ||
- [currentActiveStyles
- containsObject:@([BlockQuoteStyle getStyleType])] ||
- [currentActiveStyles
- containsObject:@([CodeBlockStyle getStyleType])] ||
- [currentActiveStyles
- containsObject:@([CheckBoxStyle getStyleType])] ||
- [currentActiveStyles
- containsObject:@([ContentStyle getStyleType])]) {
- [result appendString:@"\n"];
- } else if ([currentActiveStyles
- containsObject:@([DividerStyle getStyleType])]) {
-
- } else {
- [result appendString:@"\n
"];
- }
- }
-
- // get styles that have ended
- NSMutableSet *endedStyles =
- [previousActiveStyles mutableCopy];
- [endedStyles minusSet:currentActiveStyles];
-
- // also finish styles that should be ended because they are nested in a
- // style that ended
- NSMutableSet *fixedEndedStyles = [endedStyles mutableCopy];
- NSMutableSet *stylesToBeReAdded = [[NSMutableSet alloc] init];
-
- for (NSNumber *style in endedStyles) {
- NSInteger styleBeginning =
- [currentActiveStylesBeginning[style] integerValue];
-
- for (NSNumber *activeStyle in currentActiveStyles) {
- NSInteger activeStyleBeginning =
- [currentActiveStylesBeginning[activeStyle] integerValue];
-
- if ((activeStyleBeginning > styleBeginning &&
- activeStyleBeginning < i) ||
- (activeStyleBeginning == styleBeginning &&
- activeStyleBeginning<
- i && [activeStyle integerValue]>[style integerValue])) {
- [fixedEndedStyles addObject:activeStyle];
- [stylesToBeReAdded addObject:activeStyle];
- }
- }
- }
-
- // if a style begins but there is a style inner to it that is (and was
- // previously) active, it also should be closed and readded
-
- // newly added styles
- NSMutableSet *newStyles = [currentActiveStyles mutableCopy];
- [newStyles minusSet:previousActiveStyles];
- // styles that were and still are active
- NSMutableSet *stillActiveStyles = [previousActiveStyles mutableCopy];
- [stillActiveStyles intersectSet:currentActiveStyles];
-
- for (NSNumber *style in newStyles) {
- for (NSNumber *ongoingStyle in stillActiveStyles) {
- if ([ongoingStyle integerValue] > [style integerValue]) {
- // the prev style is inner; needs to be closed and re-added later
- [fixedEndedStyles addObject:ongoingStyle];
- [stylesToBeReAdded addObject:ongoingStyle];
- }
- }
- }
-
- // they are sorted in a descending order
- NSArray *sortedEndedStyles = [fixedEndedStyles
- sortedArrayUsingDescriptors:@[ [NSSortDescriptor
- sortDescriptorWithKey:@"intValue"
- ascending:NO] ]];
-
- // append closing tags
- for (NSNumber *style in sortedEndedStyles) {
- if ([style isEqualToNumber:@([ImageStyle getStyleType])] ||
- [style isEqualToNumber:@([DividerStyle getStyleType])] ||
- [style isEqualToNumber:@([ContentStyle getStyleType])]) {
- continue;
- }
- NSString *tagContent = [self tagContentForStyle:style
- openingTag:NO
- location:currentRange.location];
- [result appendString:[NSString stringWithFormat:@"%@>", tagContent]];
- }
-
- // all styles that have begun: new styles + the ones that need to be
- // re-added they are sorted in a ascending manner to properly keep tags'
- // FILO order
- [newStyles unionSet:stylesToBeReAdded];
-
- NSNumber *dividerType = @([DividerStyle getStyleType]);
- if ([newStyles containsObject:dividerType]) {
- // Close lists/blockquote/heading if currently open - hr should be
- // outside them
- if (inUnorderedList) {
- inUnorderedList = NO;
- [result appendString:@"\n
"];
- }
- if (inOrderedList) {
- inOrderedList = NO;
- [result appendString:@"\n"];
- }
- if (inBlockQuote) {
- inBlockQuote = NO;
- [result appendString:@"\n"];
- }
-
- // Close any open headings
- if ([previousActiveStyles containsObject:@([H1Style getStyleType])]) {
- [result appendString:@""];
- }
- if ([previousActiveStyles containsObject:@([H2Style getStyleType])]) {
- [result appendString:@""];
- }
- if ([previousActiveStyles containsObject:@([H3Style getStyleType])]) {
- [result appendString:@""];
- }
-
- // Close paragraph if inside a normal paragraph
- BOOL insideParagraphBlock =
- previousActiveStyles.count != 0 &&
- !([previousActiveStyles
- containsObject:@([UnorderedListStyle getStyleType])] ||
- [previousActiveStyles
- containsObject:@([OrderedListStyle getStyleType])] ||
- [previousActiveStyles containsObject:@([H1Style getStyleType])] ||
- [previousActiveStyles containsObject:@([H2Style getStyleType])] ||
- [previousActiveStyles containsObject:@([H3Style getStyleType])] ||
- [previousActiveStyles containsObject:@([H4Style getStyleType])] ||
- [previousActiveStyles containsObject:@([H5Style getStyleType])] ||
- [previousActiveStyles containsObject:@([H6Style getStyleType])] ||
- [previousActiveStyles
- containsObject:@([BlockQuoteStyle getStyleType])] ||
- [previousActiveStyles containsObject:dividerType]);
-
- if (insideParagraphBlock) {
- [result appendString:@""];
- }
-
- // Emit hr on its own line
- [result appendString:@"\n
"];
-
- // Remove divider from newStyles so it doesn't get treated as a normal
- // opening tag
- [newStyles removeObject:dividerType];
-
- // Reset parser state
- previousActiveStyles = [[NSSet alloc] init];
- isDivider = NO;
- newLine = YES;
- }
-
- // Now append remaining opening tags (except divider which we already
- // handled)
- if ([currentCharacterStr isEqualToString:@"\uFFFC"]) {
- ContentStyle *contentStyle =
- _input->stylesDict[@([ContentStyle getStyleType])];
- if (contentStyle != nil &&
- [currentActiveStyles
- containsObject:@([ContentStyle getStyleType])]) {
- // Close lists/blockquote/heading if currently open - content should
- // be outside them
- if (inUnorderedList) {
- inUnorderedList = NO;
- [result appendString:@"\n"];
- }
- if (inOrderedList) {
- inOrderedList = NO;
- [result appendString:@"\n"];
- }
- if (inBlockQuote) {
- inBlockQuote = NO;
- [result appendString:@"\n"];
- }
-
- // Close any open headings
- if ([previousActiveStyles containsObject:@([H1Style getStyleType])]) {
- [result appendString:@""];
- }
- if ([previousActiveStyles containsObject:@([H2Style getStyleType])]) {
- [result appendString:@""];
- }
- if ([previousActiveStyles containsObject:@([H3Style getStyleType])]) {
- [result appendString:@""];
- }
-
- // Close paragraph if inside a normal paragraph
- BOOL insideParagraphBlock =
- previousActiveStyles.count != 0 &&
- !([previousActiveStyles
- containsObject:@([UnorderedListStyle getStyleType])] ||
- [previousActiveStyles
- containsObject:@([OrderedListStyle getStyleType])] ||
- [previousActiveStyles
- containsObject:@([H1Style getStyleType])] ||
- [previousActiveStyles
- containsObject:@([H2Style getStyleType])] ||
- [previousActiveStyles
- containsObject:@([H3Style getStyleType])] ||
- [previousActiveStyles
- containsObject:@([BlockQuoteStyle getStyleType])]);
-
- if (insideParagraphBlock) {
- [result appendString:@""];
- }
-
- ContentParams *cParams =
- [contentStyle getContentParams:currentRange.location];
- if (cParams != nil) {
- NSString *tag =
- [self tagContentForStyle:@([ContentStyle getStyleType])
- openingTag:YES
- location:currentRange.location];
-
- [result appendFormat:@"<%@/>", tag];
- newLine = YES;
- previousActiveStyles = [[NSSet alloc] init];
- continue;
- }
- }
- }
-
- // Now append remaining opening tags
- NSArray *sortedNewStyles = [newStyles
- sortedArrayUsingDescriptors:@[ [NSSortDescriptor
- sortDescriptorWithKey:@"intValue"
- ascending:YES] ]];
-
- // append opening tags
- for (NSNumber *style in sortedNewStyles) {
- NSString *tagContent = [self tagContentForStyle:style
- openingTag:YES
- location:currentRange.location];
- if ([style isEqualToNumber:@([ImageStyle getStyleType])]) {
- [result
- appendString:[NSString stringWithFormat:@"<%@/>", tagContent]];
- [currentActiveStyles removeObject:@([ImageStyle getStyleType])];
- } else {
- [result appendString:[NSString stringWithFormat:@"<%@>", tagContent]];
- }
- }
-
- // append the letter and escape it if needed
- [result appendString:[NSString stringByEscapingHtml:currentCharacterStr]];
-
- // save current styles for next character's checks
- previousActiveStyles = currentActiveStyles;
-
- previousColor = currentColor;
- }
-
- // set last character
- lastCharacter = currentCharacterChar;
- }
-
- if (![[NSCharacterSet newlineCharacterSet] characterIsMember:lastCharacter]) {
- // not-newline character was last - finish the paragraph
- // close all pending tags
- NSArray *sortedEndedStyles = [previousActiveStyles
- sortedArrayUsingDescriptors:@[ [NSSortDescriptor
- sortDescriptorWithKey:@"intValue"
- ascending:NO] ]];
-
- // append closing tags
- for (NSNumber *style in sortedEndedStyles) {
- if ([style isEqualToNumber:@([ImageStyle getStyleType])] ||
- [style isEqualToNumber:@([DividerStyle getStyleType])] ||
- [style isEqualToNumber:@([ContentStyle getStyleType])]) {
- continue;
- }
- NSString *tagContent = [self
- tagContentForStyle:style
- openingTag:NO
- location:_input->textView.textStorage.string.length - 1];
- [result appendString:[NSString stringWithFormat:@"%@>", tagContent]];
- }
-
- // finish the paragraph
- // handle ending of some paragraph styles
- if ([previousActiveStyles
- containsObject:@([UnorderedListStyle getStyleType])]) {
- [result appendString:@"\n"];
- } else if ([previousActiveStyles
- containsObject:@([OrderedListStyle getStyleType])]) {
- [result appendString:@"\n"];
- } else if ([previousActiveStyles
- containsObject:@([BlockQuoteStyle getStyleType])]) {
- [result appendString:@"\n"];
- } else if ([previousActiveStyles
- containsObject:@([CodeBlockStyle getStyleType])]) {
- [result appendString:@"\n"];
- } else if ([previousActiveStyles
- containsObject:@([H1Style getStyleType])] ||
- [previousActiveStyles
- containsObject:@([H2Style getStyleType])] ||
- [previousActiveStyles
- containsObject:@([H3Style getStyleType])] ||
- [previousActiveStyles
- containsObject:@([H4Style getStyleType])] ||
- [previousActiveStyles
- containsObject:@([H5Style getStyleType])] ||
- [previousActiveStyles
- containsObject:@([H6Style getStyleType])] ||
- [previousActiveStyles
- containsObject:@([CheckBoxStyle getStyleType])]) {
- // do nothing, heading closing tag has already been appended
- } else if ([previousActiveStyles
- containsObject:@([DividerStyle getStyleType])]) {
- // do nothing, heading closing tag has already ben appended
- } else if ([previousActiveStyles
- containsObject:@([ContentStyle getStyleType])]) {
- // do nothing
- } else {
- [result appendString:@""];
- }
- } else {
- // newline character was last - some paragraph styles need to be closed
- if (inUnorderedList) {
- inUnorderedList = NO;
- [result appendString:@"\n"];
- }
- if (inOrderedList) {
- inOrderedList = NO;
- [result appendString:@"\n"];
- }
- if (inBlockQuote) {
- inBlockQuote = NO;
- [result appendString:@"\n"];
- }
- if (inCodeBlock) {
- inCodeBlock = NO;
- [result appendString:@"\n"];
- }
- }
-
- [result appendString:@"\n"];
-
- // remove zero width spaces in the very end
- NSRange resultRange = NSMakeRange(0, result.length);
- [result replaceOccurrencesOfString:@"\u200B"
- withString:@""
- options:0
- range:resultRange];
- return result;
-}
-
-- (NSString *)tagContentForStyle:(NSNumber *)style
- openingTag:(BOOL)openingTag
- location:(NSInteger)location {
- if ([style isEqualToNumber:@([BoldStyle getStyleType])]) {
- return @"b";
- } else if ([style isEqualToNumber:@([ItalicStyle getStyleType])]) {
- return @"i";
- } else if ([style isEqualToNumber:@([ImageStyle getStyleType])]) {
- if (openingTag) {
- ImageStyle *imageStyle =
- (ImageStyle *)_input->stylesDict[@([ImageStyle getStyleType])];
- if (imageStyle != nullptr) {
- ImageData *data = [imageStyle getImageDataAt:location];
- if (data != nullptr && data.uri != nullptr) {
- return [NSString
- stringWithFormat:@"img src=\"%@\" width=\"%f\" height=\"%f\"",
- data.uri, data.width, data.height];
- }
- }
- return @"img";
- } else {
- return @"";
- }
- } else if ([style isEqualToNumber:@([UnderlineStyle getStyleType])]) {
- return @"u";
- } else if ([style isEqualToNumber:@([StrikethroughStyle getStyleType])]) {
- return @"s";
- } else if ([style isEqualToNumber:@([ColorStyle getStyleType])]) {
- if (openingTag) {
- ColorStyle *colorSpan = _input->stylesDict[@([ColorStyle getStyleType])];
- UIColor *color = [colorSpan getColorAt:location];
- if (color) {
- NSString *hex = [color hexString];
- return [NSString stringWithFormat:@"font color=\"%@\"", hex];
- };
- } else {
- return @"font";
- }
- } else if ([style isEqualToNumber:@([InlineCodeStyle getStyleType])]) {
- return @"code";
- } else if ([style isEqualToNumber:@([LinkStyle getStyleType])]) {
- if (openingTag) {
- LinkStyle *linkStyle =
- (LinkStyle *)_input->stylesDict[@([LinkStyle getStyleType])];
- if (linkStyle != nullptr) {
- LinkData *data = [linkStyle getLinkDataAt:location];
- if (data != nullptr && data.url != nullptr) {
- return [NSString stringWithFormat:@"a href=\"%@\"", data.url];
- }
- }
- return @"a";
- } else {
- return @"a";
- }
- } else if ([style isEqualToNumber:@([MentionStyle getStyleType])]) {
- if (openingTag) {
- MentionStyle *mentionStyle =
- (MentionStyle *)_input->stylesDict[@([MentionStyle getStyleType])];
- if (mentionStyle != nullptr) {
- MentionParams *params = [mentionStyle getMentionParamsAt:location];
- // attributes can theoretically be nullptr
- if (params != nullptr && params.indicator != nullptr &&
- params.text != nullptr) {
- NSMutableString *attrsStr =
- [[NSMutableString alloc] initWithString:@""];
- if (params.attributes != nullptr) {
- // turn attributes to Data and then into dict
- NSData *attrsData =
- [params.attributes dataUsingEncoding:NSUTF8StringEncoding];
- NSError *jsonError;
- NSDictionary *json =
- [NSJSONSerialization JSONObjectWithData:attrsData
- options:0
- error:&jsonError];
- // format dict keys and values into string
- [json enumerateKeysAndObjectsUsingBlock:^(
- id _Nonnull key, id _Nonnull obj, BOOL *_Nonnull stop) {
- [attrsStr
- appendString:[NSString stringWithFormat:@" %@=\"%@\"",
- (NSString *)key,
- (NSString *)obj]];
- }];
- }
- return [NSString
- stringWithFormat:@"mention text=\"%@\" indicator=\"%@\"%@",
- params.text, params.indicator, attrsStr];
- }
- }
- return @"mention";
- } else {
- return @"mention";
- }
- } else if ([style isEqualToNumber:@([DividerStyle getStyleType])]) {
- return @"hr";
- } else if ([style isEqualToNumber:@([ContentStyle getStyleType])]) {
- ContentStyle *contentStyle =
- (ContentStyle *)_input->stylesDict[@([ContentStyle getStyleType])];
- if (openingTag) {
- if (contentStyle != nil) {
- ContentParams *params = [contentStyle getContentParams:location];
- NSMutableString *attrsStr =
- [[NSMutableString alloc] initWithString:@""];
- if (params != nil) {
- NSData *attrsData =
- [params.attributes dataUsingEncoding:NSUTF8StringEncoding];
- NSError *jsonError;
- NSDictionary *json =
- [NSJSONSerialization JSONObjectWithData:attrsData
- options:0
- error:&jsonError];
- [json enumerateKeysAndObjectsUsingBlock:^(
- id _Nonnull key, id _Nonnull obj, BOOL *_Nonnull stop) {
- [attrsStr appendString:[NSString stringWithFormat:@" %@=\"%@\"",
- (NSString *)key,
- (NSString *)obj]];
- }];
- }
- return [NSString stringWithFormat:@"content text=\"%@\" type=\"%@\"%@",
- params.text, params.type, attrsStr];
- }
- return @"content";
- } else {
- // self-closing: no trailing tag name needed
- return @"";
- }
- } else if ([style isEqualToNumber:@([H1Style getStyleType])]) {
- return @"h1";
- } else if ([style isEqualToNumber:@([H2Style getStyleType])]) {
- return @"h2";
- } else if ([style isEqualToNumber:@([H3Style getStyleType])]) {
- return @"h3";
- } else if ([style isEqualToNumber:@([H4Style getStyleType])]) {
- return @"h4";
- } else if ([style isEqualToNumber:@([H5Style getStyleType])]) {
- return @"h5";
- } else if ([style isEqualToNumber:@([H6Style getStyleType])]) {
- return @"h6";
- } else if ([style isEqualToNumber:@([UnorderedListStyle getStyleType])] ||
- [style isEqualToNumber:@([OrderedListStyle getStyleType])]) {
- return @"li";
- } else if ([style isEqualToNumber:@([BlockQuoteStyle getStyleType])] ||
- [style isEqualToNumber:@([CodeBlockStyle getStyleType])]) {
- // blockquotes and codeblock use tags the same way lists use
- return @"p";
- } else if ([style isEqualToNumber:@([CheckBoxStyle getStyleType])]) {
- if (openingTag) {
- CheckBoxStyle *checkBoxStyle =
- _input->stylesDict[@([CheckBoxStyle getStyleType])];
- if (checkBoxStyle) {
- BOOL isChecked = [checkBoxStyle isCheckedAt:location];
- NSString *checkedStr = isChecked ? @"true" : @"false";
- return
- [NSString stringWithFormat:@"checklist checked=\"%@\"", checkedStr];
- }
- }
-
- return @"checklist";
- }
- return @"";
+ return [_htmlParser buildHtmlFromAttributedString:sub pretify:YES];
}
- (void)replaceWholeFromHtml:(NSString *_Nonnull)html {
diff --git a/ios/styles/BlockQuoteStyle.mm b/ios/styles/BlockQuoteStyle.mm
index f8d039cd9..bd024a6ec 100644
--- a/ios/styles/BlockQuoteStyle.mm
+++ b/ios/styles/BlockQuoteStyle.mm
@@ -18,6 +18,22 @@ + (BOOL)isParagraphStyle {
return YES;
}
++ (const char *)tagName {
+ return "blockquote";
+}
+
++ (const char *)subTagName {
+ return "p";
+}
+
++ (BOOL)isSelfClosing {
+ return NO;
+}
+
++ (NSAttributedStringKey)attributeKey {
+ return NSParagraphStyleAttributeName;
+}
+
- (instancetype)initWithInput:(id)input {
self = [super init];
_input = (EnrichedTextInputView *)input;
@@ -186,7 +202,7 @@ - (BOOL)handleBackspaceInRange:(NSRange)range replacementText:(NSString *)text {
return NO;
}
-- (BOOL)styleCondition:(id _Nullable)value:(NSRange)range {
+- (BOOL)styleCondition:(id _Nullable)value range:(NSRange)range {
NSParagraphStyle *pStyle = (NSParagraphStyle *)value;
return pStyle != nullptr && pStyle.headIndent == [self getHeadIndent] &&
pStyle.firstLineHeadIndent == [self getHeadIndent] &&
@@ -199,7 +215,7 @@ - (BOOL)detectStyle:(NSRange)range {
withInput:_input
inRange:range
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
} else {
return [OccurenceUtils detect:NSParagraphStyleAttributeName
@@ -207,7 +223,7 @@ - (BOOL)detectStyle:(NSRange)range {
atIndex:range.location
checkPrevious:YES
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
}
}
@@ -217,7 +233,7 @@ - (BOOL)anyOccurence:(NSRange)range {
withInput:_input
inRange:range
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
}
@@ -226,7 +242,7 @@ - (BOOL)anyOccurence:(NSRange)range {
withInput:_input
inRange:range
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
}
diff --git a/ios/styles/BoldStyle.mm b/ios/styles/BoldStyle.mm
index 1afe0d609..d969c2fb0 100644
--- a/ios/styles/BoldStyle.mm
+++ b/ios/styles/BoldStyle.mm
@@ -15,6 +15,22 @@ + (BOOL)isParagraphStyle {
return NO;
}
++ (const char *)tagName {
+ return "b";
+}
+
++ (NSAttributedStringKey)attributeKey {
+ return NSFontAttributeName;
+}
+
++ (const char *)subTagName {
+ return nil;
+}
+
++ (BOOL)isSelfClosing {
+ return NO;
+}
+
- (instancetype)initWithInput:(id)input {
self = [super init];
_input = (EnrichedTextInputView *)input;
@@ -123,7 +139,7 @@ - (BOOL)boldHeadingConflictsInRange:(NSRange)range type:(StyleType)type {
: [headingStyle detectStyle:range];
}
-- (BOOL)styleCondition:(id _Nullable)value:(NSRange)range {
+- (BOOL)styleCondition:(id _Nullable)value range:(NSRange)range {
UIFont *font = (UIFont *)value;
return font != nullptr && [font isBold] &&
![self boldHeadingConflictsInRange:range type:H1] &&
@@ -140,7 +156,7 @@ - (BOOL)detectStyle:(NSRange)range {
withInput:_input
inRange:range
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
} else {
return [OccurenceUtils detect:NSFontAttributeName
@@ -148,7 +164,7 @@ - (BOOL)detectStyle:(NSRange)range {
atIndex:range.location
checkPrevious:NO
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
}
}
@@ -158,7 +174,7 @@ - (BOOL)anyOccurence:(NSRange)range {
withInput:_input
inRange:range
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
}
@@ -167,7 +183,7 @@ - (BOOL)anyOccurence:(NSRange)range {
withInput:_input
inRange:range
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
}
diff --git a/ios/styles/CodeBlockStyle.mm b/ios/styles/CodeBlockStyle.mm
index 1f5aa9976..7b3172cfb 100644
--- a/ios/styles/CodeBlockStyle.mm
+++ b/ios/styles/CodeBlockStyle.mm
@@ -6,6 +6,8 @@
#import "StyleHeaders.h"
#import "TextInsertionUtils.h"
+static NSString *const CodeBlockMarker = @"codeblock";
+
@implementation CodeBlockStyle {
EnrichedTextInputView *_input;
NSArray *_stylesToExclude;
@@ -19,6 +21,22 @@ + (BOOL)isParagraphStyle {
return YES;
}
++ (const char *)tagName {
+ return "codeblock";
+}
+
++ (const char *)subTagName {
+ return "p";
+}
+
++ (NSAttributedStringKey)attributeKey {
+ return NSParagraphStyleAttributeName;
+}
+
++ (BOOL)isSelfClosing {
+ return NO;
+}
+
- (instancetype)initWithInput:(id)input {
self = [super init];
_input = (EnrichedTextInputView *)input;
@@ -38,7 +56,7 @@ - (void)applyStyle:(NSRange)range {
- (void)addAttributes:(NSRange)range withTypingAttr:(BOOL)withTypingAttr {
NSTextList *codeBlockList =
- [[NSTextList alloc] initWithMarkerFormat:@"codeblock" options:0];
+ [[NSTextList alloc] initWithMarkerFormat:CodeBlockMarker options:0];
NSArray *paragraphs =
[ParagraphsUtils getSeparateParagraphsRangesIn:_input->textView
range:range];
@@ -177,11 +195,10 @@ - (BOOL)handleBackspaceInRange:(NSRange)range replacementText:(NSString *)text {
return NO;
}
-- (BOOL)styleCondition:(id _Nullable)value:(NSRange)range {
+- (BOOL)styleCondition:(id _Nullable)value range:(NSRange)range {
NSParagraphStyle *paragraph = (NSParagraphStyle *)value;
return paragraph != nullptr && paragraph.textLists.count == 1 &&
- [paragraph.textLists.firstObject.markerFormat
- isEqualToString:@"codeblock"];
+ paragraph.textLists.firstObject.markerFormat == CodeBlockMarker;
}
- (BOOL)detectStyle:(NSRange)range {
@@ -190,7 +207,7 @@ - (BOOL)detectStyle:(NSRange)range {
withInput:_input
inRange:range
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
} else {
return [OccurenceUtils detect:NSParagraphStyleAttributeName
@@ -198,7 +215,7 @@ - (BOOL)detectStyle:(NSRange)range {
atIndex:range.location
checkPrevious:YES
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
}
}
@@ -208,7 +225,7 @@ - (BOOL)anyOccurence:(NSRange)range {
withInput:_input
inRange:range
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
}
@@ -217,7 +234,7 @@ - (BOOL)anyOccurence:(NSRange)range {
withInput:_input
inRange:range
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
}
diff --git a/ios/styles/H1Style.mm b/ios/styles/H1Style.mm
index ca22c1154..409077ceb 100644
--- a/ios/styles/H1Style.mm
+++ b/ios/styles/H1Style.mm
@@ -8,6 +8,9 @@ + (StyleType)getStyleType {
+ (BOOL)isParagraphStyle {
return YES;
}
++ (const char *)tagName {
+ return "h1";
+}
- (CGFloat)getHeadingFontSize {
return [((EnrichedTextInputView *)input)->config h1FontSize];
}
diff --git a/ios/styles/H2Style.mm b/ios/styles/H2Style.mm
index a943b96a7..3efab05ec 100644
--- a/ios/styles/H2Style.mm
+++ b/ios/styles/H2Style.mm
@@ -8,6 +8,9 @@ + (StyleType)getStyleType {
+ (BOOL)isParagraphStyle {
return YES;
}
++ (const char *)tagName {
+ return "h2";
+}
- (CGFloat)getHeadingFontSize {
return [((EnrichedTextInputView *)input)->config h2FontSize];
}
diff --git a/ios/styles/H3Style.mm b/ios/styles/H3Style.mm
index e6c5765b3..295c2cdb5 100644
--- a/ios/styles/H3Style.mm
+++ b/ios/styles/H3Style.mm
@@ -8,6 +8,9 @@ + (StyleType)getStyleType {
+ (BOOL)isParagraphStyle {
return YES;
}
++ (const char *)tagName {
+ return "h3";
+}
- (CGFloat)getHeadingFontSize {
return [((EnrichedTextInputView *)input)->config h3FontSize];
}
diff --git a/ios/styles/HeadingStyleBase.mm b/ios/styles/HeadingStyleBase.mm
index 7727af8c7..af29b2766 100644
--- a/ios/styles/HeadingStyleBase.mm
+++ b/ios/styles/HeadingStyleBase.mm
@@ -17,6 +17,18 @@ - (BOOL)isHeadingBold {
return false;
}
++ (const char *)subTagName {
+ return nil;
+}
+
++ (NSAttributedStringKey)attributeKey {
+ return NSFontAttributeName;
+}
+
++ (BOOL)isSelfClosing {
+ return NO;
+}
+
- (EnrichedTextInputView *)typedInput {
return (EnrichedTextInputView *)input;
}
@@ -98,7 +110,7 @@ - (void)removeAttributes:(NSRange)range {
options:0
usingBlock:^(id _Nullable value, NSRange range,
BOOL *_Nonnull stop) {
- if ([self styleCondition:value:range]) {
+ if ([self styleCondition:value range:range]) {
UIFont *newFont = [(UIFont *)value
setSize:[[[self typedInput]->config primaryFontSize]
floatValue]];
@@ -137,7 +149,7 @@ - (void)removeTypingAttributes {
[self removeAttributes:[self typedInput]->textView.selectedRange];
}
-- (BOOL)styleCondition:(id _Nullable)value:(NSRange)range {
+- (BOOL)styleCondition:(id _Nullable)value range:(NSRange)range {
UIFont *font = (UIFont *)value;
return font != nullptr && font.pointSize == [self getHeadingFontSize];
}
@@ -148,7 +160,7 @@ - (BOOL)detectStyle:(NSRange)range {
withInput:[self typedInput]
inRange:range
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
} else {
return [OccurenceUtils detect:NSFontAttributeName
@@ -156,7 +168,7 @@ - (BOOL)detectStyle:(NSRange)range {
atIndex:range.location
checkPrevious:YES
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
}
}
@@ -166,7 +178,7 @@ - (BOOL)anyOccurence:(NSRange)range {
withInput:[self typedInput]
inRange:range
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
}
@@ -175,7 +187,7 @@ - (BOOL)anyOccurence:(NSRange)range {
withInput:[self typedInput]
inRange:range
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
}
diff --git a/ios/styles/ImageStyle.mm b/ios/styles/ImageStyle.mm
index 2470d826d..6725fef41 100644
--- a/ios/styles/ImageStyle.mm
+++ b/ios/styles/ImageStyle.mm
@@ -18,6 +18,34 @@ + (BOOL)isParagraphStyle {
return NO;
}
++ (NSAttributedStringKey)attributeKey {
+ return ImageAttributeName;
+}
+
++ (const char *)tagName {
+ return "img";
+}
+
++ (NSString *)subTagName {
+ return nil;
+}
+
++ (BOOL)isSelfClosing {
+ return YES;
+}
+
++ (NSDictionary *)getParametersFromValue:(id)value {
+ ImageData *img = (ImageData *)value;
+ if (!img)
+ return nil;
+
+ return @{
+ @"src" : img.uri ?: @"",
+ @"width" : [NSString stringWithFormat:@"%f", img.width],
+ @"height" : [NSString stringWithFormat:@"%f", img.height]
+ };
+}
+
- (instancetype)initWithInput:(id)input {
self = [super init];
_input = (EnrichedTextInputView *)input;
@@ -52,7 +80,7 @@ - (void)removeTypingAttributes {
_input->textView.typingAttributes = currentAttributes;
}
-- (BOOL)styleCondition:(id _Nullable)value:(NSRange)range {
+- (BOOL)styleCondition:(id _Nullable)value range:(NSRange)range {
return [value isKindOfClass:[ImageData class]];
}
@@ -61,7 +89,7 @@ - (BOOL)anyOccurence:(NSRange)range {
withInput:_input
inRange:range
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
}
@@ -71,7 +99,7 @@ - (BOOL)detectStyle:(NSRange)range {
withInput:_input
inRange:range
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
} else {
return [OccurenceUtils detect:ImageAttributeName
@@ -79,7 +107,7 @@ - (BOOL)detectStyle:(NSRange)range {
atIndex:range.location
checkPrevious:YES
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
}
}
@@ -89,7 +117,7 @@ - (BOOL)detectStyle:(NSRange)range {
withInput:_input
inRange:range
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
}
diff --git a/ios/styles/InlineCodeStyle.mm b/ios/styles/InlineCodeStyle.mm
index 3bc7d0ab8..fe1ba8ffc 100644
--- a/ios/styles/InlineCodeStyle.mm
+++ b/ios/styles/InlineCodeStyle.mm
@@ -17,6 +17,22 @@ + (BOOL)isParagraphStyle {
return NO;
}
++ (const char *)tagName {
+ return "code";
+}
+
++ (NSString *)subTagName {
+ return nil;
+}
+
++ (NSAttributedStringKey)attributeKey {
+ return NSBackgroundColorAttributeName;
+}
+
++ (BOOL)isSelfClosing {
+ return NO;
+}
+
- (instancetype)initWithInput:(id)input {
self = [super init];
_input = (EnrichedTextInputView *)input;
@@ -162,7 +178,7 @@ - (void)handleNewlines {
[_input->textView.textStorage attribute:NSBackgroundColorAttributeName
atIndex:i
effectiveRange:&mockRange];
- if ([self styleCondition:bgColor:NSMakeRange(i, 1)]) {
+ if ([self styleCondition:bgColor range:NSMakeRange(i, 1)]) {
[self removeAttributes:NSMakeRange(i, 1)];
}
}
@@ -171,7 +187,7 @@ - (void)handleNewlines {
// emojis don't retain monospace font attribute so we check for the background
// color if there is no mention
-- (BOOL)styleCondition:(id _Nullable)value:(NSRange)range {
+- (BOOL)styleCondition:(id _Nullable)value range:(NSRange)range {
UIColor *bgColor = (UIColor *)value;
MentionStyle *mStyle = _input->stylesDict[@([MentionStyle getStyleType])];
return bgColor != nullptr && mStyle != nullptr && ![mStyle detectStyle:range];
@@ -194,7 +210,7 @@ - (BOOL)detectStyle:(NSRange)range {
withInput:_input
inRange:currentRange
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
detected = detected && currentDetected;
}
@@ -206,7 +222,7 @@ - (BOOL)detectStyle:(NSRange)range {
atIndex:range.location
checkPrevious:NO
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
}
}
@@ -216,7 +232,7 @@ - (BOOL)anyOccurence:(NSRange)range {
withInput:_input
inRange:range
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
}
@@ -225,7 +241,7 @@ - (BOOL)anyOccurence:(NSRange)range {
withInput:_input
inRange:range
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
}
diff --git a/ios/styles/ItalicStyle.mm b/ios/styles/ItalicStyle.mm
index 82ddf7072..789ba2f57 100644
--- a/ios/styles/ItalicStyle.mm
+++ b/ios/styles/ItalicStyle.mm
@@ -15,6 +15,22 @@ + (BOOL)isParagraphStyle {
return NO;
}
++ (const char *)tagName {
+ return "i";
+}
+
++ (const char *)subTagName {
+ return nil;
+}
+
++ (NSAttributedStringKey)attributeKey {
+ return NSFontAttributeName;
+}
+
++ (BOOL)isSelfClosing {
+ return NO;
+}
+
- (instancetype)initWithInput:(id)input {
self = [super init];
_input = (EnrichedTextInputView *)input;
@@ -91,7 +107,7 @@ - (void)removeTypingAttributes {
}
}
-- (BOOL)styleCondition:(id _Nullable)value:(NSRange)range {
+- (BOOL)styleCondition:(id _Nullable)value range:(NSRange)range {
UIFont *font = (UIFont *)value;
return font != nullptr && [font isItalic];
}
@@ -102,7 +118,7 @@ - (BOOL)detectStyle:(NSRange)range {
withInput:_input
inRange:range
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
} else {
return [OccurenceUtils detect:NSFontAttributeName
@@ -110,7 +126,7 @@ - (BOOL)detectStyle:(NSRange)range {
atIndex:range.location
checkPrevious:NO
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
}
}
@@ -120,7 +136,7 @@ - (BOOL)anyOccurence:(NSRange)range {
withInput:_input
inRange:range
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
}
@@ -129,7 +145,7 @@ - (BOOL)anyOccurence:(NSRange)range {
withInput:_input
inRange:range
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
}
diff --git a/ios/styles/LinkStyle.mm b/ios/styles/LinkStyle.mm
index c9da3fc54..3ed18a274 100644
--- a/ios/styles/LinkStyle.mm
+++ b/ios/styles/LinkStyle.mm
@@ -10,6 +10,8 @@
static NSString *const ManualLinkAttributeName = @"ManualLinkAttributeName";
static NSString *const AutomaticLinkAttributeName =
@"AutomaticLinkAttributeName";
+// custom NSAttributedStringKey to differentiate the link during html creation
+static NSString *const LinkAttributeName = @"LinkAttributeName";
@implementation LinkStyle {
EnrichedTextInputView *_input;
@@ -23,6 +25,29 @@ + (BOOL)isParagraphStyle {
return NO;
}
++ (const char *)tagName {
+ return "a";
+}
+
++ (const char *)subTagName {
+ return nil;
+}
+
++ (NSAttributedStringKey)attributeKey {
+ return LinkAttributeName;
+}
+
++ (NSDictionary *)getParametersFromValue:(id)value {
+ NSString *url = value;
+ if (!url)
+ return nil;
+ return @{@"href" : url};
+}
+
++ (BOOL)isSelfClosing {
+ return NO;
+}
+
- (instancetype)initWithInput:(id)input {
self = [super init];
_input = (EnrichedTextInputView *)input;
@@ -52,6 +77,8 @@ - (void)removeAttributes:(NSRange)range {
range:linkRange];
[_input->textView.textStorage removeAttribute:AutomaticLinkAttributeName
range:linkRange];
+ [_input->textView.textStorage removeAttribute:LinkAttributeName
+ range:linkRange];
[_input->textView.textStorage addAttribute:NSForegroundColorAttributeName
value:[_input->config primaryColor]
range:linkRange];
@@ -92,6 +119,8 @@ - (void)removeTypingAttributes {
range:linkRange];
[_input->textView.textStorage removeAttribute:AutomaticLinkAttributeName
range:linkRange];
+ [_input->textView.textStorage removeAttribute:LinkAttributeName
+ range:linkRange];
[_input->textView.textStorage addAttribute:NSForegroundColorAttributeName
value:[_input->config primaryColor]
range:linkRange];
@@ -121,7 +150,7 @@ - (void)removeTypingAttributes {
_input->textView.typingAttributes = newTypingAttrs;
}
-- (BOOL)styleCondition:(id _Nullable)value:(NSRange)range {
+- (BOOL)styleCondition:(id _Nullable)value range:(NSRange)range {
NSString *linkValue = (NSString *)value;
return linkValue != nullptr;
}
@@ -133,7 +162,7 @@ - (BOOL)detectStyle:(NSRange)range {
withInput:_input
inRange:range
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
return onlyLinks ? [self isSingleLinkIn:range] : NO;
} else {
@@ -147,7 +176,7 @@ - (BOOL)anyOccurence:(NSRange)range {
withInput:_input
inRange:range
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
}
@@ -157,7 +186,7 @@ - (BOOL)anyOccurence:(NSRange)range {
withInput:_input
inRange:range
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
}
@@ -175,6 +204,7 @@ - (void)addLink:(NSString *)text
newAttrs[NSForegroundColorAttributeName] = [_input->config linkColor];
newAttrs[NSUnderlineColorAttributeName] = [_input->config linkColor];
newAttrs[NSStrikethroughColorAttributeName] = [_input->config linkColor];
+ newAttrs[LinkAttributeName] = [url copy];
if ([_input->config linkDecorationLine] == DecorationUnderline) {
newAttrs[NSUnderlineStyleAttributeName] = @(NSUnderlineStyleSingle);
}
@@ -505,6 +535,9 @@ - (void)handleManualLinks:(NSString *)word inRange:(NSRange)wordRange {
[_input->textView.textStorage addAttribute:ManualLinkAttributeName
value:manualLinkMinValue
range:newRange];
+ [_input->textView.textStorage addAttribute:LinkAttributeName
+ value:manualLinkMinValue
+ range:newRange];
}
// link typing attributes need to be fixed after these changes
@@ -551,14 +584,14 @@ - (void)removeConnectedLinksIfNeeded:(NSString *)word range:(NSRange)wordRange {
withInput:_input
inRange:wordRange
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
BOOL anyManual =
[OccurenceUtils any:ManualLinkAttributeName
withInput:_input
inRange:wordRange
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
// both manual and automatic links are somewhere - delete!
@@ -574,7 +607,7 @@ - (void)removeConnectedLinksIfNeeded:(NSString *)word range:(NSRange)wordRange {
withInput:_input
inRange:wordRange
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
// only one link might be present!
diff --git a/ios/styles/MentionStyle.mm b/ios/styles/MentionStyle.mm
index a60b687fb..e99ac139f 100644
--- a/ios/styles/MentionStyle.mm
+++ b/ios/styles/MentionStyle.mm
@@ -24,6 +24,44 @@ + (BOOL)isParagraphStyle {
return NO;
}
++ (const char *)tagName {
+ return "mention";
+}
+
++ (const char *)subTagName {
+ return nil;
+}
+
++ (NSAttributedStringKey)attributeKey {
+ return MentionAttributeName;
+}
+
++ (NSDictionary *)getParametersFromValue:(id)value {
+ MentionParams *mp = value;
+ if (!mp)
+ return nil;
+
+ NSMutableDictionary *params =
+ [@{@"text" : mp.text ?: @"", @"indicator" : mp.indicator ?: @""}
+ mutableCopy];
+
+ if (mp.attributes) {
+ NSData *data = [mp.attributes dataUsingEncoding:NSUTF8StringEncoding];
+ NSDictionary *extraAttrs = [NSJSONSerialization JSONObjectWithData:data
+ options:0
+ error:nil];
+ if ([extraAttrs isKindOfClass:[NSDictionary class]]) {
+ [params addEntriesFromDictionary:extraAttrs];
+ }
+ }
+
+ return params;
+}
+
++ (BOOL)isSelfClosing {
+ return NO;
+}
+
- (instancetype)initWithInput:(id)input {
self = [super init];
_input = (EnrichedTextInputView *)input;
@@ -136,7 +174,7 @@ - (void)removeTypingAttributes {
_input->textView.typingAttributes = newTypingAttrs;
}
-- (BOOL)styleCondition:(id _Nullable)value:(NSRange)range {
+- (BOOL)styleCondition:(id _Nullable)value range:(NSRange)range {
MentionParams *params = (MentionParams *)value;
return params != nullptr;
}
@@ -147,7 +185,7 @@ - (BOOL)detectStyle:(NSRange)range {
withInput:_input
inRange:range
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
} else {
return [self getMentionParamsAt:range.location] != nullptr;
@@ -159,7 +197,7 @@ - (BOOL)anyOccurence:(NSRange)range {
withInput:_input
inRange:range
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
}
@@ -168,7 +206,7 @@ - (BOOL)anyOccurence:(NSRange)range {
withInput:_input
inRange:range
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
}
diff --git a/ios/styles/OrderedListStyle.mm b/ios/styles/OrderedListStyle.mm
index d9c918efa..b0154474f 100644
--- a/ios/styles/OrderedListStyle.mm
+++ b/ios/styles/OrderedListStyle.mm
@@ -17,6 +17,22 @@ + (BOOL)isParagraphStyle {
return YES;
}
++ (const char *)tagName {
+ return "ol";
+}
+
++ (const char *)subTagName {
+ return "li";
+}
+
++ (BOOL)isSelfClosing {
+ return NO;
+}
+
++ (NSAttributedStringKey)attributeKey {
+ return NSParagraphStyleAttributeName;
+}
+
- (CGFloat)getHeadIndent {
// lists are drawn manually
// margin before marker + gap between marker and paragraph
@@ -235,7 +251,7 @@ - (BOOL)tryHandlingListShorcutInRange:(NSRange)range
return NO;
}
-- (BOOL)styleCondition:(id _Nullable)value:(NSRange)range {
+- (BOOL)styleCondition:(id _Nullable)value range:(NSRange)range {
NSParagraphStyle *paragraph = (NSParagraphStyle *)value;
return paragraph != nullptr && paragraph.textLists.count == 1 &&
paragraph.textLists.firstObject.markerFormat ==
@@ -248,7 +264,7 @@ - (BOOL)detectStyle:(NSRange)range {
withInput:_input
inRange:range
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
} else {
return [OccurenceUtils detect:NSParagraphStyleAttributeName
@@ -256,7 +272,7 @@ - (BOOL)detectStyle:(NSRange)range {
atIndex:range.location
checkPrevious:YES
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
}
}
@@ -266,7 +282,7 @@ - (BOOL)anyOccurence:(NSRange)range {
withInput:_input
inRange:range
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
}
@@ -275,7 +291,7 @@ - (BOOL)anyOccurence:(NSRange)range {
withInput:_input
inRange:range
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
}
diff --git a/ios/styles/StrikethroughStyle.mm b/ios/styles/StrikethroughStyle.mm
index 9cf96d34a..6decc6cd9 100644
--- a/ios/styles/StrikethroughStyle.mm
+++ b/ios/styles/StrikethroughStyle.mm
@@ -14,6 +14,22 @@ + (BOOL)isParagraphStyle {
return NO;
}
++ (const char *)tagName {
+ return "s";
+}
+
++ (const char *)subTagName {
+ return nil;
+}
+
++ (NSAttributedStringKey)attributeKey {
+ return NSStrikethroughStyleAttributeName;
+}
+
++ (BOOL)isSelfClosing {
+ return NO;
+}
+
- (instancetype)initWithInput:(id)input {
self = [super init];
_input = (EnrichedTextInputView *)input;
@@ -56,10 +72,11 @@ - (void)removeTypingAttributes {
_input->textView.typingAttributes = newTypingAttrs;
}
-- (BOOL)styleCondition:(id _Nullable)value:(NSRange)range {
+- (BOOL)styleCondition:(id _Nullable)value range:(NSRange)range {
+ if (!value)
+ return NO;
NSNumber *strikethroughStyle = (NSNumber *)value;
- return strikethroughStyle != nullptr &&
- [strikethroughStyle intValue] != NSUnderlineStyleNone;
+ return [strikethroughStyle intValue] != NSUnderlineStyleNone;
}
- (BOOL)detectStyle:(NSRange)range {
@@ -68,7 +85,7 @@ - (BOOL)detectStyle:(NSRange)range {
withInput:_input
inRange:range
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
} else {
return [OccurenceUtils detect:NSStrikethroughStyleAttributeName
@@ -76,7 +93,7 @@ - (BOOL)detectStyle:(NSRange)range {
atIndex:range.location
checkPrevious:NO
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
}
}
@@ -86,7 +103,7 @@ - (BOOL)anyOccurence:(NSRange)range {
withInput:_input
inRange:range
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
}
@@ -95,7 +112,7 @@ - (BOOL)anyOccurence:(NSRange)range {
withInput:_input
inRange:range
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
}
diff --git a/ios/styles/UnderlineStyle.mm b/ios/styles/UnderlineStyle.mm
index 98050475d..da331775d 100644
--- a/ios/styles/UnderlineStyle.mm
+++ b/ios/styles/UnderlineStyle.mm
@@ -14,6 +14,22 @@ + (BOOL)isParagraphStyle {
return NO;
}
++ (const char *)tagName {
+ return "u";
+}
+
++ (const char *)subTagName {
+ return nil;
+}
+
++ (NSAttributedStringKey)attributeKey {
+ return NSUnderlineStyleAttributeName;
+}
+
++ (BOOL)isSelfClosing {
+ return NO;
+}
+
- (instancetype)initWithInput:(id)input {
self = [super init];
_input = (EnrichedTextInputView *)input;
@@ -91,7 +107,7 @@ - (BOOL)underlinedMentionConflictsInRange:(NSRange)range {
return conflicted;
}
-- (BOOL)styleCondition:(id _Nullable)value:(NSRange)range {
+- (BOOL)styleCondition:(id _Nullable)value range:(NSRange)range {
NSNumber *underlineStyle = (NSNumber *)value;
return underlineStyle != nullptr &&
[underlineStyle intValue] != NSUnderlineStyleNone &&
@@ -105,7 +121,7 @@ - (BOOL)detectStyle:(NSRange)range {
withInput:_input
inRange:range
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
} else {
return [OccurenceUtils detect:NSUnderlineStyleAttributeName
@@ -113,7 +129,7 @@ - (BOOL)detectStyle:(NSRange)range {
atIndex:range.location
checkPrevious:NO
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
}
}
@@ -123,7 +139,7 @@ - (BOOL)anyOccurence:(NSRange)range {
withInput:_input
inRange:range
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
}
@@ -132,7 +148,7 @@ - (BOOL)anyOccurence:(NSRange)range {
withInput:_input
inRange:range
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
}
diff --git a/ios/styles/UnorderedListStyle.mm b/ios/styles/UnorderedListStyle.mm
index 9fb2f7b24..b1f8dcd2b 100644
--- a/ios/styles/UnorderedListStyle.mm
+++ b/ios/styles/UnorderedListStyle.mm
@@ -17,6 +17,22 @@ + (BOOL)isParagraphStyle {
return YES;
}
++ (const char *)tagName {
+ return "ul";
+}
+
++ (const char *)subTagName {
+ return "li";
+}
+
++ (NSAttributedStringKey)attributeKey {
+ return NSParagraphStyleAttributeName;
+}
+
++ (BOOL)isSelfClosing {
+ return NO;
+}
+
- (CGFloat)getHeadIndent {
// lists are drawn manually
// margin before bullet + gap between bullet and paragraph
@@ -234,7 +250,7 @@ - (BOOL)tryHandlingListShorcutInRange:(NSRange)range
return NO;
}
-- (BOOL)styleCondition:(id _Nullable)value:(NSRange)range {
+- (BOOL)styleCondition:(id _Nullable)value range:(NSRange)range {
NSParagraphStyle *paragraph = (NSParagraphStyle *)value;
return paragraph != nullptr && paragraph.textLists.count == 1 &&
paragraph.textLists.firstObject.markerFormat == NSTextListMarkerDisc;
@@ -246,7 +262,7 @@ - (BOOL)detectStyle:(NSRange)range {
withInput:_input
inRange:range
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
} else {
return [OccurenceUtils detect:NSParagraphStyleAttributeName
@@ -254,7 +270,7 @@ - (BOOL)detectStyle:(NSRange)range {
atIndex:range.location
checkPrevious:YES
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
}
}
@@ -264,7 +280,7 @@ - (BOOL)anyOccurence:(NSRange)range {
withInput:_input
inRange:range
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
}
@@ -273,7 +289,7 @@ - (BOOL)anyOccurence:(NSRange)range {
withInput:_input
inRange:range
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
}
diff --git a/ios/utils/BaseStyleProtocol.h b/ios/utils/BaseStyleProtocol.h
index 5e08e558a..db34114bc 100644
--- a/ios/utils/BaseStyleProtocol.h
+++ b/ios/utils/BaseStyleProtocol.h
@@ -5,6 +5,11 @@
@protocol BaseStyleProtocol
+ (StyleType)getStyleType;
+ (BOOL)isParagraphStyle;
++ (const char *_Nonnull)tagName;
++ (const char *_Nullable)subTagName;
++ (NSAttributedStringKey _Nonnull)attributeKey;
++ (BOOL)isSelfClosing;
+- (BOOL)styleCondition:(id _Nullable)value range:(NSRange)range;
- (instancetype _Nonnull)initWithInput:(id _Nonnull)input;
- (void)applyStyle:(NSRange)range;
- (void)addAttributes:(NSRange)range withTypingAttr:(BOOL)withTypingAttr;
diff --git a/ios/utils/ParameterizedStyleProtocol.h b/ios/utils/ParameterizedStyleProtocol.h
new file mode 100644
index 000000000..6a7a6d214
--- /dev/null
+++ b/ios/utils/ParameterizedStyleProtocol.h
@@ -0,0 +1,5 @@
+@protocol ParameterizedStyleProtocol
+
++ (NSDictionary *_Nullable)getParametersFromValue:(id)value;
+
+@end
diff --git a/ios/utils/StyleHeaders.h b/ios/utils/StyleHeaders.h
index 1a2c8d3c7..ea9c5deef 100644
--- a/ios/utils/StyleHeaders.h
+++ b/ios/utils/StyleHeaders.h
@@ -5,6 +5,7 @@
#import "ImageData.h"
#import "LinkData.h"
#import "MentionParams.h"
+#import "ParameterizedStyleProtocol.h"
@interface BoldStyle : NSObject
@end
@@ -22,7 +23,7 @@
- (void)handleNewlines;
@end
-@interface ColorStyle : NSObject
+@interface ColorStyle : NSObject
@property(nonatomic, strong) UIColor *color;
- (UIColor *)getColorAt:(NSUInteger)location;
- (void)applyStyle:(NSRange)range color:(UIColor *)color;
@@ -30,7 +31,7 @@
- (void)removeColorInSelectedRange;
@end
-@interface LinkStyle : NSObject
+@interface LinkStyle : NSObject
- (void)addLink:(NSString *)text
url:(NSString *)url
range:(NSRange)range
@@ -44,7 +45,8 @@
replacementText:(NSString *)text;
@end
-@interface MentionStyle : NSObject
+@interface MentionStyle
+ : NSObject
- (void)addMention:(NSString *)indicator
text:(NSString *)text
attributes:(NSString *)attributes;
@@ -109,7 +111,7 @@
- (BOOL)handleBackspaceInRange:(NSRange)range replacementText:(NSString *)text;
@end
-@interface ImageStyle : NSObject
+@interface ImageStyle : NSObject
- (void)addImage:(NSString *)uri width:(CGFloat)width height:(CGFloat)height;
- (void)addImageAtRange:(NSRange)range
imageData:(ImageData *)imageData
@@ -117,7 +119,7 @@
- (ImageData *)getImageDataAt:(NSUInteger)location;
@end
-@interface CheckBoxStyle : NSObject
+@interface CheckBoxStyle : NSObject
- (BOOL)handleBackspaceInRange:(NSRange)range replacementText:(NSString *)text;
- (BOOL)handleNewlinesInRange:(NSRange)range replacementText:(NSString *)text;
- (BOOL)isCheckedAt:(NSUInteger)location;
@@ -131,7 +133,7 @@
- (void)insertDividerAtNewLine;
@end
-@interface ContentStyle : NSObject
+@interface ContentStyle : NSObject
- (void)addContentAtRange:(NSRange)range params:(ContentParams *)params;
- (ContentParams *)getContentParams:(NSUInteger)location;
@end
From 35081d77b93de4377a79439dd80fcfda95248ca6 Mon Sep 17 00:00:00 2001
From: IvanIhnatsiuk
Date: Sat, 13 Dec 2025 22:52:46 +0100
Subject: [PATCH 2/8] fix: remove pragma
---
ios/inputParser/EnrichedHTMLParser.mm | 9 ---------
1 file changed, 9 deletions(-)
diff --git a/ios/inputParser/EnrichedHTMLParser.mm b/ios/inputParser/EnrichedHTMLParser.mm
index 9a0076d29..d5d2380e4 100644
--- a/ios/inputParser/EnrichedHTMLParser.mm
+++ b/ios/inputParser/EnrichedHTMLParser.mm
@@ -9,8 +9,6 @@ @implementation EnrichedHTMLParser {
NSArray *_paragraphOrder;
}
-#pragma mark - Init
-
- (instancetype)initWithStyles:(NSDictionary *)stylesDict {
self = [super init];
if (!self)
@@ -43,8 +41,6 @@ - (instancetype)initWithStyles:(NSDictionary *)stylesDict {
return self;
}
-#pragma mark - Public API
-
- (NSString *)buildHtmlFromAttributedString:(NSAttributedString *)text
pretify:(BOOL)pretify {
@@ -132,8 +128,6 @@ - (HTMLElement *)buildRootNodeFromAttributedString:(NSAttributedString *)text {
return root;
}
-#pragma mark - Block container helpers
-
- (HTMLElement *)containerForBlock:(NSNumber *)currentParagraphType
reuseLastOf:(NSNumber *)previousParagraphType
previousNode:(HTMLElement *)previousNode
@@ -181,9 +175,6 @@ - (HTMLElement *)getNextContainer:(NSNumber *)blockType
return container;
}
-
-#pragma mark - Inline styling
-
- (HTMLNode *)getInlineStyleNodes:(NSAttributedString *)text
range:(NSRange)range
attrs:(NSDictionary *)attrs
From d5579e9ac85c0f91c06aa19362f5f314407ead2e Mon Sep 17 00:00:00 2001
From: IvanIhnatsiuk
Date: Sat, 13 Dec 2025 23:08:52 +0100
Subject: [PATCH 3/8] fix: remove pargrma mark
---
ios/inputParser/EnrichedHTMLParser.h | 2 --
1 file changed, 2 deletions(-)
diff --git a/ios/inputParser/EnrichedHTMLParser.h b/ios/inputParser/EnrichedHTMLParser.h
index 53d6c2a76..1c53e54ec 100644
--- a/ios/inputParser/EnrichedHTMLParser.h
+++ b/ios/inputParser/EnrichedHTMLParser.h
@@ -3,8 +3,6 @@
@class HTMLNode;
@class HTMLTextNode;
-#pragma mark - Public Builder
-
@interface EnrichedHTMLParser : NSObject
- (instancetype)initWithStyles:(NSDictionary *)stylesDict;
- (NSString *)buildHtmlFromAttributedString:(NSAttributedString *)text
From 3e6fce14dc566f9948e07d16438362e0fd8125b3 Mon Sep 17 00:00:00 2001
From: IvanIhnatsiuk
Date: Sat, 13 Dec 2025 23:13:48 +0100
Subject: [PATCH 4/8] fix: simplify namings
---
ios/inputParser/EnrichedHTMLParser.mm | 36 +++++++++++++-------------
ios/inputParser/EnrichedHTMLTagUtils.h | 10 +++----
2 files changed, 23 insertions(+), 23 deletions(-)
diff --git a/ios/inputParser/EnrichedHTMLParser.mm b/ios/inputParser/EnrichedHTMLParser.mm
index d5d2380e4..0f5952452 100644
--- a/ios/inputParser/EnrichedHTMLParser.mm
+++ b/ios/inputParser/EnrichedHTMLParser.mm
@@ -139,13 +139,13 @@ - (HTMLElement *)containerForBlock:(NSNumber *)currentParagraphType
return outer;
}
- BOOL isTheSameBlock = currentParagraphType == previousParagraphType;
+ BOOL isTheSameParagraph = currentParagraphType == previousParagraphType;
id styleObject = _styles[currentParagraphType];
Class styleClass = styleObject.class;
BOOL hasSubTags = [styleClass subTagName] != NULL;
- if (isTheSameBlock && hasSubTags)
+ if (isTheSameParagraph && hasSubTags)
return previousNode;
HTMLElement *outer = [HTMLElement new];
@@ -187,23 +187,23 @@ - (HTMLNode *)getInlineStyleNodes:(NSAttributedString *)text
for (NSNumber *sty in _inlineOrder) {
id obj = _styles[sty];
- Class cls = obj.class;
+ Class styleClass = obj.class;
- NSString *key = [cls attributeKey];
+ NSString *key = [styleClass attributeKey];
id v = attrs[key];
if (!v || ![obj styleCondition:v range:range])
continue;
HTMLElement *wrap = [HTMLElement new];
- const char *tag = [cls tagName];
+ const char *tag = [styleClass tagName];
wrap.tag = tag;
wrap.attributes =
- [cls respondsToSelector:@selector(getParametersFromValue:)]
- ? [cls getParametersFromValue:v]
+ [styleClass respondsToSelector:@selector(getParametersFromValue:)]
+ ? [styleClass getParametersFromValue:v]
: nullptr;
- wrap.selfClosing = [cls isSelfClosing];
+ wrap.selfClosing = [styleClass isSelfClosing];
[wrap.children addObject:currentNode];
currentNode = wrap;
}
@@ -211,8 +211,6 @@ - (HTMLNode *)getInlineStyleNodes:(NSAttributedString *)text
return currentNode;
}
-#pragma mark - Rendering
-
- (void)createHtmlFromNode:(HTMLNode *)node
into:(NSMutableData *)buf
pretify:(BOOL)pretify {
@@ -225,25 +223,27 @@ - (void)createHtmlFromNode:(HTMLNode *)node
if (![node isKindOfClass:[HTMLElement class]])
return;
- HTMLElement *el = (HTMLElement *)node;
+ HTMLElement *element = (HTMLElement *)node;
- BOOL addNewLineBefore = pretify && isBlockTag(el.tag);
- BOOL addNewLineAfter = pretify && needsNewLineAfter(el.tag);
+ BOOL addNewLineBefore = pretify && isBlockTag(element.tag);
+ BOOL addNewLineAfter = pretify && needsNewLineAfter(element.tag);
- if (el.selfClosing) {
- appendSelfClosingTagC(buf, el.tag, el.attributes, addNewLineBefore);
+ if (element.selfClosing) {
+ appendSelfClosingTag(buf, element.tag, element.attributes,
+ addNewLineBefore);
return;
}
- appendOpenTagC(buf, el.tag, el.attributes ?: nullptr, addNewLineBefore);
+ appendOpenTag(buf, element.tag, element.attributes ?: nullptr,
+ addNewLineBefore);
- for (HTMLNode *child in el.children)
+ for (HTMLNode *child in element.children)
[self createHtmlFromNode:child into:buf pretify:pretify];
if (addNewLineAfter)
appendC(buf, "\n");
- appendCloseTagC(buf, el.tag);
+ appendCloseTag(buf, element.tag);
}
@end
diff --git a/ios/inputParser/EnrichedHTMLTagUtils.h b/ios/inputParser/EnrichedHTMLTagUtils.h
index e32d26cc7..444f18664 100644
--- a/ios/inputParser/EnrichedHTMLTagUtils.h
+++ b/ios/inputParser/EnrichedHTMLTagUtils.h
@@ -93,8 +93,8 @@ static inline BOOL needsNewLineAfter(const char *t) {
strcmp(t, "html") == 0);
}
-static inline void appendOpenTagC(NSMutableData *buf, const char *t,
- NSDictionary *attrs, BOOL block) {
+static inline void appendOpenTag(NSMutableData *buf, const char *t,
+ NSDictionary *attrs, BOOL block) {
if (block)
appendC(buf, "\n<");
else
@@ -107,8 +107,8 @@ static inline void appendOpenTagC(NSMutableData *buf, const char *t,
appendC(buf, ">");
}
-static inline void appendSelfClosingTagC(NSMutableData *buf, const char *t,
- NSDictionary *attrs, BOOL block) {
+static inline void appendSelfClosingTag(NSMutableData *buf, const char *t,
+ NSDictionary *attrs, BOOL block) {
if (block)
appendC(buf, "\n<");
else
@@ -121,7 +121,7 @@ static inline void appendSelfClosingTagC(NSMutableData *buf, const char *t,
appendC(buf, "/>");
}
-static inline void appendCloseTagC(NSMutableData *buf, const char *t) {
+static inline void appendCloseTag(NSMutableData *buf, const char *t) {
appendC(buf, "");
appendC(buf, t);
appendC(buf, ">");
From 5041b4b535e60e82301a0672bdbe1d8fd5d42366 Mon Sep 17 00:00:00 2001
From: IvanIhnatsiuk
Date: Sat, 13 Dec 2025 23:43:19 +0100
Subject: [PATCH 5/8] fix: better names for magic hex
---
ios/inputParser/EnrichedHTMLParser.mm | 26 +++---
ios/inputParser/EnrichedHTMLTagUtils.h | 123 ++++++++++++++++---------
2 files changed, 92 insertions(+), 57 deletions(-)
diff --git a/ios/inputParser/EnrichedHTMLParser.mm b/ios/inputParser/EnrichedHTMLParser.mm
index 0f5952452..40693f1ec 100644
--- a/ios/inputParser/EnrichedHTMLParser.mm
+++ b/ios/inputParser/EnrichedHTMLParser.mm
@@ -184,15 +184,15 @@ - (HTMLNode *)getInlineStyleNodes:(NSAttributedString *)text
textNode.range = range;
HTMLNode *currentNode = textNode;
- for (NSNumber *sty in _inlineOrder) {
+ for (NSNumber *inlineStyleType in _inlineOrder) {
- id obj = _styles[sty];
- Class styleClass = obj.class;
+ id styleObject = _styles[inlineStyleType];
+ Class styleClass = styleObject.class;
NSString *key = [styleClass attributeKey];
- id v = attrs[key];
+ id value = attrs[key];
- if (!v || ![obj styleCondition:v range:range])
+ if (!value || ![styleObject styleCondition:value range:range])
continue;
HTMLElement *wrap = [HTMLElement new];
@@ -201,7 +201,7 @@ - (HTMLNode *)getInlineStyleNodes:(NSAttributedString *)text
wrap.tag = tag;
wrap.attributes =
[styleClass respondsToSelector:@selector(getParametersFromValue:)]
- ? [styleClass getParametersFromValue:v]
+ ? [styleClass getParametersFromValue:value]
: nullptr;
wrap.selfClosing = [styleClass isSelfClosing];
[wrap.children addObject:currentNode];
@@ -212,11 +212,11 @@ - (HTMLNode *)getInlineStyleNodes:(NSAttributedString *)text
}
- (void)createHtmlFromNode:(HTMLNode *)node
- into:(NSMutableData *)buf
+ into:(NSMutableData *)buffer
pretify:(BOOL)pretify {
if ([node isKindOfClass:[HTMLTextNode class]]) {
HTMLTextNode *t = (HTMLTextNode *)node;
- appendEscapedRange(buf, t.source, t.range);
+ appendEscapedRange(buffer, t.source, t.range);
return;
}
@@ -229,21 +229,21 @@ - (void)createHtmlFromNode:(HTMLNode *)node
BOOL addNewLineAfter = pretify && needsNewLineAfter(element.tag);
if (element.selfClosing) {
- appendSelfClosingTag(buf, element.tag, element.attributes,
+ appendSelfClosingTag(buffer, element.tag, element.attributes,
addNewLineBefore);
return;
}
- appendOpenTag(buf, element.tag, element.attributes ?: nullptr,
+ appendOpenTag(buffer, element.tag, element.attributes ?: nullptr,
addNewLineBefore);
for (HTMLNode *child in element.children)
- [self createHtmlFromNode:child into:buf pretify:pretify];
+ [self createHtmlFromNode:child into:buffer pretify:pretify];
if (addNewLineAfter)
- appendC(buf, "\n");
+ appendC(buffer, "\n");
- appendCloseTag(buf, element.tag);
+ appendCloseTag(buffer, element.tag);
}
@end
diff --git a/ios/inputParser/EnrichedHTMLTagUtils.h b/ios/inputParser/EnrichedHTMLTagUtils.h
index 444f18664..78c1a35d4 100644
--- a/ios/inputParser/EnrichedHTMLTagUtils.h
+++ b/ios/inputParser/EnrichedHTMLTagUtils.h
@@ -1,6 +1,40 @@
#pragma once
-
#import
+static const unichar ZeroWidthSpace = 0x200B;
+
+static const int UTF8_1ByteLimit = 0x80;
+static const int UTF8_2ByteLimit = 0x800;
+
+static const char UTF8_2ByteLeadMask = 0xC0;
+static const char UTF8_3ByteLeadMask = 0xE0;
+static const char UTF8_ContinuationMask = 0x80;
+static const char UTF8_ContinuationPayloadMask = 0x3F;
+
+static const unichar HtmlLessThanChar = '<';
+static const unichar HtmlGreaterThanChar = '>';
+static const unichar HtmlAmpersandChar = '&';
+
+static const char *NewlineOpenTag = "\n<";
+
+static const char *OpenTagStart = "<";
+static const char *CloseTagStart = "";
+static const char *SelfCloseTagSuffix = "/>";
+static const char *TagEnd = ">";
+static const char *Space = " ";
+static const char *EqualsSign = "=";
+static const char *Quote = "\"";
+
+static const char *EscapeLT = "<";
+static const char *EscapeGT = ">";
+static const char *EscapeAmp = "&";
+
+static const char *HtmlTagUL = "ul";
+static const char *HtmlTagOL = "ol";
+static const char *HtmlTagLI = "li";
+static const char *HtmlTagBR = "br";
+static const char *HtmlTagHTML = "html";
+static const char *HtmlTagBlockquote = "blockquote";
+static const char *HtmlTagCodeblock = "codeblock";
static inline void appendC(NSMutableData *buf, const char *c) {
if (!c)
@@ -16,36 +50,41 @@ static inline void appendEscapedRange(NSMutableData *buf, NSString *src,
for (NSUInteger i = 0; i < len; i++) {
unichar c = tmp[i];
- if (c == 0x200B)
+ if (c == ZeroWidthSpace)
continue;
switch (c) {
- case '<':
- appendC(buf, "<");
+ case HtmlLessThanChar:
+ appendC(buf, EscapeLT);
break;
- case '>':
- appendC(buf, ">");
+ case HtmlGreaterThanChar:
+ appendC(buf, EscapeGT);
break;
- case '&':
- appendC(buf, "&");
+ case HtmlAmpersandChar:
+ appendC(buf, EscapeAmp);
break;
default: {
char out[4];
int n = 0;
- if (c < 0x80) {
+
+ if (c < UTF8_1ByteLimit) {
out[0] = (char)c;
n = 1;
- } else if (c < 0x800) {
- out[0] = 0xC0 | (c >> 6);
- out[1] = 0x80 | (c & 0x3F);
+
+ } else if (c < UTF8_2ByteLimit) {
+ out[0] = UTF8_2ByteLeadMask | (c >> 6);
+ out[1] = UTF8_ContinuationMask | (c & UTF8_ContinuationPayloadMask);
n = 2;
+
} else {
- out[0] = 0xE0 | (c >> 12);
- out[1] = 0x80 | ((c >> 6) & 0x3F);
- out[2] = 0x80 | (c & 0x3F);
+ out[0] = UTF8_3ByteLeadMask | (c >> 12);
+ out[1] =
+ UTF8_ContinuationMask | ((c >> 6) & UTF8_ContinuationPayloadMask);
+ out[2] = UTF8_ContinuationMask | (c & UTF8_ContinuationPayloadMask);
n = 3;
}
+
[buf appendBytes:out length:n];
}
}
@@ -54,32 +93,33 @@ static inline void appendEscapedRange(NSMutableData *buf, NSString *src,
static inline void appendKeyVal(NSMutableData *buf, NSString *key,
NSString *val) {
- appendC(buf, " ");
- const char *k = key.UTF8String;
- appendC(buf, k);
- appendC(buf, "=\"");
+ appendC(buf, Space);
+ appendC(buf, key.UTF8String);
+ appendC(buf, EqualsSign);
+ appendC(buf, Quote);
appendEscapedRange(buf, val, NSMakeRange(0, val.length));
- appendC(buf, "\"");
+ appendC(buf, Quote);
}
static inline BOOL isBlockTag(const char *t) {
if (!t)
return NO;
+
switch (t[0]) {
case 'p':
- return t[1] == 0;
+ return t[1] == '\0';
case 'h':
- return (t[2] == 0 && (t[1] == '1' || t[1] == '2' || t[1] == '3'));
+ return t[2] == '\0' && (t[1] == '1' || t[1] == '2' || t[1] == '3');
case 'u':
- return strcmp(t, "ul") == 0;
+ return strcmp(t, HtmlTagUL) == 0;
case 'o':
- return strcmp(t, "ol") == 0;
+ return strcmp(t, HtmlTagOL) == 0;
case 'l':
- return strcmp(t, "li") == 0;
+ return strcmp(t, HtmlTagLI) == 0;
case 'b':
- return strcmp(t, "br") == 0 || strcmp(t, "blockquote") == 0;
+ return strcmp(t, HtmlTagBR) == 0 || strcmp(t, HtmlTagBlockquote) == 0;
case 'c':
- return strcmp(t, "codeblock") == 0;
+ return strcmp(t, HtmlTagCodeblock) == 0;
default:
return NO;
}
@@ -88,41 +128,36 @@ static inline BOOL isBlockTag(const char *t) {
static inline BOOL needsNewLineAfter(const char *t) {
if (!t)
return NO;
- return (strcmp(t, "ul") == 0 || strcmp(t, "ol") == 0 ||
- strcmp(t, "blockquote") == 0 || strcmp(t, "codeblock") == 0 ||
- strcmp(t, "html") == 0);
+
+ return (strcmp(t, HtmlTagUL) == 0 || strcmp(t, HtmlTagOL) == 0 ||
+ strcmp(t, HtmlTagBlockquote) == 0 ||
+ strcmp(t, HtmlTagCodeblock) == 0 || strcmp(t, HtmlTagHTML) == 0);
}
static inline void appendOpenTag(NSMutableData *buf, const char *t,
NSDictionary *attrs, BOOL block) {
- if (block)
- appendC(buf, "\n<");
- else
- appendC(buf, "<");
-
+ appendC(buf, block ? NewlineOpenTag : OpenTagStart);
appendC(buf, t);
+
for (NSString *key in attrs)
appendKeyVal(buf, key, attrs[key]);
- appendC(buf, ">");
+ appendC(buf, TagEnd);
}
static inline void appendSelfClosingTag(NSMutableData *buf, const char *t,
NSDictionary *attrs, BOOL block) {
- if (block)
- appendC(buf, "\n<");
- else
- appendC(buf, "<");
-
+ appendC(buf, block ? NewlineOpenTag : OpenTagStart);
appendC(buf, t);
+
for (NSString *key in attrs)
appendKeyVal(buf, key, attrs[key]);
- appendC(buf, "/>");
+ appendC(buf, SelfCloseTagSuffix);
}
static inline void appendCloseTag(NSMutableData *buf, const char *t) {
- appendC(buf, "");
+ appendC(buf, CloseTagStart);
appendC(buf, t);
- appendC(buf, ">");
+ appendC(buf, TagEnd);
}
From 21e40749ae9f27f2c793b44eb4697f16c0416adb Mon Sep 17 00:00:00 2001
From: IvanIhnatsiuk
Date: Sun, 14 Dec 2025 22:47:59 +0100
Subject: [PATCH 6/8] feat: faster html generation
---
ios/inputParser/EnrichedHTMLParser.mm | 170 +++++++++++++++-----------
1 file changed, 101 insertions(+), 69 deletions(-)
diff --git a/ios/inputParser/EnrichedHTMLParser.mm b/ios/inputParser/EnrichedHTMLParser.mm
index 40693f1ec..8be4c1c6d 100644
--- a/ios/inputParser/EnrichedHTMLParser.mm
+++ b/ios/inputParser/EnrichedHTMLParser.mm
@@ -5,8 +5,8 @@
@implementation EnrichedHTMLParser {
NSDictionary> *_styles;
- NSArray *_inlineOrder;
- NSArray *_paragraphOrder;
+ NSArray> *_inlineStyles;
+ NSArray> *_paragraphStyles;
}
- (instancetype)initWithStyles:(NSDictionary *)stylesDict {
@@ -16,27 +16,27 @@ - (instancetype)initWithStyles:(NSDictionary *)stylesDict {
_styles = stylesDict ?: @{};
- NSMutableArray *inlineArr = [NSMutableArray array];
- NSMutableArray *paragraphArr = [NSMutableArray array];
+ NSMutableArray *inlineStylesArray = [NSMutableArray array];
+ NSMutableArray *paragraphStylesArray = [NSMutableArray array];
- for (NSNumber *type in _styles) {
- id style = _styles[type];
+ NSArray *allKeys = stylesDict.allKeys;
+ for (NSInteger i = 0; i < allKeys.count; i++) {
+ NSNumber *key = allKeys[i];
+ id style = stylesDict[key];
Class cls = style.class;
- BOOL isParagraph = ([cls respondsToSelector:@selector(isParagraphStyle)] &&
- [cls isParagraphStyle]);
+ BOOL isParagraph = ([cls respondsToSelector:@selector(isParagraphStyle)]) &&
+ [cls isParagraphStyle];
- if (isParagraph)
- [paragraphArr addObject:type];
- else
- [inlineArr addObject:type];
+ if (isParagraph) {
+ [paragraphStylesArray addObject:style];
+ } else {
+ [inlineStylesArray addObject:style];
+ }
}
- [inlineArr sortUsingSelector:@selector(compare:)];
- [paragraphArr sortUsingSelector:@selector(compare:)];
-
- _inlineOrder = inlineArr.copy;
- _paragraphOrder = paragraphArr.copy;
+ _inlineStyles = inlineStylesArray.copy;
+ _paragraphStyles = paragraphStylesArray.copy;
return self;
}
@@ -49,10 +49,10 @@ - (NSString *)buildHtmlFromAttributedString:(NSAttributedString *)text
HTMLElement *root = [self buildRootNodeFromAttributedString:text];
- NSMutableData *buf = [NSMutableData data];
- [self createHtmlFromNode:root into:buf pretify:pretify];
+ NSMutableData *buffer = [NSMutableData data];
+ [self createHtmlFromNode:root into:buffer pretify:pretify];
- return [[NSString alloc] initWithData:buf encoding:NSUTF8StringEncoding];
+ return [[NSString alloc] initWithData:buffer encoding:NSUTF8StringEncoding];
}
- (HTMLElement *)buildRootNodeFromAttributedString:(NSAttributedString *)text {
@@ -65,7 +65,7 @@ - (HTMLElement *)buildRootNodeFromAttributedString:(NSAttributedString *)text {
br.tag = "br";
br.selfClosing = YES;
- __block NSNumber *previousParagraphType = nil;
+ __block id previousParagraphStyle = nil;
__block HTMLElement *previousNode = nil;
[plain
@@ -77,37 +77,27 @@ - (HTMLElement *)buildRootNodeFromAttributedString:(NSAttributedString *)text {
BOOL *__unused stop) {
if (paragraphRange.length == 0) {
[root.children addObject:br];
- previousParagraphType = nil;
+ previousParagraphStyle = nil;
previousNode = nil;
return;
}
- NSDictionary *attrsAtStart =
- [text attributesAtIndex:paragraphRange.location
- effectiveRange:nil];
-
- NSNumber *ptype = nil;
- for (NSNumber *sty in self->_paragraphOrder) {
- id s = self->_styles[sty];
- NSString *key = [s.class attributeKey];
- id val = attrsAtStart[key];
- if (val && [s styleCondition:val
- range:paragraphRange]) {
- ptype = sty;
- break;
- }
- }
- HTMLElement *container =
- [self containerForBlock:ptype
- reuseLastOf:previousParagraphType
- previousNode:previousNode
- rootNode:root];
+ id paragraphStyle =
+ [self detectParagraphStyle:text
+ paragraphRange:paragraphRange];
- previousParagraphType = ptype;
+ HTMLElement *container = [self
+ containerForParagraphStyle:paragraphStyle
+ previousParagraphStyle:previousParagraphStyle
+ previousNode:previousNode
+ rootNode:root];
+
+ previousParagraphStyle = paragraphStyle;
previousNode = container;
- HTMLElement *target = [self getNextContainer:ptype
- currentContainer:container];
+ HTMLElement *target =
+ [self nextContainerForParagraphStyle:paragraphStyle
+ currentContainer:container];
[text
enumerateAttributesInRange:paragraphRange
@@ -128,10 +118,47 @@ - (HTMLElement *)buildRootNodeFromAttributedString:(NSAttributedString *)text {
return root;
}
-- (HTMLElement *)containerForBlock:(NSNumber *)currentParagraphType
- reuseLastOf:(NSNumber *)previousParagraphType
- previousNode:(HTMLElement *)previousNode
- rootNode:(HTMLElement *)rootNode {
+- (HTMLElement *)nextContainerForParagraphStyle:
+ (id _Nullable)style
+ currentContainer:(HTMLElement *)container {
+ if (!style)
+ return container;
+
+ const char *sub = [style.class subTagName];
+ if (!sub)
+ return container;
+
+ HTMLElement *inner = [HTMLElement new];
+ inner.tag = sub;
+ [container.children addObject:inner];
+ return inner;
+}
+
+- (id _Nullable)
+ detectParagraphStyle:(NSAttributedString *)text
+ paragraphRange:(NSRange)paragraphRange {
+ NSDictionary *attrsAtStart = [text attributesAtIndex:paragraphRange.location
+ effectiveRange:nil];
+ id _Nullable foundParagraphStyle = nil;
+ for (NSInteger i = 0; i < _paragraphStyles.count; i++) {
+ id paragraphStyle = _paragraphStyles[i];
+ Class paragraphStyleClass = paragraphStyle.class;
+
+ NSAttributedStringKey attributeKey = [paragraphStyleClass attributeKey];
+ id value = attrsAtStart[attributeKey];
+
+ if (value && [paragraphStyle styleCondition:value range:paragraphRange]) {
+ return paragraphStyle;
+ }
+ }
+
+ return foundParagraphStyle;
+}
+
+- (HTMLElement *)currentParagraphType:(NSNumber *)currentParagraphType
+ previousParagraphType:(NSNumber *)previousParagraphType
+ previousNode:(HTMLElement *)previousNode
+ rootNode:(HTMLElement *)rootNode {
if (!currentParagraphType) {
HTMLElement *outer = [HTMLElement new];
outer.tag = "p";
@@ -156,25 +183,31 @@ - (HTMLElement *)containerForBlock:(NSNumber *)currentParagraphType
return outer;
}
-- (HTMLElement *)getNextContainer:(NSNumber *)blockType
- currentContainer:(HTMLElement *)container {
-
- if (!blockType)
- return container;
-
- id style = _styles[blockType];
+- (HTMLElement *)
+ containerForParagraphStyle:(id _Nullable)currentStyle
+ previousParagraphStyle:(id _Nullable)previousStyle
+ previousNode:(HTMLElement *)previousNode
+ rootNode:(HTMLElement *)rootNode {
+ if (!currentStyle) {
+ HTMLElement *outer = [HTMLElement new];
+ outer.tag = "p";
+ [rootNode.children addObject:outer];
+ return outer;
+ }
- const char *subTagName = [style.class subTagName];
+ BOOL sameStyle = (currentStyle == previousStyle);
+ Class styleClass = currentStyle.class;
+ BOOL hasSub = ([styleClass subTagName] != NULL);
- if (subTagName) {
- HTMLElement *inner = [HTMLElement new];
- inner.tag = subTagName;
- [container.children addObject:inner];
- return inner;
- }
+ if (sameStyle && hasSub)
+ return previousNode;
- return container;
+ HTMLElement *outer = [HTMLElement new];
+ outer.tag = [styleClass tagName];
+ [rootNode.children addObject:outer];
+ return outer;
}
+
- (HTMLNode *)getInlineStyleNodes:(NSAttributedString *)text
range:(NSRange)range
attrs:(NSDictionary *)attrs
@@ -184,13 +217,12 @@ - (HTMLNode *)getInlineStyleNodes:(NSAttributedString *)text
textNode.range = range;
HTMLNode *currentNode = textNode;
- for (NSNumber *inlineStyleType in _inlineOrder) {
-
- id styleObject = _styles[inlineStyleType];
+ for (NSInteger i = 0; i < _inlineStyles.count; i++) {
+ id styleObject = _inlineStyles[i];
Class styleClass = styleObject.class;
- NSString *key = [styleClass attributeKey];
- id value = attrs[key];
+ NSAttributedStringKey attributeKey = [styleClass attributeKey];
+ id value = attrs[attributeKey];
if (!value || ![styleObject styleCondition:value range:range])
continue;
From 85cf2cb722cedbaf186f7cee11685d86bd0ca58e Mon Sep 17 00:00:00 2001
From: IvanIhnatsiuk
Date: Mon, 15 Dec 2025 11:47:33 +0100
Subject: [PATCH 7/8] fix: better class namings
---
...ser.h => EnrichedAttributedStringHTMLSerializer.h} | 2 +-
...r.mm => EnrichedAttributedStringHTMLSerializer.mm} | 6 +++---
... EnrichedAttributedStringHTMLSerializerTagUtils.h} | 0
ios/inputParser/InputParser.mm | 11 +++++++----
4 files changed, 11 insertions(+), 8 deletions(-)
rename ios/inputParser/{EnrichedHTMLParser.h => EnrichedAttributedStringHTMLSerializer.h} (82%)
rename ios/inputParser/{EnrichedHTMLParser.mm => EnrichedAttributedStringHTMLSerializer.mm} (98%)
rename ios/inputParser/{EnrichedHTMLTagUtils.h => EnrichedAttributedStringHTMLSerializerTagUtils.h} (100%)
diff --git a/ios/inputParser/EnrichedHTMLParser.h b/ios/inputParser/EnrichedAttributedStringHTMLSerializer.h
similarity index 82%
rename from ios/inputParser/EnrichedHTMLParser.h
rename to ios/inputParser/EnrichedAttributedStringHTMLSerializer.h
index 1c53e54ec..150b2b803 100644
--- a/ios/inputParser/EnrichedHTMLParser.h
+++ b/ios/inputParser/EnrichedAttributedStringHTMLSerializer.h
@@ -3,7 +3,7 @@
@class HTMLNode;
@class HTMLTextNode;
-@interface EnrichedHTMLParser : NSObject
+@interface EnrichedAttributedStringHTMLSerializer : NSObject
- (instancetype)initWithStyles:(NSDictionary *)stylesDict;
- (NSString *)buildHtmlFromAttributedString:(NSAttributedString *)text
pretify:(BOOL)pretify;
diff --git a/ios/inputParser/EnrichedHTMLParser.mm b/ios/inputParser/EnrichedAttributedStringHTMLSerializer.mm
similarity index 98%
rename from ios/inputParser/EnrichedHTMLParser.mm
rename to ios/inputParser/EnrichedAttributedStringHTMLSerializer.mm
index 8be4c1c6d..15c90cfb7 100644
--- a/ios/inputParser/EnrichedHTMLParser.mm
+++ b/ios/inputParser/EnrichedAttributedStringHTMLSerializer.mm
@@ -1,9 +1,9 @@
-#import "EnrichedHtmlParser.h"
-#import "EnrichedHTMLTagUtils.h"
+#import "EnrichedAttributedStringHTMLSerializer.h"
+#import "EnrichedAttributedStringHTMLSerializerTagUtils.h"
#import "HtmlNode.h"
#import "StyleHeaders.h"
-@implementation EnrichedHTMLParser {
+@implementation EnrichedAttributedStringHTMLSerializer {
NSDictionary> *_styles;
NSArray> *_inlineStyles;
NSArray> *_paragraphStyles;
diff --git a/ios/inputParser/EnrichedHTMLTagUtils.h b/ios/inputParser/EnrichedAttributedStringHTMLSerializerTagUtils.h
similarity index 100%
rename from ios/inputParser/EnrichedHTMLTagUtils.h
rename to ios/inputParser/EnrichedAttributedStringHTMLSerializerTagUtils.h
diff --git a/ios/inputParser/InputParser.mm b/ios/inputParser/InputParser.mm
index e7cb3e3dc..88f9f723c 100644
--- a/ios/inputParser/InputParser.mm
+++ b/ios/inputParser/InputParser.mm
@@ -1,5 +1,5 @@
#import "InputParser.h"
-#import "EnrichedHTMLParser.h"
+#import "EnrichedAttributedStringHTMLSerializer.h"
#import "EnrichedTextInputView.h"
#import "StringExtension.h"
#import "StyleHeaders.h"
@@ -8,13 +8,15 @@
@implementation InputParser {
EnrichedTextInputView *_input;
- EnrichedHTMLParser *_htmlParser;
+ EnrichedAttributedStringHTMLSerializer *_attributedStringHTMLSerializer;
}
- (instancetype)initWithInput:(id)input {
self = [super init];
_input = (EnrichedTextInputView *)input;
- _htmlParser = [[EnrichedHTMLParser alloc] initWithStyles:_input->stylesDict];
+ _attributedStringHTMLSerializer =
+ [[EnrichedAttributedStringHTMLSerializer alloc]
+ initWithStyles:_input->stylesDict];
return self;
}
@@ -22,7 +24,8 @@ - (NSString *)parseToHtmlFromRange:(NSRange)range {
NSAttributedString *sub =
[_input->textView.textStorage attributedSubstringFromRange:range];
- return [_htmlParser buildHtmlFromAttributedString:sub pretify:YES];
+ return [_attributedStringHTMLSerializer buildHtmlFromAttributedString:sub
+ pretify:YES];
}
- (void)replaceWholeFromHtml:(NSString *_Nonnull)html {
From ab6251179a96e9aa0913212945c45cab327d20c5 Mon Sep 17 00:00:00 2001
From: IvanIhnatsiuk
Date: Tue, 16 Dec 2025 00:12:10 +0100
Subject: [PATCH 8/8] feat: integrate our styles with new html parser
---
example/src/App.tsx | 10 +-
ios/EnrichedTextInputView.mm | 154 +++++----
.../EnrichedAttributedStringHTMLSerializer.mm | 26 +-
...edAttributedStringHTMLSerializerTagUtils.h | 9 +-
ios/inputParser/InputParser.mm | 1 +
ios/styles/CheckBoxStyle.mm | 42 ++-
ios/styles/ColorStyle.mm | 302 ++++++++++--------
ios/styles/ContentStyle.mm | 47 ++-
ios/styles/DividerStyle.mm | 104 +++++-
ios/utils/CheckboxHitTestUtils.h | 13 +
ios/utils/CheckboxHitTestUtils.mm | 76 +++++
ios/utils/ContentHitTestUtils.h | 12 +
ios/utils/ContentHitTestUtils.mm | 77 +++++
ios/utils/DividerHitTestUtils.h | 12 +
ios/utils/DividerHitTestUtils.mm | 77 +++++
ios/utils/StyleHeaders.h | 8 +-
16 files changed, 737 insertions(+), 233 deletions(-)
create mode 100644 ios/utils/CheckboxHitTestUtils.h
create mode 100644 ios/utils/CheckboxHitTestUtils.mm
create mode 100644 ios/utils/ContentHitTestUtils.h
create mode 100644 ios/utils/ContentHitTestUtils.mm
create mode 100644 ios/utils/DividerHitTestUtils.h
create mode 100644 ios/utils/DividerHitTestUtils.mm
diff --git a/example/src/App.tsx b/example/src/App.tsx
index 5205c3254..9cb8a7e7d 100644
--- a/example/src/App.tsx
+++ b/example/src/App.tsx
@@ -87,7 +87,9 @@ const DEBUG_SCROLLABLE = false;
// See: https://github.com/software-mansion/react-native-enriched/issues/229
const ANDROID_EXPERIMENTAL_SYNCHRONOUS_EVENTS = false;
-const html = `
+const contentHtml = Array(1)
+ .fill(
+ `
Title H1
Test test
@@ -99,7 +101,11 @@ const html = `
Bold text
Italic text
Underline text
-`;
+`
+ )
+ .join('');
+
+const html = '' + contentHtml + '';
export default function App() {
const [isChannelPopupOpen, setIsChannelPopupOpen] = useState(false);
diff --git a/ios/EnrichedTextInputView.mm b/ios/EnrichedTextInputView.mm
index 4f3a27688..0e9bad620 100644
--- a/ios/EnrichedTextInputView.mm
+++ b/ios/EnrichedTextInputView.mm
@@ -1,6 +1,9 @@
#import "EnrichedTextInputView.h"
+#import "CheckboxHitTestUtils.h"
#import "ColorExtension.h"
+#import "ContentHitTestUtils.h"
#import "CoreText/CoreText.h"
+#import "DividerHitTestUtils.h"
#import "EnrichedImageLoader.h"
#import "LayoutManagerExtension.h"
#import "ParagraphAttributesUtils.h"
@@ -1262,7 +1265,13 @@ - (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args {
[self toggleRegularStyle:[StrikethroughStyle getStyleType]];
} else if ([commandName isEqualToString:@"setColor"]) {
NSString *colorText = (NSString *)args[0];
- [self setColor:colorText];
+ UIColor *color = [UIColor colorFromString:colorText];
+ id baseStyle = stylesDict[@(Colored)];
+ if ([baseStyle isKindOfClass:[ColorStyle class]]) {
+ ColorStyle *colorStyle = (ColorStyle *)baseStyle;
+ [colorStyle applyStyle:textView.selectedRange color:color];
+ }
+ [self anyTextMayHaveBeenModified];
} else if ([commandName isEqualToString:@"removeColor"]) {
[self removeColor];
[self anyTextMayHaveBeenModified];
@@ -1478,14 +1487,14 @@ - (void)requestHTML:(NSInteger)requestId {
- (void)setColor:(NSString *)colorText {
UIColor *color = [UIColor colorFromString:colorText];
- ColorStyle *colorStyle = stylesDict[@(Colored)];
+ ColorStyle *colorStyle = (ColorStyle *)stylesDict[@(Colored)];
[colorStyle applyStyle:textView.selectedRange color:color];
[self anyTextMayHaveBeenModified];
}
- (void)removeColor {
- ColorStyle *colorStyle = stylesDict[@(Colored)];
+ ColorStyle *colorStyle = (ColorStyle *)stylesDict[@(Colored)];
[colorStyle removeColorInSelectedRange];
[self anyTextMayHaveBeenModified];
}
@@ -1688,7 +1697,8 @@ - (void)manageSelectionBasedChanges {
- (void)handleWordModificationBasedChanges:(NSString *)word
inRange:(NSRange)range {
// manual links refreshing and automatic links detection handling
- LinkStyle *linkStyle = [stylesDict objectForKey:@([LinkStyle getStyleType])];
+ LinkStyle *linkStyle =
+ (LinkStyle *)[stylesDict objectForKey:@([LinkStyle getStyleType])];
if (linkStyle != nullptr) {
// manual links need to be handled first because they can block automatic
@@ -1887,24 +1897,62 @@ - (void)textViewDidEndEditing:(UITextView *)textView {
}
}
+- (BOOL)isReadOnlyParagraphAtLocation:(NSUInteger)location {
+ NSTextStorage *storage = textView.textStorage;
+ NSUInteger length = storage.length;
+
+ if (length == 0) {
+ return NO;
+ }
+ if (location >= length) {
+ location = length - 1;
+ }
+
+ id currentValue = [storage attribute:ReadOnlyParagraphKey
+ atIndex:location
+ effectiveRange:nil];
+ if (currentValue) {
+ return YES;
+ }
+
+ if (location > 0) {
+ id previousValue = [storage attribute:ReadOnlyParagraphKey
+ atIndex:location - 1
+ effectiveRange:nil];
+ if (previousValue) {
+ return YES;
+ }
+ }
+
+ return NO;
+}
+
- (bool)textView:(UITextView *)textView
shouldChangeTextInRange:(NSRange)range
replacementText:(NSString *)text {
+ if (![text isEqualToString:@"\n"] &&
+ [self isReadOnlyParagraphAtLocation:range.location]) {
+ if (text.length == 0)
+ return YES;
+ return NO;
+ }
recentlyChangedRange = NSMakeRange(range.location, text.length);
UnorderedListStyle *uStyle = stylesDict[@([UnorderedListStyle getStyleType])];
OrderedListStyle *oStyle = stylesDict[@([OrderedListStyle getStyleType])];
BlockQuoteStyle *bqStyle = stylesDict[@([BlockQuoteStyle getStyleType])];
CodeBlockStyle *cbStyle = stylesDict[@([CodeBlockStyle getStyleType])];
- LinkStyle *linkStyle = stylesDict[@([LinkStyle getStyleType])];
- MentionStyle *mentionStyle = stylesDict[@([MentionStyle getStyleType])];
+ LinkStyle *linkStyle = (LinkStyle *)stylesDict[@([LinkStyle getStyleType])];
+ MentionStyle *mentionStyle =
+ (MentionStyle *)stylesDict[@([MentionStyle getStyleType])];
H1Style *h1Style = stylesDict[@([H1Style getStyleType])];
H2Style *h2Style = stylesDict[@([H2Style getStyleType])];
H3Style *h3Style = stylesDict[@([H3Style getStyleType])];
H4Style *h4Style = stylesDict[@([H4Style getStyleType])];
H5Style *h5Style = stylesDict[@([H5Style getStyleType])];
H6Style *h6Style = stylesDict[@([H6Style getStyleType])];
- CheckBoxStyle *checkBoxStyle = stylesDict[@([CheckBoxStyle getStyleType])];
+ CheckBoxStyle *checkBoxStyle =
+ (CheckBoxStyle *)stylesDict[@([CheckBoxStyle getStyleType])];
// some of the changes these checks do could interfere with later checks and
// cause a crash so here I rely on short circuiting evaluation of the logical
@@ -1983,8 +2031,6 @@ - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
return YES;
}
-#pragma mark - Checkbox Helpers
-
- (CGPoint)adjustedPointForViewPoint:(CGPoint)pt {
CGPoint tvPoint = [self convertPoint:pt toView:textView];
tvPoint.x -= textView.textContainerInset.left;
@@ -2006,55 +2052,45 @@ - (BOOL)getCharIndex:(NSUInteger *)charIndex forAdjustedPoint:(CGPoint)pt {
return YES;
}
-- (CGRect)checkboxRectForGlyphIndex:(NSUInteger)glyphIndex {
- NSRange effective = {0, 0};
- CGRect lineRect =
- [textView.layoutManager lineFragmentRectForGlyphAtIndex:glyphIndex
- effectiveRange:&effective];
-
- CGFloat w = [config checkBoxWidth];
- CGFloat h = [config checkBoxHeight];
- CGFloat ml = [config checkboxListMarginLeft];
- CGFloat gap = [config checkboxListGapWidth];
-
- return CGRectMake(ml + gap,
- lineRect.origin.y + (lineRect.size.height - h) / 2.0, w, h);
-}
-
-- (CheckBoxStyle *)checkboxStyleForCharIndex:(NSUInteger)charIndex {
- CheckBoxStyle *check = stylesDict[@([CheckBoxStyle getStyleType])];
- if (check && [check detectStyle:NSMakeRange(charIndex, 0)]) {
- return check;
- }
- return nil;
-}
-
- (void)handleTap:(UITapGestureRecognizer *)gr {
if (gr.state != UIGestureRecognizerStateEnded)
return;
CGPoint adjusted = [self adjustedPointForViewPoint:[gr locationInView:self]];
+ NSInteger dividerCharIndex =
+ [DividerHitTestUtils hitTestDividerAtPoint:adjusted inInput:self];
+ if (dividerCharIndex >= 0) {
+ NSUInteger newLocation = (NSUInteger)dividerCharIndex + 1;
+ if (newLocation > textView.textStorage.length) {
+ newLocation = textView.textStorage.length;
+ }
- NSUInteger charIndex;
- if (![self getCharIndex:&charIndex forAdjustedPoint:adjusted])
+ textView.selectedRange = NSMakeRange(newLocation, 0);
return;
+ }
- CheckBoxStyle *check = [self checkboxStyleForCharIndex:charIndex];
- if (!check)
+ NSInteger contentCharIndex =
+ [ContentHitTestUtils hitTestContentAtPoint:adjusted inInput:self];
+ if (contentCharIndex >= 0) {
+ NSUInteger newLocation = (NSUInteger)contentCharIndex + 1;
+ if (newLocation > textView.textStorage.length) {
+ newLocation = textView.textStorage.length;
+ }
+ textView.selectedRange = NSMakeRange(newLocation, 0);
return;
+ }
- // Get glyphIndex separately for the rectangle
- NSUInteger glyphIndex =
- [textView.layoutManager glyphIndexForPoint:adjusted
- inTextContainer:textView.textContainer
- fractionOfDistanceThroughGlyph:nil];
-
- CGRect checkboxRect = [self checkboxRectForGlyphIndex:glyphIndex];
- if (!CGRectContainsPoint(checkboxRect, adjusted))
+ NSInteger checkboxCharIndex =
+ [CheckboxHitTestUtils hitTestCheckboxAtPoint:adjusted inInput:self];
+ if (checkboxCharIndex >= 0) {
+ CheckBoxStyle *check =
+ (CheckBoxStyle *)stylesDict[@([CheckBoxStyle getStyleType])];
+ if (check) {
+ [check toggleCheckedAt:(NSUInteger)checkboxCharIndex];
+ [self anyTextMayHaveBeenModified];
+ }
return;
-
- [check toggleCheckedAt:charIndex];
- [self anyTextMayHaveBeenModified];
+ }
}
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
@@ -2064,23 +2100,17 @@ - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
CGPoint adjusted = [self adjustedPointForViewPoint:point];
- NSUInteger charIndex;
- if (![self getCharIndex:&charIndex forAdjustedPoint:adjusted])
- return hit;
-
- CheckBoxStyle *check = [self checkboxStyleForCharIndex:charIndex];
- if (!check)
- return hit;
-
- NSUInteger glyphIndex =
- [textView.layoutManager glyphIndexForPoint:adjusted
- inTextContainer:textView.textContainer
- fractionOfDistanceThroughGlyph:nil];
+ if ([DividerHitTestUtils hitTestDividerAtPoint:adjusted inInput:self] >= 0) {
+ return self;
+ }
- CGRect checkboxRect = [self checkboxRectForGlyphIndex:glyphIndex];
+ if ([ContentHitTestUtils hitTestContentAtPoint:adjusted inInput:self] >= 0) {
+ return self;
+ }
- if (CGRectContainsPoint(checkboxRect, adjusted)) {
- return self; // intercept touch
+ if ([CheckboxHitTestUtils hitTestCheckboxAtPoint:adjusted
+ inInput:self] >= 0) {
+ return self;
}
return hit;
diff --git a/ios/inputParser/EnrichedAttributedStringHTMLSerializer.mm b/ios/inputParser/EnrichedAttributedStringHTMLSerializer.mm
index 15c90cfb7..cb70a3d54 100644
--- a/ios/inputParser/EnrichedAttributedStringHTMLSerializer.mm
+++ b/ios/inputParser/EnrichedAttributedStringHTMLSerializer.mm
@@ -81,16 +81,21 @@ - (HTMLElement *)buildRootNodeFromAttributedString:(NSAttributedString *)text {
previousNode = nil;
return;
}
+ NSDictionary *attrsAtStart =
+ [text attributesAtIndex:paragraphRange.location
+ effectiveRange:nil];
id paragraphStyle =
[self detectParagraphStyle:text
- paragraphRange:paragraphRange];
+ paragraphRange:paragraphRange
+ attrsAtStart:attrsAtStart];
HTMLElement *container = [self
containerForParagraphStyle:paragraphStyle
previousParagraphStyle:previousParagraphStyle
previousNode:previousNode
- rootNode:root];
+ rootNode:root
+ attrsAtStart:attrsAtStart];
previousParagraphStyle = paragraphStyle;
previousNode = container;
@@ -136,9 +141,8 @@ - (HTMLElement *)nextContainerForParagraphStyle:
- (id _Nullable)
detectParagraphStyle:(NSAttributedString *)text
- paragraphRange:(NSRange)paragraphRange {
- NSDictionary *attrsAtStart = [text attributesAtIndex:paragraphRange.location
- effectiveRange:nil];
+ paragraphRange:(NSRange)paragraphRange
+ attrsAtStart:(NSDictionary *)attrsAtStart {
id _Nullable foundParagraphStyle = nil;
for (NSInteger i = 0; i < _paragraphStyles.count; i++) {
id paragraphStyle = _paragraphStyles[i];
@@ -174,7 +178,6 @@ - (HTMLElement *)currentParagraphType:(NSNumber *)currentParagraphType
if (isTheSameParagraph && hasSubTags)
return previousNode;
-
HTMLElement *outer = [HTMLElement new];
outer.tag = [styleClass tagName];
@@ -187,7 +190,8 @@ - (HTMLElement *)currentParagraphType:(NSNumber *)currentParagraphType
containerForParagraphStyle:(id _Nullable)currentStyle
previousParagraphStyle:(id _Nullable)previousStyle
previousNode:(HTMLElement *)previousNode
- rootNode:(HTMLElement *)rootNode {
+ rootNode:(HTMLElement *)rootNode
+ attrsAtStart:(NSDictionary *)attrsAtStart {
if (!currentStyle) {
HTMLElement *outer = [HTMLElement new];
outer.tag = "p";
@@ -204,6 +208,14 @@ - (HTMLElement *)currentParagraphType:(NSNumber *)currentParagraphType
HTMLElement *outer = [HTMLElement new];
outer.tag = [styleClass tagName];
+ outer.selfClosing = [styleClass isSelfClosing];
+ NSAttributedStringKey attributeKey = [styleClass attributeKey];
+ id value = attrsAtStart[attributeKey];
+ if (value &&
+ [styleClass respondsToSelector:@selector(getParametersFromValue:)]) {
+ outer.attributes = [styleClass getParametersFromValue:value];
+ }
+
[rootNode.children addObject:outer];
return outer;
}
diff --git a/ios/inputParser/EnrichedAttributedStringHTMLSerializerTagUtils.h b/ios/inputParser/EnrichedAttributedStringHTMLSerializerTagUtils.h
index 78c1a35d4..25de752ce 100644
--- a/ios/inputParser/EnrichedAttributedStringHTMLSerializerTagUtils.h
+++ b/ios/inputParser/EnrichedAttributedStringHTMLSerializerTagUtils.h
@@ -35,6 +35,9 @@ static const char *HtmlTagBR = "br";
static const char *HtmlTagHTML = "html";
static const char *HtmlTagBlockquote = "blockquote";
static const char *HtmlTagCodeblock = "codeblock";
+static const char *HtmlHRTag = "hr";
+static const char *HtmlChecklistTag = "checklist";
+static const char *HtmlContentTag = "content";
static inline void appendC(NSMutableData *buf, const char *c) {
if (!c)
@@ -109,7 +112,8 @@ static inline BOOL isBlockTag(const char *t) {
case 'p':
return t[1] == '\0';
case 'h':
- return t[2] == '\0' && (t[1] == '1' || t[1] == '2' || t[1] == '3');
+ return t[2] == '\0' &&
+ (t[1] == '1' || t[1] == '2' || t[1] == '3' || t[1] == 'r');
case 'u':
return strcmp(t, HtmlTagUL) == 0;
case 'o':
@@ -119,7 +123,8 @@ static inline BOOL isBlockTag(const char *t) {
case 'b':
return strcmp(t, HtmlTagBR) == 0 || strcmp(t, HtmlTagBlockquote) == 0;
case 'c':
- return strcmp(t, HtmlTagCodeblock) == 0;
+ return strcmp(t, HtmlTagCodeblock) == 0 || strcmp(t, HtmlContentTag) ||
+ strcmp(t, HtmlChecklistTag);
default:
return NO;
}
diff --git a/ios/inputParser/InputParser.mm b/ios/inputParser/InputParser.mm
index 88f9f723c..5ce8169ca 100644
--- a/ios/inputParser/InputParser.mm
+++ b/ios/inputParser/InputParser.mm
@@ -1,4 +1,5 @@
#import "InputParser.h"
+#import "ColorExtension.h"
#import "EnrichedAttributedStringHTMLSerializer.h"
#import "EnrichedTextInputView.h"
#import "StringExtension.h"
diff --git a/ios/styles/CheckBoxStyle.mm b/ios/styles/CheckBoxStyle.mm
index 3006bef80..4feda8e29 100644
--- a/ios/styles/CheckBoxStyle.mm
+++ b/ios/styles/CheckBoxStyle.mm
@@ -6,6 +6,9 @@
#import "StyleHeaders.h"
#import "TextInsertionUtils.h"
+static NSString *const CheckedValueString = @"true";
+static NSString *const UnckedValueString = @"false";
+
@implementation CheckBoxStyle {
EnrichedTextInputView *_input;
}
@@ -15,10 +18,36 @@ @implementation CheckBoxStyle {
+ (StyleType)getStyleType {
return Checkbox;
}
+
+ (BOOL)isParagraphStyle {
return YES;
}
++ (const char *)tagName {
+ return "checklist";
+}
+
++ (const char *)subTagName {
+ return nil;
+}
+
++ (NSAttributedStringKey)attributeKey {
+ return NSParagraphStyleAttributeName;
+}
+
++ (BOOL)isSelfClosing {
+ return NO;
+}
+
++ (NSDictionary *)getParametersFromValue:(id)value {
+ NSParagraphStyle *paragraphStyle = (NSParagraphStyle *)value;
+ NSString *marker = paragraphStyle.textLists.firstObject.markerFormat;
+ return @{
+ @"checked" : marker == NSTextListMarkerCheck ? CheckedValueString
+ : UnckedValueString
+ };
+}
+
#pragma mark - Init
- (instancetype)initWithInput:(id)input {
@@ -186,14 +215,13 @@ - (BOOL)handleNewlinesInRange:(NSRange)range replacementText:(NSString *)text {
#pragma mark - Detection
-- (BOOL)styleCondition:(id)value {
+- (BOOL)styleCondition:(id)value range:(NSRange)range {
NSParagraphStyle *paragraphStyle = (NSParagraphStyle *)value;
if (!paragraphStyle || paragraphStyle.textLists.count != 1)
return NO;
NSString *marker = paragraphStyle.textLists.firstObject.markerFormat;
- return [marker isEqualToString:NSTextListMarkerBox] ||
- [marker isEqualToString:NSTextListMarkerCheck];
+ return marker == NSTextListMarkerBox || marker == NSTextListMarkerCheck;
}
- (BOOL)detectStyle:(NSRange)range {
@@ -203,7 +231,7 @@ - (BOOL)detectStyle:(NSRange)range {
withInput:_input
inRange:range
withCondition:^BOOL(id value, NSRange r) {
- return [self styleCondition:value];
+ return [self styleCondition:value range:r];
}];
}
@@ -212,7 +240,7 @@ - (BOOL)detectStyle:(NSRange)range {
atIndex:range.location
checkPrevious:YES
withCondition:^BOOL(id value, NSRange r) {
- return [self styleCondition:value];
+ return [self styleCondition:value range:r];
}];
}
@@ -221,7 +249,7 @@ - (BOOL)anyOccurence:(NSRange)range {
withInput:_input
inRange:range
withCondition:^BOOL(id value, NSRange r) {
- return [self styleCondition:value];
+ return [self styleCondition:value range:r];
}];
}
@@ -230,7 +258,7 @@ - (BOOL)anyOccurence:(NSRange)range {
withInput:_input
inRange:range
withCondition:^BOOL(id value, NSRange r) {
- return [self styleCondition:value];
+ return [self styleCondition:value range:r];
}];
}
diff --git a/ios/styles/ColorStyle.mm b/ios/styles/ColorStyle.mm
index 7ef3a346d..33e185217 100644
--- a/ios/styles/ColorStyle.mm
+++ b/ios/styles/ColorStyle.mm
@@ -26,6 +26,30 @@ + (BOOL)isParagraphStyle {
return NO;
}
++ (BOOL)isSelfClosing {
+ return NO;
+}
+
++ (const char *)tagName {
+ return "font";
+}
+
++ (const char *_Nullable)subTagName {
+ return nil;
+}
+
++ (NSAttributedStringKey)attributeKey {
+ return NSForegroundColorAttributeName;
+}
+
++ (NSDictionary *)getParametersFromValue:(id)value {
+ UIColor *color = value;
+
+ return @{
+ @"color" : [color hexString],
+ };
+}
+
- (void)applyStyle:(NSRange)range color:(UIColor *)color {
BOOL isStylePresent = [self detectStyle:range color:color];
@@ -72,67 +96,35 @@ - (void)addTypingAttributes:(UIColor *)color {
#pragma mark - Remove attributes
- (void)removeAttributes:(NSRange)range {
- NSTextStorage *textStorage = _input->textView.textStorage;
-
- LinkStyle *linkStyle = _input->stylesDict[@(Link)];
- InlineCodeStyle *inlineCodeStyle = _input->stylesDict[@(InlineCode)];
- BlockQuoteStyle *blockQuoteStyle = _input->stylesDict[@(BlockQuote)];
- MentionStyle *mentionStyle = _input->stylesDict[@(Mention)];
-
- NSArray *linkOccurrences = [linkStyle findAllOccurences:range];
- NSArray *inlineOccurrences =
- [inlineCodeStyle findAllOccurences:range];
- NSArray *blockQuoteOccurrences =
- [blockQuoteStyle findAllOccurences:range];
- NSArray *mentionOccurrences =
- [mentionStyle findAllOccurences:range];
-
- NSMutableSet *points = [NSMutableSet new];
- [points addObject:@(range.location)];
- [points addObject:@(NSMaxRange(range))];
-
- for (StylePair *pair in linkOccurrences) {
- [points addObject:@([pair.rangeValue rangeValue].location)];
- [points addObject:@(NSMaxRange([pair.rangeValue rangeValue]))];
- }
- for (StylePair *pair in inlineOccurrences) {
- [points addObject:@([pair.rangeValue rangeValue].location)];
- [points addObject:@(NSMaxRange([pair.rangeValue rangeValue]))];
- }
- for (StylePair *pair in blockQuoteOccurrences) {
- [points addObject:@([pair.rangeValue rangeValue].location)];
- [points addObject:@(NSMaxRange([pair.rangeValue rangeValue]))];
- }
- for (StylePair *pair in mentionOccurrences) {
- [points addObject:@([pair.rangeValue rangeValue].location)];
- [points addObject:@(NSMaxRange([pair.rangeValue rangeValue]))];
- }
+ NSTextStorage *ts = _input->textView.textStorage;
+ if (range.length == 0)
+ return;
+
+ NSUInteger len = ts.length;
+ if (range.location >= len)
+ return;
+
+ NSUInteger max = MIN(NSMaxRange(range), len);
+
+ [ts beginEditing];
- NSArray *sortedPoints =
- [points.allObjects sortedArrayUsingSelector:@selector(compare:)];
-
- [textStorage beginEditing];
- for (NSUInteger i = 0; i < sortedPoints.count - 1; i++) {
- NSUInteger start = sortedPoints[i].unsignedIntegerValue;
- NSUInteger end = sortedPoints[i + 1].unsignedIntegerValue;
- if (start >= end)
- continue;
-
- NSRange subrange = NSMakeRange(start, end - start);
-
- UIColor *baseColor = [self baseColorForLocation:subrange.location];
-
- [textStorage addAttribute:NSForegroundColorAttributeName
- value:baseColor
- range:subrange];
- [textStorage addAttribute:NSUnderlineColorAttributeName
- value:baseColor
- range:subrange];
- [textStorage addAttribute:NSStrikethroughColorAttributeName
- value:baseColor
- range:subrange];
+ for (NSUInteger i = range.location; i < max; i++) {
+ UIColor *restoreColor = [self originalColorAtIndex:i];
+
+ [ts addAttribute:NSForegroundColorAttributeName
+ value:restoreColor
+ range:NSMakeRange(i, 1)];
+
+ [ts addAttribute:NSUnderlineColorAttributeName
+ value:restoreColor
+ range:NSMakeRange(i, 1)];
+
+ [ts addAttribute:NSStrikethroughColorAttributeName
+ value:restoreColor
+ range:NSMakeRange(i, 1)];
}
- [textStorage endEditing];
+
+ [ts endEditing];
}
- (void)removeTypingAttributes {
@@ -141,7 +133,7 @@ - (void)removeTypingAttributes {
NSRange selectedRange = _input->textView.selectedRange;
NSUInteger location = selectedRange.location;
- UIColor *baseColor = [self baseColorForLocation:location];
+ UIColor *baseColor = [self originalColorAtIndex:location];
newTypingAttrs[NSForegroundColorAttributeName] = baseColor;
newTypingAttrs[NSUnderlineColorAttributeName] = baseColor;
@@ -151,98 +143,115 @@ - (void)removeTypingAttributes {
#pragma mark - Detection
-- (BOOL)isInStyle:(NSRange)range styleType:(StyleType)styleType {
- id style = _input->stylesDict[@(styleType)];
-
- return (range.length > 0 ? [style anyOccurence:range]
- : [style detectStyle:range]);
++ (BOOL)isInLink:(NSDictionary *)attrs
+ sameColor:(UIColor *)color
+ input:(EnrichedTextInputView *)input {
+ id link = input->stylesDict[@(Link)];
+ NSAttributedStringKey key = [[link class] attributeKey];
+ return attrs[key] && [color isEqual:input->config.linkColor];
}
-- (BOOL)isInCodeBlockAndHasTheSameColor:(id)value:(NSRange)range {
- BOOL isInCodeBlock = [self isInStyle:range styleType:CodeBlock];
++ (BOOL)isInInlineCode:(NSDictionary *)attrs
+ sameColor:(UIColor *)color
+ input:(EnrichedTextInputView *)input {
+ id inlineCode = input->stylesDict[@(InlineCode)];
+ NSAttributedStringKey key = [[inlineCode class] attributeKey];
+ return attrs[key] && [color isEqual:input->config.inlineCodeFgColor];
+}
- return isInCodeBlock &&
- [(UIColor *)value isEqualToColor:[_input->config codeBlockFgColor]];
++ (BOOL)isInBlockQuote:(NSDictionary *)attrs
+ sameColor:(UIColor *)color
+ input:(EnrichedTextInputView *)input {
+ id bq = input->stylesDict[@(BlockQuote)];
+ NSAttributedStringKey key = [[bq class] attributeKey];
+ return attrs[key] && [color isEqual:input->config.blockquoteColor];
}
-- (BOOL)inLinkAndForegroundColorIsLinkColor:(id)value:(NSRange)range {
- BOOL isInLink = [self isInStyle:range styleType:Link];
++ (BOOL)isInMention:(NSDictionary *)attrs
+ location:(NSUInteger)loc
+ sameColor:(UIColor *)color
+ input:(EnrichedTextInputView *)input {
+ id mention = input->stylesDict[@(Mention)];
+ NSAttributedStringKey key = [[mention class] attributeKey];
+ if (!attrs[key])
+ return NO;
- return isInLink &&
- [(UIColor *)value isEqualToColor:[_input->config linkColor]];
-}
+ MentionStyle *mentionStyle = (MentionStyle *)mention;
+ MentionParams *params = [mentionStyle getMentionParamsAt:loc];
+ if (!params)
+ return NO;
-- (BOOL)inInlineCodeAndHasTheSameColor:(id)value:(NSRange)range {
- BOOL isInInlineCode = [self isInStyle:range styleType:InlineCode];
+ MentionStyleProps *props =
+ [input->config mentionStylePropsForIndicator:params.indicator];
- return isInInlineCode &&
- [(UIColor *)value isEqualToColor:[_input->config inlineCodeFgColor]];
+ return [color isEqual:props.color];
}
-- (BOOL)inBlockQuoteAndHasTheSameColor:(id)value:(NSRange)range {
- BOOL isInBlockQuote = [self isInStyle:range styleType:BlockQuote];
-
- return isInBlockQuote &&
- [(UIColor *)value isEqualToColor:[_input->config blockquoteColor]];
++ (BOOL)isInCodeBlock:(NSDictionary *)attrs
+ sameColor:(UIColor *)color
+ input:(EnrichedTextInputView *)input {
+ id code = input->stylesDict[@(CodeBlock)];
+ NSAttributedStringKey key = [[code class] attributeKey];
+ return attrs[key] && [color isEqual:input->config.codeBlockFgColor];
}
-- (BOOL)inMentionAndHasTheSameColor:(id)value:(NSRange)range {
- MentionStyle *mentionStyle = _input->stylesDict[@(Mention)];
- BOOL isInMention = [self isInStyle:range styleType:Mention];
+#pragma mark - Main detection entry
- if (!isInMention)
+- (BOOL)styleCondition:(id _Nullable)value range:(NSRange)range {
+ if (!value)
return NO;
- MentionParams *params = [mentionStyle getMentionParamsAt:range.location];
- if (params == nil)
+ UIColor *color = (UIColor *)value;
+
+ if ([color isEqual:_input->config.primaryColor])
return NO;
- MentionStyleProps *styleProps =
- [_input->config mentionStylePropsForIndicator:params.indicator];
+ NSTextStorage *ts = _input->textView.textStorage;
+ NSUInteger len = ts.length;
- return [(UIColor *)value isEqualToColor:styleProps.color];
-}
+ BOOL useTypingAttributes =
+ (range.length == 0) || (range.location == 0) || (range.location >= len);
-- (BOOL)styleCondition:(id)value:(NSRange)range {
- if (value == nil) {
- return NO;
- }
+ NSDictionary *attrs = nil;
- if ([(UIColor *)value isEqualToColor:_input->config.primaryColor]) {
- return NO;
+ NSUInteger loc = range.location;
+ if (loc >= len && len > 0)
+ loc = len - 1;
+
+ if (useTypingAttributes) {
+ attrs = _input->textView.typingAttributes;
+ } else {
+ attrs = [ts attributesAtIndex:loc effectiveRange:nil];
}
- if ([self inBlockQuoteAndHasTheSameColor:value:range]) {
+
+ if ([ColorStyle isInLink:attrs sameColor:color input:_input])
return NO;
- }
- if ([self inLinkAndForegroundColorIsLinkColor:value:range]) {
+ if ([ColorStyle isInInlineCode:attrs sameColor:color input:_input])
return NO;
- }
- if ([self inInlineCodeAndHasTheSameColor:value:range]) {
+ if ([ColorStyle isInBlockQuote:attrs sameColor:color input:_input])
return NO;
- }
- if ([self inMentionAndHasTheSameColor:value:range]) {
+
+ if ([ColorStyle isInMention:attrs location:loc sameColor:color input:_input])
return NO;
- }
- if ([self isInCodeBlockAndHasTheSameColor:value:range]) {
+
+ if ([ColorStyle isInCodeBlock:attrs sameColor:color input:_input])
return NO;
- }
return YES;
}
- (BOOL)detectStyle:(NSRange)range {
if (range.length >= 1) {
- UIColor *color = [self getColorInRange:range];
return [OccurenceUtils detect:NSForegroundColorAttributeName
withInput:_input
inRange:range
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
} else {
id value =
_input->textView.typingAttributes[NSForegroundColorAttributeName];
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}
}
@@ -253,7 +262,7 @@ - (BOOL)detectStyle:(NSRange)range color:(UIColor *)color {
inRange:range
withCondition:^BOOL(id _Nullable value, NSRange range) {
return [(UIColor *)value isEqualToColor:color] &&
- [self styleCondition:value:range];
+ [self styleCondition:value range:range];
}];
} else {
return [OccurenceUtils detect:NSForegroundColorAttributeName
@@ -262,7 +271,7 @@ - (BOOL)detectStyle:(NSRange)range color:(UIColor *)color {
checkPrevious:NO
withCondition:^BOOL(id _Nullable value, NSRange range) {
return [(UIColor *)value isEqualToColor:color] &&
- [self styleCondition:value:range];
+ [self styleCondition:value range:range];
}];
}
}
@@ -280,7 +289,7 @@ - (BOOL)anyOccurence:(NSRange)range {
withInput:_input
inRange:range
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
}
@@ -289,7 +298,7 @@ - (BOOL)anyOccurence:(NSRange)range {
withInput:_input
inRange:range
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
}
@@ -336,31 +345,46 @@ - (UIColor *)getColorInRange:(NSRange)range {
return color;
}
-- (UIColor *)baseColorForLocation:(NSUInteger)location {
- BOOL inLink = [self isInStyle:NSMakeRange(location, 0) styleType:Link];
- BOOL inInlineCode = [self isInStyle:NSMakeRange(location, 0)
- styleType:InlineCode];
- BOOL inBlockQuote = [self isInStyle:NSMakeRange(location, 0)
- styleType:BlockQuote];
- BOOL inMention = [self isInStyle:NSMakeRange(location, 0) styleType:Mention];
-
- UIColor *baseColor = [_input->config primaryColor];
- if (inMention) {
- MentionStyle *mentionStyle = _input->stylesDict[@(Mention)];
- MentionParams *params = [mentionStyle getMentionParamsAt:location];
- if (params != nil) {
- MentionStyleProps *styleProps =
- [_input->config mentionStylePropsForIndicator:params.indicator];
- baseColor = styleProps.color;
+- (UIColor *)originalColorAtIndex:(NSUInteger)index {
+ NSTextStorage *ts = _input->textView.textStorage;
+ NSUInteger len = ts.length;
+
+ if (len == 0)
+ return _input->config.primaryColor;
+
+ if (index >= len)
+ index = len - 1;
+
+ NSDictionary *attrs = [ts attributesAtIndex:index effectiveRange:nil];
+
+ UIColor *color = attrs[NSForegroundColorAttributeName];
+
+ if (!color)
+ return _input->config.primaryColor;
+
+ if ([ColorStyle isInLink:attrs sameColor:color input:_input])
+ return _input->config.linkColor;
+
+ if ([ColorStyle isInInlineCode:attrs sameColor:color input:_input])
+ return _input->config.inlineCodeFgColor;
+
+ if ([ColorStyle isInBlockQuote:attrs sameColor:color input:_input])
+ return _input->config.blockquoteColor;
+
+ if ([ColorStyle isInMention:attrs
+ location:index
+ sameColor:color
+ input:_input]) {
+ MentionStyle *mention = (MentionStyle *)_input->stylesDict[@(Mention)];
+ MentionParams *p = [mention getMentionParamsAt:index];
+ if (p) {
+ MentionStyleProps *props =
+ [_input->config mentionStylePropsForIndicator:p.indicator];
+ return props.color;
}
- } else if (inLink) {
- baseColor = [_input->config linkColor];
- } else if (inInlineCode) {
- baseColor = [_input->config inlineCodeFgColor];
- } else if (inBlockQuote) {
- baseColor = [_input->config blockquoteColor];
}
- return baseColor;
+
+ return _input->config.primaryColor;
}
- (void)removeColorInSelectedRange {
diff --git a/ios/styles/ContentStyle.mm b/ios/styles/ContentStyle.mm
index 58f262175..083b8dc13 100644
--- a/ios/styles/ContentStyle.mm
+++ b/ios/styles/ContentStyle.mm
@@ -25,6 +25,45 @@ + (BOOL)isParagraphStyle {
return YES;
}
++ (BOOL)isSelfClosing {
+ return YES;
+}
+
++ (const char *)tagName {
+ return "content";
+}
+
++ (const char *)subTagName {
+ return nil;
+}
+
++ (NSAttributedStringKey)attributeKey {
+ return ContentAttributeName;
+}
+
++ (NSDictionary *)getParametersFromValue:(id)value {
+ ContentParams *contentParams = value;
+
+ NSMutableDictionary *params = [@{
+ @"type" : contentParams.type,
+ @"src" : contentParams.url,
+ @"text" : contentParams.text,
+ } mutableCopy];
+
+ if (contentParams.attributes) {
+ NSData *data =
+ [contentParams.attributes dataUsingEncoding:NSUTF8StringEncoding];
+ NSDictionary *extraAttrs = [NSJSONSerialization JSONObjectWithData:data
+ options:0
+ error:nil];
+ if ([extraAttrs isKindOfClass:[NSDictionary class]]) {
+ [params addEntriesFromDictionary:extraAttrs];
+ }
+ }
+
+ return params;
+}
+
#pragma mark - Init
- (instancetype)initWithInput:(id)input {
@@ -81,6 +120,7 @@ - (void)addContentAtRange:(NSRange)range params:(ContentParams *)params {
NSMutableDictionary *attrs = [_input->defaultTypingAttributes mutableCopy];
attrs[NSAttachmentAttributeName] = [self prepareAttachment:params];
attrs[ContentAttributeName] = params;
+ attrs[ReadOnlyParagraphKey] = @(YES);
[TextInsertionUtils replaceText:placeholder
at:range
@@ -118,15 +158,12 @@ - (void)removeAttributes:(NSRange)range {
- (BOOL (^)(id _Nullable, NSRange))contentCondition {
return ^BOOL(id _Nullable value, NSRange range) {
- NSString *substr =
- [self->_input->textView.textStorage.string substringWithRange:range];
- return ([value isKindOfClass:BaseLabelAttachment.class] &&
- [substr isEqualToString:placeholder]);
+ return [self styleCondition:value range:range];
};
}
- (BOOL)styleCondition:(id)value range:(NSRange)range {
- return self.contentCondition(value, range);
+ return value != nullptr;
}
- (BOOL)detectStyle:(NSRange)range {
diff --git a/ios/styles/DividerStyle.mm b/ios/styles/DividerStyle.mm
index 26d53bcc5..037ca56df 100644
--- a/ios/styles/DividerStyle.mm
+++ b/ios/styles/DividerStyle.mm
@@ -18,6 +18,22 @@ + (BOOL)isParagraphStyle {
return YES;
}
++ (const char *)tagName {
+ return "hr";
+}
+
++ (const char *)subTagName {
+ return nil;
+}
+
++ (BOOL)isSelfClosing {
+ return YES;
+}
+
++ (NSAttributedStringKey)attributeKey {
+ return NSAttachmentAttributeName;
+}
+
- (instancetype)initWithInput:(id)input {
if (self = [super init]) {
_input = (EnrichedTextInputView *)input;
@@ -126,6 +142,7 @@ - (NSDictionary *)prepareAttributes {
NSFontAttributeName : config.primaryFont,
NSForegroundColorAttributeName : config.primaryColor,
NSFontAttributeName : config.primaryFont,
+ ReadOnlyParagraphKey : @(YES),
};
}
@@ -137,16 +154,86 @@ - (void)insertDividerAt:(NSUInteger)index setSelection:(BOOL)setSelection {
NSString *string = textStorage.string;
NSDictionary *dividerAttrs = [self prepareAttributes];
-
_input->blockEmitting = YES;
- BOOL needsNewlineBefore =
- (index > 0 && [string characterAtIndex:index - 1] != '\n');
- BOOL needsNewlineAfter =
- (index < string.length && [string characterAtIndex:index] != '\n');
+
+ // empty paragraph
+ NSRange paragraphRange =
+ [string paragraphRangeForRange:NSMakeRange(index, 0)];
+
+ NSString *paragraphText = [string substringWithRange:paragraphRange];
+ BOOL isEmptyParagraph =
+ (paragraphRange.length <= 1 || [paragraphText isEqualToString:@"\n"]);
+
+ if (isEmptyParagraph) {
+ [textStorage beginEditing];
+
+ if (paragraphRange.length > 0) {
+ [textStorage replaceCharactersInRange:paragraphRange withString:@""];
+ }
+ NSUInteger insertPos = paragraphRange.location;
+ [TextInsertionUtils insertText:placeholder
+ at:insertPos
+ additionalAttributes:input->defaultTypingAttributes
+ input:input
+ withSelection:NO];
+
+ NSRange phRange = NSMakeRange(insertPos, 1);
+ [TextInsertionUtils replaceText:placeholder
+ at:phRange
+ additionalAttributes:dividerAttrs
+ input:_input
+ withSelection:setSelection];
+
+ [TextInsertionUtils insertText:@"\n"
+ at:insertPos + 1
+ additionalAttributes:input->defaultTypingAttributes
+ input:input
+ withSelection:NO];
+
+ [textStorage endEditing];
+
+ if (setSelection) {
+ _input->textView.selectedRange = NSMakeRange(insertPos + 2, 0);
+ }
+
+ _input->blockEmitting = NO;
+ return;
+ }
+
+ BOOL beforeIsNewline =
+ (index > 0 && [string characterAtIndex:index - 1] == '\n');
+
+ BOOL afterIsNewline =
+ (index < string.length && [string characterAtIndex:index] == '\n');
+
+ BOOL isPrevDivider = NO;
+ if (index > 0) {
+ id prevAttr = [textStorage attribute:NSAttachmentAttributeName
+ atIndex:index - 1
+ effectiveRange:nil];
+ if ([prevAttr isKindOfClass:[DividerAttachment class]]) {
+ isPrevDivider = YES;
+ }
+ }
+
+ BOOL isNextDivider = NO;
+ if (index < string.length) {
+ id nextAttr = [textStorage attribute:NSAttachmentAttributeName
+ atIndex:index
+ effectiveRange:nil];
+ if ([nextAttr isKindOfClass:[DividerAttachment class]]) {
+ isNextDivider = YES;
+ }
+ }
+
+ BOOL needsNewlineBefore = !beforeIsNewline && !isPrevDivider;
+ BOOL needsNewlineAfter = !afterIsNewline && !isNextDivider;
NSInteger insertIndex = index;
input->textView.typingAttributes = input->defaultTypingAttributes;
+
[textStorage beginEditing];
+
if (needsNewlineBefore) {
[TextInsertionUtils insertText:@"\n"
at:insertIndex
@@ -162,13 +249,14 @@ - (void)insertDividerAt:(NSUInteger)index setSelection:(BOOL)setSelection {
input:input
withSelection:NO];
- NSRange placeholderRange = NSMakeRange(insertIndex, 1);
+ NSRange phRange = NSMakeRange(insertIndex, 1);
[TextInsertionUtils replaceText:placeholder
- at:placeholderRange
+ at:phRange
additionalAttributes:dividerAttrs
input:_input
withSelection:setSelection];
+
if (needsNewlineAfter) {
[TextInsertionUtils insertText:@"\n"
at:insertIndex
@@ -177,11 +265,13 @@ - (void)insertDividerAt:(NSUInteger)index setSelection:(BOOL)setSelection {
withSelection:NO];
insertIndex += 1;
}
+
[textStorage endEditing];
if (setSelection) {
_input->textView.selectedRange = NSMakeRange(insertIndex + 1, 0);
}
+
_input->blockEmitting = NO;
}
diff --git a/ios/utils/CheckboxHitTestUtils.h b/ios/utils/CheckboxHitTestUtils.h
new file mode 100644
index 000000000..d981acac4
--- /dev/null
+++ b/ios/utils/CheckboxHitTestUtils.h
@@ -0,0 +1,13 @@
+#import
+
+@class EnrichedTextInputView;
+
+@interface CheckboxHitTestUtils : NSObject
+
++ (CGRect)checkboxRectAtGlyphIndex:(NSUInteger)glyphIndex
+ inInput:(EnrichedTextInputView *)input;
+
++ (NSInteger)hitTestCheckboxAtPoint:(CGPoint)pt
+ inInput:(EnrichedTextInputView *)input;
+
+@end
diff --git a/ios/utils/CheckboxHitTestUtils.mm b/ios/utils/CheckboxHitTestUtils.mm
new file mode 100644
index 000000000..fb7856d5f
--- /dev/null
+++ b/ios/utils/CheckboxHitTestUtils.mm
@@ -0,0 +1,76 @@
+#import "CheckboxHitTestUtils.h"
+#import "EnrichedTextInputView.h"
+#import "InputConfig.h"
+#import "StyleHeaders.h"
+
+@implementation CheckboxHitTestUtils
+
++ (CGRect)checkboxRectAtGlyphIndex:(NSUInteger)glyphIndex
+ inInput:(EnrichedTextInputView *)input {
+ InputTextView *textView = input->textView;
+ NSTextStorage *storage = textView.textStorage;
+
+ NSUInteger charIndex =
+ [textView.layoutManager characterIndexForGlyphAtIndex:glyphIndex];
+
+ if (charIndex >= storage.length) {
+ return CGRectNull;
+ }
+
+ CheckBoxStyle *checkboxStyle =
+ (CheckBoxStyle *)input->stylesDict[@([CheckBoxStyle getStyleType])];
+
+ if (!checkboxStyle ||
+ ![checkboxStyle detectStyle:NSMakeRange(charIndex, 0)]) {
+ return CGRectNull;
+ }
+
+ InputConfig *config = input->config;
+ if (!config) {
+ return CGRectNull;
+ }
+
+ CGRect lineRect =
+ [textView.layoutManager lineFragmentRectForGlyphAtIndex:glyphIndex
+ effectiveRange:nil];
+
+ CGFloat checkboxWidth = config.checkBoxWidth;
+ CGFloat checkboxHeight = config.checkBoxHeight;
+ CGFloat marginLeft = config.checkboxListMarginLeft;
+ CGFloat checkboxGap = config.checkboxListGapWidth;
+
+ CGFloat originY =
+ lineRect.origin.y + (lineRect.size.height - checkboxHeight) / 2.0;
+
+ return CGRectMake(marginLeft + checkboxGap, originY, checkboxWidth,
+ checkboxHeight);
+}
+
++ (NSInteger)hitTestCheckboxAtPoint:(CGPoint)point
+ inInput:(EnrichedTextInputView *)input {
+ UITextView *textView = input->textView;
+
+ NSUInteger glyphIndex =
+ [textView.layoutManager glyphIndexForPoint:point
+ inTextContainer:textView.textContainer
+ fractionOfDistanceThroughGlyph:nil];
+
+ if (glyphIndex == NSNotFound) {
+ return -1;
+ }
+
+ CGRect checkboxRect = [self checkboxRectAtGlyphIndex:glyphIndex
+ inInput:input];
+
+ if (CGRectIsNull(checkboxRect)) {
+ return -1;
+ }
+
+ if (!CGRectContainsPoint(checkboxRect, point)) {
+ return -1;
+ }
+
+ return [textView.layoutManager characterIndexForGlyphAtIndex:glyphIndex];
+}
+
+@end
diff --git a/ios/utils/ContentHitTestUtils.h b/ios/utils/ContentHitTestUtils.h
new file mode 100644
index 000000000..da0d6cf8a
--- /dev/null
+++ b/ios/utils/ContentHitTestUtils.h
@@ -0,0 +1,12 @@
+#import
+@class EnrichedTextInputView;
+
+@interface ContentHitTestUtils : NSObject
+
++ (CGRect)contentRectAtGlyphIndex:(NSUInteger)glyphIndex
+ inInput:(EnrichedTextInputView *)input;
+
++ (NSInteger)hitTestContentAtPoint:(CGPoint)pt
+ inInput:(EnrichedTextInputView *)input;
+
+@end
diff --git a/ios/utils/ContentHitTestUtils.mm b/ios/utils/ContentHitTestUtils.mm
new file mode 100644
index 000000000..cd88dd859
--- /dev/null
+++ b/ios/utils/ContentHitTestUtils.mm
@@ -0,0 +1,77 @@
+#import "ContentHitTestUtils.h"
+#import "EnrichedTextInputView.h"
+
+@implementation ContentHitTestUtils
+
++ (CGRect)contentRectAtGlyphIndex:(NSUInteger)glyphIndex
+ inInput:(EnrichedTextInputView *)input {
+ if (!input || !input->textView) {
+ return CGRectNull;
+ }
+
+ NSLayoutManager *layoutManager = input->textView.layoutManager;
+ NSTextContainer *textContainer = input->textView.textContainer;
+
+ if (glyphIndex == NSNotFound || glyphIndex >= layoutManager.numberOfGlyphs) {
+ return CGRectNull;
+ }
+
+ CGRect glyphRect =
+ [layoutManager boundingRectForGlyphRange:NSMakeRange(glyphIndex, 1)
+ inTextContainer:textContainer];
+
+ CGFloat hitboxPadding = 1.0;
+ return CGRectInset(glyphRect, -hitboxPadding, -hitboxPadding);
+}
+
++ (NSInteger)hitTestContentAtPoint:(CGPoint)point
+ inInput:(EnrichedTextInputView *)input {
+ if (!input || !input->textView) {
+ return -1;
+ }
+
+ UITextView *textView = input->textView;
+ NSLayoutManager *layoutManager = textView.layoutManager;
+ NSTextContainer *textContainer = textView.textContainer;
+
+ CGFloat fraction = 0.0;
+
+ NSUInteger glyphIndex = [layoutManager glyphIndexForPoint:point
+ inTextContainer:textContainer
+ fractionOfDistanceThroughGlyph:&fraction];
+
+ if (glyphIndex == NSNotFound) {
+ return -1;
+ }
+
+ NSUInteger charIndex =
+ [layoutManager characterIndexForGlyphAtIndex:glyphIndex];
+
+ if (charIndex == NSNotFound || charIndex >= textView.textStorage.length) {
+ return -1;
+ }
+
+ unichar character = [textView.textStorage.string characterAtIndex:charIndex];
+
+ if (character != 0xFFFC) { // attachment object replacement char
+ return -1;
+ }
+
+ id attachment = [textView.textStorage attribute:NSAttachmentAttributeName
+ atIndex:charIndex
+ effectiveRange:nil];
+
+ if (!attachment) {
+ return -1;
+ }
+
+ CGRect contentRect = [self contentRectAtGlyphIndex:glyphIndex inInput:input];
+
+ if (CGRectIsNull(contentRect) || !CGRectContainsPoint(contentRect, point)) {
+ return -1;
+ }
+
+ return (NSInteger)charIndex;
+}
+
+@end
diff --git a/ios/utils/DividerHitTestUtils.h b/ios/utils/DividerHitTestUtils.h
new file mode 100644
index 000000000..153ab1bff
--- /dev/null
+++ b/ios/utils/DividerHitTestUtils.h
@@ -0,0 +1,12 @@
+#import
+@class EnrichedTextInputView;
+
+@interface DividerHitTestUtils : NSObject
+
++ (CGRect)dividerRectAtGlyphIndex:(NSUInteger)glyphIndex
+ inInput:(EnrichedTextInputView *)input;
+
++ (NSInteger)hitTestDividerAtPoint:(CGPoint)pt
+ inInput:(EnrichedTextInputView *)input;
+
+@end
diff --git a/ios/utils/DividerHitTestUtils.mm b/ios/utils/DividerHitTestUtils.mm
new file mode 100644
index 000000000..44c9cd4e4
--- /dev/null
+++ b/ios/utils/DividerHitTestUtils.mm
@@ -0,0 +1,77 @@
+#import "DividerHitTestUtils.h"
+#import "EnrichedTextInputView.h"
+#import "StyleHeaders.h"
+
+@implementation DividerHitTestUtils
+
++ (CGRect)dividerRectAtGlyphIndex:(NSUInteger)glyphIndex
+ inInput:(EnrichedTextInputView *)input {
+ if (!input || !input->textView) {
+ return CGRectNull;
+ }
+
+ UITextView *textView = input->textView;
+ NSLayoutManager *layoutManager = textView.layoutManager;
+ NSTextStorage *textStorage = textView.textStorage;
+
+ NSUInteger charIndex =
+ [layoutManager characterIndexForGlyphAtIndex:glyphIndex];
+
+ if (charIndex >= textStorage.length) {
+ return CGRectNull;
+ }
+
+ id attachment = [textStorage attribute:NSAttachmentAttributeName
+ atIndex:charIndex
+ effectiveRange:nil];
+
+ if (![attachment isKindOfClass:[DividerAttachment class]]) {
+ return CGRectNull;
+ }
+
+ DividerAttachment *dividerAttachment = (DividerAttachment *)attachment;
+
+ // Position of the text line where the divider lives
+ CGRect lineFragmentRect =
+ [layoutManager lineFragmentRectForGlyphAtIndex:glyphIndex
+ effectiveRange:nil];
+
+ CGFloat dividerHeight = dividerAttachment.height;
+ CGFloat dividerY = lineFragmentRect.origin.y +
+ (lineFragmentRect.size.height - dividerHeight) / 2.0;
+
+ return CGRectMake(lineFragmentRect.origin.x, dividerY,
+ lineFragmentRect.size.width, dividerHeight);
+}
+
++ (NSInteger)hitTestDividerAtPoint:(CGPoint)point
+ inInput:(EnrichedTextInputView *)input {
+ if (!input || !input->textView) {
+ return -1;
+ }
+
+ UITextView *textView = input->textView;
+ NSLayoutManager *layoutManager = textView.layoutManager;
+ NSTextContainer *textContainer = textView.textContainer;
+
+ NSUInteger glyphIndex = [layoutManager glyphIndexForPoint:point
+ inTextContainer:textContainer
+ fractionOfDistanceThroughGlyph:nil];
+
+ if (glyphIndex == NSNotFound) {
+ return -1;
+ }
+
+ CGRect dividerRect = [self dividerRectAtGlyphIndex:glyphIndex inInput:input];
+
+ if (CGRectIsNull(dividerRect)) {
+ return -1;
+ }
+ if (!CGRectContainsPoint(dividerRect, point)) {
+ return -1;
+ }
+
+ return (NSInteger)[layoutManager characterIndexForGlyphAtIndex:glyphIndex];
+}
+
+@end
diff --git a/ios/utils/StyleHeaders.h b/ios/utils/StyleHeaders.h
index ea9c5deef..3f9336259 100644
--- a/ios/utils/StyleHeaders.h
+++ b/ios/utils/StyleHeaders.h
@@ -7,6 +7,8 @@
#import "MentionParams.h"
#import "ParameterizedStyleProtocol.h"
+static NSString *const ReadOnlyParagraphKey = @"ReadOnlyParagraph";
+
@interface BoldStyle : NSObject
@end
@@ -119,7 +121,8 @@
- (ImageData *)getImageDataAt:(NSUInteger)location;
@end
-@interface CheckBoxStyle : NSObject
+@interface CheckBoxStyle
+ : NSObject
- (BOOL)handleBackspaceInRange:(NSRange)range replacementText:(NSString *)text;
- (BOOL)handleNewlinesInRange:(NSRange)range replacementText:(NSString *)text;
- (BOOL)isCheckedAt:(NSUInteger)location;
@@ -133,7 +136,8 @@
- (void)insertDividerAtNewLine;
@end
-@interface ContentStyle : NSObject
+@interface ContentStyle
+ : NSObject
- (void)addContentAtRange:(NSRange)range params:(ContentParams *)params;
- (ContentParams *)getContentParams:(NSUInteger)location;
@end