Repository: facelessuser/soupsieve

Issue:
Attribute Selector Case Sensitivity: Whitespace
The CSS Level 4 specification doesn't explicitly require whitespace before the attribute selector's case-sensitivity identifier; simply that the identifier (`i|s`) must occur before the closing bracket [1]. The following example does not throw any errors in either Firefox or Chrome:

``` javascript
document.querySelector("a[attr='value'i]");
```

All the examples given by the specification incorporate whitespace. In the examples below (bare attributes or unquoted attribute values) a space is clearly required.

``` css
a[attr=value i]
a[attr i]
```

Do you think soupsieve should compile the following CSS selector, or throw an error?

``` python
sv.compile("a[attr='value'i]")
```

soupsieve: 2.4

[1] [CSS Level 4, Attribute Selectors, Case-sensitivity](https://www.w3.org/TR/selectors-4/#attribute-case)

Discussion:

Patch:
diff --git a/docs/src/markdown/about/changelog.md b/docs/src/markdown/about/changelog.md
index 9d1aade..bbff9a1 100644
--- a/docs/src/markdown/about/changelog.md
+++ b/docs/src/markdown/about/changelog.md
@@ -1,5 +1,9 @@
 # Changelog
 
+## 2.4.1
+
+- **FIX**: Attribute syntax for case insensitive flag optionally allows a space, it does not require one.
+
 ## 2.4
 
 - **NEW**: Update to support changes related to `:lang()` in the official CSS spec. `:lang("")` should match unspecified
diff --git a/soupsieve/__meta__.py b/soupsieve/__meta__.py
index 5369314..6f35e15 100644
--- a/soupsieve/__meta__.py
+++ b/soupsieve/__meta__.py
@@ -193,5 +193,5 @@ def parse_version(ver: str) -> Version:
     return Version(major, minor, micro, release, pre, post, dev)
 
 
-__version_info__ = Version(2, 4, 0, "final")
+__version_info__ = Version(2, 4, 1, "final")
 __version__ = __version_info__._get_canonical()
diff --git a/soupsieve/css_parser.py b/soupsieve/css_parser.py
index 3698f2e..10ac6f2 100644
--- a/soupsieve/css_parser.py
+++ b/soupsieve/css_parser.py
@@ -113,7 +113,7 @@ VALUE = r'''
 '''.format(nl=NEWLINE, ident=IDENTIFIER)
 # Attribute value comparison. `!=` is handled special as it is non-standard.
 ATTR = r'''
-(?:{ws}*(?P<cmp>[!~^|*$]?=){ws}*(?P<value>{value})(?:{ws}+(?P<case>[is]))?)?{ws}*\]
+(?:{ws}*(?P<cmp>[!~^|*$]?=){ws}*(?P<value>{value})(?:{ws}*(?P<case>[is]))?)?{ws}*\]
 '''.format(ws=WSC, value=VALUE)
 
 # Selector patterns


Test patch:
diff --git a/tests/test_level4/test_attribute.py b/tests/test_level4/test_attribute.py
index 873e11f..42bcc47 100644
--- a/tests/test_level4/test_attribute.py
+++ b/tests/test_level4/test_attribute.py
@@ -36,6 +36,23 @@ class TestAttribute(util.TestCase):
             flags=util.HTML
         )
 
+    def test_attribute_case_insensitive_flag_spacing(self):
+        """Test attribute value case insensitivity spacing."""
+
+        self.assert_selector(
+            self.MARKUP,
+            "[class*=WORDSi]",
+            [],
+            flags=util.HTML
+        )
+
+        self.assert_selector(
+            self.MARKUP,
+            "[class*='WORDS'i]",
+            ["0", "3", "pre"],
+            flags=util.HTML
+        )
+
     def test_attribute_forced_case_insensitive_xml(self):
         """Test that attribute value case insensitivity can be forced in XML."""