/** <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/NSSet.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/NSFontDescriptor.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 writing 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];
  NSUInteger 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];
  NSUInteger 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];
  NSUInteger 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)
    {
      type = NSPlainTextDocumentType;
    }

  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;

      if (encoding == GSUndefinedEncoding)
	{
	  encoding = NSUTF8StringEncoding;

	  if ([data length] >= 2)
	    {	
	      static const unichar byteOrderMark = 0xFEFF;
	      static const unichar byteOrderMarkSwapped = 0xFFFE;
	      const unichar firstChar = ((const unichar *)[data bytes])[0];
	      if (firstChar == byteOrderMark
		  || firstChar == byteOrderMarkSwapped)
		{
		  encoding = NSUnicodeStringEncoding;
		}
	    }
	}

      if (dict != NULL)
	{
	  *dict = [NSDictionary dictionaryWithObjectsAndKeys:
				  NSPlainTextDocumentType, NSDocumentTypeDocumentAttribute,
			 [NSNumber numberWithUnsignedInteger: encoding], NSCharacterEncodingDocumentAttribute,
				nil];
	}

      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;
          NSUInteger 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;
          NSUInteger 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;
	  NSUInteger 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;
  NSUInteger 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;
  NSUInteger	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
{
  NSUInteger count;
  NSUInteger 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;
}

- (NSFontDescriptor*)_substituteFontDescriptorFor: (unichar)uchar
{
  NSString *chars = [NSString stringWithCharacters: &uchar length: 1];
  NSCharacterSet *requiredCharacterSet = [NSCharacterSet characterSetWithCharactersInString: chars];
  NSDictionary *fontAttributes = [NSDictionary dictionaryWithObjectsAndKeys: requiredCharacterSet, NSFontCharacterSetAttribute, nil];
  NSSet *mandatoryKeys = [NSSet setWithObjects: NSFontCharacterSetAttribute, nil];
  NSFontDescriptor *fd = [NSFontDescriptor fontDescriptorWithFontAttributes: fontAttributes];
  return [fd matchingFontDescriptorWithMandatoryKeys: mandatoryKeys];
}

- (NSFont*)_substituteFontFor: (unichar)uchar font: (NSFont*)baseFont
{
  NSFont *subFont;
  NSFontDescriptor *descriptor;

  // 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;
    }

  // Fast way with font descriptors
  descriptor = [self _substituteFontDescriptorFor: uchar];
  if (descriptor != nil)
    {
      NSCharacterSet *newSet = [descriptor objectForKey: NSFontCharacterSetAttribute];
      if ([newSet characterIsMember: uchar])
        {
          NSString *fName = [descriptor objectForKey: NSFontFamilyAttribute];
 
          ASSIGN(lastFont, fName);
          ASSIGN(lastSet, newSet);
          return [self _substituteFontWithName: fName font: baseFont];
        }
    }

  
  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);
  NSUInteger i;
  NSUInteger lastMax;
  NSUInteger start;
  unichar chars[64];
  CREATE_AUTORELEASE_POOL(pool);
  NSCharacterSet *controlset = [NSCharacterSet controlCharacterSet];
  
  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)
        {
          NSUInteger 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];

          /* If we don't have an attribute for NSFontAttributeName,
          ** we take a default font so we can carry on with the
          ** substitution.
          */
          if (nil == font)
            {
              font = [NSFont userFontOfSize: 0.0];
            }

          charset = [font coveredCharacterSet];
        }
      
      if (charset != nil && ![charset characterIsMember: uchar]
          && (uchar != NSAttachmentCharacter)
          && ![controlset characterIsMember: uchar])
        {
          // 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];
  NSUInteger 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;
      NSUInteger	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];
  NSUInteger location = range.location;
  NSUInteger 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];
	  NSUInteger	pos = 0;
	  NSUInteger	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];
  NSUInteger location = 0;
  NSUInteger 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
{
  id value;
  NSUInteger loc = range.location;
  
  if (NSMaxRange(range) > [self length])
    {
      [NSException raise: NSRangeException
		  format: @"RangeError in method -setBaseWritingDirection: 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 setBaseWritingDirection: writingDirection];

      [self addAttribute: NSParagraphStyleAttributeName
		   value: value
		   range: newRange];
      if (copiedStyle == YES)
	{
	  RELEASE(value);
	}
      loc = NSMaxRange(effRange);
    }
}

@end
