Repository: postcss/postcss-selector-parser
Pull request #267 (2022-11-19 11:51:05)

attribute case insensitivity
fixes : https://github.com/postcss/postcss-selector-parser/issues/201

These are all valid and some where not correctly parsed :

```css
[foo="a" i] {
	order: 5.0;
}

[foo="b" I] {
	order: 5.1;
}

[foo="c"i] {
	order: 5.2;
}

[foo="d"I] {
	order: 5.3;
}

[foo="e" i ] {
	order: 5.4;
}

[foo="f" I ] {
	order: 5.5;
}

[foo="g" i
] {
	order: 5.6;
}

[foo="h" I
] {
	order: 5.7;
}
```

-----

744 tests passed

File               | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s                                                            
-------------------|---------|----------|---------|---------|------------------------------------------------------------------------------
All files          |   95.01 |    88.32 |   96.14 |   95.46 |                                                                              
 src               |   95.12 |    89.81 |   98.77 |   95.76 |                                                                              
  index.js         |     100 |      100 |     100 |     100 |                                                                              
  parser.js        |   94.09 |    88.45 |   98.18 |   94.48 | 162,224-226,229-231,236,247,301,325-328,331-339,514,536,710,762,831-834,1020 
  processor.js     |   94.55 |       90 |     100 |     100 | 10,19,28                                                                     
  sortAscending.js |     100 |      100 |     100 |     100 |                                                                              
  tokenTypes.js    |     100 |      100 |     100 |     100 |                                                                              
  tokenize.js      |   98.41 |    97.14 |     100 |    98.4 | 131-132                                                                      
 src/selectors     |   94.44 |     86.3 |   94.56 |   94.72 |                                                                              
  attribute.js     |   86.96 |    79.03 |   81.82 |   87.43 | 40,48-57,69-71,154,175,187,286,289,314,350,353,358,379,389                   
  className.js     |     100 |      100 |     100 |     100 |                                                                              
  combinator.js    |     100 |      100 |     100 |     100 |                                                                              
  comment.js       |     100 |      100 |     100 |     100 |                                                                              
  constructors.js  |     100 |      100 |     100 |     100 |                                                                              
  container.js     |   98.65 |    89.86 |     100 |   98.63 | 30,170                                                                       
  guards.js        |     100 |      100 |     100 |     100 |                                                                              
  id.js            |     100 |      100 |     100 |     100 |                                                                              
  index.js         |     100 |      100 |     100 |     100 |                                                                              
  namespace.js     |   96.43 |    94.12 |     100 |   96.43 | 13                                                                           
  nesting.js       |     100 |      100 |     100 |     100 |                                                                              
  node.js          |    93.9 |    90.79 |   89.47 |   94.94 | 19,22,62,147                                                                 
  pseudo.js        |     100 |       75 |     100 |     100 | 6                                                                            
  root.js          |   92.31 |    66.67 |     100 |   92.31 | 22                                                                           
  selector.js      |     100 |       50 |     100 |     100 | 6                                                                            
  string.js        |     100 |      100 |     100 |     100 |                                                                              
  tag.js           |     100 |       50 |     100 |     100 | 6                                                                            
  types.js         |     100 |      100 |     100 |     100 |                                                                              
  universal.js     |     100 |       50 |     100 |     100 | 6                                                                            
 src/util          |   98.59 |     87.8 |     100 |   98.51 |                                                                              
  ensureObject.js  |     100 |       75 |     100 |     100 | 1                                                                            
  getProp.js       |     100 |       75 |     100 |     100 | 1                                                                            
  index.js         |     100 |      100 |     100 |     100 |                                                                              
  stripComments.js |     100 |      100 |     100 |     100 |                                                                              
  unesc.js         |    97.5 |    90.32 |     100 |   97.37 | 35                                                                           


Patch:
diff --git a/src/parser.js b/src/parser.js
index 26c773f..5b17e64 100644
--- a/src/parser.js
+++ b/src/parser.js
@@ -301,7 +301,7 @@ export default class Parser {
                         node.raws.attribute += content;
                     }
                     lastAdded = 'attribute';
