Created
March 25, 2015 17:58
-
-
Save jaanus/fdf0e9ab0fc64c029610 to your computer and use it in GitHub Desktop.
Create NSAttributedString out of NSString with Markdown-like syntax for bold text and links.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#if TARGET_OS_IPHONE | |
@import UIKit; | |
#define JKColor UIColor | |
#define JKFont UIFont | |
#else | |
@import AppKit; | |
#define JKColor NSColor | |
#define JKFont NSFont | |
#endif | |
@interface NSString (JKAttributer) | |
/** | |
A very minimalist partial implementation of parsing Markdown-style links and strong emphasis texts. | |
Supports exactly two things: hyperlinks in the inline style [link](http://example.com), and **bold text**. Links are turned into URL-s, and strong font attribute is added to the strong parts. | |
Supports only the “happy path.” Is not a comprehensive Markdown implementation. | |
*/ | |
- (NSAttributedString *)attributedStringFromLinksAndStrongTextWithBaseFont:(JKFont *)baseFont strongFont:(JKFont *)strongFont urlColor:(JKColor *)urlColor; | |
@end | |
@implementation NSString (JKAttributer) | |
- (NSAttributedString *)attributedStringFromLinksAndStrongTextWithBaseFont:(JKFont *)baseFont strongFont:(JKFont *)strongFont urlColor:(JKColor *)urlColor | |
{ | |
__block NSString *s = [self copy]; | |
// Part 1. Process URL-s. The outcome is a plaintext string + URL ranges and their targets | |
NSError *error = NULL; | |
NSRegularExpression *linkRegex = [NSRegularExpression regularExpressionWithPattern:@"\\[([^]]+)\\]\\(([^\\)]+)\\)" options:0 error:&error]; | |
NSMutableArray *linkRanges = [NSMutableArray array]; | |
NSMutableArray *linkTargets = [NSMutableArray array]; | |
// How much to offset the following match ranges | |
__block NSInteger offset = 0; | |
[linkRegex enumerateMatchesInString:self options:0 range:NSMakeRange(0, self.length) usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) { | |
NSRange range = [result rangeAtIndex:0]; | |
range.location += offset; | |
NSRange urlRangeInTargetString = [result rangeAtIndex:1]; | |
urlRangeInTargetString.location += offset - 1; | |
s = [s stringByReplacingCharactersInRange:range withString:[self substringWithRange:[result rangeAtIndex:1]]]; | |
[linkRanges addObject:[NSValue valueWithRange:urlRangeInTargetString]]; | |
[linkTargets addObject:[self substringWithRange:[result rangeAtIndex:2]]]; | |
offset -= [result rangeAtIndex:2].length + 4; | |
}]; | |
// Part 2. Process **strong** parts | |
NSString *strongInput = [s copy]; | |
__block NSInteger strongOffset = 0; | |
NSMutableArray *strongRanges = [NSMutableArray array]; | |
NSRegularExpression *strongRegex = [NSRegularExpression regularExpressionWithPattern:@"\\*\\*([^*]+)\\*\\*" options:0 error:nil]; | |
[strongRegex enumerateMatchesInString:strongInput options:0 range:NSMakeRange(0, strongInput.length) usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) { | |
NSRange rangeInString = [result rangeAtIndex:1]; | |
rangeInString.location += strongOffset - 2; | |
[strongRanges addObject:[NSValue valueWithRange:rangeInString]]; | |
NSRange needleRange = [result rangeAtIndex:0]; | |
needleRange.location += strongOffset; | |
NSRange replacementRange = [result rangeAtIndex:1]; | |
s = [s stringByReplacingCharactersInRange:needleRange withString:[strongInput substringWithRange:replacementRange]]; | |
// Shift URL-s back by a step | |
for (NSInteger i = 0; i < linkRanges.count; i++) { | |
NSValue *value = linkRanges[i]; | |
NSRange urlRange; | |
[value getValue:&urlRange]; | |
if (urlRange.location > rangeInString.location) { | |
urlRange.location -= 4; | |
[linkRanges replaceObjectAtIndex:i withObject:[NSValue valueWithRange:urlRange]]; | |
} | |
} | |
strongOffset -= 4; | |
}]; | |
// Part 3. Compose the output. | |
NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:s attributes: | |
@{ | |
NSFontAttributeName: baseFont | |
}]; | |
[attributedString beginEditing]; | |
for (NSValue *strongValue in strongRanges) { | |
NSRange strongRange; | |
[strongValue getValue:&strongRange]; | |
[attributedString addAttribute:NSFontAttributeName value:strongFont range:strongRange]; | |
} | |
for (NSInteger i = 0; i < linkRanges.count; i++) { | |
NSString *urlString = linkTargets[i]; | |
NSValue *urlRangeValue = linkRanges[i]; | |
NSRange urlRange; | |
[urlRangeValue getValue:&urlRange]; | |
[attributedString addAttributes:@{ | |
NSLinkAttributeName: [NSURL URLWithString:urlString], | |
NSForegroundColorAttributeName: urlColor, | |
NSFontAttributeName: strongFont | |
} range:urlRange]; | |
} | |
[attributedString endEditing]; | |
return attributedString; | |
} | |
@end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment