Bug 1512386. Add support for 's' flag on attribute selectors. r=emilio
authorBoris Zbarsky <bzbarsky@mit.edu>
Mon, 10 Dec 2018 22:24:49 +0000
changeset 449784 bfd73976f27e1dc2b64f0203f9460f726e3ebedd
parent 449783 a412f608259788f14481b97c32e9067f4873b2bc
child 449785 755ac698de795b016bec6f6b14d15d6dd5f88a23
push id74474
push userbzbarsky@mozilla.com
push dateTue, 11 Dec 2018 04:12:58 +0000
treeherderautoland@bfd73976f27e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersemilio
bugs1512386
milestone66.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1512386. Add support for 's' flag on attribute selectors. r=emilio We could keep using ParsedCaseSensitivity::CaseSensitive as a temporary stand-in for "case-sensitive or maybe not depending on what HTML says" until we check the attribute list, but it seems better to make that explicit. Differential Revision: https://phabricator.services.mozilla.com/D14093
servo/components/selectors/attr.rs
servo/components/selectors/parser.rs
testing/web-platform/tests/css/selectors/attribute-selectors/attribute-case/cssom.html
testing/web-platform/tests/css/selectors/attribute-selectors/attribute-case/resources/syntax-quirks.html
testing/web-platform/tests/css/selectors/attribute-selectors/attribute-case/resources/syntax-xml.xhtml
testing/web-platform/tests/css/selectors/attribute-selectors/attribute-case/semantics.html
testing/web-platform/tests/css/selectors/attribute-selectors/attribute-case/syntax.html
--- a/servo/components/selectors/attr.rs
+++ b/servo/components/selectors/attr.rs
@@ -129,33 +129,41 @@ impl AttrSelectorOperator {
     }
 }
 
 /// The definition of whitespace per CSS Selectors Level 3 ยง 4.
 pub static SELECTOR_WHITESPACE: &'static [char] = &[' ', '\t', '\n', '\r', '\x0C'];
 
 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
 pub enum ParsedCaseSensitivity {
+    // 's' was specified.
+    ExplicitCaseSensitive,
+    // 'i' was specified.
+    AsciiCaseInsensitive,
+    // No flags were specified and HTML says this is a case-sensitive attribute.
     CaseSensitive,
-    AsciiCaseInsensitive,
+    // No flags were specified and HTML says this is a case-insensitive attribute.
     AsciiCaseInsensitiveIfInHtmlElementInHtmlDocument,
 }
 
 impl ParsedCaseSensitivity {
     pub fn to_unconditional(self, is_html_element_in_html_document: bool) -> CaseSensitivity {
         match self {
             ParsedCaseSensitivity::AsciiCaseInsensitiveIfInHtmlElementInHtmlDocument
                 if is_html_element_in_html_document =>
             {
                 CaseSensitivity::AsciiCaseInsensitive
             },
             ParsedCaseSensitivity::AsciiCaseInsensitiveIfInHtmlElementInHtmlDocument => {
                 CaseSensitivity::CaseSensitive
             },
-            ParsedCaseSensitivity::CaseSensitive => CaseSensitivity::CaseSensitive,
+            ParsedCaseSensitivity::CaseSensitive |
+            ParsedCaseSensitivity::ExplicitCaseSensitive => {
+                CaseSensitivity::CaseSensitive
+            },
             ParsedCaseSensitivity::AsciiCaseInsensitive => CaseSensitivity::AsciiCaseInsensitive,
         }
     }
 }
 
 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
 pub enum CaseSensitivity {
     CaseSensitive,
--- a/servo/components/selectors/parser.rs
+++ b/servo/components/selectors/parser.rs
@@ -1218,16 +1218,17 @@ impl<Impl: SelectorImpl> ToCss for Compo
                 operator.to_css(dest)?;
                 dest.write_char('"')?;
                 write!(CssStringWriter::new(dest), "{}", value)?;
                 dest.write_char('"')?;
                 match case_sensitivity {
                     ParsedCaseSensitivity::CaseSensitive |
                     ParsedCaseSensitivity::AsciiCaseInsensitiveIfInHtmlElementInHtmlDocument => {},
                     ParsedCaseSensitivity::AsciiCaseInsensitive => dest.write_str(" i")?,
+                    ParsedCaseSensitivity::ExplicitCaseSensitive => dest.write_str(" s")?,
                 }
                 dest.write_char(']')
             },
             AttributeOther(ref attr_selector) => attr_selector.to_css(dest),
 
             // Pseudo-classes
             Negation(ref arg) => {
                 dest.write_str(":not(")?;
@@ -1296,16 +1297,17 @@ impl<Impl: SelectorImpl> ToCss for AttrS
                 operator.to_css(dest)?;
                 dest.write_char('"')?;
                 write!(CssStringWriter::new(dest), "{}", expected_value)?;
                 dest.write_char('"')?;
                 match case_sensitivity {
                     ParsedCaseSensitivity::CaseSensitive |
                     ParsedCaseSensitivity::AsciiCaseInsensitiveIfInHtmlElementInHtmlDocument => {},
                     ParsedCaseSensitivity::AsciiCaseInsensitive => dest.write_str(" i")?,
+                    ParsedCaseSensitivity::ExplicitCaseSensitive => dest.write_str(" i")?,
                 }
             },
         }
         dest.write_char(']')
     }
 }
 
 impl<Impl: SelectorImpl> ToCss for LocalName<Impl> {
@@ -1706,34 +1708,25 @@ where
 
         AttrSelectorOperator::Includes => value.is_empty() || value.contains(SELECTOR_WHITESPACE),
 
         AttrSelectorOperator::Prefix |
         AttrSelectorOperator::Substring |
         AttrSelectorOperator::Suffix => value.is_empty(),
     };
 
