diff --git a/HTMLKit.xcodeproj/project.pbxproj b/HTMLKit.xcodeproj/project.pbxproj
index 472307f..1646a5e 100644
--- a/HTMLKit.xcodeproj/project.pbxproj
+++ b/HTMLKit.xcodeproj/project.pbxproj
@@ -149,6 +149,9 @@
628AF6331BC9A63D00496128 /* CSSNthExpressionSelector.h in Headers */ = {isa = PBXBuildFile; fileRef = 628AF6311BC9A63D00496128 /* CSSNthExpressionSelector.h */; settings = {ASSET_TAGS = (); }; };
628AF6341BC9A63D00496128 /* CSSNthExpressionSelector.m in Sources */ = {isa = PBXBuildFile; fileRef = 628AF6321BC9A63D00496128 /* CSSNthExpressionSelector.m */; settings = {ASSET_TAGS = (); }; };
628AF6351BC9A63D00496128 /* CSSNthExpressionSelector.m in Sources */ = {isa = PBXBuildFile; fileRef = 628AF6321BC9A63D00496128 /* CSSNthExpressionSelector.m */; settings = {ASSET_TAGS = (); }; };
+ 628AF63E1BC9D6FB00496128 /* CSSStructuralPseudoSelectors.h in Headers */ = {isa = PBXBuildFile; fileRef = 628AF63C1BC9D6FB00496128 /* CSSStructuralPseudoSelectors.h */; settings = {ASSET_TAGS = (); }; };
+ 628AF63F1BC9D6FB00496128 /* CSSStructuralPseudoSelectors.m in Sources */ = {isa = PBXBuildFile; fileRef = 628AF63D1BC9D6FB00496128 /* CSSStructuralPseudoSelectors.m */; settings = {ASSET_TAGS = (); }; };
+ 628AF6401BC9D6FB00496128 /* CSSStructuralPseudoSelectors.m in Sources */ = {isa = PBXBuildFile; fileRef = 628AF63D1BC9D6FB00496128 /* CSSStructuralPseudoSelectors.m */; settings = {ASSET_TAGS = (); }; };
62AE593519F97D880043F069 /* HTMLParseErrorToken.h in Headers */ = {isa = PBXBuildFile; fileRef = 62AE593319F97D880043F069 /* HTMLParseErrorToken.h */; };
62AE593619F97D880043F069 /* HTMLParseErrorToken.m in Sources */ = {isa = PBXBuildFile; fileRef = 62AE593419F97D880043F069 /* HTMLParseErrorToken.m */; };
62AE593719F97D880043F069 /* HTMLParseErrorToken.m in Sources */ = {isa = PBXBuildFile; fileRef = 62AE593419F97D880043F069 /* HTMLParseErrorToken.m */; };
@@ -334,6 +337,8 @@
628AF62E1BC99A6C00496128 /* CSSNthExpressionsParserTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CSSNthExpressionsParserTests.m; sourceTree = ""; };
628AF6311BC9A63D00496128 /* CSSNthExpressionSelector.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CSSNthExpressionSelector.h; sourceTree = ""; };
628AF6321BC9A63D00496128 /* CSSNthExpressionSelector.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CSSNthExpressionSelector.m; sourceTree = ""; };
+ 628AF63C1BC9D6FB00496128 /* CSSStructuralPseudoSelectors.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CSSStructuralPseudoSelectors.h; sourceTree = ""; };
+ 628AF63D1BC9D6FB00496128 /* CSSStructuralPseudoSelectors.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CSSStructuralPseudoSelectors.m; sourceTree = ""; };
628B7CE61A080E1000602C87 /* HTMLNamespaces.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = HTMLNamespaces.h; sourceTree = ""; };
62ACB8DB1BC5C13E00E283C1 /* CSSPseudoFunctionSelector.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CSSPseudoFunctionSelector.h; sourceTree = ""; };
62ACB8DC1BC5C13E00E283C1 /* CSSPseudoFunctionSelector.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CSSPseudoFunctionSelector.m; sourceTree = ""; };
@@ -427,6 +432,8 @@
624B28BE1B0413200048D328 /* CSSAttributeSelector.m */,
620EE9471BC46F2A0028ED34 /* CSSPseudoClassSelector.h */,
620EE9481BC46F2A0028ED34 /* CSSPseudoClassSelector.m */,
+ 628AF63C1BC9D6FB00496128 /* CSSStructuralPseudoSelectors.h */,
+ 628AF63D1BC9D6FB00496128 /* CSSStructuralPseudoSelectors.m */,
628AF6311BC9A63D00496128 /* CSSNthExpressionSelector.h */,
628AF6321BC9A63D00496128 /* CSSNthExpressionSelector.m */,
62ACB8DB1BC5C13E00E283C1 /* CSSPseudoFunctionSelector.h */,
@@ -713,6 +720,7 @@
62D89DB71AE7081600B6243D /* HTMLQuirksMode.h in Headers */,
62D89DB01AE7080300B6243D /* HTMLDOM.h in Headers */,
62F873EB19E088C90062683C /* HTMLParser.h in Headers */,
+ 628AF63E1BC9D6FB00496128 /* CSSStructuralPseudoSelectors.h in Headers */,
624AC8FF19FBF59800BD3C4A /* HTMLTokens.h in Headers */,
62AE594419F992F30043F069 /* HTMLCommentToken.h in Headers */,
624493AC19CD0CBE00BCDDF4 /* HTMLToken.h in Headers */,
@@ -917,6 +925,7 @@
62FA04E51BCC360D009ABF98 /* CSSCombinatorSelector.m in Sources */,
6238C9871AB8D6330006512E /* HTMLKitDOMExceptions.m in Sources */,
62FA980A1BD5A7E200A2AF86 /* CSSSelectorBlock.m in Sources */,
+ 628AF6401BC9D6FB00496128 /* CSSStructuralPseudoSelectors.m in Sources */,
6247171A1B22333200C11912 /* HTMLNodeTraversal.m in Sources */,
62F873ED19E088C90062683C /* HTMLParser.m in Sources */,
623803A11AB63A8C008A53D0 /* HTMLEOFToken.m in Sources */,
@@ -989,6 +998,7 @@
62AE593B19F97E1C0043F069 /* HTMLDOCTYPEToken.m in Sources */,
623406E21ADB04F9004677A3 /* HTMLTemplate.m in Sources */,
62FA04E41BCC360D009ABF98 /* CSSCombinatorSelector.m in Sources */,
+ 628AF63F1BC9D6FB00496128 /* CSSStructuralPseudoSelectors.m in Sources */,
62FA98091BD5A7E200A2AF86 /* CSSSelectorBlock.m in Sources */,
6238C9861AB8D6330006512E /* HTMLKitDOMExceptions.m in Sources */,
624717191B22333200C11912 /* HTMLNodeTraversal.m in Sources */,
diff --git a/HTMLKit/CSSStructuralPseudoSelectors.h b/HTMLKit/CSSStructuralPseudoSelectors.h
new file mode 100644
index 0000000..013bd9a
--- /dev/null
+++ b/HTMLKit/CSSStructuralPseudoSelectors.h
@@ -0,0 +1,38 @@
+//
+// CSSStructuralPseudoSelector.h
+// HTMLKit
+//
+// Created by Iska on 11/10/15.
+// Copyright © 2015 BrainCookie. All rights reserved.
+//
+
+@class CSSSelector;
+
+NS_ASSUME_NONNULL_BEGIN
+
+extern CSSSelector * rootSelector();
+extern CSSSelector * emptySelector();
+extern CSSSelector * parentSelector();
+
+extern CSSSelector * buttonSelector();
+extern CSSSelector * checkboxSelector();
+extern CSSSelector * fileSelector();
+extern CSSSelector * headerSelector();
+extern CSSSelector * imageSelector();
+extern CSSSelector * inputSelector();
+extern CSSSelector * linkSelector();
+extern CSSSelector * passwordSelector();
+extern CSSSelector * radioSelector();
+extern CSSSelector * resetSelector();
+extern CSSSelector * submitSelector();
+extern CSSSelector * textSelector();
+
+extern CSSSelector * enabledSelector();
+extern CSSSelector * disabledSelector();
+extern CSSSelector * checkedSelector();
+
+extern CSSSelector * ltSelector();
+extern CSSSelector * gtSelector();
+extern CSSSelector * eqSelector();
+
+NS_ASSUME_NONNULL_END
diff --git a/HTMLKit/CSSStructuralPseudoSelectors.m b/HTMLKit/CSSStructuralPseudoSelectors.m
new file mode 100644
index 0000000..7d5b411
--- /dev/null
+++ b/HTMLKit/CSSStructuralPseudoSelectors.m
@@ -0,0 +1,271 @@
+//
+// CSSStructuralPseudoSelector.m
+// HTMLKit
+//
+// Created by Iska on 11/10/15.
+// Copyright © 2015 BrainCookie. All rights reserved.
+//
+
+#import "CSSStructuralPseudoSelectors.h"
+#import "CSSSelectors.h"
+#import "HTMLElement.h"
+#import "NSString+HTMLKit.h"
+
+#pragma mark - Elements
+
+CSSSelector * rootSelector()
+{
+ return namedBlockSelector(@"root", ^BOOL(HTMLElement * element) {
+ return !!element.parentElement;
+ });
+}
+
+CSSSelector * emptySelector()
+{
+ return namedBlockSelector(@"empty", ^BOOL(HTMLElement * element) {
+ for (HTMLNode *child in element.childNodes) {
+ if (child.nodeType == HTMLNodeElement) {
+ return NO;
+ } else if (child.nodeType == HTMLNodeText && child.textContent.length > 0) {
+ return NO;
+ }
+ }
+ return YES;
+ });
+}
+
+CSSSelector * parentSelector()
+{
+ return namedBlockSelector(@"parent", ^BOOL(HTMLElement * element) {
+ return element.childNodesCount > 0;
+ });
+}
+
+CSSSelector * buttonSelector()
+{
+ return namedBlockSelector(@"button", ^BOOL(HTMLElement * _Nonnull element) {
+ if ([element.tagName isEqualToString:@"button"]) {
+ return YES;
+ }
+ if ([element.tagName isEqualToString:@"input"] && [element[@"type"] isEqualToString:@"button"]) {
+ return YES;
+ }
+ return NO;
+ });
+}
+
+CSSSelector * checkboxSelector()
+{
+ return namedBlockSelector(@"checkbox", ^BOOL(HTMLElement * _Nonnull element) {
+ if ([element[@"type"] isEqualToString:@"checkbox"]) {
+ return YES;
+ }
+ return NO;
+ });
+}
+
+CSSSelector * fileSelector()
+{
+ return namedBlockSelector(@"file", ^BOOL(HTMLElement * _Nonnull element) {
+ if ([element[@"type"] isEqualToString:@"file"]) {
+ return YES;
+ }
+ return NO;
+ });
+}
+
+CSSSelector * headerSelector()
+{
+ return namedBlockSelector(@"header", ^BOOL(HTMLElement * _Nonnull element) {
+ if ([element.tagName isEqualToAny:@"h1", @"h2", @"h3", @"h4", @"h5", @"h6", nil]) {
+ return YES;
+ }
+ return NO;
+ });
+}
+
+CSSSelector * imageSelector()
+{
+ return namedBlockSelector(@"image", ^BOOL(HTMLElement * _Nonnull element) {
+ if ([element[@"type"] isEqualToString:@"image"]) {
+ return YES;
+ }
+ return NO;
+ });
+}
+
+CSSSelector * inputSelector()
+{
+ return namedBlockSelector(@"input", ^BOOL(HTMLElement * _Nonnull element) {
+ if ([element.tagName isEqualToAny:@"button", @"input", @"select", @"textarea", nil]) {
+ return YES;
+ }
+ return NO;
+ });
+}
+
+CSSSelector * linkSelector()
+{
+ // https://html.spec.whatwg.org/multipage/scripting.html#selector-link
+ return namedBlockSelector(@"root", ^BOOL(HTMLElement * element) {
+ if ([element hasAttribute:@"href"]) {
+ return [element.tagName isEqualToAny:@"a", @"area", @"link", nil];
+ }
+ return NO;
+ });
+}
+
+CSSSelector * passwordSelector()
+{
+ return namedBlockSelector(@"password", ^BOOL(HTMLElement * _Nonnull element) {
+ if ([element[@"type"] isEqualToString:@"password"]) {
+ return YES;
+ }
+ return NO;
+ });
+}
+
+CSSSelector * radioSelector()
+{
+ return namedBlockSelector(@"radio", ^BOOL(HTMLElement * _Nonnull element) {
+ if ([element[@"type"] isEqualToString:@"radio"]) {
+ return YES;
+ }
+ return NO;
+ });
+}
+
+CSSSelector * resetSelector()
+{
+ return namedBlockSelector(@"reset", ^BOOL(HTMLElement * _Nonnull element) {
+ if ([element[@"type"] isEqualToString:@"reset"]) {
+ return YES;
+ }
+ return NO;
+ });
+}
+
+CSSSelector * submitSelector()
+{
+ return namedBlockSelector(@"submit", ^BOOL(HTMLElement * _Nonnull element) {
+ if ([element.tagName isEqualToString:@"input"] && [element[@"type"] isEqualToString:@"submit"]) {
+ return YES;
+ }
+ if ([element.tagName isEqualToString:@"button"] && [element[@"type"] isEqualToString:@"submit"]) {
+ return YES;
+ }
+ return NO;
+ });
+}
+
+CSSSelector * textSelector()
+{
+ return namedBlockSelector(@"text", ^BOOL(HTMLElement * _Nonnull element) {
+ if ([element[@"type"] isEqualToString:@"text"]) {
+ return YES;
+ }
+ return NO;
+ });
+}
+
+#pragma mark - State
+
+CSSSelector * enabledSelector()
+{
+ // https://html.spec.whatwg.org/multipage/scripting.html#selector-enabled
+ CSSSelector *candiate = anyOf(@[
+ typeSelector(@"button"),
+ typeSelector(@"input"),
+ typeSelector(@"select"),
+ typeSelector(@"textarea"),
+ typeSelector(@"optgroup"),
+ typeSelector(@"option"),
+ typeSelector(@"menuitem"),
+ typeSelector(@"fieldset"),
+ ]);
+ return namedPseudoSelector(@"enabled", allOf(@[candiate, nay(disabledSelector())]));
+}
+
+CSSSelector * disabledSelector()
+{
+ // https://html.spec.whatwg.org/multipage/scripting.html#selector-disabled
+ CSSSelector *disabledAttribute = hasAttributeSelector(@"disabled");
+
+ // https://html.spec.whatwg.org/multipage/forms.html#concept-fieldset-disabled
+ CSSSelector *disabledFieldset = allOf(@[typeSelector(@"fieldset"), disabledAttribute]);
+ CSSSelector *firstLegend = allOf(@[typeSelector(@"legend"), firstOfTypeSelector()]);
+ CSSSelector *firstLegendDecendantDisabledFieldSet = allOf(@[firstLegend, descendantOfElementSelector(disabledFieldset)]);
+
+ // https://html.spec.whatwg.org/multipage/forms.html#concept-fe-disabled
+ CSSSelector *disabledForm = anyOf(@[
+ allOf(@[
+ allOf(@[typeSelector(@"button"), disabledAttribute]),
+ allOf(@[typeSelector(@"input"), disabledAttribute]),
+ allOf(@[typeSelector(@"select"), disabledAttribute]),
+ allOf(@[typeSelector(@"textarea"), disabledAttribute])
+ ]),
+ allOf(@[
+ descendantOfElementSelector(disabledFieldset),
+ nay(firstLegendDecendantDisabledFieldSet)
+ ])
+ ]);
+
+ // https://html.spec.whatwg.org/multipage/scripting.html#selector-disabled
+ CSSSelector *disabledMenuItem = allOf(@[typeSelector(@"menuitem"), disabledAttribute]);
+ CSSSelector *disabledOptgroup = allOf(@[typeSelector(@"optgroup"), disabledAttribute]);
+
+ // https://html.spec.whatwg.org/multipage/forms.html#concept-option-disabled
+ CSSSelector *disabledOption = allOf(@[
+ typeSelector(@"option"),
+ anyOf(@[
+ disabledAttribute,
+ descendantOfElementSelector(disabledOptgroup)])
+ ]);
+ return namedPseudoSelector(@"disabled",
+ anyOf(@[disabledOption, disabledOptgroup, disabledMenuItem, disabledForm, disabledFieldset]));
+}
+
+CSSSelector * checkedElementSelector()
+{
+ // https://html.spec.whatwg.org/multipage/scripting.html#selector-checked
+ CSSSelector *candidate = anyOf(@[
+ typeSelector(@"input"),
+ typeSelector(@"option"),
+ typeSelector(@"menutitem")
+ ]);
+ CSSSelector *hasAttribute = anyOf(@[
+ hasAttributeSelector(@"checked"),
+ hasAttributeSelector(@"selected")
+ ]);
+
+ return namedPseudoSelector(@"checked", allOf(@[candidate, hasAttribute]));
+}
+
+#pragma mark - Positional
+
+CSSSelector * ltSelector(NSUInteger index)
+{
+ return namedBlockSelector(@"lt", ^BOOL(HTMLElement * _Nonnull element) {
+ return [element.parentElement indexOfChildNode:element] < index;
+ });
+}
+
+CSSSelector * gtSelector(NSUInteger index)
+{
+ return namedBlockSelector(@"gt", ^BOOL(HTMLElement * _Nonnull element) {
+ return [element.parentElement indexOfChildNode:element] > index;
+ });
+}
+
+CSSSelector * eqSelector(NSInteger index)
+{
+ return namedBlockSelector(@"eq", ^BOOL(HTMLElement * _Nonnull element) {
+ NSUInteger elementIndex = [element.parentElement indexOfChildNode:element];
+
+ if (index > 0) {
+ return elementIndex == index;
+ } else {
+ return elementIndex == element.parentElement.childNodesCount - index - 1;
+ }
+ });
+}