// Copyright 1997-2000 Omni Development, Inc.  All rights reserved.
//
// This software may only be used and reproduced according to the
// terms in the file OmniSourceLicense.html, which should be
// distributed with this project and can also be found at
// http://www.omnigroup.com/DeveloperResources/OmniSourceLicense.html.

#import "OHTextBuilder.h"

#import <Foundation/Foundation.h>
#import <AppKit/AppKit.h>
#import <OmniBase/OmniBase.h>
#import <OmniFoundation/OmniFoundation.h>
#import <OmniAppKit/OmniAppKit.h>
#import <OWF/OWF.h>

#import "OHBreak.h"
#import "OHColorPalette.h"
#import "OHFontStyle.h"
#import "OHHTMLAnchor.h"
#import "OHHTMLDocument.h"
#import "OHHTMLOwner.h"
#import "OHHTMLPageView.h"
#import "OHHTMLView.h"
#import "OHTextBuilderDefaultsHolder.h"

RCS_ID("$Header: /Network/Source/CVS/OmniGroup/Frameworks/OmniHTML/Building.subproj/OHTextBuilder.m,v 1.27 2000/01/20 00:27:45 kc Exp $")

NSString *OHHTMLAnchorAttributeName = @"OHHTMLAnchor";

@interface OHTextBuilder (Private)
+ (NSSet *)fontFamilyNames;
- (void)setupAttributedString;
- (void)emitAlignmentChange;
- (void)emitStyleChanges;
- (void)flushProcessedCharactersToAttributedString;
- (void)cachedFontIsInvalid;
- (NSMutableDictionary *)_currentAttributesDictionaryRetained;
- (void)flushAttributedStringToHTMLView;
- (void)lastFlushToHTMLViewComplete;
@end

@interface OHMutableKnownKeyAttributeDictionary:OFMutableKnownKeyDictionary
@end

// Constants
#define FIGURE_SPACE 0x50
#define INITIAL_LEFT_INDENT (0)
#define INFINITE_VERTICAL_SPACE (20000.0)

/* This was true on Netscape 4.0 Windows, but not Netscape 4.0 Mac
enum {
    OWBaseFontSize = 14,
};
static const double fontSizes[] = {
    10.0, 12.0, OWBaseFontSize, 18.0, 24.0, 32.0, 48.0,
};
*/

enum {
    OWBaseFontSize = 12,
};

#warning WJS: REDO font size checks use Mac
static const double fontSizes[] = {
    9.0, 10.0, OWBaseFontSize, 16.0, 24.0, 32.0, 48.0,
};
static const float FigureSpaceWidth = 10.0;


@implementation OHTextBuilder


// Class variables
static BOOL debugRTFGeneration = NO;
static OFKnownKeyDictionaryTemplate *OHAttributeDictionaryTemplate;


+ (void)initialize;
{
    static BOOL initialized = NO;

    [super initialize];
    if (initialized)
        return;
    initialized = YES;

    // Workaround for a bug in DR2: if you render a web page (which happens in another thread) before you bring up a font panel, the NSFontManager will attach itself to the wrong thread, and raise an exception ("NSDistantObject accessed attempted from another thread") if you try to access it from the main thread (say, by bringing up a font panel).  After that exception, no more pages will ever render (presumably because NSFontManager calls fail from other threads too, after that).
    [[NSFontManager sharedFontManager] availableFonts];

    OHAttributeDictionaryTemplate = [OFKnownKeyDictionaryTemplate templateWithKeys:[NSArray arrayWithObjects:OHHTMLAnchorAttributeName, NSFontAttributeName, NSForegroundColorAttributeName, NSParagraphStyleAttributeName, NSUnderlineStyleAttributeName, NSBaselineOffsetAttributeName, NSLigatureAttributeName, NSAttachmentAttributeName, nil]];
}

+ (void)debug;
{
    debugRTFGeneration = YES;
}


+ (void)readDefaults;
{
    // PENDING: We ought to do this somewhere else now since this class no longer cares about this preference.
    [OWDataStream setDefaultStringEncoding:NSISOLatin1StringEncoding];
}    

+ (OWTextAlignment)alignmentFromString:(NSString *)alignmentString;
{
    alignmentString = [alignmentString lowercaseString];
    if ([alignmentString isEqualToString:@"center"])
	return OW_CENTERED;
    else if ([alignmentString isEqualToString:@"right"])
        return OW_RIGHTALIGNED;
    else if ([alignmentString isEqualToString:@"justified"])
        return OW_JUSTIFIED;
    else
        return OW_LEFTALIGNED;
}

- initWithHTMLOwner:(OHHTMLOwner *)anHTMLOwner colorPalette:(OHColorPalette *)aColorPalette;
{
    if (![super init])
        return nil;

    htmlOwner = [anHTMLOwner retain];

    defaultsHolder = [[OHTextBuilderDefaultsHolder defaultsHolder] retain];

    [self setupAttributedString];

    flags.inBlock = NO;
    flags.inParagraph = NO;

    paragraphAttributes.flags.preserveWhitespace = NO;
    paragraphAttributes.flags.obeyLines = NO;

    // Character

    characterAttributes.fontAttributes.bold = NO;
    characterAttributes.fontAttributes.italic = NO;
    characterAttributes.fontStyle = FONTSTYLE_NORMAL;
    // Round the font size to the nearest integer to work around a bug in NSLayoutManager when using non-integral font sizes.
    characterAttributes.fontAttributes.size = rint(defaultsHolder->normalFontSize);
    characterAttributes.lastWrittenFontSize = defaultsHolder->normalFontSize;
    characterAttributes.textColorIndex = 0;
    characterAttributes.textColorOverride = nil;
    characterAttributes.baselineOffset = 0;
    characterAttributes.flags.underlined = NO;
    characterAttributes.ligatureType = 1;

    characterAttributes.baseFontSizeIndex = 3;
    characterAttributes.textColorIndex = OHColorPaletteTextIndex;
    characterAttributes.fontSizeIndex = 3;
    characterAttributes.fontSizeMultiple = 1.0;
    characterAttributes.baselineOffset = 0;
    characterAttributes.fontFaceNameOverride = nil;

    // Paragraph

    paragraphAttributes.textAlignment = OW_LEFTALIGNED;
    paragraphAttributes.paragraphAlignment = OW_UNSPECIFIED_ALIGNMENT;
    paragraphAttributes.divisionAlignment = OW_UNSPECIFIED_ALIGNMENT;

    paragraphAttributes.firstIndent = 0.0;
    paragraphAttributes.leftIndent = 0.0;
    [self setLeftIndent:INITIAL_LEFT_INDENT];
    paragraphAttributes.lineBreakMode = NSLineBreakByWordWrapping;

    //

    [self setColorPalette:aColorPalette];

    currentVerticalSpace = INFINITE_VERTICAL_SPACE;
    flags.justDidSpace = YES;
    flags.needVerticalSpace = NO;
    needLineBreakSize = 0.0;
    needLineBreakCause = OW_CAUSE_NONE;
    flags.atStartOfLine = YES;
    lastFlushTimeInterval = [NSDate timeIntervalSinceReferenceDate];
    flags.flushToHTMLViewInProgress = NO;

    return self;
}

