// Copyright 1997-2002 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 "OWCSSStyleSheet.h"

#import <OmniBase/OmniBase.h>
#import <Foundation/Foundation.h>
#import <OmniFoundation/OmniFoundation.h>

#import "OWAddress.h"
#import "OWContentInfo.h"
#import "OWContentType.h"
#import "OWCSSIdentifier.h"
#import "OWCSSNumber.h"
#import "OWCSSSelector.h"
#import "OWCSSSelectorGroup.h"
#import "OWCSSTokenizer.h"
#import "OWDataStream.h"
#import "OWDataStreamCharacterCursor.h"
#import "OWDataStreamScanner.h"
#import "OWCSSDeclarations.h"
#import "OWObjectStreamCursor.h"
#import "OWSGMLProcessor.h"
#import "OWSGMLTag.h"
#import "OWSimpleTarget.h"
#import "OWWebPipeline.h"

RCS_ID("$Header: /Network/Source/CVS/OmniGroup/Frameworks/OWF/CSS.subproj/OWCSSStyleSheet.m,v 1.96 2002/03/09 01:53:48 kc Exp $")


@interface OWCSSStyleSheet (Private)
+ (void)_readDefaults;
+ (void)_parseIntoDeclarations:(OWCSSDeclarations *)declarations fromTokenizer:(OWCSSTokenizer *)tokenizer;
+ (BOOL)_parseDeclarationIntoDeclarations:(OWCSSDeclarations *)declarations fromTokenizer:(OWCSSTokenizer *)tokenizer;
+ (BOOL)_atEndOfDeclarationForTokenizer:(OWCSSTokenizer *)tokenizer;
// parse generic values
+ (BOOL)_parseValueIntoDeclarations:(OWCSSDeclarations *)declarations declarationIndex:(CSSDeclarationIndexes)declarationIndex fromTokenizer:(OWCSSTokenizer *)tokenizer allowedTokensMask:(unsigned int)allowedTokensMask;
// parse specific values
+ (BOOL)_parseBorderIntoDeclarations:(OWCSSDeclarations *)declarations declarationIndex:(CSSDeclarationIndexes)declarationIndex fromTokenizer:(OWCSSTokenizer *)tokenizer;
+ (BOOL)_parseBorderColorIntoDeclarations:(OWCSSDeclarations *)declarations declarationIndex:(CSSDeclarationIndexes)declarationIndex fromTokenizer:(OWCSSTokenizer *)tokenizer;
+ (BOOL)_parseBorderStyleIntoDeclarations:(OWCSSDeclarations *)declarations declarationIndex:(CSSDeclarationIndexes)declarationIndex fromTokenizer:(OWCSSTokenizer *)tokenizer;
+ (id)_convertBorderWidthIdentifierToLength:(id)widthIdentifier;
+ (BOOL)_parseBorderWidthIntoDeclarations:(OWCSSDeclarations *)declarations declarationIndex:(CSSDeclarationIndexes)declarationIndex fromTokenizer:(OWCSSTokenizer *)tokenizer;
+ (BOOL)_parseBorderSingleSideIntoDeclarations:(OWCSSDeclarations *)declarations declarationIndex:(CSSDeclarationIndexes)declarationIndex fromTokenizer:(OWCSSTokenizer *)tokenizer;
+ (BOOL)_parseBorderStyleForSingleSideIntoDeclarations:(OWCSSDeclarations *)declarations declarationIndex:(CSSDeclarationIndexes)declarationIndex fromTokenizer:(OWCSSTokenizer *)tokenizer;
+ (BOOL)_parseBorderWidthForSingleSideIntoDeclarations:(OWCSSDeclarations *)declarations declarationIndex:(CSSDeclarationIndexes)declarationIndex fromTokenizer:(OWCSSTokenizer *)tokenizer;
+ (BOOL)_parseBackgroundIntoDeclarations:(OWCSSDeclarations *)declarations declarationIndex:(CSSDeclarationIndexes)declarationIndex fromTokenizer:(OWCSSTokenizer *)tokenizer;
+ (BOOL)_parseBoxValueIntoDeclarations:(OWCSSDeclarations *)declarations declarationIndex:(CSSDeclarationIndexes)declarationIndex fromTokenizer:(OWCSSTokenizer *)tokenizer;
+ (void)_parseColorToken:(id)colorValue intoDeclarations:(OWCSSDeclarations *)declarations declarationIndex:(CSSDeclarationIndexes)declarationIndex;
+ (BOOL)_parseColorValueIntoDeclarations:(OWCSSDeclarations *)declarations declarationIndex:(CSSDeclarationIndexes)declarationIndex fromTokenizer:(OWCSSTokenizer *)tokenizer;
+ (BOOL)_parseFontIntoDeclarations:(OWCSSDeclarations *)declarations declarationIndex:(CSSDeclarationIndexes)declarationIndex fromTokenizer:(OWCSSTokenizer *)tokenizer;
+ (BOOL)_parseFontFamiliesIntoDeclarations:(OWCSSDeclarations *)declarations declarationIndex:(CSSDeclarationIndexes)declarationIndex fromTokenizer:(OWCSSTokenizer *)tokenizer;
+ (BOOL)_parseMarginIntoDeclarations:(OWCSSDeclarations *)declarations declarationIndex:(CSSDeclarationIndexes)declarationIndex fromTokenizer:(OWCSSTokenizer *)tokenizer;
+ (BOOL)_parseTextDecorationsIntoDeclarations:(OWCSSDeclarations *)declarations declarationIndex:(CSSDeclarationIndexes)declarationIndex fromTokenizer:(OWCSSTokenizer *)tokenizer;
+ (BOOL)_isValidClassSelector:(NSString *)classSelector;
//
- (void)_parseStyleSheetFromScanner:(OFCharacterScanner *)aScanner parentContentInfo:(OWContentInfo *)parentContentInfo;
//
- (void)_addSelectorGroup:(OWCSSSelectorGroup *)group;
- (void)_addDeclarations:(OWCSSDeclarations *)declarations inDictionary:(NSDictionary *)dictionary forName:(NSString *)name tag:(OWSGMLTag *)tag tagStack:(OFStaticArray *)tagStack;
- (BOOL)_checkTagStack:(OFStaticArray *)tagStack forSelectorGroup:(OWCSSSelectorGroup *)group;
@end

@interface NSObject (GettingTagFromStackItem)
- (OWSGMLTag *)tag;
@end


@implementation OWCSSStyleSheet

static BOOL OWCSSDisabled = NO;
unsigned int OWFDebugStyleSheetsLevel = 0;

static OWContentType *cssContentType;
static BOOL weKnowIfWeRepondToCompileColorValue = NO;
static BOOL weRespondToCompileColorValue;
typedef BOOL (*parseIMPType)(id, SEL, OWCSSDeclarations *declarations, CSSDeclarationIndexes declarationIndex, OWCSSTokenizer *tokenizer); 
static struct {
    parseIMPType parseIMP;
    SEL selector;
    unsigned int allowedTokensMask;
} OWCSSPropertyParsingTable[CSSDeclarationsCount];

