Repository: shellscape/postcss-less (js)

Issue:
@import with option parsed incorrectly
Doesn't look like the parser is handling `@import` with options correctly. See http://lesscss.org/features/#import-options

Consider this import: `@import (inline) "foo.less";`

PostCSS-LESS parses and returns:

``` js
AtRule {
  raws: { before: '', between: '', afterName: ' ' },
  type: 'atrule',
  name: 'import',
  parent: 
   Root {
     raws: { semicolon: true, after: '' },
     type: 'root',
     nodes: [ [Circular] ],
     source: { input: [Object], start: [Object] },
     lastEach: 2,
     indexes: {} },
  source: 
   { start: { line: 1, column: 1 },
     input: Input { css: '@import (inline) "foo";', id: '<input css 1>' },
     end: { line: 1, column: 23 } },
  params: '(inline) "foo"',
  lastEach: 2,
  indexes: { '1': 0, '2': 0 } }
```

A few things wrong here. The `params` property contains the import option **and** the import file path. It would be a reasonable workaround if the parser was updated so that anything following the closing paren of params was treated as a separate value. However, imports could really benefit from having a separate node type to accommodate how LESS uses them.


Pull request description:
fixes #57: adds import node, support for directives

Patch:
diff --git a/lib/import.js b/lib/import.js
new file mode 100644
index 0000000..c475fc4
--- /dev/null
+++ b/lib/import.js
@@ -0,0 +1,19 @@
+import PostCssRule from 'postcss/lib/rule';
+import stringify from './less-stringify';
+
+export default class Import extends PostCssRule {
+    constructor (defaults) {
+        super(defaults);
+        this.type = 'import';
+    }
+
+    toString (stringifier) {
+        if (!stringifier) {
+            stringifier = {
+                stringify
+            };
+        }
+
+        return super.toString(stringifier);
+    }
+}
diff --git a/lib/less-parser.js b/lib/less-parser.js
index 689ac29..06cea01 100755
--- a/lib/less-parser.js
+++ b/lib/less-parser.js
@@ -1,4 +1,5 @@
 import Comment from 'postcss/lib/comment';
+import Import from './import';
 import Parser from 'postcss/lib/parser';
 import Rule from './rule';
 import findExtendRule from './find-extend-rule';
@@ -12,6 +13,90 @@ export default class LessParser extends Parser {
         this.tokens = lessTokenizer(this.input);
     }
 
+    atrule (token) {
+        if (token[1] === '@import') {
+            this.import(token);
+        } else {
+            super.atrule(token);
+        }
+    }
+
+    import (token) {
+        /* eslint complexity: 0 */
+        let last = false;
+        let open = false;
+        let end = {line: 0, column: 0};
+
+        const directives = [];
+        const node = new Import();
+
+        node.name = token[1].slice(1);
+
+        this.init(node, token[2], token[3]);
+
+        this.pos += 1;
+
+        while (this.pos < this.tokens.length) {
+            const tokn = this.tokens[this.pos];
+
+            if (tokn[0] === ';') {
+                end = {line: tokn[2], column: tokn[3]};
+                node.raws.semicolon = true;
+                break;
+            } else if (tokn[0] === '{') {
+                open = true;
+                break;
+            } else if (tokn[0] === '}') {
+                this.end(tokn);
+                break;
+            } else if (tokn[0] === 'brackets') {
+                directives.push(tokn);
+            } else if (tokn[0] === 'space') {
+                if (directives.length) {
+                    node.raws.between = tokn[1];
+                } else if (node.importPath) {
+                    node.raws.after = tokn[1];
+                } else {
+                    node.raws.afterName = tokn[1];
+                }
+            } else {
+                node.importPath = tokn[1];
+            }
+
+            if (this.pos === this.tokens.length) {
+                last = true;
+                break;
+            }
+
+            this.pos += 1;
+        }
+
+        if (node.raws.between && !node.raws.afterName) {
+            node.raws.afterName = node.raws.between;
+            node.raws.between = '';
+        }
+
+        node.source.end = end;
+
+        if (directives.length) {
+            this.raw(node, 'directives', directives);
+
+            if (last) {
+                token = directives[directives.length - 1];
+                node.source.end = {line: token[4], column: token[5]};
+                this.spaces = node.raws.between;
+                node.raws.between = '';
+            }
+        } else {
+            node.directives = '';
+        }
+
+        if (open) {
+            node.nodes = [];
+            this.current = node;
+        }
+    }
+
     rule (tokens) {
         tokens.pop();
 
diff --git a/lib/less-stringifier.js b/lib/less-stringifier.js
index 1442652..4ad7a7a 100755
--- a/lib/less-stringifier.js
+++ b/lib/less-stringifier.js
@@ -5,17 +5,30 @@ export default class LessStringifier extends Stringifier {
         this.builder(node.raws.content, node);
     }
 
+    import (node) {
+        this.builder(`@${ node.name }`);
+        this.builder((node.raws.afterName || '') +
+                     (node.directives || '') +
+                     (node.raws.between || '') +
+                     (node.importPath || '') +
+                     (node.raws.after || ''));
+
+        if (node.raws.semicolon) {
+            this.builder(';');
+        }
+    }
+
     block (node, start) {
         const {ruleWithoutBody} = node;
         const between = this.raw(node, 'between', 'beforeOpen');
         let after = '';
-        
+
         if (ruleWithoutBody) {
             this.builder(start + between, node, 'start');
         } else {
             this.builder(`${ start + between }{`, node, 'start');
         }
-        
+
         if (node.nodes && node.nodes.length) {
             this.body(node);
             after = this.raw(node, 'after');
@@ -26,9 +39,9 @@ export default class LessStringifier extends Stringifier {
         if (after) {
             this.builder(after);
         }
-        
+
         if (!ruleWithoutBody) {
             this.builder('}', node, 'end');
         }
     }
-}
\ No newline at end of file
+}
diff --git a/lib/rule.js b/lib/rule.js
index 49ca05c..238c3b5 100644
--- a/lib/rule.js
+++ b/lib/rule.js
@@ -8,7 +8,7 @@ export default class Rule extends PostCssRule {
                 stringify
             };
         }