- initWithHTMLOwner:(OHHTMLOwner *)anHTMLOwner;
{
    return [self initWithHTMLOwner:anHTMLOwner colorPalette:[OHColorPalette defaultPalette]];
}

- (void)dealloc;
{
    OBPRECONDITION(htmlOwner == nil);

    
    [mutableAttributedString release];
    [lastMutableAttributedString release];
    
    [mutableString release];

    [colorPalette release];
    
    [characterAttributes.fontFaceNameOverride release];
    [characterAttributes.textColorOverride release];
    [characterAttributes.anchor release]; // sometimes -endAnchor isn't called
    [characterAttributes.font release];
    [characterAttributes.fontStyleArray release];

    [defaultsHolder release];
    
    [super dealloc];
}

// Declared methods

- (OHHTMLOwner *)htmlOwner;
{
    return htmlOwner;
}
    
- (void)setTextBuilderObserver:(id <OHTextBuilderObserver>)anObserver;
{
    nonretainedTextBuilderObserver = anObserver;
}

static inline unsigned int
totalCharactersProcessed(OHTextBuilder *self)
{
    return self->charactersWrittenFromProcessedBuffer + self->processedCharacterCount;
}

- (BOOL)isPristine;
{
    return totalCharactersProcessed(self) == 0;
}

- (OHHTMLView *)htmlView;
{
    return [htmlOwner htmlView];
}

//

static inline void
flushAttributedStringToHTMLViewIfNecessary(OHTextBuilder *self)
{
    if (!self->defaultsHolder->flags.incrementalDisplay)
	return;

    // If we haven't written anything, don't bother flushing.
    if (totalCharactersProcessed(self) == 0)
        return;
    
    // If the main thread hasn't finished processing the previous flush yet, don't flush now.
    if (self->flags.flushToHTMLViewInProgress)
	return;

    // Flush no more frequently than every 0.8 seconds.
    if ([NSDate timeIntervalSinceReferenceDate] - self->lastFlushTimeInterval <= 0.8)
	return;
    
    [self flushAttributedStringToHTMLView];
}

static inline void
addProcessedCharacter(OHTextBuilder *self, unichar unicodeCharacter)
{
    self->characterAttributes.lastWrittenFontSize = self->characterAttributes.fontAttributes.size;
    self->processedCharacters[self->processedCharacterCount++] = unicodeCharacter;
    if (self->processedCharacterCount >= BUILDER_BUFFER_SIZE)
        [self flushProcessedCharactersToAttributedString];
}


static inline void
outputNewlineIfNecessary(OHTextBuilder *self)
{
    if (!self->needLineBreakSize)
	return;

    if (self->needLineBreakSize == self->characterAttributes.fontAttributes.size) {
        addProcessedCharacter(self, '\n');
    } else {
        float oldFontSize;

        oldFontSize = [self setFontSize:self->needLineBreakSize];
        addProcessedCharacter(self, '\n');
        [self setFontSize:oldFontSize];
    }
    self->needLineBreakSize = 0.0;
    self->needLineBreakCause = OW_CAUSE_NONE;
    flushAttributedStringToHTMLViewIfNecessary(self);
}


static inline void
outputVerticalSpaceIfNecessary(OHTextBuilder *self)
{
    float needVerticalAmount;

    if (!self->flags.needVerticalSpace)
	return;
	
    [self ensureAtStartOfLine];
    outputNewlineIfNecessary(self);

    needVerticalAmount = self->lastBlockWantsVerticalSpace - self->currentVerticalSpace;

    if (needVerticalAmount > 0.0) {
        if (needVerticalAmount == self->characterAttributes.fontAttributes.size) {
            addProcessedCharacter(self, '\n');
        } else {
            float oldFontSize;

            oldFontSize = [self setFontSize:needVerticalAmount];
            addProcessedCharacter(self, '\n');
            [self setFontSize:oldFontSize];
        }

        self->currentVerticalSpace = self->lastBlockWantsVerticalSpace;
        self->lastBlockWantsVerticalSpace = 0;
    }

    self->flags.needVerticalSpace = NO;
    self->flags.justDidSpace = YES;
}


static inline void
outputWhitespaceIfNecessary(OHTextBuilder *self)
{
    outputNewlineIfNecessary(self);
    outputVerticalSpaceIfNecessary(self);
}

- (void)endTextBuild;
{
    // WJS 4/10/98: Note that Netscape 4.0 and IE 4.0 will output trailing spaces as well as trailing newlines, but currently that would screw up empty table cells in OH, so we just output any trailing newlines.
    outputNewlineIfNecessary(self);

    flags.endedTextBuild = YES;
    [self flushAttributedStringToHTMLView];

    [htmlOwner release];
    htmlOwner = nil;
}

- (void)beginParagraph;
{
    [self beginBlock];
    flags.inParagraph = YES;
}

- (void)endParagraph;
{
    [self endBlock];
}

- (void)beginBlock;
{
    if (flags.inBlock)
	[self endBlock];

    if (!paragraphAttributes.flags.obeyLines) {
	flags.justDidSpace = YES;
	flags.needVerticalSpace = YES;
    }
    flags.inBlock = YES;
    flags.inParagraph = NO;
}

- (void)endBlock;
{
    if (!flags.inBlock)
	return;

    [self ensureAtStartOfLine];

    if (paragraphAttributes.paragraphAlignment != OW_UNSPECIFIED_ALIGNMENT) {
	paragraphAttributes.paragraphAlignment = OW_UNSPECIFIED_ALIGNMENT;
	[self emitAlignmentChange];
    }

    lastBlockWantsVerticalSpace = self->characterAttributes.fontAttributes.size;
    flags.needVerticalSpace = NO;
    flags.inBlock = NO;
    flags.inParagraph = NO;
}

- (void)ensureAtStartOfLine;
{
    if (flags.atStartOfLine)
	return;

    needLineBreakSize = characterAttributes.lastWrittenFontSize;
    needLineBreakCause = OW_CAUSE_OTHER;
    flags.justDidSpace = YES;
    flags.atStartOfLine = YES;
    currentVerticalSpace = 0.0;
    [self setFirstIndent:paragraphAttributes.hangingIndent];
    paragraphAttributes.hangingIndent = 0.0;
}

- (void)ensureAtStartOfLineIgnoringInfiniteHack;
{
    if (flags.atStartOfLine && currentVerticalSpace != INFINITE_VERTICAL_SPACE)
	return;
	
    flags.atStartOfLine = NO;
    [self ensureAtStartOfLine];
}