+ (void)initialize;
{
    unsigned int propertyIndex;
    
    OBINITIALIZE;

    cssContentType = [OWContentType contentTypeForString:@"text/css"];
    
    for (propertyIndex = 0; propertyIndex < CSSDeclarationsCount; propertyIndex++) {
        OWCSSPropertyParsingTable[propertyIndex].selector = NULL;
        OWCSSPropertyParsingTable[propertyIndex].parseIMP = NULL;
        OWCSSPropertyParsingTable[propertyIndex].allowedTokensMask = 0;
    }
    
#define OWCSSUseCustomParsingSelectorStartingWith(parseTableIndex, selectorStart) { \
        OWCSSPropertyParsingTable[parseTableIndex].selector = @selector(selectorStart##declarationIndex:fromTokenizer:); \
        OWCSSPropertyParsingTable[parseTableIndex].parseIMP = (parseIMPType)[self methodForSelector:OWCSSPropertyParsingTable[parseTableIndex].selector]; \
    }

    // Background
    OWCSSUseCustomParsingSelectorStartingWith(CSSBackgroundDeclarationIndex, _parseBackgroundIntoDeclarations:);
    OWCSSPropertyParsingTable[CSSBackgroundAttachmentDeclarationIndex].allowedTokensMask = OWCSSTokenIdentifier;
    OWCSSUseCustomParsingSelectorStartingWith(CSSBackgroundColorDeclarationIndex, _parseColorValueIntoDeclarations:);
    OWCSSPropertyParsingTable[CSSBackgroundImageDeclarationIndex].allowedTokensMask = OWCSSTokenFunction;
    OWCSSPropertyParsingTable[CSSBackgroundPositionDeclarationIndex].allowedTokensMask = OWCSSTokenIdentifier;
    OWCSSPropertyParsingTable[CSSBackgroundRepeatDeclarationIndex].allowedTokensMask = OWCSSTokenIdentifier;

    // Border
    OWCSSUseCustomParsingSelectorStartingWith(CSSBorderDeclarationIndex, _parseBorderIntoDeclarations:);
    OWCSSUseCustomParsingSelectorStartingWith(CSSBorderColorDeclarationIndex, _parseBorderColorIntoDeclarations:);
    OWCSSUseCustomParsingSelectorStartingWith(CSSBorderStyleDeclarationIndex, _parseBorderStyleIntoDeclarations:);
    OWCSSUseCustomParsingSelectorStartingWith(CSSBorderWidthDeclarationIndex, _parseBorderWidthIntoDeclarations:);
    
    OWCSSUseCustomParsingSelectorStartingWith(CSSBorderTopDeclarationIndex, _parseBorderSingleSideIntoDeclarations:);
    OWCSSUseCustomParsingSelectorStartingWith(CSSBorderTopColorDeclarationIndex, _parseColorValueIntoDeclarations:);
    OWCSSUseCustomParsingSelectorStartingWith(CSSBorderTopStyleDeclarationIndex, _parseBorderStyleForSingleSideIntoDeclarations:);
    OWCSSUseCustomParsingSelectorStartingWith(CSSBorderTopWidthDeclarationIndex , _parseBorderWidthForSingleSideIntoDeclarations:);
    
    OWCSSUseCustomParsingSelectorStartingWith(CSSBorderRightDeclarationIndex, _parseBorderSingleSideIntoDeclarations:);
    OWCSSUseCustomParsingSelectorStartingWith(CSSBorderRightColorDeclarationIndex, _parseColorValueIntoDeclarations:);
    OWCSSUseCustomParsingSelectorStartingWith(CSSBorderRightStyleDeclarationIndex, _parseBorderStyleForSingleSideIntoDeclarations:);
    OWCSSUseCustomParsingSelectorStartingWith(CSSBorderRightWidthDeclarationIndex , _parseBorderWidthForSingleSideIntoDeclarations:);
    
    OWCSSUseCustomParsingSelectorStartingWith(CSSBorderBottomDeclarationIndex, _parseBorderSingleSideIntoDeclarations:);
    OWCSSUseCustomParsingSelectorStartingWith(CSSBorderBottomColorDeclarationIndex, _parseColorValueIntoDeclarations:);
    OWCSSUseCustomParsingSelectorStartingWith(CSSBorderBottomStyleDeclarationIndex, _parseBorderStyleForSingleSideIntoDeclarations:);
    OWCSSUseCustomParsingSelectorStartingWith(CSSBorderBottomWidthDeclarationIndex , _parseBorderWidthForSingleSideIntoDeclarations:);
    
    OWCSSUseCustomParsingSelectorStartingWith(CSSBorderLeftDeclarationIndex, _parseBorderSingleSideIntoDeclarations:);
    OWCSSUseCustomParsingSelectorStartingWith(CSSBorderLeftColorDeclarationIndex, _parseColorValueIntoDeclarations:);
    OWCSSUseCustomParsingSelectorStartingWith(CSSBorderLeftStyleDeclarationIndex, _parseBorderStyleForSingleSideIntoDeclarations:);
    OWCSSUseCustomParsingSelectorStartingWith(CSSBorderLeftWidthDeclarationIndex , _parseBorderWidthForSingleSideIntoDeclarations:);
    // Bottom
    OWCSSPropertyParsingTable[CSSBottomDeclarationIndex].allowedTokensMask = OWCSSTokenIdentifier|OWCSSTokenNumber;
    // Clear
    OWCSSPropertyParsingTable[CSSClearDeclarationIndex].allowedTokensMask = OWCSSTokenIdentifier;
    // Color
    OWCSSUseCustomParsingSelectorStartingWith(CSSColorDeclarationIndex, _parseColorValueIntoDeclarations:);
    // Display
    OWCSSPropertyParsingTable[CSSDisplayDeclarationIndex].allowedTokensMask = OWCSSTokenIdentifier;
    // Float
    OWCSSPropertyParsingTable[CSSFloatDeclarationIndex].allowedTokensMask = OWCSSTokenIdentifier;
    // Font
    OWCSSUseCustomParsingSelectorStartingWith(CSSFontDeclarationIndex, _parseFontIntoDeclarations:);
    OWCSSUseCustomParsingSelectorStartingWith(CSSFontFamilyDeclarationIndex, _parseFontFamiliesIntoDeclarations:);
    OWCSSPropertyParsingTable[CSSFontSizeDeclarationIndex].allowedTokensMask = OWCSSTokenIdentifier|OWCSSTokenNumber;
    OWCSSPropertyParsingTable[CSSFontSizeAdjustDeclarationIndex].allowedTokensMask = OWCSSTokenIdentifier|OWCSSTokenNumber;
    OWCSSPropertyParsingTable[CSSFontStretchDeclarationIndex].allowedTokensMask = OWCSSTokenIdentifier;
    OWCSSPropertyParsingTable[CSSFontStyleDeclarationIndex].allowedTokensMask = OWCSSTokenIdentifier;
    OWCSSPropertyParsingTable[CSSFontVariantDeclarationIndex].allowedTokensMask = OWCSSTokenIdentifier;
    OWCSSPropertyParsingTable[CSSFontWeightDeclarationIndex].allowedTokensMask = OWCSSTokenIdentifier|OWCSSTokenNumber;
    // Height
    OWCSSPropertyParsingTable[CSSHeightDeclarationIndex].allowedTokensMask = OWCSSTokenIdentifier|OWCSSTokenNumber;
    OWCSSUseCustomParsingSelectorStartingWith(CSSHeightDeclarationIndex, _parseBoxValueIntoDeclarations:);
    // Left
    OWCSSPropertyParsingTable[CSSLeftDeclarationIndex].allowedTokensMask = OWCSSTokenIdentifier|OWCSSTokenNumber;
    // Line-height
    OWCSSPropertyParsingTable[CSSLineHeightDeclarationIndex].allowedTokensMask = OWCSSTokenIdentifier|OWCSSTokenNumber;
    // Margin
    OWCSSUseCustomParsingSelectorStartingWith(CSSMarginDeclarationIndex, _parseMarginIntoDeclarations:);
    OWCSSPropertyParsingTable[CSSMarginBottomDeclarationIndex].allowedTokensMask = OWCSSTokenIdentifier|OWCSSTokenNumber;
    OWCSSPropertyParsingTable[CSSMarginLeftDeclarationIndex].allowedTokensMask = OWCSSTokenIdentifier|OWCSSTokenNumber;
    OWCSSPropertyParsingTable[CSSMarginRightDeclarationIndex].allowedTokensMask = OWCSSTokenIdentifier|OWCSSTokenNumber;
    OWCSSPropertyParsingTable[CSSMarginTopDeclarationIndex].allowedTokensMask = OWCSSTokenIdentifier|OWCSSTokenNumber;
    // Padding
    OWCSSUseCustomParsingSelectorStartingWith(CSSPaddingDeclarationIndex, _parsePaddingIntoDeclarations:);
    OWCSSPropertyParsingTable[CSSPaddingBottomDeclarationIndex].allowedTokensMask = OWCSSTokenIdentifier|OWCSSTokenNumber;
    OWCSSPropertyParsingTable[CSSPaddingLeftDeclarationIndex].allowedTokensMask = OWCSSTokenIdentifier|OWCSSTokenNumber;
    OWCSSPropertyParsingTable[CSSPaddingRightDeclarationIndex].allowedTokensMask = OWCSSTokenIdentifier|OWCSSTokenNumber;
    OWCSSPropertyParsingTable[CSSPaddingTopDeclarationIndex].allowedTokensMask = OWCSSTokenIdentifier|OWCSSTokenNumber;
    // Position
    OWCSSPropertyParsingTable[CSSPositionDeclarationIndex].allowedTokensMask = OWCSSTokenIdentifier;
    // Right
    OWCSSPropertyParsingTable[CSSRightDeclarationIndex].allowedTokensMask = OWCSSTokenIdentifier|OWCSSTokenNumber;
    // Text
    OWCSSPropertyParsingTable[CSSTextAlignDeclarationIndex].allowedTokensMask = OWCSSTokenIdentifier|OWCSSTokenString;
    OWCSSUseCustomParsingSelectorStartingWith(CSSTextDecorationDeclarationIndex, _parseTextDecorationsIntoDeclarations:);
    OWCSSPropertyParsingTable[CSSTextIndentDeclarationIndex].allowedTokensMask = OWCSSTokenNumber;
    // Top
    OWCSSPropertyParsingTable[CSSTopDeclarationIndex].allowedTokensMask = OWCSSTokenIdentifier|OWCSSTokenNumber;
    // Vertical-align
    OWCSSPropertyParsingTable[CSSVerticalAlignDeclarationIndex].allowedTokensMask = OWCSSTokenIdentifier|OWCSSTokenNumber;
    // Visibility
    OWCSSPropertyParsingTable[CSSVisibilityDeclarationIndex].allowedTokensMask = OWCSSTokenIdentifier;
    // White-space
    OWCSSPropertyParsingTable[CSSWhiteSpaceDeclarationIndex].allowedTokensMask = OWCSSTokenIdentifier;
    // Width
    OWCSSPropertyParsingTable[CSSWidthDeclarationIndex].allowedTokensMask = OWCSSTokenIdentifier|OWCSSTokenNumber;
    OWCSSUseCustomParsingSelectorStartingWith(CSSWidthDeclarationIndex, _parseBoxValueIntoDeclarations:);
    // Z-index
    OWCSSPropertyParsingTable[CSSZIndexDeclarationIndex].allowedTokensMask = OWCSSTokenIdentifier|OWCSSTokenNumber;
    
#undef OWCSSUseCustomParsingSelectorStartingWith

    [[OFController sharedController] addObserver:self];
}

+ (void)parseIntoDeclarations:(OWCSSDeclarations *)declarations fromString:(NSString *)declarationsString;
{
    OFStringScanner *scanner;
    OWCSSTokenizer *tokenizer;
    
    scanner = [[OFStringScanner alloc] initWithString:declarationsString];
    tokenizer = [[OWCSSTokenizer alloc] initWithScanner:scanner baseAddress:nil];
    [scanner release];
    [self _parseIntoDeclarations:declarations fromTokenizer:tokenizer];
    [tokenizer release];
}

// Init and dealloc

- init;
{
    if ([super init] == nil)
        return nil;
    currentPosition = 0;
    globalDeclarations = [[OWCSSDeclarations alloc] init];
    return self;
}

- (void)dealloc;
{
    [lastSelectorsByTag release];
    [lastSelectorsByClass release];
    [lastSelectorsByID release];
    [lastSelectorsByPseudoclass release];
    [globalDeclarations release];
    [super dealloc];
}

// API

- (void)parseStyleSheetString:(NSString *)sheet parentContentInfo:(OWContentInfo *)parentContentInfo;
{
    OFCharacterScanner *scanner;

    scanner = [[OFStringScanner alloc] initWithString:sheet];
    NS_DURING {
        [self _parseStyleSheetFromScanner:scanner parentContentInfo:parentContentInfo];
    } NS_HANDLER {
        NSLog(@"%@: Exception parsing embedded style sheet: %@", [[parentContentInfo address] addressString], [localException reason]);
    } NS_ENDHANDLER;
    [scanner release];
}

