diff --git a/HTMLKit/CSSSelectorParser.h b/HTMLKit/CSSSelectorParser.h new file mode 100644 index 0000000..1d55788 --- /dev/null +++ b/HTMLKit/CSSSelectorParser.h @@ -0,0 +1,17 @@ +// +// CSSSelectorParser.h +// HTMLKit +// +// Created by Iska on 02/10/15. +// Copyright © 2015 BrainCookie. All rights reserved. +// + +#import + +@class CSSSelector; + +@interface CSSSelectorParser : NSObject + ++ (CSSSelector *)parseSelector:(NSString *)string error:(NSError **)error; + +@end diff --git a/HTMLKit/CSSSelectorParser.m b/HTMLKit/CSSSelectorParser.m new file mode 100644 index 0000000..c550351 --- /dev/null +++ b/HTMLKit/CSSSelectorParser.m @@ -0,0 +1,367 @@ +// +// CSSSelectorParser.m +// HTMLKit +// +// Created by Iska on 02/10/15. +// Copyright © 2015 BrainCookie. All rights reserved. +// + +#import "CSSSelectorParser.h" +#import "CSSInputStream.h" +#import "CSSCodePoints.h" +#import "CSSSelectors.h" +#import "NSString+HTMLKit.h" +#import "NSCharacterSet+HTMLKit.h" +#import "CSSNthExpressionParser.h" +#import "CSSCompoundSelector.h" +#import "HTMLKitErrorDomain.h" + +@interface CSSSelectorParser () +{ + NSString *_string; + CSSInputStream *_inputStream; + NSUInteger _location; + + NSMutableArray *_selectors; +} +@end + +@implementation CSSSelectorParser + ++ (CSSSelector *)parseSelector:(NSString *)string error:(NSError * __autoreleasing *)error +{ + CSSSelectorParser *parser = [[CSSSelectorParser alloc] initWithString:string]; + CSSSelector *selector = [parser parse:error]; + + return selector; +} + +#pragma mark - Init + +- (instancetype)initWithString:(NSString *)string +{ + self = [super init]; + if (self) { + _string = [self preprocessInput:string]; + _location = 0; + } + return self; +} + +- (NSString *)preprocessInput:(NSString *)string +{ + string = [string stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; + string = [string stringByReplacingOccurrencesOfString:@"\r\n" withString:@"\n"]; + string = [string stringByReplacingOccurrencesOfString:@"\r" withString:@"\n"]; + string = [string stringByReplacingOccurrencesOfString:@"\f" withString:@"\n"]; + string = [string stringByReplacingOccurrencesOfString:@"\0" withString:@"\uFFFD"]; + + return string; +} + +#pragma mark - Errors + +- (void)emitError:(NSError * __autoreleasing *)error reason:(NSString *)reason location:(NSUInteger)location +{ + NSDictionary *userInfo = @{ + NSLocalizedDescriptionKey: @"Error parsing selector", + NSLocalizedFailureReasonErrorKey: reason, + CSSSelectorStringKey: _string, + CSSSelectorErrorLocationKey: @(location) + }; + + if(error) { + *error = [NSError errorWithDomain:HTMLKitSelectorErrorDomain code:HTMLKitSelectorParseError userInfo:userInfo]; + } +} + +#pragma mark - Parsing + +- (CSSSelector *)parse:(NSError * __autoreleasing *)error +{ + if (_string.length == 0) { + [self emitError:error reason:@"Empty selector" location:0]; + return nil; + } + + NSArray *allSubSelectors = [_string componentsSeparatedByString:@","]; + NSMutableArray *parsed = [NSMutableArray array]; + + for (NSString *subSelector in allSubSelectors) { + if ([subSelector isEqualToString:@""]) { + [self emitError:error reason:@"Empty selector" location:_location]; + break; + } + + CSSSelector *selector = [self parseSelector:subSelector error:error]; + if (selector == nil) { + break; + } + [parsed addObject:selector]; + + _location += subSelector.length; + } + + if (*error != nil) { + return nil; + } + + if (parsed.count > 1) { + return anyOf(parsed); + } + + return parsed.firstObject; +} + +- (CSSSelector *)parseSelector:(NSString *)selectorString error:(NSError * __autoreleasing *)error +{ + _inputStream = [[CSSInputStream alloc] initWithString:selectorString]; + [_inputStream consumeWhitespace]; + + CSSSelector *result = nil; + + while (YES) { + CSSSelector *selector = [self parseSequenceOfSimpleSelectors:error]; + if (selector == nil) { + break; + } + + result = result ? allOf(@[result, selector]) : selector; + + UTF32Char next = _inputStream.nextInputCharacter; + + if (isCombinator(next)) { + NSString *combinator = [_inputStream consumeCombinator]; + + if ([combinator isEqualToString:@""]) { + result = descendantOfElementSelector(result); + } else if ([combinator isEqualToString:@">"]) { + result = childOfElementSelector(result); + } else if ([combinator isEqualToString:@"+"]) { + result = adjacentSiblingSelector(result); + } else if ([combinator isEqualToString:@"~"]) { + result = generalSiblingSelector(result); + } + } + } + + return result; +} + +- (CSSSelector *)parseSequenceOfSimpleSelectors:(NSError * __autoreleasing *)error +{ + NSMutableArray *selectors = [NSMutableArray array]; + + CSSSelector *typeSelector = [self parseTypeSelector:error]; + if (typeSelector != nil) { + [selectors addObject:typeSelector]; + } + + while (YES) { + UTF32Char next = _inputStream.nextInputCharacter; + if (next == EOF || isCombinator(next)) { + break; + } + + CSSSelector *simpleSelector = [self parseSimpleSelector:error]; + if (simpleSelector == nil) { + return nil; + } + [selectors addObject:simpleSelector]; + } + + if (selectors.count > 1) { + return allOf(selectors); + } + + return selectors.firstObject; +} + +- (CSSSelector *)parseTypeSelector:(NSError * __autoreleasing *)error +{ + NSString *identifier = [_inputStream consumeIdentifier]; + if (identifier != nil) { + return typeSelector(identifier); + } + + if ([_inputStream consumeCharacter:ASTERIX]) { + return universalSelector(); + } + + return nil; +} + +- (CSSSelector *)parseSimpleSelector:(NSError * __autoreleasing *)error +{ + CSSSelector *typeSelector = [self parseTypeSelector:error]; + if (typeSelector != nil) { + return typeSelector; + } + + UTF32Char codePoint = [_inputStream consumeNextInputCharacter]; + switch (codePoint) { + case NUMBER_SIGN: + { + NSString *elementId = [_inputStream consumeIdentifier]; + if (elementId == nil) { + [self emitError:error reason:@"Invalid character" location:_location + _inputStream.currentLocation]; + return nil; + } + return idSelector(elementId); + } + case FULL_STOP: + { + NSString *className = [_inputStream consumeIdentifier]; + if (className == nil) { + [self emitError:error reason:@"Invalid character" location:_location + _inputStream.currentLocation]; + return nil; + } + return classSelector(className); + } + case LEFT_SQUARE_BRACKET: + { + return [self parseAttributeSelector:error]; + } + case COLON: + { + return [self parsePseudoSelector:error]; + } + default: + { + [self emitError:error reason:@"Invalid character" location:_location + _inputStream.currentLocation]; + return nil; + } + } +} + +- (CSSSelector *)parseAttributeSelector:(NSError * __autoreleasing *)error +{ + NSString *attribute = [_inputStream consumeIdentifier]; + if (attribute == nil) { + [self emitError:error reason:@"Invalid character" location:_location + _inputStream.currentLocation + 1]; + return nil; + } + [_inputStream consumeWhitespace]; + + CSSAttributeSelectorType type = CSSAttributeSelectorExists; + + NSString *operator = [_inputStream consumeCharactersInString:@"=~|^$*!"]; + + if ([operator isEqualToString:@"="]) { + type = CSSAttributeSelectorExactMatch; + } else if ([operator isEqualToString:@"~="]) { + type = CSSAttributeSelectorIncludes; + } else if ([operator isEqualToString:@"|="]) { + type = CSSAttributeSelectorHyphen; + } else if ([operator isEqualToString:@"^="]) { + type = CSSAttributeSelectorBegins; + } else if ([operator isEqualToString:@"$="]) { + type = CSSAttributeSelectorEnds; + } else if ([operator isEqualToString:@"*="]) { + type = CSSAttributeSelectorContains; + } else if ([operator isEqualToString:@"!="]) { + type = CSSAttributeSelectorNot; + } + + NSString *value = nil; + [_inputStream consumeWhitespace]; + + UTF32Char next = _inputStream.nextInputCharacter; + if (isQuote(next)) { + UTF32Char quote = [_inputStream consumeNextInputCharacter]; + value = [_inputStream consumeStringWithEndingCodePoint:quote]; + } else { + value = [_inputStream consumeIdentifier]; + } + + [_inputStream consumeWhitespace]; + + // Consume RIGHT_SQUARE_BRACKET + if (![_inputStream consumeCharacter:RIGHT_SQUARE_BRACKET]) { + [self emitError:error reason:@"Expected closing right square bracket ']'" location:_location + _inputStream.currentLocation]; + } + + if (type == CSSAttributeSelectorExists) { + return hasAttributeSelector(attribute); + } + + return attributeSelector(type, attribute, value); +} + +- (CSSSelector *)parsePseudoSelector:(NSError * __autoreleasing *)error +{ + NSString *pseudoClass = [_inputStream consumeIdentifier]; + + if ([pseudoClass hasPrefix:@"nth"]) { + [_inputStream consumeWhitespace]; + if (![_inputStream consumeCharacter:LEFT_PARENTHESIS]) { + [self emitError:error reason:@"Expected opening left parenthesis '('" location:_location + _inputStream.currentLocation]; + } + + NSString *functionExpression = [_inputStream consumeCharactersUpToString:@")"]; + CSSNthExpression expression = [CSSNthExpressionParser parseExpression:functionExpression]; + + [_inputStream consumeWhitespace]; + if (![_inputStream consumeCharacter:RIGHT_PARENTHESIS]) { + [self emitError:error reason:@"Expected closing right parenthesis ')'" location:_location + _inputStream.currentLocation]; + } + + if ([pseudoClass isEqualToString:@"nth-child"]) { + return nthChildSelector(expression); + } else if ([pseudoClass isEqualToString:@"nth-last-child"]) { + return nthLastChildSelector(expression); + } else if ([pseudoClass isEqualToString:@"nth-of-type"]) { + return nthOfTypeSelector(expression); + } else if ([pseudoClass isEqualToString:@"nth-last-of-type"]) { + return nthLastOfTypeSelector(expression); + } + } else if ([pseudoClass isEqualToString:@"not"]) { + [_inputStream consumeWhitespace]; + if (![_inputStream consumeCharacter:LEFT_PARENTHESIS]) { + [self emitError:error reason:@"Expected opening left parenthesis '('" location:_location + _inputStream.currentLocation]; + } + + CSSSelector *subSelector = [self parseSimpleSelector:error]; + [_inputStream consumeWhitespace]; + if (![_inputStream consumeCharacter:RIGHT_PARENTHESIS]) { + [self emitError:error reason:@"Expected closing right parenthesis ')'" location:_location + _inputStream.currentLocation]; + } + + return nay(subSelector); + } else { + if ([pseudoClass isEqualToString:@"even"]) { + return evenSlector(); + } else if ([pseudoClass isEqualToString:@"odd"]) { + return oddSelector(); + } else if ([pseudoClass isEqualToString:@"first-child"]) { + return firstChildSelector(); + } else if ([pseudoClass isEqualToString:@"last-child"]) { + return lastChildSelector(); + } else if ([pseudoClass isEqualToString:@"first-of-type"]) { + return firstOfTypeSelector(); + } else if ([pseudoClass isEqualToString:@"last-of-type"]) { + return lastOfTypeSelector(); + } else if ([pseudoClass isEqualToString:@"only-child"]) { + return onlyChildSelector(); + } else if ([pseudoClass isEqualToString:@"only-of-type"]) { + return onlyOfTypeSelector(); + } else if ([pseudoClass isEqualToString:@"root"]) { + return rootSelector(); + } else if ([pseudoClass isEqualToString:@"empty"]) { + return emptySelector(); + } else if ([pseudoClass isEqualToString:@"link"]) { + return linkSelector(); + } else if ([pseudoClass isEqualToString:@"enabled"]) { + return enabledSelector(); + } else if ([pseudoClass isEqualToString:@"disabled"]) { + return disabledSelector(); + } else if ([pseudoClass isEqualToString:@"checked"]) { + return checkedSelector(); + } + } + NSString *reason = [NSString stringWithFormat:@"Unknown pseudo class: %@", pseudoClass]; + [self emitError:error reason:reason location:_location + _inputStream.currentLocation]; + return nil; +} + +@end