- (void)addLineBreak;
{
    float oldVerticalSpace = 0.0;

    // Netscape 2.0 compatibility: in PRE mode, Netscape ignores all but the first <BR>, which it treats like a LF.  Thus, if we just processed a <BR> or a CR, we ignore this <BR>.
    if (paragraphAttributes.flags.obeyLines && (needLineBreakCause == OW_CAUSE_BR || needLineBreakCause == OW_CAUSE_CR)) {
	needLineBreakCause = OW_CAUSE_BR;
	return;
    }
    
    outputNewlineIfNecessary(self);
    if (flags.atStartOfLine) {
        if (currentVerticalSpace >= INFINITE_VERTICAL_SPACE)
            currentVerticalSpace = 0;
        oldVerticalSpace = currentVerticalSpace + characterAttributes.fontAttributes.size;
	flags.atStartOfLine = NO;
    }
    [self ensureAtStartOfLine];
    needLineBreakCause = OW_CAUSE_BR;
    currentVerticalSpace = oldVerticalSpace; 
}

- (void)addWhitespace;
{
    if (flags.justDidSpace)
	return;

    addProcessedCharacter(self, self->characterAttributes.noWrapMode ? NO_BREAK_SPACE : ' ');
    flags.atStartOfLine = NO;
    flags.justDidSpace = YES;
}

- (void)pretendInfiniteVerticalSpace;
{
    currentVerticalSpace = INFINITE_VERTICAL_SPACE;
}

- (void)pretendAtStartOfLine;
{
    flags.atStartOfLine = YES;
}

- (OHColorPalette *)colorPalette;
{
    return colorPalette;
}

- (void)setColorPalette:(OHColorPalette *)aPalette;
{
    if (colorPalette == aPalette)
	return;

    // Force set of the text color with the new color table
    [self emitStyleChanges];
    
    [colorPalette release];
    if ([aPalette zone] == [[htmlOwner htmlView] zone])
        colorPalette = [aPalette retain];
    else
        colorPalette = [aPalette copyWithZone:[[htmlOwner htmlView] zone]];

    [[htmlOwner htmlView] setBackgroundColor:[colorPalette backgroundColor]];
}

// Paragraph Styles

// Alignment

- (OWTextAlignment)textAlignment;
{
    return paragraphAttributes.textAlignment;
}

- (void)setTextAlignment:(OWTextAlignment)newTextAlignment;
{

    if (paragraphAttributes.textAlignment == newTextAlignment)
	return;
	
    [self emitStyleChanges];
    
    paragraphAttributes.textAlignment = newTextAlignment;
}

- (OWTextAlignment)paragraphAlignment;
{
    return paragraphAttributes.paragraphAlignment;
}

- (OWTextAlignment)setParagraphAlignment:(OWTextAlignment)newParagraphAlignment;
{
    OWTextAlignment oldParagraphAlignment;

    if (newParagraphAlignment == paragraphAttributes.paragraphAlignment)
	return paragraphAttributes.paragraphAlignment;

    oldParagraphAlignment = paragraphAttributes.paragraphAlignment;
    paragraphAttributes.paragraphAlignment = newParagraphAlignment;
    [self emitAlignmentChange];
    return oldParagraphAlignment;
}

- (OWTextAlignment)setParagraphAlignmentString:(NSString *)alignmentString;
{
    return [self setParagraphAlignment:[[self class] alignmentFromString:alignmentString]];
}

- (OWTextAlignment)setDivisionAlignment:(OWTextAlignment)newDivisionAlignment;
{
    OWTextAlignment oldDivisionAlignment;

    if (newDivisionAlignment == paragraphAttributes.divisionAlignment)
	return paragraphAttributes.divisionAlignment;
    oldDivisionAlignment = paragraphAttributes.divisionAlignment;
    paragraphAttributes.divisionAlignment = newDivisionAlignment;
    [self emitAlignmentChange];
    return oldDivisionAlignment;
}

- (OWTextAlignment)setDivisionAlignmentString:(NSString *)alignmentString;
{
    return [self setDivisionAlignment:[[self class] alignmentFromString:alignmentString]];
}

- (void)startCentering;
{
    paragraphAttributes.centeringCount++;
    [self emitAlignmentChange];
}
- (void)stopCentering;
{
    if (paragraphAttributes.centeringCount > 0)
	paragraphAttributes.centeringCount--;
    [self emitAlignmentChange];
}

//

- (float)firstIndent;
{
    return paragraphAttributes.firstIndent + paragraphAttributes.hangingIndent;
}

// Note that first indent for us is currently an offset from the left indent, like RTF but unlike the -[NSMutableParagraphStyle setFirstLineHeadIndent:].
- (float)setFirstIndent:(float)newFirstIndent;
{
    float oldFirstIndent;
    
    if (paragraphAttributes.firstIndent == newFirstIndent - paragraphAttributes.hangingIndent)
	return newFirstIndent;
	
    [self emitStyleChanges];
    
    oldFirstIndent = paragraphAttributes.firstIndent + paragraphAttributes.hangingIndent;
    paragraphAttributes.firstIndent = newFirstIndent - paragraphAttributes.hangingIndent;

    return oldFirstIndent;
}

- (float)hangingIndent;
{
    return paragraphAttributes.hangingIndent;
}

- (float)setHangingIndent:(float)newHangingIndent;
{
    float oldHangingIndent;
    float oldLeftIndent;

    if (paragraphAttributes.hangingIndent == newHangingIndent)
        return paragraphAttributes.hangingIndent;
    oldHangingIndent = paragraphAttributes.hangingIndent;
    oldLeftIndent = [self leftIndent];
    paragraphAttributes.hangingIndent = newHangingIndent;
    [self setLeftIndent:oldLeftIndent];
    [self setFirstIndent:0.0];
    return oldHangingIndent;
}

- (float)leftIndent;
{
    return paragraphAttributes.leftIndent - paragraphAttributes.hangingIndent;
}

- (float)setLeftIndent:(float)newLeftIndent;
{
    float oldLeftIndent;

    if (paragraphAttributes.leftIndent == newLeftIndent + paragraphAttributes.hangingIndent)
        return newLeftIndent;

    [self emitStyleChanges];
    
    oldLeftIndent = paragraphAttributes.leftIndent - paragraphAttributes.hangingIndent;
    paragraphAttributes.leftIndent = newLeftIndent + paragraphAttributes.hangingIndent;
    return oldLeftIndent;
}

- (float)rightIndent;
{
    return paragraphAttributes.rightIndent;
}

- (float)setRightIndent:(float)newRightIndent;
{
    float oldRightIndent;

    if (paragraphAttributes.rightIndent == newRightIndent)
        return paragraphAttributes.rightIndent;

    [self emitStyleChanges];
    
    oldRightIndent = paragraphAttributes.rightIndent;
    paragraphAttributes.rightIndent = newRightIndent;
    return oldRightIndent;
}


// Character styles

// Font style includes size, color, and face

