Repository: facelessuser/soupsieve

Issue:
Add support for more selectors that are HTML only or may never match due to environment
We could probably implement some things like: :hover, :active, :focus, :target, :visited, etc. Our environment (not in a browser) doesn't actually have these states, so we could have them just have them never match. The library cssselect apparently does this. We can pick up any CSS4 selectors (if any like this) and do the same.

We could also implement HTML only selectors, or selectors that are really only defined for HTML. We would have to make some assumptions based on our environment.  For instance, `:link` would match all elements as in our environment, all links are unvisited.

Basically, we want to work towards having all selectors defined in a way that makes sense for our environment if possible.

This is an initial list that is targeted. It may change. Not all undefined selectors are being targeted. Some may be implemented at a later time.

- [x] :active
- [x] :hover
- [x] :focus
- [x] :focus-within
- [x] :focus-visible
- [x] :target
- [x] :target-within
- [x] :visited
- [x] :current
- [x] :past
- [x] :future
- [x] :enabled
- [x] :disabled
- [x] :required
- [x] :optional
- [x] :link
- [x] :any-link
- [x] :default
- [x] :indeterminate
- [x] :placeholder-shown
- [x] :lang


Discussion:

Patch:
diff --git a/.gitignore b/.gitignore
index 510c73d..3df6727 100644
--- a/.gitignore
+++ b/.gitignore
@@ -112,3 +112,6 @@ dmypy.json
 
 # Pyre type checker
 .pyre/
+
+# Patches
+*.patch
diff --git a/README.md b/README.md
index ac677b2..08a0d8b 100644
--- a/README.md
+++ b/README.md
@@ -15,9 +15,9 @@ filtering with using modern CSS selectors.
 While Beautiful Soup comes with a builtin CSS selection API, it is not without issues. In addition, it also lacks
 support for some more modern CSS features.
 
-Soup Sieve supports a subset of CSS4 selectors which allows for filtering of tags in a Beautiful Soup object. Soup
-Sieve does not attempt to support all CSS4 selectors as many don't make sense in a non-browser environment. Some of the
-supported selectors are:
+Soup Sieve implements most of the CSS4 selectors, though there are a number that don't make sense in a non-browser
+environment. Selectors that cannot provide meaningful functionality simply do not match anything. Some of the supported
+selectors are:
 
 - `.classes`
 - `#ids`
diff --git a/docs/src/dictionary/en-custom.txt b/docs/src/dictionary/en-custom.txt
index 48ce11f..f61e303 100644
--- a/docs/src/dictionary/en-custom.txt
+++ b/docs/src/dictionary/en-custom.txt
@@ -27,6 +27,7 @@ html
 iterable
 linter
 lxml
+matchable
 matcher
 matchers
 namespace
@@ -41,6 +42,8 @@ substring
 tuple
 tuples
 un
+unmatchable
 unpickle
+unvisited
 whitespace
 wordlist
diff --git a/docs/src/markdown/about/changelog.md b/docs/src/markdown/about/changelog.md
index cf1534c..7aae1e8 100644
--- a/docs/src/markdown/about/changelog.md
+++ b/docs/src/markdown/about/changelog.md
@@ -1,5 +1,15 @@
 # Changelog
 
+## 1.1.0
+
+- **NEW**: Adds support for `[attr!=value]` which is equivalent to `:not([attr=value])`.
+- **NEW**: Add support for `:active`, `:focus`, `:hover`, `:visited`, `:target`, `:focus-within`, `:focus-visible`, `:target-within`, `:current()`/`:current`, `:past`, and `:future`, but they will never match as these states don't exist in the Soup Sieve environment.
+- **NEW**: Add support for `:checked`, `:enabled`, `:disabled`, `:required`, `:optional`, `:default`, and `:placeholder-shown` which will only match in HTML documents as these concepts are not defined in XML.
+- **NEW**: Add support for `:link` and `:any-link`, both of which will target all `<a>`, `<area>`, and `<link>` elements with an `href` attribute as all links will be treated as unvisited in Soup Sieve.
+- **NEW**: Add support for `:lang()` (CSS4) which works in XML and HTML.
+- **FIX**: Attributes in the form `prefix:attr` can be matched with the form `[prefix\:attr]` without specifying a namespaces if desired.
+- **FIX**: Fix exception when `[type]` is used (with no value).
+
 ## 1.0.2
 
 - **FIX**: Use proper CSS identifier patterns for tag names, classes, ids, etc. Things like `#3` or `#-3` should not match and should require `#\33` or `#-\33`.
diff --git a/docs/src/markdown/about/development.md b/docs/src/markdown/about/development.md
index 03aef76..8b8fb9c 100644
--- a/docs/src/markdown/about/development.md
+++ b/docs/src/markdown/about/development.md
@@ -171,7 +171,8 @@ class SelectorList:
 Attribute      | Description
 -------------- | -----------
 `selectors`    | A list of `Selector` objects.
-`is_not`       | Are the selectors in the selector list from a `:not()`.
+`is_not`       | The selectors in the selector list are from a `:not()`.
+`is_html`      | The selectors in the selector list are HTML specific.
 
 ### `Selector`
 
@@ -179,22 +180,31 @@ Attribute      | Description
 class Selector:
     """Selector."""
 