- (void)parseStyleSheetFromURLString:(NSString *)urlString parentContentInfo:(OWContentInfo *)parentContentInfo;
{
    OWAddress *sourceAddress;
    OWSimpleTarget *fetchTarget;
    id <OWContent> fetchedContent;
    OWDataStream *scriptDataStream;
    OWDataStreamCharacterCursor *scriptDataCursor;
    OWDataStreamScanner *dataStreamScanner;
    OWPipeline *sheetPipeline;
    OWContentInfo *sheetContentInfo;

    if (urlString == nil)
        return;

    sourceAddress = [(OWAddress *)[parentContentInfo address] addressForRelativeString:urlString];
    if (sourceAddress == nil)
        return;

    fetchTarget = [[OWSimpleTarget alloc] initWithParentContentInfo:parentContentInfo targetContentType:cssContentType];
    [fetchTarget setAcceptsAlternateContent:YES];
    [fetchTarget setTargetTypeFormatString:NSLocalizedStringFromTableInBundle(@"CSS Stylesheet", @"OWF", [OWCSSStyleSheet bundle], target type format string for CSS stylesheets fetched from external urls)];
    sheetPipeline = [[OWWebPipeline alloc] initWithContent:sourceAddress target:fetchTarget];
    [sheetPipeline startProcessingContent];
    fetchedContent = [fetchTarget resultingContent];
    [parentContentInfo addChildFossil:fetchTarget];
    [fetchTarget release];
    if (![fetchedContent isKindOfClass:[OWDataStream class]])
        return;

    sheetContentInfo = [sheetPipeline contentInfo];
    OBASSERT(sheetContentInfo != nil);
    scriptDataStream = (OWDataStream *)fetchedContent;
    // Using an OWDataStreamCharacterCursor automatically checks for the charset header, obeys preferences, etc.
    scriptDataCursor = [[OWDataStreamCharacterCursor alloc] initForDataCursor:[scriptDataStream newCursor]];
    dataStreamScanner = [[OWDataStreamScanner alloc] initWithCursor:scriptDataCursor];
    [scriptDataCursor release];
    NS_DURING {
        [self _parseStyleSheetFromScanner:dataStreamScanner parentContentInfo:sheetContentInfo];
    } NS_HANDLER {
        NSLog(@"%@: Exception parsing style sheet: %@", [[sheetContentInfo address] addressString], [localException reason]);
    } NS_ENDHANDLER;
    [dataStreamScanner release];
    [sheetPipeline release];
}


// Fill in declarations object with relevant styles

- (void)addDeclarations:(OWCSSDeclarations *)declarations cssTagName:(NSString *)name tag:(OWSGMLTag *)tag tagStack:(OFStaticArray *)tagStack;
{
    return [self _addDeclarations:declarations inDictionary:lastSelectorsByTag forName:name tag:tag tagStack:tagStack];
}

- (void)addDeclarations:(OWCSSDeclarations *)declarations cssClassName:(NSString *)name tag:(OWSGMLTag *)tag tagStack:(OFStaticArray *)tagStack;
{
#warning WJS: 2/21/02 Punting on class="foo bar" for until POST 4.1.
#ifdef 0
    if ([name rangeOfString:@" "].length != 0) {
        NSEnumerator *nameEnumerator;
        NSString *subname;

        nameEnumerator = [[name componentsSeparatedByString:@" "] objectEnumerator];
        while ((subname = [nameEnumerator nextObject]))
            if ([OWCSSStyleSheet _isValidClassSelector:subname])
                [self _addDeclarations:declarations inDictionary:lastSelectorsByClass forName:subname tag:tag tagStack:tagStack];
    } else if ([OWCSSStyleSheet _isValidClassSelector:name])
#endif
        [self _addDeclarations:declarations inDictionary:lastSelectorsByClass forName:name tag:tag tagStack:tagStack];
}

- (void)addDeclarations:(OWCSSDeclarations *)declarations cssIDName:(NSString *)name tag:(OWSGMLTag *)tag tagStack:(OFStaticArray *)tagStack;
{
    [self _addDeclarations:declarations inDictionary:lastSelectorsByID forName:name tag:tag tagStack:tagStack];
}

- (void)addDeclarations:(OWCSSDeclarations *)declarations cssPseudoClassName:(NSString *)name tag:(OWSGMLTag *)tag tagStack:(OFStaticArray *)tagStack;
{
    [self _addDeclarations:declarations inDictionary:lastSelectorsByPseudoclass forName:name tag:tag tagStack:tagStack];
}

- (OWCSSDeclarations *)globalDeclarations;
{
    return globalDeclarations;
}

@end


@implementation OWCSSStyleSheet (Private)

+ (void)_readDefaults;
{
    OWCSSDisabled = [[NSUserDefaults standardUserDefaults] boolForKey:@"OWCSSDisabled"];
}

/* This wants to parse strings of the form:
{
    property : value value ;
    property : value
    property : any ( any ) any [ any ] any { any }
}
eg:
{	padding: 4px;
	background-color: #ff7f6b;
	border-bottom: 2px white solid;
	background: url( http://foo/bar.gif );
	background: url( "http://foo;/bar{}.gif" );
	font: 14px/14pt ";Trebuchet \"MS\"", Trebuchet, Arial, sans-serif;
	font-weight: bold
}
*/
#warning -_parseIntoDeclarations:fromTokenizer: currently ignores things like "!important"
+ (void)_parseIntoDeclarations:(OWCSSDeclarations *)declarations fromTokenizer:(OWCSSTokenizer *)tokenizer;
{
    if (OWCSSDisabled)
        return;
    while (1) {
        id tokenValue;

        switch ([tokenizer getNextToken:&tokenValue]) {
            case OWCSSTokenEOF:
                return;
            case OWCSSTokenWhitespace:
                continue;
            case OWCSSTokenPunctuation: {
                unichar punctuation;
                
                punctuation = [tokenValue characterAtIndex:0];
                if (punctuation == '{' || punctuation == ';')
                    continue;
                else if (punctuation == '}')
                    return;
                else
                    break;
            }
            case OWCSSTokenString:
            case OWCSSTokenIdentifier:
                [tokenizer ungetLastToken];
                if ([self _parseDeclarationIntoDeclarations:declarations fromTokenizer:tokenizer])
                    continue;
                break;
            default:
                break;
        }
        
        // Skip to the start of the next declaration if the last one didn't parse correctly
        [tokenizer skipTokensUpToAndIncludingPunctuation:@";}"];
        [tokenizer ungetLastToken];
    }
}

+ (BOOL)_parseDeclarationIntoDeclarations:(OWCSSDeclarations *)declarations fromTokenizer:(OWCSSTokenizer *)tokenizer;
{
    OWCSSTokenType tokenType;
    id tokenValue;
    OWCSSTokenType propertyTokenType;
    OWCSSIdentifier *propertyName;
    unsigned int propertyParsingTableIndex;

    // { [property] : value value ; property value }
    propertyTokenType = [tokenizer getNextToken:&propertyName]; // We know we're a string or identifier when we get here
//    if (propertyTokenType == OWCSSTokenString) propertyName = [propertyName lowercaseString];
    
    // { property[ ]: value value ; property value }
    tokenType = [tokenizer getNextToken:&tokenValue skipWhitespace:YES]; // NOTE: Strict CSS2 guidelines don't allow whitespace here anyhow, but who are we hurting?
        
    // { property [:] value value ; property value }
    if (tokenType != OWCSSTokenPunctuation || ![tokenValue isEqualToString:@":"])
        return NO;

    // { property :[ ]value value ; property value }
    tokenType = [tokenizer getNextToken:&tokenValue];
    if (tokenType != OWCSSTokenWhitespace)
        [tokenizer ungetLastToken];
    
    
    // { property :[ value value ]garbage ; property value }
    if (propertyTokenType != OWCSSTokenIdentifier) // Property wasn't a valid identifier
        return NO;
   
    propertyParsingTableIndex = [propertyName declarationIndex];     
    if (propertyParsingTableIndex == NSNotFound) // Property was an identifier, but not one we recognize as being a property name (eg, "url" would be bad)
        return NO;

    if (OWCSSPropertyParsingTable[propertyParsingTableIndex].parseIMP != NULL) {
        return OWCSSPropertyParsingTable[propertyParsingTableIndex].parseIMP(self,  OWCSSPropertyParsingTable[propertyParsingTableIndex].selector, declarations, propertyParsingTableIndex, tokenizer);
    } else if (OWCSSPropertyParsingTable[propertyParsingTableIndex].allowedTokensMask > 0) {
        return [self _parseValueIntoDeclarations:declarations declarationIndex:propertyParsingTableIndex fromTokenizer:tokenizer allowedTokensMask:OWCSSPropertyParsingTable[propertyParsingTableIndex].allowedTokensMask];
    }
    
    return NO;
}

+ (BOOL)_atEndOfDeclarationForTokenizer:(OWCSSTokenizer *)tokenizer;
{
    OWCSSTokenType tokenType;
    id tokenValue;

    tokenType = [tokenizer getNextToken:&tokenValue];
    [tokenizer ungetLastToken];
    if (tokenType == OWCSSTokenPunctuation) {
        unichar character;
        
        character = [tokenValue characterAtIndex:0];
        return (character == ';' || character == '}');
    } else
        return NO;
}

// parse generic values

+ (BOOL)_parseValueIntoDeclarations:(OWCSSDeclarations *)declarations declarationIndex:(CSSDeclarationIndexes)declarationIndex fromTokenizer:(OWCSSTokenizer *)tokenizer allowedTokensMask:(unsigned int)allowedTokensMask;
{
    id value;
    OWCSSTokenType tokenType;
        
    tokenType = [tokenizer getNextToken:&value];

    if (tokenType & allowedTokensMask) {
        [declarations setObject:value atIndex:declarationIndex];
        return [self _atEndOfDeclarationForTokenizer:tokenizer];
    } else {
        [tokenizer ungetLastToken];
        return NO;
    }
}


// parse specific values

