From 61d7c683fbc7502813200358e8d33e1095e808fd Mon Sep 17 00:00:00 2001 From: iska Date: Wed, 21 Oct 2015 22:15:36 +0200 Subject: [PATCH] Add implementations for structural pseudo class selectors https://html.spec.whatwg.org/multipage/scripting.html#selectors http://api.jquery.com/category/selectors/jquery-selector-extensions/ http://sizzlejs.com --- HTMLKit.xcodeproj/project.pbxproj | 10 + HTMLKit/CSSStructuralPseudoSelectors.h | 38 ++++ HTMLKit/CSSStructuralPseudoSelectors.m | 271 +++++++++++++++++++++++++ 3 files changed, 319 insertions(+) create mode 100644 HTMLKit/CSSStructuralPseudoSelectors.h create mode 100644 HTMLKit/CSSStructuralPseudoSelectors.m 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; + } + }); +}