-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathNSAttributedString+TJFormatting.m
86 lines (78 loc) · 4.58 KB
/
NSAttributedString+TJFormatting.m
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
//
// NSAttributedString+TJFormatting.m
// OpenerCore
//
// Created by Tim Johnsen on 2/10/18.
// Copyright © 2018 tijo. All rights reserved.
//
#import "NSAttributedString+TJFormatting.h"
#if defined(__has_attribute) && __has_attribute(objc_direct_members)
__attribute__((objc_direct_members))
#endif
@implementation NSAttributedString (TJFormatting)
+ (instancetype)attributedStringWithMarkupString:(NSString *const)markupString
attributes:(nullable NSDictionary<NSAttributedStringKey,id> *const)attributes
customizerBlock:(TJFormattingCustomizerBlock)block
{
return [self attributedStringWithMarkupString:markupString
supportNesting:YES
attributes:attributes
customizerBlock:block];
}
+ (instancetype)attributedStringWithMarkupString:(NSString *const)markupString
supportNesting:(const BOOL)supportNesting
attributes:(NSDictionary<NSAttributedStringKey,id> *const)attributes
customizerBlock:(TJFormattingCustomizerBlock)block
{
static NSRegularExpression *regex;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
regex = [NSRegularExpression regularExpressionWithPattern:@"<(.*?)>(.*?)</\\1>" options:NSRegularExpressionDotMatchesLineSeparators error:nil];
});
NSMutableAttributedString *const mutableAttributedString = (markupString != nil) ? [[NSMutableAttributedString alloc] initWithString:markupString attributes:attributes] : nil;
BOOL mayContainUnhandledTags = YES;
while (mayContainUnhandledTags) {
NSString *const underlyingString = mutableAttributedString.string;
const NSUInteger underlyingStringLength = underlyingString.length;
if (!underlyingStringLength) {
break;
}
NSArray<NSTextCheckingResult *> *const matches = [regex matchesInString:underlyingString options:0 range:NSMakeRange(0, underlyingStringLength)];
mayContainUnhandledTags = supportNesting && matches.count > 0;
for (NSTextCheckingResult *const result in matches.reverseObjectEnumerator) {
NSString *const parsedTag = [underlyingString substringWithRange:[result rangeAtIndex:1]];
const NSRange textRange = [result rangeAtIndex:2];
const NSRange fullRange = result.range;
if (supportNesting) {
// Apply attributes if needed
[mutableAttributedString enumerateAttributesInRange:textRange
options:0
usingBlock:^(NSDictionary<NSAttributedStringKey,id> * _Nonnull subrangeAttributes, NSRange subrange, BOOL * _Nonnull stop) {
NSDictionary *const customizedAttributes = block(parsedTag, subrangeAttributes);
if (customizedAttributes.count) {
[mutableAttributedString addAttributes:customizedAttributes range:subrange];
}
}];
// Remove enclosing tags
[mutableAttributedString replaceCharactersInRange:fullRange
withAttributedString:[mutableAttributedString attributedSubstringFromRange:textRange]];
} else {
NSString *const text = [underlyingString substringWithRange:textRange];
NSDictionary<NSAttributedStringKey, id> *const customizedAttributes = block(parsedTag, attributes);
if (customizedAttributes.count) {
// Apply attributes if needed
NSMutableDictionary<NSAttributedStringKey, id> *const mutableAttributes = [NSMutableDictionary dictionaryWithDictionary:attributes];
[mutableAttributes addEntriesFromDictionary:customizedAttributes];
[mutableAttributedString replaceCharactersInRange:fullRange
withAttributedString:[[NSAttributedString alloc] initWithString:text attributes:mutableAttributes]];
} else {
// Otherwise just remove enclosing tags
[mutableAttributedString replaceCharactersInRange:fullRange
withString:text];
}
}
}
}
return mutableAttributedString ? [[self alloc] initWithAttributedString:mutableAttributedString] : nil;
}
@end