// background: [<'background-color'> || <'background-image'> || <'background-repeat'> || <'background-attachment'> || <'background-position'>] | inherit
// background: red
// background: url("chess.png") gray 50% repeat fixed
+ (BOOL)_parseBackgroundIntoDeclarations:(OWCSSDeclarations *)declarations declarationIndex:(CSSDeclarationIndexes)declarationIndex fromTokenizer:(OWCSSTokenizer *)tokenizer;
{
    // [<'background-color'> || <'background-image'> || <'background-repeat'> || <'background-attachment'> || <'background-position'>] | inherit
    while (YES) { 
        OWCSSTokenType tokenType;
        id tokenValue;

        tokenType = [tokenizer getNextToken:&tokenValue excludingIdentifiers:NO excludingNumbers:NO];
    
        // End of background declaration?
        if (tokenType == OWCSSTokenPunctuation) {
            unichar character = [tokenValue characterAtIndex:0];
            return (character == ';' || character == '}');
        }
            
        // inherit ?
        if (tokenValue == OWCSSInheritIdentifier) {
            [declarations setObject:OWCSSInheritIdentifier atIndex:CSSBackgroundColorDeclarationIndex];
            [declarations setObject:OWCSSInheritIdentifier atIndex:CSSBackgroundImageDeclarationIndex];
            [declarations setObject:OWCSSInheritIdentifier atIndex:CSSBackgroundPositionDeclarationIndex];
            [declarations setObject:OWCSSInheritIdentifier atIndex:CSSBackgroundRepeatDeclarationIndex];
            return [self _atEndOfDeclarationForTokenizer:tokenizer];
        }

        // background-image ?
        if (tokenType == OWCSSTokenFunction && [tokenValue objectAtIndex:0] == OWCSSURLIdentifier) {
            [declarations setObject:tokenValue atIndex:CSSBackgroundImageDeclarationIndex];
            continue;
        }
            
        // background-attachment ?
        if (tokenValue == OWCSSScrollIdentifier || tokenValue == OWCSSFixedIdentifier) {
            [declarations setObject:tokenValue atIndex:CSSBackgroundAttachmentDeclarationIndex];
            continue;
        }

        // background-position ?
        if (tokenValue == OWCSSTopIdentifier || tokenValue == OWCSSCenterIdentifier || tokenValue == OWCSSBottomIdentifier || tokenValue == OWCSSLeftIdentifier || tokenValue == OWCSSRightIdentifier || tokenType == OWCSSTokenNumber) {
            id currentPositionValue;
            
            currentPositionValue = declarations->declarations[CSSBackgroundPositionDeclarationIndex];
            if (currentPositionValue == nil) {
                // Create a new position array to hold this value
                [declarations setObject:[NSArray arrayWithObjects:tokenValue, nil] atIndex:CSSBackgroundPositionDeclarationIndex];
            } else {
                // Add this value to the existing position array
                [declarations setObject:[currentPositionValue arrayByAddingObject:tokenValue] atIndex:CSSBackgroundPositionDeclarationIndex];
            }
            continue;
        }
        
        // background-repeat ?
        if (tokenValue == OWCSSRepeatIdentifier || tokenValue == OWCSSRepeatXIdentifier || tokenValue == OWCSSRepeatYIdentifier || tokenValue == OWCSSNoRepeatIdentifier) {
            [declarations setObject:tokenValue atIndex:CSSBackgroundRepeatDeclarationIndex];
            continue;
        }

        // background-color ?
        if (tokenType == OWCSSTokenString || tokenType == OWCSSTokenIdentifier || tokenType == OWCSSTokenFunction) {
            [tokenizer ungetLastToken];
            [self _parseColorValueIntoDeclarations:declarations declarationIndex:CSSBackgroundColorDeclarationIndex fromTokenizer:tokenizer];
            continue;
        }

        if (tokenType == OWCSSTokenEOF)
            return [self _atEndOfDeclarationForTokenizer:tokenizer];
    }
}

// border: thin
// border: solid
// border: red
// border: thin solid red
// border: red thin solid
// border: solid red thin

+ (BOOL)_parseBorderIntoDeclarations:(OWCSSDeclarations *)declarations declarationIndex:(CSSDeclarationIndexes)declarationIndex fromTokenizer:(OWCSSTokenizer *)tokenizer;
{
    BOOL stylesDone = NO;
    
    do { // [ <border-width> || <border-style> || <border-color> ]?
        OWCSSTokenType tokenType;
        id tokenValue;

        tokenType = [tokenizer getNextToken:&tokenValue];
        if (OWFDebugStyleSheetsLevel) NSLog(@"got %@", [tokenValue description]);
        switch (tokenType) {
            case OWCSSTokenIdentifier:
                if ([tokenValue isBorderWidthIdentifier]) { // border-width: thin | medium | thick | <length>
                    [tokenizer ungetLastToken];
                    if (![self _parseBorderWidthIntoDeclarations:declarations declarationIndex:declarationIndex fromTokenizer:tokenizer])
                        return NO;                
                } else if ([tokenValue isBorderStyleIdentifier]) { // border-style: none | hidden | dotted | dashed | solid | double | groove | ridge | inset | outset
                    [tokenizer ungetLastToken];
                    if (![self _parseBorderStyleIntoDeclarations:declarations declarationIndex:declarationIndex fromTokenizer:tokenizer])
                        return NO;                
                } else { // border-color: <color values>
                    [tokenizer ungetLastToken];
                    if (![self _parseBorderColorIntoDeclarations:declarations declarationIndex:declarationIndex fromTokenizer:tokenizer])
                        return NO;
                }
                break;
            case OWCSSTokenString:
            case OWCSSTokenFunction:
                if (OWFDebugStyleSheetsLevel) NSLog(@"got a string or function! %@", [tokenValue description]);
                [tokenizer ungetLastToken];
                if (![self _parseBorderColorIntoDeclarations:declarations declarationIndex:declarationIndex fromTokenizer:tokenizer])
                    return NO;
                break;
            case OWCSSTokenNumber:
                if ([tokenValue isLength]) {
                    [tokenizer ungetLastToken];
                    if (![self _parseBorderWidthIntoDeclarations:declarations declarationIndex:declarationIndex fromTokenizer:tokenizer])
                        return NO;
                } else
                    stylesDone = YES;
                break;
            case OWCSSTokenWhitespace:
                break;
            case OWCSSTokenPunctuation: {
                unichar character;
                
                character = [tokenValue characterAtIndex:0];
                [tokenizer ungetLastToken];
                return character == ';' || character == '}';
            }
            case OWCSSTokenEOF:
                return YES;
            default:
                stylesDone = YES;
                break;
        }
        if (OWFDebugStyleSheetsLevel) NSLog(@"BORDER: maybe style %@, isStyle %d", tokenValue, !stylesDone);
    } while (!stylesDone);
    
    return YES;
}

+ (BOOL)_parseBorderColorIntoDeclarations:(OWCSSDeclarations *)declarations declarationIndex:(CSSDeclarationIndexes)declarationIndex fromTokenizer:(OWCSSTokenizer *)tokenizer;
{
    BOOL lastTokenWasNotStyleToken = NO;
    BOOL returnValue = YES;
    unsigned int tokenValueCount = 0;
    id tokenValues[4];

    do { // [<border-style>]{1,4}
        OWCSSTokenType tokenType;
        id tokenValue;

        tokenType = [tokenizer getNextToken:&tokenValue];
        switch (tokenType) {
            case OWCSSTokenIdentifier:
                // We handle these cases because they might be seen if we're parsing a aggregate border declaration
                if ([tokenValue isBorderWidthIdentifier]) {// border-width: thin | medium | thick | <length>
                    lastTokenWasNotStyleToken = YES;
                } else if ([tokenValue isBorderStyleIdentifier]) { // border-style: none | hidden | dotted | dashed | solid | double | groove | ridge | inset | outset
                    lastTokenWasNotStyleToken = YES;
                } else { // border-color: <color values>
                    tokenValues[tokenValueCount++] = tokenValue;
                }
                break;
            case OWCSSTokenString:
            case OWCSSTokenFunction:
                tokenValues[tokenValueCount++] = tokenValue;
                break;
            case OWCSSTokenWhitespace:
                break;
            case OWCSSTokenPunctuation: {
                unichar character;
                
                character = [tokenValue characterAtIndex:0];
                lastTokenWasNotStyleToken = YES;
                returnValue = character == ';' || character == '}';
                break;
            }
            default:
            case OWCSSTokenEOF:
                lastTokenWasNotStyleToken = YES;
                break;
        }
        if (OWFDebugStyleSheetsLevel) NSLog(@"BORDER: maybe style %@, isStyle %d", tokenValue, !lastTokenWasNotStyleToken);
    } while (!lastTokenWasNotStyleToken && tokenValueCount < 4);

    if (lastTokenWasNotStyleToken)
        [tokenizer ungetLastToken];

    switch (tokenValueCount) {
        case 1: tokenValues[1] = tokenValues[0];
        case 2: tokenValues[2] = tokenValues[0];
        case 3: tokenValues[3] = tokenValues[1];
        case 4:
            [self _parseColorToken:tokenValues[0] intoDeclarations:declarations declarationIndex:CSSBorderTopColorDeclarationIndex];
            [self _parseColorToken:tokenValues[1] intoDeclarations:declarations declarationIndex:CSSBorderRightColorDeclarationIndex];
            [self _parseColorToken:tokenValues[2] intoDeclarations:declarations declarationIndex:CSSBorderBottomColorDeclarationIndex];
            [self _parseColorToken:tokenValues[3] intoDeclarations:declarations declarationIndex:CSSBorderLeftColorDeclarationIndex];
        default:
            break;
    }
    
    if (tokenValueCount)
        [declarations setNeedsToStartABoxAtThisLevel];
    
    return returnValue;
}

+ (BOOL)_parseBorderStyleIntoDeclarations:(OWCSSDeclarations *)declarations declarationIndex:(CSSDeclarationIndexes)declarationIndex fromTokenizer:(OWCSSTokenizer *)tokenizer;
{
    BOOL lastTokenWasNotStyleToken = NO;
    BOOL returnValue = YES;
    unsigned int tokenValueCount = 0;
    id tokenValues[4];

    do { // [<border-style>]{1,4}
        OWCSSTokenType tokenType;
        id tokenValue;

        tokenType = [tokenizer getNextToken:&tokenValue];
        switch (tokenType) {
            case OWCSSTokenIdentifier:
                if ([tokenValue isBorderStyleIdentifier]) { // border-style: none | hidden | dotted | dashed | solid | double | groove | ridge | inset | outset
                    tokenValues[tokenValueCount++] = tokenValue;
                } else {
                    lastTokenWasNotStyleToken = YES;
                }
                break;
            case OWCSSTokenWhitespace:
                break;
            case OWCSSTokenPunctuation:
                {
                    unichar character;
                    
                    character = [tokenValue characterAtIndex:0];
                    returnValue = character == ';' || character == '}';
                    lastTokenWasNotStyleToken = YES;
                    break;
                }
            default:
            case OWCSSTokenNumber:
            case OWCSSTokenString:
            case OWCSSTokenFunction:
            case OWCSSTokenEOF:
                lastTokenWasNotStyleToken = YES;
                break;
        }
        if (OWFDebugStyleSheetsLevel) NSLog(@"BORDER: maybe style %@, isStyle %d", tokenValue, !lastTokenWasNotStyleToken);
    } while (!lastTokenWasNotStyleToken && tokenValueCount < 4);

    if (lastTokenWasNotStyleToken)
        [tokenizer ungetLastToken];

    switch (tokenValueCount) {
        case 1: tokenValues[1] = tokenValues[0];
        case 2: tokenValues[2] = tokenValues[0];
        case 3: tokenValues[3] = tokenValues[1];
        case 4:
            [declarations setObject:tokenValues[0] atIndex:CSSBorderTopStyleDeclarationIndex];
            [declarations setObject:tokenValues[1] atIndex:CSSBorderRightStyleDeclarationIndex];
            [declarations setObject:tokenValues[2] atIndex:CSSBorderBottomStyleDeclarationIndex];
            [declarations setObject:tokenValues[3] atIndex:CSSBorderLeftStyleDeclarationIndex];
        default:
            break;
    }
    
    if (tokenValueCount)
        [declarations setNeedsToStartABoxAtThisLevel];

    return returnValue;
}

