Repository: facelessuser/soupsieve

Issue:
:placeholder-shown should not match if inputs have a value
If the input/textarea have text that overrides the placeholder, they should not match. For inputs, we need to check the value attribute and see if it is empty or not.  Textarea we must check the content of the element. Firefox seems to look to count a single newline as not being real content.

Discussion:
facelessuser: This will most likely fail on Windows. Still looking for a resolution to work with the different parsers.
facelessuser: I guess it will pass, I forgot we normalize line endings in our tests to avoid issues on different platforms 🤷‍♂ . We still need to figure out what we want as the final solution though to handle the differences in the parsers in relation to carriage returns.

Patch:
diff --git a/docs/src/markdown/about/changelog.md b/docs/src/markdown/about/changelog.md
index 1988f77..9cc5b1d 100644
--- a/docs/src/markdown/about/changelog.md
+++ b/docs/src/markdown/about/changelog.md
@@ -5,6 +5,10 @@
 - **NEW**: `SelectorSyntaxError` is derived from `Exception` not `SyntaxError`.
 - **NEW**: Drop support for EOL Python versions (Python 2 and Python < 3.5).
 
+## 1.9.5
+
+- **FIX**: `:placeholder-shown` should not match if the element has content that overrides the placeholder.
+
 ## 1.9.4
 
 - **FIX**: `:checked` rule was too strict with `option` elements. The specification for `:checked` does not require an