-    let mut case_sensitivity = parse_attribute_flags(input)?;
+    let attribute_flags = parse_attribute_flags(input)?;
 
     let value = value.as_ref().into();
     let local_name_lower;
     let local_name_is_ascii_lowercase;
+    let case_sensitivity;
     {
         let local_name_lower_cow = to_ascii_lowercase(&local_name);
-        if let ParsedCaseSensitivity::CaseSensitive = case_sensitivity {
-            if namespace.is_none() && include!(concat!(
-                env!("OUT_DIR"),
-                "/ascii_case_insensitive_html_attributes.rs"
-            ))
-            .contains(&*local_name_lower_cow)
-            {
-                case_sensitivity =
-                    ParsedCaseSensitivity::AsciiCaseInsensitiveIfInHtmlElementInHtmlDocument
-            }
-        }
+        case_sensitivity = attribute_flags.to_case_sensitivity(local_name_lower_cow.as_ref(), namespace.is_some());
         local_name_lower = local_name_lower_cow.as_ref().into();
         local_name_is_ascii_lowercase = matches!(local_name_lower_cow, Cow::Borrowed(..));
     }
     let local_name = local_name.as_ref().into();
     if namespace.is_some() || !local_name_is_ascii_lowercase {
         Ok(Component::AttributeOther(Box::new(
             AttrSelectorWithOptionalNamespace {
                 namespace,
@@ -1753,30 +1746,77 @@ where
             operator: operator,
             value: value,
             case_sensitivity: case_sensitivity,
             never_matches: never_matches,
         })
     }
 }
 
+/// An attribute selector can have 's' or 'i' as flags, or no flags at all.
+enum AttributeFlags {
+    // Matching should be case-sensitive ('s' flag).
+    CaseSensitive,
+    // Matching should be case-insensitive ('i' flag).
+    AsciiCaseInsensitive,
+    // No flags.  Matching behavior depends on the name of the attribute.
+    CaseSensitivityDependsOnName
+}
+
+impl AttributeFlags {
+    fn to_case_sensitivity(
+        self,
+        local_name: &str,
+        have_namespace: bool,
+    ) -> ParsedCaseSensitivity {
+        match self {
+            AttributeFlags::CaseSensitive =>
+                ParsedCaseSensitivity::ExplicitCaseSensitive,
+            AttributeFlags::AsciiCaseInsensitive =>
+                ParsedCaseSensitivity::AsciiCaseInsensitive,
+            AttributeFlags::CaseSensitivityDependsOnName => {
+                if !have_namespace && include!(concat!(
+                    env!("OUT_DIR"),
+                    "/ascii_case_insensitive_html_attributes.rs"
+                ))
+                    .contains(local_name)
+                {
+                    ParsedCaseSensitivity::AsciiCaseInsensitiveIfInHtmlElementInHtmlDocument
+                } else {
+                    ParsedCaseSensitivity::CaseSensitive
+                }
+            }
+        }
+    }
+}
+
 fn parse_attribute_flags<'i, 't>(
     input: &mut CssParser<'i, 't>,
-) -> Result<ParsedCaseSensitivity, BasicParseError<'i>> {
+) -> Result<AttributeFlags, BasicParseError<'i>> {
     let location = input.current_source_location();
