Repository: postcss/postcss-selector-parser
Pull request #85 (2016-08-06 15:15:58)

Update ava to version 0.16.0 🚀
Hello lovely humans,

[ava](https://www.npmjs.com/package/ava) just published its new version 0.16.0.

<table>
  <tr>
    <th align=left>
      State
    </th>
    <td>
      Update :rocket:
    </td>
  </tr>
  <tr>
    <th align=left>
      Dependency
    </td>
    <td>
      ava
    </td>
  </tr>
  <tr>
    <th align=left>
      New version
    </td>
    <td>
      0.16.0
    </td>
  </tr>
  <tr>
    <th align=left>
      Type
    </td>
    <td>
      devDependency
    </td>
  </tr>
</table>

This version is **not covered** by your **current version range**.

Without accepting this pull request your project will work just like it did before. There might be a bunch of new features, fixes and perf improvements that the maintainers worked on for you though.

I recommend you look into these changes and try to get onto the latest version of ava.
Given that you have a decent test suite, a passing build is a strong indicator that you can take advantage of these changes by merging the proposed change into your project. Otherwise this branch is a great starting point for you to work on the update.


Do you have any ideas how I could improve these pull requests? Did I report anything you think isn’t right?
Are you unsure about how things are supposed to work?

There is a collection of [frequently asked questions](https://greenkeeper.io/faq.html) and while I’m just a bot, there is a group of people who are happy to teach me new things. [Let them know](https://github.com/greenkeeperio/greenkeeper/issues/new).

Good luck with your project :sparkles:

You rock!

:palm_tree:

---
[GitHub Release](https://github.com/avajs/ava/releases/tag/v0.16.0)

<p>Hope you all are having a great summer! <g-emoji alias="sunglasses" fallback-src="https://assets-cdn.github.com/images/icons/emoji/unicode/1f60e.png">😎</g-emoji></p>

<h2>Highlights</h2>

<ul>
<li>Add <a href="https://urls.greenkeeper.io/avajs/ava#notregexcontents-regex-message"><code>t.notRegex()</code></a>. <a href="https://urls.greenkeeper.io/avajs/ava/commit/c7402023d6a3a6b7a03abf2d52d948d0964683c8" class="commit-link"><tt>c740202</tt></a>
</li>
<li>Provide clear error message when users attempt nested or async calls to <code>test()</code>. <a href="https://urls.greenkeeper.io/avajs/ava/commit/8edd9c2ed169909bf19a3f9229143d8fc87d5284" class="commit-link"><tt>8edd9c2</tt></a>
</li>
<li>TypeScript definition improvements. <a href="https://urls.greenkeeper.io/avajs/ava/commit/1dd6d5b9127a6850d9a03c242e677ea359d67053" class="commit-link"><tt>1dd6d5b</tt></a> <a href="https://urls.greenkeeper.io/avajs/ava/commit/2e984380c4df2bfe5a0af2ec37a970f4001c9f53" class="commit-link"><tt>2e98438</tt></a>
</li>
<li>Document <a href="https://urls.greenkeeper.io/avajs/ava/blob/master/docs/common-pitfalls.md">common pitfalls</a> when using AVA. <a href="https://urls.greenkeeper.io/avajs/ava/commit/187b8b49b0f0cedbeb2ba3a833b110a72e658fa8" class="commit-link"><tt>187b8b4</tt></a>
</li>
<li>New <a href="https://urls.greenkeeper.io/avajs/ava/blob/master/docs/recipes/jspm-systemjs.md">JSPM and SystemJS recipe</a>. <a href="https://urls.greenkeeper.io/avajs/ava/commit/c3539c1701aba896bfb088ec838e60b34913168d" class="commit-link"><tt>c3539c1</tt></a>
</li>
</ul>

<h2>All Changes</h2>

<p><a href="https://urls.greenkeeper.io/avajs/ava/compare/v0.15.2...v0.16.0" class="commit-link"><tt>v0.15.2...v0.16.0</tt></a></p>

---
The new version differs by 47 commits .

- [`01a110e`](https://github.com/avajs/ava/commit/01a110e15fecd84c44f464c09799d60416ee93fc) <code>0.16.0</code>
- [`1bff531`](https://github.com/avajs/ava/commit/1bff53163943b2e3a8d6a4ed7708a407b7709fa5) <code>bump dependencies and remove unused ones</code>
- [`f9fe8f2`](https://github.com/avajs/ava/commit/f9fe8f242dd58d02ab1110f677e82172652d9d3d) <code>Add type definitions to notRegex (#979)</code>
- [`1c6abe0`](https://github.com/avajs/ava/commit/1c6abe02e9ade13a1bf891c0d4b9ff5da8eef357) <code>Update Juan Soto link</code>
- [`8edd9c2`](https://github.com/avajs/ava/commit/8edd9c2ed169909bf19a3f9229143d8fc87d5284) <code>provide clear error message when users attempt nested/async calls to `test` (#961)</code>
- [`c82f980`](https://github.com/avajs/ava/commit/c82f98015af9a0e887a39fb0dc2fadbb8648a567) <code>minor code style tweaks</code>
- [`1dd6d5b`](https://github.com/avajs/ava/commit/1dd6d5b9127a6850d9a03c242e677ea359d67053) <code>Auto generate TypeScript definition to allow chaining (#884)</code>
- [`05f1890`](https://github.com/avajs/ava/commit/05f1890eb7b60eb54cbc12d24d78b353bbd844f6) <code>add Dependency CI badge</code>
- [`7440ac9`](https://github.com/avajs/ava/commit/7440ac9d03713dcd35dc1de1786228a7270542c3) <code>minor readme tweaks</code>
- [`b54eafa`](https://github.com/avajs/ava/commit/b54eafa3dd01c6492bb01a3a0f3924e8de09bff5) <code>add new reporters section in the readme (#955)</code>
- [`a539c7e`](https://github.com/avajs/ava/commit/a539c7e1b409778da138754825275be7778b5f7a) <code>Update JSPM+SystemJS recipe per @jamestalmage.</code>
- [`7827fa1`](https://github.com/avajs/ava/commit/7827fa17e0fdbbda60d4a0216aa2312fca5a2822) <code>Translate React recipe to Spanish (es_ES) (#940)</code>
- [`6635af0`](https://github.com/avajs/ava/commit/6635af0e407b7fe4116751e673b43fa06356cb40) <code>fix lint issues</code>
- [`13cbdc2`](https://github.com/avajs/ava/commit/13cbdc228714438f669109b6fef9028a48969047) <code>Docs: Add link to french translation of JSPM recipe (#957)</code>
- [`5cfe0d1`](https://github.com/avajs/ava/commit/5cfe0d1054e50c816643697d30dc3e09a73f157f) <code>fix import-order linting errors</code>


There are 47 commits in total. See the [full diff](https://github.com/avajs/ava/compare/c9c00d626e4c7f1fe7ff31eea763ff9b6983032c...01a110e15fecd84c44f464c09799d60416ee93fc).

---
This pull request was created by [greenkeeper.io](https://greenkeeper.io/).

<sub>Tired of seeing this sponsor message? :zap: `greenkeeper upgrade`</sub>

coveralls: 
[![Coverage Status](https://coveralls.io/builds/7335529/badge)](https://coveralls.io/builds/7335529)

Coverage increased (+0.01%) to 96.572% when pulling **4be4b0043a82ed280407500e462b55d1c6ab5921 on greenkeeper-ava-0.16.0** into **400007a784848930789419e9e62f97decf8ceea6 on master**.


Patch:
diff --git a/API.md b/API.md
index 553450d..3cbcc8f 100644
--- a/API.md
+++ b/API.md
@@ -11,19 +11,29 @@ This is the module's main entry point.
 var parser = require('postcss-selector-parser');
 ```
 
-### `parser(transform)`
+### `parser([transform])`
+
+Creates a new `processor` instance
 
 ```js
+var processor = parser();
+
+// or, with optional transform function
 var transform = function (selectors) {
     selectors.eachUniversal(function (selector) {
         selector.remove();
     });
 };
 
-var result = parser(transform).process('*.class').result;
+var processor = parser(transform)
+
+// Example
+var result = processor.process('*.class').result;
 // => .class
 ```
 
+[See processor documentation](#processor)
+
 Arguments:
 
 * `transform (function)`: Provide a function to work with the parsed AST.
@@ -586,3 +596,29 @@ around it.
 [id=Bar i ] /* " i " */
 [id=Bar   i  ] /* "   i  " */
 ```
+
+## `processor`
+
+### `process(cssText, [options])`
+
+Processes the `cssText`, returning the parsed output
+
+```js
+var processor = parser();
+
+var result = processor.process(' .class').result;
+// =>  .class
+
+// To have the parser normalize whitespace values, utilize the options
+var result = processor.process('  .class  ', {lossless: false}).result;
+// => .class
+```
+
+Arguments:
+
+* `cssText (string)`: The css to be parsed.
+* `[options] (object)`: Process options
+
+Options:
+
+* `loseless (boolean)`: false to normalize the selector whitespace, defaults to true
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0708eaf..751a3b8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,8 @@
+# 2.2.0
+
+* Added a new option to normalize whitespace when parsing the selector string
+  (thanks to @adam-26).
+
 # 2.1.1
 
 * Better unquoted value handling within attribute selectors
diff --git a/README.md b/README.md
index 4362dea..aa52db3 100644
--- a/README.md
+++ b/README.md
@@ -24,6 +24,14 @@ var transform = function (selectors) {
 var transformed = parser(transform).process('h1, h2, h3').result;
 ```
 
+To normalize selector whitespace:
+
+```js
+var parser = require('postcss-selector-parser');
+var normalized = parser().process('h1, h2, h3', {lossless:false}).result;
+// -> h1,h2,h3
+```
+
 ## API
 
 Please see [API.md](API.md).
diff --git a/package.json b/package.json
index f28d462..dce7225 100644
--- a/package.json
+++ b/package.json
@@ -1,8 +1,8 @@
 {
   "name": "postcss-selector-parser",
-  "version": "2.1.1",
+  "version": "2.2.0",
   "devDependencies": {
-    "ava": "^0.15.0",
+    "ava": "^0.16.0",
     "babel-cli": "^6.4.0",
     "babel-core": "^6.4.0",
     "babel-plugin-add-module-exports": "^0.2.0",
diff --git a/src/parser.js b/src/parser.js
index c14ddb8..d6bda91 100644
--- a/src/parser.js
+++ b/src/parser.js
@@ -23,6 +23,7 @@ import * as types from './selectors/types';
 export default class Parser {
     constructor (input) {
         this.input = input;
+        this.lossy = input.options.lossless === false;
         this.position = 0;
         this.root = new Root();
 
@@ -30,7 +31,11 @@ export default class Parser {
         this.root.append(selectors);
 
         this.current = selectors;
-        this.tokens = tokenize(input);
+        if (this.lossy) {
+            this.tokens = tokenize({safe: input.safe, css: input.css.trim()});
+        } else {
+            this.tokens = tokenize(input);
+        }
 
         return this.loop();
     }
@@ -68,21 +73,23 @@ export default class Parser {
             if (namespace[0] === '') {
                 namespace[0] = true;
             }
-            attributeProps.attribute = namespace[2];
-            attributeProps.namespace = namespace[0];
+            attributeProps.attribute = this.parseValue(namespace[2]);
+            attributeProps.namespace = this.parseNamespace(namespace[0]);
         } else {
-            attributeProps.attribute = parts[0];
+            attributeProps.attribute = this.parseValue(parts[0]);
         }
         attr = new Attribute(attributeProps);
 
         if (parts[2]) {
             let insensitive = parts[2].split(/(\s+i\s*?)$/);
-            attr.value = insensitive[0];
+            let trimmedValue = insensitive[0].trim();
+            attr.value = this.lossy ? trimmedValue : insensitive[0];
             if (insensitive[1]) {
                 attr.insensitive = true;
-                attr.raws.insensitive = insensitive[1];
+                if (!this.lossy) {
+                    attr.raws.insensitive = insensitive[1];
+                }
             }
-            let trimmedValue = attr.value.trim();
             attr.quoted = trimmedValue[0] === '\'' || trimmedValue[0] === '"';
             attr.raws.unquoted = (attr.quoted) ? trimmedValue.slice(1, -1) : trimmedValue;
         }
@@ -112,16 +119,18 @@ export default class Parser {
                 (this.currToken[0] === 'space' ||
                 this.currToken[0] === 'combinator')) {
             if (this.nextToken && this.nextToken[0] === 'combinator') {
-                node.spaces.before = this.currToken[1];
+                node.spaces.before = this.parseSpace(this.currToken[1]);
                 node.source.start.line = this.nextToken[2];
                 node.source.start.column = this.nextToken[3];
                 node.source.end.column = this.nextToken[3];
                 node.source.end.line = this.nextToken[2];
                 node.sourceIndex = this.nextToken[4];
             } else if (this.prevToken && this.prevToken[0] === 'combinator') {
-                node.spaces.after = this.currToken[1];
-            } else if (this.currToken[0] === 'space' || this.currToken[0] === 'combinator') {
+                node.spaces.after = this.parseSpace(this.currToken[1]);
+            } else if (this.currToken[0] === 'combinator') {
                 node.value = this.currToken[1];
+            } else if (this.currToken[0] === 'space' && !(this.lossy && this.prevToken[0] === '&')) {
+                node.value = this.parseSpace(this.currToken[1], ' ');
             }
             this.position ++;
         }
@@ -239,7 +248,7 @@ export default class Parser {
                 if (this.currToken[0] === ')') {
                     balanced--;
                 }
-                last.value += this.currToken[1];
+                last.value += this.parseParenthesisToken(this.currToken);
                 this.position++;
             }
             if (balanced) {
@@ -290,10 +299,10 @@ export default class Parser {
         let token = this.currToken;
         // Handle space before and after the selector
         if (this.position === 0 || this.prevToken[0] === ',' || this.prevToken[0] === '(') {
-            this.spaces = token[1];
+            this.spaces = this.parseSpace(token[1]);
             this.position ++;
         } else if (this.position === (this.tokens.length - 1) || this.nextToken[0] === ',' || this.nextToken[0] === ')') {
-            this.current.last.spaces.after = token[1];
+            this.current.last.spaces.after = this.parseSpace(token[1]);
             this.position ++;
         } else {
             this.combinator();
@@ -352,7 +361,7 @@ export default class Parser {
             if (current.lastIndexOf('\\') === current.length - 1) {
                 let next = this.nextToken;
                 if (next && next[0] === 'space') {
-                    word += next[1];
+                    word += this.parseSpace(next[1], ' ');
                     this.position ++;
                 }
             }
@@ -491,9 +500,42 @@ export default class Parser {
      * Helpers
      */
 
+    parseNamespace (namespace) {
+        if (this.lossy && typeof namespace === 'string') {
+            const trimmed = namespace.trim();
+            if (!trimmed.length) {
+                return true;
+            }
+
+            return trimmed;
+        }
+
+        return namespace;
+    }
+
+    parseSpace (space, replacement) {
+        return this.lossy ? (replacement || '') : space;
+    }
+
+    parseValue (value) {
+        return this.lossy && value && typeof value === 'string' ? value.trim() : value;
+    }
+
+    parseParenthesisToken (token) {
+        if (!this.lossy) {
+            return token[1];
+        }
+
+        if (token[0] === 'space') {
+            return this.parseSpace(token[1], ' ');
+        }
+
+        return this.parseValue(token[1]);
+    }
+
     newNode (node, namespace) {
         if (namespace) {
-            node.namespace = namespace;
+            node.namespace = this.parseNamespace(namespace);
         }
         if (this.spaces) {
             node.spaces.before = this.spaces;
diff --git a/src/processor.js b/src/processor.js
index a6969e9..fd24437 100644
--- a/src/processor.js
+++ b/src/processor.js
@@ -6,12 +6,13 @@ export default class Processor {
         return this;
     }
 
-    process (selectors) {
+    process (selectors, options = {}) {
         let input = new Parser({
             css: selectors,
             error: (e) => {
                 throw new Error(e);
             },
+            options: options,
         });
         this.res = input;
         this.func(input);


Test patch:
diff --git a/src/__tests__/lossy.js b/src/__tests__/lossy.js
new file mode 100644
index 0000000..aa9fdc4
--- /dev/null
+++ b/src/__tests__/lossy.js
@@ -0,0 +1,80 @@
+import ava from 'ava';
+import parser from '../index';
+
+export const parse = (input, options, transform) => {
+    return parser(transform).process(input, options).result;
+};
+
+export const testLossy = (t, input, expected) => {
+    let result = parse(input, {lossless:false});
+    t.deepEqual(result, expected);
+};
+
+ava('combinator, descendant - single', testLossy, '.one .two', '.one .two');
+ava('combinator, descendant - multiple', testLossy, '.one   .two', '.one .two');
+ava('combinator, child - space before', testLossy, '.one >.two', '.one>.two');
+ava('combinator, child - space after', testLossy, '.one> .two', '.one>.two');
+ava('combinator, sibling - space before', testLossy, '.one ~.two', '.one~.two');
+ava('combinator, sibling - space after', testLossy, '.one~ .two', '.one~.two');
+ava('combinator, adj sibling - space before', testLossy, '.one +.two', '.one+.two');
+ava('combinator, adj sibling - space after', testLossy, '.one+ .two', '.one+.two');
+
+ava('classes, extraneous spaces', testLossy, '  .h1   ,  .h2   ', '.h1,.h2');
+ava('ids, extraneous spaces', testLossy, '  #h1   ,  #h2   ', '#h1,#h2');
+
+ava('attribute, spaces in selector', testLossy, 'h1[  href  *=  "test"  ]', 'h1[href*="test"]');
+ava('attribute, insensitive flag 1', testLossy, '[href="test" i  ]', '[href="test" i]');
+ava('attribute, insensitive flag 2', testLossy, '[href=TEsT i  ]', '[href=TEsT i]');
+ava('attribute, insensitive flag 3', testLossy, '[href=test i  ]', '[href=test i]');
+ava('attribute, extreneous whitespace', testLossy, '  [href]   ,  [class]   ', '[href],[class]');
+
+ava('namespace, space before', testLossy, '   postcss|button', 'postcss|button');
+ava('namespace, space after', testLossy, 'postcss|button     ', 'postcss|button');
+ava('namespace - all elements, space before', testLossy, '   postcss|*', 'postcss|*');
+ava('namespace - all elements, space after', testLossy, 'postcss|*     ', 'postcss|*');
+ava('namespace - all namespaces, space before', testLossy, '   *|button', '*|button');
+ava('namespace - all namespaces, space after', testLossy, '*|button     ', '*|button');
+ava('namespace - all elements in all namespaces, space before', testLossy, '   *|*', '*|*');
+ava('namespace - all elements in all namespaces, space after', testLossy, '*|*     ', '*|*');
+ava('namespace - all elements without namespace, space before', testLossy, '   |*', '|*');
+ava('namespace - all elements without namespace, space after', testLossy, '|*     ', '|*');
+ava('namespace - tag with no namespace, space before', testLossy, '   |button', '|button');
+ava('namespace - tag with no namespace, space after', testLossy, '|button     ', '|button');
+ava('namespace - inside attribute, space before', testLossy, ' [  postcss|href=test]', '[postcss|href=test]');
+ava('namespace - inside attribute, space after', testLossy, '[postcss|href=  test  ] ', '[postcss|href=test]');
+ava('namespace - inside attribute (2), space before', testLossy, ' [  postcss|href]', '[postcss|href]');
+ava('namespace - inside attribute (2), space after', testLossy, '[postcss|href ] ', '[postcss|href]');
+ava('namespace - inside attribute (3), space before', testLossy, ' [  *|href=test]', '[*|href=test]');
+ava('namespace - inside attribute (3), space after', testLossy, '[*|href=  test  ] ', '[*|href=test]');
+ava('namespace - inside attribute (4), space before', testLossy, ' [  |href=test]', '[|href=test]');
+ava('namespace - inside attribute (4), space after', testLossy, '[|href=  test  ] ', '[|href=test]');
+
+ava('tag - extraneous whitespace', testLossy, '  h1   ,  h2   ', 'h1,h2');
+ava('tag - trailing comma', testLossy, 'h1, ', 'h1,');
+ava('tag - trailing comma (1)', testLossy, 'h1,', 'h1,');
+ava('tag - trailing comma (2)', testLossy, 'h1', 'h1');
+ava('tag - trailing slash (1)', testLossy, 'h1\\    ', 'h1\\');
+ava('tag - trailing slash (2)', testLossy, 'h1\\    h2\\', 'h1\\ h2\\');
+
+ava('universal - combinator', testLossy, ' * + * ', '*+*');
+ava('universal - extraneous whitespace', testLossy, '  *   ,  *   ', '*,*');
+ava('universal - qualified universal selector', testLossy, '*[href] *:not(*.green)', '*[href] *:not(*.green)');
+
+ava('nesting - spacing before', testLossy, '  &.class', '&.class');
+ava('nesting - spacing after', testLossy, '&.class  ', '&.class');
+ava('nesting - spacing between', testLossy, '&  .class  ', '&.class');
+
+ava('pseudo (single) - spacing before', testLossy, '  :after', ':after');
+ava('pseudo (single) - spacing after', testLossy, ':after  ', ':after');
+ava('pseudo (double) - spacing before', testLossy, '  ::after', '::after');
+ava('pseudo (double) - spacing after', testLossy, '::after  ', '::after');
+ava('pseudo - multiple', testLossy, ' *:target::before ,   a:after  ', '*:target::before,a:after');
+ava('pseudo - negated', testLossy, 'h1:not( .heading )', 'h1:not(.heading)');
+ava('pseudo - negated with combinators (1)', testLossy, 'h1:not(.heading > .title)   >  h1', 'h1:not(.heading>.title)>h1');
+ava('pseudo - negated with combinators (2)', testLossy, '.foo:nth-child(2n + 1)', '.foo:nth-child(2n+1)');
+ava('pseudo - extra whitespace', testLossy, 'a:not(   h2   )', 'a:not(h2)');
+
+ava('@words - space before', testLossy, '  @media', '@media');
+ava('@words - space after', testLossy, '@media  ', '@media');
+ava('@words - maintains space between', testLossy, '@media (min-width: 700px) and (orientation: landscape)', '@media (min-width: 700px) and (orientation: landscape)');
+ava('@words - extraneous space between', testLossy, '@media  (min-width:  700px)  and   (orientation:   landscape)', '@media (min-width: 700px) and (orientation: landscape)');