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/10] 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 196377a0..0fcada6d 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 32733b39..4369ccb0 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 00000000..4a97c1ba --- /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 00000000..c08f1fd5 --- /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 1ed38519..6428f049 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 0cc59b72..c15df871 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 a6e84dd6..32b8b164 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 fc07d871..02b953ea 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 bc9e5875..f66e8e55 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 008f0fce..6d1a73af 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 41a3d27d..5075c551 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 f43c5322..36a39386 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 568b8e55..c4613e20 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 701d79ea..a5c163fc 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 00000000..024bf8de --- /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 fde9abfa..f125f66d 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 1f930a01..bd1ea372 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 f7205b17..32d0d6bf 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 ba31ba97..77ca3455 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 2eb29ded..9edc89e7 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 a669e412..bc4ed42c 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 4ea1742c..cbb80c88 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 439f685d..6f4fcb17 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/10] 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 eb16451c..6c9e35c9 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 c15df871..01394bd1 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 32b8b164..35754569 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 77ca3455..72cd2a55 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/10] 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 4369ccb0..48d0c7c2 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/10] 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 48d0c7c2..7539d2ce 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 4a97c1ba..7ee405f8 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 6428f049..570995dd 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 01394bd1..21cdd096 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 35754569..36819ba1 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 36a39386..1c0fac94 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/10] 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 570995dd..4b261257 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 21cdd096..f67ca44e 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 36819ba1..b57608be 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/10] 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 00000000..ae0e7964 --- /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 ce6e15e4..6b5541e7 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 024bf8de..45def651 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/10] 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 00000000..04ad337d --- /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 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 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 00000000..e304b5b9 --- /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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 z 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 literal 0 HcmV?d00001 diff --git a/apps/example/ios/Podfile.lock b/apps/example/ios/Podfile.lock index 0a2926cc..b18f8dd6 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 ae0e7964..ad428cb1 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/10] 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 476ed678..8c1f7ff9 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 b57608be..b7e06caa 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 0b8095f4..077332ad 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 cf3739e26ce186eefdc7231842f6a371b7b38e40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20=C5=BB=C3=B3=C5=82kiewski?= Date: Mon, 22 Jun 2026 08:47:07 +0200 Subject: [PATCH 09/10] fix: custom style parsing --- ios/htmlParser/HtmlParser.mm | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/ios/htmlParser/HtmlParser.mm b/ios/htmlParser/HtmlParser.mm index b7e06caa..50e2b1a6 100644 --- a/ios/htmlParser/HtmlParser.mm +++ b/ios/htmlParser/HtmlParser.mm @@ -1173,6 +1173,19 @@ + (NSString *)parseToHtmlFromRange:(NSRange)range if (![currentData isEqual:lastCustomStyleData]) { [fixedEndedStyles addObject:customType]; [stylesToBeReAdded addObject:customType]; + + // Inner styles (e.g. bold) must also close before the span ends and + // reopen inside the new span, otherwise tags cross span boundaries. + for (NSNumber *activeStyle in currentActiveStyles) { + if ([activeStyle isEqualToNumber:customType] || + [activeStyle isEqualToNumber:@([ImageStyle getType])]) { + continue; + } + if ([activeStyle integerValue] > [customType integerValue]) { + [fixedEndedStyles addObject:activeStyle]; + [stylesToBeReAdded addObject:activeStyle]; + } + } } } From 4ab75310b7d6201ab8389f9e634f44dd6d530a50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20=C5=BB=C3=B3=C5=82kiewski?= Date: Mon, 22 Jun 2026 15:15:42 +0200 Subject: [PATCH 10/10] fix: align normalizer to properly handle span with colors --- cpp/parser/GumboNormalizer.c | 38 +++++++++++++++++++ cpp/tests/GumboParserTest.cpp | 69 +++++++++++++++++++++++++++++++++++ 2 files changed, 107 insertions(+) diff --git a/cpp/parser/GumboNormalizer.c b/cpp/parser/GumboNormalizer.c index c4ece832..af434a98 100644 --- a/cpp/parser/GumboNormalizer.c +++ b/cpp/parser/GumboNormalizer.c @@ -720,9 +720,47 @@ static void walk_node(GumboNode *node, buffer_t *out) { const char *sval = get_attr(el, "style"); size_t slen = sval ? strlen(sval) : 0; css_styles_t s = parse_css_style(sval, slen); + + size_t fg_len = 0; + const char *fg = find_css_value(sval, slen, "color", &fg_len); + + size_t bg_len = 0; + const char *bg = find_css_value(sval, slen, "background-color", &bg_len); + + int has_fg = (fg && fg_len > 0); + int has_bg = (bg && bg_len > 0); + + // emit the wrapper span if colors exist + if (has_fg || has_bg) { + buffer_append_str(out, ""); + } + + // handle inner formatting (, , etc.) and children emit_styles_open(out, s); walk_children(node, out); emit_styles_close(out, s); + + // close the wrapper span + if (has_fg || has_bg) { + buffer_append_str(out, ""); + } + return; } diff --git a/cpp/tests/GumboParserTest.cpp b/cpp/tests/GumboParserTest.cpp index f45b7ec9..185c5525 100644 --- a/cpp/tests/GumboParserTest.cpp +++ b/cpp/tests/GumboParserTest.cpp @@ -253,6 +253,75 @@ TEST(GumboParserTest, SpanRemappings) { "x"), "x"); + + // Foreground color only + EXPECT_EQ( + GumboParser::normalizeHtml("x"), + "x"); + EXPECT_EQ( + GumboParser::normalizeHtml("x"), + "x"); + EXPECT_EQ( + GumboParser::normalizeHtml( + "x"), + "x"); + + // Background color only + EXPECT_EQ( + GumboParser::normalizeHtml( + "x"), + "x"); + EXPECT_EQ( + GumboParser::normalizeHtml( + "x"), + "x"); + + // Both colors combined + EXPECT_EQ( + GumboParser::normalizeHtml( + "x"), + "x"); + EXPECT_EQ( + GumboParser::normalizeHtml( + "x"), + "x"); + + // Color + Single Formatter + EXPECT_EQ( + GumboParser::normalizeHtml( + "x"), + "x"); + EXPECT_EQ( + GumboParser::normalizeHtml( + "x"), + "x"); + EXPECT_EQ( + GumboParser::normalizeHtml( + "x"), + "x"); + EXPECT_EQ( + GumboParser::normalizeHtml( + "x"), + "x"); + + // Color + Multiple inline styles + EXPECT_EQ( + GumboParser::normalizeHtml( + "x"), + "x"); + + // Mix of colors and inline styles + EXPECT_EQ( + GumboParser::normalizeHtml( + "x"), + "x"); } TEST(GumboParserTest, EnrichedTagRemappings) {