+ (id)_convertBorderWidthIdentifierToLength:(id)widthIdentifier;
{
    if (widthIdentifier == OWCSSThinIdentifier) {
        return [[OWCSSNumber alloc] initWithFloatValue:1 unitsIdentifier:OWCSSPTIdentifier];
    } else if (widthIdentifier == OWCSSMediumIdentifier) {
        return [[OWCSSNumber alloc] initWithFloatValue:3 unitsIdentifier:OWCSSPTIdentifier];
    } else if (widthIdentifier == OWCSSThickIdentifier) {
        return [[OWCSSNumber alloc] initWithFloatValue:5 unitsIdentifier:OWCSSPTIdentifier];
    } else {
        NSLog(@"+[OWCSSStyleSheet _convertBorderWidthIdentifierToLength:]: Unrecognized width identifier \"%@\", defaulting to 5pt", widthIdentifier);
        return [[OWCSSNumber alloc] initWithFloatValue:5 unitsIdentifier:OWCSSPTIdentifier];
    }
}

+ (BOOL)_parseBorderWidthIntoDeclarations:(OWCSSDeclarations *)declarations declarationIndex:(CSSDeclarationIndexes)declarationIndex fromTokenizer:(OWCSSTokenizer *)tokenizer;
{
    BOOL lastTokenWasNotStyleToken = NO;
    BOOL returnValue = YES;
    unsigned int tokenValueCount = 0;
    id tokenValues[4];
    
    do { // [<border-style>]{1,4}
        OWCSSTokenType tokenType;
        id tokenValue;

        tokenType = [tokenizer getNextToken:&tokenValue];
        switch (tokenType) {
            case OWCSSTokenIdentifier:
                if ([tokenValue isBorderWidthIdentifier]) {// border-width: thin | medium | thick | <length>
                    tokenValues[tokenValueCount++] = [self _convertBorderWidthIdentifierToLength:tokenValue];
                } else {
                    lastTokenWasNotStyleToken = YES;
                }
                break;
            case OWCSSTokenNumber:
                if ([tokenValue isLength])
                    tokenValues[tokenValueCount++] = tokenValue;
                break;
            case OWCSSTokenWhitespace:
                break;
            case OWCSSTokenPunctuation:
            {
                unichar character;
                
                character = [tokenValue characterAtIndex:0];
                returnValue = character == ';' || character == '}';
                lastTokenWasNotStyleToken = YES;
                break;
            }
            default:
            case OWCSSTokenString:
            case OWCSSTokenFunction:
            case OWCSSTokenEOF:
                lastTokenWasNotStyleToken = YES;
                break;
        }
        if (OWFDebugStyleSheetsLevel) NSLog(@"BORDER: maybe style %@, isStyle %d", tokenValue, !lastTokenWasNotStyleToken);
    } while (!lastTokenWasNotStyleToken && tokenValueCount < 4);

    if (lastTokenWasNotStyleToken)
        [tokenizer ungetLastToken];
        
    switch (tokenValueCount) {
        case 1: tokenValues[1] = tokenValues[0];
        case 2: tokenValues[2] = tokenValues[0];
        case 3: tokenValues[3] = tokenValues[1];
        case 4:
            [declarations setObject:tokenValues[0] atIndex:CSSBorderTopWidthDeclarationIndex];
            [declarations setObject:tokenValues[1] atIndex:CSSBorderRightWidthDeclarationIndex];
            [declarations setObject:tokenValues[2] atIndex:CSSBorderBottomWidthDeclarationIndex];
            [declarations setObject:tokenValues[3] atIndex:CSSBorderLeftWidthDeclarationIndex];
        default:
            break;
    }
        
    if (tokenValueCount)
        [declarations setNeedsToStartABoxAtThisLevel];

    return returnValue;
}

+ (BOOL)_parseBorderSingleSideIntoDeclarations:(OWCSSDeclarations *)declarations declarationIndex:(CSSDeclarationIndexes)declarationIndex fromTokenizer:(OWCSSTokenizer *)tokenizer;
{
    BOOL stylesDone = NO;
        
    do { // [ <border-width> || <border-style> || <border-color> ]?
        OWCSSTokenType tokenType;
        id tokenValue;

        tokenType = [tokenizer getNextToken:&tokenValue];
        if (OWFDebugStyleSheetsLevel) NSLog(@"value = %@", tokenValue);
        switch (tokenType) {
            case OWCSSTokenIdentifier:
                if (OWFDebugStyleSheetsLevel) NSLog(@"ident = %@", tokenValue);
                if ([tokenValue isBorderWidthIdentifier]) { // border-width: thin | medium | thick | <length> 
                    [tokenizer ungetLastToken];
                    if (![self _parseBorderWidthForSingleSideIntoDeclarations:declarations declarationIndex:declarationIndex + CSSBorderWidthIndex fromTokenizer:tokenizer])
                        return NO;
                } else if ([tokenValue isBorderStyleIdentifier]) { // border-style: none | hidden | dotted | dashed | solid | double | groove | ridge | inset | outset
                    [tokenizer ungetLastToken];
                    if (![self _parseBorderStyleForSingleSideIntoDeclarations:declarations declarationIndex:declarationIndex + CSSBorderStyleIndex fromTokenizer:tokenizer])
                        return NO;
                } else if ((tokenType == OWCSSTokenString || tokenType == OWCSSTokenIdentifier || tokenType == OWCSSTokenFunction)) { // border-color: <color values>
                    [tokenizer ungetLastToken];
                    if (![self _parseColorValueIntoDeclarations:declarations declarationIndex:declarationIndex + CSSBorderColorIndex fromTokenizer:tokenizer])
                        return NO;
                } else {
                    stylesDone = YES;
                }
                break;
            case OWCSSTokenNumber:
                if (OWFDebugStyleSheetsLevel) NSLog(@"number = %@", tokenValue);
                if ([tokenValue isLength]) {
                    [tokenizer ungetLastToken];
                    if (![self _parseBorderWidthForSingleSideIntoDeclarations:declarations declarationIndex:declarationIndex + CSSBorderWidthIndex fromTokenizer:tokenizer])
                        return NO;
                } else {
                    stylesDone = YES;
                }
                break;
            case OWCSSTokenWhitespace:
                break;
            case OWCSSTokenPunctuation:
            {
                unichar character;
                
                character = [tokenValue characterAtIndex:0];
                [tokenizer ungetLastToken];
                return character == ';' || character == '}';
            }
            default:
            case OWCSSTokenEOF:
                stylesDone = YES;
                break;
        }
        if (OWFDebugStyleSheetsLevel) NSLog(@"BORDER: maybe style %@, isStyle %d", tokenValue, !stylesDone);
    } while (!stylesDone);
        
    return YES;
}

+ (BOOL)_parseBorderStyleForSingleSideIntoDeclarations:(OWCSSDeclarations *)declarations declarationIndex:(CSSDeclarationIndexes)declarationIndex fromTokenizer:(OWCSSTokenizer *)tokenizer;
{
    OWCSSTokenType tokenType;
    id tokenValue;
        
    tokenType = [tokenizer getNextToken:&tokenValue];

    switch (tokenType) {
        case OWCSSTokenIdentifier:
            if ([tokenValue isBorderStyleIdentifier]) { // border-style: none | hidden | dotted | dashed | solid | double | groove | ridge | inset | outset
                [declarations setObject:tokenValue atIndex:declarationIndex];
            } else {
                return YES;
            }
            break;
        default:
            return YES;
            break;
    }
    
    [declarations setNeedsToStartABoxAtThisLevel];

    return YES;
}

+ (BOOL)_parseBorderWidthForSingleSideIntoDeclarations:(OWCSSDeclarations *)declarations declarationIndex:(CSSDeclarationIndexes)declarationIndex fromTokenizer:(OWCSSTokenizer *)tokenizer;
{
    OWCSSTokenType tokenType;
    id tokenValue;
    
    tokenType = [tokenizer getNextToken:&tokenValue];

    if (tokenType == OWCSSTokenIdentifier) {
        if ([tokenValue isBorderWidthIdentifier]) {// border-width: thin | medium | thick | <length>
            [declarations setObject:[self _convertBorderWidthIdentifierToLength:tokenValue] atIndex:declarationIndex];
        } else {
            [tokenizer ungetLastToken];
        }
    } else if (tokenType == OWCSSTokenNumber) {
        if ([tokenValue isLength])
            [declarations setObject:tokenValue atIndex:declarationIndex];
    } else {
        [tokenizer ungetLastToken];
    }
            
    [declarations setNeedsToStartABoxAtThisLevel];

    return YES;
}

+ (BOOL)_parseBoxValueIntoDeclarations:(OWCSSDeclarations *)declarations declarationIndex:(CSSDeclarationIndexes)declarationIndex fromTokenizer:(OWCSSTokenizer *)tokenizer;
{
    [declarations setNeedsToStartABoxAtThisLevel];
    return [self _parseValueIntoDeclarations:declarations declarationIndex:declarationIndex fromTokenizer:tokenizer allowedTokensMask:OWCSSPropertyParsingTable[declarationIndex].allowedTokensMask];
}

// CLW: Pulled this functionality out so I could use it without a tokenizier, in the instance of border-colors, where we don't know the declaration index until AFTER we've seen the whole gamut of values.
+ (void)_parseColorToken:(id)colorValue intoDeclarations:(OWCSSDeclarations *)declarations declarationIndex:(CSSDeclarationIndexes)declarationIndex;
{
    if (OWFDebugStyleSheetsLevel >= 1) NSLog(@"\tgot color value:%@", colorValue);

    if (!weKnowIfWeRepondToCompileColorValue) {
        // multi-thread race condition actually has no ill effects if you do statements in this order:
        weRespondToCompileColorValue = [self respondsToSelector:@selector(compileColorValue:)];
        weKnowIfWeRepondToCompileColorValue = YES;
    }
    if (weRespondToCompileColorValue) {
        colorValue = [self compileColorValue:colorValue];
        if (OWFDebugStyleSheetsLevel >= 2) NSLog(@"\tcompiled color value to:%@", colorValue);
    }
    
    [declarations setObject:colorValue atIndex:declarationIndex];
}

// color: black
// color: #f00              /* #rgb */
// color: #ff0000           /* #rrggbb */
// color: ff0000		/* not-up-to-spec format used on excite.com */
// color: rgb(255,0,0)      /* integer range 0 - 255 */
// color: rgb(100%, 0%, 0%) /* float range 0.0% - 100.0% */
+ (BOOL)_parseColorValueIntoDeclarations:(OWCSSDeclarations *)declarations declarationIndex:(CSSDeclarationIndexes)declarationIndex fromTokenizer:(OWCSSTokenizer *)tokenizer;
{
    OWCSSTokenType tokenType;
    id colorValue;

    if (OWFDebugStyleSheetsLevel >= 2) NSLog(@"_parseColorValueIntoDeclarations:declarationIndex:fromTokenizer:");

    tokenType = [tokenizer getNextToken:&colorValue excludingIdentifiers:NO excludingNumbers:YES];
    [self _parseColorToken:colorValue intoDeclarations:declarations declarationIndex:declarationIndex];

    return [self _atEndOfDeclarationForTokenizer:tokenizer];
}

