libs-gui/Source/NSAttributedString.m
David Chisnall b4e3915c33 Turn release messages sent to autorelease pools into drain messages. No
functionality change in non-GC mode, in GC mode it invokes a quick GC pass to
try to delete the short-lived objects.

Also deleted some [pool release] lines just before exit() or return-from-main
statements.  These cause objects to be swapped in and destructors to be run to
no benefit (the OS will reclaim this memory without requiring stuff to be
swapped in when the process exits).



git-svn-id: svn+ssh://svn.gna.org/svn/gnustep/libs/gui/trunk@33146 72102866-910b-0410-8b05-ffd578937521
2011-05-27 12:42:37 +00:00

1852 lines
51 KiB
Objective-C

/** <title>NSAttributedStringAdditions</title>
<abstract>Categories which add capabilities to NSAttributedString</abstract>
Copyright (C) 1999 Free Software Foundation, Inc.
Author: Richard Frith-Macdonald <richard@brainstorm.co.uk>
Date: July 1999
Modifications: Fred Kiefer <FredKiefer@gmx.de>
Date: June 2000
This file is part of the GNUstep GUI Library.
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; see the file COPYING.LIB.
If not, see <http://www.gnu.org/licenses/> or write to the
Free Software Foundation, 51 Franklin Street, Fifth Floor,
Boston, MA 02110-1301, USA.
*/
#import <Foundation/NSArray.h>
#import <Foundation/NSAutoreleasePool.h>
#import <Foundation/NSBundle.h>
#import <Foundation/NSCharacterSet.h>
#import <Foundation/NSData.h>
#import <Foundation/NSDebug.h>
#import <Foundation/NSError.h>
#import <Foundation/NSException.h>
#import <Foundation/NSFileManager.h>
#import <Foundation/NSPathUtilities.h>
#import <Foundation/NSRange.h>
#import <Foundation/NSString.h>
#import <Foundation/NSValue.h>
#import "AppKit/NSAttributedString.h"
#import "AppKit/NSDocumentController.h"
#import "AppKit/NSParagraphStyle.h"
#import "AppKit/NSPasteboard.h"
#import "AppKit/NSTextAttachment.h"
#import "AppKit/NSColor.h"
#import "AppKit/NSFileWrapper.h"
#import "AppKit/NSFont.h"
#import "AppKit/NSFontManager.h"
// For the colour name spaces
#import "AppKit/NSGraphics.h"
#import "AppKit/NSTextTable.h"
#import "GNUstepGUI/GSTextConverter.h"
#import "GSGuiPrivate.h"
/* Cache class pointers to avoid the expensive lookup by string. */
static Class dictionaryClass = nil;
static Class stringClass = nil;
/* A character set containing characters that separate words. */
static NSCharacterSet *wordBreakCSet = nil;
/* A character set containing characters that are legal within words. */
static NSCharacterSet *wordCSet = nil;
/* Character sets containing characters that are white space and
not white space */
static NSCharacterSet *whiteCSet = nil;
static NSCharacterSet *nonWhiteCSet = nil;
/* A String containing the attachment character */
static NSString *attachmentString = nil;
/* This function initializes all the previous cached values. */
static void cache_init_real(void)
{
NSMutableCharacterSet *m;
NSCharacterSet *cset;
unichar ch = NSAttachmentCharacter;
/* Initializes Class pointer cache */
dictionaryClass = [NSDictionary class];
stringClass = [NSString class];
/* Initializes wordBreakCSet */
m = [NSMutableCharacterSet new];
cset = [NSCharacterSet whitespaceAndNewlineCharacterSet];
[m formUnionWithCharacterSet: cset];
cset = [NSCharacterSet punctuationCharacterSet];
[m formUnionWithCharacterSet: cset];
cset = [NSCharacterSet controlCharacterSet];
[m formUnionWithCharacterSet: cset];
cset = [NSCharacterSet illegalCharacterSet];
[m formUnionWithCharacterSet: cset];
[m addCharactersInString: @"<>"];
[m removeCharactersInString: @"_"];
wordBreakCSet = [m copy];
RELEASE (m);
/* Initializes wordCSet */
wordCSet = [[wordBreakCSet invertedSet] copy];
/* Initializes white space and non-white space character sets */
whiteCSet = [[NSCharacterSet whitespaceCharacterSet] copy];
nonWhiteCSet = [[whiteCSet invertedSet] copy];
/* Initializes attachmentString */
attachmentString = [stringClass stringWithCharacters: &ch length: 1];
RETAIN (attachmentString);
}
/* This inline function calls cache_init_real () the first time it is
invoked, and does nothing afterwards. Thus we get both speed
(cache_init is inlined and only compares a pointer to nil when the
cache has been initialized) and limit memory consumption (we are
not copying everywhere the real initialization code, which is in
cache_real_init (), which is not inlined.).*/
static inline void cache_init(void)
{
if (dictionaryClass == nil)
{
cache_init_real ();
}
}
/* Return the class that handles format from the first bundle it finds */
static
Class converter_bundles(NSString *format, BOOL producer)
{
Class converter_class = Nil;
NSEnumerator *benum;
NSString *dpath;
/* Find the bundle paths */
benum = [NSStandardLibraryPaths() objectEnumerator];
while ((dpath = [benum nextObject]))
{
NSEnumerator *direnum;
NSString *path;
dpath = [dpath stringByAppendingPathComponent: @"Bundles"];
dpath = [dpath stringByAppendingPathComponent: @"TextConverters"];
if ([[NSFileManager defaultManager] fileExistsAtPath: dpath])
direnum = [[NSFileManager defaultManager] enumeratorAtPath: dpath];
else
direnum = nil;
while (direnum && (path = [direnum nextObject]))
{
Class bclass;
NSString *full_path;
NSBundle *aBundle;
if ([[path pathExtension] isEqual: @"bundle"] == NO)
continue;
full_path = [dpath stringByAppendingPathComponent: path];
aBundle = [NSBundle bundleWithPath: full_path];
if (aBundle && ((bclass = [aBundle principalClass])))
{
if ([bclass respondsToSelector:
@selector(classForFormat: producer: )])
{
converter_class = (Class)[bclass classForFormat: format
producer: producer];
}
else
{
NSString *converter_name;
if (producer)
{
converter_name
= [format stringByAppendingString: @"Producer"];
}
else
{
converter_name
= [format stringByAppendingString: @"Consumer"];
}
converter_class = [aBundle classNamed: converter_name];
}
}
if (converter_class)
break;
}
if (converter_class)
break;
}
return converter_class;
}
/*
Return a suitable converter for the text format supplied as argument.
If producer is YES a class capable of writting that format is returned,
otherwise a class able to read the format is returned.
*/
static Class converter_class(NSString *format, BOOL producer)
{
static NSMutableDictionary *p_classes = nil;
static NSMutableDictionary *c_classes = nil;
Class found;
if (producer)
{
if (p_classes == nil)
p_classes = [NSMutableDictionary new];
found = [p_classes objectForKey: format];
if (found == Nil)
{
found = converter_bundles(format, producer);
if (found != Nil)
NSDebugLog(@"Found converter %@ for format %@", found, format);
if (found != Nil)
[p_classes setObject: found forKey: format];
}
return found;
}
else
{
if (c_classes == nil)
c_classes = [NSMutableDictionary new];
found = [c_classes objectForKey: format];
if (found == Nil)
{
found = converter_bundles(format, producer);
if (found != Nil)
NSDebugLog(@"Found converter %@ for format %@", found, format);
if (found != Nil)
[c_classes setObject: found forKey: format];
}
return found;
}
return Nil;
}
static inline NSError*
create_error(int code, NSString* desc)
{
return [NSError errorWithDomain: @"NSAttributedString"
code: code
userInfo: [NSDictionary
dictionaryWithObjectsAndKeys: desc,
NSLocalizedDescriptionKey, nil]];
}
@implementation NSAttributedString (AppKit)
+ (NSArray *) textFileTypes
{
// FIXME: Apply service filters
return [self textUnfilteredFileTypes];
}
+ (NSArray *) textPasteboardTypes
{
// FIXME: Apply service filters
return [self textUnfilteredPasteboardTypes];
}
+ (NSArray *) textTypes
{
// FIXME: Apply service filters
return [self textUnfilteredTypes];
}
+ (NSArray *) textUnfilteredFileTypes
{
return [NSArray arrayWithObjects: @"txt", @"rtf", @"rtfd", @"html", nil];
}
+ (NSArray *) textUnfilteredPasteboardTypes
{
return [NSArray arrayWithObjects: NSStringPboardType, NSRTFPboardType,
NSRTFDPboardType, NSHTMLPboardType, nil];
}
+ (NSArray *) textUnfilteredTypes
{
return [NSArray arrayWithObjects: @"public.plain-text",
@"public.rtf",
@"com.apple.rtfd",
@"public.html",
/*
@"public.xml",
@"com.apple.webarchive",
@"com.microsoft.word.doc",
@"com.microsoft.word.wordml",
@"org.openxmlformats.wordprocessingml.document",
@"org.oasis-open.opendocument.text",
@"com.apple.traditional-mac-plain-text",
*/
nil];
}
+ (NSAttributedString *) attributedStringWithAttachment:
(NSTextAttachment *)attachment
{
NSDictionary *attributes;
cache_init ();
attributes = [dictionaryClass dictionaryWithObject: attachment
forKey: NSAttachmentAttributeName];
return AUTORELEASE ([[self alloc] initWithString: attachmentString
attributes: attributes]);
}
- (BOOL) containsAttachments
{
NSRange aRange;
cache_init ();
aRange = [[self string] rangeOfString: attachmentString];
if (aRange.length > 0)
{
return YES;
}
else
{
return NO;
}
}
- (NSDictionary *) fontAttributesInRange: (NSRange)range
{
NSDictionary *all;
static SEL sel = 0;
IMP objForKey;
id objects[8];
id keys[8];
int count = 0;
if (NSMaxRange(range) > [self length])
{
[NSException raise: NSRangeException
format: @"RangeError in method -fontAttributesInRange: "];
}
all = [self attributesAtIndex: range.location
effectiveRange: &range];
if (sel == 0)
{
sel = @selector (objectForKey: );
}
objForKey = [all methodForSelector: sel];
#define NSATT_GET_ATTRIBUTE(attribute) \
keys[count] = attribute; \
objects[count] = (*objForKey) (all, sel, keys[count]); \
if (objects[count] != nil) count++;
NSATT_GET_ATTRIBUTE (NSFontAttributeName);
NSATT_GET_ATTRIBUTE (NSForegroundColorAttributeName);
NSATT_GET_ATTRIBUTE (NSBackgroundColorAttributeName);
NSATT_GET_ATTRIBUTE (NSUnderlineStyleAttributeName);
NSATT_GET_ATTRIBUTE (NSSuperscriptAttributeName);
NSATT_GET_ATTRIBUTE (NSBaselineOffsetAttributeName);
NSATT_GET_ATTRIBUTE (NSKernAttributeName);
NSATT_GET_ATTRIBUTE (NSLigatureAttributeName);
#undef NSATT_GET_ATTRIBUTE
cache_init ();
return [dictionaryClass dictionaryWithObjects: objects
forKeys: keys
count: count];
}
- (NSDictionary*) rulerAttributesInRange: (NSRange)range
{
id style;
cache_init ();
if (NSMaxRange (range) > [self length])
{
[NSException raise: NSRangeException
format: @"RangeError in method -rulerAttributesInRange: "];
}
style = [self attribute: NSParagraphStyleAttributeName
atIndex: range.location
effectiveRange: &range];
if (style != nil)
{
return [dictionaryClass dictionaryWithObject: style
forKey: NSParagraphStyleAttributeName];
}
return [dictionaryClass dictionary];
}
- (NSUInteger) lineBreakByHyphenatingBeforeIndex: (NSUInteger)location
withinRange: (NSRange)aRange
{
// FIXME
return [self lineBreakBeforeIndex: location
withinRange: aRange];
}
- (NSUInteger) lineBreakBeforeIndex: (NSUInteger)location
withinRange: (NSRange)aRange
{
NSString *str = [self string];
unsigned length = [str length];
NSRange scanRange;
NSRange startRange;
cache_init ();
if (NSMaxRange (aRange) > length || location > length)
{
[NSException raise: NSRangeException
format: @"RangeError in method -lineBreakBeforeIndex: withinRange: "];
}
if (!NSLocationInRange (location, aRange))
{
return NSNotFound;
}
scanRange = NSMakeRange (aRange.location, location - aRange.location);
startRange = [str rangeOfCharacterFromSet: wordBreakCSet
options: NSBackwardsSearch | NSLiteralSearch
range: scanRange];
while (startRange.length > 0 && startRange.location > 0
&& [str characterAtIndex: startRange.location] == '\''
&& [wordCSet characterIsMember:
[str characterAtIndex: startRange.location-1]])
{
location = startRange.location - 1;
scanRange = NSMakeRange (0, location);
startRange = [str rangeOfCharacterFromSet: wordBreakCSet
options: NSBackwardsSearch|NSLiteralSearch range: scanRange];
}
if (startRange.length == 0)
{
return NSNotFound;
}
else
{
return NSMaxRange (startRange);
}
}
- (NSRange) doubleClickAtIndex: (NSUInteger)location
{
NSString *str = [self string];
unsigned length = [str length];
NSRange scanRange;
NSRange startRange;
NSRange endRange;
NSCharacterSet *breakCSet;
cache_init ();
if (location > length)
{
[NSException raise: NSRangeException
format: @"RangeError in method -doubleClickAtIndex: "];
}
/*
* Double clicking on a white space character selects all surrounding
* white space. Otherwise, if the location lies between words, a double
* click selects only the character actually clicked on.
*/
if ([whiteCSet characterIsMember: [str characterAtIndex: location]])
{
breakCSet = nonWhiteCSet;
}
else if ([wordBreakCSet characterIsMember: [str characterAtIndex: location]])
{
if (location == 0 || location == length - 1
|| [str characterAtIndex: location] != '\''
|| ! [wordCSet characterIsMember: [str characterAtIndex: location - 1]]
|| ! [wordCSet characterIsMember: [str characterAtIndex: location + 1]])
{
return NSMakeRange(location, 1);
}
breakCSet = wordBreakCSet;
}
else
{
breakCSet = wordBreakCSet;
}
scanRange = NSMakeRange (0, location);
startRange = [str rangeOfCharacterFromSet: breakCSet
options: NSBackwardsSearch|NSLiteralSearch
range: scanRange];
/*
* Don't treat single quotes embedded within a word as break characters.
* Note: The loop condition is always false when breakCSet==nonWhiteSetCSet.
*/
while (startRange.length > 0
&& startRange.location > 0 && startRange.location < length - 1
&& [str characterAtIndex: startRange.location] == '\''
&& [wordCSet characterIsMember:
[str characterAtIndex: startRange.location - 1]]
&& [wordCSet characterIsMember:
[str characterAtIndex: startRange.location + 1]])
{
location = startRange.location - 1;
scanRange = NSMakeRange (0, location);
startRange = [str rangeOfCharacterFromSet: wordBreakCSet
options: NSBackwardsSearch|NSLiteralSearch range: scanRange];
}
scanRange = NSMakeRange (location, length - location);
endRange = [str rangeOfCharacterFromSet: breakCSet
options: NSLiteralSearch
range: scanRange];
/*
* Don't treat single quotes embedded within a word as break characters.
* Note: The loop condition is always false when breakCSet==nonWhiteSetCSet.
*/
while (endRange.length > 0
&& endRange.location > 0 && endRange.location < length - 1
&& [str characterAtIndex: endRange.location] == '\''
&& [wordCSet characterIsMember:
[str characterAtIndex: endRange.location - 1]]
&& [wordCSet characterIsMember:
[str characterAtIndex: endRange.location + 1]])
{
location = endRange.location + 1;
scanRange = NSMakeRange (location, length - location);
endRange = [str rangeOfCharacterFromSet: wordBreakCSet
options: NSLiteralSearch range: scanRange];
}
if (startRange.length == 0)
{
location = 0;
}
else
{
location = NSMaxRange (startRange);
}
if (endRange.length == 0)
{
length = length - location;
}
else
{
length = endRange.location - location;
}
return NSMakeRange (location, length);
}
- (NSUInteger) nextWordFromIndex: (NSUInteger)location
forward: (BOOL)isForward
{
NSString *str = [self string];
unsigned length = [str length];
NSRange range;
if (location > length)
{
[NSException raise: NSRangeException
format: @"RangeError in method -nextWordFromIndex: forward: "];
}
/* Please note that we consider ' a valid word separator. This is
what Emacs does and is perfectly correct. If you want to change
the word separators, the right approach is to use a different
character set for word separators - the following code should be
unchanged whatever characters you use to separate words. */
cache_init ();
if (isForward)
{
/* What we want to do is: move forward to the next chunk of
non-word separator characters, skip them all, and return the
location just after them. */
if (location == length)
{
return length;
}
/* Move forward to the next non-word separator. */
range = NSMakeRange (location, length - location);
range = [str rangeOfCharacterFromSet: wordCSet
options: NSLiteralSearch
range: range];
if (range.location == NSNotFound)
{
return length;
}
/* rangeOfCharacterFromSet: options: range: only returns the range
of the first non-word-separator character ... we want to skip
them all! So we need to search again, this time for the
first word-separator character, and return the first such
character. */
range = NSMakeRange (range.location, length - range.location);
range = [str rangeOfCharacterFromSet: wordBreakCSet
options: NSLiteralSearch
range: range];
if (range.location == NSNotFound)
{
return length;
}
return range.location;
}
else
{
/* What we want to do is: move backward to the next chunk of
non-word separator characters, skip them all, and return the
location just at the beginning of the chunk. */
if (location == 0)
{
return 0;
}
/* Move backward to the next non-word separator. */
range = NSMakeRange (0, location);
range = [str rangeOfCharacterFromSet: wordCSet
options: NSBackwardsSearch | NSLiteralSearch
range: range];
if (range.location == NSNotFound)
{
return 0;
}
/* rangeOfCharacterFromSet: options: range: only returns the range
of the first non-word-separator character ... we want to skip
them all! So we need to search again, this time for the
first word-separator character. */
range = NSMakeRange (0, range.location);
range = [str rangeOfCharacterFromSet: wordBreakCSet
options: NSBackwardsSearch | NSLiteralSearch
range: range];
if (range.location == NSNotFound)
{
return 0;
}
return NSMaxRange (range);
}
}
- (id) initWithRTFDFileWrapper: (NSFileWrapper *)wrapper
documentAttributes: (NSDictionary **)dict
{
return [self initWithRTFD: [wrapper serializedRepresentation]
documentAttributes: dict];
}
- (id) initWithRTFD: (NSData*)data
documentAttributes: (NSDictionary**)dict
{
NSDictionary *options;
options = [NSDictionary dictionaryWithObject: NSRTFDTextDocumentType
forKey: NSDocumentTypeDocumentOption];
return [self initWithData: data
options: options
documentAttributes: dict
error: NULL];
}
- (id) initWithRTF: (NSData *)data
documentAttributes: (NSDictionary **)dict
{
NSDictionary *options;
options = [NSDictionary dictionaryWithObject: NSRTFTextDocumentType
forKey: NSDocumentTypeDocumentOption];
return [self initWithData: data
options: options
documentAttributes: dict
error: NULL];
}
- (id) initWithHTML: (NSData *)data
documentAttributes: (NSDictionary **)dict
{
return [self initWithHTML: data
baseURL: nil
documentAttributes: dict];
}
- (id) initWithHTML: (NSData *)data
baseURL: (NSURL *)base
documentAttributes: (NSDictionary **)dict
{
NSDictionary *options = nil;
if (base != nil)
options = [NSDictionary dictionaryWithObject: base
forKey: NSBaseURLDocumentOption];
return [self initWithHTML: data
options: options
documentAttributes: dict];
}
- (id) initWithHTML: (NSData *)data
options: (NSDictionary *)options
documentAttributes: (NSDictionary **)dict
{
if (options == nil)
{
options = [NSDictionary dictionaryWithObject: NSHTMLTextDocumentType
forKey: NSDocumentTypeDocumentOption];
}
else if ([options objectForKey: NSDocumentTypeDocumentOption] == nil)
{
options = AUTORELEASE([options mutableCopy]);
[(NSMutableDictionary*)options setObject: NSHTMLTextDocumentType
forKey: NSDocumentTypeDocumentOption];
}
/*
The converter should support:
NSHTMLTextDocumentType
@"public.html"
@"html"
*/
return [self initWithData: data
options: options
documentAttributes: dict
error: NULL];
}
- (id) initWithDocFormat: (NSData *)data
documentAttributes: (NSDictionary **)dict
{
NSDictionary *options;
options = [NSDictionary dictionaryWithObject: NSDocFormatTextDocumentType
forKey: NSDocumentTypeDocumentOption];
return [self initWithData: data
options: options
documentAttributes: dict
error: NULL];
}
- (id) initWithData: (NSData *)data
options: (NSDictionary *)options
documentAttributes: (NSDictionary **)dict
error: (NSError **)error
{
NSString *type = [options objectForKey: NSDocumentTypeDocumentOption];
Class converter;
if (data == nil)
{
if (error)
*error = create_error(0, NSLocalizedString(@"No data specified for data loading.",
@"Error description"));
RELEASE(self);
return nil;
}
if (type == nil)
{
// Make sure this buffer is long enough
char prefix[14];
NSUInteger l = [data length];
if (l < sizeof(prefix))
{
[data getBytes: prefix length: l];
prefix[l] = 0;
}
else
{
[data getBytes: prefix length: sizeof(prefix)];
}
// The list of file types below was derived from Apache's conf/magic file
// FIXME extend the list
if (strncmp(prefix, "{\\rtf", 5) == 0)
{
type = NSRTFTextDocumentType;
}
else if (strncasecmp(prefix, "<!doctype html", 14) == 0 ||
strncasecmp(prefix, "<head", 5) == 0 ||
strncasecmp(prefix, "<title", 6) == 0 ||
strncasecmp(prefix, "<html", 5) == 0 ||
strncmp(prefix, "<!--", 4) == 0 ||
strncasecmp(prefix, "<h1", 3) == 0)
{
type = NSHTMLTextDocumentType;
}
}
if (type == nil)
{
if (error)
*error = create_error(0, NSLocalizedString(@"No type specified for data.",
@"Error description"));
RELEASE(self);
return nil;
}
converter = converter_class(type, NO);
if (converter != Nil)
{
NSAttributedString *new;
new = [converter
parseData: data
options: options
documentAttributes: dict
error: error
class: [self class]];
// We do not return self but the newly created object
RELEASE(self);
return RETAIN(new);
}
else if ([type isEqualToString: NSPlainTextDocumentType]
|| [type isEqualToString: @"public.plain-text"]
|| [type isEqualToString: @"text"])
{
// FIXME: Should we have a proper converter for this type?
NSStringEncoding encoding = [[options objectForKey: @"CharacterEncoding"]
intValue];
NSDictionary *defaultAttrs = [options objectForKey: @"DefaultAttributes"];
NSString *str;
str = [[NSString alloc] initWithData: data
encoding: encoding];
self = [self initWithString: str
attributes: defaultAttrs];
RELEASE(str);
return self;
}
if (error)
*error = create_error(0, NSLocalizedString(@"Could not load data.",
@"Error description"));
RELEASE(self);
return nil;
}
- (id) initWithPath: (NSString *)path
documentAttributes: (NSDictionary **)dict
{
BOOL isDir = NO;
if (path == nil)
{
RELEASE (self);
return nil;
}
if ([[NSFileManager defaultManager]
fileExistsAtPath: path isDirectory: &isDir] && isDir)
{
// FIXME: This expects the file to be RTFD
NSFileWrapper *fw;
fw = [[NSFileWrapper alloc] initWithPath: path];
AUTORELEASE (fw);
return [self initWithRTFDFileWrapper: fw documentAttributes: dict];
}
else
{
return [self initWithURL: [NSURL fileURLWithPath: path]
documentAttributes: dict];
}
}
- (id) initWithURL: (NSURL *)url
documentAttributes: (NSDictionary **)dict
{
NSURL *baseURL = [url baseURL];
NSDictionary *options = nil;
if (baseURL != nil)
{
[NSDictionary dictionaryWithObject: baseURL
forKey: NSBaseURLDocumentOption];
}
return [self initWithURL: url
options: options
documentAttributes: dict
error: NULL];
}
- (id) initWithURL: (NSURL *)url
options: (NSDictionary *)options
documentAttributes: (NSDictionary **)dict
error: (NSError **)error
{
NSURL *baseURL;
NSData *data = [url resourceDataUsingCache: YES];
if (data == nil)
{
if (error)
*error = create_error(0, NSLocalizedString(@"Could not load data from URL.",
@"Error description"));
RELEASE(self);
return nil;
}
// Pass on baseURL
baseURL = [url baseURL];
if (baseURL != nil)
{
if (options == nil)
options = [NSDictionary dictionaryWithObject: baseURL
forKey: NSBaseURLDocumentOption];
else if ([options objectForKey: NSBaseURLDocumentOption] == nil)
{
options = AUTORELEASE([options mutableCopy]);
[(NSMutableDictionary*)options setObject: baseURL
forKey: NSBaseURLDocumentOption];
}
}
return [self initWithData: data
options: options
documentAttributes: dict
error: error];
}
- (NSData *) RTFFromRange: (NSRange)range
documentAttributes: (NSDictionary *)dict
{
if (dict == nil)
{
dict = [NSDictionary dictionaryWithObject: NSRTFTextDocumentType
forKey: NSDocumentTypeDocumentOption];
}
else if ([dict objectForKey: NSDocumentTypeDocumentOption] == nil)
{
dict = AUTORELEASE([dict mutableCopy]);
[(NSMutableDictionary*)dict setObject: NSRTFTextDocumentType
forKey: NSDocumentTypeDocumentOption];
}
return [self dataFromRange: range
documentAttributes: dict
error: NULL];
}
- (NSData *) RTFDFromRange: (NSRange)range
documentAttributes: (NSDictionary *)dict
{
if (dict == nil)
{
dict = [NSDictionary dictionaryWithObject: NSRTFDTextDocumentType
forKey: NSDocumentTypeDocumentOption];
}
else if ([dict objectForKey: NSDocumentTypeDocumentOption] == nil)
{
dict = AUTORELEASE([dict mutableCopy]);
[(NSMutableDictionary*)dict setObject: NSRTFDTextDocumentType
forKey: NSDocumentTypeDocumentOption];
}
return [self dataFromRange: range
documentAttributes: dict
error: NULL];
}
- (NSFileWrapper *) RTFDFileWrapperFromRange: (NSRange)range
documentAttributes: (NSDictionary *)dict
{
return AUTORELEASE([[NSFileWrapper alloc]
initWithSerializedRepresentation:
[self RTFDFromRange: range
documentAttributes: dict]]);
}
- (NSData *) docFormatFromRange: (NSRange)range
documentAttributes: (NSDictionary *)dict
{
if (dict == nil)
{
dict = [NSDictionary dictionaryWithObject: NSDocFormatTextDocumentType
forKey: NSDocumentTypeDocumentOption];
}
else if ([dict objectForKey: NSDocumentTypeDocumentOption] == nil)
{
dict = AUTORELEASE([dict mutableCopy]);
[(NSMutableDictionary*)dict setObject: NSDocFormatTextDocumentType
forKey: NSDocumentTypeDocumentOption];
}
return [self dataFromRange: range
documentAttributes: dict
error: NULL];
}
- (NSData *) dataFromRange: (NSRange)range
documentAttributes: (NSDictionary *)dict
error: (NSError **)error
{
NSString *type = [dict objectForKey: NSDocumentTypeDocumentOption];
Class converter;
if (type == nil)
{
if (error)
*error = create_error(0, NSLocalizedString(@"No type specified for data.",
@"Error description"));
return nil;
}
converter = converter_class(type, YES);
if (converter != Nil)
{
return [converter
produceDataFrom:
[self attributedSubstringFromRange: range]
documentAttributes: dict
error: error];
}
else if ([type isEqualToString: NSPlainTextDocumentType]
|| [type isEqualToString: @"public.plain-text"]
|| [type isEqualToString: @"text"])
{
NSStringEncoding encoding = [[dict objectForKey: @"CharacterEncoding"]
intValue];
if (!encoding)
encoding = [NSString defaultCStringEncoding];
return [[self string] dataUsingEncoding: encoding];
}
if (error)
*error = create_error(0, NSLocalizedString(@"Could not create data for type.",
@"Error description"));
return nil;
}
- (NSFileWrapper *) fileWrapperFromRange: (NSRange)range
documentAttributes: (NSDictionary *)dict
error: (NSError **)error
{
NSFileWrapper *wrapper;
NSData *data;
data = [self dataFromRange: range
documentAttributes: dict
error: error];
if (data != nil)
{
// FIXME: This wont work for directory bundles.
wrapper = [[NSFileWrapper alloc] initRegularFileWithContents: data];
return AUTORELEASE(wrapper);
}
if (error)
*error = create_error(0, NSLocalizedString(@"Could not create data for type.",
@"Error description"));
return nil;
}
- (NSInteger) itemNumberInTextList: (NSTextList *)list
atIndex: (NSUInteger)location
{
NSParagraphStyle *style = [self attribute: NSParagraphStyleAttributeName
atIndex: location
effectiveRange: NULL];
if (style != nil)
{
NSArray *textLists = [style textLists];
if (textLists != nil)
{
return [textLists indexOfObject: list];
}
}
return NSNotFound;
}
- (NSRange) rangeOfTextBlock: (NSTextBlock *)block
atIndex: (NSUInteger)location
{
NSRange effRange;
NSParagraphStyle *style = [self attribute: NSParagraphStyleAttributeName
atIndex: location
effectiveRange: &effRange];
if (style != nil)
{
NSArray *textBlocks = [style textBlocks];
if ((textBlocks != nil) && [textBlocks containsObject: block])
{
NSRange newEffRange;
unsigned len = [self length];
while ((effRange.location > 0) && style && textBlocks)
{
style = [self attribute: NSParagraphStyleAttributeName
atIndex: effRange.location - 1
effectiveRange: &newEffRange];
if (style != nil)
{
textBlocks = [style textBlocks];
if ((textBlocks != nil) && [textBlocks containsObject: block])
{
effRange.location = newEffRange.location;
effRange.length += newEffRange.length;
}
}
}
while (NSMaxRange(effRange) < len && style && textBlocks)
{
style = [self attribute: NSParagraphStyleAttributeName
atIndex: NSMaxRange(effRange)
effectiveRange: &newEffRange];
if (style != nil)
{
textBlocks = [style textBlocks];
if ((textBlocks != nil) && [textBlocks containsObject: block])
{
effRange.length += newEffRange.length;
}
}
}
return effRange;
}
}
return NSMakeRange(NSNotFound, 0);
}
- (NSRange) rangeOfTextList: (NSTextList *)list
atIndex: (NSUInteger)location
{
NSRange effRange;
NSParagraphStyle *style = [self attribute: NSParagraphStyleAttributeName
atIndex: location
effectiveRange: &effRange];
if (style != nil)
{
NSArray *textLists = [style textLists];
if ((textLists != nil) && [textLists containsObject: list])
{
NSRange newEffRange;
unsigned len = [self length];
while ((effRange.location > 0) && style && textLists)
{
style = [self attribute: NSParagraphStyleAttributeName
atIndex: effRange.location - 1
effectiveRange: &newEffRange];
if (style != nil)
{
textLists = [style textLists];
if ((textLists != nil) && [textLists containsObject: list])
{
effRange.location = newEffRange.location;
effRange.length += newEffRange.length;
}
}
}
while (NSMaxRange(effRange) < len && style && textLists)
{
style = [self attribute: NSParagraphStyleAttributeName
atIndex: NSMaxRange(effRange)
effectiveRange: &newEffRange];
if (style != nil)
{
textLists = [style textLists];
if ((textLists != nil) && [textLists containsObject: list])
{
effRange.length += newEffRange.length;
}
}
}
return effRange;
}
}
return NSMakeRange(NSNotFound, 0);
}
static inline
BOOL containsTable(NSArray *textBlocks, NSTextTable *table)
{
NSEnumerator *benum = [textBlocks objectEnumerator];
NSTextTableBlock *block;
while ((block = [benum nextObject]))
{
if ([table isEqual: [block table]])
{
return YES;
}
}
return NO;
}
- (NSRange) rangeOfTextTable: (NSTextTable *)table
atIndex: (NSUInteger)location
{
NSRange effRange;
NSParagraphStyle *style = [self attribute: NSParagraphStyleAttributeName
atIndex: location
effectiveRange: &effRange];
if (style != nil)
{
NSArray *textBlocks = [style textBlocks];
if ((textBlocks != nil) && containsTable(textBlocks, table))
{
NSRange newEffRange;
unsigned len = [self length];
while ((effRange.location > 0) && style && textBlocks)
{
style = [self attribute: NSParagraphStyleAttributeName
atIndex: effRange.location - 1
effectiveRange: &newEffRange];
if (style != nil)
{
textBlocks = [style textBlocks];
if ((textBlocks != nil) && containsTable(textBlocks, table))
{
effRange.location = newEffRange.location;
effRange.length += newEffRange.length;
}
}
}
while (NSMaxRange(effRange) < len && style && textBlocks)
{
style = [self attribute: NSParagraphStyleAttributeName
atIndex: NSMaxRange(effRange)
effectiveRange: &newEffRange];
if (style != nil)
{
textBlocks = [style textBlocks];
if ((textBlocks != nil) && containsTable(textBlocks, table))
{
effRange.length += newEffRange.length;
}
}
}
return effRange;
}
}
return NSMakeRange(NSNotFound, 0);
}
@end
@implementation NSMutableAttributedString (AppKit)
- (void) superscriptRange: (NSRange)range
{
id value;
int sValue;
NSRange effRange;
if (NSMaxRange (range) > [self length])
{
[NSException raise: NSRangeException
format: @"RangeError in method -superscriptRange: "];
}
// We take the value from the first character and use it for the whole range
value = [self attribute: NSSuperscriptAttributeName
atIndex: range.location
effectiveRange: &effRange];
if (value != nil)
{
sValue = [value intValue] + 1;
}
else
{
sValue = 1;
}
[self addAttribute: NSSuperscriptAttributeName
value: [NSNumber numberWithInt: sValue]
range: range];
}
- (void) subscriptRange: (NSRange)range
{
id value;
int sValue;
NSRange effRange;
if (NSMaxRange (range) > [self length])
{
[NSException raise: NSRangeException
format: @"RangeError in method -subscriptRange: "];
}
// We take the value form the first character and use it for the whole range
value = [self attribute: NSSuperscriptAttributeName
atIndex: range.location
effectiveRange: &effRange];
if (value != nil)
{
sValue = [value intValue] - 1;
}
else
{
sValue = -1;
}
[self addAttribute: NSSuperscriptAttributeName
value: [NSNumber numberWithInt: sValue]
range: range];
}
- (void) unscriptRange: (NSRange)range
{
if (NSMaxRange (range) > [self length])
{
[NSException raise: NSRangeException
format: @"RangeError in method -unscriptRange: "];
}
[self removeAttribute: NSSuperscriptAttributeName
range: range];
}
- (void) applyFontTraits: (NSFontTraitMask)traitMask
range: (NSRange)range
{
NSFont *font;
unsigned loc = range.location;
NSRange effRange;
NSFontManager *fm = [NSFontManager sharedFontManager];
if (NSMaxRange (range) > [self length])
{
[NSException raise: NSRangeException
format: @"RangeError in method -applyFontTraits: range: "];
}
while (loc < NSMaxRange (range))
{
font = [self attribute: NSFontAttributeName
atIndex: loc
effectiveRange: &effRange];
if (font != nil)
{
font = [fm convertFont: font
toHaveTrait: traitMask];
if (font != nil)
{
[self addAttribute: NSFontAttributeName
value: font
range: NSIntersectionRange (effRange, range)];
}
}
loc = NSMaxRange(effRange);
}
}
- (void) setAlignment: (NSTextAlignment)alignment
range: (NSRange)range
{
id value;
unsigned loc = range.location;
if (NSMaxRange(range) > [self length])
{
[NSException raise: NSRangeException
format: @"RangeError in method -setAlignment: range: "];
}
while (loc < NSMaxRange(range))
{
BOOL copiedStyle = NO;
NSRange effRange;
NSRange newRange;
value = [self attribute: NSParagraphStyleAttributeName
atIndex: loc
effectiveRange: &effRange];
newRange = NSIntersectionRange (effRange, range);
if (value == nil)
{
value = [NSMutableParagraphStyle defaultParagraphStyle];
}
else
{
value = [value mutableCopy];
copiedStyle = YES;
}
[value setAlignment: alignment];
[self addAttribute: NSParagraphStyleAttributeName
value: value
range: newRange];
if (copiedStyle == YES)
{
RELEASE(value);
}
loc = NSMaxRange (effRange);
}
}
- (void) fixAttributesInRange: (NSRange)range
{
[self fixFontAttributeInRange: range];
[self fixParagraphStyleAttributeInRange: range];
[self fixAttachmentAttributeInRange: range];
}
static NSString *lastFont = nil;
static NSCharacterSet *lastSet = nil;
static NSMutableDictionary *cachedCSets = nil;
- (NSFont*)_substituteFontWithName: (NSString*)fontName
font: (NSFont*)baseFont
{
return [[NSFontManager sharedFontManager] convertFont: baseFont
toFace: fontName];
}
- (NSFont*)_substituteFontFor: (unichar)uchar
font: (NSFont*)baseFont
fromList: (NSArray *)fonts
{
unsigned int count;
unsigned int i;
if (cachedCSets == nil)
{
cachedCSets = [NSMutableDictionary new];
}
count = [fonts count];
for (i = 0; i < count; i++)
{
NSFont *newFont;
NSString *fName;
NSCharacterSet *newSet;
fName = [fonts objectAtIndex: i];
newSet = [cachedCSets objectForKey: fName];
if (newSet == nil)
{
newFont = [self _substituteFontWithName: fName font: baseFont];
newSet = [newFont coveredCharacterSet];
if ((newSet != nil) && ([cachedCSets count] < 10))
{
[cachedCSets setObject: newSet forKey: fName];
}
}
else
{
newFont = nil;
}
if ([newSet characterIsMember: uchar])
{
ASSIGN(lastFont, fName);
ASSIGN(lastSet, newSet);
if (newFont != nil)
{
return newFont;
}
else
{
return [self _substituteFontWithName: fName font: baseFont];
}
}
}
return nil;
}
- (NSFont*)_substituteFontFor: (unichar)uchar font: (NSFont*)baseFont
{
NSFont *subFont;
// Caching one font may lead to the selected substitution font not being
// from the prefered list, although there is one there with this character.
if (lastSet && [lastSet characterIsMember: uchar])
{
return [self _substituteFontWithName: lastFont font: baseFont];
}
subFont = [self _substituteFontFor: uchar
font: baseFont
fromList: [NSFont preferredFontNames]];
if (subFont != nil)
{
return subFont;
}
subFont = [self _substituteFontFor: uchar font: baseFont fromList:
[[NSFontManager sharedFontManager] availableFonts]];
if (subFont != nil)
{
return subFont;
}
return nil;
}
- (void) fixFontAttributeInRange: (NSRange)range
{
NSString *string;
NSFont *font = nil;
NSCharacterSet *charset = nil;
NSRange fontRange = NSMakeRange(NSNotFound, 0);
unsigned int i;
unsigned int lastMax;
unsigned int start;
unichar chars[64];
CREATE_AUTORELEASE_POOL(pool);
if (NSMaxRange (range) > [self length])
{
[NSException raise: NSRangeException
format: @"RangeError in method -fixFontAttributeInRange: "];
}
// Check for each character if it is supported by the
// assigned font
/*
Note that this needs to be done on a script basis. Per-character checks
are difficult to do at all, don't give reasonable results, and would have
really poor performance.
*/
string = [self string];
lastMax = range.location;
start = lastMax;
for (i = range.location; i < NSMaxRange(range); i++)
{
unichar uchar;
if (i >= lastMax)
{
unsigned int dist;
start = lastMax;
dist = MIN(64, NSMaxRange(range) - start);
lastMax = start + dist;
[string getCharacters: chars range: NSMakeRange(start, dist)];
}
uchar = chars[i - start];
if (!NSLocationInRange(i, fontRange))
{
font = [self attribute: NSFontAttributeName
atIndex: i
effectiveRange: &fontRange];
charset = [font coveredCharacterSet];
}
if (charset != nil && ![charset characterIsMember: uchar]
&& (uchar > 31))
{
// Find a replacement font
NSFont *subFont;
subFont = [self _substituteFontFor: uchar font: font];
if (subFont != nil)
{
// Set substitution font permanently
[self addAttribute: NSFontAttributeName
value: subFont
range: NSMakeRange(i, 1)];
}
}
}
[pool drain];
}
- (void) fixParagraphStyleAttributeInRange: (NSRange)range
{
NSString *str = [self string];
unsigned loc = range.location;
NSRange r;
if (NSMaxRange (range) > [self length])
{
[NSException raise: NSRangeException
format: @"RangeError in method -fixParagraphStyleAttributeInRange: "];
}
while (loc < NSMaxRange (range))
{
NSParagraphStyle *style;
NSRange found;
unsigned end;
/* Extend loc to take in entire paragraph if necessary. */
r = [str lineRangeForRange: NSMakeRange (loc, 1)];
end = NSMaxRange (r);
/* Get the style in effect at the paragraph start. */
style = [self attribute: NSParagraphStyleAttributeName
atIndex: r.location
longestEffectiveRange: &found
inRange: r];
if (style == nil)
{
/* No style found at the beginning of paragraph. found is
the range without the style set. */
if ((NSMaxRange (found) + 1) < end)
{
/* There is a paragraph style for part of the paragraph. Set
this style for the entire paragraph.
Since NSMaxRange(found) + 1 is outside the longest effective
range for the nil style, it must be non-nil.
*/
style = [self attribute: NSParagraphStyleAttributeName
atIndex: NSMaxRange(found) + 1
effectiveRange: NULL];
[self addAttribute: NSParagraphStyleAttributeName
value: style
range: r];
}
else
{
/* All the paragraph without a style ... too bad, fixup
the whole paragraph using the default paragraph style. */
[self addAttribute: NSParagraphStyleAttributeName
value: [NSParagraphStyle defaultParagraphStyle]
range: r];
}
}
else
{
if (NSMaxRange (found) < end)
{
/* Not the whole paragraph has the same style ... add
the style found at the beginning to the remainder of
the paragraph. */
found.location = NSMaxRange (found);
found.length = end - found.location;
[self addAttribute: NSParagraphStyleAttributeName
value: style
range: found];
}
}
/* Move on to the next paragraph. */
loc = end;
}
}
- (void) fixAttachmentAttributeInRange: (NSRange)range
{
NSString *string = [self string];
unsigned location = range.location;
unsigned end = NSMaxRange (range);
cache_init ();
if (end > [self length])
{
[NSException raise: NSRangeException
format: @"RangeError in method -fixAttachmentAttributeInRange: "];
}
// Check for attachments with the wrong character
while (location < end)
{
NSDictionary *attr;
NSRange eRange;
attr = [self attributesAtIndex: location effectiveRange: &eRange];
if ([attr objectForKey: NSAttachmentAttributeName] != nil)
{
unichar buf[eRange.length];
unsigned pos = 0;
unsigned start = eRange.location;
// Leave only one character with the attachment
[string getCharacters: buf range: eRange];
while (pos < eRange.length && buf[pos] != NSAttachmentCharacter)
pos++;
if (pos)
[self removeAttribute: NSAttachmentAttributeName
range: NSMakeRange (start, pos)];
pos++;
if (pos < eRange.length)
[self removeAttribute: NSAttachmentAttributeName
range: NSMakeRange (start + pos, eRange.length - pos)];
}
location = NSMaxRange (eRange);
}
// Check for attachment characters without attachments
location = range.location;
string = [self string];
while (location < end)
{
NSRange eRange = [string rangeOfString: attachmentString
options: NSLiteralSearch
range: NSMakeRange (location, end - location)];
NSTextAttachment *attachment;
if (!eRange.length)
break;
attachment = [self attribute: NSAttachmentAttributeName
atIndex: eRange.location
effectiveRange: NULL];
if (attachment == nil)
{
[self deleteCharactersInRange: NSMakeRange (eRange.location, 1)];
eRange.length--;
end--;
// Need to reset this after every character change
string = [self string];
}
location = NSMaxRange (eRange);
}
}
- (void) updateAttachmentsFromPath: (NSString *)path
{
NSString *string = [self string];
unsigned location = 0;
unsigned end = [string length];
cache_init ();
while (location < end)
{
NSRange range = [string rangeOfString: attachmentString
options: NSLiteralSearch
range: NSMakeRange (location, end - location)];
NSTextAttachment *attachment;
NSFileWrapper *fileWrapper;
if (!range.length)
break;
attachment = [self attribute: NSAttachmentAttributeName
atIndex: range.location
effectiveRange: NULL];
fileWrapper = [attachment fileWrapper];
// FIXME: Is this the correct thing to do?
[fileWrapper updateFromPath: [path stringByAppendingPathComponent:
[fileWrapper filename]]];
location = NSMaxRange (range);
}
}
- (BOOL) readFromURL: (NSURL *)url
options: (NSDictionary *)options
documentAttributes: (NSDictionary**)documentAttributes
{
return [self readFromURL: url
options: options
documentAttributes: documentAttributes
error: NULL];
}
- (BOOL) readFromURL: (NSURL *)url
options: (NSDictionary *)options
documentAttributes: (NSDictionary **)documentAttributes
error: (NSError **)error
{
NSAttributedString *attr;
attr = [[NSAttributedString alloc]
initWithURL: url
options: options
documentAttributes: documentAttributes
error: error];
if (attr != nil)
{
[self setAttributedString: attr];
RELEASE(attr);
return YES;
}
return NO;
}
- (BOOL) readFromData: (NSData *)data
options: (NSDictionary *)options
documentAttributes: (NSDictionary **)documentAttributes
{
return [self readFromData: data
options: options
documentAttributes: documentAttributes
error: NULL];
}
- (BOOL) readFromData: (NSData *)data
options: (NSDictionary *)options
documentAttributes: (NSDictionary **)documentAttributes
error: (NSError **)error
{
NSAttributedString *attr;
attr = [[NSAttributedString alloc]
initWithData: data
options: options
documentAttributes: documentAttributes
error: error];
if (attr)
{
[self setAttributedString: attr];
RELEASE(attr);
return YES;
}
return NO;
}
- (void) setBaseWritingDirection: (NSWritingDirection)writingDirection
range: (NSRange)range
{
[self setAttributes: [NSDictionary dictionaryWithObject:
[NSNumber numberWithInt: writingDirection]
forKey: @"WritingDirection"]
range: range];
}
@end