- (void)pushFontStyle;
{
    OHFontStyle *fontStyle;

    fontStyle = [[OHFontStyle alloc] initFontFaceName: characterAttributes.fontFaceNameOverride textColor:characterAttributes.textColorOverride fontSizeIndex: characterAttributes.fontSizeIndex];

    // Add to array
    if (!characterAttributes.fontStyleArray)
        characterAttributes.fontStyleArray = [[NSMutableArray alloc] init];

    [characterAttributes.fontStyleArray addObject:fontStyle];
    [fontStyle release];
}

- (void)popFontStyle;
{
    OHFontStyle *fontStyle;
    unsigned int styleCount;

    // Pop off array
    styleCount = [characterAttributes.fontStyleArray count];
    if (styleCount == 0)
        return;

    fontStyle = [characterAttributes.fontStyleArray objectAtIndex:styleCount-1];

    [fontStyle restoreFontStyleInTextBuilder:self];

    [characterAttributes.fontStyleArray removeObjectAtIndex:styleCount-1];
}

// Bold

- (BOOL)fontBold;
{
    return characterAttributes.fontAttributes.bold;
}

- (BOOL)setFontBold:(BOOL)newFontBold;
{
    BOOL oldFontBold;

    if (characterAttributes.fontAttributes.bold == newFontBold)
        return characterAttributes.fontAttributes.bold;

    [self emitStyleChanges];

    oldFontBold = characterAttributes.fontAttributes.bold;
    characterAttributes.fontAttributes.bold = newFontBold;
    [self cachedFontIsInvalid];
    return oldFontBold;
}

- (void)startBold;
{
    characterAttributes.boldCount++;
    [self setFontBold:YES];
}

- (void)stopBold;
{
    if (characterAttributes.boldCount > 0)
        characterAttributes.boldCount--;
    [self setFontBold:characterAttributes.boldCount > 0];
}

// Italic

- (BOOL)fontItalic;
{
    return characterAttributes.fontAttributes.italic;
}

- (BOOL)setFontItalic:(BOOL)newFontItalic;
{
    BOOL oldFontItalic;

    if (characterAttributes.fontAttributes.italic == newFontItalic)
        return characterAttributes.fontAttributes.italic;

    [self emitStyleChanges];

    oldFontItalic = characterAttributes.fontAttributes.italic;
    characterAttributes.fontAttributes.italic = newFontItalic;
    [self cachedFontIsInvalid];
    return oldFontItalic;
}

- (void)startItalic;
{
    characterAttributes.italicCount++;
    [self setFontItalic:YES];
}

- (void)stopItalic;
{
    if (characterAttributes.italicCount > 0)
        characterAttributes.italicCount--;
    [self setFontItalic:characterAttributes.italicCount > 0];
}


// Underlining
- (BOOL)underlined;
{
    return characterAttributes.flags.underlined;
}

- (BOOL)setUnderlined:(BOOL)shouldUnderline;
{
    BOOL wasUnderlined;

    if (characterAttributes.flags.underlined == shouldUnderline)
        return characterAttributes.flags.underlined;
    
    [self emitStyleChanges];

    wasUnderlined = characterAttributes.flags.underlined;
    characterAttributes.flags.underlined = shouldUnderline;
    return wasUnderlined;
}

- (void)startUnderlining;
{
    characterAttributes.underliningCount++;
    [self setUnderlined:YES];
}

- (void)stopUnderlining;
{
    if (characterAttributes.underliningCount > 0)
        characterAttributes.underliningCount--;
    [self setUnderlined:characterAttributes.underliningCount>0];
}

- (int)ligatureType;
{
    return characterAttributes.ligatureType;
}

- (int)setLigatureType:(int)newLigatureType;
{
    int oldLigatureType;

    if (newLigatureType == characterAttributes.ligatureType)
        return characterAttributes.ligatureType;

    [self emitStyleChanges];

    oldLigatureType = characterAttributes.ligatureType;
    characterAttributes.ligatureType = newLigatureType;
    return oldLigatureType;
}

// Text color

- (OHColorPaletteIndex)textColorIndex;
{
    return characterAttributes.textColorIndex;
}

- (OHColorPaletteIndex)setTextColorIndex:(OHColorPaletteIndex)anIndex;
{
    OHColorPaletteIndex oldTextColorIndex;

    if (characterAttributes.textColorIndex == anIndex)
	return characterAttributes.textColorIndex;

    [self emitStyleChanges];

    oldTextColorIndex = characterAttributes.textColorIndex;
    characterAttributes.textColorIndex = anIndex;
    return oldTextColorIndex;
}

- (NSColor *)setTextColorOverride:(NSColor *)color;
{
    NSColor *oldTextColorOverride;

    if (color == characterAttributes.textColorOverride)
        return [[characterAttributes.textColorOverride retain] autorelease];

    [self emitStyleChanges];

    oldTextColorOverride = characterAttributes.textColorOverride;
    characterAttributes.textColorOverride = [color retain];
    if (color != nil && characterAttributes.anchor != nil)
        [characterAttributes.anchor setHasOverrideColor];

    return [oldTextColorOverride autorelease];
}

// Font stuff

- (float)fontSize;
{
    return characterAttributes.fontAttributes.size;
}

- (float)setFontSize:(float)newFontSize;
{
    float oldFontSize;

    // Round the font size to the nearest integer to work around a bug in NSLayoutManager when using non-integral font sizes.
    newFontSize = rint(newFontSize);

    if (characterAttributes.fontAttributes.size == newFontSize)
        return characterAttributes.fontAttributes.size;

    [self emitStyleChanges];

    oldFontSize = characterAttributes.fontAttributes.size;
    characterAttributes.fontAttributes.size = newFontSize;
    [self cachedFontIsInvalid];
    return oldFontSize;
}

- (float)fontSizeMultiple;
{
    return characterAttributes.fontSizeMultiple;
}

- (float)setFontSizeMultiple:(float)newFontSizeMultiple;
{
    float baseFontSize;
    float oldFontSizeMultiple;
    
    if (characterAttributes.fontSizeMultiple == newFontSizeMultiple)
	return characterAttributes.fontSizeMultiple;
    oldFontSizeMultiple = characterAttributes.fontSizeMultiple;
    characterAttributes.fontSizeMultiple = newFontSizeMultiple;
    switch (characterAttributes.fontStyle) {
        default:
            baseFontSize = defaultsHolder->normalFontSize;
            break;
        case FONTSTYLE_TITLE:
            baseFontSize = defaultsHolder->titleFontSize;
            break;
        case FONTSTYLE_FIXEDPITCH:
            baseFontSize = defaultsHolder->fixedPitchFontSize;
            break;
    }
    [self setFontSize:characterAttributes.fontSizeMultiple * baseFontSize];
    return oldFontSizeMultiple;
}

- (int)baseFontSizeIndex;
{
    return characterAttributes.baseFontSizeIndex;
}