// font: [ [ <font-style> || <font-variant> || <font-weight> ]? <font-size> [ / <line-height> ]? <font-family> ] | caption | icon | menu | message-box | small-caption | status-bar | inherit
+ (BOOL)_parseFontIntoDeclarations:(OWCSSDeclarations *)declarations declarationIndex:(CSSDeclarationIndexes)declarationIndex fromTokenizer:(OWCSSTokenizer *)tokenizer;
{
    OWCSSTokenType tokenType;
    id tokenValue, fontSizeValue;
    BOOL stylesDone = NO;
    
    tokenType = [tokenizer getNextToken:&tokenValue];
    // caption | icon | menu | message-box | small-caption | status-bar | inherit
    if (tokenValue == OWCSSCaptionIdentifier || tokenValue == OWCSSIconIdentifier || tokenValue == OWCSSMenuIdentifier || tokenValue == OWCSSMessageBoxIdentifier || tokenValue == OWCSSSmallCaptionIdentifier || tokenValue == OWCSSStatusBarIdentifier || tokenValue == OWCSSInheritIdentifier) {
        [declarations setObject:tokenValue atIndex:declarationIndex];
        if (OWFDebugStyleSheetsLevel) NSLog(@"FONT: ident %@", tokenValue);
        return [self _atEndOfDeclarationForTokenizer:tokenizer];
    }

    do { // [ <font-style> || <font-variant> || <font-weight> ]?
        switch (tokenType) {
            case OWCSSTokenIdentifier:
                if ([tokenValue isFontStyleIdentifier]) // font-style: normal | italic | oblique | inherit
                    [declarations setObject:tokenValue atIndex:CSSFontStyleDeclarationIndex];
                else if ([tokenValue isFontVariantIdentifier]) // font-variant: normal | small-caps | inherit
                    [declarations setObject:tokenValue atIndex:CSSFontVariantDeclarationIndex];
                else if ([tokenValue isFontWeightIdentifier]) // font-weight: normal | bold | bolder | lighter | 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900 | inherit 
                    [declarations setObject:tokenValue atIndex:CSSFontWeightDeclarationIndex];
                else
                    stylesDone = YES;
                break;
            case OWCSSTokenNumber:
                if (![tokenValue isLength])
                    [declarations setObject:tokenValue atIndex:CSSFontWeightDeclarationIndex];
                else
                    stylesDone = YES;
                break;
            case OWCSSTokenWhitespace:
                break;
            case OWCSSTokenPunctuation: {
                unichar character;
                
                character = [tokenValue characterAtIndex:0];
                [tokenizer ungetLastToken];
                return (character == ';' || character == '}');
            }
            case OWCSSTokenEOF:
                return YES;
            default:
                stylesDone = YES;
                break;
        }
        if (OWFDebugStyleSheetsLevel) NSLog(@"FONT: maybe style %@, isStyle %d", tokenValue, !stylesDone);
        if (stylesDone)
            break;
        tokenType = [tokenizer getNextToken:&tokenValue];
    } while (YES);
    
    // <font-size>
    fontSizeValue = tokenValue;
    if (tokenType == OWCSSTokenIdentifier && [fontSizeValue isFontSizeIdentifier]) // font-size: [xx-small | x-small | small | medium | large | x-large | xx-large ] | [ larger | smaller ] | <length> | <percentage> | inherit 
        [declarations setObject:fontSizeValue atIndex:CSSFontSizeDeclarationIndex];
    else if (tokenType == OWCSSTokenNumber && [fontSizeValue isLength])
        [declarations setObject:fontSizeValue atIndex:CSSFontSizeDeclarationIndex];
    else
        return NO;
    if (OWFDebugStyleSheetsLevel) NSLog(@"FONT: font-size %@", fontSizeValue);
        
    // [ / <line-height> ]? 
    tokenType = [tokenizer getNextToken:&tokenValue skipWhitespace:YES];
    if (tokenType == OWCSSTokenPunctuation) {
        unichar character;
            
        character = [tokenValue characterAtIndex:0];
        if (character == ';' || character == '}') {
            [tokenizer ungetLastToken];
            return YES;
        }
        if (character == '/') {
            tokenType = [tokenizer getNextToken:&tokenValue skipWhitespace:YES];
            if (OWFDebugStyleSheetsLevel) NSLog(@"FONT: supposed line-height %@", tokenValue);
            if (tokenValue == OWCSSNormalIdentifier || tokenValue == OWCSSInheritIdentifier)
                [declarations setObject:tokenValue atIndex:CSSLineHeightDeclarationIndex];
            else if (tokenType == OWCSSTokenNumber) {
                if ([tokenValue isLength])
                    [declarations setObject:tokenValue atIndex:CSSLineHeightDeclarationIndex];
                else
                    [declarations setObject:[OWCSSNumber numberWithFloatValue:[tokenValue floatValue] unitsIdentifier:[fontSizeValue unitsIdentifier]] atIndex:CSSLineHeightDeclarationIndex];
            } else
                [tokenizer ungetLastToken];
        } else
            [tokenizer ungetLastToken];
    } else
        [tokenizer ungetLastToken];
    
    // <font-family>
    return [self _parseFontFamiliesIntoDeclarations:declarations declarationIndex:CSSFontFamilyDeclarationIndex fromTokenizer:tokenizer];
}

// font-family: [[ <family-name> | <generic-family> ],]* [ <family-name> | <generic-family> ] | inherit
/*
<family-name> 
The name of a font family of choice. In the previous example, "Baskerville", "Heisi Mincho W3", and "Symbol" are font families. Font family names containing whitespace should be quoted. If quoting is omitted, any whitespace characters before and after the font name are ignored and any sequence of whitespace characters inside the font name is converted to a single space. 
<generic-family> 
The following generic families are defined: 'serif', 'sans-serif', 'cursive', 'fantasy', and 'monospace'. Please see the section on generic font families for descriptions of these families. Generic font family names are keywords, and therefore must not be quoted. 
*/
+ (BOOL)_parseFontFamiliesIntoDeclarations:(OWCSSDeclarations *)declarations declarationIndex:(CSSDeclarationIndexes)declarationIndex fromTokenizer:(OWCSSTokenizer *)tokenizer;
{
    OWCSSTokenType tokenType;
    NSMutableArray *mutableFamilyNames;
    id familyNameOrGenericFamilyToken;

    mutableFamilyNames = [[[NSMutableArray alloc] init] autorelease];

    if (OWFDebugStyleSheetsLevel >= 2) NSLog(@"_readIntoFontFamilies:fromTokenizer:");

    while (1) {
        
        tokenType = [tokenizer getNextToken:&familyNameOrGenericFamilyToken];
        if (OWFDebugStyleSheetsLevel >= 1) NSLog(@"\tgot family name:%@", familyNameOrGenericFamilyToken);

        switch (tokenType) {
            case OWCSSTokenWhitespace:
                continue;
            case OWCSSTokenIdentifier: /* eg: serif, sans-serif */
                if (familyNameOrGenericFamilyToken != OWCSSSerifIdentifier && familyNameOrGenericFamilyToken != OWCSSSansSerifIdentifier && familyNameOrGenericFamilyToken != OWCSSCursiveIdentifier && familyNameOrGenericFamilyToken != OWCSSFantasyIdentifier && familyNameOrGenericFamilyToken != OWCSSMonospaceIdentifier)
                    goto ungetTokenAndReturnThatWeAreNotAtEndOfStatement;
                // else fall through!
            case OWCSSTokenString: {
                BOOL encounteredComma = NO;
                
                while (!encounteredComma) {
                    OWCSSTokenType furtherTokenType;
                    id furtherTokenValue;

                    furtherTokenType = [tokenizer getNextToken:&furtherTokenValue excludingIdentifiers:YES excludingNumbers:YES];
                    if (OWFDebugStyleSheetsLevel >= 1) NSLog(@"\tgot further token:%@", furtherTokenValue);

                    switch (furtherTokenType) {
                        case OWCSSTokenEOF:
                            goto addFamilyNameToArrayAndReturn;
                        case OWCSSTokenString: // This font name has multiple words, so append them together.
                            if (tokenType == OWCSSTokenIdentifier)
                                goto ungetTokenAndReturnThatWeAreNotAtEndOfStatement; // But we can't append more words to the OWCSSSerifIdentifier, for example
                            familyNameOrGenericFamilyToken = [[familyNameOrGenericFamilyToken stringByAppendingString:@" "] stringByAppendingString:furtherTokenValue];
                            continue;
                        case OWCSSTokenPunctuation: {
                            unichar character;

                            character = [furtherTokenValue characterAtIndex:0];
                            if (character == ',') {
                                [mutableFamilyNames addObject:familyNameOrGenericFamilyToken];
                                [declarations setObject:mutableFamilyNames atIndex:declarationIndex];
                                encounteredComma = YES;
                                continue;
                            } else if (character == ';' || character ==  '}') {
                                goto addFamilyNameToArrayAndReturn;
                            } else {
                                goto ungetTokenAndReturnThatWeAreNotAtEndOfStatement;
                            }
                            break;
                        }
                        case OWCSSTokenWhitespace:
                            continue;
                        default:
                            goto ungetTokenAndReturnThatWeAreNotAtEndOfStatement;
                    }
                }
                break;
            }
            case OWCSSTokenEOF:
                if ([mutableFamilyNames count] > 0)
                    [declarations setObject:mutableFamilyNames atIndex:declarationIndex];
                [tokenizer ungetLastToken];
                return YES;
            default:
                goto ungetTokenAndReturnThatWeAreNotAtEndOfStatement;
        }
    } // No fallthrough

addFamilyNameToArrayAndReturn:
    [mutableFamilyNames addObject:familyNameOrGenericFamilyToken];
    [declarations setObject:mutableFamilyNames atIndex:declarationIndex];
    [tokenizer ungetLastToken];
    return YES;

ungetTokenAndReturnThatWeAreNotAtEndOfStatement:
    [tokenizer ungetLastToken];
    return NO;
}

// <http://www.w3.org/TR/REC-CSS2/box.html#margin-properties>
// margin: <margin-width> {1,4} | inherit
// margin-width: <length> | <percentage> | auto