-    match input.next() {
-        Err(_) => {
-            // Selectors spec says language-defined, but HTML says sensitive.
-            Ok(ParsedCaseSensitivity::CaseSensitive)
-        },
-        Ok(&Token::Ident(ref value)) if value.eq_ignore_ascii_case("i") => {
-            Ok(ParsedCaseSensitivity::AsciiCaseInsensitive)
-        },
-        Ok(t) => Err(location.new_basic_unexpected_token_error(t.clone())),
-    }
+    let token = match input.next() {
+        Ok(t) => t,
+        Err(..) => {
+            // Selectors spec says language-defined; HTML says it depends on the
+            // exact attribute name.
+            return Ok(AttributeFlags::CaseSensitivityDependsOnName);
+        }
+    };
+
+    let ident = match *token {
+        Token::Ident(ref i) => i,
+        ref other => return Err(location.new_basic_unexpected_token_error(other.clone())),
+    };
+
+    Ok(match_ignore_ascii_case! {
+        ident,
+        "i" => AttributeFlags::AsciiCaseInsensitive,
+        "s" => AttributeFlags::CaseSensitive,
+        _ => return Err(location.new_basic_unexpected_token_error(token.clone())),
+    })
 }
 
 /// Level 3: Parse **one** simple_selector.  (Though we might insert a second
 /// implied "<defaultns>|*" type selector.)
 fn parse_negation<'i, 't, P, Impl>(
     parser: &P,
     input: &mut CssParser<'i, 't>,
 ) -> Result<Component<Impl>, ParseError<'i, P::Error>>