diff --git a/docs/src/markdown/selectors.md b/docs/src/markdown/selectors.md
index a59dbb0..a4d3556 100644
--- a/docs/src/markdown/selectors.md
+++ b/docs/src/markdown/selectors.md
@@ -2007,6 +2007,17 @@ Selects any `#!html <input>` or `#!html <textarea>` element that is currently di
 [<input id="0" placeholder="This is some text"/>, <textarea id="1" placeholder="This is some text"></textarea>]
 ```
 
+!!! note "Parser Differences"
+    In general, when an input has a placeholder, but the element also has valid content, the placeholder is not shown.
+    For instance, when a `textarea` has actual text associated with the element, the placeholder is overridden with the
+    actual content. A `textarea` is allowed no more than a single newline to be considered as having no content
+    (carriage returns don't count).
+
+    `html5lib` will strip out carriage returns, but `lxml` and `html.parser` will not. This will cause a difference
+    between the parsers when dealing with Windows style line endings and `textareas`. `html5lib` seems to follow
+    *closest* to what real browsers do. Soup Sieve is simply following the specification as best it can. Unfortunately,
+    it can't account for the quirks of the parsers in this case without introducing other issues.
+
 !!! tip "Additional Reading"
     https://developer.mozilla.org/en-US/docs/Web/CSS/:placeholder-shown
 
diff --git a/soupsieve/css_match.py b/soupsieve/css_match.py
index 20dc9b0..109be63 100644
--- a/soupsieve/css_match.py
+++ b/soupsieve/css_match.py
@@ -1288,6 +1288,21 @@ class _Match(object):
             self.get_prefix(el) is not None
         )
 
+    def match_placeholder_shown(self, el):
+        """
+        Match placeholder shown according to HTML spec.
+
+        - text area should be checked if they have content. A single newline does not count as content.
+
+        """
+
+        match = False
+        content = self.get_text(el)
+        if content in ('', '\n'):
+            match = True
+
+        return match
+
     def match_selectors(self, el, selectors):
         """Check if element matches one of the selectors."""
 
@@ -1320,6 +1335,9 @@ class _Match(object):
                 # Verify element is scope
                 if selector.flags & ct.SEL_SCOPE and not self.match_scope(el):
                     continue
+                # Verify element has placeholder shown
+                if selector.flags & ct.SEL_PLACEHOLDER_SHOWN and not self.match_placeholder_shown(el):
+                    continue
                 # Verify `nth` matches
                 if not self.match_nth(el, selector.nth):
                     continue
diff --git a/soupsieve/css_parser.py b/soupsieve/css_parser.py
index 0eb6443..9103a77 100644
--- a/soupsieve/css_parser.py
+++ b/soupsieve/css_parser.py
@@ -190,6 +190,7 @@ FLG_INDETERMINATE = 0x20
 FLG_OPEN = 0x40
 FLG_IN_RANGE = 0x80
 FLG_OUT_OF_RANGE = 0x100
+FLG_PLACEHOLDER_SHOWN = 0x200
 
 # Maximum cached patterns to store
 _MAXCACHE = 500
@@ -878,6 +879,7 @@ class CSSParser(object):
         is_indeterminate = bool(flags & FLG_INDETERMINATE)
         is_in_range = bool(flags & FLG_IN_RANGE)
         is_out_of_range = bool(flags & FLG_OUT_OF_RANGE)
+        is_placeholder_shown = bool(flags & FLG_PLACEHOLDER_SHOWN)
 
         if self.debug:  # pragma: no cover
             if is_pseudo:
@@ -898,6 +900,8 @@ class CSSParser(object):
                 print('    is_in_range: True')
             if is_out_of_range:
                 print('    is_out_of_range: True')
+            if is_placeholder_shown:
+                print('    is_placeholder_shown: True')
 
         if is_relative:
             selectors.append(_Selector())
@@ -1004,6 +1008,8 @@ class CSSParser(object):
             selectors[-1].flags = ct.SEL_IN_RANGE
         if is_out_of_range:
             selectors[-1].flags = ct.SEL_OUT_OF_RANGE
+        if is_placeholder_shown:
+            selectors[-1].flags = ct.SEL_PLACEHOLDER_SHOWN
 
         return ct.SelectorList([s.freeze() for s in selectors], is_not, is_html)
 
@@ -1122,22 +1128,20 @@ CSS_OPTIONAL = CSSParser(
 # CSS pattern for `:placeholder-shown`
 CSS_PLACEHOLDER_SHOWN = CSSParser(
     '''
-    html|*:is(
-        input:is(
-            :not([type]),
-            [type=""],
-            [type=text],
-            [type=search],
-            [type=url],
-            [type=tel],
-            [type=email],
-            [type=password],
-            [type=number]
-        ),
-        textarea
-    )[placeholder][placeholder!='']
+    html|input:is(
+        :not([type]),
+        [type=""],
+        [type=text],
+        [type=search],
+        [type=url],
+        [type=tel],
+        [type=email],
+        [type=password],
+        [type=number]
+    )[placeholder][placeholder!='']:is(:not([value]), [value=""]),
+    html|textarea[placeholder][placeholder!='']
     '''
-).process_selectors(flags=FLG_PSEUDO | FLG_HTML)
+).process_selectors(flags=FLG_PSEUDO | FLG_HTML | FLG_PLACEHOLDER_SHOWN)
 # CSS pattern default for `:nth-child` "of S" feature
 CSS_NTH_OF_S_DEFAULT = CSSParser(
     '*|*'
diff --git a/soupsieve/css_types.py b/soupsieve/css_types.py
index 0886f0d..629c2a9 100644
--- a/soupsieve/css_types.py
+++ b/soupsieve/css_types.py
@@ -26,6 +26,7 @@ SEL_DIR_RTL = 0x40
 SEL_IN_RANGE = 0x80
 SEL_OUT_OF_RANGE = 0x100
 SEL_DEFINED = 0x200
+SEL_PLACEHOLDER_SHOWN = 0x400
 
 
 class Immutable(object):


Test patch:
diff --git a/tests/test_level4/test_placeholder_shown.py b/tests/test_level4/test_placeholder_shown.py
index fe5bc7f..90fea7c 100644
--- a/tests/test_level4/test_placeholder_shown.py
+++ b/tests/test_level4/test_placeholder_shown.py
@@ -9,12 +9,15 @@ class TestPlaceholderShown(util.TestCase):
         """Test placeholder shown."""
 
         markup = """
+        /* These have a placeholder. */
         <input id="0" placeholder="This is some text">
         <textarea id="1" placeholder="This is some text"></textarea>
 
+        /* These do not have a placeholder. */
         <input id="2" placeholder="">
         <input id="3">
 
+        /* All types that should register has having a placeholder. */
         <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">
@@ -25,6 +28,7 @@ class TestPlaceholderShown(util.TestCase):
         <input id="11" type="" placeholder="This is some text">
         <input id="12" type placeholder="This is some text">
 
+        /* Types that should not register has having a placeholder. */
         <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">
@@ -40,11 +44,28 @@ class TestPlaceholderShown(util.TestCase):
         <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">
+
+        /* Value will not override this instance as value is empty. */
+        <input id="28" type placeholder="This is some text" value="">
+
+        /* Value will override this input */
+        <input id="29" type placeholder="This is some text" value="Actual value">
+
+        /* Text area content overides the placeholder
+        <textarea id="30" placeholder="This is some text">Value</textarea>
+        <textarea id="31" placeholder="This is some text">
+
+
+        </textarea>
+
+        /* Text area is still considered empty with a single new line (does not include carriage return). */
+        <textarea id="32" placeholder="This is some text">
+        </textarea>
         """
 
         self.assert_selector(
             markup,
             ":placeholder-shown",
-            ['0', '1', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+            ['0', '1', '4', '5', '6', '7', '8', '9', '10', '11', '12', '28', '32'],
             flags=util.HTML
         )