Repository: styled-components/stylelint-processor-styled-components (js)

Issue:
Breaks on complex interpolations
Currently, this breaks on a complex interpolation:

``` JS
const background = 'background: red;';
const Button = styled.div`
  color: red;
  ${background}
`;
```

We represent this for stylelint like so:

``` CSS
.selector {
  color: red;
  $background
}
```

The problem is that, while stylelint ignores Sass variables, those cannot represent full strings, which means stylelint throws a "CssSyntaxError" on the above code eventhough it's not a syntax error at all. 😕

Any ideas what we could do here @davidtheclark?

(also related to #4)


Pull request description:
Fixed complex interpolations
Merged tests from #8 and fixed #6. This change complicates `interleave` function and also introduces *vendor prefixed* property `-styled-mixin`.

```js
const Button = styled.button`
  color: blue;
  ${additionalButtonStyles}
`
```

Is now transformed to:
```css
.selector {
  color: blue;
  -styled-mixin: additionalButtonStyles;
}
```

Patch:
diff --git a/src/utils/general.js b/src/utils/general.js
index e86d0f4..0c5ba6d 100644
--- a/src/utils/general.js
+++ b/src/utils/general.js
@@ -29,9 +29,26 @@ const fixIndentation = (str) => {
   }
 }
 
+/**
+ * Checks if last line of text has whitespaces only
+ */
+const isLastLineWhitespaceOnly = (text) => {
+  for (let i = text.length - 1; i >= 0; i -= 1) {
+    const char = text.charAt(i)
+    if (char === '\n') {
+      return true
+    }
+    if (char !== '\t' && char !== ' ') {
+      return false
+    }
+  }
+  return true
+}
+
 const wrapSelector = (content) => `.selector {${content}}\n`
 const wrapKeyframes = (content) => `@keyframes {${content}}\n`
 
 exports.wrapKeyframes = wrapKeyframes
 exports.wrapSelector = wrapSelector
 exports.fixIndentation = fixIndentation
+exports.isLastLineWhitespaceOnly = isLastLineWhitespaceOnly
diff --git a/src/utils/tagged-template-literal.js b/src/utils/tagged-template-literal.js
index 7e076bf..671b444 100644
--- a/src/utils/tagged-template-literal.js
+++ b/src/utils/tagged-template-literal.js
@@ -1,3 +1,5 @@
+const isLastLineWhitespaceOnly = require('./general').isLastLineWhitespaceOnly
+
 /**
  * Check if a node is a tagged template literal
  */
@@ -11,11 +13,26 @@ const hasInterpolations = (node) => !node.quasi.quasis[0].tail
 /**
  * Merges the interpolations in a parsed tagged template literals with the strings
  */
-const interleave = (quasis, expressions) => (
-  expressions.reduce((prev, expression, index) => (
-    prev.concat(`$${expression.name}`, quasis[index + 1].value.raw)
-  ), [quasis[0].value.raw]).join('')
-)
+const interleave = (quasis, expressions) => {
+  let css = ''
+  for (let i = 0, l = expressions.length; i < l; i += 1) {
+    const prevText = quasis[i].value.raw
+    const nextText = quasis[i + 1].value.raw
+    const expression = expressions[i]
+
+    css += prevText
+    if (isLastLineWhitespaceOnly(prevText)) {
+      css += `-styled-mixin: ${expression.name}`
+      if (nextText.charAt(0) !== ';') {
+        css += ';'
+      }
+    } else {
+      css += `$${expression.name}`
+    }
+  }
+  css += quasis[quasis.length - 1].value.raw
+  return css
+}
 
 /**
  * Get the content of a tagged template literal


Test patch:
diff --git a/test/fixtures/interpolations/complex.js b/test/fixtures/interpolations/complex.js
new file mode 100644
index 0000000..3c1a01c
--- /dev/null
+++ b/test/fixtures/interpolations/complex.js
@@ -0,0 +1,17 @@
+import styled, { css } from 'styled-components';
+
+const interpolatedStyle = css`
+  background-color: gray;
+  color: gray;
+`;
+
+// Interpolation of chunk
+const Div = styled.div`
+  ${interpolatedStyle}
+`;
+
+// Conditional interpolation of chunk
+const Button = styled.button`
+  ${props => props.isHovering && interpolatedStyle}
+`;
+
diff --git a/test/interpolations.test.js b/test/interpolations.test.js
index a6ab55a..9e999bd 100644
--- a/test/interpolations.test.js
+++ b/test/interpolations.test.js
@@ -75,4 +75,33 @@ describe('interpolations', () => {
       expect(data.results[0].warnings[0].rule).toEqual('indentation')
     })
   })
+
+  describe('complex interpolations', () => {
+    beforeAll(() => {
+      fixture = path.join(__dirname, './fixtures/interpolations/complex.js')
+    })
+
+    it('should have one result', () => {
+      expect(data.results.length).toEqual(1)
+    })
+
+    it('should use the right file', () => {
+      expect(data.results[0].source).toEqual(fixture)
+    })
+
+    it('should not have errored', () => {
+      expect(data.errored).toEqual(false)
+    })
+
+    it('should not have any warnings', () => {
+      expect(data.results[0].warnings.length).toEqual(0)
+    })
+
+    it('should not result in a CssSyntaxError', () => {
+      const warning = data.results[0].warnings[0]
+        && data.results[0].warnings[0].rule
+
+      expect(warning).not.toEqual('CssSyntaxError')
+    })
+  })
 })
diff --git a/test/utils.test.js b/test/utils.test.js
index 133461c..7f08c78 100644
--- a/test/utils.test.js
+++ b/test/utils.test.js
@@ -1,4 +1,5 @@
 const interleave = require('../src/utils/tagged-template-literal').interleave
+const isLastLineWhitespaceOnly = require('../src/utils/general').isLastLineWhitespaceOnly
 
 describe('utils', () => {
   describe('interleave', () => {
@@ -28,4 +29,26 @@ describe('utils', () => {
       expect(interleave(quasis, expressions)).toEqual('\n  display: block;\n  color: $color;\n  background: blue;\n')
     })
   })
+
+  describe('isLastLineWhitespaceOnly', () => {
+    it('should return true for empty string', () => {
+      expect(isLastLineWhitespaceOnly('')).toEqual(true)
+    })
+
+    it('should return true for string of spaces', () => {
+      expect(isLastLineWhitespaceOnly('   ')).toEqual(true)
+    })
+
+    it('should return true for string of spaces and tabs', () => {
+      expect(isLastLineWhitespaceOnly(' \t  ')).toEqual(true)
+    })
+
+    it('should return false for string with something other than space and tab', () => {
+      expect(isLastLineWhitespaceOnly('not space')).toEqual(false)
+    })
+
+    it('should return true if last line has only space and tab', () => {
+      expect(isLastLineWhitespaceOnly('not space\n  ')).toEqual(true)
+    })
+  })
 })