Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

Add support for cascading classes #544

Merged
merged 1 commit into from

2 participants

@amro

Hi Oliver. I needed support for cascading css classes so I wrote the code to do it. I'm not sure what might be an applicable unit test for this but my test case was the following markup:

<head>
    <style>
        .foo .bar {
            font-size: 25pt;
        }

        .foo .bar .bing {
            color: green;
        }

        .foo .bing {
            font-weight: bold;
        }

        .bar .bing {
            text-decoration: underline;
        }
        .zing {
            color:orange;
        }
        .bar .zing {
            font-style:italic;
        }
    </style>
</head>
<body>
    <div class="foo">
        <div>
            <div class="bar">
                <div class="bing">
                    me
                    <span class="zing">ow</span>
                </div>
                <span style="color:red;">Me<span class="bing">ow</span></span>
            </div>
        </div>
    </div>
</body>
</html>

which yielded the following fabulous output:

screen shot 2013-08-15 at 11 36 32 pm

@Cocoanetics
Owner

Exactly the thing you used for your testing would make a perfect unit test. Can you add it, or should I?

@Cocoanetics Cocoanetics was assigned
@Cocoanetics Cocoanetics merged commit b312cb4 into Cocoanetics:develop
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Aug 16, 2013
  1. @amro

    add support for cascading classes

    amro authored
This page is out of date. Refresh to see the latest.
Showing with 85 additions and 33 deletions.
  1. +85 −33 Core/Source/DTCSSStylesheet.m
