AngleSharp by Florian Rappl

<PackageReference Include="AngleSharp" Version="0.7.0" />

 CssSelectorConstructor

sealed class CssSelectorConstructor
Class for construction for CSS selectors as specified in http://www.w3.org/html/wg/drafts/html/master/selectors.html.
using AngleSharp.Css; using AngleSharp.DOM; using AngleSharp.DOM.Css; using AngleSharp.DOM.Html; using AngleSharp.Extensions; using System; using System.Diagnostics; namespace AngleSharp.Parser.Css { [DebuggerStepThrough] internal sealed class CssSelectorConstructor { private enum State { Data, Attribute, AttributeOperator, AttributeValue, AttributeEnd, Class, PseudoClass, PseudoClassFunction, PseudoClassFunctionEnd, PseudoElement } private abstract class NthChildSelector { public int step; public int offset; public Priority Specifity => Priority.OneClass; } private sealed class NthFirstChildSelector : NthChildSelector, ISelector, ICssObject { public string Text => ToCss(); public bool Match(IElement element) { INode parent = element.Parent; if (parent == null) return false; int num = 1; for (int i = 0; i < parent.ChildNodes.Length; i++) { if (parent.ChildNodes[i] == element) { if (step != 0) return (num - offset) % step == 0; return num == offset; } if (parent.ChildNodes[i] is IElement) num++; } return true; } public string ToCss() { return string.Format(":{0}({1}n+{2})", new object[3] { "nth-child", step, offset }); } } private sealed class NthLastChildSelector : NthChildSelector, ISelector, ICssObject { public string Text => ToCss(); public bool Match(IElement element) { IElement parentElement = element.ParentElement; if (parentElement == null) return false; int num = 1; for (int num2 = parentElement.ChildNodes.Length - 1; num2 >= 0; num2--) { if (parentElement.ChildNodes[num2] == element) { if (step != 0) return (num - offset) % step == 0; return num == offset; } if (parentElement.ChildNodes[num2] is IElement) num++; } return true; } public string ToCss() { return string.Format(":{0}({1}n+{2})", new object[3] { "nth-last-child", step, offset }); } } private sealed class FirstChildSelector : ISelector, ICssObject { private static FirstChildSelector instance; public static FirstChildSelector Instance => instance ?? (instance = new FirstChildSelector()); public string Text => ToCss(); public Priority Specifity => Priority.OneClass; private FirstChildSelector() { } public bool Match(IElement element) { INode parent = element.Parent; if (parent == null) return false; for (int i = 0; i <= parent.ChildNodes.Length; i++) { if (parent.ChildNodes[i] == element) return true; if (parent.ChildNodes[i] is IElement) return false; } return false; } public string ToCss() { return ":first-child"; } } private sealed class LastChildSelector : ISelector, ICssObject { private static LastChildSelector instance; public static LastChildSelector Instance => instance ?? (instance = new LastChildSelector()); public string Text => ToCss(); public Priority Specifity => Priority.OneClass; private LastChildSelector() { } public bool Match(IElement element) { IElement parentElement = element.ParentElement; if (parentElement == null) return false; for (int num = parentElement.ChildNodes.Length - 1; num >= 0; num--) { if (parentElement.ChildNodes[num] == element) return true; if (parentElement.ChildNodes[num] is IElement) return false; } return false; } public string ToCss() { return ":last-child"; } } private const string pseudoClassRoot = "root"; private const string pseudoClassFirstOfType = "first-of-type"; private const string pseudoClassLastOfType = "last-of-type"; private const string pseudoClassOnlyChild = "only-child"; private const string pseudoClassFirstChild = "first-child"; private const string pseudoClassLastChild = "last-child"; private const string pseudoClassEmpty = "empty"; private const string pseudoClassLink = "link"; private const string pseudoClassVisited = "visited"; private const string pseudoClassActive = "active"; private const string pseudoClassHover = "hover"; private const string pseudoClassFocus = "focus"; private const string pseudoClassTarget = "target"; private const string pseudoClassEnabled = "enabled"; private const string pseudoClassDisabled = "disabled"; private const string pseudoClassChecked = "checked"; private const string pseudoClassUnchecked = "unchecked"; private const string pseudoClassIndeterminate = "indeterminate"; private const string pseudoClassDefault = "default"; private const string pseudoClassValid = "valid"; private const string pseudoClassInvalid = "invalid"; private const string pseudoClassRequired = "required"; private const string pseudoClassInRange = "in-range"; private const string pseudoClassOutOfRange = "out-of-range"; private const string pseudoClassOptional = "optional"; private const string pseudoClassReadOnly = "read-only"; private const string pseudoClassReadWrite = "read-write"; private const string pseudoClassFunctionDir = "dir"; private const string pseudoClassFunctionNthChild = "nth-child"; private const string pseudoClassFunctionNthLastChild = "nth-last-child"; private const string pseudoClassFunctionNot = "not"; private const string pseudoClassFunctionLang = "lang"; private const string pseudoClassFunctionContains = "contains"; private const string pseudoElementBefore = "before"; private const string pseudoElementAfter = "after"; private const string pseudoElementSelection = "selection"; private const string pseudoElementFirstLine = "first-line"; private const string pseudoElementFirstLetter = "first-letter"; private static readonly string nthChildOdd = "odd"; private static readonly string nthChildEven = "even"; private static readonly string nthChildN = "n"; private State state; private ISelector temp; private ListSelector group; private ComplexSelector complex; private bool hasCombinator; private CssCombinator combinator; private CssSelectorConstructor nested; private string attrName; private string attrValue; private string attrOp; public ISelector Result { get { if (complex != null) { complex.ConcludeSelector(temp); temp = complex; } if (group == null || group.Length == 0) return temp ?? SimpleSelector.All; if (temp == null && group.Length == 1) return group[0]; if (temp != null) { group.Add(temp); temp = null; } return group; } } public CssSelectorConstructor() { Reset(); } public bool Apply(CssToken token) { switch (state) { case State.Data: return OnData(token); case State.Class: return OnClass(token); case State.Attribute: return OnAttribute(token); case State.AttributeOperator: return OnAttributeOperator(token); case State.AttributeValue: return OnAttributeValue(token); case State.AttributeEnd: return OnAttributeEnd(token); case State.PseudoClass: return OnPseudoClass(token); case State.PseudoClassFunction: return OnPseudoClassFunction(token); case State.PseudoClassFunctionEnd: return OnPseudoClassFunctionEnd(token); case State.PseudoElement: return OnPseudoElement(token); default: return false; } } public CssSelectorConstructor Reset() { attrName = null; attrValue = null; attrOp = string.Empty; state = State.Data; combinator = CssCombinator.Descendent; hasCombinator = false; temp = null; group = null; complex = null; return this; } private bool OnData(CssToken token) { switch (token.Type) { case CssTokenType.SquareBracketOpen: attrName = null; attrValue = null; attrOp = string.Empty; state = State.Attribute; return true; case CssTokenType.Colon: state = State.PseudoClass; return true; case CssTokenType.Hash: Insert(SimpleSelector.Id(((CssKeywordToken)token).Data)); return true; case CssTokenType.Ident: Insert(SimpleSelector.Type(((CssKeywordToken)token).Data)); return true; case CssTokenType.Whitespace: Insert(CssCombinator.Descendent); return true; case CssTokenType.Delim: return OnDelim(token); case CssTokenType.Comma: InsertOr(); return true; default: return false; } } private bool OnAttribute(CssToken token) { if (token.Type == CssTokenType.Whitespace) return true; state = State.AttributeOperator; if (token.Type == CssTokenType.Ident) attrName = ((CssKeywordToken)token).Data; else { if (token.Type != 0) { state = State.Data; return false; } attrName = ((CssStringToken)token).Data; } return true; } private bool OnAttributeOperator(CssToken token) { if (token.Type == CssTokenType.Whitespace) return true; state = State.AttributeValue; if (token.Type == CssTokenType.SquareBracketClose) return OnAttributeEnd(token); if (!(token is CssMatchToken) && token.Type != CssTokenType.Delim) { state = State.AttributeEnd; return false; } attrOp = token.ToValue(); return true; } private bool OnAttributeValue(CssToken token) { if (token.Type == CssTokenType.Whitespace) return true; state = State.AttributeEnd; if (token.Type == CssTokenType.Ident) attrValue = ((CssKeywordToken)token).Data; else if (token.Type == CssTokenType.String) { attrValue = ((CssStringToken)token).Data; } else { if (token.Type != CssTokenType.Number) { state = State.Data; return false; } attrValue = ((CssNumberToken)token).Data.ToString(); } return true; } private bool OnAttributeEnd(CssToken token) { if (token.Type == CssTokenType.Whitespace) return true; state = State.Data; if (token.Type == CssTokenType.SquareBracketClose) { switch (attrOp) { case "=": Insert(SimpleSelector.AttrMatch(attrName, attrValue)); break; case "~=": Insert(SimpleSelector.AttrList(attrName, attrValue)); break; case "|=": Insert(SimpleSelector.AttrHyphen(attrName, attrValue)); break; case "^=": Insert(SimpleSelector.AttrBegins(attrName, attrValue)); break; case "$=": Insert(SimpleSelector.AttrEnds(attrName, attrValue)); break; case "*=": Insert(SimpleSelector.AttrContains(attrName, attrValue)); break; case "!=": Insert(SimpleSelector.AttrNotMatch(attrName, attrValue)); break; default: Insert(SimpleSelector.AttrAvailable(attrName)); break; } return true; } return false; } private bool OnPseudoClass(CssToken token) { state = State.Data; if (token.Type == CssTokenType.Colon) { state = State.PseudoElement; return true; } if (token.Type == CssTokenType.Function) { attrName = ((CssKeywordToken)token).Data; attrValue = string.Empty; state = State.PseudoClassFunction; if (nested != null) nested.Reset(); return true; } if (token.Type == CssTokenType.Ident) { ISelector pseudoSelector = GetPseudoSelector(token); if (pseudoSelector == null) return false; Insert(pseudoSelector); return true; } return false; } private bool OnPseudoElement(CssToken token) { if (token.Type == CssTokenType.Ident) { string data = ((CssKeywordToken)token).Data; switch (data) { case "before": Insert(SimpleSelector.PseudoElement(MatchBefore, "before")); return true; case "after": Insert(SimpleSelector.PseudoElement(MatchAfter, "after")); return true; case "selection": Insert(SimpleSelector.PseudoElement((IElement el) => true, "selection")); return true; case "first-line": Insert(SimpleSelector.PseudoElement(MatchFirstLine, "first-line")); return true; case "first-letter": Insert(SimpleSelector.PseudoElement(MatchFirstLetter, "first-letter")); return true; default: Insert(SimpleSelector.PseudoElement((IElement el) => false, data)); return false; } } return false; } private bool OnClass(CssToken token) { state = State.Data; if (token.Type == CssTokenType.Ident) { Insert(SimpleSelector.Class(((CssKeywordToken)token).Data)); return true; } return false; } private bool OnPseudoClassFunction(CssToken token) { if (token.Type == CssTokenType.Whitespace) return true; switch (attrName) { case "nth-child": case "nth-last-child": switch (token.Type) { case CssTokenType.Ident: case CssTokenType.Number: case CssTokenType.Dimension: attrValue += token.ToValue(); return true; case CssTokenType.Delim: { char c = token.Data[0]; if (c == '+' || c == '-') { attrValue += token.Data; return true; } break; } } break; case "not": if (nested == null) nested = new CssSelectorConstructor(); if (token.Type != CssTokenType.RoundBracketClose || nested.state != 0) { nested.Apply(token); return true; } break; case "dir": if (token.Type == CssTokenType.Ident) attrValue = ((CssKeywordToken)token).Data; state = State.PseudoClassFunctionEnd; return true; case "lang": if (token.Type == CssTokenType.Ident) attrValue = ((CssKeywordToken)token).Data; state = State.PseudoClassFunctionEnd; return true; case "contains": if (token.Type == CssTokenType.String) attrValue = ((CssStringToken)token).Data; else if (token.Type == CssTokenType.Ident) { attrValue = ((CssKeywordToken)token).Data; } state = State.PseudoClassFunctionEnd; return true; } return OnPseudoClassFunctionEnd(token); } private bool OnPseudoClassFunctionEnd(CssToken token) { state = State.Data; if (token.Type == CssTokenType.RoundBracketClose) { switch (attrName) { case "nth-child": { ISelector childSelector = GetChildSelector<NthFirstChildSelector>(); if (childSelector == null) return false; Insert(childSelector); return true; } case "nth-last-child": { ISelector childSelector2 = GetChildSelector<NthLastChildSelector>(); if (childSelector2 == null) return false; Insert(childSelector2); return true; } case "not": { ISelector sel = nested.Result; string pseudoClass4 = "not" + "(" + sel.ToCss() + ")"; Insert(SimpleSelector.PseudoClass((IElement el) => !sel.Match(el), pseudoClass4)); return true; } case "dir": { string pseudoClass2 = "dir" + "(" + attrValue + ")"; Insert(SimpleSelector.PseudoClass(delegate(IElement el) { if (el is IHtmlElement) return ((IHtmlElement)el).Direction.Equals(attrValue, StringComparison.OrdinalIgnoreCase); return false; }, pseudoClass2)); return true; } case "lang": { string pseudoClass3 = "lang" + "(" + attrValue + ")"; Insert(SimpleSelector.PseudoClass(delegate(IElement el) { if (el is IHtmlElement) return ((IHtmlElement)el).Language.StartsWith(attrValue, StringComparison.OrdinalIgnoreCase); return false; }, pseudoClass3)); return true; } case "contains": { string pseudoClass = "contains" + "(" + attrValue + ")"; Insert(SimpleSelector.PseudoClass((IElement el) => el.TextContent.Contains(attrValue), pseudoClass)); return true; } default: return true; } } return false; } private void InsertOr() { if (temp != null) { if (group == null) group = new ListSelector(); if (complex != null) { complex.ConcludeSelector(temp); group.Add(complex); complex = null; } else group.Add(temp); temp = null; } } private void Insert(ISelector selector) { if (temp != null) { if (!hasCombinator) { CompoundSelector compoundSelector = temp as CompoundSelector; if (compoundSelector == null) { compoundSelector = new CompoundSelector(); compoundSelector.Add(temp); } compoundSelector.Add(selector); temp = compoundSelector; } else { if (complex == null) complex = new ComplexSelector(); complex.AppendSelector(temp, combinator); combinator = CssCombinator.Descendent; hasCombinator = false; temp = selector; } } else { combinator = CssCombinator.Descendent; hasCombinator = false; temp = selector; } } private void Insert(CssCombinator cssCombinator) { hasCombinator = true; if (cssCombinator != CssCombinator.Descendent) combinator = cssCombinator; } private bool OnDelim(CssToken token) { switch (token.Data[0]) { case ',': InsertOr(); return true; case '>': Insert(CssCombinator.Child); return true; case '+': Insert(CssCombinator.AdjacentSibling); return true; case '~': Insert(CssCombinator.Sibling); return true; case '*': Insert(SimpleSelector.All); return true; case '.': state = State.Class; return true; default: return false; } } private ISelector GetChildSelector<T>() where T : NthChildSelector, ISelector, new { new NthFirstChildSelector(); T val = new T(); if (attrValue.Equals(nthChildOdd, StringComparison.OrdinalIgnoreCase)) { val.step = 2; val.offset = 1; } else if (attrValue.Equals(nthChildEven, StringComparison.OrdinalIgnoreCase)) { val.step = 2; val.offset = 0; } else if (!int.TryParse(attrValue, out val.offset)) { int num = attrValue.IndexOf(nthChildN, StringComparison.OrdinalIgnoreCase); if (attrValue.Length <= 0 || num == -1) return null; string text = attrValue.Substring(0, num).Replace(" ", ""); string text2 = attrValue.Substring(num + 1).Replace(" ", ""); if (text == string.Empty || (text.Length == 1 && text[0] == '+')) val.step = 1; else if (text.Length == 1 && text[0] == '-') { val.step = -1; } else if (!int.TryParse(text, out val.step)) { throw new DomException(ErrorCode.Syntax); } if (text2 == string.Empty) val.offset = 0; else if (!int.TryParse(text2, out val.offset)) { return null; } } return val; } private ISelector GetPseudoSelector(CssToken token) { switch (((CssKeywordToken)token).Data) { case "root": return SimpleSelector.PseudoClass((IElement el) => el.Owner.DocumentElement == el, "root"); case "first-of-type": return SimpleSelector.PseudoClass(delegate(IElement el) { IElement parentElement2 = el.ParentElement; if (parentElement2 == null) return true; for (int i = 0; i < parentElement2.ChildNodes.Length; i++) { if (parentElement2.ChildNodes[i].NodeName == el.NodeName) return parentElement2.ChildNodes[i] == el; } return false; }, "first-of-type"); case "last-of-type": return SimpleSelector.PseudoClass(delegate(IElement el) { IElement parentElement = el.ParentElement; if (parentElement == null) return true; for (int num = parentElement.ChildNodes.Length - 1; num >= 0; num--) { if (parentElement.ChildNodes[num].NodeName == el.NodeName) return parentElement.ChildNodes[num] == el; } return false; }, "last-of-type"); case "only-child": return SimpleSelector.PseudoClass((IElement el) => el.IsOnlyChild(), "only-child"); case "first-child": return FirstChildSelector.Instance; case "last-child": return LastChildSelector.Instance; case "empty": return SimpleSelector.PseudoClass((IElement el) => el.ChildNodes.Length == 0, "empty"); case "link": return SimpleSelector.PseudoClass((IElement el) => el.IsLink(), "link"); case "visited": return SimpleSelector.PseudoClass((IElement el) => el.IsVisited(), "visited"); case "active": return SimpleSelector.PseudoClass((IElement el) => el.IsActive(), "active"); case "hover": return SimpleSelector.PseudoClass((IElement el) => el.IsHovered(), "hover"); case "focus": return SimpleSelector.PseudoClass((IElement el) => el.IsFocused(), "focus"); case "target": return SimpleSelector.PseudoClass(delegate(IElement el) { if (el.Owner != null) return el.Id == el.Owner.Location.Hash; return false; }, "target"); case "enabled": return SimpleSelector.PseudoClass((IElement el) => el.IsEnabled(), "enabled"); case "disabled": return SimpleSelector.PseudoClass((IElement el) => el.IsDisabled(), "disabled"); case "default": return SimpleSelector.PseudoClass((IElement el) => el.IsDefault(), "default"); case "checked": return SimpleSelector.PseudoClass((IElement el) => el.IsChecked(), "checked"); case "indeterminate": return SimpleSelector.PseudoClass((IElement el) => el.IsIndeterminate(), "indeterminate"); case "unchecked": return SimpleSelector.PseudoClass((IElement el) => el.IsUnchecked(), "unchecked"); case "valid": return SimpleSelector.PseudoClass((IElement el) => el.IsValid(), "valid"); case "invalid": return SimpleSelector.PseudoClass((IElement el) => el.IsInvalid(), "invalid"); case "required": return SimpleSelector.PseudoClass((IElement el) => el.IsRequired(), "required"); case "read-only": return SimpleSelector.PseudoClass((IElement el) => el.IsReadOnly(), "read-only"); case "read-write": return SimpleSelector.PseudoClass((IElement el) => el.IsEditable(), "read-write"); case "in-range": return SimpleSelector.PseudoClass((IElement el) => el.IsInRange(), "in-range"); case "out-of-range": return SimpleSelector.PseudoClass((IElement el) => el.IsOutOfRange(), "out-of-range"); case "optional": return SimpleSelector.PseudoClass((IElement el) => el.IsOptional(), "optional"); case "before": return SimpleSelector.PseudoClass(MatchBefore, "before"); case "after": return SimpleSelector.PseudoClass(MatchAfter, "after"); case "first-line": return SimpleSelector.PseudoClass(MatchFirstLine, "first-line"); case "first-letter": return SimpleSelector.PseudoClass(MatchFirstLetter, "first-letter"); default: return null; } } private static bool MatchBefore(IElement element) { return true; } private static bool MatchAfter(IElement element) { return true; } private static bool MatchFirstLine(IElement element) { return true; } private static bool MatchFirstLetter(IElement element) { return true; } } }