Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
74e63d3
fix(ios): apply obliqueness if italics are not supported
hejsztynx Aug 5, 2026
bb2e588
feat: drop the neutral kind behavior
hejsztynx Aug 5, 2026
e443c55
chore: update e2e baseline screenshots
hejsztynx Aug 5, 2026
bd9ec44
refactor: neutral character set optimalization
hejsztynx Aug 5, 2026
e063bd5
Merge branch 'main' into @ksienkiewicz/fix-ios-cjk-italics
hejsztynx Aug 5, 2026
43e32ea
refactor: stylePriority rename
hejsztynx Aug 6, 2026
01b3185
Merge branch 'main' into @ksienkiewicz/fix-ios-cjk-italics
hejsztynx Aug 10, 2026
23961d2
refactor: less merge conflicts with customstyle
hejsztynx Aug 13, 2026
f412b2e
refactor: move logic to ItalicStyle.mm
hejsztynx Aug 13, 2026
e5705ad
refactor: extract priorityStyling sorting logic
hejsztynx Aug 13, 2026
e459c79
Merge branch 'main' into @ksienkiewicz/fix-ios-cjk-italics
hejsztynx Aug 13, 2026
71baac6
Merge branch 'main' into @ksienkiewicz/fix-ios-cjk-italics
hejsztynx Aug 20, 2026
799ee36
fix: grammar
hejsztynx Aug 21, 2026
f68c7cc
refactor: italicstyle file
hejsztynx Aug 21, 2026
b20c423
refactor: sorting array util extraction
hejsztynx Aug 21, 2026
b3f78f5
refactor: introduce ItalicUtils file
hejsztynx Aug 21, 2026
b66fb35
fix: remove unnecessary console warning
hejsztynx Aug 21, 2026
ee170d2
refactor: privatization of a constant
hejsztynx Aug 24, 2026
6c23d92
Merge branch 'main' into @ksienkiewicz/fix-ios-cjk-italics
hejsztynx Aug 24, 2026
0a64ac7
Merge branch 'main' into @ksienkiewicz/fix-ios-cjk-italics
hejsztynx Aug 25, 2026
c34015d
Merge branch 'main' into @ksienkiewicz/fix-ios-cjk-italics
hejsztynx Aug 25, 2026
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
Binary file modified .maestro/enrichedInput/screenshots/ios/inline_styles_merge.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified .maestro/enrichedInput/screenshots/ios/inline_styles_removal.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions ios/extensions/ArrayExtension.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#import <Foundation/Foundation.h>
#pragma once

@interface NSArray (ArrayExtension)
- (NSArray *)sortedArrayBySortKey:(NSInteger (^)(id item))sortKeyForItem;
@end
18 changes: 18 additions & 0 deletions ios/extensions/ArrayExtension.mm
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#import "ArrayExtension.h"

@implementation NSArray (ArrayExtension)

- (NSArray *)sortedArrayBySortKey:(NSInteger (^)(id item))sortKeyForItem {
return [self sortedArrayWithOptions:NSSortStable
usingComparator:^NSComparisonResult(id a, id b) {
NSInteger aKey = sortKeyForItem(a);
NSInteger bKey = sortKeyForItem(b);
if (aKey == bKey) {
return NSOrderedSame;
}
return aKey < bKey ? NSOrderedAscending
: NSOrderedDescending;
}];
}

@end
1 change: 1 addition & 0 deletions ios/extensions/FontExtension.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@
- (UIFont *)setItalic;
- (UIFont *)withFontTraits:(UIFont *)from;
- (UIFont *)setSize:(CGFloat)size;
- (BOOL)coversCharacters:(const unichar *)chars count:(CFIndex)count;
@end
9 changes: 7 additions & 2 deletions ios/extensions/FontExtension.mm
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#import "FontExtension.h"
#import <CoreText/CoreText.h>
#import <React/RCTLog.h>

@implementation UIFont (FontExtension)
Expand Down Expand Up @@ -40,8 +41,6 @@ - (UIFont *)setItalic {
if (fontDescriptor != nullptr) {
return [UIFont fontWithDescriptor:fontDescriptor size:0];
} else {
RCTLogWarn(
@"[EnrichedTextInput]: Couldn't apply italic trait to the font.");
Comment thread
hejsztynx marked this conversation as resolved.
return self;
}
}
Expand Down Expand Up @@ -69,4 +68,10 @@ - (UIFont *)setSize:(CGFloat)size {
}
}

- (BOOL)coversCharacters:(const unichar *)chars count:(CFIndex)count {
CGGlyph glyphs[2] = {0, 0};
return CTFontGetGlyphsForCharacters((__bridge CTFontRef)self, chars, glyphs,
count);
}

