Epub阅读器0.0.1
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
//
|
||||
// CTLineUtils.h
|
||||
// DTCoreText
|
||||
//
|
||||
// Created by Oleksandr Deundiak on 7/15/15.
|
||||
// Copyright 2015. All rights reserved.
|
||||
//
|
||||
|
||||
#import <CoreText/CoreText.h>
|
||||
|
||||
BOOL areLinesEqual(CTLineRef line1, CTLineRef line2);
|
||||
CFIndex getTruncationIndex(CTLineRef line, CTLineRef trunc);
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
//
|
||||
// CTLineUtils.m
|
||||
// DTCoreText
|
||||
//
|
||||
// Created by Oleksandr Deundiak on 7/15/15.
|
||||
// Copyright 2015. All rights reserved.
|
||||
//
|
||||
|
||||
#import "CTLineUtils.h"
|
||||
|
||||
BOOL areLinesEqual(CTLineRef line1, CTLineRef line2)
|
||||
{
|
||||
if(line1 == nil || line2 == nil) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
CFArrayRef glyphRuns1 = CTLineGetGlyphRuns(line1);
|
||||
CFArrayRef glyphRuns2 = CTLineGetGlyphRuns(line2);
|
||||
CFIndex runCount1 = CFArrayGetCount(glyphRuns1), runCount2 = CFArrayGetCount(glyphRuns2);
|
||||
|
||||
if (runCount1 != runCount2)
|
||||
return NO;
|
||||
|
||||
for (CFIndex i = 0; i < runCount1; i++)
|
||||
{
|
||||
CTRunRef run1 = CFArrayGetValueAtIndex(glyphRuns1, i);
|
||||
CTRunRef run2 = CFArrayGetValueAtIndex(glyphRuns2, i);
|
||||
|
||||
CFIndex countInRun1 = CTRunGetGlyphCount(run1), countInRun2 = CTRunGetGlyphCount(run2);
|
||||
if (countInRun1 != countInRun2)
|
||||
return NO;
|
||||
|
||||
const CGGlyph* constGlyphs1 = CTRunGetGlyphsPtr(run1);
|
||||
CGGlyph* glyphs1 = NULL;
|
||||
if (constGlyphs1 == NULL)
|
||||
{
|
||||
glyphs1 = (CGGlyph*)malloc(countInRun1*sizeof(CGGlyph));
|
||||
CTRunGetGlyphs(run1, CFRangeMake(0, countInRun1), glyphs1);
|
||||
constGlyphs1 = glyphs1;
|
||||
}
|
||||
|
||||
const CGGlyph* constGlyphs2 = CTRunGetGlyphsPtr(run2);
|
||||
CGGlyph* glyphs2 = NULL;
|
||||
if (constGlyphs2 == NULL)
|
||||
{
|
||||
glyphs2 = (CGGlyph*)malloc(countInRun2*sizeof(CGGlyph));
|
||||
CTRunGetGlyphs(run2, CFRangeMake(0, countInRun2), glyphs2);
|
||||
constGlyphs2 = glyphs2;
|
||||
}
|
||||
|
||||
BOOL result = YES;
|
||||
for (CFIndex j = 0; j < countInRun1; j++)
|
||||
{
|
||||
if (constGlyphs1[j] != constGlyphs2[j])
|
||||
{
|
||||
result = NO;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (glyphs1 != NULL)
|
||||
free(glyphs1);
|
||||
|
||||
if (glyphs2 != NULL)
|
||||
free(glyphs2);
|
||||
|
||||
if (!result)
|
||||
return NO;
|
||||
}
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
CFIndex getTruncationIndex(CTLineRef line, CTLineRef trunc)
|
||||
{
|
||||
if (line == nil || trunc == nil) return 0;
|
||||
|
||||
CFIndex truncCount = CFArrayGetCount(CTLineGetGlyphRuns(trunc));
|
||||
|
||||
CFArrayRef lineRuns = CTLineGetGlyphRuns(line);
|
||||
CFIndex lineRunsCount = CFArrayGetCount(lineRuns);
|
||||
|
||||
CFIndex index = lineRunsCount - truncCount - 1;
|
||||
|
||||
// If the index is negative, CFArrayGetValueAtIndex will crash on iOS 10 beta.
|
||||
// We will just return 0 because on iOS 9, CFArrayGetValueAtIndex would have
|
||||
// returned nil anyways and the return truncation index would be 0.
|
||||
// Apple might have enabled an assert that only appears in the iOS 10 beta
|
||||
// release, but we will just avoid passing invalid arguments just to be safe.
|
||||
if (index < 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
CTRunRef lineLastRun = CFArrayGetValueAtIndex(lineRuns, index);
|
||||
|
||||
CFRange lastRunRange = CTRunGetStringRange(lineLastRun);
|
||||
|
||||
return lastRunRange.location = lastRunRange.length;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
//
|
||||
// DTAccessibilityElement.h
|
||||
// DTCoreText
|
||||
//
|
||||
// Created by Austen Green on 3/13/13.
|
||||
// Copyright (c) 2013 Drobnik.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import "DTCompatibility.h"
|
||||
|
||||
#if TARGET_OS_IPHONE && !TARGET_OS_WATCH
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
/**
|
||||
A UIAccessibilityElement subclass that automatically converts its local accessibilityFrame to screen coordinates.
|
||||
*/
|
||||
@interface DTAccessibilityElement : UIAccessibilityElement
|
||||
/**
|
||||
The frame for the accessibility element in terms of the receiver's superview.
|
||||
*/
|
||||
@property (nonatomic, assign) CGRect localCoordinateAccessibilityFrame;
|
||||
|
||||
/**
|
||||
The point for activating accessibility events in terms of the receiver's superview.
|
||||
*/
|
||||
@property (nonatomic, assign) CGPoint localCoordinateAccessibilityActivationPoint;
|
||||
|
||||
/**
|
||||
The designated initializer. This class should be initialized with a UIView as its accessibility container.
|
||||
@param parentView The logical superview for the onscreen element the receiver represents.
|
||||
@returns Returns an initialized DTAccessibilityElement */
|
||||
|
||||
- (id)initWithParentView:(UIView *)parentView;
|
||||
|
||||
@end
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,56 @@
|
||||
//
|
||||
// DTAccessibilityElement.m
|
||||
// DTCoreText
|
||||
//
|
||||
// Created by Austen Green on 3/13/13.
|
||||
// Copyright (c) 2013 Drobnik.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import "DTAccessibilityElement.h"
|
||||
|
||||
#if TARGET_OS_IPHONE && !TARGET_OS_WATCH
|
||||
|
||||
#import <DTFoundation/DTWeakSupport.h>
|
||||
|
||||
static const CGPoint DTAccessibilityElementNullActivationPoint = {CGFLOAT_MAX, CGFLOAT_MAX};
|
||||
|
||||
@interface DTAccessibilityElement()
|
||||
@property (nonatomic, DT_WEAK_PROPERTY) UIView *parentView;
|
||||
@end
|
||||
|
||||
@implementation DTAccessibilityElement
|
||||
|
||||
- (id)initWithParentView:(UIView *)parentView
|
||||
{
|
||||
self = [super initWithAccessibilityContainer:parentView];
|
||||
if (self)
|
||||
{
|
||||
_parentView = parentView;
|
||||
_localCoordinateAccessibilityActivationPoint = DTAccessibilityElementNullActivationPoint;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (CGRect)accessibilityFrame
|
||||
{
|
||||
CGRect frame = self.localCoordinateAccessibilityFrame;
|
||||
frame = [self.parentView.window convertRect:frame fromView:self.parentView];
|
||||
return frame;
|
||||
}
|
||||
|
||||
- (CGPoint)accessibilityActivationPoint
|
||||
{
|
||||
CGPoint point = self.localCoordinateAccessibilityActivationPoint;
|
||||
if (CGPointEqualToPoint(point, DTAccessibilityElementNullActivationPoint))
|
||||
{
|
||||
point = [super accessibilityActivationPoint];
|
||||
}
|
||||
|
||||
point = [self.parentView.window convertPoint:point fromView:self.parentView];
|
||||
|
||||
return point;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,61 @@
|
||||
//
|
||||
// DTAccessibilityViewProxy.h
|
||||
// DTCoreText
|
||||
//
|
||||
// Created by Austen Green on 5/6/13.
|
||||
// Copyright (c) 2013 Drobnik.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import "DTCompatibility.h"
|
||||
#import "DTAccessibilityElement.h"
|
||||
|
||||
#if TARGET_OS_IPHONE && !TARGET_OS_WATCH
|
||||
|
||||
#import "DTTextAttachment.h"
|
||||
#import <DTFoundation/DTWeakSupport.h>
|
||||
|
||||
@protocol DTAccessibilityViewProxyDelegate;
|
||||
|
||||
/**
|
||||
UIView proxy for DTAttributedTextContentView custom subviews for text attachments.
|
||||
*/
|
||||
|
||||
@interface DTAccessibilityViewProxy : NSObject
|
||||
/**
|
||||
The delegate for the proxy
|
||||
*/
|
||||
@property (nonatomic, DT_WEAK_PROPERTY, readonly) id<DTAccessibilityViewProxyDelegate> delegate;
|
||||
|
||||
/**
|
||||
The text attachment represented by the proxy
|
||||
*/
|
||||
@property (nonatomic, strong, readonly) DTTextAttachment *textAttachment;
|
||||
|
||||
/**
|
||||
Creates a text attachment proxy for use with the VoiceOver system.
|
||||
@param textAttachment The <DTTextAttachment> that will be represented by a view.
|
||||
@param delegate An object conforming to <DTAccessibilityViewProxyDelegate> that will provide a view when needed by the proxy.
|
||||
@returns A new proxy object
|
||||
*/
|
||||
|
||||
- (id)initWithTextAttachment:(DTTextAttachment *)textAttachment delegate:(id<DTAccessibilityViewProxyDelegate>)delegate;
|
||||
|
||||
@end
|
||||
|
||||
/**
|
||||
Protocol to provide custom views for accessibility elements representing a DTTextAttachment.
|
||||
*/
|
||||
@protocol DTAccessibilityViewProxyDelegate
|
||||
@required
|
||||
/**
|
||||
Provides a view for an attachment, e.g. an imageView for images
|
||||
|
||||
@param attachment The <DTTextAttachment> that the requested view should represent
|
||||
@param proxy The frame that the view should use to fit on top of the space reserved for the attachment.
|
||||
@returns The sender requesting the view.
|
||||
*/
|
||||
|
||||
- (UIView *)viewForTextAttachment:(DTTextAttachment *)attachment proxy:(DTAccessibilityViewProxy *)proxy;
|
||||
@end
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,62 @@
|
||||
//
|
||||
// DTAccessibilityViewProxy.m
|
||||
// DTCoreText
|
||||
//
|
||||
// Created by Austen Green on 5/6/13.
|
||||
// Copyright (c) 2013 Drobnik.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import "DTAccessibilityViewProxy.h"
|
||||
|
||||
#if TARGET_OS_IPHONE && !TARGET_OS_WATCH
|
||||
|
||||
@implementation DTAccessibilityViewProxy
|
||||
|
||||
- (id)initWithTextAttachment:(DTTextAttachment *)textAttachment delegate:(id<DTAccessibilityViewProxyDelegate>)delegate
|
||||
{
|
||||
_textAttachment = textAttachment;
|
||||
_delegate = delegate;
|
||||
return self;
|
||||
}
|
||||
|
||||
- (UIView *)proxiedView
|
||||
{
|
||||
return [self.delegate viewForTextAttachment:self.textAttachment proxy:self];
|
||||
}
|
||||
|
||||
- (Class)class
|
||||
{
|
||||
Class aClass = [[self proxiedView] class];
|
||||
|
||||
if (!aClass)
|
||||
aClass = [DTAccessibilityViewProxy class];
|
||||
|
||||
return aClass;
|
||||
}
|
||||
|
||||
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel
|
||||
{
|
||||
NSMethodSignature *signature = [UIView instanceMethodSignatureForSelector:sel];
|
||||
|
||||
return signature;
|
||||
}
|
||||
|
||||
- (void)forwardInvocation:(NSInvocation *)invocation
|
||||
{
|
||||
UIView *view = [self proxiedView];
|
||||
[invocation invokeWithTarget:view];
|
||||
}
|
||||
|
||||
- (BOOL)isEqual:(id)object
|
||||
{
|
||||
return [[self proxiedView] isEqual:object];
|
||||
}
|
||||
|
||||
- (NSUInteger)hash
|
||||
{
|
||||
return [[self proxiedView] hash];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,21 @@
|
||||
//
|
||||
// DTHTMLElementA.h
|
||||
// DTCoreText
|
||||
//
|
||||
// Created by Oliver Drobnik on 21.03.13.
|
||||
// Copyright (c) 2013 Drobnik.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import "DTHTMLElement.h"
|
||||
|
||||
/**
|
||||
Specialized subclass of <DTHTMLElement> that represents a hyperlink.
|
||||
*/
|
||||
@interface DTAnchorHTMLElement : DTHTMLElement
|
||||
|
||||
/**
|
||||
Foreground text color of the receiver when highlighted
|
||||
*/
|
||||
@property (nonatomic, strong) DTColor *highlightedTextColor;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,54 @@
|
||||
//
|
||||
// DTHTMLElementA.m
|
||||
// DTCoreText
|
||||
//
|
||||
// Created by Oliver Drobnik on 21.03.13.
|
||||
// Copyright (c) 2013 Drobnik.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import "DTCompatibility.h"
|
||||
#import "DTAnchorHTMLElement.h"
|
||||
#import "DTColorFunctions.h"
|
||||
|
||||
@implementation DTAnchorHTMLElement
|
||||
{
|
||||
DTColor *_highlightedTextColor;
|
||||
}
|
||||
|
||||
- (void)applyStyleDictionary:(NSDictionary *)styles
|
||||
{
|
||||
[super applyStyleDictionary:styles];
|
||||
|
||||
// get highlight color from a:active pseudo-selector
|
||||
NSString *activeColor = [styles objectForKey:@"active:color"];
|
||||
|
||||
if (activeColor)
|
||||
{
|
||||
self.highlightedTextColor = DTColorCreateWithHTMLName(activeColor);
|
||||
}
|
||||
}
|
||||
|
||||
- (NSAttributedString *)attributedString
|
||||
{
|
||||
// super returns a mutable attributed string
|
||||
NSMutableAttributedString *mutableAttributedString = (NSMutableAttributedString *)[super attributedString];
|
||||
|
||||
if (_highlightedTextColor)
|
||||
{
|
||||
NSRange range = NSMakeRange(0, [mutableAttributedString length]);
|
||||
|
||||
// this additional attribute keeps the highlight color
|
||||
[mutableAttributedString addAttribute:DTLinkHighlightColorAttribute value:(id)_highlightedTextColor range:range];
|
||||
|
||||
// we need to set the text color via the graphics context
|
||||
[mutableAttributedString addAttribute:(id)kCTForegroundColorFromContextAttributeName value:[NSNumber numberWithBool:YES] range:range];
|
||||
}
|
||||
|
||||
return mutableAttributedString;
|
||||
}
|
||||
|
||||
#pragma mark - Properties
|
||||
|
||||
@synthesize highlightedTextColor = _highlightedTextColor;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,44 @@
|
||||
//
|
||||
// DTAttributedLabel.h
|
||||
// DTCoreText
|
||||
//
|
||||
// Created by Brian Kenny on 1/17/13.
|
||||
// Copyright (c) 2013 Cocoanetics.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import "DTCompatibility.h"
|
||||
|
||||
#if TARGET_OS_IPHONE && !TARGET_OS_WATCH
|
||||
|
||||
#import "DTAttributedTextContentView.h"
|
||||
|
||||
/**
|
||||
A Rich Text replacement for `UILabel`. It inherits from <DTAttributedTextContentView> and as such you can also set the delegate to provide custom subviews i.e. for images or hyperlinks.
|
||||
|
||||
Contrary to DTAttributedTextContentView the intrinsicContentSize is only as wide as the text content. To shrink the DTAttributedLabel to that call -sizeToFit.
|
||||
*/
|
||||
|
||||
@interface DTAttributedLabel : DTAttributedTextContentView
|
||||
|
||||
/**
|
||||
@name Setting Attributes
|
||||
*/
|
||||
|
||||
/**
|
||||
The number of lines to display in the receiver
|
||||
*/
|
||||
@property(nonatomic, assign) NSInteger numberOfLines;
|
||||
|
||||
/**
|
||||
The line break mode of the receiver
|
||||
*/
|
||||
@property(nonatomic, assign) NSLineBreakMode lineBreakMode;
|
||||
|
||||
/**
|
||||
The string to append to the visible string in case a truncation occurs
|
||||
*/
|
||||
@property(nonatomic, strong) NSAttributedString *truncationString;
|
||||
|
||||
@end
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,125 @@
|
||||
//
|
||||
// DTAttributedLabel.m
|
||||
// DTCoreText
|
||||
//
|
||||
// Created by Brian Kenny on 1/17/13.
|
||||
// Copyright (c) 2013 Drobnik.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import "DTAttributedLabel.h"
|
||||
|
||||
#if TARGET_OS_IPHONE && !TARGET_OS_WATCH
|
||||
|
||||
#import "DTCoreTextLayoutFrame.h"
|
||||
#import <QuartzCore/QuartzCore.h>
|
||||
|
||||
@implementation DTAttributedLabel
|
||||
|
||||
+ (Class)layerClass
|
||||
{
|
||||
// most likely the label will be less than a screen size and so we don't want any tiling behavior
|
||||
return [CALayer class];
|
||||
}
|
||||
|
||||
- (void) setupAttributedLabel
|
||||
{
|
||||
// we want to relayout the text if height or width change
|
||||
self.relayoutMask = DTAttributedTextContentViewRelayoutOnHeightChanged | DTAttributedTextContentViewRelayoutOnWidthChanged;
|
||||
|
||||
self.layoutFrameHeightIsConstrainedByBounds = YES; // height is not flexible
|
||||
}
|
||||
|
||||
- (id)initWithFrame:(CGRect)frame
|
||||
{
|
||||
self = [super initWithFrame:frame];
|
||||
|
||||
if (self)
|
||||
{
|
||||
[self setupAttributedLabel];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (id) initWithCoder:(NSCoder *)aDecoder {
|
||||
self = [super initWithCoder:aDecoder];
|
||||
|
||||
if (self != nil)
|
||||
{
|
||||
[self setupAttributedLabel];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
|
||||
|
||||
- (void) awakeFromNib
|
||||
{
|
||||
[super awakeFromNib];
|
||||
[self setupAttributedLabel];
|
||||
}
|
||||
|
||||
#pragma mark - Sizing
|
||||
|
||||
- (CGSize)intrinsicContentSize
|
||||
{
|
||||
if (!self.layoutFrame) // creates new layout frame if possible
|
||||
{
|
||||
return CGSizeMake(-1, -1); // UIViewNoIntrinsicMetric as of iOS 6
|
||||
}
|
||||
|
||||
// we have a layout frame and from this we get the needed size
|
||||
CGSize intrisicContentSize = [_layoutFrame intrinsicContentFrame].size;
|
||||
return CGSizeMake(intrisicContentSize.width + _edgeInsets.left + _edgeInsets.right,
|
||||
intrisicContentSize.height + _edgeInsets.top + _edgeInsets.bottom);
|
||||
}
|
||||
|
||||
#pragma mark - Properties
|
||||
|
||||
- (NSInteger)numberOfLines
|
||||
{
|
||||
return _numberOfLines;
|
||||
}
|
||||
|
||||
- (void)setNumberOfLines:(NSInteger)numberOfLines
|
||||
{
|
||||
if (numberOfLines != _numberOfLines)
|
||||
{
|
||||
_numberOfLines = numberOfLines;
|
||||
[self relayoutText];
|
||||
}
|
||||
}
|
||||
|
||||
- (NSLineBreakMode)lineBreakMode
|
||||
{
|
||||
return _lineBreakMode;
|
||||
}
|
||||
|
||||
- (void)setLineBreakMode:(NSLineBreakMode)lineBreakMode
|
||||
{
|
||||
if (lineBreakMode != _lineBreakMode)
|
||||
{
|
||||
_lineBreakMode = lineBreakMode;
|
||||
[self relayoutText];
|
||||
}
|
||||
}
|
||||
|
||||
- (NSAttributedString*)truncationString
|
||||
{
|
||||
return _truncationString;
|
||||
}
|
||||
|
||||
- (void)setTruncationString:(NSAttributedString *)truncationString
|
||||
{
|
||||
if (![truncationString isEqualToAttributedString:_truncationString])
|
||||
{
|
||||
_truncationString = truncationString;
|
||||
[self relayoutText];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@end
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,93 @@
|
||||
//
|
||||
// DTAttributedTextCell.h
|
||||
// DTCoreText
|
||||
//
|
||||
// Created by Oliver Drobnik on 8/4/11.
|
||||
// Copyright 2011 Drobnik.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import "DTCompatibility.h"
|
||||
|
||||
#if TARGET_OS_IPHONE && !TARGET_OS_WATCH
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
#import "DTAttributedTextContentView.h"
|
||||
#import <DTFoundation/DTWeakSupport.h>
|
||||
|
||||
/**
|
||||
This class represents a tableview cell that contains an attributed text as its content.
|
||||
*/
|
||||
@interface DTAttributedTextCell : UITableViewCell
|
||||
|
||||
/**
|
||||
@name Creating Cells
|
||||
*/
|
||||
|
||||
/**
|
||||
Creates a tableview cell with a given reuse identifier.
|
||||
@param reuseIdentifier The reuse identifier to use for the cell
|
||||
@returns A prepared cell
|
||||
*/
|
||||
- (id)initWithReuseIdentifier:(NSString *)reuseIdentifier;
|
||||
|
||||
/**
|
||||
@name Setting Attributed Content
|
||||
*/
|
||||
|
||||
/**
|
||||
The attributed string content of the receiver
|
||||
*/
|
||||
@property (nonatomic, strong) NSAttributedString *attributedString;
|
||||
|
||||
/**
|
||||
A delegate implementing DTAttributedTextContentViewDelegate to provide custom subviews for images and links.
|
||||
*/
|
||||
@property (nonatomic, DT_WEAK_PROPERTY) IBOutlet id <DTAttributedTextContentViewDelegate> textDelegate;
|
||||
|
||||
/**
|
||||
This method allows to set HTML text directly as content of the receiver.
|
||||
|
||||
This will be converted to an attributed string.
|
||||
@param html The HTML string to set as the receiver's text content
|
||||
*/
|
||||
- (void)setHTMLString:(NSString *)html;
|
||||
|
||||
/**
|
||||
This method allows to set HTML text directly as content of the receiver.
|
||||
|
||||
This will be converted to an attributed string.
|
||||
@param html The HTML string to set as the receiver's text content
|
||||
@param options The options used for rendering the HTML
|
||||
*/
|
||||
- (void) setHTMLString:(NSString *)html options:(NSDictionary*) options;
|
||||
|
||||
|
||||
/**
|
||||
@name Getting Information
|
||||
*/
|
||||
|
||||
/**
|
||||
Determines the row height that is needed in a specific table view to show the entire text content.
|
||||
|
||||
The table view is necessary because from this the method can know the style. Also the accessory type needs to be set before calling this method because this reduces the available space.
|
||||
@note This value is only useful for table views with variable row height.
|
||||
@param tableView The table view to determine the height for.
|
||||
*/
|
||||
- (CGFloat)requiredRowHeightInTableView:(UITableView *)tableView;
|
||||
|
||||
/**
|
||||
Determines whether the cells built-in contentView is allowed to dictate the size available for text. If active then attributedTextContextView's height always matches the cell height.
|
||||
|
||||
Set this to `YES` for use in fixed row height table views, leave it `NO` for flexible row height table views.
|
||||
*/
|
||||
@property (nonatomic, assign) BOOL hasFixedRowHeight;
|
||||
|
||||
/**
|
||||
The attributed text content view that the receiver uses to display the attributed text content.
|
||||
*/
|
||||
@property (nonatomic, readonly) DTAttributedTextContentView *attributedTextContextView;
|
||||
|
||||
@end
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,276 @@
|
||||
//
|
||||
// DTAttributedTextCell.m
|
||||
// DTCoreText
|
||||
//
|
||||
// Created by Oliver Drobnik on 8/4/11.
|
||||
// Copyright 2011 Drobnik.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import "DTCompatibility.h"
|
||||
|
||||
#import "DTAttributedTextCell.h"
|
||||
|
||||
#if TARGET_OS_IPHONE && !TARGET_OS_WATCH
|
||||
|
||||
#import "DTCoreText.h"
|
||||
#import "DTCSSStylesheet.h"
|
||||
|
||||
#import <DTFoundation/DTLog.h>
|
||||
|
||||
@implementation DTAttributedTextCell
|
||||
{
|
||||
DTAttributedTextContentView *_attributedTextContextView;
|
||||
|
||||
DT_WEAK_VARIABLE id <DTAttributedTextContentViewDelegate> _textDelegate;
|
||||
|
||||
NSUInteger _htmlHash; // preserved hash to avoid relayouting for same HTML
|
||||
|
||||
BOOL _hasFixedRowHeight;
|
||||
DT_WEAK_VARIABLE UITableView *_containingTableView;
|
||||
}
|
||||
|
||||
- (id)initWithReuseIdentifier:(NSString *)reuseIdentifier
|
||||
{
|
||||
self = [super initWithStyle:UITableViewCellStyleDefault reuseIdentifier:reuseIdentifier];
|
||||
|
||||
if (self)
|
||||
{
|
||||
// content view created lazily
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
_textDelegate = nil;
|
||||
_containingTableView = nil;
|
||||
}
|
||||
|
||||
- (void)layoutSubviews
|
||||
{
|
||||
[super layoutSubviews];
|
||||
|
||||
if (!self.superview)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_hasFixedRowHeight)
|
||||
{
|
||||
self.attributedTextContextView.frame = self.contentView.bounds;
|
||||
}
|
||||
else
|
||||
{
|
||||
CGFloat neededContentHeight = [self requiredRowHeightInTableView:_containingTableView];
|
||||
|
||||
// after the first call here the content view size is correct
|
||||
CGRect frame = CGRectMake(0, 0, self.contentView.bounds.size.width, neededContentHeight);
|
||||
self.attributedTextContextView.frame = frame;
|
||||
}
|
||||
}
|
||||
|
||||
- (UITableView *)_findContainingTableView
|
||||
{
|
||||
UIView *tableView = self.superview;
|
||||
|
||||
while (tableView)
|
||||
{
|
||||
if ([tableView isKindOfClass:[UITableView class]])
|
||||
{
|
||||
return (UITableView *)tableView;
|
||||
}
|
||||
|
||||
tableView = tableView.superview;
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (void)didMoveToSuperview
|
||||
{
|
||||
[super didMoveToSuperview];
|
||||
|
||||
_containingTableView = [self _findContainingTableView];
|
||||
|
||||
// on < iOS 7 we need to make the background translucent to avoid artifacts at rounded edges
|
||||
if (_containingTableView.style == UITableViewStyleGrouped)
|
||||
{
|
||||
if (NSFoundationVersionNumber < DTNSFoundationVersionNumber_iOS_7_0)
|
||||
{
|
||||
_attributedTextContextView.backgroundColor = [UIColor clearColor];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// http://stackoverflow.com/questions/4708085/how-to-determine-margin-of-a-grouped-uitableview-or-better-how-to-set-it/4872199#4872199
|
||||
- (CGFloat)_groupedCellMarginWithTableWidth:(CGFloat)tableViewWidth
|
||||
{
|
||||
CGFloat marginWidth;
|
||||
if(tableViewWidth > 20)
|
||||
{
|
||||
if(tableViewWidth < 400 || [UIDevice currentDevice].userInterfaceIdiom==UIUserInterfaceIdiomPhone)
|
||||
{
|
||||
marginWidth = 10;
|
||||
}
|
||||
else
|
||||
{
|
||||
marginWidth = MAX(31.f, MIN(45.f, tableViewWidth*0.06f));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
marginWidth = tableViewWidth - 10;
|
||||
}
|
||||
return marginWidth;
|
||||
}
|
||||
|
||||
- (CGFloat)requiredRowHeightInTableView:(UITableView *)tableView
|
||||
{
|
||||
if (_hasFixedRowHeight)
|
||||
{
|
||||
DTLogWarning(@"You are calling %s even though the cell is configured with fixed row height", (const char *)__PRETTY_FUNCTION__);
|
||||
}
|
||||
|
||||
BOOL ios6Style = (NSFoundationVersionNumber < DTNSFoundationVersionNumber_iOS_7_0);
|
||||
CGFloat contentWidth = tableView.frame.size.width;
|
||||
|
||||
// reduce width for grouped table views
|
||||
if (ios6Style && tableView.style == UITableViewStyleGrouped)
|
||||
{
|
||||
contentWidth -= [self _groupedCellMarginWithTableWidth:contentWidth] * 2;
|
||||
}
|
||||
|
||||
// reduce width for accessories
|
||||
|
||||
switch (self.accessoryType)
|
||||
{
|
||||
case UITableViewCellAccessoryDisclosureIndicator:
|
||||
{
|
||||
contentWidth -= ios6Style ? 20.0f : 10.0f + 8.0f + 15.0f;
|
||||
break;
|
||||
}
|
||||
|
||||
case UITableViewCellAccessoryCheckmark:
|
||||
{
|
||||
contentWidth -= ios6Style ? 20.0f : 10.0f + 14.0f + 15.0f;
|
||||
break;
|
||||
}
|
||||
#if TARGET_OS_IOS
|
||||
case UITableViewCellAccessoryDetailDisclosureButton:
|
||||
{
|
||||
contentWidth -= ios6Style ? 33.0f : 10.0f + 42.0f + 15.0f;
|
||||
break;
|
||||
}
|
||||
|
||||
#if __IPHONE_OS_VERSION_MAX_ALLOWED > __IPHONE_6_1
|
||||
case UITableViewCellAccessoryDetailButton:
|
||||
{
|
||||
contentWidth -= 10.0f + 22.0f + 15.0f;
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
case UITableViewCellAccessoryNone:
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
{
|
||||
DTLogWarning(@"AccessoryType %d not implemented on %@", self.accessoryType, NSStringFromClass([self class]));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
CGSize neededSize = [self.attributedTextContextView suggestedFrameSizeToFitEntireStringConstraintedToWidth:contentWidth];
|
||||
|
||||
// note: non-integer row heights caused trouble < iOS 5.0
|
||||
return neededSize.height;
|
||||
}
|
||||
|
||||
#pragma mark Properties
|
||||
|
||||
- (void)setSelected:(BOOL)selected animated:(BOOL)animated
|
||||
{
|
||||
[super setSelected:selected animated:animated];
|
||||
|
||||
// Configure the view for the selected state
|
||||
}
|
||||
|
||||
- (void)setHTMLString:(NSString *)html
|
||||
{
|
||||
[self setHTMLString:html options:nil];
|
||||
}
|
||||
|
||||
- (void) setHTMLString:(NSString *)html options:(NSDictionary*) options {
|
||||
|
||||
NSUInteger newHash = [html hash];
|
||||
|
||||
if (newHash == _htmlHash)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_htmlHash = newHash;
|
||||
|
||||
NSData *data = [html dataUsingEncoding:NSUTF8StringEncoding];
|
||||
NSAttributedString *string = [[NSAttributedString alloc] initWithHTMLData:data options:options documentAttributes:NULL];
|
||||
self.attributedString = string;
|
||||
|
||||
[self setNeedsLayout];
|
||||
|
||||
}
|
||||
|
||||
- (void)setAttributedString:(NSAttributedString *)attributedString
|
||||
{
|
||||
// passthrough
|
||||
self.attributedTextContextView.attributedString = attributedString;
|
||||
}
|
||||
|
||||
- (NSAttributedString *)attributedString
|
||||
{
|
||||
// passthrough
|
||||
return _attributedTextContextView.attributedString;
|
||||
}
|
||||
|
||||
- (DTAttributedTextContentView *)attributedTextContextView
|
||||
{
|
||||
if (!_attributedTextContextView)
|
||||
{
|
||||
// don't know size because there's no string in it
|
||||
_attributedTextContextView = [[DTAttributedTextContentView alloc] initWithFrame:self.contentView.bounds];
|
||||
|
||||
_attributedTextContextView.edgeInsets = UIEdgeInsetsMake(5, 5, 5, 5);
|
||||
_attributedTextContextView.layoutFrameHeightIsConstrainedByBounds = _hasFixedRowHeight;
|
||||
_attributedTextContextView.delegate = _textDelegate;
|
||||
|
||||
[self.contentView addSubview:_attributedTextContextView];
|
||||
}
|
||||
|
||||
return _attributedTextContextView;
|
||||
}
|
||||
|
||||
- (void)setHasFixedRowHeight:(BOOL)hasFixedRowHeight
|
||||
{
|
||||
if (_hasFixedRowHeight != hasFixedRowHeight)
|
||||
{
|
||||
_hasFixedRowHeight = hasFixedRowHeight;
|
||||
|
||||
[self setNeedsLayout];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setTextDelegate:(id)textDelegate
|
||||
{
|
||||
_textDelegate = textDelegate;
|
||||
_attributedTextContextView.delegate = _textDelegate;
|
||||
}
|
||||
|
||||
@synthesize attributedTextContextView = _attributedTextContextView;
|
||||
@synthesize hasFixedRowHeight = _hasFixedRowHeight;
|
||||
@synthesize textDelegate = _textDelegate;
|
||||
|
||||
@end
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,370 @@
|
||||
//
|
||||
// DTAttributedTextContentView.h
|
||||
// DTCoreText
|
||||
//
|
||||
// Created by Oliver Drobnik on 1/9/11.
|
||||
// Copyright 2011 Drobnik.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import "DTCompatibility.h"
|
||||
|
||||
#if TARGET_OS_IPHONE && !TARGET_OS_WATCH
|
||||
|
||||
#import "DTCoreTextLayoutFrame.h"
|
||||
#import <DTFoundation/DTWeakSupport.h>
|
||||
|
||||
@class DTAttributedTextContentView;
|
||||
@class DTCoreTextLayoutFrame;
|
||||
@class DTTextBlock;
|
||||
@class DTCoreTextLayouter;
|
||||
@class DTTextAttachment;
|
||||
|
||||
/**
|
||||
notification that gets sent as soon as the receiver has done a layout pass
|
||||
*/
|
||||
extern NSString * const DTAttributedTextContentViewDidFinishLayoutNotification;
|
||||
|
||||
/**
|
||||
Protocol to provide custom views for elements in an DTAttributedTextContentView. Also the delegate gets notified once the text view has been drawn.
|
||||
*/
|
||||
@protocol DTAttributedTextContentViewDelegate <NSObject>
|
||||
|
||||
@optional
|
||||
|
||||
/**
|
||||
@name Notifications
|
||||
*/
|
||||
|
||||
/**
|
||||
Called before a layout frame or a part of it is drawn. The text delegate can draw contents that goes under the text in this method.
|
||||
|
||||
@param attributedTextContentView The content view that will be drawing a layout frame
|
||||
@param layoutFrame The layout frame that will be drawn for
|
||||
@param context The graphics context that will drawn into
|
||||
*/
|
||||
- (void)attributedTextContentView:(DTAttributedTextContentView *)attributedTextContentView willDrawLayoutFrame:(DTCoreTextLayoutFrame *)layoutFrame inContext:(CGContextRef)context;
|
||||
|
||||
|
||||
/**
|
||||
Called after a layout frame or a part of it is drawn. The text delegate can draw contents that goes over the text in this method.
|
||||
|
||||
@param attributedTextContentView The content view that drew a layout frame
|
||||
@param layoutFrame The layout frame that was drawn for
|
||||
@param context The graphics context that was drawn into
|
||||
*/
|
||||
- (void)attributedTextContentView:(DTAttributedTextContentView *)attributedTextContentView didDrawLayoutFrame:(DTCoreTextLayoutFrame *)layoutFrame inContext:(CGContextRef)context;
|
||||
|
||||
|
||||
/**
|
||||
Called before the text belonging to a text block is drawn.
|
||||
|
||||
This gives the developer an opportunity to draw a custom background below a text block.
|
||||
|
||||
@param attributedTextContentView The content view that drew a layout frame
|
||||
@param textBlock The text block
|
||||
@param frame The frame within the content view's coordinate system that will be drawn into
|
||||
@param context The graphics context that will be drawn into
|
||||
@param layoutFrame The layout frame that will be drawn for
|
||||
@returns `YES` is the standard fill of the text block should be drawn, `NO` if it should not
|
||||
*/
|
||||
- (BOOL)attributedTextContentView:(DTAttributedTextContentView *)attributedTextContentView shouldDrawBackgroundForTextBlock:(DTTextBlock *)textBlock frame:(CGRect)frame context:(CGContextRef)context forLayoutFrame:(DTCoreTextLayoutFrame *)layoutFrame;
|
||||
|
||||
/**
|
||||
@name Providing Custom Views for Content
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
Provide custom view for an attachment, e.g. an imageView for images
|
||||
|
||||
@param attributedTextContentView The content view asking for a custom view
|
||||
@param attachment The <DTTextAttachment> that this view should represent
|
||||
@param frame The frame that the view should use to fit on top of the space reserved for the attachment
|
||||
@returns The view that should represent the given attachment
|
||||
*/
|
||||
- (UIView *)attributedTextContentView:(DTAttributedTextContentView *)attributedTextContentView viewForAttachment:(DTTextAttachment *)attachment frame:(CGRect)frame;
|
||||
|
||||
|
||||
/**
|
||||
Provide button to be placed over links, the identifier is used to link multiple parts of the same A tag
|
||||
|
||||
@param attributedTextContentView The content view asking for a custom view
|
||||
@param url The `NSURL` of the hyperlink
|
||||
@param identifier An identifier that uniquely identifies the hyperlink within the document
|
||||
@param frame The frame that the view should use to fit on top of the space reserved for the attachment
|
||||
@returns The view that should represent the given hyperlink
|
||||
*/
|
||||
- (UIView *)attributedTextContentView:(DTAttributedTextContentView *)attributedTextContentView viewForLink:(NSURL *)url identifier:(NSString *)identifier frame:(CGRect)frame;
|
||||
|
||||
|
||||
/**
|
||||
Provide generic views for all attachments.
|
||||
|
||||
This is only called if the more specific delegate methods are not implemented.
|
||||
|
||||
@param attributedTextContentView The content view asking for a custom view
|
||||
@param string The attributed sub-string containing this element
|
||||
@param frame The frame that the view should use to fit on top of the space reserved for the attachment
|
||||
@returns The view that should represent the given hyperlink or text attachment
|
||||
@see attributedTextContentView:viewForAttachment:frame: and attributedTextContentView:viewForAttachment:frame:
|
||||
*/
|
||||
- (UIView *)attributedTextContentView:(DTAttributedTextContentView *)attributedTextContentView viewForAttributedString:(NSAttributedString *)string frame:(CGRect)frame;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
enum {
|
||||
DTAttributedTextContentViewRelayoutNever = 0,
|
||||
DTAttributedTextContentViewRelayoutOnWidthChanged = 1 << 0,
|
||||
DTAttributedTextContentViewRelayoutOnHeightChanged = 1 << 1,
|
||||
};
|
||||
typedef NSUInteger DTAttributedTextContentViewRelayoutMask;
|
||||
|
||||
|
||||
/**
|
||||
Attributed Text Content Views display attributed strings generated by DTHTMLAttributedStringBuilder. They can display images and hyperlinks inline or optionally place custom subviews (which get provided via the <delegate> in the appropriate places. By itself content views do not scroll, for that there is the `UIScrollView` subclass <DTAttributedTextView>.
|
||||
|
||||
Generally you have two options to providing content:
|
||||
|
||||
- set the attributed string
|
||||
- set a layout frame
|
||||
|
||||
The first you would normally use, the second you would use if you are layouting a larger text and then simply want to display individual parts (e.g. pages from an e-book) in a content view.
|
||||
|
||||
DTAttributedTextContentView is designed to be used as the content view inside a DTAttributedTextView and thus sizes its intrinsicContentSize always to be the same as the width of the set frame. Use DTAttributedLabel if you don't require scrolling behavior.
|
||||
*/
|
||||
|
||||
@interface DTAttributedTextContentView : UIView
|
||||
{
|
||||
NSAttributedString *_attributedString;
|
||||
DTCoreTextLayoutFrame *_layoutFrame;
|
||||
|
||||
UIEdgeInsets _edgeInsets;
|
||||
|
||||
NSMutableDictionary *customViewsForAttachmentsIndex;
|
||||
|
||||
BOOL _flexibleHeight;
|
||||
|
||||
// for layoutFrame
|
||||
NSInteger _numberOfLines;
|
||||
NSLineBreakMode _lineBreakMode;
|
||||
NSAttributedString *_truncationString;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
@name Sizing
|
||||
*/
|
||||
|
||||
/**
|
||||
Calculates the suggested frame size that would fit the entire <attributedString> with a maximum width.
|
||||
|
||||
This does a full layout pass that is cached in <DTCoreTextLayouter>. If you specify a frame that fits the result from this method then the resulting layoutFrame is reused.
|
||||
|
||||
Since this obeys the <edgeInsets> you have to add these to the final frame size.
|
||||
|
||||
@param width The maximum width to layout for
|
||||
@returns The suggested frame size
|
||||
*/
|
||||
- (CGSize)suggestedFrameSizeToFitEntireStringConstraintedToWidth:(CGFloat)width; // obeys the edge insets
|
||||
|
||||
/**
|
||||
The size of contents of the receiver. This is possibly used by auto-layout, but also for example if you want to get the size of the receiver necessary for a scroll view
|
||||
|
||||
This method is defined as of iOS 6, but to support earlier OS versions
|
||||
*/
|
||||
- (CGSize)intrinsicContentSize;
|
||||
|
||||
/**
|
||||
Whether the receiver calculates layout limited to the view bounds.
|
||||
|
||||
If set to `YES` then the layout process calculates the layoutFrame with open ended height. If set to ´NO` then the current bounds of the receiver determine the height.
|
||||
*/
|
||||
@property (nonatomic, assign) BOOL layoutFrameHeightIsConstrainedByBounds;
|
||||
|
||||
|
||||
/**
|
||||
@name Layouting
|
||||
*/
|
||||
|
||||
/**
|
||||
Discards the current <layoutFrame> and creates a new one based on the <attributedString>.
|
||||
*/
|
||||
- (void)relayoutText;
|
||||
|
||||
|
||||
/**
|
||||
The layouter to use for the receiver. Created by default.
|
||||
|
||||
By default this is generated automatically for the current <attributedString>. You can also supply your own if you require special layouting behavior.
|
||||
*/
|
||||
@property (atomic, strong) DTCoreTextLayouter *layouter;
|
||||
|
||||
|
||||
/**
|
||||
The layout frame to use for the receiver. Created by default.
|
||||
|
||||
A layout frame is basically one rectangle, inset by the <edgeInsets>. By default this is automatically generated for the current <attributedString>. You can also create a <DTCoreTextLayoutFrame> seperately and set this property to display the layout frame. This is usedful for example if you layout entire e-book and then set the <layoutFrame> for displaying individual pages.
|
||||
*/
|
||||
@property (atomic, strong) DTCoreTextLayoutFrame *layoutFrame;
|
||||
|
||||
|
||||
/**
|
||||
@name Working with Custom Subviews
|
||||
*/
|
||||
|
||||
/**
|
||||
Removes all custom subviews (excluding views representing links) from the receiver.
|
||||
*/
|
||||
- (void)removeAllCustomViews;
|
||||
|
||||
|
||||
/**
|
||||
Removes all custom subviews representing links from the receiver
|
||||
*/
|
||||
- (void)removeAllCustomViewsForLinks;
|
||||
|
||||
|
||||
/**
|
||||
Removes invisible custom subviews and lays out subviews visible in the given rectangle
|
||||
@param rect The bounds of the visible area to layout custom subviews in.
|
||||
*/
|
||||
- (void)layoutSubviewsInRect:(CGRect)rect;
|
||||
|
||||
|
||||
/**
|
||||
@name Providing Content
|
||||
*/
|
||||
|
||||
/**
|
||||
The attributed string to display in the receiver
|
||||
*/
|
||||
@property (nonatomic, copy) NSAttributedString *attributedString;
|
||||
|
||||
|
||||
/**
|
||||
The delegate that is in charge of supplying custom behavior for the receiver. It must conform to <DTAttributedTextContentViewDelegate> and provide custom subviews, link buttons, etc.
|
||||
*/
|
||||
|
||||
@property (nonatomic, DT_WEAK_PROPERTY) IBOutlet id <DTAttributedTextContentViewDelegate> delegate;
|
||||
|
||||
/**
|
||||
@name Customizing Content Display
|
||||
*/
|
||||
|
||||
/**
|
||||
The insets to apply around the text content
|
||||
*/
|
||||
@property (nonatomic) UIEdgeInsets edgeInsets;
|
||||
|
||||
/**
|
||||
Specifies if the receiver should draw image text attachments.
|
||||
|
||||
Set to `NO` if you use the delegate methods to provide custom subviews to display images.
|
||||
*/
|
||||
@property (nonatomic) BOOL shouldDrawImages;
|
||||
|
||||
|
||||
/**
|
||||
Specified if the receiver should draw hyperlinks.
|
||||
|
||||
If set to `NO` then your custom subview/button for hyperlinks is responsible for displaying hyperlinks. You can use <DTLinkButton> to have links show a differently for normal and highlighted style
|
||||
*/
|
||||
@property (nonatomic) BOOL shouldDrawLinks;
|
||||
|
||||
|
||||
/**
|
||||
Specifies if the receiver should layout custom subviews in layoutSubviews.
|
||||
|
||||
If set to `YES` then all custom subviews will always be layouted. Set to `NO` to only layout visible subviews, e.g. in a scroll view. Defaults to `YES` if used stand-alone, `NO` inside a <DTAttributedTextView>.
|
||||
*/
|
||||
@property (nonatomic) BOOL shouldLayoutCustomSubviews;
|
||||
|
||||
|
||||
/**
|
||||
The amount by which all contents of the receiver will offset of display and subview layouting
|
||||
*/
|
||||
@property (nonatomic) CGPoint layoutOffset;
|
||||
|
||||
|
||||
/**
|
||||
The offset to apply for drawing the background.
|
||||
|
||||
If you set a pattern color as background color you can have the pattern phase be offset by this value.
|
||||
*/
|
||||
@property (nonatomic) CGSize backgroundOffset;
|
||||
|
||||
|
||||
/**
|
||||
An integer bit mask that determines how the receiver relayouts its contents when its bounds change.
|
||||
|
||||
When the view’s bounds change, that view automatically re-layouts its text according to the relayout mask. You specify the value of this mask by combining the constants described in DTAttributedTextContentViewRelayoutMask using the C bitwise OR operator. Combining these constants lets you specify which dimensions will cause a re-layout if modified. The default value of this property is DTAttributedTextContentViewRelayoutOnWidthChanged, which indicates that the text will be re-layouted if the width changes, but not if the height changes.
|
||||
*/
|
||||
@property (nonatomic) DTAttributedTextContentViewRelayoutMask relayoutMask;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
/**
|
||||
You can globally customize the layer class to be used for new instances of <DTAttributedTextContentView>. By itself it makes most sense to go with the default `CALayer`. For larger bodies of text, i.e. if there is scrolling then you should use a `CATiledLayer` subclass instead.
|
||||
*/
|
||||
@interface DTAttributedTextContentView (Tiling)
|
||||
|
||||
/**
|
||||
Sets the layer class globally to use in new instances of content views. Defaults to `CALayer`.
|
||||
|
||||
While being fine for most use cases you should use a `CATiledLayer` subclass for anything larger than a screen full, e.g. in scroll views.
|
||||
@param layerClass The class to use, should be a `CALayer` subclass
|
||||
*/
|
||||
+ (void)setLayerClass:(Class)layerClass;
|
||||
|
||||
/**
|
||||
The current layer class that is used for new instances
|
||||
@returns The `CALayer` subclass that new instances are using
|
||||
*/
|
||||
+ (Class)layerClass;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
/**
|
||||
Methods for drawing the content view
|
||||
*/
|
||||
@interface DTAttributedTextContentView (Drawing)
|
||||
|
||||
/**
|
||||
Creates an image from a part of the receiver's content view
|
||||
@param bounds The bounds of the content to draw
|
||||
@param options The drawing options to apply when drawing
|
||||
@see [DTCoreTextLayoutFrame drawInContext:options:] for a list of available drawing options
|
||||
@returns A `UIImage` with the specified content
|
||||
*/
|
||||
- (UIImage *)contentImageWithBounds:(CGRect)bounds options:(DTCoreTextLayoutFrameDrawingOptions)options;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
/**
|
||||
Methods for getting cursor position and frame. Those are convenience methods that call through to the layoutFrame property which has the same coordinate system as the receiver.
|
||||
*/
|
||||
@interface DTAttributedTextContentView (Cursor)
|
||||
|
||||
/**
|
||||
Determines the closest string index to a point in the receiver's frame.
|
||||
|
||||
This can be used to find the cursor position to position an input caret at.
|
||||
@param point The point
|
||||
@returns The resulting string index
|
||||
*/
|
||||
- (NSInteger)closestCursorIndexToPoint:(CGPoint)point;
|
||||
|
||||
/**
|
||||
The rectangle to draw a caret for a given index
|
||||
@param index The string index for which to determine a cursor frame
|
||||
@returns The cursor rectangle
|
||||
*/
|
||||
- (CGRect)cursorRectAtIndex:(NSInteger)index;
|
||||
|
||||
@end
|
||||
|
||||
#endif
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,136 @@
|
||||
//
|
||||
// DTAttributedTextView.h
|
||||
// DTCoreText
|
||||
//
|
||||
// Created by Oliver Drobnik on 1/12/11.
|
||||
// Copyright 2011 Drobnik.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import "DTCompatibility.h"
|
||||
|
||||
#if TARGET_OS_IPHONE && !TARGET_OS_WATCH
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
#import "DTAttributedTextContentView.h"
|
||||
|
||||
@class DTAttributedTextView;
|
||||
|
||||
/**
|
||||
This view is designed to be a replacement for `UITextView`. It is a `UIScrollView` subclass and creates a <DTAttributedTextContentView> as content view for displaying the text.
|
||||
|
||||
The content view of type <DTAttributedTextContentView> is created lazily. You should not set values on it directly if you use it in conjunction with this class for scrolling.
|
||||
*/
|
||||
|
||||
@interface DTAttributedTextView : UIScrollView
|
||||
{
|
||||
// ivars needed by subclasses
|
||||
DTAttributedTextContentView *_attributedTextContentView;
|
||||
}
|
||||
|
||||
/**
|
||||
@name Providing Content
|
||||
*/
|
||||
|
||||
/**
|
||||
The attributed text to be displayed in the text content view of the receiver.
|
||||
*/
|
||||
@property (nonatomic, strong) NSAttributedString *attributedString;
|
||||
|
||||
|
||||
/**
|
||||
A delegate implementing DTAttributedTextContentViewDelegate to provide custom subviews for images and links.
|
||||
*/
|
||||
@property (nonatomic, DT_WEAK_PROPERTY) IBOutlet id <DTAttributedTextContentViewDelegate> textDelegate;
|
||||
|
||||
|
||||
/**
|
||||
Performs a new layout pass on the receiver. This destroys the frame setter, calls relayoutText on the content view and marks the receiver as needing layout so that custom subviews get appropriately sized.
|
||||
*/
|
||||
- (void)relayoutText;
|
||||
|
||||
/**
|
||||
@name Accessing Subviews
|
||||
*/
|
||||
|
||||
/**
|
||||
References to the DTAttributedTextContentView that display the text. This is not named contentView because this class inherits from `UIScrollView` which has an internal property of this name
|
||||
*/
|
||||
@property (nonatomic, strong, readonly) DTAttributedTextContentView *attributedTextContentView;
|
||||
|
||||
/**
|
||||
A view to be displayed behind the text content view
|
||||
*/
|
||||
@property (nonatomic, strong) IBOutlet UIView *backgroundView;
|
||||
|
||||
|
||||
/**
|
||||
@name Customizing Display
|
||||
*/
|
||||
|
||||
/**
|
||||
If the content view of the receiver should draw links. Set to `NO` if displaying links as custom views via textDelegate;
|
||||
|
||||
Defaults to `YES` if you supply your own link drawing then set this property to NO and supply your custom view (e.g. <DTLinkButton>) via the <textDelegate>.
|
||||
*/
|
||||
@property (nonatomic, assign) BOOL shouldDrawLinks;
|
||||
|
||||
/**
|
||||
If the content view of the receiver should draw images. Set to `NO` if displaying images as custom views via textDelegate;
|
||||
|
||||
Defaults to `YES` if you supply your own image drawing then set this property to NO and supply your custom image view (e.g. <DTLazyImageView>) via the <textDelegate>.
|
||||
*/
|
||||
@property (nonatomic, assign) BOOL shouldDrawImages;
|
||||
|
||||
|
||||
/**
|
||||
@name Customizing Content View
|
||||
*/
|
||||
|
||||
/**
|
||||
You can override this method to provide a different class to use for the content view. If you replace the content view class then it should inherit from <DTAttributedTextContentView> which is also the default.
|
||||
@returns The class to use for the content view.
|
||||
*/
|
||||
- (Class)classForContentView;
|
||||
|
||||
/**
|
||||
@name User Interaction
|
||||
*/
|
||||
|
||||
/**
|
||||
Scrolls the receiver to the anchor with the given name to the top.
|
||||
@param anchorName The name of the href anchor.
|
||||
@param animated `YES` if the movement should be animated.
|
||||
*/
|
||||
- (void)scrollToAnchorNamed:(NSString *)anchorName animated:(BOOL)animated;
|
||||
|
||||
/**
|
||||
Scrolls the receiver until the text in the specified range is visible.
|
||||
@param range The range of text to scroll into view.
|
||||
@param animated `YES` if the movement should be animated.
|
||||
*/
|
||||
- (void)scrollRangeToVisible:(NSRange)range animated:(BOOL)animated;
|
||||
|
||||
/**
|
||||
@name Working with a Cursor
|
||||
*/
|
||||
|
||||
/**
|
||||
Determines the closest string index to a point in the receiver's frame.
|
||||
|
||||
This can be used to find the cursor position to position an input caret at.
|
||||
@param point The point
|
||||
@returns The resulting string index
|
||||
*/
|
||||
- (NSInteger)closestCursorIndexToPoint:(CGPoint)point;
|
||||
|
||||
/**
|
||||
The rectangle to draw a caret for a given index
|
||||
@param index The string index for which to determine a cursor frame
|
||||
@returns The cursor rectangle
|
||||
*/
|
||||
- (CGRect)cursorRectAtIndex:(NSInteger)index;
|
||||
|
||||
@end
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,429 @@
|
||||
//
|
||||
// DTAttributedTextView.m
|
||||
// DTCoreText
|
||||
//
|
||||
// Created by Oliver Drobnik on 1/12/11.
|
||||
// Copyright 2011 Drobnik.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import "DTAttributedTextView.h"
|
||||
|
||||
#if TARGET_OS_IPHONE && !TARGET_OS_WATCH
|
||||
|
||||
#import <QuartzCore/QuartzCore.h>
|
||||
|
||||
#import "DTCoreText.h"
|
||||
|
||||
#import <DTFoundation/DTTiledLayerWithoutFade.h>
|
||||
#import <DTFoundation/DTBlockFunctions.h>
|
||||
|
||||
|
||||
@interface DTAttributedTextView ()
|
||||
|
||||
- (void)_setup;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
|
||||
@implementation DTAttributedTextView
|
||||
{
|
||||
UIView *_backgroundView;
|
||||
|
||||
// these are pass-through, i.e. store until the content view is created
|
||||
DT_WEAK_VARIABLE id textDelegate;
|
||||
NSAttributedString *_attributedString;
|
||||
|
||||
BOOL _shouldDrawLinks;
|
||||
BOOL _shouldDrawImages;
|
||||
}
|
||||
|
||||
- (id)initWithFrame:(CGRect)frame
|
||||
{
|
||||
self = [super initWithFrame:frame];
|
||||
|
||||
if (self)
|
||||
{
|
||||
[self _setup];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||||
}
|
||||
|
||||
- (void)layoutSubviews
|
||||
{
|
||||
[super layoutSubviews];
|
||||
|
||||
self.attributedTextContentView.edgeInsets = self.contentInset;
|
||||
|
||||
// layout custom subviews for visible area
|
||||
[_attributedTextContentView layoutSubviewsInRect:self.bounds];
|
||||
}
|
||||
|
||||
- (void)awakeFromNib
|
||||
{
|
||||
[super awakeFromNib];
|
||||
[self _setup];
|
||||
}
|
||||
|
||||
- (void)safeAreaInsetsDidChange
|
||||
{
|
||||
[super safeAreaInsetsDidChange];
|
||||
}
|
||||
|
||||
// default
|
||||
- (void)_setup
|
||||
{
|
||||
if (self.backgroundColor)
|
||||
{
|
||||
CGFloat alpha = [self.backgroundColor alphaComponent];
|
||||
|
||||
if (alpha < 1.0)
|
||||
{
|
||||
self.opaque = NO;
|
||||
}
|
||||
else
|
||||
{
|
||||
self.opaque = YES;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
self.backgroundColor = [DTColor whiteColor];
|
||||
self.opaque = YES;
|
||||
}
|
||||
|
||||
self.autoresizesSubviews = NO;
|
||||
self.clipsToBounds = YES;
|
||||
|
||||
// defaults
|
||||
_shouldDrawLinks = YES;
|
||||
_shouldDrawImages = YES;
|
||||
}
|
||||
|
||||
// override class e.g. for mutable content view
|
||||
- (Class)classForContentView
|
||||
{
|
||||
return [DTAttributedTextContentView class];
|
||||
}
|
||||
|
||||
#pragma mark External Methods
|
||||
- (void)scrollToAnchorNamed:(NSString *)anchorName animated:(BOOL)animated
|
||||
{
|
||||
NSRange range = [self.attributedTextContentView.attributedString rangeOfAnchorNamed:anchorName];
|
||||
|
||||
if (range.location != NSNotFound)
|
||||
{
|
||||
[self scrollRangeToVisible:range animated:animated];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)scrollRangeToVisible:(NSRange)range animated:(BOOL)animated
|
||||
{
|
||||
// get the line of the first index of the anchor range
|
||||
DTCoreTextLayoutLine *line = [self.attributedTextContentView.layoutFrame lineContainingIndex:range.location];
|
||||
|
||||
// make sure we don't scroll too far
|
||||
CGFloat maxScrollPos = self.contentSize.height - self.bounds.size.height + self.contentInset.bottom + self.contentInset.top;
|
||||
CGFloat scrollPos = MIN(line.frame.origin.y, maxScrollPos);
|
||||
|
||||
// scroll
|
||||
[self setContentOffset:CGPointMake(0, scrollPos) animated:animated];
|
||||
}
|
||||
|
||||
- (void)relayoutText
|
||||
{
|
||||
DT_WEAK_VARIABLE typeof(self) weakSelf = self;
|
||||
DTBlockPerformSyncIfOnMainThreadElseAsync(^{
|
||||
DTAttributedTextView *strongSelf = weakSelf;
|
||||
|
||||
// need to reset the layouter because otherwise we get the old framesetter or cached layout frames
|
||||
strongSelf->_attributedTextContentView.layouter = nil;
|
||||
|
||||
// here we're layouting the entire string, might be more efficient to only relayout the paragraphs that contain these attachments
|
||||
[strongSelf->_attributedTextContentView relayoutText];
|
||||
|
||||
// layout custom subviews for visible area
|
||||
[strongSelf setNeedsLayout];
|
||||
});
|
||||
}
|
||||
|
||||
#pragma mark - Working with a Cursor
|
||||
|
||||
- (NSInteger)closestCursorIndexToPoint:(CGPoint)point
|
||||
{
|
||||
// the point is in the coordinate system of the receiver, need to convert into those of the content view first
|
||||
CGPoint pointInContentView = [self.attributedTextContentView convertPoint:point fromView:self];
|
||||
|
||||
return [self.attributedTextContentView closestCursorIndexToPoint:pointInContentView];
|
||||
}
|
||||
|
||||
- (CGRect)cursorRectAtIndex:(NSInteger)index
|
||||
{
|
||||
CGRect rectInContentView = [self.attributedTextContentView cursorRectAtIndex:index];
|
||||
|
||||
// the point is in the coordinate system of the content view, need to convert into those of the receiver first
|
||||
CGRect rect = [self.attributedTextContentView convertRect:rectInContentView toView:self];
|
||||
|
||||
return rect;
|
||||
}
|
||||
|
||||
#pragma mark Notifications
|
||||
- (void)contentViewDidLayout:(NSNotification *)notification
|
||||
{
|
||||
DT_WEAK_VARIABLE typeof(self) weakSelf = self;
|
||||
DTBlockPerformSyncIfOnMainThreadElseAsync(^{
|
||||
DTAttributedTextView *strongSelf = weakSelf;
|
||||
|
||||
NSDictionary *userInfo = [notification userInfo];
|
||||
CGRect optimalFrame = [[userInfo objectForKey:@"OptimalFrame"] CGRectValue];
|
||||
|
||||
CGRect frame = UIEdgeInsetsInsetRect(self.bounds, self.contentInset);
|
||||
|
||||
// ignore possibly delayed layout notification for a different width
|
||||
if (optimalFrame.size.width == frame.size.width)
|
||||
{
|
||||
strongSelf->_attributedTextContentView.frame = optimalFrame;
|
||||
strongSelf.contentSize = [strongSelf->_attributedTextContentView intrinsicContentSize];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#pragma mark Properties
|
||||
- (DTAttributedTextContentView *)attributedTextContentView
|
||||
{
|
||||
if (!_attributedTextContentView)
|
||||
{
|
||||
// subclasses can specify a DTAttributedTextContentView subclass instead
|
||||
Class classToUse = [self classForContentView];
|
||||
|
||||
CGRect frame = UIEdgeInsetsInsetRect(self.bounds, self.contentInset);
|
||||
|
||||
if (frame.size.width<=0 || frame.size.height<=0)
|
||||
{
|
||||
frame = CGRectZero;
|
||||
}
|
||||
|
||||
// make sure we always have a tiled layer
|
||||
Class previousLayerClass = nil;
|
||||
|
||||
// for DTAttributedTextContentView subclasses we force a tiled layer
|
||||
if ([classToUse isSubclassOfClass:[DTAttributedTextContentView class]])
|
||||
{
|
||||
Class layerClass = [DTAttributedTextContentView layerClass];
|
||||
|
||||
if (![layerClass isSubclassOfClass:[CATiledLayer class]])
|
||||
{
|
||||
[DTAttributedTextContentView setLayerClass:[DTTiledLayerWithoutFade class]];
|
||||
previousLayerClass = layerClass;
|
||||
}
|
||||
}
|
||||
|
||||
_attributedTextContentView = [[classToUse alloc] initWithFrame:frame];
|
||||
|
||||
// restore previous layer class if we changed the layer class for the content view
|
||||
if (previousLayerClass)
|
||||
{
|
||||
[DTAttributedTextContentView setLayerClass:previousLayerClass];
|
||||
}
|
||||
|
||||
_attributedTextContentView.userInteractionEnabled = YES;
|
||||
_attributedTextContentView.backgroundColor = self.backgroundColor;
|
||||
_attributedTextContentView.shouldLayoutCustomSubviews = NO; // we call layout when scrolling
|
||||
|
||||
// adjust opaqueness based on background color alpha
|
||||
CGFloat alpha = [self.backgroundColor alphaComponent];
|
||||
|
||||
if (alpha < 1.0)
|
||||
{
|
||||
_attributedTextContentView.opaque = NO;
|
||||
}
|
||||
else
|
||||
{
|
||||
_attributedTextContentView.opaque = YES;
|
||||
}
|
||||
|
||||
// set text delegate if it was set before instantiation of content view
|
||||
_attributedTextContentView.delegate = self->_textDelegate;
|
||||
|
||||
// pass on setting
|
||||
_attributedTextContentView.shouldDrawLinks = _shouldDrawLinks;
|
||||
|
||||
// notification that tells us about the actual size of the content view
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(contentViewDidLayout:) name:DTAttributedTextContentViewDidFinishLayoutNotification object:_attributedTextContentView];
|
||||
|
||||
// temporary frame to specify the width
|
||||
_attributedTextContentView.frame = frame;
|
||||
|
||||
// set text we previously got, this also triggers a relayout
|
||||
_attributedTextContentView.attributedString = _attributedString;
|
||||
|
||||
// this causes a relayout and the resulting notification will allow us to set the final frame
|
||||
|
||||
[self addSubview:_attributedTextContentView];
|
||||
}
|
||||
|
||||
return _attributedTextContentView;
|
||||
}
|
||||
|
||||
- (void)setBackgroundColor:(DTColor *)newColor
|
||||
{
|
||||
if ([newColor alphaComponent] < 1.0)
|
||||
{
|
||||
super.backgroundColor = newColor;
|
||||
_attributedTextContentView.backgroundColor = [DTColor clearColor];
|
||||
self.opaque = NO;
|
||||
}
|
||||
else
|
||||
{
|
||||
super.backgroundColor = newColor;
|
||||
|
||||
if (_attributedTextContentView.opaque)
|
||||
{
|
||||
_attributedTextContentView.backgroundColor = newColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setContentInset:(UIEdgeInsets)contentInset
|
||||
{
|
||||
[super setContentInset:contentInset];
|
||||
|
||||
// height does not matter, that will be determined anyhow
|
||||
CGRect contentFrame = CGRectMake(0, 0, self.frame.size.width - self.contentInset.left - self.contentInset.right, _attributedTextContentView.frame.size.height);
|
||||
|
||||
if (CGRectEqualToRect(contentFrame, self.attributedTextContentView.frame))
|
||||
{
|
||||
self.attributedTextContentView.frame = contentFrame;
|
||||
}
|
||||
}
|
||||
|
||||
- (UIView *)backgroundView
|
||||
{
|
||||
if (!_backgroundView)
|
||||
{
|
||||
_backgroundView = [[UIView alloc] initWithFrame:self.bounds];
|
||||
_backgroundView.backgroundColor = [DTColor whiteColor];
|
||||
|
||||
// default is no interaction because background should have no interaction
|
||||
_backgroundView.userInteractionEnabled = NO;
|
||||
|
||||
[self insertSubview:_backgroundView belowSubview:self.attributedTextContentView];
|
||||
|
||||
// make content transparent so that we see the background
|
||||
_attributedTextContentView.backgroundColor = [DTColor clearColor];
|
||||
_attributedTextContentView.opaque = NO;
|
||||
}
|
||||
|
||||
return _backgroundView;
|
||||
}
|
||||
|
||||
- (void)setBackgroundView:(UIView *)backgroundView
|
||||
{
|
||||
if (_backgroundView != backgroundView)
|
||||
{
|
||||
[_backgroundView removeFromSuperview];
|
||||
_backgroundView = backgroundView;
|
||||
|
||||
if (_attributedTextContentView)
|
||||
{
|
||||
[self insertSubview:_backgroundView belowSubview:_attributedTextContentView];
|
||||
}
|
||||
else
|
||||
{
|
||||
[self addSubview:_backgroundView];
|
||||
}
|
||||
|
||||
if (_backgroundView)
|
||||
{
|
||||
// make content transparent so that we see the background
|
||||
_attributedTextContentView.backgroundColor = [DTColor clearColor];
|
||||
_attributedTextContentView.opaque = NO;
|
||||
}
|
||||
else
|
||||
{
|
||||
_attributedTextContentView.backgroundColor = [DTColor whiteColor];
|
||||
_attributedTextContentView.opaque = YES;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setAttributedString:(NSAttributedString *)string
|
||||
{
|
||||
_attributedString = string;
|
||||
|
||||
// might need layout for visible custom views
|
||||
[self setNeedsLayout];
|
||||
|
||||
if (_attributedTextContentView)
|
||||
{
|
||||
// pass it along if contentView already exists
|
||||
_attributedTextContentView.attributedString = string;
|
||||
|
||||
// this causes a relayout and the resulting notification will allow us to set the frame and contentSize
|
||||
}
|
||||
}
|
||||
|
||||
- (NSAttributedString *)attributedString
|
||||
{
|
||||
return _attributedString;
|
||||
}
|
||||
|
||||
- (void)setFrame:(CGRect)frame
|
||||
{
|
||||
CGRect oldFrame = self.frame;
|
||||
|
||||
if (!CGRectEqualToRect(oldFrame, frame))
|
||||
{
|
||||
[super setFrame:frame]; // need to set own frame first because layout completion needs this updated frame
|
||||
|
||||
if (oldFrame.size.width != frame.size.width)
|
||||
{
|
||||
// height does not matter, that will be determined anyhow
|
||||
CGRect contentFrame = CGRectMake(0, 0, frame.size.width - self.contentInset.left - self.contentInset.right, _attributedTextContentView.frame.size.height);
|
||||
|
||||
_attributedTextContentView.frame = contentFrame;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setTextDelegate:(id<DTAttributedTextContentViewDelegate>)aTextDelegate
|
||||
{
|
||||
// store unsafe pointer to delegate because we might not have a contentView yet
|
||||
self->_textDelegate = aTextDelegate;
|
||||
|
||||
// set it if possible, otherwise it will be set in contentView lazy property
|
||||
_attributedTextContentView.delegate = aTextDelegate;
|
||||
}
|
||||
|
||||
- (id<DTAttributedTextContentViewDelegate>)textDelegate
|
||||
{
|
||||
return _attributedTextContentView.delegate ?: self->_textDelegate;
|
||||
}
|
||||
|
||||
- (void)setShouldDrawLinks:(BOOL)shouldDrawLinks
|
||||
{
|
||||
_shouldDrawLinks = shouldDrawLinks;
|
||||
_attributedTextContentView.shouldDrawLinks = _shouldDrawLinks;
|
||||
}
|
||||
|
||||
- (void)setShouldDrawImages:(BOOL)shouldDrawImages
|
||||
{
|
||||
_shouldDrawImages = shouldDrawImages;
|
||||
_attributedTextContentView.shouldDrawImages = _shouldDrawImages;
|
||||
}
|
||||
|
||||
@synthesize attributedTextContentView = _attributedTextContentView;
|
||||
@synthesize attributedString = _attributedString;
|
||||
@synthesize textDelegate = _textDelegate;
|
||||
|
||||
@synthesize shouldDrawLinks = _shouldDrawLinks;
|
||||
|
||||
@end
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// DTHTMLElementBR.h
|
||||
// DTCoreText
|
||||
//
|
||||
// Created by Oliver Drobnik on 26.12.12.
|
||||
// Copyright (c) 2012 Drobnik.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import "DTHTMLElement.h"
|
||||
|
||||
/**
|
||||
Specialized subclass of <DTHTMLElement> that represents a line break.
|
||||
*/
|
||||
|
||||
@interface DTBreakHTMLElement : DTHTMLElement
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,22 @@
|
||||
//
|
||||
// DTHTMLElementBR.m
|
||||
// DTCoreText
|
||||
//
|
||||
// Created by Oliver Drobnik on 26.12.12.
|
||||
// Copyright (c) 2012 Drobnik.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import "DTBreakHTMLElement.h"
|
||||
|
||||
@implementation DTBreakHTMLElement
|
||||
|
||||
- (NSAttributedString *)attributedString
|
||||
{
|
||||
@synchronized(self)
|
||||
{
|
||||
NSDictionary *attributes = [self attributesForAttributedStringRepresentation];
|
||||
return [[NSAttributedString alloc] initWithString:UNICODE_LINE_FEED attributes:attributes];
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,264 @@
|
||||
//
|
||||
// DTCSSListStyle.h
|
||||
// DTCoreText
|
||||
//
|
||||
// Created by Oliver Drobnik on 8/11/11.
|
||||
// Copyright 2011 Drobnik.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import "DTCompatibility.h"
|
||||
|
||||
/**
|
||||
List Styles
|
||||
*/
|
||||
typedef NS_ENUM(NSUInteger, DTCSSListStyleType)
|
||||
{
|
||||
/**
|
||||
The list style should be inherited from the parent
|
||||
*/
|
||||
DTCSSListStyleTypeInherit = 0,
|
||||
|
||||
/**
|
||||
No list style
|
||||
*/
|
||||
DTCSSListStyleTypeNone,
|
||||
|
||||
/**
|
||||
Circle bullet list style
|
||||
*/
|
||||
DTCSSListStyleTypeCircle,
|
||||
|
||||
/**
|
||||
Decimal number list style
|
||||
*/
|
||||
DTCSSListStyleTypeDecimal,
|
||||
|
||||
/**
|
||||
Decimal number list style with a leading zero
|
||||
*/
|
||||
DTCSSListStyleTypeDecimalLeadingZero,
|
||||
|
||||
/**
|
||||
Disc bullet list style
|
||||
*/
|
||||
DTCSSListStyleTypeDisc,
|
||||
|
||||
/**
|
||||
Square bullet list style
|
||||
*/
|
||||
DTCSSListStyleTypeSquare,
|
||||
|
||||
/**
|
||||
Numbered list style with uppercase letters
|
||||
*/
|
||||
DTCSSListStyleTypeUpperAlpha,
|
||||
|
||||
/**
|
||||
Numbered list style with uppercase letters
|
||||
*/
|
||||
DTCSSListStyleTypeUpperLatin,
|
||||
|
||||
/**
|
||||
Numbered list style with uppercase roman numbers
|
||||
*/
|
||||
DTCSSListStyleTypeUpperRoman,
|
||||
|
||||
/**
|
||||
Numbered list style with lowercase letters
|
||||
*/
|
||||
DTCSSListStyleTypeLowerAlpha,
|
||||
|
||||
/**
|
||||
Numbered list style with lowercase letters
|
||||
*/
|
||||
DTCSSListStyleTypeLowerLatin,
|
||||
|
||||
/**
|
||||
Numbered list style with lowercase roman numbers
|
||||
*/
|
||||
DTCSSListStyleTypeLowerRoman,
|
||||
|
||||
/**
|
||||
Plus bullet list style
|
||||
*/
|
||||
DTCSSListStyleTypePlus,
|
||||
|
||||
/**
|
||||
Underscore bullet list style
|
||||
*/
|
||||
DTCSSListStyleTypeUnderscore,
|
||||
|
||||
/**
|
||||
Image bullet list style
|
||||
*/
|
||||
DTCSSListStyleTypeImage,
|
||||
|
||||
/**
|
||||
Value used to represent an invalid list style
|
||||
*/
|
||||
DTCSSListStyleTypeInvalid = NSIntegerMax
|
||||
};
|
||||
|
||||
/**
|
||||
List Marker Positions
|
||||
*/
|
||||
typedef NS_ENUM(NSUInteger, DTCSSListStylePosition)
|
||||
{
|
||||
/**
|
||||
List position should be inherited
|
||||
*/
|
||||
DTCSSListStylePositionInherit = 0,
|
||||
|
||||
/**
|
||||
List prefix position inside
|
||||
*/
|
||||
DTCSSListStylePositionInside,
|
||||
|
||||
/**
|
||||
List prefix position outside
|
||||
*/
|
||||
DTCSSListStylePositionOutside,
|
||||
|
||||
/**
|
||||
Value used to represent an invalid list style position
|
||||
*/
|
||||
DTCSSListStylePositionInvalid = NSIntegerMax
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
This class is the equivalent of `NSTextList` on Mac with the added handling of the marker position.
|
||||
*/
|
||||
@interface DTCSSListStyle : NSObject <NSCoding>
|
||||
|
||||
|
||||
/**
|
||||
@name Getting Types from Strings
|
||||
*/
|
||||
|
||||
/**
|
||||
Convert a string into a list style type.
|
||||
|
||||
@param string The string to convert
|
||||
*/
|
||||
+ (DTCSSListStyleType)listStyleTypeFromString:(NSString *)string;
|
||||
|
||||
|
||||
/**
|
||||
Convert a string into a marker position.
|
||||
|
||||
@param string The string to convert
|
||||
*/
|
||||
+ (DTCSSListStylePosition)listStylePositionFromString:(NSString *)string;
|
||||
|
||||
/**
|
||||
@name Creating List Styles
|
||||
*/
|
||||
|
||||
/**
|
||||
Creates a list style from the passed CSS style dictionary
|
||||
|
||||
@param styles A CSS style dictionary from which the construct a suitable list style
|
||||
*/
|
||||
- (id)initWithStyles:(NSDictionary *)styles;
|
||||
|
||||
/**
|
||||
@name Working with CSS Styles
|
||||
*/
|
||||
|
||||
/**
|
||||
Update the receiver from the CSS styles dictionary passed
|
||||
|
||||
@param styles A dictionary of CSS styles.
|
||||
*/
|
||||
- (void)updateFromStyleDictionary:(NSDictionary *)styles;
|
||||
|
||||
|
||||
/**
|
||||
@name Working with Prefixes
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
Returns the prefix for lists of the receiver's settings.
|
||||
|
||||
@param counter The counter value to use for ordered lists.
|
||||
@returns The prefix string to prepend to list items.
|
||||
*/
|
||||
- (NSString *)prefixWithCounter:(NSInteger)counter;
|
||||
|
||||
|
||||
/**
|
||||
@name Managing Item Numbering
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
Sets the starting item number for the text list.
|
||||
|
||||
The default value is `1`. This value will be used only for ordered lists, and ignored in other cases.
|
||||
|
||||
@param itemNum The item number.
|
||||
*/
|
||||
- (void)setStartingItemNumber:(NSInteger)itemNum;
|
||||
|
||||
|
||||
/**
|
||||
Returns the starting item number for the text list.
|
||||
|
||||
The default value is `1`. This value will be used only for ordered lists, and ignored in other cases.
|
||||
@returns The item number.
|
||||
*/
|
||||
- (NSInteger)startingItemNumber;
|
||||
|
||||
|
||||
/**
|
||||
@name Comparing Lists
|
||||
*/
|
||||
|
||||
/**
|
||||
Determine if another list style has equivalent settings. Note that this does not mean that they are identical, only that they look the same.
|
||||
@param otherListStyle The other list style to compare the receiver with
|
||||
@returns `YES` if the other list style has the same values
|
||||
*/
|
||||
- (BOOL)isEqualToListStyle:(DTCSSListStyle *)otherListStyle;
|
||||
|
||||
|
||||
/**
|
||||
@name Getting Information about Lists
|
||||
*/
|
||||
|
||||
/**
|
||||
Returns if the receiver is an ordered or unordered list
|
||||
|
||||
@returns `YES` if the receiver is ordered, `NO` if it is unordered
|
||||
*/
|
||||
- (BOOL)isOrdered;
|
||||
|
||||
/**
|
||||
If the list style is inherited.
|
||||
|
||||
@warn This is not implemented.
|
||||
*/
|
||||
@property (nonatomic, assign) BOOL inherit;
|
||||
|
||||
|
||||
/**
|
||||
The type of the text list. See DTCSSListStyleType for available types
|
||||
*/
|
||||
@property (nonatomic, assign) DTCSSListStyleType type;
|
||||
|
||||
|
||||
/**
|
||||
The position of the marker in the prefix. See DTCSSListStylePosition for available positions.
|
||||
*/
|
||||
@property (nonatomic, assign) DTCSSListStylePosition position;
|
||||
|
||||
|
||||
/**
|
||||
The image name to use for the marker
|
||||
*/
|
||||
@property (nonatomic, copy) NSString *imageName;
|
||||
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,483 @@
|
||||
//
|
||||
// DTCSSListStyle.m
|
||||
// DTCoreText
|
||||
//
|
||||
// Created by Oliver Drobnik on 8/11/11.
|
||||
// Copyright 2011 Drobnik.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import "DTCSSListStyle.h"
|
||||
|
||||
#import "DTCoreTextConstants.h"
|
||||
|
||||
#import "NSScanner+HTML.h"
|
||||
#import "NSNumber+RomanNumerals.h"
|
||||
|
||||
|
||||
@interface DTCSSListStyle ()
|
||||
|
||||
- (void)updateFromStyleDictionary:(NSDictionary *)styles;
|
||||
|
||||
@property (nonatomic, assign) NSInteger startingItemNumber;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
|
||||
@implementation DTCSSListStyle
|
||||
{
|
||||
BOOL _inherit;
|
||||
|
||||
DTCSSListStyleType _type;
|
||||
DTCSSListStylePosition _position;
|
||||
|
||||
NSString *_imageName;
|
||||
NSInteger _startingItemNumber;
|
||||
}
|
||||
|
||||
- (id)initWithStyles:(NSDictionary *)styles
|
||||
{
|
||||
self = [super init];
|
||||
|
||||
if (self)
|
||||
{
|
||||
// default
|
||||
_position = DTCSSListStylePositionOutside;
|
||||
_startingItemNumber = 1;
|
||||
|
||||
[self updateFromStyleDictionary:styles];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_inherit = [aDecoder decodeBoolForKey:@"inherit"];
|
||||
_type = [aDecoder decodeIntegerForKey:@"type"];
|
||||
_position = [aDecoder decodeIntegerForKey:@"position"];
|
||||
_imageName = [aDecoder decodeObjectForKey:@"imageName"];
|
||||
_startingItemNumber = [aDecoder decodeIntegerForKey:@"startingItemNumber"];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)encodeWithCoder:(NSCoder *)aCoder {
|
||||
[aCoder encodeBool:_inherit forKey:@"inherit"];
|
||||
[aCoder encodeInteger:_type forKey:@"type"];
|
||||
[aCoder encodeInteger:_position forKey:@"position"];
|
||||
[aCoder encodeObject:_imageName forKey:@"imageName"];
|
||||
[aCoder encodeInteger:_startingItemNumber forKey:@"startingItemNumber"];
|
||||
}
|
||||
|
||||
// convert string to listStyleType
|
||||
+ (DTCSSListStyleType)listStyleTypeFromString:(NSString *)string
|
||||
{
|
||||
if (!string)
|
||||
{
|
||||
return DTCSSListStyleTypeInvalid;
|
||||
}
|
||||
|
||||
// always compare lower case
|
||||
string = [string lowercaseString];
|
||||
|
||||
if ([string isEqualToString:@"inherit"])
|
||||
{
|
||||
return DTCSSListStyleTypeInherit;
|
||||
}
|
||||
else if ([string isEqualToString:@"none"])
|
||||
{
|
||||
return DTCSSListStyleTypeNone;
|
||||
}
|
||||
else if ([string isEqualToString:@"circle"])
|
||||
{
|
||||
return DTCSSListStyleTypeCircle;
|
||||
}
|
||||
else if ([string isEqualToString:@"square"])
|
||||
{
|
||||
return DTCSSListStyleTypeSquare;
|
||||
}
|
||||
else if ([string isEqualToString:@"decimal"])
|
||||
{
|
||||
return DTCSSListStyleTypeDecimal;
|
||||
}
|
||||
else if ([string isEqualToString:@"decimal-leading-zero"])
|
||||
{
|
||||
return DTCSSListStyleTypeDecimalLeadingZero;
|
||||
}
|
||||
else if ([string isEqualToString:@"disc"])
|
||||
{
|
||||
return DTCSSListStyleTypeDisc;
|
||||
}
|
||||
else if ([string isEqualToString:@"upper-alpha"]||[string isEqualToString:@"upper-latin"])
|
||||
{
|
||||
return DTCSSListStyleTypeUpperAlpha;
|
||||
}
|
||||
else if ([string isEqualToString:@"lower-alpha"]||[string isEqualToString:@"lower-latin"])
|
||||
{
|
||||
return DTCSSListStyleTypeLowerAlpha;
|
||||
}
|
||||
else if ([string isEqualToString:@"lower-roman"])
|
||||
{
|
||||
return DTCSSListStyleTypeLowerRoman;
|
||||
}
|
||||
else if ([string isEqualToString:@"upper-roman"])
|
||||
{
|
||||
return DTCSSListStyleTypeUpperRoman;
|
||||
}
|
||||
else if ([string isEqualToString:@"plus"])
|
||||
{
|
||||
return DTCSSListStyleTypePlus;
|
||||
}
|
||||
else if ([string isEqualToString:@"underscore"])
|
||||
{
|
||||
return DTCSSListStyleTypeUnderscore;
|
||||
}
|
||||
else
|
||||
{
|
||||
return DTCSSListStyleTypeNone;
|
||||
}
|
||||
}
|
||||
|
||||
+ (DTCSSListStylePosition)listStylePositionFromString:(NSString *)string
|
||||
{
|
||||
if (!string)
|
||||
{
|
||||
return DTCSSListStylePositionInvalid;
|
||||
}
|
||||
|
||||
// always compare lower case
|
||||
string = [string lowercaseString];
|
||||
|
||||
if ([string isEqualToString:@"inherit"])
|
||||
{
|
||||
return DTCSSListStylePositionInherit;
|
||||
}
|
||||
else if ([string isEqualToString:@"inside"])
|
||||
{
|
||||
return DTCSSListStylePositionInside;
|
||||
}
|
||||
else if ([string isEqualToString:@"outside"])
|
||||
{
|
||||
return DTCSSListStylePositionOutside;
|
||||
}
|
||||
else
|
||||
{
|
||||
return DTCSSListStylePositionInherit;
|
||||
}
|
||||
}
|
||||
|
||||
// returns NO if not a valid type
|
||||
- (BOOL)setTypeWithString:(NSString *)string
|
||||
{
|
||||
DTCSSListStyleType type = [DTCSSListStyle listStyleTypeFromString:string];
|
||||
if (type == DTCSSListStyleTypeInvalid)
|
||||
{
|
||||
return NO;
|
||||
}
|
||||
|
||||
_type = type;
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
// returns NO if not a valid type
|
||||
- (BOOL)setPositionWithString:(NSString *)string
|
||||
{
|
||||
DTCSSListStylePosition position = [DTCSSListStyle listStylePositionFromString:string];
|
||||
|
||||
if (position == DTCSSListStylePositionInvalid)
|
||||
{
|
||||
return NO;
|
||||
}
|
||||
|
||||
_position = position;
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)updateFromStyleDictionary:(NSDictionary *)styles
|
||||
{
|
||||
NSString *shortHand = [[styles objectForKey:@"list-style"] lowercaseString];
|
||||
|
||||
if (shortHand)
|
||||
{
|
||||
if ([shortHand isEqualToString:@"inherit"])
|
||||
{
|
||||
_inherit = YES;
|
||||
return;
|
||||
}
|
||||
|
||||
NSArray *components = [shortHand componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
|
||||
|
||||
BOOL typeWasSet = NO;
|
||||
BOOL positionWasSet = NO;
|
||||
|
||||
|
||||
for (NSString *oneComponent in components)
|
||||
{
|
||||
if ([oneComponent hasPrefix:@"url"])
|
||||
{
|
||||
// list-style-image
|
||||
NSString *urlString;
|
||||
NSScanner *scanner = [NSScanner scannerWithString:oneComponent];
|
||||
|
||||
if ([scanner scanCSSURL:&urlString])
|
||||
{
|
||||
self.imageName = urlString;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (!typeWasSet && [self setTypeWithString:oneComponent])
|
||||
{
|
||||
typeWasSet = YES;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!positionWasSet && [self setPositionWithString:oneComponent])
|
||||
{
|
||||
positionWasSet = YES;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// not a short hand, set from individual types
|
||||
|
||||
[self setTypeWithString:[styles objectForKey:@"list-style-type"]];
|
||||
[self setPositionWithString:[styles objectForKey:@"list-style-position"]];
|
||||
|
||||
NSObject *tmpValue = [styles objectForKey:@"list-style-image"];
|
||||
|
||||
if ([tmpValue isKindOfClass:NSString.class])
|
||||
{
|
||||
// extract just the name
|
||||
|
||||
NSString *urlString;
|
||||
NSScanner *scanner = [NSScanner scannerWithString:(NSString *)tmpValue];
|
||||
|
||||
if ([scanner scanCSSURL:&urlString])
|
||||
{
|
||||
self.imageName = urlString;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#ifndef COVERAGE
|
||||
// exclude methods from coverage testing
|
||||
|
||||
- (NSString *)description
|
||||
{
|
||||
return [NSString stringWithFormat:@"<%@ %p type=%d position=%d>", NSStringFromClass([self class]), self, (int)_type, (int)_position];
|
||||
}
|
||||
|
||||
- (NSUInteger)hash
|
||||
{
|
||||
NSUInteger calcHash = 7;
|
||||
|
||||
calcHash = calcHash*31 + [_imageName hash];
|
||||
calcHash = calcHash*31 + (NSUInteger)_type;
|
||||
calcHash = calcHash*31 + (NSUInteger)_position;
|
||||
calcHash = calcHash*31 + (NSUInteger)_startingItemNumber;
|
||||
calcHash = calcHash*31 + (NSUInteger)_inherit;
|
||||
|
||||
return calcHash;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
/*
|
||||
Note: this is not isEqual: because on iOS 7 -[NSMutableAttributedString initWithString:attributes:] calls this via -[NSArray isEqualToArray:]. There isEqual: needs to be returning NO, because otherwise there is some weird internal caching side effect where it reuses previous list arrays
|
||||
*/
|
||||
- (BOOL)isEqualToListStyle:(DTCSSListStyle *)otherListStyle
|
||||
{
|
||||
if (!otherListStyle)
|
||||
{
|
||||
return NO;
|
||||
}
|
||||
|
||||
if (otherListStyle == self)
|
||||
{
|
||||
return YES;
|
||||
}
|
||||
|
||||
if (![otherListStyle isKindOfClass:[DTCSSListStyle class]])
|
||||
{
|
||||
return NO;
|
||||
}
|
||||
|
||||
if (_inherit != otherListStyle->_inherit)
|
||||
{
|
||||
return NO;
|
||||
}
|
||||
|
||||
if (_type != otherListStyle->_type)
|
||||
{
|
||||
return NO;
|
||||
}
|
||||
|
||||
if (_position != otherListStyle->_position)
|
||||
{
|
||||
return NO;
|
||||
}
|
||||
|
||||
if (_startingItemNumber != otherListStyle->_startingItemNumber)
|
||||
{
|
||||
return NO;
|
||||
}
|
||||
|
||||
if (_imageName == otherListStyle->_imageName)
|
||||
{
|
||||
return YES;
|
||||
}
|
||||
|
||||
return ([_imageName isEqualToString:otherListStyle->_imageName]);
|
||||
}
|
||||
|
||||
#pragma mark Copying
|
||||
|
||||
- (id)copyWithZone:(NSZone *)zone
|
||||
{
|
||||
DTCSSListStyle *newStyle = [[DTCSSListStyle allocWithZone:zone] init];
|
||||
newStyle.type = self.type;
|
||||
newStyle.position = self.position;
|
||||
newStyle.imageName = self.imageName;
|
||||
newStyle.startingItemNumber = self.startingItemNumber;
|
||||
|
||||
return newStyle;
|
||||
}
|
||||
|
||||
#pragma mark Utilities
|
||||
|
||||
- (NSString *)prefixWithCounter:(NSInteger)counter
|
||||
{
|
||||
NSString *token = nil;
|
||||
|
||||
DTCSSListStyleType listStyleType = _type;
|
||||
|
||||
if (self.imageName)
|
||||
{
|
||||
listStyleType = DTCSSListStyleTypeImage;
|
||||
}
|
||||
|
||||
|
||||
switch (listStyleType)
|
||||
{
|
||||
case DTCSSListStyleTypeNone:
|
||||
case DTCSSListStyleTypeInherit: // should never be called with inherit
|
||||
case DTCSSListStyleTypeInvalid:
|
||||
{
|
||||
return nil;
|
||||
}
|
||||
case DTCSSListStyleTypeImage:
|
||||
{
|
||||
token = UNICODE_OBJECT_PLACEHOLDER;
|
||||
break;
|
||||
}
|
||||
case DTCSSListStyleTypeCircle:
|
||||
{
|
||||
token = @"\u25e6";
|
||||
break;
|
||||
}
|
||||
case DTCSSListStyleTypeSquare:
|
||||
{
|
||||
token = @"\u25aa";
|
||||
break;
|
||||
}
|
||||
case DTCSSListStyleTypeDecimal:
|
||||
{
|
||||
token = [NSString stringWithFormat:@"%d.", (int)counter];
|
||||
break;
|
||||
}
|
||||
case DTCSSListStyleTypeDecimalLeadingZero:
|
||||
{
|
||||
token = [NSString stringWithFormat:@"%02d.", (int)counter];
|
||||
break;
|
||||
}
|
||||
case DTCSSListStyleTypeDisc:
|
||||
{
|
||||
token = @"\u2022";
|
||||
break;
|
||||
}
|
||||
case DTCSSListStyleTypeUpperAlpha:
|
||||
case DTCSSListStyleTypeUpperLatin:
|
||||
{
|
||||
char letter = 'A' + (char)(counter - 1);
|
||||
token = [NSString stringWithFormat:@"%c.", letter];
|
||||
break;
|
||||
}
|
||||
case DTCSSListStyleTypeLowerAlpha:
|
||||
case DTCSSListStyleTypeLowerLatin:
|
||||
{
|
||||
char letter = 'a' + (char)(counter - 1);
|
||||
token = [NSString stringWithFormat:@"%c.", letter];
|
||||
break;
|
||||
}
|
||||
case DTCSSListStyleTypePlus:
|
||||
{
|
||||
token = @"+";
|
||||
break;
|
||||
}
|
||||
case DTCSSListStyleTypeUnderscore:
|
||||
{
|
||||
token = @"_";
|
||||
break;
|
||||
}
|
||||
case DTCSSListStyleTypeUpperRoman:
|
||||
{
|
||||
token = [NSString stringWithFormat:@"%@.",[@(counter) romanNumeral]];
|
||||
break;
|
||||
}
|
||||
case DTCSSListStyleTypeLowerRoman:
|
||||
{
|
||||
token = [NSString stringWithFormat:@"%@.",[[@(counter) romanNumeral] lowercaseString]];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (_position == DTCSSListStylePositionInside)
|
||||
{
|
||||
// iOS needs second tab, Mac ignores position outside
|
||||
#if TARGET_OS_IPHONE
|
||||
return [NSString stringWithFormat:@"\x09\x09%@", token];
|
||||
#else
|
||||
return [NSString stringWithFormat:@"\x09%@\x09", token];
|
||||
#endif
|
||||
}
|
||||
else
|
||||
{
|
||||
return [NSString stringWithFormat:@"\x09%@\x09", token];
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)isOrdered
|
||||
{
|
||||
switch (_type)
|
||||
{
|
||||
case DTCSSListStyleTypeDecimal:
|
||||
case DTCSSListStyleTypeDecimalLeadingZero:
|
||||
case DTCSSListStyleTypeUpperAlpha:
|
||||
case DTCSSListStyleTypeUpperLatin:
|
||||
case DTCSSListStyleTypeLowerAlpha:
|
||||
case DTCSSListStyleTypeLowerLatin:
|
||||
return YES;
|
||||
|
||||
default:
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark Properties
|
||||
|
||||
@synthesize inherit = _inherit;
|
||||
@synthesize type = _type;
|
||||
@synthesize position = _position;
|
||||
@synthesize imageName = _imageName;
|
||||
@synthesize startingItemNumber = _startingItemNumber;
|
||||
|
||||
@end
|
||||
|
||||
// TO DO: Implement image
|
||||
@@ -0,0 +1,86 @@
|
||||
//
|
||||
// DTCSSStylesheet.h
|
||||
// DTCoreText
|
||||
//
|
||||
// Created by Oliver Drobnik on 9/5/11.
|
||||
// Copyright (c) 2011 Drobnik.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@class DTHTMLElement;
|
||||
|
||||
/**
|
||||
This class represents a CSS style sheet used for specifying formatting for certain CSS selectors.
|
||||
|
||||
It supports matching styles by class, by id or by tag name. Hierarchy matching is not supported yet.
|
||||
*/
|
||||
@interface DTCSSStylesheet : NSObject <NSCopying>
|
||||
|
||||
|
||||
/**
|
||||
@name Creating Stylesheets
|
||||
*/
|
||||
|
||||
/**
|
||||
Creates the default stylesheet.
|
||||
|
||||
This stylesheet is based on the standard styles that Webkit provides for these tags. This stylesheet is loaded from default.css.
|
||||
*/
|
||||
+ (DTCSSStylesheet *)defaultStyleSheet;
|
||||
|
||||
|
||||
/**
|
||||
Creates a stylesheet with a given style block
|
||||
|
||||
@param css The CSS string for the style block
|
||||
*/
|
||||
- (id)initWithStyleBlock:(NSString *)css;
|
||||
|
||||
|
||||
/**
|
||||
@name Working with CSS Style Blocks
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
Parses a style block string and adds the found style rules to the receiver.
|
||||
|
||||
@param css The CSS string for the style block
|
||||
*/
|
||||
- (void)parseStyleBlock:(NSString *)css;
|
||||
|
||||
|
||||
/**
|
||||
Merges styles from given stylesheet into the receiver
|
||||
|
||||
@param stylesheet the stylesheet to merge
|
||||
*/
|
||||
- (void)mergeStylesheet:(DTCSSStylesheet *)stylesheet;
|
||||
|
||||
|
||||
/**
|
||||
@name Accessing Style Information
|
||||
*/
|
||||
|
||||
/**
|
||||
Returns a dictionary that contains the merged style for a given element and the applicable style rules from the receiver.
|
||||
|
||||
@param element The HTML element.
|
||||
@param matchedSelectors The CSS selectors that caused a match
|
||||
@param ignoreInlineStyle If `YES` then the inline styles of the element will be ignored and only the receiver's styles used
|
||||
@returns The merged style dictionary containing only styles which selector matches the element
|
||||
*/
|
||||
- (NSDictionary *)mergedStyleDictionaryForElement:(DTHTMLElement *)element matchedSelectors:(NSSet * __autoreleasing*)matchedSelectors ignoreInlineStyle:(BOOL)ignoreInlineStyle;
|
||||
|
||||
/**
|
||||
Returns a dictionary of the styles of the receiver
|
||||
*/
|
||||
- (NSDictionary *)styles;
|
||||
|
||||
/**
|
||||
Returns an ordered (by declaration) set of the selectors for all of the styles.
|
||||
*/
|
||||
- (NSArray *)orderedSelectors;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,976 @@
|
||||
//
|
||||
// DTCSSStylesheet.m
|
||||
// DTCoreText
|
||||
//
|
||||
// Created by Oliver Drobnik on 9/5/11.
|
||||
// Copyright (c) 2011 Drobnik.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import "DTCSSStylesheet.h"
|
||||
#import "DTCSSListStyle.h"
|
||||
|
||||
#import "DTHTMLElement.h"
|
||||
#import "NSScanner+HTML.h"
|
||||
#import "NSString+CSS.h"
|
||||
#import "NSString+HTML.h"
|
||||
|
||||
|
||||
|
||||
|
||||
@implementation DTCSSStylesheet
|
||||
{
|
||||
NSMutableDictionary *_styles;
|
||||
NSMutableDictionary *_orderedSelectorWeights;
|
||||
NSMutableArray *_orderedSelectors;
|
||||
}
|
||||
|
||||
#pragma mark Creating Stylesheets
|
||||
|
||||
+ (DTCSSStylesheet *)defaultStyleSheet
|
||||
{
|
||||
static DTCSSStylesheet *defaultDTCSSStylesheet = nil;
|
||||
if (defaultDTCSSStylesheet)
|
||||
{
|
||||
return defaultDTCSSStylesheet;
|
||||
}
|
||||
|
||||
@synchronized(self)
|
||||
{
|
||||
if (!defaultDTCSSStylesheet)
|
||||
{
|
||||
#if SWIFT_PACKAGE
|
||||
// get resource bundle via macro
|
||||
NSString *path = [SWIFTPM_MODULE_BUNDLE pathForResource:@"default" ofType:@"css"];
|
||||
#else
|
||||
NSBundle *bundle = [NSBundle bundleForClass:self];
|
||||
NSString *path = [[NSBundle bundleForClass:self] pathForResource:@"default" ofType:@"css"];
|
||||
|
||||
// Cocoapods uses a separate Resources bundle to include default.css
|
||||
if (!path)
|
||||
{
|
||||
NSString *resourcesBundlePath = [bundle pathForResource:@"Resources" ofType:@"bundle"];
|
||||
NSBundle *resourcesBundle = [NSBundle bundleWithPath:resourcesBundlePath];
|
||||
path = [resourcesBundle pathForResource:@"default" ofType:@"css"];
|
||||
}
|
||||
#endif
|
||||
|
||||
NSAssert(path != nil, @"Missing default.css");
|
||||
|
||||
NSString *cssString = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil];
|
||||
|
||||
defaultDTCSSStylesheet = [[DTCSSStylesheet alloc] initWithStyleBlock:cssString];
|
||||
}
|
||||
}
|
||||
return defaultDTCSSStylesheet;
|
||||
}
|
||||
|
||||
- (id)initWithStyleBlock:(NSString *)css
|
||||
{
|
||||
self = [super init];
|
||||
|
||||
if (self)
|
||||
{
|
||||
_styles = [[NSMutableDictionary alloc] init];
|
||||
_orderedSelectorWeights = [[NSMutableDictionary alloc] init];
|
||||
_orderedSelectors = [[NSMutableArray alloc] init];
|
||||
|
||||
[self parseStyleBlock:css];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (id)initWithStylesheet:(DTCSSStylesheet *)stylesheet
|
||||
{
|
||||
self = [super init];
|
||||
|
||||
if (self)
|
||||
{
|
||||
_styles = [[NSMutableDictionary alloc] init];
|
||||
_orderedSelectorWeights = [[NSMutableDictionary alloc] init];
|
||||
_orderedSelectors = [[NSMutableArray alloc] init];
|
||||
|
||||
[self mergeStylesheet:stylesheet];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
#ifndef COVERAGE
|
||||
|
||||
- (NSString *)description
|
||||
{
|
||||
return [_styles description];
|
||||
|
||||
}
|
||||
#endif
|
||||
|
||||
#pragma mark Working with Style Blocks
|
||||
|
||||
- (void)_uncompressShorthands:(NSMutableDictionary *)styles
|
||||
{
|
||||
// list-style shorthand
|
||||
NSString *shortHand = [[styles objectForKey:@"list-style"] lowercaseString];
|
||||
|
||||
if (shortHand && [shortHand isKindOfClass:[NSString class]])
|
||||
{
|
||||
[styles removeObjectForKey:@"list-style"];
|
||||
|
||||
if ([shortHand isEqualToString:@"inherit"])
|
||||
{
|
||||
[styles setObject:@"inherit" forKey:@"list-style-type"];
|
||||
[styles setObject:@"inherit" forKey:@"list-style-position"];
|
||||
return;
|
||||
}
|
||||
|
||||
NSArray *components = [shortHand componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
|
||||
|
||||
BOOL typeWasSet = NO;
|
||||
BOOL positionWasSet = NO;
|
||||
|
||||
DTCSSListStyleType listStyleType = DTCSSListStyleTypeNone;
|
||||
DTCSSListStylePosition listStylePosition = DTCSSListStylePositionInherit;
|
||||
|
||||
for (NSString *oneComponent in components)
|
||||
{
|
||||
if ([oneComponent hasPrefix:@"url"])
|
||||
{
|
||||
// list-style-image
|
||||
NSScanner *scanner = [NSScanner scannerWithString:oneComponent];
|
||||
|
||||
if ([scanner scanCSSURL:NULL])
|
||||
{
|
||||
[styles setObject:oneComponent forKey:@"list-style-image"];
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (!typeWasSet)
|
||||
{
|
||||
// check if valid type
|
||||
listStyleType = [DTCSSListStyle listStyleTypeFromString:oneComponent];
|
||||
|
||||
if (listStyleType != DTCSSListStyleTypeInvalid)
|
||||
{
|
||||
[styles setObject:oneComponent forKey:@"list-style-type"];
|
||||
|
||||
typeWasSet = YES;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (!positionWasSet)
|
||||
{
|
||||
// check if valid position
|
||||
listStylePosition = [DTCSSListStyle listStylePositionFromString:oneComponent];
|
||||
|
||||
if (listStylePosition != DTCSSListStylePositionInvalid)
|
||||
{
|
||||
[styles setObject:oneComponent forKey:@"list-style-position"];
|
||||
|
||||
positionWasSet = YES;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// font shorthand, see http://www.w3.org/TR/CSS21/fonts.html#font-shorthand
|
||||
shortHand = [styles objectForKey:@"font"];
|
||||
|
||||
if (shortHand && [shortHand isKindOfClass:[NSString class]])
|
||||
{
|
||||
NSString *fontStyle = @"normal";
|
||||
NSArray *validFontStyles = [NSArray arrayWithObjects:@"italic", @"oblique", nil];
|
||||
|
||||
NSString *fontVariant = @"normal";
|
||||
NSArray *validFontVariants = [NSArray arrayWithObjects:@"small-caps", nil];
|
||||
BOOL fontVariantSet = NO;
|
||||
|
||||
NSString *fontWeight = @"normal";
|
||||
NSArray *validFontWeights = [NSArray arrayWithObjects:@"bold", @"bolder", @"lighter", @"100", @"200", @"300", @"400", @"500", @"600", @"700", @"800", @"900", nil];
|
||||
BOOL fontWeightSet = NO;
|
||||
|
||||
NSString *fontSize = @"normal";
|
||||
NSArray *validFontSizes = [NSArray arrayWithObjects:@"xx-small", @"x-small", @"small", @"medium", @"large", @"x-large", @"xx-large", @"larger", @"smaller", nil];
|
||||
BOOL fontSizeSet = NO;
|
||||
|
||||
NSArray *suffixesToIgnore = [NSArray arrayWithObjects:@"caption", @"icon", @"menu", @"message-box", @"small-caption", @"status-bar", @"inherit", nil];
|
||||
|
||||
NSString *lineHeight = @"normal";
|
||||
|
||||
NSMutableString *fontFamily = [NSMutableString string];
|
||||
|
||||
NSArray *components = [shortHand componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
|
||||
|
||||
for (NSString *oneComponent in components)
|
||||
{
|
||||
// try font size keywords
|
||||
if ([validFontSizes containsObject:oneComponent])
|
||||
{
|
||||
fontSize = oneComponent;
|
||||
fontSizeSet = YES;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
NSInteger slashIndex = [oneComponent rangeOfString:@"/"].location;
|
||||
|
||||
if (slashIndex != NSNotFound)
|
||||
{
|
||||
// font-size / line-height
|
||||
|
||||
fontSize = [oneComponent substringToIndex:slashIndex];
|
||||
fontSizeSet = YES;
|
||||
|
||||
lineHeight = [oneComponent substringFromIndex:slashIndex+1];
|
||||
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
// length
|
||||
if ([oneComponent hasSuffix:@"%"] || [oneComponent hasSuffix:@"em"] || [oneComponent hasSuffix:@"px"] || [oneComponent hasSuffix:@"pt"])
|
||||
{
|
||||
fontSize = oneComponent;
|
||||
fontSizeSet = YES;
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (fontSizeSet)
|
||||
{
|
||||
if ([suffixesToIgnore containsObject:oneComponent])
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
// assume that this is part of font family
|
||||
if ([fontFamily length])
|
||||
{
|
||||
[fontFamily appendString:@" "];
|
||||
}
|
||||
|
||||
[fontFamily appendString:oneComponent];
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!fontWeightSet && [validFontStyles containsObject:oneComponent])
|
||||
{
|
||||
fontStyle = oneComponent;
|
||||
}
|
||||
else if (!fontVariantSet && [validFontVariants containsObject:oneComponent])
|
||||
{
|
||||
fontVariant = oneComponent;
|
||||
fontVariantSet = YES;
|
||||
}
|
||||
else if (!fontWeightSet && [validFontWeights containsObject:oneComponent])
|
||||
{
|
||||
fontWeight = oneComponent;
|
||||
fontWeightSet = YES;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[styles removeObjectForKey:@"font"];
|
||||
|
||||
// size and family are mandatory, without them this is invalid
|
||||
if ([fontSize length] && [fontFamily length])
|
||||
{
|
||||
[styles setObject:fontStyle forKey:@"font-style"];
|
||||
[styles setObject:fontWeight forKey:@"font-weight"];
|
||||
[styles setObject:fontVariant forKey:@"font-variant"];
|
||||
[styles setObject:fontSize forKey:@"font-size"];
|
||||
[styles setObject:lineHeight forKey:@"line-height"];
|
||||
[styles setObject:fontFamily forKey:@"font-family"];
|
||||
}
|
||||
}
|
||||
|
||||
shortHand = [styles objectForKey:@"margin"];
|
||||
|
||||
if (shortHand && [shortHand isKindOfClass:[NSString class]])
|
||||
{
|
||||
NSArray *parts = [shortHand componentsSeparatedByString:@" "];
|
||||
|
||||
NSString *topMargin;
|
||||
NSString *rightMargin;
|
||||
NSString *bottomMargin;
|
||||
NSString *leftMargin;
|
||||
|
||||
if ([parts count] == 4)
|
||||
{
|
||||
topMargin = [parts objectAtIndex:0];
|
||||
rightMargin = [parts objectAtIndex:1];
|
||||
bottomMargin = [parts objectAtIndex:2];
|
||||
leftMargin = [parts objectAtIndex:3];
|
||||
}
|
||||
else if ([parts count] == 3)
|
||||
{
|
||||
topMargin = [parts objectAtIndex:0];
|
||||
rightMargin = [parts objectAtIndex:1];
|
||||
bottomMargin = [parts objectAtIndex:2];
|
||||
leftMargin = [parts objectAtIndex:1];
|
||||
}
|
||||
else if ([parts count] == 2)
|
||||
{
|
||||
topMargin = [parts objectAtIndex:0];
|
||||
rightMargin = [parts objectAtIndex:1];
|
||||
bottomMargin = [parts objectAtIndex:0];
|
||||
leftMargin = [parts objectAtIndex:1];
|
||||
}
|
||||
else
|
||||
{
|
||||
NSString *onlyValue = [parts objectAtIndex:0];
|
||||
|
||||
topMargin = onlyValue;
|
||||
rightMargin = onlyValue;
|
||||
bottomMargin = onlyValue;
|
||||
leftMargin = onlyValue;
|
||||
}
|
||||
|
||||
// only apply the ones where there is no previous direct setting
|
||||
|
||||
if (![styles objectForKey:@"margin-top"])
|
||||
{
|
||||
[styles setObject:topMargin forKey:@"margin-top"];
|
||||
}
|
||||
|
||||
if (![styles objectForKey:@"margin-right"])
|
||||
{
|
||||
[styles setObject:rightMargin forKey:@"margin-right"];
|
||||
}
|
||||
|
||||
if (![styles objectForKey:@"margin-bottom"])
|
||||
{
|
||||
[styles setObject:bottomMargin forKey:@"margin-bottom"];
|
||||
}
|
||||
|
||||
if (![styles objectForKey:@"margin-left"])
|
||||
{
|
||||
[styles setObject:leftMargin forKey:@"margin-left"];
|
||||
}
|
||||
|
||||
// remove the shorthand
|
||||
[styles removeObjectForKey:@"margin"];
|
||||
}
|
||||
|
||||
shortHand = [styles objectForKey:@"padding"];
|
||||
|
||||
if (shortHand && [shortHand isKindOfClass:[NSString class]])
|
||||
{
|
||||
NSArray *parts = [shortHand componentsSeparatedByString:@" "];
|
||||
|
||||
NSString *topPadding;
|
||||
NSString *rightPadding;
|
||||
NSString *bottomPadding;
|
||||
NSString *leftPadding;
|
||||
|
||||
if ([parts count] == 4)
|
||||
{
|
||||
topPadding = [parts objectAtIndex:0];
|
||||
rightPadding = [parts objectAtIndex:1];
|
||||
bottomPadding = [parts objectAtIndex:2];
|
||||
leftPadding = [parts objectAtIndex:3];
|
||||
}
|
||||
else if ([parts count] == 3)
|
||||
{
|
||||
topPadding = [parts objectAtIndex:0];
|
||||
rightPadding = [parts objectAtIndex:1];
|
||||
bottomPadding = [parts objectAtIndex:2];
|
||||
leftPadding = [parts objectAtIndex:1];
|
||||
}
|
||||
else if ([parts count] == 2)
|
||||
{
|
||||
topPadding = [parts objectAtIndex:0];
|
||||
rightPadding = [parts objectAtIndex:1];
|
||||
bottomPadding = [parts objectAtIndex:0];
|
||||
leftPadding = [parts objectAtIndex:1];
|
||||
}
|
||||
else
|
||||
{
|
||||
NSString *onlyValue = [parts objectAtIndex:0];
|
||||
|
||||
topPadding = onlyValue;
|
||||
rightPadding = onlyValue;
|
||||
bottomPadding = onlyValue;
|
||||
leftPadding = onlyValue;
|
||||
}
|
||||
|
||||
// only apply the ones where there is no previous direct setting
|
||||
|
||||
if (![styles objectForKey:@"padding-top"])
|
||||
{
|
||||
[styles setObject:topPadding forKey:@"padding-top"];
|
||||
}
|
||||
|
||||
if (![styles objectForKey:@"padding-right"])
|
||||
{
|
||||
[styles setObject:rightPadding forKey:@"padding-right"];
|
||||
}
|
||||
|
||||
if (![styles objectForKey:@"padding-bottom"])
|
||||
{
|
||||
[styles setObject:bottomPadding forKey:@"padding-bottom"];
|
||||
}
|
||||
|
||||
if (![styles objectForKey:@"padding-left"])
|
||||
{
|
||||
[styles setObject:leftPadding forKey:@"padding-left"];
|
||||
}
|
||||
|
||||
// remove the shorthand
|
||||
[styles removeObjectForKey:@"padding"];
|
||||
}
|
||||
|
||||
shortHand = [styles objectForKey:@"background"];
|
||||
|
||||
if (shortHand && [shortHand isKindOfClass:[NSString class]])
|
||||
{
|
||||
// ignore most tokens except background-color
|
||||
|
||||
[styles removeObjectForKey:@"background"];
|
||||
|
||||
NSCharacterSet *tokenDelimiters = [NSCharacterSet whitespaceAndNewlineCharacterSet];
|
||||
NSString *trimmedString = [shortHand stringByTrimmingCharactersInSet:tokenDelimiters];
|
||||
NSScanner *scanner = [NSScanner scannerWithString:trimmedString];
|
||||
|
||||
while (![scanner isAtEnd])
|
||||
{
|
||||
NSString *colorName;
|
||||
if ([scanner scanHTMLColor:NULL HTMLName:&colorName])
|
||||
{
|
||||
[styles setObject:colorName forKey:@"background-color"];
|
||||
break;
|
||||
}
|
||||
[scanner scanUpToCharactersFromSet:tokenDelimiters intoString:NULL];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)_addStyleRule:(NSString *)rule withSelector:(NSString*)selectors
|
||||
{
|
||||
NSArray *split = [selectors componentsSeparatedByString:@","];
|
||||
|
||||
for (NSString *selector in split)
|
||||
{
|
||||
NSString *cleanSelector = [selector stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
|
||||
|
||||
NSMutableDictionary *ruleDictionary = [[rule dictionaryOfCSSStyles] mutableCopy];
|
||||
|
||||
// remove !important, we're ignoring these
|
||||
for (NSString *oneKey in [ruleDictionary allKeys])
|
||||
{
|
||||
id value = [ruleDictionary objectForKey:oneKey];
|
||||
if ([value isKindOfClass:[NSString class]])
|
||||
{
|
||||
NSRange rangeOfImportant = [value rangeOfString:@"!important" options:NSCaseInsensitiveSearch];
|
||||
|
||||
if (rangeOfImportant.location != NSNotFound)
|
||||
{
|
||||
value = [value stringByReplacingCharactersInRange:rangeOfImportant withString:@""];
|
||||
value = [value stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
|
||||
|
||||
[ruleDictionary setObject:value forKey:oneKey];
|
||||
}
|
||||
|
||||
} else if ([value isKindOfClass:[NSArray class]])
|
||||
{
|
||||
NSMutableArray *newVal;
|
||||
|
||||
for (NSUInteger i = 0; i < [(NSArray*)value count]; ++i)
|
||||
{
|
||||
NSString *s = [value objectAtIndex:i];
|
||||
|
||||
NSRange rangeOfImportant = [s rangeOfString:@"!important" options:NSCaseInsensitiveSearch];
|
||||
|
||||
if (rangeOfImportant.location != NSNotFound)
|
||||
{
|
||||
s = [s stringByReplacingCharactersInRange:rangeOfImportant withString:@""];
|
||||
s = [s stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
|
||||
|
||||
if (!newVal)
|
||||
{
|
||||
if ([value isKindOfClass:[NSMutableArray class]])
|
||||
{
|
||||
newVal = value;
|
||||
} else
|
||||
{
|
||||
newVal = [value mutableCopy];
|
||||
}
|
||||
}
|
||||
|
||||
// replace the value that had !important with a version without it
|
||||
[newVal replaceObjectAtIndex:i withObject:s];
|
||||
}
|
||||
}
|
||||
|
||||
if (newVal)
|
||||
{
|
||||
[ruleDictionary setObject:newVal forKey:oneKey];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// need to uncompress because otherwise we might get shorthands and non-shorthands together
|
||||
[self _uncompressShorthands:ruleDictionary];
|
||||
|
||||
// check if there is a pseudo selector
|
||||
NSRange colonRange = [cleanSelector rangeOfString:@":"];
|
||||
NSString *pseudoSelector = nil;
|
||||
|
||||
if (colonRange.length==1)
|
||||
{
|
||||
pseudoSelector = [cleanSelector substringFromIndex:colonRange.location+1];
|
||||
cleanSelector = [cleanSelector substringToIndex:colonRange.location];
|
||||
|
||||
// prefix all rules with the pseudo-selector
|
||||
for (NSString *oneRuleKey in [ruleDictionary allKeys])
|
||||
{
|
||||
id value = [ruleDictionary objectForKey:oneRuleKey];
|
||||
|
||||
// prefix key with the pseudo selector
|
||||
NSString *prefixedKey = [NSString stringWithFormat:@"%@:%@", pseudoSelector, oneRuleKey];
|
||||
[ruleDictionary setObject:value forKey:prefixedKey];
|
||||
[ruleDictionary removeObjectForKey:oneRuleKey];
|
||||
}
|
||||
}
|
||||
|
||||
NSDictionary *existingRulesForSelector = [_styles objectForKey:cleanSelector];
|
||||
|
||||
if (existingRulesForSelector)
|
||||
{
|
||||
// substitute new rules over old ones
|
||||
NSMutableDictionary *tmpDict = [existingRulesForSelector mutableCopy];
|
||||
|
||||
// append new rules
|
||||
[tmpDict addEntriesFromDictionary:ruleDictionary];
|
||||
|
||||
// save it
|
||||
[self _addStyles:tmpDict withSelector:cleanSelector];
|
||||
}
|
||||
else
|
||||
{
|
||||
[self _addStyles:ruleDictionary withSelector:cleanSelector];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
- (void)parseStyleBlock:(NSString*)css
|
||||
{
|
||||
NSUInteger braceMarker = 0;
|
||||
|
||||
NSInteger braceLevel = 0;
|
||||
|
||||
NSString* selector;
|
||||
|
||||
NSUInteger length = [css length];
|
||||
|
||||
for (NSUInteger i = 0; i < length; i++)
|
||||
{
|
||||
unichar c = [css characterAtIndex:i];
|
||||
|
||||
if (c == '/')
|
||||
{
|
||||
i++;
|
||||
|
||||
if (i < length)
|
||||
{
|
||||
c = [css characterAtIndex:i];
|
||||
|
||||
if (c == '*')
|
||||
{
|
||||
// skip comment until closing /
|
||||
|
||||
for (; i < length; i++)
|
||||
{
|
||||
if ([css characterAtIndex:i] == '/')
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (i < length)
|
||||
{
|
||||
braceMarker = i+1;
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
// end of string
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// not a comment
|
||||
i--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// An opening brace! It could be the start of a new rule, or it could be a nested brace.
|
||||
if (c == '{')
|
||||
{
|
||||
// If we start a new rule...
|
||||
|
||||
if (braceLevel == 0)
|
||||
{
|
||||
// Grab the selector and clean up extraneous spaces (we'll process it in a moment)
|
||||
selector = [css substringWithRange:NSMakeRange(braceMarker, i-braceMarker)];
|
||||
NSArray *selectorParts = [selector componentsSeparatedByString:@" "];
|
||||
NSMutableArray *cleanSelectorParts = [NSMutableArray array];
|
||||
for (NSString *partialSelector in selectorParts)
|
||||
{
|
||||
if (partialSelector.length)
|
||||
{
|
||||
[cleanSelectorParts addObject:partialSelector];
|
||||
}
|
||||
}
|
||||
selector = [cleanSelectorParts componentsJoinedByString:@" "];
|
||||
|
||||
// And mark our position so we can grab the rule's CSS when it is closed
|
||||
braceMarker = i + 1;
|
||||
}
|
||||
|
||||
// Increase the brace level.
|
||||
braceLevel += 1;
|
||||
}
|
||||
|
||||
// A closing brace!
|
||||
else if (c == '}')
|
||||
{
|
||||
// If we finished a rule...
|
||||
if (braceLevel == 1)
|
||||
{
|
||||
NSString *rule = [css substringWithRange:NSMakeRange(braceMarker, i-braceMarker)];
|
||||
|
||||
[self _addStyleRule:rule withSelector: selector];
|
||||
|
||||
braceMarker = i + 1;
|
||||
}
|
||||
// Skip unpaired closing brace
|
||||
else if (braceLevel < 1) {
|
||||
braceMarker += 1;
|
||||
}
|
||||
|
||||
braceLevel = MAX(braceLevel-1, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
- (void)mergeStylesheet:(DTCSSStylesheet *)stylesheet
|
||||
{
|
||||
NSArray *otherStylesheetStyleKeys = stylesheet.orderedSelectors;
|
||||
|
||||
for (NSString *oneKey in otherStylesheetStyleKeys)
|
||||
{
|
||||
NSDictionary *existingStyles = [_styles objectForKey:oneKey];
|
||||
NSDictionary *stylesToMerge = [[stylesheet styles] objectForKey:oneKey];
|
||||
if (existingStyles)
|
||||
{
|
||||
NSMutableDictionary *mutableStyles = [existingStyles mutableCopy];
|
||||
|
||||
for (NSString *oneStyleKey in stylesToMerge)
|
||||
{
|
||||
NSString *mergingStyleString = [stylesToMerge objectForKey:oneStyleKey];
|
||||
|
||||
[mutableStyles setObject:mergingStyleString forKey:oneStyleKey];
|
||||
}
|
||||
|
||||
[self _addStyles:mutableStyles withSelector:oneKey];
|
||||
}
|
||||
else
|
||||
{
|
||||
// nothing to worry
|
||||
[self _addStyles:stylesToMerge withSelector:oneKey];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)_addStyles:(NSDictionary *)styles withSelector:(NSString *)selector {
|
||||
[_styles setObject:styles forKey:selector];
|
||||
|
||||
if (![_orderedSelectors containsObject:selector])
|
||||
{
|
||||
[_orderedSelectors addObject:selector];
|
||||
[_orderedSelectorWeights setObject:@([self _weightForSelector:selector]) forKey:selector];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark Accessing Style Information
|
||||
|
||||
- (NSDictionary *)mergedStyleDictionaryForElement:(DTHTMLElement *)element matchedSelectors:(NSSet * __autoreleasing*)matchedSelectors ignoreInlineStyle:(BOOL)ignoreInlineStyle
|
||||
{
|
||||
// We are going to combine all the relevant styles for this tag.
|
||||
// (Note that when styles are applied, the later styles take precedence,
|
||||
// so the order in which we grab them matters!)
|
||||
|
||||
NSMutableDictionary *tmpDict = [NSMutableDictionary dictionary];
|
||||
|
||||
// Get based on element
|
||||
NSDictionary *byTagName = [self.styles objectForKey:element.name];
|
||||
|
||||
if (byTagName)
|
||||
{
|
||||
[tmpDict addEntriesFromDictionary:byTagName];
|
||||
}
|
||||
|
||||
// Get based on class(es)
|
||||
NSString *classString = [element.attributes objectForKey:@"class"];
|
||||
NSArray *classes = [classString componentsSeparatedByString:@" "];
|
||||
|
||||
// Cascaded selectors with more than one part are sorted by specificity
|
||||
NSMutableArray *matchingCascadingSelectors = [self matchingComplexCascadingSelectorsForElement:element];
|
||||
[matchingCascadingSelectors sortUsingComparator:^NSComparisonResult(NSString *selector1, NSString *selector2)
|
||||
{
|
||||
NSInteger weightForSelector1 = [[self->_orderedSelectorWeights objectForKey:selector1] integerValue];
|
||||
NSInteger weightForSelector2 = [[self->_orderedSelectorWeights objectForKey:selector2] integerValue];
|
||||
|
||||
if (weightForSelector1 == weightForSelector2)
|
||||
{
|
||||
weightForSelector1 += [self->_orderedSelectors indexOfObject:selector1];
|
||||
weightForSelector2 += [self->_orderedSelectors indexOfObject:selector2];
|
||||
}
|
||||
|
||||
if (weightForSelector1 > weightForSelector2)
|
||||
{
|
||||
return (NSComparisonResult)NSOrderedDescending;
|
||||
}
|
||||
|
||||
if (weightForSelector1 < weightForSelector2)
|
||||
{
|
||||
return (NSComparisonResult)NSOrderedAscending;
|
||||
}
|
||||
|
||||
return (NSComparisonResult)NSOrderedSame;
|
||||
}];
|
||||
|
||||
NSMutableSet *tmpMatchedSelectors;
|
||||
|
||||
if (matchedSelectors)
|
||||
{
|
||||
tmpMatchedSelectors = [NSMutableSet set];
|
||||
}
|
||||
|
||||
// Apply complex cascading selectors first, then apply most specific selectors
|
||||
for (NSString *cascadingSelector in matchingCascadingSelectors)
|
||||
{
|
||||
NSDictionary *byCascadingSelector = [_styles objectForKey:cascadingSelector];
|
||||
[tmpDict addEntriesFromDictionary:byCascadingSelector];
|
||||
[tmpMatchedSelectors addObject:cascadingSelector];
|
||||
}
|
||||
|
||||
// Applied the parameter element's classes last
|
||||
for (NSString *class in classes)
|
||||
{
|
||||
NSString *classRule = [NSString stringWithFormat:@".%@", class];
|
||||
NSDictionary *byClass = [_styles objectForKey: classRule];
|
||||
|
||||
if (byClass)
|
||||
{
|
||||
[tmpDict addEntriesFromDictionary:byClass];
|
||||
[tmpMatchedSelectors addObject:class];
|
||||
}
|
||||
|
||||
NSString *classAndTagRule = [NSString stringWithFormat:@"%@.%@", element.name, class];
|
||||
NSDictionary *byClassAndName = [_styles objectForKey:classAndTagRule];
|
||||
|
||||
if (byClassAndName)
|
||||
{
|
||||
[tmpDict addEntriesFromDictionary:byClassAndName];
|
||||
[tmpMatchedSelectors addObject:classAndTagRule];
|
||||
}
|
||||
}
|
||||
|
||||
// Get based on id
|
||||
NSString *idRule = [NSString stringWithFormat:@"#%@", [element.attributes objectForKey:@"id"]];
|
||||
NSDictionary *byID = [_styles objectForKey:idRule];
|
||||
|
||||
if (byID)
|
||||
{
|
||||
[tmpDict addEntriesFromDictionary:byID];
|
||||
[tmpMatchedSelectors addObject:idRule];
|
||||
}
|
||||
|
||||
if (!ignoreInlineStyle)
|
||||
{
|
||||
// Get tag's local style attribute
|
||||
NSString *styleString = [element.attributes objectForKey:@"style"];
|
||||
|
||||
if ([styleString length])
|
||||
{
|
||||
NSMutableDictionary *localStyles = [[styleString dictionaryOfCSSStyles] mutableCopy];
|
||||
|
||||
// need to uncompress because otherwise we might get shorthands and non-shorthands together
|
||||
[self _uncompressShorthands:localStyles];
|
||||
|
||||
[tmpDict addEntriesFromDictionary:localStyles];
|
||||
}
|
||||
}
|
||||
|
||||
if ([tmpDict count])
|
||||
{
|
||||
if (matchedSelectors && [tmpMatchedSelectors count])
|
||||
{
|
||||
*matchedSelectors = [tmpMatchedSelectors copy];
|
||||
}
|
||||
|
||||
return tmpDict;
|
||||
}
|
||||
else
|
||||
{
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
||||
- (NSDictionary *)styles
|
||||
{
|
||||
return _styles;
|
||||
}
|
||||
|
||||
- (NSArray *)orderedSelectors
|
||||
{
|
||||
return _orderedSelectors;
|
||||
}
|
||||
|
||||
// This looks for cascaded selectors with more than one part to them
|
||||
- (NSMutableArray *)matchingComplexCascadingSelectorsForElement:(DTHTMLElement *)element
|
||||
{
|
||||
__block NSMutableArray *matchedSelectors = [NSMutableArray array];
|
||||
|
||||
for (NSString *selector in _orderedSelectors)
|
||||
{
|
||||
// We only process the selector if our selector has more than 1 part to it (e.g. ".foo" would be skipped and ".foo .bar" would not)
|
||||
if (![selector rangeOfString:@" "].length) {
|
||||
continue;
|
||||
}
|
||||
|
||||
NSArray *selectorParts = [selector componentsSeparatedByString:@" "];
|
||||
|
||||
if (selectorParts.count < 2)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
DTHTMLElement *nextElement = element;
|
||||
|
||||
// Walking up the hierarchy so start at the right side of the selector and work to the left
|
||||
// Aside: Manual for loop here is faster than for in with reverseObjectEnumerator
|
||||
for (NSUInteger j = selectorParts.count; j-- > 0;)
|
||||
{
|
||||
NSString *selectorPart = [selectorParts objectAtIndex:j];
|
||||
BOOL matched = NO;
|
||||
|
||||
if (selectorPart.length)
|
||||
{
|
||||
while (nextElement != nil)
|
||||
{
|
||||
DTHTMLElement *currentElement = nextElement;
|
||||
|
||||
//This must be set to advance here, above all of the breaks, so the loop properly advances.
|
||||
nextElement = currentElement.parentElement;
|
||||
|
||||
if ([selectorPart characterAtIndex:0] == '#')
|
||||
{
|
||||
// If we're at an id and it doesn't match the current element then the style doesn't apply
|
||||
NSString *currentElementId = [currentElement.attributes objectForKey:@"id"];
|
||||
if (currentElementId && [[selectorPart substringFromIndex:1] isEqualToString:currentElementId])
|
||||
{
|
||||
matched = YES;
|
||||
break;
|
||||
}
|
||||
} else if ([selectorPart characterAtIndex:0] == '.')
|
||||
{
|
||||
NSString *currentElementClassesString = [currentElement.attributes objectForKey:@"class"];
|
||||
NSArray *currentElementClasses = [currentElementClassesString componentsSeparatedByString:@" "];
|
||||
for (NSString *currentElementClass in currentElementClasses)
|
||||
{
|
||||
if ([currentElementClass isEqualToString:[selectorPart substringFromIndex:1]])
|
||||
{
|
||||
matched = YES;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (matched)
|
||||
{
|
||||
break;
|
||||
}
|
||||
} else if ([selectorPart isEqualToString:currentElement.name] && (selectorParts.count > 1))
|
||||
{
|
||||
// This condition depends on the "if (selectorParts.count < 2)" conditional above. If that's removed, we must make sure selectorParts
|
||||
// contains > 1 item for this to be matched (we want the element name alone to be matched last).
|
||||
matched = YES;
|
||||
break;
|
||||
}
|
||||
|
||||
// break if the right most portion of the selector doesn't match the target element
|
||||
if (!matched && ([currentElement isEqual:element])) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!matched)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
//Only match if we really are on the last part of the selector and all other parts have matched so far
|
||||
if (j == 0)
|
||||
{
|
||||
if (matched && ![matchedSelectors containsObject:selector])
|
||||
{
|
||||
[matchedSelectors addObject:selector];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return matchedSelectors;
|
||||
}
|
||||
|
||||
// This computes the specificity for a given selector
|
||||
- (NSUInteger)_weightForSelector:(NSString *)selector {
|
||||
if ((selector == nil) || (selector.length == 0))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
NSUInteger weight = 0;
|
||||
|
||||
NSArray *selectorParts = [selector componentsSeparatedByString:@" "];
|
||||
for (NSString *selectorPart in selectorParts)
|
||||
{
|
||||
if (selectorPart.length == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ([selectorPart characterAtIndex:0] == '#')
|
||||
{
|
||||
weight += 100;
|
||||
} else if ([selectorPart characterAtIndex:0] == '.')
|
||||
{
|
||||
weight += 10;
|
||||
} else {
|
||||
weight += 1;
|
||||
}
|
||||
}
|
||||
|
||||
return weight;
|
||||
}
|
||||
|
||||
#pragma mark NSCopying
|
||||
|
||||
- (id)copyWithZone:(NSZone *)zone
|
||||
{
|
||||
DTCSSStylesheet *newStylesheet = [[DTCSSStylesheet allocWithZone:zone] initWithStylesheet:self];
|
||||
|
||||
return newStylesheet;
|
||||
}
|
||||
|
||||
@end
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
//
|
||||
// DTColor+Compatibility.h
|
||||
// DTCoreText
|
||||
//
|
||||
// Created by Oliver Drobnik on 1/9/11.
|
||||
// Copyright 2011 Drobnik.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import "DTCompatibility.h"
|
||||
|
||||
#if TARGET_OS_IPHONE
|
||||
|
||||
#import <UIKit/UIColor.h>
|
||||
|
||||
/**
|
||||
Implementations of methods on NSColor/UIColor which are missing on the other platform.
|
||||
*/
|
||||
@interface UIColor (HTML)
|
||||
|
||||
|
||||
/**
|
||||
A quick method to return the alpha component of this UIColor by using the CGColorGetAlpha method.
|
||||
@returns The floating point alpha value of this UIColor.
|
||||
*/
|
||||
- (CGFloat)alphaComponent;
|
||||
|
||||
@end
|
||||
|
||||
#endif
|
||||
|
||||
#if TARGET_OS_OSX
|
||||
#import <AppKit/NSColor.h>
|
||||
|
||||
/**
|
||||
Methods used to work with HTML representations of colors.
|
||||
*/
|
||||
@interface NSColor (HTML)
|
||||
|
||||
|
||||
/**
|
||||
Return a string hexadecimal representation of this NSColor. Splits the color into components with CGColor methods, re-maps them from percentages in the range 0-255, and returns the RGB color (alpha is stripped) in a six character string.
|
||||
@returns A CSS hexadecimal NSString specifying this NSColor.
|
||||
*/
|
||||
//- (NSString *)htmlHexString;
|
||||
|
||||
#if MAC_OS_X_VERSION_MAX_ALLOWED <= MAC_OS_X_VERSION_10_7
|
||||
/**
|
||||
Converts a CGColorRef into an NSColor by placing each component into an NSColor and pending on the component count to return a grayscale or rgb color. If there are not 2 (grayscale) or 4 (rgba) components the color is from an unsupported color space and nil is returned.
|
||||
@param cgColor The CGColorRef to convert
|
||||
@returns An NSColor of this CGColorRef
|
||||
*/
|
||||
+ (NSColor *)colorWithCGColor:(CGColorRef)cgColor;
|
||||
|
||||
/**
|
||||
Converts an NSColor into a CGColorRef.
|
||||
@returns A CGColorRef of this NSColor
|
||||
*/
|
||||
- (CGColorRef)CGColor DT_RETURNS_INNER_POINTER;
|
||||
#endif
|
||||
|
||||
@end
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,122 @@
|
||||
//
|
||||
// DTColor+Compatibility.m
|
||||
// DTCoreText
|
||||
//
|
||||
// Created by Oliver Drobnik on 1/9/11.
|
||||
// Copyright 2011 Drobnik.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import "DTColor+Compatibility.h"
|
||||
|
||||
#if TARGET_OS_IPHONE
|
||||
|
||||
#import "DTColorFunctions.h"
|
||||
|
||||
@implementation UIColor (HTML)
|
||||
|
||||
- (CGFloat)alphaComponent
|
||||
{
|
||||
return CGColorGetAlpha(self.CGColor);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#else
|
||||
|
||||
#if MAC_OS_X_VERSION_MAX_ALLOWED <= MAC_OS_X_VERSION_10_7 || MAC_OS_X_VERSION_MIN_REQUIRED <= MAC_OS_X_VERSION_10_7
|
||||
#import "DTCoreTextMacros.h"
|
||||
#import <objc/runtime.h>
|
||||
|
||||
static void* DTCoreTextCGColorKey = &DTCoreTextCGColorKey;
|
||||
#endif // MAC_OS_X_VERSION_MAX_ALLOWED <= MAC_OS_X_VERSION_10_7 || MAC_OS_X_VERSION_MIN_REQUIRED <= MAC_OS_X_VERSION_10_7
|
||||
|
||||
|
||||
#if MAC_OS_X_VERSION_MIN_REQUIRED <= MAC_OS_X_VERSION_10_7
|
||||
@interface NSColor (DTCoreText)
|
||||
+ (NSColor *)DTCoreText_colorWithCGColor:(CGColorRef)cgColor;
|
||||
- (CGColorRef)DTCoreText_CGColor DT_RETURNS_INNER_POINTER;
|
||||
@end
|
||||
|
||||
static void DTCoreTextAddMissingSelector(Class aClass, SEL aSelector, SEL implementationSelector)
|
||||
{
|
||||
Method method = class_getInstanceMethod(aClass, aSelector);
|
||||
if (method == NULL) {
|
||||
method = class_getInstanceMethod(aClass, implementationSelector);
|
||||
NSCAssert(method != NULL, @"missing implementation method");
|
||||
|
||||
IMP methodImplementation = method_getImplementation(method);
|
||||
const char *methodTypeEncoding = method_getTypeEncoding(method);
|
||||
|
||||
#if !defined(NS_BLOCK_ASSERTIONS)
|
||||
BOOL rc = class_addMethod(aClass, aSelector, methodImplementation, methodTypeEncoding);
|
||||
NSCAssert(rc, @"failed to add missing method");
|
||||
#else
|
||||
(void) class_addMethod(aClass, aSelector, methodImplementation, methodTypeEncoding);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
__attribute__((constructor))
|
||||
static void DTCoreTextNSColorInitialization(void)
|
||||
{
|
||||
Class NSColorClass = objc_getClass("NSColor");
|
||||
Class NSColorMetaClass = object_getClass(NSColorClass);
|
||||
DTCoreTextAddMissingSelector(NSColorMetaClass, @selector(colorWithCGColor:), @selector(DTCoreText_colorWithCGColor:));
|
||||
DTCoreTextAddMissingSelector(NSColorClass, @selector(CGColor), @selector(DTCoreText_CGColor));
|
||||
}
|
||||
|
||||
#define colorWithCGColor DTCoreText_colorWithCGColor
|
||||
#define CGColor DTCoreText_CGColor
|
||||
#define HTML DTCoreText
|
||||
#endif // MAC_OS_X_VERSION_MIN_REQUIRED <= MAC_OS_X_VERSION_10_7
|
||||
|
||||
@implementation NSColor (HTML)
|
||||
|
||||
#if MAC_OS_X_VERSION_MAX_ALLOWED <= MAC_OS_X_VERSION_10_7 || MAC_OS_X_VERSION_MIN_REQUIRED <= MAC_OS_X_VERSION_10_7
|
||||
+ (NSColor *)colorWithCGColor:(CGColorRef)cgColor
|
||||
{
|
||||
size_t count = CGColorGetNumberOfComponents(cgColor);
|
||||
const CGFloat *components = CGColorGetComponents(cgColor);
|
||||
|
||||
// Grayscale
|
||||
if (count == 2)
|
||||
{
|
||||
return [NSColor colorWithDeviceWhite:components[0] alpha:components[1]];
|
||||
}
|
||||
|
||||
// RGB
|
||||
else if (count == 4)
|
||||
{
|
||||
return [NSColor colorWithDeviceRed:components[0] green:components[1] blue:components[2] alpha:components[3]];
|
||||
}
|
||||
|
||||
// neither grayscale nor rgba
|
||||
return nil;
|
||||
}
|
||||
|
||||
// From https://gist.github.com/1593255
|
||||
- (CGColorRef)CGColor
|
||||
{
|
||||
CGColorRef color = (__bridge CGColorRef)objc_getAssociatedObject(self, DTCoreTextCGColorKey);
|
||||
if (color == NULL)
|
||||
{
|
||||
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
|
||||
|
||||
NSColor *selfCopy = [self colorUsingColorSpaceName:NSDeviceRGBColorSpace];
|
||||
|
||||
CGFloat colorValues[4];
|
||||
[selfCopy getRed:&colorValues[0] green:&colorValues[1] blue:&colorValues[2] alpha:&colorValues[3]];
|
||||
|
||||
color = CGColorCreate(colorSpace, colorValues);
|
||||
CGColorSpaceRelease(colorSpace);
|
||||
|
||||
objc_setAssociatedObject(self, DTCoreTextCGColorKey, CFBridgingRelease(color), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
||||
}
|
||||
|
||||
return color;
|
||||
}
|
||||
#endif // MAC_OS_X_VERSION_MAX_ALLOWED <= MAC_OS_X_VERSION_10_7 || MAC_OS_X_VERSION_MIN_REQUIRED <= MAC_OS_X_VERSION_10_7
|
||||
|
||||
@end
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,34 @@
|
||||
//
|
||||
// DTColorFunctions.h
|
||||
// DTCoreText
|
||||
//
|
||||
// Created by Oliver Drobnik on 9/9/13.
|
||||
// Copyright (c) 2013 Drobnik.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import "DTCompatibility.h"
|
||||
|
||||
/**
|
||||
Takes a CSS color string ('333', 'F9FFF9'), determines the RGB values used, and returns a UIColor object of that color.
|
||||
For each part of the RGB color those numbers for that color are converted to a number using a category on NSString. Then that number is divided by the maximum value, 15 for 3 character strings and 255 for 6 character strings, making the color a percentage and within the range 0.0 and 1.0 that UIColor uses.
|
||||
@param hexString A CSS hexadecimal color string of length 6 or 3.
|
||||
@returns A UIColor object generated from the hexadecimal color string with alpha 1.0.
|
||||
*/
|
||||
DTColor *DTColorCreateWithHexString(NSString *hexString);
|
||||
|
||||
|
||||
/**
|
||||
Takes an English string representing a color and maps it to a numeric RGB value as declared by the HTML and CSS specifications (see http://www.w3schools.com/html/html_colornames.asp). Also accepts CSS `#` hexadecimal colors, `rgba`, and `rgb` and does the right thing returning a corresponding UIColor.
|
||||
If a color begins with a `#` we know that it is a hexadecimal color and send it to colorWithHexString:. If the string is an `rgba()` color declaration the comma delimited r, g, b, and a values are made into percentages and then made into a UIColor which is returned. If the string is an `rgb()` color declaration the same process happens except with an alpha of 1.0.
|
||||
The last case is that the color string is not a numeric declaration `#`, nor a `rgba` or `rgb` declaration so the CSS color value matching the English string is found in a lookup dictionary and then passed to colorWithHexString: which will make a UIColor out of the hexadecimal string.
|
||||
@param name The CSS color string that we want to map from a name into an RGB color.
|
||||
@returns A UIColor object representing the name parameter as numeric values declared by the HTML and CSS specifications, a `rgba()` color, or a `rgb()` color.
|
||||
*/
|
||||
DTColor *DTColorCreateWithHTMLName(NSString *name);
|
||||
|
||||
|
||||
/**
|
||||
Return a string hexadecimal representation of this UIColor. Splits the color into components with CGColor methods, re-maps them from percentages to the range 0-255, and depending on the number of components returns a grayscale (repeating string of two characters) or color RGB (alpha is stripped) six character string. In the event of a non-2 or non-4 component color nil is returned as it is from an unsupported color space.
|
||||
@returns A CSS hexadecimal NSString specifying this UIColor.
|
||||
*/
|
||||
NSString *DTHexStringFromDTColor(DTColor *color);
|
||||
@@ -0,0 +1,305 @@
|
||||
//
|
||||
// DTColorFunctions.m
|
||||
// DTCoreText
|
||||
//
|
||||
// Created by Oliver Drobnik on 9/9/13.
|
||||
// Copyright (c) 2013 Drobnik.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import "DTCompatibility.h"
|
||||
#import "DTColorFunctions.h"
|
||||
|
||||
|
||||
static NSDictionary *colorLookup = nil;
|
||||
|
||||
#pragma mark - Private Functions
|
||||
|
||||
NSUInteger _integerValueFromHexString(NSString *hexString);
|
||||
|
||||
#pragma mark - Implementations
|
||||
|
||||
NSUInteger _integerValueFromHexString(NSString *hexString)
|
||||
{
|
||||
int result = 0;
|
||||
sscanf([hexString UTF8String], "%x", &result);
|
||||
return result;
|
||||
}
|
||||
|
||||
//- (BOOL)isNumeric
|
||||
//{
|
||||
// const char *s = [self UTF8String];
|
||||
//
|
||||
// for (size_t i=0;i<strlen(s);i++)
|
||||
// {
|
||||
// if ((s[i]<'0' || s[i]>'9') && (s[i] != '.'))
|
||||
// {
|
||||
// return NO;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return YES;
|
||||
//}
|
||||
|
||||
|
||||
DTColor *DTColorCreateWithHexString(NSString *hexString)
|
||||
{
|
||||
if ([hexString length]!=6 && [hexString length]!=3)
|
||||
{
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSUInteger digits = [hexString length]/3;
|
||||
CGFloat maxValue = (digits==1)?15.0:255.0;
|
||||
|
||||
NSUInteger redValue = _integerValueFromHexString([hexString substringWithRange:NSMakeRange(0, digits)]);
|
||||
NSUInteger greenValue = _integerValueFromHexString([hexString substringWithRange:NSMakeRange(digits, digits)]);
|
||||
NSUInteger blueValue = _integerValueFromHexString([hexString substringWithRange:NSMakeRange(2*digits, digits)]);
|
||||
|
||||
CGFloat red = redValue/maxValue;
|
||||
CGFloat green = greenValue/maxValue;
|
||||
CGFloat blue = blueValue/maxValue;
|
||||
|
||||
#if TARGET_OS_IPHONE
|
||||
return [DTColor colorWithRed:red green:green blue:blue alpha:1.0];
|
||||
#else
|
||||
return (DTColor *)[NSColor colorWithDeviceRed:red green:green blue:blue alpha:1.0];
|
||||
#endif
|
||||
}
|
||||
|
||||
DTColor *DTColorCreateWithHTMLName(NSString *name)
|
||||
{
|
||||
if ([name hasPrefix:@"#"])
|
||||
{
|
||||
return DTColorCreateWithHexString([name substringFromIndex:1]);
|
||||
}
|
||||
|
||||
if ([name hasPrefix:@"rgba"])
|
||||
{
|
||||
NSString *rgbaName = [name stringByTrimmingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"rgba() "]];
|
||||
NSArray *rgba = [rgbaName componentsSeparatedByString:@","];
|
||||
|
||||
if ([rgba count] != 4)
|
||||
{
|
||||
// Incorrect syntax
|
||||
return nil;
|
||||
}
|
||||
|
||||
CGFloat red = (CGFloat)[[rgba objectAtIndex:0] floatValue] / 255;
|
||||
CGFloat green = [[rgba objectAtIndex:1] floatValue] / 255;
|
||||
CGFloat blue = [[rgba objectAtIndex:2] floatValue] / 255;
|
||||
CGFloat alpha = [[rgba objectAtIndex:3] floatValue];
|
||||
|
||||
#if TARGET_OS_IPHONE
|
||||
return [DTColor colorWithRed:red green:green blue:blue alpha:alpha];
|
||||
#else
|
||||
return (DTColor *)[NSColor colorWithDeviceRed:red green:green blue:blue alpha:alpha];
|
||||
#endif
|
||||
}
|
||||
|
||||
if([name hasPrefix:@"rgb"])
|
||||
{
|
||||
NSString * rgbName = [name stringByTrimmingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"rbg() "]];
|
||||
NSArray* rgb = [rgbName componentsSeparatedByCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@","]];
|
||||
|
||||
if ([rgb count] != 3)
|
||||
{
|
||||
// Incorrect syntax
|
||||
return nil;
|
||||
}
|
||||
|
||||
CGFloat red = [[rgb objectAtIndex:0] floatValue] / 255;
|
||||
CGFloat green = [[rgb objectAtIndex:1] floatValue] / 255;
|
||||
CGFloat blue = [[rgb objectAtIndex:2] floatValue] / 255;
|
||||
CGFloat alpha = 1.0;
|
||||
|
||||
#if TARGET_OS_IPHONE
|
||||
return [DTColor colorWithRed:red green:green blue:blue alpha:alpha];
|
||||
#else
|
||||
return (DTColor *)[NSColor colorWithDeviceRed:red green:green blue:blue alpha:alpha];
|
||||
#endif
|
||||
}
|
||||
|
||||
static dispatch_once_t predicate;
|
||||
dispatch_once(&predicate, ^{
|
||||
colorLookup = [[NSDictionary alloc] initWithObjectsAndKeys:
|
||||
@"F0F8FF", @"aliceblue",
|
||||
@"FAEBD7", @"antiquewhite",
|
||||
@"00FFFF", @"aqua",
|
||||
@"7FFFD4", @"aquamarine",
|
||||
@"F0FFFF", @"azure",
|
||||
@"F5F5DC", @"beige",
|
||||
@"FFE4C4", @"bisque",
|
||||
@"000000", @"black",
|
||||
@"FFEBCD", @"blanchedalmond",
|
||||
@"0000FF", @"blue",
|
||||
@"8A2BE2", @"blueviolet",
|
||||
@"A52A2A", @"brown",
|
||||
@"DEB887", @"burlywood",
|
||||
@"5F9EA0", @"cadetblue",
|
||||
@"7FFF00", @"chartreuse",
|
||||
@"D2691E", @"chocolate",
|
||||
@"FF7F50", @"coral",
|
||||
@"6495ED", @"cornflowerblue",
|
||||
@"FFF8DC", @"cornsilk",
|
||||
@"DC143C", @"crimson",
|
||||
@"00FFFF", @"cyan",
|
||||
@"00008B", @"darkblue",
|
||||
@"008B8B", @"darkcyan",
|
||||
@"B8860B", @"darkgoldenrod",
|
||||
@"A9A9A9", @"darkgray",
|
||||
@"A9A9A9", @"darkgrey",
|
||||
@"006400", @"darkgreen",
|
||||
@"BDB76B", @"darkkhaki",
|
||||
@"8B008B", @"darkmagenta",
|
||||
@"556B2F", @"darkolivegreen",
|
||||
@"FF8C00", @"darkorange",
|
||||
@"9932CC", @"darkorchid",
|
||||
@"8B0000", @"darkred",
|
||||
@"E9967A", @"darksalmon",
|
||||
@"8FBC8F", @"darkseagreen",
|
||||
@"483D8B", @"darkslateblue",
|
||||
@"2F4F4F", @"darkslategray",
|
||||
@"2F4F4F", @"darkslategrey",
|
||||
@"00CED1", @"darkturquoise",
|
||||
@"9400D3", @"darkviolet",
|
||||
@"FF1493", @"deeppink",
|
||||
@"00BFFF", @"deepskyblue",
|
||||
@"696969", @"dimgray",
|
||||
@"696969", @"dimgrey",
|
||||
@"1E90FF", @"dodgerblue",
|
||||
@"B22222", @"firebrick",
|
||||
@"FFFAF0", @"floralwhite",
|
||||
@"228B22", @"forestgreen",
|
||||
@"FF00FF", @"fuchsia",
|
||||
@"DCDCDC", @"gainsboro",
|
||||
@"F8F8FF", @"ghostwhite",
|
||||
@"FFD700", @"gold",
|
||||
@"DAA520", @"goldenrod",
|
||||
@"808080", @"gray",
|
||||
@"808080", @"grey",
|
||||
@"008000", @"green",
|
||||
@"ADFF2F", @"greenyellow",
|
||||
@"F0FFF0", @"honeydew",
|
||||
@"FF69B4", @"hotpink",
|
||||
@"CD5C5C", @"indianred",
|
||||
@"4B0082", @"indigo",
|
||||
@"FFFFF0", @"ivory",
|
||||
@"F0E68C", @"khaki",
|
||||
@"E6E6FA", @"lavender",
|
||||
@"FFF0F5", @"lavenderblush",
|
||||
@"7CFC00", @"lawngreen",
|
||||
@"FFFACD", @"lemonchiffon",
|
||||
@"ADD8E6", @"lightblue",
|
||||
@"F08080", @"lightcoral",
|
||||
@"E0FFFF", @"lightcyan",
|
||||
@"FAFAD2", @"lightgoldenrodyellow",
|
||||
@"D3D3D3", @"lightgray",
|
||||
@"D3D3D3", @"lightgrey",
|
||||
@"90EE90", @"lightgreen",
|
||||
@"FFB6C1", @"lightpink",
|
||||
@"FFA07A", @"lightsalmon",
|
||||
@"20B2AA", @"lightseagreen",
|
||||
@"87CEFA", @"lightskyblue",
|
||||
@"778899", @"lightslategray",
|
||||
@"778899", @"lightslategrey",
|
||||
@"B0C4DE", @"lightsteelblue",
|
||||
@"FFFFE0", @"lightyellow",
|
||||
@"00FF00", @"lime",
|
||||
@"32CD32", @"limegreen",
|
||||
@"FAF0E6", @"linen",
|
||||
@"FF00FF", @"magenta",
|
||||
@"800000", @"maroon",
|
||||
@"66CDAA", @"mediumaquamarine",
|
||||
@"0000CD", @"mediumblue",
|
||||
@"BA55D3", @"mediumorchid",
|
||||
@"9370D8", @"mediumpurple",
|
||||
@"3CB371", @"mediumseagreen",
|
||||
@"7B68EE", @"mediumslateblue",
|
||||
@"00FA9A", @"mediumspringgreen",
|
||||
@"48D1CC", @"mediumturquoise",
|
||||
@"C71585", @"mediumvioletred",
|
||||
@"191970", @"midnightblue",
|
||||
@"F5FFFA", @"mintcream",
|
||||
@"FFE4E1", @"mistyrose",
|
||||
@"FFE4B5", @"moccasin",
|
||||
@"FFDEAD", @"navajowhite",
|
||||
@"000080", @"navy",
|
||||
@"FDF5E6", @"oldlace",
|
||||
@"808000", @"olive",
|
||||
@"6B8E23", @"olivedrab",
|
||||
@"FFA500", @"orange",
|
||||
@"FF4500", @"orangered",
|
||||
@"DA70D6", @"orchid",
|
||||
@"EEE8AA", @"palegoldenrod",
|
||||
@"98FB98", @"palegreen",
|
||||
@"AFEEEE", @"paleturquoise",
|
||||
@"D87093", @"palevioletred",
|
||||
@"FFEFD5", @"papayawhip",
|
||||
@"FFDAB9", @"peachpuff",
|
||||
@"CD853F", @"peru",
|
||||
@"FFC0CB", @"pink",
|
||||
@"DDA0DD", @"plum",
|
||||
@"B0E0E6", @"powderblue",
|
||||
@"800080", @"purple",
|
||||
@"FF0000", @"red",
|
||||
@"BC8F8F", @"rosybrown",
|
||||
@"4169E1", @"royalblue",
|
||||
@"8B4513", @"saddlebrown",
|
||||
@"FA8072", @"salmon",
|
||||
@"F4A460", @"sandybrown",
|
||||
@"2E8B57", @"seagreen",
|
||||
@"FFF5EE", @"seashell",
|
||||
@"A0522D", @"sienna",
|
||||
@"C0C0C0", @"silver",
|
||||
@"87CEEB", @"skyblue",
|
||||
@"6A5ACD", @"slateblue",
|
||||
@"708090", @"slategray",
|
||||
@"708090", @"slategrey",
|
||||
@"FFFAFA", @"snow",
|
||||
@"00FF7F", @"springgreen",
|
||||
@"4682B4", @"steelblue",
|
||||
@"D2B48C", @"tan",
|
||||
@"008080", @"teal",
|
||||
@"D8BFD8", @"thistle",
|
||||
@"FF6347", @"tomato",
|
||||
@"40E0D0", @"turquoise",
|
||||
@"EE82EE", @"violet",
|
||||
@"F5DEB3", @"wheat",
|
||||
@"FFFFFF", @"white",
|
||||
@"F5F5F5", @"whitesmoke",
|
||||
@"FFFF00", @"yellow",
|
||||
@"9ACD32", @"yellowgreen",
|
||||
nil];
|
||||
});
|
||||
|
||||
NSString *hexString = [colorLookup objectForKey:[name lowercaseString]];
|
||||
|
||||
return DTColorCreateWithHexString(hexString);
|
||||
}
|
||||
|
||||
NSString *DTHexStringFromDTColor(DTColor *color)
|
||||
{
|
||||
CGColorRef cgColor = color.CGColor;
|
||||
size_t count = CGColorGetNumberOfComponents(cgColor);
|
||||
const CGFloat *components = CGColorGetComponents(cgColor);
|
||||
|
||||
static NSString *stringFormat = @"%02x%02x%02x";
|
||||
|
||||
// Grayscale
|
||||
if (count == 2)
|
||||
{
|
||||
NSUInteger white = (NSUInteger)(components[0] * (CGFloat)255);
|
||||
return [NSString stringWithFormat:stringFormat, white, white, white];
|
||||
}
|
||||
|
||||
// RGB
|
||||
else if (count == 4)
|
||||
{
|
||||
return [NSString stringWithFormat:stringFormat, (NSUInteger)(components[0] * (CGFloat)255),
|
||||
(NSUInteger)(components[1] * (CGFloat)255), (NSUInteger)(components[2] * (CGFloat)255)];
|
||||
}
|
||||
|
||||
// Unsupported color space
|
||||
return nil;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
//
|
||||
// DTCompatibility.h
|
||||
// DTCoreText
|
||||
//
|
||||
// Created by Oliver Letterer on 09.04.12.
|
||||
// Copyright (c) 2012 Drobnik.com. All rights reserved.
|
||||
//
|
||||
#import <Availability.h>
|
||||
#import <TargetConditionals.h>
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#pragma mark - iOS
|
||||
|
||||
#if TARGET_OS_IPHONE
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
// Compatibility Aliases
|
||||
#define DTColor UIColor
|
||||
#define DTImage UIImage
|
||||
#define DTFont UIFont
|
||||
|
||||
// Edge Insets
|
||||
#define DTEdgeInsets UIEdgeInsets
|
||||
#define DTEdgeInsetsMake(top, left, bottom, right) UIEdgeInsetsMake(top, left, bottom, right)
|
||||
|
||||
// NS-style text attributes are possible with iOS SDK 6.0 or higher
|
||||
#if __IPHONE_OS_VERSION_MAX_ALLOWED > __IPHONE_5_1
|
||||
#define DTCORETEXT_SUPPORT_NS_ATTRIBUTES 1
|
||||
#endif
|
||||
|
||||
// NSParagraphStyle supports tabs as of iOS SDK 7.0 or higher
|
||||
#if __IPHONE_OS_VERSION_MAX_ALLOWED > __IPHONE_6_1
|
||||
#define DTCORETEXT_SUPPORT_NSPARAGRAPHSTYLE_TABS 1
|
||||
#endif
|
||||
|
||||
// iOS before 5.0 has leak in CoreText replacing attributes
|
||||
#if __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_5_0
|
||||
#define DTCORETEXT_NEEDS_ATTRIBUTE_REPLACEMENT_LEAK_FIX 1
|
||||
#endif
|
||||
|
||||
// iOS 7 bug (rdar://14684188) workaround, can be removed once this bug is fixed
|
||||
#if __IPHONE_OS_VERSION_MAX_ALLOWED > __IPHONE_6_1
|
||||
#define DTCORETEXT_FIX_14684188 1
|
||||
#endif
|
||||
|
||||
// use NSURLSession if NSURLConnection is deprecated
|
||||
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_9_0
|
||||
#define DTCORETEXT_USES_NSURLSESSION 1
|
||||
#endif
|
||||
|
||||
// constant for checking for iOS 6
|
||||
#define DTNSFoundationVersionNumber_iOS_6_0 992.00
|
||||
|
||||
// constant for checking for iOS 7
|
||||
#define DTNSFoundationVersionNumber_iOS_7_0 1047.00
|
||||
|
||||
|
||||
// runtime-check if NS-style attributes are allowed
|
||||
static inline BOOL DTCoreTextModernAttributesPossible(void);
|
||||
static inline BOOL DTCoreTextModernAttributesPossible(void)
|
||||
{
|
||||
#if DTCORETEXT_SUPPORT_NS_ATTRIBUTES
|
||||
if (floor(NSFoundationVersionNumber) >= DTNSFoundationVersionNumber_iOS_6_0)
|
||||
{
|
||||
return YES;
|
||||
}
|
||||
#endif
|
||||
return NO;
|
||||
}
|
||||
|
||||
// runtime-check if CoreText draws underlines
|
||||
static inline BOOL DTCoreTextDrawsUnderlinesWithGlyphs(void);
|
||||
static inline BOOL DTCoreTextDrawsUnderlinesWithGlyphs(void)
|
||||
{
|
||||
if (floor(NSFoundationVersionNumber) >= DTNSFoundationVersionNumber_iOS_7_0)
|
||||
{
|
||||
return YES;
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
#if TARGET_CPU_ARM64 || TARGET_CPU_X86_64
|
||||
#define DTNSNumberFromCGFloat(x) [NSNumber numberWithDouble:x]
|
||||
#else
|
||||
#define DTNSNumberFromCGFloat(x) [NSNumber numberWithFloat:x]
|
||||
#endif
|
||||
|
||||
#endif
|
||||
|
||||
|
||||
#pragma mark - Mac
|
||||
|
||||
|
||||
#if !TARGET_OS_IPHONE
|
||||
|
||||
|
||||
#import <AppKit/AppKit.h>
|
||||
|
||||
// Compatibility Aliases
|
||||
#define DTColor NSColor
|
||||
#define DTImage NSImage
|
||||
#define DTFont NSFont
|
||||
|
||||
// Edge Insets
|
||||
#define DTEdgeInsets NSEdgeInsets
|
||||
#define DTEdgeInsetsMake(top, left, bottom, right) NSEdgeInsetsMake(top, left, bottom, right)
|
||||
|
||||
// Mac supports NS-Style Text Attributes since 10.0
|
||||
#define DTCORETEXT_SUPPORT_NS_ATTRIBUTES 1
|
||||
#define DTCORETEXT_SUPPORT_NSPARAGRAPHSTYLE_TABS 1
|
||||
|
||||
// theoretically MacOS before 10.8 might have a leak in CoreText replacing attributes
|
||||
#if __MAC_OS_X_VERSION_MIN_REQUIRED < __MAC_10_7
|
||||
#define DTCORETEXT_NEEDS_ATTRIBUTE_REPLACEMENT_LEAK_FIX 1
|
||||
#endif
|
||||
|
||||
// use NSURLSession if NSURLConnection is deprecated
|
||||
#if __MAC_OS_X_VERSION_MAX_ALLOWED >= __MAC_10_11
|
||||
#define DTCORETEXT_USES_NSURLSESSION 1
|
||||
#endif
|
||||
|
||||
// NSValue has sizeValue on Mac, CGSizeValue on iOS
|
||||
#define CGSizeValue sizeValue
|
||||
|
||||
// String functions named differently on Mac
|
||||
static inline NSString *NSStringFromCGRect(const CGRect rect);
|
||||
static inline NSString *NSStringFromCGRect(const CGRect rect)
|
||||
{
|
||||
return NSStringFromRect(NSRectFromCGRect(rect));
|
||||
}
|
||||
|
||||
static inline NSString *NSStringFromCGSize(const CGSize size);
|
||||
static inline NSString *NSStringFromCGSize(const CGSize size)
|
||||
{
|
||||
return NSStringFromSize(NSSizeFromCGSize(size));
|
||||
}
|
||||
|
||||
static inline NSString *NSStringFromCGPoint(const CGPoint point);
|
||||
static inline NSString *NSStringFromCGPoint(const CGPoint point)
|
||||
{
|
||||
return NSStringFromPoint(NSPointFromCGPoint(point));
|
||||
}
|
||||
|
||||
// runtime-check if NS-style attributes are allowed
|
||||
static inline BOOL DTCoreTextModernAttributesPossible();
|
||||
static inline BOOL DTCoreTextModernAttributesPossible()
|
||||
{
|
||||
return YES;
|
||||
}
|
||||
|
||||
// runtime-check if CoreText draws underlines
|
||||
static inline BOOL DTCoreTextDrawsUnderlinesWithGlyphs();
|
||||
static inline BOOL DTCoreTextDrawsUnderlinesWithGlyphs()
|
||||
{
|
||||
return NO;
|
||||
}
|
||||
|
||||
#define DTNSNumberFromCGFloat(x) [NSNumber numberWithDouble:x]
|
||||
#endif
|
||||
|
||||
// this enables generic ceil, floor, abs, round functions that work for 64 and 32 bit
|
||||
#include <tgmath.h>
|
||||
@@ -0,0 +1,96 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <CoreGraphics/CoreGraphics.h>
|
||||
|
||||
#if TARGET_OS_IPHONE
|
||||
#import <CoreText/CoreText.h>
|
||||
#elif TARGET_OS_MAC
|
||||
#import <ApplicationServices/ApplicationServices.h>
|
||||
#endif
|
||||
|
||||
// global constants
|
||||
#import "DTCoreTextMacros.h"
|
||||
#import "DTCoreTextConstants.h"
|
||||
#import "DTCompatibility.h"
|
||||
|
||||
#import "DTColor+Compatibility.h"
|
||||
#import "DTImage+HTML.h"
|
||||
|
||||
// common utilities
|
||||
#if TARGET_OS_IPHONE
|
||||
#import "DTCoreTextFunctions.h"
|
||||
#endif
|
||||
|
||||
#import "DTColorFunctions.h"
|
||||
|
||||
// common classes
|
||||
#import "DTCSSListStyle.h"
|
||||
#import "DTTextBlock.h"
|
||||
#import "DTCSSStylesheet.h"
|
||||
#import "DTCoreTextFontDescriptor.h"
|
||||
#import "DTCoreTextParagraphStyle.h"
|
||||
#import "DTHTMLAttributedStringBuilder.h"
|
||||
#import "DTHTMLElement.h"
|
||||
#import "DTAnchorHTMLElement.h"
|
||||
#import "DTBreakHTMLElement.h"
|
||||
#import "DTListItemHTMLElement.h"
|
||||
#import "DTHorizontalRuleHTMLElement.h"
|
||||
#import "DTStylesheetHTMLElement.h"
|
||||
#import "DTTextAttachmentHTMLElement.h"
|
||||
#import "DTTextHTMLElement.h"
|
||||
#import "DTHTMLWriter.h"
|
||||
#import "NSCharacterSet+HTML.h"
|
||||
#import "NSCoder+DTCompatibility.h"
|
||||
#import "NSDictionary+DTCoreText.h"
|
||||
#import "NSAttributedString+HTML.h"
|
||||
#import "NSAttributedString+SmallCaps.h"
|
||||
#import "NSAttributedString+DTCoreText.h"
|
||||
#import "NSMutableAttributedString+HTML.h"
|
||||
#import "NSMutableString+HTML.h"
|
||||
#import "NSScanner+HTML.h"
|
||||
#import "NSString+CSS.h"
|
||||
#import "NSString+HTML.h"
|
||||
#import "NSString+Paragraphs.h"
|
||||
#import "NSNumber+RomanNumerals.h"
|
||||
|
||||
// parsing classes
|
||||
#import "DTHTMLParserNode.h"
|
||||
#import "DTHTMLParserTextNode.h"
|
||||
|
||||
// text attachment cluster
|
||||
#import "DTTextAttachment.h"
|
||||
#import "DTDictationPlaceholderTextAttachment.h"
|
||||
#import "DTIframeTextAttachment.h"
|
||||
#import "DTImageTextAttachment.h"
|
||||
#import "DTObjectTextAttachment.h"
|
||||
#import "DTVideoTextAttachment.h"
|
||||
|
||||
#import "NSAttributedStringRunDelegates.h"
|
||||
|
||||
#import "DTCoreTextGlyphRun.h"
|
||||
#import "DTCoreTextLayoutFrame.h"
|
||||
#import "DTCoreTextLayoutFrame+Cursor.h"
|
||||
#import "DTCoreTextLayoutLine.h"
|
||||
#import "DTCoreTextLayouter.h"
|
||||
|
||||
// TARGET_OS_IPHONE is both tvOS and iOS
|
||||
#if TARGET_OS_IPHONE
|
||||
|
||||
#import "DTLazyImageView.h"
|
||||
#import "DTLinkButton.h"
|
||||
|
||||
#import "DTAttributedLabel.h"
|
||||
#import "DTAttributedTextCell.h"
|
||||
#import "DTAttributedTextContentView.h"
|
||||
#import "DTAttributedTextView.h"
|
||||
#import "DTCoreTextFontCollection.h"
|
||||
|
||||
#import "DTDictationPlaceholderView.h"
|
||||
|
||||
#import "UIFont+DTCoreText.h"
|
||||
|
||||
#import "DTAccessibilityElement.h"
|
||||
#import "DTAccessibilityViewProxy.h"
|
||||
#import "DTCoreTextLayoutFrameAccessibilityElementGenerator.h"
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
#import "DTCompatibility.h"
|
||||
|
||||
// unicode characters
|
||||
|
||||
#define UNICODE_OBJECT_PLACEHOLDER @"\ufffc"
|
||||
#define UNICODE_LINE_FEED @"\u2028"
|
||||
|
||||
// unicode spaces used in CharacterSet.ignorableWhitespaceCharacterSet
|
||||
|
||||
#define UNICODE_NON_BREAKING_SPACE @"\u00a0"
|
||||
#define UNICODE_OGHAM_SPACE_MARK @"\u1680"
|
||||
#define UNICODE_MONGOLIAN_VOWEL_SEPARATOR @"\u180e"
|
||||
#define UNICODE_EN_QUAD @"\u2000"
|
||||
#define UNICODE_EM_QUAD @"\u2001"
|
||||
#define UNICODE_EN_SPACE @"\u2002"
|
||||
#define UNICODE_EM_SPACE @"\u2003"
|
||||
#define UNICODE_THREE_PER_EM_SPACE @"\u2004"
|
||||
#define UNICODE_FOUR_PER_EM_SPACE @"\u2005"
|
||||
#define UNICODE_SIX_PER_EM_SPACE @"\u2006"
|
||||
#define UNICODE_FIGURE_SPACE @"\u2007"
|
||||
#define UNICODE_PUNCTUATION_SPACE @"\u2008"
|
||||
#define UNICODE_THIN_SPACE @"\u2009"
|
||||
#define UNICODE_HAIR_SPACE @"\u200a"
|
||||
#define UNICODE_ZERO_WIDTH_SPACE @"\u200b"
|
||||
#define UNICODE_NARROW_NO_BREAK_SPACE @"\u202f"
|
||||
#define UNICODE_MEDIUM_MATHEMATICAL_SPACE @"\u205f"
|
||||
#define UNICODE_IDEOGRAPHIC_SPACE @"\u3000"
|
||||
#define UNICODE_ZERO_WIDTH_NO_BREAK_SPACE @"\ufeff"
|
||||
|
||||
// standard options
|
||||
|
||||
#if TARGET_OS_IPHONE
|
||||
extern NSString * const NSBaseURLDocumentOption;
|
||||
extern NSString * const NSTextEncodingNameDocumentOption;
|
||||
extern NSString * const NSTextSizeMultiplierDocumentOption;
|
||||
|
||||
#if __IPHONE_OS_VERSION_MAX_ALLOWED < __IPHONE_7_0
|
||||
extern NSString * const NSAttachmentAttributeName;
|
||||
#endif
|
||||
|
||||
#endif
|
||||
|
||||
// custom options
|
||||
|
||||
extern NSString * const DTMaxImageSize;
|
||||
extern NSString * const DTDefaultFontFamily;
|
||||
extern NSString * const DTDefaultFontName;
|
||||
extern NSString * const DTDefaultFontSize;
|
||||
extern NSString * const DTDefaultFontDescriptor;
|
||||
extern NSString * const DTDefaultTextColor;
|
||||
extern NSString * const DTDefaultLinkColor;
|
||||
extern NSString * const DTDefaultLinkDecoration;
|
||||
extern NSString * const DTDefaultLinkHighlightColor;
|
||||
extern NSString * const DTDefaultTextAlignment;
|
||||
extern NSString * const DTDefaultLineHeightMultiplier;
|
||||
extern NSString * const DTDefaultLineHeightMultiplier;
|
||||
extern NSString * const DTDefaultFirstLineHeadIndent;
|
||||
extern NSString * const DTDefaultHeadIndent;
|
||||
extern NSString * const DTDefaultStyleSheet;
|
||||
extern NSString * const DTUseiOS6Attributes;
|
||||
extern NSString * const DTWillFlushBlockCallBack;
|
||||
extern NSString * const DTProcessCustomHTMLAttributes;
|
||||
extern NSString * const DTIgnoreInlineStylesOption;
|
||||
extern NSString * const DTDocumentPreserveTrailingSpaces;
|
||||
|
||||
|
||||
// attributed string attribute constants
|
||||
|
||||
extern NSString * const DTTextListsAttribute;
|
||||
extern NSString * const DTAttachmentParagraphSpacingAttribute;
|
||||
extern NSString * const DTLinkAttribute;
|
||||
extern NSString * const DTLinkHighlightColorAttribute;
|
||||
extern NSString * const DTAnchorAttribute;
|
||||
extern NSString * const DTGUIDAttribute;
|
||||
extern NSString * const DTHeaderLevelAttribute;
|
||||
extern NSString * const DTStrikeOutAttribute;
|
||||
extern NSString * const DTBackgroundColorAttribute;
|
||||
extern NSString * const DTShadowsAttribute;
|
||||
extern NSString * const DTHorizontalRuleStyleAttribute;
|
||||
extern NSString * const DTTextBlocksAttribute;
|
||||
extern NSString * const DTFieldAttribute;
|
||||
extern NSString * const DTCustomAttributesAttribute;
|
||||
extern NSString * const DTAscentMultiplierAttribute;
|
||||
extern NSString * const DTBackgroundStrokeColorAttribute;
|
||||
extern NSString * const DTBackgroundStrokeWidthAttribute;
|
||||
extern NSString * const DTBackgroundCornerRadiusAttribute;
|
||||
extern NSString * const DTArchivingAttribute;
|
||||
|
||||
// field constants
|
||||
|
||||
extern NSString * const DTListPrefixField;
|
||||
|
||||
// iOS 6 compatibility
|
||||
extern BOOL ___useiOS6Attributes;
|
||||
|
||||
// exceptions
|
||||
extern NSString * const DTCoreTextFontDescriptorException;
|
||||
|
||||
// macros
|
||||
|
||||
#define IS_WHITESPACE(_c) (_c == ' ' || _c == '\t' || _c == 0xA || _c == 0xB || _c == 0xC || _c == 0xD || _c == 0x85)
|
||||
|
||||
// types
|
||||
|
||||
/**
|
||||
DTHTMLElement display style
|
||||
*/
|
||||
typedef NS_ENUM(NSUInteger, DTHTMLElementDisplayStyle)
|
||||
{
|
||||
/**
|
||||
The element is inline text
|
||||
*/
|
||||
DTHTMLElementDisplayStyleInline = 0, // default
|
||||
|
||||
/**
|
||||
The element is not displayed
|
||||
*/
|
||||
DTHTMLElementDisplayStyleNone,
|
||||
|
||||
/**
|
||||
The element is a block
|
||||
*/
|
||||
DTHTMLElementDisplayStyleBlock,
|
||||
|
||||
/**
|
||||
The element is an item in a list
|
||||
*/
|
||||
DTHTMLElementDisplayStyleListItem,
|
||||
|
||||
/**
|
||||
The element is a table
|
||||
*/
|
||||
DTHTMLElementDisplayStyleTable,
|
||||
};
|
||||
|
||||
/**
|
||||
DTHTMLElement floating style
|
||||
*/
|
||||
typedef NS_ENUM(NSUInteger, DTHTMLElementFloatStyle)
|
||||
{
|
||||
/**
|
||||
The element does not float
|
||||
*/
|
||||
DTHTMLElementFloatStyleNone = 0,
|
||||
|
||||
|
||||
/**
|
||||
The element should float left-aligned
|
||||
*/
|
||||
DTHTMLElementFloatStyleLeft,
|
||||
|
||||
|
||||
/**
|
||||
The element should float right-aligned
|
||||
*/
|
||||
DTHTMLElementFloatStyleRight
|
||||
};
|
||||
|
||||
/**
|
||||
DTHTMLElement font variants
|
||||
*/
|
||||
typedef NS_ENUM(NSUInteger, DTHTMLElementFontVariant)
|
||||
{
|
||||
/**
|
||||
The element inherits the font variant
|
||||
*/
|
||||
DTHTMLElementFontVariantInherit = 0,
|
||||
|
||||
/**
|
||||
The element uses the normal font variant
|
||||
*/
|
||||
DTHTMLElementFontVariantNormal,
|
||||
|
||||
/**
|
||||
The element should display in small caps
|
||||
*/
|
||||
DTHTMLElementFontVariantSmallCaps
|
||||
};
|
||||
|
||||
/**
|
||||
The algorithm that DTCoreTextLayoutFrame uses for positioning lines
|
||||
*/
|
||||
typedef NS_ENUM(NSUInteger, DTCoreTextLayoutFrameLinePositioningOptions)
|
||||
{
|
||||
/**
|
||||
The line positioning algorithm is similar to how Safari positions lines
|
||||
*/
|
||||
DTCoreTextLayoutFrameLinePositioningOptionAlgorithmWebKit = 1,
|
||||
|
||||
/**
|
||||
The line positioning algorithm is how it was before the implementation of DTCoreTextLayoutFrameLinePositioningOptionAlgorithmWebKit
|
||||
*/
|
||||
DTCoreTextLayoutFrameLinePositioningOptionAlgorithmLegacy = 2
|
||||
};
|
||||
|
||||
// layouting
|
||||
|
||||
// the value to use if the width is unknown
|
||||
#define CGFLOAT_WIDTH_UNKNOWN 16777215.0f
|
||||
|
||||
// the value to use if the height is unknown
|
||||
#define CGFLOAT_HEIGHT_UNKNOWN 16777215.0f
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
#import "DTCoreTextConstants.h"
|
||||
|
||||
// standard options
|
||||
|
||||
#if TARGET_OS_IPHONE
|
||||
NSString * const NSBaseURLDocumentOption = @"NSBaseURLDocumentOption";
|
||||
NSString * const NSTextEncodingNameDocumentOption = @"NSTextEncodingNameDocumentOption";
|
||||
NSString * const NSTextSizeMultiplierDocumentOption = @"NSTextSizeMultiplierDocumentOption";
|
||||
|
||||
#if __IPHONE_OS_VERSION_MAX_ALLOWED < __IPHONE_7_0
|
||||
NSString * const NSAttachmentAttributeName = @"NSAttachmentAttributeName";
|
||||
#endif
|
||||
|
||||
#endif
|
||||
|
||||
// custom options
|
||||
|
||||
NSString * const DTMaxImageSize = @"DTMaxImageSize";
|
||||
NSString * const DTDefaultFontFamily = @"DTDefaultFontFamily";
|
||||
NSString * const DTDefaultFontName = @"DTDefaultFontName";
|
||||
NSString * const DTDefaultFontSize = @"DTDefaultFontSize";
|
||||
NSString * const DTDefaultFontDescriptor = @"DTDefaultFontDescriptor";
|
||||
NSString * const DTDefaultTextColor = @"DTDefaultTextColor";
|
||||
NSString * const DTDefaultLinkColor = @"DTDefaultLinkColor";
|
||||
NSString * const DTDefaultLinkHighlightColor = @"DTDefaultLinkHighlightColor";
|
||||
NSString * const DTDefaultLinkDecoration = @"DTDefaultLinkDecoration";
|
||||
NSString * const DTDefaultTextAlignment = @"DTDefaultTextAlignment";
|
||||
NSString * const DTDefaultLineHeightMultiplier = @"DTDefaultLineHeightMultiplier";
|
||||
NSString * const DTDefaultFirstLineHeadIndent = @"DTDefaultFirstLineHeadIndent";
|
||||
NSString * const DTDefaultHeadIndent = @"DTDefaultHeadIndent";
|
||||
NSString * const DTDefaultStyleSheet = @"DTDefaultStyleSheet";
|
||||
NSString * const DTUseiOS6Attributes = @"DTUseiOS6Attributes";
|
||||
NSString * const DTWillFlushBlockCallBack = @"DTWillFlushBlockCallBack";
|
||||
NSString * const DTProcessCustomHTMLAttributes = @"DTProcessCustomHTMLAttributes";
|
||||
NSString * const DTIgnoreInlineStylesOption = @"DTIgnoreInlineStyles";
|
||||
NSString * const DTDocumentPreserveTrailingSpaces = @"DTDocumentPreserveTrailingSpaces";
|
||||
|
||||
// attributed string attribute constants
|
||||
|
||||
NSString * const DTTextListsAttribute = @"DTTextLists";
|
||||
NSString * const DTAttachmentParagraphSpacingAttribute = @"DTAttachmentParagraphSpacing";
|
||||
NSString * const DTLinkAttribute = @"NSLink";
|
||||
NSString * const DTLinkHighlightColorAttribute = @"DTLinkHighlightColor";
|
||||
NSString * const DTAnchorAttribute = @"DTAnchor";
|
||||
NSString * const DTGUIDAttribute = @"DTGUID";
|
||||
NSString * const DTHeaderLevelAttribute = @"DTHeaderLevel";
|
||||
NSString * const DTStrikeOutAttribute = @"DTStrikethrough";
|
||||
NSString * const DTBackgroundColorAttribute = @"DTBackgroundColor";
|
||||
NSString * const DTShadowsAttribute = @"DTShadows";
|
||||
NSString * const DTHorizontalRuleStyleAttribute = @"DTHorizontalRuleStyle";
|
||||
NSString * const DTTextBlocksAttribute = @"DTTextBlocks";
|
||||
NSString * const DTFieldAttribute = @"DTField";
|
||||
NSString * const DTCustomAttributesAttribute = @"DTCustomAttributes";
|
||||
NSString * const DTAscentMultiplierAttribute = @"DTAscentMultiplierAttribute";
|
||||
NSString * const DTBackgroundStrokeColorAttribute = @"DTBackgroundStrokeColor";
|
||||
NSString * const DTBackgroundStrokeWidthAttribute = @"DTBackgroundStrokeWidth";
|
||||
NSString * const DTBackgroundCornerRadiusAttribute = @"DTBackgroundCornerRadius";
|
||||
NSString * const DTArchivingAttribute = @"DTArchivingAttribute";
|
||||
|
||||
// field constants
|
||||
NSString * const DTListPrefixField = @"{listprefix}";
|
||||
|
||||
// iOS 6 compatibility
|
||||
|
||||
BOOL ___useiOS6Attributes = NO; // this gets set globally by DTHTMLAttributedStringBuilder
|
||||
|
||||
|
||||
// exceptions
|
||||
|
||||
NSString * const DTCoreTextFontDescriptorException = @"DTCoreTextFontDescriptorException";
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
//
|
||||
// DTCoreTextFontCollection.h
|
||||
// DTCoreText
|
||||
//
|
||||
// Created by Oliver Drobnik on 5/23/11.
|
||||
// Copyright 2011 Drobnik.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
|
||||
@class DTCoreTextFontDescriptor;
|
||||
|
||||
/**
|
||||
Class representing a collection of fonts
|
||||
*/
|
||||
|
||||
@interface DTCoreTextFontCollection : NSObject
|
||||
|
||||
/**
|
||||
@name Creating Font Collections
|
||||
*/
|
||||
|
||||
/**
|
||||
Creates a font collection with all available fonts on the system
|
||||
*/
|
||||
+ (DTCoreTextFontCollection *)availableFontsCollection;
|
||||
|
||||
/**
|
||||
@name Getting Information about Font Collections
|
||||
*/
|
||||
|
||||
/**
|
||||
The font family names that occur in the receiver's list of fonts
|
||||
*/
|
||||
- (NSArray *)fontFamilyNames;
|
||||
|
||||
/**
|
||||
The font descriptors describing all fonts in the receiver's font collection
|
||||
*/
|
||||
- (NSArray *)fontDescriptors;
|
||||
|
||||
/**
|
||||
@name Searching for Fonts
|
||||
*/
|
||||
|
||||
/**
|
||||
The font descriptor describing a font in the receiver's collection that matches a given descriptor
|
||||
@param descriptor The font descriptor to search for
|
||||
@returns The first found font descriptor in the font collection
|
||||
*/
|
||||
- (DTCoreTextFontDescriptor *)matchingFontDescriptorForFontDescriptor:(DTCoreTextFontDescriptor *)descriptor;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,162 @@
|
||||
//
|
||||
// DTCoreTextFontCollection.m
|
||||
// DTCoreText
|
||||
//
|
||||
// Created by Oliver Drobnik on 5/23/11.
|
||||
// Copyright 2011 Drobnik.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import "DTCompatibility.h"
|
||||
#import "DTCoreTextFontCollection.h"
|
||||
#import "DTCoreTextFontDescriptor.h"
|
||||
|
||||
#if TARGET_OS_IPHONE
|
||||
#import <CoreText/CoreText.h>
|
||||
#elif TARGET_OS_MAC
|
||||
#import <ApplicationServices/ApplicationServices.h>
|
||||
#endif
|
||||
|
||||
@interface DTCoreTextFontCollection ()
|
||||
|
||||
@property (nonatomic, strong) NSArray *fontDescriptors;
|
||||
@property (nonatomic, strong) NSCache *fontMatchCache;
|
||||
|
||||
- (id)initWithAvailableFonts;
|
||||
|
||||
@end
|
||||
|
||||
static DTCoreTextFontCollection *_availableFontsCollection = nil;
|
||||
|
||||
|
||||
@implementation DTCoreTextFontCollection
|
||||
{
|
||||
NSArray *_fontDescriptors;
|
||||
NSCache *_fontMatchCache;
|
||||
}
|
||||
|
||||
+ (DTCoreTextFontCollection *)availableFontsCollection
|
||||
{
|
||||
static dispatch_once_t predicate;
|
||||
|
||||
dispatch_once(&predicate, ^{
|
||||
_availableFontsCollection = [[DTCoreTextFontCollection alloc] initWithAvailableFonts];
|
||||
});
|
||||
|
||||
return _availableFontsCollection;
|
||||
}
|
||||
|
||||
- (id)initWithAvailableFonts
|
||||
{
|
||||
self = [super init];
|
||||
|
||||
if (self)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
|
||||
- (DTCoreTextFontDescriptor *)matchingFontDescriptorForFontDescriptor:(DTCoreTextFontDescriptor *)descriptor
|
||||
{
|
||||
DTCoreTextFontDescriptor *firstMatch = nil;
|
||||
NSString *cacheKey = [NSString stringWithFormat:@"fontFamily BEGINSWITH[cd] %@ and boldTrait == %d and italicTrait == %d", descriptor.fontFamily, descriptor.boldTrait, descriptor.italicTrait];
|
||||
|
||||
// try cache
|
||||
firstMatch = [self.fontMatchCache objectForKey:cacheKey];
|
||||
|
||||
if (firstMatch)
|
||||
{
|
||||
DTCoreTextFontDescriptor *retMatch = [firstMatch copy];
|
||||
retMatch.pointSize = descriptor.pointSize;
|
||||
return retMatch;
|
||||
}
|
||||
|
||||
// need to search
|
||||
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"fontFamily BEGINSWITH[cd] %@ and boldTrait == %d and italicTrait == %d", descriptor.fontFamily, descriptor.boldTrait, descriptor.italicTrait];
|
||||
|
||||
NSArray *matchingDescriptors = [self.fontDescriptors filteredArrayUsingPredicate:predicate];
|
||||
|
||||
//NSLog(@"%@", matchingDescriptors);
|
||||
|
||||
if ([matchingDescriptors count])
|
||||
{
|
||||
firstMatch = [matchingDescriptors objectAtIndex:0];
|
||||
[self.fontMatchCache setObject:firstMatch forKey:cacheKey];
|
||||
|
||||
DTCoreTextFontDescriptor *retMatch = [firstMatch copy];
|
||||
|
||||
retMatch.pointSize = descriptor.pointSize;
|
||||
return retMatch;
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
#pragma mark Properties
|
||||
|
||||
- (NSArray *)fontDescriptors
|
||||
{
|
||||
if (!_fontDescriptors)
|
||||
{
|
||||
CTFontCollectionRef fonts = CTFontCollectionCreateFromAvailableFonts(NULL);
|
||||
|
||||
CFArrayRef matchingFonts = CTFontCollectionCreateMatchingFontDescriptors(fonts);
|
||||
|
||||
if (matchingFonts)
|
||||
{
|
||||
// convert all to our objects
|
||||
NSMutableArray *tmpArray = [[NSMutableArray alloc] init];
|
||||
|
||||
for (NSInteger i=0; i<CFArrayGetCount(matchingFonts); i++)
|
||||
{
|
||||
CTFontDescriptorRef fontDesc = CFArrayGetValueAtIndex(matchingFonts, i);
|
||||
|
||||
|
||||
DTCoreTextFontDescriptor *desc = [[DTCoreTextFontDescriptor alloc] initWithCTFontDescriptor:fontDesc];
|
||||
[tmpArray addObject:desc];
|
||||
}
|
||||
|
||||
CFRelease(matchingFonts);
|
||||
|
||||
self.fontDescriptors = tmpArray;
|
||||
}
|
||||
|
||||
CFRelease(fonts);
|
||||
}
|
||||
|
||||
return _fontDescriptors;
|
||||
}
|
||||
|
||||
- (NSCache *)fontMatchCache
|
||||
{
|
||||
if (!_fontMatchCache)
|
||||
{
|
||||
_fontMatchCache = [[NSCache alloc] init];
|
||||
}
|
||||
|
||||
return _fontMatchCache;
|
||||
}
|
||||
|
||||
- (NSArray *)fontFamilyNames
|
||||
{
|
||||
NSMutableArray *tmpArray = [NSMutableArray array];
|
||||
|
||||
for (DTCoreTextFontDescriptor *oneDescriptor in [self fontDescriptors])
|
||||
{
|
||||
NSString *familyName = oneDescriptor.fontFamily;
|
||||
|
||||
if (![tmpArray containsObject:familyName])
|
||||
{
|
||||
[tmpArray addObject:familyName];
|
||||
}
|
||||
}
|
||||
|
||||
return [tmpArray sortedArrayUsingSelector:@selector(compare:)];
|
||||
}
|
||||
|
||||
@synthesize fontDescriptors = _fontDescriptors;
|
||||
@synthesize fontMatchCache = _fontMatchCache;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,228 @@
|
||||
//
|
||||
// DTCoreTextFontDescriptor.h
|
||||
// DTCoreText
|
||||
//
|
||||
// Created by Oliver Drobnik on 1/26/11.
|
||||
// Copyright 2011 Drobnik.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <CoreText/CoreText.h>
|
||||
|
||||
|
||||
/**
|
||||
This class describes the attributes of a font. It is used to represent fonts throughout the parsing and when needed is able to generated matching `CTFont` instances.
|
||||
*/
|
||||
@interface DTCoreTextFontDescriptor : NSObject <NSCopying, NSCoding>
|
||||
|
||||
/**
|
||||
@name Creating Font Descriptors
|
||||
*/
|
||||
|
||||
/**
|
||||
Convenience method to create a font descriptor from a font attributes dictionary
|
||||
@param attributes The dictionary of font attributes
|
||||
@returns An initialized font descriptor
|
||||
*/
|
||||
+ (DTCoreTextFontDescriptor *)fontDescriptorWithFontAttributes:(NSDictionary *)attributes;
|
||||
|
||||
/**
|
||||
Convenience method for creates a font descriptor from a Core Text font
|
||||
@param ctFont The Core Text font
|
||||
@returns An initialized font descriptor
|
||||
*/
|
||||
+ (DTCoreTextFontDescriptor *)fontDescriptorForCTFont:(CTFontRef)ctFont;
|
||||
|
||||
/**
|
||||
Creates a font descriptor from a font attributes dictionary
|
||||
@param attributes The dictionary of font attributes
|
||||
@returns An initialized font descriptor
|
||||
*/
|
||||
- (id)initWithFontAttributes:(NSDictionary *)attributes;
|
||||
|
||||
/**
|
||||
Creates a font descriptor from a Core Text font descriptor
|
||||
@param ctFontDescriptor The Core Text font descriptor
|
||||
@returns An initialized font descriptor
|
||||
*/
|
||||
- (id)initWithCTFontDescriptor:(CTFontDescriptorRef)ctFontDescriptor;
|
||||
|
||||
/**
|
||||
Creates a font descriptor from a Core Text font
|
||||
@param ctFont The Core Text font
|
||||
@returns An initialized font descriptor
|
||||
*/
|
||||
- (id)initWithCTFont:(CTFontRef)ctFont;
|
||||
|
||||
|
||||
/**
|
||||
@name Creating Fonts from Font Descriptors
|
||||
*/
|
||||
|
||||
/**
|
||||
Creates a `CTFont` matching the receiver's attribute
|
||||
@returns a +1 owning reference of a Core Text font
|
||||
*/
|
||||
- (CTFontRef)newMatchingFont;
|
||||
|
||||
/**
|
||||
@name Specifying Font Attributes
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
Sets the font attributes from a dictionary
|
||||
@param newAttributes The font attributes dictionary
|
||||
*/
|
||||
- (void)setFontAttributes:(NSDictionary *)newAttributes;
|
||||
|
||||
/**
|
||||
Retrieves a dictionary of font attributes
|
||||
*/
|
||||
- (NSDictionary *)fontAttributes;
|
||||
|
||||
|
||||
/**
|
||||
The font family name of the described font
|
||||
*/
|
||||
@property (nonatomic, copy) NSString *fontFamily;
|
||||
|
||||
/**
|
||||
The font name of the described font
|
||||
*/
|
||||
@property (nonatomic, copy) NSString *fontName;
|
||||
|
||||
/**
|
||||
The point size of the described font
|
||||
*/
|
||||
@property (nonatomic) CGFloat pointSize;
|
||||
|
||||
/**
|
||||
Whether the described font has the bold trait
|
||||
*/
|
||||
@property (nonatomic) BOOL boldTrait;
|
||||
|
||||
/**
|
||||
Whether the described font has the italic trait
|
||||
*/
|
||||
@property (nonatomic) BOOL italicTrait;
|
||||
|
||||
/**
|
||||
Whether the described font has the expanded trait
|
||||
*/
|
||||
@property (nonatomic) BOOL expandedTrait;
|
||||
|
||||
/**
|
||||
Whether the described font has the condensed trait
|
||||
*/
|
||||
@property (nonatomic) BOOL condensedTrait;
|
||||
|
||||
/**
|
||||
Whether the described font has the monospace trait
|
||||
*/
|
||||
@property (nonatomic) BOOL monospaceTrait;
|
||||
|
||||
/**
|
||||
Whether the described font has the vertical trait
|
||||
*/
|
||||
@property (nonatomic) BOOL verticalTrait;
|
||||
|
||||
/**
|
||||
Whether the described font is optimized for use in User Interfaces
|
||||
*/
|
||||
@property (nonatomic) BOOL UIoptimizedTrait;
|
||||
|
||||
/**
|
||||
The symbolic traits of the receiver
|
||||
*/
|
||||
@property (nonatomic) CTFontSymbolicTraits symbolicTraits;
|
||||
|
||||
/**
|
||||
The stylistic class of the receiver
|
||||
*/
|
||||
@property (nonatomic) CTFontStylisticClass stylisticClass;
|
||||
|
||||
/**
|
||||
`YES` if the small caps style is enabled, `NO` if not
|
||||
*/
|
||||
@property (nonatomic) BOOL smallCapsFeature;
|
||||
|
||||
/**
|
||||
Determining if the font described by the receiver has native small caps support
|
||||
@returns `YES` if this font supports native small caps
|
||||
*/
|
||||
- (BOOL)supportsNativeSmallCaps;
|
||||
|
||||
/**
|
||||
Working with CSS
|
||||
*/
|
||||
|
||||
/**
|
||||
The CSS style sheet representation of the receiver
|
||||
@returns A CSS style string
|
||||
*/
|
||||
- (NSString *)cssStyleRepresentation;
|
||||
|
||||
|
||||
/**
|
||||
@name Global Font Overriding
|
||||
*/
|
||||
|
||||
/**
|
||||
A call to the method is ideally placed into your app delegate. This loads all available system fonts into a look up table to allow DTCoreText to quickly find a specific combination of font-family and italic and bold attributes. Please refer to the [Programming Guide](../docs/Programming%20Guide.html) for information when you should be using this.
|
||||
|
||||
Calling this does not replace entries already existing in the lookup table, for example loaded from the `DTCoreTextFontOverrides.plist` included in the app bundle.
|
||||
*/
|
||||
+ (void)asyncPreloadFontLookupTable;
|
||||
|
||||
/**
|
||||
Sets the font family to use if the font family in a font descriptor is invalid.
|
||||
|
||||
The fallback font family cannot be `nil` and must be a valid font family. The default is **Times New Roman**.
|
||||
@param fontFamily The font family
|
||||
*/
|
||||
+ (void)setFallbackFontFamily:(NSString *)fontFamily;
|
||||
|
||||
/**
|
||||
Returns the font family to use if the font family in a font descriptor is invalid. The default is **Times New Roman**.
|
||||
@returns The font family
|
||||
*/
|
||||
+ (NSString *)fallbackFontFamily;
|
||||
|
||||
/**
|
||||
Sets the global font name override to use when encountering a font family with given bold and italic attributes.
|
||||
@param fontName The font name to use
|
||||
@param fontFamily The font family to use this for
|
||||
@param bold The bold trait
|
||||
@param italic The italic trait
|
||||
*/
|
||||
+ (void)setOverrideFontName:(NSString *)fontName forFontFamily:(NSString *)fontFamily bold:(BOOL)bold italic:(BOOL)italic;
|
||||
|
||||
/**
|
||||
Retrieves the global font name override for a given font family with bold and italic traits.
|
||||
@param fontFamily The font family to retrieve the override for
|
||||
@param bold The bold trait
|
||||
@param italic The italic trait
|
||||
@returns The font name to use for this combination of parameters
|
||||
*/
|
||||
+ (NSString *)overrideFontNameforFontFamily:(NSString *)fontFamily bold:(BOOL)bold italic:(BOOL)italic;
|
||||
|
||||
/**
|
||||
Sets the global font name override to use when encountering small caps text in a font family with given bold and italic attributes.
|
||||
@param fontName The font name to use
|
||||
@param fontFamily The font family to use this for
|
||||
@param bold The bold trait
|
||||
@param italic The italic trait
|
||||
*/
|
||||
+ (void)setSmallCapsFontName:(NSString *)fontName forFontFamily:(NSString *)fontFamily bold:(BOOL)bold italic:(BOOL)italic;
|
||||
|
||||
/**
|
||||
Retrieves the global font name override to use for small caps text for a given font family with bold and italic traits.
|
||||
@param fontFamily The font family to retrieve the override for
|
||||
@param bold The bold trait
|
||||
@param italic The italic trait
|
||||
@returns The font name to use for this combination of parameters
|
||||
*/
|
||||
+ (NSString *)smallCapsFontNameforFontFamily:(NSString *)fontFamily bold:(BOOL)bold italic:(BOOL)italic;
|
||||
|
||||
@end
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,69 @@
|
||||
//
|
||||
// DTCoreTextFunctions.h
|
||||
// DTCoreText
|
||||
//
|
||||
// Created by Oliver Drobnik on 21.12.12.
|
||||
// Copyright (c) 2012 Drobnik.com. All rights reserved.
|
||||
//
|
||||
#import <CoreText/CoreText.h>
|
||||
|
||||
#import "DTCompatibility.h"
|
||||
|
||||
#if TARGET_OS_OSX
|
||||
#import <AppKit/AppKit.h>
|
||||
#endif
|
||||
|
||||
#import "DTCompatibility.h"
|
||||
|
||||
#if TARGET_OS_IPHONE
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
/**
|
||||
Creates a CTFont from a UIFont
|
||||
@param font The `UIFont`
|
||||
@returns The matching CTFont
|
||||
*/
|
||||
CTFontRef DTCTFontCreateWithUIFont(UIFont *font);
|
||||
#endif
|
||||
|
||||
/**
|
||||
Converts an NSLineBreakMode into CoreText line truncation type
|
||||
*/
|
||||
CTLineTruncationType DTCTLineTruncationTypeFromNSLineBreakMode(NSLineBreakMode lineBreakMode);
|
||||
|
||||
/**
|
||||
Rounds the passed value according to the specified content scale.
|
||||
|
||||
With contentScale 1 the results are identical to roundf, with Retina content scale 2 the results are multiples of 0.5.
|
||||
*/
|
||||
CGFloat DTRoundWithContentScale(CGFloat value, CGFloat contentScale);
|
||||
|
||||
/**
|
||||
Rounds up the passed value according to the specified content scale.
|
||||
|
||||
With contentScale 1 the results are identical to roundf, with Retina content scale 2 the results are multiples of 0.5.
|
||||
*/
|
||||
CGFloat DTCeilWithContentScale(CGFloat value, CGFloat contentScale);
|
||||
|
||||
/**
|
||||
Rounds down the passed value according to the sspecifiedcontent scale.
|
||||
|
||||
With contentScale 1 the results are identical to roundf, with Retina content scale 2 the results are multiples of 0.5.
|
||||
*/
|
||||
CGFloat DTFloorWithContentScale(CGFloat value, CGFloat contentScale);
|
||||
|
||||
#pragma mark - Alignment Conversion
|
||||
|
||||
#if DTCORETEXT_SUPPORT_NS_ATTRIBUTES
|
||||
/**
|
||||
Converts from NSTextAlignment to CTTextAligment
|
||||
*/
|
||||
CTTextAlignment DTNSTextAlignmentToCTTextAlignment(NSTextAlignment nsTextAlignment);
|
||||
|
||||
/**
|
||||
Converts from CTTextAlignment to NSTextAligment
|
||||
*/
|
||||
NSTextAlignment DTNSTextAlignmentFromCTTextAlignment(CTTextAlignment ctTextAlignment);
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
//
|
||||
// DTCoreTextFunctions.m
|
||||
// DTCoreText
|
||||
//
|
||||
// Created by Oliver Drobnik on 21.12.12.
|
||||
// Copyright (c) 2012 Drobnik.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import "DTCoreTextFunctions.h"
|
||||
|
||||
#import <DTFoundation/DTLog.h>
|
||||
|
||||
#if TARGET_OS_IPHONE
|
||||
CTFontRef DTCTFontCreateWithUIFont(UIFont *font)
|
||||
{
|
||||
return CTFontCreateWithName((__bridge CFStringRef)font.fontName, font.pointSize, NULL);
|
||||
}
|
||||
#endif
|
||||
|
||||
CTLineTruncationType DTCTLineTruncationTypeFromNSLineBreakMode(NSLineBreakMode lineBreakMode)
|
||||
{
|
||||
#if TARGET_OS_IPHONE && __IPHONE_OS_VERSION_MIN_REQUIRED < 60000
|
||||
switch (lineBreakMode)
|
||||
{
|
||||
case UILineBreakModeHeadTruncation:
|
||||
return kCTLineTruncationStart;
|
||||
|
||||
case UILineBreakModeMiddleTruncation:
|
||||
return kCTLineTruncationMiddle;
|
||||
|
||||
default:
|
||||
return kCTLineTruncationEnd;
|
||||
}
|
||||
#else
|
||||
switch (lineBreakMode)
|
||||
{
|
||||
case NSLineBreakByTruncatingHead:
|
||||
return kCTLineTruncationStart;
|
||||
|
||||
case NSLineBreakByTruncatingMiddle:
|
||||
return kCTLineTruncationMiddle;
|
||||
|
||||
default:
|
||||
return kCTLineTruncationEnd;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
CGFloat DTRoundWithContentScale(CGFloat value, CGFloat contentScale)
|
||||
{
|
||||
return round(value*contentScale)/contentScale;
|
||||
}
|
||||
|
||||
CGFloat DTCeilWithContentScale(CGFloat value, CGFloat contentScale)
|
||||
{
|
||||
return ceil(value*contentScale)/contentScale;
|
||||
}
|
||||
|
||||
CGFloat DTFloorWithContentScale(CGFloat value, CGFloat contentScale)
|
||||
{
|
||||
return floor(value*contentScale)/contentScale;
|
||||
}
|
||||
|
||||
#pragma mark - Alignment Functions
|
||||
|
||||
#if DTCORETEXT_SUPPORT_NS_ATTRIBUTES
|
||||
|
||||
CTTextAlignment DTNSTextAlignmentToCTTextAlignment(NSTextAlignment nsTextAlignment)
|
||||
{
|
||||
switch (nsTextAlignment)
|
||||
{
|
||||
#if TARGET_OS_IPHONE
|
||||
case NSTextAlignmentLeft:
|
||||
{
|
||||
return kCTTextAlignmentLeft;
|
||||
}
|
||||
|
||||
case NSTextAlignmentRight:
|
||||
{
|
||||
return kCTTextAlignmentRight;
|
||||
}
|
||||
|
||||
case NSTextAlignmentCenter:
|
||||
{
|
||||
return kCTTextAlignmentCenter;
|
||||
}
|
||||
|
||||
case NSTextAlignmentJustified:
|
||||
{
|
||||
return kCTTextAlignmentJustified;
|
||||
}
|
||||
|
||||
case NSTextAlignmentNatural:
|
||||
{
|
||||
return kCTTextAlignmentNatural;
|
||||
}
|
||||
#else
|
||||
case NSLeftTextAlignment:
|
||||
{
|
||||
return kCTTextAlignmentLeft;
|
||||
}
|
||||
|
||||
case NSRightTextAlignment:
|
||||
{
|
||||
return kCTTextAlignmentRight;
|
||||
}
|
||||
|
||||
case NSCenterTextAlignment:
|
||||
{
|
||||
return kCTTextAlignmentCenter;
|
||||
}
|
||||
|
||||
case NSJustifiedTextAlignment:
|
||||
{
|
||||
return kCTTextAlignmentJustified;
|
||||
}
|
||||
|
||||
case NSNaturalTextAlignment:
|
||||
{
|
||||
return kCTTextAlignmentNatural;
|
||||
}
|
||||
#endif
|
||||
|
||||
default:
|
||||
{
|
||||
DTLogError(@"Unknown alignment %d", (int)nsTextAlignment);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NSTextAlignment DTNSTextAlignmentFromCTTextAlignment(CTTextAlignment ctTextAlignment)
|
||||
{
|
||||
#if TARGET_OS_IPHONE
|
||||
switch (ctTextAlignment)
|
||||
{
|
||||
case kCTTextAlignmentLeft:
|
||||
{
|
||||
return NSTextAlignmentLeft;
|
||||
}
|
||||
|
||||
case kCTTextAlignmentRight:
|
||||
{
|
||||
return NSTextAlignmentRight;
|
||||
}
|
||||
|
||||
case kCTTextAlignmentCenter:
|
||||
{
|
||||
return NSTextAlignmentCenter;
|
||||
}
|
||||
|
||||
case kCTTextAlignmentJustified:
|
||||
{
|
||||
return NSTextAlignmentJustified;
|
||||
}
|
||||
|
||||
case kCTTextAlignmentNatural:
|
||||
{
|
||||
return NSTextAlignmentNatural;
|
||||
}
|
||||
}
|
||||
#else
|
||||
switch (ctTextAlignment)
|
||||
{
|
||||
case kCTTextAlignmentLeft:
|
||||
{
|
||||
return NSLeftTextAlignment;
|
||||
}
|
||||
|
||||
case kCTTextAlignmentRight:
|
||||
{
|
||||
return NSRightTextAlignment;
|
||||
}
|
||||
|
||||
case kCTTextAlignmentCenter:
|
||||
{
|
||||
return NSCenterTextAlignment;
|
||||
}
|
||||
|
||||
case kCTTextAlignmentJustified:
|
||||
{
|
||||
return NSJustifiedTextAlignment;
|
||||
}
|
||||
|
||||
case kCTTextAlignmentNatural:
|
||||
{
|
||||
return NSNaturalTextAlignment;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
//
|
||||
// DTCoreTextGlyphRun.h
|
||||
// DTCoreText
|
||||
//
|
||||
// Created by Oliver Drobnik on 1/25/11.
|
||||
// Copyright 2011 Drobnik.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import "DTCompatibility.h"
|
||||
|
||||
#if TARGET_OS_IPHONE
|
||||
#import <CoreText/CoreText.h>
|
||||
#elif TARGET_OS_MAC
|
||||
#import <ApplicationServices/ApplicationServices.h>
|
||||
#endif
|
||||
|
||||
@class DTCoreTextLayoutLine;
|
||||
@class DTTextAttachment;
|
||||
|
||||
|
||||
/**
|
||||
This class is an Objective-C wrapper around `CTRun` and represents a glyph run. That is, a number of characters from the original `NSAttributedString` that share the same characteristics and attributes.
|
||||
*/
|
||||
|
||||
@interface DTCoreTextGlyphRun : NSObject
|
||||
{
|
||||
NSRange _stringRange;
|
||||
}
|
||||
|
||||
/**
|
||||
@name Creating Glyph Runs
|
||||
*/
|
||||
|
||||
/**
|
||||
Creates a new glyph run from a `CTRun`, belonging to a given layout line and with a given offset from the left line origin.
|
||||
@param run The Core Text glyph run to wrap
|
||||
@param layoutLine The layout line that this glyph run belongs to
|
||||
@param offset The offset from the left line origin to place the glyph run at
|
||||
@returns An initialized DTCoreTextGlyphRun
|
||||
*/
|
||||
- (id)initWithRun:(CTRunRef)run layoutLine:(DTCoreTextLayoutLine *)layoutLine offset:(CGFloat)offset;
|
||||
|
||||
/**
|
||||
@name Drawing
|
||||
*/
|
||||
|
||||
/**
|
||||
Draws the receiver into the given context with the position that it derives from the layout line it belongs to.
|
||||
@see drawDecorationInContext: for drawing the receiver's decoration
|
||||
@param context The graphics context to draw into
|
||||
*/
|
||||
- (void)drawInContext:(CGContextRef)context;
|
||||
|
||||
/**
|
||||
Draws the receiver's decoration into the given context with the position that it derives from the layout line it belongs to. Decoration is background highlighting, underline and strike-through.
|
||||
@param context The graphics context to draw into
|
||||
*/
|
||||
- (void)drawDecorationInContext:(CGContextRef)context;
|
||||
|
||||
/**
|
||||
Creates a `CGPath` containing the shapes of all glyphs in the receiver
|
||||
*/
|
||||
- (CGPathRef)newPathWithGlyphs;
|
||||
|
||||
/**
|
||||
@name Getting Information
|
||||
*/
|
||||
|
||||
/**
|
||||
Determines the frame of a specific glyph
|
||||
@param index The index of the glyph
|
||||
@return The frame of the glyph
|
||||
*/
|
||||
- (CGRect)frameOfGlyphAtIndex:(NSInteger)index;
|
||||
|
||||
/**
|
||||
The bounds of an image encompassing the entire run.
|
||||
@param context The graphics context used for the measurement
|
||||
@returns The rectangle containing the result
|
||||
*/
|
||||
- (CGRect)imageBoundsInContext:(CGContextRef)context;
|
||||
|
||||
/**
|
||||
The string range (of the attributed string) that is represented by the receiver
|
||||
@returns The range
|
||||
*/
|
||||
- (NSRange)stringRange;
|
||||
|
||||
/**
|
||||
The string indices of the receiver
|
||||
@returns An array of string indices
|
||||
*/
|
||||
- (NSArray *)stringIndices;
|
||||
|
||||
/**
|
||||
The frame rectangle of the glyph run, relative to the layout frame coordinate system
|
||||
*/
|
||||
@property (nonatomic, readonly) CGRect frame;
|
||||
|
||||
/**
|
||||
The number of glyphs that the receiver is made up of
|
||||
*/
|
||||
@property (nonatomic, readonly) NSInteger numberOfGlyphs;
|
||||
|
||||
/**
|
||||
The Core Text attributes that are shared by all glyphs of the receiver
|
||||
*/
|
||||
@property (nonatomic, readonly) NSDictionary *attributes;
|
||||
|
||||
/**
|
||||
Returns `YES` if the receiver is part of a hyperlink, `NO` otherwise
|
||||
*/
|
||||
@property (nonatomic, assign, readonly, getter=isHyperlink) BOOL hyperlink;
|
||||
|
||||
/**
|
||||
Returns `YES` if the receiver represents trailing whitespace in a line.
|
||||
|
||||
This can be used to avoid drawing of background color, strikeout or underline for empty trailing white space glyph runs.
|
||||
*/
|
||||
- (BOOL)isTrailingWhitespace;
|
||||
|
||||
/**
|
||||
The ascent (height above the baseline) of the receiver
|
||||
*/
|
||||
@property (nonatomic, readonly) CGFloat ascent;
|
||||
|
||||
/**
|
||||
The descent (height below the baseline) of the receiver
|
||||
*/
|
||||
@property (nonatomic, readonly) CGFloat descent;
|
||||
|
||||
/**
|
||||
The leading (additional space above the ascent) of the receiver
|
||||
*/
|
||||
@property (nonatomic, readonly) CGFloat leading;
|
||||
|
||||
/**
|
||||
The width of the receiver
|
||||
*/
|
||||
@property (nonatomic, readonly) CGFloat width;
|
||||
|
||||
/**
|
||||
`YES` if the writing direction is Right-to-Left, otherwise `NO`
|
||||
*/
|
||||
@property (nonatomic, readonly) BOOL writingDirectionIsRightToLeft;
|
||||
|
||||
/**
|
||||
The text attachment of the receiver, or `nil` if there is none
|
||||
*/
|
||||
@property (nonatomic, readonly) DTTextAttachment *attachment;
|
||||
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,541 @@
|
||||
//
|
||||
// DTCoreTextGlyphRun.m
|
||||
// DTCoreText
|
||||
//
|
||||
// Created by Oliver Drobnik on 1/25/11.
|
||||
// Copyright 2011 Drobnik.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import "DTCompatibility.h"
|
||||
#import "DTCoreTextGlyphRun.h"
|
||||
#import "DTCoreTextLayoutLine.h"
|
||||
#import "DTTextAttachment.h"
|
||||
#import "DTCoreTextConstants.h"
|
||||
#import "DTCoreTextParagraphStyle.h"
|
||||
#import "DTCoreTextFunctions.h"
|
||||
#import "NSDictionary+DTCoreText.h"
|
||||
#import <DTFoundation/DTWeakSupport.h>
|
||||
|
||||
#import <DTFoundation/DTLog.h>
|
||||
|
||||
@implementation DTCoreTextGlyphRun
|
||||
{
|
||||
CTRunRef _run;
|
||||
CGRect _frame;
|
||||
|
||||
CGFloat _offset; // x distance from line origin
|
||||
CGFloat _ascent;
|
||||
CGFloat _descent;
|
||||
CGFloat _leading;
|
||||
CGFloat _width;
|
||||
|
||||
BOOL _writingDirectionIsRightToLeft;
|
||||
BOOL _isTrailingWhitespace;
|
||||
|
||||
NSInteger _numberOfGlyphs;
|
||||
|
||||
const CGPoint *_glyphPositionPoints;
|
||||
|
||||
DT_WEAK_VARIABLE DTCoreTextLayoutLine *_line; // retain cycle, since these objects are retained by the _line
|
||||
DT_WEAK_VARIABLE NSDictionary *_attributes; // weak because it is owned by _run IVAR
|
||||
NSArray *_stringIndices;
|
||||
|
||||
DTTextAttachment *_attachment;
|
||||
BOOL _hyperlink;
|
||||
|
||||
BOOL _didCheckForAttachmentInAttributes;
|
||||
BOOL _didCheckForHyperlinkInAttributes;
|
||||
BOOL _didCalculateMetrics;
|
||||
BOOL _didDetermineTrailingWhitespace;
|
||||
}
|
||||
|
||||
- (id)initWithRun:(CTRunRef)run layoutLine:(DTCoreTextLayoutLine *)layoutLine offset:(CGFloat)offset
|
||||
{
|
||||
self = [super init];
|
||||
|
||||
if (self)
|
||||
{
|
||||
_run = run;
|
||||
CFRetain(_run);
|
||||
|
||||
_offset = offset;
|
||||
_line = layoutLine;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
if (_run)
|
||||
{
|
||||
CFRelease(_run);
|
||||
}
|
||||
}
|
||||
|
||||
#ifndef COVERAGE
|
||||
// exclude method from coverage testing
|
||||
|
||||
- (NSString *)description
|
||||
{
|
||||
return [NSString stringWithFormat:@"<%@ glyphs=%ld %@>", [self class], (long)[self numberOfGlyphs], NSStringFromCGRect(_frame)];
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
#pragma mark - Drawing
|
||||
|
||||
- (void)drawInContext:(CGContextRef)context
|
||||
{
|
||||
if (!_run || !context)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CGAffineTransform textMatrix = CTRunGetTextMatrix(_run);
|
||||
|
||||
if (CGAffineTransformIsIdentity(textMatrix))
|
||||
{
|
||||
CTRunDraw(_run, context, CFRangeMake(0, 0));
|
||||
}
|
||||
else
|
||||
{
|
||||
CGPoint pos = CGContextGetTextPosition(context);
|
||||
|
||||
// set tx and ty to current text pos according to docs
|
||||
textMatrix.tx = pos.x;
|
||||
textMatrix.ty = pos.y;
|
||||
|
||||
CGContextSetTextMatrix(context, textMatrix);
|
||||
|
||||
CTRunDraw(_run, context, CFRangeMake(0, 0));
|
||||
|
||||
// restore identity
|
||||
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)drawDecorationInContext:(CGContextRef)context
|
||||
{
|
||||
// get the scaling factor of the current translation matrix
|
||||
CGAffineTransform ctm = CGContextGetCTM(context);
|
||||
CGFloat contentScale = MAX(ctm.a, -ctm.d); // needed for rounding operations
|
||||
|
||||
if (contentScale<1 || contentScale>2)
|
||||
{
|
||||
contentScale = 2;
|
||||
}
|
||||
|
||||
CGFloat smallestPixelWidth = 1.0f/contentScale;
|
||||
|
||||
DTColor *backgroundColor = [self.attributes backgroundColor];
|
||||
|
||||
// -------------- Line-Out, Underline, Background-Color
|
||||
BOOL drawStrikeOut = [[_attributes objectForKey:DTStrikeOutAttribute] boolValue];
|
||||
BOOL drawUnderline = [[_attributes objectForKey:(id)kCTUnderlineStyleAttributeName] boolValue];
|
||||
|
||||
if (drawStrikeOut||drawUnderline||backgroundColor)
|
||||
{
|
||||
// calculate area covered by non-whitespace
|
||||
CGRect lineFrame = _line.frame;
|
||||
|
||||
// LTR line frames include trailing whitespace in width
|
||||
// we need to subtract it so that we don't highlight/underline it
|
||||
if (!_line.writingDirectionIsRightToLeft)
|
||||
{
|
||||
lineFrame.size.width -= _line.trailingWhitespaceWidth;
|
||||
}
|
||||
|
||||
// exclude trailing whitespace so that we don't underline too much
|
||||
CGRect runStrokeBounds = CGRectIntersection(lineFrame, self.frame);
|
||||
|
||||
NSInteger superscriptStyle = [[_attributes objectForKey:(id)kCTSuperscriptAttributeName] integerValue];
|
||||
|
||||
switch (superscriptStyle)
|
||||
{
|
||||
case 1:
|
||||
{
|
||||
runStrokeBounds.origin.y -= _ascent * 0.47f;
|
||||
break;
|
||||
}
|
||||
case -1:
|
||||
{
|
||||
runStrokeBounds.origin.y += _ascent * 0.25f;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (backgroundColor)
|
||||
{
|
||||
CGRect backgroundColorRect = CGRectIntegral(CGRectMake(runStrokeBounds.origin.x, lineFrame.origin.y, runStrokeBounds.size.width, lineFrame.size.height));
|
||||
|
||||
CGContextSetFillColorWithColor(context, backgroundColor.CGColor);
|
||||
CGContextFillRect(context, backgroundColorRect);
|
||||
}
|
||||
|
||||
if (drawStrikeOut || drawUnderline)
|
||||
{
|
||||
BOOL didDrawSomething = NO;
|
||||
|
||||
CGContextSaveGState(context);
|
||||
|
||||
CTFontRef usedFont = (__bridge CTFontRef)([_attributes objectForKey:(id)kCTFontAttributeName]);
|
||||
|
||||
CGFloat fontUnderlineThickness;
|
||||
|
||||
if (usedFont)
|
||||
{
|
||||
fontUnderlineThickness = CTFontGetUnderlineThickness(usedFont) * smallestPixelWidth;
|
||||
}
|
||||
else
|
||||
{
|
||||
fontUnderlineThickness = smallestPixelWidth;
|
||||
}
|
||||
|
||||
CGFloat usedUnderlineThickness = DTCeilWithContentScale(fontUnderlineThickness, contentScale);
|
||||
|
||||
CGContextSetLineWidth(context, usedUnderlineThickness);
|
||||
|
||||
if (drawStrikeOut)
|
||||
{
|
||||
CGFloat y;
|
||||
|
||||
if (usedFont)
|
||||
{
|
||||
CGFloat strokePosition = CTFontGetXHeight(usedFont)/(CGFloat)2.0;
|
||||
y = DTRoundWithContentScale(runStrokeBounds.origin.y + _ascent - strokePosition, contentScale);
|
||||
}
|
||||
else
|
||||
{
|
||||
y = DTRoundWithContentScale((runStrokeBounds.origin.y + self.frame.size.height/2.0f + 1), contentScale);
|
||||
}
|
||||
|
||||
if ((int)(usedUnderlineThickness/smallestPixelWidth)%2) // odd line width
|
||||
{
|
||||
y += smallestPixelWidth/2.0f; // shift down half a pixel to avoid aliasing
|
||||
}
|
||||
|
||||
CGContextMoveToPoint(context, runStrokeBounds.origin.x, y);
|
||||
CGContextAddLineToPoint(context, runStrokeBounds.origin.x + runStrokeBounds.size.width, y);
|
||||
|
||||
didDrawSomething = YES;
|
||||
}
|
||||
|
||||
// only draw underlines if Core Text didn't draw them yet
|
||||
if (drawUnderline && !DTCoreTextDrawsUnderlinesWithGlyphs())
|
||||
{
|
||||
CGFloat y;
|
||||
|
||||
// use lowest underline position of all glyph runs in same line
|
||||
CGFloat underlinePosition = [_line underlineOffset];
|
||||
|
||||
y = DTRoundWithContentScale(_line.baselineOrigin.y + underlinePosition - fontUnderlineThickness/2.0f, contentScale);
|
||||
|
||||
if ((int)(usedUnderlineThickness/smallestPixelWidth)%2) // odd line width
|
||||
{
|
||||
y += smallestPixelWidth/2.0f; // shift down half a pixel to avoid aliasing
|
||||
}
|
||||
|
||||
CGContextMoveToPoint(context, runStrokeBounds.origin.x, y);
|
||||
CGContextAddLineToPoint(context, runStrokeBounds.origin.x + runStrokeBounds.size.width, y);
|
||||
|
||||
didDrawSomething = YES;
|
||||
}
|
||||
|
||||
if (didDrawSomething)
|
||||
{
|
||||
CGContextStrokePath(context);
|
||||
}
|
||||
|
||||
CGContextRestoreGState(context); // restore antialiasing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (CGPathRef)newPathWithGlyphs
|
||||
{
|
||||
CTFontRef font = (__bridge CTFontRef)[self.attributes objectForKey:(id)kCTFontAttributeName];
|
||||
|
||||
if (!font)
|
||||
{
|
||||
DTLogError(@"CTFont missing on %@", self);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
const CGGlyph *glyphs = CTRunGetGlyphsPtr(_run);
|
||||
const CGPoint *positions = CTRunGetPositionsPtr(_run);
|
||||
|
||||
CGMutablePathRef mutablePath = CGPathCreateMutable();
|
||||
|
||||
for (NSUInteger i = 0; i < CTRunGetGlyphCount(_run); i++)
|
||||
{
|
||||
CGGlyph glyph = glyphs[i];
|
||||
CGPoint position = positions[i];
|
||||
|
||||
CGAffineTransform glyphTransform = CTRunGetTextMatrix(_run);
|
||||
|
||||
glyphTransform = CGAffineTransformScale(glyphTransform, 1, -1);
|
||||
|
||||
|
||||
CGPathRef glyphPath = CTFontCreatePathForGlyph(font, glyph, &glyphTransform);
|
||||
|
||||
CGAffineTransform posTransform = CGAffineTransformMakeTranslation(position.x, position.y);
|
||||
CGPathAddPath(mutablePath, &posTransform, glyphPath);
|
||||
|
||||
CGPathRelease(glyphPath);
|
||||
}
|
||||
|
||||
return mutablePath;
|
||||
}
|
||||
|
||||
#pragma mark - Calculations
|
||||
- (void)calculateMetrics
|
||||
{
|
||||
// calculate metrics
|
||||
@synchronized(self)
|
||||
{
|
||||
if (!_didCalculateMetrics)
|
||||
{
|
||||
_width = (CGFloat)CTRunGetTypographicBounds((CTRunRef)_run, CFRangeMake(0, 0), &_ascent, &_descent, &_leading);
|
||||
_didCalculateMetrics = YES;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (CGRect)frameOfGlyphAtIndex:(NSInteger)index
|
||||
{
|
||||
if (!_didCalculateMetrics)
|
||||
{
|
||||
[self calculateMetrics];
|
||||
}
|
||||
|
||||
if (!_glyphPositionPoints)
|
||||
{
|
||||
// this is a pointer to the points inside the run, thus no retain necessary
|
||||
_glyphPositionPoints = CTRunGetPositionsPtr(_run);
|
||||
}
|
||||
|
||||
if (!_glyphPositionPoints || index >= self.numberOfGlyphs)
|
||||
{
|
||||
return CGRectNull;
|
||||
}
|
||||
|
||||
CGPoint glyphPosition = _glyphPositionPoints[index];
|
||||
|
||||
CGRect rect = CGRectMake(_line.baselineOrigin.x + glyphPosition.x, _line.baselineOrigin.y - _ascent, _offset + _width - glyphPosition.x, _ascent + _descent);
|
||||
if (index < self.numberOfGlyphs-1)
|
||||
{
|
||||
rect.size.width = _glyphPositionPoints[index+1].x - glyphPosition.x;
|
||||
}
|
||||
|
||||
return rect;
|
||||
}
|
||||
|
||||
// TODO: fix indices if the stringRange is modified
|
||||
- (NSArray *)stringIndices
|
||||
{
|
||||
if (!_stringIndices)
|
||||
{
|
||||
const CFIndex *indices = CTRunGetStringIndicesPtr(_run);
|
||||
NSInteger count = self.numberOfGlyphs;
|
||||
NSMutableArray *array = [NSMutableArray arrayWithCapacity:count];
|
||||
NSInteger i;
|
||||
for (i = 0; i < count; i++)
|
||||
{
|
||||
[array addObject:[NSNumber numberWithInteger:indices[i]]];
|
||||
}
|
||||
_stringIndices = array;
|
||||
}
|
||||
return _stringIndices;
|
||||
}
|
||||
|
||||
// bounds of an image encompassing the entire run
|
||||
- (CGRect)imageBoundsInContext:(CGContextRef)context
|
||||
{
|
||||
return CTRunGetImageBounds(_run, context, CFRangeMake(0, 0));
|
||||
}
|
||||
|
||||
// range of the characters from the original string
|
||||
- (NSRange)stringRange
|
||||
{
|
||||
if (!_stringRange.length)
|
||||
{
|
||||
CFRange range = CTRunGetStringRange(_run);
|
||||
|
||||
_stringRange = NSMakeRange(range.location + _line.stringLocationOffset, range.length);
|
||||
}
|
||||
|
||||
return _stringRange;
|
||||
}
|
||||
|
||||
- (void)fixMetricsFromAttachment
|
||||
{
|
||||
if (self.attachment)
|
||||
{
|
||||
if (!_didCalculateMetrics)
|
||||
{
|
||||
[self calculateMetrics];
|
||||
}
|
||||
|
||||
_descent = 0;
|
||||
_ascent = self.attachment.displaySize.height;
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)isTrailingWhitespace
|
||||
{
|
||||
if (_didDetermineTrailingWhitespace)
|
||||
{
|
||||
return _isTrailingWhitespace;
|
||||
}
|
||||
|
||||
BOOL isTrailing;
|
||||
|
||||
if (_line.writingDirectionIsRightToLeft)
|
||||
{
|
||||
isTrailing = (self == [[_line glyphRuns] objectAtIndex:0]);
|
||||
}
|
||||
else
|
||||
{
|
||||
isTrailing = (self == [[_line glyphRuns] lastObject]);
|
||||
}
|
||||
|
||||
if (isTrailing)
|
||||
{
|
||||
if (!_didCalculateMetrics)
|
||||
{
|
||||
[self calculateMetrics];
|
||||
}
|
||||
|
||||
// this is trailing whitespace if it matches the lines's trailing whitespace
|
||||
if (_line.trailingWhitespaceWidth >= _width)
|
||||
{
|
||||
_isTrailingWhitespace = YES;
|
||||
}
|
||||
}
|
||||
|
||||
_didDetermineTrailingWhitespace = YES;
|
||||
return _isTrailingWhitespace;
|
||||
}
|
||||
|
||||
#pragma mark Properites
|
||||
- (NSInteger)numberOfGlyphs
|
||||
{
|
||||
if (!_numberOfGlyphs)
|
||||
{
|
||||
_numberOfGlyphs = CTRunGetGlyphCount(_run);
|
||||
}
|
||||
|
||||
return _numberOfGlyphs;
|
||||
}
|
||||
|
||||
- (NSDictionary *)attributes
|
||||
{
|
||||
if (!_attributes)
|
||||
{
|
||||
_attributes = (__bridge NSDictionary *)CTRunGetAttributes(_run);
|
||||
}
|
||||
|
||||
return _attributes;
|
||||
}
|
||||
|
||||
- (DTTextAttachment *)attachment
|
||||
{
|
||||
if (!_attachment)
|
||||
{
|
||||
if (!_didCheckForAttachmentInAttributes)
|
||||
{
|
||||
_attachment = [self.attributes objectForKey:NSAttachmentAttributeName];
|
||||
|
||||
_didCheckForAttachmentInAttributes = YES;
|
||||
}
|
||||
}
|
||||
|
||||
return _attachment;
|
||||
}
|
||||
|
||||
- (BOOL)isHyperlink
|
||||
{
|
||||
if (!_hyperlink)
|
||||
{
|
||||
if (!_didCheckForHyperlinkInAttributes)
|
||||
{
|
||||
_hyperlink = [self.attributes objectForKey:DTLinkAttribute]!=nil;
|
||||
|
||||
_didCheckForHyperlinkInAttributes = YES;
|
||||
}
|
||||
}
|
||||
|
||||
return _hyperlink;
|
||||
}
|
||||
|
||||
- (CGRect)frame
|
||||
{
|
||||
if (!_didCalculateMetrics)
|
||||
{
|
||||
[self calculateMetrics];
|
||||
}
|
||||
|
||||
return CGRectMake(_line.baselineOrigin.x + _offset, _line.baselineOrigin.y - _ascent, _width, _ascent + _descent);
|
||||
}
|
||||
|
||||
- (CGFloat)width
|
||||
{
|
||||
if (!_didCalculateMetrics)
|
||||
{
|
||||
[self calculateMetrics];
|
||||
}
|
||||
|
||||
return _width;
|
||||
}
|
||||
|
||||
- (CGFloat)ascent
|
||||
{
|
||||
if (!_didCalculateMetrics)
|
||||
{
|
||||
[self calculateMetrics];
|
||||
}
|
||||
|
||||
return _ascent;
|
||||
}
|
||||
|
||||
- (CGFloat)descent
|
||||
{
|
||||
if (!_didCalculateMetrics)
|
||||
{
|
||||
[self calculateMetrics];
|
||||
}
|
||||
|
||||
return _descent;
|
||||
}
|
||||
|
||||
- (CGFloat)leading
|
||||
{
|
||||
if (!_didCalculateMetrics)
|
||||
{
|
||||
[self calculateMetrics];
|
||||
}
|
||||
|
||||
return _leading;
|
||||
}
|
||||
|
||||
- (BOOL)writingDirectionIsRightToLeft
|
||||
{
|
||||
CTRunStatus status = CTRunGetStatus(_run);
|
||||
|
||||
return (status & kCTRunStatusRightToLeft)!=0;
|
||||
}
|
||||
|
||||
@synthesize frame = _frame;
|
||||
@synthesize numberOfGlyphs = _numberOfGlyphs;
|
||||
@synthesize attributes = _attributes;
|
||||
|
||||
@synthesize ascent = _ascent;
|
||||
@synthesize descent = _descent;
|
||||
@synthesize leading = _leading;
|
||||
@synthesize attachment = _attachment;
|
||||
@synthesize writingDirectionIsRightToLeft = _writingDirectionIsRightToLeft;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,33 @@
|
||||
//
|
||||
// DTCoreTextLayoutFrame+Cursor.h
|
||||
// DTCoreText
|
||||
//
|
||||
// Created by Oliver Drobnik on 10.07.13.
|
||||
// Copyright (c) 2013 Drobnik.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import "DTCoreTextLayoutFrame.h"
|
||||
|
||||
/**
|
||||
The **Cursor** category extends DTCoreTextLayoutFrame for working with a caret and determine the string index of touch coordinates.
|
||||
*/
|
||||
|
||||
@interface DTCoreTextLayoutFrame (Cursor)
|
||||
|
||||
/**
|
||||
Determines the closest string index to a point in the receiver's frame.
|
||||
|
||||
This can be used to find the cursor position to position an input caret at.
|
||||
@param point The point
|
||||
@returns The resulting string index
|
||||
*/
|
||||
- (NSInteger)closestCursorIndexToPoint:(CGPoint)point;
|
||||
|
||||
/**
|
||||
The rectangle to draw a caret for a given index
|
||||
@param index The string index for which to determine a cursor frame
|
||||
@returns The cursor rectangle
|
||||
*/
|
||||
- (CGRect)cursorRectAtIndex:(NSInteger)index;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,114 @@
|
||||
//
|
||||
// DTCoreTextLayoutFrame+Cursor.m
|
||||
// DTCoreText
|
||||
//
|
||||
// Created by Oliver Drobnik on 10.07.13.
|
||||
// Copyright (c) 2013 Drobnik.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import "DTCoreTextLayoutFrame+Cursor.h"
|
||||
#import "DTCoreTextLayoutLine.h"
|
||||
|
||||
@implementation DTCoreTextLayoutFrame (Cursor)
|
||||
|
||||
- (NSInteger)closestCursorIndexToPoint:(CGPoint)point
|
||||
{
|
||||
NSArray *lines = self.lines;
|
||||
|
||||
if (![lines count])
|
||||
{
|
||||
return NSNotFound;
|
||||
}
|
||||
|
||||
DTCoreTextLayoutLine *firstLine = [lines objectAtIndex:0];
|
||||
if (point.y < CGRectGetMinY(firstLine.frame))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
DTCoreTextLayoutLine *lastLine = [lines lastObject];
|
||||
if (point.y > CGRectGetMaxY(lastLine.frame))
|
||||
{
|
||||
NSRange stringRange = [self visibleStringRange];
|
||||
|
||||
if (stringRange.length)
|
||||
{
|
||||
return NSMaxRange([self visibleStringRange])-1;
|
||||
}
|
||||
}
|
||||
|
||||
// find closest line
|
||||
DTCoreTextLayoutLine *closestLine = nil;
|
||||
CGFloat closestDistance = CGFLOAT_MAX;
|
||||
|
||||
for (DTCoreTextLayoutLine *oneLine in lines)
|
||||
{
|
||||
// line contains point
|
||||
if (CGRectGetMinY(oneLine.frame) <= point.y && CGRectGetMaxY(oneLine.frame) >= point.y)
|
||||
{
|
||||
closestLine = oneLine;
|
||||
break;
|
||||
}
|
||||
|
||||
CGFloat top = CGRectGetMinY(oneLine.frame);
|
||||
CGFloat bottom = CGRectGetMaxY(oneLine.frame);
|
||||
|
||||
CGFloat distance = CGFLOAT_MAX;
|
||||
|
||||
if (top > point.y)
|
||||
{
|
||||
distance = top - point.y;
|
||||
}
|
||||
else if (bottom < point.y)
|
||||
{
|
||||
distance = point.y - bottom;
|
||||
}
|
||||
|
||||
if (distance < closestDistance)
|
||||
{
|
||||
closestLine = oneLine;
|
||||
closestDistance = distance;
|
||||
}
|
||||
}
|
||||
|
||||
if (!closestLine)
|
||||
{
|
||||
return NSNotFound;
|
||||
}
|
||||
|
||||
NSInteger closestIndex = [closestLine stringIndexForPosition:point];
|
||||
|
||||
NSInteger maxIndex = NSMaxRange([closestLine stringRange])-1;
|
||||
|
||||
if (closestIndex > maxIndex)
|
||||
{
|
||||
closestIndex = maxIndex;
|
||||
}
|
||||
|
||||
if (closestIndex>=0)
|
||||
{
|
||||
return closestIndex;
|
||||
}
|
||||
|
||||
return NSNotFound;
|
||||
}
|
||||
|
||||
- (CGRect)cursorRectAtIndex:(NSInteger)index
|
||||
{
|
||||
DTCoreTextLayoutLine *line = [self lineContainingIndex:index];
|
||||
|
||||
if (!line)
|
||||
{
|
||||
return CGRectZero;
|
||||
}
|
||||
|
||||
CGFloat offset = [line offsetForStringIndex:index];
|
||||
|
||||
CGRect rect = line.frame;
|
||||
rect.size.width = 3.0;
|
||||
rect.origin.x += offset;
|
||||
|
||||
return rect;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,385 @@
|
||||
//
|
||||
// DTCoreTextLayoutFrame.h
|
||||
// DTCoreText
|
||||
//
|
||||
// Created by Oliver Drobnik on 1/24/11.
|
||||
// Copyright 2011 Drobnik.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import "DTCompatibility.h"
|
||||
#import "DTCoreTextConstants.h"
|
||||
|
||||
|
||||
#import <CoreText/CoreText.h>
|
||||
|
||||
#if TARGET_OS_IPHONE && !TARGET_OS_WATCH
|
||||
#import <UIKit/UIKit.h>
|
||||
#endif
|
||||
|
||||
#if TARGET_OS_OSX
|
||||
#import <AppKit/AppKit.h>
|
||||
#import <ApplicationServices/ApplicationServices.h>
|
||||
#endif
|
||||
|
||||
@class DTCoreTextLayoutLine;
|
||||
@class DTTextBlock;
|
||||
|
||||
/**
|
||||
A handler block that is called whenever a text block attributed is encountered during text drawing
|
||||
*/
|
||||
typedef void (^DTCoreTextLayoutFrameTextBlockHandler)(DTTextBlock *textBlock, CGRect frame, CGContextRef context, BOOL *shouldDrawDefaultBackground);
|
||||
|
||||
/**
|
||||
The drawing options for DTCoreTextLayoutFrame
|
||||
*/
|
||||
typedef NS_ENUM(NSUInteger, DTCoreTextLayoutFrameDrawingOptions)
|
||||
{
|
||||
/**
|
||||
The default method for drawing draws links and attachments. Links are drawn non-highlighted
|
||||
*/
|
||||
DTCoreTextLayoutFrameDrawingDefault = 1<<0,
|
||||
|
||||
/**
|
||||
Links are not drawn, e.g. if they are displayed via custom buttons
|
||||
*/
|
||||
DTCoreTextLayoutFrameDrawingOmitLinks = 1<<1,
|
||||
|
||||
/**
|
||||
Text attachments are omitted from drawing, e.g. if they are displayed via custom views
|
||||
*/
|
||||
DTCoreTextLayoutFrameDrawingOmitAttachments = 1<<2,
|
||||
|
||||
/**
|
||||
If links are drawn they are displayed with the highlighted variant
|
||||
*/
|
||||
DTCoreTextLayoutFrameDrawingDrawLinksHighlighted = 1<<3
|
||||
} ;
|
||||
|
||||
|
||||
@class DTCoreTextLayouter;
|
||||
|
||||
/**
|
||||
This class represents a single frame of text and basically wraps CTFrame. It provides an array of text lines that fit in the given rectangle.
|
||||
|
||||
Both styles of layouting are supported: open ended (suitable for scroll views) and limited to a given rectangle. To use the open-ended style specify `CGFLOAT_HEIGHT_UNKNOWN` for the <frame> height when creating a layout frame.
|
||||
|
||||
The array of lines is built lazily the first time it is accessed or - for open-ended frames - when the frame property is being queried.
|
||||
*/
|
||||
@interface DTCoreTextLayoutFrame : NSObject
|
||||
{
|
||||
CGRect _frame;
|
||||
|
||||
NSArray *_lines;
|
||||
NSArray *_paragraphRanges;
|
||||
|
||||
NSArray *_textAttachments;
|
||||
NSAttributedString *_attributedStringFragment;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
@name Creating Layout Frames
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
Creates a Layout Frame with the given frame using the attributed string loaded into the layouter.
|
||||
|
||||
@param frame The rectangle specifying origin and size of available for text. Specify `CGFLOAT_WIDTH_UNKNOWN` to not limit the width. Specify `CGFLOAT_HEIGHT_UNKNOWN` to not limit the height.
|
||||
@param layouter A reference to the layouter for this text box.
|
||||
*/
|
||||
- (id)initWithFrame:(CGRect)frame layouter:(DTCoreTextLayouter *)layouter;
|
||||
|
||||
|
||||
/**
|
||||
Creates a Layout Frame with the given frame using the attributed string loaded into the layouter.
|
||||
|
||||
@param frame The rectangle specifying origin and size of available for text. Specify `CGFLOAT_WIDTH_UNKNOWN` to not limit the width. Specify `CGFLOAT_HEIGHT_UNKNOWN` to not limit the height.
|
||||
@param layouter A reference to the layouter for the receiver. Note: The layouter owns the attributed string.
|
||||
@param range The range within the attributed string to layout into the receiver.
|
||||
*/
|
||||
- (id)initWithFrame:(CGRect)frame layouter:(DTCoreTextLayouter *)layouter range:(NSRange)range;
|
||||
|
||||
|
||||
/**
|
||||
@name Getting Information
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
The string range that is visible i.e. fits into the given rectangle. For open-ended frames this is typically the entire string. For frame-contrained layout frames it is the substring that fits.
|
||||
*/
|
||||
- (NSRange)visibleStringRange;
|
||||
|
||||
|
||||
/**
|
||||
This is a copy of the attributed string owned by the layouter of the receiver.
|
||||
*/
|
||||
- (NSAttributedString *)attributedStringFragment;
|
||||
|
||||
|
||||
/**
|
||||
An array that maps glyphs with string indices.
|
||||
*/
|
||||
- (NSArray *)stringIndices;
|
||||
|
||||
|
||||
/**
|
||||
The frame rectangle for the layout frame.
|
||||
*/
|
||||
@property (nonatomic, assign, readonly) CGRect frame;
|
||||
|
||||
|
||||
/**
|
||||
Calculates the frame that is covered by the text content.
|
||||
|
||||
The result is calculated by enumerating over all lines and creating a union over all their frames. This is different than the frame property since this gets calculated.
|
||||
@returns The area that is covered by the text content.
|
||||
@note The width depends on how many glyphs Core Text was able to fit into a line. A line that gets broken might not have glyphs all the way to the margin. The y origin is always adjusted to be the same as frame since the first line might have some leading. The height is the minimum height that fits all layout lines.
|
||||
*/
|
||||
- (CGRect)intrinsicContentFrame;
|
||||
|
||||
|
||||
/**
|
||||
@name Drawing
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
Draws the receiver into the given graphics context.
|
||||
|
||||
@warning This method is deprecated, use -[DTCoreTextLayoutFrame drawInContext:options:] instead
|
||||
@param context A graphics context to draw into
|
||||
@param drawImages Whether images should be drawn together with the text. If you specify `NO` then space is left blank where images would go and you have to add your own views to display these images.
|
||||
@param drawLinks Whether hyperlinks should be drawn together with the text. If you specify `NO` then space is left blank where links would go and you have to add your own views to display these images.
|
||||
*/
|
||||
- (void)drawInContext:(CGContextRef)context drawImages:(BOOL)drawImages drawLinks:(BOOL)drawLinks __attribute__((deprecated("use -[DTCoreTextLayoutFrame drawInContext:options:] instead")));
|
||||
|
||||
|
||||
/**
|
||||
Draws the receiver into the given graphics context.
|
||||
|
||||
@param context A graphics context to draw into
|
||||
@param options The drawing options. See DTCoreTextLayoutFrameDrawingOptions for available options.
|
||||
*/
|
||||
- (void)drawInContext:(CGContextRef)context options:(DTCoreTextLayoutFrameDrawingOptions)options;
|
||||
|
||||
|
||||
/**
|
||||
Set a custom handler to be executed before text belonging to a text block is drawn. Of type <DTCoreTextLayoutFrameTextBlockHandler>.
|
||||
*/
|
||||
@property (nonatomic, copy) DTCoreTextLayoutFrameTextBlockHandler textBlockHandler;
|
||||
|
||||
|
||||
/**
|
||||
@name Working with Glyphs
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
Retrieves the index of the text line that contains the given glyph index.
|
||||
|
||||
@param index The index of the glyph
|
||||
@returns The index of the line containing this glyph
|
||||
*/
|
||||
- (NSInteger)lineIndexForGlyphIndex:(NSInteger)index;
|
||||
|
||||
|
||||
/**
|
||||
Retrieves the frame of the glyph at the given glyph index.
|
||||
|
||||
@param index The index of the glyph
|
||||
@returns The frame of this glyph
|
||||
*/
|
||||
- (CGRect)frameOfGlyphAtIndex:(NSInteger)index;
|
||||
|
||||
|
||||
/**
|
||||
@name Working with Text Lines
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
The text lines that belong to the receiver.
|
||||
*/
|
||||
@property (nonatomic, strong, readonly) NSArray *lines;
|
||||
|
||||
|
||||
/**
|
||||
The text lines that are visible inside the given rectangle. Also incomplete lines are included.
|
||||
|
||||
@param rect The rectangle
|
||||
@returns An array, sorted from top to bottom, of lines at least partially visible
|
||||
*/
|
||||
- (NSArray *)linesVisibleInRect:(CGRect)rect;
|
||||
|
||||
|
||||
/**
|
||||
The text lines that are visible inside the given rectangle. Only fully visible lines are included.
|
||||
|
||||
@param rect The rectangle
|
||||
@returns An array, sorted from top to bottom, of lines fully visible
|
||||
*/
|
||||
- (NSArray *)linesContainedInRect:(CGRect)rect;
|
||||
|
||||
|
||||
/**
|
||||
The layout line that contains the given string index.
|
||||
|
||||
@param index The string index
|
||||
@returns The layout line that this index belongs to
|
||||
*/
|
||||
- (DTCoreTextLayoutLine *)lineContainingIndex:(NSUInteger)index;
|
||||
|
||||
|
||||
/**
|
||||
Determins if the given line is the first in a paragraph.
|
||||
|
||||
This is needed for example to determine whether paragraphSpaceBefore needs to be applied before it.
|
||||
@param line The Line
|
||||
@returns `YES` if the given line is the first in a paragraph
|
||||
*/
|
||||
- (BOOL)isLineFirstInParagraph:(DTCoreTextLayoutLine *)line;
|
||||
|
||||
|
||||
/**
|
||||
Determins if the given line is the last in a paragraph.
|
||||
|
||||
This is needed for example to determine whether paragraph spacing needs to be applied after it.
|
||||
@param line The Line
|
||||
@returns `YES` if the given line is the last in a paragraph
|
||||
*/
|
||||
- (BOOL)isLineLastInParagraph:(DTCoreTextLayoutLine *)line;
|
||||
|
||||
|
||||
/**
|
||||
Finds the appropriate baseline origin for a line to position it at the correct distance from a previous line.
|
||||
|
||||
Support Layout options are:
|
||||
|
||||
- DTCoreTextLayoutFrameLinePositioningAlgorithmWebKit,
|
||||
- DTCoreTextLayoutFrameLinePositioningAlgorithmLegacy
|
||||
|
||||
@param line The line
|
||||
@param previousLine The line after which to position the line.
|
||||
@param options The layout options to employ for positioning lines
|
||||
@returns The correct baseline origin for the line.
|
||||
*/
|
||||
- (CGPoint)baselineOriginToPositionLine:(DTCoreTextLayoutLine *)line afterLine:(DTCoreTextLayoutLine *)previousLine options:(DTCoreTextLayoutFrameLinePositioningOptions)options;
|
||||
|
||||
/**
|
||||
Finds the appropriate baseline origin for a line to position it at the correct distance from a previous line using the DTCoreTextLayoutFrameLinePositioningOptionAlgorithmLegacy algorithm.
|
||||
|
||||
@warning This method is deprecated, use -[baselineOriginToPositionLine:afterLine:algorithm:] instead
|
||||
@param line The line
|
||||
@param previousLine The line after which to position the line.
|
||||
@returns The correct baseline origin for the line.
|
||||
*/
|
||||
- (CGPoint)baselineOriginToPositionLine:(DTCoreTextLayoutLine *)line afterLine:(DTCoreTextLayoutLine *)previousLine __attribute__((deprecated("use use -[baselineOriginToPositionLine:afterLine:algorithm:] instead")));;
|
||||
|
||||
/**
|
||||
The ratio to decide when to create a justified line
|
||||
*/
|
||||
@property (nonatomic, readwrite) CGFloat justifyRatio;
|
||||
|
||||
/**
|
||||
@name Text Attachments
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
The array of all <DTTextAttachment> instances that belong to the receiver.
|
||||
@returns All text attachments of the receiver.
|
||||
*/
|
||||
- (NSArray *)textAttachments;
|
||||
|
||||
|
||||
/**
|
||||
The array of all DTTextAttachment instances that belong to the receiver which also match the specified predicate.
|
||||
|
||||
@param predicate A predicate that uses properties of <DTTextAttachment> to reduce the returned array
|
||||
@returns A filtered array of text attachments.
|
||||
*/
|
||||
- (NSArray *)textAttachmentsWithPredicate:(NSPredicate *)predicate;
|
||||
|
||||
|
||||
/**
|
||||
@name Getting Paragraph Info
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
Finding which paragraph a given string index belongs to.
|
||||
|
||||
@param stringIndex The index in the string to look for
|
||||
@returns The index of the paragraph, numbered from 0
|
||||
*/
|
||||
- (NSUInteger)paragraphIndexContainingStringIndex:(NSUInteger)stringIndex;
|
||||
|
||||
|
||||
/**
|
||||
Determines the paragraph range (of paragraph indexes) that encompass the entire given string Range.
|
||||
|
||||
@param stringRange The string range for which the paragraph range is sought for
|
||||
@returns The range of paragraphs that fully enclose the string range
|
||||
*/
|
||||
- (NSRange)paragraphRangeContainingStringRange:(NSRange)stringRange;
|
||||
|
||||
|
||||
/**
|
||||
The text lines that belong to the specified paragraph.
|
||||
|
||||
@param index The index of the paragraph
|
||||
@returns An array, sorted from top to bottom, of lines in this paragraph
|
||||
*/
|
||||
- (NSArray *)linesInParagraphAtIndex:(NSUInteger)index;
|
||||
|
||||
|
||||
/**
|
||||
An array of `NSRange` values encapsulated in `NSValue` instances. Each range is the string range contained in the corresponding paragraph.
|
||||
*/
|
||||
@property (nonatomic, strong, readonly) NSArray *paragraphRanges;
|
||||
|
||||
|
||||
/**
|
||||
@name Debugging
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
Switches on the debug drawing mode where individual glyph runs, baselines, et cetera get individually marked.
|
||||
|
||||
@param debugFrames if the debug drawing should occur
|
||||
*/
|
||||
+ (void)setShouldDrawDebugFrames:(BOOL)debugFrames;
|
||||
|
||||
|
||||
/**
|
||||
@returns the current value of the debug frame drawing
|
||||
*/
|
||||
+ (BOOL)shouldDrawDebugFrames;
|
||||
|
||||
/**
|
||||
@name Truncation
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
Maximum number of lines to display before truncation. Default is 0 which indicates no limit.
|
||||
*/
|
||||
@property(nonatomic, assign) NSInteger numberOfLines;
|
||||
|
||||
|
||||
/**
|
||||
Line break mode used to indicate how truncation should occur
|
||||
*/
|
||||
@property(nonatomic, assign) NSLineBreakMode lineBreakMode;
|
||||
|
||||
|
||||
/**
|
||||
Optional attributed string to use as truncation indicator. If nil, will use "…" w/ attributes taken from text being truncated
|
||||
*/
|
||||
@property(nonatomic, strong)NSAttributedString *truncationString;
|
||||
|
||||
|
||||
@end
|
||||
File diff suppressed because it is too large
Load Diff
Generated
+39
@@ -0,0 +1,39 @@
|
||||
//
|
||||
// DTCoreTextLayoutFrameAccessibilityElementGenerator.h
|
||||
// DTCoreText
|
||||
//
|
||||
// Created by Austen Green on 3/13/13.
|
||||
// Copyright (c) 2013 Drobnik.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import "DTCompatibility.h"
|
||||
|
||||
#if TARGET_OS_IPHONE && !TARGET_OS_WATCH
|
||||
|
||||
#import "DTAccessibilityElement.h"
|
||||
|
||||
@class DTCoreTextLayoutFrame, DTTextAttachment;
|
||||
|
||||
/**
|
||||
A block that provides accessibility information for the passed text attachments
|
||||
*/
|
||||
typedef id(^DTAttachmentViewProvider)(DTTextAttachment *textAttachment);
|
||||
|
||||
/**
|
||||
Generates an array of objects conforming to the UIAccessibility informal protocol based on a <DTCoreTextLayoutFrame>.
|
||||
*/
|
||||
@interface DTCoreTextLayoutFrameAccessibilityElementGenerator : NSObject
|
||||
|
||||
/**
|
||||
The designated initializer. The DTAttachmentViewProvider block may be used to provide custom subviews in place of a static accessibility element.
|
||||
@param frame The <DTCoreTextLayoutFrame> to generate accessibility elements for.
|
||||
@param view The logical superview of the elements - the view that owns the local coordinate system for drawing the frame.
|
||||
@param block A callback block which takes a <DTTextAttachment> object and returns an object that conforms to the UIAccessibility informal protocol.
|
||||
@returns Returns an array of objects conforming to the UIAccessibility informal protocol, suitable for presentation for the VoiceOver system.
|
||||
*/
|
||||
|
||||
- (NSArray *)accessibilityElementsForLayoutFrame:(DTCoreTextLayoutFrame *)frame view:(UIView *)view attachmentViewProvider:(DTAttachmentViewProvider)block;
|
||||
|
||||
@end
|
||||
|
||||
#endif
|
||||
Generated
+124
@@ -0,0 +1,124 @@
|
||||
//
|
||||
// DTCoreTextLayoutFrameAccessibilityElementGenerator.m
|
||||
// DTCoreText
|
||||
//
|
||||
// Created by Austen Green on 3/13/13.
|
||||
// Copyright (c) 2013 Drobnik.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import "DTCoreTextLayoutFrameAccessibilityElementGenerator.h"
|
||||
|
||||
#if TARGET_OS_IPHONE && !TARGET_OS_WATCH
|
||||
|
||||
#import "DTCoreTextLayoutFrame.h"
|
||||
#import "DTCoreTextLayoutLine.h"
|
||||
#import "DTCoreTextGlyphRun.h"
|
||||
#import "DTAccessibilityElement.h"
|
||||
#import "DTCoreTextConstants.h"
|
||||
#import "DTTextAttachment.h"
|
||||
|
||||
@implementation DTCoreTextLayoutFrameAccessibilityElementGenerator
|
||||
|
||||
- (NSArray *)accessibilityElementsForLayoutFrame:(DTCoreTextLayoutFrame *)frame view:(UIView *)view attachmentViewProvider:(DTAttachmentViewProvider)block
|
||||
{
|
||||
NSMutableArray *elements = [NSMutableArray array];
|
||||
|
||||
for (NSUInteger idx = 0; idx < frame.paragraphRanges.count; idx++)
|
||||
{
|
||||
NSArray *paragraphElements = [self accessibilityElementsInParagraphAtIndex:idx layoutFrame:frame view:view attachmentViewProvider:block];
|
||||
[elements addObjectsFromArray:paragraphElements];
|
||||
}
|
||||
|
||||
return elements;
|
||||
}
|
||||
|
||||
- (NSArray *)accessibilityElementsInParagraphAtIndex:(NSUInteger)index layoutFrame:(DTCoreTextLayoutFrame *)frame view:(UIView *)view attachmentViewProvider:(DTAttachmentViewProvider)block
|
||||
{
|
||||
NSMutableArray *elements = [NSMutableArray array];
|
||||
|
||||
[self enumerateAccessibleGroupsInFrame:frame forParagraphAtIndex:index usingBlock:^(NSDictionary *attrs, NSRange substringRange, BOOL *stop, NSArray *runs) {
|
||||
id element = [self accessibilityElementForTextInAttributedString:frame.attributedStringFragment atRange:substringRange attributes:attrs run:runs view:view attachmentViewProvider:block];
|
||||
if (element)
|
||||
[elements addObject:element];
|
||||
}];
|
||||
|
||||
return elements;
|
||||
}
|
||||
|
||||
- (void)enumerateAccessibleGroupsInFrame:(DTCoreTextLayoutFrame *)frame forParagraphAtIndex:(NSUInteger)index usingBlock:(void(^)(NSDictionary *attrs, NSRange substringRange, BOOL *stop, NSArray *runs))block
|
||||
{
|
||||
NSValue *value = [frame.paragraphRanges objectAtIndex:index];
|
||||
NSRange paragraphRange = value.rangeValue;
|
||||
NSArray *lines = [frame linesInParagraphAtIndex:index];
|
||||
|
||||
[frame.attributedStringFragment enumerateAttributesInRange:paragraphRange options:0 usingBlock:^(NSDictionary *attrs, NSRange range, BOOL *stop) {
|
||||
NSMutableArray *runs = [NSMutableArray array];
|
||||
for (DTCoreTextLayoutLine *line in lines)
|
||||
{
|
||||
[runs addObjectsFromArray:[line glyphRunsWithRange:range]];
|
||||
}
|
||||
|
||||
block(attrs, range, stop, runs);
|
||||
}];
|
||||
}
|
||||
|
||||
- (id)accessibilityElementForTextInAttributedString:(NSAttributedString *)attributedString atRange:(NSRange)range attributes:(NSDictionary *)attributes run:(NSArray *)runs view:(UIView *)view attachmentViewProvider:(DTAttachmentViewProvider)block
|
||||
{
|
||||
DTTextAttachment *attachment = [attributes objectForKey:NSAttachmentAttributeName];
|
||||
|
||||
if (attachment != nil)
|
||||
return [self viewForAttachment:attachment attachmentViewProvider:block];
|
||||
else
|
||||
return [self accessibilityElementForTextInAttributedString:attributedString atRange:range attributes:attributes run:runs view:view];
|
||||
}
|
||||
|
||||
- (DTAccessibilityElement *)accessibilityElementForTextInAttributedString:(NSAttributedString *)attributedString atRange:(NSRange)range attributes:(NSDictionary *)attributes run:(NSArray *)runs view:(UIView *)view
|
||||
{
|
||||
NSString *text = [attributedString.string substringWithRange:range];
|
||||
|
||||
DTAccessibilityElement *element = [[DTAccessibilityElement alloc] initWithParentView:view];
|
||||
element.accessibilityLabel = text;
|
||||
element.localCoordinateAccessibilityFrame = [self frameForRuns:runs];
|
||||
|
||||
// We're trying to keep the accessibility frame behavior consistent with web view, which seems to do a union of the rects for all the runs composing a single accessibility group,
|
||||
// even if that spans across multiple lines. Set the local coordinate activation point to support multi-line links. A link that is at the end of one line and
|
||||
// wraps to the beginning of the next would have a rect that's the size of both lines combined. The center of that rect would be outside the hit areas for either of the
|
||||
// runs individually, so we set the accessibility activation point to be the origin of the first run.
|
||||
if (runs.count > 1)
|
||||
{
|
||||
DTCoreTextGlyphRun *run = [runs objectAtIndex:0];
|
||||
element.localCoordinateAccessibilityActivationPoint = run.frame.origin;
|
||||
}
|
||||
|
||||
element.accessibilityTraits = UIAccessibilityTraitStaticText;
|
||||
|
||||
if ([attributes objectForKey:DTLinkAttribute])
|
||||
element.accessibilityTraits |= UIAccessibilityTraitLink;
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
- (UIView *)viewForAttachment:(DTTextAttachment *)attachment attachmentViewProvider:(DTAttachmentViewProvider)block
|
||||
{
|
||||
UIView *view = nil;
|
||||
|
||||
if (block)
|
||||
{
|
||||
view = block(attachment);
|
||||
}
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
- (CGRect)frameForRuns:(NSArray *)runs
|
||||
{
|
||||
CGRect frame = CGRectNull;
|
||||
for (DTCoreTextGlyphRun *run in runs)
|
||||
frame = CGRectUnion(frame, run.frame);
|
||||
|
||||
return frame;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,220 @@
|
||||
//
|
||||
// DTCoreTextLayoutLine.h
|
||||
// DTCoreText
|
||||
//
|
||||
// Created by Oliver Drobnik on 1/24/11.
|
||||
// Copyright 2011 Drobnik.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import "DTCompatibility.h"
|
||||
|
||||
#if TARGET_OS_IPHONE
|
||||
#import <CoreText/CoreText.h>
|
||||
#elif TARGET_OS_MAC
|
||||
#import <ApplicationServices/ApplicationServices.h>
|
||||
#endif
|
||||
|
||||
@class DTCoreTextLayoutFrame;
|
||||
@class DTCoreTextParagraphStyle;
|
||||
@class DTTextBlock;
|
||||
|
||||
/**
|
||||
This class represents one layouted line and contains a number of glyph runs.
|
||||
*/
|
||||
@interface DTCoreTextLayoutLine : NSObject
|
||||
{
|
||||
// IVAR required by DTRichTextEditor, used in category
|
||||
NSInteger _stringLocationOffset; // offset to modify internal string location to get actual location
|
||||
}
|
||||
|
||||
/**
|
||||
@name Creating Layout Lines
|
||||
*/
|
||||
|
||||
/**
|
||||
Creates a layout line from a given `CTLine`
|
||||
@param line The Core Text line to wrap
|
||||
@returns A prepared layout line
|
||||
*/
|
||||
- (id)initWithLine:(CTLineRef)line;
|
||||
|
||||
/**
|
||||
Creates a layout line from a given `CTLine`
|
||||
@param line The Core Text line to wrap
|
||||
@param stringLocationOffset Offset to modify internal string location to get actual location
|
||||
@returns A prepared layout line
|
||||
*/
|
||||
|
||||
- (id)initWithLine:(CTLineRef)line stringLocationOffset:(NSInteger)stringLocationOffset;
|
||||
|
||||
/**
|
||||
@name Drawing Layout Lines
|
||||
*/
|
||||
|
||||
/**
|
||||
Draws the receiver in a given graphics context
|
||||
@param context The graphics context to draw into
|
||||
*/
|
||||
- (void)drawInContext:(CGContextRef)context;
|
||||
|
||||
/**
|
||||
Creates a `CGPath` containing the shapes of all glyphs in the line
|
||||
*/
|
||||
- (CGPathRef)newPathWithGlyphs;
|
||||
|
||||
/**
|
||||
@name Getting Information about Layout Lines
|
||||
*/
|
||||
|
||||
/**
|
||||
The range in the original string that is represented by the receiver
|
||||
@returns The string strange
|
||||
*/
|
||||
- (NSRange)stringRange;
|
||||
|
||||
/**
|
||||
The number of glyphs the receiver consists of
|
||||
@returns the number of glyphs
|
||||
*/
|
||||
- (NSInteger)numberOfGlyphs;
|
||||
|
||||
/**
|
||||
Determines the frame of a specific glyph
|
||||
@param index The index of the glyph
|
||||
@return The frame of the glyph
|
||||
*/
|
||||
- (CGRect)frameOfGlyphAtIndex:(NSInteger)index;
|
||||
|
||||
/**
|
||||
Retrieves the glyphRuns with a given range
|
||||
@param range The range
|
||||
@returns An array of glyph runs
|
||||
*/
|
||||
- (NSArray *)glyphRunsWithRange:(NSRange)range;
|
||||
|
||||
/**
|
||||
The frame of a number of glyphs with a given range
|
||||
@param range The range
|
||||
@returns The rectangle containing the result
|
||||
*/
|
||||
- (CGRect)frameOfGlyphsWithRange:(NSRange)range;
|
||||
|
||||
/**
|
||||
The bounds of an image encompassing the entire run.
|
||||
@param context The graphics context used for the measurement
|
||||
@returns The rectangle containing the result
|
||||
*/
|
||||
- (CGRect)imageBoundsInContext:(CGContextRef)context;
|
||||
|
||||
/**
|
||||
The string indices of the receiver
|
||||
@returns An array of string indices
|
||||
*/
|
||||
- (NSArray *)stringIndices;
|
||||
|
||||
/**
|
||||
Determines the graphical offset for a given string index
|
||||
@param index The string index
|
||||
@returns The offset
|
||||
*/
|
||||
- (CGFloat)offsetForStringIndex:(NSInteger)index;
|
||||
|
||||
/**
|
||||
Determines the string index that is closest to a given point
|
||||
@param position The position to determine the string index for
|
||||
@returns The string index
|
||||
*/
|
||||
- (NSInteger)stringIndexForPosition:(CGPoint)position;
|
||||
|
||||
/**
|
||||
The frame of the receiver relative to the layout frame
|
||||
*/
|
||||
@property (nonatomic, assign) CGRect frame;
|
||||
|
||||
/**
|
||||
The glyph runs that the line contains.
|
||||
*/
|
||||
@property (nonatomic, readonly) NSArray *glyphRuns;
|
||||
|
||||
/**
|
||||
The ascent (height above the baseline) of the receiver
|
||||
*/
|
||||
@property (nonatomic, assign) CGFloat ascent; // needs to be modifiable
|
||||
|
||||
/**
|
||||
The descent (height below the baseline) of the receiver
|
||||
*/
|
||||
@property (nonatomic, readonly) CGFloat descent;
|
||||
|
||||
/**
|
||||
The leading (additional space above the ascent) of the receiver
|
||||
*/
|
||||
@property (nonatomic, readonly) CGFloat leading;
|
||||
|
||||
/**
|
||||
The width of the trailing whitespace of the receiver
|
||||
*/
|
||||
@property (nonatomic, readonly) CGFloat trailingWhitespaceWidth;
|
||||
|
||||
/**
|
||||
The offset for the underline in positive points measured from the baseline. This is the maximum underline value of the fonts of all glyph runs of the receiver.
|
||||
*/
|
||||
@property (nonatomic, readonly) CGFloat underlineOffset;
|
||||
|
||||
/**
|
||||
The line height of the line. This is determined by getting the maximum font size of all glyph runs of the receiver.
|
||||
*/
|
||||
@property (nonatomic, readonly) CGFloat lineHeight;
|
||||
|
||||
/**
|
||||
The paragraph style of the paragraph this line belongs to. All lines in a paragraph are supposed to have the same paragraph style, so this takes the paragraph style of the first glyph run
|
||||
*/
|
||||
@property (nonatomic, readonly) DTCoreTextParagraphStyle *paragraphStyle;
|
||||
|
||||
/**
|
||||
The text blocks that the receiver belongs to.
|
||||
*/
|
||||
@property (nonatomic, readonly) NSArray *textBlocks;
|
||||
|
||||
/**
|
||||
The text attachments occurring in glyph runs of the receiver.
|
||||
*/
|
||||
@property (nonatomic, readonly) NSArray *attachments;
|
||||
|
||||
/**
|
||||
The baseline origin of the receiver
|
||||
*/
|
||||
@property (nonatomic, assign) CGPoint baselineOrigin;
|
||||
|
||||
/**
|
||||
`YES` if the writing direction is Right-to-Left, otherwise `NO`
|
||||
*/
|
||||
@property (nonatomic, assign) BOOL writingDirectionIsRightToLeft;
|
||||
|
||||
/**
|
||||
The offset to modify internal string location to get actual location
|
||||
*/
|
||||
|
||||
@property (nonatomic, readonly) NSInteger stringLocationOffset;
|
||||
|
||||
/**
|
||||
Method to efficiently determine if the receiver is a horizontal rule.
|
||||
|
||||
Note: This is used to shortcut drawing of text lines and to allow a horizontal rule line have an "endlessly wide" width so that it gets picked up by [DTCoreTextLayoutFrame linesVisibleInRect:].
|
||||
*/
|
||||
- (BOOL)isHorizontalRule;
|
||||
|
||||
|
||||
/**
|
||||
@name Creating Variants
|
||||
*/
|
||||
|
||||
/**
|
||||
Creates a version of the receiver that is justified to the given width.
|
||||
|
||||
@param justificationFactor Full or partial justification. When set to `1.0` or greater, full justification is performed. If this parameter is set to less than `1.0`, varying degrees of partial justification are performed. If it is set to `0` or less, no justification is performed.
|
||||
@param justificationWidth The width to which the resultant line is justified. If justificationWidth is less than the actual width of the line, then negative justification is performed (that is, glyphs are squeezed together).
|
||||
*/
|
||||
- (DTCoreTextLayoutLine *)justifiedLineWithFactor:(CGFloat)justificationFactor justificationWidth:(CGFloat)justificationWidth;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,571 @@
|
||||
//
|
||||
// DTCoreTextLayoutLine.m
|
||||
// DTCoreText
|
||||
//
|
||||
// Created by Oliver Drobnik on 1/24/11.
|
||||
// Copyright 2011 Drobnik.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import "DTCompatibility.h"
|
||||
#import "DTCoreTextLayoutLine.h"
|
||||
#import "DTCoreTextGlyphRun.h"
|
||||
#import "DTCoreTextLayoutFrame.h"
|
||||
#import "DTCoreTextLayouter.h"
|
||||
#import "DTTextAttachment.h"
|
||||
#import "NSDictionary+DTCoreText.h"
|
||||
#import "DTTextBlock.h"
|
||||
#import "DTCoreTextConstants.h"
|
||||
#import "DTCoreTextFunctions.h"
|
||||
|
||||
@interface DTCoreTextLayoutLine ()
|
||||
|
||||
@property (nonatomic, strong) NSArray *glyphRuns;
|
||||
|
||||
@end
|
||||
|
||||
@implementation DTCoreTextLayoutLine
|
||||
{
|
||||
CGRect _frame;
|
||||
CTLineRef _line;
|
||||
|
||||
CGPoint _baselineOrigin;
|
||||
|
||||
CGFloat _ascent;
|
||||
CGFloat _descent;
|
||||
CGFloat _leading;
|
||||
CGFloat _width;
|
||||
CGFloat _trailingWhitespaceWidth;
|
||||
|
||||
CGFloat _underlineOffset;
|
||||
CGFloat _lineHeight;
|
||||
|
||||
NSArray *_glyphRuns;
|
||||
|
||||
BOOL _didCalculateMetrics;
|
||||
|
||||
BOOL _writingDirectionIsRightToLeft;
|
||||
BOOL _needsToDetectWritingDirection;
|
||||
|
||||
BOOL _hasScannedGlyphRunsForValues;
|
||||
}
|
||||
|
||||
- (id)initWithLine:(CTLineRef)line
|
||||
{
|
||||
return [self initWithLine:line stringLocationOffset:0];
|
||||
}
|
||||
|
||||
- (id)initWithLine:(CTLineRef)line stringLocationOffset:(NSInteger)stringLocationOffset
|
||||
{
|
||||
if (!line)
|
||||
{
|
||||
return nil;
|
||||
}
|
||||
|
||||
if ((self = [super init]))
|
||||
{
|
||||
_line = line;
|
||||
CFRetain(_line);
|
||||
|
||||
// writing direction
|
||||
_needsToDetectWritingDirection = YES;
|
||||
|
||||
_stringLocationOffset = stringLocationOffset;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
CFRelease(_line);
|
||||
}
|
||||
|
||||
#ifndef COVERAGE
|
||||
// exclude method from coverage testing
|
||||
|
||||
- (NSString *)description
|
||||
{
|
||||
return [NSString stringWithFormat:@"<%@ origin=%@ frame=%@ range=%@", [self class], NSStringFromCGPoint(_baselineOrigin), NSStringFromCGRect(self.frame), NSStringFromRange([self stringRange])];
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
- (NSRange)stringRange
|
||||
{
|
||||
CFRange range = CTLineGetStringRange(_line);
|
||||
|
||||
// add offset if there is one, i.e. from merged lines
|
||||
range.location += _stringLocationOffset;
|
||||
|
||||
return NSMakeRange(range.location, range.length);
|
||||
}
|
||||
|
||||
- (NSInteger)numberOfGlyphs
|
||||
{
|
||||
NSInteger ret = 0;
|
||||
for (DTCoreTextGlyphRun *oneRun in self.glyphRuns)
|
||||
{
|
||||
ret += [oneRun numberOfGlyphs];
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
#pragma mark - Drawing
|
||||
|
||||
- (void)drawInContext:(CGContextRef)context
|
||||
{
|
||||
CTLineDraw(_line, context);
|
||||
}
|
||||
|
||||
- (CGPathRef)newPathWithGlyphs
|
||||
{
|
||||
// mutable path for the line
|
||||
CGMutablePathRef mutablePath = CGPathCreateMutable();
|
||||
|
||||
for (DTCoreTextGlyphRun *oneRun in self.glyphRuns)
|
||||
{
|
||||
CGPathRef glyphPath = [oneRun newPathWithGlyphs];
|
||||
|
||||
CGAffineTransform posTransform = CGAffineTransformMakeTranslation(_baselineOrigin.x, _baselineOrigin.y);
|
||||
CGPathAddPath(mutablePath, &posTransform, glyphPath);
|
||||
|
||||
CGPathRelease(glyphPath);
|
||||
}
|
||||
|
||||
return mutablePath;
|
||||
}
|
||||
|
||||
#pragma mark - Creating Variants
|
||||
|
||||
- (DTCoreTextLayoutLine *)justifiedLineWithFactor:(CGFloat)justificationFactor justificationWidth:(CGFloat)justificationWidth
|
||||
{
|
||||
// make this line justified
|
||||
CTLineRef justifiedLine = CTLineCreateJustifiedLine(_line, justificationFactor, justificationWidth);
|
||||
|
||||
DTCoreTextLayoutLine *newLine = [[DTCoreTextLayoutLine alloc] initWithLine:justifiedLine];
|
||||
|
||||
CFRelease(justifiedLine);
|
||||
|
||||
return newLine;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Calculations
|
||||
- (NSArray *)stringIndices
|
||||
{
|
||||
NSMutableArray *array = [NSMutableArray array];
|
||||
for (DTCoreTextGlyphRun *oneRun in self.glyphRuns)
|
||||
{
|
||||
[array addObjectsFromArray:[oneRun stringIndices]];
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
- (CGRect)frameOfGlyphAtIndex:(NSInteger)index
|
||||
{
|
||||
for (DTCoreTextGlyphRun *oneRun in self.glyphRuns)
|
||||
{
|
||||
NSInteger count = [oneRun numberOfGlyphs];
|
||||
if (index >= count)
|
||||
{
|
||||
index -= count;
|
||||
}
|
||||
else
|
||||
{
|
||||
return [oneRun frameOfGlyphAtIndex:index];
|
||||
}
|
||||
}
|
||||
|
||||
return CGRectZero;
|
||||
}
|
||||
|
||||
- (NSArray *)glyphRunsWithRange:(NSRange)range
|
||||
{
|
||||
NSMutableArray *tmpArray = [NSMutableArray arrayWithCapacity:[self numberOfGlyphs]];
|
||||
|
||||
for (DTCoreTextGlyphRun *oneRun in self.glyphRuns)
|
||||
{
|
||||
NSRange runRange = [oneRun stringRange];
|
||||
|
||||
// intersect these ranges
|
||||
NSRange intersectionRange = NSIntersectionRange(range, runRange);
|
||||
|
||||
// if intersection is longer than zero length they intersect
|
||||
if (intersectionRange.length)
|
||||
{
|
||||
[tmpArray addObject:oneRun];
|
||||
}
|
||||
}
|
||||
|
||||
return tmpArray;
|
||||
}
|
||||
|
||||
- (CGRect)frameOfGlyphsWithRange:(NSRange)range
|
||||
{
|
||||
NSArray *glyphRuns = [self glyphRunsWithRange:range];
|
||||
|
||||
CGRect tmpRect = CGRectMake(CGFLOAT_MAX, CGFLOAT_MAX, 0, 0);
|
||||
|
||||
for (DTCoreTextGlyphRun *oneRun in glyphRuns)
|
||||
{
|
||||
CGRect glyphFrame = oneRun.frame;
|
||||
|
||||
if (glyphFrame.origin.x < tmpRect.origin.x)
|
||||
{
|
||||
tmpRect.origin.x = glyphFrame.origin.x;
|
||||
}
|
||||
|
||||
if (glyphFrame.origin.y < tmpRect.origin.y)
|
||||
{
|
||||
tmpRect.origin.y = glyphFrame.origin.y;
|
||||
}
|
||||
|
||||
if (glyphFrame.size.height > tmpRect.size.height)
|
||||
{
|
||||
tmpRect.size.height = glyphFrame.size.height;
|
||||
}
|
||||
|
||||
tmpRect.size.width = glyphFrame.origin.x + glyphFrame.size.width - tmpRect.origin.x;
|
||||
}
|
||||
|
||||
CGFloat maxX = CGRectGetMaxX(self.frame) - _trailingWhitespaceWidth;
|
||||
if (CGRectGetMaxX(tmpRect) > maxX)
|
||||
{
|
||||
tmpRect.size.width = maxX - tmpRect.origin.x;
|
||||
}
|
||||
|
||||
return tmpRect;
|
||||
}
|
||||
|
||||
// bounds of an image encompassing the entire run
|
||||
- (CGRect)imageBoundsInContext:(CGContextRef)context
|
||||
{
|
||||
return CTLineGetImageBounds(_line, context);
|
||||
}
|
||||
|
||||
- (CGFloat)offsetForStringIndex:(NSInteger)index
|
||||
{
|
||||
// subtract offset if there is one, i.e. from merged lines
|
||||
index -= _stringLocationOffset;
|
||||
|
||||
return CTLineGetOffsetForStringIndex(_line, index, NULL);
|
||||
}
|
||||
|
||||
- (NSInteger)stringIndexForPosition:(CGPoint)position
|
||||
{
|
||||
// position is in same coordinate system as frame
|
||||
CGPoint adjustedPosition = position;
|
||||
CGRect frame = self.frame;
|
||||
adjustedPosition.x -= frame.origin.x;
|
||||
adjustedPosition.y -= frame.origin.y;
|
||||
|
||||
NSInteger index = CTLineGetStringIndexForPosition(_line, adjustedPosition);
|
||||
|
||||
// add offset if there is one, i.e. from merged lines
|
||||
index += _stringLocationOffset;
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
- (void)_calculateMetrics
|
||||
{
|
||||
@synchronized(self)
|
||||
{
|
||||
if (!_didCalculateMetrics)
|
||||
{
|
||||
_width = (CGFloat)CTLineGetTypographicBounds(_line, &_ascent, &_descent, &_leading);
|
||||
_trailingWhitespaceWidth = (CGFloat)CTLineGetTrailingWhitespaceWidth(_line);
|
||||
|
||||
_didCalculateMetrics = YES;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)isHorizontalRule
|
||||
{
|
||||
// HR is only a single \n
|
||||
|
||||
if (self.stringRange.length>1)
|
||||
{
|
||||
return NO;
|
||||
}
|
||||
|
||||
NSArray *runs = self.glyphRuns;
|
||||
|
||||
// thus only a single glyphRun
|
||||
|
||||
if ([runs count]>1)
|
||||
{
|
||||
return NO;
|
||||
}
|
||||
|
||||
DTCoreTextGlyphRun *singleRun = [runs lastObject];
|
||||
|
||||
if ([singleRun.attributes objectForKey:DTHorizontalRuleStyleAttribute])
|
||||
{
|
||||
return YES;
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
#pragma mark Determining Values from the glyph runs
|
||||
|
||||
- (void)_scanGlyphRunsForValues
|
||||
{
|
||||
@synchronized(self)
|
||||
{
|
||||
CGFloat maxOffset = 0;
|
||||
CGFloat maxFontSize = 0;
|
||||
|
||||
for (DTCoreTextGlyphRun *oneRun in self.glyphRuns)
|
||||
{
|
||||
CTFontRef usedFont = (__bridge CTFontRef)([oneRun.attributes objectForKey:(id)kCTFontAttributeName]);
|
||||
|
||||
if (usedFont)
|
||||
{
|
||||
maxOffset = MAX(maxOffset, fabs(CTFontGetUnderlinePosition(usedFont)));
|
||||
|
||||
maxFontSize = MAX(maxFontSize, CTFontGetSize(usedFont));
|
||||
}
|
||||
}
|
||||
|
||||
_underlineOffset = maxOffset;
|
||||
_lineHeight = maxFontSize;
|
||||
|
||||
_hasScannedGlyphRunsForValues= YES;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Properties
|
||||
- (NSArray *)glyphRuns
|
||||
{
|
||||
@synchronized(self)
|
||||
{
|
||||
if (!_glyphRuns)
|
||||
{
|
||||
// run array is owned by line
|
||||
CFArrayRef runs = CTLineGetGlyphRuns(_line);
|
||||
CFIndex runCount = CFArrayGetCount(runs);
|
||||
|
||||
if (runCount)
|
||||
{
|
||||
NSMutableArray *tmpArray = [[NSMutableArray alloc] initWithCapacity:runCount];
|
||||
|
||||
for (CFIndex i=0; i<runCount; i++)
|
||||
{
|
||||
CTRunRef oneRun = CFArrayGetValueAtIndex(runs, i);
|
||||
|
||||
CGPoint *positions = (CGPoint*)CTRunGetPositionsPtr(oneRun);
|
||||
|
||||
BOOL shouldFreePositions = NO;
|
||||
|
||||
if (positions == NULL) // Ptr gave NULL, we'll need to copy positions array and later free it
|
||||
{
|
||||
CFIndex glyphCount = CTRunGetGlyphCount(oneRun);
|
||||
|
||||
shouldFreePositions = YES;
|
||||
|
||||
size_t positionsBufferSize = sizeof(CGPoint) * glyphCount;
|
||||
CGPoint *positionsBuffer = malloc(positionsBufferSize);
|
||||
CTRunGetPositions(oneRun, CFRangeMake(0, 0), positionsBuffer);
|
||||
positions = positionsBuffer;
|
||||
}
|
||||
|
||||
// assumption: position of first glyph is also the correct offset of the entire run
|
||||
CGPoint position = positions[0];
|
||||
|
||||
DTCoreTextGlyphRun *glyphRun = [[DTCoreTextGlyphRun alloc] initWithRun:oneRun layoutLine:self offset:position.x];
|
||||
[tmpArray addObject:glyphRun];
|
||||
|
||||
if ( shouldFreePositions )
|
||||
{
|
||||
free(positions);
|
||||
}
|
||||
}
|
||||
|
||||
_glyphRuns = tmpArray;
|
||||
}
|
||||
}
|
||||
|
||||
return _glyphRuns;
|
||||
}
|
||||
}
|
||||
|
||||
- (CGRect)frame
|
||||
{
|
||||
if (!_didCalculateMetrics)
|
||||
{
|
||||
[self _calculateMetrics];
|
||||
}
|
||||
|
||||
CGRect frame = CGRectMake(_baselineOrigin.x, _baselineOrigin.y - _ascent, _width, _ascent + _descent);
|
||||
|
||||
// make sure that HR are extremely wide to be be picked up
|
||||
if ([self isHorizontalRule])
|
||||
{
|
||||
frame.size.width = CGFLOAT_MAX;
|
||||
}
|
||||
|
||||
return frame;
|
||||
}
|
||||
|
||||
- (CGFloat)width
|
||||
{
|
||||
if (!_didCalculateMetrics)
|
||||
{
|
||||
[self _calculateMetrics];
|
||||
}
|
||||
|
||||
return _width;
|
||||
}
|
||||
|
||||
- (NSArray *)attachments
|
||||
{
|
||||
NSMutableArray *tmpArray = [NSMutableArray array];
|
||||
|
||||
for (DTCoreTextGlyphRun *oneRun in self.glyphRuns)
|
||||
{
|
||||
DTTextAttachment *attachment = oneRun.attachment;
|
||||
|
||||
if (attachment)
|
||||
{
|
||||
[tmpArray addObject:attachment];
|
||||
}
|
||||
}
|
||||
|
||||
if ([tmpArray count])
|
||||
{
|
||||
return tmpArray;
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
|
||||
- (CGFloat)ascent
|
||||
{
|
||||
if (!_didCalculateMetrics)
|
||||
{
|
||||
[self _calculateMetrics];
|
||||
}
|
||||
|
||||
return _ascent;
|
||||
}
|
||||
|
||||
- (void)setAscent:(CGFloat)ascent
|
||||
{
|
||||
// need to get metrics because otherwise ascent gets overwritten
|
||||
if (!_didCalculateMetrics)
|
||||
{
|
||||
[self _calculateMetrics];
|
||||
}
|
||||
|
||||
_ascent = ascent;
|
||||
}
|
||||
|
||||
|
||||
- (CGFloat)descent
|
||||
{
|
||||
if (!_didCalculateMetrics)
|
||||
{
|
||||
[self _calculateMetrics];
|
||||
}
|
||||
|
||||
return _descent;
|
||||
}
|
||||
|
||||
- (CGFloat)leading
|
||||
{
|
||||
if (!_didCalculateMetrics)
|
||||
{
|
||||
[self _calculateMetrics];
|
||||
}
|
||||
|
||||
return _leading;
|
||||
}
|
||||
|
||||
- (CGFloat)underlineOffset
|
||||
{
|
||||
if (!_hasScannedGlyphRunsForValues)
|
||||
{
|
||||
[self _scanGlyphRunsForValues];
|
||||
}
|
||||
|
||||
return _underlineOffset;
|
||||
}
|
||||
|
||||
- (CGFloat)lineHeight
|
||||
{
|
||||
if (!_hasScannedGlyphRunsForValues)
|
||||
{
|
||||
[self _scanGlyphRunsForValues];
|
||||
}
|
||||
|
||||
return _lineHeight;
|
||||
}
|
||||
|
||||
- (DTCoreTextParagraphStyle *)paragraphStyle
|
||||
{
|
||||
// get paragraph style from any glyph
|
||||
DTCoreTextGlyphRun *lastRun = [self.glyphRuns lastObject];
|
||||
NSDictionary *attributes = lastRun.attributes;
|
||||
|
||||
return [attributes paragraphStyle];
|
||||
}
|
||||
|
||||
- (NSArray *)textBlocks
|
||||
{
|
||||
// get text blocks from any glyph
|
||||
DTCoreTextGlyphRun *lastRun = [self.glyphRuns lastObject];
|
||||
NSDictionary *attributes = lastRun.attributes;
|
||||
|
||||
return [attributes objectForKey:DTTextBlocksAttribute];
|
||||
}
|
||||
|
||||
- (CGFloat)trailingWhitespaceWidth
|
||||
{
|
||||
if (!_didCalculateMetrics)
|
||||
{
|
||||
[self _calculateMetrics];
|
||||
}
|
||||
|
||||
return _trailingWhitespaceWidth;
|
||||
}
|
||||
|
||||
- (BOOL)writingDirectionIsRightToLeft
|
||||
{
|
||||
if (_needsToDetectWritingDirection)
|
||||
{
|
||||
if ([self.glyphRuns count])
|
||||
{
|
||||
DTCoreTextGlyphRun *firstRun = [self.glyphRuns objectAtIndex:0];
|
||||
|
||||
_writingDirectionIsRightToLeft = [firstRun writingDirectionIsRightToLeft];
|
||||
}
|
||||
}
|
||||
|
||||
return _writingDirectionIsRightToLeft;
|
||||
}
|
||||
|
||||
- (void)setWritingDirectionIsRightToLeft:(BOOL)writingDirectionIsRightToLeft
|
||||
{
|
||||
_writingDirectionIsRightToLeft = writingDirectionIsRightToLeft;
|
||||
_needsToDetectWritingDirection = NO;
|
||||
}
|
||||
|
||||
@synthesize frame =_frame;
|
||||
@synthesize glyphRuns = _glyphRuns;
|
||||
|
||||
@synthesize ascent = _ascent;
|
||||
@synthesize descent = _descent;
|
||||
@synthesize leading = _leading;
|
||||
@synthesize trailingWhitespaceWidth = _trailingWhitespaceWidth;
|
||||
|
||||
@synthesize baselineOrigin = _baselineOrigin;
|
||||
@synthesize writingDirectionIsRightToLeft = _writingDirectionIsRightToLeft;
|
||||
|
||||
@synthesize stringLocationOffset = _stringLocationOffset;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,69 @@
|
||||
//
|
||||
// DTCoreTextLayouter.h
|
||||
// DTCoreText
|
||||
//
|
||||
// Created by Oliver Drobnik on 1/24/11.
|
||||
// Copyright 2011 Drobnik.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import "DTCompatibility.h"
|
||||
|
||||
#if TARGET_OS_IPHONE
|
||||
#import <CoreText/CoreText.h>
|
||||
#elif TARGET_OS_MAC
|
||||
#import <ApplicationServices/ApplicationServices.h>
|
||||
#endif
|
||||
|
||||
#import "DTCoreTextLayoutFrame.h"
|
||||
#import "DTCoreTextLayoutLine.h"
|
||||
#import "DTCoreTextGlyphRun.h"
|
||||
|
||||
/**
|
||||
This class owns an attributed string and is able to create layoutFrames for certain ranges in this string. Optionally it caches these layout frames.
|
||||
*/
|
||||
@interface DTCoreTextLayouter : NSObject
|
||||
|
||||
/**
|
||||
@name Creating a Layouter
|
||||
*/
|
||||
|
||||
/**
|
||||
Designated Initializer. Creates a new Layouter with an attributed string
|
||||
@param attributedString The `NSAttributedString` to layout for
|
||||
@returns An initialized layouter
|
||||
*/
|
||||
- (id)initWithAttributedString:(NSAttributedString *)attributedString;
|
||||
|
||||
|
||||
/**
|
||||
@name Creating Layout Frames
|
||||
*/
|
||||
|
||||
/**
|
||||
Creates a layout frame with a given rectangle and string range. The layouter fills the layout frame with as many lines as fit. You can query [DTCoreTextLayoutFrame visibleStringRange] for the range the fits and create another layout frame that continues the text from there to create multiple pages, for example for an e-book.
|
||||
@param frame The rectangle to fill with text
|
||||
@param range The string range to fill, pass {0,0} for the entire string (as much as fits)
|
||||
*/
|
||||
- (DTCoreTextLayoutFrame *)layoutFrameWithRect:(CGRect)frame range:(NSRange)range;
|
||||
|
||||
/**
|
||||
If set to `YES` then the receiver will cache layout frames generated with layoutFrameWithRect:range: for a given rect
|
||||
*/
|
||||
@property (nonatomic, assign) BOOL shouldCacheLayoutFrames;
|
||||
|
||||
|
||||
/**
|
||||
@name Getting Information
|
||||
*/
|
||||
|
||||
/**
|
||||
The attributed string that the layouter currently owns
|
||||
*/
|
||||
@property (nonatomic, strong) NSAttributedString *attributedString;
|
||||
|
||||
/**
|
||||
The internal framesetter of the receiver
|
||||
*/
|
||||
@property (nonatomic, readonly) CTFramesetterRef framesetter;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,153 @@
|
||||
//
|
||||
// DTCoreTextLayouter.m
|
||||
// DTCoreText
|
||||
//
|
||||
// Created by Oliver Drobnik on 1/24/11.
|
||||
// Copyright 2011 Drobnik.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import "DTCompatibility.h"
|
||||
#import "DTCoreTextLayouter.h"
|
||||
|
||||
@interface DTCoreTextLayouter ()
|
||||
|
||||
@property (nonatomic, strong) NSMutableArray *frames;
|
||||
|
||||
- (void)_discardFramesetter;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation DTCoreTextLayouter
|
||||
{
|
||||
CTFramesetterRef _framesetter;
|
||||
NSAttributedString *_attributedString;
|
||||
BOOL _shouldCacheLayoutFrames;
|
||||
NSCache *_layoutFrameCache;
|
||||
}
|
||||
|
||||
- (id)initWithAttributedString:(NSAttributedString *)attributedString
|
||||
{
|
||||
if ((self = [super init]))
|
||||
{
|
||||
if (!attributedString)
|
||||
{
|
||||
return nil;
|
||||
}
|
||||
|
||||
self.attributedString = attributedString;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
[self _discardFramesetter];
|
||||
}
|
||||
|
||||
- (DTCoreTextLayoutFrame *)layoutFrameWithRect:(CGRect)frame range:(NSRange)range
|
||||
{
|
||||
DTCoreTextLayoutFrame *newFrame = nil;
|
||||
NSString *cacheKey = nil;
|
||||
|
||||
// need to have a non zero
|
||||
if (!(frame.size.width > 0 && frame.size.height > 0))
|
||||
{
|
||||
return nil;
|
||||
}
|
||||
|
||||
if (_shouldCacheLayoutFrames)
|
||||
{
|
||||
cacheKey = [NSString stringWithFormat:@"%lud-%@-%@", (unsigned long)[_attributedString hash], NSStringFromCGRect(frame), NSStringFromRange(range)];
|
||||
|
||||
DTCoreTextLayoutFrame *cachedLayoutFrame = [_layoutFrameCache objectForKey:cacheKey];
|
||||
|
||||
if (cachedLayoutFrame)
|
||||
{
|
||||
return cachedLayoutFrame;
|
||||
}
|
||||
}
|
||||
|
||||
@autoreleasepool
|
||||
{
|
||||
newFrame = [[DTCoreTextLayoutFrame alloc] initWithFrame:frame layouter:self range:range];
|
||||
};
|
||||
|
||||
if (newFrame && _shouldCacheLayoutFrames)
|
||||
{
|
||||
[_layoutFrameCache setObject:newFrame forKey:cacheKey];
|
||||
}
|
||||
|
||||
return newFrame;
|
||||
}
|
||||
|
||||
- (void)_discardFramesetter
|
||||
{
|
||||
// framesetter needs to go
|
||||
if (_framesetter)
|
||||
{
|
||||
CFRelease(_framesetter);
|
||||
_framesetter = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark Properties
|
||||
|
||||
- (CTFramesetterRef)framesetter
|
||||
{
|
||||
@synchronized(self)
|
||||
{
|
||||
if (!_framesetter)
|
||||
{
|
||||
_framesetter = CTFramesetterCreateWithAttributedString((__bridge CFAttributedStringRef)self.attributedString);
|
||||
}
|
||||
|
||||
|
||||
return _framesetter;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setAttributedString:(NSAttributedString *)attributedString
|
||||
{
|
||||
@synchronized(self)
|
||||
{
|
||||
if (_attributedString != attributedString)
|
||||
{
|
||||
_attributedString = attributedString;
|
||||
|
||||
[self _discardFramesetter];
|
||||
|
||||
// clear the cache
|
||||
[_layoutFrameCache removeAllObjects];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (NSAttributedString *)attributedString
|
||||
{
|
||||
return _attributedString;
|
||||
}
|
||||
|
||||
- (void)setShouldCacheLayoutFrames:(BOOL)shouldCacheLayoutFrames
|
||||
{
|
||||
if (_shouldCacheLayoutFrames != shouldCacheLayoutFrames)
|
||||
{
|
||||
_shouldCacheLayoutFrames = shouldCacheLayoutFrames;
|
||||
|
||||
if (shouldCacheLayoutFrames)
|
||||
{
|
||||
_layoutFrameCache = [[NSCache alloc] init];
|
||||
}
|
||||
else
|
||||
{
|
||||
_layoutFrameCache = nil;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@synthesize attributedString = _attributedString;
|
||||
@synthesize framesetter = _framesetter;
|
||||
@synthesize shouldCacheLayoutFrames = _shouldCacheLayoutFrames;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// DTCoreTextMacros.h
|
||||
// DTCoreText
|
||||
//
|
||||
// Created by Jean-Charles BERTIN on 5/28/14.
|
||||
// Copyright (c) 2014 Axinoe. All rights reserved.
|
||||
//
|
||||
|
||||
#import "DTCompatibility.h"
|
||||
|
||||
#ifndef DT_RETURNS_INNER_POINTER
|
||||
#if __has_attribute(objc_returns_inner_pointer)
|
||||
#define DT_RETURNS_INNER_POINTER __attribute__((objc_returns_inner_pointer))
|
||||
#else
|
||||
#define DT_RETURNS_INNER_POINTER
|
||||
#endif
|
||||
#endif
|
||||
@@ -0,0 +1,212 @@
|
||||
//
|
||||
// DTCoreTextParagraphStyle.h
|
||||
// DTCoreText
|
||||
//
|
||||
// Created by Oliver Drobnik on 4/14/11.
|
||||
// Copyright 2011 Drobnik.com. All rights reserved.
|
||||
//
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <CoreText/CoreText.h>
|
||||
|
||||
/**
|
||||
`DTCoreTextParagraphStyle` encapsulates the paragraph or ruler attributes used by the NSAttributedString classes on iOS. It is a replacement for `NSParagraphStyle` which is not implemented on iOS.
|
||||
|
||||
Since `NSAttributedString` instances use CTParagraphStyle object there are methods to bridge from and to these. Because of this distinction there is no need for a mutable variant of this class.
|
||||
*/
|
||||
@interface DTCoreTextParagraphStyle : NSObject <NSCopying>
|
||||
|
||||
/**
|
||||
@name Creating a DTCoreTextParagraphStyle
|
||||
*/
|
||||
|
||||
/**
|
||||
Returns the default paragraph style.
|
||||
*/
|
||||
+ (DTCoreTextParagraphStyle *)defaultParagraphStyle;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
@name Bridging to and from CTParagraphStyle
|
||||
*/
|
||||
|
||||
/**
|
||||
Create a new paragraph style instance from a `CTParagraphStyle`.
|
||||
|
||||
@param ctParagraphStyle the `CTParagraphStyle` from which to copy this new style's attributes.
|
||||
*/
|
||||
+ (DTCoreTextParagraphStyle *)paragraphStyleWithCTParagraphStyle:(CTParagraphStyleRef)ctParagraphStyle;
|
||||
|
||||
|
||||
/**
|
||||
Create a new paragraph style instance from a `CTParagraphStyle`.
|
||||
|
||||
@param ctParagraphStyle the `CTParagraphStyle` from which to copy this new style's attributes.
|
||||
*/
|
||||
- (id)initWithCTParagraphStyle:(CTParagraphStyleRef)ctParagraphStyle;
|
||||
|
||||
/**
|
||||
Create a new `CTParagraphStyle` from the receiver for use as attribute in `NSAttributedString`
|
||||
|
||||
@returns The `CTParagraphStyle` based on the receiver's attributes.
|
||||
*/
|
||||
- (CTParagraphStyleRef)createCTParagraphStyle;
|
||||
|
||||
/**
|
||||
@name Bridging to and from NSParagraphStyle
|
||||
*/
|
||||
|
||||
#if DTCORETEXT_SUPPORT_NS_ATTRIBUTES
|
||||
/**
|
||||
Create a new paragraph style instance from an `NSParagraphStyle`.
|
||||
|
||||
Note: on iOS no tab stops are supported.
|
||||
@param paragraphStyle the `NSParagraphStyle` from which to copy this new style's attributes.
|
||||
*/
|
||||
+ (DTCoreTextParagraphStyle *)paragraphStyleWithNSParagraphStyle:(NSParagraphStyle *)paragraphStyle;
|
||||
|
||||
/**
|
||||
Create a new `NSParagraphStyle` from the receiver for use as attribute in `NSAttributedString`.
|
||||
|
||||
Note: This method is requires iOS 6 or greater. This does not support tab stops.
|
||||
|
||||
@returns The `NSParagraphStyle` based on the receiver's attributes.
|
||||
*/
|
||||
- (NSParagraphStyle *)NSParagraphStyle;
|
||||
#endif
|
||||
|
||||
|
||||
/**-------------------------------------------------------------------------------------
|
||||
@name Accessing Style Information
|
||||
---------------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
/**
|
||||
The indentation of the first line of the receiver.
|
||||
*/
|
||||
@property (nonatomic, assign) CGFloat firstLineHeadIndent;
|
||||
|
||||
|
||||
/**
|
||||
The document-wide default tab interval.
|
||||
|
||||
The default tab interval in points. Tabs after the last specified in tabStops are placed at integer multiples of this distance (if positive). Default return value is 0.0.
|
||||
*/
|
||||
@property (nonatomic, assign) CGFloat defaultTabInterval;
|
||||
|
||||
|
||||
/**
|
||||
The distance between the paragraph’s top and the beginning of its text content.
|
||||
*/
|
||||
@property (nonatomic, assign) CGFloat paragraphSpacingBefore;
|
||||
|
||||
|
||||
/**
|
||||
The space after the end of the paragraph.
|
||||
*/
|
||||
@property (nonatomic, assign) CGFloat paragraphSpacing;
|
||||
|
||||
|
||||
/**
|
||||
The line height multiple.
|
||||
|
||||
Internally line height multiples get converted into minimum and maximum line height.
|
||||
*/
|
||||
@property (nonatomic, assign) CGFloat lineHeightMultiple;
|
||||
|
||||
|
||||
/**
|
||||
The minimum height in points that any line in the receiver will occupy, regardless of the font size or size of any attached graphic. This value is always nonnegative.
|
||||
*/
|
||||
@property (nonatomic, assign) CGFloat minimumLineHeight;
|
||||
|
||||
|
||||
/**
|
||||
The maximum height in points that any line in the receiver will occupy, regardless of the font size or size of any attached graphic. This value is always nonnegative. The default value is 0.
|
||||
*/
|
||||
@property (nonatomic, assign) CGFloat maximumLineHeight;
|
||||
|
||||
|
||||
/**
|
||||
The distance in points from the margin of a text container to the end of lines.
|
||||
|
||||
@note This value is negative if it is to be measured from the trailing margin, positive if measured from the same margin as the headIndent.
|
||||
*/
|
||||
@property (nonatomic, assign) CGFloat tailIndent;
|
||||
|
||||
/**
|
||||
The distance in points from the leading margin of a text container to the beginning of lines other than the first. This value is always nonnegative.
|
||||
*/
|
||||
@property (nonatomic, assign) CGFloat headIndent;
|
||||
|
||||
/**
|
||||
The text alignment of the receiver.
|
||||
|
||||
Natural text alignment is realized as left or right alignment depending on the line sweep direction of the first script contained in the paragraph.
|
||||
*/
|
||||
@property (nonatomic, assign) CTTextAlignment alignment;
|
||||
|
||||
|
||||
/**
|
||||
The base writing direction for the receiver.
|
||||
|
||||
*/
|
||||
@property (nonatomic, assign) CTWritingDirection baseWritingDirection;
|
||||
|
||||
|
||||
/**-------------------------------------------------------------------------------------
|
||||
@name Setting Tab Stops
|
||||
---------------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
/**
|
||||
The CTTextTab objects, sorted by location, that define the tab stops for the paragraph style.
|
||||
*/
|
||||
@property (nonatomic, copy) NSArray *tabStops;
|
||||
|
||||
|
||||
/**
|
||||
Adds a tab stop to the receiver.
|
||||
|
||||
@param position the tab stop position
|
||||
@param alignment the tab alignment for this tab stop
|
||||
*/
|
||||
- (void)addTabStopAtPosition:(CGFloat)position alignment:(CTTextAlignment)alignment;
|
||||
|
||||
|
||||
/**-------------------------------------------------------------------------------------
|
||||
@name Interacting with CSS
|
||||
---------------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
/**
|
||||
Create a representation suitable for CSS.
|
||||
|
||||
@returns A string with the receiver's style encoded as CSS.
|
||||
*/
|
||||
- (NSString *)cssStyleRepresentation;
|
||||
|
||||
|
||||
/**-------------------------------------------------------------------------------------
|
||||
@name Setting Text Lists
|
||||
---------------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
/**
|
||||
Text lists containing the paragraph, nested from outermost to innermost. Each text list is a DTCSSListStyle object.
|
||||
*/
|
||||
@property (nonatomic, copy) NSArray *textLists;
|
||||
|
||||
|
||||
/**-------------------------------------------------------------------------------------
|
||||
@name Setting Text Blocks
|
||||
---------------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
/**
|
||||
Text blocks containing the paragraph, nested from outermost to innermost, to array. Each text block is a DTTextBlock object.
|
||||
*/
|
||||
@property (nonatomic, copy) NSArray *textBlocks;
|
||||
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,664 @@
|
||||
//
|
||||
// DTCoreTextParagraphStyle.m
|
||||
// DTCoreText
|
||||
//
|
||||
// Created by Oliver Drobnik on 4/14/11.
|
||||
// Copyright 2011 Drobnik.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import "DTCoreTextParagraphStyle.h"
|
||||
#import "DTTextBlock.h"
|
||||
#import "DTCSSListStyle.h"
|
||||
#import <DTFoundation/DTWeakSupport.h>
|
||||
#import "DTCoreTextFunctions.h"
|
||||
|
||||
@implementation DTCoreTextParagraphStyle
|
||||
{
|
||||
CGFloat _firstLineHeadIndent;
|
||||
CGFloat _defaultTabInterval;
|
||||
CGFloat _paragraphSpacingBefore;
|
||||
CGFloat _paragraphSpacing;
|
||||
CGFloat _headIndent;
|
||||
CGFloat _tailIndent;
|
||||
CGFloat _lineHeightMultiple;
|
||||
CGFloat _minimumLineHeight;
|
||||
CGFloat _maximumLineHeight;
|
||||
|
||||
CTTextAlignment _alignment;
|
||||
CTWritingDirection _baseWritingDirection;
|
||||
|
||||
NSMutableArray *_tabStops;
|
||||
}
|
||||
|
||||
+ (DTCoreTextParagraphStyle *)defaultParagraphStyle
|
||||
{
|
||||
return [[DTCoreTextParagraphStyle alloc] init];
|
||||
}
|
||||
|
||||
+ (DTCoreTextParagraphStyle *)paragraphStyleWithCTParagraphStyle:(CTParagraphStyleRef)ctParagraphStyle
|
||||
{
|
||||
return [[DTCoreTextParagraphStyle alloc] initWithCTParagraphStyle:ctParagraphStyle];
|
||||
}
|
||||
|
||||
#if DTCORETEXT_SUPPORT_NS_ATTRIBUTES
|
||||
+ (DTCoreTextParagraphStyle *)paragraphStyleWithNSParagraphStyle:(NSParagraphStyle *)paragraphStyle
|
||||
{
|
||||
if ( ! paragraphStyle) {
|
||||
paragraphStyle = [NSParagraphStyle defaultParagraphStyle];
|
||||
}
|
||||
|
||||
DTCoreTextParagraphStyle *retStyle = [[DTCoreTextParagraphStyle alloc] init];
|
||||
|
||||
retStyle.firstLineHeadIndent = paragraphStyle.firstLineHeadIndent;
|
||||
retStyle.headIndent = paragraphStyle.headIndent;
|
||||
|
||||
retStyle.paragraphSpacing = paragraphStyle.paragraphSpacing;
|
||||
retStyle.paragraphSpacingBefore = paragraphStyle.paragraphSpacingBefore;
|
||||
|
||||
retStyle.lineHeightMultiple = paragraphStyle.lineHeightMultiple;
|
||||
retStyle.minimumLineHeight = paragraphStyle.minimumLineHeight;
|
||||
retStyle.maximumLineHeight = paragraphStyle.maximumLineHeight;
|
||||
|
||||
retStyle.alignment = DTNSTextAlignmentToCTTextAlignment(paragraphStyle.alignment);
|
||||
|
||||
switch (paragraphStyle.baseWritingDirection)
|
||||
{
|
||||
case NSWritingDirectionNatural:
|
||||
{
|
||||
retStyle.baseWritingDirection = kCTWritingDirectionNatural;
|
||||
break;
|
||||
}
|
||||
|
||||
case NSWritingDirectionLeftToRight:
|
||||
{
|
||||
retStyle.baseWritingDirection = kCTWritingDirectionLeftToRight;
|
||||
break;
|
||||
}
|
||||
case NSWritingDirectionRightToLeft:
|
||||
{
|
||||
retStyle.baseWritingDirection = kCTWritingDirectionRightToLeft;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
#if DTCORETEXT_SUPPORT_NSPARAGRAPHSTYLE_TABS
|
||||
if (NSClassFromString(@"NSTextTab") && [NSParagraphStyle instancesRespondToSelector:@selector(tabStops)])
|
||||
{
|
||||
NSArray *tabStops = [paragraphStyle valueForKey:@"tabStops"];
|
||||
|
||||
NSMutableArray *tmpArray = [NSMutableArray array];
|
||||
|
||||
for (NSTextTab *textTab in tabStops)
|
||||
{
|
||||
CTTextAlignment alignment = DTNSTextAlignmentToCTTextAlignment(textTab.alignment);
|
||||
CGFloat location = textTab.location;
|
||||
|
||||
CTTextTabRef tab = CTTextTabCreate(alignment, location, NULL);
|
||||
|
||||
if (tab)
|
||||
{
|
||||
[tmpArray addObject:(__bridge id)(tab)];
|
||||
CFRelease(tab);
|
||||
}
|
||||
}
|
||||
|
||||
if ([tmpArray count])
|
||||
{
|
||||
retStyle.tabStops = tmpArray;
|
||||
}
|
||||
}
|
||||
|
||||
retStyle.defaultTabInterval = paragraphStyle.defaultTabInterval;
|
||||
#endif
|
||||
|
||||
return retStyle;
|
||||
}
|
||||
#endif
|
||||
|
||||
- (id)init
|
||||
{
|
||||
if ((self = [super init]))
|
||||
{
|
||||
// defaults
|
||||
_firstLineHeadIndent = 0.0;
|
||||
_defaultTabInterval = 36.0;
|
||||
_baseWritingDirection = kCTWritingDirectionNatural;
|
||||
#if DTCORETEXT_SUPPORT_NS_ATTRIBUTES
|
||||
_alignment = kCTTextAlignmentNatural;
|
||||
#else
|
||||
_alignment = kCTNaturalTextAlignment;
|
||||
#endif
|
||||
_lineHeightMultiple = 0.0;
|
||||
_minimumLineHeight = 0.0;
|
||||
_maximumLineHeight = 0.0;
|
||||
_paragraphSpacing = 0.0;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
|
||||
- (id)initWithCTParagraphStyle:(CTParagraphStyleRef)ctParagraphStyle
|
||||
{
|
||||
if ((self = [super init]))
|
||||
{
|
||||
// text alignment
|
||||
CTParagraphStyleGetValueForSpecifier(ctParagraphStyle, kCTParagraphStyleSpecifierAlignment,sizeof(_alignment), &_alignment);
|
||||
|
||||
// indents
|
||||
CTParagraphStyleGetValueForSpecifier(ctParagraphStyle, kCTParagraphStyleSpecifierFirstLineHeadIndent, sizeof(_firstLineHeadIndent), &_firstLineHeadIndent);
|
||||
CTParagraphStyleGetValueForSpecifier(ctParagraphStyle, kCTParagraphStyleSpecifierHeadIndent, sizeof(_headIndent), &_headIndent);
|
||||
CTParagraphStyleGetValueForSpecifier(ctParagraphStyle, kCTParagraphStyleSpecifierTailIndent, sizeof(_tailIndent), &_tailIndent);
|
||||
|
||||
// paragraph spacing
|
||||
CTParagraphStyleGetValueForSpecifier(ctParagraphStyle, kCTParagraphStyleSpecifierParagraphSpacing, sizeof(_paragraphSpacing), &_paragraphSpacing);
|
||||
CTParagraphStyleGetValueForSpecifier(ctParagraphStyle, kCTParagraphStyleSpecifierParagraphSpacingBefore,sizeof(_paragraphSpacingBefore), &_paragraphSpacingBefore);
|
||||
|
||||
|
||||
// tab stops
|
||||
CTParagraphStyleGetValueForSpecifier(ctParagraphStyle, kCTParagraphStyleSpecifierDefaultTabInterval, sizeof(_defaultTabInterval), &_defaultTabInterval);
|
||||
|
||||
CFArrayRef stops; // Could use a CFArray too, leave as a reminder how to do this in the future
|
||||
if (CTParagraphStyleGetValueForSpecifier(ctParagraphStyle, kCTParagraphStyleSpecifierTabStops, sizeof(stops), &stops))
|
||||
{
|
||||
self.tabStops = (__bridge NSArray *) stops;
|
||||
}
|
||||
|
||||
|
||||
CTParagraphStyleGetValueForSpecifier(ctParagraphStyle, kCTParagraphStyleSpecifierBaseWritingDirection, sizeof(_baseWritingDirection), &_baseWritingDirection);
|
||||
|
||||
// line height
|
||||
CTParagraphStyleGetValueForSpecifier(ctParagraphStyle, kCTParagraphStyleSpecifierMinimumLineHeight, sizeof(_minimumLineHeight), &_minimumLineHeight);
|
||||
CTParagraphStyleGetValueForSpecifier(ctParagraphStyle, kCTParagraphStyleSpecifierMaximumLineHeight, sizeof(_maximumLineHeight), &_maximumLineHeight);
|
||||
|
||||
|
||||
CTParagraphStyleGetValueForSpecifier(ctParagraphStyle, kCTParagraphStyleSpecifierLineHeightMultiple, sizeof(_lineHeightMultiple), &_lineHeightMultiple);
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (CTParagraphStyleRef)createCTParagraphStyle
|
||||
{
|
||||
// This just makes it that much easier to track down memory issues with tabstops
|
||||
CFArrayRef stops = _tabStops ? CFArrayCreateCopy (NULL, (__bridge CFArrayRef)_tabStops) : NULL;
|
||||
|
||||
CTParagraphStyleSetting settings[] =
|
||||
{
|
||||
{kCTParagraphStyleSpecifierAlignment, sizeof(_alignment), &_alignment},
|
||||
{kCTParagraphStyleSpecifierFirstLineHeadIndent, sizeof(_firstLineHeadIndent), &_firstLineHeadIndent},
|
||||
{kCTParagraphStyleSpecifierDefaultTabInterval, sizeof(_defaultTabInterval), &_defaultTabInterval},
|
||||
|
||||
{kCTParagraphStyleSpecifierTabStops, sizeof(stops), &stops},
|
||||
|
||||
{kCTParagraphStyleSpecifierParagraphSpacing, sizeof(_paragraphSpacing), &_paragraphSpacing},
|
||||
{kCTParagraphStyleSpecifierParagraphSpacingBefore, sizeof(_paragraphSpacingBefore), &_paragraphSpacingBefore},
|
||||
|
||||
{kCTParagraphStyleSpecifierHeadIndent, sizeof(_headIndent), &_headIndent},
|
||||
{kCTParagraphStyleSpecifierTailIndent, sizeof(_tailIndent), &_tailIndent},
|
||||
{kCTParagraphStyleSpecifierBaseWritingDirection, sizeof(_baseWritingDirection), &_baseWritingDirection},
|
||||
{kCTParagraphStyleSpecifierLineHeightMultiple, sizeof(_lineHeightMultiple), &_lineHeightMultiple},
|
||||
|
||||
{kCTParagraphStyleSpecifierMinimumLineHeight, sizeof(_minimumLineHeight), &_minimumLineHeight},
|
||||
{kCTParagraphStyleSpecifierMaximumLineHeight, sizeof(_maximumLineHeight), &_maximumLineHeight}
|
||||
};
|
||||
|
||||
CTParagraphStyleRef ret = CTParagraphStyleCreate(settings, 12);
|
||||
|
||||
if (stops)
|
||||
{
|
||||
CFRelease(stops);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
#if DTCORETEXT_SUPPORT_NS_ATTRIBUTES
|
||||
- (NSParagraphStyle *)NSParagraphStyle
|
||||
{
|
||||
NSMutableParagraphStyle *mps = [[NSMutableParagraphStyle alloc] init];
|
||||
|
||||
[mps setFirstLineHeadIndent:_firstLineHeadIndent];
|
||||
|
||||
[mps setParagraphSpacing:_paragraphSpacing];
|
||||
[mps setParagraphSpacingBefore:_paragraphSpacingBefore];
|
||||
|
||||
[mps setHeadIndent:_headIndent];
|
||||
[mps setTailIndent:_tailIndent];
|
||||
|
||||
[mps setMinimumLineHeight:_minimumLineHeight];
|
||||
[mps setMaximumLineHeight:_maximumLineHeight];
|
||||
[mps setLineHeightMultiple:_lineHeightMultiple];
|
||||
|
||||
[mps setAlignment:DTNSTextAlignmentFromCTTextAlignment(_alignment)];
|
||||
|
||||
switch (_baseWritingDirection)
|
||||
{
|
||||
case kCTWritingDirectionNatural:
|
||||
{
|
||||
[mps setBaseWritingDirection:NSWritingDirectionNatural];
|
||||
break;
|
||||
}
|
||||
|
||||
case kCTWritingDirectionLeftToRight:
|
||||
{
|
||||
[mps setBaseWritingDirection:NSWritingDirectionLeftToRight];
|
||||
break;
|
||||
}
|
||||
|
||||
case kCTWritingDirectionRightToLeft:
|
||||
{
|
||||
[mps setBaseWritingDirection:NSWritingDirectionRightToLeft];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
#if DTCORETEXT_SUPPORT_NSPARAGRAPHSTYLE_TABS
|
||||
if (NSClassFromString(@"NSTextTab") && [NSParagraphStyle instancesRespondToSelector:@selector(tabStops)])
|
||||
{
|
||||
NSMutableArray *tabs = [NSMutableArray array];
|
||||
|
||||
for (id object in _tabStops)
|
||||
{
|
||||
CTTextTabRef tab = (__bridge CTTextTabRef)(object);
|
||||
|
||||
CTTextAlignment alignment = CTTextTabGetAlignment(tab);
|
||||
NSTextAlignment nsAlignment = DTNSTextAlignmentFromCTTextAlignment(alignment);
|
||||
CGFloat location = (CGFloat)CTTextTabGetLocation(tab);
|
||||
|
||||
NSTextTab *textTab = [[NSTextTab alloc] initWithTextAlignment:nsAlignment location:location options:[NSDictionary dictionary]];
|
||||
|
||||
if (!textTab)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
[tabs addObject:textTab];
|
||||
}
|
||||
|
||||
if ([tabs count])
|
||||
{
|
||||
[mps setValue:tabs forKey:@"tabStops"];
|
||||
}
|
||||
|
||||
mps.defaultTabInterval = _defaultTabInterval;
|
||||
}
|
||||
#endif
|
||||
|
||||
return (NSParagraphStyle *)mps;
|
||||
}
|
||||
#endif
|
||||
|
||||
- (void)addTabStopAtPosition:(CGFloat)position alignment:(CTTextAlignment)alignment
|
||||
{
|
||||
CTTextTabRef tab = CTTextTabCreate(alignment, position, NULL);
|
||||
if(tab)
|
||||
{
|
||||
if (!_tabStops)
|
||||
{
|
||||
_tabStops = [[NSMutableArray alloc] init];
|
||||
}
|
||||
[_tabStops addObject:CFBridgingRelease(tab)];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark HTML Encoding
|
||||
|
||||
// representation of this paragraph style in css (as far as possible)
|
||||
- (NSString *)cssStyleRepresentation
|
||||
{
|
||||
NSMutableString *retString = [NSMutableString string];
|
||||
|
||||
switch (_alignment)
|
||||
{
|
||||
|
||||
#if DTCORETEXT_SUPPORT_NS_ATTRIBUTES
|
||||
case kCTTextAlignmentLeft:
|
||||
#else
|
||||
case kCTLeftTextAlignment:
|
||||
#endif
|
||||
[retString appendString:@"text-align:left;"];
|
||||
break;
|
||||
|
||||
#if DTCORETEXT_SUPPORT_NS_ATTRIBUTES
|
||||
case kCTTextAlignmentRight:
|
||||
#else
|
||||
case kCTRightTextAlignment:
|
||||
#endif
|
||||
[retString appendString:@"text-align:right;"];
|
||||
break;
|
||||
|
||||
#if DTCORETEXT_SUPPORT_NS_ATTRIBUTES
|
||||
case kCTTextAlignmentCenter:
|
||||
#else
|
||||
case kCTCenterTextAlignment:
|
||||
#endif
|
||||
[retString appendString:@"text-align:center;"];
|
||||
break;
|
||||
|
||||
#if DTCORETEXT_SUPPORT_NS_ATTRIBUTES
|
||||
case kCTTextAlignmentJustified:
|
||||
#else
|
||||
case kCTJustifiedTextAlignment:
|
||||
#endif
|
||||
[retString appendString:@"text-align:justify;"];
|
||||
break;
|
||||
|
||||
#if DTCORETEXT_SUPPORT_NS_ATTRIBUTES
|
||||
case kCTTextAlignmentNatural:
|
||||
#else
|
||||
case kCTNaturalTextAlignment:
|
||||
#endif
|
||||
// no output, this is default
|
||||
break;
|
||||
}
|
||||
|
||||
if (_lineHeightMultiple!=0 && _lineHeightMultiple!=1.0f)
|
||||
{
|
||||
NSNumber *number = DTNSNumberFromCGFloat(_lineHeightMultiple);
|
||||
[retString appendFormat:@"line-height:%@em;", number];
|
||||
}
|
||||
|
||||
switch (_baseWritingDirection)
|
||||
{
|
||||
case kCTWritingDirectionRightToLeft:
|
||||
[retString appendString:@"direction:rtl;"];
|
||||
break;
|
||||
case kCTWritingDirectionLeftToRight:
|
||||
[retString appendString:@"direction:ltr;"];
|
||||
break;
|
||||
case kCTWritingDirectionNatural:
|
||||
// no output, this is default
|
||||
break;
|
||||
}
|
||||
|
||||
// Spacing at the bottom
|
||||
if (_paragraphSpacing!=0.0f)
|
||||
{
|
||||
NSNumber *number = DTNSNumberFromCGFloat(_paragraphSpacing);
|
||||
[retString appendFormat:@"margin-bottom:%@px;", number];
|
||||
}
|
||||
|
||||
// Spacing at the top
|
||||
if (_paragraphSpacingBefore!=0.0f)
|
||||
{
|
||||
NSNumber *number = DTNSNumberFromCGFloat(_paragraphSpacingBefore);
|
||||
[retString appendFormat:@"margin-top:%@px;", number];
|
||||
}
|
||||
|
||||
// Spacing at the left
|
||||
if (_headIndent!=0.0f)
|
||||
{
|
||||
NSNumber *number = DTNSNumberFromCGFloat(_headIndent);
|
||||
[retString appendFormat:@"margin-left:%@px;", number];
|
||||
}
|
||||
|
||||
// Spacing at the right
|
||||
if (_tailIndent!=0.0f)
|
||||
{
|
||||
// tail indent is negative if from trailing margin
|
||||
NSNumber *number = DTNSNumberFromCGFloat(-_tailIndent);
|
||||
[retString appendFormat:@"margin-right:%@px;", number];
|
||||
}
|
||||
|
||||
// return nil if no content
|
||||
if ([retString length])
|
||||
{
|
||||
return retString;
|
||||
}
|
||||
else
|
||||
{
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark Copying
|
||||
|
||||
- (id)copyWithZone:(NSZone *)zone
|
||||
{
|
||||
DTCoreTextParagraphStyle *newObject = [[DTCoreTextParagraphStyle allocWithZone:zone] init];
|
||||
|
||||
newObject.firstLineHeadIndent = self.firstLineHeadIndent;
|
||||
newObject.tailIndent = self.tailIndent;
|
||||
newObject.defaultTabInterval = self.defaultTabInterval;
|
||||
newObject.paragraphSpacing = self.paragraphSpacing;
|
||||
newObject.paragraphSpacingBefore = self.paragraphSpacingBefore;
|
||||
newObject.lineHeightMultiple = self.lineHeightMultiple;
|
||||
newObject.minimumLineHeight = self.minimumLineHeight;
|
||||
newObject.maximumLineHeight = self.maximumLineHeight;
|
||||
newObject.headIndent = self.headIndent;
|
||||
newObject.alignment = self.alignment;
|
||||
newObject.baseWritingDirection = self.baseWritingDirection;
|
||||
newObject.tabStops = self.tabStops; // copy
|
||||
newObject.textLists = self.textLists; //copy
|
||||
newObject.textBlocks = self.textBlocks; //copy
|
||||
|
||||
return newObject;
|
||||
}
|
||||
|
||||
#pragma mark - Comparing
|
||||
|
||||
- (BOOL)isEqual:(id)object
|
||||
{
|
||||
if (object == self)
|
||||
{
|
||||
return YES;
|
||||
}
|
||||
|
||||
if (!object)
|
||||
{
|
||||
return NO;
|
||||
}
|
||||
|
||||
if (![object isKindOfClass:[DTCoreTextParagraphStyle class]])
|
||||
{
|
||||
return NO;
|
||||
}
|
||||
|
||||
DTCoreTextParagraphStyle *otherStyle = object;
|
||||
|
||||
|
||||
if (_firstLineHeadIndent != otherStyle->_firstLineHeadIndent)
|
||||
{
|
||||
return NO;
|
||||
}
|
||||
|
||||
if (_headIndent != otherStyle->_headIndent)
|
||||
{
|
||||
return NO;
|
||||
}
|
||||
|
||||
if (_tailIndent != otherStyle->_tailIndent)
|
||||
{
|
||||
return NO;
|
||||
}
|
||||
|
||||
if (_defaultTabInterval != otherStyle->_defaultTabInterval)
|
||||
{
|
||||
return NO;
|
||||
}
|
||||
|
||||
if (_paragraphSpacing != otherStyle->_paragraphSpacing)
|
||||
{
|
||||
return NO;
|
||||
}
|
||||
|
||||
if (_paragraphSpacingBefore != otherStyle->_paragraphSpacingBefore)
|
||||
{
|
||||
return NO;
|
||||
}
|
||||
|
||||
if (_lineHeightMultiple != otherStyle->_lineHeightMultiple)
|
||||
{
|
||||
return NO;
|
||||
}
|
||||
|
||||
if (_minimumLineHeight != otherStyle->_minimumLineHeight)
|
||||
{
|
||||
return NO;
|
||||
}
|
||||
|
||||
if (_maximumLineHeight != otherStyle->_maximumLineHeight)
|
||||
{
|
||||
return NO;
|
||||
}
|
||||
|
||||
if (_alignment != otherStyle->_alignment)
|
||||
{
|
||||
return NO;
|
||||
}
|
||||
|
||||
if (_baseWritingDirection != otherStyle->_baseWritingDirection)
|
||||
{
|
||||
return NO;
|
||||
}
|
||||
|
||||
if (_textLists && ![_textLists isEqualToArray:otherStyle->_textLists])
|
||||
{
|
||||
return NO;
|
||||
}
|
||||
|
||||
if (_textBlocks && ![_textBlocks isEqualToArray:otherStyle->_textBlocks])
|
||||
{
|
||||
return NO;
|
||||
}
|
||||
|
||||
if (_tabStops && ![_tabStops isEqualToArray:otherStyle->_tabStops])
|
||||
{
|
||||
return NO;
|
||||
}
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
#pragma mark Properties
|
||||
|
||||
- (void)setTabStops:(NSArray *)tabStops
|
||||
{
|
||||
if (tabStops != _tabStops)
|
||||
{
|
||||
_tabStops = [tabStops mutableCopy]; // keep mutability
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setFirstLineHeadIndent:(CGFloat)firstLineHeadIndent
|
||||
{
|
||||
if (_firstLineHeadIndent != firstLineHeadIndent)
|
||||
{
|
||||
_firstLineHeadIndent = firstLineHeadIndent;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setDefaultTabInterval:(CGFloat)defaultTabInterval
|
||||
{
|
||||
if (_defaultTabInterval != defaultTabInterval)
|
||||
{
|
||||
_defaultTabInterval = defaultTabInterval;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setParagraphSpacingBefore:(CGFloat)paragraphSpacingBefore
|
||||
{
|
||||
if (_paragraphSpacingBefore != paragraphSpacingBefore)
|
||||
{
|
||||
_paragraphSpacingBefore = paragraphSpacingBefore;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setParagraphSpacing:(CGFloat)paragraphSpacing
|
||||
{
|
||||
if (_paragraphSpacing != paragraphSpacing)
|
||||
{
|
||||
_paragraphSpacing = paragraphSpacing;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setLineHeightMultiple:(CGFloat)lineHeightMultiple
|
||||
{
|
||||
if (_lineHeightMultiple != lineHeightMultiple)
|
||||
{
|
||||
_lineHeightMultiple = lineHeightMultiple;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setMinimumLineHeight:(CGFloat)minimumLineHeight
|
||||
{
|
||||
if (_minimumLineHeight != minimumLineHeight)
|
||||
{
|
||||
_minimumLineHeight = minimumLineHeight;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setMaximumLineHeight:(CGFloat)maximumLineHeight
|
||||
{
|
||||
if (_maximumLineHeight != maximumLineHeight)
|
||||
{
|
||||
_maximumLineHeight = maximumLineHeight;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setHeadIndent:(CGFloat)headIndent
|
||||
{
|
||||
if (_headIndent != headIndent)
|
||||
{
|
||||
_headIndent = headIndent;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setTailIndent:(CGFloat)tailIndent
|
||||
{
|
||||
if (_tailIndent != tailIndent)
|
||||
{
|
||||
_tailIndent = tailIndent;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setAlignment:(CTTextAlignment)alignment
|
||||
{
|
||||
if (_alignment != alignment)
|
||||
{
|
||||
_alignment = alignment;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setTextLists:(NSArray *)textLists
|
||||
{
|
||||
if (_textLists != textLists)
|
||||
{
|
||||
_textLists = [textLists copy];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setTextBlocks:(NSArray *)textBlocks
|
||||
{
|
||||
if (_textBlocks != textBlocks)
|
||||
{
|
||||
_textBlocks = [textBlocks copy];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setBaseWritingDirection:(CTWritingDirection)baseWritingDirection
|
||||
{
|
||||
if (_baseWritingDirection != baseWritingDirection)
|
||||
{
|
||||
_baseWritingDirection = baseWritingDirection;
|
||||
}
|
||||
}
|
||||
|
||||
@synthesize firstLineHeadIndent = _firstLineHeadIndent;
|
||||
@synthesize defaultTabInterval = _defaultTabInterval;
|
||||
@synthesize paragraphSpacingBefore = _paragraphSpacingBefore;
|
||||
@synthesize paragraphSpacing = _paragraphSpacing;
|
||||
|
||||
@synthesize lineHeightMultiple = _lineHeightMultiple;
|
||||
@synthesize minimumLineHeight = _minimumLineHeight;
|
||||
@synthesize maximumLineHeight = _maximumLineHeight;
|
||||
@synthesize headIndent = _headIndent;
|
||||
@synthesize tailIndent = _tailIndent;
|
||||
@synthesize alignment = _alignment;
|
||||
@synthesize textLists = _textLists;
|
||||
@synthesize textBlocks = _textBlocks;
|
||||
@synthesize baseWritingDirection = _baseWritingDirection;
|
||||
@synthesize tabStops = _tabStops;
|
||||
|
||||
@end
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
//
|
||||
// DTDictationPlaceholderTextAttachment.h
|
||||
// DTCoreText
|
||||
//
|
||||
// Created by Oliver Drobnik on 06.02.13.
|
||||
// Copyright (c) 2013 Drobnik.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import "DTTextAttachment.h"
|
||||
|
||||
/**
|
||||
This is a special subclass of DTTextAttachment used to represent the dictation placeholder.
|
||||
|
||||
When encountering such an element DTAttributedTextContentView does not call the delegate to provide a subclass but automatically creates and adds a DTDictationPlaceholderView.
|
||||
*/
|
||||
|
||||
@interface DTDictationPlaceholderTextAttachment : DTTextAttachment
|
||||
|
||||
/**
|
||||
The string that inserting the dictation placeholder replaced, used for Undoing
|
||||
*/
|
||||
@property (nonatomic, retain) NSAttributedString *replacedAttributedString;
|
||||
|
||||
@end
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
//
|
||||
// DTDictationPlaceholderTextAttachment.m
|
||||
// DTCoreText
|
||||
//
|
||||
// Created by Oliver Drobnik on 06.02.13.
|
||||
// Copyright (c) 2013 Drobnik.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import "DTDictationPlaceholderTextAttachment.h"
|
||||
|
||||
@implementation DTDictationPlaceholderTextAttachment
|
||||
{
|
||||
NSAttributedString *_replacedAttributedString;
|
||||
}
|
||||
|
||||
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
|
||||
self = [super initWithCoder:aDecoder];
|
||||
if (self) {
|
||||
_replacedAttributedString = [aDecoder decodeObjectForKey:@"replacedAttributedString"];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)encodeWithCoder:(NSCoder *)aCoder {
|
||||
[super encodeWithCoder:aCoder];
|
||||
[aCoder encodeObject:_replacedAttributedString forKey:@"replacedAttributedString"];
|
||||
}
|
||||
|
||||
// if you change any of these then also make sure to adjust the sizes in DTDictationPlaceholderTextAttachment
|
||||
#define DOT_WIDTH 10.0f
|
||||
#define DOT_DISTANCE 2.5f
|
||||
#define DOT_OUTSIDE_MARGIN 3.0f
|
||||
|
||||
// several hard-coded items
|
||||
- (CGSize)displaySize
|
||||
{
|
||||
return CGSizeMake(DOT_OUTSIDE_MARGIN*2.0f + DOT_WIDTH*3.0f + DOT_DISTANCE*2.0f, DOT_OUTSIDE_MARGIN*2.0f + DOT_WIDTH);
|
||||
}
|
||||
|
||||
- (CGSize)originalSize
|
||||
{
|
||||
return [self displaySize];
|
||||
}
|
||||
|
||||
- (CGFloat)ascentForLayout
|
||||
{
|
||||
return self.displaySize.height;
|
||||
}
|
||||
|
||||
- (CGFloat)descentForLayout
|
||||
{
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
#pragma mark - Properties
|
||||
|
||||
@synthesize replacedAttributedString = _replacedAttributedString;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,33 @@
|
||||
//
|
||||
// DTDictationPlaceholderView.h
|
||||
// DTRichTextEditor
|
||||
//
|
||||
// Created by Oliver Drobnik on 05.02.13.
|
||||
// Copyright (c) 2013 Cocoanetics. All rights reserved.
|
||||
//
|
||||
|
||||
#import "DTCompatibility.h"
|
||||
|
||||
#if TARGET_OS_IPHONE && !TARGET_OS_WATCH
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
/**
|
||||
A dictation placeholder to display in editors between the time the recording is complete until a recognized response is received.
|
||||
*/
|
||||
|
||||
@interface DTDictationPlaceholderView : UIView
|
||||
|
||||
/**
|
||||
Creates an appropriately sized DTDictationPlaceholderView with 3 animated purple dots
|
||||
*/
|
||||
+ (DTDictationPlaceholderView *)placeholderView;
|
||||
|
||||
/**
|
||||
The context of the receiver. This can be any object, for example the selection range to replace with the dictation result text
|
||||
*/
|
||||
@property (nonatomic, strong) id context;
|
||||
|
||||
@end
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,123 @@
|
||||
//
|
||||
// DTDictationPlaceholderView.m
|
||||
// DTRichTextEditor
|
||||
//
|
||||
// Created by Oliver Drobnik on 05.02.13.
|
||||
// Copyright (c) 2013 Cocoanetics. All rights reserved.
|
||||
//
|
||||
|
||||
#import "DTDictationPlaceholderView.h"
|
||||
|
||||
#if TARGET_OS_IPHONE && !TARGET_OS_WATCH
|
||||
|
||||
// if you change any of these then also make sure to adjust the sizes in DTDictationPlaceholderTextAttachment
|
||||
#define DOT_WIDTH 10.0f
|
||||
#define DOT_DISTANCE 2.5f
|
||||
#define DOT_OUTSIDE_MARGIN 3.0f
|
||||
|
||||
@implementation DTDictationPlaceholderView
|
||||
{
|
||||
NSUInteger _phase;
|
||||
NSTimer *_phaseTimer;
|
||||
}
|
||||
|
||||
+ (DTDictationPlaceholderView *)placeholderView;
|
||||
{
|
||||
return [[DTDictationPlaceholderView alloc] initWithFrame:CGRectZero];
|
||||
}
|
||||
|
||||
- (id)initWithFrame:(CGRect)frame
|
||||
{
|
||||
self = [super initWithFrame:frame];
|
||||
if (self)
|
||||
{
|
||||
self.backgroundColor = [UIColor clearColor];
|
||||
|
||||
[self sizeToFit];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (CGSize)sizeThatFits:(CGSize)size
|
||||
{
|
||||
return CGSizeMake(DOT_OUTSIDE_MARGIN*2.0f + DOT_WIDTH*3.0f + DOT_DISTANCE*2.0f, DOT_OUTSIDE_MARGIN*2.0f + DOT_WIDTH);
|
||||
}
|
||||
|
||||
- (UIColor *)_lightDotColor
|
||||
{
|
||||
return [UIColor colorWithRed:(CGFloat)(238.0/255.0) green:(CGFloat)(128.0/255.0) blue:(CGFloat)(238.0/255.0) alpha:1.0];
|
||||
}
|
||||
|
||||
- (UIColor *)_darkDotColor
|
||||
{
|
||||
return [UIColor colorWithRed:(CGFloat)(191.0/255.0) green:(CGFloat)(51.0/255.0) blue:(CGFloat)(191.0/255.0) alpha:1.0];
|
||||
}
|
||||
|
||||
- (void)willMoveToSuperview:(UIView *)newSuperview
|
||||
{
|
||||
[super willMoveToSuperview:newSuperview];
|
||||
|
||||
[_phaseTimer invalidate];
|
||||
_phaseTimer = nil;
|
||||
|
||||
if (newSuperview)
|
||||
{
|
||||
_phaseTimer = [NSTimer scheduledTimerWithTimeInterval:0.25 target:self selector:@selector(_phaseTimerTick:) userInfo:nil repeats:YES];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
- (void)drawRect:(CGRect)rect
|
||||
{
|
||||
CGContextRef ctx = UIGraphicsGetCurrentContext();
|
||||
|
||||
CGRect dotRect = CGRectMake(DOT_OUTSIDE_MARGIN, 4, DOT_WIDTH, DOT_WIDTH);
|
||||
|
||||
if (_phase==0)
|
||||
{
|
||||
CGContextSetFillColorWithColor(ctx, [self _darkDotColor].CGColor);
|
||||
}
|
||||
else
|
||||
{
|
||||
CGContextSetFillColorWithColor(ctx, [self _lightDotColor].CGColor);
|
||||
}
|
||||
|
||||
CGContextFillEllipseInRect(ctx, dotRect);
|
||||
|
||||
dotRect.origin.x = DOT_DISTANCE + CGRectGetMaxX(dotRect);
|
||||
|
||||
if (_phase==1)
|
||||
{
|
||||
CGContextSetFillColorWithColor(ctx, [self _darkDotColor].CGColor);
|
||||
}
|
||||
else
|
||||
{
|
||||
CGContextSetFillColorWithColor(ctx, [self _lightDotColor].CGColor);
|
||||
}
|
||||
|
||||
CGContextFillEllipseInRect(ctx, dotRect);
|
||||
|
||||
dotRect.origin.x = DOT_DISTANCE + CGRectGetMaxX(dotRect);
|
||||
|
||||
if (_phase==2)
|
||||
{
|
||||
CGContextSetFillColorWithColor(ctx, [self _darkDotColor].CGColor);
|
||||
}
|
||||
else
|
||||
{
|
||||
CGContextSetFillColorWithColor(ctx, [self _lightDotColor].CGColor);
|
||||
}
|
||||
|
||||
CGContextFillEllipseInRect(ctx, dotRect);
|
||||
}
|
||||
|
||||
- (void)_phaseTimerTick:(id)sender
|
||||
{
|
||||
_phase = (_phase+1)%3;
|
||||
[self setNeedsDisplay];
|
||||
}
|
||||
|
||||
|
||||
@end
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,95 @@
|
||||
//
|
||||
// DTHTMLAttributedStringBuilder.h
|
||||
// DTCoreText
|
||||
//
|
||||
// Created by Oliver Drobnik on 21.01.12.
|
||||
// Copyright (c) 2012 Drobnik.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import <DTFoundation/DTHTMLParser.h>
|
||||
|
||||
@class DTHTMLElement;
|
||||
|
||||
/**
|
||||
The block that gets executed whenever an element is flushed to the output string
|
||||
*/
|
||||
typedef void(^DTHTMLAttributedStringBuilderWillFlushCallback)(DTHTMLElement *);
|
||||
|
||||
/**
|
||||
The block that gets executed whenever html tag parsing error
|
||||
*/
|
||||
typedef void(^DTHTMLAttributedStringBuilderParseErrorCallback)(NSAttributedString *attr, NSError *);
|
||||
|
||||
|
||||
/**
|
||||
Class for building an `NSAttributedString` from an HTML document.
|
||||
*/
|
||||
@interface DTHTMLAttributedStringBuilder : NSObject <DTHTMLParserDelegate>
|
||||
|
||||
/**
|
||||
@name Creating an Attributed String Builder
|
||||
*/
|
||||
|
||||
/**
|
||||
Initializes and returns a new `NSAttributedString` object from the HTML contained in the given object and base URL.
|
||||
|
||||
Options can be:
|
||||
|
||||
- DTMaxImageSize: the maximum CGSize that a text attachment can fill
|
||||
- DTDefaultFontFamily: the default font family to use instead of Times New Roman
|
||||
- DTDefaultFontName: the default font face to use instead of Times New Roman
|
||||
- DTDefaultFontSize: the default font size to use instead of 12
|
||||
- DTDefaultFontDescriptor: the default font descriptor. This supercedes font family/name
|
||||
- DTDefaultTextColor: the default text color
|
||||
- DTDefaultLinkColor: the default color for hyperlink text
|
||||
- DTDefaultLinkDecoration: the default decoration for hyperlinks
|
||||
- DTDefaultLinkHighlightColor: the color to show while the hyperlink is highlighted
|
||||
- DTDefaultTextAlignment: the default text alignment for paragraphs
|
||||
- DTDefaultLineHeightMultiplier: The multiplier for line heights
|
||||
- DTDefaultFirstLineHeadIndent: The default indent for left margin on first line
|
||||
- DTDefaultHeadIndent: The default indent for left margin except first line
|
||||
- DTDefaultListIndent: The amount by which lists are indented
|
||||
- DTDefaultStyleSheet: The default style sheet to use
|
||||
- DTUseiOS6Attributes: use iOS 6 attributes for building (UITextView compatible)
|
||||
- DTWillFlushBlockCallBack: a block to be executed whenever content is flushed to the output string
|
||||
- DTIgnoreInlineStylesOption: All inline style information is being ignored and only style blocks used
|
||||
|
||||
@param data The data in HTML format from which to create the attributed string.
|
||||
@param options Specifies how the document should be loaded. Contains values described in NSAttributedString(HTML).
|
||||
@param docAttributes Currently not in use.
|
||||
@returns Returns an initialized object, or `nil` if the data can’t be decoded.
|
||||
*/
|
||||
- (id)initWithHTML:(NSData *)data options:(NSDictionary *)options documentAttributes:(NSDictionary * __autoreleasing*)docAttributes;
|
||||
|
||||
|
||||
/**
|
||||
@name Generating Attributed Strings
|
||||
*/
|
||||
|
||||
/**
|
||||
Creates the attributed string when called the first time.
|
||||
@returns An `NSAttributedString` representing the HTML document passed in the initializer.
|
||||
*/
|
||||
- (NSAttributedString *)generatedAttributedString;
|
||||
|
||||
|
||||
/**
|
||||
This block is called before the element is written to the output attributed string
|
||||
*/
|
||||
@property (nonatomic, copy) DTHTMLAttributedStringBuilderWillFlushCallback willFlushCallback;
|
||||
/**
|
||||
The block that gets executed whenever html tag parsing error
|
||||
*/
|
||||
@property (nonatomic, copy) DTHTMLAttributedStringBuilderParseErrorCallback parseErrorCallback;
|
||||
|
||||
/**
|
||||
Setting this property to `YES` causes the tree of parse nodes to be preserved until the end of the generation process. This allows to output the HTML structure of the document for debugging.
|
||||
*/
|
||||
@property (nonatomic, assign) BOOL shouldKeepDocumentNodeTree;
|
||||
|
||||
/**
|
||||
This func can abort AttributedString building.
|
||||
*/
|
||||
- (void)abortParsing;
|
||||
|
||||
@end
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,350 @@
|
||||
//
|
||||
// DTHTMLElement.h
|
||||
// DTCoreText
|
||||
//
|
||||
// Created by Oliver Drobnik on 4/14/11.
|
||||
// Copyright 2011 Drobnik.com. All rights reserved.
|
||||
//
|
||||
|
||||
@class DTCoreTextParagraphStyle;
|
||||
@class DTCoreTextFontDescriptor;
|
||||
@class DTTextAttachment;
|
||||
@class DTCSSListStyle;
|
||||
|
||||
#import "DTCompatibility.h"
|
||||
#import "DTCoreTextConstants.h"
|
||||
#import "DTHTMLParserNode.h"
|
||||
#import "DTTextAttachment.h"
|
||||
|
||||
@class DTBreakHTMLElement;
|
||||
|
||||
/**
|
||||
Class to represent a element (aka "tag") in a HTML document. Structure information - like parent or children - is inherited from its superclass <DTHTMLParserNode>.
|
||||
*/
|
||||
@interface DTHTMLElement : DTHTMLParserNode
|
||||
{
|
||||
DTCoreTextFontDescriptor *_fontDescriptor;
|
||||
DTCoreTextParagraphStyle *_paragraphStyle;
|
||||
DTTextAttachment *_textAttachment;
|
||||
DTTextAttachmentVerticalAlignment _textAttachmentAlignment;
|
||||
NSURL *_link;
|
||||
NSString *_anchorName;
|
||||
|
||||
DTColor *_textColor;
|
||||
DTColor *_backgroundColor;
|
||||
|
||||
DTColor *_backgroundStrokeColor;
|
||||
CGFloat _backgroundStrokeWidth;
|
||||
CGFloat _backgroundCornerRadius;
|
||||
|
||||
CTUnderlineStyle _underlineStyle;
|
||||
DTColor *_underlineColor;
|
||||
|
||||
NSString *_beforeContent;
|
||||
|
||||
NSString *_linkGUID;
|
||||
|
||||
BOOL _strikeOut;
|
||||
NSInteger _superscriptStyle;
|
||||
|
||||
NSInteger _headerLevel;
|
||||
|
||||
NSArray *_shadows;
|
||||
|
||||
DTHTMLElementDisplayStyle _displayStyle;
|
||||
DTHTMLElementFloatStyle _floatStyle;
|
||||
|
||||
BOOL _isColorInherited;
|
||||
|
||||
BOOL _preserveNewlines;
|
||||
BOOL _containsAppleConvertedSpace;
|
||||
|
||||
DTHTMLElementFontVariant _fontVariant;
|
||||
|
||||
CGFloat _textScale;
|
||||
CGSize _size;
|
||||
|
||||
NSMutableArray *_children;
|
||||
|
||||
NSDictionary *_styles;
|
||||
|
||||
BOOL _didOutput;
|
||||
|
||||
// margins/padding
|
||||
DTEdgeInsets _margins;
|
||||
DTEdgeInsets _padding;
|
||||
|
||||
// indent of lists
|
||||
CGFloat _listIndent;
|
||||
|
||||
// indent of tag <p>
|
||||
CGFloat _pTextIndent;
|
||||
|
||||
BOOL _shouldProcessCustomHTMLAttributes;
|
||||
}
|
||||
|
||||
/**
|
||||
@name Creating HTML Elements
|
||||
*/
|
||||
|
||||
/**
|
||||
Designed initializer, creates the appropriate element sub type
|
||||
@param name The element name
|
||||
@param attributes The attributes dictionary of the tag
|
||||
@param options The parsing options dictionary
|
||||
@returns the initialized element
|
||||
*/
|
||||
+ (DTHTMLElement *)elementWithName:(NSString *)name attributes:(NSDictionary *)attributes options:(NSDictionary *)options;
|
||||
|
||||
|
||||
/**
|
||||
@name Creating Attributed Strings
|
||||
*/
|
||||
|
||||
/**
|
||||
Creates an `NSAttributedString` that represents the receiver including all its children. This method is typically overwritten in subclasses of <DTHTMLElement> that represent specific HTML elements.
|
||||
@returns An attributed string that also contains the children
|
||||
*/
|
||||
- (NSAttributedString *)attributedString;
|
||||
|
||||
/**
|
||||
The dictionary of Core Text attributes for creating an `NSAttributedString` representation for the receiver
|
||||
@returns The dictionary of attributes
|
||||
*/
|
||||
- (NSDictionary *)attributesForAttributedStringRepresentation;
|
||||
|
||||
/**
|
||||
Creates a <DTCSSListStyle> to match the CSS styles
|
||||
*/
|
||||
- (DTCSSListStyle *)listStyle;
|
||||
|
||||
|
||||
/**
|
||||
@name Getting Element Information
|
||||
*/
|
||||
|
||||
/**
|
||||
Font Descriptor describing the font state of the receiver
|
||||
*/
|
||||
@property (nonatomic, copy) DTCoreTextFontDescriptor *fontDescriptor;
|
||||
|
||||
/**
|
||||
Paragraph Style describing the paragraph state of the receiver
|
||||
*/
|
||||
@property (nonatomic, copy) DTCoreTextParagraphStyle *paragraphStyle;
|
||||
|
||||
/**
|
||||
Text Attachment of the receiver, or `nil` if there is no attachment
|
||||
*/
|
||||
@property (nonatomic, strong) DTTextAttachment *textAttachment;
|
||||
|
||||
/**
|
||||
Hyperlink URL of the receiver, or `nil` if there is no hyperlink
|
||||
*/
|
||||
@property (nonatomic, copy) NSURL *link;
|
||||
|
||||
/**
|
||||
Anchor name, used by hyperlinks, of the receiver that can be used to scroll to.
|
||||
*/
|
||||
@property (nonatomic, copy) NSString *anchorName;
|
||||
|
||||
/**
|
||||
Foreground text color of the receiver
|
||||
*/
|
||||
@property (nonatomic, strong) DTColor *textColor;
|
||||
|
||||
/**
|
||||
Background color of text in the receiver
|
||||
*/
|
||||
@property (nonatomic, strong) DTColor *backgroundColor;
|
||||
|
||||
/**
|
||||
Background stroke color in the receiver
|
||||
*/
|
||||
@property (nonatomic, strong) DTColor *backgroundStrokeColor;
|
||||
|
||||
/**
|
||||
Background stroke width in the receiver
|
||||
*/
|
||||
@property (nonatomic, assign) CGFloat backgroundStrokeWidth;
|
||||
|
||||
/**
|
||||
Background stroke width in the receiver
|
||||
*/
|
||||
@property (nonatomic, assign) CGFloat backgroundCornerRadius;
|
||||
|
||||
/**
|
||||
Tag <p> text indent
|
||||
*/
|
||||
@property (nonatomic, assign) CGFloat pTextIndent;
|
||||
|
||||
/**
|
||||
The custom letter spacing of the receiver, default is 0px
|
||||
*/
|
||||
@property (nonatomic, assign) CGFloat letterSpacing;
|
||||
|
||||
/**
|
||||
Additional text to be inserted before the text content of the receiver
|
||||
*/
|
||||
@property (nonatomic, copy) NSString *beforeContent;
|
||||
|
||||
/**
|
||||
Array of shadows attached to the text contents of the receiver
|
||||
*/
|
||||
@property (nonatomic, copy) NSArray *shadows;
|
||||
|
||||
/**
|
||||
The underline style of the receiver, at present only none or single line are supported
|
||||
*/
|
||||
@property (nonatomic, assign) CTUnderlineStyle underlineStyle;
|
||||
|
||||
/**
|
||||
The underline color of the receiver
|
||||
*/
|
||||
@property (nonatomic, strong) DTColor *underlineColor;
|
||||
|
||||
/**
|
||||
The strike-out style of the receiver
|
||||
*/
|
||||
@property (nonatomic, assign) BOOL strikeOut;
|
||||
|
||||
/**
|
||||
The superscript style of the receiver or 0 if it does not have superscript text.
|
||||
*/
|
||||
@property (nonatomic, assign) NSInteger superscriptStyle;
|
||||
|
||||
/**
|
||||
The header level of the receiver, or 0 if it is not a header
|
||||
*/
|
||||
@property (nonatomic, assign) NSInteger headerLevel;
|
||||
|
||||
/**
|
||||
The display style of the receiver.
|
||||
*/
|
||||
@property (nonatomic, assign) DTHTMLElementDisplayStyle displayStyle;
|
||||
|
||||
/**
|
||||
Whether the receiver is marked as float. While floating is not currently supported this can be used to add additional paragraph breaks.
|
||||
*/
|
||||
@property (nonatomic, readonly) DTHTMLElementFloatStyle floatStyle;
|
||||
|
||||
/**
|
||||
Specifies that the textColor was inherited. Assigning textColor sets this flag to `NO`
|
||||
*/
|
||||
@property (nonatomic, assign) BOOL isColorInherited;
|
||||
|
||||
/**
|
||||
Specifies that whitespace and new lines should be preserved. Default is to compress white space.
|
||||
*/
|
||||
@property (nonatomic, assign) BOOL preserveNewlines;
|
||||
|
||||
/**
|
||||
The current font variant of the receiver, normal or small caps.
|
||||
*/
|
||||
|
||||
@property (nonatomic, assign) DTHTMLElementFontVariant fontVariant;
|
||||
|
||||
/**
|
||||
The current unscaled font size (used when inheriting font size). You're probably looking for fontDescriptor.pointSize.
|
||||
*/
|
||||
@property (nonatomic, assign) CGFloat currentTextSize;
|
||||
|
||||
/**
|
||||
The scale by which all fonts are scaled
|
||||
*/
|
||||
@property (nonatomic, assign) CGFloat textScale;
|
||||
|
||||
/**
|
||||
The size of the receiver, either from width/height attributes or width/hight styles.
|
||||
*/
|
||||
@property (nonatomic, assign) CGSize size;
|
||||
|
||||
/**
|
||||
The value of the CSS margins. Margin support is incomplete.
|
||||
*/
|
||||
@property (nonatomic, assign) DTEdgeInsets margins;
|
||||
|
||||
/** The value of the CSS padding. Padding are added to DTTextBlock instances for block-level elements.
|
||||
*/
|
||||
@property (nonatomic, assign) DTEdgeInsets padding;
|
||||
|
||||
/**
|
||||
Specifies that whitespace contained in the receiver's text has been converted with Apple's algorithm.
|
||||
*/
|
||||
@property (nonatomic, assign) BOOL containsAppleConvertedSpace;
|
||||
|
||||
/**
|
||||
Prevents adding custom HTML attributes to output
|
||||
*/
|
||||
@property (nonatomic, assign) BOOL shouldProcessCustomHTMLAttributes;
|
||||
|
||||
/**
|
||||
@name Working with HTML Attributes
|
||||
*/
|
||||
|
||||
/**
|
||||
Retrieves an attribute with a given key
|
||||
@param key The attribute name to retrieve
|
||||
@returns the attribute string
|
||||
*/
|
||||
- (NSString *)attributeForKey:(NSString *)key;
|
||||
|
||||
/**
|
||||
Copies and inherits relevant attributes from the given parent element
|
||||
@param element The element to inherit attributes from
|
||||
*/
|
||||
- (void)inheritAttributesFromElement:(DTHTMLElement *)element;
|
||||
|
||||
/**
|
||||
Interprets the tag attributes for e.g. writing direction. Usually you would call this after inheritAttributesFromElement:.
|
||||
*/
|
||||
- (void)interpretAttributes;
|
||||
|
||||
/**
|
||||
The HTML attributes that should be attached to the generated attributed string. Typically all attributes that were processed by -interpretAttributes are in this list. All other attributes get added to the generated attributed string with the DTCustomAttributesAttribute key.
|
||||
*/
|
||||
+ (NSSet *)attributesToIgnoreForCustomAttributesAttribute;
|
||||
|
||||
/**
|
||||
The CSS class names that are not to be added to the "class" custom attribute in the DTCustomAttributesAttribute key. Those are usually the class names
|
||||
*/
|
||||
@property(nonatomic, strong) NSSet *CSSClassNamesToIgnoreForCustomAttributes;
|
||||
|
||||
/**
|
||||
@name Working with CSS Styles
|
||||
*/
|
||||
|
||||
/**
|
||||
Applies the style information contained in a styles dictionary to the receiver
|
||||
@param styles A style dictionary
|
||||
*/
|
||||
- (void)applyStyleDictionary:(NSDictionary *)styles;
|
||||
|
||||
|
||||
/**
|
||||
@name HTML Node Hierarchy
|
||||
*/
|
||||
|
||||
/**
|
||||
Returns the parent element. That's the same as the parent node but with adjusted type for convenience.
|
||||
*/
|
||||
- (DTHTMLElement *)parentElement;
|
||||
|
||||
|
||||
/**
|
||||
@name Output State (Internal)
|
||||
*/
|
||||
|
||||
/**
|
||||
Internal state during string building to mark the receiver as having been flushed
|
||||
*/
|
||||
@property (nonatomic, assign) BOOL didOutput;
|
||||
|
||||
/**
|
||||
Internal method that determines if this element still requires output, based on its own didOutput
|
||||
state and the didOutput state of its children
|
||||
@returns `YES` if it still requires output
|
||||
*/
|
||||
- (BOOL)needsOutput;
|
||||
|
||||
@end
|
||||
+1805
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,78 @@
|
||||
//
|
||||
// DTHTMLParserNode.h
|
||||
// DTCoreText
|
||||
//
|
||||
// Created by Oliver Drobnik on 26.12.12.
|
||||
// Copyright (c) 2012 Drobnik.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#import <DTFoundation/DTWeakSupport.h>
|
||||
|
||||
@class DTHTMLParserTextNode;
|
||||
|
||||
/**
|
||||
This class represents one node in an HTML DOM tree.
|
||||
*/
|
||||
@interface DTHTMLParserNode : NSObject
|
||||
{
|
||||
NSDictionary *_attributes;
|
||||
}
|
||||
|
||||
/**
|
||||
Designated initializer
|
||||
@param name The element name
|
||||
@param attributes The attributes dictionary
|
||||
@returns An initialized parser node.
|
||||
*/
|
||||
- (id)initWithName:(NSString *)name attributes:(NSDictionary *)attributes;
|
||||
|
||||
/**
|
||||
The name of the receiver
|
||||
*/
|
||||
@property (nonatomic, copy) NSString *name;
|
||||
|
||||
/**
|
||||
The attributes of the receiver.
|
||||
*/
|
||||
@property (nonatomic, copy) NSDictionary *attributes;
|
||||
|
||||
/**
|
||||
A weak link to the parent node of the receiver
|
||||
*/
|
||||
@property (nonatomic, DT_WEAK_PROPERTY) DTHTMLParserNode *parentNode;
|
||||
|
||||
/**
|
||||
The child nodes of the receiver
|
||||
*/
|
||||
@property (nonatomic, readonly) NSArray *childNodes;
|
||||
|
||||
/**
|
||||
Adds a child node to the receiver.
|
||||
@param childNode The child node to be appended to the list of children
|
||||
*/
|
||||
- (void)addChildNode:(DTHTMLParserNode *)childNode;
|
||||
|
||||
/**
|
||||
Removes a child node from the receiver
|
||||
@param childNode The child node to remove
|
||||
*/
|
||||
- (void)removeChildNode:(DTHTMLParserNode *)childNode;
|
||||
|
||||
/**
|
||||
Removes all child nodes from the receiver
|
||||
*/
|
||||
- (void)removeAllChildNodes;
|
||||
|
||||
/**
|
||||
Hierarchy representation of the receiver including all attributes and children
|
||||
*/
|
||||
- (NSString *)debugDescription;
|
||||
|
||||
/**
|
||||
Concatenated contents of all text nodes
|
||||
*/
|
||||
- (NSString *)text;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,167 @@
|
||||
//
|
||||
// DTHTMLParserNode.m
|
||||
// DTCoreText
|
||||
//
|
||||
// Created by Oliver Drobnik on 26.12.12.
|
||||
// Copyright (c) 2012 Drobnik.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import "DTHTMLParserNode.h"
|
||||
#import "DTHTMLParserTextNode.h"
|
||||
#import <DTFoundation/DTWeakSupport.h>
|
||||
|
||||
@implementation DTHTMLParserNode
|
||||
{
|
||||
NSString *_name;
|
||||
DT_WEAK_VARIABLE DTHTMLParserNode *_parentNode;
|
||||
NSMutableArray *_childNodes;
|
||||
}
|
||||
|
||||
|
||||
- (id)initWithName:(NSString *)name attributes:(NSDictionary *)attributes
|
||||
{
|
||||
self = [super init];
|
||||
|
||||
if (self)
|
||||
{
|
||||
_name = [name copy];
|
||||
[self setAttributes:attributes]; // property to allow overriding
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)addChildNode:(DTHTMLParserNode *)childNode
|
||||
{
|
||||
@synchronized(self)
|
||||
{
|
||||
// first child creates array
|
||||
if (!_childNodes)
|
||||
{
|
||||
_childNodes = [[NSMutableArray alloc] init];
|
||||
}
|
||||
|
||||
childNode.parentNode = self;
|
||||
[_childNodes addObject:childNode];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)removeChildNode:(DTHTMLParserNode *)childNode
|
||||
{
|
||||
@synchronized(self)
|
||||
{
|
||||
[_childNodes removeObject:childNode];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)removeAllChildNodes
|
||||
{
|
||||
@synchronized(self)
|
||||
{
|
||||
[_childNodes removeAllObjects];
|
||||
}
|
||||
}
|
||||
|
||||
#ifndef COVERAGE
|
||||
// exclude methods from coverage testing
|
||||
|
||||
- (NSString *)description
|
||||
{
|
||||
return [NSString stringWithFormat:@"<%@ name='%@'>", NSStringFromClass([self class]), _name];
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
- (void)_appendHTMLToString:(NSMutableString *)string indentLevel:(NSUInteger)indentLevel
|
||||
{
|
||||
@synchronized(self)
|
||||
{
|
||||
// indent to the level
|
||||
for (NSUInteger i=0; i<indentLevel; i++)
|
||||
{
|
||||
[string appendString:@" "];
|
||||
}
|
||||
|
||||
// write own name tag open
|
||||
[string appendFormat:@"<%@", _name];
|
||||
|
||||
// sort attribute names
|
||||
NSArray *sortedKeys = [_attributes.allKeys sortedArrayUsingSelector:@selector(compare:)];
|
||||
|
||||
for (NSString *oneKey in sortedKeys)
|
||||
{
|
||||
NSString *attribute = [_attributes objectForKey:oneKey];
|
||||
[string appendFormat:@" %@=\"%@\"", oneKey, attribute];
|
||||
}
|
||||
|
||||
if (![_childNodes count])
|
||||
{
|
||||
[string appendString:@" \\>\n"];
|
||||
return;
|
||||
}
|
||||
|
||||
[string appendFormat:@">\n"];
|
||||
|
||||
// output attributes
|
||||
for (DTHTMLParserNode *childNode in _childNodes)
|
||||
{
|
||||
[childNode _appendHTMLToString:string indentLevel:indentLevel+1];
|
||||
}
|
||||
|
||||
// indent to the level
|
||||
for (NSUInteger i=0; i<indentLevel; i++)
|
||||
{
|
||||
[string appendString:@" "];
|
||||
}
|
||||
|
||||
// write own name tag close
|
||||
[string appendFormat:@"</%@>\n", _name];
|
||||
}
|
||||
}
|
||||
|
||||
- (NSString *)debugDescription
|
||||
{
|
||||
@synchronized(self)
|
||||
{
|
||||
NSMutableString *tmpString = [NSMutableString string];
|
||||
|
||||
[self _appendHTMLToString:tmpString indentLevel:0];
|
||||
|
||||
return tmpString;
|
||||
}
|
||||
}
|
||||
|
||||
- (NSString *)text
|
||||
{
|
||||
@synchronized(self)
|
||||
{
|
||||
NSMutableString *text = [NSMutableString string];
|
||||
|
||||
for (DTHTMLParserTextNode *oneChild in self.childNodes)
|
||||
{
|
||||
if ([oneChild isKindOfClass:[DTHTMLParserTextNode class]])
|
||||
{
|
||||
[text appendString:[oneChild characters]];
|
||||
}
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Properties
|
||||
|
||||
- (NSArray *)childNodes
|
||||
{
|
||||
@synchronized(self)
|
||||
{
|
||||
return _childNodes;
|
||||
}
|
||||
}
|
||||
|
||||
@synthesize name = _name;
|
||||
@synthesize attributes = _attributes;
|
||||
@synthesize parentNode = _parentNode;
|
||||
@synthesize childNodes = _childNodes;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,30 @@
|
||||
//
|
||||
// DTHTMLParserTextNode.h
|
||||
// DTCoreText
|
||||
//
|
||||
// Created by Oliver Drobnik on 26.12.12.
|
||||
// Copyright (c) 2012 Drobnik.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#import "DTHTMLParserNode.h"
|
||||
|
||||
/**
|
||||
Specialized sub class of <DTHTMLParserNode> that represents text inside a node
|
||||
*/
|
||||
@interface DTHTMLParserTextNode : DTHTMLParserNode
|
||||
|
||||
/**
|
||||
Designated initializer with the characters that make up the text.
|
||||
@param characters The characters of the string
|
||||
@returns The initialized text node
|
||||
*/
|
||||
- (id)initWithCharacters:(NSString *)characters;
|
||||
|
||||
/**
|
||||
Returns the receivers character contents
|
||||
*/
|
||||
@property (nonatomic, readonly) NSString *characters;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,56 @@
|
||||
//
|
||||
// DTHTMLParserTextNode.m
|
||||
// DTCoreText
|
||||
//
|
||||
// Created by Oliver Drobnik on 26.12.12.
|
||||
// Copyright (c) 2012 Drobnik.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import "DTHTMLParserTextNode.h"
|
||||
#import "NSString+HTML.h"
|
||||
|
||||
@implementation DTHTMLParserTextNode
|
||||
{
|
||||
NSString *_characters;
|
||||
}
|
||||
|
||||
- (id)initWithCharacters:(NSString *)characters
|
||||
{
|
||||
self = [super init];
|
||||
|
||||
if (self)
|
||||
{
|
||||
self.name = @"#TEXT#";
|
||||
|
||||
_characters = characters;
|
||||
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
#ifndef COVERAGE
|
||||
// exclude methods from coverage testing
|
||||
|
||||
- (NSString *)description
|
||||
{
|
||||
return [NSString stringWithFormat:@"<%@ content='%@'>", NSStringFromClass([self class]), _characters];
|
||||
}
|
||||
|
||||
- (void)_appendHTMLToString:(NSMutableString *)string indentLevel:(NSUInteger)indentLevel
|
||||
{
|
||||
// indent to the level
|
||||
for (NSUInteger i=0; i<indentLevel; i++)
|
||||
{
|
||||
[string appendString:@" "];
|
||||
}
|
||||
|
||||
[string appendFormat:@"\"%@\"\n", [_characters stringByNormalizingWhitespace]];
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
#pragma mark - Properties
|
||||
|
||||
@synthesize characters = _characters;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,69 @@
|
||||
//
|
||||
// DTHTMLWriter.h
|
||||
// DTCoreText
|
||||
//
|
||||
// Created by Oliver Drobnik on 23.12.12.
|
||||
// Copyright (c) 2012 Drobnik.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <CoreGraphics/CoreGraphics.h>
|
||||
|
||||
/**
|
||||
Class to generate HTML from `NSAttributedString` instances.
|
||||
*/
|
||||
@interface DTHTMLWriter : NSObject
|
||||
|
||||
/**
|
||||
@name Creating an HTML Writer
|
||||
*/
|
||||
|
||||
/**
|
||||
Creates a writer with a given `NSAttributedString` as input
|
||||
@param attributedString An attributed string
|
||||
*/
|
||||
- (id)initWithAttributedString:(NSAttributedString *)attributedString;
|
||||
|
||||
/**
|
||||
@name Generating HTML
|
||||
*/
|
||||
|
||||
/**
|
||||
Generates a HTML representation of the attributed string
|
||||
@returns The generated string
|
||||
*/
|
||||
- (NSString *)HTMLString;
|
||||
|
||||
|
||||
/**
|
||||
Generates a HTML fragment representation of the attributed string including inlined styles and no html or head elements
|
||||
@returns The generated string
|
||||
*/
|
||||
- (NSString *)HTMLFragment;
|
||||
|
||||
/**
|
||||
@name Properties
|
||||
*/
|
||||
|
||||
/**
|
||||
If specified then all absolute font sizes (px) will be divided by this value. This is useful if you specified a text size multiplier when converting HTML to the attributed string you are processing.
|
||||
*/
|
||||
@property (nonatomic, assign) CGFloat textScale;
|
||||
|
||||
/**
|
||||
If YES, preserve whitespaces in HTML by using "Apple-converted-space". Default YES.
|
||||
*/
|
||||
@property (nonatomic, assign) BOOL useAppleConvertedSpace;
|
||||
|
||||
/**
|
||||
The attributed string that the writer is processing.
|
||||
*/
|
||||
@property (nonatomic, readonly) NSAttributedString *attributedString;
|
||||
|
||||
|
||||
/**
|
||||
The HTML element tag name to use for paragraphs. Defaults to @"p".
|
||||
*/
|
||||
@property (nonatomic, strong) NSString *paragraphTagName;
|
||||
|
||||
@end
|
||||
+1038
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// DTHTMLElementHR.h
|
||||
// DTCoreText
|
||||
//
|
||||
// Created by Oliver Drobnik on 26.12.12.
|
||||
// Copyright (c) 2012 Drobnik.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import "DTHTMLElement.h"
|
||||
|
||||
/**
|
||||
Specialized subclass of <DTHTMLElement> that deals with list items.
|
||||
*/
|
||||
|
||||
@interface DTHorizontalRuleHTMLElement : DTHTMLElement
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,30 @@
|
||||
//
|
||||
// DTHTMLElementHR.m
|
||||
// DTCoreText
|
||||
//
|
||||
// Created by Oliver Drobnik on 26.12.12.
|
||||
// Copyright (c) 2012 Drobnik.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import "DTHorizontalRuleHTMLElement.h"
|
||||
|
||||
@implementation DTHorizontalRuleHTMLElement
|
||||
|
||||
- (NSDictionary *)attributesForAttributedStringRepresentation
|
||||
{
|
||||
NSMutableDictionary *dict = [[super attributesForAttributedStringRepresentation] mutableCopy];
|
||||
[dict setObject:[NSNumber numberWithBool:YES] forKey:DTHorizontalRuleStyleAttribute];
|
||||
|
||||
return dict;
|
||||
}
|
||||
|
||||
- (NSAttributedString *)attributedString
|
||||
{
|
||||
@synchronized(self)
|
||||
{
|
||||
NSDictionary *attributes = [self attributesForAttributedStringRepresentation];
|
||||
return [[NSAttributedString alloc] initWithString:@"\n" attributes:attributes];
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// DTTextAttachmentIframe.h
|
||||
// DTCoreText
|
||||
//
|
||||
// Created by Oliver Drobnik on 22.04.13.
|
||||
// Copyright (c) 2013 Drobnik.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import "DTTextAttachment.h"
|
||||
|
||||
/**
|
||||
A specialized subclass in the DTTextAttachment class cluster to represent an IFRAME
|
||||
*/
|
||||
|
||||
@interface DTIframeTextAttachment : DTTextAttachment <DTTextAttachmentHTMLPersistence>
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,115 @@
|
||||
//
|
||||
// DTTextAttachmentIframe.m
|
||||
// DTCoreText
|
||||
//
|
||||
// Created by Oliver Drobnik on 22.04.13.
|
||||
// Copyright (c) 2013 Drobnik.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import "DTIframeTextAttachment.h"
|
||||
#import "DTCoreTextConstants.h"
|
||||
#import "DTHTMLElement.h"
|
||||
#import "NSString+HTML.h"
|
||||
|
||||
@implementation DTIframeTextAttachment
|
||||
|
||||
- (id)initWithElement:(DTHTMLElement *)element options:(NSDictionary *)options
|
||||
{
|
||||
self = [super initWithElement:element options:options];
|
||||
|
||||
if (self)
|
||||
{
|
||||
// get base URL
|
||||
NSURL *baseURL = [options objectForKey:NSBaseURLDocumentOption];
|
||||
NSString *src = [element.attributes objectForKey:@"src"];
|
||||
|
||||
// prepend http: if URL string starts with // (seems to do with youtube iframes as standard)
|
||||
if ([src hasPrefix:@"//"]) {
|
||||
src = [@"http:" stringByAppendingString:src];
|
||||
}
|
||||
|
||||
// content URL
|
||||
_contentURL = [NSURL URLWithString:src relativeToURL:baseURL];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark - DTTextAttachmentHTMLEncoding
|
||||
|
||||
- (NSString *)stringByEncodingAsHTML
|
||||
{
|
||||
NSMutableString *retString = [NSMutableString string];
|
||||
|
||||
[retString appendString:@"<iframe"];
|
||||
|
||||
if (_contentURL)
|
||||
{
|
||||
[retString appendFormat:@" src=\"%@\"", [_contentURL absoluteString]];
|
||||
}
|
||||
|
||||
// build style for img/video
|
||||
NSMutableString *styleString = [NSMutableString string];
|
||||
|
||||
switch (_verticalAlignment)
|
||||
{
|
||||
case DTTextAttachmentVerticalAlignmentBaseline:
|
||||
{
|
||||
// [classStyleString appendString:@"vertical-align:baseline;"];
|
||||
break;
|
||||
}
|
||||
case DTTextAttachmentVerticalAlignmentTop:
|
||||
{
|
||||
[styleString appendString:@"vertical-align:text-top;"];
|
||||
break;
|
||||
}
|
||||
case DTTextAttachmentVerticalAlignmentCenter:
|
||||
{
|
||||
[styleString appendString:@"vertical-align:middle;"];
|
||||
break;
|
||||
}
|
||||
case DTTextAttachmentVerticalAlignmentBottom:
|
||||
{
|
||||
[styleString appendString:@"vertical-align:text-bottom;"];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (_originalSize.width>0)
|
||||
{
|
||||
[styleString appendFormat:@"width:%.0fpx;", _originalSize.width];
|
||||
}
|
||||
|
||||
if (_originalSize.height>0)
|
||||
{
|
||||
[styleString appendFormat:@"height:%.0fpx;", _originalSize.height];
|
||||
}
|
||||
|
||||
// add local style for size, since sizes might vary quite a bit
|
||||
if ([styleString length])
|
||||
{
|
||||
[retString appendFormat:@" style=\"%@\"", styleString];
|
||||
}
|
||||
|
||||
// attach the attributes dictionary
|
||||
NSMutableDictionary *tmpAttributes = [_attributes mutableCopy];
|
||||
|
||||
// remove src,style, width and height we already have these
|
||||
[tmpAttributes removeObjectForKey:@"src"];
|
||||
[tmpAttributes removeObjectForKey:@"style"];
|
||||
[tmpAttributes removeObjectForKey:@"width"];
|
||||
[tmpAttributes removeObjectForKey:@"height"];
|
||||
|
||||
for (__strong NSString *oneKey in [tmpAttributes allKeys])
|
||||
{
|
||||
oneKey = [oneKey stringByAddingHTMLEntities];
|
||||
NSString *value = [[tmpAttributes objectForKey:oneKey] stringByAddingHTMLEntities];
|
||||
[retString appendFormat:@" %@=\"%@\"", oneKey, value];
|
||||
}
|
||||
|
||||
[retString appendString:@" />"];
|
||||
|
||||
return retString;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,49 @@
|
||||
//
|
||||
// DTImage+HTML.h
|
||||
// DTCoreText
|
||||
//
|
||||
// Created by Oliver Drobnik on 1/9/11.
|
||||
// Copyright 2011 Drobnik.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import "DTCompatibility.h"
|
||||
|
||||
#if TARGET_OS_IPHONE
|
||||
|
||||
#import <UIKit/UIImage.h>
|
||||
|
||||
/**
|
||||
Category used to have the same method available for unit testing on Mac on iOS.
|
||||
*/
|
||||
@interface UIImage (HTML)
|
||||
|
||||
/**
|
||||
Retrieve the NSData representation of a UIImage. Used to encode UIImages in DTTextAttachments.
|
||||
|
||||
@returns The NSData representation of the UIImage instance receiving this message. Convenience method for UIImagePNGRepresentation().
|
||||
*/
|
||||
- (NSData *)dataForPNGRepresentation;
|
||||
|
||||
@end
|
||||
|
||||
#endif
|
||||
|
||||
#if TARGET_OS_OSX
|
||||
#import <AppKit/NSImage.h>
|
||||
|
||||
/**
|
||||
Category used to have the same method available for unit testing on Mac on iOS.
|
||||
*/
|
||||
@interface NSImage (HTML)
|
||||
|
||||
|
||||
/**
|
||||
Retrieve the NSData representation of a NSImage.
|
||||
|
||||
@returns The NSData representation of the NSImage instance receiving this message.
|
||||
*/
|
||||
- (NSData *)dataForPNGRepresentation;
|
||||
|
||||
@end
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,39 @@
|
||||
//
|
||||
// DTImage+HTML.m
|
||||
// DTCoreText
|
||||
//
|
||||
// Created by Oliver Drobnik on 31.01.12.
|
||||
// Copyright (c) 2012 Drobnik.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import "DTImage+HTML.h"
|
||||
|
||||
#if TARGET_OS_IPHONE
|
||||
|
||||
@implementation UIImage (HTML)
|
||||
|
||||
- (NSData *)dataForPNGRepresentation
|
||||
{
|
||||
return UIImagePNGRepresentation(self);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#endif
|
||||
|
||||
#if TARGET_OS_OSX
|
||||
|
||||
@implementation NSImage (HTML)
|
||||
|
||||
- (NSData *)dataForPNGRepresentation
|
||||
{
|
||||
[self lockFocus];
|
||||
NSBitmapImageRep *bitmapRep = [[NSBitmapImageRep alloc] initWithFocusedViewRect:NSMakeRect(0, 0, self.size.width, self.size.height)];
|
||||
[self unlockFocus];
|
||||
|
||||
return [bitmapRep representationUsingType:NSPNGFileType properties:[NSDictionary new]];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,42 @@
|
||||
//
|
||||
// DTTextAttachmentImage.h
|
||||
// DTCoreText
|
||||
//
|
||||
// Created by Oliver Drobnik on 22.04.13.
|
||||
// Copyright (c) 2013 Drobnik.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import "DTCompatibility.h"
|
||||
#import "DTTextAttachment.h"
|
||||
|
||||
@class DTImage;
|
||||
|
||||
/**
|
||||
A specialized subclass in the DTTextAttachment class cluster to represent an embedded image
|
||||
*/
|
||||
|
||||
@interface DTImageTextAttachment : DTTextAttachment <DTTextAttachmentDrawing, DTTextAttachmentHTMLPersistence>
|
||||
|
||||
/**
|
||||
The designated initializer which will be called by [DTTextAttachment textAttachmentWithElement:options:] for image attachments.
|
||||
@param element A DTHTMLElement that must have a valid tag name and should have a size. Any element attributes are copied to the text attachment's elements.
|
||||
@param options An NSDictionary of options. Used to specify the max image size with the key DTMaxImageSize.
|
||||
*/
|
||||
- (id)initWithElement:(DTHTMLElement *)element options:(NSDictionary *)options;
|
||||
|
||||
/**
|
||||
@name Alternate Representations
|
||||
*/
|
||||
|
||||
/**
|
||||
Retrieves a string which is in the format "data:image/png;base64,%@" with this DTTextAttachment's content's data representation encoded in Base64 string encoding. For image contents only.
|
||||
@returns A Base64 encoded string of the png data representation of this text attachment's image contents.
|
||||
*/
|
||||
- (NSString *)dataURLRepresentation;
|
||||
|
||||
/**
|
||||
The image represented by the receiver
|
||||
*/
|
||||
@property (nonatomic, strong) DTImage *image;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,501 @@
|
||||
//
|
||||
// DTImageTextAttachment.m
|
||||
// DTCoreText
|
||||
//
|
||||
// Created by Oliver Drobnik on 22.04.13.
|
||||
// Copyright (c) 2013 Drobnik.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import "DTImageTextAttachment.h"
|
||||
|
||||
#import "DTCoreTextConstants.h"
|
||||
#import "DTHTMLElement.h"
|
||||
#import "NSString+CSS.h"
|
||||
#import "NSString+HTML.h"
|
||||
#import "DTImage+HTML.h"
|
||||
|
||||
#if TARGET_OS_IPHONE
|
||||
#import <DTFoundation/DTAnimatedGIF.h>
|
||||
#endif
|
||||
|
||||
#import <DTFoundation/DTBase64Coding.h>
|
||||
|
||||
static NSCache *imageCache = nil;
|
||||
|
||||
@interface DTImageTextAttachment () // private stuff
|
||||
|
||||
+ (NSCache *)sharedImageCache;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation DTImageTextAttachment
|
||||
{
|
||||
DTImage *_image;
|
||||
}
|
||||
|
||||
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
|
||||
self = [super initWithCoder:aDecoder];
|
||||
if (self) {
|
||||
_image = [aDecoder decodeObjectForKey:@"image"];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)encodeWithCoder:(NSCoder *)aCoder {
|
||||
[super encodeWithCoder:aCoder];
|
||||
[aCoder encodeObject:_image forKey:@"image"];
|
||||
}
|
||||
|
||||
- (id)initWithElement:(DTHTMLElement *)element options:(NSDictionary *)options
|
||||
{
|
||||
self = [super initWithElement:element options:options];
|
||||
|
||||
if (self)
|
||||
{
|
||||
[self _decodeImageFromElement:element options:options];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (id)initWithImage:(DTImage *)image
|
||||
{
|
||||
self = [super init];
|
||||
|
||||
if (self)
|
||||
{
|
||||
self.image = image;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
|
||||
- (void)_decodeImageFromElement:(DTHTMLElement *)element options:(NSDictionary *)options
|
||||
{
|
||||
// get base URL
|
||||
NSURL *baseURL = [options objectForKey:NSBaseURLDocumentOption];
|
||||
NSString *src = [element.attributes objectForKey:@"src"];
|
||||
|
||||
NSURL *contentURL = nil;
|
||||
|
||||
// decode content URL
|
||||
if ([src length]) // guard against img with no src
|
||||
{
|
||||
if ([src hasPrefix:@"data:"])
|
||||
{
|
||||
NSString *cleanStr = [[src componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] componentsJoinedByString:@""];
|
||||
|
||||
NSURL *dataURL = [NSURL URLWithString:cleanStr];
|
||||
|
||||
// try native decoding first
|
||||
NSData *decodedData = [NSData dataWithContentsOfURL:dataURL];
|
||||
|
||||
// try own base64 decoding
|
||||
if (!decodedData)
|
||||
{
|
||||
NSRange range = [cleanStr rangeOfString:@"base64,"];
|
||||
|
||||
if (range.length)
|
||||
{
|
||||
NSString *encodedData = [cleanStr substringFromIndex:range.location + range.length];
|
||||
|
||||
decodedData = [DTBase64Coding dataByDecodingString:encodedData];
|
||||
}
|
||||
}
|
||||
|
||||
// if we have image data, get the default display size
|
||||
if (decodedData)
|
||||
{
|
||||
DTImage *decodedImage = [[DTImage alloc] initWithData:decodedData];
|
||||
|
||||
// we don't know the content scale from such images, need to infer it from size in style
|
||||
NSString *styles = [element.attributes objectForKey:@"style"];
|
||||
|
||||
// that only works if there is a style dictionary
|
||||
if (styles)
|
||||
{
|
||||
NSDictionary *attributes = [styles dictionaryOfCSSStyles];
|
||||
|
||||
NSString *widthStr = attributes[@"width"];
|
||||
NSString *heightStr = attributes[@"height"];
|
||||
|
||||
if ([widthStr hasSuffix:@"px"] && [heightStr hasSuffix:@"px"])
|
||||
{
|
||||
CGSize sizeAccordingToStyle;
|
||||
|
||||
// those style size values are the original image size
|
||||
sizeAccordingToStyle.width = [widthStr pixelSizeOfCSSMeasureRelativeToCurrentTextSize:0 textScale:1];
|
||||
sizeAccordingToStyle.height = [heightStr pixelSizeOfCSSMeasureRelativeToCurrentTextSize:0 textScale:1];
|
||||
|
||||
// if _orgiginal width and height are a fraction of decode image size, it must be a scaled image
|
||||
if (sizeAccordingToStyle.width != 0 && sizeAccordingToStyle.width < decodedImage.size.width &&
|
||||
sizeAccordingToStyle.height != 0 && sizeAccordingToStyle.height < decodedImage.size.height)
|
||||
{
|
||||
// determine image scale
|
||||
CGFloat scale = round(decodedImage.size.width/sizeAccordingToStyle.width);
|
||||
|
||||
// sanity check, accept from @2x - @5x
|
||||
if (scale>=2.0 && scale<=5.0)
|
||||
{
|
||||
#if TARGET_OS_IPHONE
|
||||
// on iOS change the scale by making a new image with same pixels
|
||||
decodedImage = [DTImage imageWithCGImage:decodedImage.CGImage scale:scale orientation:decodedImage.imageOrientation];
|
||||
#else
|
||||
// on OS X we can set the size
|
||||
[decodedImage setSize:sizeAccordingToStyle];
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.image = decodedImage;
|
||||
|
||||
// prevent remote loading of image
|
||||
_contentURL = nil;
|
||||
}
|
||||
}
|
||||
else // normal URL
|
||||
{
|
||||
contentURL = [NSURL URLWithString:src];
|
||||
|
||||
if (!contentURL)
|
||||
{
|
||||
src = [src stringByAddingHTMLEntities];
|
||||
contentURL = [NSURL URLWithString:src relativeToURL:baseURL];
|
||||
}
|
||||
|
||||
if (!contentURL)
|
||||
{
|
||||
src = [src stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]];
|
||||
contentURL = [NSURL URLWithString:src];
|
||||
}
|
||||
|
||||
if (![contentURL scheme])
|
||||
{
|
||||
// possibly a relative url
|
||||
if (baseURL)
|
||||
{
|
||||
contentURL = [NSURL URLWithString:src relativeToURL:baseURL];
|
||||
}
|
||||
else
|
||||
{
|
||||
// file in app bundle
|
||||
NSBundle *bundle = [NSBundle bundleForClass:[self class]];
|
||||
NSString *path = [bundle pathForResource:src ofType:nil];
|
||||
|
||||
if (path)
|
||||
{
|
||||
// Prevent a crash if path turns up nil.
|
||||
contentURL = [NSURL fileURLWithPath:path];
|
||||
}
|
||||
else
|
||||
{
|
||||
// might also be in a different bundle, e.g. when unit testing
|
||||
bundle = [NSBundle bundleForClass:[DTTextAttachment class]];
|
||||
|
||||
path = [bundle pathForResource:src ofType:nil];
|
||||
if (path)
|
||||
{
|
||||
// Prevent a crash if path turns up nil.
|
||||
contentURL = [NSURL fileURLWithPath:path];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if it's a local file we need to inspect it to get it's dimensions
|
||||
if (_displaySize.width==0 || _displaySize.height==0)
|
||||
{
|
||||
DTImage *image = _image;
|
||||
|
||||
// let's check if we have a cached image already then we can inspect that
|
||||
if (!_image)
|
||||
{
|
||||
image = [[DTImageTextAttachment sharedImageCache] objectForKey:[contentURL absoluteString]];
|
||||
}
|
||||
|
||||
if (!image)
|
||||
{
|
||||
// only local files we can directly load without punishment
|
||||
if ([contentURL isFileURL])
|
||||
{
|
||||
#if TARGET_OS_IPHONE
|
||||
NSString *ext = [[[contentURL lastPathComponent] pathExtension] lowercaseString];
|
||||
|
||||
if ([ext isEqualToString:@"gif"])
|
||||
{
|
||||
image = DTAnimatedGIFFromFile([contentURL path]);
|
||||
}
|
||||
else
|
||||
#endif
|
||||
{
|
||||
image = [[DTImage alloc] initWithContentsOfFile:[contentURL path]];
|
||||
}
|
||||
}
|
||||
|
||||
// cache that for later
|
||||
if (image)
|
||||
{
|
||||
[[DTImageTextAttachment sharedImageCache] setObject:image forKey:[contentURL absoluteString]];
|
||||
}
|
||||
}
|
||||
|
||||
// we have an image, so we can set the original size and default display size
|
||||
if (image)
|
||||
{
|
||||
_contentURL = nil;
|
||||
[self _updateSizesFromImage:image];
|
||||
}
|
||||
}
|
||||
|
||||
// only remote images should have a URL
|
||||
_contentURL = contentURL;
|
||||
}
|
||||
|
||||
- (void)_updateSizesFromImage:(DTImage *)image
|
||||
{
|
||||
// set original size if there is none set yet
|
||||
if (CGSizeEqualToSize(_originalSize, CGSizeZero))
|
||||
{
|
||||
_originalSize = image.size;
|
||||
}
|
||||
else
|
||||
{
|
||||
// get the other dimension if one is missing
|
||||
|
||||
if (_originalSize.width==0 && _originalSize.height!=0)
|
||||
{
|
||||
CGFloat factor = _originalSize.height/image.size.height;
|
||||
_originalSize.width = image.size.width * factor;
|
||||
}
|
||||
else if (_originalSize.width!=0 && _originalSize.height==0)
|
||||
{
|
||||
CGFloat factor = _originalSize.width/image.size.width;
|
||||
_originalSize.height = image.size.height * factor;
|
||||
}
|
||||
}
|
||||
|
||||
// initial display size matches original
|
||||
if (CGSizeEqualToSize(CGSizeZero, _displaySize))
|
||||
{
|
||||
[self setDisplaySize:_originalSize withMaxDisplaySize:_maxImageSize];
|
||||
}
|
||||
else
|
||||
{
|
||||
// get the other dimension if one is missing
|
||||
|
||||
if (_displaySize.width==0 && _displaySize.height!=0)
|
||||
{
|
||||
CGSize newDisplaySize = _displaySize;
|
||||
|
||||
CGFloat factor = _displaySize.height/_originalSize.height;
|
||||
newDisplaySize.width = _originalSize.width * factor;
|
||||
|
||||
[self setDisplaySize:newDisplaySize withMaxDisplaySize:_maxImageSize];
|
||||
}
|
||||
else if (_displaySize.width!=0 && _displaySize.height==0)
|
||||
{
|
||||
CGSize newDisplaySize = _displaySize;
|
||||
|
||||
CGFloat factor = _displaySize.width/_originalSize.width;
|
||||
newDisplaySize.height = _originalSize.height * factor;
|
||||
|
||||
[self setDisplaySize:newDisplaySize withMaxDisplaySize:_maxImageSize];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+ (NSCache *)sharedImageCache {
|
||||
if (imageCache) return imageCache;
|
||||
|
||||
static dispatch_once_t onceToken; // lock
|
||||
dispatch_once(&onceToken, ^{ // this block run only once
|
||||
imageCache = [[NSCache alloc] init];
|
||||
});
|
||||
return imageCache;
|
||||
}
|
||||
|
||||
#pragma mark - Alternative Representations
|
||||
|
||||
// makes a data URL of the image
|
||||
- (NSString *)dataURLRepresentation
|
||||
{
|
||||
DTImage *image = self.image;
|
||||
|
||||
if (!image)
|
||||
{
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSData *data = [image dataForPNGRepresentation];
|
||||
NSString *encoded = [DTBase64Coding stringByEncodingData:data];
|
||||
|
||||
return [@"data:image/png;base64," stringByAppendingString:encoded];
|
||||
}
|
||||
|
||||
#pragma mark - DTTextAttachmentDrawing
|
||||
|
||||
- (void)drawInRect:(CGRect)rect context:(CGContextRef)context
|
||||
{
|
||||
[self.image drawInRect:rect];
|
||||
}
|
||||
|
||||
#pragma mark - DTTextAttachmentHTMLEncoding
|
||||
|
||||
- (NSString *)stringByEncodingAsHTML
|
||||
{
|
||||
NSMutableString *retString = [NSMutableString string];
|
||||
NSString *urlString;
|
||||
|
||||
if (_contentURL)
|
||||
{
|
||||
|
||||
if ([_contentURL isFileURL])
|
||||
{
|
||||
NSString *path = [_contentURL path];
|
||||
|
||||
NSRange range = [path rangeOfString:@".app/"];
|
||||
|
||||
if (range.length)
|
||||
{
|
||||
urlString = [path substringFromIndex:NSMaxRange(range)];
|
||||
}
|
||||
else
|
||||
{
|
||||
urlString = [_contentURL absoluteString];
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
urlString = [_contentURL relativeString];
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
urlString = [self dataURLRepresentation];
|
||||
}
|
||||
|
||||
// output tag start
|
||||
[retString appendString:@"<img"];
|
||||
|
||||
// build style for img/video
|
||||
NSMutableString *styleString = [NSMutableString string];
|
||||
|
||||
switch (_verticalAlignment)
|
||||
{
|
||||
case DTTextAttachmentVerticalAlignmentBaseline:
|
||||
{
|
||||
// [classStyleString appendString:@"vertical-align:baseline;"];
|
||||
break;
|
||||
}
|
||||
case DTTextAttachmentVerticalAlignmentTop:
|
||||
{
|
||||
[styleString appendString:@"vertical-align:text-top;"];
|
||||
break;
|
||||
}
|
||||
case DTTextAttachmentVerticalAlignmentCenter:
|
||||
{
|
||||
[styleString appendString:@"vertical-align:middle;"];
|
||||
break;
|
||||
}
|
||||
case DTTextAttachmentVerticalAlignmentBottom:
|
||||
{
|
||||
[styleString appendString:@"vertical-align:text-bottom;"];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (_originalSize.width>0)
|
||||
{
|
||||
[styleString appendFormat:@"width:%.0fpx;", _originalSize.width];
|
||||
}
|
||||
|
||||
if (_originalSize.height>0)
|
||||
{
|
||||
[styleString appendFormat:@"height:%.0fpx;", _originalSize.height];
|
||||
}
|
||||
|
||||
// add local style for size, since sizes might vary quite a bit
|
||||
if ([styleString length])
|
||||
{
|
||||
[retString appendFormat:@" style=\"%@\"", styleString];
|
||||
}
|
||||
|
||||
[retString appendFormat:@" src=\"%@\"", urlString];
|
||||
|
||||
// attach the attributes dictionary
|
||||
NSMutableDictionary *tmpAttributes = [_attributes mutableCopy];
|
||||
|
||||
// remove src,style, width and height we already have these
|
||||
[tmpAttributes removeObjectForKey:@"src"];
|
||||
[tmpAttributes removeObjectForKey:@"style"];
|
||||
[tmpAttributes removeObjectForKey:@"width"];
|
||||
[tmpAttributes removeObjectForKey:@"height"];
|
||||
|
||||
for (__strong NSString *oneKey in [tmpAttributes allKeys])
|
||||
{
|
||||
oneKey = [oneKey stringByAddingHTMLEntities];
|
||||
NSString *value = [[tmpAttributes objectForKey:oneKey] stringByAddingHTMLEntities];
|
||||
[retString appendFormat:@" %@=\"%@\"", oneKey, value];
|
||||
}
|
||||
|
||||
// end
|
||||
[retString appendString:@" />"];
|
||||
|
||||
return retString;
|
||||
}
|
||||
|
||||
#pragma mark - Properties
|
||||
|
||||
/**
|
||||
Accessor for the contents instance variable. If the content type is DTTextAttachmentTypeImage this returns a DTImage instance of the contents.
|
||||
@returns Contents. If it is an image, a DTImage instance is returned. Otherwise it is returned as is.
|
||||
*/
|
||||
- (DTImage *)image
|
||||
{
|
||||
if (!_image)
|
||||
{
|
||||
if (_contentURL)
|
||||
{
|
||||
DTImage *image = [[DTImageTextAttachment sharedImageCache] objectForKey:[_contentURL absoluteString]];
|
||||
|
||||
// only local files can be loaded into cache
|
||||
if (!image && [_contentURL isFileURL])
|
||||
{
|
||||
image = [[DTImage alloc] initWithContentsOfFile:[_contentURL path]];
|
||||
|
||||
// cache it
|
||||
if (image)
|
||||
{
|
||||
[[DTImageTextAttachment sharedImageCache] setObject:image forKey:[_contentURL absoluteString]];
|
||||
}
|
||||
}
|
||||
|
||||
return image;
|
||||
}
|
||||
}
|
||||
|
||||
return _image;
|
||||
}
|
||||
|
||||
- (void)setImage:(DTImage *)image
|
||||
{
|
||||
if (_image != image)
|
||||
{
|
||||
_image = image;
|
||||
|
||||
[self _updateSizesFromImage:_image];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setDisplaySize:(CGSize)displaySize
|
||||
{
|
||||
_displaySize = displaySize;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,87 @@
|
||||
//
|
||||
// DTLazyImageView.h
|
||||
// DTCoreText
|
||||
//
|
||||
// Created by Oliver Drobnik on 5/20/11.
|
||||
// Copyright 2011 Drobnik.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import "DTCompatibility.h"
|
||||
|
||||
#if TARGET_OS_IPHONE && !TARGET_OS_WATCH
|
||||
|
||||
#import <DTFoundation/DTWeakSupport.h>
|
||||
#import "DTAttributedTextContentView.h"
|
||||
|
||||
@class DTLazyImageView;
|
||||
|
||||
// Notifications
|
||||
extern NSString * const DTLazyImageViewWillStartDownloadNotification;
|
||||
extern NSString * const DTLazyImageViewDidFinishDownloadNotification;
|
||||
|
||||
/**
|
||||
Protocol for delegates of <DTLazyImageView> to inform them about the downloaded image dimensions.
|
||||
*/
|
||||
@protocol DTLazyImageViewDelegate <NSObject>
|
||||
@optional
|
||||
|
||||
/**
|
||||
Method that informs the delegate about the image size so that it can re-layout text.
|
||||
@param lazyImageView The image view
|
||||
@param size The image size that is now known
|
||||
*/
|
||||
- (void)lazyImageView:(DTLazyImageView *)lazyImageView didChangeImageSize:(CGSize)size;
|
||||
@end
|
||||
|
||||
/**
|
||||
This `UIImageView` subclass lazily loads an image from a URL and informs a delegate once the size of the image is known.
|
||||
*/
|
||||
|
||||
@interface DTLazyImageView : UIImageView
|
||||
|
||||
/**
|
||||
@name Providing Content
|
||||
*/
|
||||
|
||||
/**
|
||||
The URL of the remote image
|
||||
*/
|
||||
@property (nonatomic, strong) NSURL *url;
|
||||
|
||||
/**
|
||||
The URL Request that is to be used for downloading the image. If this is left `nil` the a new URL Request will be created
|
||||
*/
|
||||
@property (nonatomic, strong) NSMutableURLRequest *urlRequest;
|
||||
|
||||
/**
|
||||
The DTAttributedTextContentView used to display remote images with DTAttributedTextCell
|
||||
*/
|
||||
@property (nonatomic, DT_WEAK_PROPERTY) DTAttributedTextContentView *contentView;
|
||||
|
||||
/**
|
||||
@name Getting Information
|
||||
*/
|
||||
|
||||
/**
|
||||
Set to `YES` to support progressive display of progressive downloads
|
||||
*/
|
||||
@property (nonatomic, assign) BOOL shouldShowProgressiveDownload;
|
||||
|
||||
/**
|
||||
The delegate, conforming to <DTLazyImageViewDelegate>, to inform when the image dimensions were determined
|
||||
*/
|
||||
@property (nonatomic, DT_WEAK_PROPERTY) id<DTLazyImageViewDelegate> delegate;
|
||||
|
||||
|
||||
/**
|
||||
@name Cancelling Download
|
||||
*/
|
||||
|
||||
/**
|
||||
Cancels the image downloading
|
||||
*/
|
||||
- (void)cancelLoading;
|
||||
|
||||
@end
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,420 @@
|
||||
//
|
||||
// DTLazyImageView.m
|
||||
// DTCoreText
|
||||
//
|
||||
// Created by Oliver Drobnik on 5/20/11.
|
||||
// Copyright 2011 Drobnik.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import "DTLazyImageView.h"
|
||||
|
||||
#if TARGET_OS_IPHONE && !TARGET_OS_WATCH
|
||||
|
||||
#import <ImageIO/ImageIO.h>
|
||||
|
||||
#import <DTFoundation/DTLog.h>
|
||||
|
||||
static NSCache *_imageCache = nil;
|
||||
|
||||
NSString * const DTLazyImageViewWillStartDownloadNotification = @"DTLazyImageViewWillStartDownloadNotification";
|
||||
NSString * const DTLazyImageViewDidFinishDownloadNotification = @"DTLazyImageViewDidFinishDownloadNotification";
|
||||
|
||||
@interface DTLazyImageView ()
|
||||
#if DTCORETEXT_USES_NSURLSESSION
|
||||
<NSURLSessionDataDelegate>
|
||||
#else
|
||||
<NSURLConnectionDelegate>
|
||||
#endif
|
||||
|
||||
- (void)_notifyDelegate;
|
||||
|
||||
@end
|
||||
|
||||
@implementation DTLazyImageView
|
||||
{
|
||||
NSURL *_url;
|
||||
NSMutableURLRequest *_urlRequest;
|
||||
|
||||
|
||||
#if DTCORETEXT_USES_NSURLSESSION
|
||||
NSURLSessionDataTask *_dataTask;
|
||||
NSURLSession *_session;
|
||||
#else
|
||||
NSURLConnection *_connection;
|
||||
#endif
|
||||
|
||||
NSMutableData *_receivedData;
|
||||
|
||||
/* For progressive download */
|
||||
CGImageSourceRef _imageSource;
|
||||
CGFloat _fullHeight;
|
||||
CGFloat _fullWidth;
|
||||
NSUInteger _expectedSize;
|
||||
|
||||
BOOL shouldShowProgressiveDownload;
|
||||
|
||||
DT_WEAK_VARIABLE id<DTLazyImageViewDelegate> _delegate;
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
_delegate = nil; // to avoid late notification
|
||||
|
||||
#if DTCORETEXT_USES_NSURLSESSION
|
||||
[_dataTask cancel];
|
||||
#else
|
||||
[_connection cancel];
|
||||
#endif
|
||||
|
||||
if (_imageSource) CFRelease(_imageSource);
|
||||
}
|
||||
|
||||
- (void)loadImageAtURL:(NSURL *)url
|
||||
{
|
||||
// local files we don't need to get asynchronously
|
||||
if ([url isFileURL] || [url.scheme isEqualToString:@"data"])
|
||||
{
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
NSData *data = [NSData dataWithContentsOfURL:url];
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[self completeDownloadWithData:data];
|
||||
});
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@autoreleasepool
|
||||
{
|
||||
if (!_urlRequest)
|
||||
{
|
||||
_urlRequest = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:NSURLRequestReturnCacheDataElseLoad timeoutInterval:10.0];
|
||||
}
|
||||
else
|
||||
{
|
||||
[_urlRequest setCachePolicy:NSURLRequestReturnCacheDataElseLoad];
|
||||
[_urlRequest setTimeoutInterval:10.0];
|
||||
}
|
||||
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:DTLazyImageViewWillStartDownloadNotification object:self];
|
||||
|
||||
#if DTCORETEXT_USES_NSURLSESSION
|
||||
|
||||
if (!_session)
|
||||
{
|
||||
_session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:nil];
|
||||
}
|
||||
|
||||
_dataTask = [_session dataTaskWithRequest:_urlRequest];
|
||||
[_dataTask resume];
|
||||
#else
|
||||
_connection = [[NSURLConnection alloc] initWithRequest:_urlRequest delegate:self startImmediately:NO];
|
||||
[_connection scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
|
||||
[_connection start];
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
- (void)didMoveToSuperview
|
||||
{
|
||||
[super didMoveToSuperview];
|
||||
|
||||
if (!self.image && (_url || _urlRequest) &&
|
||||
#if DTCORETEXT_USES_NSURLSESSION
|
||||
!_dataTask
|
||||
#else
|
||||
!_connection
|
||||
#endif
|
||||
&& self.superview)
|
||||
{
|
||||
UIImage *image = [_imageCache objectForKey:_url];
|
||||
|
||||
if (image)
|
||||
{
|
||||
self.image = image;
|
||||
_fullWidth = image.size.width;
|
||||
_fullHeight = image.size.height;
|
||||
|
||||
// this has to be synchronous
|
||||
[self _notifyDelegate];
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
[self loadImageAtURL:_url];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)cancelLoading
|
||||
{
|
||||
#if DTCORETEXT_USES_NSURLSESSION
|
||||
[_dataTask cancel];
|
||||
_dataTask = nil;
|
||||
#else
|
||||
[_connection cancel];
|
||||
_connection = nil;
|
||||
#endif
|
||||
|
||||
_receivedData = nil;
|
||||
}
|
||||
|
||||
#pragma mark Progressive Image
|
||||
-(CGImageRef)newTransitoryImage:(CGImageRef)partialImg
|
||||
{
|
||||
const size_t height = CGImageGetHeight(partialImg);
|
||||
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
|
||||
size_t lFullWidth = (size_t)ceil(_fullWidth);
|
||||
size_t lFullHeight = (size_t)ceil(_fullHeight);
|
||||
CGContextRef bmContext = CGBitmapContextCreate(NULL, lFullWidth, lFullHeight, 8, lFullWidth * 4, colorSpace, kCGBitmapByteOrderDefault | kCGImageAlphaPremultipliedFirst);
|
||||
CGColorSpaceRelease(colorSpace);
|
||||
if (!bmContext)
|
||||
{
|
||||
// fail creating context
|
||||
return NULL;
|
||||
}
|
||||
CGContextDrawImage(bmContext, (CGRect){.origin.x = 0.0f, .origin.y = 0.0f, .size.width = _fullWidth, .size.height = height}, partialImg);
|
||||
CGImageRef goodImageRef = CGBitmapContextCreateImage(bmContext);
|
||||
CGContextRelease(bmContext);
|
||||
return goodImageRef;
|
||||
}
|
||||
|
||||
- (void)createAndShowProgressiveImage
|
||||
{
|
||||
if (!_imageSource)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
/* For progressive download */
|
||||
const NSUInteger totalSize = [_receivedData length];
|
||||
CGImageSourceUpdateData(_imageSource, (__bridge CFDataRef)_receivedData, (totalSize == _expectedSize) ? true : false);
|
||||
|
||||
if (_fullHeight > 0 && _fullWidth > 0)
|
||||
{
|
||||
CGImageRef image = CGImageSourceCreateImageAtIndex(_imageSource, 0, NULL);
|
||||
if (image)
|
||||
{
|
||||
CGImageRef imgTmp = [self newTransitoryImage:image]; // iOS fix to correctly handle JPG see : http://www.cocoabyss.com/mac-os-x/progressive-image-download-imageio/
|
||||
if (imgTmp)
|
||||
{
|
||||
UIImage *uimage = [[UIImage alloc] initWithCGImage:imgTmp];
|
||||
CGImageRelease(imgTmp);
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{ self.image = uimage; } );
|
||||
}
|
||||
CGImageRelease(image);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
CFDictionaryRef properties = CGImageSourceCopyPropertiesAtIndex(_imageSource, 0, NULL);
|
||||
if (properties)
|
||||
{
|
||||
CFTypeRef val = CFDictionaryGetValue(properties, kCGImagePropertyPixelHeight);
|
||||
if (val)
|
||||
CFNumberGetValue(val, kCFNumberFloatType, &_fullHeight);
|
||||
val = CFDictionaryGetValue(properties, kCGImagePropertyPixelWidth);
|
||||
if (val)
|
||||
CFNumberGetValue(val, kCFNumberFloatType, &_fullWidth);
|
||||
CFRelease(properties);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark NSURL Loading
|
||||
|
||||
- (void)_notifyDelegate
|
||||
{
|
||||
if ([self.delegate respondsToSelector:@selector(lazyImageView:didChangeImageSize:)]) {
|
||||
[self.delegate lazyImageView:self didChangeImageSize:CGSizeMake(_fullWidth, _fullHeight)];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)completeDownloadWithData:(NSData *)data
|
||||
{
|
||||
UIImage *image = [[UIImage alloc] initWithData:data];
|
||||
|
||||
self.image = image;
|
||||
_fullWidth = image.size.width;
|
||||
_fullHeight = image.size.height;
|
||||
|
||||
[self _notifyDelegate];
|
||||
|
||||
static dispatch_once_t predicate;
|
||||
|
||||
dispatch_once(&predicate, ^{
|
||||
_imageCache = [[NSCache alloc] init];
|
||||
});
|
||||
|
||||
if (_url)
|
||||
{
|
||||
if (image)
|
||||
{
|
||||
// cache image
|
||||
[_imageCache setObject:image forKey:_url];
|
||||
}
|
||||
else
|
||||
{
|
||||
DTLogWarning(@"Warning, %@ did not get an image for %@", NSStringFromClass([self class]), [_url absoluteString]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if DTCORETEXT_USES_NSURLSESSION
|
||||
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
|
||||
didReceiveResponse:(NSURLResponse *)response
|
||||
completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler
|
||||
#else
|
||||
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
|
||||
#endif
|
||||
{
|
||||
// every time we get an response it might be a forward, so we discard what data we have
|
||||
_receivedData = nil;
|
||||
|
||||
// does not fire for local file URLs
|
||||
if ([response isKindOfClass:[NSHTTPURLResponse class]])
|
||||
{
|
||||
NSHTTPURLResponse *httpResponse = (id)response;
|
||||
|
||||
if (![[httpResponse MIMEType] hasPrefix:@"image"])
|
||||
{
|
||||
#if DTCORETEXT_USES_NSURLSESSION
|
||||
completionHandler(NSURLSessionResponseCancel);
|
||||
#else
|
||||
[self cancelLoading];
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
#if DTCORETEXT_USES_NSURLSESSION
|
||||
completionHandler(NSURLSessionResponseAllow);
|
||||
#endif
|
||||
}
|
||||
|
||||
/* For progressive download */
|
||||
_fullWidth = _fullHeight = -1.0f;
|
||||
_expectedSize = (NSUInteger)[response expectedContentLength];
|
||||
|
||||
_receivedData = [[NSMutableData alloc] init];
|
||||
}
|
||||
|
||||
#if DTCORETEXT_USES_NSURLSESSION
|
||||
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
|
||||
didReceiveData:(NSData *)data
|
||||
#else
|
||||
-(void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
|
||||
#endif
|
||||
{
|
||||
[_receivedData appendData:data];
|
||||
|
||||
if (!&CGImageSourceCreateIncremental || !shouldShowProgressiveDownload)
|
||||
{
|
||||
// don't show progressive
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_imageSource)
|
||||
{
|
||||
_imageSource = CGImageSourceCreateIncremental(NULL);
|
||||
}
|
||||
|
||||
[self createAndShowProgressiveImage];
|
||||
}
|
||||
|
||||
|
||||
- (void)removeFromSuperview
|
||||
{
|
||||
[super removeFromSuperview];
|
||||
|
||||
#if DTCORETEXT_USES_NSURLSESSION
|
||||
[_dataTask cancel];
|
||||
[_session invalidateAndCancel];
|
||||
#else
|
||||
[_connection cancel];
|
||||
#endif
|
||||
}
|
||||
|
||||
#if DTCORETEXT_USES_NSURLSESSION
|
||||
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
|
||||
didCompleteWithError:(nullable NSError *)error
|
||||
#else
|
||||
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
|
||||
#endif
|
||||
{
|
||||
#if DTCORETEXT_USES_NSURLSESSION
|
||||
if (error)
|
||||
{
|
||||
[self connection:nil didFailWithError:error];
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (_receivedData)
|
||||
{
|
||||
[self performSelectorOnMainThread:@selector(completeDownloadWithData:) withObject:_receivedData waitUntilDone:YES];
|
||||
|
||||
_receivedData = nil;
|
||||
}
|
||||
|
||||
#if DTCORETEXT_USES_NSURLSESSION
|
||||
[_session finishTasksAndInvalidate];
|
||||
_dataTask = nil;
|
||||
#else
|
||||
_connection = nil;
|
||||
#endif
|
||||
|
||||
/* For progressive download */
|
||||
if (_imageSource)
|
||||
{
|
||||
CFRelease(_imageSource);
|
||||
_imageSource = NULL;
|
||||
}
|
||||
|
||||
CFRunLoopStop(CFRunLoopGetCurrent());
|
||||
|
||||
// success = no userInfo
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:DTLazyImageViewDidFinishDownloadNotification object:self];
|
||||
}
|
||||
|
||||
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
|
||||
{
|
||||
#if DTCORETEXT_USES_NSURLSESSION
|
||||
[_session invalidateAndCancel];
|
||||
_dataTask = nil;
|
||||
#else
|
||||
_connection = nil;
|
||||
#endif
|
||||
|
||||
_receivedData = nil;
|
||||
|
||||
/* For progressive download */
|
||||
if (_imageSource)
|
||||
{
|
||||
CFRelease(_imageSource);
|
||||
_imageSource = NULL;
|
||||
}
|
||||
|
||||
CFRunLoopStop(CFRunLoopGetCurrent());
|
||||
|
||||
// send completion notification, pack in error as well
|
||||
NSDictionary *userInfo = [NSDictionary dictionaryWithObject:error forKey:@"Error"];
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:DTLazyImageViewDidFinishDownloadNotification object:self userInfo:userInfo];
|
||||
}
|
||||
|
||||
#pragma mark Properties
|
||||
|
||||
- (void) setUrlRequest:(NSMutableURLRequest *)request
|
||||
{
|
||||
_urlRequest = request;
|
||||
self.url = [_urlRequest URL];
|
||||
}
|
||||
|
||||
@synthesize delegate=_delegate;
|
||||
@synthesize shouldShowProgressiveDownload;
|
||||
@synthesize url = _url;
|
||||
@synthesize urlRequest = _urlRequest;
|
||||
|
||||
@end
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,53 @@
|
||||
//
|
||||
// DTLinkButton.h
|
||||
// DTCoreText
|
||||
//
|
||||
// Created by Oliver Drobnik on 1/16/11.
|
||||
// Copyright 2011 Drobnik.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import "DTCompatibility.h"
|
||||
|
||||
#if TARGET_OS_IPHONE && !TARGET_OS_WATCH
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
/**
|
||||
Constant for highlighting notification
|
||||
*/
|
||||
extern NSString *DTLinkButtonDidHighlightNotification;
|
||||
|
||||
/**
|
||||
A button that corresponds to a hyperlink.
|
||||
|
||||
Multiple parts of the same hyperlink synchronize their looks through the guid. You can show link text in a different color for normal and highlighted mode by setting the button images for these states.
|
||||
*/
|
||||
@interface DTLinkButton : UIButton
|
||||
|
||||
|
||||
/**
|
||||
The URL that this button corresponds to.
|
||||
*/
|
||||
@property (nonatomic, copy) NSURL *URL;
|
||||
|
||||
|
||||
/**
|
||||
The unique identifier (GUID) that all parts of the same hyperlink have in common.
|
||||
*/
|
||||
@property (nonatomic, copy) NSString *GUID;
|
||||
|
||||
|
||||
/**
|
||||
The minimum size that the receiver should respond on hits with. Adjusts the bounds if they are smaller than the passed size.
|
||||
*/
|
||||
@property (nonatomic, assign) CGSize minimumHitSize;
|
||||
|
||||
|
||||
/**
|
||||
A Boolean value that determines whether tapping the button causes it to show a gray rounded rectangle. Default is YES.
|
||||
*/
|
||||
@property(nonatomic) BOOL showsTouchWhenHighlighted;
|
||||
|
||||
|
||||
@end
|
||||
|
||||
#endif
|
||||
+185
@@ -0,0 +1,185 @@
|
||||
//
|
||||
// DTLinkButton.m
|
||||
// DTCoreText
|
||||
//
|
||||
// Created by Oliver Drobnik on 1/16/11.
|
||||
// Copyright 2011 Drobnik.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import "DTLinkButton.h"
|
||||
|
||||
#if TARGET_OS_IPHONE && !TARGET_OS_WATCH
|
||||
|
||||
#import "DTCoreText.h"
|
||||
|
||||
// constant for notification
|
||||
NSString *DTLinkButtonDidHighlightNotification = @"DTLinkButtonDidHighlightNotification";
|
||||
|
||||
|
||||
@interface DTLinkButton ()
|
||||
|
||||
- (void)highlightNotification:(NSNotification *)notification;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation DTLinkButton
|
||||
{
|
||||
NSURL *_URL;
|
||||
NSString *_GUID;
|
||||
|
||||
CGSize _minimumHitSize;
|
||||
BOOL _showsTouchWhenHighlighted;
|
||||
}
|
||||
|
||||
- (id)initWithFrame:(CGRect)frame
|
||||
{
|
||||
self = [super initWithFrame:frame];
|
||||
if (self)
|
||||
{
|
||||
self.userInteractionEnabled = YES;
|
||||
self.enabled = YES;
|
||||
self.opaque = NO;
|
||||
|
||||
_showsTouchWhenHighlighted = YES;
|
||||
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(highlightNotification:) name:DTLinkButtonDidHighlightNotification object:nil];
|
||||
}
|
||||
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||||
}
|
||||
|
||||
#pragma mark Drawing the Run
|
||||
|
||||
- (void)drawRect:(CGRect)rect
|
||||
{
|
||||
CGContextRef ctx = UIGraphicsGetCurrentContext();
|
||||
|
||||
if (self.highlighted)
|
||||
{
|
||||
if (_showsTouchWhenHighlighted)
|
||||
{
|
||||
CGRect imageRect = [self contentRectForBounds:self.bounds];
|
||||
|
||||
UIBezierPath *roundedPath = [UIBezierPath bezierPathWithRoundedRect:imageRect cornerRadius:3.0f];
|
||||
CGContextSetGrayFillColor(ctx, 0.73f, 0.4f);
|
||||
[roundedPath fill];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark Utilitiy
|
||||
|
||||
- (void)_adjustBoundsIfNecessary
|
||||
{
|
||||
CGRect bounds = self.bounds;
|
||||
CGFloat widthExtend = 0;
|
||||
CGFloat heightExtend = 0;
|
||||
|
||||
if (bounds.size.width < _minimumHitSize.width)
|
||||
{
|
||||
widthExtend = _minimumHitSize.width - bounds.size.width;
|
||||
}
|
||||
|
||||
if (bounds.size.height < _minimumHitSize.height)
|
||||
{
|
||||
heightExtend = _minimumHitSize.height - bounds.size.height;
|
||||
}
|
||||
|
||||
if (widthExtend>0 || heightExtend>0)
|
||||
{
|
||||
UIEdgeInsets edgeInsets = UIEdgeInsetsMake(ceil(heightExtend/2.0f), ceil(widthExtend/2.0f), ceil(heightExtend/2.0f), ceil(widthExtend/2.0f));
|
||||
|
||||
// extend bounds by the calculated necessary edge insets
|
||||
bounds.size.width += edgeInsets.left + edgeInsets.right;
|
||||
bounds.size.height += edgeInsets.top + edgeInsets.bottom;
|
||||
|
||||
// apply bounds and insets
|
||||
self.bounds = bounds;
|
||||
self.contentEdgeInsets = edgeInsets;
|
||||
}
|
||||
else
|
||||
{
|
||||
self.contentEdgeInsets = UIEdgeInsetsMake(0, 0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark Notifications
|
||||
- (void)highlightNotification:(NSNotification *)notification
|
||||
{
|
||||
if ([notification object] == self)
|
||||
{
|
||||
// that was me
|
||||
return;
|
||||
}
|
||||
|
||||
NSDictionary *userInfo = [notification userInfo];
|
||||
|
||||
NSString *guid = [userInfo objectForKey:@"GUID"];
|
||||
|
||||
if ([guid isEqualToString:_GUID])
|
||||
{
|
||||
BOOL highlighted = [[userInfo objectForKey:@"Highlighted"] boolValue];
|
||||
[super setHighlighted:highlighted];
|
||||
[self setNeedsDisplay];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
#pragma mark Properties
|
||||
|
||||
- (void)setHighlighted:(BOOL)highlighted
|
||||
{
|
||||
[super setHighlighted:highlighted];
|
||||
[self setNeedsDisplay];
|
||||
|
||||
// notify other parts of the same link
|
||||
if (_GUID)
|
||||
{
|
||||
NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithBool:highlighted], @"Highlighted", _GUID, @"GUID", nil];
|
||||
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:DTLinkButtonDidHighlightNotification object:self userInfo:userInfo];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setFrame:(CGRect)frame
|
||||
{
|
||||
[super setFrame:frame];
|
||||
|
||||
if (CGRectIsEmpty(frame))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
[self _adjustBoundsIfNecessary];
|
||||
}
|
||||
|
||||
|
||||
- (void)setMinimumHitSize:(CGSize)minimumHitSize
|
||||
{
|
||||
if (CGSizeEqualToSize(_minimumHitSize, minimumHitSize))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_minimumHitSize = minimumHitSize;
|
||||
|
||||
[self _adjustBoundsIfNecessary];
|
||||
}
|
||||
|
||||
@synthesize URL = _URL;
|
||||
@synthesize GUID = _GUID;
|
||||
|
||||
@synthesize minimumHitSize = _minimumHitSize;
|
||||
@synthesize showsTouchWhenHighlighted = _showsTouchWhenHighlighted;
|
||||
|
||||
@end
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,16 @@
|
||||
//
|
||||
// DTHTMLElementLI.h
|
||||
// DTCoreText
|
||||
//
|
||||
// Created by Oliver Drobnik on 27.12.12.
|
||||
// Copyright (c) 2012 Drobnik.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import "DTHTMLElement.h"
|
||||
|
||||
/**
|
||||
Specialized subclass of <DTHTMLElement> that deals with list items.
|
||||
*/
|
||||
@interface DTListItemHTMLElement : DTHTMLElement
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,339 @@
|
||||
//
|
||||
// DTHTMLElementLI.m
|
||||
// DTCoreText
|
||||
//
|
||||
// Created by Oliver Drobnik on 27.12.12.
|
||||
// Copyright (c) 2012 Drobnik.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import "DTListItemHTMLElement.h"
|
||||
|
||||
#import "DTCompatibility.h"
|
||||
#import "DTCoreTextParagraphStyle.h"
|
||||
#import "DTCoreTextFontDescriptor.h"
|
||||
#import "NSDictionary+DTCoreText.h"
|
||||
#import "DTCSSListStyle.h"
|
||||
#import "NSString+CSS.h"
|
||||
#import "DTImageTextAttachment.h"
|
||||
#import "NSMutableAttributedString+HTML.h"
|
||||
#import "NSAttributedStringRunDelegates.h"
|
||||
|
||||
#if TARGET_OS_IPHONE
|
||||
#import "UIFont+DTCoreText.h"
|
||||
#endif
|
||||
|
||||
@implementation DTListItemHTMLElement
|
||||
|
||||
- (NSUInteger)_indexOfListItemInListRoot:(DTHTMLElement *)listRoot
|
||||
{
|
||||
@synchronized(self)
|
||||
{
|
||||
NSInteger index = -1;
|
||||
|
||||
NSArray *childNodes = [listRoot.childNodes copy];
|
||||
for (DTHTMLElement *oneElement in childNodes)
|
||||
{
|
||||
if ([oneElement isKindOfClass:[DTListItemHTMLElement class]])
|
||||
{
|
||||
index++;
|
||||
}
|
||||
|
||||
if (oneElement == self)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return index;
|
||||
}
|
||||
}
|
||||
|
||||
// calculates the accumulated list indent
|
||||
- (CGFloat)_sumOfListIndents
|
||||
{
|
||||
CGFloat indent = 0;
|
||||
|
||||
DTHTMLElement *element = self.parentElement;
|
||||
|
||||
while (element)
|
||||
{
|
||||
if ([element.name isEqualToString:@"ul"] || [element.name isEqualToString:@"ol"])
|
||||
{
|
||||
indent += element->_listIndent;
|
||||
}
|
||||
else if (element.displayStyle == DTHTMLElementDisplayStyleListItem)
|
||||
{
|
||||
// we accept these
|
||||
indent += element.padding.left;
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
element = element.parentElement;
|
||||
}
|
||||
|
||||
return indent;
|
||||
}
|
||||
|
||||
- (void)applyStyleDictionary:(NSDictionary *)styles
|
||||
{
|
||||
[super applyStyleDictionary:styles];
|
||||
|
||||
CGFloat parentPadding = self.parentElement->_listIndent;
|
||||
CGFloat listIndents = [self _sumOfListIndents];
|
||||
|
||||
self.paragraphStyle.headIndent = listIndents + _padding.left + _margins.left;
|
||||
self.paragraphStyle.firstLineHeadIndent = self.paragraphStyle.headIndent;
|
||||
|
||||
_margins.left += parentPadding;
|
||||
}
|
||||
|
||||
// creates an attributed list prefix
|
||||
- (NSAttributedString *)_listPrefix
|
||||
{
|
||||
DTCoreTextParagraphStyle *paragraphStyle = [[self attributesForAttributedStringRepresentation] paragraphStyle];
|
||||
NSParameterAssert(paragraphStyle);
|
||||
|
||||
DTCoreTextFontDescriptor *fontDescriptor = [[self attributesForAttributedStringRepresentation] fontDescriptor];
|
||||
NSParameterAssert(fontDescriptor);
|
||||
|
||||
DTCSSListStyle *effectiveList = [self.paragraphStyle.textLists lastObject];
|
||||
DTHTMLElement *listRoot = self.parentElement;
|
||||
NSUInteger listCounter = [self _indexOfListItemInListRoot:listRoot]+effectiveList.startingItemNumber;
|
||||
|
||||
// make a temporary version of self that has same font attributes as list root
|
||||
DTListItemHTMLElement *tmpCopy = [[DTListItemHTMLElement alloc] init];
|
||||
[tmpCopy inheritAttributesFromElement:self];
|
||||
|
||||
// take the parents text color
|
||||
tmpCopy.textColor = listRoot.textColor;
|
||||
|
||||
// check for list-style:none modifier
|
||||
NSDictionary *styles = [[self attributeForKey:@"style"] dictionaryOfCSSStyles];
|
||||
|
||||
if (styles)
|
||||
{
|
||||
// make a temp copy
|
||||
effectiveList = [effectiveList copy];
|
||||
|
||||
// update from styles
|
||||
[effectiveList updateFromStyleDictionary:styles];
|
||||
}
|
||||
|
||||
NSDictionary *attributes = [tmpCopy attributesForAttributedStringRepresentation];
|
||||
|
||||
// modify paragraph style
|
||||
paragraphStyle.firstLineHeadIndent = self.paragraphStyle.headIndent - _margins.left - _padding.left;; // first line has prefix and starts at list indent;
|
||||
paragraphStyle.defaultTabInterval = 100;
|
||||
|
||||
// resets tabs
|
||||
paragraphStyle.tabStops = nil;
|
||||
|
||||
// set tab stops
|
||||
if (effectiveList.type != DTCSSListStyleTypeNone)
|
||||
{
|
||||
if (_margins.left<=0)
|
||||
{
|
||||
return nil;
|
||||
}
|
||||
|
||||
// first tab is to right-align bullet, numbering against
|
||||
CGFloat tabOffset = paragraphStyle.headIndent - (CGFloat)5.0; // TODO: change with font size
|
||||
|
||||
#if DTCORETEXT_SUPPORT_NS_ATTRIBUTES
|
||||
[paragraphStyle addTabStopAtPosition:tabOffset alignment:kCTTextAlignmentRight];
|
||||
#else
|
||||
[paragraphStyle addTabStopAtPosition:tabOffset alignment:kCTRightTextAlignment];
|
||||
#endif
|
||||
}
|
||||
|
||||
// second tab is for the beginning of first line after bullet
|
||||
#if DTCORETEXT_SUPPORT_NS_ATTRIBUTES
|
||||
[paragraphStyle addTabStopAtPosition:paragraphStyle.headIndent alignment:kCTTextAlignmentLeft];
|
||||
#else
|
||||
[paragraphStyle addTabStopAtPosition:paragraphStyle.headIndent alignment:kCTLeftTextAlignment];
|
||||
#endif
|
||||
|
||||
NSMutableDictionary *newAttributes = [NSMutableDictionary dictionary];
|
||||
|
||||
// make a font without italic or bold
|
||||
fontDescriptor.boldTrait = NO;
|
||||
fontDescriptor.italicTrait = NO;
|
||||
|
||||
CTFontRef font = [fontDescriptor newMatchingFont];
|
||||
|
||||
if (font)
|
||||
{
|
||||
#if DTCORETEXT_SUPPORT_NS_ATTRIBUTES && __IPHONE_OS_VERSION_MAX_ALLOWED > __IPHONE_5_1
|
||||
if (___useiOS6Attributes)
|
||||
{
|
||||
UIFont *uiFont = [UIFont fontWithCTFont:font];
|
||||
[newAttributes setObject:uiFont forKey:NSFontAttributeName];
|
||||
|
||||
CFRelease(font);
|
||||
}
|
||||
else
|
||||
#endif
|
||||
{
|
||||
[newAttributes setObject:CFBridgingRelease(font) forKey:(id)kCTFontAttributeName];
|
||||
}
|
||||
}
|
||||
|
||||
CGColorRef textColor = (__bridge CGColorRef)[attributes objectForKey:(id)kCTForegroundColorAttributeName];
|
||||
|
||||
if (textColor)
|
||||
{
|
||||
[newAttributes setObject:(__bridge id)textColor forKey:(id)kCTForegroundColorAttributeName];
|
||||
}
|
||||
#if DTCORETEXT_SUPPORT_NS_ATTRIBUTES
|
||||
else if (___useiOS6Attributes)
|
||||
{
|
||||
DTColor *uiColor = [attributes foregroundColor];
|
||||
|
||||
if (uiColor)
|
||||
{
|
||||
[newAttributes setObject:uiColor forKey:NSForegroundColorAttributeName];
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// add paragraph style (this has the tabs)
|
||||
if (paragraphStyle)
|
||||
{
|
||||
#if DTCORETEXT_SUPPORT_NS_ATTRIBUTES
|
||||
if (___useiOS6Attributes)
|
||||
{
|
||||
NSParagraphStyle *style = [paragraphStyle NSParagraphStyle];
|
||||
[newAttributes setObject:style forKey:NSParagraphStyleAttributeName];
|
||||
}
|
||||
else
|
||||
#endif
|
||||
{
|
||||
CTParagraphStyleRef newParagraphStyle = [paragraphStyle createCTParagraphStyle];
|
||||
[newAttributes setObject:CFBridgingRelease(newParagraphStyle) forKey:(id)kCTParagraphStyleAttributeName];
|
||||
}
|
||||
}
|
||||
|
||||
// add textBlock if there's one (this has padding and background color)
|
||||
NSArray *textBlocks = [attributes objectForKey:DTTextBlocksAttribute];
|
||||
if (textBlocks)
|
||||
{
|
||||
[newAttributes setObject:textBlocks forKey:DTTextBlocksAttribute];
|
||||
}
|
||||
|
||||
// transfer all lists so that
|
||||
NSArray *lists = [attributes objectForKey:DTTextListsAttribute];
|
||||
if (lists)
|
||||
{
|
||||
[newAttributes setObject:lists forKey:DTTextListsAttribute];
|
||||
}
|
||||
|
||||
// add a marker so that we know that this is a field/prefix
|
||||
[newAttributes setObject:DTListPrefixField forKey:DTFieldAttribute];
|
||||
|
||||
NSString *prefix = [effectiveList prefixWithCounter:listCounter];
|
||||
|
||||
if (!prefix)
|
||||
{
|
||||
return nil;
|
||||
}
|
||||
|
||||
DTImage *image = nil;
|
||||
|
||||
if (effectiveList.imageName)
|
||||
{
|
||||
image = [DTImage imageNamed:effectiveList.imageName];
|
||||
|
||||
if (!image)
|
||||
{
|
||||
// image invalid
|
||||
effectiveList.imageName = nil;
|
||||
|
||||
prefix = [effectiveList prefixWithCounter:listCounter];
|
||||
}
|
||||
}
|
||||
|
||||
if (!prefix)
|
||||
{
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSMutableAttributedString *tmpStr = [[NSMutableAttributedString alloc] initWithString:prefix attributes:newAttributes];
|
||||
|
||||
if (image)
|
||||
{
|
||||
// make an attachment for the image
|
||||
DTImageTextAttachment *attachment = [[DTImageTextAttachment alloc] init];
|
||||
attachment.image = image;
|
||||
attachment.displaySize = image.size;
|
||||
|
||||
#if DTCORETEXT_SUPPORT_NS_ATTRIBUTES && TARGET_OS_IPHONE
|
||||
// need run delegate for sizing
|
||||
CTRunDelegateRef embeddedObjectRunDelegate = createEmbeddedObjectRunDelegate(attachment);
|
||||
[newAttributes setObject:CFBridgingRelease(embeddedObjectRunDelegate) forKey:(id)kCTRunDelegateAttributeName];
|
||||
#endif
|
||||
|
||||
// add attachment
|
||||
[newAttributes setObject:attachment forKey:NSAttachmentAttributeName];
|
||||
|
||||
if (effectiveList.position == DTCSSListStylePositionInside)
|
||||
{
|
||||
[tmpStr setAttributes:newAttributes range:NSMakeRange(2, 1)];
|
||||
}
|
||||
else
|
||||
{
|
||||
[tmpStr setAttributes:newAttributes range:NSMakeRange(1, 1)];
|
||||
}
|
||||
}
|
||||
|
||||
// estimate width of the prefix
|
||||
NSString *trimmedPrefix = [prefix stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
|
||||
NSAttributedString *tmpAttributedString = [[NSAttributedString alloc] initWithString:trimmedPrefix attributes:newAttributes];
|
||||
|
||||
CTLineRef tmpLine = CTLineCreateWithAttributedString((__bridge CFAttributedStringRef)(tmpAttributedString));
|
||||
double width = CTLineGetTypographicBounds(tmpLine, NULL, NULL, NULL);
|
||||
CFRelease(tmpLine);
|
||||
|
||||
// if the non-whitespace characters are too wide then we omit the prefix
|
||||
if ((width+5.0)>_margins.left)
|
||||
{
|
||||
return nil;
|
||||
}
|
||||
|
||||
return tmpStr;
|
||||
}
|
||||
|
||||
- (NSAttributedString *)attributedString
|
||||
{
|
||||
NSMutableAttributedString *tmpString = [[NSMutableAttributedString alloc] init];
|
||||
|
||||
// append child elements
|
||||
NSAttributedString *childrenString = [super attributedString];
|
||||
|
||||
// append list prefix
|
||||
NSAttributedString *listPrefix = [self _listPrefix];
|
||||
|
||||
if (listPrefix)
|
||||
{
|
||||
[tmpString appendAttributedString:listPrefix];
|
||||
|
||||
// add NL if there is immediately another list prefix following
|
||||
NSString *field = [childrenString attribute:DTFieldAttribute atIndex:0 effectiveRange:NULL];
|
||||
|
||||
if ([field isEqualToString:DTListPrefixField])
|
||||
{
|
||||
[tmpString appendEndOfParagraph];
|
||||
}
|
||||
}
|
||||
|
||||
if (childrenString)
|
||||
{
|
||||
[tmpString appendAttributedString:childrenString];
|
||||
}
|
||||
|
||||
return tmpString;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,22 @@
|
||||
//
|
||||
// DTObjectTextAttachment.h
|
||||
// DTCoreText
|
||||
//
|
||||
// Created by Oliver Drobnik on 22.04.13.
|
||||
// Copyright (c) 2013 Drobnik.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import "DTTextAttachment.h"
|
||||
|
||||
/**
|
||||
A specialized subclass in the DTTextAttachment class cluster to represent an generic object
|
||||
*/
|
||||
|
||||
@interface DTObjectTextAttachment : DTTextAttachment <DTTextAttachmentHTMLPersistence>
|
||||
|
||||
/**
|
||||
The DTHTMLElement child nodes of the receiver. This array is only used for object tags at the moment.
|
||||
*/
|
||||
@property (nonatomic, strong) NSArray *childNodes;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,142 @@
|
||||
//
|
||||
// DTObjectTextAttachment.m
|
||||
// DTCoreText
|
||||
//
|
||||
// Created by Oliver Drobnik on 22.04.13.
|
||||
// Copyright (c) 2013 Drobnik.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import "DTObjectTextAttachment.h"
|
||||
#import "DTCoreTextConstants.h"
|
||||
#import "DTHTMLElement.h"
|
||||
#import "NSString+HTML.h"
|
||||
|
||||
@implementation DTObjectTextAttachment
|
||||
|
||||
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
|
||||
self = [super initWithCoder:aDecoder];
|
||||
if (self) {
|
||||
_childNodes = [aDecoder decodeObjectForKey:@"childNodes"];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)encodeWithCoder:(NSCoder *)aCoder {
|
||||
[super encodeWithCoder:aCoder];
|
||||
[aCoder encodeObject:_childNodes forKey:@"childNodes"];
|
||||
}
|
||||
|
||||
- (id)initWithElement:(DTHTMLElement *)element options:(NSDictionary *)options
|
||||
{
|
||||
self = [super initWithElement:element options:options];
|
||||
|
||||
if (self)
|
||||
{
|
||||
// get base URL
|
||||
NSURL *baseURL = [options objectForKey:NSBaseURLDocumentOption];
|
||||
NSString *src = [element.attributes objectForKey:@"src"];
|
||||
|
||||
// content URL
|
||||
_contentURL = [NSURL URLWithString:src relativeToURL:baseURL];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark - DTTextAttachmentHTMLEncoding
|
||||
|
||||
- (NSString *)stringByEncodingAsHTML
|
||||
{
|
||||
NSMutableString *retString = [NSMutableString string];
|
||||
|
||||
[retString appendString:@"<object"];
|
||||
|
||||
if (_contentURL)
|
||||
{
|
||||
[retString appendFormat:@" src=\"%@\"", [_contentURL absoluteString]];
|
||||
}
|
||||
|
||||
// build style for img/video
|
||||
NSMutableString *styleString = [NSMutableString string];
|
||||
|
||||
switch (_verticalAlignment)
|
||||
{
|
||||
case DTTextAttachmentVerticalAlignmentBaseline:
|
||||
{
|
||||
// [classStyleString appendString:@"vertical-align:baseline;"];
|
||||
break;
|
||||
}
|
||||
case DTTextAttachmentVerticalAlignmentTop:
|
||||
{
|
||||
[styleString appendString:@"vertical-align:text-top;"];
|
||||
break;
|
||||
}
|
||||
case DTTextAttachmentVerticalAlignmentCenter:
|
||||
{
|
||||
[styleString appendString:@"vertical-align:middle;"];
|
||||
break;
|
||||
}
|
||||
case DTTextAttachmentVerticalAlignmentBottom:
|
||||
{
|
||||
[styleString appendString:@"vertical-align:text-bottom;"];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (_originalSize.width>0)
|
||||
{
|
||||
[styleString appendFormat:@"width:%.0fpx;", _originalSize.width];
|
||||
}
|
||||
|
||||
if (_originalSize.height>0)
|
||||
{
|
||||
[styleString appendFormat:@"height:%.0fpx;", _originalSize.height];
|
||||
}
|
||||
|
||||
// add local style for size, since sizes might vary quite a bit
|
||||
if ([styleString length])
|
||||
{
|
||||
[retString appendFormat:@" style=\"%@\"", styleString];
|
||||
}
|
||||
|
||||
// attach the attributes dictionary
|
||||
NSMutableDictionary *tmpAttributes = [_attributes mutableCopy];
|
||||
|
||||
// remove src,style, width and height we already have these
|
||||
[tmpAttributes removeObjectForKey:@"src"];
|
||||
[tmpAttributes removeObjectForKey:@"style"];
|
||||
[tmpAttributes removeObjectForKey:@"width"];
|
||||
[tmpAttributes removeObjectForKey:@"height"];
|
||||
|
||||
for (__strong NSString *oneKey in [tmpAttributes allKeys])
|
||||
{
|
||||
oneKey = [oneKey stringByAddingHTMLEntities];
|
||||
NSString *value = [[tmpAttributes objectForKey:oneKey] stringByAddingHTMLEntities];
|
||||
[retString appendFormat:@" %@=\"%@\"", oneKey, value];
|
||||
}
|
||||
|
||||
if (_childNodes)
|
||||
{
|
||||
[retString appendString:@">"];
|
||||
|
||||
for (DTHTMLElement *oneChild in _childNodes)
|
||||
{
|
||||
[retString appendString:[oneChild debugDescription]];
|
||||
}
|
||||
|
||||
[retString appendString:@"</object>"];
|
||||
}
|
||||
else
|
||||
{
|
||||
[retString appendString:@" />"];
|
||||
}
|
||||
|
||||
|
||||
return retString;
|
||||
}
|
||||
|
||||
#pragma mark - Properties
|
||||
|
||||
@synthesize childNodes = _childNodes;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,23 @@
|
||||
//
|
||||
// DTHTMLElementStylesheet.h
|
||||
// DTCoreText
|
||||
//
|
||||
// Created by Oliver Drobnik on 29.12.12.
|
||||
// Copyright (c) 2012 Drobnik.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import "DTHTMLElement.h"
|
||||
|
||||
@class DTCSSStylesheet;
|
||||
|
||||
/**
|
||||
This is a specialized subclass of <DTHTMLElement> representing a style block.
|
||||
*/
|
||||
@interface DTStylesheetHTMLElement : DTHTMLElement
|
||||
|
||||
/**
|
||||
Parses the text children and assembles the resulting stylesheet.
|
||||
*/
|
||||
- (DTCSSStylesheet *)stylesheet;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,26 @@
|
||||
//
|
||||
// DTHTMLElementStylesheet.m
|
||||
// DTCoreText
|
||||
//
|
||||
// Created by Oliver Drobnik on 29.12.12.
|
||||
// Copyright (c) 2012 Drobnik.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import "DTStylesheetHTMLElement.h"
|
||||
#import "DTCSSStylesheet.h"
|
||||
|
||||
@implementation DTStylesheetHTMLElement
|
||||
|
||||
- (NSAttributedString *)attributedString
|
||||
{
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (DTCSSStylesheet *)stylesheet
|
||||
{
|
||||
NSString *text = [self text];
|
||||
|
||||
return [[DTCSSStylesheet alloc] initWithStyleBlock:text];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,199 @@
|
||||
//
|
||||
// DTTextAttachment.h
|
||||
// DTCoreText
|
||||
//
|
||||
// Created by Oliver on 14.01.11.
|
||||
// Copyright 2011 Drobnik.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import "DTCompatibility.h"
|
||||
|
||||
#if TARGET_OS_IPHONE
|
||||
#elif TARGET_OS_MAC
|
||||
#import <ApplicationServices/ApplicationServices.h>
|
||||
#endif
|
||||
|
||||
#import <CoreText/CoreText.h>
|
||||
|
||||
@class DTHTMLElement;
|
||||
|
||||
/**
|
||||
Text Attachment vertical alignment
|
||||
*/
|
||||
typedef NS_ENUM(NSUInteger, DTTextAttachmentVerticalAlignment)
|
||||
{
|
||||
/**
|
||||
Baseline alignment (default)
|
||||
*/
|
||||
DTTextAttachmentVerticalAlignmentBaseline = 0,
|
||||
|
||||
/**
|
||||
Align with top edge
|
||||
*/
|
||||
DTTextAttachmentVerticalAlignmentTop,
|
||||
|
||||
/**
|
||||
Align with center
|
||||
*/
|
||||
DTTextAttachmentVerticalAlignmentCenter,
|
||||
|
||||
/**
|
||||
Align with bottom edge
|
||||
*/
|
||||
DTTextAttachmentVerticalAlignmentBottom
|
||||
};
|
||||
|
||||
/**
|
||||
Methods to implement for attachments to support inline drawing.
|
||||
*/
|
||||
@protocol DTTextAttachmentDrawing <NSObject>
|
||||
|
||||
/**
|
||||
Draws the contents of the receiver into a graphics context
|
||||
@param rect The rectangle to draw the receiver into
|
||||
@param context The graphics context
|
||||
*/
|
||||
- (void)drawInRect:(CGRect)rect context:(CGContextRef)context;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
/**
|
||||
Methods to implement for attachments to support output to HTML.
|
||||
*/
|
||||
@protocol DTTextAttachmentHTMLPersistence <NSObject>
|
||||
|
||||
/**
|
||||
Creates a HTML representation of the receiver
|
||||
@returns A HTML string with the receiver encoded as HTML
|
||||
*/
|
||||
- (NSString *)stringByEncodingAsHTML;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
/**
|
||||
An object to represent an attachment in an HTML/rich text view.
|
||||
*/
|
||||
@interface DTTextAttachment : NSTextAttachment
|
||||
{
|
||||
CGSize _displaySize; // the display dimensions of the attachment
|
||||
CGSize _originalSize; // the original dimensions of the attachment
|
||||
CGSize _maxImageSize; // the maximum dimensions to size to
|
||||
NSURL *_contentURL;
|
||||
NSDictionary *_attributes; // attributes transferred from HTML element
|
||||
DTTextAttachmentVerticalAlignment _verticalAlignment; // alignment in relation to the baseline
|
||||
}
|
||||
|
||||
/**
|
||||
@name Creating Text Attachments
|
||||
*/
|
||||
|
||||
/**
|
||||
Initialize and return a DTTextAttachment with the specified DTHTMLElement and options. Convenience initializer.
|
||||
The element must have a valid tagName. The size of the returned text attachment is determined by the element, constrained by the option's key for DTMaxImageSize. Any valid image resource included in the element (denoted by the method attributeForKey: "src") is loaded and determines the text attachment size if it was not known before. If a size is too large the image is downsampled with sizeThatFitsKeepingAspectRatio() which preserves the aspect ratio.
|
||||
@param element A DTHTMLElement that must have a valid tag name and should have a size. Any element attributes are copied to the text attachment's elements.
|
||||
@param options An NSDictionary of options. Used to specify the max image size with the key DTMaxImageSize.
|
||||
@returns Returns the appropriate subclass of the class cluster
|
||||
*/
|
||||
+ (DTTextAttachment *)textAttachmentWithElement:(DTHTMLElement *)element options:(NSDictionary *)options;
|
||||
|
||||
/**
|
||||
The designated initializer for members of the DTTextAttachment class cluster. If you need additional setup for custom subclasses then you should override this initializer.
|
||||
@param element A DTHTMLElement that must have a valid tag name and should have a size. Any element attributes are copied to the text attachment's elements.
|
||||
@param options An NSDictionary of options. Used to specify the max image size with the key DTMaxImageSize.
|
||||
@returns Returns an initialized DTTextAttachment built using the element and options parameters. */
|
||||
- (id)initWithElement:(DTHTMLElement *)element options:(NSDictionary *)options;
|
||||
|
||||
/**
|
||||
@name Vertical Alignment
|
||||
*/
|
||||
|
||||
/**
|
||||
Inspects the given font and records the font's ascent, descent and leading. These values are then used during layout to properly respond with ascentForLayout, descentForLayout for the receiver's verticalAlignment.
|
||||
@param font The font to inspect
|
||||
*/
|
||||
- (void)adjustVerticalAlignmentForFont:(CTFontRef)font;
|
||||
|
||||
/**
|
||||
The ascent to use during layout so that the receiver can be display at its verticalAlignment.
|
||||
*/
|
||||
- (CGFloat)ascentForLayout;
|
||||
|
||||
/**
|
||||
The descent to use during layout so that the receiver can be display at its verticalAlignment.
|
||||
*/
|
||||
- (CGFloat)descentForLayout;
|
||||
|
||||
/**
|
||||
The vertical alignment of the receiver
|
||||
*/
|
||||
@property (nonatomic, assign) DTTextAttachmentVerticalAlignment verticalAlignment;
|
||||
|
||||
|
||||
/**
|
||||
@name Retrieving Information about Attachments
|
||||
*/
|
||||
|
||||
/**
|
||||
The size of the receiver according to width/height HTML attribute or CSS style attribute
|
||||
*/
|
||||
@property (nonatomic, assign) CGSize originalSize;
|
||||
|
||||
/**
|
||||
The size to use for displaying/laying out the receiver
|
||||
*/
|
||||
@property (nonatomic, assign) CGSize displaySize;
|
||||
|
||||
/**
|
||||
Updates the display size optionally passing a maximum size that it should not exceed.
|
||||
|
||||
This method in contrast to using the displaySize property will use the originalSize and max display size to calculate missing dimensions.
|
||||
@param displaySize The new size to display the content with
|
||||
@param maxDisplaySize the maximum size that the content should be scaled to fit
|
||||
*/
|
||||
- (void)setDisplaySize:(CGSize)displaySize withMaxDisplaySize:(CGSize)maxDisplaySize;
|
||||
|
||||
/**
|
||||
The URL representing the content
|
||||
*/
|
||||
@property (nonatomic, strong) NSURL *contentURL;
|
||||
|
||||
/**
|
||||
The hyperlink URL of the receiver.
|
||||
*/
|
||||
@property (nonatomic, strong) NSURL *hyperLinkURL;
|
||||
|
||||
/**
|
||||
The GUID of the hyperlink that this is a part of. All parts of a hyperlink have the same GUID so that they can highlight together.
|
||||
*/
|
||||
@property (nonatomic, strong) NSString *hyperLinkGUID;
|
||||
|
||||
/**
|
||||
The attributes dictionary of the attachment.
|
||||
|
||||
If initialized from HTML, the values of this dictionary are transferred from the give HTML element. If you wish to add custom attribute values to be written to and read from HTML, be aware that the attribute name will be lowercased in compliance with W3C recommendations. Therefore, you may set a camel-case name and persist to HTML, but you will receive a lowercase name when the HTML is transformed into an attributed string.
|
||||
*/
|
||||
@property (nonatomic, strong) NSDictionary *attributes;
|
||||
|
||||
/**
|
||||
@name Customizing Attachments
|
||||
*/
|
||||
|
||||
/**
|
||||
Registers your own class for use when encountering a specific tag Name. If you register a class for a previously registered class (or one of the predefined ones (img, iframe, object, video) then this replaces this with the newer registration.
|
||||
|
||||
These registrations are permanent during the run time of your app. Custom attachment classes must implement the initWithElement:options: initializer and can implement the DTTextAttachmentDrawing and/or DTTextAttachmentHTMLPersistence protocols.
|
||||
@param theClass The class to instantiate in textAttachmentWithElement:options: when encountering a tag with this name
|
||||
@param tagName The tag name to use this class for
|
||||
*/
|
||||
+ (void)registerClass:(Class)theClass forTagName:(NSString *)tagName;
|
||||
|
||||
/**
|
||||
The class to use for a tag name
|
||||
@param tagName The tag name
|
||||
@returns The class to use for attachments with with tag name, or `nil` if this should not be an attachment
|
||||
*/
|
||||
+ (Class)registeredClassForTagName:(NSString *)tagName;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,263 @@
|
||||
//
|
||||
// DTTextAttachment.m
|
||||
// DTCoreText
|
||||
//
|
||||
// Created by Oliver on 14.01.11.
|
||||
// Copyright 2011 Drobnik.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import "DTTextAttachment.h"
|
||||
|
||||
#import "DTHTMLElement.h"
|
||||
#import "DTDictationPlaceholderTextAttachment.h"
|
||||
#import "DTIframeTextAttachment.h"
|
||||
#import "DTImageTextAttachment.h"
|
||||
#import "DTObjectTextAttachment.h"
|
||||
#import "DTVideoTextAttachment.h"
|
||||
#import "NSCoder+DTCompatibility.h"
|
||||
|
||||
#import <DTFoundation/DTLog.h>
|
||||
#import <DTFoundation/DTCoreGraphicsUtils.h>
|
||||
|
||||
|
||||
static NSMutableDictionary *_classForTagNameLookup = nil;
|
||||
|
||||
@interface DTTextAttachment ()
|
||||
|
||||
@end
|
||||
|
||||
@implementation DTTextAttachment
|
||||
{
|
||||
NSURL *_hyperLinkURL;
|
||||
NSString *_hyperLinkGUID;
|
||||
|
||||
CGFloat _fontLeading;
|
||||
CGFloat _fontAscent;
|
||||
CGFloat _fontDescent;
|
||||
}
|
||||
|
||||
+ (void)initialize
|
||||
{
|
||||
// this gets called from each subclass
|
||||
// prevent calling from children
|
||||
if (self != [DTTextAttachment class])
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_classForTagNameLookup = [[NSMutableDictionary alloc] init];
|
||||
|
||||
// register standard tags
|
||||
[DTTextAttachment registerClass:[DTImageTextAttachment class] forTagName:@"img"];
|
||||
[DTTextAttachment registerClass:[DTVideoTextAttachment class] forTagName:@"video"];
|
||||
[DTTextAttachment registerClass:[DTIframeTextAttachment class] forTagName:@"iframe"];
|
||||
[DTTextAttachment registerClass:[DTObjectTextAttachment class] forTagName:@"object"];
|
||||
}
|
||||
|
||||
+ (DTTextAttachment *)textAttachmentWithElement:(DTHTMLElement *)element options:(NSDictionary *)options
|
||||
{
|
||||
Class class = [DTTextAttachment registeredClassForTagName:element.name];
|
||||
|
||||
if (!class)
|
||||
{
|
||||
return nil;
|
||||
}
|
||||
|
||||
DTTextAttachment *attachment = [class alloc];
|
||||
|
||||
return [attachment initWithElement:element options:options];
|
||||
}
|
||||
|
||||
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_displaySize = [aDecoder decodeCGSizeForKey:@"displaySize"];
|
||||
_originalSize = [aDecoder decodeCGSizeForKey:@"originalSize"];
|
||||
_maxImageSize = [aDecoder decodeCGSizeForKey:@"maxImageSize"];
|
||||
_contentURL = [aDecoder decodeObjectForKey:@"contentURL"];
|
||||
_attributes = [aDecoder decodeObjectForKey:@"attributes"];
|
||||
_verticalAlignment = [aDecoder decodeIntegerForKey:@"verticalAlignment"];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)encodeWithCoder:(NSCoder *)aCoder {
|
||||
[aCoder encodeCGSize:_displaySize forKey:@"displaySize"];
|
||||
[aCoder encodeCGSize:_originalSize forKey:@"originalSize"];
|
||||
[aCoder encodeCGSize:_maxImageSize forKey:@"maxImageSize"];
|
||||
[aCoder encodeObject:_contentURL forKey:@"contentURL"];
|
||||
[aCoder encodeObject:_attributes forKey:@"attributes"];
|
||||
[aCoder encodeInteger:_verticalAlignment forKey:@"verticalAlignment"];
|
||||
}
|
||||
|
||||
- (id)initWithElement:(DTHTMLElement *)element options:(NSDictionary *)options
|
||||
{
|
||||
self = [super init];
|
||||
|
||||
if (self)
|
||||
{
|
||||
// width, height from tag
|
||||
_originalSize = element.size; // initially not known
|
||||
|
||||
// determine if there is a display size restriction
|
||||
_maxImageSize = CGSizeZero;
|
||||
|
||||
NSValue *maxImageSizeValue =[options objectForKey:DTMaxImageSize];
|
||||
if (maxImageSizeValue)
|
||||
{
|
||||
#if TARGET_OS_IPHONE
|
||||
_maxImageSize = [maxImageSizeValue CGSizeValue];
|
||||
#else
|
||||
_maxImageSize = [maxImageSizeValue sizeValue];
|
||||
#endif
|
||||
}
|
||||
|
||||
// set the display size from the original size, restricted to the max size
|
||||
[self setDisplaySize:_originalSize withMaxDisplaySize:_maxImageSize];
|
||||
|
||||
_attributes = element.attributes;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)adjustVerticalAlignmentForFont:(CTFontRef)font
|
||||
{
|
||||
_fontLeading = CTFontGetLeading(font);
|
||||
_fontAscent = CTFontGetAscent(font);
|
||||
_fontDescent = CTFontGetDescent(font);
|
||||
}
|
||||
|
||||
- (CGFloat)ascentForLayout
|
||||
{
|
||||
switch (_verticalAlignment)
|
||||
{
|
||||
case DTTextAttachmentVerticalAlignmentBaseline:
|
||||
{
|
||||
return _displaySize.height;
|
||||
}
|
||||
case DTTextAttachmentVerticalAlignmentTop:
|
||||
{
|
||||
return _fontAscent;
|
||||
}
|
||||
case DTTextAttachmentVerticalAlignmentCenter:
|
||||
{
|
||||
CGFloat halfHeight = (_fontAscent + _fontDescent) / 2.0f;
|
||||
|
||||
return halfHeight - _fontDescent + _displaySize.height/2.0f;
|
||||
}
|
||||
case DTTextAttachmentVerticalAlignmentBottom:
|
||||
{
|
||||
return _displaySize.height - _fontDescent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (CGFloat)descentForLayout
|
||||
{
|
||||
switch (_verticalAlignment)
|
||||
{
|
||||
case DTTextAttachmentVerticalAlignmentBaseline:
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
case DTTextAttachmentVerticalAlignmentTop:
|
||||
{
|
||||
return _displaySize.height - _fontAscent;
|
||||
}
|
||||
case DTTextAttachmentVerticalAlignmentCenter:
|
||||
{
|
||||
CGFloat halfHeight = (_fontAscent + _fontDescent) / 2.0f;
|
||||
|
||||
return halfHeight - _fontAscent + _displaySize.height/2.0f;
|
||||
}
|
||||
case DTTextAttachmentVerticalAlignmentBottom:
|
||||
{
|
||||
return _fontDescent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Subclass Customization
|
||||
|
||||
+ (void)registerClass:(Class)class forTagName:(NSString *)tagName
|
||||
{
|
||||
Class previousClass = [DTTextAttachment registeredClassForTagName:tagName];
|
||||
|
||||
if (previousClass)
|
||||
{
|
||||
DTLogDebug(@"Replacing previously registered class '%@' for tag name '%@' with '%@'", NSStringFromClass(previousClass), tagName, NSStringFromClass(class));
|
||||
}
|
||||
|
||||
[_classForTagNameLookup setObject:class forKey:tagName];
|
||||
}
|
||||
|
||||
+ (Class)registeredClassForTagName:(NSString *)tagName
|
||||
{
|
||||
return [_classForTagNameLookup objectForKey:tagName];
|
||||
}
|
||||
|
||||
#pragma mark Properties
|
||||
/** Mutator for originalSize. Sets displaySize to the same value as originalSize.
|
||||
@param originalSize The CGSize to store in originalSize. */
|
||||
- (void)setOriginalSize:(CGSize)originalSize
|
||||
{
|
||||
if (!CGSizeEqualToSize(originalSize, _originalSize))
|
||||
{
|
||||
_originalSize = originalSize;
|
||||
|
||||
if (_displaySize.width==0 || _displaySize.height==0)
|
||||
{
|
||||
[self setDisplaySize:_originalSize withMaxDisplaySize:_maxImageSize];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setDisplaySize:(CGSize)displaySize withMaxDisplaySize:(CGSize)maxDisplaySize
|
||||
{
|
||||
if (_originalSize.width!=0 && _originalSize.height!=0)
|
||||
{
|
||||
// width and/or height missing
|
||||
if (displaySize.width==0 && displaySize.height==0)
|
||||
{
|
||||
displaySize = _originalSize;
|
||||
}
|
||||
else if (displaySize.width==0 && displaySize.height!=0)
|
||||
{
|
||||
// width missing, calculate it
|
||||
CGFloat factor = _originalSize.height / displaySize.height;
|
||||
displaySize.width = round(_originalSize.width / factor);
|
||||
}
|
||||
else if (displaySize.width!=0 && displaySize.height==0)
|
||||
{
|
||||
// height missing, calculate it
|
||||
CGFloat factor = _originalSize.width / displaySize.width;
|
||||
displaySize.height = round(_originalSize.height / factor);
|
||||
}
|
||||
}
|
||||
|
||||
if (maxDisplaySize.width>0 && maxDisplaySize.height>0)
|
||||
{
|
||||
if (maxDisplaySize.width < displaySize.width || maxDisplaySize.height < displaySize.height)
|
||||
{
|
||||
displaySize = DTCGSizeThatFitsKeepingAspectRatio(displaySize, maxDisplaySize);
|
||||
}
|
||||
}
|
||||
|
||||
_displaySize = displaySize;
|
||||
}
|
||||
|
||||
- (void)setDisplaySize:(CGSize)displaySize
|
||||
{
|
||||
_displaySize = displaySize;
|
||||
}
|
||||
|
||||
@synthesize originalSize = _originalSize;
|
||||
@synthesize displaySize = _displaySize;
|
||||
@synthesize contentURL = _contentURL;
|
||||
@synthesize hyperLinkURL = _hyperLinkURL;
|
||||
@synthesize attributes = _attributes;
|
||||
@synthesize verticalAlignment = _verticalAlignment;
|
||||
@synthesize hyperLinkGUID = hyperLinkGUID;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// DTHTMLElementAttachment.h
|
||||
// DTCoreText
|
||||
//
|
||||
// Created by Oliver Drobnik on 26.12.12.
|
||||
// Copyright (c) 2012 Drobnik.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import "DTHTMLElement.h"
|
||||
|
||||
/**
|
||||
Specialized subclass of <DTHTMLElement> for dealing with <DTTextAttachment> instances, e.g. images.
|
||||
*/
|
||||
|
||||
@interface DTTextAttachmentHTMLElement : DTHTMLElement
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,123 @@
|
||||
//
|
||||
// DTHTMLElementAttachment.m
|
||||
// DTCoreText
|
||||
//
|
||||
// Created by Oliver Drobnik on 26.12.12.
|
||||
// Copyright (c) 2012 Drobnik.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import "DTTextAttachmentHTMLElement.h"
|
||||
|
||||
#import "DTHTMLElement.h"
|
||||
#import "DTTextAttachment.h"
|
||||
#import "DTCoreTextParagraphStyle.h"
|
||||
#import "NSMutableAttributedString+HTML.h"
|
||||
|
||||
@implementation DTTextAttachmentHTMLElement
|
||||
{
|
||||
CGSize _maxDisplaySize;
|
||||
}
|
||||
|
||||
- (id)initWithName:(NSString *)name attributes:(NSDictionary *)attributes options:(NSDictionary *)options
|
||||
{
|
||||
self = [super initWithName:name attributes:attributes];
|
||||
|
||||
if (self)
|
||||
{
|
||||
// make appropriate attachment
|
||||
DTTextAttachment *attachment = [DTTextAttachment textAttachmentWithElement:self options:options];
|
||||
|
||||
// add it to tag
|
||||
_textAttachment = attachment;
|
||||
|
||||
// to avoid much too much space before the image
|
||||
if (nil == _paragraphStyle)
|
||||
_paragraphStyle = [[DTCoreTextParagraphStyle alloc] init];
|
||||
|
||||
_paragraphStyle.lineHeightMultiple = 1;
|
||||
|
||||
// specifying line height interferes with correct positioning
|
||||
_paragraphStyle.minimumLineHeight = 0;
|
||||
_paragraphStyle.maximumLineHeight = 0;
|
||||
|
||||
// remember the maximum display size
|
||||
_maxDisplaySize = CGSizeZero;
|
||||
|
||||
NSValue *maxImageSizeValue =[options objectForKey:DTMaxImageSize];
|
||||
if (maxImageSizeValue)
|
||||
{
|
||||
#if TARGET_OS_IPHONE
|
||||
_maxDisplaySize = [maxImageSizeValue CGSizeValue];
|
||||
#else
|
||||
_maxDisplaySize = [maxImageSizeValue sizeValue];
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (NSAttributedString *)attributedString
|
||||
{
|
||||
@synchronized(self)
|
||||
{
|
||||
NSDictionary *attributes = [self attributesForAttributedStringRepresentation];
|
||||
|
||||
// ignore text, use unicode object placeholder
|
||||
NSMutableAttributedString *tmpString = [[NSMutableAttributedString alloc] initWithString:UNICODE_OBJECT_PLACEHOLDER attributes:attributes];
|
||||
|
||||
// block-level elements get space trimmed and a newline
|
||||
if (self.displayStyle != DTHTMLElementDisplayStyleInline)
|
||||
{
|
||||
[tmpString appendString:@"\n"];
|
||||
}
|
||||
|
||||
return tmpString;
|
||||
}
|
||||
}
|
||||
|
||||
// workaround, because we don't support float yet. float causes the image to be its own block
|
||||
- (DTHTMLElementDisplayStyle)displayStyle
|
||||
{
|
||||
if ([super floatStyle]==DTHTMLElementFloatStyleNone)
|
||||
{
|
||||
return [super displayStyle];
|
||||
}
|
||||
|
||||
return DTHTMLElementDisplayStyleBlock;
|
||||
}
|
||||
|
||||
- (void)applyStyleDictionary:(NSDictionary *)styles
|
||||
{
|
||||
// element size is determined in super (tag attribute and style)
|
||||
[super applyStyleDictionary:styles];
|
||||
|
||||
// at this point we have the size from width/height attribute or style in _size
|
||||
|
||||
// set original size if it was previously unknown
|
||||
if (CGSizeEqualToSize(CGSizeZero, _textAttachment.originalSize))
|
||||
{
|
||||
_textAttachment.originalSize = _size;
|
||||
}
|
||||
|
||||
NSString *widthString = [styles objectForKey:@"width"];
|
||||
NSString *heightString = [styles objectForKey:@"height"];
|
||||
|
||||
if (widthString.length > 1 && [widthString hasSuffix:@"%"])
|
||||
{
|
||||
CGFloat scale = (CGFloat)([[widthString substringToIndex:widthString.length - 1] floatValue] / 100.0);
|
||||
|
||||
_size.width = _maxDisplaySize.width * scale;
|
||||
}
|
||||
|
||||
if (heightString.length > 1 && [heightString hasSuffix:@"%"])
|
||||
{
|
||||
CGFloat scale = (CGFloat)([[heightString substringToIndex:heightString.length - 1] floatValue] / 100.0);
|
||||
|
||||
_size.height = _maxDisplaySize.height * scale;
|
||||
}
|
||||
// update the display size
|
||||
[_textAttachment setDisplaySize:_size withMaxDisplaySize:_maxDisplaySize];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,28 @@
|
||||
//
|
||||
// DTTextBlock.h
|
||||
// DTCoreText
|
||||
//
|
||||
// Created by Oliver Drobnik on 04.03.12.
|
||||
// Copyright (c) 2012 Drobnik.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import "DTCompatibility.h"
|
||||
|
||||
/**
|
||||
Class that represents a block of text with attributes like padding or a background color.
|
||||
*/
|
||||
@interface DTTextBlock : NSObject <NSCoding>
|
||||
|
||||
/**
|
||||
The space to be applied between the layouted text and the edges of the receiver
|
||||
*/
|
||||
@property (nonatomic, assign) DTEdgeInsets padding;
|
||||
|
||||
|
||||
/**
|
||||
The background color to paint behind the text in the receiver
|
||||
*/
|
||||
@property (nonatomic, strong) DTColor *backgroundColor;
|
||||
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,85 @@
|
||||
//
|
||||
// DTTextBlock.m
|
||||
// DTCoreText
|
||||
//
|
||||
// Created by Oliver Drobnik on 04.03.12.
|
||||
// Copyright (c) 2012 Drobnik.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import "DTTextBlock.h"
|
||||
#import "NSCoder+DTCompatibility.h"
|
||||
|
||||
@implementation DTTextBlock
|
||||
{
|
||||
DTEdgeInsets _padding;
|
||||
DTColor *_backgroundColor;
|
||||
}
|
||||
|
||||
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_padding = [aDecoder decodeDTEdgeInsetsForKey:@"padding"];
|
||||
_backgroundColor = [aDecoder decodeObjectForKey:@"backgroundColor"];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)encodeWithCoder:(NSCoder *)aCoder {
|
||||
[aCoder encodeDTEdgeInsets:_padding forKey:@"padding"];
|
||||
[aCoder encodeObject:_backgroundColor forKey:@"backgroundColor"];
|
||||
}
|
||||
|
||||
- (NSUInteger)hash
|
||||
{
|
||||
NSUInteger calcHash = 7;
|
||||
|
||||
calcHash = calcHash*31 + [_backgroundColor hash];
|
||||
calcHash = calcHash*31 + (NSUInteger)_padding.left;
|
||||
calcHash = calcHash*31 + (NSUInteger)_padding.top;
|
||||
calcHash = calcHash*31 + (NSUInteger)_padding.right;
|
||||
calcHash = calcHash*31 + (NSUInteger)_padding.bottom;
|
||||
|
||||
return calcHash;
|
||||
}
|
||||
|
||||
- (BOOL)isEqual:(id)object
|
||||
{
|
||||
if (!object)
|
||||
{
|
||||
return NO;
|
||||
}
|
||||
|
||||
if (object == self)
|
||||
{
|
||||
return YES;
|
||||
}
|
||||
|
||||
if (![object isKindOfClass:[DTTextBlock class]])
|
||||
{
|
||||
return NO;
|
||||
}
|
||||
|
||||
DTTextBlock *other = object;
|
||||
|
||||
if (_padding.left != other->_padding.left ||
|
||||
_padding.top != other->_padding.top ||
|
||||
_padding.right != other->_padding.right ||
|
||||
_padding.bottom != other->_padding.bottom)
|
||||
{
|
||||
return NO;
|
||||
}
|
||||
|
||||
if (other->_backgroundColor == _backgroundColor)
|
||||
{
|
||||
return YES;
|
||||
}
|
||||
|
||||
return [other->_backgroundColor isEqual:_backgroundColor];
|
||||
}
|
||||
|
||||
#pragma mark Properties
|
||||
|
||||
@synthesize padding = _padding;
|
||||
@synthesize backgroundColor = _backgroundColor;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,22 @@
|
||||
//
|
||||
// DTHTMLElementText.h
|
||||
// DTCoreText
|
||||
//
|
||||
// Created by Oliver Drobnik on 26.12.12.
|
||||
// Copyright (c) 2012 Drobnik.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import "DTHTMLElement.h"
|
||||
|
||||
/**
|
||||
Specialized subclass of <DTHTMLElement> that deals with text. It represents a text node. The text inside a DTHTMLElement can consist of any number of such text nodes.
|
||||
*/
|
||||
|
||||
@interface DTTextHTMLElement : DTHTMLElement
|
||||
|
||||
/**
|
||||
The text content of the element.
|
||||
*/
|
||||
@property (nonatomic, strong) NSString *text;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,121 @@
|
||||
//
|
||||
// DTHTMLElementText.m
|
||||
// DTCoreText
|
||||
//
|
||||
// Created by Oliver Drobnik on 26.12.12.
|
||||
// Copyright (c) 2012 Drobnik.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import "DTCompatibility.h"
|
||||
|
||||
#import "DTTextHTMLElement.h"
|
||||
#import "NSString+HTML.h"
|
||||
#import "DTCoreTextFontDescriptor.h"
|
||||
#import "NSAttributedString+SmallCaps.h"
|
||||
|
||||
#if TARGET_OS_IPHONE
|
||||
#import "UIFont+DTCoreText.h"
|
||||
#endif
|
||||
|
||||
@implementation DTTextHTMLElement
|
||||
{
|
||||
NSString *_text;
|
||||
}
|
||||
|
||||
- (void)_appendHTMLToString:(NSMutableString *)string indentLevel:(NSUInteger)indentLevel
|
||||
{
|
||||
// indent to the level
|
||||
for (NSUInteger i=0; i<indentLevel; i++)
|
||||
{
|
||||
[string appendString:@" "];
|
||||
}
|
||||
|
||||
[string appendFormat:@"\"%@\"\n", [_text stringByNormalizingWhitespace]];
|
||||
}
|
||||
|
||||
- (NSAttributedString *)attributedString
|
||||
{
|
||||
@synchronized(self)
|
||||
{
|
||||
NSString *text;
|
||||
|
||||
if (_preserveNewlines)
|
||||
{
|
||||
text = _text;
|
||||
|
||||
// PRE ignores the first \n
|
||||
if ([text hasPrefix:@"\n"])
|
||||
{
|
||||
text = [text substringFromIndex:1];
|
||||
}
|
||||
|
||||
// PRE ignores the last \n
|
||||
if ([text hasSuffix:@"\n"])
|
||||
{
|
||||
text = [text substringWithRange:NSMakeRange(0, [text length]-1)];
|
||||
}
|
||||
|
||||
// replace paragraph breaks with line breaks
|
||||
// using \r as to not confuse this with line feeds, but still get a single paragraph
|
||||
text = [text stringByReplacingOccurrencesOfString:@"\n" withString:@"\r"];
|
||||
}
|
||||
else if (_containsAppleConvertedSpace)
|
||||
{
|
||||
// replace nbsp; with regular space
|
||||
text = [_text stringByReplacingOccurrencesOfString:UNICODE_NON_BREAKING_SPACE withString:@" "];
|
||||
}
|
||||
else
|
||||
{
|
||||
text = [_text stringByNormalizingWhitespace];
|
||||
}
|
||||
|
||||
NSDictionary *attributes = [self attributesForAttributedStringRepresentation];
|
||||
|
||||
if (self.fontVariant == DTHTMLElementFontVariantNormal)
|
||||
{
|
||||
// make a new attributed string from the text
|
||||
return [[NSAttributedString alloc] initWithString:text attributes:attributes];
|
||||
}
|
||||
else
|
||||
{
|
||||
if ([self.fontDescriptor supportsNativeSmallCaps])
|
||||
{
|
||||
DTCoreTextFontDescriptor *smallDesc = [self.fontDescriptor copy];
|
||||
smallDesc.smallCapsFeature = YES;
|
||||
|
||||
NSMutableDictionary *smallAttributes = [attributes mutableCopy];
|
||||
|
||||
CTFontRef smallerFont = [smallDesc newMatchingFont];
|
||||
|
||||
if (smallerFont)
|
||||
{
|
||||
#if DTCORETEXT_SUPPORT_NS_ATTRIBUTES && TARGET_OS_IPHONE
|
||||
if (___useiOS6Attributes)
|
||||
{
|
||||
UIFont *font = [UIFont fontWithCTFont:smallerFont];
|
||||
|
||||
[smallAttributes setObject:font forKey:NSFontAttributeName];
|
||||
CFRelease(smallerFont);
|
||||
}
|
||||
else
|
||||
#endif
|
||||
{
|
||||
[smallAttributes setObject:CFBridgingRelease(smallerFont) forKey:(id)kCTFontAttributeName];
|
||||
}
|
||||
}
|
||||
|
||||
return [[NSAttributedString alloc] initWithString:_text attributes:smallAttributes];
|
||||
}
|
||||
else
|
||||
{
|
||||
return [NSAttributedString synthesizedSmallCapsAttributedStringWithText:_text attributes:attributes];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Properties
|
||||
|
||||
@synthesize text = _text;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// DTTextAttachmentVideo.h
|
||||
// DTCoreText
|
||||
//
|
||||
// Created by Oliver Drobnik on 22.04.13.
|
||||
// Copyright (c) 2013 Drobnik.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import "DTTextAttachment.h"
|
||||
|
||||
/**
|
||||
A specialized subclass in the DTTextAttachment class cluster to represent an embedded video
|
||||
*/
|
||||
|
||||
@interface DTVideoTextAttachment : DTTextAttachment <DTTextAttachmentHTMLPersistence>
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,110 @@
|
||||
//
|
||||
// DTTextAttachmentVideo.m
|
||||
// DTCoreText
|
||||
//
|
||||
// Created by Oliver Drobnik on 22.04.13.
|
||||
// Copyright (c) 2013 Drobnik.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import "DTVideoTextAttachment.h"
|
||||
#import "DTCoreTextConstants.h"
|
||||
#import "DTHTMLElement.h"
|
||||
#import "NSString+HTML.h"
|
||||
|
||||
@implementation DTVideoTextAttachment
|
||||
|
||||
- (id)initWithElement:(DTHTMLElement *)element options:(NSDictionary *)options
|
||||
{
|
||||
self = [super initWithElement:element options:options];
|
||||
|
||||
if (self)
|
||||
{
|
||||
// get base URL
|
||||
NSURL *baseURL = [options objectForKey:NSBaseURLDocumentOption];
|
||||
NSString *src = [element.attributes objectForKey:@"src"];
|
||||
|
||||
// content URL
|
||||
_contentURL = [NSURL URLWithString:src relativeToURL:baseURL];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark - DTTextAttachmentHTMLEncoding
|
||||
|
||||
- (NSString *)stringByEncodingAsHTML
|
||||
{
|
||||
NSMutableString *retString = [NSMutableString string];
|
||||
|
||||
[retString appendString:@"<video"];
|
||||
|
||||
if (_contentURL)
|
||||
{
|
||||
[retString appendFormat:@" src=\"%@\"", [_contentURL absoluteString]];
|
||||
}
|
||||
|
||||
// build style for img/video
|
||||
NSMutableString *styleString = [NSMutableString string];
|
||||
|
||||
switch (_verticalAlignment)
|
||||
{
|
||||
case DTTextAttachmentVerticalAlignmentBaseline:
|
||||
{
|
||||
// [classStyleString appendString:@"vertical-align:baseline;"];
|
||||
break;
|
||||
}
|
||||
case DTTextAttachmentVerticalAlignmentTop:
|
||||
{
|
||||
[styleString appendString:@"vertical-align:text-top;"];
|
||||
break;
|
||||
}
|
||||
case DTTextAttachmentVerticalAlignmentCenter:
|
||||
{
|
||||
[styleString appendString:@"vertical-align:middle;"];
|
||||
break;
|
||||
}
|
||||
case DTTextAttachmentVerticalAlignmentBottom:
|
||||
{
|
||||
[styleString appendString:@"vertical-align:text-bottom;"];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (_originalSize.width>0)
|
||||
{
|
||||
[styleString appendFormat:@"width:%.0fpx;", _originalSize.width];
|
||||
}
|
||||
|
||||
if (_originalSize.height>0)
|
||||
{
|
||||
[styleString appendFormat:@"height:%.0fpx;", _originalSize.height];
|
||||
}
|
||||
|
||||
// add local style for size, since sizes might vary quite a bit
|
||||
if ([styleString length])
|
||||
{
|
||||
[retString appendFormat:@" style=\"%@\"", styleString];
|
||||
}
|
||||
|
||||
// attach the attributes dictionary
|
||||
NSMutableDictionary *tmpAttributes = [_attributes mutableCopy];
|
||||
|
||||
// remove src,style, width and height we already have these
|
||||
[tmpAttributes removeObjectForKey:@"src"];
|
||||
[tmpAttributes removeObjectForKey:@"style"];
|
||||
[tmpAttributes removeObjectForKey:@"width"];
|
||||
[tmpAttributes removeObjectForKey:@"height"];
|
||||
|
||||
for (__strong NSString *oneKey in [tmpAttributes allKeys])
|
||||
{
|
||||
oneKey = [oneKey stringByAddingHTMLEntities];
|
||||
NSString *value = [[tmpAttributes objectForKey:oneKey] stringByAddingHTMLEntities];
|
||||
[retString appendFormat:@" %@=\"%@\"", oneKey, value];
|
||||
}
|
||||
|
||||
[retString appendString:@" />"];
|
||||
|
||||
return retString;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,33 @@
|
||||
//
|
||||
// DTWeakSupport.h
|
||||
// DTFoundation
|
||||
//
|
||||
// Created by Oliver Drobnik on 6/3/13.
|
||||
// Copyright (c) 2013 Cocoanetics. All rights reserved.
|
||||
//
|
||||
|
||||
/**
|
||||
Useful defines for building code the compiles with zeroing weak references if the deployment target allows it. This is possible from minimum supported iOS 5.0 and OS X 10.7 and above. Note that on OS X 10.7 some AppKit classes do not support having a weak ref, e.g. NSWindowController or NSViewController.
|
||||
*/
|
||||
|
||||
#import <Availability.h>
|
||||
|
||||
#if __has_feature(objc_arc_weak)
|
||||
|
||||
// zeroing weak refs are supported for ivars and properties
|
||||
#define DT_WEAK_VARIABLE __weak
|
||||
#define DT_WEAK_PROPERTY weak
|
||||
|
||||
#elif __has_feature(objc_arc)
|
||||
|
||||
/// zeroing weak refs not supported, fall back to unsafe unretained and assigning
|
||||
#define DT_WEAK_VARIABLE __unsafe_unretained
|
||||
#define DT_WEAK_PROPERTY assign
|
||||
|
||||
#else
|
||||
|
||||
// define something, as this header might be included in a non-ARC project for using compiled code from an ARC static lib
|
||||
#define DT_WEAK_VARIABLE
|
||||
#define DT_WEAK_PROPERTY assign
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,141 @@
|
||||
//
|
||||
// NSAttributedString+DTCoreText.h
|
||||
// DTCoreText
|
||||
//
|
||||
// Created by Oliver Drobnik on 2/1/12.
|
||||
// Copyright (c) 2012 Drobnik.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import "DTCompatibility.h"
|
||||
|
||||
@class DTCSSListStyle;
|
||||
@class DTTextBlock;
|
||||
|
||||
/**
|
||||
Convenience Methods that mimics similar methods available on Mac
|
||||
*/
|
||||
@interface NSAttributedString (DTCoreText)
|
||||
|
||||
/**
|
||||
@name Working with Text Attachments
|
||||
*/
|
||||
|
||||
/**
|
||||
Retrieves the DTTextAttachment objects that match the given predicate.
|
||||
|
||||
With this method you can for example find all images that have a certain URL.
|
||||
|
||||
@param predicate The predicate to apply for filtering or `nil` to not filter by attachment
|
||||
@param theClass The class that attachments need to have, or `nil` for all attachments regardless of class
|
||||
@returns The filtered array of attachments
|
||||
*/
|
||||
- (NSArray *)textAttachmentsWithPredicate:(NSPredicate *)predicate class:(Class)theClass;
|
||||
|
||||
/**
|
||||
@name Calculating Ranges
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
Returns the index of the item at the given location within the list.
|
||||
|
||||
@param list The text list.
|
||||
@param location The location of the item.
|
||||
@returns Returns the index within the list.
|
||||
*/
|
||||
- (NSInteger)itemNumberInTextList:(DTCSSListStyle *)list atIndex:(NSUInteger)location;
|
||||
|
||||
|
||||
/**
|
||||
Returns the range of the given text list that contains the given location.
|
||||
|
||||
@param list The text list.
|
||||
@param location The location in the text.
|
||||
@returns The range of the given text list containing the location.
|
||||
*/
|
||||
- (NSRange)rangeOfTextList:(DTCSSListStyle *)list atIndex:(NSUInteger)location;
|
||||
|
||||
/**
|
||||
Returns the range of the given text block that contains the given location.
|
||||
|
||||
@param textBlock The text block.
|
||||
@param location The location in the text.
|
||||
@returns The range of the given text block containing the location.
|
||||
*/
|
||||
- (NSRange)rangeOfTextBlock:(DTTextBlock *)textBlock atIndex:(NSUInteger)location;
|
||||
|
||||
/**
|
||||
Returns the range of the given href anchor.
|
||||
|
||||
@param anchorName The name of the anchor.
|
||||
@returns The range of the given anchor.
|
||||
*/
|
||||
- (NSRange)rangeOfAnchorNamed:(NSString *)anchorName;
|
||||
|
||||
/**
|
||||
Returns the range of the hyperlink at the given index.
|
||||
|
||||
@param location The location to query
|
||||
@param URL The URL that is found at this location or `NULL` if this is not needed
|
||||
@returns The range of the given hyperlink.
|
||||
*/
|
||||
- (NSRange)rangeOfLinkAtIndex:(NSUInteger)location URL:(NSURL * __autoreleasing*)URL;
|
||||
|
||||
/**
|
||||
Returns the range of a field at the given index.
|
||||
|
||||
@param location The location of the field
|
||||
@returns The range of the field. If there is no field at this location it returns {NSNotFound, 0}.
|
||||
*/
|
||||
- (NSRange)rangeOfFieldAtIndex:(NSUInteger)location;
|
||||
|
||||
#ifndef COVERAGE
|
||||
// exclude method from coverage testing, those are just convenience methods
|
||||
|
||||
/**
|
||||
@name Converting to Other Representations
|
||||
*/
|
||||
|
||||
/**
|
||||
Encodes the receiver into a generic HTML representation.
|
||||
|
||||
@returns An HTML string.
|
||||
*/
|
||||
- (NSString *)htmlString;
|
||||
|
||||
|
||||
/**
|
||||
Encodes the receiver into a generic HTML fragment representation. Styles are inlined and no html or head tags are included.
|
||||
|
||||
@returns An HTML string.
|
||||
*/
|
||||
- (NSString *)htmlFragment;
|
||||
|
||||
/**
|
||||
Converts the receiver into plain text.
|
||||
|
||||
This is different from the `string` method of `NSAttributedString` by also erasing placeholders for text attachments.
|
||||
|
||||
@returns The receiver converted to plain text.
|
||||
*/
|
||||
- (NSString *)plainTextString;
|
||||
|
||||
#endif
|
||||
|
||||
/**
|
||||
@name Creating Special Attributed Strings
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
Create a prefix for a paragraph in a list
|
||||
|
||||
@param listCounter The value for the list item.
|
||||
@param listStyle The list style
|
||||
@param listIndent The amount in px to indent the list
|
||||
@param attributes The attribute dictionary for the text to be prefixed
|
||||
@returns An attributed string with the list prefix
|
||||
*/
|
||||
+ (NSAttributedString *)prefixForListItemWithCounter:(NSUInteger)listCounter listStyle:(DTCSSListStyle *)listStyle listIndent:(CGFloat)listIndent attributes:(NSDictionary *)attributes;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,532 @@
|
||||
//
|
||||
// NSAttributedString+DTCoreText.m
|
||||
// DTCoreText
|
||||
//
|
||||
// Created by Oliver Drobnik on 2/1/12.
|
||||
// Copyright (c) 2012 Drobnik.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import "NSAttributedString+DTCoreText.h"
|
||||
#import "DTHTMLWriter.h"
|
||||
#import "DTCoreTextConstants.h"
|
||||
#import "DTCoreTextFontDescriptor.h"
|
||||
#import "DTCoreTextParagraphStyle.h"
|
||||
#import "DTCSSListStyle.h"
|
||||
#import "DTImageTextAttachment.h"
|
||||
#import "NSString+Paragraphs.h"
|
||||
#import "NSDictionary+DTCoreText.h"
|
||||
#import "NSAttributedStringRunDelegates.h"
|
||||
|
||||
#import <DTFoundation/NSURL+DTComparing.h>
|
||||
|
||||
#if TARGET_OS_IPHONE
|
||||
#import "UIFont+DTCoreText.h"
|
||||
#endif
|
||||
|
||||
@implementation NSAttributedString (DTCoreText)
|
||||
|
||||
#pragma mark Text Attachments
|
||||
- (NSArray *)textAttachmentsWithPredicate:(NSPredicate *)predicate class:(Class)class
|
||||
{
|
||||
if (![self length])
|
||||
{
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSMutableArray *foundAttachments = [NSMutableArray array];
|
||||
|
||||
NSRange entireRange = NSMakeRange(0, [self length]);
|
||||
[self enumerateAttribute:NSAttachmentAttributeName inRange:entireRange options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired usingBlock:^(DTTextAttachment *attachment, NSRange range, BOOL *stop) {
|
||||
|
||||
if (attachment == nil)
|
||||
{
|
||||
// no attachment value
|
||||
return;
|
||||
}
|
||||
|
||||
if (predicate && ![predicate evaluateWithObject:attachment])
|
||||
{
|
||||
// doesn't fit predicate, next
|
||||
return;
|
||||
}
|
||||
|
||||
if (class && ![attachment isKindOfClass:class])
|
||||
{
|
||||
// doesn't fit class, next
|
||||
return;
|
||||
}
|
||||
|
||||
[foundAttachments addObject:attachment];
|
||||
}];
|
||||
|
||||
if ([foundAttachments count])
|
||||
{
|
||||
return foundAttachments;
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark Calculating Ranges
|
||||
|
||||
- (NSInteger)itemNumberInTextList:(DTCSSListStyle *)list atIndex:(NSUInteger)location
|
||||
{
|
||||
NSRange effectiveRange;
|
||||
NSArray *textListsAtIndex = [self attribute:DTTextListsAttribute atIndex:location effectiveRange:&effectiveRange];
|
||||
|
||||
if (!textListsAtIndex)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
// get outermost list
|
||||
DTCSSListStyle *outermostList = [textListsAtIndex objectAtIndex:0];
|
||||
|
||||
// get the range of all lists
|
||||
NSRange totalRange = [self rangeOfTextList:outermostList atIndex:location];
|
||||
|
||||
// get naked NSString
|
||||
NSString *string = [[self string] substringWithRange:totalRange];
|
||||
|
||||
// entire string
|
||||
NSRange range = NSMakeRange(0, [string length]);
|
||||
|
||||
NSMutableDictionary *countersPerList = [NSMutableDictionary dictionary];
|
||||
|
||||
// enumerating through the paragraphs in the plain text string
|
||||
[string enumerateSubstringsInRange:range options:NSStringEnumerationByParagraphs usingBlock:^(NSString *substring, NSRange substringRange, NSRange enclosingRange, BOOL *stop)
|
||||
{
|
||||
NSRange paragraphListRange;
|
||||
NSArray *textLists = [self attribute:DTTextListsAttribute atIndex:substringRange.location + totalRange.location effectiveRange:¶graphListRange];
|
||||
|
||||
DTCSSListStyle *currentEffectiveList = [textLists lastObject];
|
||||
|
||||
NSNumber *key = [NSNumber numberWithInteger:(NSInteger)currentEffectiveList]; // list address is identifier
|
||||
NSNumber *currentCounterNum = [countersPerList objectForKey:key];
|
||||
|
||||
NSInteger currentCounter=0;
|
||||
|
||||
if (!currentCounterNum)
|
||||
{
|
||||
currentCounter = currentEffectiveList.startingItemNumber;
|
||||
}
|
||||
else
|
||||
{
|
||||
currentCounter = [currentCounterNum integerValue]+1;
|
||||
}
|
||||
|
||||
currentCounterNum = [NSNumber numberWithInteger:currentCounter];
|
||||
[countersPerList setObject:currentCounterNum forKey:key];
|
||||
|
||||
// calculate the actual range
|
||||
NSRange actualRange = enclosingRange; // includes a potential \n
|
||||
actualRange.location += totalRange.location;
|
||||
|
||||
if (NSLocationInRange(location, actualRange))
|
||||
{
|
||||
*stop = YES;
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
NSNumber *key = [NSNumber numberWithInteger:(NSInteger)list]; // list address is identifier
|
||||
NSNumber *currentCounterNum = [countersPerList objectForKey:key];
|
||||
|
||||
return [currentCounterNum integerValue];
|
||||
}
|
||||
|
||||
- (NSRange)_rangeOfObject:(id)object inArrayBehindAttribute:(NSString *)attribute atIndex:(NSUInteger)location
|
||||
{
|
||||
@synchronized(self)
|
||||
{
|
||||
NSUInteger stringLength = [self length];
|
||||
NSUInteger searchIndex = location;
|
||||
|
||||
NSArray *arrayAtIndex;
|
||||
|
||||
NSRange totalRange = NSMakeRange(NSNotFound, 0);
|
||||
|
||||
BOOL foundList = NO;
|
||||
|
||||
do
|
||||
{
|
||||
NSRange effectiveRange;
|
||||
arrayAtIndex = [self attribute:attribute atIndex:searchIndex effectiveRange:&effectiveRange];
|
||||
|
||||
if (!arrayAtIndex || [arrayAtIndex indexOfObjectIdenticalTo:object] == NSNotFound)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
searchIndex = effectiveRange.location;
|
||||
foundList = YES;
|
||||
|
||||
// enhance found range
|
||||
if (totalRange.location == NSNotFound)
|
||||
{
|
||||
totalRange = effectiveRange;
|
||||
}
|
||||
else
|
||||
{
|
||||
totalRange = NSUnionRange(totalRange, effectiveRange);
|
||||
}
|
||||
|
||||
if (searchIndex == 0)
|
||||
{
|
||||
// reached beginning of string
|
||||
break;
|
||||
}
|
||||
|
||||
searchIndex--;
|
||||
}
|
||||
while (foundList);
|
||||
|
||||
// if we didn't find the list at all, return
|
||||
if (!foundList)
|
||||
{
|
||||
return NSMakeRange(NSNotFound, 0);
|
||||
}
|
||||
|
||||
// now search forward
|
||||
|
||||
searchIndex = NSMaxRange(totalRange);
|
||||
|
||||
while (searchIndex < stringLength)
|
||||
{
|
||||
NSRange effectiveRange;
|
||||
arrayAtIndex = [self attribute:attribute atIndex:searchIndex effectiveRange:&effectiveRange];
|
||||
|
||||
if (!arrayAtIndex || [arrayAtIndex indexOfObjectIdenticalTo:object] == NSNotFound)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
searchIndex = NSMaxRange(effectiveRange);
|
||||
|
||||
// enhance found range
|
||||
totalRange = NSUnionRange(totalRange, effectiveRange);
|
||||
}
|
||||
|
||||
return totalRange;
|
||||
}
|
||||
}
|
||||
|
||||
- (NSRange)rangeOfTextList:(DTCSSListStyle *)list atIndex:(NSUInteger)location
|
||||
{
|
||||
NSParameterAssert(list);
|
||||
|
||||
NSRange listRange = [self _rangeOfObject:list inArrayBehindAttribute:DTTextListsAttribute atIndex:location];
|
||||
|
||||
if (listRange.location == NSNotFound)
|
||||
{
|
||||
// list was not found
|
||||
return listRange;
|
||||
}
|
||||
|
||||
// extend list range to full paragraphs to be safe
|
||||
listRange = [self.string rangeOfParagraphsContainingRange:listRange parBegIndex:NULL parEndIndex:NULL];
|
||||
|
||||
return listRange;
|
||||
}
|
||||
|
||||
- (NSRange)rangeOfTextBlock:(DTTextBlock *)textBlock atIndex:(NSUInteger)location
|
||||
{
|
||||
NSParameterAssert(textBlock);
|
||||
|
||||
return [self _rangeOfObject:textBlock inArrayBehindAttribute:DTTextBlocksAttribute atIndex:location];
|
||||
}
|
||||
|
||||
- (NSRange)rangeOfAnchorNamed:(NSString *)anchorName
|
||||
{
|
||||
__block NSRange foundRange = NSMakeRange(NSNotFound, 0);
|
||||
|
||||
[self enumerateAttribute:DTAnchorAttribute inRange:NSMakeRange(0, [self length]) options:0 usingBlock:^(NSString *value, NSRange range, BOOL *stop) {
|
||||
if ([value isEqualToString:anchorName])
|
||||
{
|
||||
*stop = YES;
|
||||
foundRange = range;
|
||||
}
|
||||
}];
|
||||
|
||||
return foundRange;
|
||||
}
|
||||
|
||||
- (NSRange)rangeOfLinkAtIndex:(NSUInteger)location URL:(NSURL * __autoreleasing*)URL
|
||||
{
|
||||
NSRange rangeSoFar;
|
||||
|
||||
NSURL *foundURL = [self attribute:DTLinkAttribute atIndex:location effectiveRange:&rangeSoFar];
|
||||
|
||||
if (!foundURL)
|
||||
{
|
||||
return NSMakeRange(NSNotFound, 0);
|
||||
}
|
||||
|
||||
// search towards beginning
|
||||
while (rangeSoFar.location>0)
|
||||
{
|
||||
NSRange extendedRange;
|
||||
NSURL *extendedURL = [self attribute:DTLinkAttribute atIndex:rangeSoFar.location-1 effectiveRange:&extendedRange];
|
||||
|
||||
// abort search if key not found or value not identical
|
||||
if (!extendedURL || ![extendedURL isEqualToURL:foundURL])
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
rangeSoFar = NSUnionRange(rangeSoFar, extendedRange);
|
||||
}
|
||||
|
||||
NSUInteger length = [self length];
|
||||
|
||||
// search towards end
|
||||
while (NSMaxRange(rangeSoFar)<length)
|
||||
{
|
||||
NSRange extendedRange;
|
||||
NSURL *extendedURL = [self attribute:DTLinkAttribute atIndex:NSMaxRange(rangeSoFar) effectiveRange:&extendedRange];
|
||||
|
||||
// abort search if key not found or value not identical
|
||||
if (!extendedURL || ![extendedURL isEqualToURL:foundURL])
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
rangeSoFar = NSUnionRange(rangeSoFar, extendedRange);
|
||||
}
|
||||
|
||||
if (URL)
|
||||
{
|
||||
*URL = foundURL;
|
||||
}
|
||||
|
||||
return rangeSoFar;
|
||||
}
|
||||
|
||||
- (NSRange)rangeOfFieldAtIndex:(NSUInteger)location
|
||||
{
|
||||
if (location<[self length])
|
||||
{
|
||||
// get range of prefix
|
||||
NSRange fieldRange;
|
||||
NSString *fieldAttribute = [self attribute:DTFieldAttribute atIndex:location effectiveRange:&fieldRange];
|
||||
|
||||
if (fieldAttribute)
|
||||
{
|
||||
return fieldRange;
|
||||
}
|
||||
}
|
||||
|
||||
return NSMakeRange(NSNotFound, 0);
|
||||
}
|
||||
|
||||
#pragma mark HTML Encoding
|
||||
|
||||
#ifndef COVERAGE
|
||||
// exclude method from coverage testing, those are just convenience methods
|
||||
|
||||
- (NSString *)htmlString
|
||||
{
|
||||
// create a writer
|
||||
DTHTMLWriter *writer = [[DTHTMLWriter alloc] initWithAttributedString:self];
|
||||
|
||||
// return it's output
|
||||
return [writer HTMLString];
|
||||
}
|
||||
|
||||
- (NSString *)htmlFragment
|
||||
{
|
||||
// create a writer
|
||||
DTHTMLWriter *writer = [[DTHTMLWriter alloc] initWithAttributedString:self];
|
||||
|
||||
// return it's output
|
||||
return [writer HTMLFragment];
|
||||
}
|
||||
|
||||
- (NSString *)plainTextString
|
||||
{
|
||||
NSString *tmpString = [self string];
|
||||
|
||||
return [tmpString stringByReplacingOccurrencesOfString:UNICODE_OBJECT_PLACEHOLDER withString:@""];
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
#pragma mark Generating Special Attributed Strings
|
||||
+ (NSAttributedString *)prefixForListItemWithCounter:(NSUInteger)listCounter listStyle:(DTCSSListStyle *)listStyle listIndent:(CGFloat)listIndent attributes:(NSDictionary *)attributes
|
||||
{
|
||||
// get existing values from attributes
|
||||
CTParagraphStyleRef paraStyle = (__bridge CTParagraphStyleRef)[attributes objectForKey:(id)kCTParagraphStyleAttributeName];
|
||||
CTFontRef font = (__bridge CTFontRef)[attributes objectForKey:(id)kCTFontAttributeName];
|
||||
|
||||
DTCoreTextFontDescriptor *fontDescriptor = nil;
|
||||
DTCoreTextParagraphStyle *paragraphStyle = nil;
|
||||
|
||||
if (paraStyle)
|
||||
{
|
||||
paragraphStyle = [DTCoreTextParagraphStyle paragraphStyleWithCTParagraphStyle:paraStyle];
|
||||
|
||||
paragraphStyle.tabStops = nil;
|
||||
|
||||
paragraphStyle.headIndent = listIndent;
|
||||
|
||||
if (listStyle.type != DTCSSListStyleTypeNone)
|
||||
{
|
||||
// first tab is to right-align bullet, numbering against
|
||||
CGFloat tabOffset = paragraphStyle.headIndent - (CGFloat)5.0; // TODO: change with font size
|
||||
#if DTCORETEXT_SUPPORT_NS_ATTRIBUTES
|
||||
[paragraphStyle addTabStopAtPosition:tabOffset alignment:kCTTextAlignmentRight];
|
||||
#else
|
||||
[paragraphStyle addTabStopAtPosition:tabOffset alignment:kCTRightTextAlignment];
|
||||
#endif
|
||||
}
|
||||
|
||||
// second tab is for the beginning of first line after bullet
|
||||
#if DTCORETEXT_SUPPORT_NS_ATTRIBUTES
|
||||
[paragraphStyle addTabStopAtPosition:paragraphStyle.headIndent alignment:kCTTextAlignmentLeft];
|
||||
#else
|
||||
[paragraphStyle addTabStopAtPosition:paragraphStyle.headIndent alignment:kCTLeftTextAlignment];
|
||||
#endif
|
||||
}
|
||||
|
||||
if (font)
|
||||
{
|
||||
fontDescriptor = [DTCoreTextFontDescriptor fontDescriptorForCTFont:font];
|
||||
}
|
||||
|
||||
NSMutableDictionary *newAttributes = [NSMutableDictionary dictionary];
|
||||
|
||||
if (fontDescriptor)
|
||||
{
|
||||
// make a font without italic or bold
|
||||
DTCoreTextFontDescriptor *fontDesc = [fontDescriptor copy];
|
||||
|
||||
fontDesc.boldTrait = NO;
|
||||
fontDesc.italicTrait = NO;
|
||||
|
||||
font = [fontDesc newMatchingFont];
|
||||
|
||||
if (font)
|
||||
{
|
||||
#if DTCORETEXT_SUPPORT_NS_ATTRIBUTES && __IPHONE_OS_VERSION_MAX_ALLOWED > __IPHONE_5_1
|
||||
if (___useiOS6Attributes)
|
||||
{
|
||||
UIFont *uiFont = [UIFont fontWithCTFont:font];
|
||||
[newAttributes setObject:uiFont forKey:NSFontAttributeName];
|
||||
|
||||
CFRelease(font);
|
||||
}
|
||||
else
|
||||
#endif
|
||||
{
|
||||
[newAttributes setObject:CFBridgingRelease(font) forKey:(id)kCTFontAttributeName];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CGColorRef textColor = (__bridge CGColorRef)[attributes objectForKey:(id)kCTForegroundColorAttributeName];
|
||||
|
||||
if (textColor)
|
||||
{
|
||||
[newAttributes setObject:(__bridge id)textColor forKey:(id)kCTForegroundColorAttributeName];
|
||||
}
|
||||
#if DTCORETEXT_SUPPORT_NS_ATTRIBUTES
|
||||
else if (___useiOS6Attributes)
|
||||
{
|
||||
DTColor *uiColor = [attributes foregroundColor];
|
||||
|
||||
if (uiColor)
|
||||
{
|
||||
[newAttributes setObject:uiColor forKey:NSForegroundColorAttributeName];
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// add paragraph style (this has the tabs)
|
||||
if (paragraphStyle)
|
||||
{
|
||||
#if DTCORETEXT_SUPPORT_NS_ATTRIBUTES
|
||||
if (___useiOS6Attributes)
|
||||
{
|
||||
NSParagraphStyle *style = [paragraphStyle NSParagraphStyle];
|
||||
[newAttributes setObject:style forKey:NSParagraphStyleAttributeName];
|
||||
}
|
||||
else
|
||||
#endif
|
||||
{
|
||||
CTParagraphStyleRef newParagraphStyle = [paragraphStyle createCTParagraphStyle];
|
||||
[newAttributes setObject:CFBridgingRelease(newParagraphStyle) forKey:(id)kCTParagraphStyleAttributeName];
|
||||
}
|
||||
}
|
||||
|
||||
// add textBlock if there's one (this has padding and background color)
|
||||
NSArray *textBlocks = [attributes objectForKey:DTTextBlocksAttribute];
|
||||
if (textBlocks)
|
||||
{
|
||||
[newAttributes setObject:textBlocks forKey:DTTextBlocksAttribute];
|
||||
}
|
||||
|
||||
// transfer all lists so that
|
||||
NSArray *lists = [attributes objectForKey:DTTextListsAttribute];
|
||||
if (lists)
|
||||
{
|
||||
[newAttributes setObject:lists forKey:DTTextListsAttribute];
|
||||
}
|
||||
|
||||
// add a marker so that we know that this is a field/prefix
|
||||
[newAttributes setObject:DTListPrefixField forKey:DTFieldAttribute];
|
||||
|
||||
NSString *prefix = [listStyle prefixWithCounter:listCounter];
|
||||
|
||||
if (prefix)
|
||||
{
|
||||
DTImage *image = nil;
|
||||
|
||||
if (listStyle.imageName)
|
||||
{
|
||||
image = [DTImage imageNamed:listStyle.imageName];
|
||||
|
||||
if (!image)
|
||||
{
|
||||
// image invalid
|
||||
listStyle.imageName = nil;
|
||||
|
||||
prefix = [listStyle prefixWithCounter:listCounter];
|
||||
}
|
||||
}
|
||||
|
||||
NSMutableAttributedString *tmpStr = [[NSMutableAttributedString alloc] initWithString:prefix attributes:newAttributes];
|
||||
|
||||
if (image)
|
||||
{
|
||||
// make an attachment for the image
|
||||
DTImageTextAttachment *attachment = [[DTImageTextAttachment alloc] init];
|
||||
attachment.image = image;
|
||||
attachment.displaySize = image.size;
|
||||
|
||||
#if DTCORETEXT_SUPPORT_NS_ATTRIBUTES && TARGET_OS_IPHONE
|
||||
// need run delegate for sizing
|
||||
CTRunDelegateRef embeddedObjectRunDelegate = createEmbeddedObjectRunDelegate(attachment);
|
||||
[newAttributes setObject:CFBridgingRelease(embeddedObjectRunDelegate) forKey:(id)kCTRunDelegateAttributeName];
|
||||
#endif
|
||||
|
||||
// add attachment
|
||||
[newAttributes setObject:attachment forKey:NSAttachmentAttributeName];
|
||||
|
||||
if (listStyle.position == DTCSSListStylePositionInside)
|
||||
{
|
||||
[tmpStr setAttributes:newAttributes range:NSMakeRange(2, 1)];
|
||||
}
|
||||
else
|
||||
{
|
||||
[tmpStr setAttributes:newAttributes range:NSMakeRange(1, 1)];
|
||||
}
|
||||
}
|
||||
|
||||
return tmpStr;
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,18 @@
|
||||
//
|
||||
// NSAttributedString+DTDebug.h
|
||||
// DTCoreText
|
||||
//
|
||||
// Created by Oliver Drobnik on 29.04.13.
|
||||
// Copyright (c) 2013 Drobnik.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
/**
|
||||
The *DTDebug* category contains methods for debugging and dumping attributed strings
|
||||
*/
|
||||
@interface NSAttributedString (DTDebug)
|
||||
|
||||
- (void)dumpRangesOfAttribute:(id)attribute;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,39 @@
|
||||
//
|
||||
// NSAttributedString+DTDebug.m
|
||||
// DTCoreText
|
||||
//
|
||||
// Created by Oliver Drobnik on 29.04.13.
|
||||
// Copyright (c) 2013 Drobnik.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import "NSAttributedString+DTDebug.h"
|
||||
|
||||
|
||||
@implementation NSAttributedString (DTDebug)
|
||||
|
||||
- (void)dumpRangesOfAttribute:(id)attribute
|
||||
{
|
||||
NSMutableString *tmpString = [NSMutableString string];
|
||||
|
||||
NSRange entireRange = NSMakeRange(0, [self length]);
|
||||
[self enumerateAttribute:attribute inRange:entireRange options:0 usingBlock:^(id value, NSRange range, BOOL *stop) {
|
||||
NSString *rangeString = [[self string] substringWithRange:range];
|
||||
NSString *valueString;
|
||||
|
||||
if ([value isKindOfClass:[NSArray class]])
|
||||
{
|
||||
valueString = [(NSArray *)value componentsJoinedByString:@", "];
|
||||
}
|
||||
else
|
||||
{
|
||||
valueString = [value debugDescription];
|
||||
}
|
||||
|
||||
[tmpString appendFormat:@"%@ %@ '%@'\n", NSStringFromRange(range), valueString, [rangeString stringByReplacingOccurrencesOfString:@"\n" withString:@"\\n"]];
|
||||
}];
|
||||
|
||||
printf("%s", [tmpString UTF8String]);
|
||||
}
|
||||
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,96 @@
|
||||
//
|
||||
// NSAttributedString+HTML.h
|
||||
// DTCoreText
|
||||
//
|
||||
// Created by Oliver Drobnik on 1/9/11.
|
||||
// Copyright 2011 Drobnik.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import "DTCompatibility.h"
|
||||
|
||||
@class NSAttributedString;
|
||||
|
||||
/**
|
||||
Methods for generating an `NSAttributedString` from HTML data. Those methods exist on Mac but have not been ported (publicly) to iOS. This project aims to remedy this.
|
||||
|
||||
For a list of available options to pass to any of these methods please refer to [DTHTMLAttributedStringBuilder initWithHTML:options:documentAttributes:].
|
||||
*/
|
||||
|
||||
@interface NSAttributedString (HTML)
|
||||
|
||||
/**
|
||||
@name Creating an NSAttributedString
|
||||
*/
|
||||
|
||||
/**
|
||||
Initializes and returns a new `NSAttributedString` object from the HTML contained in the given object and base URL.
|
||||
@param data The data in HTML format from which to create the attributed string.
|
||||
@param docAttributes Currently not in used.
|
||||
@returns Returns an initialized object, or `nil` if the data can’t be decoded.
|
||||
@see [DTHTMLAttributedStringBuilder initWithHTML:options:documentAttributes:] for a list of available options
|
||||
*/
|
||||
- (id)initWithHTMLData:(NSData *)data documentAttributes:(NSDictionary * __autoreleasing*)docAttributes;
|
||||
|
||||
/**
|
||||
Initializes and returns a new `NSAttributedString` object from the HTML contained in the given object and base URL.
|
||||
@param data The data in HTML format from which to create the attributed string.
|
||||
@param baseURL An `NSURL` that represents the base URL for all links within the HTML.
|
||||
@param docAttributes Currently not in used.
|
||||
@returns Returns an initialized object, or `nil` if the data can’t be decoded.
|
||||
@see [DTHTMLAttributedStringBuilder initWithHTML:options:documentAttributes:] for a list of available options
|
||||
*/
|
||||
- (id)initWithHTMLData:(NSData *)data baseURL:(NSURL *)baseURL documentAttributes:(NSDictionary * __autoreleasing*)docAttributes;
|
||||
|
||||
/**
|
||||
Initializes and returns a new `NSAttributedString` object from the HTML contained in the given object and base URL.
|
||||
|
||||
@param data The data in HTML format from which to create the attributed string.
|
||||
@param options Specifies how the document should be loaded.
|
||||
@param docAttributes Currently not in used.
|
||||
@returns Returns an initialized object, or `nil` if the data can’t be decoded.
|
||||
@see [DTHTMLAttributedStringBuilder initWithHTML:options:documentAttributes:] for a list of available options
|
||||
*/
|
||||
- (id)initWithHTMLData:(NSData *)data options:(NSDictionary *)options documentAttributes:(NSDictionary * __autoreleasing*)docAttributes;
|
||||
|
||||
|
||||
/**
|
||||
@name Working with Custom HTML Attributes
|
||||
*/
|
||||
|
||||
/**
|
||||
Retrieves the dictionary of custom HTML attributes active at the given string index
|
||||
@param index The string index to query
|
||||
@returns The custom HTML attributes dictionary or `nil` if there aren't any at this index
|
||||
*/
|
||||
- (NSDictionary *)HTMLAttributesAtIndex:(NSUInteger)index;
|
||||
|
||||
/**
|
||||
Retrieves the range that an attribute with a given name is active for, beginning with the passed index
|
||||
|
||||
Since a custom HTML attribute can occur in multiple individual attribute dictionaries this extends the range from the passed index outwards until the full range of the custom HTML attribute has been found. Those range extensions have to have an identical value, as established by comparing them to the value of the custom attribute at the index with isEqual:
|
||||
@param name The name of the custom attribute to remove
|
||||
@param index The string index to query
|
||||
@returns The custom HTML attributes dictionary or `nil` if there aren't any at this index
|
||||
*/
|
||||
- (NSRange)rangeOfHTMLAttribute:(NSString *)name atIndex:(NSUInteger)index;
|
||||
|
||||
/**
|
||||
Retrieves the NSAttributedString with NSData
|
||||
|
||||
Currently only supports iOS by `___useiOS6Attributes`, if error occur return nil.
|
||||
|
||||
@param data The data must generate by `convertToData` function
|
||||
@return NSAttributedString from unarchiveObjectWithData, the data must generate by `convertToData` function
|
||||
*/
|
||||
+ (NSAttributedString *)attributedStringWithData:(NSData *)data;
|
||||
|
||||
/**
|
||||
Retrieves NSData with self
|
||||
|
||||
Currently only supports iOS by `___useiOS6Attributes`, if error occur return nil.
|
||||
|
||||
@return NSData from NSAttributedString execute archivedDataWithRootObject:
|
||||
*/
|
||||
- (NSData *)convertToData;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,277 @@
|
||||
//
|
||||
// NSAttributedString+HTML.m
|
||||
// DTCoreText
|
||||
//
|
||||
// Created by Oliver Drobnik on 1/9/11.
|
||||
// Copyright 2011 Drobnik.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import "NSAttributedString+HTML.h"
|
||||
|
||||
#if TARGET_OS_IPHONE
|
||||
#elif TARGET_OS_MAC
|
||||
#import <ApplicationServices/ApplicationServices.h>
|
||||
#endif
|
||||
|
||||
#import "DTHTMLElement.h"
|
||||
#import "DTCoreTextConstants.h"
|
||||
#import "DTHTMLAttributedStringBuilder.h"
|
||||
#import "DTTextAttachment.h"
|
||||
|
||||
#if TARGET_OS_IPHONE
|
||||
#import "NSAttributedStringRunDelegates.h"
|
||||
#endif
|
||||
|
||||
@implementation NSAttributedString (HTML)
|
||||
|
||||
- (id)initWithHTMLData:(NSData *)data documentAttributes:(NSDictionary * __autoreleasing*)docAttributes
|
||||
{
|
||||
return [self initWithHTMLData:data options:nil documentAttributes:docAttributes];
|
||||
}
|
||||
|
||||
- (id)initWithHTMLData:(NSData *)data baseURL:(NSURL *)base documentAttributes:(NSDictionary * __autoreleasing*)docAttributes
|
||||
{
|
||||
NSDictionary *optionsDict = nil;
|
||||
|
||||
if (base)
|
||||
{
|
||||
optionsDict = [NSDictionary dictionaryWithObject:base forKey:NSBaseURLDocumentOption];
|
||||
}
|
||||
|
||||
return [self initWithHTMLData:data options:optionsDict documentAttributes:docAttributes];
|
||||
}
|
||||
|
||||
- (id)initWithHTMLData:(NSData *)data options:(NSDictionary *)options documentAttributes:(NSDictionary * __autoreleasing*)docAttributes
|
||||
{
|
||||
// only with valid data
|
||||
if (![data length])
|
||||
{
|
||||
return nil;
|
||||
}
|
||||
|
||||
DTHTMLAttributedStringBuilder *stringBuilder = [[DTHTMLAttributedStringBuilder alloc] initWithHTML:data options:options documentAttributes:docAttributes];
|
||||
|
||||
void (^callBackBlock)(DTHTMLElement *element) = [options objectForKey:DTWillFlushBlockCallBack];
|
||||
|
||||
if (callBackBlock)
|
||||
{
|
||||
[stringBuilder setWillFlushCallback:callBackBlock];
|
||||
}
|
||||
|
||||
// This needs to be on a separate line so that ARC can handle releasing the object properly
|
||||
// return [stringBuilder generatedAttributedString]; shows leak in instruments
|
||||
id string = [stringBuilder generatedAttributedString];
|
||||
|
||||
return string;
|
||||
}
|
||||
|
||||
#pragma mark - NSAttributedString Archiving
|
||||
+ (NSMutableDictionary *)getArchivingDictionaryWith:(NSDictionary *)attrs
|
||||
{
|
||||
|
||||
NSDictionary *archiveDict = attrs[DTArchivingAttribute];
|
||||
NSMutableDictionary *dict = nil;
|
||||
|
||||
if (![archiveDict isKindOfClass:[NSDictionary class]])
|
||||
{
|
||||
dict = [NSMutableDictionary dictionary];
|
||||
}
|
||||
else
|
||||
{
|
||||
dict = [archiveDict mutableCopy];
|
||||
}
|
||||
|
||||
|
||||
return dict;
|
||||
}
|
||||
|
||||
- (NSData *)convertToData
|
||||
{
|
||||
NSMutableAttributedString *appendString = [self mutableCopy];
|
||||
#if DTCORETEXT_SUPPORT_NS_ATTRIBUTES && TARGET_OS_IPHONE
|
||||
NSUInteger length = [self length];
|
||||
if (length)
|
||||
{
|
||||
[self enumerateAttributesInRange:NSMakeRange(0, length-1) options:0 usingBlock:^(NSDictionary<NSString *,id> * _Nonnull attrs, NSRange range, BOOL * _Nonnull stop) {
|
||||
|
||||
NSMutableDictionary *dict = [[self class] getArchivingDictionaryWith:attrs];
|
||||
|
||||
if (attrs[NSAttachmentAttributeName])
|
||||
{
|
||||
DTTextAttachment *attatchment = attrs[NSAttachmentAttributeName];
|
||||
|
||||
NSString *imgPath = nil;
|
||||
|
||||
if ([[attatchment.contentURL scheme] isEqualToString:@"file"])
|
||||
{
|
||||
imgPath = [attatchment.contentURL path];
|
||||
NSUInteger homeDirLength = [NSHomeDirectory() length];
|
||||
|
||||
if ([imgPath hasPrefix:NSHomeDirectory()] && [imgPath length] > homeDirLength)
|
||||
{
|
||||
imgPath = [imgPath substringFromIndex:homeDirLength];
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
imgPath = [attatchment.contentURL absoluteString];
|
||||
}
|
||||
|
||||
if (imgPath)
|
||||
{
|
||||
[dict setObject:imgPath forKey:NSAttachmentAttributeName];
|
||||
[appendString addAttribute:DTArchivingAttribute value:dict range:range];
|
||||
}
|
||||
|
||||
[appendString removeAttribute:(id)kCTRunDelegateAttributeName range:range];
|
||||
}
|
||||
// if there will others attribute to archiving , implement like this.
|
||||
if (attrs[DTBackgroundStrokeColorAttribute])
|
||||
{
|
||||
CGColorRef strokeColor = (__bridge CGColorRef)(attrs[DTBackgroundStrokeColorAttribute]);
|
||||
|
||||
UIColor *stoke = [[UIColor alloc] initWithCGColor:strokeColor];
|
||||
[dict setObject:stoke forKey:DTBackgroundStrokeColorAttribute];
|
||||
|
||||
[appendString addAttribute:DTArchivingAttribute value:dict range:range];
|
||||
[appendString removeAttribute:(id)DTBackgroundStrokeColorAttribute range:range];
|
||||
}
|
||||
|
||||
}];
|
||||
}
|
||||
#endif
|
||||
|
||||
NSData *data = nil;
|
||||
@try
|
||||
{
|
||||
data = [NSKeyedArchiver archivedDataWithRootObject:appendString];
|
||||
}
|
||||
@catch (NSException *exception)
|
||||
{
|
||||
data = nil;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
+ (NSAttributedString *)attributedStringWithData:(NSData *)data
|
||||
{
|
||||
NSMutableAttributedString *appendString = nil;
|
||||
@try
|
||||
{
|
||||
appendString = (NSMutableAttributedString *)[NSKeyedUnarchiver unarchiveObjectWithData:data];
|
||||
}
|
||||
@catch (NSException *exception)
|
||||
{
|
||||
appendString = nil;
|
||||
}
|
||||
|
||||
NSUInteger length = [appendString length];
|
||||
if (length)
|
||||
{
|
||||
[appendString enumerateAttributesInRange:NSMakeRange(0, length-1) options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired usingBlock:^(NSDictionary<NSString *,id> * _Nonnull attrs, NSRange range, BOOL * _Nonnull stop) {
|
||||
|
||||
#if DTCORETEXT_SUPPORT_NS_ATTRIBUTES && TARGET_OS_IPHONE
|
||||
|
||||
if (attrs[NSAttachmentAttributeName])
|
||||
{
|
||||
DTTextAttachment *attatchment = attrs[NSAttachmentAttributeName];
|
||||
|
||||
if ([[attatchment.contentURL scheme] isEqualToString:@"file"])
|
||||
{
|
||||
NSMutableDictionary *dict = [[self class] getArchivingDictionaryWith:attrs];
|
||||
|
||||
NSString *imgPath = dict[NSAttachmentAttributeName];
|
||||
if (imgPath)
|
||||
{
|
||||
if (![imgPath hasPrefix:NSHomeDirectory()])
|
||||
{
|
||||
imgPath = [NSHomeDirectory() stringByAppendingPathComponent:imgPath];
|
||||
}
|
||||
attatchment.contentURL = [NSURL fileURLWithPath:imgPath];
|
||||
}
|
||||
}
|
||||
|
||||
CTRunDelegateRef embeddedObjectRunDelegate = createEmbeddedObjectRunDelegate(attatchment);
|
||||
|
||||
[appendString addAttribute:(id)kCTRunDelegateAttributeName value:CFBridgingRelease(embeddedObjectRunDelegate) range:range];
|
||||
}
|
||||
// if there will others attribute to archiving , implement like this.
|
||||
if (attrs[DTBackgroundStrokeColorAttribute])
|
||||
{
|
||||
NSMutableDictionary *dict = [self getArchivingDictionaryWith:attrs];
|
||||
UIColor *stroke = dict[DTBackgroundStrokeColorAttribute];
|
||||
CGColorRef strokeColor = stroke.CGColor;
|
||||
|
||||
[appendString addAttribute:DTBackgroundStrokeColorAttribute value:(__bridge id)strokeColor range:range];
|
||||
}
|
||||
#endif
|
||||
|
||||
}];
|
||||
}
|
||||
return [appendString copy];
|
||||
}
|
||||
|
||||
#pragma mark - Working with Custom HTML Attributes
|
||||
|
||||
- (NSDictionary *)HTMLAttributesAtIndex:(NSUInteger)index
|
||||
{
|
||||
return [self attribute:DTCustomAttributesAttribute atIndex:index effectiveRange:NULL];
|
||||
}
|
||||
|
||||
- (NSRange)rangeOfHTMLAttribute:(NSString *)name atIndex:(NSUInteger)index
|
||||
{
|
||||
NSRange rangeSoFar;
|
||||
|
||||
NSDictionary *attributes = [self attribute:DTCustomAttributesAttribute atIndex:index effectiveRange:&rangeSoFar];
|
||||
|
||||
NSAssert(attributes, @"No custom attribute '%@' at index %d", name, (int)index);
|
||||
|
||||
// check if there is a value for this custom attribute name
|
||||
id value = [attributes objectForKey:name];
|
||||
|
||||
if (!attributes || !value)
|
||||
{
|
||||
return NSMakeRange(NSNotFound, 0);
|
||||
}
|
||||
|
||||
// search towards beginning
|
||||
while (rangeSoFar.location>0)
|
||||
{
|
||||
NSRange extendedRange;
|
||||
attributes = [self attribute:DTCustomAttributesAttribute atIndex:rangeSoFar.location-1 effectiveRange:&extendedRange];
|
||||
|
||||
id extendedValue = [attributes objectForKey:name];
|
||||
|
||||
// abort search if key not found or value not identical
|
||||
if (!extendedValue || ![extendedValue isEqual:value])
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
rangeSoFar = NSUnionRange(rangeSoFar, extendedRange);
|
||||
}
|
||||
|
||||
NSUInteger length = [self length];
|
||||
|
||||
// search towards end
|
||||
while (NSMaxRange(rangeSoFar)<length)
|
||||
{
|
||||
NSRange extendedRange;
|
||||
attributes = [self attribute:DTCustomAttributesAttribute atIndex:NSMaxRange(rangeSoFar) effectiveRange:&extendedRange];
|
||||
|
||||
id extendedValue = [attributes objectForKey:name];
|
||||
|
||||
// abort search if key not found or value not identical
|
||||
if (!extendedValue || ![extendedValue isEqual:value])
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
rangeSoFar = NSUnionRange(rangeSoFar, extendedRange);
|
||||
}
|
||||
|
||||
return rangeSoFar;
|
||||
}
|
||||
|
||||
@end
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user