-                } else if ((!node.value && node.value !== "") || (lastAdded === "value" && !spaceAfterMeaningfulToken)) {
+                } else if ((!node.value && node.value !== "") || (lastAdded === "value" && !(spaceAfterMeaningfulToken || node.quoteMark))) {
                     let unescaped = unesc(content);
                     let oldRawValue = getProp(node, 'raws', 'value') || '';
                     let oldValue = node.value || '';
@@ -464,8 +464,8 @@ export default class Parser {
     }
 
     /**
-     * 
-     * @param {*} nodes 
+     *
+     * @param {*} nodes
      */
     convertWhitespaceNodesToSpace (nodes, requiredSpace = false) {
         let space = "";
diff --git a/src/selectors/attribute.js b/src/selectors/attribute.js
index 723863a..6114755 100644
--- a/src/selectors/attribute.js
+++ b/src/selectors/attribute.js
@@ -238,6 +238,31 @@ export default class Attribute extends Namespace {
         return this._value;
     }
 
+    get insensitive () {
+        return this._insensitive;
+    }
+
+    /**
+     * Set the case insensitive flag.
+     * If the case insensitive flag changes, the raw (escaped) value at `attr.raws.insensitiveFlag`
+     * of the attribute is updated accordingly.
+     *
+     * @param {true | false} insensitive true if the attribute should match case-insensitively.
+     */
+    set insensitive (insensitive) {
+        if (!insensitive) {
+            this._insensitive = false;
+
+            // "i" and "I" can be used in "this.raws.insensitiveFlag" to store the original notation.
+            // When setting `attr.insensitive = false` both should be erased to ensure correct serialization.
+            if (this.raws && (this.raws.insensitiveFlag === 'I' || this.raws.insensitiveFlag === 'i')) {
+                this.raws.insensitiveFlag = undefined;
+            }
+        }
+
+        this._insensitive = insensitive;
+    }
+
     /**
      * Before 3.0, the value had to be set to an escaped value including any wrapped
      * quote marks. In 3.0, the semantics of `Attribute.value` changed so that the value


Test patch:
diff --git a/src/__tests__/attributes.js b/src/__tests__/attributes.js
index 9239b56..5d4869c 100644
--- a/src/__tests__/attributes.js
+++ b/src/__tests__/attributes.js
@@ -352,10 +352,38 @@ test('insensitive attribute selector 2', '[href=TEsT i  ]', (t, tree) => {
 test('insensitive attribute selector 3', '[href=test i]', (t, tree) => {
     t.deepEqual(tree.nodes[0].nodes[0].value, 'test');
     t.deepEqual(tree.nodes[0].nodes[0].insensitive, true);
+
+    tree.nodes[0].nodes[0].insensitive = false;
+
+    t.deepEqual(tree.toString(), '[href=test ]');
 });
 test('capitalized insensitive attribute selector 3', '[href=test I]', (t, tree) => {
     t.deepEqual(tree.nodes[0].nodes[0].value, 'test');
     t.deepEqual(tree.nodes[0].nodes[0].insensitive, true);
+
+    tree.nodes[0].nodes[0].insensitive = false;
+
+    t.deepEqual(tree.toString(), '[href=test ]');
+});
+
+test('insensitive attribute selector 4', '[href="test"i]', (t, tree) => {
+    t.deepEqual(tree.nodes[0].nodes[0].value, 'test');
+    t.deepEqual(tree.nodes[0].nodes[0].insensitive, true);
+});
+
+test('capitalized insensitive attribute selector 4', '[href="test"I]', (t, tree) => {
+    t.deepEqual(tree.nodes[0].nodes[0].value, 'test');
+    t.deepEqual(tree.nodes[0].nodes[0].insensitive, true);
+});
+
+test('insensitive attribute selector 5', '[href="test" i ]', (t, tree) => {
+    t.deepEqual(tree.nodes[0].nodes[0].value, 'test');
+    t.deepEqual(tree.nodes[0].nodes[0].insensitive, true);
+});
+
+test('insensitive attribute selector 6', '[href=test i ]', (t, tree) => {
+    t.deepEqual(tree.nodes[0].nodes[0].value, 'test');
+    t.deepEqual(tree.nodes[0].nodes[0].insensitive, true);
 });
 
 test('extraneous non-combinating whitespace', '  [href]   ,  [class]   ', (t, tree) => {