- (int)setBaseFontSizeIndex:(int)anIndex;
{
    int oldBaseFontSizeIndex;

    oldBaseFontSizeIndex = characterAttributes.baseFontSizeIndex;
    if (anIndex < 1)
	anIndex = 1;
    else if (anIndex > 7)
	anIndex = 7;
    characterAttributes.baseFontSizeIndex = anIndex;
    return oldBaseFontSizeIndex;
}

- (int)setFontSizeIndex:(int)anIndex;
{
    int oldFontSizeIndex;

    oldFontSizeIndex = characterAttributes.fontSizeIndex;
    if (anIndex < 1)
	anIndex = 1;
    else if (anIndex > 7)
	anIndex = 7;
    characterAttributes.fontSizeIndex = anIndex;
    [self setFontSizeMultiple:fontSizes[anIndex - 1] / OWBaseFontSize];
    return oldFontSizeIndex;
}

- (int)setRelativeFontSizeIndex:(int)anIndex;
{
    return [self setFontSizeIndex:characterAttributes.baseFontSizeIndex + anIndex];
}

- (OWFontStyle)fontStyle;
{
    return characterAttributes.fontStyle;
}

- (OWFontStyle)setFontStyle:(OWFontStyle)aFontStyle;
{
    float oldFontStyle;
    float oldFontSizeMultiple;
    
    if (characterAttributes.fontStyle == aFontStyle)
	return characterAttributes.fontStyle;

    [self emitStyleChanges];

    oldFontStyle = characterAttributes.fontStyle;
    characterAttributes.fontStyle = aFontStyle;
    [self cachedFontIsInvalid];
    
    // Force size change if new style has a different base size
    oldFontSizeMultiple = characterAttributes.fontSizeMultiple;
    characterAttributes.fontSizeMultiple = 0;
    [self setFontSizeMultiple:oldFontSizeMultiple];
    
    return oldFontStyle;
}

- (NSString *)setFontFaceOverrideFromNameArray:(NSArray *)faceNameArray;
{
    NSSet *fontFamilyNames;
    int nameIndex, nameCount;

    fontFamilyNames = [isa fontFamilyNames];
    
    nameCount = [faceNameArray count];
    for (nameIndex = 0; nameIndex < nameCount; nameIndex++) {
        NSString *name;

        name = [faceNameArray objectAtIndex:nameIndex];
        if ([fontFamilyNames containsObject:name])
            return [self setFontFaceNameOverride:name];
    }
    return [[characterAttributes.fontFaceNameOverride retain] autorelease];
}

- (NSString *)setFontFaceNameOverride:(NSString *)fontFaceName;
{
    NSString *oldFontFaceName;
    
    if ((fontFaceName == characterAttributes.fontFaceNameOverride) || [fontFaceName isEqualToString:characterAttributes.fontFaceNameOverride])
        return [[characterAttributes.fontFaceNameOverride retain] autorelease];

    [self emitStyleChanges];
    oldFontFaceName = characterAttributes.fontFaceNameOverride;
    characterAttributes.fontFaceNameOverride = [fontFaceName retain];
    [self cachedFontIsInvalid];

    return [oldFontFaceName autorelease];
}

- (NSString *)fontFaceNameOverride;
{
    return characterAttributes.fontFaceNameOverride;
}

- (NSFont *)currentFont;
{
    NSString *family;

    if (characterAttributes.font)
        return characterAttributes.font;

    family = characterAttributes.fontFaceNameOverride;
    if (!family) {
        switch (characterAttributes.fontStyle) {
            case FONTSTYLE_NORMAL:
                family = defaultsHolder->normalFontName;
                break;
            case FONTSTYLE_TITLE:
                family = defaultsHolder->titleFontName;
                break;
            case FONTSTYLE_FIXEDPITCH:
                family = defaultsHolder->fixedPitchFontName;
                break;
            default:
                return nil;
        }
    }

    characterAttributes.font = [[OAFontCache fontWithFamily:family attributes:characterAttributes.fontAttributes] retain];
    return characterAttributes.font;
}


//

- (int)setBaselineOffset:(int)offset;
{
    int oldBaselineOffset;

    if (characterAttributes.baselineOffset == offset)
        return characterAttributes.baselineOffset;

    [self emitStyleChanges];

    oldBaselineOffset = characterAttributes.baselineOffset;
    characterAttributes.baselineOffset = offset;
    return oldBaselineOffset;
}

- (int)changeBaselineOffsetBy:(int)offset;
{
    return [self setBaselineOffset:characterAttributes.baselineOffset+offset];
}

//

- (BOOL)preserveWhitespace;
{
    return paragraphAttributes.flags.preserveWhitespace;
}

- (BOOL)setPreserveWhitespace:(BOOL)shouldPreserveWhitespace;
{
    BOOL oldPreserveWhitespace;

    oldPreserveWhitespace = paragraphAttributes.flags.preserveWhitespace;
    paragraphAttributes.flags.preserveWhitespace = shouldPreserveWhitespace;
    if (paragraphAttributes.flags.preserveWhitespace)
	outputWhitespaceIfNecessary(self);
    return oldPreserveWhitespace;
}

- (BOOL)obeyLines;
{
    return paragraphAttributes.flags.obeyLines;
}

- (BOOL)setObeyLines:(BOOL)shouldObeyLines;
{
    BOOL oldObeyLines;

    oldObeyLines = paragraphAttributes.flags.obeyLines;
    paragraphAttributes.flags.obeyLines = shouldObeyLines;
    if (paragraphAttributes.flags.obeyLines)
	outputWhitespaceIfNecessary(self);
    return oldObeyLines;
}

- (BOOL)noWrapMode;
{
    return characterAttributes.noWrapMode;
}

- (BOOL)setNoWrapMode:(BOOL)newMode;
{
    BOOL oldNoWrapMode;

    oldNoWrapMode = characterAttributes.noWrapMode;
    characterAttributes.noWrapMode = newMode;
    return oldNoWrapMode;
}

- (NSLineBreakMode)lineBreakMode;
{
    return paragraphAttributes.lineBreakMode;
}

- (NSLineBreakMode)setLineBreakMode:(NSLineBreakMode)newMode;
{
    NSLineBreakMode oldLineBreakMode;

    if (newMode == paragraphAttributes.lineBreakMode)
        return paragraphAttributes.lineBreakMode;

    [self emitStyleChanges];
    
    oldLineBreakMode = paragraphAttributes.lineBreakMode;
    paragraphAttributes.lineBreakMode = newMode;
    return oldLineBreakMode;
}

- (void)setRequiredCharacterWidth:(unsigned int)aCharacterWidth;
{
    float width;

    width = 20 /* text object slop */ + aCharacterWidth * FigureSpaceWidth;
    [[htmlOwner htmlView] setRequiredWidth:width];
}


//
// Call these if your string contains any whitespace that shouldn't really appear (eg, normal CDATA).
//

