From 8016f7305d45145ac8424912a010d1e99d087b7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20=C5=BB=C3=B3=C5=82kiewski?= Date: Tue, 16 Jun 2026 11:13:50 +0200 Subject: [PATCH 01/11] feat(iOS): add custom style --- apps/example/src/constants/editorConfig.ts | 4 + ios/EnrichedTextInputView.mm | 86 +++++++- ios/customStyleData/CustomStyleData.h | 16 ++ ios/customStyleData/CustomStyleData.mm | 46 ++++ ios/extensions/ColorExtension.h | 4 + ios/extensions/ColorExtension.mm | 56 +++++ ios/htmlParser/HtmlParser.mm | 127 ++++++++++- .../InputAttributesManager.mm | 19 +- ios/inputHtmlParser/InputHtmlParser.mm | 6 + ios/interfaces/EnrichedTextStyleHeaders.h | 3 + ios/interfaces/StyleBase.h | 1 + ios/interfaces/StyleBase.mm | 7 + ios/interfaces/StyleHeaders.h | 11 + ios/interfaces/StyleTypeEnum.h | 1 + ios/styles/CustomStyle.mm | 208 ++++++++++++++++++ ios/styles/EnrichedTextStyles.mm | 3 + ios/textHtmlParser/TextHtmlParser.mm | 6 + ios/utils/StyleUtils.mm | 38 +++- src/native/EnrichedTextInput.tsx | 38 +++- src/spec/EnrichedTextInputNativeComponent.ts | 13 ++ src/types.ts | 26 ++- src/web/EnrichedTextInput.tsx | 1 + src/web/useOnChangeState.ts | 4 + 23 files changed, 681 insertions(+), 43 deletions(-) create mode 100644 ios/customStyleData/CustomStyleData.h create mode 100644 ios/customStyleData/CustomStyleData.mm create mode 100644 ios/styles/CustomStyle.mm diff --git a/apps/example/src/constants/editorConfig.ts b/apps/example/src/constants/editorConfig.ts index 196377a03..0fcada6d3 100644 --- a/apps/example/src/constants/editorConfig.ts +++ b/apps/example/src/constants/editorConfig.ts @@ -33,6 +33,10 @@ export const DEFAULT_STYLES: StylesState = { mention: DEFAULT_STYLE_STATE, checkboxList: DEFAULT_STYLE_STATE, alignment: 'left', + customStyle: { + foregroundColor: '', + backgroundColor: '', + }, }; export const DEFAULT_LINK_STATE = { diff --git a/ios/EnrichedTextInputView.mm b/ios/EnrichedTextInputView.mm index 32733b390..4369ccb0e 100644 --- a/ios/EnrichedTextInputView.mm +++ b/ios/EnrichedTextInputView.mm @@ -1,6 +1,7 @@ #import "EnrichedTextInputView.h" #import "AlignmentUtils.h" #import "AttachmentLayoutUtils.h" +#import "ColorExtension.h" #import "CoreText/CoreText.h" #import "DotReplacementUtils.h" #import "HtmlParser.h" @@ -17,6 +18,7 @@ #import "WordsUtils.h" #import "ZeroWidthSpaceUtils.h" #import +#import #import #import #import @@ -60,6 +62,7 @@ @implementation EnrichedTextInputView { NSString *_submitBehavior; NSDictionary *_capturedAttributesBeforeChange; NSString *_recentlyEmittedAlignment; + CustomStyleData *_recentlyEmittedCustomStyle; } @synthesize blockEmitting = blockEmitting; @@ -1092,6 +1095,15 @@ - (void)tryUpdatingActiveStyles { updateNeeded = YES; } + // detect custom style change + CustomStyle *customStyle = stylesDict[@([CustomStyle getType])]; + CustomStyleData *currentCustomStyle = + [customStyle getCustomStyleDataAt:textView.selectedRange.location]; + if (currentCustomStyle != _recentlyEmittedCustomStyle && + ![currentCustomStyle isEqual:_recentlyEmittedCustomStyle]) { + updateNeeded = YES; + } + if (updateNeeded) { auto emitter = [self getEventEmitter]; if (emitter != nullptr) { @@ -1099,6 +1111,7 @@ - (void)tryUpdatingActiveStyles { _activeStyles = newActiveStyles; _blockedStyles = newBlockedStyles; _recentlyEmittedAlignment = currentAlignment; + _recentlyEmittedCustomStyle = currentCustomStyle; emitter->onChangeState( {.bold = GET_STYLE_STATE([BoldStyle getType]), @@ -1120,7 +1133,14 @@ - (void)tryUpdatingActiveStyles { .codeBlock = GET_STYLE_STATE([CodeBlockStyle getType]), .image = GET_STYLE_STATE([ImageStyle getType]), .checkboxList = GET_STYLE_STATE([CheckboxListStyle getType]), - .alignment = [currentAlignment UTF8String]}); + .alignment = [currentAlignment UTF8String], + .customStyle = { + .foregroundColor = + [[currentCustomStyle.foregroundColor rgbaString] UTF8String] + ?: "", + .backgroundColor = + [[currentCustomStyle.backgroundColor rgbaString] UTF8String] + ?: ""}}); } } @@ -1268,6 +1288,9 @@ - (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args { if (!_placeholderLabel.isHidden) { [self refreshPlaceholderLabelStyles]; } + } else if ([commandName isEqualToString:@"setStyle"]) { + NSString *styleJSON = (NSString *)args[0]; + [self setStyle:styleJSON]; } } @@ -1475,6 +1498,52 @@ - (void)toggleRegularStyle:(StyleType)type { } } +- (void)setStyle:(NSString *)styleJSON { + NSData *jsonData = [styleJSON dataUsingEncoding:NSUTF8StringEncoding]; + if (jsonData == nil) + return; + NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:jsonData + options:0 + error:nil]; + if (dict == nil) + return; + + NSRange selectedRange = textView.selectedRange; + CustomStyle *customStyleClass = + (CustomStyle *)stylesDict[@([CustomStyle getType])]; + if (customStyleClass == nil) + return; + + if (![StyleUtils handleStyleBlocksAndConflicts:[CustomStyle getType] + range:selectedRange + forHost:self]) { + return; + } + + // Convert raw JSON values (NSNumber ARGB integers from processColor) to + // UIColor. NSNull is passed through as-is so mergeFromDict: can clear + // the color when the caller explicitly passes null. + NSMutableDictionary *processedDict = [NSMutableDictionary new]; + + id fgRaw = dict[@"foregroundColor"]; + if (fgRaw != nil) { + processedDict[@"foregroundColor"] = [fgRaw isKindOfClass:[NSNull class]] + ? [NSNull null] + : [RCTConvert UIColor:fgRaw]; + } + + id bgRaw = dict[@"backgroundColor"]; + if (bgRaw != nil) { + processedDict[@"backgroundColor"] = [bgRaw isKindOfClass:[NSNull class]] + ? [NSNull null] + : [RCTConvert UIColor:bgRaw]; + } + + [customStyleClass applyStyleFromDict:processedDict + selectedRange:selectedRange]; + [self anyTextMayHaveBeenModified]; +} + - (void)toggleCheckboxList:(BOOL)checked { CheckboxListStyle *style = (CheckboxListStyle *)stylesDict[@([CheckboxListStyle getType])]; @@ -1827,6 +1896,10 @@ - (void)emitOnContextMenuItemPressEvent:(NSString *)itemText { AlignmentStyle *alignmentStyle = stylesDict[@([AlignmentStyle getType])]; NSString *currentAlignment = [alignmentStyle getStyleState]; + CustomStyle *customStyle = stylesDict[@([CustomStyle getType])]; + CustomStyleData *contextCustomStyleData = + [customStyle getCustomStyleDataAt:textView.selectedRange.location]; + emitter->onContextMenuItemPress( {.itemText = [itemText toCppString], .selectedText = [selectedText toCppString], @@ -1853,7 +1926,16 @@ - (void)emitOnContextMenuItemPressEvent:(NSString *)itemText { .image = GET_STYLE_STATE([ImageStyle getType]), .mention = GET_STYLE_STATE([MentionStyle getType]), .checkboxList = GET_STYLE_STATE([CheckboxListStyle getType]), - .alignment = [currentAlignment UTF8String]}}); + .alignment = [currentAlignment UTF8String], + .customStyle = { + .foregroundColor = + [[contextCustomStyleData.foregroundColor rgbaString] + UTF8String] + ?: "", + .backgroundColor = + [[contextCustomStyleData.backgroundColor rgbaString] + UTF8String] + ?: ""}}}); } } diff --git a/ios/customStyleData/CustomStyleData.h b/ios/customStyleData/CustomStyleData.h new file mode 100644 index 000000000..4a97c1ba4 --- /dev/null +++ b/ios/customStyleData/CustomStyleData.h @@ -0,0 +1,16 @@ +#pragma once +#import + +@interface CustomStyleData : NSObject + +@property(nonatomic, strong, nullable) UIColor *foregroundColor; +@property(nonatomic, strong, nullable) UIColor *backgroundColor; + +- (BOOL)isEmpty; + +// Applies a partial update from a dict (keys: "foregroundColor", +// "backgroundColor"). A key absent from the dict leaves the field unchanged; +// NSNull or any non-UIColor value clears it; a UIColor value sets it. +- (void)mergeFromDict:(NSDictionary *)dict; + +@end diff --git a/ios/customStyleData/CustomStyleData.mm b/ios/customStyleData/CustomStyleData.mm new file mode 100644 index 000000000..c08f1fd54 --- /dev/null +++ b/ios/customStyleData/CustomStyleData.mm @@ -0,0 +1,46 @@ +#import "CustomStyleData.h" + +@implementation CustomStyleData + +- (BOOL)isEmpty { + return _foregroundColor == nil && _backgroundColor == nil; +} + +- (void)mergeFromDict:(NSDictionary *)dict { + id fgVal = dict[@"foregroundColor"]; + if (fgVal != nil) { + self.foregroundColor = + [fgVal isKindOfClass:[UIColor class]] ? (UIColor *)fgVal : nil; + } + id bgVal = dict[@"backgroundColor"]; + if (bgVal != nil) { + self.backgroundColor = + [bgVal isKindOfClass:[UIColor class]] ? (UIColor *)bgVal : nil; + } +} + +- (BOOL)isEqual:(id)object { + if (self == object) + return YES; + if (![object isKindOfClass:[CustomStyleData class]]) + return NO; + CustomStyleData *other = (CustomStyleData *)object; + BOOL fgEqual = (_foregroundColor == other.foregroundColor) || + [_foregroundColor isEqual:other.foregroundColor]; + BOOL bgEqual = (_backgroundColor == other.backgroundColor) || + [_backgroundColor isEqual:other.backgroundColor]; + return fgEqual && bgEqual; +} + +- (NSUInteger)hash { + return [_foregroundColor hash] ^ [_backgroundColor hash]; +} + +- (id)copyWithZone:(NSZone *)zone { + CustomStyleData *copy = [[CustomStyleData allocWithZone:zone] init]; + copy.foregroundColor = self.foregroundColor; + copy.backgroundColor = self.backgroundColor; + return copy; +} + +@end diff --git a/ios/extensions/ColorExtension.h b/ios/extensions/ColorExtension.h index 1ed38519a..6428f049a 100644 --- a/ios/extensions/ColorExtension.h +++ b/ios/extensions/ColorExtension.h @@ -4,4 +4,8 @@ @interface UIColor (ColorExtension) - (BOOL)isEqualToColor:(UIColor *)otherColor; - (UIColor *)colorWithAlphaIfNotTransparent:(CGFloat)newAlpha; +- (NSString *)rgbaString; +/// Parses a CSS rgba() string, e.g. @"rgba(255, 0, 0, 1.00)". Returns nil if +/// the string is not a valid rgba() value. ++ (UIColor *_Nullable)colorFromRgbaString:(NSString *_Nullable)rgba; @end diff --git a/ios/extensions/ColorExtension.mm b/ios/extensions/ColorExtension.mm index 0cc59b727..c15df871e 100644 --- a/ios/extensions/ColorExtension.mm +++ b/ios/extensions/ColorExtension.mm @@ -35,4 +35,60 @@ - (UIColor *)colorWithAlphaIfNotTransparent:(CGFloat)newAlpha { } return self; } + +- (NSString *)rgbaString { + CGFloat red = 0.0; + CGFloat green = 0.0; + CGFloat blue = 0.0; + CGFloat alpha = 0.0; + + // getRed:green:blue:alpha: returns YES if the color can be converted to RGB. + // It natively handles monochrome/grayscale colors as well. + if ([self getRed:&red green:&green blue:&blue alpha:&alpha]) { + // Convert 0.0-1.0 floats to 0-255 integers for RGB + int r = (int)round(red * 255.0); + int g = (int)round(green * 255.0); + int b = (int)round(blue * 255.0); + + return + [NSString stringWithFormat:@"rgba(%d, %d, %d, %.2f)", r, g, b, alpha]; + } + + // Fallback for unsupported color + return @""; +} + ++ (UIColor *)colorFromRgbaString:(NSString *)rgba { + if (rgba.length == 0) + return nil; + + static NSRegularExpression *regex; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + regex = [NSRegularExpression + regularExpressionWithPattern: + @"rgba\\(\\s*(\\d+)\\s*,\\s*(\\d+)\\s*,\\s*(\\d+)\\s*," + @"\\s*([\\d.]+)\\s*\\)" + options:NSRegularExpressionCaseInsensitive + error:nil]; + }); + + NSTextCheckingResult *match = + [regex firstMatchInString:rgba + options:0 + range:NSMakeRange(0, rgba.length)]; + if (!match || match.numberOfRanges < 5) + return nil; + + CGFloat r = + [[rgba substringWithRange:[match rangeAtIndex:1]] integerValue] / 255.0; + CGFloat g = + [[rgba substringWithRange:[match rangeAtIndex:2]] integerValue] / 255.0; + CGFloat b = + [[rgba substringWithRange:[match rangeAtIndex:3]] integerValue] / 255.0; + CGFloat a = [[rgba substringWithRange:[match rangeAtIndex:4]] doubleValue]; + + return [UIColor colorWithRed:r green:g blue:b alpha:a]; +} + @end diff --git a/ios/htmlParser/HtmlParser.mm b/ios/htmlParser/HtmlParser.mm index a6e84dd6e..32b8b164e 100644 --- a/ios/htmlParser/HtmlParser.mm +++ b/ios/htmlParser/HtmlParser.mm @@ -1,6 +1,9 @@ #import "HtmlParser.h" #import "AlignmentEntry.h" #import "AlignmentUtils.h" +#import "ColorExtension.h" +#import "CustomStyleData.h" +#include "GumboParser.hpp" #import "ImageData.h" #import "LinkData.h" #import "MentionParams.h" @@ -8,8 +11,6 @@ #import "StyleHeaders.h" #import "StylePair.h" -#include "GumboParser.hpp" - @implementation HtmlParser + (BOOL)isBlockTag:(NSString *)tagName { @@ -41,9 +42,10 @@ + (BOOL)isBlockTag:(NSString *)tagName { * you MUST add it to the `textTags` set below. */ + (NSString *)stripExtraWhiteSpacesAndNewlines:(NSString *)html { - NSSet *textTags = [NSSet setWithObjects:@"p", @"h1", @"h2", @"h3", @"h4", - @"h5", @"h6", @"li", @"b", @"a", @"s", - @"mention", @"code", @"u", @"i", nil]; + NSSet *textTags = + [NSSet setWithObjects:@"p", @"h1", @"h2", @"h3", @"h4", @"h5", @"h6", + @"li", @"b", @"a", @"s", @"mention", @"code", @"u", + @"i", @"span", nil]; NSMutableString *output = [NSMutableString stringWithCapacity:html.length]; NSMutableString *currentTagBuffer = [NSMutableString string]; @@ -817,6 +819,13 @@ + (NSArray *_Nonnull)getTextAndStylesFromHtml:(NSString *_Nonnull)fixedHtml { [styleArr addObject:@([BlockQuoteStyle getType])]; } else if ([tagName isEqualToString:@"codeblock"]) { [styleArr addObject:@([CodeBlockStyle getType])]; + } else if ([tagName isEqualToString:@"span"]) { + CustomStyleData *data = [self parseCustomStyleDataFromSpanParams:params]; + if (data == nil || data.isEmpty) { + continue; + } + [styleArr addObject:@([CustomStyle getType])]; + stylePair.styleValue = data; } else { // some other external tags like span just don't get put into the // processed styles @@ -849,6 +858,7 @@ + (NSString *)parseToHtmlFromRange:(NSRange)range BOOL inCodeBlock = NO; BOOL inCheckboxList = NO; unichar lastCharacter = 0; + CustomStyleData *lastCustomStyleData = nil; for (int i = 0; i < text.length; i++) { NSRange currentRange = NSMakeRange(offset + i, 1); @@ -988,6 +998,7 @@ + (NSString *)parseToHtmlFromRange:(NSRange)range // clear the previous styles previousActiveStyles = [[NSSet alloc] init]; + lastCustomStyleData = nil; // next character opens new paragraph newLine = YES; @@ -1149,6 +1160,23 @@ + (NSString *)parseToHtmlFromRange:(NSRange)range } } + // Force close+reopen if CustomStyle is continuously active but its data + // changed, so adjacent runs with different styles produce separate + // tags instead of being merged into one. + NSNumber *customType = @([CustomStyle getType]); + if (![endedStyles member:customType] && + [currentActiveStyles member:customType] && + [previousActiveStyles member:customType]) { + CustomStyle *customStyleObj = + (CustomStyle *)host.stylesDict[customType]; + CustomStyleData *currentData = + [customStyleObj getStoredCustomStyleDataAt:currentRange.location]; + if (![currentData isEqual:lastCustomStyleData]) { + [fixedEndedStyles addObject:customType]; + [stylesToBeReAdded addObject:customType]; + } + } + // they are sorted in a descending order NSArray *sortedEndedStyles = [fixedEndedStyles sortedArrayUsingDescriptors:@[ [NSSortDescriptor @@ -1194,6 +1222,11 @@ + (NSString *)parseToHtmlFromRange:(NSRange)range // append the letter and escape it if needed [result appendString:[NSString stringByEscapingHtml:currentCharacterStr]]; + // track CustomStyleData for the next character's data-change check + lastCustomStyleData = + [(CustomStyle *)host.stylesDict[@([CustomStyle getType])] + getStoredCustomStyleDataAt:currentRange.location]; + // save current styles for next character's checks previousActiveStyles = currentActiveStyles; } @@ -1413,6 +1446,34 @@ + (NSString *)tagContentForStyle:(NSNumber *)style [style isEqualToNumber:@([CodeBlockStyle getType])]) { // blockquotes and codeblock use

tags the same way lists use

  • return [NSString stringWithFormat:@"p%@", cssStyleString]; + } else if ([style isEqualToNumber:@([CustomStyle getType])]) { + if (openingTag) { + CustomStyle *customStyle = + (CustomStyle *)host.stylesDict[@([CustomStyle getType])]; + if (customStyle != nil) { + CustomStyleData *data = + [customStyle getStoredCustomStyleDataAt:location]; + if (data != nil && !data.isEmpty) { + NSMutableString *cssProps = [NSMutableString string]; + NSString *fg = [[data foregroundColor] rgbaString]; + NSString *bg = [[data backgroundColor] rgbaString]; + if (fg != nil) { + [cssProps appendFormat:@"color: %@;", fg]; + } + if (bg != nil) { + if (cssProps.length > 0) + [cssProps appendString:@" "]; + [cssProps appendFormat:@"background-color: %@;", bg]; + } + if (cssProps.length > 0) { + return [NSString stringWithFormat:@"span style=\"%@\"", cssProps]; + } + } + } + return @"span"; + } else { + return @"span"; + } } return @""; } @@ -1437,6 +1498,62 @@ + (NSString *)prepareCssStyleString:(NSInteger)location return @""; } ++ (CustomStyleData *_Nullable)parseCustomStyleDataFromSpanParams: + (NSString *)params { + static NSRegularExpression *styleAttrRegex; + static NSRegularExpression *fgRegex; + static NSRegularExpression *bgRegex; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + styleAttrRegex = [NSRegularExpression + regularExpressionWithPattern:@"style\\s*=\\s*[\"']([^\"']*)[\"']" + options:NSRegularExpressionCaseInsensitive + error:nil]; + // Captures everything after "color:" until a semicolon or end of string + fgRegex = [NSRegularExpression + regularExpressionWithPattern:@"(?:^|;)\\s*color\\s*:\\s*([^;]+)" + options:NSRegularExpressionCaseInsensitive + error:nil]; + + // Captures everything after "background-color:" until a semicolon or end of + // string + bgRegex = [NSRegularExpression + regularExpressionWithPattern:@"background-color\\s*:\\s*([^;]+)" + options:NSRegularExpressionCaseInsensitive + error:nil]; + }); + + NSTextCheckingResult *attrMatch = + [styleAttrRegex firstMatchInString:params + options:0 + range:NSMakeRange(0, params.length)]; + if (!attrMatch) + return nil; + + NSString *css = [params substringWithRange:[attrMatch rangeAtIndex:1]]; + CustomStyleData *data = [[CustomStyleData alloc] init]; + + NSTextCheckingResult *fgMatch = + [fgRegex firstMatchInString:css + options:0 + range:NSMakeRange(0, css.length)]; + if (fgMatch) { + data.foregroundColor = [UIColor + colorFromRgbaString:[css substringWithRange:[fgMatch rangeAtIndex:1]]]; + } + + NSTextCheckingResult *bgMatch = + [bgRegex firstMatchInString:css + options:0 + range:NSMakeRange(0, css.length)]; + if (bgMatch) { + data.backgroundColor = [UIColor + colorFromRgbaString:[css substringWithRange:[bgMatch rangeAtIndex:1]]]; + } + + return data.isEmpty ? nil : data; +} + + (void)checkForAlignments:(NSArray *)tagData plainText:(NSString *)plainText foundAlignments:(NSMutableArray *)foundAlignments diff --git a/ios/inputAttributesManager/InputAttributesManager.mm b/ios/inputAttributesManager/InputAttributesManager.mm index fc07d871f..02b953eae 100644 --- a/ios/inputAttributesManager/InputAttributesManager.mm +++ b/ios/inputAttributesManager/InputAttributesManager.mm @@ -95,17 +95,20 @@ - (void)handleDirtyRangesStyling { [ZeroWidthSpaceUtils applyKernForZeroWidthSpacesInRange:dirtyRange host:_input]; - // Sort style types so paragraph styles come first. Their broad visual - // attributes (e.g. foreground color, font) are laid down before inline - // styles override them on their specific sub-ranges. + // Sort style types by priority (0=paragraph, 1=custom, 2=inline) so + // paragraph styles come first. Their broad visual attributes (e.g. + // foreground color, font) are laid down before custom and inline styles + // override them on their specific sub-ranges. NSArray *sortedStyleTypes = [presentStyles.allKeys sortedArrayUsingComparator:^NSComparisonResult(NSNumber *a, NSNumber *b) { - BOOL aPara = [_input->stylesDict[a] isParagraph]; - BOOL bPara = [_input->stylesDict[b] isParagraph]; - if (aPara == bPara) - return NSOrderedSame; - return aPara ? NSOrderedAscending : NSOrderedDescending; + NSInteger aPriority = [_input->stylesDict[a] stylePriority]; + NSInteger bPriority = [_input->stylesDict[b] stylePriority]; + if (aPriority < bPriority) + return NSOrderedAscending; + if (aPriority > bPriority) + return NSOrderedDescending; + return NSOrderedSame; }]; // re-apply meta-attributes and apply visual styling following the saved diff --git a/ios/inputHtmlParser/InputHtmlParser.mm b/ios/inputHtmlParser/InputHtmlParser.mm index bc9e5875b..f66e8e558 100644 --- a/ios/inputHtmlParser/InputHtmlParser.mm +++ b/ios/inputHtmlParser/InputHtmlParser.mm @@ -179,6 +179,12 @@ - (void)applyProcessedStyles:(NSArray *)processedStyles } } } + } else if ([styleType isEqualToNumber:@([CustomStyle getType])]) { + CustomStyle *customStyle = (CustomStyle *)baseStyle; + [customStyle setCustomStyleData:stylePair.styleValue + range:styleRange + withTyping:shouldAddTypingAttr + withDirtyRange:YES]; } else { [baseStyle add:styleRange withTyping:shouldAddTypingAttr diff --git a/ios/interfaces/EnrichedTextStyleHeaders.h b/ios/interfaces/EnrichedTextStyleHeaders.h index 008f0fce4..6d1a73af8 100644 --- a/ios/interfaces/EnrichedTextStyleHeaders.h +++ b/ios/interfaces/EnrichedTextStyleHeaders.h @@ -34,6 +34,9 @@ @interface EnrichedTextH6Style : H6Style @end +@interface EnrichedTextCustomStyle : CustomStyle +@end + @interface EnrichedTextBlockQuoteStyle : BlockQuoteStyle @end diff --git a/ios/interfaces/StyleBase.h b/ios/interfaces/StyleBase.h index 41a3d27de..5075c5513 100644 --- a/ios/interfaces/StyleBase.h +++ b/ios/interfaces/StyleBase.h @@ -12,6 +12,7 @@ - (NSString *)getValue; - (NSString *)getMarkerPrefix; - (BOOL)isParagraph; +- (NSInteger)stylePriority; - (BOOL)needsZWS; - (BOOL)appliesStylingToTyping; - (instancetype)initWithHost:(id)host; diff --git a/ios/interfaces/StyleBase.mm b/ios/interfaces/StyleBase.mm index f43c53224..36a393863 100644 --- a/ios/interfaces/StyleBase.mm +++ b/ios/interfaces/StyleBase.mm @@ -37,6 +37,13 @@ - (BOOL)isParagraph { return false; } +// Returns the application priority for this style. +// 0 = paragraph, 1 = custom (parametric colors), 2 = inline (default). +// Styles are applied in ascending priority order so inline styles win. +- (NSInteger)stylePriority { + return [self isParagraph] ? 0 : 2; +} + - (BOOL)needsZWS { return NO; } diff --git a/ios/interfaces/StyleHeaders.h b/ios/interfaces/StyleHeaders.h index 568b8e55a..c4613e20e 100644 --- a/ios/interfaces/StyleHeaders.h +++ b/ios/interfaces/StyleHeaders.h @@ -1,9 +1,20 @@ #pragma once +#import "CustomStyleData.h" #import "ImageData.h" #import "LinkData.h" #import "MentionParams.h" #import "StyleBase.h" +@interface CustomStyle : StyleBase +- (void)applyStyleFromDict:(NSDictionary *)dict selectedRange:(NSRange)range; +- (void)setCustomStyleData:(CustomStyleData *)data + range:(NSRange)range + withTyping:(BOOL)withTyping + withDirtyRange:(BOOL)withDirtyRange; +- (CustomStyleData *_Nullable)getCustomStyleDataAt:(NSUInteger)location; +- (CustomStyleData *_Nullable)getStoredCustomStyleDataAt:(NSUInteger)location; +@end + @interface BoldStyle : StyleBase @end diff --git a/ios/interfaces/StyleTypeEnum.h b/ios/interfaces/StyleTypeEnum.h index 701d79eaf..a5c163fc8 100644 --- a/ios/interfaces/StyleTypeEnum.h +++ b/ios/interfaces/StyleTypeEnum.h @@ -15,6 +15,7 @@ typedef NS_ENUM(NSInteger, StyleType) { H4, H5, H6, + Custom, Link, Mention, Image, diff --git a/ios/styles/CustomStyle.mm b/ios/styles/CustomStyle.mm new file mode 100644 index 000000000..024bf8de0 --- /dev/null +++ b/ios/styles/CustomStyle.mm @@ -0,0 +1,208 @@ +#import "CustomStyleData.h" +#import "EnrichedTextInputView.h" +#import "RangeUtils.h" +#import "StyleHeaders.h" + +static NSString *const CustomStyleAttributeName = @"EnrichedCustomStyle"; + +@implementation CustomStyle + ++ (StyleType)getType { + return Custom; +} + +- (NSString *)getKey { + return CustomStyleAttributeName; +} + +- (BOOL)isParagraph { + return NO; +} + +- (NSInteger)stylePriority { + return 1; +} + +- (BOOL)styleCondition:(id)value range:(NSRange)range { + if (![value isKindOfClass:[CustomStyleData class]]) + return NO; + return ![(CustomStyleData *)value isEmpty]; +} + +- (void)applyStyling:(NSRange)range { + if (range.length == 0) + return; + + NSUInteger storageLength = self.host.textView.textStorage.length; + if (storageLength == 0) + return; + + NSRange safeRange = NSMakeRange( + range.location, MIN(range.length, storageLength - range.location)); + + // Enumerate each sub-range that carries its own CustomStyleData so that + // characters with different data values each get the correct visual attrs. + [self.host.textView.textStorage + enumerateAttribute:CustomStyleAttributeName + inRange:safeRange + options:0 + usingBlock:^(id value, NSRange subRange, BOOL *stop) { + if (![value isKindOfClass:[CustomStyleData class]]) + return; + CustomStyleData *data = (CustomStyleData *)value; + if (data.isEmpty) + return; + + NSMutableDictionary *attrs = [NSMutableDictionary dictionary]; + if (data.foregroundColor != nil) { + attrs[NSForegroundColorAttributeName] = data.foregroundColor; + attrs[NSUnderlineColorAttributeName] = data.foregroundColor; + attrs[NSStrikethroughColorAttributeName] = + data.foregroundColor; + } + if (data.backgroundColor != nil) { + attrs[NSBackgroundColorAttributeName] = data.backgroundColor; + } + if (attrs.count == 0) + return; + + // Skip newline characters so background color doesn't bleed. + NSArray *nonNewlineRanges = + [RangeUtils getNonNewlineRangesIn:self.host.textView + range:subRange]; + for (NSValue *rangeVal in nonNewlineRanges) { + [self.host.textView.textStorage + addAttributes:attrs + range:[rangeVal rangeValue]]; + } + }]; +} + +- (void)reapplyFromStylePair:(StylePair *)pair { + NSRange range = [pair.rangeValue rangeValue]; + CustomStyleData *data = (CustomStyleData *)pair.styleValue; + if (data == nil || data.isEmpty) + return; + [self.host.textView.textStorage addAttribute:CustomStyleAttributeName + value:data + range:range]; +} + +- (AttributeEntry *)getEntryIfPresent:(NSRange)range { + CustomStyleData *data = [self getCustomStyleDataAt:range.location]; + if (data == nil || data.isEmpty) + return nullptr; + + AttributeEntry *entry = [[AttributeEntry alloc] init]; + entry.key = CustomStyleAttributeName; + entry.value = data; + return entry; +} + +// MARK: - Public non-standard methods + +- (void)setCustomStyleData:(CustomStyleData *)data + range:(NSRange)range + withTyping:(BOOL)withTyping + withDirtyRange:(BOOL)withDirtyRange { + if (range.length > 0) { + if (data == nil || data.isEmpty) { + [self remove:range withDirtyRange:withDirtyRange]; + return; + } + [self.host.textView.textStorage addAttribute:CustomStyleAttributeName + value:data + range:range]; + if (withDirtyRange) { + [self.host.attributesManager addDirtyRange:range]; + } + } + + if (withTyping) { + if (data == nil || data.isEmpty) { + [self removeTyping]; + } else { + NSMutableDictionary *newTypingAttrs = + [self.host.textView.typingAttributes mutableCopy]; + newTypingAttrs[CustomStyleAttributeName] = data; + self.host.textView.typingAttributes = newTypingAttrs; + } + } +} + +- (CustomStyleData *_Nullable)getCustomStyleDataAt:(NSUInteger)location { + NSRange selectedRange = self.host.textView.selectedRange; + if (self.host.textView.isEditable && selectedRange.length == 0 && + selectedRange.location == location) { + id typingValue = + self.host.textView.typingAttributes[CustomStyleAttributeName]; + if ([typingValue isKindOfClass:[CustomStyleData class]]) + return (CustomStyleData *)typingValue; + } + + return [self getStoredCustomStyleDataAt:location]; +} + +// Reads CustomStyleData directly from textStorage, bypassing typingAttributes. +- (CustomStyleData *_Nullable)getStoredCustomStyleDataAt:(NSUInteger)location { + NSUInteger length = self.host.textView.textStorage.length; + if (length == 0) + return nil; + NSUInteger searchLocation = (location >= length) ? length - 1 : location; + id value = [self.host.textView.textStorage attribute:CustomStyleAttributeName + atIndex:searchLocation + longestEffectiveRange:nil + inRange:NSMakeRange(0, length)]; + if (![value isKindOfClass:[CustomStyleData class]]) + return nil; + return (CustomStyleData *)value; +} + +- (void)applyStyleFromDict:(NSDictionary *)dict selectedRange:(NSRange)range { + BOOL withTyping = range.length == 0; + + if (!withTyping) { + // Enumerate each existing sub-range and merge the partial update into its + // own data so per-character differences (e.g. fg color on some chars) are + // preserved when only one field (e.g. bg color) is being changed. + NSUInteger storageLength = self.host.textView.textStorage.length; + if (storageLength == 0) + return; + + NSRange safeRange = NSMakeRange( + range.location, MIN(range.length, storageLength - range.location)); + + [self.host.textView.textStorage + enumerateAttribute:CustomStyleAttributeName + inRange:safeRange + options:0 + usingBlock:^(id value, NSRange subRange, BOOL *stop) { + CustomStyleData *existing = + [value isKindOfClass:[CustomStyleData class]] + ? (CustomStyleData *)value + : nil; + CustomStyleData *merged = + existing != nil ? [existing copy] + : [[CustomStyleData alloc] init]; + [merged mergeFromDict:dict]; + [self setCustomStyleData:merged + range:subRange + withTyping:NO + withDirtyRange:YES]; + }]; + } else { + // Cursor only: merge into current data and update typing attributes. + CustomStyleData *existing = [self getCustomStyleDataAt:range.location]; + CustomStyleData *merged = + existing != nil ? [existing copy] : [[CustomStyleData alloc] init]; + [merged mergeFromDict:dict]; + [self setCustomStyleData:merged + range:range + withTyping:YES + withDirtyRange:NO]; + [self.host.attributesManager + didRemoveTypingAttribute:CustomStyleAttributeName]; + } +} + +@end diff --git a/ios/styles/EnrichedTextStyles.mm b/ios/styles/EnrichedTextStyles.mm index fde9abfac..f125f66dc 100644 --- a/ios/styles/EnrichedTextStyles.mm +++ b/ios/styles/EnrichedTextStyles.mm @@ -33,6 +33,9 @@ @implementation EnrichedTextH5Style @implementation EnrichedTextH6Style @end +@implementation EnrichedTextCustomStyle +@end + @implementation EnrichedTextBlockQuoteStyle @end diff --git a/ios/textHtmlParser/TextHtmlParser.mm b/ios/textHtmlParser/TextHtmlParser.mm index 1f930a015..bd1ea3728 100644 --- a/ios/textHtmlParser/TextHtmlParser.mm +++ b/ios/textHtmlParser/TextHtmlParser.mm @@ -122,6 +122,12 @@ - (void)applyProcessedStyles:(NSArray *_Nonnull)processedStyles { } } } + } else if ([styleType isEqualToNumber:@([CustomStyle getType])]) { + CustomStyle *customStyle = (CustomStyle *)style; + [customStyle setCustomStyleData:stylePair.styleValue + range:styleRange + withTyping:NO + withDirtyRange:NO]; } else { [style add:styleRange withTyping:NO withDirtyRange:NO]; } diff --git a/ios/utils/StyleUtils.mm b/ios/utils/StyleUtils.mm index f7205b171..32d0d6bf1 100644 --- a/ios/utils/StyleUtils.mm +++ b/ios/utils/StyleUtils.mm @@ -91,7 +91,8 @@ + (NSDictionary *)conflictMap { @([CheckboxListStyle getType]) ], @([ImageStyle getType]) : - @[ @([LinkStyle getType]), @([MentionStyle getType]) ] + @[ @([LinkStyle getType]), @([MentionStyle getType]) ], + @([CustomStyle getType]) : @[] }; } @@ -123,23 +124,35 @@ + (NSDictionary *)blockingMap { @([AlignmentStyle getType]) : @[], @([BlockQuoteStyle getType]) : @[], @([CodeBlockStyle getType]) : @[], - @([ImageStyle getType]) : @[ @([InlineCodeStyle getType]) ] + @([ImageStyle getType]) : @[ @([InlineCodeStyle getType]) ], + @([CustomStyle getType]) : @[] }; } + (NSDictionary *)stylesDictForHost:(id)host isInput:(BOOL)isInput { NSArray *baseClasses = @[ - [BoldStyle class], [ItalicStyle class], - [UnderlineStyle class], [StrikethroughStyle class], - [InlineCodeStyle class], [LinkStyle class], - [MentionStyle class], [H1Style class], - [H2Style class], [H3Style class], - [H4Style class], [H5Style class], - [H6Style class], [UnorderedListStyle class], - [OrderedListStyle class], [CheckboxListStyle class], - [AlignmentStyle class], [BlockQuoteStyle class], - [CodeBlockStyle class], [ImageStyle class] + [BoldStyle class], + [ItalicStyle class], + [UnderlineStyle class], + [StrikethroughStyle class], + [InlineCodeStyle class], + [LinkStyle class], + [MentionStyle class], + [H1Style class], + [H2Style class], + [H3Style class], + [H4Style class], + [H5Style class], + [H6Style class], + [CustomStyle class], + [UnorderedListStyle class], + [OrderedListStyle class], + [CheckboxListStyle class], + [AlignmentStyle class], + [BlockQuoteStyle class], + [CodeBlockStyle class], + [ImageStyle class] ]; NSArray *viewerClasses = @[ @@ -156,6 +169,7 @@ + (NSDictionary *)stylesDictForHost:(id)host [EnrichedTextH4Style class], [EnrichedTextH5Style class], [EnrichedTextH6Style class], + [EnrichedTextCustomStyle class], [EnrichedTextUnorderedListStyle class], [EnrichedTextOrderedListStyle class], [EnrichedTextCheckboxListStyle class], diff --git a/src/native/EnrichedTextInput.tsx b/src/native/EnrichedTextInput.tsx index ba31ba978..77ca3455f 100644 --- a/src/native/EnrichedTextInput.tsx +++ b/src/native/EnrichedTextInput.tsx @@ -14,13 +14,15 @@ import EnrichedTextInputNativeComponent, { type OnMentionDetectedInternal, type OnRequestHtmlResultEvent, } from '../spec/EnrichedTextInputNativeComponent'; -import type { - HostInstance, - MeasureInWindowOnSuccessCallback, - MeasureLayoutOnSuccessCallback, - MeasureOnSuccessCallback, - NativeMethods, - NativeSyntheticEvent, +import { + processColor, + type ColorValue, + type HostInstance, + type MeasureInWindowOnSuccessCallback, + type MeasureLayoutOnSuccessCallback, + type MeasureOnSuccessCallback, + type NativeMethods, + type NativeSyntheticEvent, } from 'react-native'; import { normalizeHtmlStyle } from '../utils/normalizeHtmlStyle'; import { toNativeRegexConfig } from '../utils/regexParser'; @@ -276,6 +278,28 @@ export const EnrichedTextInput = ({ ) => { Commands.setTextAlignment(nullthrows(nativeRef.current), alignment); }, + setStyle: (customStyle: { + foregroundColor?: ColorValue | null; + backgroundColor?: ColorValue | null; + }) => { + const payload: { + foregroundColor?: number | null; + backgroundColor?: number | null; + } = {}; + if ('foregroundColor' in customStyle) { + payload.foregroundColor = + customStyle.foregroundColor != null + ? (processColor(customStyle.foregroundColor) as number) + : null; + } + if ('backgroundColor' in customStyle) { + payload.backgroundColor = + customStyle.backgroundColor != null + ? (processColor(customStyle.backgroundColor) as number) + : null; + } + Commands.setStyle(nullthrows(nativeRef.current), JSON.stringify(payload)); + }, })); const handleMentionEvent = (e: NativeSyntheticEvent) => { diff --git a/src/spec/EnrichedTextInputNativeComponent.ts b/src/spec/EnrichedTextInputNativeComponent.ts index 2eb29ded1..9edc89e7f 100644 --- a/src/spec/EnrichedTextInputNativeComponent.ts +++ b/src/spec/EnrichedTextInputNativeComponent.ts @@ -124,6 +124,10 @@ export interface OnChangeStateEvent { isBlocking: boolean; }; alignment: string; + customStyle: { + foregroundColor: string; + backgroundColor: string; + }; } export interface OnLinkDetected { @@ -275,6 +279,10 @@ export interface OnContextMenuItemPressEvent { isBlocking: boolean; }; alignment: string; + customStyle: { + foregroundColor: string; + backgroundColor: string; + }; }; } @@ -476,6 +484,10 @@ interface NativeCommands { viewRef: React.ElementRef, alignment: string ) => void; + setStyle: ( + viewRef: React.ElementRef, + styleJSON: string + ) => void; } export const Commands: NativeCommands = codegenNativeCommands({ @@ -510,6 +522,7 @@ export const Commands: NativeCommands = codegenNativeCommands({ 'addMention', 'requestHTML', 'setTextAlignment', + 'setStyle', ], }); diff --git a/src/types.ts b/src/types.ts index a669e412a..bc4ed42c2 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,13 +1,13 @@ import type { RefObject } from 'react'; -import type { - ColorValue, - DimensionValue, - NativeMethods, - NativeSyntheticEvent, - ReturnKeyTypeOptions, - TargetedEvent, - TextStyle, - ViewProps, +import { + type ColorValue, + type DimensionValue, + type NativeMethods, + type NativeSyntheticEvent, + type ReturnKeyTypeOptions, + type TargetedEvent, + type TextStyle, + type ViewProps, } from 'react-native'; /** @@ -348,6 +348,10 @@ export interface OnChangeStateEvent { isBlocking: boolean; }; alignment: string; + customStyle: { + foregroundColor: string; + backgroundColor: string; + }; } export interface OnLinkDetected { @@ -428,6 +432,10 @@ export interface EnrichedTextInputInstance extends NativeMethods { setTextAlignment: ( alignment: 'left' | 'center' | 'right' | 'justify' | 'auto' ) => void; + setStyle: (customStyle: { + foregroundColor?: ColorValue | null; + backgroundColor?: ColorValue | null; + }) => void; } export interface ContextMenuItem { diff --git a/src/web/EnrichedTextInput.tsx b/src/web/EnrichedTextInput.tsx index 4ea1742c9..cbb80c88e 100644 --- a/src/web/EnrichedTextInput.tsx +++ b/src/web/EnrichedTextInput.tsx @@ -355,6 +355,7 @@ export const EnrichedTextInput = ({ measureLayout: () => {}, setNativeProps: () => {}, setTextAlignment: () => {}, + setStyle: () => {}, }), [editor] ); diff --git a/src/web/useOnChangeState.ts b/src/web/useOnChangeState.ts index 439f685de..6f4fcb175 100644 --- a/src/web/useOnChangeState.ts +++ b/src/web/useOnChangeState.ts @@ -98,6 +98,10 @@ function buildState( isBlocking: isFormatBlocked('image', editor, htmlStyle), }, alignment: 'left', + customStyle: { + foregroundColor: '', + backgroundColor: '', + }, }; } From fb8cdc0d64c8340f011d12f8d7a14130fbd728fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20=C5=BB=C3=B3=C5=82kiewski?= Date: Tue, 16 Jun 2026 11:36:40 +0200 Subject: [PATCH 02/11] fix: copilot code review --- .../enriched/textinput/EnrichedTextInputViewManager.kt | 7 +++++++ ios/extensions/ColorExtension.mm | 8 ++++---- ios/htmlParser/HtmlParser.mm | 4 ++-- src/native/EnrichedTextInput.tsx | 4 ++-- 4 files changed, 15 insertions(+), 8 deletions(-) diff --git a/android/src/main/java/com/swmansion/enriched/textinput/EnrichedTextInputViewManager.kt b/android/src/main/java/com/swmansion/enriched/textinput/EnrichedTextInputViewManager.kt index eb16451ca..6c9e35c92 100644 --- a/android/src/main/java/com/swmansion/enriched/textinput/EnrichedTextInputViewManager.kt +++ b/android/src/main/java/com/swmansion/enriched/textinput/EnrichedTextInputViewManager.kt @@ -463,6 +463,13 @@ class EnrichedTextInputViewManager : TODO("Not yet implemented") } + override fun setStyle( + view: EnrichedTextInputView?, + styleJSON: String, + ) { + // TODO: Implement + } + override fun measure( context: Context, localData: ReadableMap?, diff --git a/ios/extensions/ColorExtension.mm b/ios/extensions/ColorExtension.mm index c15df871e..01394bd13 100644 --- a/ios/extensions/ColorExtension.mm +++ b/ios/extensions/ColorExtension.mm @@ -46,9 +46,9 @@ - (NSString *)rgbaString { // It natively handles monochrome/grayscale colors as well. if ([self getRed:&red green:&green blue:&blue alpha:&alpha]) { // Convert 0.0-1.0 floats to 0-255 integers for RGB - int r = (int)round(red * 255.0); - int g = (int)round(green * 255.0); - int b = (int)round(blue * 255.0); + int r = (int)(red * 255.0 + 0.5); + int g = (int)(green * 255.0 + 0.5); + int b = (int)(blue * 255.0 + 0.5); return [NSString stringWithFormat:@"rgba(%d, %d, %d, %.2f)", r, g, b, alpha]; @@ -58,7 +58,7 @@ - (NSString *)rgbaString { return @""; } -+ (UIColor *)colorFromRgbaString:(NSString *)rgba { ++ (UIColor *_Nullable)colorFromRgbaString:(NSString *_Nullable)rgba { if (rgba.length == 0) return nil; diff --git a/ios/htmlParser/HtmlParser.mm b/ios/htmlParser/HtmlParser.mm index 32b8b164e..357545697 100644 --- a/ios/htmlParser/HtmlParser.mm +++ b/ios/htmlParser/HtmlParser.mm @@ -1457,10 +1457,10 @@ + (NSString *)tagContentForStyle:(NSNumber *)style NSMutableString *cssProps = [NSMutableString string]; NSString *fg = [[data foregroundColor] rgbaString]; NSString *bg = [[data backgroundColor] rgbaString]; - if (fg != nil) { + if (fg.length > 0) { [cssProps appendFormat:@"color: %@;", fg]; } - if (bg != nil) { + if (bg.length > 0) { if (cssProps.length > 0) [cssProps appendString:@" "]; [cssProps appendFormat:@"background-color: %@;", bg]; diff --git a/src/native/EnrichedTextInput.tsx b/src/native/EnrichedTextInput.tsx index 77ca3455f..72cd2a555 100644 --- a/src/native/EnrichedTextInput.tsx +++ b/src/native/EnrichedTextInput.tsx @@ -286,13 +286,13 @@ export const EnrichedTextInput = ({ foregroundColor?: number | null; backgroundColor?: number | null; } = {}; - if ('foregroundColor' in customStyle) { + if (customStyle.foregroundColor !== undefined) { payload.foregroundColor = customStyle.foregroundColor != null ? (processColor(customStyle.foregroundColor) as number) : null; } - if ('backgroundColor' in customStyle) { + if (customStyle.backgroundColor !== undefined) { payload.backgroundColor = customStyle.backgroundColor != null ? (processColor(customStyle.backgroundColor) as number) From e4f4804934ee20d7ddcea84230b9853dc0350589 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20=C5=BB=C3=B3=C5=82kiewski?= Date: Tue, 16 Jun 2026 11:57:30 +0200 Subject: [PATCH 03/11] fix: reapplying attributes --- ios/EnrichedTextInputView.mm | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/ios/EnrichedTextInputView.mm b/ios/EnrichedTextInputView.mm index 4369ccb0e..48d0c7c21 100644 --- a/ios/EnrichedTextInputView.mm +++ b/ios/EnrichedTextInputView.mm @@ -1502,11 +1502,12 @@ - (void)setStyle:(NSString *)styleJSON { NSData *jsonData = [styleJSON dataUsingEncoding:NSUTF8StringEncoding]; if (jsonData == nil) return; - NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:jsonData - options:0 - error:nil]; - if (dict == nil) + id parsed = [NSJSONSerialization JSONObjectWithData:jsonData + options:0 + error:nil]; + if (![parsed isKindOfClass:[NSDictionary class]]) return; + NSDictionary *dict = (NSDictionary *)parsed; NSRange selectedRange = textView.selectedRange; CustomStyle *customStyleClass = @@ -1962,8 +1963,10 @@ - (bool)textView:(UITextView *)textView replacementText:(NSString *)text { // Capture the attributes at range.location that are being replaced // (autocorrect / predictive) so didProcessEditing: can re-stamp them onto the - // replacement. - if (range.length > 0) { + // replacement. Skip pure deletions (text.length == 0) — there is no incoming + // text to receive attributes, and capturing here would cause the deleted + // character's CustomStyleData to be re-stamped onto the widened editedRange. + if (range.length > 0 && text.length > 0) { _capturedAttributesBeforeChange = [textView.textStorage attributesAtIndex:range.location effectiveRange:NULL]; From 2c29d83f23117738e3dd5919808b9cdc0b02e5c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20=C5=BB=C3=B3=C5=82kiewski?= Date: Wed, 17 Jun 2026 10:56:08 +0200 Subject: [PATCH 04/11] fix: use hex for colors --- ios/EnrichedTextInputView.mm | 8 +-- ios/customStyleData/CustomStyleData.h | 5 +- ios/extensions/ColorExtension.h | 6 +- ios/extensions/ColorExtension.mm | 79 ++++++++++++++------------- ios/htmlParser/HtmlParser.mm | 12 ++-- ios/interfaces/StyleBase.mm | 2 +- 6 files changed, 58 insertions(+), 54 deletions(-) diff --git a/ios/EnrichedTextInputView.mm b/ios/EnrichedTextInputView.mm index 48d0c7c21..7539d2ce8 100644 --- a/ios/EnrichedTextInputView.mm +++ b/ios/EnrichedTextInputView.mm @@ -1136,10 +1136,10 @@ - (void)tryUpdatingActiveStyles { .alignment = [currentAlignment UTF8String], .customStyle = { .foregroundColor = - [[currentCustomStyle.foregroundColor rgbaString] UTF8String] + [[currentCustomStyle.foregroundColor hexString] UTF8String] ?: "", .backgroundColor = - [[currentCustomStyle.backgroundColor rgbaString] UTF8String] + [[currentCustomStyle.backgroundColor hexString] UTF8String] ?: ""}}); } } @@ -1930,11 +1930,11 @@ - (void)emitOnContextMenuItemPressEvent:(NSString *)itemText { .alignment = [currentAlignment UTF8String], .customStyle = { .foregroundColor = - [[contextCustomStyleData.foregroundColor rgbaString] + [[contextCustomStyleData.foregroundColor hexString] UTF8String] ?: "", .backgroundColor = - [[contextCustomStyleData.backgroundColor rgbaString] + [[contextCustomStyleData.backgroundColor hexString] UTF8String] ?: ""}}}); } diff --git a/ios/customStyleData/CustomStyleData.h b/ios/customStyleData/CustomStyleData.h index 4a97c1ba4..7ee405f8e 100644 --- a/ios/customStyleData/CustomStyleData.h +++ b/ios/customStyleData/CustomStyleData.h @@ -8,9 +8,8 @@ - (BOOL)isEmpty; -// Applies a partial update from a dict (keys: "foregroundColor", -// "backgroundColor"). A key absent from the dict leaves the field unchanged; -// NSNull or any non-UIColor value clears it; a UIColor value sets it. +// Applies a partial update from a dict. A key absent from the dict leaves the +// field unchanged; NSNull value clears it. - (void)mergeFromDict:(NSDictionary *)dict; @end diff --git a/ios/extensions/ColorExtension.h b/ios/extensions/ColorExtension.h index 6428f049a..570995dd8 100644 --- a/ios/extensions/ColorExtension.h +++ b/ios/extensions/ColorExtension.h @@ -4,8 +4,6 @@ @interface UIColor (ColorExtension) - (BOOL)isEqualToColor:(UIColor *)otherColor; - (UIColor *)colorWithAlphaIfNotTransparent:(CGFloat)newAlpha; -- (NSString *)rgbaString; -/// Parses a CSS rgba() string, e.g. @"rgba(255, 0, 0, 1.00)". Returns nil if -/// the string is not a valid rgba() value. -+ (UIColor *_Nullable)colorFromRgbaString:(NSString *_Nullable)rgba; +- (NSString *)hexString; ++ (UIColor *_Nullable)colorFromHexString:(NSString *_Nullable)hex; @end diff --git a/ios/extensions/ColorExtension.mm b/ios/extensions/ColorExtension.mm index 01394bd13..21cdd0960 100644 --- a/ios/extensions/ColorExtension.mm +++ b/ios/extensions/ColorExtension.mm @@ -36,57 +36,60 @@ - (UIColor *)colorWithAlphaIfNotTransparent:(CGFloat)newAlpha { return self; } -- (NSString *)rgbaString { +// Returns a CSS hex color string. +// Opaque colors produce 6-digit form (#RRGGBB); semi-transparent produce +// 8-digit form (#RRGGBBAA). Returns @"" if the color cannot be expressed +// in RGB. +- (NSString *)hexString { CGFloat red = 0.0; CGFloat green = 0.0; CGFloat blue = 0.0; CGFloat alpha = 0.0; - // getRed:green:blue:alpha: returns YES if the color can be converted to RGB. - // It natively handles monochrome/grayscale colors as well. - if ([self getRed:&red green:&green blue:&blue alpha:&alpha]) { - // Convert 0.0-1.0 floats to 0-255 integers for RGB - int r = (int)(red * 255.0 + 0.5); - int g = (int)(green * 255.0 + 0.5); - int b = (int)(blue * 255.0 + 0.5); + if (![self getRed:&red green:&green blue:&blue alpha:&alpha]) + return @""; - return - [NSString stringWithFormat:@"rgba(%d, %d, %d, %.2f)", r, g, b, alpha]; - } + int r = (int)(red * 255.0 + 0.5); + int g = (int)(green * 255.0 + 0.5); + int b = (int)(blue * 255.0 + 0.5); + int a = (int)(alpha * 255.0 + 0.5); - // Fallback for unsupported color - return @""; + if (a == 255) + return [NSString stringWithFormat:@"#%02X%02X%02X", r, g, b]; + return [NSString stringWithFormat:@"#%02X%02X%02X%02X", r, g, b, a]; } -+ (UIColor *_Nullable)colorFromRgbaString:(NSString *_Nullable)rgba { - if (rgba.length == 0) +// Parses a CSS hex color string (#RRGGBB or #RRGGBBAA). Returns nil if +// the string is not a valid hex color value. ++ (UIColor *_Nullable)colorFromHexString:(NSString *_Nullable)hex { + if (hex.length == 0) return nil; - static NSRegularExpression *regex; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - regex = [NSRegularExpression - regularExpressionWithPattern: - @"rgba\\(\\s*(\\d+)\\s*,\\s*(\\d+)\\s*,\\s*(\\d+)\\s*," - @"\\s*([\\d.]+)\\s*\\)" - options:NSRegularExpressionCaseInsensitive - error:nil]; - }); - - NSTextCheckingResult *match = - [regex firstMatchInString:rgba - options:0 - range:NSMakeRange(0, rgba.length)]; - if (!match || match.numberOfRanges < 5) + NSString *str = hex; + if ([str hasPrefix:@"#"]) + str = [str substringFromIndex:1]; + + NSUInteger len = str.length; + if (len != 6 && len != 8) return nil; - CGFloat r = - [[rgba substringWithRange:[match rangeAtIndex:1]] integerValue] / 255.0; - CGFloat g = - [[rgba substringWithRange:[match rangeAtIndex:2]] integerValue] / 255.0; - CGFloat b = - [[rgba substringWithRange:[match rangeAtIndex:3]] integerValue] / 255.0; - CGFloat a = [[rgba substringWithRange:[match rangeAtIndex:4]] doubleValue]; + unsigned int value = 0; + NSScanner *scanner = [NSScanner scannerWithString:str]; + if (![scanner scanHexInt:&value]) + return nil; + + CGFloat r, g, b, a; + if (len == 6) { + r = ((value >> 16) & 0xFF) / 255.0; + g = ((value >> 8) & 0xFF) / 255.0; + b = (value & 0xFF) / 255.0; + a = 1.0; + } else { + r = ((value >> 24) & 0xFF) / 255.0; + g = ((value >> 16) & 0xFF) / 255.0; + b = ((value >> 8) & 0xFF) / 255.0; + a = (value & 0xFF) / 255.0; + } return [UIColor colorWithRed:r green:g blue:b alpha:a]; } diff --git a/ios/htmlParser/HtmlParser.mm b/ios/htmlParser/HtmlParser.mm index 357545697..36819ba17 100644 --- a/ios/htmlParser/HtmlParser.mm +++ b/ios/htmlParser/HtmlParser.mm @@ -1455,8 +1455,8 @@ + (NSString *)tagContentForStyle:(NSNumber *)style [customStyle getStoredCustomStyleDataAt:location]; if (data != nil && !data.isEmpty) { NSMutableString *cssProps = [NSMutableString string]; - NSString *fg = [[data foregroundColor] rgbaString]; - NSString *bg = [[data backgroundColor] rgbaString]; + NSString *fg = [[data foregroundColor] hexString]; + NSString *bg = [[data backgroundColor] hexString]; if (fg.length > 0) { [cssProps appendFormat:@"color: %@;", fg]; } @@ -1539,7 +1539,9 @@ + (CustomStyleData *_Nullable)parseCustomStyleDataFromSpanParams: range:NSMakeRange(0, css.length)]; if (fgMatch) { data.foregroundColor = [UIColor - colorFromRgbaString:[css substringWithRange:[fgMatch rangeAtIndex:1]]]; + colorFromHexString:[[css substringWithRange:[fgMatch rangeAtIndex:1]] + stringByTrimmingCharactersInSet: + NSCharacterSet.whitespaceCharacterSet]]; } NSTextCheckingResult *bgMatch = @@ -1548,7 +1550,9 @@ + (CustomStyleData *_Nullable)parseCustomStyleDataFromSpanParams: range:NSMakeRange(0, css.length)]; if (bgMatch) { data.backgroundColor = [UIColor - colorFromRgbaString:[css substringWithRange:[bgMatch rangeAtIndex:1]]]; + colorFromHexString:[[css substringWithRange:[bgMatch rangeAtIndex:1]] + stringByTrimmingCharactersInSet: + NSCharacterSet.whitespaceCharacterSet]]; } return data.isEmpty ? nil : data; diff --git a/ios/interfaces/StyleBase.mm b/ios/interfaces/StyleBase.mm index 36a393863..1c0fac945 100644 --- a/ios/interfaces/StyleBase.mm +++ b/ios/interfaces/StyleBase.mm @@ -38,7 +38,7 @@ - (BOOL)isParagraph { } // Returns the application priority for this style. -// 0 = paragraph, 1 = custom (parametric colors), 2 = inline (default). +// 0 = paragraph, 1 = custom, 2 = inline (default). // Styles are applied in ascending priority order so inline styles win. - (NSInteger)stylePriority { return [self isParagraph] ? 0 : 2; From c2c0613df302afe0539bc6181e63ce4fc7b92f70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20=C5=BB=C3=B3=C5=82kiewski?= Date: Wed, 17 Jun 2026 11:29:31 +0200 Subject: [PATCH 05/11] feat: handle named colors in html parsing --- ios/extensions/ColorExtension.h | 3 +- ios/extensions/ColorExtension.mm | 254 ++++++++++++++++++++++++++++--- ios/htmlParser/HtmlParser.mm | 8 +- 3 files changed, 233 insertions(+), 32 deletions(-) diff --git a/ios/extensions/ColorExtension.h b/ios/extensions/ColorExtension.h index 570995dd8..4b2612571 100644 --- a/ios/extensions/ColorExtension.h +++ b/ios/extensions/ColorExtension.h @@ -5,5 +5,6 @@ - (BOOL)isEqualToColor:(UIColor *)otherColor; - (UIColor *)colorWithAlphaIfNotTransparent:(CGFloat)newAlpha; - (NSString *)hexString; -+ (UIColor *_Nullable)colorFromHexString:(NSString *_Nullable)hex; + ++ (UIColor *_Nullable)colorFromCSSString:(NSString *_Nullable)cssString; @end diff --git a/ios/extensions/ColorExtension.mm b/ios/extensions/ColorExtension.mm index 21cdd0960..f67ca44ea 100644 --- a/ios/extensions/ColorExtension.mm +++ b/ios/extensions/ColorExtension.mm @@ -1,5 +1,163 @@ #import "ColorExtension.h" +static NSDictionary *getNamedHexColors(void) { + static NSDictionary *namedColorHexes = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + namedColorHexes = @{ + @"aliceblue" : @"#F0F8FFFF", + @"antiquewhite" : @"#FAEBD7FF", + @"aqua" : @"#00FFFFFF", + @"aquamarine" : @"#7FFFD4FF", + @"azure" : @"#F0FFFFFF", + @"beige" : @"#F5F5DCFF", + @"bisque" : @"#FFE4C4FF", + @"black" : @"#000000FF", + @"blanchedalmond" : @"#FFEBCDFF", + @"blue" : @"#0000FFFF", + @"blueviolet" : @"#8A2BE2FF", + @"brown" : @"#A52A2AFF", + @"burlywood" : @"#DEB887FF", + @"cadetblue" : @"#5F9EA0FF", + @"chartreuse" : @"#7FFF00FF", + @"chocolate" : @"#D2691EFF", + @"coral" : @"#FF7F50FF", + @"cornflowerblue" : @"#6495EDFF", + @"cornsilk" : @"#FFF8DCFF", + @"crimson" : @"#DC143CFF", + @"cyan" : @"#00FFFFFF", + @"darkblue" : @"#00008BFF", + @"darkcyan" : @"#008B8BFF", + @"darkgoldenrod" : @"#B8860BFF", + @"darkgray" : @"#A9A9A9FF", + @"darkgrey" : @"#A9A9A9FF", + @"darkgreen" : @"#006400FF", + @"darkkhaki" : @"#BDB76BFF", + @"darkmagenta" : @"#8B008BFF", + @"darkolivegreen" : @"#556B2FFF", + @"darkorange" : @"#FF8C00FF", + @"darkorchid" : @"#9932CCFF", + @"darkred" : @"#8B0000FF", + @"darksalmon" : @"#E9967AFF", + @"darkseagreen" : @"#8FBC8FFF", + @"darkslateblue" : @"#483D8BFF", + @"darkslategray" : @"#2F4F4FFF", + @"darkslategrey" : @"#2F4F4FFF", + @"darkturquoise" : @"#00CED1FF", + @"darkviolet" : @"#9400D3FF", + @"deeppink" : @"#FF1493FF", + @"deepskyblue" : @"#00BFFFFF", + @"dimgray" : @"#696969FF", + @"dimgrey" : @"#696969FF", + @"dodgerblue" : @"#1E90FFFF", + @"firebrick" : @"#B22222FF", + @"floralwhite" : @"#FFFAF0FF", + @"forestgreen" : @"#228B22FF", + @"fuchsia" : @"#FF00FFFF", + @"gainsboro" : @"#DCDCDCFF", + @"ghostwhite" : @"#F8F8FFFF", + @"gold" : @"#FFD700FF", + @"goldenrod" : @"#DAA520FF", + @"gray" : @"#808080FF", + @"grey" : @"#808080FF", + @"green" : @"#008000FF", + @"greenyellow" : @"#ADFF2FFF", + @"honeydew" : @"#F0FFF0FF", + @"hotpink" : @"#FF69B4FF", + @"indianred" : @"#CD5C5CFF", + @"indigo" : @"#4B0082FF", + @"ivory" : @"#FFFFF0FF", + @"khaki" : @"#F0E68CFF", + @"lavender" : @"#E6E6FAFF", + @"lavenderblush" : @"#FFF0F5FF", + @"lawngreen" : @"#7CFC00FF", + @"lemonchiffon" : @"#FFFACDFF", + @"lightblue" : @"#ADD8E6FF", + @"lightcoral" : @"#F08080FF", + @"lightcyan" : @"#E0FFFFFF", + @"lightgoldenrodyellow" : @"#FAFAD2FF", + @"lightgray" : @"#D3D3D3FF", + @"lightgrey" : @"#D3D3D3FF", + @"lightgreen" : @"#90EE90FF", + @"lightpink" : @"#FFB6C1FF", + @"lightsalmon" : @"#FFA07AFF", + @"lightseagreen" : @"#20B2AAFF", + @"lightskyblue" : @"#87CEFAFF", + @"lightslategray" : @"#778899FF", + @"lightslategrey" : @"#778899FF", + @"lightsteelblue" : @"#B0C4DEFF", + @"lightyellow" : @"#FFFFE0FF", + @"lime" : @"#00FF00FF", + @"limegreen" : @"#32CD32FF", + @"linen" : @"#FAF0E6FF", + @"magenta" : @"#FF00FFFF", + @"maroon" : @"#800000FF", + @"mediumaquamarine" : @"#66CDAAFF", + @"mediumblue" : @"#0000CDFF", + @"mediumorchid" : @"#BA55D3FF", + @"mediumpurple" : @"#9370D8FF", + @"mediumseagreen" : @"#3CB371FF", + @"mediumslateblue" : @"#7B68EEFF", + @"mediumspringgreen" : @"#00FA9AFF", + @"mediumturquoise" : @"#48D1CCFF", + @"mediumvioletred" : @"#C71585FF", + @"midnightblue" : @"#191970FF", + @"mintcream" : @"#F5FFFAFF", + @"mistyrose" : @"#FFE4E1FF", + @"moccasin" : @"#FFE4B5FF", + @"navajowhite" : @"#FFDEADFF", + @"navy" : @"#000080FF", + @"oldlace" : @"#FDF5E6FF", + @"olive" : @"#808000FF", + @"olivedrab" : @"#6B8E23FF", + @"orange" : @"#FFA500FF", + @"orangered" : @"#FF4500FF", + @"orchid" : @"#DA70D6FF", + @"palegoldenrod" : @"#EEE8AAFF", + @"palegreen" : @"#98FB98FF", + @"paleturquoise" : @"#AFEEEEFF", + @"palevioletred" : @"#D87093FF", + @"papayawhip" : @"#FFEFD5FF", + @"peachpuff" : @"#FFDAB9FF", + @"peru" : @"#CD853FFF", + @"pink" : @"#FFC0CBFF", + @"plum" : @"#DDA0DDFF", + @"powderblue" : @"#B0E0E6FF", + @"purple" : @"#800080FF", + @"rebeccapurple" : @"#663399FF", + @"red" : @"#FF0000FF", + @"rosybrown" : @"#BC8F8FFF", + @"royalblue" : @"#4169E1FF", + @"saddlebrown" : @"#8B4513FF", + @"salmon" : @"#FA8072FF", + @"sandybrown" : @"#F4A460FF", + @"seagreen" : @"#2E8B57FF", + @"seashell" : @"#FFF5EEFF", + @"sienna" : @"#A0522DFF", + @"silver" : @"#C0C0C0FF", + @"skyblue" : @"#87CEEBFF", + @"slateblue" : @"#6A5ACDFF", + @"slategray" : @"#708090FF", + @"slategrey" : @"#708090FF", + @"snow" : @"#FFFAFAFF", + @"springgreen" : @"#00FF7FFF", + @"steelblue" : @"#4682B4FF", + @"tan" : @"#D2B48CFF", + @"teal" : @"#008080FF", + @"thistle" : @"#D8BFD8FF", + @"tomato" : @"#FF6347FF", + @"turquoise" : @"#40E0D0FF", + @"violet" : @"#EE82EEFF", + @"wheat" : @"#F5DEB3FF", + @"white" : @"#FFFFFFFF", + @"whitesmoke" : @"#F5F5F5FF", + @"yellow" : @"#FFFF00FF", + @"yellowgreen" : @"#9ACD32FF" + }; + }); + return namedColorHexes; +} + @implementation UIColor (ColorExtension) - (BOOL)isEqualToColor:(UIColor *)otherColor { CGColorSpaceRef colorSpaceRGB = CGColorSpaceCreateDeviceRGB(); @@ -59,39 +217,85 @@ - (NSString *)hexString { return [NSString stringWithFormat:@"#%02X%02X%02X%02X", r, g, b, a]; } -// Parses a CSS hex color string (#RRGGBB or #RRGGBBAA). Returns nil if -// the string is not a valid hex color value. -+ (UIColor *_Nullable)colorFromHexString:(NSString *_Nullable)hex { - if (hex.length == 0) +// Converts a CSS color string (Hex, RGB, RGBA, or Named) into a UIColor. ++ (UIColor *_Nullable)colorFromCSSString:(NSString *_Nullable)cssString { + if (cssString.length == 0) return nil; - NSString *str = hex; - if ([str hasPrefix:@"#"]) + // Trim whitespace and force lowercase for easier matching + NSString *str = + [cssString + stringByTrimmingCharactersInSet:[NSCharacterSet + whitespaceAndNewlineCharacterSet]] + .lowercaseString; + + // Handle Hex (#FFF, #FFFFFF, #FFFFFFFF) + if ([str hasPrefix:@"#"]) { str = [str substringFromIndex:1]; + NSUInteger len = str.length; - NSUInteger len = str.length; - if (len != 6 && len != 8) - return nil; + unsigned int value = 0; + NSScanner *scanner = [NSScanner scannerWithString:str]; + if (![scanner scanHexInt:&value]) + return nil; - unsigned int value = 0; - NSScanner *scanner = [NSScanner scannerWithString:str]; - if (![scanner scanHexInt:&value]) - return nil; + CGFloat r, g, b, a = 1.0; + + if (len == 3) { + r = ((value >> 8) & 0xF) / 15.0; + g = ((value >> 4) & 0xF) / 15.0; + b = (value & 0xF) / 15.0; + } else if (len == 6) { + r = ((value >> 16) & 0xFF) / 255.0; + g = ((value >> 8) & 0xFF) / 255.0; + b = (value & 0xFF) / 255.0; + } else if (len == 8) { + r = ((value >> 24) & 0xFF) / 255.0; + g = ((value >> 16) & 0xFF) / 255.0; + b = ((value >> 8) & 0xFF) / 255.0; + a = (value & 0xFF) / 255.0; + } else { + return nil; // Invalid hex length + } + + return [UIColor colorWithRed:r green:g blue:b alpha:a]; + } + + // Handle rgb() and rgba() + if ([str hasPrefix:@"rgb"]) { + NSScanner *scanner = [NSScanner scannerWithString:str]; + + [scanner scanUpToString:@"(" intoString:NULL]; + if (![scanner scanString:@"(" intoString:NULL]) + return nil; + + float r = 0, g = 0, b = 0, a = 1.0; + + [scanner scanFloat:&r]; + [scanner scanString:@"," intoString:NULL]; + [scanner scanFloat:&g]; + [scanner scanString:@"," intoString:NULL]; + [scanner scanFloat:&b]; + + if ([scanner scanString:@"," intoString:NULL]) { + [scanner scanFloat:&a]; + } + + return [UIColor colorWithRed:r / 255.0 + green:g / 255.0 + blue:b / 255.0 + alpha:a]; + } - CGFloat r, g, b, a; - if (len == 6) { - r = ((value >> 16) & 0xFF) / 255.0; - g = ((value >> 8) & 0xFF) / 255.0; - b = (value & 0xFF) / 255.0; - a = 1.0; - } else { - r = ((value >> 24) & 0xFF) / 255.0; - g = ((value >> 16) & 0xFF) / 255.0; - b = ((value >> 8) & 0xFF) / 255.0; - a = (value & 0xFF) / 255.0; + // Handle Named Colors + NSString *hexForName = getNamedHexColors()[str]; + if (hexForName) { + // We found a match! Pass the 8-digit hex string right back into this very + // method to reuse the Hex parsing logic. + return [self colorFromCSSString:hexForName]; } - return [UIColor colorWithRed:r green:g blue:b alpha:a]; + return nil; } @end diff --git a/ios/htmlParser/HtmlParser.mm b/ios/htmlParser/HtmlParser.mm index 36819ba17..b57608be2 100644 --- a/ios/htmlParser/HtmlParser.mm +++ b/ios/htmlParser/HtmlParser.mm @@ -1539,9 +1539,7 @@ + (CustomStyleData *_Nullable)parseCustomStyleDataFromSpanParams: range:NSMakeRange(0, css.length)]; if (fgMatch) { data.foregroundColor = [UIColor - colorFromHexString:[[css substringWithRange:[fgMatch rangeAtIndex:1]] - stringByTrimmingCharactersInSet: - NSCharacterSet.whitespaceCharacterSet]]; + colorFromCSSString:[css substringWithRange:[fgMatch rangeAtIndex:1]]]; } NSTextCheckingResult *bgMatch = @@ -1550,9 +1548,7 @@ + (CustomStyleData *_Nullable)parseCustomStyleDataFromSpanParams: range:NSMakeRange(0, css.length)]; if (bgMatch) { data.backgroundColor = [UIColor - colorFromHexString:[[css substringWithRange:[bgMatch rangeAtIndex:1]] - stringByTrimmingCharactersInSet: - NSCharacterSet.whitespaceCharacterSet]]; + colorFromCSSString:[css substringWithRange:[bgMatch rangeAtIndex:1]]]; } return data.isEmpty ? nil : data; From ce12b5726a95bbf6a6aa95af1e93040c4bb79f36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20=C5=BB=C3=B3=C5=82kiewski?= Date: Wed, 17 Jun 2026 12:05:33 +0200 Subject: [PATCH 06/11] feat: add colors to toolbar --- .../example/src/components/ColorPickerRow.tsx | 86 ++++++++ apps/example/src/components/Toolbar.tsx | 198 ++++++++++++++++-- ios/styles/CustomStyle.mm | 1 + 3 files changed, 271 insertions(+), 14 deletions(-) create mode 100644 apps/example/src/components/ColorPickerRow.tsx diff --git a/apps/example/src/components/ColorPickerRow.tsx b/apps/example/src/components/ColorPickerRow.tsx new file mode 100644 index 000000000..ae0e79649 --- /dev/null +++ b/apps/example/src/components/ColorPickerRow.tsx @@ -0,0 +1,86 @@ +import { type FC } from 'react'; +import { Pressable, ScrollView, StyleSheet, Text } from 'react-native'; + +interface Props { + colors: string[]; + activeColor: string; + onSelectColor: (color: string) => void; + onClear: () => void; +} + +export const ColorPickerRow: FC = ({ + colors, + activeColor, + onSelectColor, + onClear, +}) => { + return ( + + + + + {colors.map((color) => { + const isActive = color.toLowerCase() === activeColor?.toLowerCase(); + return ( + onSelectColor(color)} + style={[ + styles.swatch, + { backgroundColor: color }, + isActive && styles.swatchActive, + color === '#FFFFFF' && styles.swatchBordered, + ]} + /> + ); + })} + + ); +}; + +const SWATCH_SIZE = 28; + +const styles = StyleSheet.create({ + container: { + width: '100%', + backgroundColor: 'rgba(0, 26, 114, 0.9)', + }, + content: { + flexDirection: 'row', + alignItems: 'center', + paddingHorizontal: 8, + paddingVertical: 8, + gap: 8, + }, + clearButton: { + width: SWATCH_SIZE, + height: SWATCH_SIZE, + borderRadius: SWATCH_SIZE / 2, + backgroundColor: 'rgba(255,255,255,0.15)', + justifyContent: 'center', + alignItems: 'center', + }, + clearText: { + color: 'white', + fontSize: 14, + lineHeight: 16, + }, + swatch: { + width: SWATCH_SIZE, + height: SWATCH_SIZE, + borderRadius: SWATCH_SIZE / 2, + }, + swatchActive: { + borderWidth: 3, + borderColor: 'white', + }, + swatchBordered: { + borderWidth: 1, + borderColor: 'rgba(0,0,0,0.25)', + }, +}); diff --git a/apps/example/src/components/Toolbar.tsx b/apps/example/src/components/Toolbar.tsx index ce6e15e4e..6b5541e7e 100644 --- a/apps/example/src/components/Toolbar.tsx +++ b/apps/example/src/components/Toolbar.tsx @@ -1,5 +1,14 @@ -import { FlatList, type ListRenderItemInfo, StyleSheet } from 'react-native'; +import { useState } from 'react'; +import { + FlatList, + type ListRenderItemInfo, + Pressable, + StyleSheet, + Text, + View, +} from 'react-native'; import { ToolbarButton } from './ToolbarButton'; +import { ColorPickerRow } from './ColorPickerRow'; import type { OnChangeStateEvent, EnrichedTextInputInstance, @@ -8,6 +17,25 @@ import type { FC } from 'react'; const GRID_COLUMNS = 8; +const COLORS = [ + '#000000', + '#FFFFFF', + '#808080', + '#FF0000', + '#FF6600', + '#FFFF00', + '#00FF00', + '#008000', + '#00FFFF', + '#0000FF', + '#800080', + '#FF00FF', + '#FF69B4', + '#A52A2A', + '#FFA500', + '#ADD8E6', +]; + const STYLE_ITEMS = [ { name: 'bold', @@ -97,10 +125,19 @@ const STYLE_ITEMS = [ name: 'align-right', icon: 'align-right', }, + { + name: 'text-color', + text: 'A', + }, + { + name: 'bg-color', + text: 'BG', + }, ] as const; type Item = (typeof STYLE_ITEMS)[number]; type StylesState = OnChangeStateEvent; +type OpenPicker = 'text-color' | 'bg-color' | null; export interface ToolbarProps { stylesState: StylesState; @@ -117,6 +154,20 @@ export const Toolbar: FC = ({ onSelectImage, layout = 'horizontal', }) => { + const [openPicker, setOpenPicker] = useState(null); + + const activeFgColor = stylesState.customStyle?.foregroundColor ?? ''; + const activeBgColor = stylesState.customStyle?.backgroundColor ?? ''; + + const fgIndicatorColor = + activeFgColor.length > 0 ? activeFgColor : 'transparent'; + const fgIndicatorBorder = + activeFgColor.length > 0 ? activeFgColor : 'rgba(255,255,255,0.4)'; + const bgIndicatorColor = + activeBgColor.length > 0 ? activeBgColor : 'transparent'; + const bgIndicatorBorder = + activeBgColor.length > 0 ? activeBgColor : 'rgba(255,255,255,0.4)'; + const handlePress = (item: Item) => { const currentRef = editorRef?.current; if (!currentRef) return; @@ -168,7 +219,6 @@ export const Toolbar: FC = ({ editorRef.current?.toggleOrderedList(); break; case 'checkbox-list': - // Make checkbox checked by default editorRef.current?.toggleCheckboxList(true); break; case 'link': @@ -289,6 +339,62 @@ export const Toolbar: FC = ({ }; const renderItem = ({ item }: ListRenderItemInfo) => { + if (item.name === 'text-color') { + return ( + + setOpenPicker((prev) => + prev === 'text-color' ? null : 'text-color' + ) + } + style={[ + styles.colorButton, + layout === 'grid' ? styles.gridItem : undefined, + openPicker === 'text-color' && styles.colorButtonActive, + ]} + > + A + + + ); + } + + if (item.name === 'bg-color') { + return ( + + setOpenPicker((prev) => (prev === 'bg-color' ? null : 'bg-color')) + } + style={[ + styles.colorButton, + layout === 'grid' ? styles.gridItem : undefined, + openPicker === 'bg-color' && styles.colorButtonActive, + ]} + > + BG + + + ); + } + return ( = ({ const keyExtractor = (item: Item) => item.name; + const handleSelectFgColor = (color: string) => { + editorRef?.current?.setStyle({ foregroundColor: color }); + setOpenPicker(null); + }; + + const handleClearFgColor = () => { + editorRef?.current?.setStyle({ foregroundColor: null }); + setOpenPicker(null); + }; + + const handleSelectBgColor = (color: string) => { + editorRef?.current?.setStyle({ backgroundColor: color }); + setOpenPicker(null); + }; + + const handleClearBgColor = () => { + editorRef?.current?.setStyle({ backgroundColor: null }); + setOpenPicker(null); + }; + return ( - + + + {openPicker === 'text-color' && ( + + )} + {openPicker === 'bg-color' && ( + + )} + ); }; const styles = StyleSheet.create({ - container: { + wrapper: { + width: '100%', + }, + list: { width: '100%', }, gridItem: { flexBasis: `${100 / GRID_COLUMNS}%`, aspectRatio: 1, }, + colorButton: { + justifyContent: 'center', + alignItems: 'center', + width: 56, + height: 56, + backgroundColor: 'rgba(0, 26, 114, 0.8)', + gap: 2, + }, + colorButtonActive: { + backgroundColor: 'rgb(0, 26, 114)', + }, + colorButtonLabel: { + color: 'white', + fontSize: 15, + fontWeight: '700', + lineHeight: 17, + }, + colorIndicator: { + width: 20, + height: 5, + borderRadius: 2, + borderWidth: 1, + }, }); diff --git a/ios/styles/CustomStyle.mm b/ios/styles/CustomStyle.mm index 024bf8de0..45def651a 100644 --- a/ios/styles/CustomStyle.mm +++ b/ios/styles/CustomStyle.mm @@ -138,6 +138,7 @@ - (CustomStyleData *_Nullable)getCustomStyleDataAt:(NSUInteger)location { self.host.textView.typingAttributes[CustomStyleAttributeName]; if ([typingValue isKindOfClass:[CustomStyleData class]]) return (CustomStyleData *)typingValue; + return nil; } return [self getStoredCustomStyleDataAt:location]; From ec7521bfd7f14f6c8a365cb2e2d07fec616c2e8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20=C5=BB=C3=B3=C5=82kiewski?= Date: Wed, 17 Jun 2026 12:43:42 +0200 Subject: [PATCH 07/11] test: add e2e test for colors --- .../flows/custom_style_colors_visual.yaml | 118 ++++++++++++++++++ .../screenshots/ios/custom_style_colors.png | Bin 0 -> 41180 bytes .../flows/custom_style_colors_visual.yaml | 28 +++++ .../ios/custom_style_colors_visual.png | Bin 0 -> 57384 bytes apps/example/ios/Podfile.lock | 8 +- .../example/src/components/ColorPickerRow.tsx | 8 +- 6 files changed, 157 insertions(+), 5 deletions(-) create mode 100644 .maestro/enrichedInput/flows/custom_style_colors_visual.yaml create mode 100644 .maestro/enrichedInput/screenshots/ios/custom_style_colors.png create mode 100644 .maestro/enrichedText/flows/custom_style_colors_visual.yaml create mode 100644 .maestro/enrichedText/screenshots/ios/custom_style_colors_visual.png diff --git a/.maestro/enrichedInput/flows/custom_style_colors_visual.yaml b/.maestro/enrichedInput/flows/custom_style_colors_visual.yaml new file mode 100644 index 000000000..04ad337d1 --- /dev/null +++ b/.maestro/enrichedInput/flows/custom_style_colors_visual.yaml @@ -0,0 +1,118 @@ +appId: swmansion.enriched.example +--- +# Validates custom colors on plain text, inline styles, and paragraph styles. +- launchApp + +- tapOn: + id: 'toggle-screen-button' + +- tapOn: + id: 'editor-input' + +# Section 1: Plain text with colors + +- tapOn: + id: 'toolbar-text-color' +- tapOn: + id: 'color-swatch-FF0000' +- inputText: 'Red text' +- tapOn: + id: 'toolbar-text-color' +- tapOn: + id: 'color-swatch-clear' +- inputText: ' ' + +- tapOn: + id: 'toolbar-bg-color' +- tapOn: + id: 'color-swatch-FFFF00' +- inputText: 'Yellow back' +- tapOn: + id: 'toolbar-bg-color' +- tapOn: + id: 'color-swatch-clear' +- inputText: ' ' + +- tapOn: + id: 'toolbar-text-color' +- tapOn: + id: 'color-swatch-FF0000' +- tapOn: + id: 'toolbar-bg-color' +- tapOn: + id: 'color-swatch-FFFF00' +- inputText: 'Red+Yellow' +- tapOn: + id: 'toolbar-text-color' +- tapOn: + id: 'color-swatch-clear' +- tapOn: + id: 'toolbar-bg-color' +- tapOn: + id: 'color-swatch-clear' +- inputText: ' ' + +# Section 2: Inline styles + color + +- tapOn: + id: 'toolbar-bold' +- tapOn: + id: 'toolbar-text-color' +- tapOn: + id: 'color-swatch-FF0000' +- inputText: 'Bold red' +- tapOn: + id: 'toolbar-bold' +- tapOn: + id: 'toolbar-text-color' +- tapOn: + id: 'color-swatch-clear' +- inputText: ' ' + +- tapOn: + id: 'toolbar-italic' +- tapOn: + id: 'toolbar-bg-color' +- tapOn: + id: 'color-swatch-FFFF00' +- inputText: 'Italic yellow back' +- tapOn: + id: 'toolbar-italic' +- tapOn: + id: 'toolbar-bg-color' +- tapOn: + id: 'color-swatch-clear' +- inputText: ' ' +- pressKey: Enter + +# Section 3: Paragraph styles + color + +- tapOn: + id: 'toolbar-heading-5' +- tapOn: + id: 'toolbar-text-color' +- tapOn: + id: 'color-swatch-FF0000' +- inputText: 'H5 red' +- pressKey: Enter +- tapOn: + id: 'toolbar-text-color' +- tapOn: + id: 'color-swatch-clear' + +- tapOn: + id: 'toolbar-quote' +- tapOn: + id: 'toolbar-bg-color' +- tapOn: + id: 'color-swatch-FFFF00' +- inputText: 'Quote yellow back' +- tapOn: + id: 'toolbar-bg-color' +- tapOn: + id: 'color-swatch-clear' + +- runFlow: + file: '../subflows/capture_or_assert_screenshot.yaml' + env: + SCREENSHOT_NAME: 'custom_style_colors' diff --git a/.maestro/enrichedInput/screenshots/ios/custom_style_colors.png b/.maestro/enrichedInput/screenshots/ios/custom_style_colors.png new file mode 100644 index 0000000000000000000000000000000000000000..e8e39c7150aa3e0c9289c0723491a63a38a6279e GIT binary patch literal 41180 zcmeFZWmjF@vNamqH35P{aF^ijF2REbcY?bF2p-(sA=tv*-Q6v?yF1)T_I}RU=RH5* zwkxgWL)r{$GS(P1s(SU_E5Tpn#F5}};NQJ_ha@Q>qWJC|r0TnOU|F!>z>{vI0`_9qh!XU-?5U zaixfIw?9V+_@_^UsJY|uDwI$fnF26Ym+o?t

    9)b}`G4CuO&;ZG7JCyczb^(J3*h z1tv`XM6kYp|9rCkh70$vzxonwK*Rg|`?+rr2_@vGe|{)D6E*1Hp9tdc6(aht55WHa zANap@m)0oZNunh55Y%{Zw|>tUN<@MCl>|4{X*$O#D)#L_YZwYu&YRJ{8hflpE3SRfa+er!on#gCni z5kM?8KEnBuw0)EyRb1=D7=z{{2^N7Du0KH&Sr%~S7qr>Jf&d<9)o&~inUDhC0d|hp zHwbs{gaS7FNuMZ6!sq`zUrPV{;Ov#w3=b7$cMicw04Q-1>B}bAM^8k(mq9N;)6qZp!drHs}tTRw=e& zw(Sl+I6+BWfq)^f9v9FX4hx}Cp*Ed8TI<0&ZP`Sw_pI}HW~D!Qg;Pi2$xKtwTWO>^ zoLx8E2zvM3_w$$`j&@`Dver}jSrH1qAsk$Db+<@-a)^@OB<}ZEJo}Mk&|<#MGpV+% z*S;^8ClHnG*t#ZC!cXw!Dn|LKy|KkflNnT}zelw88DG3jZT7M8yJ9`4SS{1kAC9#X z(XTJR+(%NunVSX6!0o%LV!j#(HvBQbcD%6UWSO_Wqbl zudAb#1rB06iHm$!ck$Y%S(K?j@=RMRpZFstm_$ewlSU1c8wGQ99J=~GJvP>m+UgIF zC9IUy63!~KCzo8@_Kf@_#%==r@2NiEGAMy~WeO>I&sGpL{4 zy5j3_tERQ!YE7HFv*o97TCbCr$oZG`Ks&N?YS`?XoBf!F+tvE3kNNfbqR)4qbzPw- z-it%Jyw|b|p6G`#9_=oJPgtx9WHmR8wI61p<*#mA6I$o$3RqWG{~w3Lxcvalzxxd9 z`&&G*5{pn?ra0Q>iBO_Y?-2bG>gtha3(b-wEA`B+IvlOFXXGvD#DJhfK-O}M+y4_J z)L@d)Vzw^y1SKA#J;u*Kh<=&{@6$Y6I)SyFVF&-V-Tcnp5|)JgI?bGRjZQ0bIe}?* zpa$|fZEimE(IP^&+mb59*^&-j)M7|Na59Rno@Mtwd~Ac>*o7!Z!3>gSxLJ^qN-@rH zvqp`F+~)qD6-vmv4FpTrFFLsYGs+mZlTExjS36_fj~r=~xQcRix~*ZfcRz2V^5xFY zOd$EpGUpZ9ysLS-$#rCZg9D02h>^IIUs_|wYyNz;8nTEIzZs`W}a zt1281q*MlvI)lxw=J)k2X2G@H-z|vbGK`C^?f1J?U4iEaUFxSTCe-U4FcrjKU9A$` zu45c}?(+~1u55d%I${z01rBYKW(Q3M@$yskMiCli?RrbXtzjXPzqLA`3b=U?P@g7A z!(O}YvF4s1FywSPjzK|z{*Id~n(57{&;j=suno^>;wOFb7a+5ok7ROVNtUX% zy0Nqjl>|rexw$^b!*W?GDz>ppB0965UWKV!)@Ql?v76q-fCy8nGeWJ#c2R4M-(Cqn zda=lOeL%4vaw&~9y}t?69;$>kR0*Yu#w{%1l_Hfr`QdJ`xxYQFH}Wnoc($b5RWL7V zk@eM~U|f;RnjeOK+pO68;DK+me{bD>JT1K*Vc%EEKk~R7DFK6km*YV*nvP>KMtn&= z%R;3bJ-5FN6j3IQ96T|}z>wy!+0UCti-9Bn52i`QYg_|Q)gp&|!n?i|bYkP)C(Wr* z1L5f|33yrcb2$Xnm>U_EDEgPG`|!cG>hvOT`yXqru?~}sFVW3l(ic&fDWc?N$dbjiL?(py z*}!{?X%RwjtL5_bZv}F0a6KwTxh2}6ycID10(`(Cd(=8NdP+Q8b2{dJqv@^-c-a%J zc(+ACxt#J^!V3TI$PM9xq&6N@5rR`mil%VY|5AYp?t}R`ui1t)Y#IaE5O?xQGsREUxu-F3)PM!`r!YwqzTRSIJIBUgySvT zURFdSNd_0IHV|w-Nz0Iw%DkdM9BmGQX?#G*PqY&y1ot6NMz~jD?#|RFg#>TWZ*_}G z5A?u{-{WT<6D}hTQ}f2g0J+l#F++kQr+M>HgH>M-F;p-ZQbLBS~4D< zXLT^W;l15**ta{|2)$i(P@0865%8C}KjpV{9GNX^ogCt^x!+#!eC+hD^Sr^X1h(p2 zRE_WDYs`F4>ReIWO9RNN%A}xP^9OVCP$ef7)nNzBI$G6zg04+61TNbV9-HO&(?8t3 z5#m(DX*mH;`tL^4ELa{B8#|YX&6J|Dgk{rrGy0%mo*!s%x#N-W`mI}&v^`l5rvi}( zhP|GmFg|=MNNZ-3X?ZZ(-mW-pGa2I>dblx~(-{hcMgG|S$1BD2o=iUaS>o}gVj}0B zia|F#Cbh;A+x>uw&5m@b;XMVX#UjhiUXEo4_)~)#r&ePL?F$`4OSg9iNPB_z_RJ^t zvq>ccj14h86nlHN=uwQk3+MC0IrBQQ>mKpV#gOFyx~kT%=aaaMF|UVUB<#*m|5cDC z`_0|vkspf7a_gM!(wD%gx(}(3-N8?TT`Qlw!U1%ZKM4-!vsNd83ZF!TeRM^m)XQu{ zq-1S8I3u&)726?mA4LVCDfCx`HPRW2?VEXhpG@qvyrE+ZqmAJ@7>nOHQB$;&d1 zNZ8e@PFS>%*qmWNNO!Su=O2n~m1$7LC*=4+_5Jg<^jyry4UZ~8?EbkvIc*)M}Mb>Ub z3LkFSHN4s(P!bi110-cw6dCG&IKvP46K!vu={Xi;SR0B=7xnapbv4j#*XSM0Fqj{z zI^C}1uNa_RA68Lh7#0?Cea6;*7TZh+C+thly}IX2AZ~yRhzw+yCxly zPKfG|ZxBNwk<4|nVlzc0q@YSLF2JC1k$_X1^brf2*Zl5U51YKkNW@mGij{XQ zy-=jWkwZQ-Rrh|G+9X|FUryrNzD zgTW|_G3=H@&qYYQC|!%FL4}=O^=RMOC63TTA1T<}aQ!%+cY`zOavi!j7~3?W+4pJ2 zWH3g?*5jkyHO=+hC9{@IY5C$E)PX0NJ1uJ!3)YiNiWFZrP@yLR1vs@EGtoneRJzD% zCJSUkmp@XfodCsS9I-2jEF00M*6Vu4rx=7TPC$%*FmJn5qMn>2l=-oHY=scr=cfR- zuJ?*^|I__E@w>Xy^#G&^S+hcj1l^NLrnE}q5g(eeSn0?(`xVDUoo@;O0%qPXR9Gis z+`!%&6+$|y6HQK~1hxt)RR?EF%VOq#q-n>gKxF+Hk(~Q^ab5O>%LnJWND*7=Ln@5e^*s8(- zYs%b?hsImre9s?nj~p%r#6YXtZI$j(9{ZSd`m7c>go|M5gkJ4=1IZjwHD-R7Ts8w_ zw4dK&=@)M3{}G023&o{Y{>IS!t%9W7sCl4w_pQ69EHOILYWc6{v|Pp z*{Y0rOkRm!58d)bigL-gazXG!2}FlCll{cd!kJoTbqkZbS?$LRuUvxwMEiCIqbX`yh3M3Jx@9`^ zCghGY)CAuX?Kx?77cG?3K;2MqBJaV^TPX9YB2~1qeA%ed{y8A9p8~jSZy06shU?~Q zVAdCG60r|zAX+a2JwIp|hDO)}J*PIWFZLIjwJa+oNH~^{S4vtTN!9!>{T55A!+CHA zl7dtDsL+{X_MldraK=GY*m>{gYh>@ha^;u8f=&o1tPgue3+Ej$*_Y$~SX|#U|t zdjx--<>{OM*foXoQi0WInw`E@sI!)gIqAg>LrH3Ky1&}@Xd{Z+j4;t|JR8ju^!qxN zUSg%GWZo6v;cwl5r+tE(|MC>VwXpxkJxq%W6|KfpZ&6rsYeKw~ySqEDOP&*|Km#7< zVFPDd1_{|Hg|^;-3xlTe`LUuSW;{=`K2oL3+9Nu$J)Aqdk8$sI=Evsi9*I`wZxp_E zb(YMp^}>sFyMm$(b{rTdcny@f%{)$rr5riXcq%dEPlNqDFC6rJ+i6wCNNvcU%36$> z*WM0$9>_!b`C9a`HsDIRAIHWW9N*)f<3^ZBVb%0Od-eb|nY%{dR^oud02u)|`DEj;PR7%9se<*# zc@I)iCg5VEdR$;W^7}s``#7Nx(6Xt0`mNOfw54wcDc*O2Q$ErRGYG?0b8@d*bNQ=YUH2;BZL|Oo2!S$QbtZI*1s7LQ zZfQK>SuW3d5uEGNSxJ_{HKV^cHko%+ zIfSS=mGAKqpp{IXfTXnQHGZ}(hM6zMhl*CJVKgkUP!%|)>$XQ66Sg1|Dka723Ty_n zxjDXC+nIxO|jccuH ztarP6VN2%x?Hl^ZhGcMO~^xy>ux zKH8BbSZm7~i_!DLGy8I83h`B6xS~_*eZI?+ zKae$^4`?i@jrw2+Kg<+C)n+f&^k}sT6QOw*=lR>I&z6bnWo@4=0}RiFzc}@i6LNxF z*D5F1k5$ZQ>(+(o+V`BtEICk?1-}rLNG>jh(zwWw33sZP9f)9~c$1e|WOB~C^g<0< z!uiwL(~MH`8|-C6{Sp|)UhW{=iEPlh!wK9GaShYlLF;qD^H+iMim3{h+X?e)DjRuI zO>P9<%TBi7L5O87v;E+yUX4~P2oq>pjql~Yq3~OBB6E> zUW*AjLjL$E8;#^ngye3WT%s6Cq}d*mZo_`wfoRM9R{KantFBI_AsfyoWab_P!~aMn zu~N9wY><&Xl!5(4(K@T^z-_8=p*5O1X1LG0nd?Ei)+|}GG221g9wfouTq6YU^TDwP z{QOKLtv6O;tm_MX&0J(}F=2Q^eq_(~>F+`wrQASbwTgz*{+`S6MsB8!V}(pP`N`+b zOVl}DtbXFi=)|vIn`-l1l=}F-{V-k{W&hKz`%NsBn6~IF(V&vR&9txHcFA~br_s!grnTPpD(81S zyw^vBJ_oYOJP|@TwzcEE{Vw-zAE7J)9Vz+ZC`{KIQ%3JijH+BQmN?m)%I-r|MOpx| zfHZ-5`~$5WIHNMb;*FoOf3&dkEuT$kk`zU;sG+cwN*{Mlaot~Ffo-#unyIJfk}+2L z_>KHHrMvTd3a7IS(B-3W!4cC~`quIdb4K0nlP6u`VT#+*8}}dzyCpsTQzD|;oA>8? zJF)Rr0^TiEDd5IFHaRDZgpANf@d;%DMChRUhts|spkIV|gV6wk0@LWQ;5|g=v=K^N zYo$4#>9ehib%Tf(B+Xzn*3OCv)=s=AjYx4$y6kCDo=;6a7=zoajr2rRoNE zS&n<;hKke1d86;;*OX;Pk(AWapW0qWU*pR@JJGUzHSb3yK{#7+o{vnmWPZAO&lcqN z;@ZV&{sEXjF%DLz7_od$ zU)P(|5761R*=@%PwxAOzVRdI41-;SpG7Vo~9by`xE7IH;w|TA^t~Su;8;9S=$Y_wv zqp#F#VmMC6qz_d_%P|xFXp-tS>CGW|1Ig7AToU>8Q?%SsLXFcv<17ENI^rDLwyN-! zQd4-t$AtMZ<($J6NtljT;SR#9D|P3rgv2iY-ZDsZaQw!zPM|DW%Ev$YdAFrdX)KH8pz7j=&enc5AZ(v@7~I5CTf2MK$+HSFUpIFG-G99hs7LoBCf|L z(*QJHYOQS!2W#G1_j{6AbEF_hcmo3>Er|qC3WMQ~=PW2%w*GAV=8szg8=qj#%I$TS z1^fCqx%_lLvZ41CnDrB=%kvuDxnFk{BAd^bld3J%a!AGqcTFp%Kj{++P35CCS~wgp znIxmy_2JLIJD8tfT((D%!^mUUhYmYBvQ#P<)cmej1xm%6g2&XkC8`A0Y&_f!%^lpL z(>@qCzb1^O_3eY{TooyOD~y`Kqm$(sX%n}}EG@z1o-$~dyKw8M5m03QvVbqsU+KiT z2!!R~j2Yr$U$=FJjMOE2dYY2O9&p=0o;#ir>M{WWA+8?DT!tEbki<+NLH5@@1R5pPq| zjr!&*=D(qc+-rccsa3ux9u%o;L_@ouHX>l-3E&HgQ$WJ+RuWa7wCVCp@yhd5P*F+Z z@qV?LlpGyHLmS)u2=_Evj)s#!CZ7}u6(yV_CKf-&*98r8eXJQ>tihU^I{?x%r?`FT z6q<^x6({V0h6%-*FUM+GsMS)USBw(Ok)V&x#^76aLpOg{@I!i8?UFu8tW>jui0SAJ>OzrK{300=AQkchLOO#2`P{NsOO_!m-4`~h0C;` zG0%^3&-K1h){S7Jao6{qFcB(Ch7%IDZ=0~uF7EgKmm~L+4!+8No>vRZ5@_=*z0=kLi|nzTf7UQ7=f<-#VT}DtL&je@Qpu zU;5sL4ZYTdQqfVo#cgu*b&6wPu}1s2`R5yXqeTf>G!zRRZN4Th6^z%Ei}*}oSeO|= zU)d)|^0WDO^ot(fM}|0w`{h(MWFNa3e;5&Y7T-+$+A9pSnhC8SSy48sTvQ91S4PSZV z)@@7 zbZ*P+t=>{EraJ%O)o0P4F<8Nf#FxlR?xV}*)ZNQwtR4!=P> zrm}6Nb{d^7zexH}#5;s!7PPTC5jeP$TgI6)w{WzMQ$*Z*Sf6exUf>E$Xq}LOVA5iL zN~@OObgp-O2`O!i{XtB%eqTzsL3oxemw@1&Nw;)QAW@4Fe_ zp>dmhf?whM-mLe8^+P$~^YTCG%4vvrt8j{1?6>zWTRNSLtEk-|`fk$nm%xt=b03Kr zigHa?Oyuh};O%udAs-ejgLG797=1l{3J`0X_ecYD8h@8Z^2AfPDvB_@tv~M&;V0d% z`f^%h0$Q>i2d@3Mxl**Oq#AfU0PP$pOtdRnAqsS5LvCwet_f|Oi#6M#adu-|2nzyK zqkf*siZ9P<>6GadNOQM1-~gMVaP!DV@DBjD!3N)Nr>l9*PwA#b#sxD2C{e@}-9EAoBWmboX4$Nul|5K z^+0nY&wZaP^?=rPDI-R%voIST!L|NkTFGOLu$%MY0tNYNfIyB|5)Z220D(NZe4DHN z=4v|}#o3w6s6G`SsWNWc0bgU#NQ%%tKB#T(L)B{C7c$YuU6(?U<{pNfIGPFKe3lsG z_B!f9B+q-s)NK+-_(e!lS}$+CA^P&IMZOZc=suOx{NYI3J-#TBrW5^{o{(l8tLT zl22_PX@Nz$Jv%C4u6`YDb0!36Henjw0#&+H9EA-`CgH#hWl?Pi4W7h8rHXZc;ZUwl zuBulW&lcs_blt@+oGREz(EuI*Z_BVEiEC2xusYBYIHkhrca?fq$38U4YdY?up`c|sDYHxv#fjM=j z?I6V5=|DTVr7`wrwLJB5*ndRsYxG(PS)Q0O9r5EADwQd{%X5h_;cYQuj8bcclMkf4 zDEHvOZbdVH;m(_Aq}8UgjQC}0-5^2~)XC*oUZ5wuRY7w4>+=^KGGZ`Rw&nnVjX%K? zged>e33qTlY}U(Hhh-ZlT=xa($@*=%^Ct!A=xv|cL=nn--O^X;z7Mv&E@EhEwKfm2 zjyQDdMCH5(_qPW^m(9Jl-mf&@k^w=&)P=V%2(W!f1KHz`+fZztEbbWBqh%{pLG$k) z8)N`_2vkmk$Ka{UvY2M9YKBulA>HFl93iJ6RLbm59kfw#0AlB`y zR!H9;6z!W?sC&ORcTZ5@fM%tjUPGgXZji1wHAP$3~!q(wFT=qj>4n z!ZGF=Wn5iStYB)8ve_Y)?i$a{e*|%Kw2b=AQAdF379SA2ulFWh z1D2QukxgqXS#ei?aSXF2~26wySC}Q362EXzv=e^ z9~}RKQ?%Ye8;TTxtmIA^Y*F;fH+nWnW)9O(&hJTZKGDPKzKzo%32&4`DNhtBHr`J! z@kfAgpy{B3md1+rO3bA9qc=GsH~f$u*gPchy1et3hX#ZyC*D^cYRI3MVuKAkI18J` zhc~g_uW@}TTkPM%-!PypjD&V;M4(V+&R?0|BTxW04+`$BJH@94)Fk!ltTy&Lf(_byE3t6pvOfngtggxVs&}{r*Qq=)eHxVzU zuY=;SKPzc90g*w%I+1Irg`ac520Vsb&(H4*R=3+DTQP?eYA3<5aFFZOOgy(^r#g%0 zLtzveIO4IeE%9l-tX0#OC&^|9z!agBmiPS({^}@LX^d+~qUD4u)w~d-9Yza8>CQ+1 zn9HM;ikoY-WrMO~63}T}(WUsST?wc&;IXAM0`qH{Q#49rGvNU`2WT|2 z$bIsyJ{t$6A9+0+%J-hFXZJ+|4xB;kTR;l0RG0ht2O!w{^Q%s8mi{wngH44p!w%;{ z9V>r}g|Vc7e)-18OqP*oN7QnugemUAzsAGf;?z1z@ zWOWY7a}GI51OGQY08AVFNXo7NX}RWwsX6_;Z&9wO45u9B>`p@EEdEXv{b%Ax^wB)k zVQ!F^TfOTIJH~LUw|E~A(p*Y|amnwc#FtxWV~KcqrDADl8^4q~kgSRO{}Rrn&3iMc z0GW4S04QG}@L*Oi=i(DJRy#5aU30Z;^7~N>fGsH1cDG!wpL`U2yynm!D^k`3XeujL zZ-LI!Vfr6uNOhm7(`xH{)~6eHOqJ5)o5$1nb%Y90P%KT|cGGEjSI9I5O^EpnItq6r zs_EDdFgKr`IBL<7R9xXETAU@mXXPXC(eD$VR6Z6aUa$FWV9Yx|9t){zdZgxjz6kA5 zd=VnDb*wpW zCq`{w*SJXNomdI9kET?eWv!ji@|6fa0{qY2r{-vPW+5RqaLIqc2~9GpMAS{bhZeIb zy4rKmXly3+K7>%k_7QuT$RH-0&&1Y7Z}}I?fHxTJS^ZH70%ZX;%)9*C%`>+cz`kfx z-OcP~O=e%U#m#_k(IetAH>GBfM#r^no5dFDQ439(uwC>Xrh(rsB} zgkWF(ffVm{CP*SZQ)GFlYAJv|r)|0vkMd!<;ojI1v&JTCz4^iXrVFLl`3(WDDniDIA!%K+lHwRRV(Z59+P9niI%eSSoxWSueOz8wB72F>+TAcPANjc#`( zC$J)=l=5%J4*}dRjvp%3Dw9Uj$r0K0Ywi1Z55+G#nuBMNXf%nxXmBJv$Yl*n zpsC+h9N1q@_?L*z$Ke3UU!;_->z zH)a`7mjUkxs`R|~X4rgI;56)1n$UX1b>JTLH(26QTeA|L!Ai%9G5Z`^T&iM$tDs^hIk%$uwByZTj>rsIbQl7PIBLxj&x1Mh+X?~{mQ8eBX-u-KH~ zcw{;V5U2AH2h#(LsV@3Ip-OsSmAdF56^At|!2v-5KR(E}eZP;8&X;T#6&4bV7_V7k z4ulyR{nw)QCJ>eFpu0&!%R0lQ+Z~XnSBk5mDYYtzE2^tb3OS;goG(#G667-YPkd<{ zRD{j_X#;~iD&k0y?|b=bMhfzcww8EB<(i@+h9Xs~xkzHooiTa!D)Qq9{!GJ_Hmh0M z^V6PQH5X0-*$?3Oj(?q1fSCxbbWQ=Z3`qDL$fh=BI$_j|TT+^pkdIQ5_qVIV3A(c; zLwMHHu-wXkoE`X5UQs5Fa#(5?9h2omRm9n*{<#7g8XX)gU!fKb&K(*ts9sayV*=EO z^YZ0h&b$KrlOkYd+Z*jP2w$jnVPZ=hV zb;AW2$gEOS?)h@#>Ja-8V$TPOX6%93s4M#QsYxkt>@vV|;m6XsZN!KcR`5Haye zVx)l|N8c3IzD`2lckTBR<}`?def$GZ zci^0mAv)?P{J+}rKLllNwP9IMgT4@#sT4o2G!Y&x-=CfL4MEE`rF&vxX-EMjtdbCJ zk#i#;CO-6&shulN@;{6bU{o^&~*iv59BO^hB8DuS| zB%Wb4QiCmtDX==Llu9Q$Hn=-(aqhR&3}DX8IGX!*#waewyUhCaE>|@a7MU!l1{>m2 zBDV*#W(7o+nr(1aZCn2V^)iAn6X=I&fro3?*mMv*2}D9695_LcsOVy~-sNrzcz>R>t7+~;mLD1dQy;2?LLfr+ zHvAgsa3O}l?D_fj$txiAwFAaOh4);obq7Z)JO%ifI!p7_KQ`bdX+{S$e+8=wz>ZsTS1v*`117yOH=`hKj->WT$X0BD$Dr&^mu@^*VO-!zrjpLYorJS4;K8hyZ7z*a6HZxr>gyy(rj)1Er!I2HJP7F z$}4@q|JtAO#BuoL@#aD_=imKnFaSowOa-tEfXvc|97nh6vAM~B{Y7OwJB*yovcL6| zk29eL9i3U%9f{c=4IB&x)CljLL+aYB{w0Cz_;$623>99E4?7(vy8}JwYq$O_&jqYg z;<p~l^?9#1EA4q+gJ`htg_Z*uH?^PVN}U%1w$EpMmX&CfI`KdJ5=z* zo>1PO0-3AcCVhn8??}KqQtD@-$rFepvz>T@bFh8vN14QC-`M&$ix83M!{&p=K&DyT zwBWi}jpeQOy6=>?)S!OB&&eohph8;A)Prjrk4a1VPy(O|Ze5XqeT-S?0KZ08!Fgq3 zPRBkcK#=cO{A38w2K`_ar;d?%CvQ>}zD zVXfq-=_G`Mr`yg7I#;&sncR3GHCB1Ny3n#p+3RvAC7zZOXg}{RsgGnOZOxZ*` z6Q{p6q~f|oyLiBy&7;QSz-FMirF_cfy{BZI@Jl58uMdu~IT)``+ia=OQG46Zhsc7c zi*!5zLBQ1OH}u2qehftl!lgWX>IxEjzb@S%GY{-mwSy581Ify@P6Nk2#E#sfA z$Vor9=?a+t)_co2_;z61JIUlAjyelE$F+BdQ!uy1^Ank1?#DH-F~R~$`HA6DTfMjy z3xT={kD6MwcT2TlJUtS#{ZaWsqx^)pM=YL=R0fB$wK@IS(+#`89E3ODD`Ym2Elvc! z4ZYJdf3~91y*}KiKyyNT@5mX#0V2V#(CD~yYaDJ9nC@O)x38z0r0Fk53mh0!{22^J z#Mynyz_ideYMYiXIW!6McZb57)7QS&+1;fSQEN0065gkEN)V1sSrT!vhY;6y+vp-Q zhJ)RT-kR%|gt?Pi)T_2X)ME!(`^^4i1a{~iQ0)k#8{OMn9;i)^#f}1+MtP6qKm@N!OViE;m5QCijb~ADqEa82-Ww z4lOr)c54;#t9C5>`e78DE)OW<2NS2h3ZG9{fC!s1*N1$`_>B|!CeZGH(v zUQDY1pqFhjmeVg>wMk!jnd(ERNuNkFU^ZoGrg(!|LW74iMq2p}ifw?j?{gO>HZ^Egp$~?0GQBf5+S@Ctt z-OLv{pGNS zUTYgksq%qvHO=Czs=9|2%a`-er~pqeDByqHvWN}!l;Vn`xtu|Gc|b6k$jf+P>We+< z+@h^VO82PDJ(G%aVNbZ>1e}^{;0#oy)6VM*7x@fQnoGeqEdq_Tzaa+XI^^5XRg9## z^M4tI97+w=NU!W{yA-nIJIx^)NyUF7P+1h8_(j!8how1*#TyFPHqY|pjHFBo-3ziC zy?sO+d1nK~C1GEcJp{oEJ zLEjDBd%LAtjJ|RX1n{^1p9{Fv<0q@1ZNByHP4K-Ar=>P~UTmdWfGQ5rsOfe2>O3EZ z)pTs{59SswM%Yn7{ZN=Hh2XOINAzT#z$WFE&Va*x!C{fRLIzC(0>8zKlsuj3;Fqt>fQ5V3Hr_tnG>6asBAS%$&o0EP8~?=u)H67|8TUY8U9vjb4)JEL+J zE%9kKA1m8?f$;>OX&M`?{@V* zV@5oDt8_TSd*|@|9XsuPp%d{`}0ILBe8;h|A;~;p)do zL;6oyQpPMsKH8vV=Uw;B7u!V;nEWm#2*Q+usjB@OC zF_2c)hCC6KK_2+daP{GSmwVNXrr7(HH-ZN|jGcW%!t_I)d|LAT!=X9^^pV>-R1iwm zaiA=&tFzKWELBojOReXI$Zp|ichvgpAuyxa0q7-SUrBT(qUBGl5I^1N>o+pvUg&)qC`_Q3FO)L`_PQpDRx zcW0muKU3JU7BF#}TS@vfO#(a%I?I}qmmy)t`scU(f9jQA|MviFCHK~jFGx6WemA3P zrucl93A8=5fTddwOy}+9o^4$xyI;q!lz#0fHuTV`+t9DI#BQ^KN3c~CSuY?EM1>Um ziPGXFq&L;+9~i-_sweaQU!$cB0@n1Vz{uqcXoN*9th11*xYbiSyg`W~m9#kX$4Kgc zbxT~0MPIsi`thGIBvSSZhY`;^$>fukx~)dXy&o|fg3ikdb`L;>&JFsHBmUV5P-zpJ zgtCBRt&bcF2m)D#7UjvccW$@6g%yR>EUK*@159eAgRhpf?jo|Orl}-nqulcXA(Ulv z%fK9**`@Jjqe{tM0mas9%^I&FNNZqUt$63Yw&TCU=%hUs4r)N*mz)AR*SX`Z7+dosEFND6jq3J7H2(}YNZ=&U_^uw{{J#ii^1AM9tUxfReCHy`B*QThX!%up6d$0net*(H^7WrxLkoER}!mRNg z*co8BNsjLF;Li$&=Uttp4W`!t8t`V3yYWMc4?;*jm<((h zwJhO~owy4`2pe_kRr8~1clOcJt}Q#jSgmZS2C&p%3LEpJIkN)ng4e4l4%Y1dxSCn0 z=CD`mfnt6T^KFEelUW$CyEy`Pq@0EfPFo&f)|ZFIN6)>QD{-h1W

    {(UPvuj6c2e(<`NXIV-&TmA5SEGPayzu$C8(!?6jy%&T$T*`OB0dtYSs{n&P zUT-kPhO*+v3%8M$2g&3=9#i$!NH}ZL%nawXX7i{8jcEV5_ninncJ^V~us+VKJf!JN4 zIB-=_*WU}5WdMU#r%lJT%e7W0+Aiw=L}CN!GUVrjG-~Yc&RRSlw$klK*TuqauJ@SAnW18B?^flK z5=gZ?P`j^z&6eWos-9#8-g&n@j&=#n?4}yv4FEPy6;(Op)%?r-f_o7J&KbF$9$@&5 z&GMuBldaQ|l?TE0QmLBnUokmg5>9GlYy%D=)3|c`k%X{r$H-Fqc8W=f*I5GCi z+V1w-uF!!QKF9sems@^5Fw05O__VMz((?M*VX^ zFT?ShrPr_L)Wx9X>}IRYo_?5&Zn6JJH7^hjykoeSH+Vu$P+BP#hS_Wcrdz4tKm)%O zo@g>>mu(>2x6s)1A$ja<;Z87=Jw-zDH&Wd}whuO>m0)Ib^fxz4`9)#>T;Ym~2TAoR zXmDI6<9@)?%Ghk@Oej;m3P=X!o=sFWN)$>(xPYO(4r-ghcUJTDr9_AK2#a%{ z^pCM)6}HFqff&ek)24{lRKQ~Tw@(ahT+%?otM&5y9j1s7`1b_iX^LIP!rcr8sv>>S z`1EkV8zn2_c^Ydz<3jW0=0_&n!VYvE`(2Q~p@jrki+p{6UUbDOpRml0O5JW2{v~A zOcF^iE|Bm|aY9l(*_LXN64u1VQKzxFJ``|SbV8CaXxR*5U!Gm2xfSWzXIg9XLFv*9 z#=a%Cvi-}k`x0Vy!0+IWk?L-Cg_e$L$hE4_s0!M$a!9iu?SSS6#*?>viRc(maJ9_) zkaO_9y?DI9_-k4-d78p$q3$Y_G=i5!G-j7^d|?;G`$RSl_%~;|uB`_D$+Q%zI{ZyA zNYNxA)nwAhj(bGjAj!}u2Sv5A(fvi0TFzqNKYQ2(bHtq~my(o-ywoWEJHdmm13O(+ zFcH9U%A=$xhioy?Dd8v_N9_?C{<{OaBeUb zoVKywA3w-^s9DN&{vM-V;O5wB{d2x53nGlO;dc;v+g>MvZIR#2@n+SQW2Qf~d+VJq zd$;Hsm#!_;;amjo^@qcmP(bu}(56-iVI&xPD^D;j7a>%k(0E$|6qjG=nb0V}>Q({} z&;ZqQ-C;q}Zp9H8(&?4Ezn>>TuuhpR4bw{JN{jTm@1cabJ7kz@XUsH~oy`v`ERZ1G z;JVhE7ALC@fnbUV!Xw$&z1@l-2Mk$N)z#6;|CE8?xVf=(z*}1ZX#h-=M)9*Kx?iJ9 zmxp@8A5ITg&7BA&&;uEep+r}g4G(azRp52kCVl4Z<}X^qML3)hCWOWSPjk(m__a3$ zQ$T%?arycfaoBW1Z@aF(a*!%b!Fo}Qz0cf^9;?S!iRsf9l+0am+mUhsev&N4_mf5v2?zag@z znOSJX={Byo!6%17{k3F>-8!`}X{iWfHC{PAr^XsT!#GE&j9_T6lR*2e2)<6q<9g*I zC-|=6f&W-uMiSs7nlp8;1Npsw^20^+qBkgx!qt=_f^n%8}2h+$~-X=mhf?=L30 z;&QL&@Ct)%Gtqse0vrOL*cuCjWVSeIXbb{-nYV88QgC?X);umPoe)43@{5$TrhE~!nk73uB{>25Z?;azy$m)G^b?|bH%dFTD|%)FnR zQO|RqYpq`#zd9DT)x^da$o+pXk=Z&SBmvX*o574K0a17`c!%bD6M&*t**rZg>G-1SRPAm$QbQo2QCRn_$Q(No%sUCQy6647v=&TW^gS)a&E zCPz)FhSDDoT-5dke4-MAdtblx%)7aev>qX_5L9m-&OFp|v z$Y$`3+4j1l*iHicRxa&#gDq%Gw|y%4HYetld!v}apyE#q5gwoB)xSZvTz;(wXd%-E zoz9ze^FaX$XjK6X}=3OG7cquy)DS(F9KRCHKKkl*15vZL%FjEg7vk@v2RtT z<2v@Cv)d#1S!VC070PqpNxxaB<0xTPPjHOg4eSvPR$`{TK#|`MJv)k1Oi6hfq`fi3 z)L=o@JNq-=bb!b+@IHLL9zc|%ivxQhGRfspeVU?iU%tnEqte<*Fg@q-@}D`c3EpeI zA2tZ*G!1)Ks7Sl_hTRx|LWS5*6pN33$OT;Q;B~^RijF(IXVFBM*2}-$m?Is%H}L|K zB?zYcFBd@NORxe8=)7_x-YYi@83dNTg0xbz(6y=&`u!%-V$=ENb0yi(VB<^XinmE3 zpqsf(Bf?888H(U6ryDM63B*yDbEG9)zJ5$xX4J@w;h2Z|(v|$~Cvc56OP&1NU>Jpa z@~yKMXv_cf40O#mbMx8Dt%?y_2wmJGPjqR1ZQ1YBb3Y6>lvB?qw8Lr&9fTfr2sx^s z8inynm%-Vf=LbS?^X^@}52=S=SC#Wk*zU+jjLx%p)b{b;?E7djjAi)cMc;M2hU-OQ zd7>*y=LzKoeheewa(W+xt8#*Ws8YDQa6joXj}@BYs4IZET(Gy%(%(V9Lh2G9fL?(< z3UK=0RoNNLNA3<42HI|3wLOKmnontVhPid~wtjQDfSou4TP6t-tqXS~wW~_x`6y|V z32w|?dtgGtSLDG2Cv>;ipHt*CyEn5p0R;`yk%srYiV)4$(*@n$czixJ=mC9YZS0A6 z={~=j8*Pw^eDntUy{-hLb^Uzb1|J_nW)m>k8B>YLFIqn2vPcr2>x@|1Lh(C(4LS+p z2P5s-1I^EEKddWd1%t-cZNJ%Xt%L>)t$pyTWm$*xI~ zl@z2!^t!iUzy{Ifujc2u0!BQt-h&9}R_7Ayz-wloP$>b^HU;H6U}6 z0p`YaUBCJE4T0W|@CnHpWifhQ8T-dJJ-mCfA=o&HQvei+l{mZNO`W@iY8GzC@C_YX z?-|64hgYsm6}QYuo(MhmVEAT$!^N{Qq!t(;??>TUILV}UwQ$&ZvBLevpIBHhy&>M^ zdsOJ8+KrCe&19kfhCQIQZhE?|SZ?n7Vsr;t>Dgf*JqN+=c!~^!H`(>fM)}ZlY_)cAvEFX)T8!HV&f@|VeYxt?mr4{!rVj&C+{1Zq z1$TXBdFOA%GfG3m#%;ZN>)kVd2sj3=hCH^C!R_K7OO}l1R4z(v^2+OcGtkYel(~bf zpOjt>m9TF6a%8^MpA>0Wz1Q9Gxs8a_!+GUle6HyhT?ABb3(^cE-$B?(-fYZYjN?s-f)qA{rTJB z3u+|W&KH`(d^8DK77lFVJ2oR~+W)ERCZJ_*lVG|N9!x2gjswyVwXmAp|N{gL?*8~GQ1B+;=Lcf0Cv9b63jBI zC~y;zgf5Tzd%*japhoJKu~?VAwuxIim*x3N zGdc5;n{yp7a2j^kP_oTH~;{7q(%55axTXK-u}`N;2=kZz=B_rij%1A5+XoMESxP0o?!%?^U=^b&6YGa!KfmZv^9iy)2&t>C`V2KG` zbZaXs=G+oF$o0VIF??~-hdi$*FOR-3S?3aoT^XtKqa=m<-z>;#P>ut1%kdw$(c#4Z z)=j4y5d#I}?U6Gjv|J~1VC=u$x6EN$U?coB=w1Rd@$$`p@yVv))4pn!2_nd4|C{J= zwL>OiRmbVUL=CB1NNozrQXv_=CkkAX0C=PeDi>5Hruqo{p4&GhnI zYjL8W4U4>4?mMrg`irVc6Yu@zoWz60=vT#^9yn0x#f7hHK?0Uf0)C@0IjTpBJT&wK zK}wim!e){SsH6HpAF9rYso+~_bEN$at z);8n0wk*TTA$XELTcp&YhsCbZ+e}mpIaRfA;I#arWaxJm#bN$Qi8F-(kAY#-8q>aC zviaHyZjiB2Y$gtGZ~zhM_Fh@F{Y#L1*h0~~x+iObtTvrm1H&COMT@pr)zn@iYL3I) zo!)37Gn_;3uoc}KdT65~U?t|HN%zzD2jqDCp?-BLXqcLy@HyAu^gWhyl;G9XX?`Zm zp~U~md~LV(b%j-^KJ&lA#o%oMlZkjXlAtw?O*v6S#$Z5Vb2Kk{)@G}!|8ci*2%VdWn5Vn$H#G7f zudS*U9yE8p-nygvR3Q2OE|7KisoBVq=gVlp3y_DbIr2QsnZ+C~Y`V^$5-Bk4PD&oq zcuji(zAKmL)_t6=E4(q{Fr-3DS(!dgH)mb)_C?opU0_&@OiM%pwb(#tw3SlBZ&0~= z(BuuXrb}{448{D8wn!}zVUdAPOHLdA!DI&30dn>>n5|^mIE2n!@8k8_q8J8ju3$YE+=@uNJziFu#7iq}nVN_~g!}71wl;e2VTezGU zzQ0s%S_-K8iih5`pCVVOJmcMurp8dGmYTKq=%AC%zuK7=!+tlX($N3ioVo+)`zXS0 z92r>*)61zWTBf6H>+JN&1sg<^Rq5xAU`H=%HfF| zR|y&jz3@sJ$&Z^zW&E>vxhI=~g2qZ^$Oo&>6(*%A4zg#B_y9J^cQB`sMxDaSuaDhT zbieg&Zl&fKTb`BR4WZenyJQ0Q-0c7Da6t3BqFMo9L4DH=m_fzB5I^8znZHHOZH3zF z04d@-!+BY8c0EGmabOmU%RDM9Tsm6P} z>S>pj1)onfwZ2aj-#v^zuY&r8Yc*0Nq?_ajOPZ zhdF!dUj-H4u9;2?46qbuC2g5q?<+BU6$Mh-qM5G(?-NF8PQ$xeNApM#;haRog108@ zXl_s%1PKc@+&8h_O5l$xsl60BG`X20bX~=!cU7x!8@pU&a?WpzRmSgMNm{=rnCS4% zBwf(33VEN|Q0-vX!)tsQ$W2)}_XRM1HnmD9173wnNSrt!hv?@kGD;9=<2!D_j*sUJ zKV;LtNY;XKY7#9c1y2yX*3bZ1gWm59@x70^_`LVKORa)le2~_UlhU8{wAi=5I__f@R<}Eza8|aRA?H2ItfmFTFvi0ZJ;v?P_^i-n zWqk4XuSq{zPVC@6!maT&=Xg06(2PVd67Y(Bq95*odm;c?nVFYxe(Iyst0!eU3U#(?nUucWj5 z(x<7oG7pDZk{N@XPrbCE{hpV*8j_Fp6sJI+IS5J{`=~(%5)g5TG=F8209fRePT15a=5uKEBwIyp5YDqX0li84PB~P+*1a}G2_a0>nzg_XqIWF1EWku z-+U<>>=+xbDZH6l-|1itZEMU2a=+QXxp68e@2ULpW^EIf{&lEvPi)i0 z@lS)hY7s(*>X1AA4IeWx-mcx*@ugaFvl5$SV1QK*9`6d+``C@xuq(G^$TP`fG+=Po zQB)@*T_$ZZiPnY}YjE($o0oc)*VWTA_Xc7({3IaRHu#8SNb=ln?AgxcMNN)Aw#ig1 z)eh*w=)TGGYa6kgH_K_NwZ*4>A>;)ypES5KVf^Q^r;)B}utW3N9J|o~3BtQGvS{jjj6QLYJmHC_`RW<)v z;5{FI>TRche~DrnY*(B&>WU6`bon(HAtQ9tf2yKfx-`s+Xl#lEw1+v#rb@ig8GCWu zGi3&CDY&eSNoV5)uc5S`!woi?!#_(&vl|v09GEEGHL3AupXW>#olM2L?67J2((IeS z7jq${U7ovXegBw328TejL&u*fH-%Y(87q)$@UBy)tAkLAE=x<&-rlhvjX?Px=Z{E> z$$QbngeC+NNE4UpC86`=V4>$z_=Mg}m-E4u1jc^)xSa8%WFyFaX`ZzGn|l|Jw}{+^ zRL+S%-)h2BPcR+$Bq3p2oZ65b91D`#bE`>Clq^&r6R#xWO(Wq6`@2$ml|sCl5WmBI zM=cwz81qp4kwep$`1Qil`0x5Tsb)imQ;ri$fN2dYMpC=}ytit(eJpgZn}=qzNBBn) zn0TQ#)$K{FlqX#&@Bwrn;{;%y=+&o;yR6M=@&vjt@6tOYG55KjgXSy*H|gEI7!p1; z-M|jQa?sP;UyQklPqU?ikEc;ZuK`#Vv?^bVq1PbD7tt8Rfj@BUU2I9bHQ|HhqKX4(q&{nc8q_Ij8XJ9i%g9YvdmPX{ANoGk2~fi5JDQ0uTYb1h zxIGk4QO|SjK4$i{St*2v?2EyTJI?NdcVwDk_$$(L8;UlbH$c95MYj!#RuKsMOa6&G zw2G3L_m{URgaiOR_|??H0PI?EFUan*zY3m4PTGMf@a_A2_GdmgTJ>k$N(&!G%S;pi zFLF!~B9Zg$_d-$~FZ2Tb(BUGvr02BzAHU6^y#( z)&>LG5zQPs6Ww)bQ7WK0~;mvEcn`r4g zvYcus?{c(jOu)htb0ZBR8&9T!eOJqQFBdO?DV|p6Ci5Y5@|<}x@Ym$5+9!GAJp-Ww z^HY1FW)MLcaNF8-rR|!J*C((lVjs2b%)&y3NOgDg9k;S<87gr+Pnuk5(Tp?FcjDOR zKvv*J?1eDOz&&<{MlAAViM)Pl;B0$e)QjMxm9sVcs^E045`SRtaS9mUAN?qHKPzZj z!~J_3L6Q0EE2~!_dM|?o^cscn$oGDU9s=(De|K{HZq~Vjt6AR8F>5f+VF7w{SpL<> z;Agt9gwAmHv6Zz;pEytC%STmzLJA<~nW1sG>1lt{Zj{fc%dLX_z10A2WX~4Yt zI-URib*uDdKhS9|RtMYkpm&M<7F7Pn2c+J9ptqdRa#2)wh8CQvdC7Adhj~ywgHaI^ z^QGvWYgnd=nBcuIaC-d|c8Sh#+7klH8h+^#q)Axt<;?#|lU9znw z#K${1vX_oc0rHFe^TabqxSf%)V>It_SJ~IEFfghpEz9L8eFNvwk`6wa*N*;i+rWao zw{v1RT~`u;*!!TW5eziy^Lrk99K71pJ|UipopqFGXtOOl;aP2~=ns9)!}Ye1XlOTt zKP3ss$dcZ8xR7+mt_T}VZ3_5WA3MJ90%s^P&1Fm-r+L*N0$F?#thw0xkzZdr2roef6h; z577&jErt5~>33msWuip6t3u3XM>AP2mKBXRg9rewZ}l-vC%;iQ3Qpg^m_sXu){ zCC{j_@f@iV;K%EJIOg_5PF0v&3+yE|ol6-eA1LM)tvw4<%u(#`&oElwktb=IVej6M zWKy-5sC6+~kI&A?AYo;RzI)o(Ou072Z!^SbhiCms6LH}@khzosdX$dl4hzp=0U^TfuotMmZmnfqeZZ)!py`rIYC-dqG5`)zMl}B_!vjw9j|j( z!A8}QPALZ11N3%(+TPtL=0#<>9zev?DU{;)ww2(d+SRc$(eiMYHA4)m%E#JxV6JJ& z_17)?z4QG=U-Ly!7&XEMPh&=a-;a=rTmVlWUY@sX5SvL%z$1(DjJJJojHraeqL{;{ zXb9BA@9#ZbSLB#TC#4VhwdC~}Jh}(LiP&kOB0Jgd$Vp6`!WVe!h+D42Y2H3EWS8v( zEe)ocn0AqH3>I;ei8Cs^+07x@5#1p-+-h|D=xX7i%TmdWx*nn70yJ*hRV^EL%@`t? zI>58XvF4pZ9u}S`hcQ9)G0@Mkl>6&Vc$t09xj~@C#T;%aIr#DOG|UpG%xIAu!;V+S z*XM3vUx~d}$kpE(MT@YpwUrvhtb4N)&=LS8XjMQT<9{KYTn3^AW=#v!FAO_cYopXF z&A7oUMeRn&?5(F4=5fw;BnheNhgrtY{?-<;Afi?sA8G1R{1Cq@ZK7VF_uf!If5@8r z=gA0>R@^`Tmy(uX5B<+~NZ|O?di}q7>`uIlbo5pN$Etjhj>JTT@H3O%Lb}A)>NUd_ zj~^GR7o*BY4yPSPLW3U2CE0ba#&QPRua=nZ>XjytYu#m;JZZpn+p3W(_@uefhRgCg zM|o8_fv+WwTe!b`9VkXvmYkes{p7}7N-g1Ik-Eux{%rx+d|(S;eAX@)&v8WtIj&g0 z{qM2 z5&v3Ch!)rv?h5IS#Ix-RO)IyfsR%|D1M-;g<)osz_nzy+5AN}C(0F=OVX8?oD;a^B zVJxl_w4;S8OBu}PY(LA%kcPTny3eHkd8ZB~gE!G|!NkE&d@bQ~>lSHN%Q$W*(CEC? z`Z0RwYbJ$;Nfu_`8mA*p@Zs+?VYL=-;||_ql_EX|rYCvchPYRCUCjRM8nM=I0j)?R zOixJH8Ns6Xb=)jze#`PbdGxXUZo0W?*~0XZ*?MiTjEzD5|ia$>atK9!2Xa}}?6i_u;ft;}9$46w)TIuHFEM%$~V1~S_g7oMzsylLsSEqG!_ z^ZQ2%!AktCanj+ZZ)fVseo%_MW>qwpX$>VGDdN`USB#Gkaie<^B8(rOaCsViB6Ybx zg5zLgQ{s@fjv-D2uV}QeJvpcL?KzhIhoN>j%6Y@9_c<4OP*1oVXR;NRI?m-9s62Ui zlhYZ0zHmRa0GaH@4Y?_W%PPCW4PZN3SzP0U320s z2I;wV(SEyMweC(Ldi=*_wDQ?uid7|aq&F7T(Rn3Zf4paiWPAV}x4-;MsD%=cYF(BG z^O`lG7$2~g)d{Q;goSqsCW5%3R)@vm0(O}j=5Ys=l`rbtpeN^cc7*FA&^YexUozYo zr1aEoShxRLWQ2(56KIqOSduL9OJVJEwya!>{Zw%}&=ca0@f$*XCy`2~ z4mXyjCgG%!(}Z_If~{|+V)FG>?ncQ8bCO1+Ijk$93(IqQUL^DgLl1AR6m>X%xyu>7 zkDyo`?%D{M+LE9Qtuwfd+*sZDv;@>1NRuqf58dA?14+TW`P`)nvwbJw$Kpn^)ayz{ z@BX>D!#9Z(lWUvKtF%=jg1)W?&(3(hIh{LK(d+i>>tDK3gnP6}|&@-YKN?m|?MCeQKJ4SSrh25n}DuY#xwRroK zn=9q2M&ceAZl_Bc`CbC+|GXv737oLBX7%7kOn8^BM~qJTXGrX=fA$(eD{ryU_6ZOF z7Fif5S07f{Rlp9%hL2`ukfZw8*YSmhGi9VA8I9a7C4&Q}YE43`xMpW-F~^a|^G3!k z$}Yr})1&-0sbek0A26=BVx8KSxx1AjFE?E0aK@%z_nJGrU6HTiAp1*>=)K>+X$6jp zi1rYlD)Fai03Nxc@cB10hMI|2k%~)14L{%E*w>mWm1dE{K ztP;@$rdV~mcI@FmAD5+po8an4Tsi{;ZG~MN;&iR(4B>Bh|2I6e{zydM%ERgnf2vMh zV=R4fj(z?1n{%XEX%>~FbTs8=?+?glZAB(!YiAq}7G6f+EG?|AZ#K7`BD)ODcvZ53 z#(o3>NTyu%h;-c_6$>3DNcLxK0Sq~ARkI6Q)_)yLd?;;5?9IHJkMkw{{lkm$mcPvt z(a#18KiAV=B!WpYshBMTvSxRRQEsycNNi_Nw9a$4Pu}4~EB57PM%>ZpBjjhkcYs(3|gLezC$-y~FjMtP)_sucAc$15wP^Vk+J~+-8jq33bsM z&3wC!7xK+&3b;D(2*v7X-v-%rS*c_QC4O7+^lLEcSQ;!%N6r=Dk8C;w+#jVq2GTTD z`ErgI>$*X z-3g;C4}@MNKFhcdsCKfg41=E)$<$mM0EtH z-$rg(7WOs=ULi)UHL%U>XO$7h>qWN3ul`oI64D?ddJ1^meh`G42DqqRfV;rrAFv`U z9PM7H$lJycc7)0%bdro$+H5!am1HCWm)I#}RiK+RFY^~H0!j;@SA>WklUln2d&z#) za;H_wQZ}Y5QU!d9tko@odWzNlCP+#kIna?uT5G&x-%`d}>dN$*7Q&E!osk%h!S)*l z?}&(-BnxY+W9)`s#^?qg$)oeaWKF)7EKj?2k=TBdqGPT6zA>-k8v4k#&GpX;oUTWT zv}nO2^0Jn{TUNxli!r0q_5u$c-4XC62o6npd*n>3LfY2sDx2h1IM$NUIFz@j_g}Jc z^}Dwiy>HJrr$Gb-0qXb(052iEw`2Xzdq`#?F{!rwPL!6|SBw{*Hev)^*3Y)%^7e$= z*o|6;dc!sAYr*SeW;~ck&Hp(gV|g6=d=;iDS64^jz0ZFw!xCg27oQ^^_r#kX;*@UM zEw_Wmb&^E@o1hUlH-I&)|NRYMAQHR*jyR0{>kYK1^HT6Z7#P^G?hO|E4Igbe11z+^ zecxDmwU@798}rYBUL6Hap4wH&Ff>r=()XxTD_cAnQ?VbHG|Io+Hq}PI^u<@+ss}S$ zrURW;Rut{4Pg`#Nwl2{C&L%L>_7<8BEL4+89;2MD9?it$Q^|{hbXfxtA4zgc+9R1`44kjAfubZ?Xbp%l& z6JP+Y9Ojaz?+TernXcnz*0F%wnNHMfXOcdpSLYT2k_UeZSy_l0mf*J!S%14=G6BDWIc}%X z-&~OZ!Y|rd{Idf2NPv7~!rV)V$lYoUr8y1Nh^?+nF_S9A*7Ou4DCw~=q(`*yx&H9jQh7|I}}qvPq00#I6rtK9;EGYa>2hf%I4+>!rNvD@4K#8AoqEU z8Ttj9SLmZ=;z_y1m=E9-k0urZ%Z-@A7osO+vCEescj{Tts9<$kfw zf@?nfHGy-jAgazH&1?#QD=2E`#c%xNIgh6T&k5E!yh-u5gWdR;i^(@#TeU&%y|DD= z8}-XdOsQkkkrHyPE&4{tpzWe8sLlxmr9I%!y;LbT3<{1IDqY;~>56P+X{pgCS6H`8 zRY(IH{=j@}&mOsUv1JiTNzUYH@{ff`VFave7nK|2TNEI>1qpt3g?cS6PS?xAXczN* zXxZ2#g)c1%MI9wU*T^p(#a`aQyy)|kzcM*$dpbB}wi)pYWMfyTfqBrjwt9Z2d?eQW z{CIuiNuMB$SEa&A&mbwVw9lXD_1Ny5|HbV@AFdXyvS+#z-7LmO;?{`=VjsC!b)I@! z{q<3i0%}+Hi#=>%j|emFeCGCX+^;!saea-~dcqRK{2+kgA4PZ)j5uS4rdf_ZMjeD# zU7uZE7@ZnE+3&Kdf;;>`_vMbi!DlM&Z*{RJz5fw4ifknA-c}>zSrz}X;k-yv-dU!Y zu>eJac=cW3Ri5Fz!;KbrCSIK-!l1UY*&Vdc4H@6v>);h;ZI$VliX7)Q3fN!f zT!q69HKa16Y-4VR-=#tzKj7zQJfL_c0x!1RXlE){yADzg-FtdCSFZP3W@!zj```w_ zZE`M59@4GvArFn*xB<}m<~jOpU^$*9dV_JbxEO#X#i8+Lme&p9&mVaLxq;I$2UpFxgGNvWjlvwQ;bquz|7fl0vj9l z84`EsU3LMLyFEalD&`rB_9sfftazqMh6^6e6g_EKA`&d$KUr34v9>HmE8Fy7N!-8l)ocaG%N+$$xTH%*|D3AH z^DFOq!TdrGlynOdK((ZvL0=0IJ9i7kPq1z(;Q6WVY5&!K{}z z#^OM2VI$G_F&N1@oAt%NMd4ib!uNywS*~2y&+@7BCepR}2~;c1*{eXl!J4B!xTPGI zMfP+*NM+@g>;ngJW#-vkN*JFtxJz79>CeMgBiUIaxpZah<6X`UM!s$d%3T>p^}d)` z?v!j@ed3Y(1Gw4?4}EC83ka573ebQ+@F*=z{m#gufvnR1t(n>1*5m)2Q{Jt_{1WW> zxaF&RZB%GzFoD=>7X2-)YiOVE1Y@VbPBNG7l?IP2h{)b}*l5}5&V+^*I=4&x7?%JYr@>SjfHZ)hJ)f zcbd-eTzLKPglVGEQZE1Hi56*C7xz&CC>4BZ`4IBd*`QNMCmf^7Qnje?nS)uLKPzRt z@a3HWKAT){%)4M%m18n*Alt8yj|8>J!;4k_a>yP2D4BI(Wo zH`;PVnm6V-c|*v@%5s#qr@1k+VFt<>2klIxndyzK8H97s0`KFM*DFBl$3(bV4c+u; z+h?lfE$geGSZLIW0+c=n&%f*b^)ZnL2QZ~9I3wg(r;0?jA8v_2o%y#m5zReb>zy{tXXm`2b*qnkjwX2bh`F^mLu9Db_XkYnR1AY6kir9-jaDd?gM(I zC9APY)dE65d@9RwP8D8Oc8+^D-l^q5!crIi1@Q3Sy;-O}|Kb&vmi#lcjnR zG_TjYfV0>5@FtzDUwTu!CuZluv$OKh$qR>qySox%Z6dy&FjX_Y*8>?n7fcbC#iMn1Pjg*XPbRmq-7s z&9j(L6%!^n=&5;!_J3T%>I>(d#3`hTc5!ML#nM!!bhOYg`uyCJ2fT`};R<7QH7lh( zI<_6(`sB<>S9<|HZcHoz8@R@q@0ZB+r*ICQT^d~B8`M2 zY_F|jsi(-RMRN(%C88x?iYljh&`yw_Y*yUV^>8UMuPqj5UX=z`v zyj-f;?oQ;)Th(?d-=0P_cPE_Px@XMi#mzwO z`?-s^C?{2zmD~kC;cuV+`k|8O#Bz|$tvg(tRCLU=TTE%%_9k12`^$0&-Cjk>MjYYq2j2VT$wRy zYoi`33x`YO$d?lDNO{Z9-)iV;d)c%jE>rKmIpkXpO`5@<`pkIdev<|)+x2gfiRi=GPxche+ttbi8i?o~S&8j=f| z*`^_+yza2TiZsNw#>bL1!+ zsb`b0O{y28Ou<%?ho&@qm)1vX!A$S|OE;#fOFebdscOWKb9=;aiMJ&<@d8A9t;=!; z7o7KKQbDx{II{NZw`pdoX{B=`pYn?8GEHQ2`QKfHp{+%#v2bkRlkuqOjCmeeNyrgN z%k><6$56}e5odXs`SwTK(bR}0Wn^UBHY>>T^&zI$98|;&P{aT1LGOR0j!xZrEx~myCygAH zhhz^?ddJW0O@`BK%||F7lDB48d3d9TBNy}La!&%O>?-P{Xq^CR))X;mo`ID^xo}i zy1aaM<{uYm;(fcTg{bc$GoNMg2CnuEH0MdB+1%L#lq8=Wb-3w&w%1|ji@%-EtrR10 zB2~qqf|jqXHB5@yK8n`nYCWwY@ie|k){|7A2CZZiT_p1ozEBrOOakug&@5TrYI^Y} z0NJcrF|W<}V|Q>}*tXGDXg)YQsM6tTz47xdZ~WKmQ$jR@y0;>m{CoQ1u#ROlT@J^j z13u>Yon4^BL7ul_Cb3)+_RJdhrJ1L(kU)uS(oA+w^&(dQdpD`wK~AdsG)6K`vlEU=HB=10tR`>rVi`Y!o;H zWMgDxV8&%%bm4T}P2HdQ3Yt*o)5-&*fQm>2LX=AuX0g-(Oio|01_}MMldPoNRwLE- zH8k;>wC=+hd|&|6LrUrm!ec};a426JZgWq+83C(8@f*FAxj4{)Ltpd2TiY5rxHo_9 zHp!QVdu*(9^B09#2COV*L{b=E=E4VAb7O=NK^F4@r&`7j)~@_0BJY}tp)+!5Om>?8 z@_YgnS(3}A2-S{c)PEJQa>uzS^~AK^DrKKGSs$k#n{MfN1hEl~s{m5fyan_eL zx(in9o@N0^(ljPAh<;rVyW(Z8B~f{n<-88ec2K>`jay2*ptu7IbjaEQl4sL%<3VV`O?UOD9d=d z@7xk~As$6oD&}=(g)?y~U;l=(uc;zYO#sT8pL4N1J#_lvM8L+9(Yu|pAbl)Fjgo8P zGz++OPUphpdz)mq=L~VOv~EDI`<~vrIT&i}pfF{K;aO~@F`eBff1>5fHqF7g8D)7O zfU;-LE7rU=k%sqfJQa;tX`<4MYM8azFdDC9eA3liZS4CO2Wf839@WKc56I$f3(D&K z=~3!PhEjoWO3rlxgn4*?rh2Gy`?n8I3U#tMbYRp&I$a5Tcqt3WBm_)0qn+ zb1ZaWIR-(&8JFd$cbp!-yqOH*b}T~?emMvpDAg8gGe8{{Eml%|{YXl=PCKmIC2Nq&DQJ0y_u0st?$>>rUEs9#lJMs{?fFGZC8-c6$bJx0T>~ zeUfk+k_l*cUXji{aSN)T-a58moV(+Y6y?=9p1CC9>-Eq3hqD)YAPjja?AAV0chjndd0o}Co-20= z5U#8%2qP|abPg3)M~jPwo_zoDXt{5Ok4{%#u-rWO+o|K`xE7Z+IV!n$(*_PGVHxl2 zSJl?lST1lI59eo|x`gfS&L;>sw*7Jar{6dJI<}*DUX30P1shwJ;-7;cpei42k8EkZ z{*Dn`Z3zoF!&$}#B}h` z2PzNE9wf+LwhXnO`CklI31;`F{FG6hk!Gn<;krS9#24nR8qJi+xHvDJEJCO%8#ag0 z4zX0zuV-4rmsRR_U%kllmI}f)whh?rV!ODP?yG3vW zhaMbQ1i!NE(@VA>&LlDc+vAGixlA+mN~RZCe_^G#0mpp4)|pHL-B@QlosAazxPiZY z6$yD7EVCj;DYjZ~EZ^m2HJyY9e)YRmXKz5EAFvURhG^$=(-3yu)===#6kp*QFR~ zBv(ED*3wCkRlv>rI4!ci31W+`?F%`F~hN!s?>nCR_#b;nN7gCUkf6QP|@^fG=K)9H| zr!i7LQos)h+7*47RFpkBt;p~ORL-%r5X|bHS*H? zzs)|YFzq*J{mS;hqNu@%1og##ZAgNw;-wP1^SYxY9^C;S@9|^-u~{AFV|?ly1X3fbqVU`6YMRkX)^g z8v&9L2OmjUVB6SKgOS=6f3krO-8T+MLDui!A;eS4uU$^~BPi|z;HiZh;1{FEuM7OP zW?l9BY3V2tyJe#qqqqA;q;#F}0%Unds3XMYCC3&VvtNxt$fMSRp!od6m>2aPn#KX9 z%JvI%b6m{4d|!Tq`o~9eSWQU$@=w|8V2#W-!n*lpx-=^rnFr!WJ(N_UbEc-|ksgbh zRs6-$jHl-?)g3X1crKbVux2v;(}c%{yjd5fNe-&;)VX%1GZ4+I583Wf2o2OSr$uzEsi;9|d+;n*BibT-wjNV)J((47y7P=Z(d%P10Bsu_^Bv#m~lr6rX&>nGOJK#>DI&=IKx6IQnd%J&ty;{WTIj14w|kbIod~J-;`Ap8 zMcn~uh|VMa`)vO}6SLDRXwpotQ#5UDt^$%7w2vdAXzKP?f|C_A%CbLMgonC}N9r)t z?X`a_2@LSyQxd8nGC`f@mwr+PH+l8xxwBpL*IH?7fYrO<3|2dMj(u-!4$QzPvU|M0 z+frV@=@(en!*_3QIZ4C#Uheky={oP+Sm5XYd=J_`Bs|tTPEDdU(U!|Fqgu`YU-cW&W!F=7Lt&or3G|0S3=109NvYNSJ~m@nB?J`Tj%s zj+=xf-T@vRIcREOFR9IL7Zi1-OM1?{cUfr7_g9anz_~SWcU2BjKFllvDaSt|{TaPE zAq|?4wKua%TGR}3D_S)hHpUJm#AV~@-%i=5s2BT51mEp!^=Q|4O(|w3UUco|ov#*d zZ`-*%&lRTEOlo*f3urRsoRsr;PE77|-Zv|+I#6_Snu{uC_Ew(uL?m9o=pu{t`CGSC z|K#D*@uPmHE!n7_wUl~Pz|EQyABT+a@uRQPwUc!XwJxla^azP0UJ}~YVeJz z(>+V+WIy5wb-Y1?!I{!0zacei8+v}QN2%{4ac}|X&sq=u5`ZDbs!A&@h>JVFhB|n@ z<4h5D=X=!!N1Cof5h@{d0Y0Yw2iqx|G|%k@geG-wF`bTm3KkrG^rB|<^JzmiRy#}m z%JPSS@Wv(ccU`s zz&A^UtMIJDV%+|lMM&Hg@KNuB*7X8>i^MCwD0Mp`H*{l~@rZK>Rax$KzssEa4rT1+ z7J*HghN;*T5bir`F)IH`__f}dC4-<>G)%;w zBY)_kM}VfmWvcrX`&)f!HAs3b!9QVVzptd{Rp#^d0;8N@bdB9fffb3&2s(~M2Zf3VeK#QwOJY0dGYL7#W0?F`k9sz^dXQz0T<7I#sM7%=d@?_Rm z7K%*~#N1EyCZcBxhZlrn!>peo^;>SPi5pG8HI8EBFBJ{kn9O9MQ_|HU-fg)zOZfPz za=h~H(aP6%GBWKTjsV2fbsVFv11-A~liL@29i_LsRPr3NHt`-7tdL}~UPvVeYy=yq zkg=tPTWX|AkjP#oPoGAiM}w_VYsrZm(JfTUs^1G*__H<7o1s?``IDBjfX=Gb-V_bB zs$<3KgJa+wVVjy~|4(&S7S&{ygu7eOcI#%O5f3mTDxgsk6(Q0fw5TA#zyu6SgCGL3 z@3I6!(m05+Hv-0hNXsH2ppnpk5CIWc0|b#s$H*oK7&amKL---s7dZ#!oau*on1`9y zoImH>TUEDi)mLBDov7o+Toz9@!J1EqMYe&HD+z?yxWZ4fh=XEhD7i0gRMlov0j|Js zE5ML*6h;#2c6!WD=QfoYfSG;8#I0E4QES7+Ni|rz3*Zm%S~1;Unm= z)-`Y+1?V^Ec@*9(=LB42nk1t$BIBCCo(kmVzHEs-U*!`<*sR=T35Y%l6S zb0HbW@FCymlxlEOOrv7uYYcwRtsT-e=ivhn>NPcD+h)Jt>X+C1m9=x8x)hKGGm^B9 zd&g}i5FcQ1J! z4O$ZC0DSVgcXD~L!)!&{DUTG#~~Q1v@a>=rgr?iTs>mVfUA{|mNo0WCELuTC3Ghh`G^q!Wuyqu-1WrJCtt39fZE1Vo z-h>jI8=v|=MG5#wdqo_#Xz~wMC84+coeAUXi>2-vXqj#1wI3f&^yqz(&bt?f`9p%O z!m(Z)*F5?gvSCGw0=`pCLm@2zkCREEpM#pboJs?nu{FOe%{C+GC+4ITc4enwDCBEk z&F#T5Nql)4!-3(5)`vY3IEIX>Ko?J_v+k3TT)EOK!w<+y@NRaXB@mVY!<5`c#SAx{ z5xKan>&C?7i*Axd5k0PrCyP4>kE#66Ag3 zbTS`dI(&plq`I5YlLh=-r%>9~-4N#`VM{*K#|}U25im%5kKN-KYP(_#5#1JU7po-i z>)%jA*7kRM|KmguzXaVyGovEoTGJROmVH!CVl%fdjAjXfaJt4FcaSwAYrs4PS|N#f zb$hJtW@Zj#vS@Ms$>@FdtcsF`xJ!A)XM&&IKFcX}k%7EtefOII(F>b5ZGYNyQg#R> zk9Y^Q?AP4jg#DXCk85lwbW{-P27Hd3%azZCdkS=L?Nbat8<&1q5L< zGs(A~sKt+S1;o9@2pKFO~iL?x$A$vzwDGR47jv2VQ+D_@1cad*U!A!>aHKE36LZ#Kc=s*T9!# z)=JjVdaPf^YY@Ee6|4F4_7k1@sa2EA(^)#aoYa{Ef*bcE=+wZ1XcQ=eD1)l#9Zfrh z@OM#RdIOBNJ#Y%^o*E>P|C<)<|Ka} z{z`#F(~KB@!1nI#46VqJFXsZfCBMWD`{6^c>V>kaJfyEvnR?d%6nw#>@;vr8`*Ow0 z%KbQ%H_r&-GW@EYw6r*oC`j@EXRiTi35MJNB>r*W^NgT6d^Yl)gSEoh`{dfVnMnJ* zrk3>d5|qHkDcjq=nl^9kn;g+RlDl-SW&@+<4GqM|iJWs%_eVePpRO*h36X07nY&~< zOhp0h3DWu?Tm`5%&OMLrOSHFxE%RIQL52gEPHH^1g13-OQ5lQeXf;S4T8nhs8o%A_6VVg3P@sH1sxf*lrS`?BH z8^U+_1=J0Y4z{nwHqm+UUig~gd5rY672D=BymKc;?~ns`v^Yim`M@F93V&4T<$3~K z=eL$OwO(0e4E(-1b!6lUEIcN3TmGPZfn2Jt$^mGHh+~eObX77sKe!&b?b82Z&(Zqh z`t$D^VLK!w*kKrYWULHxYpiN0wuGeR7C)p#x9U1Ofy&)SWq?>Pk{c(Efz z2hnCJDgp7lM7A-0))AQ*s*H9(q zVCrf#RdvDA^4apO_NJtpe>l2e=ZwBnL0)9KzOf24PHb5qWXH(<3nZ|%nR$Zf)h<{a zO!C(PE4wkyDQ?Xn#}{X5%NdFOV(;^&rg5HKdnfGD0J?Rl&-c#RBe0g59w?(L$r>p) z{rjp^TUW%%YLJSoT(!C`>J@DS^L2z1Q`uET__YkxX=*v1-Eb*r0GZq62o4^Y4|he+}97Bl~{62y**>=4UV7+=8;p7gEO-)tIMAtJR6!F@VSRf6-d|9}>9!J3Yh8 ZD!8`EOBSZ>YAfO~J!gLQvC-x0e*%p5X$AlQ literal 0 HcmV?d00001 diff --git a/.maestro/enrichedText/flows/custom_style_colors_visual.yaml b/.maestro/enrichedText/flows/custom_style_colors_visual.yaml new file mode 100644 index 000000000..e304b5b94 --- /dev/null +++ b/.maestro/enrichedText/flows/custom_style_colors_visual.yaml @@ -0,0 +1,28 @@ +appId: swmansion.enriched.example +--- +# Validates that custom style colors are displayed correctly +- launchApp + +- tapOn: + id: 'toggle-screen-button' + +- tapOn: + id: 'toggle-enriched-text-screen-button' + +- runFlow: + file: '../subflows/set_enriched_text_value.yaml' + env: + VALUE: > + +

    Standard 6-digit Hex text

    +

    White text on black background

    +

    25% transparent green background

    +

    50% transparent blue text

    +

    Red 3-digit shorthand text

    +
    Black text on green
    + + +- runFlow: + file: '../subflows/capture_or_assert_screenshot.yaml' + env: + SCREENSHOT_NAME: 'custom_style_colors_visual' diff --git a/.maestro/enrichedText/screenshots/ios/custom_style_colors_visual.png b/.maestro/enrichedText/screenshots/ios/custom_style_colors_visual.png new file mode 100644 index 0000000000000000000000000000000000000000..fc417d9b2da379ff072c43acf4c8acc740cd23e4 GIT binary patch literal 57384 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
    x
    ', '

    x

    '], + ])('%s → %s', (input, expected) => { + expect(normalizeHtml(input)).toBe(expected); + }); + }); + + describe('GoogleDocsWrapper', () => { + test.each([ + ['x', 'x'], + ['', ''], + ])('%s → %s', (input, expected) => { + expect(normalizeHtml(input)).toBe(expected); + }); + }); + + describe('TagOmissions', () => { + test.each([ + ["", ''], + ['', ''], + ['', ''], + ['', ''], + ["", ''], + ['', ''], + ['', ''], + ['', ''], + // Nested tags + ['', ''], + ['

    x

    ', '

    x

    '], + ['

    x

    ', '

    x

    '], + ['

    x

    ', '

    x

    '], + ])('%s → %s', (input, expected) => { + expect(normalizeHtml(input)).toBe(expected); + }); + }); + + describe('TableOmissions', () => { + test.each([ + ['
    ', ''], + ['', ''], + ['', ''], + ['', ''], + ['', ''], + ['', ''], + ['', ''], + ['', ''], + ['', ''], + ['', ''], + [ + '
    EmilTobiasLinus
    ', + '

    Emil Tobias Linus

    ', + ], + [ + '
    EmilTobiasLinus
    161410
    ', + '

    Emil Tobias Linus

    16 14 10

    ', + ], + [ + '
    Person 1Person 2Person 3
    EmilTobiasLinus
    161410
    ', + '

    Person 1 Person 2 Person 3

    Emil Tobias Linus

    16 14 10

    ', + ], + ])('%s → %s', (input, expected) => { + expect(normalizeHtml(input)).toBe(expected); + }); + }); + + describe('SpanRemappings', () => { + test.each([ + // Bold + ['x', 'x'], + ['x', 'x'], + ["x", 'x'], + + // Italic + ['x', 'x'], + ['x', 'x'], + ["x", 'x'], + + // Underline + ['x', 'x'], + ['x', 'x'], + ["x", 'x'], + + // Strikethrough + ['x', 'x'], + ['x', 'x'], + ["x", 'x'], + + // Bold + Italic (either order) + [ + 'x', + 'x', + ], + [ + 'x', + 'x', + ], + [ + "x", + 'x', + ], + [ + 'x', + 'x', + ], + [ + 'x', + 'x', + ], + [ + "x", + 'x', + ], + + // Bold + Underline (either order) + [ + 'x', + 'x', + ], + [ + 'x', + 'x', + ], + [ + "x", + 'x', + ], + [ + 'x', + 'x', + ], + [ + 'x', + 'x', + ], + [ + "x", + 'x', + ], + + // Bold + Strikethrough (either order) + [ + 'x', + 'x', + ], + [ + 'x', + 'x', + ], + [ + "x", + 'x', + ], + [ + 'x', + 'x', + ], + [ + 'x', + 'x', + ], + [ + "x", + 'x', + ], + + // Underline + Strikethrough (either order) + [ + 'x', + 'x', + ], + [ + 'x', + 'x', + ], + [ + "x", + 'x', + ], + [ + 'x', + 'x', + ], + [ + 'x', + 'x', + ], + [ + "x", + 'x', + ], + + // Three-way combinations + [ + 'x', + 'x', + ], + [ + 'x', + 'x', + ], + [ + "x", + 'x', + ], + [ + "x", + 'x', + ], + [ + "x", + 'x', + ], + [ + "x", + 'x', + ], + ])('%s → %s', (input, expected) => { + expect(normalizeHtml(input)).toBe(expected); + }); + }); + + describe('EnrichedTagRemappings', () => { + test.each([ + // Block elements + ['x', '

    x

    '], + ['

    x

    ', '

    x

    '], + ['
    x
    ', '

    x

    '], + [ + '

    x

    ', + '

    x

    ', + ], + + // Headings + ['

    x

    ', '

    x

    '], + ['

    x

    ', '

    x

    '], + ['

    x

    ', '

    x

    '], + ['

    x

    ', '

    x

    '], + ['
    x
    ', '
    x
    '], + ['
    x
    ', '
    x
    '], + + // Self-closing tags + ['
    ', '
    '], + [ + '', + '', + ], + [ + "", + '', + ], + + // Lists + ['
    • x
    ', '
    • x
    '], + ['
    1. x
    ', '
    1. x
    '], + + // Checkbox lists + [ + "
    • x
    ", + '
    • x
    ', + ], + [ + '
    • x
    ', + '
    • x
    ', + ], + [ + "
    • x
    ", + '
    • x
    ', + ], + [ + '
    • x
    ', + '
    • x
    ', + ], + + // Mentions (note: cpp reorders attrs to id, text, indicator) + [ + "@John Doe", + '@John Doe', + ], + [ + '@John Doe', + '@John Doe', + ], + + // Link + [ + '
    Google', + 'Google', + ], + [ + "Google", + 'Google', + ], + + // Inline + ['x', 'x'], + ['x', 'x'], + ['x', 'x'], + ['x', 'x'], + ['x', 'x'], + ])('%s → %s', (input, expected) => { + expect(normalizeHtml(input)).toBe(expected); + }); + }); + + describe('DivRemappings', () => { + test.each([ + ['
    x
    ', '

    x

    '], + ['

    x

    ', '

    x

    '], + ['

    x

    y

    ', '

    x

    y

    '], + ['
    x
    ', '

    x

    '], + [ + '
    x
    y
    ', + '

    x

    y

    ', + ], + + // Without whitespace + [ + '--
    John Doe
    Software Engineer
    ', + '

    --

    John Doe

    Software Engineer

    ', + ], + [ + '
    John Doe
    Software Engineer
    ', + '

    John Doe

    Software Engineer

    ', + ], + [ + "--
    John Doe
    Software Engineer
    ", + '

    --

    John Doe

    Software Engineer

    ', + ], + [ + "--
    John Doe
    Software Engineer
    ", + '

    --

    John Doe

    Software Engineer

    ', + ], + [ + "--
    John Doe
    Software Engineer
    ", + '

    --

    John Doe

    Software Engineer

    ', + ], + [ + "--
    John Doe
    Software Engineer
    ", + '

    --

    John Doe

    Software Engineer

    ', + ], + + [ + '

    here's more!

    image.png', + '

    here's more!


    image.png

    ', + ], + [ + '
    what do you think of this craziness
    • another one hello

      hi
    ', + '

    what do you think of this craziness

    another one hello

    hi

    ', + ], + ])('%s → %s', (input, expected) => { + expect(normalizeHtml(input)).toBe(expected); + }); + }); + + describe('ListFlattening', () => { + test.each([ + [ + '
      1. x
      2. y
    ', + '
    • x
    • y
    ', + ], + [ + '
    • x
      1. y
      2. z
    ', + '
    • x
    • y
    • z
    ', + ], + [ + '
      1. x
      2. y
    • z
    ', + '
    • x
    • y
    • z
    ', + ], + [ + '
    1. x
      • y
      • z
    ', + '
    1. x
    2. y
    3. z
    ', + ], + [ + '
      • x
      • y
    1. z
    ', + '
    1. x
    2. y
    3. z
    ', + ], + [ + "
      • x
      • y
    1. z
    ", + '
    1. x
    2. y
    3. z
    ', + ], + [ + "
      1. x
      2. y
    • z
    ", + '
    • x
    • y
    • z
    ', + ], + [ + '
    • x
      1. y
        • z
    ', + '
    • x
    • y
    • z
    ', + ], + [ + "
    • x
      1. y
        • z
    ", + '
    • x
    • y
    • z
    ', + ], + [ + "
    • x
      1. y
        • z
    ", + '
    • x
    • y
    • z
    ', + ], + [ + '
    • another one hi kacper,

      hi
    ', + '
    • another one hi kacper,
    • hi
    ', + ], + ])('%s → %s', (input, expected) => { + expect(normalizeHtml(input)).toBe(expected); + }); + }); + + describe('BrRemappings', () => { + test('inline collapses around
    stay flat', () => { + expect( + normalizeHtml( + "

    Asdasdasd



    Sent with Net

    " + ) + ).toBe( + '

    Asdasdasd



    Sent with Net

    ' + ); + }); + }); + + describe('character escaping', () => { + // Each special character in text content is re-emitted as its entity. + test.each([ + ['

    a & b

    ', '

    a & b

    '], + ['

    a < b

    ', '

    a < b

    '], + ['

    a > b

    ', '

    a > b

    '], + ['

    "quoted"

    ', '

    "quoted"

    '], + ["

    it's

    ", '

    it's

    '], + ['

    &<>"\'

    ', '

    &<>"'

    '], + ['

    &<>"\'

    ', '

    &<>"'

    '], + [ + 'x', + 'x', + ], + ])('%s → %s', (input, expected) => { + expect(normalizeHtml(input)).toBe(expected); + }); + }); +}); diff --git a/src/web/checkboxHtmlNormalizer.ts b/src/web/normalization/checkboxHtmlNormalizer.ts similarity index 100% rename from src/web/checkboxHtmlNormalizer.ts rename to src/web/normalization/checkboxHtmlNormalizer.ts diff --git a/src/web/normalization/htmlNormalizer.ts b/src/web/normalization/htmlNormalizer.ts new file mode 100644 index 000000000..be34e44f9 --- /dev/null +++ b/src/web/normalization/htmlNormalizer.ts @@ -0,0 +1,609 @@ +/* + * Custom HTML normalizer for TipTap input. + * Mirrors the native GumboNormalizer (cpp/parser/GumboNormalizer.c) + */ + +type CssStyles = { + bold: boolean; + italic: boolean; + underline: boolean; + strikethrough: boolean; +}; + +const INLINE_TAGS = new Set([ + 'b', + 'i', + 'u', + 's', + 'code', + 'a', + 'strong', + 'em', + 'del', + 'strike', + 'ins', + 'mention', +]); + +const BLOCK_TAGS = new Set([ + 'p', + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'ul', + 'ol', + 'li', + 'blockquote', + 'codeblock', + 'pre', +]); + +const SELF_CLOSING_TAGS = new Set(['br', 'img']); + +const PASS_TAGS = new Set(['html', 'head', 'body']); + +const STRIPPED_TAGS = new Set(['meta', 'style', 'script', 'title', 'link']); + +const TABLE_TAGS = new Set([ + 'table', + 'thead', + 'tbody', + 'tfoot', + 'tr', + 'td', + 'th', + 'caption', + 'colgroup', + 'col', +]); + +function canonicalName(name: string): string { + switch (name) { + case 'strong': + return 'b'; + case 'em': + return 'i'; + case 'del': + case 'strike': + return 's'; + case 'ins': + return 'u'; + case 'pre': + return 'codeblock'; + default: + return name; + } +} + +type TagClass = 'skip' | 'inline' | 'block' | 'self-closing' | 'pass'; + +function classifyTag(name: string): TagClass { + if (INLINE_TAGS.has(name)) return 'inline'; + if (BLOCK_TAGS.has(name)) return 'block'; + if (SELF_CLOSING_TAGS.has(name)) return 'self-closing'; + if (PASS_TAGS.has(name)) return 'pass'; + return 'skip'; +} + +function isElement(node: Node | null): node is Element { + return !!node && node.nodeType === Node.ELEMENT_NODE; +} + +function isText(node: Node | null): boolean { + return !!node && node.nodeType === Node.TEXT_NODE; +} + +function tagName(node: Node | null): string | null { + if (!isElement(node)) return null; + return node.tagName.toLowerCase(); +} + +function isListNode(node: Node | null): boolean { + const n = tagName(node); + return n === 'ul' || n === 'ol'; +} + +function isBlockquoteNode(node: Node | null): boolean { + return tagName(node) === 'blockquote'; +} + +function isBrNode(node: Node | null): boolean { + return tagName(node) === 'br'; +} + +function isBlockProducing(node: Node | null): boolean { + const n = tagName(node); + if (!n) return false; + if (classifyTag(n) === 'block') return true; + return n === 'div' || n === 'table' || n === 'tr'; +} + +function isPurelyInline(node: Element): boolean { + for (const child of Array.from(node.childNodes)) { + if (isBlockProducing(child)) return false; + } + return true; +} + +function hasBlockOrBqChild(node: Element): boolean { + for (const child of Array.from(node.childNodes)) { + if (isBlockProducing(child) || isBlockquoteNode(child)) return true; + } + return false; +} + +function findCssValue(style: string, prop: string): string | null { + // Returns the value of the last declaration with this property name. + // Mirrors GumboNormalizer's find_css_value scanning behavior. + const re = new RegExp( + `(?:^|;)\\s*${prop.replace(/[-/\\^$*+?.()|[\\]{}]/g, '\\$&')}\\s*:\\s*([^;]*)`, + 'i' + ); + + const m = re.exec(style); + if (m !== null) { + return (m[1] ?? '').trim(); + } + return null; +} + +function findAllCssValues(style: string, prop: string): string[] { + const re = new RegExp( + `(?:^|;)\\s*${prop.replace(/[-/\\^$*+?.()|[\\]{}]/g, '\\$&')}\\s*:\\s*([^;]*)`, + 'gi' + ); + const out: string[] = []; + let m: RegExpExecArray | null; + while ((m = re.exec(style)) !== null) { + out.push((m[1] ?? '').trim()); + } + return out; +} + +function parseCssStyle(style: string | null): CssStyles { + const result: CssStyles = { + bold: false, + italic: false, + underline: false, + strikethrough: false, + }; + if (!style) return result; + + const fw = findCssValue(style, 'font-weight'); + if (fw) { + const lower = fw.toLowerCase(); + if (lower.includes('bold') || lower.includes('bolder')) { + result.bold = true; + } else { + const num = parseInt(fw, 10); + if (!Number.isNaN(num) && num >= 700) result.bold = true; + } + } + + const fs = findCssValue(style, 'font-style'); + if (fs) { + const lower = fs.toLowerCase(); + if (lower.includes('italic') || lower.includes('oblique')) + result.italic = true; + } + + // text-decoration / text-decoration-line: scan ALL declarations + const decorations = [ + ...findAllCssValues(style, 'text-decoration-line'), + ...findAllCssValues(style, 'text-decoration'), + ]; + for (const v of decorations) { + const lower = v.toLowerCase(); + if (lower.includes('underline')) result.underline = true; + if (lower.includes('line-through')) result.strikethrough = true; + } + + return result; +} + +function extraStyles(s: CssStyles, tag: string): CssStyles { + return { + bold: s.bold && tag !== 'b', + italic: s.italic && tag !== 'i', + underline: s.underline && tag !== 'u', + strikethrough: s.strikethrough && tag !== 's', + }; +} + +function emitStylesOpen(s: CssStyles): string { + let out = ''; + if (s.bold) out += ''; + if (s.italic) out += ''; + if (s.underline) out += ''; + if (s.strikethrough) out += ''; + return out; +} + +function emitStylesClose(s: CssStyles): string { + let out = ''; + if (s.strikethrough) out += ''; + if (s.underline) out += ''; + if (s.italic) out += ''; + if (s.bold) out += ''; + return out; +} + +function emitOneAttr(el: Element, attr: string): string { + const val = el.getAttribute(attr); + if (val == null || val === '') return ''; + const escaped = escapeText(val); + return ` ${attr}="${escaped}"`; +} + +function emitAttributes(el: Element, name: string): string { + switch (name) { + case 'a': + return emitOneAttr(el, 'href'); + case 'img': + return ( + emitOneAttr(el, 'src') + + emitOneAttr(el, 'alt') + + emitOneAttr(el, 'width') + + emitOneAttr(el, 'height') + ); + case 'ul': { + const val = el.getAttribute('data-type'); + return val === 'checkbox' ? ' data-type="checkbox"' : ''; + } + case 'li': + return el.hasAttribute('checked') ? ' checked' : ''; + case 'mention': + return ( + emitOneAttr(el, 'id') + + emitOneAttr(el, 'text') + + emitOneAttr(el, 'indicator') + ); + default: + return ''; + } +} + +function isGoogleDocsWrapper(el: Element, tag: string): boolean { + if (tag !== 'b') return false; + const id = el.getAttribute('id'); + return !!id && id.startsWith('docs-internal-guid-') && id.length > 20; +} + +function escapeText(s: string): string { + return s.replace(/[&<>"']/g, (match) => { + switch (match) { + case '&': + return '&'; + case '<': + return '<'; + case '>': + return '>'; + case '"': + return '"'; + case "'": + return '''; + default: + return match; + } + }); +} + +// --- Blockquote content flattening --- + +function flushInlineP(ib: { buf: string }, out: { buf: string }): void { + if (ib.buf.length > 0) { + out.buf += `

    ${ib.buf}

    `; + ib.buf = ''; + } +} + +function flattenBqChildren( + node: Element, + ib: { buf: string }, + out: { buf: string } +): void { + for (const child of Array.from(node.childNodes)) { + flattenBqNode(child, ib, out); + } +} + +function flattenBqNode( + node: Node, + ib: { buf: string }, + out: { buf: string } +): void { + if (isText(node)) { + const t = node.textContent ?? ''; + ib.buf += escapeText(t); + return; + } + if (!isElement(node)) return; + if (isBrNode(node)) { + flushInlineP(ib, out); + return; + } + if (isBlockProducing(node) || isBlockquoteNode(node)) { + flushInlineP(ib, out); + flattenBqChildren(node, ib, out); + flushInlineP(ib, out); + return; + } + walkNode(node, ib); +} + +// --- List item content flattening --- + +type LiCtx = { + el: Element; + styles: CssStyles; + nestedLists: Element[]; +}; + +function flushLiBuffer( + ib: { buf: string }, + out: { buf: string }, + ctx: LiCtx +): void { + if (ib.buf.length === 0) return; + out.buf += ``; + out.buf += emitStylesOpen(ctx.styles); + out.buf += ib.buf; + out.buf += emitStylesClose(ctx.styles); + out.buf += '

    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 z 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 literal 0 HcmV?d00001 diff --git a/apps/example/ios/Podfile.lock b/apps/example/ios/Podfile.lock index 0a2926cc5..b18f8dd6e 100644 --- a/apps/example/ios/Podfile.lock +++ b/apps/example/ios/Podfile.lock @@ -2026,7 +2026,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: FBLazyVector: c00c20551d40126351a6783c47ce75f5b374851b - hermes-engine: 484c595d9e6a0b7b7607e8ead508ba5c472493c7 + hermes-engine: b4dad6ba67535bb03c8ff1006b337cba14db16cb RCTDeprecation: 3bb167081b134461cfeb875ff7ae1945f8635257 RCTRequired: 74839f55d5058a133a0bc4569b0afec750957f64 RCTSwiftUI: 87a316382f3eab4dd13d2a0d0fd2adcce917361a @@ -2035,7 +2035,7 @@ SPEC CHECKSUMS: React: 1b1536b9099195944034e65b1830f463caaa8390 React-callinvoker: 6dff6d17d1d6cc8fdf85468a649bafed473c65f5 React-Core: 39ee05b5798296f433dd3c3624c57a187c1510e3 - React-Core-prebuilt: 3ca7a49d919f940e7de8fb0c2a3f5cfcb665f09b + React-Core-prebuilt: 69556f895326f23c007f3a6869340045d7dca106 React-CoreModules: e78bfd2617075bc0e50c689df4a29232bd72ad82 React-cxxreact: 3fe21801d46097cf74c3dff6953677bebc4a3c2a React-debug: e1f00fcd2cef58a2897471a6d76a4ef5f5f90c74 @@ -2097,8 +2097,8 @@ SPEC CHECKSUMS: ReactAppDependencyProvider: 706b65371b90b5cc797b6639e8979f2e5cecd6da ReactCodegen: ab01ebfffac5cda9140204eb872ed97c15df225f ReactCommon: 47ef95b0920948a0b54d7439f7452501eeeac071 - ReactNativeDependencies: 652705a9bc92800d0b1e15177a61ba70d89d24dd - ReactNativeEnrichedHtml: 93722241410f2daaa8c20ce6bcfcf4666bfd9166 + ReactNativeDependencies: 8a208df374583424130645685d86306befc275cf + ReactNativeEnrichedHtml: 7d90df4aced7f533c7bd15ac296879b214413361 Yoga: e83c3121d079541e69f3c5c623faaaf933fb5812 PODFILE CHECKSUM: 88c10840d02e9884b2dc3f457d5120f83ac3803b diff --git a/apps/example/src/components/ColorPickerRow.tsx b/apps/example/src/components/ColorPickerRow.tsx index ae0e79649..ad428cb11 100644 --- a/apps/example/src/components/ColorPickerRow.tsx +++ b/apps/example/src/components/ColorPickerRow.tsx @@ -21,14 +21,20 @@ export const ColorPickerRow: FC = ({ style={styles.container} contentContainerStyle={styles.content} > - + {colors.map((color) => { const isActive = color.toLowerCase() === activeColor?.toLowerCase(); + const swatchId = `color-swatch-${color.replace('#', '').toUpperCase()}`; return ( onSelectColor(color)} style={[ styles.swatch, From 9c0c0957510a6aeee7b96529f55434a1c648012c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20=C5=BB=C3=B3=C5=82kiewski?= Date: Wed, 17 Jun 2026 13:35:42 +0200 Subject: [PATCH 08/11] fix: handling rgb colors --- ios/extensions/ColorExtension.mm | 30 ++++++++++++++++++++++++------ ios/htmlParser/HtmlParser.mm | 3 +-- src/native/EnrichedTextInput.tsx | 23 +++++++++++++++-------- 3 files changed, 40 insertions(+), 16 deletions(-) diff --git a/ios/extensions/ColorExtension.mm b/ios/extensions/ColorExtension.mm index 476ed6788..8c1f7ff9a 100644 --- a/ios/extensions/ColorExtension.mm +++ b/ios/extensions/ColorExtension.mm @@ -275,22 +275,40 @@ + (UIColor *_Nullable)colorFromCSSString:(NSString *_Nullable)cssString { if ([str hasPrefix:@"rgb"]) { NSScanner *scanner = [NSScanner scannerWithString:str]; + // Scan up to and including the opening parenthesis [scanner scanUpToString:@"(" intoString:NULL]; if (![scanner scanString:@"(" intoString:NULL]) return nil; float r = 0, g = 0, b = 0, a = 1.0; - [scanner scanFloat:&r]; - [scanner scanString:@"," intoString:NULL]; - [scanner scanFloat:&g]; - [scanner scanString:@"," intoString:NULL]; - [scanner scanFloat:&b]; + // Scan Red, then require a comma + if (![scanner scanFloat:&r]) + return nil; + if (![scanner scanString:@"," intoString:NULL]) + return nil; + + // Scan Green, then require a comma + if (![scanner scanFloat:&g]) + return nil; + if (![scanner scanString:@"," intoString:NULL]) + return nil; + // Scan Blue (comma not required yet, might be alpha or closing parenthesis) + if (![scanner scanFloat:&b]) + return nil; + + // Check if there is a 4th parameter (Alpha) if ([scanner scanString:@"," intoString:NULL]) { - [scanner scanFloat:&a]; + if (![scanner scanFloat:&a]) + return nil; } + // Require the closing parenthesis to guarantee the string wasn't malformed + // or cut off + if (![scanner scanString:@")" intoString:NULL]) + return nil; + return [UIColor colorWithRed:r / 255.0 green:g / 255.0 blue:b / 255.0 diff --git a/ios/htmlParser/HtmlParser.mm b/ios/htmlParser/HtmlParser.mm index b57608be2..b7e06caa2 100644 --- a/ios/htmlParser/HtmlParser.mm +++ b/ios/htmlParser/HtmlParser.mm @@ -827,8 +827,7 @@ + (NSArray *_Nonnull)getTextAndStylesFromHtml:(NSString *_Nonnull)fixedHtml { [styleArr addObject:@([CustomStyle getType])]; stylePair.styleValue = data; } else { - // some other external tags like span just don't get put into the - // processed styles + // some other external tags don't get put into the processed styles continue; } diff --git a/src/native/EnrichedTextInput.tsx b/src/native/EnrichedTextInput.tsx index 0b8095f4a..077332ade 100644 --- a/src/native/EnrichedTextInput.tsx +++ b/src/native/EnrichedTextInput.tsx @@ -41,6 +41,19 @@ const warnMentionIndicators = (indicator: string) => { ); }; +const getSafeColorInt = ( + color: ColorValue | null | undefined +): number | null => { + if (color == null) return null; + + const processed = processColor(color); + if (typeof processed === 'number') { + return processed; + } + + return null; +}; + type ComponentType = (Component & NativeMethods) | null; type HtmlRequest = { @@ -288,16 +301,10 @@ export const EnrichedTextInput = ({ backgroundColor?: number | null; } = {}; if (customStyle.foregroundColor !== undefined) { - payload.foregroundColor = - customStyle.foregroundColor != null - ? (processColor(customStyle.foregroundColor) as number) - : null; + payload.foregroundColor = getSafeColorInt(customStyle.foregroundColor); } if (customStyle.backgroundColor !== undefined) { - payload.backgroundColor = - customStyle.backgroundColor != null - ? (processColor(customStyle.backgroundColor) as number) - : null; + payload.backgroundColor = getSafeColorInt(customStyle.backgroundColor); } Commands.setStyle(nullthrows(nativeRef.current), JSON.stringify(payload)); }, From 4953d405edbd301a1c2242d70b5f766ec073c1c0 Mon Sep 17 00:00:00 2001 From: Krystian Sienkiewicz <146986839+hejsztynx@users.noreply.github.com> Date: Thu, 18 Jun 2026 13:50:23 +0200 Subject: [PATCH 09/11] fix: disable autocorrection in mobile e2e tests (#644) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Summary Disabled the autocorrection in e2e maestro testing devices to prevent unexpected behavior. ## Compatibility | OS | Implemented | | ------- | :---------: | | iOS | ✅ | | Android | ✅ | ## Checklist - [x] E2E tests are passing - [ ] Required E2E tests have been added (if applicable) --- .../android/codeblock_no_link_detection.png | Bin 6515 -> 6450 bytes .../android/codeblock_style_blocking.png | Bin 7653 -> 7588 bytes .../android/paragraph_styles_blocks.png | Bin 20772 -> 20701 bytes .maestro/scripts/setup-android-emulator.sh | 1 + .maestro/scripts/setup-ios-simulator.sh | 15 +++++++++++++++ 5 files changed, 16 insertions(+) diff --git a/.maestro/enrichedInput/screenshots/android/codeblock_no_link_detection.png b/.maestro/enrichedInput/screenshots/android/codeblock_no_link_detection.png index 22acdaf6052091c05d6ccb51ffd07d5f1a0e9d2b..78e27577dc789760551845909f76e2dc5c0a44a2 100644 GIT binary patch literal 6450 zcmd5>c_5T)`&OM273Gj3R7fRMDv_mA*0QCLFtRJg*eBbJ7RpkV2{D#bMz$FlW^6-} zJ%r2{V+P4G24fq`Ff+eb=bX;@zJGuJe1APW-uHR#_kOPXy07OFZEk9?M^HkLkB@JU zk)f^yAK$hgz|Rpow{Fgqo=fH9JFsS?`^T*Zpc(3#zi1%0c|OAL*$O76H$KTM zhLgUKXi%CKJ?pKGmdW_qd*xjdi?dxz^Ykf_aY$7=dUJEbcW2i(#|W3hMcaT;K<_p` zFo?vI2mynfsG}4x7#{zm0SpP|PXdAAaL!H!FkHT}kGKVR@yXHu$rl9#8L27`+_j19 z(9I~1CSE+5^KJawccW{!d1I*#)2Pj_j``zFzDc2;#YPEitSvbk#%}dK0!+VtAou#3 zae3?cAUz>Qsq@N4uH}b|p1|2z{B6kJlZa(k%P}c_OEfssI$iG@2 zc*E|kT-!hZ<93lF=MG%Zg4$hG3TfMlg2ab=PP9IH>%ArK0G=NkqW`s?mLss%__YVj zzMyN8WOrNbQ10&r)UBpxytnXe1AVDG^Za~KsA7KCjnAcHsqQno^(kN zO|x(5rCO?2E^7IhZMQWepBP`qdvK%o%{>OH^;oh^RElsi+_)I2QRmC$FP1rUG5lla z=qO>-UGR5PF05q~br;~T5gR~ik!jf*ZCeK~NVG{-dSQvR-U zm_^s>lwg&hfWR>F@>oWPzc+?puW`^u-GXiNwc;+TzR$g?>wV?ozL5&0FuBi|Vk~%! zn;eT2A1a?kBT0j&kSkSvq}Ki1nSmkg3VOZ#rIfJnVMc?y5QWUPUae;YL~Oa-T6&O_ z`|uEvBxA4QeqJjUp5)($mI)%O`{#rzp3d}0450M&ves(5#-?W-d?D$#RE>HfGO>nl z23$q-S*K)hFb~BrtMcL=tf3%YtnR)Ji3?+_BRmz>Q#*_w*B)H@oTBg|L*1#`yvE7s z!9rarMxI7D-GPy|^M7~DeuGTw+7LdHUgCBIo-J4-vGzF$mb+j+Hd)Y(;)hg$XTZV2 zxN}t7BldddMVzplAIl1OP5RI!uF?G(>b}DTk4cI_XFl@1b|J?xdOq%EXc4q9vtfN1 zer#k^vT!@>;6{LTWSy_%nxT)${s(&PG29@N zCVF`$h74lI%nh^J>~^2#J8lCM={dfJs*FqG7nHvx`|YU0&UWgV#HP&NX7?7K7QH*`l9dyPAt83rDMS_v8A%# zwRAg*Xu^tt*oS%=SU**|mUt~#1>H>vYXI=`_|2@7amawhPH&+#}&?BQO>8xf3OC92|_BB&yx=hdJ zD>KW&AGg=N=gjC^*j6 zK(=0rC!;7`0QZW=y!}KdbULLb zSvf(Mg~7qef_i7$2$HibjhBn>i(4Ln-f8w2!3F6-oNiXg@^D?H3i-q5!b3G7bIS+{ zIMUe^nzn>ei+m@4b8hG{uWx(yEa#i3OW8<&R4+AerZC*-W6#j->szs6C)m_n5R@VO z$c#eJOFbebs4JkOg zEHA(C*m`CE8sD~vBv)1R^+sVvu-W}sPS^MGMo@h=o=3j0UY2#gFV9weeIVv^NTQ#K z!WKdLa=nRg$RZ6OqhX(~)Ltzx^&AEU8x6A^dBw0_HeOT(%_l4zVJr5BXRZfi!Tj10 z_iJ}fdSdd3CgmL`Ko5--z&;=-kRZ09ws7(CiQ*}rWD*acEu4CzVgmiSJ!BGfHBXpM6vq)>+cv_5 zxXqq~{HbHT0~GusK>k-6pbgPGw64QbXMg*OojB+1d_bW4KEtb5Ml6fM&M0oljD2hk zJ@6~#+fEiSJf&wTKH1>eW@I&OX*}fa=r){s9Ozv}k5NfYH79_XM~jnHG>96<(r$T~ zIDAOY60AIEi2hD?1===S9Abe^qe)sv}TKt0&5DmZmLr<+PoR4CFKvrQ+&bkn2bCjziE!=Mi512MC7hxrn>L z&`Wi`wQ8eRS;FLT*rZNv9<%vSe1A~dTK(hN?^jS=E8*Vm&#v4IMZm-<8E zCruXF^8Dv?s>;<^s>ES#AR{J445yB$L{5ZYHA~m_Q4ZAOhUih8*EY+Ihq48SOj_yPF+P= z2tmW{B^9#isr_F*7@jX zgl@_pGc%g}v^sr?ey0*X7A`s4YpwLu6qhP$Zw@U}ejNyK2vSu)XuDxkoLW1e zb^rvynlqZf8L~%2)G(Fg(4qIrsZig!shQvZjXk77{C@^9B%$6exo>%yaQi}M>`iBy zE5*!PZSg{uN12`@4hJC+mV4xsLm)x~8G$_uxkQcVzaoSy(%K@qC4UpcnCM($44z=Y_Y*0GsuwVNuY^fI~*U0U%b3|3Ly{ILqj~! zKvC6)%Kax8SYoXz5IFflb&hx|9M^73qfQMGl&BxRU0AuQBiPN8JP&oExEAN#VJ*ft z-FiOH)v9q?oqa(KiZWhOb!sa;P!(vv2q1trfhi?5D3P8UP{>2U4iZX7#ULrIPC>p;xZbY?m( zdiI{83<`66(g6M9g;kxOE(JOf>#;OIl#~zH<|RewH)@Vd15L)}3M6F+vPu9i3rKw+ zI9}4I*u*9gki6t^V?Yh}BO5j6&PvwrI&?TIac_%hp6zGwICHAu0zAUJeFNc!I)hQ2Qv7xVMjgR&b`h4&}mnz)Cp&J#Arj`78ZuQeaK6qL5|3U^Z|Q0TDjG01)aGsd{Gmtg6=%bGg=Q-9oLESFm=r zg{~F}Xj`EDFt}j7809(UB#?kpxwft&+mtb8lz=cqmMgV85O>sgZY%#YM>*Z=+SBx8 z`%>rVKU;~)JNSP^6#^M*7IcFv`5jq0y4DpGtoV)+`Fs~4m|0YfEzE1TI!@K1vi(K? zQXLR%&X_%Gbc>x80Z6?sqCb{qoeP^o8sSuqHDxe$P0pOy^{nq7NEtKnwGc&i59wnZ zMq0(W!RmHWhGorK9fbt#(x_uU3{8}JC4|vCwwl$qXgsjhe{I9D;0mGD%p}a z)U{owhBG#!!dMvy3&R%se4#3Y?zpq3u>Kf2<5U4BwsAf0S)Q zEmJegJn6V(<5R@I&7{iA)`G=X;CG|CHzFMR0ty9xcApVc!o$=s!zpo z99}U!y8^NkO$t=M19Wu-lh$fjmkNJ)Rb4^=2F27-ywXC6L(Z|KbK__=6CHoGuHs8~ zCU`AS-1;;iGj*uz`J%t+fKLuQwkG}8H=Ztv1u}Hq8l@0@0?280rSCYiSAcTOyz<2_-Yz zqdz5WXQ_y`TUZCUtnKmrF^VJA8@3qYB6vz(H}<hYtNaopmc28KeTUL@*+Qy`(rduXLTMs4) zFAYs%Gk)w=GJ0R<87UQrBvN~4;CJ2O2)PS9=)*IyKB+VG{Behhj^IOk91%r)9t3{+U71hFeyCmUoGzST#R z*jU>c?)Rzvn8j`-nW_?BNA?TYL5|NXKhKVA27#~OX3jL;z~5QRS+FJ1rm{)MV?}8t z1z<~$l;;L=)y9uvVRxSw4M$@Yyb|N5V*#C4Q@K?c1i9FK`h(jWc;eEfkRf9@a4s}P zc=Z}QL?38@j?*E_yESDo2N)AgI13i*})bh)nsaSRKvw*VeTrI4QOCLZ6XzZ?dz_IJ9#0Z74exxYK4-J|7KCWp8^dHdT zKD_pQ7-vE7>d7PJObx@AgjjjGe=;^cqdy7ufn&Emv96tMuVedYxMaZ*(Cuwe$u)}I z=EOn>e_Erecjzu@fh`+4r<1>V#`=+nYQ}`YUqBQ}{1v&*g#P zNqy=M*sn8-;gK>mAyS;bRph;-%bvh}?L!nRR$OJEZ~JhZ=^X$~B^~lbfT4#VWE)P( z03g$L9fLCT`7ZzS`1Mp}GVr+Z+zJY?hJ{5uz)gVwzG0S}CeaAbO~d$&*dpdSrb&jV zVpnn3S|UR7=cN1%BFeV;JK(RXIN%ibeB85XF?S~&_v|fp_I=7KoXR@Qm$7Lr@6CC| z+=j4vQqq~qxU9KpJ?kFlV)`)q<%?n+U~7$Nt%&!VPV}>w&(=r}=NlO%$}l!vp?VLW z?xu--Sx&BE;FM(Qn@EkqY@NfyqOT^ z6vZmcujyQ2M9BV}JaVSI!iaOfyMf*Jjdj|t4sgaJbQ2@)d@gj*1TMJu24!{t7OcRH zqR_fkdHEs-6DmA&LLRVbUq6$3v40FU#W6C!ZH6fJANjOh*#)rIFMG*_UHi@Cma*A& zyW1;vRRQ$xjj;E>K`8UBmCWPmH6}MJ~p26?^zv_x$wRqhpsV@4;-ADfiUMJJg literal 6515 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 #1qi9XbMkz1V7nSQqU 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 literal 7653 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 diff --git a/.maestro/scripts/setup-android-emulator.sh b/.maestro/scripts/setup-android-emulator.sh index 3c74de08d..605c8f6bd 100755 --- a/.maestro/scripts/setup-android-emulator.sh +++ b/.maestro/scripts/setup-android-emulator.sh @@ -77,6 +77,7 @@ until adb -s "$SERIAL" shell getprop sys.boot_completed 2>/dev/null | grep -q "^ done adb -s "$SERIAL" shell pm disable-user --user 0 com.google.android.inputmethod.latin +adb -s "$SERIAL" shell settings put secure spell_checker_enabled 0 echo "Emulator ready: $AVD_NAME ($SERIAL)" echo "DEVICE_ID=$SERIAL" diff --git a/.maestro/scripts/setup-ios-simulator.sh b/.maestro/scripts/setup-ios-simulator.sh index 66f4cb743..5f9d8ea23 100755 --- a/.maestro/scripts/setup-ios-simulator.sh +++ b/.maestro/scripts/setup-ios-simulator.sh @@ -25,6 +25,21 @@ if [ -z "$UDID" ]; then exit 1 fi +# disable automatic text manipulation: auto-correction, spelling-check and auto-capitalization +SIM_PREFS_DIR="$HOME/Library/Developer/CoreSimulator/Devices/$UDID/data/Library/Preferences" +mkdir -p "$SIM_PREFS_DIR" + +PLIST="$SIM_PREFS_DIR/com.apple.keyboard.preferences.plist" + +/usr/libexec/PlistBuddy -c "Add :KeyboardAutocorrection bool false" "$PLIST" 2>/dev/null || \ +/usr/libexec/PlistBuddy -c "Set :KeyboardAutocorrection bool false" "$PLIST" + +/usr/libexec/PlistBuddy -c "Add :KeyboardCheckSpelling bool false" "$PLIST" 2>/dev/null || \ +/usr/libexec/PlistBuddy -c "Set :KeyboardCheckSpelling bool false" "$PLIST" + +/usr/libexec/PlistBuddy -c "Add :KeyboardAutocapitalization bool false" "$PLIST" 2>/dev/null || \ +/usr/libexec/PlistBuddy -c "Set :KeyboardAutocapitalization bool false" "$PLIST" + STATE=$(xcrun simctl list devices | grep "$UDID" | grep -oE '\(Booted\)|\(Shutdown\)' || true) if [ "$STATE" != "(Booted)" ]; then echo "Booting '$DEVICE_NAME' ($UDID)..." From 6723ce9aed2096a48a4b791a6452bbea9b4c478f Mon Sep 17 00:00:00 2001 From: Krystian Sienkiewicz <146986839+hejsztynx@users.noreply.github.com> Date: Thu, 18 Jun 2026 20:45:55 +0200 Subject: [PATCH 10/11] feat(web): HTML normalization (#630) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Summary Web implementation was missing the HTML normalizer - implemented the normalizer, mirroring the one in the native code - tests also mirror the native ones ## Test Plan Try pasting styled text from `Google Docs`, `MS Word` or any other platform ## Compatibility | OS | Implemented | | ------- | :---------: | | iOS | ❌ | | Android | ❌ | | Web | ✅ | ## Checklist - [x] E2E tests are passing - [ ] Required E2E tests have been added (if applicable) --- apps/example-web/src/App.tsx | 1 + docs/INPUT_API_REFERENCE.md | 6 +- docs/WEB.md | 2 +- package.json | 39 +- src/web/EnrichedTextInput.tsx | 19 +- src/web/__tests__/htmlNormalizer.test.ts | 450 +++++++++++++ .../checkboxHtmlNormalizer.ts | 0 src/web/normalization/htmlNormalizer.ts | 609 ++++++++++++++++++ .../tiptapHtmlNormalizer.ts | 9 +- src/web/useOnChangeHtml.ts | 2 +- yarn.lock | 432 ++++++++++++- 11 files changed, 1547 insertions(+), 22 deletions(-) create mode 100644 src/web/__tests__/htmlNormalizer.test.ts rename src/web/{ => normalization}/checkboxHtmlNormalizer.ts (100%) create mode 100644 src/web/normalization/htmlNormalizer.ts rename src/web/{ => normalization}/tiptapHtmlNormalizer.ts (82%) diff --git a/apps/example-web/src/App.tsx b/apps/example-web/src/App.tsx index c5809f625..8ad87c255 100644 --- a/apps/example-web/src/App.tsx +++ b/apps/example-web/src/App.tsx @@ -261,6 +261,7 @@ function App() { onMentionDetected={handleOnMentionDetected} mentionIndicators={['@', '#']} htmlStyle={WEB_DEFAULT_HTML_STYLE} + useHtmlNormalizer /> /apps/example/node_modules", - "/lib/" - ], - "testPathIgnorePatterns": [ - "/node_modules/", - "/.playwright/", - "/apps/example-web/" + "projects": [ + { + "displayName": "native", + "preset": "react-native", + "modulePathIgnorePatterns": [ + "/apps/example/node_modules", + "/lib/" + ], + "testPathIgnorePatterns": [ + "/node_modules/", + "/.playwright/", + "/apps/example-web/", + "/src/web/" + ] + }, + { + "displayName": "web", + "testEnvironment": "jsdom", + "testMatch": [ + "/src/web/**/__tests__/**/*.test.{ts,tsx}" + ], + "modulePathIgnorePatterns": [ + "/apps/example/node_modules", + "/lib/" + ], + "transform": { + "^.+\\.(js|ts|tsx)$": "babel-jest" + } + } ] }, "commitlint": { diff --git a/src/web/EnrichedTextInput.tsx b/src/web/EnrichedTextInput.tsx index 4ea1742c9..06a0c264a 100644 --- a/src/web/EnrichedTextInput.tsx +++ b/src/web/EnrichedTextInput.tsx @@ -35,7 +35,7 @@ import { useOnLinkDetected } from './useOnLinkDetected'; import { prepareHtmlForTiptap, normalizeHtmlFromTiptap, -} from './tiptapHtmlNormalizer'; +} from './normalization/tiptapHtmlNormalizer'; import { ENRICHED_TEXT_INPUT_DEFAULT_PROPS } from '../utils/EnrichedTextInputDefaultProps'; import { enrichedInputStyleToCSSProperties } from './styleConversion/enrichedInputStyleToCSSProperties'; import { enrichedInputThemingToCSSProperties } from './styleConversion/enrichedInputThemingToCSSProperties'; @@ -111,9 +111,12 @@ export const EnrichedTextInput = ({ onChangeMention, onEndMention, htmlStyle, + useHtmlNormalizer, }: EnrichedTextInputProps) => { const tiptapContent = - defaultValue != null ? prepareHtmlForTiptap(defaultValue) : defaultValue; + defaultValue != null + ? prepareHtmlForTiptap(defaultValue, useHtmlNormalizer) + : defaultValue; const resolvedHtmlStyle = useMemo( () => mergeWithDefaultHtmlStyle(htmlStyle), @@ -165,6 +168,11 @@ export const EnrichedTextInput = ({ onKeyPressRef.current = onKeyPress; }, [onKeyPress]); + const useHtmlNormalizerRef = useRef(useHtmlNormalizer); + useEffect(() => { + useHtmlNormalizerRef.current = useHtmlNormalizer; + }, [useHtmlNormalizer]); + const handleKeyDown = (doc: Node, event: KeyboardEvent): boolean => { onKeyPressRef.current?.(adaptWebToNativeEvent(event, { key: event.key })); if (event.key !== 'Enter') { @@ -264,6 +272,9 @@ export const EnrichedTextInput = ({ autoCapitalize, enterkeyhint: returnKeyTypeToEnterKeyHint(returnKeyType), }, + transformPastedHTML: (html) => { + return prepareHtmlForTiptap(html, useHtmlNormalizerRef.current); + }, }, }, [tiptapContent, extensions] @@ -307,7 +318,9 @@ export const EnrichedTextInput = ({ focus: () => editor.commands.focus(), blur: () => editor.commands.blur(), setValue: (value: string) => - editor.commands.setContent(prepareHtmlForTiptap(value)), + editor.commands.setContent( + prepareHtmlForTiptap(value, useHtmlNormalizerRef.current) + ), setSelection: (start, end) => { const doc = editor.state.doc; runFocused(editor, (c) => diff --git a/src/web/__tests__/htmlNormalizer.test.ts b/src/web/__tests__/htmlNormalizer.test.ts new file mode 100644 index 000000000..3802e54f6 --- /dev/null +++ b/src/web/__tests__/htmlNormalizer.test.ts @@ -0,0 +1,450 @@ +/* + * Port of cpp/tests/GumboParserTest.cpp. Each describe() mirrors a TEST() group + * from the C++ suite. The expected outputs are the same as the native + * normalizer's outputs + */ +import { normalizeHtml } from '../normalization/htmlNormalizer'; + +describe('htmlNormalizer', () => { + describe('TagRemappings', () => { + test.each([ + ['x', 'x'], + ['x', 'x'], + ['x', 'x'], + ['x', 'x'], + ['x', 'x'], + ['

  • '; + ib.buf = ''; +} + +function flattenLiChildren( + node: Element, + ib: { buf: string }, + out: { buf: string }, + ctx: LiCtx +): void { + for (const child of Array.from(node.childNodes)) { + flattenLiNode(child, ib, out, ctx); + } +} + +function flattenLiNode( + node: Node, + ib: { buf: string }, + out: { buf: string }, + ctx: LiCtx +): void { + if (isText(node)) { + ib.buf += escapeText(node.textContent ?? ''); + return; + } + if (!isElement(node)) return; + if (isListNode(node)) { + ctx.nestedLists.push(node); + return; + } + if (isBrNode(node)) { + flushLiBuffer(ib, out, ctx); + return; + } + if (isBlockProducing(node) || isBlockquoteNode(node)) { + flushLiBuffer(ib, out, ctx); + flattenLiChildren(node, ib, out, ctx); + flushLiBuffer(ib, out, ctx); + return; + } + walkNode(node, ib); +} + +// --- Main walker --- + +function walkChildren(node: Element, out: { buf: string }): void { + const children = Array.from(node.childNodes); + const parentIsList = isListNode(node); + + let hasBlock = false; + for (const c of children) { + if (isBlockProducing(c)) { + hasBlock = true; + break; + } + } + + let i = 0; + while (i < children.length) { + const child = children[i]!; + + // Flatten list-inside-list + if (parentIsList && isElement(child) && isListNode(child)) { + walkChildren(child, out); + i++; + continue; + } + + // Merge consecutive blockquotes + if (isElement(child) && isBlockquoteNode(child)) { + out.buf += '
    '; + const bqIb = { buf: '' }; + while (i < children.length) { + const cur = children[i]; + if (!cur || !isElement(cur) || !isBlockquoteNode(cur)) break; + flattenBqChildren(cur, bqIb, out); + i++; + } + flushInlineP(bqIb, out); + out.buf += '
    '; + continue; + } + + // Auto-paragraph: group inline runs into

    when mixed with blocks + if ( + hasBlock && + !parentIsList && + !isBlockProducing(child) && + !(isElement(child) && isBlockquoteNode(child)) + ) { + const ib = { buf: '' }; + while (i < children.length) { + const cur = children[i]!; + if ( + isBlockProducing(cur) || + (isElement(cur) && isBlockquoteNode(cur)) + ) { + break; + } + if (isElement(cur) && isBrNode(cur)) { + if (ib.buf.length > 0) { + flushInlineP(ib, out); + } else { + out.buf += '
    '; + } + i++; + continue; + } + // Transparent inline wrapper for block/bq children + if (isElement(cur) && hasBlockOrBqChild(cur)) { + flushInlineP(ib, out); + walkChildren(cur, out); + i++; + continue; + } + walkNode(cur, ib); + i++; + } + flushInlineP(ib, out); + continue; + } + + walkNode(child, out); + i++; + } +} + +function walkNode(node: Node, out: { buf: string }): void { + if (isText(node)) { + out.buf += escapeText(node.textContent ?? ''); + return; + } + if (!isElement(node)) return; + + const name = node.tagName.toLowerCase(); + + if (STRIPPED_TAGS.has(name)) return; + + if (isGoogleDocsWrapper(node, name)) { + walkChildren(node, out); + return; + } + + const outName = canonicalName(name); + const cls = classifyTag(name); + + // : CSS style → inline tags + if (name === 'span') { + const s = parseCssStyle(node.getAttribute('style')); + out.buf += emitStylesOpen(s); + walkChildren(node, out); + out.buf += emitStylesClose(s); + return; + } + + //

    : becomes

    or passes through + if (name === 'div') { + const s = parseCssStyle(node.getAttribute('style')); + if (isPurelyInline(node)) { + const pb = { buf: '' }; + for (const dc of Array.from(node.childNodes)) { + if (isElement(dc) && isBrNode(dc)) { + if (pb.buf.length > 0) { + out.buf += `

    ${emitStylesOpen(s)}${pb.buf}${emitStylesClose(s)}

    `; + } else { + out.buf += '
    '; + } + pb.buf = ''; + continue; + } + walkNode(dc, pb); + } + if (pb.buf.length > 0) { + out.buf += `

    ${emitStylesOpen(s)}${pb.buf}${emitStylesClose(s)}

    `; + } + } else { + out.buf += emitStylesOpen(s); + walkChildren(node, out); + out.buf += emitStylesClose(s); + } + return; + } + + // Table elements + if (TABLE_TAGS.has(name)) { + if (name === 'td' || name === 'th') { + walkChildren(node, out); + // Append space if there is a next element sibling + let sib: Node | null = node.nextSibling; + while (sib && !isElement(sib)) sib = sib.nextSibling; + if (sib) out.buf += ' '; + } else if (name === 'tr') { + const row = { buf: '' }; + walkChildren(node, row); + if (row.buf.length > 0) { + out.buf += `

    ${row.buf}

    `; + } + } else { + walkChildren(node, out); + } + return; + } + + if (cls === 'pass' || cls === 'skip') { + walkChildren(node, out); + return; + } + + if (cls === 'self-closing') { + out.buf += `<${outName}${emitAttributes(node, outName)}`; + out.buf += outName === 'img' ? ' />' : '>'; + return; + } + + // inline or block + const es = extraStyles(parseCssStyle(node.getAttribute('style')), outName); + + //
  • : flatten + if (outName === 'li') { + const nestedLists: Element[] = []; + const liIb = { buf: '' }; + const ctx: LiCtx = { el: node, styles: es, nestedLists }; + flattenLiChildren(node, liIb, out, ctx); + flushLiBuffer(liIb, out, ctx); + for (const nl of nestedLists) walkChildren(nl, out); + return; + } + + // : wrap inline content in

    + if (outName === 'codeblock') { + const wrap = isPurelyInline(node); + out.buf += ''; + if (wrap) out.buf += '

    '; + walkChildren(node, out); + if (wrap) out.buf += '

    '; + out.buf += '
    '; + return; + } + + // Generic block/inline + out.buf += `<${outName}${emitAttributes(node, outName)}>`; + out.buf += emitStylesOpen(es); + walkChildren(node, out); + out.buf += emitStylesClose(es); + out.buf += ``; +} + +export function normalizeHtml(html: string): string { + const parser = new DOMParser(); + const doc = parser.parseFromString(`${html}`, 'text/html'); + const body = doc.body; + const out = { buf: '' }; + walkChildren(body, out); + return out.buf; +} diff --git a/src/web/tiptapHtmlNormalizer.ts b/src/web/normalization/tiptapHtmlNormalizer.ts similarity index 82% rename from src/web/tiptapHtmlNormalizer.ts rename to src/web/normalization/tiptapHtmlNormalizer.ts index 5f9e0a3c5..9ebd2c653 100644 --- a/src/web/tiptapHtmlNormalizer.ts +++ b/src/web/normalization/tiptapHtmlNormalizer.ts @@ -2,8 +2,15 @@ import { checkboxHtmlForTiptap, checkboxHtmlFromTiptap, } from './checkboxHtmlNormalizer'; +import { normalizeHtml } from './htmlNormalizer'; -export function prepareHtmlForTiptap(html: string): string { +export function prepareHtmlForTiptap( + html: string, + useHtmlNormalizer: boolean | undefined +): string { + if (useHtmlNormalizer) { + html = normalizeHtml(html); + } html = checkboxHtmlForTiptap(html); html = html.replace(//gi, '

    '); return html; diff --git a/src/web/useOnChangeHtml.ts b/src/web/useOnChangeHtml.ts index 8953cec95..90d159d97 100644 --- a/src/web/useOnChangeHtml.ts +++ b/src/web/useOnChangeHtml.ts @@ -2,7 +2,7 @@ import { type Editor } from '@tiptap/react'; import type { OnChangeHtmlEvent } from '../types'; import type { NativeSyntheticEvent } from 'react-native'; import { useOnEditorChange } from './useOnEditorChange'; -import { normalizeHtmlFromTiptap } from './tiptapHtmlNormalizer'; +import { normalizeHtmlFromTiptap } from './normalization/tiptapHtmlNormalizer'; export const useOnChangeHtml = ( editor: Editor, diff --git a/yarn.lock b/yarn.lock index 46bce3472..3422547d6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4304,6 +4304,13 @@ __metadata: languageName: node linkType: hard +"@tootallnate/once@npm:2": + version: 2.0.1 + resolution: "@tootallnate/once@npm:2.0.1" + checksum: 10c0/23b01a341485be711c602077936d70f8e695405bb88ab4433dc6d1e6cb4556401518789574d399eded790b70b27738136c9a8f02df7ae4219f4ba28bb22d586b + languageName: node + linkType: hard + "@tootallnate/quickjs-emscripten@npm:^0.23.0": version: 0.23.0 resolution: "@tootallnate/quickjs-emscripten@npm:0.23.0" @@ -4419,6 +4426,17 @@ __metadata: languageName: node linkType: hard +"@types/jsdom@npm:^20.0.0": + version: 20.0.1 + resolution: "@types/jsdom@npm:20.0.1" + dependencies: + "@types/node": "npm:*" + "@types/tough-cookie": "npm:*" + parse5: "npm:^7.0.0" + checksum: 10c0/3d4b2a3eab145674ee6da482607c5e48977869109f0f62560bf91ae1a792c9e847ac7c6aaf243ed2e97333cb3c51aef314ffa54a19ef174b8f9592dfcb836b25 + languageName: node + linkType: hard + "@types/json-schema@npm:^7.0.15, @types/json-schema@npm:^7.0.9": version: 7.0.15 resolution: "@types/json-schema@npm:7.0.15" @@ -4523,6 +4541,13 @@ __metadata: languageName: node linkType: hard +"@types/tough-cookie@npm:*": + version: 4.0.5 + resolution: "@types/tough-cookie@npm:4.0.5" + checksum: 10c0/68c6921721a3dcb40451543db2174a145ef915bc8bcbe7ad4e59194a0238e776e782b896c7a59f4b93ac6acefca9161fccb31d1ce3b3445cb6faa467297fb473 + languageName: node + linkType: hard + "@types/use-sync-external-store@npm:^0.0.6": version: 0.0.6 resolution: "@types/use-sync-external-store@npm:0.0.6" @@ -4897,6 +4922,13 @@ __metadata: languageName: node linkType: hard +"abab@npm:^2.0.6": + version: 2.0.6 + resolution: "abab@npm:2.0.6" + checksum: 10c0/0b245c3c3ea2598fe0025abf7cc7bb507b06949d51e8edae5d12c1b847a0a0c09639abcb94788332b4e2044ac4491c1e8f571b51c7826fd4b0bda1685ad4a278 + languageName: node + linkType: hard + "abbrev@npm:^3.0.0": version: 3.0.1 resolution: "abbrev@npm:3.0.1" @@ -4933,6 +4965,16 @@ __metadata: languageName: node linkType: hard +"acorn-globals@npm:^7.0.0": + version: 7.0.1 + resolution: "acorn-globals@npm:7.0.1" + dependencies: + acorn: "npm:^8.1.0" + acorn-walk: "npm:^8.0.2" + checksum: 10c0/7437f58e92d99292dbebd0e79531af27d706c9f272f31c675d793da6c82d897e75302a8744af13c7f7978a8399840f14a353b60cf21014647f71012982456d2b + languageName: node + linkType: hard + "acorn-jsx@npm:^5.3.2": version: 5.3.2 resolution: "acorn-jsx@npm:5.3.2" @@ -4942,6 +4984,24 @@ __metadata: languageName: node linkType: hard +"acorn-walk@npm:^8.0.2": + version: 8.3.5 + resolution: "acorn-walk@npm:8.3.5" + dependencies: + acorn: "npm:^8.11.0" + checksum: 10c0/e31bf5b5423ed1349437029d66d708b9fbd1b77a644b031501e2c753b028d13b56348210ed901d5b1d0d86eb3381c0a0fc0d0998511a9d546d1194936266a332 + languageName: node + linkType: hard + +"acorn@npm:^8.1.0, acorn@npm:^8.11.0, acorn@npm:^8.8.1": + version: 8.16.0 + resolution: "acorn@npm:8.16.0" + bin: + acorn: bin/acorn + checksum: 10c0/c9c52697227661b68d0debaf972222d4f622aa06b185824164e153438afa7b08273432ca43ea792cadb24dada1d46f6f6bb1ef8de9956979288cc1b96bf9914e + languageName: node + linkType: hard + "acorn@npm:^8.14.0, acorn@npm:^8.8.2": version: 8.14.1 resolution: "acorn@npm:8.14.1" @@ -4958,6 +5018,15 @@ __metadata: languageName: node linkType: hard +"agent-base@npm:6": + version: 6.0.2 + resolution: "agent-base@npm:6.0.2" + dependencies: + debug: "npm:4" + checksum: 10c0/dc4f757e40b5f3e3d674bc9beb4f1048f4ee83af189bae39be99f57bf1f48dde166a8b0a5342a84b5944ee8e6ed1e5a9d801858f4ad44764e84957122fe46261 + languageName: node + linkType: hard + "agent-base@npm:^7.1.0, agent-base@npm:^7.1.2": version: 7.1.3 resolution: "agent-base@npm:7.1.3" @@ -5305,6 +5374,13 @@ __metadata: languageName: node linkType: hard +"asynckit@npm:^0.4.0": + version: 0.4.0 + resolution: "asynckit@npm:0.4.0" + checksum: 10c0/d73e2ddf20c4eb9337e1b3df1a0f6159481050a5de457c55b14ea2e5cb6d90bb69e004c9af54737a5ee0917fcf2c9e25de67777bbe58261847846066ba75bc9d + languageName: node + linkType: hard + "atomically@npm:^2.0.3": version: 2.0.3 resolution: "atomically@npm:2.0.3" @@ -6024,6 +6100,15 @@ __metadata: languageName: node linkType: hard +"combined-stream@npm:^1.0.8": + version: 1.0.8 + resolution: "combined-stream@npm:1.0.8" + dependencies: + delayed-stream: "npm:~1.0.0" + checksum: 10c0/0dbb829577e1b1e839fa82b40c07ffaf7de8a09b935cadd355a73652ae70a88b4320db322f6634a4ad93424292fa80973ac6480986247f1734a1137debf271d5 + languageName: node + linkType: hard + "command-exists@npm:^1.2.8": version: 1.2.9 resolution: "command-exists@npm:1.2.9" @@ -6450,6 +6535,29 @@ __metadata: languageName: node linkType: hard +"cssom@npm:^0.5.0": + version: 0.5.0 + resolution: "cssom@npm:0.5.0" + checksum: 10c0/8c4121c243baf0678c65dcac29b201ff0067dfecf978de9d5c83b2ff127a8fdefd2bfd54577f5ad8c80ed7d2c8b489ae01c82023545d010c4ecb87683fb403dd + languageName: node + linkType: hard + +"cssom@npm:~0.3.6": + version: 0.3.8 + resolution: "cssom@npm:0.3.8" + checksum: 10c0/d74017b209440822f9e24d8782d6d2e808a8fdd58fa626a783337222fe1c87a518ba944d4c88499031b4786e68772c99dfae616638d71906fe9f203aeaf14411 + languageName: node + linkType: hard + +"cssstyle@npm:^2.3.0": + version: 2.3.0 + resolution: "cssstyle@npm:2.3.0" + dependencies: + cssom: "npm:~0.3.6" + checksum: 10c0/863400da2a458f73272b9a55ba7ff05de40d850f22eb4f37311abebd7eff801cf1cd2fb04c4c92b8c3daed83fe766e52e4112afb7bc88d86c63a9c2256a7d178 + languageName: node + linkType: hard + "csstype@npm:^3.0.2": version: 3.1.3 resolution: "csstype@npm:3.1.3" @@ -6478,6 +6586,17 @@ __metadata: languageName: node linkType: hard +"data-urls@npm:^3.0.2": + version: 3.0.2 + resolution: "data-urls@npm:3.0.2" + dependencies: + abab: "npm:^2.0.6" + whatwg-mimetype: "npm:^3.0.0" + whatwg-url: "npm:^11.0.0" + checksum: 10c0/051c3aaaf3e961904f136aab095fcf6dff4db23a7fc759dd8ba7b3e6ba03fc07ef608086caad8ab910d864bd3b5e57d0d2f544725653d77c96a2c971567045f4 + languageName: node + linkType: hard + "data-view-buffer@npm:^1.0.2": version: 1.0.2 resolution: "data-view-buffer@npm:1.0.2" @@ -6563,6 +6682,13 @@ __metadata: languageName: node linkType: hard +"decimal.js@npm:^10.4.2": + version: 10.6.0 + resolution: "decimal.js@npm:10.6.0" + checksum: 10c0/07d69fbcc54167a340d2d97de95f546f9ff1f69d2b45a02fd7a5292412df3cd9eb7e23065e532a318f5474a2e1bccf8392fdf0443ef467f97f3bf8cb0477e5aa + languageName: node + linkType: hard + "dedent@npm:^0.7.0": version: 0.7.0 resolution: "dedent@npm:0.7.0" @@ -6714,6 +6840,13 @@ __metadata: languageName: node linkType: hard +"delayed-stream@npm:~1.0.0": + version: 1.0.0 + resolution: "delayed-stream@npm:1.0.0" + checksum: 10c0/d758899da03392e6712f042bec80aa293bbe9e9ff1b2634baae6a360113e708b91326594c8a486d475c69d6259afb7efacdc3537bfcda1c6c648e390ce601b19 + languageName: node + linkType: hard + "depd@npm:2.0.0": version: 2.0.0 resolution: "depd@npm:2.0.0" @@ -6767,6 +6900,15 @@ __metadata: languageName: node linkType: hard +"domexception@npm:^4.0.0": + version: 4.0.0 + resolution: "domexception@npm:4.0.0" + dependencies: + webidl-conversions: "npm:^7.0.0" + checksum: 10c0/774277cd9d4df033f852196e3c0077a34dbd15a96baa4d166e0e47138a80f4c0bdf0d94e4703e6ff5883cec56bb821a6fff84402d8a498e31de7c87eb932a294 + languageName: node + linkType: hard + "dot-prop@npm:^5.1.0": version: 5.3.0 resolution: "dot-prop@npm:5.3.0" @@ -6884,6 +7026,13 @@ __metadata: languageName: node linkType: hard +"entities@npm:^6.0.0": + version: 6.0.1 + resolution: "entities@npm:6.0.1" + checksum: 10c0/ed836ddac5acb34341094eb495185d527bd70e8632b6c0d59548cbfa23defdbae70b96f9a405c82904efa421230b5b3fd2283752447d737beffd3f3e6ee74414 + languageName: node + linkType: hard + "env-paths@npm:^2.2.0, env-paths@npm:^2.2.1": version: 2.2.1 resolution: "env-paths@npm:2.2.1" @@ -7211,7 +7360,7 @@ __metadata: languageName: node linkType: hard -"escodegen@npm:^2.1.0": +"escodegen@npm:^2.0.0, escodegen@npm:^2.1.0": version: 2.1.0 resolution: "escodegen@npm:2.1.0" dependencies: @@ -7932,6 +8081,19 @@ __metadata: languageName: node linkType: hard +"form-data@npm:^4.0.0": + version: 4.0.5 + resolution: "form-data@npm:4.0.5" + dependencies: + asynckit: "npm:^0.4.0" + combined-stream: "npm:^1.0.8" + es-set-tostringtag: "npm:^2.1.0" + hasown: "npm:^2.0.2" + mime-types: "npm:^2.1.12" + checksum: 10c0/dd6b767ee0bbd6d84039db12a0fa5a2028160ffbfaba1800695713b46ae974a5f6e08b3356c3195137f8530dcd9dfcb5d5ae1eeff53d0db1e5aad863b619ce3b + languageName: node + linkType: hard + "fresh@npm:0.5.2": version: 0.5.2 resolution: "fresh@npm:0.5.2" @@ -8544,6 +8706,15 @@ __metadata: languageName: node linkType: hard +"html-encoding-sniffer@npm:^3.0.0": + version: 3.0.0 + resolution: "html-encoding-sniffer@npm:3.0.0" + dependencies: + whatwg-encoding: "npm:^2.0.0" + checksum: 10c0/b17b3b0fb5d061d8eb15121c3b0b536376c3e295ecaf09ba48dd69c6b6c957839db124fe1e2b3f11329753a4ee01aa7dedf63b7677999e86da17fbbdd82c5386 + languageName: node + linkType: hard + "html-escaper@npm:^2.0.0": version: 2.0.2 resolution: "html-escaper@npm:2.0.2" @@ -8571,6 +8742,17 @@ __metadata: languageName: node linkType: hard +"http-proxy-agent@npm:^5.0.0": + version: 5.0.0 + resolution: "http-proxy-agent@npm:5.0.0" + dependencies: + "@tootallnate/once": "npm:2" + agent-base: "npm:6" + debug: "npm:4" + checksum: 10c0/32a05e413430b2c1e542e5c74b38a9f14865301dd69dff2e53ddb684989440e3d2ce0c4b64d25eb63cf6283e6265ff979a61cf93e3ca3d23047ddfdc8df34a32 + languageName: node + linkType: hard + "http-proxy-agent@npm:^7.0.0, http-proxy-agent@npm:^7.0.1": version: 7.0.2 resolution: "http-proxy-agent@npm:7.0.2" @@ -8581,6 +8763,16 @@ __metadata: languageName: node linkType: hard +"https-proxy-agent@npm:^5.0.1": + version: 5.0.1 + resolution: "https-proxy-agent@npm:5.0.1" + dependencies: + agent-base: "npm:6" + debug: "npm:4" + checksum: 10c0/6dd639f03434003577c62b27cafdb864784ef19b2de430d8ae2a1d45e31c4fd60719e5637b44db1a88a046934307da7089e03d6089ec3ddacc1189d8de8897d1 + languageName: node + linkType: hard + "https-proxy-agent@npm:^7.0.1, https-proxy-agent@npm:^7.0.5, https-proxy-agent@npm:^7.0.6": version: 7.0.6 resolution: "https-proxy-agent@npm:7.0.6" @@ -8621,7 +8813,7 @@ __metadata: languageName: node linkType: hard -"iconv-lite@npm:^0.6.2": +"iconv-lite@npm:0.6.3, iconv-lite@npm:^0.6.2": version: 0.6.3 resolution: "iconv-lite@npm:0.6.3" dependencies: @@ -9148,6 +9340,13 @@ __metadata: languageName: node linkType: hard +"is-potential-custom-element-name@npm:^1.0.1": + version: 1.0.1 + resolution: "is-potential-custom-element-name@npm:1.0.1" + checksum: 10c0/b73e2f22bc863b0939941d369486d308b43d7aef1f9439705e3582bfccaa4516406865e32c968a35f97a99396dac84e2624e67b0a16b0a15086a785e16ce7db9 + languageName: node + linkType: hard + "is-regex@npm:^1.2.1": version: 1.2.1 resolution: "is-regex@npm:1.2.1" @@ -9598,6 +9797,27 @@ __metadata: languageName: node linkType: hard +"jest-environment-jsdom@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-environment-jsdom@npm:29.7.0" + dependencies: + "@jest/environment": "npm:^29.7.0" + "@jest/fake-timers": "npm:^29.7.0" + "@jest/types": "npm:^29.6.3" + "@types/jsdom": "npm:^20.0.0" + "@types/node": "npm:*" + jest-mock: "npm:^29.7.0" + jest-util: "npm:^29.7.0" + jsdom: "npm:^20.0.0" + peerDependencies: + canvas: ^2.5.0 + peerDependenciesMeta: + canvas: + optional: true + checksum: 10c0/139b94e2c8ec1bb5a46ce17df5211da65ce867354b3fd4e00fa6a0d1da95902df4cf7881273fc6ea937e5c325d39d6773f0d41b6c469363334de9d489d2c321f + languageName: node + linkType: hard + "jest-environment-node@npm:^29.7.0": version: 29.7.0 resolution: "jest-environment-node@npm:29.7.0" @@ -9966,6 +10186,45 @@ __metadata: languageName: node linkType: hard +"jsdom@npm:^20.0.0": + version: 20.0.3 + resolution: "jsdom@npm:20.0.3" + dependencies: + abab: "npm:^2.0.6" + acorn: "npm:^8.8.1" + acorn-globals: "npm:^7.0.0" + cssom: "npm:^0.5.0" + cssstyle: "npm:^2.3.0" + data-urls: "npm:^3.0.2" + decimal.js: "npm:^10.4.2" + domexception: "npm:^4.0.0" + escodegen: "npm:^2.0.0" + form-data: "npm:^4.0.0" + html-encoding-sniffer: "npm:^3.0.0" + http-proxy-agent: "npm:^5.0.0" + https-proxy-agent: "npm:^5.0.1" + is-potential-custom-element-name: "npm:^1.0.1" + nwsapi: "npm:^2.2.2" + parse5: "npm:^7.1.1" + saxes: "npm:^6.0.0" + symbol-tree: "npm:^3.2.4" + tough-cookie: "npm:^4.1.2" + w3c-xmlserializer: "npm:^4.0.0" + webidl-conversions: "npm:^7.0.0" + whatwg-encoding: "npm:^2.0.0" + whatwg-mimetype: "npm:^3.0.0" + whatwg-url: "npm:^11.0.0" + ws: "npm:^8.11.0" + xml-name-validator: "npm:^4.0.0" + peerDependencies: + canvas: ^2.5.0 + peerDependenciesMeta: + canvas: + optional: true + checksum: 10c0/b109073bb826a966db7828f46cb1d7371abecd30f182b143c52be5fe1ed84513bbbe995eb3d157241681fcd18331381e61e3dc004d4949f3a63bca02f6214902 + languageName: node + linkType: hard + "jsesc@npm:^3.0.2": version: 3.1.0 resolution: "jsesc@npm:3.1.0" @@ -11030,7 +11289,7 @@ __metadata: languageName: node linkType: hard -"mime-types@npm:2.1.35, mime-types@npm:^2.1.27, mime-types@npm:~2.1.24, mime-types@npm:~2.1.34": +"mime-types@npm:2.1.35, mime-types@npm:^2.1.12, mime-types@npm:^2.1.27, mime-types@npm:~2.1.24, mime-types@npm:~2.1.34": version: 2.1.35 resolution: "mime-types@npm:2.1.35" dependencies: @@ -11419,6 +11678,13 @@ __metadata: languageName: node linkType: hard +"nwsapi@npm:^2.2.2": + version: 2.2.24 + resolution: "nwsapi@npm:2.2.24" + checksum: 10c0/9bc04ee9c7698f1b5506778d36f7382962f71667205d441d6a50f6180ee92328e770b76be78b907817ee103241b29984d3a17ae387e4723aebe0aeaed7a7c3a1 + languageName: node + linkType: hard + "ob1@npm:0.83.1": version: 0.83.1 resolution: "ob1@npm:0.83.1" @@ -11874,6 +12140,15 @@ __metadata: languageName: node linkType: hard +"parse5@npm:^7.0.0, parse5@npm:^7.1.1": + version: 7.3.0 + resolution: "parse5@npm:7.3.0" + dependencies: + entities: "npm:^6.0.0" + checksum: 10c0/7fd2e4e247e85241d6f2a464d0085eed599a26d7b0a5233790c49f53473232eb85350e8133344d9b3fd58b89339e7ad7270fe1f89d28abe50674ec97b87f80b5 + languageName: node + linkType: hard + "parseurl@npm:~1.3.3": version: 1.3.3 resolution: "parseurl@npm:1.3.3" @@ -12347,6 +12622,15 @@ __metadata: languageName: node linkType: hard +"psl@npm:^1.1.33": + version: 1.15.0 + resolution: "psl@npm:1.15.0" + dependencies: + punycode: "npm:^2.3.1" + checksum: 10c0/d8d45a99e4ca62ca12ac3c373e63d80d2368d38892daa40cfddaa1eb908be98cd549ac059783ef3a56cfd96d57ae8e2fd9ae53d1378d90d42bc661ff924e102a + languageName: node + linkType: hard + "pump@npm:^3.0.0": version: 3.0.2 resolution: "pump@npm:3.0.2" @@ -12364,7 +12648,7 @@ __metadata: languageName: node linkType: hard -"punycode@npm:^2.1.0": +"punycode@npm:^2.1.0, punycode@npm:^2.1.1, punycode@npm:^2.3.1": version: 2.3.1 resolution: "punycode@npm:2.3.1" checksum: 10c0/14f76a8206bc3464f794fb2e3d3cc665ae416c01893ad7a02b23766eb07159144ee612ad67af5e84fa4479ccfe67678c4feb126b0485651b302babf66f04f9e9 @@ -12396,6 +12680,13 @@ __metadata: languageName: node linkType: hard +"querystringify@npm:^2.1.1": + version: 2.2.0 + resolution: "querystringify@npm:2.2.0" + checksum: 10c0/3258bc3dbdf322ff2663619afe5947c7926a6ef5fb78ad7d384602974c467fadfc8272af44f5eb8cddd0d011aae8fabf3a929a8eee4b86edcc0a21e6bd10f9aa + languageName: node + linkType: hard + "queue-microtask@npm:^1.2.2": version: 1.2.3 resolution: "queue-microtask@npm:1.2.3" @@ -12594,6 +12885,7 @@ __metadata: eslint-config-prettier: "npm:^10.1.1" eslint-plugin-prettier: "npm:^5.2.3" jest: "npm:^29.7.0" + jest-environment-jsdom: "npm:^29.7.0" playwright: "npm:1.58.2" prettier: "npm:^3.0.3" react: "npm:19.1.0" @@ -13032,6 +13324,13 @@ __metadata: languageName: node linkType: hard +"requires-port@npm:^1.0.0": + version: 1.0.0 + resolution: "requires-port@npm:1.0.0" + checksum: 10c0/b2bfdd09db16c082c4326e573a82c0771daaf7b53b9ce8ad60ea46aa6e30aaf475fe9b164800b89f93b748d2c234d8abff945d2551ba47bf5698e04cd7713267 + languageName: node + linkType: hard + "resolve-cwd@npm:^3.0.0": version: 3.0.0 resolution: "resolve-cwd@npm:3.0.0" @@ -13341,6 +13640,15 @@ __metadata: languageName: node linkType: hard +"saxes@npm:^6.0.0": + version: 6.0.0 + resolution: "saxes@npm:6.0.0" + dependencies: + xmlchars: "npm:^2.2.0" + checksum: 10c0/3847b839f060ef3476eb8623d099aa502ad658f5c40fd60c105ebce86d244389b0d76fcae30f4d0c728d7705ceb2f7e9b34bb54717b6a7dbedaf5dad2d9a4b74 + languageName: node + linkType: hard + "scheduler@npm:0.26.0, scheduler@npm:^0.26.0": version: 0.26.0 resolution: "scheduler@npm:0.26.0" @@ -14034,6 +14342,13 @@ __metadata: languageName: node linkType: hard +"symbol-tree@npm:^3.2.4": + version: 3.2.4 + resolution: "symbol-tree@npm:3.2.4" + checksum: 10c0/dfbe201ae09ac6053d163578778c53aa860a784147ecf95705de0cd23f42c851e1be7889241495e95c37cabb058edb1052f141387bef68f705afc8f9dd358509 + languageName: node + linkType: hard + "synckit@npm:^0.11.0": version: 0.11.4 resolution: "synckit@npm:0.11.4" @@ -14163,6 +14478,27 @@ __metadata: languageName: node linkType: hard +"tough-cookie@npm:^4.1.2": + version: 4.1.4 + resolution: "tough-cookie@npm:4.1.4" + dependencies: + psl: "npm:^1.1.33" + punycode: "npm:^2.1.1" + universalify: "npm:^0.2.0" + url-parse: "npm:^1.5.3" + checksum: 10c0/aca7ff96054f367d53d1e813e62ceb7dd2eda25d7752058a74d64b7266fd07be75908f3753a32ccf866a2f997604b414cfb1916d6e7f69bc64d9d9939b0d6c45 + languageName: node + linkType: hard + +"tr46@npm:^3.0.0": + version: 3.0.0 + resolution: "tr46@npm:3.0.0" + dependencies: + punycode: "npm:^2.1.1" + checksum: 10c0/cdc47cad3a9d0b6cb293e39ccb1066695ae6fdd39b9e4f351b010835a1f8b4f3a6dc3a55e896b421371187f22b48d7dac1b693de4f6551bdef7b6ab6735dfe3b + languageName: node + linkType: hard + "trim-newlines@npm:^4.0.2": version: 4.1.1 resolution: "trim-newlines@npm:4.1.1" @@ -14577,6 +14913,13 @@ __metadata: languageName: node linkType: hard +"universalify@npm:^0.2.0": + version: 0.2.0 + resolution: "universalify@npm:0.2.0" + checksum: 10c0/cedbe4d4ca3967edf24c0800cfc161c5a15e240dac28e3ce575c689abc11f2c81ccc6532c8752af3b40f9120fb5e454abecd359e164f4f6aa44c29cd37e194fe + languageName: node + linkType: hard + "universalify@npm:^2.0.0": version: 2.0.1 resolution: "universalify@npm:2.0.1" @@ -14639,6 +14982,16 @@ __metadata: languageName: node linkType: hard +"url-parse@npm:^1.5.3": + version: 1.5.10 + resolution: "url-parse@npm:1.5.10" + dependencies: + querystringify: "npm:^2.1.1" + requires-port: "npm:^1.0.0" + checksum: 10c0/bd5aa9389f896974beb851c112f63b466505a04b4807cea2e5a3b7092f6fbb75316f0491ea84e44f66fed55f1b440df5195d7e3a8203f64fcefa19d182f5be87 + languageName: node + linkType: hard + "use-sync-external-store@npm:^1.4.0": version: 1.6.0 resolution: "use-sync-external-store@npm:1.6.0" @@ -14759,6 +15112,15 @@ __metadata: languageName: node linkType: hard +"w3c-xmlserializer@npm:^4.0.0": + version: 4.0.0 + resolution: "w3c-xmlserializer@npm:4.0.0" + dependencies: + xml-name-validator: "npm:^4.0.0" + checksum: 10c0/02cc66d6efc590bd630086cd88252444120f5feec5c4043932b0d0f74f8b060512f79dc77eb093a7ad04b4f02f39da79ce4af47ceb600f2bf9eacdc83204b1a8 + languageName: node + linkType: hard + "walker@npm:^1.0.7, walker@npm:^1.0.8": version: 1.0.8 resolution: "walker@npm:1.0.8" @@ -14777,6 +15139,22 @@ __metadata: languageName: node linkType: hard +"webidl-conversions@npm:^7.0.0": + version: 7.0.0 + resolution: "webidl-conversions@npm:7.0.0" + checksum: 10c0/228d8cb6d270c23b0720cb2d95c579202db3aaf8f633b4e9dd94ec2000a04e7e6e43b76a94509cdb30479bd00ae253ab2371a2da9f81446cc313f89a4213a2c4 + languageName: node + linkType: hard + +"whatwg-encoding@npm:^2.0.0": + version: 2.0.0 + resolution: "whatwg-encoding@npm:2.0.0" + dependencies: + iconv-lite: "npm:0.6.3" + checksum: 10c0/91b90a49f312dc751496fd23a7e68981e62f33afe938b97281ad766235c4872fc4e66319f925c5e9001502b3040dd25a33b02a9c693b73a4cbbfdc4ad10c3e3e + languageName: node + linkType: hard + "whatwg-fetch@npm:^3.0.0": version: 3.6.20 resolution: "whatwg-fetch@npm:3.6.20" @@ -14784,6 +15162,23 @@ __metadata: languageName: node linkType: hard +"whatwg-mimetype@npm:^3.0.0": + version: 3.0.0 + resolution: "whatwg-mimetype@npm:3.0.0" + checksum: 10c0/323895a1cda29a5fb0b9ca82831d2c316309fede0365047c4c323073e3239067a304a09a1f4b123b9532641ab604203f33a1403b5ca6a62ef405bcd7a204080f + languageName: node + linkType: hard + +"whatwg-url@npm:^11.0.0": + version: 11.0.0 + resolution: "whatwg-url@npm:11.0.0" + dependencies: + tr46: "npm:^3.0.0" + webidl-conversions: "npm:^7.0.0" + checksum: 10c0/f7ec264976d7c725e0696fcaf9ebe056e14422eacbf92fdbb4462034609cba7d0c85ffa1aab05e9309d42969bcf04632ba5ed3f3882c516d7b093053315bf4c1 + languageName: node + linkType: hard + "when-exit@npm:^2.1.1": version: 2.1.4 resolution: "when-exit@npm:2.1.4" @@ -15005,6 +15400,21 @@ __metadata: languageName: node linkType: hard +"ws@npm:^8.11.0": + version: 8.21.0 + resolution: "ws@npm:8.21.0" + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ">=5.0.2" + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + checksum: 10c0/ef4a243476283fc49bc7550966c4af4aa0eef56273837211e700de3b664e08604a760cdddcb5ba43c049140e74ccfec5b0ee0bb439e08c2adf9138902fdde5f9 + languageName: node + linkType: hard + "xdg-basedir@npm:^5.1.0": version: 5.1.0 resolution: "xdg-basedir@npm:5.1.0" @@ -15012,6 +15422,20 @@ __metadata: languageName: node linkType: hard +"xml-name-validator@npm:^4.0.0": + version: 4.0.0 + resolution: "xml-name-validator@npm:4.0.0" + checksum: 10c0/c1bfa219d64e56fee265b2bd31b2fcecefc063ee802da1e73bad1f21d7afd89b943c9e2c97af2942f60b1ad46f915a4c81e00039c7d398b53cf410e29d3c30bd + languageName: node + linkType: hard + +"xmlchars@npm:^2.2.0": + version: 2.2.0 + resolution: "xmlchars@npm:2.2.0" + checksum: 10c0/b64b535861a6f310c5d9bfa10834cf49127c71922c297da9d4d1b45eeaae40bf9b4363275876088fbe2667e5db028d2cd4f8ee72eed9bede840a67d57dab7593 + languageName: node + linkType: hard + "y18n@npm:^4.0.0": version: 4.0.3 resolution: "y18n@npm:4.0.3" From 77d0ee6787cdd7086fb3bbfe4270e63f3ec04a0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20=C5=BB=C3=B3=C5=82kiewski?= <74975508+kacperzolkiewski@users.noreply.github.com> Date: Fri, 19 Jun 2026 09:56:22 +0200 Subject: [PATCH 11/11] fix(android): set piority of applied spans (#629) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Summary Apply priority to android spans that inline spans will be applied after paragraph spans. See: https://stackoverflow.com/questions/46002947/how-to-define-order-setpriority-of-spannable-in-android ## Test Plan Play around with styles on android and check if inline styles override paragraph styles. For example: Inline code inside blockquote. ## Screenshots / Videos https://github.com/user-attachments/assets/9bacb687-ad62-43fe-a0d0-4c135548f1ef ## Compatibility | OS | Implemented | | ------- | :---------: | | iOS | ❌ | | Android | ✅ | ## Checklist - [x] E2E tests are passing - [ ] Required E2E tests have been added (if applicable) --- .../enriched/common/EnrichedSpanFlags.kt | 39 +++++++++++++++++++ .../common/parser/EnrichedParser.java | 20 ++++------ .../enriched/text/EnrichedTextView.kt | 3 +- .../textinput/EnrichedTextInputView.kt | 3 +- .../textinput/styles/AlignmentStyles.kt | 13 +++---- .../enriched/textinput/styles/InlineStyles.kt | 3 +- .../enriched/textinput/styles/ListStyles.kt | 7 ++-- .../textinput/styles/ParagraphStyles.kt | 7 ++-- .../textinput/styles/ParametrizedStyles.kt | 7 ++-- .../textinput/utils/EnrichedSpannable.kt | 3 +- .../enriched/textinput/utils/Utils.kt | 3 +- 11 files changed, 75 insertions(+), 33 deletions(-) create mode 100644 android/src/main/java/com/swmansion/enriched/common/EnrichedSpanFlags.kt diff --git a/android/src/main/java/com/swmansion/enriched/common/EnrichedSpanFlags.kt b/android/src/main/java/com/swmansion/enriched/common/EnrichedSpanFlags.kt new file mode 100644 index 000000000..d9a0ddb50 --- /dev/null +++ b/android/src/main/java/com/swmansion/enriched/common/EnrichedSpanFlags.kt @@ -0,0 +1,39 @@ +package com.swmansion.enriched.common + +import android.text.Spannable +import com.swmansion.enriched.common.spans.EnrichedAlignmentSpan +import com.swmansion.enriched.common.spans.interfaces.EnrichedInlineSpan + +// Higher priority spans are processed first, so styles with lower priorities are painted on top of previously applied styles. +// For example, inline styles are applied on top of paragraph styles, allowing them to override paragraph-level styling. +// Alignment styles are applied last, ensuring they position the final, fully styled text. +object EnrichedSpanFlags { + private const val ALIGNMENT_SPAN_PRIORITY = 0 + private const val INLINE_SPAN_PRIORITY = 1 + private const val PARAGRAPH_SPAN_PRIORITY = 2 + + @JvmStatic + @JvmOverloads + fun forSpan( + span: Any?, + baseFlags: Int = Spannable.SPAN_EXCLUSIVE_EXCLUSIVE, + ): Int { + val priority = + when (span) { + is EnrichedAlignmentSpan -> ALIGNMENT_SPAN_PRIORITY + is EnrichedInlineSpan -> INLINE_SPAN_PRIORITY + else -> PARAGRAPH_SPAN_PRIORITY + } + return applyPriority(baseFlags, priority) + } + + private fun applyPriority( + flags: Int, + priority: Int, + ): Int { + // Cleaning up priority bits + val cleared = flags and Spannable.SPAN_PRIORITY.inv() + // Injecting priority bits + return cleared or ((priority shl Spannable.SPAN_PRIORITY_SHIFT) and Spannable.SPAN_PRIORITY) + } +} diff --git a/android/src/main/java/com/swmansion/enriched/common/parser/EnrichedParser.java b/android/src/main/java/com/swmansion/enriched/common/parser/EnrichedParser.java index 02d52f706..08198b3c6 100644 --- a/android/src/main/java/com/swmansion/enriched/common/parser/EnrichedParser.java +++ b/android/src/main/java/com/swmansion/enriched/common/parser/EnrichedParser.java @@ -7,6 +7,7 @@ import android.text.TextUtils; import android.text.style.ParagraphStyle; import com.swmansion.enriched.common.EnrichedConstants; +import com.swmansion.enriched.common.EnrichedSpanFlags; import com.swmansion.enriched.common.spans.EnrichedAlignmentSpan; import com.swmansion.enriched.common.spans.EnrichedBoldSpan; import com.swmansion.enriched.common.spans.EnrichedCheckboxListSpan; @@ -467,10 +468,7 @@ public Spanned convert() { if (end == start) { mSpannableStringBuilder.removeSpan(obj[i]); } else { - // TODO: verify if Spannable.SPAN_EXCLUSIVE_EXCLUSIVE does not break anything. - // Previously it was SPAN_PARAGRAPH. I've changed that in order to fix ranges for list - // items. - mSpannableStringBuilder.setSpan(obj[i], start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + mSpannableStringBuilder.setSpan(obj[i], start, end, EnrichedSpanFlags.forSpan(obj[i])); } } @@ -505,7 +503,7 @@ public Spanned convert() { mSpannableStringBuilder.removeSpan(zeroWidthSpaceSpan); mSpannableStringBuilder.setSpan( - zeroWidthSpaceSpan, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + zeroWidthSpaceSpan, start, end, EnrichedSpanFlags.forSpan(zeroWidthSpaceSpan)); } return mSpannableStringBuilder; @@ -802,7 +800,7 @@ private static void setSpanFromMark(Spannable text, Object mark, Object... spans int len = text.length(); if (where != len) { for (Object span : spans) { - text.setSpan(span, where, len, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + text.setSpan(span, where, len, EnrichedSpanFlags.forSpan(span)); } } } @@ -825,7 +823,7 @@ private static void setParagraphSpanFromMark(Editable text, Object mark, Object. if (where != len) { for (Object span : spans) { - text.setSpan(span, where, len, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + text.setSpan(span, where, len, EnrichedSpanFlags.forSpan(span)); } } } @@ -850,11 +848,9 @@ private static void startImg( int len = text.length(); text.append(""); - text.setSpan( - spanFactory.createImageSpan(src, Integer.parseInt(width), Integer.parseInt(height)), - len, - text.length(), - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + Object imageSpan = + spanFactory.createImageSpan(src, Integer.parseInt(width), Integer.parseInt(height)); + text.setSpan(imageSpan, len, text.length(), EnrichedSpanFlags.forSpan(imageSpan)); } private static void startA(Editable text, Attributes attributes) { diff --git a/android/src/main/java/com/swmansion/enriched/text/EnrichedTextView.kt b/android/src/main/java/com/swmansion/enriched/text/EnrichedTextView.kt index d3133ee8c..53a0a503a 100644 --- a/android/src/main/java/com/swmansion/enriched/text/EnrichedTextView.kt +++ b/android/src/main/java/com/swmansion/enriched/text/EnrichedTextView.kt @@ -23,6 +23,7 @@ import com.facebook.react.views.text.ReactTypefaceUtils.applyStyles import com.facebook.react.views.text.ReactTypefaceUtils.parseFontStyle import com.facebook.react.views.text.ReactTypefaceUtils.parseFontWeight import com.swmansion.enriched.common.EnrichedConstants +import com.swmansion.enriched.common.EnrichedSpanFlags import com.swmansion.enriched.common.GumboNormalizer import com.swmansion.enriched.common.parser.EnrichedParser import com.swmansion.enriched.common.pixelFromSpOrDp @@ -277,7 +278,7 @@ class EnrichedTextView : AppCompatTextView { spannable.removeSpan(span) val newSpan = span.rebuildWithStyle(enrichedStyle) - spannable.setSpan(newSpan, start, end, flags) + spannable.setSpan(newSpan, start, end, EnrichedSpanFlags.forSpan(newSpan, flags)) modified = true } diff --git a/android/src/main/java/com/swmansion/enriched/textinput/EnrichedTextInputView.kt b/android/src/main/java/com/swmansion/enriched/textinput/EnrichedTextInputView.kt index cdcad4e2f..b2a0c16c4 100644 --- a/android/src/main/java/com/swmansion/enriched/textinput/EnrichedTextInputView.kt +++ b/android/src/main/java/com/swmansion/enriched/textinput/EnrichedTextInputView.kt @@ -38,6 +38,7 @@ import com.facebook.react.views.text.ReactTypefaceUtils.applyStyles import com.facebook.react.views.text.ReactTypefaceUtils.parseFontStyle import com.facebook.react.views.text.ReactTypefaceUtils.parseFontWeight import com.swmansion.enriched.common.EnrichedConstants +import com.swmansion.enriched.common.EnrichedSpanFlags import com.swmansion.enriched.common.GumboNormalizer import com.swmansion.enriched.common.parser.EnrichedParser import com.swmansion.enriched.common.pixelFromSpOrDp @@ -1108,7 +1109,7 @@ class EnrichedTextInputView : spannable.removeSpan(span) val newSpan = span.rebuildWithStyle(htmlStyle) - spannable.setSpan(newSpan, start, end, flags) + spannable.setSpan(newSpan, start, end, EnrichedSpanFlags.forSpan(newSpan, flags)) } if (shouldEmitStateChange) { diff --git a/android/src/main/java/com/swmansion/enriched/textinput/styles/AlignmentStyles.kt b/android/src/main/java/com/swmansion/enriched/textinput/styles/AlignmentStyles.kt index d7df95109..e7353f677 100644 --- a/android/src/main/java/com/swmansion/enriched/textinput/styles/AlignmentStyles.kt +++ b/android/src/main/java/com/swmansion/enriched/textinput/styles/AlignmentStyles.kt @@ -4,6 +4,7 @@ import android.text.Editable import android.text.Spannable import android.text.SpannableStringBuilder import com.swmansion.enriched.common.EnrichedConstants +import com.swmansion.enriched.common.EnrichedSpanFlags import com.swmansion.enriched.textinput.EnrichedTextInputView import com.swmansion.enriched.textinput.spans.EnrichedInputAlignmentSpan import com.swmansion.enriched.textinput.spans.EnrichedInputCheckboxListSpan @@ -24,12 +25,8 @@ class AlignmentStyles( flags: Int = Spannable.SPAN_EXCLUSIVE_EXCLUSIVE, ) { val (safeStart, safeEnd) = spannable.getSafeSpanBoundaries(start, end) - spannable.setSpan( - EnrichedInputAlignmentSpan(cssValue), - safeStart, - safeEnd, - flags, - ) + val span = EnrichedInputAlignmentSpan(cssValue) + spannable.setSpan(span, safeStart, safeEnd, EnrichedSpanFlags.forSpan(span, flags)) } private fun toCssValue(alignment: String): String = @@ -302,7 +299,9 @@ class AlignmentStyles( // INCLUSIVE_EXCLUSIVE is intentional here: autoStretchAlignmentSpan will convert // it to EXCLUSIVE_EXCLUSIVE once the merge is complete. val (safeStart, safeEnd) = s.getSafeSpanBoundaries(paraStart, paraEnd) - dominantTopSpan?.let { s.setSpan(it, safeStart, safeEnd, Spannable.SPAN_INCLUSIVE_EXCLUSIVE) } + dominantTopSpan?.let { + s.setSpan(it, safeStart, safeEnd, EnrichedSpanFlags.forSpan(it, Spannable.SPAN_INCLUSIVE_EXCLUSIVE)) + } return cursorPosition } diff --git a/android/src/main/java/com/swmansion/enriched/textinput/styles/InlineStyles.kt b/android/src/main/java/com/swmansion/enriched/textinput/styles/InlineStyles.kt index 26c871cec..802f78f1b 100644 --- a/android/src/main/java/com/swmansion/enriched/textinput/styles/InlineStyles.kt +++ b/android/src/main/java/com/swmansion/enriched/textinput/styles/InlineStyles.kt @@ -2,6 +2,7 @@ package com.swmansion.enriched.textinput.styles import android.text.Editable import android.text.Spannable +import com.swmansion.enriched.common.EnrichedSpanFlags import com.swmansion.enriched.textinput.EnrichedTextInputView import com.swmansion.enriched.textinput.spans.EnrichedSpans import com.swmansion.enriched.textinput.utils.getSafeSpanBoundaries @@ -41,7 +42,7 @@ class InlineStyles( val span = type.getDeclaredConstructor(HtmlStyle::class.java).newInstance(view.htmlStyle) val (safeStart, safeEnd) = spannable.getSafeSpanBoundaries(minimum, maximum) - spannable.setSpan(span, safeStart, safeEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + spannable.setSpan(span, safeStart, safeEnd, EnrichedSpanFlags.forSpan(span)) } private fun setAndMergeSpans( diff --git a/android/src/main/java/com/swmansion/enriched/textinput/styles/ListStyles.kt b/android/src/main/java/com/swmansion/enriched/textinput/styles/ListStyles.kt index 7bce4dcd4..236560f10 100644 --- a/android/src/main/java/com/swmansion/enriched/textinput/styles/ListStyles.kt +++ b/android/src/main/java/com/swmansion/enriched/textinput/styles/ListStyles.kt @@ -5,6 +5,7 @@ import android.text.Spannable import android.text.SpannableStringBuilder import android.text.Spanned import com.swmansion.enriched.common.EnrichedConstants +import com.swmansion.enriched.common.EnrichedSpanFlags import com.swmansion.enriched.textinput.EnrichedTextInputView import com.swmansion.enriched.textinput.spans.EnrichedInputCheckboxListSpan import com.swmansion.enriched.textinput.spans.EnrichedInputOrderedListSpan @@ -66,18 +67,18 @@ class ListStyles( when (name) { EnrichedSpans.UNORDERED_LIST -> { val span = EnrichedInputUnorderedListSpan(view.htmlStyle) - spannable.setSpan(span, safeStart, safeEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + spannable.setSpan(span, safeStart, safeEnd, EnrichedSpanFlags.forSpan(span)) } EnrichedSpans.ORDERED_LIST -> { val index = getOrderedListIndex(spannable, safeStart) val span = EnrichedInputOrderedListSpan(index, view.htmlStyle) - spannable.setSpan(span, safeStart, safeEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + spannable.setSpan(span, safeStart, safeEnd, EnrichedSpanFlags.forSpan(span)) } EnrichedSpans.CHECKBOX_LIST -> { val span = EnrichedInputCheckboxListSpan(isChecked ?: false, view.htmlStyle) - spannable.setSpan(span, safeStart, safeEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + spannable.setSpan(span, safeStart, safeEnd, EnrichedSpanFlags.forSpan(span)) // Invalidate layout to update checkbox drawing in case checkbox is bigger than line height view.layoutManager.invalidateLayout() diff --git a/android/src/main/java/com/swmansion/enriched/textinput/styles/ParagraphStyles.kt b/android/src/main/java/com/swmansion/enriched/textinput/styles/ParagraphStyles.kt index b7d46637f..af822b7fe 100644 --- a/android/src/main/java/com/swmansion/enriched/textinput/styles/ParagraphStyles.kt +++ b/android/src/main/java/com/swmansion/enriched/textinput/styles/ParagraphStyles.kt @@ -4,6 +4,7 @@ import android.text.Editable import android.text.Spannable import android.text.SpannableStringBuilder import android.util.Log +import com.swmansion.enriched.common.EnrichedSpanFlags import com.swmansion.enriched.textinput.EnrichedTextInputView import com.swmansion.enriched.textinput.spans.EnrichedSpans import com.swmansion.enriched.textinput.spans.interfaces.EnrichedInputSpan @@ -89,7 +90,7 @@ class ParagraphStyles( } val (safeStart, safeEnd) = spannable.getSafeSpanBoundaries(newStart, newEnd) - spannable.setSpan(span, safeStart, safeEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + spannable.setSpan(span, safeStart, safeEnd, EnrichedSpanFlags.forSpan(span)) } private fun setSpan( @@ -105,7 +106,7 @@ class ParagraphStyles( val span = type.getDeclaredConstructor(HtmlStyle::class.java).newInstance(view.htmlStyle) val (safeStart, safeEnd) = spannable.getSafeSpanBoundaries(start, end) - spannable.setSpan(span, safeStart, safeEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + spannable.setSpan(span, safeStart, safeEnd, EnrichedSpanFlags.forSpan(span)) } // Removes spans of the given type in the specified range. @@ -232,7 +233,7 @@ class ParagraphStyles( val (safeStart, safeEnd) = s.getSafeSpanBoundaries(newStart, newEnd) val span = type.getDeclaredConstructor(HtmlStyle::class.java).newInstance(view.htmlStyle) - s.setSpan(span, safeStart, safeEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + s.setSpan(span, safeStart, safeEnd, EnrichedSpanFlags.forSpan(span)) } private fun handleConflictsDuringNewlineDeletion( diff --git a/android/src/main/java/com/swmansion/enriched/textinput/styles/ParametrizedStyles.kt b/android/src/main/java/com/swmansion/enriched/textinput/styles/ParametrizedStyles.kt index be6265b7d..c441224ba 100644 --- a/android/src/main/java/com/swmansion/enriched/textinput/styles/ParametrizedStyles.kt +++ b/android/src/main/java/com/swmansion/enriched/textinput/styles/ParametrizedStyles.kt @@ -5,6 +5,7 @@ import android.text.Spannable import android.text.SpannableStringBuilder import android.text.Spanned import com.swmansion.enriched.common.EnrichedConstants +import com.swmansion.enriched.common.EnrichedSpanFlags import com.swmansion.enriched.textinput.EnrichedTextInputView import com.swmansion.enriched.textinput.spans.EnrichedInputImageSpan import com.swmansion.enriched.textinput.spans.EnrichedInputLinkSpan @@ -63,7 +64,7 @@ class ParametrizedStyles( val spanEnd = start + text.length val span = EnrichedInputLinkSpan(url, view.htmlStyle, true) val (safeStart, safeEnd) = spannable.getSafeSpanBoundaries(start, spanEnd) - spannable.setSpan(span, safeStart, safeEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + spannable.setSpan(span, safeStart, safeEnd, EnrichedSpanFlags.forSpan(span)) view.selection?.validateStyles() isSettingLinkSpan = false @@ -160,7 +161,7 @@ class ParametrizedStyles( span, safeStart, safeEnd, - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE, + EnrichedSpanFlags.forSpan(span), ) } } @@ -373,7 +374,7 @@ class ParametrizedStyles( val span = EnrichedInputMentionSpan(text, indicator, attributes, view.htmlStyle) val spanEnd = start + text.length val (safeStart, safeEnd) = spannable.getSafeSpanBoundaries(start, spanEnd) - spannable.setSpan(span, safeStart, safeEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + spannable.setSpan(span, safeStart, safeEnd, EnrichedSpanFlags.forSpan(span)) val hasSpaceAtTheEnd = spannable.length > safeEnd && spannable[safeEnd] == ' ' if (!hasSpaceAtTheEnd) { diff --git a/android/src/main/java/com/swmansion/enriched/textinput/utils/EnrichedSpannable.kt b/android/src/main/java/com/swmansion/enriched/textinput/utils/EnrichedSpannable.kt index 60ebd1a14..576e10218 100644 --- a/android/src/main/java/com/swmansion/enriched/textinput/utils/EnrichedSpannable.kt +++ b/android/src/main/java/com/swmansion/enriched/textinput/utils/EnrichedSpannable.kt @@ -3,6 +3,7 @@ package com.swmansion.enriched.textinput.utils import android.text.Spannable import android.text.SpannableString import android.text.SpannableStringBuilder +import com.swmansion.enriched.common.EnrichedSpanFlags import com.swmansion.enriched.common.spans.interfaces.EnrichedBlockSpan import com.swmansion.enriched.common.spans.interfaces.EnrichedParagraphSpan import com.swmansion.enriched.common.spans.interfaces.EnrichedSpan @@ -144,7 +145,7 @@ fun Spannable.mergeSpannables( val (_, newParagraphEnd) = builder.getParagraphBounds(spanStart, pasteEnd) val flags = builder.getSpanFlags(span) builder.removeSpan(span) - builder.setSpan(span, spanStart, newParagraphEnd, flags) + builder.setSpan(span, spanStart, newParagraphEnd, EnrichedSpanFlags.forSpan(span, flags)) } } diff --git a/android/src/main/java/com/swmansion/enriched/textinput/utils/Utils.kt b/android/src/main/java/com/swmansion/enriched/textinput/utils/Utils.kt index 5f378a321..57f265b12 100644 --- a/android/src/main/java/com/swmansion/enriched/textinput/utils/Utils.kt +++ b/android/src/main/java/com/swmansion/enriched/textinput/utils/Utils.kt @@ -7,6 +7,7 @@ import android.text.Spanned import android.util.Log import android.view.MotionEvent import android.widget.TextView +import com.swmansion.enriched.common.EnrichedSpanFlags import com.swmansion.enriched.textinput.spans.EnrichedInputCheckboxListSpan import org.json.JSONObject @@ -78,7 +79,7 @@ fun TextView.setCheckboxClickListener() { // Reapply span so changes are visible without need to redraw entire TextView spannable.removeSpan(span) - spannable.setSpan(span, start, end, flags) + spannable.setSpan(span, start, end, EnrichedSpanFlags.forSpan(span, flags)) // For focused input, ensure cursor is active for affected paragraph if (tv.isFocused) {