Add CSS Selectors Parser implementation
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// CSSSelectorParser.h
|
||||
// HTMLKit
|
||||
//
|
||||
// Created by Iska on 02/10/15.
|
||||
// Copyright © 2015 BrainCookie. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@class CSSSelector;
|
||||
|
||||
@interface CSSSelectorParser : NSObject
|
||||
|
||||
+ (CSSSelector *)parseSelector:(NSString *)string error:(NSError **)error;
|
||||
|
||||
@end
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user