-    def __init__(self, tag, ids, classes, attributes, nth, selectors, relation, rel_type, empty, root):
+    def __init__(
+        self, tag, ids, classes, attributes, nth, selectors, relation,
+        rel_type, contains, lang, empty, root, default, indeterminate,
+        no_match
+    ):
         """Initialize."""
 ```
 
-Attribute    | Description
------------- | -----------
-`tag`        | Contains a single [`SelectorTag`](#selectortag) object, or `None`.
-`id`         | Contains a tuple of ids to match. Usually if multiple conflicting ids are present, it simply won't match a tag, but it allows multiple to handle the syntax `tag#1#2` even if it is invalid.
-`classes`    | Contains a tuple of class names to match.
-`attributes` | Contains a tuple of attributes. Each attribute is represented as a [`SelectorAttribute`](#selectorattribute).
-`nth`        | Contains a tuple containing `nth` selectors, each selector being represented as a [`SelectorNth`](#selectornth). `nth` selectors contain things like `:first-child`, `:only-child`, `#!css :nth-child()`, `#!css :nth-of-type()`, etc.
-`selectors`  | Contains a tuple of `SelectorList` objects for each pseudo class selector  part of the compound selector: `#!css :is()`, `#!css :not()`, `#!css :has()`, etc.
-`relation`   | This will contain a `SelectorList` object with one `Selector` object, which could in turn chain an additional relation depending on the complexity of the compound selector.  For instance, `div > p + a` would be a `Selector` for `a` that contains a `relation` for `p` (another `SelectorList` object) which also contains a relation of `div`.  When matching, we would match that the tag is `a`, and then walk its relation chain verifying that they all match. In this case, the relation chain would be a direct, previous sibling of `p`, which has a direct parent of `div`. A `:has()` pseudo class would walk this in the opposite order. `div:has(> p + a)` would verify `div`, and then check for a child of `p` with a sibling of `a`.
-`rel_type`   | `rel_type` is attached to relational selectors. In the case of `#!css div > p + a`, the relational selectors of `div` and `p` would get a relational type of `>` and `+` respectively. `:has()` relational `rel_type` are preceded with `:` to signify a forward looking relation.
-`empty`      | This is `True` if the current selector contained a `:empty` pseudo.
-`root`       | This is `True` if the current compound selector contains `:root`.
+Attribute       | Description
+--------------- | -----------
+`tag`           | Contains a single [`SelectorTag`](#selectortag) object, or `None`.
+`id`            | Contains a tuple of ids to match. Usually if multiple conflicting ids are present, it simply won't match a tag, but it allows multiple to handle the syntax `tag#1#2` even if it is invalid.
+`classes`       | Contains a tuple of class names to match.
+`attributes`    | Contains a tuple of attributes. Each attribute is represented as a [`SelectorAttribute`](#selectorattribute).
+`nth`           | Contains a tuple containing `nth` selectors, each selector being represented as a [`SelectorNth`](#selectornth). `nth` selectors contain things like `:first-child`, `:only-child`, `#!css :nth-child()`, `#!css :nth-of-type()`, etc.
+`selectors`     | Contains a tuple of `SelectorList` objects for each pseudo class selector  part of the compound selector: `#!css :is()`, `#!css :not()`, `#!css :has()`, etc.
+`relation`      | This will contain a `SelectorList` object with one `Selector` object, which could in turn chain an additional relation depending on the complexity of the compound selector.  For instance, `div > p + a` would be a `Selector` for `a` that contains a `relation` for `p` (another `SelectorList` object) which also contains a relation of `div`.  When matching, we would match that the tag is `a`, and then walk its relation chain verifying that they all match. In this case, the relation chain would be a direct, previous sibling of `p`, which has a direct parent of `div`. A `:has()` pseudo class would walk this in the opposite order. `div:has(> p + a)` would verify `div`, and then check for a child of `p` with a sibling of `a`.
+`rel_type`      | `rel_type` is attached to relational selectors. In the case of `#!css div > p + a`, the relational selectors of `div` and `p` would get a relational type of `>` and `+` respectively. `:has()` relational `rel_type` are preceded with `:` to signify a forward looking relation.
+`contains`      | Contains a tuple of strings of content to match in an element.
+`lang`          | Contains a tuple of [`SelectorLang`](#selectorlang) objects.
+`empty`         | This is `True` if the current selector contained a `:empty` pseudo.
+`root`          | This is `True` if the current compound selector contains `:root`.
+`default`       | This compound selector has a `:default` pattern  and requires additional logic to determine if it is the first `submit` button in a form.
+`indeterminate` | This compound selector has a `:indeterminate` pattern and requires additional logic to ensure a `radio` element and all of the `radio` elements with the same `name` under a form are not set.
+`no_match`      | This compound selector has an unmatchable selector, such as `:focus`. Such selectors cannot match as certain states are not applicable to Soup Sieve.
 
 ### `SelectorTag`
 
@@ -248,4 +258,18 @@ Attribute     | Description
 `last`        | `True` if the `nth` pseudo class is a `*last*` variant.
 `selectors`   | A `SelectorList` object representing the `of S` portion of `:nth-chld(an+b [of S]?)`.
 
+### `SelectorLang`
+
+```py3
+class SelectorLang:
+    """Selector language rules."""
+
+    def __init__(self, languages):
+        """Initialize."""
+```
+
+Attribute     | Description
+------------- | -----------
+`languages`  | A list of regular expression objects that match a language pattern.
+
 --8<-- "links.txt"
diff --git a/docs/src/markdown/index.md b/docs/src/markdown/index.md
index 5423adb..20f05fa 100644
--- a/docs/src/markdown/index.md
+++ b/docs/src/markdown/index.md
@@ -6,7 +6,7 @@ Soup Sieve is a CSS selector library designed to be used with [Beautiful Soup 4]
 
 While Beautiful Soup comes with a builtin CSS selection API, it is not without issues. In addition, it also lacks support for some more modern CSS features.
 
-Sieve does not attempt to support all CSS4 selectors as many don't make sense in a non-browser environment. Some of the supported selectors are:
+Soup Sieve implements most of the CSS4 selectors, though there are a number that don't make sense in a non-browser environment. Selectors that cannot provide meaningful functionality simply do not match anything. Some of the supported selectors are:
 
 - `#!css .classes`
 - `#!css #ids`
diff --git a/docs/src/markdown/selectors.md b/docs/src/markdown/selectors.md
index 39febb0..3d666eb 100644
--- a/docs/src/markdown/selectors.md
+++ b/docs/src/markdown/selectors.md
@@ -2,7 +2,9 @@
 
 ## Level 1-4 Selectors
 
-The CSS selectors are based on a limited subset of CSS4 selectors. Primarily support has been added for selectors that were feasible to implement and most likely to get practical use.
+### HTML and XML Selectors
+
+The CSS selectors are based off of the CSS level 4 specification. Primarily support has been added for selectors that were feasible to implement and most likely to get practical use. Selectors that cannot provide meaningful matches, simply match nothing. An example would be `:focus` which will match nothing because elements cannot be focused outside of a browser. Though most of the selectors have been implemented, there are still a few that are not.
 
 Below shows accepted selectors. When speaking about namespaces, they only apply to XML, XHTML, or when dealing with recognized foreign tags in HTML5. You must configure the CSS [namespaces](./api.md#namespaces) when attempting to evaluate namespaces.
 
@@ -37,25 +39,53 @@ Selector                        | Example                             | Descript
 `[attribute*=value]`            | `#!css a[href*="sometext"]`         | Selects every `#!html <a>` element whose `href` attribute value contains the substring `sometext`.
 `[attribute=value i]`           | `#!css [title=flower i]`            | Selects any element with a `title` that equals `flower` regardless of case.
 `[attribute=value s]`           | `#!css [type=submit s]`             | Selects any element with a `type` that equals `submit`. Case sensitivity will be forced.
-`:not(sel, sel)`                | `#!css :not(.some-class, #some-id)` | Selects elements that do not have class `some-class` and ID `some-id`.
-`:is(sel, sel)`                 | `#!css :is(div, .some-class)`       | Selects elements that are not `#!html <div>` and do not have class `some-class`. The alias `:matches` is allowed as well. In CSS4 `:where` is like `:is` except specificity is always zero. Soup Sieve doesn't care about specificity, so `:where` is exactly like `:is`.
-`:has(> sel, + sel)`            | `#!css :has(> div, + p)`            | Selects elements that have a direct child that is a `#!html <div>` or that have sibling of `#!html <p>` immediately following.
+`:empty`                        | `#!css p:empty`                     | Selects every `#!html <p>` element that has no children and either no text. Whitespace and comments are ignored.
 `:first-child`                  | `#!css p:first-child`               | Selects every `#!html <p>` element that is the first child of its parent.
-`:last-child`                   | `#!css p:last-child`                | Selects every `#!html <p>` element that is the last child of its parent.
 `:first-of-type`                | `#!css p:first-of-type`             | Selects every `#!html <p>` element that is the first `#!html <p>` element of its parent.
+`:has(> sel, + sel)`            | `#!css :has(> div, + p)`            | Selects elements that have a direct child that is a `#!html <div>` or that have sibling of `#!html <p>` immediately following.
+`:is(sel, sel)`                 | `#!css :is(div, .some-class)`       | Selects elements that are not `#!html <div>` and do not have class `some-class`. The alias `:matches` is allowed as well. In CSS4 `:where` is like `:is` except specificity is always zero. Soup Sieve doesn't care about specificity, so `:where` is exactly like `:is`.
+`:lang(l1, l2)`                 | `#!css :lang('*-CH', en)`           | Select all elements with language `de-CH`, `it-CH`, `fr-CH`, and `rm-CH`. Will also match `en`, `en-US`, and `en-GB`. See CSS4 specification for more info.
+`:last-child`                   | `#!css p:last-child`                | Selects every `#!html <p>` element that is the last child of its parent.
 `:last-of-type`                 | `#!css p:last-of-type`              | Selects every `#!html <p>` element that is the last `#!html <p>` element of its parent.
-`:only-child`                   | `#!css p:only-child`                | Selects every `#!html <p>` element that is the only child of its parent.
-`:only-of-type`                 | `#!css p:only-of-type`              | Selects every `#!html <p>` element that is the only `#!html <p>` element of its parent.
+`:not(sel, sel)`                | `#!css :not(.some-class, #some-id)` | Selects elements that do not have class `some-class` and ID `some-id`.
 `:nth-child(an+b [of S]?)`      | `#!css p:nth-child(2)`              | Selects every `#!html <p>` element that is the second child of its parent. Please see CSS specification for more info on format.
 `:nth-last-child(an+b [of S]?)` | `#!css p:nth-last-child(2)`         | Selects every `#!html <p>` element that is the second child of its parent, counting from the last child. Please see CSS specification for more info on format.
-`:nth-of-type(an+b)`            | `#!css p:nth-of-type(2)`            | Selects every `#!html <p>` element that is the second `#!html <p>` element of its parent. Please see CSS specification for more info on format.
 `:nth-last-of-type(an+b)`       | `#!css p:nth-last-of-type(2)`       | Selects every `#!html <p>` element that is the second `#!html <p>` element of its parent, counting from the last child. Please see CSS specification for more info on format.
+`:nth-of-type(an+b)`            | `#!css p:nth-of-type(2)`            | Selects every `#!html <p>` element that is the second `#!html <p>` element of its parent. Please see CSS specification for more info on format.
+`:only-child`                   | `#!css p:only-child`                | Selects every `#!html <p>` element that is the only child of its parent.
+`:only-of-type`                 | `#!css p:only-of-type`              | Selects every `#!html <p>` element that is the only `#!html <p>` element of its parent.
 `:root`                         | `#!css :root`                       | Selects the root element. In HTML, this is usually the `#!html <html>` element.
-`:empty`                        | `#!css p:empty`                     | Selects every `#!html <p>` element that has no children and either no text. Whitespace and comments are ignored.
 
 !!! warning "Experimental Selectors"
     `:has()` and `of S` support (in `:nth-child(an+b [of S]?)`) is experimental and may change. There are currently no reference implementations available in any browsers, not to mention the CSS4 specifications have not been finalized, so current implementation is based on our best interpretation. Any issues should be reported.
 
+### HTML Only Selectors
+
+There are a number of selectors that apply specifically to HTML documents. Such selectors will only match tags in HTML documents. Use of these selectors are not restricted from XML, but when used with XML documents, they will never match.
+
+Selectors that require states that only exist within a live HTML document, or are specifically tied to user interaction with a live document are allowed (if implemented), but will never match as well.
+
+Selector                        | Example                             | Description
+------------------------------- | ----------------------------------- | -----------
+`:active`                       | `#!css a:active`                    | Active states are not applicable, so this will never match.
+`:any-link`                     | `#!css a:any-link`                  | All links are treated as unvisited, so this will match every `#!html <a>` element with an `href` attribute.
+`:checked`                      | `#!css input:checked`               | Selects every checked `#!html <input>` element.
+`:current`                      | `#!css p:current`                   | As the document is not rendered, this will never match.
+`:current(sel, sel)`            | `#!css :current(p, li, dt, dd)`     | As the document is not rendered, this will never match.
+`:default`                      | `#!css input:default`               | Selects all `#!html <inputs>` that are the default among their related elements. See CSS specification to learn more about all that this targets.
+`:disabled`                     | `#!css input:disabled`              | Selects every disabled `#!html <input>` element.
+`:enabled`                      | `#!css input:enabled`               | Selects every enabled `#!html <input>` element.
+`:focus`                        | `#!css input:focus`                 | Focus states are not applicable, so this will never match.
+`:future`                       | `#!css p:future`                    | As the document is not rendered, this will never match.
+`:hover`                        | `#!css a:focus`                     | Focus states are not applicable, so this will never match.
+`:link`                         | `#!css a:link`                      | All links are treated as unvisited, so this will match every `#!html <a>` element with an `href` attribute.
+`:optional`                     | `#!css input:optional`              | Select every `#!html <input>` element without a `required` attribute.
+`:past`                         | `#!css p:past`                      | As the document is not rendered, this will never match.
+`:placeholder-shown`            | `#!css input:placeholder-shown`     | Selects every `#!html <input>` element that is showing a placeholder via the `placeholder` attribute.
+`:required`                     | `#!css input:required`              | Select every `#!html <input>` element with a `required` attribute.
+`:target`                       | `#!css #news:target`                | Elements cannot be targeted, so this will never match.
+`:visited`                      | `#!css a:visited`                   | All links are treated unvisited, so this will never match.
+
 ## Custom Selectors
 
 Below is listed non-standard CSS selectors. These can contain useful selectors that were rejected from the official CSS specifications, selectors implemented by other systems such as JQuery, or even selectors specific to Soup Sieve.
@@ -64,6 +94,7 @@ Just because we include selectors from one source, does not mean we have intenti
 
 Selector                        | Example                             | Description
 ------------------------------- | ----------------------------------- | -----------
+`[attribute!=value]`            | `#!css [target!=_blank]`            | Equivalent to `#!css :not([target=_blank])`.
 `:contains(text)`               | `#!css p:contains(text)`            | Select all `#!html <p>` elements that contain "text" in their content, either directly in themselves or indirectly in their decedents.
 
 --8<--
diff --git a/soupsieve/__meta__.py b/soupsieve/__meta__.py
index 77a37c1..402045e 100644
--- a/soupsieve/__meta__.py
+++ b/soupsieve/__meta__.py
@@ -186,5 +186,5 @@ def parse_version(ver, pre=False):
     return Version(major, minor, micro, release, pre, post, dev)
 
 
-__version_info__ = Version(1, 0, 2, "final")
+__version_info__ = Version(1, 1, 0, "final")
 __version__ = __version_info__._get_canonical()
diff --git a/soupsieve/css_match.py b/soupsieve/css_match.py
index 3df84e1..0512cce 100644
--- a/soupsieve/css_match.py
+++ b/soupsieve/css_match.py
@@ -28,6 +28,9 @@ class CSSMatch:
     def __init__(self, selectors, namespaces, flags):
         """Initialize."""
 
+        self.meta_lang = None
+        self.default_forms = []
+        self.indeterminate_forms = []
         self.selectors = selectors
         self.namespaces = namespaces
         self.flags = flags
@@ -69,8 +72,15 @@ class CSSMatch:
                     p = ''
                     a = k
                 # Can't match a prefix attribute as we haven't specified one to match
+                # Try to match it normally as a whole `p:a` as selector may be trying `p\:a`.
                 if not prefix and p:
-                    continue
+                    if (self.is_xml and attr == k) or (not self.is_xml and util.lower(attr) == util.lower(k)):
+                        value = v
+                        break
+                    # Coverage is not finding this even though it is executed.
+                    # Adding a print statement before this (and erasing coverage) causes coverage to find the line.
+                    # Ignore the false positive message.
+                    continue  # pragma: no cover
                 # We can't match our desired prefix attribute as the attribute doesn't have a prefix
                 if prefix and not p and prefix != '*':
                     continue
@@ -416,42 +426,237 @@ class CSSMatch:
                 break
         return match
 
+    def match_default(self, el):
+        """Match default."""
+
+        match = False
+
+        # Find this input's form
+        form = None
+        parent = el.parent
+        while parent and form is None:
+            if util.lower(parent.name) == 'form':
+                form = parent
+            else:
+                parent = parent.parent
+
+        # Look in form cache to see if we've already located its default button
+        found_form = False
+        for f, t in self.default_forms:
+            if f is form:
+                found_form = True
+                if t is el:
+                    match = True
+                break
+
+        # We didn't have the form cached, so look for its default button
+        if not found_form:
+            child_found = False
+            for child in form.descendants:
+                if not isinstance(child, util.TAG):
+                    continue
+                name = util.lower(child.name)
+                # Can't do nested forms (haven't figured out why we never hit this)
+                if name == 'form':  # pragma: no cover
+                    break
+                if name in ('input', 'button'):
+                    for k, v in child.attrs.items():
+                        if util.lower(k) == 'type' and util.lower(v) == 'submit':
+                            child_found = True
+                            self.default_forms.append([form, child])
+                            if el is child:
+                                match = True
+                            break
+                if child_found:
+                    break
+        return match
+
+    def match_indeterminate(self, el):
+        """Match default."""
+
+        match = False
+        name = el.attrs.get('name')
+
+        def get_parent_form(el):
+            """Find this input's form."""
+            form = None
+            parent = el.parent
+            while form is None:
+                if util.lower(parent.name) == 'form':
+                    form = parent
+                    break
+                elif parent.parent:
+                    parent = parent.parent
+                else:
+                    form = parent
+                    break
+            return form
+
+        form = get_parent_form(el)
+
+        # Look in form cache to see if we've already evaluated that its fellow radio buttons are indeterminate
+        found_form = False
+        for f, n, i in self.indeterminate_forms:
+            if f is form and n == name:
+                found_form = True
+                if i is True:
+                    match = True
+                break
+
+        # We didn't have the form cached, so validate that the radio button is indeterminate
+        if not found_form:
+            checked = False
+            for child in form.descendants:
+                if not isinstance(child, util.TAG) or child is el:
+                    continue
+                tag_name = util.lower(child.name)
+                if tag_name == 'input':
+                    is_radio = False
+                    check = False
+                    has_name = False
+                    for k, v in child.attrs.items():
+                        if util.lower(k) == 'type' and util.lower(v) == 'radio':
+                            is_radio = True
+                        elif util.lower(k) == 'name' and v == name:
+                            has_name = True
+                        elif util.lower(k) == 'checked':
+                            check = True
+                        if is_radio and check and has_name and get_parent_form(child) is form:
+                            checked = True
+                            break
+                if checked:
+                    break
+            if not checked:
+                match = True
+            self.indeterminate_forms.append([form, name, match])
+
+        return match
+
+    def match_lang(self, el, langs):
+        """Match languages."""
+
+        match = False
+        has_ns = self.supports_namespaces()
+
+        # Walk parents looking for `lang` (HTML) or `xml:lang` XML property.
+        parent = el
+        found_lang = None
+        while parent.parent and not found_lang:
+            ns = self.is_html_ns(parent)
+            for k, v in parent.attrs.items():
+                if (
+                    (self.is_xml and k == 'xml:lang') or
+                    (
+                        not self.is_xml and (
+                            ((not has_ns or ns) and util.lower(k) == 'lang') or
+                            (has_ns and not ns and util.lower(k) == 'xml:lang')
+                        )
+                    )
+                ):
+                    found_lang = v
+                    break
+            parent = parent.parent
+
+        # Use cached meta language.
+        if not found_lang and self.meta_lang:
+            found_lang = self.meta_lang
+
+        # If we couldn't find a language, and the document is HTML, look to meta to determine language.
+        if not found_lang and not self.is_xml:
+            found = False
+            for tag in ('html', 'head'):
+                found = False
+                for child in parent.children:
+                    if isinstance(child, util.TAG) and util.lower(child.name) == tag:
+                        found = True
+                        parent = child
+                        break
+                if not found:  # pragma: no cover
+                    break
+
+            if found:
+                for child in parent:
+                    if isinstance(child, util.TAG) and util.lower(child.name) == 'meta':
+                        c_lang = False
+                        content = None
+                        for k, v in child.attrs.items():
+                            if util.lower(k) == 'http-equiv' and util.lower(v) == 'content-language':
+                                c_lang = True
+                            if util.lower(k) == 'content':
+                                content = v
+                            if c_lang and content:
+                                found_lang = content
+                                self.meta_lang = found_lang
+                                break
+                    if found_lang:
+                        break
+
+        # If we determined a language, compare.
+        if found_lang:
+            for patterns in langs:
+                match = False
+                for pattern in patterns:
+                    # print('PATTERN: ', pattern)
+                    if pattern.match(found_lang):
+                        # print('MATCHED')
+                        match = True
+                if not match:
+                    break
+
+        return match
+
     def match_selectors(self, el, selectors):
         """Check if element matches one of the selectors."""
 
         match = False
         is_not = selectors.is_not
-        for selector in selectors:
-            match = is_not
-            # Verify tag matches
-            if not self.match_tag(el, selector.tag):
-                continue
-            # Verify `nth` matches
-            if not self.match_nth(el, selector.nth):
-                continue
-            if not self.match_empty(el, selector.empty):
-                continue
-            # Verify id matches
-            if selector.ids and not self.match_id(el, selector.ids):
-                continue
-            # Verify classes match
-            if selector.classes and not self.match_classes(el, selector.classes):
-                continue
-            # Verify attribute(s) match
-            if not self.match_attributes(el, selector.attributes):
-                continue
-            if selector.root and not self.match_root(el):
-                continue
-            # Verify pseudo selector patterns
-            if selector.selectors and not self.match_subselectors(el, selector.selectors):
-                continue
-            # Verify relationship selectors
-            if selector.relation and not self.match_relations(el, selector.relation):
-                continue
-            if not self.match_contains(el, selector.contains):
-                continue
-            match = not is_not
-            break
+        is_html = selectors.is_html
+        if not (is_html and self.is_xml):
+            for selector in selectors:
+                match = is_not
+                # We have a un-matchable situation (like `:focus` as you can focus an element in this environment)
+                if selector.no_match:
+                    continue
+                # Verify tag matches
+                if not self.match_tag(el, selector.tag):
+                    continue
+                # Verify `nth` matches
+                if not self.match_nth(el, selector.nth):
+                    continue
+                if not self.match_empty(el, selector.empty):
+                    continue
+                # Verify id matches
+                if selector.ids and not self.match_id(el, selector.ids):
+                    continue
+                # Verify classes match
+                if selector.classes and not self.match_classes(el, selector.classes):
+                    continue
+                # Verify attribute(s) match
+                if not self.match_attributes(el, selector.attributes):
+                    continue
+                # Verify element is root
+                if selector.root and not self.match_root(el):
+                    continue
+                # Verify language patterns
+                if selector.lang and not self.match_lang(el, selector.lang):
+                    continue
+                # Verify pseudo selector patterns
+                if selector.selectors and not self.match_subselectors(el, selector.selectors):
+                    continue
+                # Verify relationship selectors
+                if selector.relation and not self.match_relations(el, selector.relation):
+                    continue
+                # Validate that the current default selector match corresponds to the first submit button in the form
+                if selector.default and not self.match_default(el):
+                    continue
+                # Validate that the unset radio button is among radio buttons with the same name in a form that are
+                # also not set.
+                if selector.indeterminate and not self.match_indeterminate(el):
+                    continue
+                if not self.match_contains(el, selector.contains):
+                    continue
+                match = not is_not
+                break
 
         return match
 
@@ -496,9 +701,11 @@ class SoupSieve(util.Immutable):
     def _walk(self, node, capture=True, comments=False):
         """Recursively return selected tags."""
 
+        match = CSSMatch(self.selectors, self.namespaces, self.flags).match
+
         # Walk children
         for child in node.descendants:
-            if capture and isinstance(child, util.TAG) and self.match(child):
+            if capture and isinstance(child, util.TAG) and match(child):
                 yield child
             elif comments and isinstance(child, util.COMMENT):
                 yield child
diff --git a/soupsieve/css_parser.py b/soupsieve/css_parser.py
index 1fe7086..bae9681 100644
--- a/soupsieve/css_parser.py
+++ b/soupsieve/css_parser.py
@@ -6,27 +6,68 @@ from . import css_match as cm
 from . import css_types as ct
 from collections import OrderedDict
 
-SUPPORTED_PSEUDO = {
+# Simple pseudo classes that take no parameters
+PSEUDO_SIMPLE = {
     ":root",
     ":empty",
-    ":has",
-    ":contains",
-    ":is",
-    ":matches",
-    ":not",
-    ":where",
+    ":link",
+    ":any-link",
     ":first-child",
     ":first-of-type",
     ":last-child",
     ":last-of-type",
     ":only-child",
     ":only-of-type",
+    ':checked',
+    ':disabled',
+    ':enabled',
+    ':required',
+    ':optional',
+    ':default',
+    ':indeterminate',
+    ':placeholder-shown'
+}
+
+# Supported, simple pseudo classes that match nothing in the Soup Sieve environment
+PSEUDO_SIMPLE_NO_MATCH = {
+    ":visited",
+    ':hover',
+    ':active',
+    ':focus',
+    ':target',
+    ':current',
+    ':past',
+    ':future',
+    ':focus-within',
+    ':focus-visible',
+    ':target-within'
+}
+
+# Complex pseudo classes that take selector lists
+PSEUDO_COMPLEX = {
+    ":has",
+    ":contains",
+    ":is",
+    ":matches",
+    ":not",
+    ":where"
+}
+
+PSEUDO_COMPLEX_NO_MATCH = {
+    ":current"
+}
+
+# Complex pseudo classes that take very specific parameters and are handled special
+PSEUDO_SPECIAL = {
     ":nth-child",
     ":nth-last-child",
     ":nth-of-type",
-    ":nth-last-of-type"
+    ":nth-last-of-type",
+    ":lang"
 }
 
+PSEUDO_SUPPORTED = PSEUDO_SIMPLE | PSEUDO_SIMPLE_NO_MATCH | PSEUDO_COMPLEX | PSEUDO_COMPLEX_NO_MATCH | PSEUDO_SPECIAL
+
 # Sub-patterns parts
 WS = r'[ \t\r\n\f]'
 
@@ -39,14 +80,15 @@ IDENTIFIER = r'''
 NTH = r'(?:[-+])?(?:\d+n?|n)(?:(?<=n){ws}*(?:[-+]){ws}*(?:\d+))?'.format(ws=WS)
 
 VALUE = r'''
-(?P<value>
-    "(?:\\.|[^\\"]+)*?"|
-    '(?:\\.|[^\\']+)*?'|
+(?:
+    "(?:\\.|[^\\"]*)*?"|
+    '(?:\\.|[^\\']*)*?'|
     {ident}+
 )
 '''.format(ident=IDENTIFIER)
 
-ATTR = r'''(?:{ws}*(?P<cmp>[~^|*$]?=){ws}*{value}(?P<case>{ws}+[is])?)?{ws}*\]'''.format(ws=WS, value=VALUE)
+# Attribute value comparison. `!=` is handled special as it is non-standard.
+ATTR = r'''(?:{ws}*(?P<cmp>[!~^|*$]?=){ws}*(?P<value>{value})(?P<case>{ws}+[is])?)?{ws}*\]'''.format(ws=WS, value=VALUE)
 
 # Selector patterns
 PAT_ID = r'\#{ident}'.format(ident=IDENTIFIER)
@@ -60,11 +102,9 @@ PAT_ATTR = r'''
 {attr}
 '''.format(ws=WS, ident=IDENTIFIER, attr=ATTR)
 
-PAT_PSEUDO_OPEN = r':(?:has|is|matches|not|where)\('
-
 PAT_PSEUDO_CLOSE = r'{ws}*\)'.format(ws=WS)
 
-PAT_PSEUDO = r':(?:empty|root|(?:first|last|only)-(?:child|of-type))\b'
+PAT_PSEUDO = r':{ident}+(?:\({ws}*)?'.format(ws=WS, ident=IDENTIFIER)
 
 PAT_PSEUDO_NTH_CHILD = r'''
 (?P<pseudo_nth_child>:nth-(?:last-)?child
@@ -76,12 +116,12 @@ PAT_PSEUDO_NTH_TYPE = r'''
 \({ws}*(?P<nth_type>{nth}|even|odd){ws}*\))
 '''.format(ws=WS, nth=NTH)
 
+PAT_PSEUDO_LANG = r':lang\({ws}*(?P<lang>{value}(?:{ws}*,{ws}*{value})*){ws}*\)'.format(ws=WS, value=VALUE)
+
 PAT_SPLIT = r'{ws}*?(?P<relation>[,+>~]|{ws}(?![,+>~])){ws}*'.format(ws=WS)
 
 # Extra selector patterns
-PAT_PSEUDO_CONTAINS = r':contains\({ws}*{value}{ws}*\)'.format(ws=WS, value=VALUE)
-
-PAT_PSEUDO_INVALID = r':{ident}'.format(ident=IDENTIFIER)
+PAT_PSEUDO_CONTAINS = r':contains\({ws}*(?P<value>{value}){ws}*\)'.format(ws=WS, value=VALUE)
 
 # CSS escape pattern
 RE_CSS_ESC = re.compile(r'(?:(\\[a-f0-9]{{1,6}}{ws}?)|(\\.))'.format(ws=WS), re.I)
@@ -89,9 +129,19 @@ RE_CSS_ESC = re.compile(r'(?:(\\[a-f0-9]{{1,6}}{ws}?)|(\\.))'.format(ws=WS), re.
 # Pattern to break up `nth` specifiers
 RE_NTH = re.compile(r'(?P<s1>[-+])?(?P<a>\d+n?|n)(?:(?<=n){ws}*(?P<s2>[-+]){ws}*(?P<b>\d+))?'.format(ws=WS), re.I)
 
+RE_LANG = re.compile(r'(?:(?P<value>{value})|(?P<split>{ws}*,{ws}*))'.format(ws=WS, value=VALUE), re.X)
+
 SPLIT = ','
 REL_HAS_CHILD = ": "
 
+# Parse flags
+FLG_PSEUDO = 0x01
+FLG_NOT = 0x02
+FLG_HAS = 0x04
+FLG_DEFAULT = 0x08
+FLG_HTML = 0x10
+FLG_INDETERMINATE = 0x20
+
 _MAXCACHE = 500
 
 
@@ -159,8 +209,12 @@ class _Selector:
         self.relations = kwargs.get('relations', [])
         self.rel_type = kwargs.get('rel_type', None)
         self.contains = kwargs.get('contains', [])
+        self.lang = kwargs.get('lang', [])
         self.empty = kwargs.get('empty', False)
         self.root = kwargs.get('root', False)
+        self.default = kwargs.get('default', False)
+        self.indeterminate = kwargs.get('indeterminate', False)
+        self.no_match = kwargs.get('no_match', False)
 
     def _freeze_relations(self, relations):
         """Freeze relation."""
@@ -185,8 +239,12 @@ class _Selector:
             self._freeze_relations(self.relations),
             self.rel_type,
             tuple(self.contains),
+            tuple(self.lang),
             self.empty,
-            self.root
+            self.root,
+            self.default,
+            self.indeterminate,
+            self.no_match,
         )
 
     def __str__(self):  # pragma: no cover
@@ -194,10 +252,10 @@ class _Selector:
 
         return (
             '_Selector(tag=%r, ids=%r, classes=%r, attributes=%r, nth=%r, selectors=%r, '
-            'relations=%r, rel_type=%r, empty=%r, root=%r)'
+            'relations=%r, rel_type=%r, contains=%r, lang=%r, empty=%r, root=%r)'
         ) % (
             self.tag, self.ids, self.classes, self.attributes, self.nth, self.selectors,
-            self.relations, self.rel_type, self.empty, self.root
+            self.relations, self.rel_type, self.contains, self.lang, self.empty, self.root
         )
 
     __repr__ = __str__
@@ -208,13 +266,12 @@ class CSSParser:
 
     css_tokens = OrderedDict(
         [
-            ("pseudo_open", SelectorPattern(PAT_PSEUDO_OPEN)),
             ("pseudo_close", SelectorPattern(PAT_PSEUDO_CLOSE)),
-            ("pseudo", SelectorPattern(PAT_PSEUDO)),
             ("pseudo_contains", SelectorPattern(PAT_PSEUDO_CONTAINS)),
             ("pseudo_nth_child", SelectorPattern(PAT_PSEUDO_NTH_CHILD)),
             ("pseudo_nth_type", SelectorPattern(PAT_PSEUDO_NTH_TYPE)),
-            ("pseudo_invalid", SelectorPattern(PAT_PSEUDO_INVALID)),
+            ("pseudo_lang", SelectorPattern(PAT_PSEUDO_LANG)),
+            ("pseudo", SelectorPattern(PAT_PSEUDO)),
             ("id", SelectorPattern(PAT_ID)),
             ("class", SelectorPattern(PAT_CLASS)),
             ("tag", SelectorPattern(PAT_TAG)),
@@ -231,62 +288,79 @@ class CSSParser:
         dflags = self.flags & util.DEPRECATED_FLAGS
         if dflags:
             util.warn_deprecated(
-                "The following flags are deprecated and may be repurposed in the future '0x%02X'" % dflags
+                "The following flags are deprecated and may be repurposed in the future '0x%02X'" % dflags,
+                stacklevel=3
             )
 
     def parse_attribute_selector(self, sel, m, has_selector):
         """Create attribute selector from the returned regex match."""
 
-        case = util.lower(m.group('case').strip()) if m.group('case') else None
-        parts = [css_unescape(a.strip()) for a in m.group('ns_attr').split('|')]
-        ns = ''
-        is_type = False
-        pattern2 = None
-        if len(parts) > 1:
-            ns = parts[0]
-            attr = parts[1]
-        else:
-            attr = parts[0]
-        if case:
-            flags = re.I if case == 'i' else 0
-        elif util.lower(attr) == 'type':
-            flags = re.I
-            is_type = True
-        else:
-            flags = 0
-
         op = m.group('cmp')
-        if op:
-            value = css_unescape(
-                m.group('value')[1:-1] if m.group('value').startswith(('"', "'")) else m.group('value')
+        if op and op.startswith('!'):
+            # Equivalent to `:not([attr=value])`
+            attr = m.group('ns_attr')
+            value = m.group('value')
+            case = m.group('case')
+            if not case:
+                case = ''
+            sel.selectors.append(
+                self.parse_selectors(
+                    # Simulate the content of `:not`, but make the attribute as `=` instead of `!=`.
+                    self.selector_iter('[{}={} {}])'.format(attr, value, case)),
+                    FLG_PSEUDO | FLG_NOT
+                )
             )
+            has_selector = True
         else:
-            value = None
-        if not op:
-            # Attribute name
-            pattern = None
-        elif op.startswith('^'):
-            # Value start with
-            pattern = re.compile(r'^%s.*' % re.escape(value), flags)
-        elif op.startswith('$'):
-            # Value ends with
-            pattern = re.compile(r'.*?%s$' % re.escape(value), flags)
-        elif op.startswith('*'):
-            # Value contains
-            pattern = re.compile(r'.*?%s.*' % re.escape(value), flags)
-        elif op.startswith('~'):
-            # Value contains word within space separated list
-            pattern = re.compile(r'.*?(?:(?<=^)|(?<= ))%s(?=(?:[ ]|$)).*' % re.escape(value), flags)
-        elif op.startswith('|'):
-            # Value starts with word in dash separated list
-            pattern = re.compile(r'^%s(?:-.*)?$' % re.escape(value), flags)
-        else:
-            # Value matches
-            pattern = re.compile(r'^%s$' % re.escape(value), flags)
-        if is_type:
-            pattern2 = re.compile(pattern.pattern)
-        has_selector = True
-        sel.attributes.append(ct.SelectorAttribute(attr, ns, pattern, pattern2))
+            case = util.lower(m.group('case').strip()) if m.group('case') else None
+            parts = [css_unescape(a.strip()) for a in m.group('ns_attr').split('|')]
+            ns = ''
+            is_type = False
+            pattern2 = None
+            if len(parts) > 1:
+                ns = parts[0]
+                attr = parts[1]
+            else:
+                attr = parts[0]
+            if case:
+                flags = re.I if case == 'i' else 0
+            elif util.lower(attr) == 'type':
+                flags = re.I
+                is_type = True
+            else:
+                flags = 0
+
+            if op:
+                value = css_unescape(
+                    m.group('value')[1:-1] if m.group('value').startswith(('"', "'")) else m.group('value')
+                )
+            else:
+                value = None
+            if not op:
+                # Attribute name
+                pattern = None
+            elif op.startswith('^'):
+                # Value start with
+                pattern = re.compile(r'^%s.*' % re.escape(value), flags)
+            elif op.startswith('$'):
+                # Value ends with
+                pattern = re.compile(r'.*?%s$' % re.escape(value), flags)
+            elif op.startswith('*'):
+                # Value contains
+                pattern = re.compile(r'.*?%s.*' % re.escape(value), flags)
+            elif op.startswith('~'):
+                # Value contains word within space separated list
+                pattern = re.compile(r'.*?(?:(?<=^)|(?<= ))%s(?=(?:[ ]|$)).*' % re.escape(value), flags)
+            elif op.startswith('|'):
+                # Value starts with word in dash separated list
+                pattern = re.compile(r'^%s(?:-.*)?$' % re.escape(value), flags)
+            else:
+                # Value matches
+                pattern = re.compile(r'^%s$' % re.escape(value), flags)
+            if is_type and pattern:
+                pattern2 = re.compile(pattern.pattern)
+            has_selector = True
+            sel.attributes.append(ct.SelectorAttribute(attr, ns, pattern, pattern2))
         return has_selector
 
     def parse_tag_pattern(self, sel, m, has_selector):
@@ -303,38 +377,167 @@ class CSSParser:
         has_selector = True
         return has_selector
 
-    def parse_pseudo(self, sel, m, has_selector):
+    def parse_pseudo(self, sel, m, has_selector, iselector):
         """Parse pseudo."""
 
-        pseudo = util.lower(m.group(0)[1:])
-        if pseudo == 'root':
-            sel.root = True
-        elif pseudo == 'empty':
-            sel.empty = True
-        elif pseudo == 'first-child':
-            sel.nth.append(ct.SelectorNth(1, False, 0, False, False, ct.SelectorList()))
-        elif pseudo == 'last-child':
-            sel.nth.append(ct.SelectorNth(1, False, 0, False, True, ct.SelectorList()))
-        elif pseudo == 'first-of-type':
-            sel.nth.append(ct.SelectorNth(1, False, 0, True, False, ct.SelectorList()))
-        elif pseudo == 'last-of-type':
-            sel.nth.append(ct.SelectorNth(1, False, 0, True, True, ct.SelectorList()))
-        elif pseudo == 'only-child':
-            sel.nth.extend(
-                [
-                    ct.SelectorNth(1, False, 0, False, False, ct.SelectorList()),
-                    ct.SelectorNth(1, False, 0, False, True, ct.SelectorList())
-                ]
-            )
-        elif pseudo == 'only-of-type':
-            sel.nth.extend(
-                [
-                    ct.SelectorNth(1, False, 0, True, False, ct.SelectorList()),
-                    ct.SelectorNth(1, False, 0, True, True, ct.SelectorList())
-                ]
+        complex_pseudo = False
+        pseudo = util.lower(m.group(0)).strip()
+        if pseudo.endswith('('):
+            complex_pseudo = True
+            pseudo = pseudo[:-1]
+        if complex_pseudo and pseudo in PSEUDO_COMPLEX:
+            has_selector = self.parse_pseudo_open(sel, pseudo, has_selector, iselector)
+        elif not complex_pseudo and pseudo in PSEUDO_SIMPLE:
+            if pseudo == ':root':
+                sel.root = True
+            elif pseudo == ':empty':
+                sel.empty = True
+            elif pseudo in (':link', ':any-link'):
+                sel.selectors.append(
+                    self.parse_selectors(
+                        self.selector_iter(':is(a, area, link)[href])'),
+                        FLG_PSEUDO | FLG_HTML
+                    )
+                )
+            elif pseudo == ':checked':
+                sel.selectors.append(
+                    self.parse_selectors(
+                        self.selector_iter(
+                            '''
+                            :is(input[type=checkbox], input[type=radio])[checked],
+                            select > option[selected])
+                            '''
+                        ),
+                        FLG_PSEUDO | FLG_HTML
+                    )
+                )
+            elif pseudo == ':default':
+                sel.selectors.append(
+                    self.parse_selectors(
+                        self.selector_iter(':checked, form :is(button, input)[type="submit"])'),
+                        FLG_PSEUDO | FLG_HTML | FLG_DEFAULT
+                    )
+                )
+            elif pseudo == ':indeterminate':
+                sel.selectors.append(
+                    self.parse_selectors(
+                        self.selector_iter(
+                            '''
+                            input[type="checkbox"][indeterminate],
+                            input[type="radio"]:is(:not([name]), [name=""]):not([checked]),
+                            progress:not([value]),
+                            input[type="radio"][name][name!='']:not([checked]))
+                            '''
+                        ),
+                        FLG_PSEUDO | FLG_HTML | FLG_INDETERMINATE
+                    )
+                )
+            elif pseudo == ":disabled":
+                sel.selectors.append(
+                    self.parse_selectors(
+                        self.selector_iter(
+                            '''
+                            :is(input[type!=hidden], button, select, textarea, fieldset, optgroup, option)[disabled],
+                            optgroup[disabled] > option,
+                            fieldset[disabled] > :not(legend) :is(input[type!=hidden], button, select, textarea),
+                            fieldset[disabled] > :is(input[type!=hidden], button, select, textarea))
+                            '''
+                        ),
+                        FLG_PSEUDO | FLG_HTML
+                    )
+                )
+            elif pseudo == ":enabled":
+                sel.selectors.append(
+                    self.parse_selectors(
+                        self.selector_iter(
+                            '''
+                            :is(a, area, link)[href],
+                            :is(fieldset, optgroup):not([disabled]),
+                            option:not(optgroup[disabled] *):not([disabled]),
+                            :is(input[type!=hidden], button, select, textarea):not(
+                                fieldset[disabled] > :not(legend) *,
+                                fieldset[disabled] > *
+                            ):not([disabled]))
+                            '''
+                        ),
+                        FLG_PSEUDO | FLG_HTML
+                    )
+                )
+            elif pseudo == ":required":
+                sel.selectors.append(
+                    self.parse_selectors(
+                        self.selector_iter(':is(input, textarea, select)[required])'),
+                        FLG_PSEUDO | FLG_HTML
+                    )
+                )
+            elif pseudo == ":optional":
+                sel.selectors.append(
+                    self.parse_selectors(
+                        self.selector_iter(':is(input, textarea, select):not([required]))'),
+                        FLG_PSEUDO | FLG_HTML
+                    )
+                )
+            elif pseudo == ":placeholder-shown":
+                sel.selectors.append(
+                    self.parse_selectors(
+                        self.selector_iter(
+                            '''
+                            :is(
+                                input:is(
+                                    [type=text],
+                                    [type=search],
+                                    [type=url],
+                                    [type=tel],
+                                    [type=email],
+                                    [type=password],
+                                    [type=number],
+                                    :not([type]),
+                                    [type=""]
+                                ),
+                                textarea
+                            )[placeholder][placeholder!=''])
+                            '''
+                        ),
+                        FLG_PSEUDO | FLG_HTML
+                    )
+                )
+            elif pseudo == ':first-child':
+                sel.nth.append(ct.SelectorNth(1, False, 0, False, False, ct.SelectorList()))
+            elif pseudo == ':last-child':
+                sel.nth.append(ct.SelectorNth(1, False, 0, False, True, ct.SelectorList()))
+            elif pseudo == ':first-of-type':
+                sel.nth.append(ct.SelectorNth(1, False, 0, True, False, ct.SelectorList()))
+            elif pseudo == ':last-of-type':
+                sel.nth.append(ct.SelectorNth(1, False, 0, True, True, ct.SelectorList()))
+            elif pseudo == ':only-child':
+                sel.nth.extend(
+                    [
+                        ct.SelectorNth(1, False, 0, False, False, ct.SelectorList()),
+                        ct.SelectorNth(1, False, 0, False, True, ct.SelectorList())
+                    ]
+                )
+            elif pseudo == ':only-of-type':
+                sel.nth.extend(
+                    [
+                        ct.SelectorNth(1, False, 0, True, False, ct.SelectorList()),
+                        ct.SelectorNth(1, False, 0, True, True, ct.SelectorList())
+                    ]
+                )
+            has_selector = True
+        elif complex_pseudo and pseudo in PSEUDO_COMPLEX_NO_MATCH:
+            self.parse_selectors(iselector, FLG_PSEUDO)
+            sel.no_match = True
+            has_selector = True
+        elif not complex_pseudo and pseudo in PSEUDO_SIMPLE_NO_MATCH:
+            sel.no_match = True
+            has_selector = True
+        elif pseudo in PSEUDO_SUPPORTED:
+            raise SyntaxError("Invalid syntax for pseudo class '{}'".format(pseudo))
+        else:
+            raise NotImplementedError(
+                "'{}' pseudo class/element is not implemented at this time".format(pseudo)
             )
 
-        has_selector = True
         return has_selector
 
     def parse_pseudo_nth(self, sel, m, has_selector, iselector):
@@ -378,12 +581,7 @@ class CSSParser:
             else:
                 # Use default `*|*` for `of S`. Simulate un-closed pseudo.
                 temp_sel = self.selector_iter('*|*)')
-            nth_sel = self.parse_selectors(
-                temp_sel,
-                True,
-                False,
-                False
-            )
+            nth_sel = self.parse_selectors(temp_sel, FLG_PSEUDO)
             if pseudo_sel.startswith(':nth-child'):
                 sel.nth.append(ct.SelectorNth(s1, var, s2, False, False, nth_sel))
             elif pseudo_sel.startswith(':nth-last-child'):
@@ -396,22 +594,20 @@ class CSSParser:
         has_selector = True
         return has_selector
 
-    def parse_pseudo_open(self, sel, m, has_selector, iselector, is_pseudo):
+    def parse_pseudo_open(self, sel, name, has_selector, iselector):
         """Parse pseudo with opening bracket."""
 
-        name = util.lower(m.group(0)[1:-1])
-        sel.selectors.append(
-            self.parse_selectors(
-                iselector,
-                True,
-                name == 'not',
-                name == 'has'
-            )
-        )
+        flags = FLG_PSEUDO
+        if name == ':not':
+            flags |= FLG_NOT
+        if name == ':has':
+            flags |= FLG_HAS
+
+        sel.selectors.append(self.parse_selectors(iselector, flags))
         has_selector = True
         return has_selector
 
-    def parse_has_split(self, sel, m, has_selector, selectors, is_pseudo, rel_type):
+    def parse_has_split(self, sel, m, has_selector, selectors, rel_type):
         """Parse splitting tokens."""
 
         if m.group('relation') == SPLIT:
@@ -468,17 +664,46 @@ class CSSParser:
             has_selector = True
         return has_selector
 
-    def parse_contains(self, sel, m, has_selector):
+    def parse_pseudo_contains(self, sel, m, has_selector):
         """Parse contains."""
 
-        content = css_unescape(m.group('value').strip())
+        content = m.group('value').strip()
         if content.startswith(("'", '"')):
             content = content[1:-1]
+        content = css_unescape(content)
         sel.contains.append(content)
         has_selector = True
         return has_selector
 
-    def parse_selectors(self, iselector, is_pseudo=False, is_not=False, is_has=False):
+    def parse_pseudo_lang(self, sel, m, has_selector):
+        """Parse pseudo language."""
+
+        lang = m.group('lang')
+        patterns = []
+        for token in RE_LANG.finditer(lang):
+            if token.group('split'):
+                continue
+            value = token.group('value')
+            if value.startswith(('"', "'")):
+                value = value[1:-1]
+            parts = css_unescape(value).split('-')
+
+            new_parts = []
+            first = True
+            for part in parts:
+                if part == '*' and first:
+                    new_parts.append('(?!x\b)[a-z0-9]+?')
+                elif part != '*':
+                    new_parts.append(('' if first else '(-(?!x\b)[a-z0-9]+)*?\\-') + re.escape(part))
+                if first:
+                    first = False
+            patterns.append(re.compile(r'^{}(?:-.*)?$'.format(''.join(new_parts)), re.I))
+        sel.lang.append(ct.SelectorLang(patterns))
+        has_selector = True
+
+        return has_selector
+
+    def parse_selectors(self, iselector, flags=0):
         """Parse selectors."""
 
         sel = _Selector()
@@ -488,6 +713,12 @@ class CSSParser:
         relations = []
         rel_type = REL_HAS_CHILD
         split_last = False
+        is_pseudo = flags & FLG_PSEUDO
+        is_has = flags & FLG_HAS
+        is_not = flags & FLG_NOT
+        is_html = flags & FLG_HTML
+        is_default = flags & FLG_DEFAULT
+        is_indeterminate = flags & FLG_INDETERMINATE
         if is_has:
             selectors.append(_Selector())
 
@@ -496,21 +727,14 @@ class CSSParser:
                 key, m = next(iselector)
 
                 # Handle parts
-                if key == 'pseudo_invalid':
-                    if util.lower(m.group(0)) in SUPPORTED_PSEUDO:
-                        raise SyntaxError("Invalid syntax for pseudo class '{}'".format(key))
-                    else:
-                        raise NotImplementedError(
-                            "'{}' pseudo class/element is not implemented at this time".format(key)
-                        )
-                elif key == 'pseudo':
-                    has_selector = self.parse_pseudo(sel, m, has_selector)
+                if key == 'pseudo':
+                    has_selector = self.parse_pseudo(sel, m, has_selector, iselector)
                 elif key == 'pseudo_contains':
-                    has_selector = self.parse_contains(sel, m, has_selector)
+                    has_selector = self.parse_pseudo_contains(sel, m, has_selector)
                 elif key in ('pseudo_nth_type', 'pseudo_nth_child'):
                     has_selector = self.parse_pseudo_nth(sel, m, has_selector, iselector)
-                elif key == 'pseudo_open':
-                    has_selector = self.parse_pseudo_open(sel, m, has_selector, iselector, is_pseudo)
+                elif key == 'pseudo_lang':
+                    has_selector = self.parse_pseudo_lang(sel, m, has_selector)
                 elif key == 'pseudo_close':
                     if split_last:
                         raise SyntaxError("Cannot end with a combining character")
@@ -524,7 +748,7 @@ class CSSParser:
                         raise SyntaxError("Cannot have combining character directly after a combining character")
                     if is_has:
                         has_selector, sel, rel_type = self.parse_has_split(
-                            sel, m, has_selector, selectors, is_pseudo, rel_type
+                            sel, m, has_selector, selectors, rel_type
                         )
                     else:
                         has_selector, sel = self.parse_split(sel, m, has_selector, selectors, relations, is_pseudo)
@@ -563,11 +787,20 @@ class CSSParser:
             # We will always need to finish a selector when `:has()` is used as it leads with combining.
             raise SyntaxError('Missing selectors after combining type.')
 
-        return ct.SelectorList([s.freeze() for s in selectors], is_not)
+        # For default, the last patter in the list will be one that requires additional
+        # logic, flag that selector as "default" so the required logic will be executed
+        # along with the pattern.
+        if is_default:
+            selectors[-1].default = True
+        if is_indeterminate:
+            selectors[-1].indeterminate = True
+
+        return ct.SelectorList([s.freeze() for s in selectors], is_not, is_html)
 
     def selector_iter(self, pattern):
         """Iterate selector tokens."""
 
+        pattern = pattern.strip()
         index = 0
         end = len(pattern) - 1
         while index <= end:
diff --git a/soupsieve/css_types.py b/soupsieve/css_types.py
index 2dd7a5e..8f6a130 100644
--- a/soupsieve/css_types.py
+++ b/soupsieve/css_types.py
@@ -29,10 +29,16 @@ class Selector(util.Immutable):
 
     __slots__ = (
         'tag', 'ids', 'classes', 'attributes', 'nth', 'selectors',
-        'relation', 'rel_type', 'contains', 'empty', 'root', '_hash'
+        'relation', 'rel_type', 'contains', 'lang', 'empty', 'root', 'default', 'indeterminate',
+        'no_match',
+        '_hash'
     )
 
-    def __init__(self, tag, ids, classes, attributes, nth, selectors, relation, rel_type, contains, empty, root):
+    def __init__(
+        self, tag, ids, classes, attributes, nth, selectors,
+        relation, rel_type, contains, lang, empty, root, default, indeterminate,
+        no_match
+    ):
         """Initialize."""
 
         super().__init__(
@@ -45,8 +51,12 @@ class Selector(util.Immutable):
             relation=relation,
             rel_type=rel_type,
             contains=contains,
+            lang=lang,
             empty=empty,
-            root=root
+            root=root,
+            default=default,
+            indeterminate=indeterminate,
+            no_match=no_match
         )
 
 
@@ -98,33 +108,62 @@ class SelectorNth(util.Immutable):
         )
 
 
+class SelectorLang(util.Immutable):
+    """Selector language rules."""
+
+    __slots__ = ("languages", "_hash",)
+
+    def __init__(self, languages):
+        """Initialize."""
+
+        super().__init__(
+            languages=tuple(languages)
+        )
+
+    def __iter__(self):
+        """Iterator."""
+
+        return iter(self.languages)
+
+    def __len__(self):  # pragma: no cover
+        """Length."""
+
+        return len(self.languages)
+
+    def __getitem__(self, index):  # pragma: no cover
+        """Get item."""
+
+        return self.languages[index]
+
+
 class SelectorList(util.Immutable):
     """Selector list."""
 
-    __slots__ = ("_selectors", "is_not", "_hash")
+    __slots__ = ("selectors", "is_not", "is_html", "_hash")
 
-    def __init__(self, selectors=tuple(), is_not=False):
+    def __init__(self, selectors=tuple(), is_not=False, is_html=False):
         """Initialize."""
 
         super().__init__(
-            _selectors=tuple(selectors),
-            is_not=is_not
+            selectors=tuple(selectors),
+            is_not=is_not,
+            is_html=is_html
         )
 
     def __iter__(self):
         """Iterator."""
 
-        return iter(self._selectors)
+        return iter(self.selectors)
 
     def __len__(self):
         """Length."""
 
-        return len(self._selectors)
+        return len(self.selectors)
 
     def __getitem__(self, index):
         """Get item."""
 
-        return self._selectors[index]
+        return self.selectors[index]
 
 
 def _pickle(p):
@@ -136,3 +175,4 @@ copyreg.pickle(SelectorTag, _pickle)
 copyreg.pickle(SelectorAttribute, _pickle)
 copyreg.pickle(SelectorNth, _pickle)
 copyreg.pickle(SelectorList, _pickle)
+copyreg.pickle(SelectorLang, _pickle)


Test patch:
diff --git a/tests/test_extra.py b/tests/test_extra.py
index 9bbf3aa..2821dbb 100644
--- a/tests/test_extra.py
+++ b/tests/test_extra.py
@@ -94,3 +94,43 @@ class TestLevel1(util.TestCase):
             ['1', '2'],
             flags=util.XML
         )
+
+    def test_attribute_not_equal(self):
+        """Test attribute with value that does not equal specified value."""
+
+        markup = """
+        <div id="div">
+        <p id="0">Some text <span id="1"> in a paragraph</span>.</p>
+        <a id="2" href="http://google.com">Link</a>
+        <span id="3">Direct child</span>
+        <pre id="pre">
+        <span id="4">Child 1</span>
+        <span id="5">Child 2</span>
+        <span id="6">Child 3</span>
+        </pre>
+        </div>
+        """
+
+        # No quotes
+        self.assert_selector(
+            markup,
+            'body [id!=\\35]',
+            ["div", "0", "1", "2", "3", "pre", "4", "6"],
+            flags=util.HTML5
+        )
+
+        # Quotes
+        self.assert_selector(
+            markup,
+            "body [id!='5']",
+            ["div", "0", "1", "2", "3", "pre", "4", "6"],
+            flags=util.HTML5
+        )
+
+        # Double quotes
+        self.assert_selector(
+            markup,
+            'body [id!="5"]',
+            ["div", "0", "1", "2", "3", "pre", "4", "6"],
+            flags=util.HTML5
+        )
diff --git a/tests/test_level1.py b/tests/test_level1.py
index af8e923..d949648 100644
--- a/tests/test_level1.py
+++ b/tests/test_level1.py
@@ -7,14 +7,10 @@ E F
 E, F
 .class
 #elementID
+:link
+:active
+:visited
 ```
-Not supported (with current opinions or plans the matter):
-
-- `:link`: visited or un-visited links have no meaning outside a browser, just search for `:is(a,area,link)[href]` to
-  target links. At most, the possibility of supporting the `:link` as an alias for `:link` may be considered in the
-  future as technically, all links are un-visited in our scenario.
-
-- `:active`: No elements in our environment can be "active", so this makes no sense in our context.
 """
 from . import util
 
@@ -163,3 +159,67 @@ class TestLevel1(util.TestCase):
             ["1"],
             flags=util.HTML5
         )
+
+    def test_link(self):
+        """Test link (all links are unvisited)."""
+
+        markup = """
+        <div>
+        <p>Some text <span id="1" class="foo:bar:foobar"> in a paragraph</span>.
+        <a id="2" class="bar" href="http://google.com">Link</a>
+        <a id="3">Placeholder text.</a>
+        </p>
+        </div>
+        """
+
+        self.assert_selector(
+            markup,
+            ":link",
+            ["2"],
+            flags=util.HTML5
+        )
+
+        self.assert_selector(
+            markup,
+            "a:link",
+            [],
+            flags=util.XML
+        )
+
+    def test_active(self):
+        """Test active."""
+
+        markup = """
+        <div>
+        <p>Some text <span id="1" class="foo:bar:foobar"> in a paragraph</span>.
+        <a id="2" class="bar" href="http://google.com">Link</a>
+        <a id="3">Placeholder text.</a>
+        </p>
+        </div>
+        """
+
+        self.assert_selector(
+            markup,
+            "a:active",
+            [],
+            flags=util.HTML5
+        )
+
+    def test_visited(self):
+        """Test visited."""
+
+        markup = """
+        <div>
+        <p>Some text <span id="1" class="foo:bar:foobar"> in a paragraph</span>.
+        <a id="2" class="bar" href="http://google.com">Link</a>
+        <a id="3">Placeholder text.</a>
+        </p>
+        </div>
+        """
+
+        self.assert_selector(
+            markup,
+            "a:visited",
+            [],
+            flags=util.HTML5
+        )
diff --git a/tests/test_level2.py b/tests/test_level2.py
index 3d615b4..7dd400d 100644
--- a/tests/test_level2.py
+++ b/tests/test_level2.py
@@ -10,17 +10,10 @@ E + F
 [foo='bar']
 [foo~='bar']
 [foo|='en']
+:hover
+:focus
+:lang(en)
 ```
-
-Not supported (with current opinions or plans the matter):
-
-- `:lang(en)`: In documents, `:lang()` can take into considerations information in `meta` and other things in the
-  header. At this point, there are no plans to implement this. If a reasonable proposal was introduced on how to
-  support this, it may be considered.
-
-- `:hover`: Items cannot be hovered in our environment, so this has little meaning and will not be implemented.
-
-- `:focus`: Items cannot be focused in our environment, so this has little meaning and will not be implemented.
 """
 from . import util
 
@@ -365,3 +358,97 @@ class TestLevel2(util.TestCase):
             ["1", "4"],
             flags=util.HTML5
         )
+
+    def test_hover(self):
+        """Test hover."""
+
+        markup = """
+        <div>
+        <p>Some text <span id="1" class="foo:bar:foobar"> in a paragraph</span>.
+        <a id="2" class="bar" href="http://google.com">Link</a>
+        <a id="3">Placeholder text.</a>
+        </p>
+        </div>
+        """
+
+        self.assert_selector(
+            markup,
+            "a:hover",
+            [],
+            flags=util.HTML5
+        )
+
+    def test_focus(self):
+        """Test focus."""
+
+        markup = """
+        <form action="#">
+          <fieldset id='a' disabled>
+            <legend>
+              Simple fieldset <input type="radio" id="1" checked>
+              <fieldset id='b' disabled>
+                <legend>Simple fieldset <input type="radio" id="2" checked></legend>
+                <input type="radio" id="3" checked>
+                <label for="radio">radio</label>
+              </fieldset>
+            </legend>
+            <fieldset id='c' disabled>
+              <legend>Simple fieldset <input type="radio" id="4" checked></legend>
+              <input type="radio" id="5" checked>
+              <label for="radio">radio</label>
+            </fieldset>
+            <input type="radio" id="6" checked>
+            <label for="radio">radio</label>
+          </fieldset>
+          <optgroup id="opt-enable">
+            <option id="7" disabled>option</option>
+          </optgroup>
+          <optgroup id="8" disabled>
+            <option id="9">option</option>
+          </optgroup>
+          <a href="" id="link">text</a>
+        </form>
+        """
+
+        self.assert_selector(
+            markup,
+            "input:focus",
+            [],
+            flags=util.HTML5
+        )
+
+        self.assert_selector(
+            markup,
+            "input:not(:focus)",
+            ["1", "2", "3", "4", "5", "6"],
+            flags=util.HTML5
+        )
+
+    def test_lang(self):
+        """Test language."""
+
+        markup = """
+        <div lang="de-DE">
+            <p id="1"></p>
+        </div>
+        <div lang="de-DE-1996">
+            <p id="2"></p>
+        </div>
+        <div lang="de-Latn-DE">
+            <p id="3"></p>
+        </div>
+        <div lang="de-Latf-DE">
+            <p id="4"></p>
+        </div>
+        <div lang="de-Latn-DE-1996">
+            <p id="5"></p>
+        </div>
+        <p id="6" lang="de-DE"></p>
+        """
+
+        self.assert_selector(
+            markup,
+            "p:lang(de)",
+            ['1', '2', '3', '4', '5', '6'],
+            flags=util.HTML5
+        )
diff --git a/tests/test_level3.py b/tests/test_level3.py
index 9d72009..dfa1c3d 100644
--- a/tests/test_level3.py
+++ b/tests/test_level3.py
@@ -19,18 +19,11 @@ E ~ F
 :nth-last-child(an+b)
 :nth-of-type(an+b)
 :nth-of-last-type(an+b)
+:target
+:checked
+:enabled
+:disabled
 ```
-
-Not supported (with current opinions or plans the matter):
-
-- `:target`: Elements cannot be targeted in our environment, so this will not be implemented.
-
-- `:enabled` / `:disabled`: There are currently no plans to implement this.
-
-- `:checked`: Checked could be complicated to implement. Would you select the first item in a `<select>` tag if no
-  option is specified to be selected? Or would you consider this something that occurs when the document is live in the
-  browser?  There would need to be some considerations. What if a user has selected multiple radio boxes on accident?
-  Is this even useful in the context of how Soup Sieve would be used?
 """
 from . import util
 
@@ -443,6 +436,22 @@ class TestLevel3(util.TestCase):
             flags=util.HTML5
         )
 
+        self.assert_selector(
+            markup,
+            '[xlink\\:href*=forw],[xlink|href="images/sprites.svg#icon-redo"]',
+            ['1', '2'],
+            namespaces={"xlink": "http://www.w3.org/1999/xlink"},
+            flags=util.HTML5
+        )
+
+        self.assert_selector(
+            markup,
+            '[xlink\\:nomatch*=forw]',
+            [],
+            namespaces={"xlink": "http://www.w3.org/1999/xlink"},
+            flags=util.HTML5
+        )
+
         self.assert_selector(
             markup,
             '[bad|href*=forw]',
@@ -824,3 +833,220 @@ class TestLevel3(util.TestCase):
             ['1', '8', '10'],
             flags=util.HTML5
         )
+
+    def test_checked(self):
+        """Test checked."""
+
+        markup = """
+        <div>
+          <input type="radio" name="my-input" id="yes" checked>
+          <label for="yes">Yes</label>
+
+          <input type="radio" name="my-input" id="no">
+          <label for="no">No</label>
+        </div>
+
+        <div>
+          <input type="checkbox" name="my-checkbox" id="opt-in" checked>
+          <label for="opt-in">Check me!</label>
+        </div>
+
+        <select name="my-select" id="fruit">
+          <option id="1" value="opt1">Apples</option>
+          <option id="2" value="opt2" selected>Grapes</option>
+          <option id="3" value="opt3">Pears</option>
+        </select>
+        """
+
+        self.assert_selector(
+            markup,
+            ":checked",
+            ['yes', 'opt-in', '2'],
+            flags=util.HTML5
+        )
+
+    def test_disabled(self):
+        """
+        Test disabled.
+
+        Form elements that have `disabled`.
+        `option` that is child of disabled `optgroup`.
+        Form elements that are children of a disabled `fieldset`, but not it's `legend`.
+        """
+
+        markup = """
+        <form action="#">
+          <fieldset id='a' disabled>
+            <legend>
+              Simple fieldset <input type="radio" id="1" checked>
+              <fieldset id='b' disabled>
+                <legend>Simple fieldset <input type="radio" id="2" checked></legend>
+                <input type="radio" id="3" checked>
+                <label for="radio">radio</label>
+              </fieldset>
+            </legend>
+            <fieldset id='c' disabled>
+              <legend>Simple fieldset <input type="radio" id="4" checked></legend>
+              <input type="radio" id="5" checked>
+              <label for="radio">radio</label>
+            </fieldset>
+            <input type="radio" id="6" checked>
+            <label for="radio">radio</label>
+          </fieldset>
+          <optgroup>
+            <option id="7" disabled>option</option>
+          </optgroup>
+          <optgroup id="8" disabled>
+            <option id="9">option</option>
+          </optgroup>
+        </form>
+        """
+
+        markup2 = """
+        <form action="#">
+          <fieldset id='a' disabled>
+            <legend>
+              Simple fieldset <input type="radio" id="1" checked>
+              <fieldset id='b'>
+                <legend>Simple fieldset <input type="radio" id="2" checked></legend>
+                <input type="radio" id="3" checked>
+                <label for="radio">radio</label>
+              </fieldset>
+            </legend>
+            <fieldset id='c' disabled>
+              <legend>Simple fieldset <input type="radio" id="4" checked></legend>
+              <input type="radio" id="5" checked>
+              <label for="radio">radio</label>
+            </fieldset>
+            <input type="radio" id="6" checked>
+            <label for="radio">radio</label>
+          </fieldset>
+          <optgroup>
+            <option id="7" disabled>option</option>
+          </optgroup>
+          <optgroup id="8" disabled>
+            <option id="9">option</option>
+          </optgroup>
+        </form>
+        """
+
+        self.assert_selector(
+            markup,
+            ":disabled",
+            ['3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c'],
+            flags=util.HTML5
+        )
+
+        self.assert_selector(
+            markup2,
+            ":disabled",
+            ['4', '5', '6', '7', '8', '9', 'a', 'c'],
+            flags=util.HTML5
+        )
+
+    def test_enable(self):
+        """
+        Test enable.
+
+        `:any-link`
+        Form elements that have `disabled`.
+        Form elements that are children of a disabled `fieldset`, but not it's `legend`.
+        """
+
+        markup = """
+        <form action="#">
+          <fieldset id='a' disabled>
+            <legend>
+              Simple fieldset <input type="radio" id="1" checked>
+              <fieldset id='b' disabled>
+                <legend>Simple fieldset <input type="radio" id="2" checked></legend>
+                <input type="radio" id="3" checked>
+                <label for="radio">radio</label>
+              </fieldset>
+            </legend>
+            <fieldset id='c' disabled>
+              <legend>Simple fieldset <input type="radio" id="4" checked></legend>
+              <input type="radio" id="5" checked>
+              <label for="radio">radio</label>
+            </fieldset>
+            <input type="radio" id="6" checked>
+            <label for="radio">radio</label>
+          </fieldset>
+          <optgroup id="opt-enable">
+            <option id="7" disabled>option</option>
+          </optgroup>
+          <optgroup id="8" disabled>
+            <option id="9">option</option>
+          </optgroup>
+          <a href="" id="link">text</a>
+        </form>
+        """
+
+        markup2 = """
+        <form action="#">
+          <fieldset id='a' disabled>
+            <legend>
+              Simple fieldset <input type="radio" id="1" checked>
+              <fieldset id='b'>
+                <legend>Simple fieldset <input type="radio" id="2" checked></legend>
+                <input type="radio" id="3" checked>
+                <label for="radio">radio</label>
+              </fieldset>
+            </legend>
+            <fieldset id='c' disabled>
+              <legend>Simple fieldset <input type="radio" id="4" checked></legend>
+              <input type="radio" id="5" checked>
+              <label for="radio">radio</label>
+            </fieldset>
+            <input type="radio" id="6" checked>
+            <label for="radio">radio</label>
+          </fieldset>
+          <optgroup id="opt-enable">
+            <option id="7" disabled>option</option>
+          </optgroup>
+          <optgroup id="8" disabled>
+            <option id="9">option</option>
+          </optgroup>
+          <a href="" id="link">text</a>
+        </form>
+        """
+
+        self.assert_selector(
+            markup,
+            ":enabled",
+            ['1', '2', 'opt-enable', 'link'],
+            flags=util.HTML5
+        )
+
+        self.assert_selector(
+            markup2,
+            ":enabled",
+            ['1', '2', 'opt-enable', 'b', '3', 'link'],
+            flags=util.HTML5
+        )
+
+    def test_target(self):
+        """Test target."""
+
+        markup = """
+        <div>
+        <h2 id="head-1">Header 1</h1>
+        <div><p>content</p></div>
+        <h2 id="head-2">Header 2</h1>
+        <div><p>content</p></div>
+        </div>
+        """
+
+        self.assert_selector(
+            markup,
+            "#head-2:target",
+            [],
+            flags=util.HTML5
+        )
+
+        self.assert_selector(
+            markup,
+            "#head-2:not(:target)",
+            ["head-2"],
+            flags=util.HTML5
+        )
diff --git a/tests/test_level4.py b/tests/test_level4.py
index 0c2a92f..63f94ce 100644
--- a/tests/test_level4.py
+++ b/tests/test_level4.py
@@ -8,9 +8,20 @@ Test selectors level 4.
 :where(s1, s2, ...) allowed, but due to our environment, works like `:is()`
 :not(s1, s2, ...)
 :has(> s1, ...)
+:any-link
+:current
+:past
+:future
+:focus-within
+:focus-visible
+:target-within
+:default
+:indeterminate
+:placeholder-shown
+:lang('*-US', de-DE, ...)
 ```
 
-Likely to be implemented:
+Not supported:
 
 - `:nth-col(n)` / `:nth-last-col(n)`: This needs further understanding before implementing and would likely only be
   implemented for HTML, XHTML, and HTML5. This would not be implemented for XML.
@@ -18,32 +29,18 @@ Likely to be implemented:
 - `E || F`: This would need more understanding before implementation. This would likely only be implemented for HTML,
   XHTML, and HTML5. This would not be implemented for XML.
 
-Not supported (with current opinions or plans the matter):
-
-- `:blank`: This applies to inputs with empty or otherwise null input. Currently, there is no plans to implement this.
+- `:blank`: This applies to inputs with empty or otherwise null input.
 
 - `:dir(ltr)`: This applies to direction of text. This direction can be inherited from parents. Due to the way Soup
   Sieve process things, it would have to scan the parents and evaluate what is inherited. it doesn't account for the CSS
   `direction` value, which is a good thing. It is doable, but not sure worth the effort. In addition, it seems there is
   reference to being able to do something like `[dir=auto]` which would select either `ltr` or `rtl`. This seems to add
-  additional logic in to attribute selections which would complicate things, but still technically doable. There are
-  currently no plans to implement this.
-
-- `:lang(en-*)`: As mentioned in level 2 tests, in documents, `:lang()` can take into considerations information in
-  `meta` and other things in the header. At this point, there are no plans to implement this. If a reasonable proposal
-  was introduced on how to support this, it may be considered.
+  additional logic in to attribute selections which would complicate things, but still technically doable.
 
-- `:local-link`: In our environment, there is no document URL. This isn't currently practical. This will not be
-  implemented.
+- `:local-link`: In our environment, there is no document URL. This isn't currently practical.
 
 - `:read-only` / `:read-write`: There are no plans to implement this at this time.
 
-- `:required` / `:optional`: There are no plans to implement this at this time.
-
-- `:placeholder-shown`: There are no plans to implement this at this time.
-
-- `:indeterminate`: There are no plans to implement this at this time.
-
 - `:valid` / `:invalid`: We currently to not validate values, so this doesn't make sense at this time.
 
 - `:user-invalid`: User cannot alter things in our environment because there is no user interaction (we are not a
@@ -56,16 +53,6 @@ Not supported (with current opinions or plans the matter):
 - `:in-range` / `:out-of-range`: This applies to form elements only. You'd have to evaluate `value`, `min`, and `max`. I
   guess you can have numerical ranges and alphabetic ranges. Currently, there are no plans to implement this.
 
-- `:current` / `:past` / `:future`: I believe this requires a live, in browser state to determine what is current, to
-  then determine what is past and future. I don't think this is applicable to our environment.
-
-- `:default`: This is in the same vain as `:checked`. If we ever implemented that, we'd probably implement this, but
-  there are no plans to do so at this time.
-
-- `:focus-within` / `:focus-visible`: There is no focus in our environment, so this will not be implemented.
-
-- `:target-within`: Elements cannot be "targeted" in our environment, so this will not be implemented.
-
 - `:playing` / `:paused`: Elements cannot be played or paused in our environment, so this will not be implemented.
 """
 from . import util
@@ -346,3 +333,703 @@ class TestLevel4(util.TestCase):
             ['0', '1', '7'],
             flags=util.HTML5
         )
+
+    def test_anylink(self):
+        """Test any link (all links are unvisited)."""
+
+        markup = """
+        <div id="div">
+        <p id="0">Some text <span id="1" class="foo:bar:foobar"> in a paragraph</span>.
+        <a id="2" class="bar" href="http://google.com">Link</a>
+        <a id="3">Placeholder text.</a>
+        </p>
+        </div>
+        """
+
+        self.assert_selector(
+            markup,
+            ":any-link",
+            ["2"],
+            flags=util.HTML5
+        )
+
+        self.assert_selector(
+            markup,
+            "a:any-link",
+            [],
+            flags=util.XML
+        )
+
+        self.assert_selector(
+            markup,
+            ":not(a:any-link)",
+            ["div", "0", "1", "2", "3"],
+            flags=util.XML
+        )
+
+    def test_focus_within(self):
+        """Test focus within."""
+
+        markup = """
+        <form id="form">
+          <input type="text">
+        </form>
+        """
+
+        self.assert_selector(
+            markup,
+            "form:focus-within",
+            [],
+            flags=util.HTML5
+        )
+
+        self.assert_selector(
+            markup,
+            "form:not(:focus-within)",
+            ["form"],
+            flags=util.HTML5
+        )
+
+    def test_focus_visible(self):
+        """Test focus visible."""
+
+        markup = """
+        <form id="form">
+          <input type="text">
+        </form>
+        """
+
+        self.assert_selector(
+            markup,
+            "form:focus-visible",
+            [],
+            flags=util.HTML5
+        )
+
+        self.assert_selector(
+            markup,
+            "form:not(:focus-visible)",
+            ["form"],
+            flags=util.HTML5
+        )
+
+    def test_target_within(self):
+        """Test target within."""
+
+        markup = """
+        <a href="#head-2">Jump</a>
+        <article id="article">
+        <h2 id="head-1">Header 1</h1>
+        <div><p>content</p></div>
+        <h2 id="head-2">Header 2</h1>
+        <div><p>content</p></div>
+        </article>
+        """
+
+        self.assert_selector(
+            markup,
+            "article:target-within",
+            [],
+            flags=util.HTML5
+        )
+
+        self.assert_selector(
+            markup,
+            "article:not(:target-within)",
+            ["article"],
+            flags=util.HTML5
+        )
+
+    def test_current_past_future(self):
+        """Test current, past, future."""
+
+        markup = """
+        <div id="div">
+        <p id="0">Some text <span id="1" class="foo:bar:foobar"> in a paragraph</span>.
+        <a id="2" class="bar" href="http://google.com">Link</a>
+        <a id="3">Placeholder text.</a>
+        </p>
+        </div>
+        """
+
+        self.assert_selector(
+            markup,
+            ":current(p, div, a)",
+            [],
+            flags=util.HTML5
+        )
+
+        self.assert_selector(
+            markup,
+            ":current(p, :not(div), a)",
+            [],
+            flags=util.HTML5
+        )
+
+        self.assert_selector(
+            markup,
+            "body :not(:current(p, div, a))",
+            ["div", "0", "1", "2", "3"],
+            flags=util.HTML5
+        )
+
+        self.assert_selector(
+            markup,
+            "body :not(:current(p, :not(div), a))",
+            ["div", "0", "1", "2", "3"],
+            flags=util.HTML5
+        )
+
+        self.assert_selector(
+            markup,
+            "p:current",
+            [],
+            flags=util.HTML5
+        )
+
+        self.assert_selector(
+            markup,
+            "p:not(:current)",
+            ["0"],
+            flags=util.HTML5
+        )
+
+        self.assert_selector(
+            markup,
+            "p:past",
+            [],
+            flags=util.HTML5
+        )
+
+        self.assert_selector(
+            markup,
+            "p:future",
+            [],
+            flags=util.HTML5
+        )
+
+    def test_required(self):
+        """Test required."""
+
+        markup = """
+        <form>
+        <input id="1" type="name" required>
+        <input id="2" type="checkbox" required>
+        <input id="3" type="email">
+        <textarea id="4" name="name" id="message" cols="30" rows="10" required></textarea>
+        <select id="5" name="nm" id="sel" required>
+            <!-- options -->
+        </select>
+        </form>
+        """
+
+        self.assert_selector(
+            markup,
+            ":required",
+            ['1', '2', '4', '5'],
+            flags=util.HTML5
+        )
+
+        self.assert_selector(
+            markup,
+            "input:required",
+            ['1', '2'],
+            flags=util.HTML5
+        )
+
+    def test_optional(self):
+        """Test optional."""
+
+        markup = """
+        <form>
+        <input id="1" type="name" required>
+        <input id="2" type="checkbox" required>
+        <input id="3" type="email">
+        <textarea id="4" name="name" id="message" cols="30" rows="10"></textarea>
+        <select id="5" name="nm" id="sel">
+            <!-- options -->
+        </select>
+        </form>
+        """
+
+        self.assert_selector(
+            markup,
+            ":optional",
+            ['3', '4', '5'],
+            flags=util.HTML5
+        )
+
+        self.assert_selector(
+            markup,
+            "input:optional",
+            ['3'],
+            flags=util.HTML5
+        )
+
+    def test_default(self):
+        """Test default."""
+
+        markup = """
+        <form>
+
+        <input type="radio" name="season" id="spring">
+        <label for="spring">Spring</label>
+
+        <input type="radio" name="season" id="summer" checked>
+        <label for="summer">Summer</label>
+
+        <input type="radio" name="season" id="fall">
+        <label for="fall">Fall</label>
+
+        <input type="radio" name="season" id="winter">
+        <label for="winter">Winter</label>
+
+        <select id="pet-select">
+            <option value="">--Please choose an option--</option>
+            <option id="dog" value="dog">Dog</option>
+            <option id="cat" value="cat">Cat</option>
+            <option id="hamster" value="hamster" selected>Hamster</option>
+            <option id="parrot" value="parrot">Parrot</option>
+            <option id="spider" value="spider">Spider</option>
+            <option id="goldfish" value="goldfish">Goldfish</option>
+        </select>
+
+        <input type="checkbox" name="enable" id="enable" checked>
+        <label for="enable">Enable</label>
+
+        <button type="button">
+        not default
+        </button>
+
+        <button id="d1" type="submit">
+        default1
+        </button>
+
+        <button id="d2" type="submit">
+        default2
+        </button>
+
+        </form>
+
+        <form>
+
+        <div>
+        <button id="d3" type="submit">
+        default3
+        </button>
+        </div>
+
+        <button id="d4" type="submit">
+        default4
+        </button>
+
+        </form>
+
+        <button id="d5" type="submit">
+        default4
+        </button>
+        """
+
+        self.assert_selector(
+            markup,
+            ":default",
+            ['summer', 'd1', 'd3', 'hamster', 'enable'],
+            flags=util.HTML5
+        )
+
+        # This is technically invalid use of forms, but browsers will generally evaluated the nested form
+        markup2 = """
+        <form>
+
+        <form>
+        <button id="d1" type="submit">
+        button1
+        </button>
+        </form>
+
+        <button id="d2" type="submit">
+        button2
+        </button>
+        </form>
+        """
+
+        self.assert_selector(
+            markup2,
+            ":default",
+            ['d1'],
+            flags=util.HTML5
+        )
+
+        # For the sake of coverage, we will do this impractical select
+        # to ensure we reuse the cached default.
+        self.assert_selector(
+            markup2,
+            ":default:default",
+            ['d1'],
+            flags=util.HTML5
+        )
+
+        # You shouldn't nest forms, but if you do,
+        # When a parent form encounters a nested form, we will bail evaluation like browsers do.
+        # We should see button 1 getting found for nested form, but button 2 will not be found
+        # for parent form.
+        markup3 = """
+        <form>
+
+        <form>
+        <span>what</span>
+        </form>
+
+        <button id="d2" type="submit">
+        button2
+        </button>
+        </form>
+        """
+
+        self.assert_selector(
+            markup3,
+            ":default",
+            [],
+            flags=util.HTML5
+        )
+
+    def test_indeterminate(self):
+        """Test indeterminate."""
+
+        markup = """
+        <input type="radio" name="" id="radio-no-name1">
+        <label>No name 1</label>
+        <input type="radio" name="" id="radio-no-name2" checked>
+        <label>no name 2</label>
+        <div>
+          <input type="checkbox" id="checkbox" indeterminate>
+          <label for="checkbox">This label starts out lime.</label>
+        </div>
+        <div>
+          <input type="radio" name="test" id="radio1">
+          <label for="radio">This label starts out lime.</label>
+          <form>
+            <input type="radio" name="test" id="radio2">
+            <label for="radio">This label starts out lime.</label>
+
+            <input type="radio" name="test" id="radio3" checked>
+            <label for="radio">This label starts out lime.</label>
+
+            <input type="radio" name="other" id="radio4">
+            <label for="radio">This label starts out lime.</label>
+
+            <input type="radio" name="other" id="radio5">
+            <label for="radio">This label starts out lime.</label>
+          </form>
+          <input type="radio" name="test" id="radio6">
+          <label for="radio">This label starts out lime.</label>
+        </div>
+        """
+
+        self.assert_selector(
+            markup,
+            ":indeterminate",
+            ['checkbox', 'radio1', 'radio6', 'radio4', 'radio5', 'radio-no-name1'],
+            flags=util.HTML5
+        )
+
+    def test_placeholder(self):
+        """Test placeholder shown."""
+
+        markup = """
+        <input id="0" placeholder="This is some text">
+        <textarea id="1" placeholder="This is some text"></textarea>
+
+        <input id="2" placeholder="">
+        <input id="3">
+
+        <input id="4" type="email" placeholder="This is some text">
+        <input id="5" type="number" placeholder="This is some text">
+        <input id="6" type="password" placeholder="This is some text">
+        <input id="7" type="search" placeholder="This is some text">
+        <input id="8" type="tel" placeholder="This is some text">
+        <input id="9" type="text" placeholder="This is some text">
+        <input id="10" type="url" placeholder="This is some text">
+        <input id="11" type="" placeholder="This is some text">
+        <input id="12" type placeholder="This is some text">
+
+        <input id="13" type="button" placeholder="This is some text">
+        <input id="14" type="checkbox" placeholder="This is some text">
+        <input id="15" type="color" placeholder="This is some text">
+        <input id="16" type="date" placeholder="This is some text">
+        <input id="17" type="datetime-local" placeholder="This is some text">
+        <input id="18" type="file" placeholder="This is some text">
+        <input id="19" type="hidden" placeholder="This is some text">
+        <input id="20" type="image" placeholder="This is some text">
+        <input id="21" type="month" placeholder="This is some text">
+        <input id="22" type="radio" placeholder="This is some text">
+        <input id="23" type="range" placeholder="This is some text">
+        <input id="24" type="reset" placeholder="This is some text">
+        <input id="25" type="submit" placeholder="This is some text">
+        <input id="26" type="time" placeholder="This is some text">
+        <input id="27" type="week" placeholder="This is some text">
+        """
+
+        self.assert_selector(
+            markup,
+            ":placeholder-shown",
+            ['0', '1', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+            flags=util.HTML5
+        )
+
+    def test_lang(self):
+        """Test language."""
+
+        markup = """
+        <div lang="de-DE">
+            <p id="1"></p>
+        </div>
+        <div lang="de-DE-1996">
+            <p id="2"></p>
+        </div>
+        <div lang="de-Latn-DE">
+            <p id="3"></p>
+        </div>
+        <div lang="de-Latf-DE">
+            <p id="4"></p>
+        </div>
+        <div lang="de-Latn-DE-1996">
+            <p id="5"></p>
+        </div>
+        <p id="6" lang="de-DE"></p>
+        """
+
+        # Implicit wild
+        self.assert_selector(
+            markup,
+            "p:lang(de-DE)",
+            ['1', '2', '3', '4', '5', '6'],
+            flags=util.HTML5
+        )
+
+        # Explicit wild
+        self.assert_selector(
+            markup,
+            "p:lang(de-\\*-DE)",
+            ['1', '2', '3', '4', '5', '6'],
+            flags=util.HTML5
+        )
+
+        # First wild has meaning (escaped)
+        self.assert_selector(
+            markup,
+            "p:lang(\\*-DE)",
+            ['1', '2', '3', '4', '5', '6'],
+            flags=util.HTML5
+        )
+
+        # First wild quoted
+        self.assert_selector(
+            markup,
+            "p:lang('*-DE')",
+            ['1', '2', '3', '4', '5', '6'],
+            flags=util.HTML5
+        )
+
+        # Normal quoted
+        self.assert_selector(
+            markup,
+            "p:lang('de-DE')",
+            ['1', '2', '3', '4', '5', '6'],
+            flags=util.HTML
+        )
+
+        # Target element with language and language attribute
+        self.assert_selector(
+            markup,
+            "p[lang]:lang(de-DE)",
+            ['6'],
+            flags=util.HTML5
+        )
+
+        # Undetermined language
+        markup = """
+        <div>
+            <p id="1"></p>
+        </div>
+        """
+
+        self.assert_selector(
+            markup,
+            "p:lang(en)",
+            [],
+            flags=util.HTML5
+        )
+
+        # Find language in header
+        markup = """
+        <!DOCTYPE html>
+        <html>
+        <head>
+        <meta http-equiv="content-language" content="en-US">
+        </head>
+        <body>
+        <div>
+            <p id="1"></p>
+        </div>
+        <div>
+            <p id="2"></p>
+        </div>
+        </body>
+        """
+
+        self.assert_selector(
+            markup,
+            "p:lang('*-US')",
+            ['1', '2'],
+            flags=util.HTML5
+        )
+
+        # XML style language when out of HTML namespace
+        markup = """
+        <math xml:lang="en">
+            <mtext id="1"></mtext>
+        </math>
+        <div xml:lang="en">
+            <mtext id="2"></mtext>
+        </div>
+        """
+
+        self.assert_selector(
+            markup,
+            "mtext:lang(en)",
+            ['1'],
+            flags=util.HTML5
+        )
+
+        # XML style language
+        markup = """
+        <?xml version="1.0" encoding="UTF-8"?>
+        <html>
+        <head>
+        </head>
+        <body>
+        <div xml:lang="de-DE">
+            <p id="1"></p>
+        </div>
+        <div xml:lang="de-DE-1996">
+            <p id="2"></p>
+        </div>
+        <div xml:lang="de-Latn-DE">
+            <p id="3"></p>
+        </div>
+        <div xml:lang="de-Latf-DE">
+            <p id="4"></p>
+        </div>
+        <div xml:lang="de-Latn-DE-1996">
+            <p id="5"></p>
+        </div>
+        <p id="6" xml:lang="de-DE"></p>
+        </body>
+        </html>
+        """
+
+        self.assert_selector(
+            markup,
+            "p:lang(de-DE)",
+            ['1', '2', '3', '4', '5', '6'],
+            flags=util.XML
+        )
+
+        # XHTML language: `lang`
+        markup = """
+        <?xml version="1.0" encoding="UTF-8"?>
+        <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
+            "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
+        <html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+        <head>
+        </head>
+        <body>
+        <div xml:lang="de-DE">
+            <p id="1"></p>
+        </div>
+        <div xml:lang="de-DE-1996">
+            <p id="2"></p>
+        </div>
+        <div xml:lang="de-Latn-DE">
+            <p id="3"></p>
+        </div>
+        <div xml:lang="de-Latf-DE">
+            <p id="4"></p>
+        </div>
+        <div xml:lang="de-Latn-DE-1996">
+            <p id="5"></p>
+        </div>
+        <p id="6" xml:lang="de-DE"></p>
+        </body>
+        </html>
+        """
+
+        self.assert_selector(
+            markup,
+            "p:lang(de-DE)",
+            [],
+            flags=util.XHTML
+        )
+
+        markup = """
+        <?xml version="1.0" encoding="UTF-8"?>
+        <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
+            "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
+        <html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+        <head>
+        </head>
+        <body>
+        <div lang="de-DE" xml:lang="de-DE">
+            <p id="1"></p>
+        </div>
+        <div lang="de-DE-1996" xml:lang="de-DE-1996">
+            <p id="2"></p>
+        </div>
+        <div lang="de-Latn-DE" xml:lang="de-Latn-DE">
+            <p id="3"></p>
+        </div>
+        <div lang="de-Latf-DE" xml:lang="de-Latf-DE">
+            <p id="4"></p>
+        </div>
+        <div lang="de-Latn-DE-1996" xml:lang="de-Latn-DE-1996">
+            <p id="5"></p>
+        </div>
+        <p id="6" lang="de-DE" xml:lang="de-DE"></p>
+        </body>
+        </html>
+        """
+
+        self.assert_selector(
+            markup,
+            "p:lang(de-DE)",
+            ['1', '2', '3', '4', '5', '6'],
+            flags=util.XML
+        )
+
+        # Multiple languages
+        markup = """
+        <div lang="de-DE">
+            <p id="1"></p>
+        </div>
+        <div lang="en">
+            <p id="2"></p>
+        </div>
+        <div lang="de-Latn-DE">
+            <p id="3"></p>
+        </div>
+        <div lang="de-Latf-DE">
+            <p id="4"></p>
+        </div>
+        <div lang="en-US">
+            <p id="5"></p>
+        </div>
+        <p id="6" lang="de-DE"></p>
+        """
+
+        self.assert_selector(
+            markup,
+            "p:lang(de-DE, '*-US')",
+            ['1', '3', '4', '5', '6'],
+            flags=util.HTML5
+        )
diff --git a/tests/util.py b/tests/util.py
index 4993748..a63aa86 100644
--- a/tests/util.py
+++ b/tests/util.py
@@ -26,6 +26,6 @@ class TestCase(unittest.TestCase):
         soup = bs4.BeautifulSoup(textwrap.dedent(markup.replace('\r\n', '\n')), bs_mode)
 
         ids = []
-        for el in sv.select(selectors, soup, namespaces=namespaces, flags=flags):
+        for el in sv.select(selectors, soup, namespaces=namespaces):
             ids.append(el.attrs['id'])
         self.assertEqual(sorted(ids), sorted(expected_ids))