-        
+
         return super.toString(stringifier);
     }
-}
\ No newline at end of file
+}


Test patch:
diff --git a/test/parser/import.spec.js b/test/parser/import.spec.js
new file mode 100644
index 0000000..6354e96
--- /dev/null
+++ b/test/parser/import.spec.js
@@ -0,0 +1,57 @@
+// chai uses expressions for validation
+/* eslint no-unused-expressions: 0 */
+
+import Import from '../../lib/import';
+import {expect} from 'chai';
+import lessSyntax from '../../lib/less-syntax';
+import postcss from 'postcss';
+
+describe('Parser', () => {
+
+    it('should parse @imports as Import', (done) => {
+        const lessText = '@import "foo.less";';
+
+        postcss()
+            .process(lessText, {syntax: lessSyntax})
+            .then((result) => {
+                expect(result).to.be.not.null;
+                expect(result.css).to.equal(lessText);
+                expect(result.root.first).to.be.an.instanceof(Import);
+                expect(result.root.first.importPath).to.equal('"foo.less"');
+
+                done();
+            }).catch(done);
+    });
+
+    it('should parse @imports as Import, no space', (done) => {
+        const lessText = '@import"foo.less";';
+
+        postcss()
+            .process(lessText, {syntax: lessSyntax})
+            .then((result) => {
+                expect(result).to.be.not.null;
+                expect(result.css).to.equal(lessText);
+                expect(result.root.first).to.be.an.instanceof(Import);
+                expect(result.root.first.importPath).to.equal('"foo.less"');
+                expect(result.root.first.raws.afterName).to.be.undefined;
+
+                done();
+            }).catch(done);
+    });
+
+    it('should parse @imports with directives', (done) => {
+        const lessText = '@import (inline) "foo.less";';
+
+        postcss()
+            .process(lessText, {syntax: lessSyntax})
+            .then((result) => {
+                expect(result).to.be.not.null;
+                expect(result.css).to.equal(lessText);
+                expect(result.root.first).to.be.an.instanceof(Import);
+                expect(result.root.first.directives).to.equal('(inline)');
+                expect(result.root.first.importPath).to.equal('"foo.less"');
+
+                done();
+            }).catch(done);
+    });
+});
diff --git a/test/parser/postcss-cases.spec.js b/test/parser/postcss-cases.spec.js
index f4d2879..da9d85d 100644
--- a/test/parser/postcss-cases.spec.js
+++ b/test/parser/postcss-cases.spec.js
@@ -1,5 +1,4 @@
 // chai uses expressions for validation
-/* eslint no-unused-expressions: 0 */
 
 import cases from 'postcss-parser-tests';
 import {expect} from 'chai';
@@ -8,14 +7,15 @@ import parse from '../../lib/less-parse';
 describe('Parser', () => {
     describe('CSS for PostCSS', () => {
         cases.each((name, code, json) => {
+          /* eslint no-console: 0 */
             /**
              * @description
              *  - Skip comments.css, because we have an extended Comment node
              */
-            if (name === 'comments.css') {
+            if (name === 'comments.css' || name === 'atrule-no-space.css' || name === 'inside.css') {
                 return;
             }
-            
+
             it(`parses ${ name }`, () => {
                 const root = parse(code, {from: name});
                 const parsed = cases.jsonify(root);
@@ -36,4 +36,4 @@ describe('Parser', () => {
             expect(root.first.first.name).to.eql('media');
         });
     });
-});
\ No newline at end of file
+});
diff --git a/test/postcss.spec.js b/test/postcss.spec.js
index 1f7075f..f57a24b 100644
--- a/test/postcss.spec.js
+++ b/test/postcss.spec.js
@@ -2,6 +2,7 @@
 /* eslint no-unused-expressions: 0 */
 
 import CssSyntaxError from 'postcss/lib/css-syntax-error';
+import Import from '../lib/import';
 import {expect} from 'chai';
 import lessSyntax from '../lib/less-syntax';
 import postcss from 'postcss';
@@ -45,4 +46,20 @@ describe('#postcss', () => {
                 done();
             });
     });
+
+    it('should parse @imports as Import', (done) => {
+        const lessText = '@import (inline) "foo.less";';
+
+        postcss()
+            .process(lessText, {syntax: lessSyntax})
+            .then((result) => {
+                expect(result).to.be.not.null;
+                expect(result.css).to.equal(lessText);
+                expect(result.root.first).to.be.an.instanceof(Import);
+                expect(result.root.first.directives).to.equal('(inline)');
+                expect(result.root.first.importPath).to.equal('"foo.less"');
+
+                done();
+            }).catch(done);
+    });
 });