mirror of
https://github.com/wu736139669/ManagerDemo.git
synced 2026-08-05 05:55:42 +00:00
954 lines
24 KiB
Objective-C
954 lines
24 KiB
Objective-C
//
|
|
// DTCSSStylesheet.m
|
|
// DTCoreText
|
|
//
|
|
// Created by Oliver Drobnik on 9/5/11.
|
|
// Copyright (c) 2011 Drobnik.com. All rights reserved.
|
|
//
|
|
|
|
#import "DTCSSStylesheet.h"
|
|
#import "DTCSSListStyle.h"
|
|
|
|
#import "DTHTMLElement.h"
|
|
#import "NSScanner+HTML.h"
|
|
#import "NSString+CSS.h"
|
|
#import "NSString+HTML.h"
|
|
|
|
|
|
// external symbols generated via custom build rule and xxd
|
|
extern unsigned char default_css[];
|
|
extern unsigned int default_css_len;
|
|
|
|
|
|
@implementation DTCSSStylesheet
|
|
{
|
|
NSMutableDictionary *_styles;
|
|
NSMutableDictionary *_orderedSelectorWeights;
|
|
NSMutableArray *_orderedSelectors;
|
|
}
|
|
|
|
#pragma mark Creating Stylesheets
|
|
|
|
+ (DTCSSStylesheet *)defaultStyleSheet
|
|
{
|
|
static DTCSSStylesheet *defaultDTCSSStylesheet = nil;
|
|
if (defaultDTCSSStylesheet)
|
|
{
|
|
return defaultDTCSSStylesheet;
|
|
}
|
|
|
|
@synchronized(self)
|
|
{
|
|
if (!defaultDTCSSStylesheet)
|
|
{
|
|
// get the data from the external symbol
|
|
NSData *data = [NSData dataWithBytes:default_css length:default_css_len];
|
|
NSString *cssString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
|
|
|
|
defaultDTCSSStylesheet = [[DTCSSStylesheet alloc] initWithStyleBlock:cssString];
|
|
}
|
|
}
|
|
return defaultDTCSSStylesheet;
|
|
}
|
|
|
|
- (id)initWithStyleBlock:(NSString *)css
|
|
{
|
|
self = [super init];
|
|
|
|
if (self)
|
|
{
|
|
_styles = [[NSMutableDictionary alloc] init];
|
|
_orderedSelectorWeights = [[NSMutableDictionary alloc] init];
|
|
_orderedSelectors = [[NSMutableArray alloc] init];
|
|
|
|
[self parseStyleBlock:css];
|
|
}
|
|
|
|
return self;
|
|
}
|
|
|
|
- (id)initWithStylesheet:(DTCSSStylesheet *)stylesheet
|
|
{
|
|
self = [super init];
|
|
|
|
if (self)
|
|
{
|
|
_styles = [[NSMutableDictionary alloc] init];
|
|
_orderedSelectorWeights = [[NSMutableDictionary alloc] init];
|
|
_orderedSelectors = [[NSMutableArray alloc] init];
|
|
|
|
[self mergeStylesheet:stylesheet];
|
|
}
|
|
|
|
return self;
|
|
}
|
|
|
|
#ifndef COVERAGE
|
|
|
|
- (NSString *)description
|
|
{
|
|
return [_styles description];
|
|
|
|
}
|
|
#endif
|
|
|
|
#pragma mark Working with Style Blocks
|
|
|
|
- (void)_uncompressShorthands:(NSMutableDictionary *)styles
|
|
{
|
|
// list-style shorthand
|
|
NSString *shortHand = [[styles objectForKey:@"list-style"] lowercaseString];
|
|
|
|
if (shortHand)
|
|
{
|
|
[styles removeObjectForKey:@"list-style"];
|
|
|
|
if ([shortHand isEqualToString:@"inherit"])
|
|
{
|
|
[styles setObject:@"inherit" forKey:@"list-style-type"];
|
|
[styles setObject:@"inherit" forKey:@"list-style-position"];
|
|
return;
|
|
}
|
|
|
|
NSArray *components = [shortHand componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
|
|
|
|
BOOL typeWasSet = NO;
|
|
BOOL positionWasSet = NO;
|
|
|
|
DTCSSListStyleType listStyleType = DTCSSListStyleTypeNone;
|
|
DTCSSListStylePosition listStylePosition = DTCSSListStylePositionInherit;
|
|
|
|
for (NSString *oneComponent in components)
|
|
{
|
|
if ([oneComponent hasPrefix:@"url"])
|
|
{
|
|
// list-style-image
|
|
NSScanner *scanner = [NSScanner scannerWithString:oneComponent];
|
|
|
|
if ([scanner scanCSSURL:NULL])
|
|
{
|
|
[styles setObject:oneComponent forKey:@"list-style-image"];
|
|
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (!typeWasSet)
|
|
{
|
|
// check if valid type
|
|
listStyleType = [DTCSSListStyle listStyleTypeFromString:oneComponent];
|
|
|
|
if (listStyleType != DTCSSListStyleTypeInvalid)
|
|
{
|
|
[styles setObject:oneComponent forKey:@"list-style-type"];
|
|
|
|
typeWasSet = YES;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (!positionWasSet)
|
|
{
|
|
// check if valid position
|
|
listStylePosition = [DTCSSListStyle listStylePositionFromString:oneComponent];
|
|
|
|
if (listStylePosition != DTCSSListStylePositionInvalid)
|
|
{
|
|
[styles setObject:oneComponent forKey:@"list-style-position"];
|
|
|
|
positionWasSet = YES;
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// font shorthand, see http://www.w3.org/TR/CSS21/fonts.html#font-shorthand
|
|
shortHand = [styles objectForKey:@"font"];
|
|
|
|
if (shortHand)
|
|
{
|
|
NSString *fontStyle = @"normal";
|
|
NSArray *validFontStyles = [NSArray arrayWithObjects:@"italic", @"oblique", nil];
|
|
|
|
NSString *fontVariant = @"normal";
|
|
NSArray *validFontVariants = [NSArray arrayWithObjects:@"small-caps", nil];
|
|
BOOL fontVariantSet = NO;
|
|
|
|
NSString *fontWeight = @"normal";
|
|
NSArray *validFontWeights = [NSArray arrayWithObjects:@"bold", @"bolder", @"lighter", @"100", @"200", @"300", @"400", @"500", @"600", @"700", @"800", @"900", nil];
|
|
BOOL fontWeightSet = NO;
|
|
|
|
NSString *fontSize = @"normal";
|
|
NSArray *validFontSizes = [NSArray arrayWithObjects:@"xx-small", @"x-small", @"small", @"medium", @"large", @"x-large", @"xx-large", @"larger", @"smaller", nil];
|
|
BOOL fontSizeSet = NO;
|
|
|
|
NSArray *suffixesToIgnore = [NSArray arrayWithObjects:@"caption", @"icon", @"menu", @"message-box", @"small-caption", @"status-bar", @"inherit", nil];
|
|
|
|
NSString *lineHeight = @"normal";
|
|
|
|
NSMutableString *fontFamily = [NSMutableString string];
|
|
|
|
NSArray *components = [shortHand componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
|
|
|
|
for (NSString *oneComponent in components)
|
|
{
|
|
// try font size keywords
|
|
if ([validFontSizes containsObject:oneComponent])
|
|
{
|
|
fontSize = oneComponent;
|
|
fontSizeSet = YES;
|
|
|
|
continue;
|
|
}
|
|
|
|
NSInteger slashIndex = [oneComponent rangeOfString:@"/"].location;
|
|
|
|
if (slashIndex != NSNotFound)
|
|
{
|
|
// font-size / line-height
|
|
|
|
fontSize = [oneComponent substringToIndex:slashIndex];
|
|
fontSizeSet = YES;
|
|
|
|
lineHeight = [oneComponent substringFromIndex:slashIndex+1];
|
|
|
|
continue;
|
|
}
|
|
else
|
|
{
|
|
// length
|
|
if ([oneComponent hasSuffix:@"%"] || [oneComponent hasSuffix:@"em"] || [oneComponent hasSuffix:@"px"] || [oneComponent hasSuffix:@"pt"])
|
|
{
|
|
fontSize = oneComponent;
|
|
fontSizeSet = YES;
|
|
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (fontSizeSet)
|
|
{
|
|
if ([suffixesToIgnore containsObject:oneComponent])
|
|
{
|
|
break;
|
|
}
|
|
|
|
// assume that this is part of font family
|
|
if ([fontFamily length])
|
|
{
|
|
[fontFamily appendString:@" "];
|
|
}
|
|
|
|
[fontFamily appendString:oneComponent];
|
|
}
|
|
else
|
|
{
|
|
if (!fontWeightSet && [validFontStyles containsObject:oneComponent])
|
|
{
|
|
fontStyle = oneComponent;
|
|
}
|
|
else if (!fontVariantSet && [validFontVariants containsObject:oneComponent])
|
|
{
|
|
fontVariant = oneComponent;
|
|
fontVariantSet = YES;
|
|
}
|
|
else if (!fontWeightSet && [validFontWeights containsObject:oneComponent])
|
|
{
|
|
fontWeight = oneComponent;
|
|
fontWeightSet = YES;
|
|
}
|
|
}
|
|
}
|
|
|
|
[styles removeObjectForKey:@"font"];
|
|
|
|
// size and family are mandatory, without them this is invalid
|
|
if ([fontSize length] && [fontFamily length])
|
|
{
|
|
[styles setObject:fontStyle forKey:@"font-style"];
|
|
[styles setObject:fontWeight forKey:@"font-weight"];
|
|
[styles setObject:fontVariant forKey:@"font-variant"];
|
|
[styles setObject:fontSize forKey:@"font-size"];
|
|
[styles setObject:lineHeight forKey:@"line-height"];
|
|
[styles setObject:fontFamily forKey:@"font-family"];
|
|
}
|
|
}
|
|
|
|
shortHand = [styles objectForKey:@"margin"];
|
|
|
|
if (shortHand)
|
|
{
|
|
NSArray *parts = [shortHand componentsSeparatedByString:@" "];
|
|
|
|
NSString *topMargin;
|
|
NSString *rightMargin;
|
|
NSString *bottomMargin;
|
|
NSString *leftMargin;
|
|
|
|
if ([parts count] == 4)
|
|
{
|
|
topMargin = [parts objectAtIndex:0];
|
|
rightMargin = [parts objectAtIndex:1];
|
|
bottomMargin = [parts objectAtIndex:2];
|
|
leftMargin = [parts objectAtIndex:3];
|
|
}
|
|
else if ([parts count] == 3)
|
|
{
|
|
topMargin = [parts objectAtIndex:0];
|
|
rightMargin = [parts objectAtIndex:1];
|
|
bottomMargin = [parts objectAtIndex:2];
|
|
leftMargin = [parts objectAtIndex:1];
|
|
}
|
|
else if ([parts count] == 2)
|
|
{
|
|
topMargin = [parts objectAtIndex:0];
|
|
rightMargin = [parts objectAtIndex:1];
|
|
bottomMargin = [parts objectAtIndex:0];
|
|
leftMargin = [parts objectAtIndex:1];
|
|
}
|
|
else
|
|
{
|
|
NSString *onlyValue = [parts objectAtIndex:0];
|
|
|
|
topMargin = onlyValue;
|
|
rightMargin = onlyValue;
|
|
bottomMargin = onlyValue;
|
|
leftMargin = onlyValue;
|
|
}
|
|
|
|
// only apply the ones where there is no previous direct setting
|
|
|
|
if (![styles objectForKey:@"margin-top"])
|
|
{
|
|
[styles setObject:topMargin forKey:@"margin-top"];
|
|
}
|
|
|
|
if (![styles objectForKey:@"margin-right"])
|
|
{
|
|
[styles setObject:rightMargin forKey:@"margin-right"];
|
|
}
|
|
|
|
if (![styles objectForKey:@"margin-bottom"])
|
|
{
|
|
[styles setObject:bottomMargin forKey:@"margin-bottom"];
|
|
}
|
|
|
|
if (![styles objectForKey:@"margin-left"])
|
|
{
|
|
[styles setObject:leftMargin forKey:@"margin-left"];
|
|
}
|
|
|
|
// remove the shorthand
|
|
[styles removeObjectForKey:@"margin"];
|
|
}
|
|
|
|
shortHand = [styles objectForKey:@"padding"];
|
|
|
|
if (shortHand)
|
|
{
|
|
NSArray *parts = [shortHand componentsSeparatedByString:@" "];
|
|
|
|
NSString *topPadding;
|
|
NSString *rightPadding;
|
|
NSString *bottomPadding;
|
|
NSString *leftPadding;
|
|
|
|
if ([parts count] == 4)
|
|
{
|
|
topPadding = [parts objectAtIndex:0];
|
|
rightPadding = [parts objectAtIndex:1];
|
|
bottomPadding = [parts objectAtIndex:2];
|
|
leftPadding = [parts objectAtIndex:3];
|
|
}
|
|
else if ([parts count] == 3)
|
|
{
|
|
topPadding = [parts objectAtIndex:0];
|
|
rightPadding = [parts objectAtIndex:1];
|
|
bottomPadding = [parts objectAtIndex:2];
|
|
leftPadding = [parts objectAtIndex:1];
|
|
}
|
|
else if ([parts count] == 2)
|
|
{
|
|
topPadding = [parts objectAtIndex:0];
|
|
rightPadding = [parts objectAtIndex:1];
|
|
bottomPadding = [parts objectAtIndex:0];
|
|
leftPadding = [parts objectAtIndex:1];
|
|
}
|
|
else
|
|
{
|
|
NSString *onlyValue = [parts objectAtIndex:0];
|
|
|
|
topPadding = onlyValue;
|
|
rightPadding = onlyValue;
|
|
bottomPadding = onlyValue;
|
|
leftPadding = onlyValue;
|
|
}
|
|
|
|
// only apply the ones where there is no previous direct setting
|
|
|
|
if (![styles objectForKey:@"padding-top"])
|
|
{
|
|
[styles setObject:topPadding forKey:@"padding-top"];
|
|
}
|
|
|
|
if (![styles objectForKey:@"padding-right"])
|
|
{
|
|
[styles setObject:rightPadding forKey:@"padding-right"];
|
|
}
|
|
|
|
if (![styles objectForKey:@"padding-bottom"])
|
|
{
|
|
[styles setObject:bottomPadding forKey:@"padding-bottom"];
|
|
}
|
|
|
|
if (![styles objectForKey:@"padding-left"])
|
|
{
|
|
[styles setObject:leftPadding forKey:@"padding-left"];
|
|
}
|
|
|
|
// remove the shorthand
|
|
[styles removeObjectForKey:@"padding"];
|
|
}
|
|
|
|
shortHand = [styles objectForKey:@"background"];
|
|
|
|
if (shortHand)
|
|
{
|
|
// ignore most tokens except background-color
|
|
|
|
[styles removeObjectForKey:@"background"];
|
|
|
|
NSCharacterSet *tokenDelimiters = [NSCharacterSet whitespaceAndNewlineCharacterSet];
|
|
NSString *trimmedString = [shortHand stringByTrimmingCharactersInSet:tokenDelimiters];
|
|
NSScanner *scanner = [NSScanner scannerWithString:trimmedString];
|
|
|
|
while (![scanner isAtEnd])
|
|
{
|
|
NSString *colorName;
|
|
if ([scanner scanHTMLColor:NULL HTMLName:&colorName])
|
|
{
|
|
[styles setObject:colorName forKey:@"background-color"];
|
|
break;
|
|
}
|
|
[scanner scanUpToCharactersFromSet:tokenDelimiters intoString:NULL];
|
|
}
|
|
}
|
|
}
|
|
|
|
- (void)_addStyleRule:(NSString *)rule withSelector:(NSString*)selectors
|
|
{
|
|
NSArray *split = [selectors componentsSeparatedByString:@","];
|
|
|
|
for (NSString *selector in split)
|
|
{
|
|
NSString *cleanSelector = [selector stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
|
|
|
|
NSMutableDictionary *ruleDictionary = [[rule dictionaryOfCSSStyles] mutableCopy];
|
|
|
|
// remove !important, we're ignoring these
|
|
for (NSString *oneKey in [ruleDictionary allKeys])
|
|
{
|
|
id value = [ruleDictionary objectForKey:oneKey];
|
|
if ([value isKindOfClass:[NSString class]])
|
|
{
|
|
NSRange rangeOfImportant = [value rangeOfString:@"!important" options:NSCaseInsensitiveSearch];
|
|
|
|
if (rangeOfImportant.location != NSNotFound)
|
|
{
|
|
value = [value stringByReplacingCharactersInRange:rangeOfImportant withString:@""];
|
|
value = [value stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
|
|
|
|
[ruleDictionary setObject:value forKey:oneKey];
|
|
}
|
|
|
|
} else if ([value isKindOfClass:[NSArray class]])
|
|
{
|
|
NSMutableArray *newVal;
|
|
|
|
for (NSUInteger i = 0; i < [(NSArray*)value count]; ++i)
|
|
{
|
|
NSString *s = [value objectAtIndex:i];
|
|
|
|
NSRange rangeOfImportant = [s rangeOfString:@"!important" options:NSCaseInsensitiveSearch];
|
|
|
|
if (rangeOfImportant.location != NSNotFound)
|
|
{
|
|
s = [s stringByReplacingCharactersInRange:rangeOfImportant withString:@""];
|
|
s = [s stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
|
|
|
|
if (!newVal)
|
|
{
|
|
if ([value isKindOfClass:[NSMutableArray class]])
|
|
{
|
|
newVal = value;
|
|
} else
|
|
{
|
|
newVal = [value mutableCopy];
|
|
}
|
|
}
|
|
|
|
// replace the value that had !important with a version without it
|
|
[newVal replaceObjectAtIndex:i withObject:s];
|
|
}
|
|
}
|
|
|
|
if (newVal)
|
|
{
|
|
[ruleDictionary setObject:newVal forKey:oneKey];
|
|
}
|
|
}
|
|
}
|
|
|
|
// need to uncompress because otherwise we might get shorthands and non-shorthands together
|
|
[self _uncompressShorthands:ruleDictionary];
|
|
|
|
// check if there is a pseudo selector
|
|
NSRange colonRange = [cleanSelector rangeOfString:@":"];
|
|
NSString *pseudoSelector = nil;
|
|
|
|
if (colonRange.length==1)
|
|
{
|
|
pseudoSelector = [cleanSelector substringFromIndex:colonRange.location+1];
|
|
cleanSelector = [cleanSelector substringToIndex:colonRange.location];
|
|
|
|
// prefix all rules with the pseudo-selector
|
|
for (NSString *oneRuleKey in [ruleDictionary allKeys])
|
|
{
|
|
id value = [ruleDictionary objectForKey:oneRuleKey];
|
|
|
|
// prefix key with the pseudo selector
|
|
NSString *prefixedKey = [NSString stringWithFormat:@"%@:%@", pseudoSelector, oneRuleKey];
|
|
[ruleDictionary setObject:value forKey:prefixedKey];
|
|
[ruleDictionary removeObjectForKey:oneRuleKey];
|
|
}
|
|
}
|
|
|
|
NSDictionary *existingRulesForSelector = [_styles objectForKey:cleanSelector];
|
|
|
|
if (existingRulesForSelector)
|
|
{
|
|
// substitute new rules over old ones
|
|
NSMutableDictionary *tmpDict = [existingRulesForSelector mutableCopy];
|
|
|
|
// append new rules
|
|
[tmpDict addEntriesFromDictionary:ruleDictionary];
|
|
|
|
// save it
|
|
[self _addStyles:tmpDict withSelector:cleanSelector];
|
|
}
|
|
else
|
|
{
|
|
[self _addStyles:ruleDictionary withSelector:cleanSelector];
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
- (void)parseStyleBlock:(NSString*)css
|
|
{
|
|
NSUInteger braceLevel = 0, braceMarker = 0;
|
|
|
|
NSString* selector;
|
|
|
|
NSUInteger length = [css length];
|
|
|
|
for (NSUInteger i = 0; i < length; i++)
|
|
{
|
|
unichar c = [css characterAtIndex:i];
|
|
|
|
if (c == '/')
|
|
{
|
|
i++;
|
|
|
|
if (i < length)
|
|
{
|
|
c = [css characterAtIndex:i];
|
|
|
|
if (c == '*')
|
|
{
|
|
// skip comment until closing /
|
|
|
|
for (; i < length; i++)
|
|
{
|
|
if ([css characterAtIndex:i] == '/')
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (i < length)
|
|
{
|
|
braceMarker = i+1;
|
|
continue;
|
|
}
|
|
else
|
|
{
|
|
// end of string
|
|
return;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// not a comment
|
|
i--;
|
|
}
|
|
}
|
|
}
|
|
|
|
// An opening brace! It could be the start of a new rule, or it could be a nested brace.
|
|
if (c == '{')
|
|
{
|
|
// If we start a new rule...
|
|
|
|
if (braceLevel == 0)
|
|
{
|
|
// Grab the selector and clean up extraneous spaces (we'll process it in a moment)
|
|
selector = [css substringWithRange:NSMakeRange(braceMarker, i-braceMarker)];
|
|
NSArray *selectorParts = [selector componentsSeparatedByString:@" "];
|
|
NSMutableArray *cleanSelectorParts = [NSMutableArray array];
|
|
for (NSString *partialSelector in selectorParts)
|
|
{
|
|
if (partialSelector.length)
|
|
{
|
|
[cleanSelectorParts addObject:partialSelector];
|
|
}
|
|
}
|
|
selector = [cleanSelectorParts componentsJoinedByString:@" "];
|
|
|
|
// And mark our position so we can grab the rule's CSS when it is closed
|
|
braceMarker = i + 1;
|
|
}
|
|
|
|
// Increase the brace level.
|
|
braceLevel += 1;
|
|
}
|
|
|
|
// A closing brace!
|
|
else if (c == '}')
|
|
{
|
|
// If we finished a rule...
|
|
if (braceLevel == 1)
|
|
{
|
|
NSString *rule = [css substringWithRange:NSMakeRange(braceMarker, i-braceMarker)];
|
|
|
|
[self _addStyleRule:rule withSelector: selector];
|
|
|
|
braceMarker = i + 1;
|
|
}
|
|
|
|
braceLevel = MAX(braceLevel-1, 0ul);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
- (void)mergeStylesheet:(DTCSSStylesheet *)stylesheet
|
|
{
|
|
NSArray *otherStylesheetStyleKeys = stylesheet.orderedSelectors;
|
|
|
|
for (NSString *oneKey in otherStylesheetStyleKeys)
|
|
{
|
|
NSDictionary *existingStyles = [_styles objectForKey:oneKey];
|
|
NSDictionary *stylesToMerge = [[stylesheet styles] objectForKey:oneKey];
|
|
if (existingStyles)
|
|
{
|
|
NSMutableDictionary *mutableStyles = [existingStyles mutableCopy];
|
|
|
|
for (NSString *oneStyleKey in stylesToMerge)
|
|
{
|
|
NSString *mergingStyleString = [stylesToMerge objectForKey:oneStyleKey];
|
|
|
|
[mutableStyles setObject:mergingStyleString forKey:oneStyleKey];
|
|
}
|
|
|
|
[self _addStyles:mutableStyles withSelector:oneKey];
|
|
}
|
|
else
|
|
{
|
|
// nothing to worry
|
|
[self _addStyles:stylesToMerge withSelector:oneKey];
|
|
}
|
|
}
|
|
}
|
|
|
|
- (void)_addStyles:(NSDictionary *)styles withSelector:(NSString *)selector {
|
|
[_styles setObject:styles forKey:selector];
|
|
|
|
if (![_orderedSelectors containsObject:selector])
|
|
{
|
|
[_orderedSelectors addObject:selector];
|
|
[_orderedSelectorWeights setObject:@([self _weightForSelector:selector]) forKey:selector];
|
|
}
|
|
}
|
|
|
|
#pragma mark Accessing Style Information
|
|
|
|
- (NSDictionary *)mergedStyleDictionaryForElement:(DTHTMLElement *)element matchedSelectors:(NSSet * __autoreleasing*)matchedSelectors ignoreInlineStyle:(BOOL)ignoreInlineStyle
|
|
{
|
|
// We are going to combine all the relevant styles for this tag.
|
|
// (Note that when styles are applied, the later styles take precedence,
|
|
// so the order in which we grab them matters!)
|
|
|
|
NSMutableDictionary *tmpDict = [NSMutableDictionary dictionary];
|
|
|
|
// Get based on element
|
|
NSDictionary *byTagName = [self.styles objectForKey:element.name];
|
|
|
|
if (byTagName)
|
|
{
|
|
[tmpDict addEntriesFromDictionary:byTagName];
|
|
}
|
|
|
|
// Get based on class(es)
|
|
NSString *classString = [element.attributes objectForKey:@"class"];
|
|
NSArray *classes = [classString componentsSeparatedByString:@" "];
|
|
|
|
// Cascaded selectors with more than one part are sorted by specificity
|
|
NSMutableArray *matchingCascadingSelectors = [self matchingComplexCascadingSelectorsForElement:element];
|
|
[matchingCascadingSelectors sortUsingComparator:^NSComparisonResult(NSString *selector1, NSString *selector2)
|
|
{
|
|
NSInteger weightForSelector1 = [[_orderedSelectorWeights objectForKey:selector1] integerValue];
|
|
NSInteger weightForSelector2 = [[_orderedSelectorWeights objectForKey:selector2] integerValue];
|
|
|
|
if (weightForSelector1 == weightForSelector2)
|
|
{
|
|
weightForSelector1 += [_orderedSelectors indexOfObject:selector1];
|
|
weightForSelector2 += [_orderedSelectors indexOfObject:selector2];
|
|
}
|
|
|
|
if (weightForSelector1 > weightForSelector2)
|
|
{
|
|
return (NSComparisonResult)NSOrderedDescending;
|
|
}
|
|
|
|
if (weightForSelector1 < weightForSelector2)
|
|
{
|
|
return (NSComparisonResult)NSOrderedAscending;
|
|
}
|
|
|
|
return (NSComparisonResult)NSOrderedSame;
|
|
}];
|
|
|
|
NSMutableSet *tmpMatchedSelectors;
|
|
|
|
if (matchedSelectors)
|
|
{
|
|
tmpMatchedSelectors = [NSMutableSet set];
|
|
}
|
|
|
|
// Apply complex cascading selectors first, then apply most specific selectors
|
|
for (NSString *cascadingSelector in matchingCascadingSelectors)
|
|
{
|
|
NSDictionary *byCascadingSelector = [_styles objectForKey:cascadingSelector];
|
|
[tmpDict addEntriesFromDictionary:byCascadingSelector];
|
|
[tmpMatchedSelectors addObject:cascadingSelector];
|
|
}
|
|
|
|
// Applied the parameter element's classes last
|
|
for (NSString *class in classes)
|
|
{
|
|
NSString *classRule = [NSString stringWithFormat:@".%@", class];
|
|
NSDictionary *byClass = [_styles objectForKey: classRule];
|
|
|
|
if (byClass)
|
|
{
|
|
[tmpDict addEntriesFromDictionary:byClass];
|
|
[tmpMatchedSelectors addObject:class];
|
|
}
|
|
|
|
NSString *classAndTagRule = [NSString stringWithFormat:@"%@.%@", element.name, class];
|
|
NSDictionary *byClassAndName = [_styles objectForKey:classAndTagRule];
|
|
|
|
if (byClassAndName)
|
|
{
|
|
[tmpDict addEntriesFromDictionary:byClassAndName];
|
|
[tmpMatchedSelectors addObject:classAndTagRule];
|
|
}
|
|
}
|
|
|
|
// Get based on id
|
|
NSString *idRule = [NSString stringWithFormat:@"#%@", [element.attributes objectForKey:@"id"]];
|
|
NSDictionary *byID = [_styles objectForKey:idRule];
|
|
|
|
if (byID)
|
|
{
|
|
[tmpDict addEntriesFromDictionary:byID];
|
|
[tmpMatchedSelectors addObject:idRule];
|
|
}
|
|
|
|
if (!ignoreInlineStyle)
|
|
{
|
|
// Get tag's local style attribute
|
|
NSString *styleString = [element.attributes objectForKey:@"style"];
|
|
|
|
if ([styleString length])
|
|
{
|
|
NSMutableDictionary *localStyles = [[styleString dictionaryOfCSSStyles] mutableCopy];
|
|
|
|
// need to uncompress because otherwise we might get shorthands and non-shorthands together
|
|
[self _uncompressShorthands:localStyles];
|
|
|
|
[tmpDict addEntriesFromDictionary:localStyles];
|
|
}
|
|
}
|
|
|
|
if ([tmpDict count])
|
|
{
|
|
if (matchedSelectors && [tmpMatchedSelectors count])
|
|
{
|
|
*matchedSelectors = [tmpMatchedSelectors copy];
|
|
}
|
|
|
|
return tmpDict;
|
|
}
|
|
else
|
|
{
|
|
return nil;
|
|
}
|
|
}
|
|
|
|
- (NSDictionary *)styles
|
|
{
|
|
return _styles;
|
|
}
|
|
|
|
- (NSArray *)orderedSelectors
|
|
{
|
|
return _orderedSelectors;
|
|
}
|
|
|
|
// This looks for cascaded selectors with more than one part to them
|
|
- (NSMutableArray *)matchingComplexCascadingSelectorsForElement:(DTHTMLElement *)element
|
|
{
|
|
__block NSMutableArray *matchedSelectors = [NSMutableArray array];
|
|
|
|
for (NSString *selector in _orderedSelectors)
|
|
{
|
|
NSArray *selectorParts = [selector componentsSeparatedByString:@" "];
|
|
|
|
// We only process the selector if our selector has more than 1 part to it (e.g. ".foo" would be skipped and ".foo .bar" would not)
|
|
if (selectorParts.count < 2)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
DTHTMLElement *nextElement = element;
|
|
|
|
// Walking up the hierarchy so start at the right side of the selector and work to the left
|
|
// Aside: Manual for loop here is faster than for in with reverseObjectEnumerator
|
|
for (NSUInteger j = selectorParts.count; j-- > 0;)
|
|
{
|
|
NSString *selectorPart = [selectorParts objectAtIndex:j];
|
|
BOOL matched = NO;
|
|
|
|
if (selectorPart.length)
|
|
{
|
|
while (nextElement != nil)
|
|
{
|
|
DTHTMLElement *currentElement = nextElement;
|
|
|
|
//This must be set to advance here, above all of the breaks, so the loop properly advances.
|
|
nextElement = currentElement.parentElement;
|
|
|
|
if ([selectorPart characterAtIndex:0] == '#')
|
|
{
|
|
// If we're at an id and it doesn't match the current element then the style doesn't apply
|
|
NSString *currentElementId = [currentElement.attributes objectForKey:@"id"];
|
|
if (currentElementId && [[selectorPart substringFromIndex:1] isEqualToString:currentElementId])
|
|
{
|
|
matched = YES;
|
|
break;
|
|
}
|
|
} else if ([selectorPart characterAtIndex:0] == '.')
|
|
{
|
|
NSString *currentElementClassesString = [currentElement.attributes objectForKey:@"class"];
|
|
NSArray *currentElementClasses = [currentElementClassesString componentsSeparatedByString:@" "];
|
|
for (NSString *currentElementClass in currentElementClasses)
|
|
{
|
|
if ([currentElementClass isEqualToString:[selectorPart substringFromIndex:1]])
|
|
{
|
|
matched = YES;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (matched)
|
|
{
|
|
break;
|
|
}
|
|
} else if ([selectorPart isEqualToString:currentElement.name] && (selectorParts.count > 1))
|
|
{
|
|
// This condition depends on the "if (selectorParts.count < 2)" conditional above. If that's removed, we must make sure selectorParts
|
|
// contains > 1 item for this to be matched (we want the element name alone to be matched last).
|
|
matched = YES;
|
|
break;
|
|
}
|
|
|
|
// break if the right most portion of the selector doesn't match the target element
|
|
if (!matched && ([currentElement isEqual:element])) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!matched)
|
|
{
|
|
break;
|
|
}
|
|
|
|
//Only match if we really are on the last part of the selector and all other parts have matched so far
|
|
if (j == 0)
|
|
{
|
|
if (matched && ![matchedSelectors containsObject:selector])
|
|
{
|
|
[matchedSelectors addObject:selector];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return matchedSelectors;
|
|
}
|
|
|
|
// This computes the specificity for a given selector
|
|
- (NSUInteger)_weightForSelector:(NSString *)selector {
|
|
if ((selector == nil) || (selector.length == 0))
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
NSUInteger weight = 0;
|
|
|
|
NSArray *selectorParts = [selector componentsSeparatedByString:@" "];
|
|
for (NSString *selectorPart in selectorParts)
|
|
{
|
|
if (selectorPart.length == 0) {
|
|
continue;
|
|
}
|
|
|
|
if ([selectorPart characterAtIndex:0] == '#')
|
|
{
|
|
weight += 100;
|
|
} else if ([selectorPart characterAtIndex:0] == '.')
|
|
{
|
|
weight += 10;
|
|
} else {
|
|
weight += 1;
|
|
}
|
|
}
|
|
|
|
return weight;
|
|
}
|
|
|
|
#pragma mark NSCopying
|
|
|
|
- (id)copyWithZone:(NSZone *)zone
|
|
{
|
|
DTCSSStylesheet *newStylesheet = [[DTCSSStylesheet allocWithZone:zone] initWithStylesheet:self];
|
|
|
|
return newStylesheet;
|
|
}
|
|
|
|
@end
|