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
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.
31 changes: 22 additions & 9 deletions ios/inputAttributesManager/InputAttributesManager.mm
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,14 @@ - (void)clearRemovedTypingAttributes {
[_removedTypingAttributes removeAllObjects];
}

// Paragraph styles always go first, inline ones are ordered by their
// stylingPriority
- (NSInteger)stylingOrderFor:(StyleBase *)style {
if (style == nullptr)
return NSIntegerMax;
return [style isParagraph] ? NSIntegerMin : [style stylingPriority];
}

- (void)handleDirtyRangesStyling {
// Filter out 0 length ranges for styling.
NSPredicate *predicate = [NSPredicate
Expand Down Expand Up @@ -97,16 +105,21 @@ - (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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And I also added sorting in here by stylePriority in customStyle. And that would fix problem with inline style applied in wrong order because there inline styles are applied as last. https://github.com/software-mansion/react-native-enriched-html/pull/641/changes#diff-f0ba56d65d53cacb60b6d3b46e66fc464076119a4d5df36d92fcdd98b2a95847R98

// themselves follow their stylingPriority.
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;
}];
sortedArrayWithOptions:NSSortStable
usingComparator:^NSComparisonResult(NSNumber *a, NSNumber *b) {
NSInteger aOrder =
[self stylingOrderFor:_input->stylesDict[a]];
NSInteger bOrder =
[self stylingOrderFor:_input->stylesDict[b]];
if (aOrder == bOrder) {
return [a compare:b];
}
return aOrder < bOrder ? NSOrderedAscending
: NSOrderedDescending;
}];

// re-apply meta-attributes and apply visual styling following the saved
// occurences.
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)stylingPriority;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

- (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)stylingPriority {
return 0;
}

- (instancetype)initWithHost:(id<EnrichedViewHost>)host {
self = [super init];
_host = host;
Expand Down
26 changes: 10 additions & 16 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 @@ -17,21 +17,15 @@ - (BOOL)isParagraph {
}

- (void)applyStyling:(NSRange)range {
[self.host.textView.textStorage
enumerateAttribute:NSFontAttributeName
inRange:range
options:0
usingBlock:^(id _Nullable value, NSRange range,
BOOL *_Nonnull stop) {
UIFont *font = (UIFont *)value;
if (font != nullptr) {
UIFont *newFont = [font setItalic];
[self.host.textView.textStorage
addAttribute:NSFontAttributeName
value:newFont
range:range];
}
}];
[ItalicUtils applyItalicInTextStorage:self.host.textView.textStorage
inRange:range];
}

// some styles might apply a new font (inline code), so we need to apply
// the italic last, that way knowing if the used font supports italics
// or we need to apply a slant
- (NSInteger)stylingPriority {
return 1;
}

@end
14 changes: 13 additions & 1 deletion ios/textHtmlParser/TextHtmlParser.mm
Original file line number Diff line number Diff line change
Expand Up @@ -153,8 +153,20 @@ - (void)applyProcessedStyles:(NSArray *_Nonnull)processedStyles {
}
}

// Respect the styling priority
NSArray *sortedInlineApply = [pendingInlineApply
sortedArrayWithOptions:NSSortStable
usingComparator:^NSComparisonResult(NSArray *a, NSArray *b) {
NSInteger aPriority = [((StyleBase *)a[0]) stylingPriority];
NSInteger bPriority = [((StyleBase *)b[0]) stylingPriority];
if (aPriority == bPriority)
return NSOrderedSame;
return aPriority < bPriority ? NSOrderedAscending
: NSOrderedDescending;
}];

// 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
8 changes: 8 additions & 0 deletions ios/utils/ItalicUtils.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#import <UIKit/UIKit.h>
#pragma once

@interface ItalicUtils : NSObject

+ (void)applyItalicInTextStorage:(NSTextStorage *)textStorage
inRange:(NSRange)range;
@end
179 changes: 179 additions & 0 deletions ios/utils/ItalicUtils.mm
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
#import "ItalicUtils.h"
#import "FontExtension.h"
#import <CoreText/CoreText.h>

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

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,
};

static NSCharacterSet *NonNeutralCharacters(void) {
static NSCharacterSet *nonNeutral = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSMutableCharacterSet *set =
[[NSCharacterSet whitespaceAndNewlineCharacterSet] mutableCopy];
[set formUnionWithCharacterSet:[NSCharacterSet controlCharacterSet]];
// ZWS
[set addCharactersInString:[NSString
stringWithFormat:@"%C", (unichar)0x200B]];
nonNeutral = [[set invertedSet] copy];
});
return nonNeutral;
}

// returns YES when the font renders the given UTF-16 sequence itself
static BOOL FontCoversCharacters(UIFont *font, const unichar *chars,
CFIndex count) {
if (font == nullptr) {
return NO;
}
CGGlyph glyphs[2] = {0, 0};
return CTFontGetGlyphsForCharacters((__bridge CTFontRef)font, chars, glyphs,
count);
}

@implementation ItalicUtils

+ (void)applyItalicInTextStorage:(NSTextStorage *)textStorage
inRange:(NSRange)range {
if (textStorage == nullptr || range.length == 0 ||
NSMaxRange(range) > textStorage.length) {
return;
}

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

+ (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;
}
}

+ (ItalicKind)kindForCluster:(NSString *)cluster
font:(UIFont *)font
italicFont:(UIFont *)italicFont
hasItalicFace:(BOOL)hasItalicFace {
if ([cluster rangeOfCharacterFromSet: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 = FontCoversCharacters(font, chars, count);

// italic style is supported - we use it
if (coveredByFont && hasItalicFace &&
FontCoversCharacters(italicFont, chars, 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