/
NSMutableAttributedString+HTML.m
313 lines (244 loc) · 8.73 KB
/
NSMutableAttributedString+HTML.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
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
//
// NSMutableAttributedString+HTML.m
// DTCoreText
//
// Created by Oliver Drobnik on 4/14/11.
// Copyright 2011 Drobnik.com. All rights reserved.
//
#import "NSMutableAttributedString+HTML.h"
#import "DTCoreTextFontDescriptor.h"
#import "DTCoreTextParagraphStyle.h"
#import "NSDictionary+DTCoreText.h"
#if TARGET_OS_IPHONE
#import "UIFont+DTCoreText.h"
#endif
@implementation NSMutableAttributedString (HTML)
// appends a plain string extending the attributes at this position
- (void)appendString:(NSString *)string
{
NSParameterAssert(string);
NSUInteger length = [self length];
NSAttributedString *appendString = nil;
if (length)
{
// get attributes at end of self
NSMutableDictionary *attributes = [[self attributesAtIndex:length-1 effectiveRange:NULL] mutableCopy];
// we need to remove the image placeholder (if any) to prevent duplication
[attributes removeObjectForKey:NSAttachmentAttributeName];
[attributes removeObjectForKey:(id)kCTRunDelegateAttributeName];
// we also remove field attribute, because appending plain strings should never extend an field
[attributes removeObjectForKey:DTFieldAttribute];
// create a temp attributed string from the appended part
appendString = [[NSAttributedString alloc] initWithString:string attributes:attributes];
}
else
{
// no attributes to extend
appendString = [[NSAttributedString alloc] initWithString:string];
}
[self appendAttributedString:appendString];
}
- (void)appendString:(NSString *)string withParagraphStyle:(DTCoreTextParagraphStyle *)paragraphStyle fontDescriptor:(DTCoreTextFontDescriptor *)fontDescriptor
{
NSUInteger selfLengthBefore = [self length];
[self.mutableString appendString:string];
NSRange appendedStringRange = NSMakeRange(selfLengthBefore, [string length]);
if (paragraphStyle || fontDescriptor)
{
NSMutableDictionary *attributes = [NSMutableDictionary dictionary];
if (paragraphStyle)
{
#if DTCORETEXT_SUPPORT_NS_ATTRIBUTES
if (___useiOS6Attributes)
{
NSParagraphStyle *style = [paragraphStyle NSParagraphStyle];
[attributes setObject:style forKey:NSParagraphStyleAttributeName];
}
else
#endif
{
CTParagraphStyleRef newParagraphStyle = [paragraphStyle createCTParagraphStyle];
[attributes setObject:CFBridgingRelease(newParagraphStyle) forKey:(id)kCTParagraphStyleAttributeName];
}
}
if (fontDescriptor)
{
CTFontRef newFont = [fontDescriptor newMatchingFont];
if (newFont)
{
#if DTCORETEXT_SUPPORT_NS_ATTRIBUTES && TARGET_OS_IPHONE
if (___useiOS6Attributes)
{
// convert to UIFont
UIFont *uiFont = [UIFont fontWithCTFont:newFont];
[attributes setObject:uiFont forKey:NSFontAttributeName];
CFRelease(newFont);
}
else
#endif
{
[attributes setObject:CFBridgingRelease(newFont) forKey:(id)kCTFontAttributeName];
}
}
}
// Replace attributes
[self setAttributes:attributes range:appendedStringRange];
}
else
{
// Remove attributes
[self setAttributes:[NSDictionary dictionary] range:appendedStringRange];
}
}
- (void)appendEndOfParagraph
{
NSUInteger length = [self length];
NSAssert(length, @"Cannot append end of paragraph to empty string");
NSRange effectiveRange;
NSDictionary *attributes = [self attributesAtIndex:length-1 effectiveRange:&effectiveRange];
NSMutableDictionary *appendAttributes = [NSMutableDictionary dictionary];
#if DTCORETEXT_SUPPORT_NS_ATTRIBUTES
if (___useiOS6Attributes)
{
id font = [attributes objectForKey:NSFontAttributeName];
if (font)
{
[appendAttributes setObject:font forKey:NSFontAttributeName];
}
id paragraphStyle = [attributes objectForKey:NSParagraphStyleAttributeName];
if (paragraphStyle)
{
[appendAttributes setObject:paragraphStyle forKey:NSParagraphStyleAttributeName];
}
}
else
#endif
{
CTFontRef font = (__bridge CTFontRef)[attributes objectForKey:(id)kCTFontAttributeName];
if (font)
{
[appendAttributes setObject:(__bridge id)(font) forKey:(id)kCTFontAttributeName];
}
CTParagraphStyleRef paragraphStyle = (__bridge CTParagraphStyleRef)[attributes objectForKey:(id)kCTParagraphStyleAttributeName];
if (paragraphStyle)
{
[appendAttributes setObject:(__bridge id)(paragraphStyle) forKey:(id)kCTParagraphStyleAttributeName];
}
}
// transfer blocks
NSArray *blocks = [attributes objectForKey:DTTextBlocksAttribute];
if (blocks)
{
[appendAttributes setObject:blocks forKey:DTTextBlocksAttribute];
}
// transfer lists
NSArray *lists = [attributes objectForKey:DTTextListsAttribute];
if (lists)
{
[appendAttributes setObject:lists forKey:DTTextListsAttribute];
}
// transfer foreground color
#if DTCORETEXT_SUPPORT_NS_ATTRIBUTES
if (___useiOS6Attributes)
{
id foregroundColor = [attributes objectForKey:NSForegroundColorAttributeName];
if (foregroundColor)
{
[appendAttributes setObject:foregroundColor forKey:NSForegroundColorAttributeName];
}
}
else
#endif
{
id foregroundColor = [attributes objectForKey:(id)kCTForegroundColorAttributeName];
if (foregroundColor)
{
#if TARGET_OS_IPHONE
if ([foregroundColor isKindOfClass:[UIColor class]])
{
[appendAttributes setObject:(id)[foregroundColor CGColor] forKey:(id)kCTForegroundColorAttributeName];
}
else
#endif
{
[appendAttributes setObject:foregroundColor forKey:(id)kCTForegroundColorAttributeName];
}
}
}
NSAttributedString *newlineString = [[NSAttributedString alloc] initWithString:@"\n" attributes:appendAttributes];
[self appendAttributedString:newlineString];
}
#pragma mark - Working with Custom HTML Attributes
- (void)addHTMLAttribute:(NSString *)name value:(id)value range:(NSRange)range replaceExisting:(BOOL)replaceExisting
{
NSRange safeRange = NSIntersectionRange(range, NSMakeRange(0, [self length]));
[self beginEditing];
NSMutableIndexSet *indexesToSetThis = [NSMutableIndexSet indexSetWithIndexesInRange:range];
[self enumerateAttribute:DTCustomAttributesAttribute inRange:safeRange options:0 usingBlock:^(NSDictionary *dictionary, NSRange effectiveRange, BOOL *stop) {
id existingValue = [dictionary objectForKey:name];
if (existingValue && !replaceExisting)
{
// exempt this range
[indexesToSetThis removeIndexesInRange:effectiveRange];
}
}];
// now our mutable index set should contain the ranges where we want to set this
[indexesToSetThis enumerateRangesInRange:safeRange options:0 usingBlock:^(NSRange indexRange, BOOL *stopEnumerateRanges) {
// for each such range, we need to add this to the attribute
[self enumerateAttribute:DTCustomAttributesAttribute inRange:indexRange options:0 usingBlock:^(NSDictionary *dictionary, NSRange effectiveRange, BOOL *stopEnumerateAttribute) {
if (dictionary)
{
// need to make it mutable and add the value
NSMutableDictionary *mutableDictionary = [dictionary mutableCopy];
[mutableDictionary setObject:value forKey:name];
// substitute attribute
#if DTCORETEXT_NEEDS_ATTRIBUTE_REPLACEMENT_LEAK_FIX
if (NSFoundationVersionNumber <= NSFoundationVersionNumber10_6_8) // less than OS X 10.7 and less than iOS 5
{
// remove old (works around iOS 4.3 leak)
[self removeAttribute:DTCustomAttributesAttribute range:effectiveRange];
}
#endif
[self addAttribute:DTCustomAttributesAttribute value:[mutableDictionary copy] range:effectiveRange];
}
else
{
// create new dictionary with the value
dictionary = [NSDictionary dictionaryWithObject:value forKey:name];
[self addAttribute:DTCustomAttributesAttribute value:dictionary range:effectiveRange];
}
}];
}];
[self endEditing];
}
- (void)removeHTMLAttribute:(NSString *)name range:(NSRange)range
{
NSRange safeRange = NSIntersectionRange(range, NSMakeRange(0, [self length]));
[self beginEditing];
[self enumerateAttribute:DTCustomAttributesAttribute inRange:safeRange options:0 usingBlock:^(NSDictionary *dictionary, NSRange effectiveRange, BOOL *stop) {
id existingValue = [dictionary objectForKey:name];
if (existingValue)
{
// need to make it mutable and remove the value
NSMutableDictionary *mutableDictionary = [dictionary mutableCopy];
[mutableDictionary removeObjectForKey:name];
// substitute attribute
// only re-add modified dictionary if it is not empty
if ([mutableDictionary count])
{
if (NSFoundationVersionNumber <= NSFoundationVersionNumber10_6_8) // less than OS X 10.7 and less than iOS 5
{
// remove old (works around iOS 4.3 leak)
[self removeAttribute:DTCustomAttributesAttribute range:effectiveRange];
}
[self addAttribute:DTCustomAttributesAttribute value:[mutableDictionary copy] range:effectiveRange];
}
else
{
[self removeAttribute:DTCustomAttributesAttribute range:effectiveRange];
}
}
}];
[self endEditing];
}
@end