- (void)writeUnprocessedString:(NSString *)aString;
{
    unsigned int position, length;

    length = [aString length];
    for (position = 0; position < length; position += BUILDER_BUFFER_SIZE) {
        unsigned int batchLength;
        
        batchLength = MIN(BUILDER_BUFFER_SIZE, length - position);
        [aString getCharacters:unprocessedCharacters range:NSMakeRange(position, batchLength)];
        [self writeUnprocessedCharacters:unprocessedCharacters length:batchLength];
    }
}

- (void)writeUnprocessedCharacters:(unichar *)characters length:(unsigned int)length;
{
    unsigned int characterIndex;

    for (characterIndex = 0; characterIndex < length; characterIndex++) {
        unichar unicodeCharacter = characters[characterIndex];

        switch (unicodeCharacter) {
            case '\n':
                if (paragraphAttributes.flags.obeyLines && needLineBreakCause == OW_CAUSE_CR) {
                    needLineBreakCause = OW_CAUSE_LF;
                    break;
                }
                // No break
            case '\r':
                if (paragraphAttributes.flags.obeyLines) {
                    outputNewlineIfNecessary(self);
                    needLineBreakSize = characterAttributes.lastWrittenFontSize;
                    needLineBreakCause = (unicodeCharacter == '\r') ? OW_CAUSE_CR : OW_CAUSE_LF;
                    flags.justDidSpace = YES;
                    flags.atStartOfLine = YES;
                    currentVerticalSpace = 0.0;
                    [self setFirstIndent:paragraphAttributes.hangingIndent];
                    paragraphAttributes.hangingIndent = 0.0;
                    break;
                }
                // No break
            case ' ':
            case '\t':
                if (paragraphAttributes.flags.preserveWhitespace) {
                    outputWhitespaceIfNecessary(self);
                    if (unicodeCharacter == ' ' && characterAttributes.noWrapMode)
                        addProcessedCharacter(self, NO_BREAK_SPACE);
                    else
                        addProcessedCharacter(self, unicodeCharacter);
                    flags.justDidSpace = YES;
                    flags.atStartOfLine = NO;
                } else if (flags.inBlock && !flags.justDidSpace) {
                    addProcessedCharacter(self, self->characterAttributes.noWrapMode ? NO_BREAK_SPACE : ' ');
                    flags.justDidSpace = YES;
                    OBASSERT(!flags.atStartOfLine);
                }
                break;
            default:
                if (!flags.inBlock)
                    [self beginParagraph];
                outputWhitespaceIfNecessary(self);

                addProcessedCharacter(self, unicodeCharacter);

                flags.justDidSpace = NO;
                flags.atStartOfLine = NO;
                break;
        }
    }
}


//
// Call these if you know you aren't writing any whitespace (eg, program-generated strings). These have the same effects as prepareForMoreText and wroteNonWhitespaceText
//

- (void)writeProcessedString:(NSString *)aString;
{
    unsigned int position, length;

    length = [aString length];
    for (position = 0; position < length; position += BUILDER_BUFFER_SIZE) {
        unsigned int batchLength;

        batchLength = MIN(BUILDER_BUFFER_SIZE, length - position);
        [aString getCharacters:unprocessedCharacters range:NSMakeRange(position, batchLength)];
        [self writeProcessedCharacters:unprocessedCharacters length:batchLength];
    }
}

- (void)writeProcessedCharacters:(unichar *)characters length:(unsigned int)length;
{
    unsigned int characterIndex;

    [self prepareForMoreText];
    for (characterIndex = 0; characterIndex < length; characterIndex++) {
        unichar unicodeCharacter = characters[characterIndex];

        addProcessedCharacter(self, unicodeCharacter);
    }
    [self wroteNonWhitespaceText];
}
    

//
// Writing cells and other specials, high-level interface
//

// CURRENTLY USED for writing
// - OHFormImage in <IMAGE>
// - OHInlineImageCell in <HR>
// - OWMarginalImage and OHInlineImageCell in <IMAGE>
// - OHImageCell in <LI>
- (void)writeNonWhitespaceCellObject:(OHBasicCell *)cellObject;
{
    if ([cellObject isMarginalCell]) {
	[self writeMarginalCellObject:cellObject];
        return;
    }

    if (!flags.inBlock)
        [self beginParagraph];
    [self writeCellObject:cellObject];
    flags.justDidSpace = NO;
    flags.atStartOfLine = NO;
}

// CURRENTLY USED for writing
// - OHFormText in <ISINDEX>
// - OHFormSelect in <SELECT>
// - OHFormTextArea in <TEXTAREA>
// - OHFormButton in <RESET>
// - OHFormFile and OHFormButton in <FILE>
// - OHFormButton in <SUBMIT>
// - OHFormText in <TEXTINPUT>
// - OHFormButton in <CHECKBOX>
// - OHTableContainerCell in <TABLE>
- (void)writeFormCellObject:(OHBasicCell *)cellObject;
{
    if ([cellObject isMarginalCell]) {
	[self writeMarginalCellObject:cellObject];
        return;
    }

    [self prepareForMoreText];
#warning 3 pixel baseline offset for form cells is arbitrary, change to align bottom of text in cell with bottom of text in font
    [self changeBaselineOffsetBy:-3];
    [self writeCellObject:cellObject];
    [self changeBaselineOffsetBy:3];
    [self wroteNonWhitespaceText];
}

// CURRENTLY USED for writing
// - side-aligned images in <IMG>
// - side-aligned tables in <TABLE>
// - side-aligned tables in <APPLET>
// - side-aligned tables in <INPUT TYPE=IMAGE>
- (void)writeMarginalCellObject:(OHBasicCell *)cellObject;
{
    [self prepareForMoreText];
    [cellObject setCharacterPosition:totalCharactersProcessed(self) htmlOwner:htmlOwner];
    [[htmlOwner htmlView] addMarginalCell:cellObject];
}

// CURRENTLY USED for writing
// - clear=xxx in <BR>
- (void)writeClearBreakObject:(OHBreak *)breakObject;
{
    outputWhitespaceIfNecessary(self);  // Force output of newline before we do set char position
    [breakObject setCharacterPosition:totalCharactersProcessed(self)];
    [[htmlOwner htmlView] addClearBreakObject:breakObject];
}



//
// Low-level interface to writing text and cells
//

// Call if you're going to write something to the attributed string, so the builder will start a paragraph if necessary
- (void)prepareForMoreText;
{
    if (!flags.inBlock)
	[self beginParagraph];
    outputWhitespaceIfNecessary(self);
}

// This just writes the cell, so you MUST call prepareForMoreText and wroteNonWhitespaceText yourself
- (void)writeCellObject:(OHBasicCell *)cellObject;
{
    // Add any whitespace, then emit the attributes for buffered characters
    outputWhitespaceIfNecessary(self);
    [self emitStyleChanges];

    // Add an attachment character, then emit attributes WITH the attachment
    [cellObject setCharacterPosition:totalCharactersProcessed(self) htmlOwner:htmlOwner];
    addProcessedCharacter(self, NSAttachmentCharacter);

    characterAttributes.nonretainedAttachmentObject = cellObject;
    [self emitStyleChanges];
    characterAttributes.nonretainedAttachmentObject = nil;
}

