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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions ios/EnrichedTextInputView.mm
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
3 changes: 3 additions & 0 deletions ios/config/InputConfig.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
36 changes: 36 additions & 0 deletions ios/config/InputConfig.mm
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ @implementation InputConfig {
UIColor *_linkColor;
TextDecorationLineEnum _linkDecorationLine;
NSDictionary *_mentionProperties;
NSArray *_mentionStyleRules;
UIColor *_codeBlockFgColor;
CGFloat _codeBlockBorderRadius;
UIColor *_codeBlockBgColor;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand All @@ -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;
}
Expand Down
1 change: 1 addition & 0 deletions ios/interfaces/MentionStyleProps.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@
@property TextDecorationLineEnum decorationLine;
+ (NSDictionary *)getSinglePropsFromFollyDynamic:(folly::dynamic)folly;
+ (NSDictionary *)getComplexPropsFromFollyDynamic:(folly::dynamic)folly;
+ (NSArray *)getStyleRulesFromFollyDynamic:(folly::dynamic)folly;
@end
36 changes: 36 additions & 0 deletions ios/interfaces/MentionStyleProps.mm
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
6 changes: 4 additions & 2 deletions ios/styles/MentionStyle.mm
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
12 changes: 12 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string>;
style: MentionStyleProperties;
}

export interface HtmlStyle {
h1?: HeadingStyle;
h2?: HeadingStyle;
Expand Down Expand Up @@ -38,6 +49,7 @@ export interface HtmlStyle {
textDecorationLine?: 'underline' | 'none';
};
mention?: Record<string, MentionStyleProperties> | MentionStyleProperties;
mentionStyleRules?: MentionStyleRule[];
ol?: {
gapWidth?: number;
marginLeft?: number;
Expand Down
27 changes: 26 additions & 1 deletion src/utils/normalizeHtmlStyle.ts
Original file line number Diff line number Diff line change
@@ -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<HtmlStyle> = {
h1: {
Expand Down Expand Up @@ -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') {
Expand Down Expand Up @@ -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(
Expand Down