facelessuser: Still needs tests
diff --git a/docs/src/markdown/selectors.md b/docs/src/markdown/selectors.md
index e40e4fe..e6f4531 100644
--- a/docs/src/markdown/selectors.md
+++ b/docs/src/markdown/selectors.md
@@ -58,7 +58,10 @@ Selector                        | Example                             | Descript
 `:scope`                        | `#!css :scope div`                  | Selects all `#!html <div>` elements under the current scope element. `:scope` is the element under match or select. In the case where a document (`BeautifulSoup` object, not a `Tag` object) is under select or match, `:scope` equals `:root`.
 
 !!! 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.
+
+    In general, CSS4 specific features and selectors are not finalized in the official CSS4 specification, and may change in the future. While some are most likely quite stable, some may be less certain.
+
+    Some implementations are based from our interpretation of the specification. It is possible our interpretation is incorrect. This is more likely with selectors that currently have no reference implementations in browsers such as `:has()` and `of S` support in `:nth-child(an+b [of S]?)`. If any issues are discovered please report the issue with details and examples so we can get them right.
 
 !!! danger "Pseudo-elements"
     Pseudo elements are not supported as they do not represent real elements.
@@ -88,6 +91,8 @@ Selector                        | Example                             | Descript
 `:paused`                       | `#!css :paused`                     | Pausing is not applicable in the Soup Sieve environment, so this will match nothing.
 `:placeholder-shown`            | `#!css input:placeholder-shown`     | Selects every `#!html <input>` element that is showing a placeholder via the `placeholder` attribute.
 `:playing`                      | `#!css :playing`                    | Playing is not applicable in the Soup Sieve environment, so this will match nothing.
+`:read-only`                    | `#!css input:read-only`             | Selects every `#!html <input>` element that is not editable by the user.
+`:read-write`                   | `#!css input:read-write`            | Selects every `#!html <input>` element that is editable by the user. 
 `: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.
 `:user-invalid`                 | `#!css input:user-invalid`          | User interaction is not applicable, so this will never match.
@@ -104,6 +109,11 @@ Selector                        | Example                             | Descript
 `[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.
 
+!!! warning "Contains"
+    Contains is an expensive operation as it scans every element's content, which includes all of the content of each child of the element under consideration.
+
+    If you use a pattern such as `#!css *:contains(text)` it will scan all the children of every element. This will cause some elements to get scanned over and over again as the tree is walked. The more specific you are the better. If you use `#!css p.special:contains(text)`, only `#!html <p>` elements with the class `special` will have their content scanned (along with their children). `contains` is evaluated as one of the last checks, so if any other comparison invalidates the match, `contains` will not be performed.
+
 --8<--
 refs.txt
 --8<--
diff --git a/soupsieve/css_parser.py b/soupsieve/css_parser.py
index 26bdaa3..a0faeb6 100644
--- a/soupsieve/css_parser.py
+++ b/soupsieve/css_parser.py
@@ -25,6 +25,8 @@ PSEUDO_SIMPLE = {
     ':indeterminate',
     ':optional',
     ':placeholder-shown',
+    ':read-only',
+    ':read-write',
     ':required',
     ':scope'
 }
@@ -409,6 +411,10 @@ class CSSParser(object):
                 sel.selectors.append(CSS_REQUIRED)
             elif pseudo == ":optional":
                 sel.selectors.append(CSS_OPTIONAL)
+            elif pseudo == ":read-only":
+                sel.selectors.append(CSS_READ_ONLY)
+            elif pseudo == ":read-write":
+                sel.selectors.append(CSS_READ_WRITE)
             elif pseudo == ":placeholder-shown":
                 sel.selectors.append(CSS_PLACEHOLDER_SHOWN)
             elif pseudo == ':first-child':
@@ -768,6 +774,8 @@ class CSSParser(object):
 
 
 # Precompile CSS selector lists for pseudo-classes (additional logic may be required beyond the pattern)