View
118 Core/Source/DTCSSStylesheet.m
@@ -55,7 +55,7 @@ - (id)initWithStyleBlock:(NSString *)css
if (self)
{
- _styles = [[NSMutableDictionary alloc] init];
+ _styles = [[NSMutableDictionary alloc] init];
[self parseStyleBlock:css];
}
@@ -69,8 +69,8 @@ - (id)initWithStylesheet:(DTCSSStylesheet *)stylesheet
if (self)
{
- _styles = [[NSMutableDictionary alloc] init];
-
+ _styles = [[NSMutableDictionary alloc] init];
+
[self mergeStylesheet:stylesheet];
}
@@ -145,7 +145,7 @@ - (void)_uncompressShorthands:(NSMutableDictionary *)styles
if (listStylePosition != DTCSSListStylePositionInvalid)
{
[styles setObject:oneComponent forKey:@"list-style-position"];
-
+
positionWasSet = YES;
continue;
}
@@ -160,7 +160,7 @@ - (void)_uncompressShorthands:(NSMutableDictionary *)styles
{
NSString *fontStyle = @"normal";
NSArray *validFontStyles = [NSArray arrayWithObjects:@"italic", @"oblique", nil];
-
+
NSString *fontVariant = @"normal";
NSArray *validFontVariants = [NSArray arrayWithObjects:@"small-caps", nil];
BOOL fontVariantSet = NO;
@@ -180,7 +180,7 @@ - (void)_uncompressShorthands:(NSMutableDictionary *)styles
NSMutableString *fontFamily = [NSMutableString string];
NSArray *components = [shortHand componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
-
+
for (NSString *oneComponent in components)
{
// try font size keywords
@@ -216,7 +216,7 @@ - (void)_uncompressShorthands:(NSMutableDictionary *)styles
continue;
}
}
-
+
if (fontSizeSet)
{
if ([suffixesToIgnore containsObject:oneComponent])
@@ -250,9 +250,9 @@ - (void)_uncompressShorthands:(NSMutableDictionary *)styles
}
}
}
-
+
[styles removeObjectForKey:@"font"];
-
+
// size and family are mandatory, without them this is invalid
if ([fontSize length] && [fontFamily length])
{
@@ -374,7 +374,7 @@ - (void)_uncompressShorthands:(NSMutableDictionary *)styles
bottomPadding = onlyValue;
leftPadding = onlyValue;
}
-
+
// only apply the ones where there is no previous direct setting
if (![styles objectForKey:@"padding-top"])
@@ -386,7 +386,7 @@ - (void)_uncompressShorthands:(NSMutableDictionary *)styles
{
[styles setObject:rightPadding forKey:@"padding-right"];
}
-
+
if (![styles objectForKey:@"padding-bottom"])
{
[styles setObject:bottomPadding forKey:@"padding-bottom"];
@@ -406,7 +406,7 @@ - (void)_addStyleRule:(NSString *)rule withSelector:(NSString*)selectors
{
NSArray *split = [selectors componentsSeparatedByString:@","];
- for (NSString *selector in split)
+ for (NSString *selector in split)
{
NSString *cleanSelector = [selector stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
@@ -462,10 +462,10 @@ - (void)_addStyleRule:(NSString *)rule withSelector:(NSString*)selectors
}
}
}
-
+
// 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;
@@ -479,7 +479,7 @@ - (void)_addStyleRule:(NSString *)rule withSelector:(NSString*)selectors
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];
@@ -489,18 +489,18 @@ - (void)_addStyleRule:(NSString *)rule withSelector:(NSString*)selectors
NSDictionary *existingRulesForSelector = [_styles objectForKey:cleanSelector];
- if (existingRulesForSelector)
+ if (existingRulesForSelector)
{
// substitute new rules over old ones
NSMutableDictionary *tmpDict = [existingRulesForSelector mutableCopy];
// append new rules
[tmpDict addEntriesFromDictionary:ruleDictionary];
-
+
// save it
[_styles setObject:tmpDict forKey:cleanSelector];
}
- else
+ else
{
[_styles setObject:ruleDictionary forKey:cleanSelector];
}
@@ -564,7 +564,7 @@ - (void)parseStyleBlock:(NSString*)css
{
// If we start a new rule...
- if (braceLevel == 0)
+ if (braceLevel == 0)
{
// Grab the selector (we'll process it in a moment)
selector = [css substringWithRange:NSMakeRange(braceMarker, i-braceMarker)];
@@ -578,10 +578,10 @@ - (void)parseStyleBlock:(NSString*)css
}
// A closing brace!
- else if (c == '}')
+ else if (c == '}')
{
// If we finished a rule...
- if (braceLevel == 1)
+ if (braceLevel == 1)
{
NSString *rule = [css substringWithRange:NSMakeRange(braceMarker, i-braceMarker)];
@@ -638,7 +638,7 @@ - (NSDictionary *)mergedStyleDictionaryForElement:(DTHTMLElement *)element match
// Get based on element
NSDictionary *byTagName = [self.styles objectForKey:element.name];
- if (byTagName)
+ if (byTagName)
{
[tmpDict addEntriesFromDictionary:byTagName];
}
@@ -647,6 +647,10 @@ - (NSDictionary *)mergedStyleDictionaryForElement:(DTHTMLElement *)element match
NSString *classString = [element.attributes objectForKey:@"class"];
NSArray *classes = [classString componentsSeparatedByString:@" "];
+ // Find all classes by walking up the heirarchy and compute possible selector combinations
+ NSArray *ancestorClassArrays = [self findAncestorClassArraysForElement:element];
+ NSArray *cascadedSelectors = [self computeCascadedClassSelectorsWithAncestorClasses:ancestorClassArrays];
+
NSMutableSet *tmpMatchedSelectors;
if (matchedSelectors)
@@ -654,25 +658,26 @@ - (NSDictionary *)mergedStyleDictionaryForElement:(DTHTMLElement *)element match
tmpMatchedSelectors = [[NSMutableSet alloc] init];
}
- for (NSString *class in classes)
+ for (NSString *class in classes)
{
- NSString *classRule = [NSString stringWithFormat:@".%@", class];
NSString *classAndTagRule = [NSString stringWithFormat:@"%@.%@", element.name, class];
-
- NSDictionary *byClass = [_styles objectForKey:classRule];
NSDictionary *byClassAndName = [_styles objectForKey:classAndTagRule];
- if (byClass)
+ if (byClassAndName)
{
- [tmpDict addEntriesFromDictionary:byClass];
-
- [tmpMatchedSelectors addObject:classRule];
+ [tmpDict addEntriesFromDictionary:byClassAndName];
+ [tmpMatchedSelectors addObject:classAndTagRule];
}
- if (byClassAndName)
+ //This covers the "by class" only case (e.g. .foo)
+ for (NSString *cascadedSelector in cascadedSelectors)
{
- [tmpDict addEntriesFromDictionary:byClassAndName];
- [tmpMatchedSelectors addObject:classAndTagRule];
+ NSDictionary *byCascadedClassName = [_styles objectForKey:cascadedSelector];
+ if (byCascadedClassName)
+ {
+ [tmpDict addEntriesFromDictionary:byCascadedClassName];
+ [tmpMatchedSelectors addObject:cascadedSelector];
+ }
}
}
@@ -719,6 +724,53 @@ - (NSDictionary *)styles
return _styles;
}
+- (NSArray *)findAncestorClassArraysForElement:(DTHTMLElement *)element
+{
+ // Walk up the heirarchy looking for parents with class attributes then compute cascades
+ NSMutableArray *ancestorClassArrays = [NSMutableArray array];
+
+ DTHTMLElement *currentElement = element;
+ while (currentElement != nil)
+ {
+ NSString *currentElementClassString = [currentElement.attributes objectForKey:@"class"];
+ NSArray *currentElementClasses = [currentElementClassString componentsSeparatedByString:@" "];
+ if (currentElementClasses.count)
+ {
+ [ancestorClassArrays insertObject:currentElementClasses atIndex:0];
+ }
+
+ currentElement = currentElement.parentElement;
+ }
+
+ return ancestorClassArrays;
+}
+
+- (NSArray *)computeCascadedClassSelectorsWithAncestorClasses:(NSArray *)ancestorClasses
+{
+ NSMutableOrderedSet *cascadedSelectors = [[NSMutableOrderedSet alloc] init];
+
+ if (ancestorClasses.count) {
+ NSArray *classes = ancestorClasses[0];
+
+ // Find selector combinations for all ancestors that are leaves of the ancesor the current class array belongs to
+ NSArray *remainingAncessorClasses = [ancestorClasses subarrayWithRange:NSMakeRange(1, ancestorClasses.count - 1)];
+ NSArray *descendentSelectors = [self computeCascadedClassSelectorsWithAncestorClasses:remainingAncessorClasses];
+
+ for (NSString *class in classes)
+ {
+ [cascadedSelectors addObject:[NSString stringWithFormat:@".%@", class]];
+
+ for (NSString *descendentSelector in descendentSelectors)
+ {
+ [cascadedSelectors addObject:[NSString stringWithFormat:@"%@", descendentSelector]];
+ [cascadedSelectors addObject:[NSString stringWithFormat:@".%@ %@", class, descendentSelector]];
+ }
+ }
+ }
+
+ return [cascadedSelectors array];
+}
+
#pragma mark NSCopying
- (id)copyWithZone:(NSZone *)zone
Something went wrong with that request. Please try again.