Repository: postcss/postcss-selector-parser (js)

Issue:
Can't create selector with spaces
```js
const result = parser(selectors => {
	const newSelector = parser.selector({ spaces: { before: ' ', after: ' ' } });
	newSelector.append(parser.tag({ value: 'span' }));
	selectors.append(newSelector);
}).process('div').result;

console.log('|' + result + '|');
```
Console output:
>|div,span|

---
postcss-selector-parser: 2.1.1

Pull request description:
Remove hardcoded initialization spaces in node constructor.
Fixed #83

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..3881a63 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",
@@ -19,7 +19,7 @@
     "eslint-plugin-import": "^1.10.2",
     "glob": "^7.0.3",
     "minimist": "^1.2.0",
-    "nyc": "^7.0.0"
+    "nyc": "^8.0.0"
   },
   "main": "dist/index.js",
   "files": [
@@ -54,6 +54,7 @@
   },
   "nyc": {
     "exclude": [
+      "node_modules",
       "**/__tests__"
     ]
   },
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);
diff --git a/src/selectors/node.js b/src/selectors/node.js
index b8cf4ee..8d23600 100644
--- a/src/selectors/node.js
+++ b/src/selectors/node.js
@@ -31,7 +31,8 @@ export default class {
         for (let key in opts) {
             this[key] = opts[key];
         }
-        this.spaces = {before: '', after: ''};
+        let {spaces: {before = '', after = ''} = {}} = opts;
+        this.spaces = {before, after};
     }
 
     remove () {


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)');
diff --git a/src/__tests__/parser.js b/src/__tests__/parser.js
index 3f69287..077168d 100644
--- a/src/__tests__/parser.js
+++ b/src/__tests__/parser.js
@@ -5,7 +5,7 @@ import parser from '../index';
 const nodeTypes = [
     ['attribute',  '[href]', {attribute: 'href'}],
     ['className',  '.classy', {value: 'classy'}],
-    ['combinator', '>>', {value: '>>'}],
+    ['combinator', ' >> ', {value: '>>', spaces: {before: ' ', after: ' '}}],
     ['comment',    '/* comment */', {value: '/* comment */'}],
     ['id',         '#test', {value: 'test'}],
     ['nesting',    '&'],