// CURRENTLY USED for writing
// - non-breaking space between the cell and field in FILE INPUT tags
// - zero-width space to give a soft word break for <WBR>
- (void)writeProcessedCharacter:(unichar)unicodeCharacter;
{
    addProcessedCharacter(self, unicodeCharacter);
}

// Call after you wrote something to the attributed that actually printed something, so the builder will know that it needs whitespace
- (void)wroteNonWhitespaceText;
{
    flags.justDidSpace = NO;
    flags.needVerticalSpace = NO;
    flags.atStartOfLine = NO;
}


// Anchors

- (void)startAnchorNamed:(NSString *)name address:(OWAddress *)address;
{
    if (characterAttributes.anchor)
        return;

    // Output newlines or spaces we have stored up before we start the anchor.
    outputWhitespaceIfNecessary(self);

    [self emitStyleChanges];

    if (defaultsHolder->flags.underlineAnchors && address)
        [self startUnderlining];

    OBPRECONDITION(characterAttributes.preAnchorTextColorOverride == nil);
    OBPRECONDITION(characterAttributes.anchor == nil);

    if (address != nil) {
        // This is a link:  reset the current font color
        // We call -setTextColorOverride: before setting the anchor because -setTextColorOverride: will notify the anchor that its font color was overridden.
        characterAttributes.preAnchorTextColorOverride = [[self setTextColorOverride:nil] retain];
    } else {
        // A (non-link) named anchor doesn't change the text color
        characterAttributes.preAnchorTextColorOverride = [characterAttributes.textColorOverride retain];
    }
    characterAttributes.anchor = [[OHHTMLAnchor allocWithZone:[[htmlOwner htmlView] zone]] initWithColorPalette:colorPalette];
    [characterAttributes.anchor setHTMLView:[htmlOwner htmlView]];
    if (name != nil) {
        [characterAttributes.anchor setName:name];
        [[[htmlOwner htmlDocument] htmlPageView] addNamedAnchor:characterAttributes.anchor];
    }
    if (address != nil) {
        [characterAttributes.anchor setAddress:address];
        [[[htmlOwner htmlDocument] htmlPageView] addLink:characterAttributes.anchor];
    }

    [characterAttributes.anchor setRange:NSMakeRange(totalCharactersProcessed(self), 0)];

    [self setTextColorIndex:[characterAttributes.anchor colorIndex]];
}

- (OHHTMLAnchor *)anchor;
{
    return characterAttributes.anchor;
}

- (void)endAnchor;
{
    unsigned int location = [characterAttributes.anchor range].location;
    
    if (!characterAttributes.anchor)
        return;

    [self emitStyleChanges];
    [characterAttributes.anchor setRange:NSMakeRange(location, totalCharactersProcessed(self)-location)];

    if (defaultsHolder->flags.underlineAnchors && [characterAttributes.anchor address])
        [self stopUnderlining];
    [self setTextColorIndex:OHColorPaletteTextIndex];

    [characterAttributes.anchor release];
    characterAttributes.anchor = nil;
    [self setTextColorOverride:characterAttributes.preAnchorTextColorOverride];
    [characterAttributes.preAnchorTextColorOverride release];
    characterAttributes.preAnchorTextColorOverride = nil;
}

// Debugging

- (NSMutableDictionary *)debugDictionary;
{
    NSMutableDictionary *debugDictionary;

    debugDictionary = [super debugDictionary];

    [debugDictionary setObject:flags.justDidSpace ? @"YES" : @"NO" forKey:@"flags.justDidSpace"];
    [debugDictionary setObject:[NSString stringWithFormat:@"%d", paragraphAttributes.textAlignment] forKey:@"paragraphAttributes.textAlignment"];
    [debugDictionary setObject:[NSString stringWithFormat:@"%d", characterAttributes.fontSizeIndex] forKey:@"characterAttributes.fontSizeIndex"];
    [debugDictionary setObject:characterAttributes.fontAttributes.bold ? @"YES" : @"NO" forKey:@"characterAttributes.fontAttributes.bold"];
    if (characterAttributes.anchor)
        [debugDictionary setObject:characterAttributes.anchor forKey:@"characterAttributes.anchor"];

    return debugDictionary;
}

@end


@implementation OHTextBuilder (Private)

static NSSet *fontFamilyNames = nil;

+ (NSSet *)fontFamilyNames;
{
    if (fontFamilyNames)
        return fontFamilyNames;

    [NSThread lockMainThread]; // This also keeps two OHTextBuilders from looking up the available fonts at the same time
    if (fontFamilyNames == nil) { // Someone else might have calculated these while we were waiting on the lock
        NS_DURING {
            fontFamilyNames = [[NSSet alloc] initWithArray:[[NSFontManager sharedFontManager] availableFonts]];
        } NS_HANDLER {
            NSLog(@"Warning: Exception raised while generating list of available fonts: %@", [localException reason]);
        } NS_ENDHANDLER;
    }
    [NSThread unlockMainThread];

    return fontFamilyNames;
}

- (void)setupAttributedString;
{
    if (mutableAttributedString)
        [mutableAttributedString release];
    if (mutableString)
        [mutableString release];
    mutableAttributedString = [[NSMutableAttributedString allocWithZone:[[htmlOwner htmlView] zone]] init];
    mutableString = [[mutableAttributedString mutableString] retain];
    indexOfStyleChangeInAttributedString = 0;
}

- (void)emitAlignmentChange;
{
    if (paragraphAttributes.paragraphAlignment != OW_UNSPECIFIED_ALIGNMENT)
        [self setTextAlignment:paragraphAttributes.paragraphAlignment];
    else if (paragraphAttributes.centeringCount > 0)
        [self setTextAlignment:OW_CENTERED];
    else if (paragraphAttributes.divisionAlignment != OW_UNSPECIFIED_ALIGNMENT)
        [self setTextAlignment:paragraphAttributes.divisionAlignment];
    else
        [self setTextAlignment:OW_LEFTALIGNED];
}

- (void)emitStyleChanges;
{
    unsigned int attributedStringLength;
    NSMutableDictionary *styleDictionary;

    [self flushProcessedCharactersToAttributedString];
    attributedStringLength = [mutableAttributedString length];
    if (indexOfStyleChangeInAttributedString == attributedStringLength)
        return;

    styleDictionary = [self _currentAttributesDictionaryRetained];
    // Set the attributes on the attributedString
    [mutableAttributedString setAttributes:styleDictionary range:NSMakeRange(indexOfStyleChangeInAttributedString, attributedStringLength - indexOfStyleChangeInAttributedString)];
    [styleDictionary release];
    indexOfStyleChangeInAttributedString = attributedStringLength;
}

