From 68d2076616822641457c18efd6c96b923f5917c2 Mon Sep 17 00:00:00 2001 From: Toni Sevener Date: Fri, 8 Dec 2023 16:11:07 -0600 Subject: [PATCH 01/18] Fix indentions and add editor heading fonts --- .../Sources/Components/Style/WKFont.swift | 130 ++++++++++-------- 1 file changed, 73 insertions(+), 57 deletions(-) diff --git a/Components/Sources/Components/Style/WKFont.swift b/Components/Sources/Components/Style/WKFont.swift index 1314ee54e95..fd8b50b5d1b 100644 --- a/Components/Sources/Components/Style/WKFont.swift +++ b/Components/Sources/Components/Style/WKFont.swift @@ -6,69 +6,85 @@ public enum WKFont { case headline case title case boldTitle - case body + case body case boldBody case italicsBody case boldItalicsBody - case smallBody - case callout - case subheadline - case boldSubheadline + case smallBody + case callout + case subheadline + case boldSubheadline case mediumSubheadline case caption1 case footnote - case boldFootnote + case boldFootnote + case editorHeading + case editorSubheading1 + case editorSubheading2 + case editorSubheading3 + case editorSubheading4 - static func `for`(_ font: WKFont, compatibleWith traitCollection: UITraitCollection = WKAppEnvironment.current.traitCollection) -> UIFont { - switch font { - case .headline: - return UIFont.preferredFont(forTextStyle: .headline, compatibleWith: traitCollection) - case .title: - return UIFont.preferredFont(forTextStyle: .title1, compatibleWith: traitCollection) - case .boldTitle: - guard let descriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: .title1, compatibleWith: traitCollection).withSymbolicTraits(.traitBold) else { - fatalError() - } - return UIFont(descriptor: descriptor, size: 0) - case .body: - return UIFont.preferredFont(forTextStyle: .body, compatibleWith: traitCollection) - case .boldBody: - guard let descriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: .body, compatibleWith: traitCollection).withSymbolicTraits(.traitBold) else { - fatalError() - } - return UIFont(descriptor: descriptor, size: 0) - case .italicsBody: - guard let descriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: .body, compatibleWith: traitCollection).withSymbolicTraits(.traitItalic) else { - fatalError() - } - return UIFont(descriptor: descriptor, size: 0) - case .boldItalicsBody: - guard let descriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: .body, compatibleWith: traitCollection).withSymbolicTraits(.traitBold.union(.traitItalic)) else { - fatalError() - } - return UIFont(descriptor: descriptor, size: 0) - case .smallBody: - return UIFontMetrics(forTextStyle: .body).scaledFont(for: UIFont.systemFont(ofSize: 15, weight: .regular)) - case .callout: - return UIFont.preferredFont(forTextStyle: .callout, compatibleWith: traitCollection) - case .subheadline: - return UIFont.preferredFont(forTextStyle: .subheadline, compatibleWith: traitCollection) - case .mediumSubheadline: - return UIFontMetrics(forTextStyle: .subheadline).scaledFont(for: UIFont.systemFont(ofSize: 15, weight: .medium)) - case .boldSubheadline: - guard let descriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: .subheadline, compatibleWith: traitCollection).withSymbolicTraits(.traitBold) else { - fatalError() - } - return UIFont(descriptor: descriptor, size: 0) - case .caption1: - return UIFont.preferredFont(forTextStyle: .caption1, compatibleWith: traitCollection) - case .footnote: - return UIFont.preferredFont(forTextStyle: .footnote, compatibleWith: traitCollection) - case .boldFootnote: - guard let descriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: .footnote, compatibleWith: traitCollection).withSymbolicTraits(.traitBold) else { - fatalError() - } - return UIFont(descriptor: descriptor, size: 0) - } + static func `for`(_ font: WKFont, compatibleWith traitCollection: UITraitCollection = WKAppEnvironment.current.traitCollection) -> UIFont { + switch font { + case .headline: + return UIFont.preferredFont(forTextStyle: .headline, compatibleWith: traitCollection) + case .title: + return UIFont.preferredFont(forTextStyle: .title1, compatibleWith: traitCollection) + case .boldTitle: + guard let descriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: .title1, compatibleWith: traitCollection).withSymbolicTraits(.traitBold) else { + fatalError() + } + return UIFont(descriptor: descriptor, size: 0) + case .body: + return UIFont.preferredFont(forTextStyle: .body, compatibleWith: traitCollection) + case .boldBody: + guard let descriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: .body, compatibleWith: traitCollection).withSymbolicTraits(.traitBold) else { + fatalError() + } + return UIFont(descriptor: descriptor, size: 0) + case .italicsBody: + guard let descriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: .body, compatibleWith: traitCollection).withSymbolicTraits(.traitItalic) else { + fatalError() + } + return UIFont(descriptor: descriptor, size: 0) + case .boldItalicsBody: + guard let descriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: .body, compatibleWith: traitCollection).withSymbolicTraits(.traitBold.union(.traitItalic)) else { + fatalError() + } + return UIFont(descriptor: descriptor, size: 0) + case .smallBody: + return UIFontMetrics(forTextStyle: .body).scaledFont(for: UIFont.systemFont(ofSize: 15, weight: .regular)) + case .callout: + return UIFont.preferredFont(forTextStyle: .callout, compatibleWith: traitCollection) + case .subheadline: + return UIFont.preferredFont(forTextStyle: .subheadline, compatibleWith: traitCollection) + case .mediumSubheadline: + return UIFontMetrics(forTextStyle: .subheadline).scaledFont(for: UIFont.systemFont(ofSize: 15, weight: .medium)) + case .boldSubheadline: + guard let descriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: .subheadline, compatibleWith: traitCollection).withSymbolicTraits(.traitBold) else { + fatalError() + } + return UIFont(descriptor: descriptor, size: 0) + case .caption1: + return UIFont.preferredFont(forTextStyle: .caption1, compatibleWith: traitCollection) + case .footnote: + return UIFont.preferredFont(forTextStyle: .footnote, compatibleWith: traitCollection) + case .boldFootnote: + guard let descriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: .footnote, compatibleWith: traitCollection).withSymbolicTraits(.traitBold) else { + fatalError() + } + return UIFont(descriptor: descriptor, size: 0) + case .editorHeading: + return UIFontMetrics(forTextStyle: .headline).scaledFont(for: UIFont.systemFont(ofSize: 28, weight: .semibold), maximumPointSize: 32, compatibleWith: traitCollection) + case .editorSubheading1: + return UIFontMetrics(forTextStyle: .headline).scaledFont(for: UIFont.systemFont(ofSize: 26, weight: .semibold), compatibleWith: traitCollection) + case .editorSubheading2: + return UIFontMetrics(forTextStyle: .headline).scaledFont(for: UIFont.systemFont(ofSize: 24, weight: .semibold), compatibleWith: traitCollection) + case .editorSubheading3: + return UIFontMetrics(forTextStyle: .headline).scaledFont(for: UIFont.systemFont(ofSize: 22, weight: .semibold), compatibleWith: traitCollection) + case .editorSubheading4: + return UIFontMetrics(forTextStyle: .headline).scaledFont(for: UIFont.systemFont(ofSize: 20, weight: .semibold), compatibleWith: traitCollection) + } + } } From fcb0e6a2bec66ac7c257498e8b1563fc24c5637d Mon Sep 17 00:00:00 2001 From: Toni Sevener Date: Fri, 8 Dec 2023 16:13:03 -0600 Subject: [PATCH 02/18] Add new fonts to WKSourceEditorFonts --- .../Source Editor/WKSourceEditorTextFrameworkMediator.swift | 5 +++++ Components/Sources/ComponentsObjC/WKSourceEditorFonts.h | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/Components/Sources/Components/Components/Editors/Source Editor/WKSourceEditorTextFrameworkMediator.swift b/Components/Sources/Components/Components/Editors/Source Editor/WKSourceEditorTextFrameworkMediator.swift index c414a31bee6..d701a037b4f 100644 --- a/Components/Sources/Components/Components/Editors/Source Editor/WKSourceEditorTextFrameworkMediator.swift +++ b/Components/Sources/Components/Components/Editors/Source Editor/WKSourceEditorTextFrameworkMediator.swift @@ -213,6 +213,11 @@ extension WKSourceEditorTextFrameworkMediator: WKSourceEditorStorageDelegate { fonts.boldFont = isSyntaxHighlightingEnabled ? WKFont.for(.boldBody, compatibleWith: traitCollection) : baseFont fonts.italicsFont = isSyntaxHighlightingEnabled ? WKFont.for(.italicsBody, compatibleWith: traitCollection) : baseFont fonts.boldItalicsFont = isSyntaxHighlightingEnabled ? WKFont.for(.boldItalicsBody, compatibleWith: traitCollection) : baseFont + fonts.headingFont = isSyntaxHighlightingEnabled ? WKFont.for(.editorHeading, compatibleWith: traitCollection) : baseFont + fonts.subheading1Font = isSyntaxHighlightingEnabled ? WKFont.for(.editorSubheading1, compatibleWith: traitCollection) : baseFont + fonts.subheading2Font = isSyntaxHighlightingEnabled ? WKFont.for(.editorSubheading2, compatibleWith: traitCollection) : baseFont + fonts.subheading3Font = isSyntaxHighlightingEnabled ? WKFont.for(.editorSubheading3, compatibleWith: traitCollection) : baseFont + fonts.subheading4Font = isSyntaxHighlightingEnabled ? WKFont.for(.editorSubheading4, compatibleWith: traitCollection) : baseFont return fonts } } diff --git a/Components/Sources/ComponentsObjC/WKSourceEditorFonts.h b/Components/Sources/ComponentsObjC/WKSourceEditorFonts.h index e7be6dd21f7..a5bdf06e53f 100644 --- a/Components/Sources/ComponentsObjC/WKSourceEditorFonts.h +++ b/Components/Sources/ComponentsObjC/WKSourceEditorFonts.h @@ -7,6 +7,11 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, strong) UIFont *boldFont; @property (nonatomic, strong) UIFont *italicsFont; @property (nonatomic, strong) UIFont *boldItalicsFont; +@property (nonatomic, strong) UIFont *headingFont; +@property (nonatomic, strong) UIFont *subheading1Font; +@property (nonatomic, strong) UIFont *subheading2Font; +@property (nonatomic, strong) UIFont *subheading3Font; +@property (nonatomic, strong) UIFont *subheading4Font; @end NS_ASSUME_NONNULL_END From a6bbd8c29de1fd39cbba2dcfff46eb33c968d391 Mon Sep 17 00:00:00 2001 From: Toni Sevener Date: Fri, 8 Dec 2023 16:13:31 -0600 Subject: [PATCH 03/18] Move common custom orange key to superclass --- Components/Sources/ComponentsObjC/WKSourceEditorFormatter.h | 3 +++ Components/Sources/ComponentsObjC/WKSourceEditorFormatter.m | 5 +++++ .../ComponentsObjC/WKSourceEditorFormatterBoldItalics.m | 3 --- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/Components/Sources/ComponentsObjC/WKSourceEditorFormatter.h b/Components/Sources/ComponentsObjC/WKSourceEditorFormatter.h index f3cb213f291..e6ff6d9cf6f 100644 --- a/Components/Sources/ComponentsObjC/WKSourceEditorFormatter.h +++ b/Components/Sources/ComponentsObjC/WKSourceEditorFormatter.h @@ -4,6 +4,9 @@ NS_ASSUME_NONNULL_BEGIN @interface WKSourceEditorFormatter : NSObject + +extern NSString *const WKSourceEditorCustomKeyColorOrange; + - (instancetype)initWithColors:(nonnull WKSourceEditorColors *)colors fonts:(nonnull WKSourceEditorFonts *)fonts; - (void)addSyntaxHighlightingToAttributedString:(NSMutableAttributedString *)attributedString inRange:(NSRange)range; diff --git a/Components/Sources/ComponentsObjC/WKSourceEditorFormatter.m b/Components/Sources/ComponentsObjC/WKSourceEditorFormatter.m index 5c4fb125102..7d69fde7dd5 100644 --- a/Components/Sources/ComponentsObjC/WKSourceEditorFormatter.m +++ b/Components/Sources/ComponentsObjC/WKSourceEditorFormatter.m @@ -3,6 +3,11 @@ #import "WKSourceEditorFonts.h" @implementation WKSourceEditorFormatter + +#pragma mark - Common Custom Attributed String Keys +// Font and Color custom attributes allow us to easily target already-formatted ranges. This is handy for speedy updates upon theme and text size change, as well as determining keyboard button selection states. +NSString * const WKSourceEditorCustomKeyColorOrange = @"WKSourceEditorKeyColorOrange"; + - (nonnull instancetype)initWithColors:(nonnull WKSourceEditorColors *)colors fonts:(nonnull WKSourceEditorFonts *)fonts { self = [super init]; return self; diff --git a/Components/Sources/ComponentsObjC/WKSourceEditorFormatterBoldItalics.m b/Components/Sources/ComponentsObjC/WKSourceEditorFormatterBoldItalics.m index 763e2947cd0..aeff59f7baf 100644 --- a/Components/Sources/ComponentsObjC/WKSourceEditorFormatterBoldItalics.m +++ b/Components/Sources/ComponentsObjC/WKSourceEditorFormatterBoldItalics.m @@ -18,9 +18,6 @@ @interface WKSourceEditorFormatterBoldItalics () @implementation WKSourceEditorFormatterBoldItalics #pragma mark - Custom Attributed String Keys - -// Font and Color custom attributes allow us to easily target already-formatted ranges. This is handy for speedy updates upon theme and text size change, as well as determining keyboard button selection states. -NSString * const WKSourceEditorCustomKeyColorOrange = @"WKSourceEditorKeyColorOrange"; NSString * const WKSourceEditorCustomKeyFontBoldItalics = @"WKSourceEditorKeyFontBoldItalics"; NSString * const WKSourceEditorCustomKeyFontBold = @"WKSourceEditorKeyFontBold"; NSString * const WKSourceEditorCustomKeyFontItalics = @"WKSourceEditorKeyFontItalics"; From 91e53f925ab7f57038aecc1efd5e969a5f21ede4 Mon Sep 17 00:00:00 2001 From: Toni Sevener Date: Fri, 8 Dec 2023 16:14:03 -0600 Subject: [PATCH 04/18] Add new WKSourceEditorFormatterHeading for syntax highlighting --- .../WKSourceEditorFormatterHeading.h | 9 + .../WKSourceEditorFormatterHeading.m | 236 ++++++++++++++++++ .../ComponentsObjC/include/ComponentsObjC.h | 1 + .../include/WKSourceEditorFormatterHeading.h | 1 + 4 files changed, 247 insertions(+) create mode 100644 Components/Sources/ComponentsObjC/WKSourceEditorFormatterHeading.h create mode 100644 Components/Sources/ComponentsObjC/WKSourceEditorFormatterHeading.m create mode 120000 Components/Sources/ComponentsObjC/include/WKSourceEditorFormatterHeading.h diff --git a/Components/Sources/ComponentsObjC/WKSourceEditorFormatterHeading.h b/Components/Sources/ComponentsObjC/WKSourceEditorFormatterHeading.h new file mode 100644 index 00000000000..72a8c6eed35 --- /dev/null +++ b/Components/Sources/ComponentsObjC/WKSourceEditorFormatterHeading.h @@ -0,0 +1,9 @@ +#import "WKSourceEditorFormatter.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface WKSourceEditorFormatterHeading : WKSourceEditorFormatter + +@end + +NS_ASSUME_NONNULL_END diff --git a/Components/Sources/ComponentsObjC/WKSourceEditorFormatterHeading.m b/Components/Sources/ComponentsObjC/WKSourceEditorFormatterHeading.m new file mode 100644 index 00000000000..1d8e6a0febf --- /dev/null +++ b/Components/Sources/ComponentsObjC/WKSourceEditorFormatterHeading.m @@ -0,0 +1,236 @@ +#import "WKSourceEditorFormatterHeading.h" +#import "WKSourceEditorColors.h" +#import "WKSourceEditorFonts.h" + +@interface WKSourceEditorFormatterHeading () +@property (nonatomic, strong) NSDictionary *headingFontAttributes; +@property (nonatomic, strong) NSDictionary *subheading1FontAttributes; +@property (nonatomic, strong) NSDictionary *subheading2FontAttributes; +@property (nonatomic, strong) NSDictionary *subheading3FontAttributes; +@property (nonatomic, strong) NSDictionary *subheading4FontAttributes; +@property (nonatomic, strong) NSDictionary *orangeAttributes; + +@property (nonatomic, strong) NSRegularExpression *headingRegex; +@property (nonatomic, strong) NSRegularExpression *subheading1Regex; +@property (nonatomic, strong) NSRegularExpression *subheading2Regex; +@property (nonatomic, strong) NSRegularExpression *subheading3Regex; +@property (nonatomic, strong) NSRegularExpression *subheading4Regex; +@end + +@implementation WKSourceEditorFormatterHeading + +#pragma mark - Custom Attributed String Keys + +NSString * const WKSourceEditorCustomKeyFontHeading = @"WKSourceEditorCustomKeyFontHeading"; +NSString * const WKSourceEditorCustomKeyFontSubheading1 = @"WKSourceEditorCustomKeyFontSubheading1"; +NSString * const WKSourceEditorCustomKeyFontSubheading2 = @"WKSourceEditorCustomKeyFontSubheading2"; +NSString * const WKSourceEditorCustomKeyFontSubheading3 = @"WKSourceEditorCustomKeyFontSubheading3"; +NSString * const WKSourceEditorCustomKeyFontSubheading4 = @"WKSourceEditorCustomKeyFontSubheading4"; + +#pragma mark - Public + +- (instancetype)initWithColors:(WKSourceEditorColors *)colors fonts:(WKSourceEditorFonts *)fonts { + self = [super initWithColors:colors fonts:fonts]; + if (self) { + _orangeAttributes = @{ + NSForegroundColorAttributeName: colors.orangeForegroundColor, + WKSourceEditorCustomKeyColorOrange: [NSNumber numberWithBool:YES] + }; + + _headingFontAttributes = @{ + NSFontAttributeName: fonts.headingFont, + WKSourceEditorCustomKeyFontSubheading1: [NSNumber numberWithBool:YES] + }; + + _subheading1FontAttributes = @{ + NSFontAttributeName: fonts.subheading1Font, + WKSourceEditorCustomKeyFontSubheading2: [NSNumber numberWithBool:YES] + }; + + _subheading2FontAttributes = @{ + NSFontAttributeName: fonts.subheading2Font, + WKSourceEditorCustomKeyFontSubheading3: [NSNumber numberWithBool:YES] + }; + + _subheading3FontAttributes = @{ + NSFontAttributeName: fonts.subheading3Font, + WKSourceEditorCustomKeyFontSubheading4: [NSNumber numberWithBool:YES] + }; + + _subheading4FontAttributes = @{ + NSFontAttributeName: fonts.subheading4Font, + WKSourceEditorCustomKeyFontSubheading4: [NSNumber numberWithBool:YES] + }; + + _headingRegex = [[NSRegularExpression alloc] initWithPattern:@"^(={2})([^=]*)(={2})(?!=)$" options:NSRegularExpressionAnchorsMatchLines error:nil]; + _subheading1Regex = [[NSRegularExpression alloc] initWithPattern:@"^(={3})([^=]*)(={3})(?!=)$" options:NSRegularExpressionAnchorsMatchLines error:nil]; + _subheading2Regex = [[NSRegularExpression alloc] initWithPattern:@"^(={4})([^=]*)(={4})(?!=)$" options:NSRegularExpressionAnchorsMatchLines error:nil]; + _subheading3Regex = [[NSRegularExpression alloc] initWithPattern:@"^(={5})([^=]*)(={5})(?!=)$" options:NSRegularExpressionAnchorsMatchLines error:nil]; + _subheading4Regex = [[NSRegularExpression alloc] initWithPattern:@"^(={6})([^=]*)(={6})(?!=)$" options:NSRegularExpressionAnchorsMatchLines error:nil]; + } + return self; +} + +#pragma mark - Overrides + +- (void)addSyntaxHighlightingToAttributedString:(nonnull NSMutableAttributedString *)attributedString inRange:(NSRange)range { + + // Reset + [attributedString removeAttribute:WKSourceEditorCustomKeyColorOrange range:range]; + [attributedString removeAttribute:WKSourceEditorCustomKeyFontHeading range:range]; + [attributedString removeAttribute:WKSourceEditorCustomKeyFontSubheading1 range:range]; + [attributedString removeAttribute:WKSourceEditorCustomKeyFontSubheading2 range:range]; + [attributedString removeAttribute:WKSourceEditorCustomKeyFontSubheading3 range:range]; + [attributedString removeAttribute:WKSourceEditorCustomKeyFontSubheading4 range:range]; + + [self enumerateAndHighlightAttributedString:attributedString range:range regex:self.headingRegex attributes:self.headingFontAttributes]; + [self enumerateAndHighlightAttributedString:attributedString range:range regex:self.subheading1Regex attributes:self.subheading1FontAttributes]; + [self enumerateAndHighlightAttributedString:attributedString range:range regex:self.subheading2Regex attributes:self.subheading2FontAttributes]; + [self enumerateAndHighlightAttributedString:attributedString range:range regex:self.subheading3Regex attributes:self.subheading3FontAttributes]; + [self enumerateAndHighlightAttributedString:attributedString range:range regex:self.subheading4Regex attributes:self.subheading4FontAttributes]; +} + +- (void)updateColors:(WKSourceEditorColors *)colors inAttributedString:(NSMutableAttributedString *)attributedString inRange:(NSRange)range { + + [self updateColorAttributesWithColors:colors]; + [self enumerateAndUpdateColorsInAttributedString:attributedString range:range]; +} + +- (void)updateFonts:(WKSourceEditorFonts *)fonts inAttributedString:(NSMutableAttributedString *)attributedString inRange:(NSRange)range { + + [self updateFontAttributesWithFonts:fonts]; + [self enumerateAndUpdateFontsInAttributedString:attributedString range:range]; +} + +#pragma mark - Private + +- (void)enumerateAndHighlightAttributedString: (nonnull NSMutableAttributedString *)attributedString range:(NSRange)range regex:(NSRegularExpression *)regex attributes:(NSDictionary *)fontAttributes { + + [regex enumerateMatchesInString:attributedString.string + options:0 + range:range + usingBlock:^(NSTextCheckingResult *_Nullable result, NSMatchingFlags flags, BOOL *_Nonnull stop) { + NSRange fullMatch = [result rangeAtIndex:0]; + NSRange openingRange = [result rangeAtIndex:1]; + NSRange textRange = [result rangeAtIndex:2]; + NSRange closingRange = [result rangeAtIndex:3]; + + if (fullMatch.location != NSNotFound) { + [attributedString addAttributes:fontAttributes range:fullMatch]; + } + + if (openingRange.location != NSNotFound) { + [attributedString addAttributes:self.orangeAttributes range:openingRange]; + } + + if (closingRange.location != NSNotFound) { + [attributedString addAttributes:self.orangeAttributes range:closingRange]; + } + }]; +} + +- (void)updateColorAttributesWithColors: (WKSourceEditorColors *)colors { + NSMutableDictionary *mutAttributes = [[NSMutableDictionary alloc] initWithDictionary:self.orangeAttributes]; + [mutAttributes setObject:colors.orangeForegroundColor forKey:NSForegroundColorAttributeName]; + self.orangeAttributes = [[NSDictionary alloc] initWithDictionary:mutAttributes]; +} + +- (void)enumerateAndUpdateColorsInAttributedString: (NSMutableAttributedString *)attributedString range: (NSRange)range { + [attributedString enumerateAttribute:WKSourceEditorCustomKeyColorOrange + inRange:range + options:nil + usingBlock:^(id value, NSRange localRange, BOOL *stop) { + if ([value isKindOfClass: [NSNumber class]]) { + NSNumber *numValue = (NSNumber *)value; + if ([numValue boolValue] == YES) { + [attributedString addAttributes:self.orangeAttributes range:localRange]; + } + } + }]; +} + +- (void)updateFontAttributesWithFonts: (WKSourceEditorFonts *)fonts { + NSMutableDictionary *mutHeadingAttributes = [[NSMutableDictionary alloc] initWithDictionary:self.headingFontAttributes]; + [mutHeadingAttributes setObject:fonts.headingFont forKey:NSFontAttributeName]; + self.headingFontAttributes = [[NSDictionary alloc] initWithDictionary:mutHeadingAttributes]; + + NSMutableDictionary *mutSubheading1Attributes = [[NSMutableDictionary alloc] initWithDictionary:self.subheading1FontAttributes]; + [mutSubheading1Attributes setObject:fonts.subheading1Font forKey:NSFontAttributeName]; + self.subheading1FontAttributes = [[NSDictionary alloc] initWithDictionary:mutSubheading1Attributes]; + + NSMutableDictionary *mutSubheading2Attributes = [[NSMutableDictionary alloc] initWithDictionary:self.subheading2FontAttributes]; + [mutSubheading2Attributes setObject:fonts.subheading2Font forKey:NSFontAttributeName]; + self.subheading2FontAttributes = [[NSDictionary alloc] initWithDictionary:mutSubheading2Attributes]; + + NSMutableDictionary *mutSubheading3Attributes = [[NSMutableDictionary alloc] initWithDictionary:self.subheading3FontAttributes]; + [mutSubheading3Attributes setObject:fonts.subheading3Font forKey:NSFontAttributeName]; + self.subheading3FontAttributes = [[NSDictionary alloc] initWithDictionary:mutSubheading3Attributes]; + + NSMutableDictionary *mutSubheading4Attributes = [[NSMutableDictionary alloc] initWithDictionary:self.subheading4FontAttributes]; + [mutSubheading4Attributes setObject:fonts.subheading4Font forKey:NSFontAttributeName]; + self.subheading4FontAttributes = [[NSDictionary alloc] initWithDictionary:mutSubheading4Attributes]; +} + +- (void)enumerateAndUpdateFontsInAttributedString: (NSMutableAttributedString *)attributedString range: (NSRange)range { + [attributedString enumerateAttribute:WKSourceEditorCustomKeyFontHeading + inRange:range + options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired + usingBlock:^(id value, NSRange localRange, BOOL *stop) { + if ([value isKindOfClass: [NSNumber class]]) { + NSNumber *numValue = (NSNumber *)value; + if ([numValue boolValue] == YES) { + [attributedString addAttributes:self.headingFontAttributes range:localRange]; + } + } + }]; + + [attributedString enumerateAttribute:WKSourceEditorCustomKeyFontSubheading1 + inRange:range + options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired + usingBlock:^(id value, NSRange localRange, BOOL *stop) { + if ([value isKindOfClass: [NSNumber class]]) { + NSNumber *numValue = (NSNumber *)value; + if ([numValue boolValue] == YES) { + [attributedString addAttributes:self.subheading1FontAttributes range:localRange]; + } + } + }]; + + [attributedString enumerateAttribute:WKSourceEditorCustomKeyFontSubheading2 + inRange:range + options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired + usingBlock:^(id value, NSRange localRange, BOOL *stop) { + if ([value isKindOfClass: [NSNumber class]]) { + NSNumber *numValue = (NSNumber *)value; + if ([numValue boolValue] == YES) { + [attributedString addAttributes:self.subheading2FontAttributes range:localRange]; + } + } + }]; + + [attributedString enumerateAttribute:WKSourceEditorCustomKeyFontSubheading3 + inRange:range + options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired + usingBlock:^(id value, NSRange localRange, BOOL *stop) { + if ([value isKindOfClass: [NSNumber class]]) { + NSNumber *numValue = (NSNumber *)value; + if ([numValue boolValue] == YES) { + [attributedString addAttributes:self.subheading3FontAttributes range:localRange]; + } + } + }]; + + [attributedString enumerateAttribute:WKSourceEditorCustomKeyFontSubheading4 + inRange:range + options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired + usingBlock:^(id value, NSRange localRange, BOOL *stop) { + if ([value isKindOfClass: [NSNumber class]]) { + NSNumber *numValue = (NSNumber *)value; + if ([numValue boolValue] == YES) { + [attributedString addAttributes:self.subheading4FontAttributes range:localRange]; + } + } + }]; +} + +@end diff --git a/Components/Sources/ComponentsObjC/include/ComponentsObjC.h b/Components/Sources/ComponentsObjC/include/ComponentsObjC.h index 078e6dec5d6..cd20c737823 100644 --- a/Components/Sources/ComponentsObjC/include/ComponentsObjC.h +++ b/Components/Sources/ComponentsObjC/include/ComponentsObjC.h @@ -8,6 +8,7 @@ #import "WKSourceEditorFormatterBase.h" #import "WKSourceEditorFormatterBoldItalics.h" #import "WKSourceEditorFormatterTemplate.h" +#import "WKSourceEditorFormatterHeading.h" #import "WKSourceEditorStorageDelegate.h" #endif /* Header_h */ diff --git a/Components/Sources/ComponentsObjC/include/WKSourceEditorFormatterHeading.h b/Components/Sources/ComponentsObjC/include/WKSourceEditorFormatterHeading.h new file mode 120000 index 00000000000..5fc70400b83 --- /dev/null +++ b/Components/Sources/ComponentsObjC/include/WKSourceEditorFormatterHeading.h @@ -0,0 +1 @@ +../WKSourceEditorFormatterHeading.h \ No newline at end of file From 4a573ea4c84440520676e341fab3f25ff83f32b4 Mon Sep 17 00:00:00 2001 From: Toni Sevener Date: Fri, 8 Dec 2023 16:14:12 -0600 Subject: [PATCH 05/18] Use new formatter in editor --- .../Source Editor/WKSourceEditorTextFrameworkMediator.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Components/Sources/Components/Components/Editors/Source Editor/WKSourceEditorTextFrameworkMediator.swift b/Components/Sources/Components/Components/Editors/Source Editor/WKSourceEditorTextFrameworkMediator.swift index d701a037b4f..46d961923b0 100644 --- a/Components/Sources/Components/Components/Editors/Source Editor/WKSourceEditorTextFrameworkMediator.swift +++ b/Components/Sources/Components/Components/Editors/Source Editor/WKSourceEditorTextFrameworkMediator.swift @@ -35,6 +35,7 @@ final class WKSourceEditorTextFrameworkMediator: NSObject { private(set) var formatters: [WKSourceEditorFormatter] = [] private(set) var boldItalicsFormatter: WKSourceEditorFormatterBoldItalics? private(set) var templateFormatter: WKSourceEditorFormatterTemplate? + private(set) var headingFormatter: WKSourceEditorFormatterHeading? var isSyntaxHighlightingEnabled: Bool = true { didSet { @@ -103,11 +104,14 @@ final class WKSourceEditorTextFrameworkMediator: NSObject { let boldItalicsFormatter = WKSourceEditorFormatterBoldItalics(colors: colors, fonts: fonts) let templateFormatter = WKSourceEditorFormatterTemplate(colors: colors, fonts: fonts) + let headingFormatter = WKSourceEditorFormatterHeading(colors: colors, fonts: fonts) self.formatters = [WKSourceEditorFormatterBase(colors: colors, fonts: fonts, textAlignment: viewModel.textAlignment), templateFormatter, - boldItalicsFormatter] + boldItalicsFormatter, + headingFormatter] self.boldItalicsFormatter = boldItalicsFormatter self.templateFormatter = templateFormatter + self.headingFormatter = headingFormatter if needsTextKit2 { if #available(iOS 16.0, *) { From 896661ddeff668a8b3a78bcd3cfdd22b253e9e8e Mon Sep 17 00:00:00 2001 From: Toni Sevener Date: Fri, 8 Dec 2023 16:29:48 -0600 Subject: [PATCH 06/18] Fix bugs --- .../ComponentsObjC/WKSourceEditorFormatterHeading.m | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Components/Sources/ComponentsObjC/WKSourceEditorFormatterHeading.m b/Components/Sources/ComponentsObjC/WKSourceEditorFormatterHeading.m index 1d8e6a0febf..dafa1f8ec77 100644 --- a/Components/Sources/ComponentsObjC/WKSourceEditorFormatterHeading.m +++ b/Components/Sources/ComponentsObjC/WKSourceEditorFormatterHeading.m @@ -39,22 +39,22 @@ - (instancetype)initWithColors:(WKSourceEditorColors *)colors fonts:(WKSourceEdi _headingFontAttributes = @{ NSFontAttributeName: fonts.headingFont, - WKSourceEditorCustomKeyFontSubheading1: [NSNumber numberWithBool:YES] + WKSourceEditorCustomKeyFontHeading: [NSNumber numberWithBool:YES] }; _subheading1FontAttributes = @{ NSFontAttributeName: fonts.subheading1Font, - WKSourceEditorCustomKeyFontSubheading2: [NSNumber numberWithBool:YES] + WKSourceEditorCustomKeyFontSubheading1: [NSNumber numberWithBool:YES] }; _subheading2FontAttributes = @{ NSFontAttributeName: fonts.subheading2Font, - WKSourceEditorCustomKeyFontSubheading3: [NSNumber numberWithBool:YES] + WKSourceEditorCustomKeyFontSubheading2: [NSNumber numberWithBool:YES] }; _subheading3FontAttributes = @{ NSFontAttributeName: fonts.subheading3Font, - WKSourceEditorCustomKeyFontSubheading4: [NSNumber numberWithBool:YES] + WKSourceEditorCustomKeyFontSubheading3: [NSNumber numberWithBool:YES] }; _subheading4FontAttributes = @{ From 9d3fcccd267c911a4e3186d41922594837a19089 Mon Sep 17 00:00:00 2001 From: Toni Sevener Date: Fri, 8 Dec 2023 16:48:43 -0600 Subject: [PATCH 07/18] Add tests --- .../WKSourceEditorFormatterTests.swift | 208 +++++++++++++++++- 1 file changed, 207 insertions(+), 1 deletion(-) diff --git a/Components/Tests/ComponentsTests/WKSourceEditorFormatterTests.swift b/Components/Tests/ComponentsTests/WKSourceEditorFormatterTests.swift index 6330fe0ba7c..9d4279fb0aa 100644 --- a/Components/Tests/ComponentsTests/WKSourceEditorFormatterTests.swift +++ b/Components/Tests/ComponentsTests/WKSourceEditorFormatterTests.swift @@ -10,8 +10,9 @@ final class WKSourceEditorFormatterTests: XCTestCase { var baseFormatter: WKSourceEditorFormatterBase! var boldItalicsFormatter: WKSourceEditorFormatterBoldItalics! var templateFormatter: WKSourceEditorFormatterTemplate! + var headingFormatter: WKSourceEditorFormatterHeading! var formatters: [WKSourceEditorFormatter] { - return [baseFormatter, templateFormatter, boldItalicsFormatter] + return [baseFormatter, templateFormatter, boldItalicsFormatter, headingFormatter] } override func setUpWithError() throws { @@ -27,10 +28,16 @@ final class WKSourceEditorFormatterTests: XCTestCase { self.fonts.boldFont = WKFont.for(.boldBody, compatibleWith: traitCollection) self.fonts.italicsFont = WKFont.for(.italicsBody, compatibleWith: traitCollection) self.fonts.boldItalicsFont = WKFont.for(.boldItalicsBody, compatibleWith: traitCollection) + self.fonts.headingFont = WKFont.for(.editorHeading, compatibleWith: traitCollection) + self.fonts.subheading1Font = WKFont.for(.editorSubheading1, compatibleWith: traitCollection) + self.fonts.subheading2Font = WKFont.for(.editorSubheading2, compatibleWith: traitCollection) + self.fonts.subheading3Font = WKFont.for(.editorSubheading3, compatibleWith: traitCollection) + self.fonts.subheading4Font = WKFont.for(.editorSubheading4, compatibleWith: traitCollection) self.baseFormatter = WKSourceEditorFormatterBase(colors: colors, fonts: fonts, textAlignment: .left) self.boldItalicsFormatter = WKSourceEditorFormatterBoldItalics(colors: colors, fonts: fonts) self.templateFormatter = WKSourceEditorFormatterTemplate(colors: colors, fonts: fonts) + self.headingFormatter = WKSourceEditorFormatterHeading(colors: colors, fonts: fonts) } override func tearDownWithError() throws { @@ -709,4 +716,203 @@ final class WKSourceEditorFormatterTests: XCTestCase { XCTAssertEqual(refAttributes[.font] as! UIFont, fonts.baseFont, "Incorrect ref formatting") XCTAssertEqual(refAttributes[.foregroundColor] as! UIColor, colors.baseForegroundColor, "Incorrect ref formatting") } + + func testHeading() { + let string = "== Test ==" + let mutAttributedString = NSMutableAttributedString(string: string) + + for formatter in formatters { + formatter.addSyntaxHighlighting(to: mutAttributedString, in: NSRange(location: 0, length: string.count)) + } + + var openingRange = NSRange(location: 0, length: 0) + let openingAttributes = mutAttributedString.attributes(at: 0, effectiveRange: &openingRange) + + var contentRange = NSRange(location: 0, length: 0) + let contentAttributes = mutAttributedString.attributes(at: 2, effectiveRange: &contentRange) + + var closingRange = NSRange(location: 0, length: 0) + let closingAttributes = mutAttributedString.attributes(at: 8, effectiveRange: &closingRange) + + // "==" + XCTAssertEqual(openingRange.location, 0, "Incorrect heading formatting") + XCTAssertEqual(openingRange.length, 2, "Incorrect heading formatting") + XCTAssertEqual(openingAttributes[.font] as! UIFont, fonts.headingFont, "Incorrect heading formatting") + XCTAssertEqual(openingAttributes[.foregroundColor] as! UIColor, colors.orangeForegroundColor, "Incorrect heading formatting") + + // " Heading Test " + XCTAssertEqual(contentRange.location, 2, "Incorrect heading formatting") + XCTAssertEqual(contentRange.length, 6, "Incorrect heading formatting") + XCTAssertEqual(contentAttributes[.font] as! UIFont, fonts.headingFont, "Incorrect heading formatting") + XCTAssertEqual(contentAttributes[.foregroundColor] as! UIColor, colors.baseForegroundColor, "Incorrect heading formatting") + + // "==" + XCTAssertEqual(closingRange.location, 8, "Incorrect heading formatting") + XCTAssertEqual(closingRange.length, 2, "Incorrect heading formatting") + XCTAssertEqual(closingAttributes[.font] as! UIFont, fonts.headingFont, "Incorrect heading formatting") + XCTAssertEqual(closingAttributes[.foregroundColor] as! UIColor, colors.orangeForegroundColor, "Incorrect heading formatting") + } + + func testSubheading1() { + let string = "=== Test ===" + let mutAttributedString = NSMutableAttributedString(string: string) + + for formatter in formatters { + formatter.addSyntaxHighlighting(to: mutAttributedString, in: NSRange(location: 0, length: string.count)) + } + + var openingRange = NSRange(location: 0, length: 0) + let openingAttributes = mutAttributedString.attributes(at: 0, effectiveRange: &openingRange) + + var contentRange = NSRange(location: 0, length: 0) + let contentAttributes = mutAttributedString.attributes(at: 3, effectiveRange: &contentRange) + + var closingRange = NSRange(location: 0, length: 0) + let closingAttributes = mutAttributedString.attributes(at: 9, effectiveRange: &closingRange) + + // "==" + XCTAssertEqual(openingRange.location, 0, "Incorrect subheading1 formatting") + XCTAssertEqual(openingRange.length, 3, "Incorrect subheading1 formatting") + XCTAssertEqual(openingAttributes[.font] as! UIFont, fonts.subheading1Font, "Incorrect subheading1 formatting") + XCTAssertEqual(openingAttributes[.foregroundColor] as! UIColor, colors.orangeForegroundColor, "Incorrect subheading1 formatting") + + // " Test " + XCTAssertEqual(contentRange.location, 3, "Incorrect subheading1 formatting") + XCTAssertEqual(contentRange.length, 6, "Incorrect subheading1 formatting") + XCTAssertEqual(contentAttributes[.font] as! UIFont, fonts.subheading1Font, "Incorrect subheading1 formatting") + XCTAssertEqual(contentAttributes[.foregroundColor] as! UIColor, colors.baseForegroundColor, "Incorrect subheading1 formatting") + + // "==" + XCTAssertEqual(closingRange.location, 9, "Incorrect subheading1 formatting") + XCTAssertEqual(closingRange.length, 3, "Incorrect subheading1 formatting") + XCTAssertEqual(closingAttributes[.font] as! UIFont, fonts.subheading1Font, "Incorrect subheading1 formatting") + XCTAssertEqual(closingAttributes[.foregroundColor] as! UIColor, colors.orangeForegroundColor, "Incorrect subheading1 formatting") + } + + func testSubheading2() { + let string = "==== Test ====" + let mutAttributedString = NSMutableAttributedString(string: string) + + for formatter in formatters { + formatter.addSyntaxHighlighting(to: mutAttributedString, in: NSRange(location: 0, length: string.count)) + } + + var openingRange = NSRange(location: 0, length: 0) + let openingAttributes = mutAttributedString.attributes(at: 0, effectiveRange: &openingRange) + + var contentRange = NSRange(location: 0, length: 0) + let contentAttributes = mutAttributedString.attributes(at: 4, effectiveRange: &contentRange) + + var closingRange = NSRange(location: 0, length: 0) + let closingAttributes = mutAttributedString.attributes(at: 10, effectiveRange: &closingRange) + + // "==" + XCTAssertEqual(openingRange.location, 0, "Incorrect subheading2 formatting") + XCTAssertEqual(openingRange.length, 4, "Incorrect subheading2 formatting") + XCTAssertEqual(openingAttributes[.font] as! UIFont, fonts.subheading2Font, "Incorrect subheading2 formatting") + XCTAssertEqual(openingAttributes[.foregroundColor] as! UIColor, colors.orangeForegroundColor, "Incorrect subheading2 formatting") + + // " Test " + XCTAssertEqual(contentRange.location, 4, "Incorrect subheading2 formatting") + XCTAssertEqual(contentRange.length, 6, "Incorrect subheading2 formatting") + XCTAssertEqual(contentAttributes[.font] as! UIFont, fonts.subheading2Font, "Incorrect subheading2 formatting") + XCTAssertEqual(contentAttributes[.foregroundColor] as! UIColor, colors.baseForegroundColor, "Incorrect subheading2 formatting") + + // "==" + XCTAssertEqual(closingRange.location, 10, "Incorrect subheading2 formatting") + XCTAssertEqual(closingRange.length, 4, "Incorrect subheading2 formatting") + XCTAssertEqual(closingAttributes[.font] as! UIFont, fonts.subheading2Font, "Incorrect subheading2 formatting") + XCTAssertEqual(closingAttributes[.foregroundColor] as! UIColor, colors.orangeForegroundColor, "Incorrect subheading2 formatting") + } + + func testSubheading3() { + let string = "===== Test =====" + let mutAttributedString = NSMutableAttributedString(string: string) + + for formatter in formatters { + formatter.addSyntaxHighlighting(to: mutAttributedString, in: NSRange(location: 0, length: string.count)) + } + + var openingRange = NSRange(location: 0, length: 0) + let openingAttributes = mutAttributedString.attributes(at: 0, effectiveRange: &openingRange) + + var contentRange = NSRange(location: 0, length: 0) + let contentAttributes = mutAttributedString.attributes(at: 5, effectiveRange: &contentRange) + + var closingRange = NSRange(location: 0, length: 0) + let closingAttributes = mutAttributedString.attributes(at: 11, effectiveRange: &closingRange) + + // "==" + XCTAssertEqual(openingRange.location, 0, "Incorrect subheading3 formatting") + XCTAssertEqual(openingRange.length, 5, "Incorrect subheading3 formatting") + XCTAssertEqual(openingAttributes[.font] as! UIFont, fonts.subheading3Font, "Incorrect subheading3 formatting") + XCTAssertEqual(openingAttributes[.foregroundColor] as! UIColor, colors.orangeForegroundColor, "Incorrect subheading3 formatting") + + // " Test " + XCTAssertEqual(contentRange.location, 5, "Incorrect subheading3 formatting") + XCTAssertEqual(contentRange.length, 6, "Incorrect subheading3 formatting") + XCTAssertEqual(contentAttributes[.font] as! UIFont, fonts.subheading3Font, "Incorrect subheading3 formatting") + XCTAssertEqual(contentAttributes[.foregroundColor] as! UIColor, colors.baseForegroundColor, "Incorrect subheading3 formatting") + + // "==" + XCTAssertEqual(closingRange.location, 11, "Incorrect subheading3 formatting") + XCTAssertEqual(closingRange.length, 5, "Incorrect subheading3 formatting") + XCTAssertEqual(closingAttributes[.font] as! UIFont, fonts.subheading3Font, "Incorrect subheading3 formatting") + XCTAssertEqual(closingAttributes[.foregroundColor] as! UIColor, colors.orangeForegroundColor, "Incorrect subheading3 formatting") + } + + func testSubeading4() { + let string = "====== Test ======" + let mutAttributedString = NSMutableAttributedString(string: string) + + for formatter in formatters { + formatter.addSyntaxHighlighting(to: mutAttributedString, in: NSRange(location: 0, length: string.count)) + } + + var openingRange = NSRange(location: 0, length: 0) + let openingAttributes = mutAttributedString.attributes(at: 0, effectiveRange: &openingRange) + + var contentRange = NSRange(location: 0, length: 0) + let contentAttributes = mutAttributedString.attributes(at: 6, effectiveRange: &contentRange) + + var closingRange = NSRange(location: 0, length: 0) + let closingAttributes = mutAttributedString.attributes(at: 12, effectiveRange: &closingRange) + + // "==" + XCTAssertEqual(openingRange.location, 0, "Incorrect subheading4 formatting") + XCTAssertEqual(openingRange.length, 6, "Incorrect subheading4 formatting") + XCTAssertEqual(openingAttributes[.font] as! UIFont, fonts.subheading4Font, "Incorrect subheading4 formatting") + XCTAssertEqual(openingAttributes[.foregroundColor] as! UIColor, colors.orangeForegroundColor, "Incorrect subheading4 formatting") + + // " Test " + XCTAssertEqual(contentRange.location, 6, "Incorrect subheading4 formatting") + XCTAssertEqual(contentRange.length, 6, "Incorrect subheading4 formatting") + XCTAssertEqual(contentAttributes[.font] as! UIFont, fonts.subheading4Font, "Incorrect subheading4 formatting") + XCTAssertEqual(contentAttributes[.foregroundColor] as! UIColor, colors.baseForegroundColor, "Incorrect subheading4 formatting") + + // "==" + XCTAssertEqual(closingRange.location, 12, "Incorrect subheading4 formatting") + XCTAssertEqual(closingRange.length, 6, "Incorrect subheading4 formatting") + XCTAssertEqual(closingAttributes[.font] as! UIFont, fonts.subheading4Font, "Incorrect subheading4 formatting") + XCTAssertEqual(closingAttributes[.foregroundColor] as! UIColor, colors.orangeForegroundColor, "Incorrect subheading4 formatting") + } + + func testInlineHeading() { + let string = "Test == Test == Test" + let mutAttributedString = NSMutableAttributedString(string: string) + + for formatter in formatters { + formatter.addSyntaxHighlighting(to: mutAttributedString, in: NSRange(location: 0, length: string.count)) + } + + var range = NSRange(location: 0, length: 0) + let attributes = mutAttributedString.attributes(at: 0, effectiveRange: &range) + + // "Test == Test == Test" + // Inline headings should not format - they must be on their own line. + XCTAssertEqual(range.location, 0, "Incorrect inline heading formatting") + XCTAssertEqual(range.length, 20, "Incorrect inline heading formatting") + XCTAssertEqual(attributes[.font] as! UIFont, fonts.baseFont, "Incorrect inline heading formatting") + XCTAssertEqual(attributes[.foregroundColor] as! UIColor, colors.baseForegroundColor, "Incorrect inline heading formatting") + } } From 1e874262b65afc1247babf396cd8eaead3fc63f6 Mon Sep 17 00:00:00 2001 From: Toni Sevener Date: Mon, 11 Dec 2023 17:31:54 -0600 Subject: [PATCH 08/18] Allow formatter to detect if range contains custom heading keys - Will use these for button selection states later --- .../WKSourceEditorFormatterHeading.h | 6 + .../WKSourceEditorFormatterHeading.m | 103 +++++++++++++++++- 2 files changed, 103 insertions(+), 6 deletions(-) diff --git a/Components/Sources/ComponentsObjC/WKSourceEditorFormatterHeading.h b/Components/Sources/ComponentsObjC/WKSourceEditorFormatterHeading.h index 72a8c6eed35..78a6013e552 100644 --- a/Components/Sources/ComponentsObjC/WKSourceEditorFormatterHeading.h +++ b/Components/Sources/ComponentsObjC/WKSourceEditorFormatterHeading.h @@ -4,6 +4,12 @@ NS_ASSUME_NONNULL_BEGIN @interface WKSourceEditorFormatterHeading : WKSourceEditorFormatter +- (BOOL)attributedString:(NSMutableAttributedString *)attributedString isHeadingInRange:(NSRange)range; +- (BOOL)attributedString:(NSMutableAttributedString *)attributedString isSubheading1InRange:(NSRange)range; +- (BOOL)attributedString:(NSMutableAttributedString *)attributedString isSubheading2InRange:(NSRange)range; +- (BOOL)attributedString:(NSMutableAttributedString *)attributedString isSubheading3InRange:(NSRange)range; +- (BOOL)attributedString:(NSMutableAttributedString *)attributedString isSubheading4InRange:(NSRange)range; + @end NS_ASSUME_NONNULL_END diff --git a/Components/Sources/ComponentsObjC/WKSourceEditorFormatterHeading.m b/Components/Sources/ComponentsObjC/WKSourceEditorFormatterHeading.m index dafa1f8ec77..ed2eb3dd9dd 100644 --- a/Components/Sources/ComponentsObjC/WKSourceEditorFormatterHeading.m +++ b/Components/Sources/ComponentsObjC/WKSourceEditorFormatterHeading.m @@ -10,6 +10,12 @@ @interface WKSourceEditorFormatterHeading () @property (nonatomic, strong) NSDictionary *subheading4FontAttributes; @property (nonatomic, strong) NSDictionary *orangeAttributes; +@property (nonatomic, strong) NSDictionary *headingContentAttributes; +@property (nonatomic, strong) NSDictionary *subheading1ContentAttributes; +@property (nonatomic, strong) NSDictionary *subheading2ContentAttributes; +@property (nonatomic, strong) NSDictionary *subheading3ContentAttributes; +@property (nonatomic, strong) NSDictionary *subheading4ContentAttributes; + @property (nonatomic, strong) NSRegularExpression *headingRegex; @property (nonatomic, strong) NSRegularExpression *subheading1Regex; @property (nonatomic, strong) NSRegularExpression *subheading2Regex; @@ -21,12 +27,20 @@ @implementation WKSourceEditorFormatterHeading #pragma mark - Custom Attributed String Keys +// Font custom keys span across entire match, i.e. "== Test ==". The entire match is a particular font. This helps us quickly seek and update fonts upon popover change. NSString * const WKSourceEditorCustomKeyFontHeading = @"WKSourceEditorCustomKeyFontHeading"; NSString * const WKSourceEditorCustomKeyFontSubheading1 = @"WKSourceEditorCustomKeyFontSubheading1"; NSString * const WKSourceEditorCustomKeyFontSubheading2 = @"WKSourceEditorCustomKeyFontSubheading2"; NSString * const WKSourceEditorCustomKeyFontSubheading3 = @"WKSourceEditorCustomKeyFontSubheading3"; NSString * const WKSourceEditorCustomKeyFontSubheading4 = @"WKSourceEditorCustomKeyFontSubheading4"; +// Content custom keys span across only the content, i.e. " Test ". This helps us detect for button selection states. +NSString * const WKSourceEditorCustomKeyContentHeading = @"WKSourceEditorCustomKeyContentHeading"; +NSString * const WKSourceEditorCustomKeyContentSubheading1 = @"WKSourceEditorCustomKeyContentSubheading1"; +NSString * const WKSourceEditorCustomKeyContentSubheading2 = @"WKSourceEditorCustomKeyContentSubheading2"; +NSString * const WKSourceEditorCustomKeyContentSubheading3 = @"WKSourceEditorCustomKeyContentSubheading3"; +NSString * const WKSourceEditorCustomKeyContentSubheading4 = @"WKSourceEditorCustomKeyContentSubheading4"; + #pragma mark - Public - (instancetype)initWithColors:(WKSourceEditorColors *)colors fonts:(WKSourceEditorFonts *)fonts { @@ -62,6 +76,26 @@ - (instancetype)initWithColors:(WKSourceEditorColors *)colors fonts:(WKSourceEdi WKSourceEditorCustomKeyFontSubheading4: [NSNumber numberWithBool:YES] }; + _headingContentAttributes = @{ + WKSourceEditorCustomKeyContentHeading: [NSNumber numberWithBool:YES] + }; + + _subheading1ContentAttributes = @{ + WKSourceEditorCustomKeyContentSubheading1: [NSNumber numberWithBool:YES] + }; + + _subheading2ContentAttributes = @{ + WKSourceEditorCustomKeyContentSubheading2: [NSNumber numberWithBool:YES] + }; + + _subheading3ContentAttributes = @{ + WKSourceEditorCustomKeyContentSubheading3: [NSNumber numberWithBool:YES] + }; + + _subheading4ContentAttributes = @{ + WKSourceEditorCustomKeyContentSubheading4: [NSNumber numberWithBool:YES] + }; + _headingRegex = [[NSRegularExpression alloc] initWithPattern:@"^(={2})([^=]*)(={2})(?!=)$" options:NSRegularExpressionAnchorsMatchLines error:nil]; _subheading1Regex = [[NSRegularExpression alloc] initWithPattern:@"^(={3})([^=]*)(={3})(?!=)$" options:NSRegularExpressionAnchorsMatchLines error:nil]; _subheading2Regex = [[NSRegularExpression alloc] initWithPattern:@"^(={4})([^=]*)(={4})(?!=)$" options:NSRegularExpressionAnchorsMatchLines error:nil]; @@ -82,12 +116,17 @@ - (void)addSyntaxHighlightingToAttributedString:(nonnull NSMutableAttributedStri [attributedString removeAttribute:WKSourceEditorCustomKeyFontSubheading2 range:range]; [attributedString removeAttribute:WKSourceEditorCustomKeyFontSubheading3 range:range]; [attributedString removeAttribute:WKSourceEditorCustomKeyFontSubheading4 range:range]; + [attributedString removeAttribute:WKSourceEditorCustomKeyContentHeading range:range]; + [attributedString removeAttribute:WKSourceEditorCustomKeyContentSubheading1 range:range]; + [attributedString removeAttribute:WKSourceEditorCustomKeyContentSubheading2 range:range]; + [attributedString removeAttribute:WKSourceEditorCustomKeyContentSubheading3 range:range]; + [attributedString removeAttribute:WKSourceEditorCustomKeyContentSubheading4 range:range]; - [self enumerateAndHighlightAttributedString:attributedString range:range regex:self.headingRegex attributes:self.headingFontAttributes]; - [self enumerateAndHighlightAttributedString:attributedString range:range regex:self.subheading1Regex attributes:self.subheading1FontAttributes]; - [self enumerateAndHighlightAttributedString:attributedString range:range regex:self.subheading2Regex attributes:self.subheading2FontAttributes]; - [self enumerateAndHighlightAttributedString:attributedString range:range regex:self.subheading3Regex attributes:self.subheading3FontAttributes]; - [self enumerateAndHighlightAttributedString:attributedString range:range regex:self.subheading4Regex attributes:self.subheading4FontAttributes]; + [self enumerateAndHighlightAttributedString:attributedString range:range regex:self.headingRegex fontAttributes:self.headingFontAttributes contentAttributes:self.headingContentAttributes]; + [self enumerateAndHighlightAttributedString:attributedString range:range regex:self.subheading1Regex fontAttributes:self.subheading1FontAttributes contentAttributes:self.subheading1ContentAttributes]; + [self enumerateAndHighlightAttributedString:attributedString range:range regex:self.subheading2Regex fontAttributes:self.subheading2FontAttributes contentAttributes:self.subheading2ContentAttributes]; + [self enumerateAndHighlightAttributedString:attributedString range:range regex:self.subheading3Regex fontAttributes:self.subheading3FontAttributes contentAttributes:self.subheading3ContentAttributes]; + [self enumerateAndHighlightAttributedString:attributedString range:range regex:self.subheading4Regex fontAttributes:self.subheading4FontAttributes contentAttributes:self.subheading4ContentAttributes]; } - (void)updateColors:(WKSourceEditorColors *)colors inAttributedString:(NSMutableAttributedString *)attributedString inRange:(NSRange)range { @@ -102,9 +141,57 @@ - (void)updateFonts:(WKSourceEditorFonts *)fonts inAttributedString:(NSMutableAt [self enumerateAndUpdateFontsInAttributedString:attributedString range:range]; } +#pragma mark - Public + +- (BOOL)attributedString:(NSMutableAttributedString *)attributedString isHeadingInRange:(NSRange)range { + return [self attributedString:attributedString isContentKey:WKSourceEditorCustomKeyContentHeading inRange:range]; +} + +- (BOOL)attributedString:(NSMutableAttributedString *)attributedString isSubheading1InRange:(NSRange)range { + return [self attributedString:attributedString isContentKey:WKSourceEditorCustomKeyContentSubheading1 inRange:range]; +} + +- (BOOL)attributedString:(NSMutableAttributedString *)attributedString isSubheading2InRange:(NSRange)range { + return [self attributedString:attributedString isContentKey:WKSourceEditorCustomKeyContentSubheading2 inRange:range]; +} + +- (BOOL)attributedString:(NSMutableAttributedString *)attributedString isSubheading3InRange:(NSRange)range { + return [self attributedString:attributedString isContentKey:WKSourceEditorCustomKeyContentSubheading3 inRange:range]; +} + +- (BOOL)attributedString:(NSMutableAttributedString *)attributedString isSubheading4InRange:(NSRange)range { + return [self attributedString:attributedString isContentKey:WKSourceEditorCustomKeyContentSubheading4 inRange:range]; +} + #pragma mark - Private -- (void)enumerateAndHighlightAttributedString: (nonnull NSMutableAttributedString *)attributedString range:(NSRange)range regex:(NSRegularExpression *)regex attributes:(NSDictionary *)fontAttributes { +- (BOOL)attributedString:(NSMutableAttributedString *)attributedString isContentKey:(NSString *)contentKey inRange:(NSRange)range { + __block BOOL isContentKey = NO; + + if (range.length == 0) { + + if (attributedString.length > range.location) { + NSDictionary *attrs = [attributedString attributesAtIndex:range.location effectiveRange:nil]; + + if (attrs[contentKey] != nil) { + isContentKey = YES; + } + } + + } else { + [attributedString enumerateAttributesInRange:range options:nil usingBlock:^(NSDictionary * _Nonnull attrs, NSRange loopRange, BOOL * _Nonnull stop) { + if ((attrs[contentKey] != nil) && + (loopRange.location == range.location && loopRange.length == range.length)) { + isContentKey = YES; + stop = YES; + } + }]; + } + + return isContentKey; +} + +- (void)enumerateAndHighlightAttributedString: (nonnull NSMutableAttributedString *)attributedString range:(NSRange)range regex:(NSRegularExpression *)regex fontAttributes:(NSDictionary *)fontAttributes contentAttributes:(NSDictionary *)contentAttributes { [regex enumerateMatchesInString:attributedString.string options:0 @@ -123,6 +210,10 @@ - (void)enumerateAndHighlightAttributedString: (nonnull NSMutableAttributedStrin [attributedString addAttributes:self.orangeAttributes range:openingRange]; } + if (textRange.location != NSNotFound) { + [attributedString addAttributes:contentAttributes range:textRange]; + } + if (closingRange.location != NSNotFound) { [attributedString addAttributes:self.orangeAttributes range:closingRange]; } From a101a0af03f00f067c724fd0dd28d898b9f0aade Mon Sep 17 00:00:00 2001 From: Toni Sevener Date: Mon, 11 Dec 2023 17:32:39 -0600 Subject: [PATCH 09/18] Add heading properties to WKSourceEditorSelectionState --- .../WKSourceEditorTextFrameworkMediator.swift | 31 ++++++++++++++++--- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/Components/Sources/Components/Components/Editors/Source Editor/WKSourceEditorTextFrameworkMediator.swift b/Components/Sources/Components/Components/Editors/Source Editor/WKSourceEditorTextFrameworkMediator.swift index 46d961923b0..544904a6dde 100644 --- a/Components/Sources/Components/Components/Editors/Source Editor/WKSourceEditorTextFrameworkMediator.swift +++ b/Components/Sources/Components/Components/Editors/Source Editor/WKSourceEditorTextFrameworkMediator.swift @@ -17,11 +17,22 @@ fileprivate var needsTextKit2: Bool { let isBold: Bool let isItalics: Bool let isHorizontalTemplate: Bool + let isHeading: Bool + let isSubheading1: Bool + let isSubheading2: Bool + let isSubheading3: Bool + let isSubheading4: Bool - init(isBold: Bool, isItalics: Bool, isHorizontalTemplate: Bool) { + init(isBold: Bool, isItalics: Bool, isHorizontalTemplate: Bool, isHeading: Bool, isSubheading1: Bool, isSubheading2: Bool, isSubheading3: Bool, isSubheading4: Bool) { self.isBold = isBold self.isItalics = isItalics self.isHorizontalTemplate = isHorizontalTemplate + self.isHeading = isHeading + self.isSubheading1 = isSubheading1 + self.isSubheading2 = isSubheading2 + self.isSubheading3 = isSubheading3 + self.isSubheading4 = isSubheading4 + } } @@ -151,24 +162,34 @@ final class WKSourceEditorTextFrameworkMediator: NSObject { if needsTextKit2 { guard let textKit2Data = textkit2SelectionData(selectedDocumentRange: selectedDocumentRange) else { - return WKSourceEditorSelectionState(isBold: false, isItalics: false, isHorizontalTemplate: false) + return WKSourceEditorSelectionState(isBold: false, isItalics: false, isHorizontalTemplate: false, isHeading: false, isSubheading1: false, isSubheading2: false, isSubheading3: false, isSubheading4: false) } let isBold = boldItalicsFormatter?.attributedString(textKit2Data.paragraphAttributedString, isBoldIn: textKit2Data.paragraphSelectedRange) ?? false let isItalics = boldItalicsFormatter?.attributedString(textKit2Data.paragraphAttributedString, isItalicsIn: textKit2Data.paragraphSelectedRange) ?? false let isHorizontalTemplate = templateFormatter?.attributedString(textKit2Data.paragraphAttributedString, isHorizontalTemplateIn: textKit2Data.paragraphSelectedRange) ?? false + let isHeading = headingFormatter?.attributedString(textKit2Data.paragraphAttributedString, isHeadingIn: textKit2Data.paragraphSelectedRange) ?? false + let isSubheading1 = headingFormatter?.attributedString(textKit2Data.paragraphAttributedString, isSubheading1In: textKit2Data.paragraphSelectedRange) ?? false + let isSubheading2 = headingFormatter?.attributedString(textKit2Data.paragraphAttributedString, isSubheading2In: textKit2Data.paragraphSelectedRange) ?? false + let isSubheading3 = headingFormatter?.attributedString(textKit2Data.paragraphAttributedString, isSubheading3In: textKit2Data.paragraphSelectedRange) ?? false + let isSubheading4 = headingFormatter?.attributedString(textKit2Data.paragraphAttributedString, isSubheading4In: textKit2Data.paragraphSelectedRange) ?? false - return WKSourceEditorSelectionState(isBold: isBold, isItalics: isItalics, isHorizontalTemplate: isHorizontalTemplate) + return WKSourceEditorSelectionState(isBold: isBold, isItalics: isItalics, isHorizontalTemplate: isHorizontalTemplate, isHeading: isHeading, isSubheading1: isSubheading1, isSubheading2: isSubheading2, isSubheading3: isSubheading3, isSubheading4: isSubheading4) } else { guard let textKit1Storage else { - return WKSourceEditorSelectionState(isBold: false, isItalics: false, isHorizontalTemplate: false) + return WKSourceEditorSelectionState(isBold: false, isItalics: false, isHorizontalTemplate: false, isHeading: false, isSubheading1: false, isSubheading2: false, isSubheading3: false, isSubheading4: false) } let isBold = boldItalicsFormatter?.attributedString(textKit1Storage, isBoldIn: selectedDocumentRange) ?? false let isItalics = boldItalicsFormatter?.attributedString(textKit1Storage, isItalicsIn: selectedDocumentRange) ?? false let isHorizontalTemplate = templateFormatter?.attributedString(textKit1Storage, isHorizontalTemplateIn: selectedDocumentRange) ?? false + let isHeading = headingFormatter?.attributedString(textKit1Storage, isHeadingIn: selectedDocumentRange) ?? false + let isSubheading1 = headingFormatter?.attributedString(textKit1Storage, isSubheading1In: selectedDocumentRange) ?? false + let isSubheading2 = headingFormatter?.attributedString(textKit1Storage, isSubheading2In: selectedDocumentRange) ?? false + let isSubheading3 = headingFormatter?.attributedString(textKit1Storage, isSubheading3In: selectedDocumentRange) ?? false + let isSubheading4 = headingFormatter?.attributedString(textKit1Storage, isSubheading4In: selectedDocumentRange) ?? false - return WKSourceEditorSelectionState(isBold: isBold, isItalics: isItalics, isHorizontalTemplate: isHorizontalTemplate) + return WKSourceEditorSelectionState(isBold: isBold, isItalics: isItalics, isHorizontalTemplate: isHorizontalTemplate, isHeading: isHeading, isSubheading1: isSubheading1, isSubheading2: isSubheading2, isSubheading3: isSubheading3, isSubheading4: isSubheading4) } } From 52ed78ddafa9e07066381bd43c7510fa38e7d279 Mon Sep 17 00:00:00 2001 From: Toni Sevener Date: Mon, 11 Dec 2023 17:35:06 -0600 Subject: [PATCH 10/18] Add didTapHeading delegate callback --- .../Input Views/WKEditorInputViewController.swift | 1 + .../Editors/Source Editor/WKSourceEditorViewController.swift | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/Components/Sources/Components/Components/Editors/Common Views/Input Views/WKEditorInputViewController.swift b/Components/Sources/Components/Components/Editors/Common Views/Input Views/WKEditorInputViewController.swift index a2ae309b88f..422cd813ac3 100644 --- a/Components/Sources/Components/Components/Editors/Common Views/Input Views/WKEditorInputViewController.swift +++ b/Components/Sources/Components/Components/Editors/Common Views/Input Views/WKEditorInputViewController.swift @@ -6,6 +6,7 @@ protocol WKEditorInputViewDelegate: AnyObject { func didTapBold(isSelected: Bool) func didTapItalics(isSelected: Bool) func didTapTemplate(isSelected: Bool) + func didTapHeading(selectedHeading: WKEditorHeaderSelectViewModel.Configuration) } class WKEditorInputViewController: WKComponentViewController { diff --git a/Components/Sources/Components/Components/Editors/Source Editor/WKSourceEditorViewController.swift b/Components/Sources/Components/Components/Editors/Source Editor/WKSourceEditorViewController.swift index 5a5d7447287..b7f45f3e0c1 100644 --- a/Components/Sources/Components/Components/Editors/Source Editor/WKSourceEditorViewController.swift +++ b/Components/Sources/Components/Components/Editors/Source Editor/WKSourceEditorViewController.swift @@ -344,6 +344,10 @@ extension WKSourceEditorViewController: WKEditorToolbarHighlightViewDelegate { // MARK: - WKEditorInputViewDelegate extension WKSourceEditorViewController: WKEditorInputViewDelegate { + func didTapHeading(selectedHeading: WKEditorHeaderSelectViewModel.Configuration) { + + } + func didTapBold(isSelected: Bool) { let action: WKSourceEditorFormatterButtonAction = isSelected ? .remove : .add textFrameworkMediator.boldItalicsFormatter?.toggleBoldFormatting(action: action, in: textView) From 1844bbecd278c2098a02a243e3d34e7c30710662 Mon Sep 17 00:00:00 2001 From: Toni Sevener Date: Mon, 11 Dec 2023 17:37:59 -0600 Subject: [PATCH 11/18] Apply selection states to keyboard input views - Listening for selection state changed notification in applicable views - Remove unused WKEditorSelectionDetailViewModel - Post selection state notification when format heading menu is triggered --- ...ditorInputHeaderSelectViewController.swift | 44 +++++++++++++++++++ .../WKEditorSelectionDetailCell.swift | 10 ++--- .../WKEditorSelectionDetailView.swift | 36 ++++++++++++--- .../WKEditorSelectionDetailViewModel.swift | 6 --- .../WKSourceEditorViewController.swift | 2 + 5 files changed, 81 insertions(+), 17 deletions(-) delete mode 100644 Components/Sources/Components/Components/Editors/Common Views/Input Views/Main/Detail Table Item/WKEditorSelectionDetailViewModel.swift diff --git a/Components/Sources/Components/Components/Editors/Common Views/Input Views/Header Selection/WKEditorInputHeaderSelectViewController.swift b/Components/Sources/Components/Components/Editors/Common Views/Input Views/Header Selection/WKEditorInputHeaderSelectViewController.swift index 9dc6a88819b..d0edf016a22 100644 --- a/Components/Sources/Components/Components/Editors/Common Views/Input Views/Header Selection/WKEditorInputHeaderSelectViewController.swift +++ b/Components/Sources/Components/Components/Editors/Common Views/Input Views/Header Selection/WKEditorInputHeaderSelectViewController.swift @@ -73,6 +73,46 @@ class WKEditorInputHeaderSelectViewController: WKComponentViewController { ]) tableView.register(WKEditorHeaderSelectCell.self, forCellReuseIdentifier: reuseIdentifier) + + NotificationCenter.default.addObserver(self, selector: #selector(updateButtonSelectionState(_:)), name: Notification.WKSourceEditorSelectionState, object: nil) + } + + // MARK: - Notifications + + @objc private func updateButtonSelectionState(_ notification: NSNotification) { + guard let selectionState = notification.userInfo?[Notification.WKSourceEditorSelectionStateKey] as? WKSourceEditorSelectionState else { + return + } + + configure(selectionState: selectionState) + tableView.reloadData() + } + + // MARK: Public + + func configure(selectionState: WKSourceEditorSelectionState) { + viewModels.forEach { $0.isSelected = false } + + let paragraphViewModel = viewModels[0] + let headingViewModel = viewModels[1] + let subheading1ViewModel = viewModels[2] + let subheading2ViewModel = viewModels[3] + let subheading3ViewModel = viewModels[4] + let subheading4ViewModel = viewModels[5] + + if selectionState.isHeading { + headingViewModel.isSelected = true + } else if selectionState.isSubheading1 { + subheading1ViewModel.isSelected = true + } else if selectionState.isSubheading2 { + subheading2ViewModel.isSelected = true + } else if selectionState.isSubheading3 { + subheading3ViewModel.isSelected = true + } else if selectionState.isSubheading4 { + subheading4ViewModel.isSelected = true + } else { + paragraphViewModel.isSelected = true + } } // MARK: Overrides @@ -147,7 +187,11 @@ extension WKEditorInputHeaderSelectViewController: UITableViewDelegate { func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { for (index, viewModel) in viewModels.enumerated() { + let alreadySelected = viewModel.isSelected && index == indexPath.row viewModel.isSelected = index == indexPath.row + if viewModel.isSelected && !alreadySelected { + delegate?.didTapHeading(selectedHeading: viewModel.configuration) + } } tableView.reloadData() } diff --git a/Components/Sources/Components/Components/Editors/Common Views/Input Views/Main/Detail Table Item/WKEditorSelectionDetailCell.swift b/Components/Sources/Components/Components/Editors/Common Views/Input Views/Main/Detail Table Item/WKEditorSelectionDetailCell.swift index 17cdfb5fc96..8f986c15ec3 100644 --- a/Components/Sources/Components/Components/Editors/Common Views/Input Views/Main/Detail Table Item/WKEditorSelectionDetailCell.swift +++ b/Components/Sources/Components/Components/Editors/Common Views/Input Views/Main/Detail Table Item/WKEditorSelectionDetailCell.swift @@ -11,6 +11,10 @@ class WKEditorSelectionDetailCell: UITableViewCell { return view }() + var lastSelectionState: WKSourceEditorSelectionState? { + return componentView.lastSelectionState + } + // MARK: - Lifecycle override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { @@ -33,10 +37,4 @@ class WKEditorSelectionDetailCell: UITableViewCell { selectedBackgroundView?.backgroundColor = .clear } - - // MARK: - Internal - - func configure(viewModel: WKEditorSelectionDetailViewModel) { - componentView.configure(viewModel: viewModel) - } } diff --git a/Components/Sources/Components/Components/Editors/Common Views/Input Views/Main/Detail Table Item/WKEditorSelectionDetailView.swift b/Components/Sources/Components/Components/Editors/Common Views/Input Views/Main/Detail Table Item/WKEditorSelectionDetailView.swift index 70ac124065d..e5b61d7784c 100644 --- a/Components/Sources/Components/Components/Editors/Common Views/Input Views/Main/Detail Table Item/WKEditorSelectionDetailView.swift +++ b/Components/Sources/Components/Components/Editors/Common Views/Input Views/Main/Detail Table Item/WKEditorSelectionDetailView.swift @@ -11,6 +11,7 @@ class WKEditorSelectionDetailView: WKComponentView { label.setContentHuggingPriority(.defaultLow, for: .horizontal) label.adjustsFontForContentSizeCategory = true label.font = WKFont.for(.body, compatibleWith: appEnvironment.traitCollection) + label.text = WKSourceEditorLocalizedStrings.current.inputViewStyle return label }() @@ -30,8 +31,8 @@ class WKEditorSelectionDetailView: WKComponentView { return imageView }() - private var viewModel: WKEditorSelectionDetailViewModel? - + private(set) var lastSelectionState: WKSourceEditorSelectionState? + // MARK: - Lifecycle required init() { @@ -62,13 +63,38 @@ class WKEditorSelectionDetailView: WKComponentView { ]) updateColors() + + NotificationCenter.default.addObserver(self, selector: #selector(updateButtonSelectionState(_:)), name: Notification.WKSourceEditorSelectionState, object: nil) + } + + // MARK: - Notifications + + @objc private func updateButtonSelectionState(_ notification: NSNotification) { + guard let selectionState = notification.userInfo?[Notification.WKSourceEditorSelectionStateKey] as? WKSourceEditorSelectionState else { + return + } + + self.lastSelectionState = selectionState + + configure(selectionState: selectionState) } // MARK: - Internal - func configure(viewModel: WKEditorSelectionDetailViewModel) { - typeLabel.text = viewModel.typeText - selectionLabel.text = viewModel.selectionText + func configure(selectionState: WKSourceEditorSelectionState) { + if selectionState.isHeading { + selectionLabel.text = WKSourceEditorLocalizedStrings.current.inputViewHeading + } else if selectionState.isSubheading1 { + selectionLabel.text = WKSourceEditorLocalizedStrings.current.inputViewSubheading1 + } else if selectionState.isSubheading2 { + selectionLabel.text = WKSourceEditorLocalizedStrings.current.inputViewSubheading2 + } else if selectionState.isSubheading3 { + selectionLabel.text = WKSourceEditorLocalizedStrings.current.inputViewSubheading3 + } else if selectionState.isSubheading4 { + selectionLabel.text = WKSourceEditorLocalizedStrings.current.inputViewSubheading4 + } else { + selectionLabel.text = WKSourceEditorLocalizedStrings.current.inputViewParagraph + } } // MARK: - Overrides diff --git a/Components/Sources/Components/Components/Editors/Common Views/Input Views/Main/Detail Table Item/WKEditorSelectionDetailViewModel.swift b/Components/Sources/Components/Components/Editors/Common Views/Input Views/Main/Detail Table Item/WKEditorSelectionDetailViewModel.swift deleted file mode 100644 index 6ad53cd739d..00000000000 --- a/Components/Sources/Components/Components/Editors/Common Views/Input Views/Main/Detail Table Item/WKEditorSelectionDetailViewModel.swift +++ /dev/null @@ -1,6 +0,0 @@ -import Foundation - -struct WKEditorSelectionDetailViewModel { - let typeText: String - let selectionText: String -} diff --git a/Components/Sources/Components/Components/Editors/Source Editor/WKSourceEditorViewController.swift b/Components/Sources/Components/Components/Editors/Source Editor/WKSourceEditorViewController.swift index b7f45f3e0c1..4d594860296 100644 --- a/Components/Sources/Components/Components/Editors/Source Editor/WKSourceEditorViewController.swift +++ b/Components/Sources/Components/Components/Editors/Source Editor/WKSourceEditorViewController.swift @@ -304,6 +304,7 @@ extension WKSourceEditorViewController: WKEditorToolbarExpandingViewDelegate { func toolbarExpandingViewDidTapFormatHeading(toolbarView: WKEditorToolbarExpandingView) { inputViewType = .headerSelect + postUpdateButtonSelectionStatesNotification(withDelay: true) } func toolbarExpandingViewDidTapTemplate(toolbarView: WKEditorToolbarExpandingView, isSelected: Bool) { @@ -338,6 +339,7 @@ extension WKSourceEditorViewController: WKEditorToolbarHighlightViewDelegate { func toolbarHighlightViewDidTapFormatHeading(toolbarView: WKEditorToolbarHighlightView) { inputViewType = .headerSelect + postUpdateButtonSelectionStatesNotification(withDelay: true) } } From 16626adafe8b330faa45179ed80d5ffbeac95a5a Mon Sep 17 00:00:00 2001 From: Toni Sevener Date: Mon, 11 Dec 2023 17:38:39 -0600 Subject: [PATCH 12/18] Update selection state when format header view is pushed onto navigation stack --- .../Main/WKEditorInputMainViewController.swift | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Components/Sources/Components/Components/Editors/Common Views/Input Views/Main/WKEditorInputMainViewController.swift b/Components/Sources/Components/Components/Editors/Common Views/Input Views/Main/WKEditorInputMainViewController.swift index bc0c898f25a..93b12a5f207 100644 --- a/Components/Sources/Components/Components/Editors/Common Views/Input Views/Main/WKEditorInputMainViewController.swift +++ b/Components/Sources/Components/Components/Editors/Common Views/Input Views/Main/WKEditorInputMainViewController.swift @@ -108,7 +108,6 @@ extension WKEditorInputMainViewController: UITableViewDataSource { cell = tableView.dequeueReusableCell(withIdentifier: detailReuseIdentifier, for: indexPath) if let detailCell = cell as? WKEditorSelectionDetailCell { - detailCell.configure(viewModel: WKEditorSelectionDetailViewModel(typeText: WKSourceEditorLocalizedStrings.current.inputViewStyle, selectionText: WKSourceEditorLocalizedStrings.current.inputViewParagraph)) detailCell.accessibilityTraits = [.button] } case 3: @@ -134,9 +133,14 @@ extension WKEditorInputMainViewController: UITableViewDelegate { let cell = tableView.cellForRow(at: indexPath) cell?.isSelected = false - if indexPath.row == 2 { + if let detailCell = cell as? WKEditorSelectionDetailCell { navigationItem.backButtonTitle = WKSourceEditorLocalizedStrings.current.inputViewTextFormatting let headerVC = WKEditorInputHeaderSelectViewController(configuration: .standard, delegate: delegate) + + if let selectionState = detailCell.lastSelectionState { + headerVC.configure(selectionState: selectionState) + } + navigationController?.pushViewController(headerVC, animated: true) } } From 2c87242b8d271f2179f6499f1aa8e006376dd1af Mon Sep 17 00:00:00 2001 From: Toni Sevener Date: Mon, 11 Dec 2023 17:39:55 -0600 Subject: [PATCH 13/18] Expose formatter base class methods for heading button action needs --- .../WKSourceEditorFormatter+ButtonActions.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Components/Sources/Components/Components/Editors/Source Editor/Formatter Extensions/WKSourceEditorFormatter+ButtonActions.swift b/Components/Sources/Components/Components/Editors/Source Editor/Formatter Extensions/WKSourceEditorFormatter+ButtonActions.swift index e0e70e9d2f6..4ff96c46667 100644 --- a/Components/Sources/Components/Components/Editors/Source Editor/Formatter Extensions/WKSourceEditorFormatter+ButtonActions.swift +++ b/Components/Sources/Components/Components/Editors/Source Editor/Formatter Extensions/WKSourceEditorFormatter+ButtonActions.swift @@ -35,7 +35,7 @@ extension WKSourceEditorFormatter { // MARK: - Expanding selected range methods - private func expandSelectedRangeUpToNearestFormattingStrings(startingFormattingString: String, endingFormattingString: String, in textView: UITextView) { + func expandSelectedRangeUpToNearestFormattingStrings(startingFormattingString: String, endingFormattingString: String, in textView: UITextView) { if let textPositions = textPositionsCloserToNearestFormattingStrings(startingFormattingString: startingFormattingString, endingFormattingString: endingFormattingString, in: textView) { textView.selectedTextRange = textView.textRange(from: textPositions.startPosition, to: textPositions.endPosition) } @@ -108,7 +108,7 @@ extension WKSourceEditorFormatter { // MARK: - Nearby formatting string determination - private func selectedRangeIsSurroundedByFormattingString(formattingString: String, in textView: UITextView) -> Bool { + func selectedRangeIsSurroundedByFormattingString(formattingString: String, in textView: UITextView) -> Bool { selectedRangeIsSurroundedByFormattingStrings(startingFormattingString: formattingString, endingFormattingString: formattingString, in: textView) } @@ -130,7 +130,7 @@ extension WKSourceEditorFormatter { return startingString == formattingString } - private func rangeIsFollowedByFormattingString(range: UITextRange?, formattingString: String, in textView: UITextView) -> Bool { + func rangeIsFollowedByFormattingString(range: UITextRange?, formattingString: String, in textView: UITextView) -> Bool { guard let range = range, let newEnd = textView.position(from: range.end, offset: formattingString.count) else { return false @@ -146,7 +146,7 @@ extension WKSourceEditorFormatter { // MARK: Adding and removing text - private func addStringFormattingCharacters(startingFormattingString: String, endingFormattingString: String, in textView: UITextView) { + func addStringFormattingCharacters(startingFormattingString: String, endingFormattingString: String, in textView: UITextView) { let startingCursorOffset = startingFormattingString.count let endingCursorOffset = endingFormattingString.count @@ -173,7 +173,7 @@ extension WKSourceEditorFormatter { } } - private func removeSurroundingFormattingStringsFromSelectedRange(startingFormattingString: String, endingFormattingString: String, in textView: UITextView) { + func removeSurroundingFormattingStringsFromSelectedRange(startingFormattingString: String, endingFormattingString: String, in textView: UITextView) { guard let originalSelectedTextRange = textView.selectedTextRange, let formattingTextStart = textView.position(from: originalSelectedTextRange.start, offset: -startingFormattingString.count), From 4b17b3adae2d65338484e6ca5d62050d454a497c Mon Sep 17 00:00:00 2001 From: Toni Sevener Date: Mon, 11 Dec 2023 17:40:39 -0600 Subject: [PATCH 14/18] Create heading formatter extension to toggle formatting --- ...EditorFormatterHeading+ButtonActions.swift | 54 +++++++++++++++++++ .../WKSourceEditorViewController.swift | 2 +- 2 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 Components/Sources/Components/Components/Editors/Source Editor/Formatter Extensions/WKSourceEditorFormatterHeading+ButtonActions.swift diff --git a/Components/Sources/Components/Components/Editors/Source Editor/Formatter Extensions/WKSourceEditorFormatterHeading+ButtonActions.swift b/Components/Sources/Components/Components/Editors/Source Editor/Formatter Extensions/WKSourceEditorFormatterHeading+ButtonActions.swift new file mode 100644 index 00000000000..123e10f1903 --- /dev/null +++ b/Components/Sources/Components/Components/Editors/Source Editor/Formatter Extensions/WKSourceEditorFormatterHeading+ButtonActions.swift @@ -0,0 +1,54 @@ +import Foundation +import ComponentsObjC + +extension WKSourceEditorFormatterHeading { + func toggleHeadingFormatting(selectedHeading: WKEditorHeaderSelectViewModel.Configuration, currentSelectionState: WKSourceEditorSelectionState, textView: UITextView) { + + var currentStateIsParagraph = false + if currentSelectionState.isHeading { + expandSelectedRangeUpToNearestFormattingStrings(startingFormattingString: "==", endingFormattingString: "==", in: textView) + removeSurroundingFormattingStringsFromSelectedRange(startingFormattingString: "==", endingFormattingString: "==", in: textView) + } else if currentSelectionState.isSubheading1 { + expandSelectedRangeUpToNearestFormattingStrings(startingFormattingString: "===", endingFormattingString: "===", in: textView) + removeSurroundingFormattingStringsFromSelectedRange(startingFormattingString: "===", endingFormattingString: "===", in: textView) + } else if currentSelectionState.isSubheading2 { + expandSelectedRangeUpToNearestFormattingStrings(startingFormattingString: "====", endingFormattingString: "====", in: textView) + removeSurroundingFormattingStringsFromSelectedRange(startingFormattingString: "====", endingFormattingString: "====", in: textView) + } else if currentSelectionState.isSubheading3 { + expandSelectedRangeUpToNearestFormattingStrings(startingFormattingString: "=====", endingFormattingString: "=====", in: textView) + removeSurroundingFormattingStringsFromSelectedRange(startingFormattingString: "=====", endingFormattingString: "=====", in: textView) + } else if currentSelectionState.isSubheading4 { + expandSelectedRangeUpToNearestFormattingStrings(startingFormattingString: "======", endingFormattingString: "======", in: textView) + removeSurroundingFormattingStringsFromSelectedRange(startingFormattingString: "======", endingFormattingString: "======", in: textView) + } else { + currentStateIsParagraph = true + } + + let currentlySurroundedByLineBreaks = selectedRangeIsSurroundedByFormattingString(formattingString: "\n", in: textView) || textView.selectedRange.location == 0 && rangeIsFollowedByFormattingString(range: textView.selectedTextRange, formattingString: "\n", in: textView) + + let surroundingLineBreak = currentStateIsParagraph && !currentlySurroundedByLineBreaks ? "\n" : "" + let startingFormattingString: String + let endingFormattingString: String + switch selectedHeading { + case .paragraph: + return + case .heading: + startingFormattingString = surroundingLineBreak + "==" + endingFormattingString = "==" + surroundingLineBreak + case .subheading1: + startingFormattingString = surroundingLineBreak + "===" + endingFormattingString = "===" + surroundingLineBreak + case .subheading2: + startingFormattingString = surroundingLineBreak + "====" + endingFormattingString = "====" + surroundingLineBreak + case .subheading3: + startingFormattingString = surroundingLineBreak + "=====" + endingFormattingString = "=====" + surroundingLineBreak + case .subheading4: + startingFormattingString = surroundingLineBreak + "======" + endingFormattingString = "======" + surroundingLineBreak + } + + addStringFormattingCharacters(startingFormattingString: startingFormattingString, endingFormattingString: endingFormattingString, in: textView) + } +} diff --git a/Components/Sources/Components/Components/Editors/Source Editor/WKSourceEditorViewController.swift b/Components/Sources/Components/Components/Editors/Source Editor/WKSourceEditorViewController.swift index 4d594860296..9b38cef9b71 100644 --- a/Components/Sources/Components/Components/Editors/Source Editor/WKSourceEditorViewController.swift +++ b/Components/Sources/Components/Components/Editors/Source Editor/WKSourceEditorViewController.swift @@ -347,7 +347,7 @@ extension WKSourceEditorViewController: WKEditorToolbarHighlightViewDelegate { extension WKSourceEditorViewController: WKEditorInputViewDelegate { func didTapHeading(selectedHeading: WKEditorHeaderSelectViewModel.Configuration) { - + textFrameworkMediator.headingFormatter?.toggleHeadingFormatting(selectedHeading: selectedHeading, currentSelectionState: selectionState(), textView: textView) } func didTapBold(isSelected: Bool) { From 8ecf9bfd2b707ff2211832f05d6a1e4435dd6d4c Mon Sep 17 00:00:00 2001 From: Toni Sevener Date: Mon, 11 Dec 2023 19:43:50 -0600 Subject: [PATCH 15/18] Fix missing bold/italic syntax highlighting on iOS 15/6 --- .../Sources/ComponentsObjC/WKSourceEditorFormatterBase.m | 5 ++++- .../ComponentsObjC/WKSourceEditorFormatterBoldItalics.m | 1 - .../Sources/ComponentsObjC/WKSourceEditorFormatterHeading.m | 1 - 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Components/Sources/ComponentsObjC/WKSourceEditorFormatterBase.m b/Components/Sources/ComponentsObjC/WKSourceEditorFormatterBase.m index d7ad812afa3..049ab29f0fd 100644 --- a/Components/Sources/ComponentsObjC/WKSourceEditorFormatterBase.m +++ b/Components/Sources/ComponentsObjC/WKSourceEditorFormatterBase.m @@ -29,10 +29,13 @@ - (instancetype)initWithColors:(nonnull WKSourceEditorColors *)colors fonts:(non - (void)addSyntaxHighlightingToAttributedString:(NSMutableAttributedString *)attributedString inRange:(NSRange)range { - // reset old attributes + // reset base attributes [attributedString removeAttribute:NSFontAttributeName range:range]; [attributedString removeAttribute:NSForegroundColorAttributeName range:range]; + // reset shared custom attributes + [attributedString removeAttribute:WKSourceEditorCustomKeyColorOrange range:range]; + [attributedString addAttributes:self.attributes range:range]; } diff --git a/Components/Sources/ComponentsObjC/WKSourceEditorFormatterBoldItalics.m b/Components/Sources/ComponentsObjC/WKSourceEditorFormatterBoldItalics.m index aeff59f7baf..fa17430e996 100644 --- a/Components/Sources/ComponentsObjC/WKSourceEditorFormatterBoldItalics.m +++ b/Components/Sources/ComponentsObjC/WKSourceEditorFormatterBoldItalics.m @@ -59,7 +59,6 @@ - (instancetype)initWithColors:(WKSourceEditorColors *)colors fonts:(WKSourceEdi - (void)addSyntaxHighlightingToAttributedString:(nonnull NSMutableAttributedString *)attributedString inRange:(NSRange)range { // Reset - [attributedString removeAttribute:WKSourceEditorCustomKeyColorOrange range:range]; [attributedString removeAttribute:WKSourceEditorCustomKeyFontBoldItalics range:range]; [attributedString removeAttribute:WKSourceEditorCustomKeyFontBold range:range]; [attributedString removeAttribute:WKSourceEditorCustomKeyFontItalics range:range]; diff --git a/Components/Sources/ComponentsObjC/WKSourceEditorFormatterHeading.m b/Components/Sources/ComponentsObjC/WKSourceEditorFormatterHeading.m index dafa1f8ec77..ea50496ee21 100644 --- a/Components/Sources/ComponentsObjC/WKSourceEditorFormatterHeading.m +++ b/Components/Sources/ComponentsObjC/WKSourceEditorFormatterHeading.m @@ -76,7 +76,6 @@ - (instancetype)initWithColors:(WKSourceEditorColors *)colors fonts:(WKSourceEdi - (void)addSyntaxHighlightingToAttributedString:(nonnull NSMutableAttributedString *)attributedString inRange:(NSRange)range { // Reset - [attributedString removeAttribute:WKSourceEditorCustomKeyColorOrange range:range]; [attributedString removeAttribute:WKSourceEditorCustomKeyFontHeading range:range]; [attributedString removeAttribute:WKSourceEditorCustomKeyFontSubheading1 range:range]; [attributedString removeAttribute:WKSourceEditorCustomKeyFontSubheading2 range:range]; From d4a4c2849eded9a5e529f77b5fc4e88fdc1253e8 Mon Sep 17 00:00:00 2001 From: Toni Sevener Date: Tue, 12 Dec 2023 09:07:51 -0600 Subject: [PATCH 16/18] Add tests --- ...urceEditorFormatterButtonActionTests.swift | 110 ++++++++++++++++++ ...urceEditorTextFrameworkMediatorTests.swift | 100 ++++++++++++++++ 2 files changed, 210 insertions(+) diff --git a/Components/Tests/ComponentsTests/WKSourceEditorFormatterButtonActionTests.swift b/Components/Tests/ComponentsTests/WKSourceEditorFormatterButtonActionTests.swift index ee99fe49cd6..1854eed06ce 100644 --- a/Components/Tests/ComponentsTests/WKSourceEditorFormatterButtonActionTests.swift +++ b/Components/Tests/ComponentsTests/WKSourceEditorFormatterButtonActionTests.swift @@ -122,4 +122,114 @@ final class WKSourceEditorFormatterButtonActionTests: XCTestCase { mediator.templateFormatter?.toggleTemplateFormatting(action: .remove, in: mediator.textView) XCTAssertEqual(mediator.textView.attributedText.string, "One Two Three Four") } + + func testHeadingAdd() throws { + let text = "Test" + let selectedRange = NSRange(location: 0, length: 4) + let textView = mediator.textView + textView.attributedText = NSAttributedString(string: text) + textView.selectedRange = selectedRange + mediator.headingFormatter?.toggleHeadingFormatting(selectedHeading: .heading, currentSelectionState: mediator.selectionState(selectedDocumentRange: selectedRange), textView: textView) + XCTAssertEqual(mediator.textView.attributedText.string, "\n==Test==\n") + } + + func testHeadingRemove() throws { + let text = "==Test==" + let selectedRange = NSRange(location: 2, length: 4) + let textView = mediator.textView + textView.attributedText = NSAttributedString(string: text) + textView.selectedRange = selectedRange + mediator.headingFormatter?.toggleHeadingFormatting(selectedHeading: .paragraph, currentSelectionState: mediator.selectionState(selectedDocumentRange: selectedRange), textView: textView) + XCTAssertEqual(mediator.textView.attributedText.string, "Test") + } + + func testSubheading1Add() throws { + let text = "Test" + let selectedRange = NSRange(location: 0, length: 4) + let textView = mediator.textView + textView.attributedText = NSAttributedString(string: text) + textView.selectedRange = selectedRange + mediator.headingFormatter?.toggleHeadingFormatting(selectedHeading: .subheading1, currentSelectionState: mediator.selectionState(selectedDocumentRange: selectedRange), textView: textView) + XCTAssertEqual(mediator.textView.attributedText.string, "\n===Test===\n") + } + + func testSubheading1Remove() throws { + let text = "===Test===" + let selectedRange = NSRange(location: 3, length: 4) + let textView = mediator.textView + textView.attributedText = NSAttributedString(string: text) + textView.selectedRange = selectedRange + mediator.headingFormatter?.toggleHeadingFormatting(selectedHeading: .paragraph, currentSelectionState: mediator.selectionState(selectedDocumentRange: selectedRange), textView: textView) + XCTAssertEqual(mediator.textView.attributedText.string, "Test") + } + + func testSubheading2Add() throws { + let text = "Test" + let selectedRange = NSRange(location: 0, length: 4) + let textView = mediator.textView + textView.attributedText = NSAttributedString(string: text) + textView.selectedRange = selectedRange + mediator.headingFormatter?.toggleHeadingFormatting(selectedHeading: .subheading2, currentSelectionState: mediator.selectionState(selectedDocumentRange: selectedRange), textView: textView) + XCTAssertEqual(mediator.textView.attributedText.string, "\n====Test====\n") + } + + func testSubheading2Remove() throws { + let text = "====Test====" + let selectedRange = NSRange(location: 4, length: 4) + let textView = mediator.textView + textView.attributedText = NSAttributedString(string: text) + textView.selectedRange = selectedRange + mediator.headingFormatter?.toggleHeadingFormatting(selectedHeading: .paragraph, currentSelectionState: mediator.selectionState(selectedDocumentRange: selectedRange), textView: textView) + XCTAssertEqual(mediator.textView.attributedText.string, "Test") + } + + func testSubheading3Add() throws { + let text = "Test" + let selectedRange = NSRange(location: 0, length: 4) + let textView = mediator.textView + textView.attributedText = NSAttributedString(string: text) + textView.selectedRange = selectedRange + mediator.headingFormatter?.toggleHeadingFormatting(selectedHeading: .subheading3, currentSelectionState: mediator.selectionState(selectedDocumentRange: selectedRange), textView: textView) + XCTAssertEqual(mediator.textView.attributedText.string, "\n=====Test=====\n") + } + + func testSubheading3Remove() throws { + let text = "=====Test=====" + let selectedRange = NSRange(location: 5, length: 4) + let textView = mediator.textView + textView.attributedText = NSAttributedString(string: text) + textView.selectedRange = selectedRange + mediator.headingFormatter?.toggleHeadingFormatting(selectedHeading: .paragraph, currentSelectionState: mediator.selectionState(selectedDocumentRange: selectedRange), textView: textView) + XCTAssertEqual(mediator.textView.attributedText.string, "Test") + } + + func testSubheading4Add() throws { + let text = "Test" + let selectedRange = NSRange(location: 0, length: 4) + let textView = mediator.textView + textView.attributedText = NSAttributedString(string: text) + textView.selectedRange = selectedRange + mediator.headingFormatter?.toggleHeadingFormatting(selectedHeading: .subheading4, currentSelectionState: mediator.selectionState(selectedDocumentRange: selectedRange), textView: textView) + XCTAssertEqual(mediator.textView.attributedText.string, "\n======Test======\n") + } + + func testSubheading4Remove() throws { + let text = "======Test======" + let selectedRange = NSRange(location: 6, length: 4) + let textView = mediator.textView + textView.attributedText = NSAttributedString(string: text) + textView.selectedRange = selectedRange + mediator.headingFormatter?.toggleHeadingFormatting(selectedHeading: .paragraph, currentSelectionState: mediator.selectionState(selectedDocumentRange: selectedRange), textView: textView) + XCTAssertEqual(mediator.textView.attributedText.string, "Test") + } + + func testHeadingSwitchToSubheading3() throws { + let text = "==Test==" + let selectedRange = NSRange(location: 2, length: 4) + let textView = mediator.textView + textView.attributedText = NSAttributedString(string: text) + textView.selectedRange = selectedRange + mediator.headingFormatter?.toggleHeadingFormatting(selectedHeading: .subheading3, currentSelectionState: mediator.selectionState(selectedDocumentRange: selectedRange), textView: textView) + XCTAssertEqual(mediator.textView.attributedText.string, "=====Test=====") + } } diff --git a/Components/Tests/ComponentsTests/WKSourceEditorTextFrameworkMediatorTests.swift b/Components/Tests/ComponentsTests/WKSourceEditorTextFrameworkMediatorTests.swift index 5f01b4b4c71..91dfb53cf63 100644 --- a/Components/Tests/ComponentsTests/WKSourceEditorTextFrameworkMediatorTests.swift +++ b/Components/Tests/ComponentsTests/WKSourceEditorTextFrameworkMediatorTests.swift @@ -121,4 +121,104 @@ final class WKSourceEditorTextFrameworkMediatorTests: XCTestCase { let selectionStates1 = mediator.selectionState(selectedDocumentRange: NSRange(location: 1, length: 0)) XCTAssertFalse(selectionStates1.isHorizontalTemplate) } + + func testHeadingSelectionState() throws { + + let text = "== Test ==" + mediator.textView.attributedText = NSAttributedString(string: text) + + let selectionStates1 = mediator.selectionState(selectedDocumentRange: NSRange(location: 3, length: 4)) + XCTAssertTrue(selectionStates1.isHeading) + XCTAssertFalse(selectionStates1.isSubheading1) + XCTAssertFalse(selectionStates1.isSubheading2) + XCTAssertFalse(selectionStates1.isSubheading3) + XCTAssertFalse(selectionStates1.isSubheading4) + + let selectionStates2 = mediator.selectionState(selectedDocumentRange: NSRange(location: 6, length: 0)) + XCTAssertTrue(selectionStates2.isHeading) + XCTAssertFalse(selectionStates2.isSubheading1) + XCTAssertFalse(selectionStates2.isSubheading2) + XCTAssertFalse(selectionStates2.isSubheading3) + XCTAssertFalse(selectionStates2.isSubheading4) + } + + func testSubheading1SelectionState() throws { + + let text = "=== Test ===" + mediator.textView.attributedText = NSAttributedString(string: text) + + let selectionStates1 = mediator.selectionState(selectedDocumentRange: NSRange(location: 4, length: 4)) + XCTAssertFalse(selectionStates1.isHeading) + XCTAssertTrue(selectionStates1.isSubheading1) + XCTAssertFalse(selectionStates1.isSubheading2) + XCTAssertFalse(selectionStates1.isSubheading3) + XCTAssertFalse(selectionStates1.isSubheading4) + + let selectionStates2 = mediator.selectionState(selectedDocumentRange: NSRange(location: 6, length: 0)) + XCTAssertFalse(selectionStates2.isHeading) + XCTAssertTrue(selectionStates2.isSubheading1) + XCTAssertFalse(selectionStates2.isSubheading2) + XCTAssertFalse(selectionStates2.isSubheading3) + XCTAssertFalse(selectionStates2.isSubheading4) + } + + func testSubheading2SelectionState() throws { + + let text = "==== Test ====" + mediator.textView.attributedText = NSAttributedString(string: text) + + let selectionStates1 = mediator.selectionState(selectedDocumentRange: NSRange(location: 5, length: 4)) + XCTAssertFalse(selectionStates1.isHeading) + XCTAssertFalse(selectionStates1.isSubheading1) + XCTAssertTrue(selectionStates1.isSubheading2) + XCTAssertFalse(selectionStates1.isSubheading3) + XCTAssertFalse(selectionStates1.isSubheading4) + + let selectionStates2 = mediator.selectionState(selectedDocumentRange: NSRange(location: 7, length: 0)) + XCTAssertFalse(selectionStates2.isHeading) + XCTAssertFalse(selectionStates2.isSubheading1) + XCTAssertTrue(selectionStates2.isSubheading2) + XCTAssertFalse(selectionStates2.isSubheading3) + XCTAssertFalse(selectionStates2.isSubheading4) + } + + func testSubheading3SelectionState() throws { + + let text = "===== Test =====" + mediator.textView.attributedText = NSAttributedString(string: text) + + let selectionStates1 = mediator.selectionState(selectedDocumentRange: NSRange(location: 6, length: 4)) + XCTAssertFalse(selectionStates1.isHeading) + XCTAssertFalse(selectionStates1.isSubheading1) + XCTAssertFalse(selectionStates1.isSubheading2) + XCTAssertTrue(selectionStates1.isSubheading3) + XCTAssertFalse(selectionStates1.isSubheading4) + + let selectionStates2 = mediator.selectionState(selectedDocumentRange: NSRange(location: 8, length: 0)) + XCTAssertFalse(selectionStates2.isHeading) + XCTAssertFalse(selectionStates2.isSubheading1) + XCTAssertFalse(selectionStates2.isSubheading2) + XCTAssertTrue(selectionStates2.isSubheading3) + XCTAssertFalse(selectionStates2.isSubheading4) + } + + func testSubheading4SelectionState() throws { + + let text = "====== Test ======" + mediator.textView.attributedText = NSAttributedString(string: text) + + let selectionStates1 = mediator.selectionState(selectedDocumentRange: NSRange(location: 7, length: 4)) + XCTAssertFalse(selectionStates1.isHeading) + XCTAssertFalse(selectionStates1.isSubheading1) + XCTAssertFalse(selectionStates1.isSubheading2) + XCTAssertFalse(selectionStates1.isSubheading3) + XCTAssertTrue(selectionStates1.isSubheading4) + + let selectionStates2 = mediator.selectionState(selectedDocumentRange: NSRange(location: 9, length: 0)) + XCTAssertFalse(selectionStates2.isHeading) + XCTAssertFalse(selectionStates2.isSubheading1) + XCTAssertFalse(selectionStates2.isSubheading2) + XCTAssertFalse(selectionStates2.isSubheading3) + XCTAssertTrue(selectionStates2.isSubheading4) + } } From bc00a5ad2cf9ced76d95672283ad25b0d1ca8f6f Mon Sep 17 00:00:00 2001 From: Toni Sevener Date: Tue, 12 Dec 2023 09:52:12 -0600 Subject: [PATCH 17/18] Fix RTL and last character cursor selection state bugs --- .../WKSourceEditorFormatterHeading.m | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/Components/Sources/ComponentsObjC/WKSourceEditorFormatterHeading.m b/Components/Sources/ComponentsObjC/WKSourceEditorFormatterHeading.m index a5fd86edfa4..0d36a57eaac 100644 --- a/Components/Sources/ComponentsObjC/WKSourceEditorFormatterHeading.m +++ b/Components/Sources/ComponentsObjC/WKSourceEditorFormatterHeading.m @@ -174,17 +174,33 @@ - (BOOL)attributedString:(NSMutableAttributedString *)attributedString isContent if (attrs[contentKey] != nil) { isContentKey = YES; + } else { + // Edge case, check previous character if we are up against closing string + if (attrs[WKSourceEditorCustomKeyColorOrange]) { + attrs = [attributedString attributesAtIndex:range.location - 1 effectiveRange:nil]; + if (attrs[contentKey] != nil) { + isContentKey = YES; + } + } } } } else { + __block NSRange unionRange = NSMakeRange(NSNotFound, 0); [attributedString enumerateAttributesInRange:range options:nil usingBlock:^(NSDictionary * _Nonnull attrs, NSRange loopRange, BOOL * _Nonnull stop) { - if ((attrs[contentKey] != nil) && - (loopRange.location == range.location && loopRange.length == range.length)) { - isContentKey = YES; + if (attrs[contentKey] != nil) { + if (unionRange.location == NSNotFound) { + unionRange = loopRange; + } else { + unionRange = NSUnionRange(unionRange, loopRange); + } stop = YES; } }]; + + if (NSEqualRanges(unionRange, range)) { + isContentKey = YES; + } } return isContentKey; From b5e74291d646cc418bf427212f4c16e3f103ac21 Mon Sep 17 00:00:00 2001 From: Toni Sevener Date: Tue, 9 Jan 2024 16:19:08 -0600 Subject: [PATCH 18/18] Bug fixes after merge --- .../Input Views/WKEditorInputView.swift | 45 ++++++++++++++++--- ...EditorFormatterHeading+ButtonActions.swift | 2 +- .../WKSourceEditorViewController.swift | 4 +- 3 files changed, 42 insertions(+), 9 deletions(-) diff --git a/Components/Sources/Components/Components/Editors/Common Views/Input Views/WKEditorInputView.swift b/Components/Sources/Components/Components/Editors/Common Views/Input Views/WKEditorInputView.swift index 9e6fead4fc2..3b14344aa86 100644 --- a/Components/Sources/Components/Components/Editors/Common Views/Input Views/WKEditorInputView.swift +++ b/Components/Sources/Components/Components/Editors/Common Views/Input Views/WKEditorInputView.swift @@ -6,6 +6,7 @@ protocol WKEditorInputViewDelegate: AnyObject { func didTapBold(isSelected: Bool) func didTapItalics(isSelected: Bool) func didTapTemplate(isSelected: Bool) + func didTapHeading(type: WKEditorInputView.HeadingButtonType) func didTapStrikethrough(isSelected: Bool) } @@ -221,6 +222,18 @@ class WKEditorInputView: WKComponentView { ]) updateColors() + + NotificationCenter.default.addObserver(self, selector: #selector(updateButtonSelectionState(_:)), name: Notification.WKSourceEditorSelectionState, object: nil) + } + + // MARK: - Notifications + + @objc private func updateButtonSelectionState(_ notification: NSNotification) { + guard let selectionState = notification.userInfo?[Notification.WKSourceEditorSelectionStateKey] as? WKSourceEditorSelectionState else { + return + } + + configure(selectionState: selectionState) } // MARK: - Overrides @@ -283,20 +296,40 @@ class WKEditorInputView: WKComponentView { switch type { case .paragraph: - paragraphButton.isSelected.toggle() + paragraphButton.isSelected = true case .heading: - headerButton.isSelected.toggle() + headerButton.isSelected = true case .subheading1: - subheader1Button.isSelected.toggle() + subheader1Button.isSelected = true case .subheading2: - subheader2Button.isSelected.toggle() + subheader2Button.isSelected = true case .subheading3: - subheader3Button.isSelected.toggle() + subheader3Button.isSelected = true case .subheading4: - subheader4Button.isSelected.toggle() + subheader4Button.isSelected = true } + + delegate?.didTapHeading(type: type) }) return UIButton(configuration: configuration, primaryAction: action) } + + func configure(selectionState: WKSourceEditorSelectionState) { + headingButtons.forEach { $0.isSelected = false } + + if selectionState.isHeading { + headerButton.isSelected = true + } else if selectionState.isSubheading1 { + subheader1Button.isSelected = true + } else if selectionState.isSubheading2 { + subheader2Button.isSelected = true + } else if selectionState.isSubheading3 { + subheader3Button.isSelected = true + } else if selectionState.isSubheading4 { + subheader4Button.isSelected = true + } else { + paragraphButton.isSelected = true + } + } } diff --git a/Components/Sources/Components/Components/Editors/Source Editor/Formatter Extensions/WKSourceEditorFormatterHeading+ButtonActions.swift b/Components/Sources/Components/Components/Editors/Source Editor/Formatter Extensions/WKSourceEditorFormatterHeading+ButtonActions.swift index 123e10f1903..869c6502bcd 100644 --- a/Components/Sources/Components/Components/Editors/Source Editor/Formatter Extensions/WKSourceEditorFormatterHeading+ButtonActions.swift +++ b/Components/Sources/Components/Components/Editors/Source Editor/Formatter Extensions/WKSourceEditorFormatterHeading+ButtonActions.swift @@ -2,7 +2,7 @@ import Foundation import ComponentsObjC extension WKSourceEditorFormatterHeading { - func toggleHeadingFormatting(selectedHeading: WKEditorHeaderSelectViewModel.Configuration, currentSelectionState: WKSourceEditorSelectionState, textView: UITextView) { + func toggleHeadingFormatting(selectedHeading: WKEditorInputView.HeadingButtonType, currentSelectionState: WKSourceEditorSelectionState, textView: UITextView) { var currentStateIsParagraph = false if currentSelectionState.isHeading { diff --git a/Components/Sources/Components/Components/Editors/Source Editor/WKSourceEditorViewController.swift b/Components/Sources/Components/Components/Editors/Source Editor/WKSourceEditorViewController.swift index bdea5b34ccd..bb43e24f3b9 100644 --- a/Components/Sources/Components/Components/Editors/Source Editor/WKSourceEditorViewController.swift +++ b/Components/Sources/Components/Components/Editors/Source Editor/WKSourceEditorViewController.swift @@ -291,8 +291,8 @@ extension WKSourceEditorViewController: WKEditorToolbarHighlightViewDelegate { // MARK: - WKEditorInputViewDelegate extension WKSourceEditorViewController: WKEditorInputViewDelegate { - func didTapHeading(selectedHeading: WKEditorHeaderSelectViewModel.Configuration) { - textFrameworkMediator.headingFormatter?.toggleHeadingFormatting(selectedHeading: selectedHeading, currentSelectionState: selectionState(), textView: textView) + func didTapHeading(type: WKEditorInputView.HeadingButtonType) { + textFrameworkMediator.headingFormatter?.toggleHeadingFormatting(selectedHeading: type, currentSelectionState: selectionState(), textView: textView) } func didTapBold(isSelected: Bool) {