/*
BODY { margin: 2em }         // all margins set to 2em 
BODY { margin: 1em 2em }     // top & bottom = 1em, right & left = 2em
BODY { margin: 1em 2em 3em }  //  top=1em, right=2em, bottom=3em, left=2em

If there is only one value, it applies to all sides. If there are two values, the top and bottom margins are set to the first value and the right and left margins are set to the second. If there are three values, the top is set to the first value, the left and right are set to the second, and the bottom is set to the third. If there are four values, they apply to the top, right, bottom, and left, respectively.
*/

+ (BOOL)_parseEdgesIntoDeclarations:(OWCSSDeclarations *)declarations declarationIndex:(CSSDeclarationIndexes)declarationIndex fromTokenizer:(OWCSSTokenizer *)tokenizer leftDeclarationIndex:(CSSDeclarationIndexes)leftDeclarationIndex rightDeclarationIndex:(CSSDeclarationIndexes)rightDeclarationIndex topDeclarationIndex:(CSSDeclarationIndexes)topDeclarationIndex bottomDeclarationIndex:(CSSDeclarationIndexes)bottomDeclarationIndex;
{
    OWCSSTokenType tokenType;
    unsigned int edgeCount;
    BOOL unrecognizedTokensAfterEdges = YES, loopDone = NO;

    edgeCount = 0;
    while (edgeCount < 4 && !loopDone) {
        id nextToken;
        unichar character;
        
        tokenType = [tokenizer getNextToken:&nextToken];

        switch (tokenType) {
            case OWCSSTokenWhitespace:
                break;
                
            case OWCSSTokenIdentifier: // auto
                if (nextToken == OWCSSAutoIdentifier) {
                    // Apply the auto identifier to edges in the same way we would apply a <length> or <percentage>
                    // NO BREAK
                } else {
                    if (nextToken == OWCSSInheritIdentifier && edgeCount == 0) {
                        // If we have zero edges and the token is "inherit", then inherit is the setting for all sides
                        [declarations setObject:nextToken atIndex:topDeclarationIndex];
                        [declarations setObject:nextToken atIndex:rightDeclarationIndex];
                        [declarations setObject:nextToken atIndex:bottomDeclarationIndex];
                        [declarations setObject:nextToken atIndex:leftDeclarationIndex];
                    } else {
                        // This token has nothing to do with edges, put it back
                        [tokenizer ungetLastToken];
                    }
                    loopDone = YES;
                    break;
                }
            case OWCSSTokenNumber: // <length> | <percentage>
                switch (edgeCount) {
                    case 0: // top
                        [declarations setObject:nextToken atIndex:topDeclarationIndex];
                        break;
                    case 1: // right
                        [declarations setObject:nextToken atIndex:rightDeclarationIndex];
                        break;
                    case 2: // bottom
                        [declarations setObject:nextToken atIndex:bottomDeclarationIndex];
                        break;
                    case 3: // left
                        [declarations setObject:nextToken atIndex:leftDeclarationIndex];
                        break;
                    default:
                        break; // not reached
                }
                edgeCount++;
                break;
                
            case OWCSSTokenPunctuation:
                character = [nextToken characterAtIndex:0];
                if (character == ';' || character == '}')
                    unrecognizedTokensAfterEdges = NO;
                // fall through

            default:
                [tokenizer ungetLastToken];
                loopDone = YES;
                break;
        }
    }

    // This looks funky, but the deal here is if the user only gives one of the pairs of top|bottom or right|left, the latter will be copied from the former.  If only top is given (and no right) right is copied from top.  See beginning of this method for another explanation, from the CSS2 spec.
    switch (edgeCount) {
        default: // none
            break;
        case 1: // top only was specified, so set right based on top
            [declarations setObject:[declarations objectAtIndex:topDeclarationIndex] atIndex:rightDeclarationIndex];
            // fall through
        case 2: // top, right only were specified, so set bottom based on top
            [declarations setObject:[declarations objectAtIndex:topDeclarationIndex] atIndex:bottomDeclarationIndex];
            // fall through
        case 3: // top, right, bottom only were specified, so set left based on right
            [declarations setObject:[declarations objectAtIndex:rightDeclarationIndex] atIndex:leftDeclarationIndex];
            break;
    }
    
    return unrecognizedTokensAfterEdges;
}

+ (BOOL)_parseMarginIntoDeclarations:(OWCSSDeclarations *)declarations declarationIndex:(CSSDeclarationIndexes)declarationIndex fromTokenizer:(OWCSSTokenizer *)tokenizer;
{
    return [self _parseEdgesIntoDeclarations:declarations declarationIndex:declarationIndex fromTokenizer:tokenizer leftDeclarationIndex:CSSMarginLeftDeclarationIndex rightDeclarationIndex:CSSMarginRightDeclarationIndex topDeclarationIndex:CSSMarginTopDeclarationIndex bottomDeclarationIndex:CSSMarginBottomDeclarationIndex];
}

// padding: <padding-width> {1,4} | inherit
// Parse as margins are parsed.
+ (BOOL)_parsePaddingIntoDeclarations:(OWCSSDeclarations *)declarations declarationIndex:(CSSDeclarationIndexes)declarationIndex fromTokenizer:(OWCSSTokenizer *)tokenizer;
{
    [declarations setNeedsToStartABoxAtThisLevel];
    return [self _parseEdgesIntoDeclarations:declarations declarationIndex:declarationIndex fromTokenizer:tokenizer leftDeclarationIndex:CSSPaddingLeftDeclarationIndex rightDeclarationIndex:CSSPaddingRightDeclarationIndex topDeclarationIndex:CSSPaddingTopDeclarationIndex bottomDeclarationIndex:CSSPaddingBottomDeclarationIndex];
}

// text-decoration: none | [ underline || overline || line-through || blink ] | inherit
+ (BOOL)_parseTextDecorationsIntoDeclarations:(OWCSSDeclarations *)declarations declarationIndex:(CSSDeclarationIndexes)declarationIndex fromTokenizer:(OWCSSTokenizer *)tokenizer;
{
    OWCSSTokenType tokenType;
    id returnObject = nil;
    NSMutableArray *mutableDecorations = nil;
    BOOL returnValue = NO;

    while (1) {
        id nextToken;
        unichar character;
        
        tokenType = [tokenizer getNextToken:&nextToken];

        switch (tokenType) {
            case OWCSSTokenIdentifier:
                if (returnObject == nil) {
                    returnObject = nextToken;
                } else {
                    if (mutableDecorations == nil) {
                        mutableDecorations = [[[NSMutableArray alloc] init] autorelease];
                        [mutableDecorations addObject:returnObject];
                        returnObject = mutableDecorations;
                    }
                    [mutableDecorations addObject:nextToken];
                }
                break;
                
            case OWCSSTokenWhitespace:
                break;
                
            case OWCSSTokenPunctuation:
                character = [nextToken characterAtIndex:0];
                if (character == ';' || character == '}')
                    returnValue = YES;
                // fall through
            default:
                [tokenizer ungetLastToken];
                [declarations setObject:returnObject atIndex:declarationIndex];
                return returnValue;
        }
    }
    
    return YES; // NOT REACHED
}

+ (BOOL)_isValidClassSelector:(NSString *)classSelector;
{
    // CSS1 Test Suite 1.4 <http://www.w3.org/Style/CSS/Test/current/sec14.htm> says that class selectors cannot begin with digits
    if ([classSelector length] == 0)
        return YES;
    switch ([classSelector characterAtIndex:0]) {
        case '0':
        case '1':
        case '2':
        case '3':
        case '4':
        case '5':
        case '6':
        case '7':
        case '8':
        case '9':
            return NO;
        default:
            return YES;
    }
}

//