--- a/testing/web-platform/tests/css/selectors/attribute-selectors/attribute-case/cssom.html
+++ b/testing/web-platform/tests/css/selectors/attribute-selectors/attribute-case/cssom.html
@@ -19,16 +19,19 @@ function new_sheet(use_media) {
 }
 
 var tests = [
   // test input, expected serialization
   ['[foo="bar"] /* sanity check */', '[foo="bar"]'],
   ['[foo="bar" i]', '[foo="bar" i]'],
   ['[foo="bar" /**/ i]', '[foo="bar" i]'],
   ['[foo="bar"/**/i]', '[foo="bar" i]'],
+  ['[foo="bar" s]', '[foo="bar" s]'],
+  ['[foo="bar" /**/ s]', '[foo="bar" s]'],
+  ['[foo="bar"/**/s]', '[foo="bar" s]'],
 ]
 
 tests.forEach(function(arr) {
   var input = arr[0];
   var expected = arr[1];
   ["", " in @media"].forEach(function(use_media) {
     test(function() {
       var sheet = new_sheet(use_media);
--- a/testing/web-platform/tests/css/selectors/attribute-selectors/attribute-case/resources/syntax-quirks.html
+++ b/testing/web-platform/tests/css/selectors/attribute-selectors/attribute-case/resources/syntax-quirks.html
@@ -1,5 +1,5 @@
 <style></style>
-<div id=test foo="BAR"></div>
+<div id=test foo="BAR" baz="quux"></div>
 <script>
 var mode = "quirks mode";
 </script>
--- a/testing/web-platform/tests/css/selectors/attribute-selectors/attribute-case/resources/syntax-xml.xhtml
+++ b/testing/web-platform/tests/css/selectors/attribute-selectors/attribute-case/resources/syntax-xml.xhtml
@@ -1,11 +1,11 @@
 <html xmlns="http://www.w3.org/1999/xhtml">
 <head>
 <style></style>
 </head>
 <body>
-<div id="test" foo="BAR"/>
+<div id="test" foo="BAR" baz="quux"/>
 <script>
 var mode = "XML";
 </script>
 </body>
 </html>
--- a/testing/web-platform/tests/css/selectors/attribute-selectors/attribute-case/semantics.html
+++ b/testing/web-platform/tests/css/selectors/attribute-selectors/attribute-case/semantics.html
@@ -7,16 +7,27 @@
 <div id=log></div>
 <iframe id="quirks" src="resources/semantics-quirks.html"></iframe>
 <iframe id="xml" src="resources/semantics-xml.xhtml"></iframe>
 <script>
 setup({explicit_done:true});
 var match = [
   // [selector, attrs...] (each attr is [ns, name, value])
   ["[foo='BAR'] /* sanity check (match) */", ["", "foo", "BAR"]],
+  ["[foo='bar'] /* sanity check (match) */", ["", "foo", "bar"]],
+  ["[align='left'] /* sanity check (match) */", ["", "align", "left"]],
+  ["[class~='a'] /* sanity check (match) */", ["", "class", "X a b"]],
+  ["[class~='A'] /* sanity check (match) */", ["", "class", "x A B"]],
+  ["[id^='a'] /* sanity check (match) */", ["", "id", "ab"]],
+  ["[id$='A'] /* sanity check (match) */", ["", "id", "XA"]],
+  ["[lang|='a'] /* sanity check (match) */", ["", "lang", "a-b"]],
+  ["[lang*='A'] /* sanity check (match) */", ["", "lang", "XAB"]],
+  ["@namespace x 'http://www.w3.org/XML/1998/namespace'; [x|lang='A'] /* sanity check (match) */",
+   ["http://www.w3.org/XML/1998/namespace", "lang", "A"]],
+  // Case-insensitive matching.
   ["[foo='bar' i]", ["", "foo", "BAR"]],
   ["[foo='' i]", ["", "foo", ""]],
   ["[foo='a\u0308' i] /* COMBINING in both */", ["", "foo", "A\u0308"]],
   ["[foo='A\u0308' i] /* COMBINING in both */", ["", "foo", "a\u0308"]],
   ["[*|foo='bar' i]", ["", "foo", "x"], ["a", "foo", "x"], ["b", "foo", "BAR"], ["c", "foo", "x"]],
   ["[*|foo='bar' i]", ["", "foo", "BAR"], ["a", "foo", "x"], ["b", "foo", "x"], ["c", "foo", "x"]],
   ["[align='left' i]", ["", "align", "LEFT"]],
   ["[align='LEFT' i]", ["", "align", "left"]],
@@ -27,19 +38,57 @@ var match = [
   ["[lang|='a' i]", ["", "lang", "A-B"]],
   ["[lang*='A' i]", ["", "lang", "xab"]],
   ["[*|lang='a' i]", ["http://www.w3.org/XML/1998/namespace", "lang", "A"]],
   ["[*|lang='A' i]", ["http://www.w3.org/XML/1998/namespace", "lang", "a"]],
   ["@namespace x 'http://www.w3.org/XML/1998/namespace'; [x|lang='A' i]", ["http://www.w3.org/XML/1998/namespace", "lang", "a"]],
   ["[foo='bar' i][foo='bar' i]", ["", "foo", "BAR"]],
   ["[foo='BAR'][foo='bar' i]", ["", "foo", "BAR"]],
   ["[foo='bar' i][foo='BAR']", ["", "foo", "BAR"]],
+  // Case-sensitive matching.
+  ["[foo='bar' s]", ["", "foo", "bar"]],
+  ["[foo='' s]", ["", "foo", ""]],
+  ["[foo='a\u0308' s] /* COMBINING in both */", ["", "foo", "a\u0308"]],
+  ["[*|foo='bar' s]", ["", "foo", "x"], ["a", "foo", "x"], ["b", "foo", "bar"], ["c", "foo", "x"]],
+  ["[*|foo='bar' s]", ["", "foo", "bar"], ["a", "foo", "x"], ["b", "foo", "x"], ["c", "foo", "x"]],
+  ["[align='left' s]", ["", "align", "left"]],
+  ["[align='LEFT' s]", ["", "align", "LEFT"]],
+  ["[class~='a' s]", ["", "class", "x a b"]],
+  ["[class~='A' s]", ["", "class", "X A B"]],
+  ["[id^='a' s]", ["", "id", "ab"]],
+  ["[id$='A' s]", ["", "id", "XA"]],
+  ["[lang|='a' s]", ["", "lang", "a-b"]],
+  ["[lang*='A' s]", ["", "lang", "XAB"]],
+  ["[*|lang='a' s]", ["http://www.w3.org/XML/1998/namespace", "lang", "a"]],
+  ["[*|lang='A' s]", ["http://www.w3.org/XML/1998/namespace", "lang", "A"]],
+  ["@namespace x 'http://www.w3.org/XML/1998/namespace'; [x|lang='A' s]", ["http://www.w3.org/XML/1998/namespace", "lang", "A"]],
+  ["[foo='BAR' s][foo='BAR' s]", ["", "foo", "BAR"]],
 ];
+
+var matchHTMLOnly = [
+  ["[align='left'] /* sanity check (match HTML) */", ["", "align", "LEFT"]],
+  ["[align='LEFT'] /* sanity check (match HTML) */", ["", "align", "left"]],
+  ["[lang|='a'] /* sanity check (match HTML) */", ["", "lang", "A-B"]],
+  ["[lang*='A'] /* sanity check (match HTML) */", ["", "lang", "xab"]],
+];
+
 var nomatch = [
   ["[missingattr] /* sanity check (no match) */", ["", "foo", "BAR"]],
+  ["[foo='bar'] /* sanity check (no match) */", ["", "foo", "BAR"]],
+  ["[class~='a'] /* sanity check (no match) */", ["", "class", "X A B"]],
+  ["[class~='A'] /* sanity check (no match) */", ["", "class", "x a b"]],
+  ["[id^='a'] /* sanity check (no match) */", ["", "id", "AB"]],
+  ["[id$='A']", ["", "id", "xa"]],
+  ["[*|lang='a'] /* sanity check (no match) */",
+   ["http://www.w3.org/XML/1998/namespace", "lang", "A"]],
+  ["[*|lang='A'] /* sanity check (no match) */",
+   ["http://www.w3.org/XML/1998/namespace", "lang", "a"]],
+  ["@namespace x 'http://www.w3.org/XML/1998/namespace'; [x|lang='A'] /* sanity check (no match) */",
+   ["http://www.w3.org/XML/1998/namespace", "lang", "a"]],
+  // Case-insensitive matching.
   ["[foo='' i]", ["", "foo", "BAR"]],
   ["[foo='\u0000' i] /* \\0 in selector */", ["", "foo", ""]],
   ["[foo='' i] /* \\0 in attribute */", ["", "foo", "\u0000"]],
   ["[foo='\u00E4' i]", ["", "foo", "\u00C4"]],
   ["[foo='\u00C4' i]", ["", "foo", "\u00E4"]],
   ["[foo='a\u0308' i] /* COMBINING in selector */", ["", "foo", "\u00C4"]],
   ["[foo~='a\u0308' i] /* COMBINING in selector */", ["", "foo", "\u00E4"]],
   ["[foo^='A\u0308' i] /* COMBINING in selector */", ["", "foo", "\u00C4"]],
@@ -68,16 +117,76 @@ var nomatch = [
   ["[|foo='bar' i]", ["", "foo", "x"], ["a", "foo", "BAR"]],
   ["[foo='bar' i]", ["", "FOO", "bar"]],
   ["[foo='\t' i] /* tab in selector */", ["", "foo", " "]],
   ["[foo=' ' i] /* tab in attribute */", ["", "foo", "\t"]],
   ["@namespace x 'a'; [x|foo='' i]", ["A", "foo", ""]],
   ["@namespace x 'A'; [x|foo='' i]", ["a", "foo", ""]],
   ["[foo='bar' i][foo='bar']", ["", "foo", "BAR"]],
   ["[foo='bar' i]", ["", "baz", "BAR"]],
+  // Case-sensitive matching
+  ["[foo='' s]", ["", "foo", "BAR"]],
+  ["[foo='\u0000' s] /* \\0 in selector */", ["", "foo", ""]],
+  ["[foo='' s] /* \\0 in attribute */", ["", "foo", "\u0000"]],
+  ["[foo='\u00E4' s]", ["", "foo", "\u00C4"]],
+  ["[foo='\u00C4' s]", ["", "foo", "\u00E4"]],
+  ["[foo='a\u0308' s] /* COMBINING in selector */", ["", "foo", "\u00C4"]],
+  ["[foo~='a\u0308' s] /* COMBINING in selector */", ["", "foo", "\u00E4"]],
+  ["[foo^='A\u0308' s] /* COMBINING in selector */", ["", "foo", "\u00C4"]],
+  ["[foo$='A\u0308' s] /* COMBINING in selector */", ["", "foo", "\u00E4"]],
+  ["[foo*='\u00E4' s] /* COMBINING in attribute */", ["", "foo", "a\u0308"]],
+  ["[foo|='\u00E4' s] /* COMBINING in attribute */", ["", "foo", "A\u0308"]],
+  ["[foo='\u00C4' s] /* COMBINING in attribute */", ["", "foo", "a\u0308"]],
+  ["[foo='\u00C4' s] /* COMBINING in attribute */", ["", "foo", "A\u0308"]],
+  ["[foo='a\u0308' s] /* COMBINING in selector */", ["", "foo", "a"]],
+  ["[foo='a\u0308' s] /* COMBINING in selector */", ["", "foo", "A"]],
+  ["[foo='A\u0308' s] /* COMBINING in selector */", ["", "foo", "a"]],
+  ["[foo='A\u0308' s] /* COMBINING in selector */", ["", "foo", "A"]],
+  ["[foo='a' s] /* COMBINING in attribute */", ["", "foo", "a\u0308"]],
+  ["[foo='A' s] /* COMBINING in attribute */", ["", "foo", "a\u0308"]],
+  ["[foo='a' s] /* COMBINING in attribute */", ["", "foo", "A\u0308"]],
+  ["[foo='A' s] /* COMBINING in attribute */", ["", "foo", "A\u0308"]],
+  ["[foo='i' s]", ["", "foo", "\u0130"]],
+  ["[foo='i' s]", ["", "foo", "\u0131"]],
+  ["[foo='I' s]", ["", "foo", "\u0130"]],
+  ["[foo='I' s]", ["", "foo", "\u0131"]],
+  ["[foo='\u0130' s]", ["", "foo", "i"]],
+  ["[foo='\u0131' s]", ["", "foo", "i"]],
+  ["[foo='\u0130' s]", ["", "foo", "I"]],
+  ["[foo='\u0131' s]", ["", "foo", "I"]],
+  ["[foo='bar' s]", ["", "foo", "x"], ["a", "foo", "BAR"]],
+  ["[|foo='bar' s]", ["", "foo", "x"], ["a", "foo", "BAR"]],
+  ["[foo='bar' s]", ["", "FOO", "bar"]],
+  ["[foo='\t' s] /* tab in selector */", ["", "foo", " "]],
+  ["[foo=' ' s] /* tab in attribute */", ["", "foo", "\t"]],
+  ["@namespace x 'a'; [x|foo='' s]", ["A", "foo", ""]],
+  ["@namespace x 'A'; [x|foo='' s]", ["a", "foo", ""]],
+  ["[foo='bar' s][foo='bar']", ["", "foo", "BAR"]],
+  ["[foo='bar' s]", ["", "baz", "BAR"]],
+  ["[foo='bar' s]", ["", "foo", "BAR"]],
+  ["[foo='a\u0308' s] /* COMBINING in both */", ["", "foo", "A\u0308"]],
+  ["[foo='A\u0308' s] /* COMBINING in both */", ["", "foo", "a\u0308"]],
+  ["[*|foo='bar' s]", ["", "foo", "x"], ["a", "foo", "x"], ["b", "foo", "BAR"], ["c", "foo", "x"]],
+  ["[*|foo='bar' s]", ["", "foo", "BAR"], ["a", "foo", "x"], ["b", "foo", "x"], ["c", "foo", "x"]],
+  ["[align='left' s]", ["", "align", "LEFT"]],
+  ["[align='LEFT' s]", ["", "align", "left"]],
+  ["[class~='a' s]", ["", "class", "X A B"]],
+  ["[class~='A' s]", ["", "class", "x a b"]],
+  ["[id^='a' s]", ["", "id", "AB"]],
+  ["[id$='A' s]", ["", "id", "xa"]],
+  ["[lang|='a' s]", ["", "lang", "A-B"]],
+  ["[lang*='A' s]", ["", "lang", "xab"]],
+  ["[*|lang='a' s]", ["http://www.w3.org/XML/1998/namespace", "lang", "A"]],
+  ["[*|lang='A' s]", ["http://www.w3.org/XML/1998/namespace", "lang", "a"]],
+  ["@namespace x 'http://www.w3.org/XML/1998/namespace'; [x|lang='A' s]", ["http://www.w3.org/XML/1998/namespace", "lang", "a"]],
+  ["[foo='bar' s][foo='bar' s]", ["", "foo", "BAR"]],
+  ["[foo='BAR' s][foo='bar']", ["", "foo", "BAR"]],
+  ["[foo='bar'][foo='BAR' s]", ["", "foo", "BAR"]],
+  ["[foo='BAR'][foo='bar' s]", ["", "foo", "BAR"]],
+  ["[foo='bar' s][foo='BAR']", ["", "foo", "bar"]],
 ];
 var mode = "standards mode";
 function format_attrs(attrs) {
   var rv = [];
   attrs.forEach(function(attr) {
     var str = "";
     var ns = attr[0];
     var name = attr[1];
@@ -102,17 +211,21 @@ onload = function() {
       elm = global.document.createElement('div');
       global.document.body.appendChild(elm);
     }
     function set_attrs(attrs) {
       attrs.forEach(function(attr) {
         elm.setAttributeNS(attr[0], attr[1], attr[2]);
       });
     }
-    match.forEach(function(arr) {
+    var localMatch = match.slice();
+    if (global != xml) {
+      localMatch.push(...matchHTMLOnly);
+    }
+    localMatch.forEach(function(arr) {
       var s = arr[0];
       var attrs = arr.slice(1);
       var ns_decl = s.substr(0, "@namespace".length) == "@namespace";
       test(function() {
         clean_slate();
         set_attrs(attrs);
         style.textContent = s + ' { visibility:hidden }';
         assert_equals(style.sheet.cssRules.length, (ns_decl ? 2 : 1), 'rule didn\'t parse into CSSOM');
--- a/testing/web-platform/tests/css/selectors/attribute-selectors/attribute-case/syntax.html
+++ b/testing/web-platform/tests/css/selectors/attribute-selectors/attribute-case/syntax.html
@@ -1,45 +1,73 @@
 <!doctype html>
 <title>Selectors: syntax of case-sensitivity attribute selector</title>
 <link rel="help" href="https://drafts.csswg.org/selectors/#attribute-case">
 <script src="/resources/testharness.js"></script>
 <script src="/resources/testharnessreport.js"></script>
 <style></style>
 <div id=log></div>
-<div id=test foo="BAR"></div>
+<div id=test foo="BAR" baz="quux"></div>
 <iframe id="quirks" src="resources/syntax-quirks.html"></iframe>
 <iframe id="xml" src="resources/syntax-xml.xhtml"></iframe>
 <script>
 setup({explicit_done:true});
 var valid = [
   "[foo='BAR'] /* sanity check (valid) */",
+  "[baz='quux'] /* sanity check (valid) */",
+  // Case-insensitive selectors.
   "[foo='bar' i]",
   "[foo='bar' I]",
   "[foo=bar i]",
   '[foo="bar" i]',
   "[foo='bar'i]",
   "[foo='bar'i ]",
   "[foo='bar' i ]",
   "[foo='bar' /**/ i]",
   "[foo='bar' i /**/ ]",
   "[foo='bar'/**/i/**/]",
   "[foo=bar/**/i]",
   "[foo='bar'\ti\t] /* \\t */",
   "[foo='bar'\ni\n] /* \\n */",
   "[foo='bar'\ri\r] /* \\r */",
   "[foo='bar' \\i]",
   "[foo='bar' \\69]",
+  "[foo='bar' \\49]",
   "[foo~='bar' i]",
   "[foo^='bar' i]",
   "[foo$='bar' i]",
   "[foo*='bar' i]",
   "[foo|='bar' i]",
   "[|foo='bar' i]",
   "[*|foo='bar' i]",
+  // Case-sensitive selectors.
+  "[baz='quux' s]",
+  "[baz='quux' S]",
+  "[baz=quux s]",
+  '[baz="quux" s]',
+  "[baz='quux's]",
+  "[baz='quux's ]",
+  "[baz='quux' s ]",
+  "[baz='quux' /**/ s]",
+  "[baz='quux' s /**/ ]",
+  "[baz='quux'/**/s/**/]",
+  "[baz=quux/**/s]",
+  "[baz='quux'\ts\t] /* \\t */",
+  "[baz='quux'\ns\n] /* \\n */",
+  "[baz='quux'\rs\r] /* \\r */",
+  "[baz='quux' \\s]",
+  "[baz='quux' \\73]",
+  "[baz='quux' \\53]",
+  "[baz~='quux' s]",
+  "[baz^='quux' s]",
+  "[baz$='quux' s]",
+  "[baz*='quux' s]",
+  "[baz|='quux' s]",
+  "[|baz='quux' s]",
+  "[*|baz='quux' s]",
 ];
 var invalid = [
   "[foo[ /* sanity check (invalid) */",
   "[foo='bar' i i]",
   "[foo i ='bar']",
   "[foo= i 'bar']",
   "[i foo='bar']",
   "[foo='bar' i\u0000] /* \\0 */",