Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Fetching contributors…

Cannot retrieve contributors at this time

469 lines (360 sloc) 12.551 kb
//
// NSAttributedString+HTML.m
// CoreTextExtensions
//
// Created by Oliver Drobnik on 1/9/11.
// Copyright 2011 Drobnik.com. All rights reserved.
//
#import <CoreText/CoreText.h>
#import "NSAttributedString+HTML.h"
#import "NSMutableAttributedString+HTML.h"
#import "NSString+HTML.h"
#import "UIColor+HTML.h"
#import "NSScanner+HTML.h"
#import "NSCharacterSet+HTML.h"
#import "NSAttributedStringRunDelegates.h"
#import "DTTextAttachment.h"
#import "DTHTMLElement.h"
#import "DTCSSListStyle.h"
#import "DTCSSStylesheet.h"
#import "DTCoreTextFontDescriptor.h"
#import "DTCoreTextParagraphStyle.h"
#import "CGUtils.h"
#import "NSString+UTF8Cleaner.h"
#import "DTCoreTextConstants.h"
#import "DTHTMLAttributedStringBuilder.h"
@implementation NSAttributedString (HTML)
- (id)initWithHTML:(NSData *)data documentAttributes:(NSDictionary **)dict
{
return [self initWithHTML:data options:nil documentAttributes:dict];
}
- (id)initWithHTML:(NSData *)data baseURL:(NSURL *)base documentAttributes:(NSDictionary **)dict
{
NSDictionary *optionsDict = nil;
if (base)
{
optionsDict = [NSDictionary dictionaryWithObject:base forKey:NSBaseURLDocumentOption];
}
return [self initWithHTML:data options:optionsDict documentAttributes:dict];
}
- (id)initWithHTML:(NSData *)data options:(NSDictionary *)options documentAttributes:(NSDictionary **)dict
{
// only with valid data
if (![data length])
{
return nil;
}
DTHTMLAttributedStringBuilder *stringBuilder = [[DTHTMLAttributedStringBuilder alloc] initWithHTML:data options:options documentAttributes:dict];
// example for setting a willFlushCallback, that gets called before elements are written to the generated attributed string
// [stringBuilder setWillFlushCallback:^(DTHTMLElement *element)
// {
// // if an element is larger than twice the font size put it in it's own block
// if (element.floatStyle || (element.displayStyle == DTHTMLElementDisplayStyleInline && element.textAttachment.displaySize.height > 2.0 * element.fontDescriptor.pointSize) )
// {
// element.displayStyle = DTHTMLElementDisplayStyleBlock;
// }
// } ];
[stringBuilder buildString];
return [stringBuilder generatedAttributedString];
}
#pragma mark Convenience Methods
+ (NSAttributedString *)attributedStringWithHTML:(NSData *)data options:(NSDictionary *)options
{
NSAttributedString *attrString = [[NSAttributedString alloc] initWithHTML:data options:options documentAttributes:NULL];
return attrString;
}
#pragma mark Utlities
+ (NSAttributedString *)synthesizedSmallCapsAttributedStringWithText:(NSString *)text attributes:(NSDictionary *)attributes
{
CTFontRef normalFont = (__bridge CTFontRef)[attributes objectForKey:(id)kCTFontAttributeName];
DTCoreTextFontDescriptor *smallerFontDesc = [DTCoreTextFontDescriptor fontDescriptorForCTFont:normalFont];
smallerFontDesc.pointSize *= 0.7;
CTFontRef smallerFont = [smallerFontDesc newMatchingFont];
NSMutableDictionary *smallAttributes = [attributes mutableCopy];
[smallAttributes setObject:CFBridgingRelease(smallerFont) forKey:(id)kCTFontAttributeName];
//CFRelease(smallerFont);
NSMutableAttributedString *tmpString = [[NSMutableAttributedString alloc] init];
NSScanner *scanner = [NSScanner scannerWithString:text];
[scanner setCharactersToBeSkipped:nil];
NSCharacterSet *lowerCaseChars = [NSCharacterSet lowercaseLetterCharacterSet];
while (![scanner isAtEnd])
{
NSString *part;
if ([scanner scanCharactersFromSet:lowerCaseChars intoString:&part])
{
part = [part uppercaseString];
NSAttributedString *partString = [[NSAttributedString alloc] initWithString:part attributes:smallAttributes];
[tmpString appendAttributedString:partString];
}
if ([scanner scanUpToCharactersFromSet:lowerCaseChars intoString:&part])
{
NSAttributedString *partString = [[NSAttributedString alloc] initWithString:part attributes:attributes];
[tmpString appendAttributedString:partString];
}
}
return tmpString;
}
- (NSArray *)textAttachmentsWithPredicate:(NSPredicate *)predicate
{
NSMutableArray *tmpArray = [NSMutableArray array];
NSUInteger index = 0;
while (index<[self length])
{
NSRange range;
NSDictionary *attributes = [self attributesAtIndex:index effectiveRange:&range];
DTTextAttachment *attachment = [attributes objectForKey:@"DTTextAttachment"];
if (attachment)
{
if ([predicate evaluateWithObject:attachment])
{
[tmpArray addObject:attachment];
}
}
index += range.length;
}
if ([tmpArray count])
{
return tmpArray;
}
return nil;
}
#pragma mark HTML Encoding
// TO DO: aggregate common styles (like font) into one span
// TO DO: correctly encode LI/OL/UL
// TO DO: correctly encode shadows
- (NSString *)htmlString
{
NSString *plainString = [self string];
// divide the string into it's blocks, we assume that these are the P
NSArray *paragraphs = [plainString componentsSeparatedByString:@"\n"];
NSMutableString *retString = [NSMutableString string];
NSInteger location = 0;
for (NSString *oneParagraph in paragraphs)
{
NSRange paragraphRange = NSMakeRange(location, [oneParagraph length]);
// skip empty paragraph at end
if (oneParagraph == [paragraphs lastObject] && !paragraphRange.length)
{
break;
}
BOOL fontIsBlockLevel = NO;
// check if font is same in all paragraph
NSRange fontEffectiveRange;
CTFontRef paragraphFont = (__bridge CTFontRef)[self attribute:(id)kCTFontAttributeName atIndex:paragraphRange.location longestEffectiveRange:&fontEffectiveRange inRange:paragraphRange];
if (NSEqualRanges(paragraphRange, fontEffectiveRange))
{
fontIsBlockLevel = YES;
}
// next paragraph start
location = location + paragraphRange.length + 1;
NSDictionary *paraAttributes = [self attributesAtIndex:paragraphRange.location effectiveRange:NULL];
CTParagraphStyleRef paraStyle = (__bridge CTParagraphStyleRef)[paraAttributes objectForKey:(id)kCTParagraphStyleAttributeName];
NSString *paraStyleString = nil;
if (paraStyle)
{
DTCoreTextParagraphStyle *para = [DTCoreTextParagraphStyle paragraphStyleWithCTParagraphStyle:paraStyle];
paraStyleString = [para cssStyleRepresentation];
}
if (!paraStyleString)
{
paraStyleString = @"";
}
if (fontIsBlockLevel)
{
if (paragraphFont)
{
DTCoreTextFontDescriptor *desc = [DTCoreTextFontDescriptor fontDescriptorForCTFont:paragraphFont];
NSString *paraFontStyle = [desc cssStyleRepresentation];
if (paraFontStyle)
{
paraStyleString = [paraStyleString stringByAppendingString:paraFontStyle];
}
}
}
NSString *blockElement;
NSNumber *headerLevel = [paraAttributes objectForKey:@"DTHeaderLevel"];
if (headerLevel)
{
blockElement = [NSString stringWithFormat:@"h%d", [headerLevel integerValue]];
}
else
{
blockElement = @"p";
if ([paragraphs lastObject] == oneParagraph)
{
// last paragraph in string
if (![plainString hasSuffix:@"\n"])
{
// not a whole paragraph, so we don't put it in P
blockElement = @"span";
}
}
}
if ([paraStyleString length])
{
[retString appendFormat:@"<%@ style=\"%@\">", blockElement, paraStyleString];
}
else
{
[retString appendFormat:@"<%@>", blockElement];
}
// add the attributed string ranges in this paragraph to the paragraph container
NSRange effectiveRange;
NSUInteger index = paragraphRange.location;
while (index < NSMaxRange(paragraphRange))
{
NSDictionary *attributes = [self attributesAtIndex:index longestEffectiveRange:&effectiveRange inRange:paragraphRange];
index += effectiveRange.length;
NSString *subString = [[plainString substringWithRange:effectiveRange] stringByAddingHTMLEntities];
if (!subString)
{
continue;
}
DTTextAttachment *attachment = [attributes objectForKey:@"DTTextAttachment"];
if (attachment)
{
NSString *urlString;
if (attachment.contentURL)
{
if ([attachment.contentURL isFileURL])
{
NSString *path = [attachment.contentURL path];
NSRange range = [path rangeOfString:@".app/"];
if (range.length)
{
urlString = [path substringFromIndex:NSMaxRange(range)];
}
else
{
urlString = [attachment.contentURL absoluteString];
}
}
else
{
urlString = [attachment.contentURL relativeString];
}
}
else
{
if (attachment.contentType == DTTextAttachmentTypeImage && attachment.contents)
{
urlString = [attachment dataURLRepresentation];
}
else
{
// no valid image remote or local
continue;
}
}
// write appropriate tag
if (attachment.contentType == DTTextAttachmentTypeVideoURL)
{
[retString appendFormat:@"<video src=\"%@\"", urlString];
}
else if (attachment.contentType == DTTextAttachmentTypeImage)
{
[retString appendFormat:@"<img src=\"%@\"", urlString];
}
// build a HTML 5 conformant size style if set
NSMutableString *styleString = [NSMutableString string];
if (attachment.originalSize.width>0)
{
[styleString appendFormat:@"width:%.0fpx;", attachment.originalSize.width];
}
if (attachment.originalSize.height>0)
{
[styleString appendFormat:@"height:%.0fpx;", attachment.originalSize.height];
}
if ([styleString length])
{
[retString appendFormat:@" style=\"%@\"", styleString];
}
// attach the attributes dictionary
NSMutableDictionary *tmpAttributes = [attachment.attributes mutableCopy];
// remove src and style, we already have that
[tmpAttributes removeObjectForKey:@"src"];
[tmpAttributes removeObjectForKey:@"style"];
for (__strong NSString *oneKey in [tmpAttributes allKeys])
{
oneKey = [oneKey stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
NSString *value = [[tmpAttributes objectForKey:oneKey] stringByAddingHTMLEntities];
[retString appendFormat:@" %@=\"%@\"", oneKey, value];
}
// end
[retString appendString:@" />"];
continue;
}
NSString *fontStyle = nil;
if (!fontIsBlockLevel)
{
CTFontRef font = (__bridge CTFontRef)[attributes objectForKey:(id)kCTFontAttributeName];
if (font)
{
DTCoreTextFontDescriptor *desc = [DTCoreTextFontDescriptor fontDescriptorForCTFont:font];
fontStyle = [desc cssStyleRepresentation];
}
}
if (!fontStyle)
{
fontStyle = @"";
}
CGColorRef textColor = (__bridge CGColorRef)[attributes objectForKey:(id)kCTForegroundColorAttributeName];
if (textColor)
{
UIColor *color = [UIColor colorWithCGColor:textColor];
fontStyle = [fontStyle stringByAppendingFormat:@"color:#%@;", [color htmlHexString]];
}
CGColorRef backgroundColor = (__bridge CGColorRef)[attributes objectForKey:@"DTBackgroundColor"];
if (backgroundColor)
{
UIColor *color = [UIColor colorWithCGColor:backgroundColor];
fontStyle = [fontStyle stringByAppendingFormat:@"background-color:#%@;", [color htmlHexString]];
}
NSNumber *underline = [attributes objectForKey:(id)kCTUnderlineStyleAttributeName];
if (underline)
{
fontStyle = [fontStyle stringByAppendingString:@"text-decoration:underline;"];
}
else
{
// there can be no underline and strike-through at the same time
NSNumber *strikout = [attributes objectForKey:@"DTStrikeOut"];
if ([strikout boolValue])
{
fontStyle = [fontStyle stringByAppendingString:@"text-decoration:line-through;"];
}
}
NSURL *url = [attributes objectForKey:@"DTLink"];
if (url)
{
if ([fontStyle length])
{
[retString appendFormat:@"<a href=\"%@\" style=\"%@\">%@</a>", [url relativeString], fontStyle, subString];
}
else
{
[retString appendFormat:@"<a href=\"%@\">%@</a>", [url relativeString], subString];
}
}
else
{
if ([fontStyle length])
{
[retString appendFormat:@"<span style=\"%@\">%@</span>\n", fontStyle, subString];
}
else
{
[retString appendString:subString];
}
}
}
[retString appendFormat:@"</%@>\n", blockElement];
}
return retString;
}
- (NSString *)plainTextString
{
NSString *tmpString = [self string];
return [tmpString stringByReplacingOccurrencesOfString:UNICODE_OBJECT_PLACEHOLDER withString:@""];
}
@end
Jump to Line
Something went wrong with that request. Please try again.