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