Skip to content

Commit

Permalink
Add link syntax highlighting with WKSourceEditorFormatterLink
Browse files Browse the repository at this point in the history
  • Loading branch information
tonisevener committed Jan 5, 2024
1 parent 6915b09 commit ea05280
Show file tree
Hide file tree
Showing 7 changed files with 228 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ final class WKSourceEditorTextFrameworkMediator: NSObject {
private(set) var boldItalicsFormatter: WKSourceEditorFormatterBoldItalics?
private(set) var templateFormatter: WKSourceEditorFormatterTemplate?
private(set) var strikethroughFormatter: WKSourceEditorFormatterStrikethrough?
private(set) var linkFormatter: WKSourceEditorFormatterLink?

var isSyntaxHighlightingEnabled: Bool = true {
didSet {
Expand Down Expand Up @@ -107,14 +108,17 @@ final class WKSourceEditorTextFrameworkMediator: NSObject {
let boldItalicsFormatter = WKSourceEditorFormatterBoldItalics(colors: colors, fonts: fonts)
let templateFormatter = WKSourceEditorFormatterTemplate(colors: colors, fonts: fonts)
let strikethroughFormatter = WKSourceEditorFormatterStrikethrough(colors: colors, fonts: fonts)
let linkFormatter = WKSourceEditorFormatterLink(colors: colors, fonts: fonts)

self.formatters = [WKSourceEditorFormatterBase(colors: colors, fonts: fonts, textAlignment: viewModel.textAlignment),
templateFormatter,
boldItalicsFormatter,
strikethroughFormatter]
strikethroughFormatter,
linkFormatter]
self.boldItalicsFormatter = boldItalicsFormatter
self.templateFormatter = templateFormatter
self.strikethroughFormatter = strikethroughFormatter
self.linkFormatter = linkFormatter

if needsTextKit2 {
if #available(iOS 16.0, *) {
Expand Down Expand Up @@ -211,6 +215,7 @@ extension WKSourceEditorTextFrameworkMediator: WKSourceEditorStorageDelegate {
colors.orangeForegroundColor = isSyntaxHighlightingEnabled ? WKAppEnvironment.current.theme.editorOrange : WKAppEnvironment.current.theme.text
colors.purpleForegroundColor = isSyntaxHighlightingEnabled ? WKAppEnvironment.current.theme.editorPurple : WKAppEnvironment.current.theme.text
colors.greenForegroundColor = isSyntaxHighlightingEnabled ? WKAppEnvironment.current.theme.editorGreen : WKAppEnvironment.current.theme.text
colors.blueForegroundColor = isSyntaxHighlightingEnabled ? WKAppEnvironment.current.theme.editorBlue : WKAppEnvironment.current.theme.text
return colors
}

Expand Down
13 changes: 9 additions & 4 deletions Components/Sources/Components/Style/WKTheme.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ public struct WKTheme: Equatable {
public let editorOrange: UIColor
public let editorPurple: UIColor
public let editorGreen: UIColor
public let editorBlue: UIColor

public static let light = WKTheme(
name: "Light",
Expand All @@ -52,7 +53,8 @@ public struct WKTheme: Equatable {
diffCompareAccent: WKColor.orange600,
editorOrange: WKColor.orange600,
editorPurple: WKColor.purple600,
editorGreen: WKColor.green600
editorGreen: WKColor.green600,
editorBlue: WKColor.blue600
)

public static let sepia = WKTheme(
Expand All @@ -79,7 +81,8 @@ public struct WKTheme: Equatable {
diffCompareAccent: WKColor.orange600,
editorOrange: WKColor.orange600,
editorPurple: WKColor.purple600,
editorGreen: WKColor.green600
editorGreen: WKColor.green600,
editorBlue: WKColor.blue600
)

public static let dark = WKTheme(
Expand All @@ -106,7 +109,8 @@ public struct WKTheme: Equatable {
diffCompareAccent: WKColor.orange600,
editorOrange: WKColor.yellow600,
editorPurple: WKColor.red100,
editorGreen: WKColor.green600
editorGreen: WKColor.green600,
editorBlue: WKColor.blue300
)

public static let black = WKTheme(
Expand All @@ -133,7 +137,8 @@ public struct WKTheme: Equatable {
diffCompareAccent: WKColor.orange600,
editorOrange: WKColor.yellow600,
editorPurple: WKColor.red100,
editorGreen: WKColor.green600
editorGreen: WKColor.green600,
editorBlue: WKColor.blue300
)

}
1 change: 1 addition & 0 deletions Components/Sources/ComponentsObjC/WKSourceEditorColors.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic, strong) UIColor *orangeForegroundColor;
@property (nonatomic, strong) UIColor *purpleForegroundColor;
@property (nonatomic, strong) UIColor *greenForegroundColor;
@property (nonatomic, strong) UIColor *blueForegroundColor;
@end

NS_ASSUME_NONNULL_END
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#import "WKSourceEditorFormatter.h"

NS_ASSUME_NONNULL_BEGIN

@interface WKSourceEditorFormatterLink : WKSourceEditorFormatter

@end

NS_ASSUME_NONNULL_END
201 changes: 201 additions & 0 deletions Components/Sources/ComponentsObjC/WKSourceEditorFormatterLink.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
#import "WKSourceEditorFormatterLink.h"
#import "WKSourceEditorColors.h"

@interface WKSourceEditorFormatterLink ()

@property (nonatomic, strong) NSDictionary *linkMarkupAttributes;
@property (nonatomic, strong) NSDictionary *linkContentAttributes;
@property (nonatomic, strong) NSDictionary *linkWithNestedLinkMarkupAndContentAttributes;
@property (nonatomic, strong) NSRegularExpression *linkRegex;
@property (nonatomic, strong) NSRegularExpression *linkWithNestedLinkRegex;

@end

#pragma mark - Custom Attributed String Keys

NSString * const WKSourceEditorCustomKeyColorBlue = @"WKSourceEditorCustomKeyColorBlue";
NSString * const WKSourceEditorCustomKeyMarkupLink = @"WKSourceEditorCustomKeyMarkupLink";
NSString * const WKSourceEditorCustomKeyContentLink = @"WKSourceEditorCustomKeyContentLink";
NSString * const WKSourceEditorCustomKeyMarkupAndContentLinkWithNestedLink = @"WKSourceEditorCustomKeyMarkupAndContentLinkWithNestedLink";

@implementation WKSourceEditorFormatterLink

#pragma mark - Public

- (instancetype)initWithColors:(nonnull WKSourceEditorColors *)colors fonts:(nonnull WKSourceEditorFonts *)fonts {
self = [super initWithColors:colors fonts:fonts];
if (self) {
_linkMarkupAttributes = @{
WKSourceEditorCustomKeyMarkupLink: [NSNumber numberWithBool:YES],
NSForegroundColorAttributeName: colors.blueForegroundColor,
WKSourceEditorCustomKeyColorBlue: [NSNumber numberWithBool:YES]
};

_linkContentAttributes = @{
WKSourceEditorCustomKeyContentLink: [NSNumber numberWithBool:YES],
NSForegroundColorAttributeName: colors.blueForegroundColor,
WKSourceEditorCustomKeyColorBlue: [NSNumber numberWithBool:YES]
};

_linkRegex = [[NSRegularExpression alloc] initWithPattern:@"(\\[{2})([^\\[\\]\\n]*)(\\]{2})" options:0 error:nil];

_linkWithNestedLinkMarkupAndContentAttributes = @{
WKSourceEditorCustomKeyMarkupAndContentLinkWithNestedLink: [NSNumber numberWithBool:YES],
NSForegroundColorAttributeName: colors.blueForegroundColor,
WKSourceEditorCustomKeyColorBlue: [NSNumber numberWithBool:YES]
};

_linkWithNestedLinkRegex = [[NSRegularExpression alloc] initWithPattern:@"\\[{2}[^\\[\\]\\n]*\\[{2}" options:0 error:nil];
}

return self;
}

#pragma mark - Overrides

- (void)addSyntaxHighlightingToAttributedString:(nonnull NSMutableAttributedString *)attributedString inRange:(NSRange)range {

// Reset
[attributedString removeAttribute:WKSourceEditorCustomKeyColorBlue range:range];
[attributedString removeAttribute:WKSourceEditorCustomKeyContentLink range:range];
[attributedString removeAttribute:WKSourceEditorCustomKeyMarkupAndContentLinkWithNestedLink range:range];

// This section finds and highlights simple links that do NOT contain nested links, e.g. [[Cat]] and [[Dog|puppy]].
[self.linkRegex 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 contentRange = [result rangeAtIndex:2];
NSRange closingRange = [result rangeAtIndex:3];

if (openingRange.location != NSNotFound) {
[attributedString addAttributes:self.linkMarkupAttributes range:openingRange];
}

if (contentRange.location != NSNotFound) {
[attributedString addAttributes:self.linkContentAttributes range:contentRange];
}

if (closingRange.location != NSNotFound) {
[attributedString addAttributes:self.linkMarkupAttributes range:closingRange];
}
}];

// Note: This section finds and highlights links with nested links, which is common in image links. The regex matches any opening markup [[ followed by non-markup characters, then another opening markup [[. We then start to loop character-by-character, matching opening and closing tags to find and highlight links that contain other links.
// Originally I tried to allow for infinite nested links via regex alone, but it performed too poorly.
[self.linkWithNestedLinkRegex enumerateMatchesInString:attributedString.string
options:0
range:range
usingBlock:^(NSTextCheckingResult *_Nullable result, NSMatchingFlags flags, BOOL *_Nonnull stop) {

NSRange match = [result rangeAtIndex:0];

if (match.location != NSNotFound) {

NSArray *linkWithNestedLinkRanges = [self linkWithNestedLinkRangesInString:attributedString.string startingIndex:match.location];

for (NSValue *value in linkWithNestedLinkRanges) {
NSRange range = [value rangeValue];
if (range.location != NSNotFound) {
[attributedString addAttributes:self.linkWithNestedLinkMarkupAndContentAttributes range:range];
}
}
}
}];
}

- (void)updateColors:(WKSourceEditorColors *)colors inAttributedString:(NSMutableAttributedString *)attributedString inRange:(NSRange)range {

NSMutableDictionary *mutLinkMarkupAttributes = [[NSMutableDictionary alloc] initWithDictionary:self.linkMarkupAttributes];
[mutLinkMarkupAttributes setObject:colors.blueForegroundColor forKey:NSForegroundColorAttributeName];
self.linkMarkupAttributes = [[NSDictionary alloc] initWithDictionary:mutLinkMarkupAttributes];

NSMutableDictionary *mutLinkContentAttributes = [[NSMutableDictionary alloc] initWithDictionary:self.linkContentAttributes];
[mutLinkContentAttributes setObject:colors.blueForegroundColor forKey:NSForegroundColorAttributeName];
self.linkContentAttributes = [[NSDictionary alloc] initWithDictionary:mutLinkContentAttributes];

NSMutableDictionary *mutLinkWithNestedLinkMarkupAndContentAttributes = [[NSMutableDictionary alloc] initWithDictionary:self.linkWithNestedLinkMarkupAndContentAttributes];
[mutLinkWithNestedLinkMarkupAndContentAttributes setObject:colors.blueForegroundColor forKey:NSForegroundColorAttributeName];
self.linkWithNestedLinkMarkupAndContentAttributes = [[NSDictionary alloc] initWithDictionary:mutLinkWithNestedLinkMarkupAndContentAttributes];

[attributedString enumerateAttribute:WKSourceEditorCustomKeyColorBlue
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:@{NSForegroundColorAttributeName: colors.blueForegroundColor} range:localRange];
}
}
}];
}

- (void)updateFonts:(WKSourceEditorFonts *)fonts inAttributedString:(NSMutableAttributedString *)attributedString inRange:(NSRange)range {
// No special font handling needed
}

#pragma mark - Private

- (NSArray *)linkWithNestedLinkRangesInString: (NSString *)string startingIndex: (NSUInteger)index {
NSMutableArray *openingRanges = [[NSMutableArray alloc] init];
NSMutableArray *completedLinkRanges = [[NSMutableArray alloc] init];
NSMutableArray *completedLinkWithNestedLinkRanges = [[NSMutableArray alloc] init];

// Loop through and evaluate characters in pairs, keeping track of opening and closing pairs
BOOL lastCompletedLinkRangeWasNested = NO;
for (NSUInteger i = index; i < string.length; i++) {

unichar currentChar = [string characterAtIndex:i];

if (currentChar == '\n') {
break;
}

if (i + 1 >= string.length) {
break;
}

NSString *currentCharString = [NSString stringWithFormat:@"%c", currentChar];
unichar nextChar = [string characterAtIndex:i + 1];
NSString *nextCharString = [NSString stringWithFormat:@"%c", nextChar];
NSString *pair = [NSString stringWithFormat:@"%@%@", currentCharString, nextCharString];

if ([pair isEqualToString:@"[["]) {
[openingRanges addObject:[NSValue valueWithRange:NSMakeRange(i, 2)]];
}

if ([pair isEqualToString:@"]]"] && openingRanges.count == 0) {
// invalid, closed markup before opening
break;
}

if ([pair isEqualToString:@"]]"]) {

NSValue *lastOpeningRange = openingRanges.lastObject;
if (lastOpeningRange) {
[openingRanges removeLastObject];
}

NSRange unionRange = NSUnionRange(lastOpeningRange.rangeValue, NSMakeRange(i, 2));
NSValue *linkRange = [NSValue valueWithRange:unionRange];
[completedLinkRanges addObject: linkRange];

if (lastCompletedLinkRangeWasNested && openingRanges.count == 0) {
[completedLinkWithNestedLinkRanges addObject:linkRange];
}

if (openingRanges.count > 0) {
lastCompletedLinkRangeWasNested = YES;
} else {
lastCompletedLinkRangeWasNested = NO;
}
}
}

return [[NSArray alloc] initWithArray:completedLinkWithNestedLinkRanges];
}

@end
1 change: 1 addition & 0 deletions Components/Sources/ComponentsObjC/include/ComponentsObjC.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#import "WKSourceEditorFormatterBoldItalics.h"
#import "WKSourceEditorFormatterTemplate.h"
#import "WKSourceEditorFormatterStrikethrough.h"
#import "WKSourceEditorFormatterLink.h"
#import "WKSourceEditorStorageDelegate.h"

#endif /* Header_h */

0 comments on commit ea05280

Please sign in to comment.