Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,13 @@ class EnrichedTextInputViewManager :
view?.experimentalSynchronousEvents = value
}

override fun setIOSExperimentalHTMLSerializer(
view: EnrichedTextInputView?,
value: Boolean,
) {
// iOS only prop
}

override fun focus(view: EnrichedTextInputView?) {
view?.requestFocusProgrammatically()
}
Expand Down
1 change: 1 addition & 0 deletions apps/example/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,7 @@ export default function App() {
onChangeText={(e) => handleChangeText(e.nativeEvent)}
onChangeHtml={(e) => handleChangeHtml(e.nativeEvent)}
onChangeState={(e) => handleChangeState(e.nativeEvent)}
iOSExperimentalHTMLSerializer={true}
onLinkDetected={handleLinkDetected}
onMentionDetected={console.log}
onStartMention={handleStartMention}
Expand Down
1 change: 1 addition & 0 deletions ios/EnrichedTextInputView.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ NS_ASSUME_NONNULL_BEGIN
NSMutableDictionary<NSNumber *, NSArray<NSNumber *> *> *blockingStyles;
@public
BOOL blockEmitting;
BOOL useExperimentalHTMLParser;
}
- (CGSize)measureSize:(CGFloat)maxWidth;
- (void)emitOnLinkDetectedEvent:(NSString *)text
Expand Down
3 changes: 3 additions & 0 deletions ios/EnrichedTextInputView.mm
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ - (void)setDefaults {
blockEmitting = NO;
_emitFocusBlur = YES;
_emitTextChange = NO;
useExperimentalHTMLParser = NO;

defaultTypingAttributes =
[[NSMutableDictionary<NSAttributedStringKey, id> alloc] init];
Expand Down Expand Up @@ -282,6 +283,8 @@ - (void)updateProps:(Props::Shared const &)props
BOOL isFirstMount = NO;
BOOL stylePropChanged = NO;

useExperimentalHTMLParser = newViewProps.iOSExperimentalHTMLSerializer;

// initial config
if (config == nullptr) {
isFirstMount = YES;
Expand Down
10 changes: 10 additions & 0 deletions ios/inputParser/EnrichedAttributedStringHTMLSerializer.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#import <Foundation/Foundation.h>

@class HTMLNode;
@class HTMLTextNode;

@interface EnrichedAttributedStringHTMLSerializer : NSObject
- (instancetype)initWithStyles:(NSDictionary<NSNumber *, id> *)stylesDict;
- (NSString *)buildHtmlFromAttributedString:(NSAttributedString *)text
pretify:(BOOL)pretify;
@end
281 changes: 281 additions & 0 deletions ios/inputParser/EnrichedAttributedStringHTMLSerializer.mm
Original file line number Diff line number Diff line change
@@ -0,0 +1,281 @@
#import "EnrichedAttributedStringHTMLSerializer.h"
#import "EnrichedAttributedStringHTMLSerializerTagUtils.h"
#import "HtmlNode.h"
#import "StyleHeaders.h"

@implementation EnrichedAttributedStringHTMLSerializer {
NSDictionary<NSNumber *, id<BaseStyleProtocol>> *_styles;
NSArray<id<BaseStyleProtocol>> *_inlineStyles;
NSArray<id<BaseStyleProtocol>> *_paragraphStyles;
}

- (instancetype)initWithStyles:(NSDictionary<NSNumber *, id> *)stylesDict {
self = [super init];
if (!self)
return nil;

_styles = stylesDict ?: @{};

NSMutableArray *inlineStylesArray = [NSMutableArray array];
NSMutableArray *paragraphStylesArray = [NSMutableArray array];

NSArray *allKeys = stylesDict.allKeys;
for (NSInteger i = 0; i < allKeys.count; i++) {
NSNumber *key = allKeys[i];
id<BaseStyleProtocol> style = stylesDict[key];
Class cls = style.class;

BOOL isParagraph = ([cls respondsToSelector:@selector(isParagraphStyle)]) &&
[cls isParagraphStyle];

if (isParagraph) {
[paragraphStylesArray addObject:style];
} else {
[inlineStylesArray addObject:style];
}
}

_inlineStyles = inlineStylesArray.copy;
_paragraphStyles = paragraphStylesArray.copy;

return self;
}

- (NSString *)buildHtmlFromAttributedString:(NSAttributedString *)text
pretify:(BOOL)pretify {

if (text.length == 0)
return DefaultHtmlValue;

HTMLElement *root = [self buildRootNodeFromAttributedString:text];

NSMutableData *buffer = [NSMutableData data];
[self createHtmlFromNode:root into:buffer pretify:pretify];

return [[NSString alloc] initWithData:buffer encoding:NSUTF8StringEncoding];
}

- (HTMLElement *)buildRootNodeFromAttributedString:(NSAttributedString *)text {
NSString *plain = text.string;

HTMLElement *root = [HTMLElement new];
root.tag = HtmlTagHTML;

HTMLElement *br = [HTMLElement new];
br.tag = HtmlTagBR;
br.selfClosing = YES;

__block id<BaseStyleProtocol> previousParagraphStyle = 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];
previousParagraphStyle = nil;
previousNode = nil;
return;
}

id<BaseStyleProtocol> paragraphStyle =
[self detectParagraphStyle:text
paragraphRange:paragraphRange];

HTMLElement *container = [self
containerForParagraphStyle:paragraphStyle
previousParagraphStyle:previousParagraphStyle
previousNode:previousNode
rootNode:root];

previousParagraphStyle = paragraphStyle;
previousNode = container;

HTMLElement *target =
[self nextContainerForParagraphStyle:paragraphStyle
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;
}

- (HTMLElement *)nextContainerForParagraphStyle:
(id<BaseStyleProtocol> _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<BaseStyleProtocol> _Nullable)
detectParagraphStyle:(NSAttributedString *)text
paragraphRange:(NSRange)paragraphRange {
NSDictionary *attrsAtStart = [text attributesAtIndex:paragraphRange.location
effectiveRange:nil];
id<BaseStyleProtocol> _Nullable foundParagraphStyle = nil;
for (NSInteger i = 0; i < _paragraphStyles.count; i++) {
id<BaseStyleProtocol> 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 = HtmlParagraphTag;
[rootNode.children addObject:outer];
return outer;
}

BOOL isTheSameParagraph = currentParagraphType == previousParagraphType;
id<BaseStyleProtocol> styleObject = _styles[currentParagraphType];
Class styleClass = styleObject.class;

BOOL hasSubTags = [styleClass subTagName] != NULL;

if (isTheSameParagraph && hasSubTags)
return previousNode;

HTMLElement *outer = [HTMLElement new];

outer.tag = [styleClass tagName];

[rootNode.children addObject:outer];
return outer;
}

- (HTMLElement *)
containerForParagraphStyle:(id<BaseStyleProtocol> _Nullable)currentStyle
previousParagraphStyle:(id<BaseStyleProtocol> _Nullable)previousStyle
previousNode:(HTMLElement *)previousNode
rootNode:(HTMLElement *)rootNode {
if (!currentStyle) {
HTMLElement *outer = [HTMLElement new];
outer.tag = HtmlParagraphTag;
[rootNode.children addObject:outer];
return outer;
}

BOOL sameStyle = (currentStyle == previousStyle);
Class styleClass = currentStyle.class;
BOOL hasSub = ([styleClass subTagName] != NULL);

if (sameStyle && hasSub)
return previousNode;

HTMLElement *outer = [HTMLElement new];
outer.tag = [styleClass tagName];
[rootNode.children addObject:outer];
return outer;
}

- (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 (NSInteger i = 0; i < _inlineStyles.count; i++) {
id<BaseStyleProtocol> styleObject = _inlineStyles[i];
Class styleClass = styleObject.class;

NSAttributedStringKey attributeKey = [styleClass attributeKey];
id value = attrs[attributeKey];

if (!value || ![styleObject styleCondition:value range:range])
continue;

HTMLElement *wrap = [HTMLElement new];
const char *tag = [styleClass tagName];

wrap.tag = tag;
wrap.attributes =
[styleClass respondsToSelector:@selector(getParametersFromValue:)]
? [styleClass getParametersFromValue:value]
: nullptr;
wrap.selfClosing = [styleClass isSelfClosing];
[wrap.children addObject:currentNode];
currentNode = wrap;
}

return currentNode;
}

- (void)createHtmlFromNode:(HTMLNode *)node
into:(NSMutableData *)buffer
pretify:(BOOL)pretify {
if ([node isKindOfClass:[HTMLTextNode class]]) {
HTMLTextNode *t = (HTMLTextNode *)node;
appendEscapedRange(buffer, t.source, t.range);
return;
}

if (![node isKindOfClass:[HTMLElement class]])
return;

HTMLElement *element = (HTMLElement *)node;

BOOL addNewLineBefore = pretify && isBlockTag(element.tag);
BOOL addNewLineAfter = pretify && needsNewLineAfter(element.tag);

if (element.selfClosing) {
appendSelfClosingTag(buffer, element.tag, element.attributes,
addNewLineBefore);
return;
}

appendOpenTag(buffer, element.tag, element.attributes ?: nullptr,
addNewLineBefore);

for (HTMLNode *child in element.children)
[self createHtmlFromNode:child into:buffer pretify:pretify];

if (addNewLineAfter)
appendC(buffer, NewLine);

appendCloseTag(buffer, element.tag);
}

@end
Loading
Loading