@end
14 changes: 6 additions & 8 deletions ios/inputAttributesManager/InputAttributesManager.mm
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
#import "InputAttributesManager.h"
#import "AlignmentUtils.h"
#import "ArrayExtension.h"
#import "AttributeEntry.h"
#import "EnrichedTextInputView.h"
#import "ParagraphAttributesUtils.h"
#import "RangeUtils.h"
#import "StyleHeaders.h"
#import "StyleUtils.h"
#import "ZeroWidthSpaceUtils.h"

@implementation InputAttributesManager {
Expand Down Expand Up @@ -99,15 +101,11 @@ - (void)handleDirtyRangesStyling {

// 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.
// styles override them on their specific sub-ranges. Inline styles among
Comment thread
kacperzolkiewski marked this conversation as resolved.
// themselves follow their stylePriority.
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;
sortedArrayBySortKey:^NSInteger(NSNumber *styleType) {
return [_input->stylesDict[styleType] stylePriority];
}];

// re-apply meta-attributes and apply visual styling following the saved
Expand Down
1 change: 1 addition & 0 deletions ios/interfaces/StyleBase.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
- (BOOL)isParagraph;
- (BOOL)needsZWS;
- (BOOL)appliesStylingToTyping;
- (NSInteger)stylePriority;
- (instancetype)initWithHost:(id<EnrichedViewHost>)host;
- (NSRange)actualUsedRange:(NSRange)range;
- (void)toggle:(NSRange)range;
Expand Down
5 changes: 5 additions & 0 deletions ios/interfaces/StyleBase.mm
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ - (BOOL)appliesStylingToTyping {
return NO;
}

// determines the order in which the styles are applied
- (NSInteger)stylePriority {
return [self isParagraph] ? 0 : 2;
}

- (instancetype)initWithHost:(id<EnrichedViewHost>)host {
self = [super init];
_host = host;
Expand Down
28 changes: 20 additions & 8 deletions ios/styles/ItalicStyle.mm
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#import "EnrichedTextInputView.h"
#import "FontExtension.h"
#import "ItalicUtils.h"
#import "StyleHeaders.h"

@implementation ItalicStyle : StyleBase
Expand All @@ -16,21 +16,33 @@ - (BOOL)isParagraph {
return NO;
}

// some styles might apply a new font (inline code), so we need to apply
// the italic last, that way we know if the used font supports italics
// or we need to apply a slant
Comment thread
hejsztynx marked this conversation as resolved.
- (NSInteger)stylePriority {
return 3;
}
Comment thread
hejsztynx marked this conversation as resolved.

- (void)applyStyling:(NSRange)range {
if (self.host.textView.textStorage == nullptr || range.length == 0 ||
NSMaxRange(range) > self.host.textView.textStorage.length) {
return;
}

// we process each present font
[self.host.textView.textStorage
enumerateAttribute:NSFontAttributeName
inRange:range
options:0
usingBlock:^(id _Nullable value, NSRange range,
usingBlock:^(id _Nullable value, NSRange fontRange,
BOOL *_Nonnull stop) {
UIFont *font = (UIFont *)value;
if (font != nullptr) {
UIFont *newFont = [font setItalic];
[self.host.textView.textStorage
addAttribute:NSFontAttributeName
value:newFont
range:range];
if (font == nullptr) {
return;
}
[ItalicUtils applyItalicForFont:font
inTextStorage:self.host.textView.textStorage
inRange:fontRange];
}];
}

Expand Down
9 changes: 8 additions & 1 deletion ios/textHtmlParser/TextHtmlParser.mm
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#import "TextHtmlParser.h"
#import "AlignmentEntry.h"
#import "ArrayExtension.h"
#import "EnrichedTextView.h"
#import "HtmlParser.h"
#import "LinkData.h"
Expand Down Expand Up @@ -153,8 +154,14 @@ - (void)applyProcessedStyles:(NSArray *_Nonnull)processedStyles {
}
}

// Respect the styling priority
NSArray *sortedInlineApply =
[pendingInlineApply sortedArrayBySortKey:^NSInteger(NSArray *entry) {
return [((StyleBase *)entry[0]) stylePriority];
}];

Comment thread
hejsztynx marked this conversation as resolved.
// Apply visual styling for inline styles
for (NSArray *entry in pendingInlineApply) {
for (NSArray *entry in sortedInlineApply) {
StyleBase *style = entry[0];
NSRange adjustedStyleRange = [((NSValue *)entry[1]) rangeValue];
[style applyStyling:adjustedStyleRange];
Expand Down
20 changes: 20 additions & 0 deletions ios/utils/ItalicUtils.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#import <UIKit/UIKit.h>
#pragma once

typedef NS_ENUM(NSInteger, ItalicKind) {
// character must not be slanted at all (whitespace, control characters,
// text attachments)
ItalicKindNone,
// font has a real italic glyph for the character
ItalicKindFont,
// no italic glyph available, the slant has to be used
ItalicKindOblique,
};

@interface ItalicUtils : NSObject

+ (void)applyItalicForFont:(UIFont *)font
inTextStorage:(NSTextStorage *)textStorage
inRange:(NSRange)range;

@end
151 changes: 151 additions & 0 deletions ios/utils/ItalicUtils.mm
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
#import "ItalicUtils.h"
#import "FontExtension.h"

// slant used when a font has no italic face
static const CGFloat kObliquenessFallback = 0.2;

@interface ItalicUtils ()

+ (NSCharacterSet *)nonNeutralCharacters;

+ (ItalicKind)kindForCluster:(NSString *)cluster
font:(UIFont *)font
italicFont:(UIFont *)italicFont
hasItalicFace:(BOOL)hasItalicFace;

+ (void)applyKind:(ItalicKind)kind
toSegment:(NSRange)segment
inTextStorage:(NSTextStorage *)textStorage
withItalicFont:(UIFont *)italicFont;

@end

@implementation ItalicUtils

+ (void)applyItalicForFont:(UIFont *)font
inTextStorage:(NSTextStorage *)textStorage
inRange:(NSRange)range {
UIFont *italicFont = [font setItalic];
BOOL hasItalicFace = [italicFont isItalic];

NSMutableArray<NSValue *> *clusterRanges = [NSMutableArray array];
NSMutableArray<NSNumber *> *clusterKinds = [NSMutableArray array];

// we process each composed character sequence and classify it to a specific
// ItalicKind
[textStorage.string
enumerateSubstringsInRange:range
options:NSStringEnumerationByComposedCharacterSequences
usingBlock:^(NSString *_Nullable cluster,
NSRange clusterRange, NSRange _,
BOOL *_Nonnull stop) {
if (cluster.length == 0) {
return;
}
[clusterRanges
addObject:[NSValue valueWithRange:clusterRange]];
[clusterKinds
addObject:@([self kindForCluster:cluster
font:font
italicFont:italicFont
hasItalicFace:hasItalicFace])];
}];

// merge neighbouring clusters of the same kind and apply the style
NSUInteger index = 0;
while (index < clusterKinds.count) {
NSUInteger endIndex = index + 1;
ItalicKind kind = (ItalicKind)[clusterKinds[index] integerValue];
while (endIndex < clusterKinds.count &&
(ItalicKind)[clusterKinds[endIndex] integerValue] == kind) {
endIndex += 1;
}

NSRange startRange = [clusterRanges[index] rangeValue];
NSRange endRange = [clusterRanges[endIndex - 1] rangeValue];
NSRange segment = NSMakeRange(startRange.location,
NSMaxRange(endRange) - startRange.location);

[self applyKind:kind
toSegment:segment
inTextStorage:textStorage
withItalicFont:italicFont];

index = endIndex;
}
}

// contains all characters except whitespaces, newlines,
// control characters and ZWS
+ (NSCharacterSet *)nonNeutralCharacters {
static NSCharacterSet *nonNeutral = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSMutableCharacterSet *set =
[[NSCharacterSet whitespaceAndNewlineCharacterSet] mutableCopy];
[set formUnionWithCharacterSet:[NSCharacterSet controlCharacterSet]];
[set addCharactersInString:[NSString
stringWithFormat:@"%C", (unichar)0x200B]];
nonNeutral = [[set invertedSet] copy];
});
return nonNeutral;
}

+ (ItalicKind)kindForCluster:(NSString *)cluster
font:(UIFont *)font
italicFont:(UIFont *)italicFont
hasItalicFace:(BOOL)hasItalicFace {
if ([cluster rangeOfCharacterFromSet:[self nonNeutralCharacters]].location ==
NSNotFound) {
return ItalicKindNone;
}

// we just need to analyze the first unicode character to classify the whole
// cluster
unichar chars[2] = {0, 0};
CFIndex count = 1;
chars[0] = [cluster characterAtIndex:0];
if (CFStringIsSurrogateHighCharacter(chars[0]) && cluster.length > 1) {
chars[1] = [cluster characterAtIndex:1];
count = 2;
}

if (chars[0] == (unichar)NSAttachmentCharacter) {
return ItalicKindNone;
}

BOOL coveredByFont = [font coversCharacters:chars count:count];

// italic style is supported - we use it
if (coveredByFont && hasItalicFace && italicFont != nullptr &&
[italicFont coversCharacters:chars count:count]) {
return ItalicKindFont;
}

// italic is not supported, we use the slant instead
return ItalicKindOblique;
}

+ (void)applyKind:(ItalicKind)kind
toSegment:(NSRange)segment
inTextStorage:(NSTextStorage *)textStorage
withItalicFont:(UIFont *)italicFont {
switch (kind) {
case ItalicKindFont:
[textStorage addAttribute:NSFontAttributeName
value:italicFont
range:segment];
[textStorage removeAttribute:NSObliquenessAttributeName range:segment];
break;
case ItalicKindOblique:
[textStorage addAttribute:NSObliquenessAttributeName
value:@(kObliquenessFallback)
range:segment];
break;
case ItalicKindNone:
[textStorage removeAttribute:NSObliquenessAttributeName range:segment];
break;
}
}

@end
Loading