+# A few patterns are order dependent as they use patterns previous compiled.
+
 # CSS pattern for `:link` and `:any-link`
 CSS_LINK = CSSParser(
     ':is(a, area, link)[href]'
@@ -829,21 +837,52 @@ CSS_PLACEHOLDER_SHOWN = CSSParser(
     '''
     :is(
         input:is(
+            :not([type]),
+            [type=""],
             [type=text],
             [type=search],
             [type=url],
             [type=tel],
             [type=email],
             [type=password],
-            [type=number],
-            :not([type]),
-            [type=""]
+            [type=number]
         ),
         textarea
     )[placeholder][placeholder!='']
     '''
 ).process_selectors(flags=FLG_PSEUDO | FLG_HTML)
-# CSS pattern default for `:nth-of-child` "of S" feature
+# CSS pattern default for `:nth-child` "of S" feature
 CSS_NTH_OF_S_DEFAULT = CSSParser(
     '*|*'
 ).process_selectors(flags=FLG_PSEUDO)
+# CSS pattern for `:read-write` (CSS_DISABLED must be compiled first)
+CSS_READ_WRITE = CSSParser(
+    '''
+    :is(
+        textarea,
+        input:is(
+            :not([type]),
+            [type=""],
+            [type=text],
+            [type=search],
+            [type=url],
+            [type=tel],
+            [type=email],
+            [type=number],
+            [type=password],
+            [type=date],
+            [type=datetime-local],
+            [type=month],
+            [type=time],
+            [type=week]
+        )
+    ):not([readonly], :disabled),
+    :is([contenteditable=""], [contenteditable="true" i])
+    '''
+).process_selectors(flags=FLG_PSEUDO | FLG_HTML)
+# CSS pattern for `:read-only`
+CSS_READ_ONLY = CSSParser(
+    '''
+    :not(:read-write)
+    '''
+).process_selectors(flags=FLG_PSEUDO | FLG_HTML)

:read-only and :read-write support
This is planned, but before it can be implemented, we need to have a well defined understanding of what can be "read only" and what is "read write". Once we understand this, we should be able to implement this no problem.
diff --git a/tests/test_level4.py b/tests/test_level4.py
index a1a45ce..a28acb5 100644
--- a/tests/test_level4.py
+++ b/tests/test_level4.py
@@ -24,6 +24,8 @@ Test selectors level 4.
 :paused
 :scope
 :local-link
+:read-only
+:read-write
 ```
 
 Not supported:
@@ -42,8 +44,6 @@ Not supported:
   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.
 
-- `:read-only` / `:read-write`: We need to understand exactly what these match before implementing..
-
 - `:valid` / `:invalid`: We currently to not validate values, so this doesn't make sense at this time.
 
 - `:in-range` / `:out-of-range`: This applies to form elements only. You'd have to evaluate `value`, `min`, and `max`. I
@@ -1209,3 +1209,113 @@ class TestLevel4(util.TestCase):
             ["1", "2"],
             flags=util.HTML5
         )
+
+    def test_read_write(self):
+        """Test read write."""
+
+        markup = """
+        <input id="0">
+        <textarea id="1"></textarea>
+
+        <input id="2">
+        <input id="3" disabled>
+
+        <input id="4" type="email">
+        <input id="5" type="number">
+        <input id="6" type="password">
+        <input id="7" type="search">
+        <input id="8" type="tel">
+        <input id="9" type="text">
+        <input id="10" type="url">
+        <input id="11" type="">
+        <input id="12" type>
+
+        <input id="13" type="button">
+        <input id="14" type="checkbox">
+        <input id="15" type="color">
+        <input id="16" type="date">
+        <input id="17" type="datetime-local">
+        <input id="18" type="file">
+        <input id="19" type="hidden">
+        <input id="20" type="image">
+        <input id="21" type="month">
+        <input id="22" type="radio">
+        <input id="23" type="range">
+        <input id="24" type="reset">
+        <input id="25" type="submit">
+        <input id="26" type="time">
+        <input id="27" type="week">
+
+        <p id="28" contenteditable="">Text</p>
+        <p id="29" contenteditable="true">Text</p>
+        <p id="30" contenteditable="TRUE">Text</p>
+        <p id="31" contenteditable="false">Text</p>
+        <p id="32">Text</p>
+
+        <input id="33" type="number" readonly>
+        """
+
+        self.assert_selector(
+            markup,
+            ":read-write",
+            [
+                '0', '1', '2', '4', '5', '6', '7', '8', '9', '10', '11',
+                '12', '16', '17', '21', '26', '27', '28', '29', '30'
+            ],
+            flags=util.HTML5
+        )
+
+    def test_read_only(self):
+        """Test read only."""
+
+        markup = """
+        <input id="0">
+        <textarea id="1"></textarea>
+
+        <input id="2">
+        <input id="3" disabled>
+
+        <input id="4" type="email">
+        <input id="5" type="number">
+        <input id="6" type="password">
+        <input id="7" type="search">
+        <input id="8" type="tel">
+        <input id="9" type="text">
+        <input id="10" type="url">
+        <input id="11" type="">
+        <input id="12" type>
+
+        <input id="13" type="button">
+        <input id="14" type="checkbox">
+        <input id="15" type="color">
+        <input id="16" type="date">
+        <input id="17" type="datetime-local">
+        <input id="18" type="file">
+        <input id="19" type="hidden">
+        <input id="20" type="image">
+        <input id="21" type="month">
+        <input id="22" type="radio">
+        <input id="23" type="range">
+        <input id="24" type="reset">
+        <input id="25" type="submit">
+        <input id="26" type="time">
+        <input id="27" type="week">
+
+        <p id="28" contenteditable="">Text</p>
+        <p id="29" contenteditable="true">Text</p>
+        <p id="30" contenteditable="TRUE">Text</p>
+        <p id="31" contenteditable="false">Text</p>
+        <p id="32">Text</p>
+
+        <input id="33" type="number" readonly>
+        """
+
+        self.assert_selector(
+            markup,
+            "body :read-only",
+            [
+                '3', '13', '14', '15', '18', '19', '20', '22',
+                '23', '24', '25', '31', '32', '33'
+            ],
+            flags=util.HTML5
+        )

pip install -e .[dev]
parse_log_pytest
requirements.txt
pytest pytest-cov coverage lxml beautifulsoup4
apt-get update apt-get install -y gcc
3.9
requirements/project.txt requirements/tests.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
beautifulsoup4==4.13.3
coverage==7.8.0
exceptiongroup==1.2.2
html5lib==1.1
iniconfig==2.1.0
lxml==5.3.1
packaging==24.2
pluggy==1.5.0
pytest==8.3.5
pytest-cov==6.0.0
six==1.17.0
-e git+https://github.com/facelessuser/soupsieve.git@474037c5b61605b82cc5166a8d0f5b809b4c4b5c#egg=soupsieve
tomli==2.2.1
typing_extensions==4.13.0
webencodings==0.5.1

name: soupsieve
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - pip=25.0=py39h06a4308_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - beautifulsoup4==4.13.3
      - coverage==7.8.0
      - exceptiongroup==1.2.2
      - html5lib==1.1
      - iniconfig==2.1.0
      - lxml==5.3.1
      - packaging==24.2
      - pluggy==1.5.0
      - pytest==8.3.5
      - pytest-cov==6.0.0
      - six==1.17.0
      - tomli==2.2.1
      - typing-extensions==4.13.0
      - webencodings==0.5.1
prefix: /opt/conda/envs/soupsieve

tests/test_level4.py::TestLevel4::test_read_only tests/test_level4.py::TestLevel4::test_read_write
tests/test_level4.py::TestLevel4::test_anylink tests/test_level4.py::TestLevel4::test_attribute_case tests/test_level4.py::TestLevel4::test_attribute_type_case tests/test_level4.py::TestLevel4::test_current_past_future tests/test_level4.py::TestLevel4::test_default tests/test_level4.py::TestLevel4::test_focus_visible tests/test_level4.py::TestLevel4::test_focus_within tests/test_level4.py::TestLevel4::test_has tests/test_level4.py::TestLevel4::test_indeterminate tests/test_level4.py::TestLevel4::test_is_matches_where tests/test_level4.py::TestLevel4::test_lang tests/test_level4.py::TestLevel4::test_local_link tests/test_level4.py::TestLevel4::test_multi_nested_not tests/test_level4.py::TestLevel4::test_nth_child_of_s tests/test_level4.py::TestLevel4::test_optional tests/test_level4.py::TestLevel4::test_paused tests/test_level4.py::TestLevel4::test_placeholder tests/test_level4.py::TestLevel4::test_playing tests/test_level4.py::TestLevel4::test_required tests/test_level4.py::TestLevel4::test_scope tests/test_level4.py::TestLevel4::test_target_within tests/test_level4.py::TestLevel4::test_user_invalid
docs/src/markdown/selectors.md soupsieve/css_parser.py
docs/src/markdown/selectors.md soupsieve/css_parser.py