- (void)flushProcessedCharactersToAttributedString
{
    NSString *string;

    if (!processedCharacterCount)
        return;

    string = [[NSString alloc] initWithCharacters:processedCharacters length:processedCharacterCount];
    [mutableString appendString:string];
    [string release];

    charactersWrittenFromProcessedBuffer += processedCharacterCount;
    processedCharacterCount = 0;
}

// Style changes

- (void)cachedFontIsInvalid;
{
    [characterAttributes.font release];
    characterAttributes.font = nil;
}

- (NSMutableDictionary *)_currentAttributesDictionaryRetained;
{
    NSFont *font;
    NSMutableDictionary *styleDictionary;
    NSMutableParagraphStyle *mutableParagraphStyle;
    OFZone *zone;   /* zone for any long-lived objects */

    zone = [OFZone zoneForObject:[htmlOwner htmlDocument]];
    
    styleDictionary = [OHMutableKnownKeyAttributeDictionary newWithTemplate:OHAttributeDictionaryTemplate zone:[zone nsZone]];
    //styleDictionary = [[NSMutableDictionary alloc] init];

    // Add character styles based on current settings

    // Font style, bold, italic, size
    font = [self currentFont];
    [styleDictionary setObject:font forKey:NSFontAttributeName];
    [characterAttributes.nonretainedAttachmentObject setFont:font];
    
    // Font color
    if (characterAttributes.textColorOverride == nil)
        [styleDictionary setObject:[colorPalette colorAtColorIndex:characterAttributes.textColorIndex] forKey:NSForegroundColorAttributeName];
    else
        [styleDictionary setObject:characterAttributes.textColorOverride forKey:NSForegroundColorAttributeName];
    // Underlined
    if (characterAttributes.flags.underlined)
        [styleDictionary setObject:[NSNumber numberWithInt:1] forKey:NSUnderlineStyleAttributeName];
    // Baseline offset (superscript or subscript)
    if (characterAttributes.baselineOffset)
        [styleDictionary setObject:[NSNumber numberWithInt:characterAttributes.baselineOffset] forKey:NSBaselineOffsetAttributeName];
    // Ligature style
    if (characterAttributes.ligatureType != 1)
        [styleDictionary setObject:[NSNumber numberWithInt:characterAttributes.ligatureType] forKey:NSLigatureAttributeName];
    
    // Add paragraph styles based on current settings
    mutableParagraphStyle = [[NSMutableParagraphStyle allocWithZone:[zone nsZone]] init];

    // Alignment
    switch (paragraphAttributes.textAlignment) {
        case OW_LEFTALIGNED:
        default:
            [mutableParagraphStyle setAlignment:NSLeftTextAlignment];
            break;
        case OW_CENTERED:
            [mutableParagraphStyle setAlignment:NSCenterTextAlignment];
            break;
        case OW_RIGHTALIGNED:
            [mutableParagraphStyle setAlignment:NSRightTextAlignment];
            break;
        case OW_JUSTIFIED:
            [mutableParagraphStyle setAlignment:NSJustifiedTextAlignment];
            break;
    }
    
    // Indents
    [mutableParagraphStyle setFirstLineHeadIndent:paragraphAttributes.leftIndent + paragraphAttributes.firstIndent];
    [mutableParagraphStyle setHeadIndent:paragraphAttributes.leftIndent];
    [mutableParagraphStyle setTailIndent:paragraphAttributes.rightIndent];

    // Line break mode
    if (paragraphAttributes.lineBreakMode != NSLineBreakByWordWrapping)
        [mutableParagraphStyle setLineBreakMode:paragraphAttributes.lineBreakMode];
    
    // Add the paragraph style to the attributes
    [styleDictionary setObject:mutableParagraphStyle forKey:NSParagraphStyleAttributeName];
    [mutableParagraphStyle release];


    // Add attachments
    if (characterAttributes.nonretainedAttachmentObject)
        [styleDictionary setObject:characterAttributes.nonretainedAttachmentObject forKey:NSAttachmentAttributeName];

    // Add anchor
    if (characterAttributes.anchor)
        [styleDictionary setObject:characterAttributes.anchor forKey:OHHTMLAnchorAttributeName];

    return styleDictionary;
}

- (void)flushAttributedStringToHTMLView;
{
    BOOL hadPreviousAttributedString;
    
    // Reset the lastFlushTimeInterval in -lastFlushToHTMLViewComplete rather than in -flushAttributedStringToHTMLView so we can spend more time processing between flushes
    // lastFlushTimeInterval = [NSDate timeIntervalSinceReferenceDate];

    [self flushProcessedCharactersToAttributedString];
    [self emitStyleChanges];

    hadPreviousAttributedString = (lastMutableAttributedString != nil);
    [lastMutableAttributedString release];
    lastMutableAttributedString = mutableAttributedString;
    mutableAttributedString = nil; // implicitly released by passing its -retain to lastMutableAttributedString
    [mutableString release];
    mutableString = nil;

    OBASSERT(!flags.flushToHTMLViewInProgress || flags.endedTextBuild);
    flags.flushToHTMLViewInProgress = YES;
    [[htmlOwner htmlView] appendAttributedString:lastMutableAttributedString];
    [self queueSelector:@selector(lastFlushToHTMLViewComplete)];

    if (!flags.endedTextBuild)
        [self setupAttributedString];
    if (!hadPreviousAttributedString)
        [nonretainedTextBuilderObserver textBuilderNoLongerPristine:self];
}

- (void)lastFlushToHTMLViewComplete;
{
    // Reset the lastFlushTimeInterval in -lastFlushToHTMLViewComplete rather than in -flushAttributedStringToHTMLView so we can spend more time processing between flushes
    lastFlushTimeInterval = [NSDate timeIntervalSinceReferenceDate];
    flags.flushToHTMLViewInProgress = NO;
}

@end

@interface NSAttributeDictionary:NSDictionary
@end
@implementation NSAttributeDictionary (OHTextBuilder_Fix_isEqualToDictionary)
static IMP old_newWithDictionary;

+ (void)performPosing
{
    old_newWithDictionary = OBReplaceMethodImplementationWithSelector(((Class)self)->isa, @selector(newWithDictionary:), @selector(OHTextBuilder_newWithDictionary:));
}

+ OHTextBuilder_newWithDictionary:(NSDictionary *)aDictionary
{
    if (![aDictionary objectForKey:OHHTMLAnchorAttributeName])
        return (*old_newWithDictionary)(self, _cmd, aDictionary);
    return [aDictionary retain];
}
@end

@implementation OHMutableKnownKeyAttributeDictionary

- mutableCopyWithZone:(NSZone *)zone;
{
    return [self mutableKnownKeyCopyWithZone:zone];
}

- newWithKey:aKey object:anObject
{
    OFMutableKnownKeyDictionary *newDictionary;

    newDictionary = [self mutableKnownKeyCopyWithZone:[self zone]];
    [newDictionary setObject:anObject forKey:aKey];
    return newDictionary;
}
@end
