diff --git a/ios/EnrichedTextInputView.mm b/ios/EnrichedTextInputView.mm index 5e81c9f2..ff634d77 100644 --- a/ios/EnrichedTextInputView.mm +++ b/ios/EnrichedTextInputView.mm @@ -698,6 +698,11 @@ - (void)updateProps:(Props::Shared const &)props getComplexPropsFromFollyDynamic:newMentionStyle]]; } + // Parse mentionStyleRules (embedded as __rules__ in the mention dict) + [newConfig setMentionStyleRules: + [MentionStyleProps + getStyleRulesFromFollyDynamic:newMentionStyle]]; + stylePropChanged = YES; } diff --git a/ios/config/InputConfig.h b/ios/config/InputConfig.h index 73685213..ea5dcb88 100644 --- a/ios/config/InputConfig.h +++ b/ios/config/InputConfig.h @@ -79,7 +79,10 @@ - (TextDecorationLineEnum)linkDecorationLine; - (void)setLinkDecorationLine:(TextDecorationLineEnum)newValue; - (void)setMentionStyleProps:(NSDictionary *)newValue; +- (void)setMentionStyleRules:(NSArray *)newValue; - (MentionStyleProps *)mentionStylePropsForIndicator:(NSString *)indicator; +- (MentionStyleProps *)mentionStylePropsForIndicator:(NSString *)indicator + attributes:(NSString *)attributes; - (UIColor *)codeBlockFgColor; - (void)setCodeBlockFgColor:(UIColor *)newValue; - (UIColor *)codeBlockBgColor; diff --git a/ios/config/InputConfig.mm b/ios/config/InputConfig.mm index 6bc55d7f..5417f30f 100644 --- a/ios/config/InputConfig.mm +++ b/ios/config/InputConfig.mm @@ -43,6 +43,7 @@ @implementation InputConfig { UIColor *_linkColor; TextDecorationLineEnum _linkDecorationLine; NSDictionary *_mentionProperties; + NSArray *_mentionStyleRules; UIColor *_codeBlockFgColor; CGFloat _codeBlockBorderRadius; UIColor *_codeBlockBgColor; @@ -104,6 +105,7 @@ - (id)copyWithZone:(NSZone *)zone { copy->_linkColor = [_linkColor copy]; copy->_linkDecorationLine = [_linkDecorationLine copy]; copy->_mentionProperties = [_mentionProperties mutableCopy]; + copy->_mentionStyleRules = [_mentionStyleRules copy]; copy->_codeBlockFgColor = [_codeBlockFgColor copy]; copy->_codeBlockBgColor = [_codeBlockBgColor copy]; copy->_codeBlockBorderRadius = _codeBlockBorderRadius; @@ -468,6 +470,10 @@ - (void)setMentionStyleProps:(NSDictionary *)newValue { _mentionProperties = [newValue mutableCopy]; } +- (void)setMentionStyleRules:(NSArray *)newValue { + _mentionStyleRules = [newValue copy]; +} + - (MentionStyleProps *)mentionStylePropsForIndicator:(NSString *)indicator { if (_mentionProperties.count == 1 && _mentionProperties[@"all"] != nullptr) { // single props for all the indicators @@ -482,6 +488,36 @@ - (MentionStyleProps *)mentionStylePropsForIndicator:(NSString *)indicator { return fallbackProps; } +- (MentionStyleProps *)mentionStylePropsForIndicator:(NSString *)indicator + attributes:(NSString *)attributes { + // Check mentionStyleRules first — first matching rule wins + if (_mentionStyleRules.count > 0 && attributes.length > 0) { + NSData *data = [attributes dataUsingEncoding:NSUTF8StringEncoding]; + NSDictionary *attrDict = + [NSJSONSerialization JSONObjectWithData:data options:0 error:nil]; + + if (attrDict) { + for (NSDictionary *rule in _mentionStyleRules) { + NSDictionary *match = rule[@"match"]; + MentionStyleProps *style = rule[@"style"]; + if (!match || !style) continue; + + BOOL allMatch = YES; + for (NSString *key in match) { + if (![attrDict[key] isEqualToString:match[key]]) { + allMatch = NO; + break; + } + } + if (allMatch) return style; + } + } + } + + // Fall back to indicator-based lookup + return [self mentionStylePropsForIndicator:indicator]; +} + - (UIColor *)codeBlockFgColor { return _codeBlockFgColor; } diff --git a/ios/interfaces/MentionStyleProps.h b/ios/interfaces/MentionStyleProps.h index d0d59dd5..df9c7bd3 100644 --- a/ios/interfaces/MentionStyleProps.h +++ b/ios/interfaces/MentionStyleProps.h @@ -10,4 +10,5 @@ @property TextDecorationLineEnum decorationLine; + (NSDictionary *)getSinglePropsFromFollyDynamic:(folly::dynamic)folly; + (NSDictionary *)getComplexPropsFromFollyDynamic:(folly::dynamic)folly; ++ (NSArray *)getStyleRulesFromFollyDynamic:(folly::dynamic)folly; @end diff --git a/ios/interfaces/MentionStyleProps.mm b/ios/interfaces/MentionStyleProps.mm index 5ea23538..34b7a920 100644 --- a/ios/interfaces/MentionStyleProps.mm +++ b/ios/interfaces/MentionStyleProps.mm @@ -51,6 +51,7 @@ + (NSDictionary *)getComplexPropsFromFollyDynamic:(folly::dynamic)folly { for (const auto &obj : folly.items()) { if (obj.first.isString() && obj.second.isObject()) { std::string key = obj.first.asString(); + if (key == "__rules__") continue; // handled by getStyleRulesFromFollyDynamic MentionStyleProps *props = [MentionStyleProps getSingleMentionStylePropsFromFollyDynamic:obj.second]; dict[[NSString fromCppString:key]] = props; @@ -60,4 +61,39 @@ + (NSDictionary *)getComplexPropsFromFollyDynamic:(folly::dynamic)folly { return dict; } ++ (NSArray *)getStyleRulesFromFollyDynamic:(folly::dynamic)folly { + if (!folly.count("__rules__") || !folly["__rules__"].isArray()) { + return @[]; + } + + NSMutableArray *rules = [[NSMutableArray alloc] init]; + for (const auto &ruleObj : folly["__rules__"]) { + if (!ruleObj.isObject()) continue; + + // Parse match dict + NSMutableDictionary *match = [[NSMutableDictionary alloc] init]; + if (ruleObj.count("match") && ruleObj["match"].isObject()) { + for (const auto &kv : ruleObj["match"].items()) { + if (kv.first.isString() && kv.second.isString()) { + match[[NSString fromCppString:kv.first.asString()]] = + [NSString fromCppString:kv.second.asString()]; + } + } + } + + // Parse style + MentionStyleProps *style = nil; + if (ruleObj.count("style") && ruleObj["style"].isObject()) { + style = [MentionStyleProps + getSingleMentionStylePropsFromFollyDynamic:ruleObj["style"]]; + } + + if (match.count > 0 && style != nil) { + [rules addObject:@{@"match" : match, @"style" : style}]; + } + } + + return rules; +} + @end diff --git a/ios/styles/MentionStyle.mm b/ios/styles/MentionStyle.mm index 5d10772e..d52fe579 100644 --- a/ios/styles/MentionStyle.mm +++ b/ios/styles/MentionStyle.mm @@ -191,7 +191,8 @@ - (void)addMention:(NSString *)indicator params.attributes = attributes; MentionStyleProps *styleProps = - [_input->config mentionStylePropsForIndicator:indicator]; + [_input->config mentionStylePropsForIndicator:indicator + attributes:attributes]; NSMutableDictionary *newAttrs = [@{ MentionAttributeName : params, @@ -231,7 +232,8 @@ - (void)addMentionAtRange:(NSRange)range params:(MentionParams *)params { _blockMentionEditing = YES; MentionStyleProps *styleProps = - [_input->config mentionStylePropsForIndicator:params.indicator]; + [_input->config mentionStylePropsForIndicator:params.indicator + attributes:params.attributes]; NSMutableDictionary *newAttrs = [@{ MentionAttributeName : params, diff --git a/src/types.ts b/src/types.ts index b7f9591b..359fcca9 100644 --- a/src/types.ts +++ b/src/types.ts @@ -11,6 +11,17 @@ export interface MentionStyleProperties { textDecorationLine?: 'underline' | 'none'; } +/** + * Attribute-based style override for mentions. + * When a mention's attributes contain all key-value pairs in `match`, + * `style` is applied instead of the indicator-based default. + * First matching rule wins. + */ +export interface MentionStyleRule { + match: Record; + style: MentionStyleProperties; +} + export interface HtmlStyle { h1?: HeadingStyle; h2?: HeadingStyle; @@ -38,6 +49,7 @@ export interface HtmlStyle { textDecorationLine?: 'underline' | 'none'; }; mention?: Record | MentionStyleProperties; + mentionStyleRules?: MentionStyleRule[]; ol?: { gapWidth?: number; marginLeft?: number; diff --git a/src/utils/normalizeHtmlStyle.ts b/src/utils/normalizeHtmlStyle.ts index 3392d080..0d233d6c 100644 --- a/src/utils/normalizeHtmlStyle.ts +++ b/src/utils/normalizeHtmlStyle.ts @@ -1,6 +1,6 @@ import { type ColorValue, processColor } from 'react-native'; import type { HtmlStyleInternal } from '../spec/EnrichedTextInputNativeComponent'; -import type { HtmlStyle, MentionStyleProperties } from '../types'; +import type { HtmlStyle, MentionStyleProperties, MentionStyleRule } from '../types'; const defaultStyle: Required = { h1: { @@ -108,6 +108,20 @@ const convertToHtmlStyleInternal = ( }; }); + // Embed mentionStyleRules as __rules__ in the mention dict so they flow + // to native through the existing htmlStyle.mention prop (UnsafeMixed). + if (style.mentionStyleRules && style.mentionStyleRules.length > 0) { + (mentionStyles as any).__rules__ = style.mentionStyleRules.map( + (rule: MentionStyleRule) => ({ + match: rule.match, + style: { + ...defaultStyle.mention, + ...rule.style, + }, + }) + ); + } + let markerFontWeight: string | undefined; if (style.ol?.markerFontWeight) { if (typeof style.ol?.markerFontWeight === 'number') { @@ -166,6 +180,17 @@ const parseColors = (style: HtmlStyleInternal): HtmlStyleInternal => { if (tagName === 'mention') { for (const [indicator, mentionStyle] of Object.entries(tagStyle)) { + // Handle __rules__ array separately from indicator style objects + if (indicator === '__rules__' && Array.isArray(mentionStyle)) { + tagStyles.__rules__ = (mentionStyle as any[]).map((rule) => ({ + match: rule.match, + style: Object.fromEntries( + Object.entries(rule.style).map(([k, v]) => [k, parseStyle(k, v)]) + ), + })); + continue; + } + tagStyles[indicator] = {}; for (const [styleName, styleValue] of Object.entries(