- (void)_parseStyleSheetFromScanner:(OFCharacterScanner *)aScanner parentContentInfo:(OWContentInfo *)parentContentInfo;
{
    OWCSSTokenizer *tokenizer;
    OWCSSTokenType tokenType;
    id tokenValue;
    BOOL importsAllowed = YES;

    if (aScanner == nil)
        return;

    tokenizer = [[OWCSSTokenizer alloc] initWithScanner:aScanner baseAddress:(OWAddress *)[parentContentInfo address]];
    [tokenizer autorelease];
        
    if (OWFDebugStyleSheetsLevel >= 4) {
        while ((tokenType = [tokenizer getNextToken:&tokenValue]) != OWCSSTokenEOF) {
            switch (tokenType) {
                case OWCSSTokenString:
                    NSLog(@"string <%@>", tokenValue);
                    break;
                case OWCSSTokenPunctuation:
                    NSLog(@"punctuation '%@'", tokenValue);
                    break;
                case OWCSSTokenWhitespace:
                    NSLog(@"whitespace", tokenValue);
                    break;
                case OWCSSTokenNumber:
                    NSLog(@"number %.1f%@", [tokenValue floatValue], [tokenValue unitsIdentifier] != nil ? [tokenValue unitsIdentifier] : (OWCSSIdentifier *)@"");
                    break;
                case OWCSSTokenFunction:
                    NSLog(@"function %@%@", [tokenValue objectAtIndex:0], [tokenValue subarrayWithRange:NSMakeRange(1, [tokenValue count]-1)]);
                    break;
                case OWCSSTokenIdentifier:
                    NSLog(@"%@", [tokenValue description]);
                    break;
                case OWCSSTokenEOF:
                    // NOT REACHED
                    break;
            }
        }
        return;
    }

    while (((tokenType = [tokenizer getNextToken:&tokenValue]) != OWCSSTokenEOF)) {
    
        if (tokenType == OWCSSTokenWhitespace)
            continue;
    
        // at-rule, including @import
        
        // From the CSS2 spec:
        // At-rules start with an at-keyword, an '@' character followed immediately by an identifier (for example, '@import', '@page').
        // An at-rule consists of everything up to and including the next semicolon (;) or the next block, whichever comes first. A CSS user agent that encounters an unrecognized at-rule must ignore the whole of the at-rule and continue parsing after it.
        if (tokenType == OWCSSTokenPunctuation && [tokenValue isEqualToString:@"@"]) {

            tokenType = [tokenizer getNextToken:&tokenValue];

            // @import
            if (tokenValue == OWCSSImportIdentifier) {
                // From the CSS2 spec:
                // The '@import' rule allows users to import style rules from other style sheets. Any @import rules must precede all rule sets in a style sheet. The '@import' keyword must be followed by the URI of the style sheet to include. A string is also allowed; it will be interpreted as if it had url(...) around it.
                //  The following lines are equivalent in meaning and illustrate both '@import' syntaxes (one with "url()" and one with a bare string):
                //    @import "mystyle.css";
                //    @import url("mystyle.css");
                // So that user agents can avoid retrieving resources for unsupported media types, authors may specify media-dependent @import rules. These conditional imports specify comma-separated media types after the URI.
                //    @import url("fineprint.css") print;
                //    @import url("bluish.css") projection, tv;
                // In the absence of any media types, the import is unconditional. Specifying 'all' for the medium has the same effect.

                if (OWFDebugStyleSheetsLevel) NSLog(@"Hey, got an import at-rule");
                
                if (importsAllowed) {
                    NSString *urlString = nil;
                    
                    tokenType = [tokenizer getNextToken:&tokenValue];
                    if (tokenType == OWCSSTokenWhitespace) // There will normally be a space after the @import statement...
                        tokenType = [tokenizer getNextToken:&tokenValue];
                    
                    if (tokenType == OWCSSTokenFunction) {
                        if ([tokenValue objectAtIndex:0] == OWCSSURLIdentifier)
                            urlString = [tokenValue objectAtIndex:1];
                    } else if (tokenType == OWCSSTokenString)
                        urlString = tokenValue;

                    if (OWFDebugStyleSheetsLevel) NSLog(@"urlString = '%@', go get it.", urlString);

#warning WJS 5/11/2001: We're currently ignoring media types, we'd parse them right here, then skip the next statement conditionally

                    [self parseStyleSheetFromURLString:urlString parentContentInfo:parentContentInfo];
                    
                } else { // Imports are NO LONGER allowed, because we've seen some declarations
                    if (OWFDebugStyleSheetsLevel) NSLog(@"Too bad imports aren't allowed now.");
                }
                
                [tokenizer skipTokensUpToAndIncludingPunctuation:@";"];

            // @notunderstood
            } else if (tokenType == OWCSSTokenString || tokenType == OWCSSTokenIdentifier) {
                BOOL importStatementFinished = NO;
                int openBraceCount = 0;
                
                if (OWFDebugStyleSheetsLevel) NSLog(@"Hey, got some unknown ATKEYWORD: %@\nI'll just skip it'", tokenValue);

                // From the CSS2 spec:
                // An at-rule consists of everything up to and including the next semicolon (;) or the next block, whichever comes first. A CSS user agent that encounters an unrecognized at-rule must ignore the whole of the at-rule and continue parsing after it.
                //A block starts with a left curly brace ({) and ends with the matching right curly brace (}). In between there may be any characters, except that parentheses ((Ê)), brackets ([Ê]) and braces ({Ê}) must always occur in matching pairs and may be nested. Single (') and double quotes (") must also occur in matching pairs, and characters between them are parsed as a string. See Tokenization above for the definition of a string.
                do {
                    tokenType = [tokenizer getNextToken:&tokenValue];
                    if (tokenType != OWCSSTokenPunctuation)
                        continue;
                        
                    if ([tokenValue isEqualToString:@";"] && openBraceCount == 0)
                        importStatementFinished = YES;
                    else if ([tokenValue isEqualToString:@"{"])
                        openBraceCount++;
                    else if ([tokenValue isEqualToString:@"}"] && --openBraceCount <= 0)
                        importStatementFinished = YES;

                } while (!importStatementFinished);
                
            // @ is followed by neither a string nor an identifier.  Huh?
            } else
                if (OWFDebugStyleSheetsLevel) NSLog(@"Hey, got an @ sign followed by invalid non-string: %@\nI'll just skip it'", tokenValue);


            continue;
            
        // declaration
        } else {
            OWCSSDeclarations *declarations;
            NSMutableArray *newGroups;
            unsigned int groupCount;

            [tokenizer ungetLastToken];
            newGroups = [NSMutableArray array];
            while (1) {
                OWCSSSelectorGroup *group;
                
                group = [[OWCSSSelectorGroup alloc] initWithTokenizer:tokenizer withPosition:currentPosition++];
                if (group == nil)
                    break;
                [newGroups addObject:group];
                [group release];
                
                tokenType = [tokenizer getNextToken:&tokenValue skipWhitespace:YES];
                if (OWFDebugStyleSheetsLevel) NSLog(@"Found token value %@ after selector group ", tokenValue);
                if (tokenType == OWCSSTokenPunctuation) {
                    unichar punctuation;
                    
                    punctuation = [tokenValue characterAtIndex:0];
                    if (punctuation == ',') {
                        continue;
                    } else if (punctuation == '{') {
                        [tokenizer ungetLastToken]; // Put bracket back
                        break;
                    }
                }
                    
                [tokenizer skipTokensUpToAndIncludingPunctuation:@"{"];
                [tokenizer ungetLastToken]; // Put bracket back
                break;
            }
            
            declarations = [[OWCSSDeclarations alloc] init];
            [isa _parseIntoDeclarations:declarations fromTokenizer:tokenizer];
            if (OWFDebugStyleSheetsLevel) NSLog(@"declaration dictionary: '%@'", declarations);
            
            importsAllowed = NO;
            
            // set declarations on groups 
            [newGroups makeObjectsPerformSelector:@selector(setDeclarations:) withObject:declarations];
            groupCount = [newGroups count];
            while (groupCount--)
                [self _addSelectorGroup:[newGroups objectAtIndex:groupCount]];
            [declarations release];
        }
    }
}


// Tags and pseudoclasses are NOT case sensitive, classes and IDs are.
- (void)_addSelectorGroup:(OWCSSSelectorGroup *)group;
{
    OWCSSSelector *lastSelector = [group lastSelector];
    NSString *key;
    NSMutableArray *list;
    NSMutableDictionary *selectorsDictionary;
    
    if (OWFDebugStyleSheetsLevel) NSLog(@"group = '%@'", group);
    if ((key = [lastSelector pseudoClassName])) {
        if (OWFDebugStyleSheetsLevel) NSLog(@"adding '%@' to the pseudoclass list", key);

        if (lastSelectorsByPseudoclass == nil)
            lastSelectorsByPseudoclass = OFCreateCaseInsensitiveKeyMutableDictionary();

        selectorsDictionary = lastSelectorsByPseudoclass;
    } else if ((key = [lastSelector tagName])) {
        if (OWFDebugStyleSheetsLevel) NSLog(@"adding '%@' to the tag list", key);

        if (lastSelectorsByTag == nil)
            lastSelectorsByTag = OFCreateCaseInsensitiveKeyMutableDictionary();

        selectorsDictionary = lastSelectorsByTag;
    } else if ((key = [lastSelector className])) {
        if (OWFDebugStyleSheetsLevel) NSLog(@"adding '%@' to the class list", key);

        if (lastSelectorsByClass == nil)
            lastSelectorsByClass = [[NSMutableDictionary alloc] init];

        selectorsDictionary = lastSelectorsByClass;
    } else if ((key = [lastSelector idName])) {
        if (OWFDebugStyleSheetsLevel) NSLog(@"adding '%@' to the ID list", key);

        if (lastSelectorsByID == nil)
            lastSelectorsByID = [[NSMutableDictionary alloc] init];

        selectorsDictionary = lastSelectorsByID;
    } else {
        if (OWFDebugStyleSheetsLevel) NSLog(@"I see an empty selector, this must be a global style sheet.");
        
        [globalDeclarations addEntriesFromDeclarations:[group declarations]];
        
        return;
    }

    list = [selectorsDictionary objectForKey:key];
    if (list == nil) {
        list = [NSMutableArray array];
        [selectorsDictionary setObject:list forKey:key];
    }
    [list addObject:group];
    [list sortUsingSelector:@selector(compare:)];
    
    if (OWFDebugStyleSheetsLevel) NSLog(@"list = '%@'", list);
}

- (void)_addDeclarations:(OWCSSDeclarations *)declarations inDictionary:(NSDictionary *)dictionary forName:(NSString *)name tag:(OWSGMLTag *)tag tagStack:(OFStaticArray *)tagStack;
{
    NSArray *matchingGroups;
    unsigned int groupIndex, groupCount;

    matchingGroups = [dictionary objectForKey:name];
    if (OWFDebugStyleSheetsLevel >= 3) {
        NSMutableArray *tagNames;
        unsigned int tagIndex, tagCount;

        tagCount = [tagStack count];
        tagNames = [[NSMutableArray alloc] initWithCapacity:[tagStack count]];
        for (tagIndex = 0; tagIndex < tagCount; tagIndex++) {
            [tagNames addObject:[(OWSGMLTag *)[[tagStack objectAtIndex:tagIndex] tag] name]];
        }
        NSLog(@"-_addDeclarations:inDictionary:%@ forName:'%@' tag:<%@> tagStack:%@\nfound matching groups %@", [dictionary allKeys], name, [tag name], tagNames, matchingGroups);
        [tagNames release];
    } else if (OWFDebugStyleSheetsLevel >= 2 && matchingGroups)
        NSLog(@"-_addDeclarations:inDictionary:[skip] forName:'%@' tag:<%@> tagStack:[skip]\nfound matching groups %@", name, [tag name], matchingGroups);

    if (!matchingGroups)
        return;
        
    groupCount = [matchingGroups count];
    for (groupIndex = 0; groupIndex < groupCount; groupIndex++) {
        OWCSSSelectorGroup *group;
        OWCSSSelector *lastSelector;

        group = [matchingGroups objectAtIndex:groupIndex];
        lastSelector = [group lastSelector];

        if ([lastSelector matchesTag:tag] && (([group count] == 0) || [self _checkTagStack:tagStack forSelectorGroup:group])) {
            [declarations addEntriesFromDeclarations:[group declarations]];
        }
    }
}

- (BOOL)_checkTagStack:(OFStaticArray *)tagStack forSelectorGroup:(OWCSSSelectorGroup *)group;
{
    NSArray *selectorList = [group selectorList];
    unsigned int selectorIndex, tagIndex;
    
    // Skip the first one, since if we're here it matches the last tag.
    selectorIndex = [selectorList count];
    if (selectorIndex-- == 0)
        return NO;
    tagIndex = [tagStack count];
    
    while (selectorIndex--) {
        OWCSSSelector *selector;
        NSString *tagName;
        NSString *className;
        NSString *idName;
        BOOL stillChecking = YES;
        
        selector = [selectorList objectAtIndex:selectorIndex];
        tagName = [selector tagName];
        className = [selector className];
        idName = [selector idName];
        
        while (stillChecking && tagIndex--) {
            OWSGMLTag *tag;
            
            tag = [[tagStack objectAtIndex:tagIndex] tag];
            
            if (tagName) {
                stillChecking = ! [tagName isEqualToString:[tag name]];

                if (OWFDebugStyleSheetsLevel) NSLog(@"looking for '%@', got '%@'", tagName, [tag name]);
            }
            if (className) {
                stillChecking = ! [className isEqualToString:[tag valueForAttribute:@"class"]];
            }
            if (idName) {
                stillChecking = ! [idName isEqualToString:[tag valueForAttribute:@"id"]];
            }
        }
        if (stillChecking)
            return NO;
    }
    return YES;
}

@end
