Repository: shellscape/postcss-less (js)

Issue:
Spaces between variable name and `:` are not handled
<!--
  Please note: This template is not optional. Please fill in all fields and
  questions otherwise the issue may be closed. Please provide actual technical
  information about errors, if an error has occurred.
-->

* Node Version: 6.11.3
* NPM Version: 3.10.10
* postcss-less Version: 1.1.1

I pin pointed tokenize-at-rule.js#L36.
When declaration contains a space between variable name and :,
It makes state.cssPart one character to shorter to enter word definition bloc.
Test named `parses variables with whitespaces between name and ":"`, merged in #93, reproduces bug.

### LESS

```less
.navbar {
  @borderTopPx : 4px;
  .media {
    i.user {
      margin-top: @borderTopPx + 10px;
    }
  }
}
```

### JavaScript

Prettier/prettier#2925

### Expected Behavior

`@borderTopPx `to be handled like a _variable_.

### Actual Behavior

`@borderTopPx ` not handled like a _variable_.

### How can we reproduce the behavior?

I noticed this bug using [prettier](https://github.com/prettier/prettier) on Less pasted above.
It is described at Prettier/prettier#2925, where @lydell can provide more accurate details about parsing expectations.

Test named `parses variables with whitespaces between name and ":"` reproduces issue.

Thank for the good work

Pull request description:
fix: handle whitespace between variable and colon
the variable declaration in less is somehow similar to at rule in css, makes its detection more complicated.

fix #92 , similar PR as #100. But this might be more generic.

<!-- PRs must be accompanied by related tests -->

Please check one:
- [ ] New tests created for this change
- [x] Tests updated for this change

This PR:
- [ ] Adds new API
- [ ] Extends existing API, backwards-compatible
- [ ] Introduces a breaking change
- [x] Fixes a bug

---

<!-- add additional comments here -->


Patch:
diff --git a/lib/tokenizer/globals.js b/lib/tokenizer/globals.js
index 6e0421a..c41c863 100644
--- a/lib/tokenizer/globals.js
+++ b/lib/tokenizer/globals.js
@@ -28,5 +28,7 @@ export const atEndPattern = /[ \n\t\r\f\{\(\)'"\\;/\[\]#]/g;
 export const wordEndPattern = /[ \n\t\r\f\(\)\{\}:,;@!'"\\\]\[#]|\/(?=\*)/g;
 export const badBracketPattern = /.[\\\/\("'\n]/;
 
+export const pageSelectorPattern = /^@page[^\w-]+/;
+export const variableSpaceColonPattern = /^\s*:/;
 export const variablePattern = /^@[^:\(\{]+:/;
 export const hashColorPattern = /^#[0-9a-fA-F]{6}$|^#[0-9a-fA-F]{3}$/;
diff --git a/lib/tokenizer/tokenize-at-rule.js b/lib/tokenizer/tokenize-at-rule.js
index c9af5a2..fa352bc 100644
--- a/lib/tokenizer/tokenize-at-rule.js
+++ b/lib/tokenizer/tokenize-at-rule.js
@@ -1,4 +1,4 @@
-import { atEndPattern, openedCurlyBracket, variablePattern, wordEndPattern } from './globals';
+import { atEndPattern, openedCurlyBracket, pageSelectorPattern, variableSpaceColonPattern, variablePattern, wordEndPattern } from './globals';
 import unclosed from './unclosed';
 
 export default function tokenizeAtRule (state) {
@@ -40,7 +40,19 @@ export default function tokenizeAtRule (state) {
       state.nextPos = state.css.length - 1;
     }
     else {
-      state.nextPos = atEndPattern.lastIndex - 2;
+      // the first condition below is special for variable in less
+      // some one may write code like `@testVar   :  #fff`
+      // we should detect this kind of existence.
+      const rest = state.css.slice(atEndPattern.lastIndex);
+      const potentialPageRule = state.css.slice(state.pos, atEndPattern.lastIndex + 1);
+
+      // we have to handle special selector like `@page :left`
+      if (variableSpaceColonPattern.test(rest) && !pageSelectorPattern.test(potentialPageRule)) {
+        state.nextPos = atEndPattern.lastIndex + rest.search(':');
+      }
+      else {
+        state.nextPos = atEndPattern.lastIndex - 2;
+      }
     }
 
     state.cssPart = state.css.slice(state.pos, state.nextPos + 1);


Test patch:
diff --git a/test/parser/variables.spec.js b/test/parser/variables.spec.js
index c06feec..23f73db 100644
--- a/test/parser/variables.spec.js
+++ b/test/parser/variables.spec.js
@@ -24,39 +24,69 @@ describe('Parser', () => {
     });
 
     // #98 was merged to resolve this but broke other scenarios
-    it('parses variables with whitespaces between name and ":"'); //, () => {
-    //   let root = parse('@onespace : 42;');
-    //
-    //   expect(root.first.prop).to.eql('@onespace');
-    //   expect(root.first.value).to.eql('42');
-    // });
+    it('parses variables with whitespaces between name and ":"', () => {
+      let root = parse('@onespace : 42;');
+
+      expect(root.first.prop).to.eql('@onespace');
+      expect(root.first.value).to.eql('42');
+    });
 
     // #98 was merged to resolve this but broke other scenarios
     // these tests are commented out until that is resolved
-    it('parses variables with no whitespace between ":" and value'); //, () => {
-    //   const root = parse('@var :42;');
-    //
-    //   expect(root.first.prop).to.eql('@var');
-    //   expect(root.first.value).to.eql('42');
-    // });
-    //
-    it('parses mutliple variables with whitespaces between name and ":"'); //, () => {
-    //   const root = parse('@foo  : 42; @bar : 35;');
-    //
-    //   expect(root.first.prop).to.eql('@foo');
-    //   expect(root.first.value).to.eql('42');
-    //   expect(root.nodes[1].prop).to.eql('@bar');
-    //   expect(root.nodes[1].value).to.eql('35');
-    // });
-    //
-    it('parses multiple variables with no whitespace between ":" and value'); //, () => {
-    //   const root = parse('@foo  :42; @bar :35');
-    //
-    //   expect(root.first.prop).to.eql('@foo');
-    //   expect(root.first.value).to.eql('42');
-    //   expect(root.nodes[1].prop).to.eql('@bar');
-    //   expect(root.nodes[1].value).to.eql('35');
-    // });
+    it('parses variables with no whitespace between ":" and value', () => {
+      const root = parse('@var :42;');
+
+      expect(root.first.prop).to.eql('@var');
+      expect(root.first.value).to.eql('42');
+    });
+
+    it('parses mutliple variables with whitespaces between name and ":"', () => {
+      const root = parse('@foo  : 42; @bar : 35;');
+
+      expect(root.first.prop).to.eql('@foo');
+      expect(root.first.value).to.eql('42');
+      expect(root.nodes[1].prop).to.eql('@bar');
+      expect(root.nodes[1].value).to.eql('35');
+    });
+
+    it('parses multiple variables with no whitespace between ":" and value', () => {
+      const root = parse('@foo  :42; @bar :35');
+
+      expect(root.first.prop).to.eql('@foo');
+      expect(root.first.value).to.eql('42');
+      expect(root.nodes[1].prop).to.eql('@bar');
+      expect(root.nodes[1].value).to.eql('35');
+    });
+
+    it('parses @pagexxx like variable but not @page selector', () => {
+      const root = parse('@pageWidth: "test";');
+
+      expect(root.first.prop).to.eql('@pageWidth');
+      expect(root.first.value).to.eql('"test"');
+
+      const root2 = parse('@page-width: "test";');
+
+      expect(root2.first.prop).to.eql('@page-width');
+      expect(root2.first.value).to.eql('"test"');
+    });
+
+    it('parses @pagexxx like variable with whitespaces between name and ":"', () => {
+      const root = parse('@pageWidth :"test";');
+
+      expect(root.first.prop).to.eql('@pageWidth');
+      expect(root.first.value).to.eql('"test"');
+
+      const root2 = parse('@page-width :"test";');
+
+      expect(root2.first.prop).to.eql('@page-width');
+      expect(root2.first.value).to.eql('"test"');
+
+      const root3 = parse('@page-width : "test";');
+
+      expect(root3.first.prop).to.eql('@page-width');
+      expect(root3.first.value).to.eql('"test"');
+    });
+
 
     it('parses string variables', () => {
       const root = parse('@var: "test";');