├── .babelrc ├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── package.json ├── preset.js └── src ├── __tests__ ├── .babelrc ├── __snapshots__ │ ├── index.js.snap │ └── react.js.snap ├── index.js └── react.js └── index.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "env", 5 | { 6 | "modules": false, 7 | "loose": true 8 | } 9 | ], 10 | "stage-2", 11 | "react" 12 | ], 13 | "env": { 14 | "test": { 15 | "presets": [ 16 | [ 17 | "env", 18 | { 19 | "loose": true 20 | } 21 | ], 22 | "stage-2", 23 | "react" 24 | ] 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /coverage 2 | /demo/dist 3 | /dist 4 | /lib 5 | /es 6 | /node_modules 7 | /umd 8 | /es 9 | npm-debug.log 10 | .idea 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "7" 5 | 6 | cache: 7 | directories: 8 | - node_modules 9 | 10 | before_install: 11 | - npm install codecov 12 | 13 | after_success: 14 | - cat ./coverage/lcov.info | ./node_modules/codecov/bin/codecov 15 | 16 | cache: 17 | directories: 18 | - node_modules 19 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Prerequisites 2 | 3 | [Node.js](http://nodejs.org/) >= v7 must be installed. 4 | 5 | ## Installation 6 | 7 | - Running `npm install` in the module's root directory will install everything you need for development. 8 | 9 | ## Running Tests 10 | 11 | - `npm test` will run the tests once. 12 | 13 | - `npm run test:coverage` will run the tests and produce a coverage report in `coverage/`. 14 | 15 | - `npm run test:watch` will run the tests on every change. 16 | 17 | ## Building 18 | 19 | - `npm run build` will build the module for publishing to npm. 20 | 21 | - `npm run clean` will delete built resources. 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Kye Hohenberger 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # babel-plugin-styled-components-attr 2 | 3 | 4 | use the CSS attr function in your styled-components code. 5 | 6 | 7 | 8 | [![npm version](https://badge.fury.io/js/babel-plugin-styled-components-attr.svg)](https://badge.fury.io/js/babel-plugin-styled-components-attr) 9 | [![Build Status](https://travis-ci.org/tkh44/babel-plugin-styled-components-attr.svg?branch=master)](https://travis-ci.org/tkh44/babel-plugin-styled-components-attr) 10 | [![codecov](https://codecov.io/gh/tkh44/babel-plugin-styled-components-attr/branch/master/graph/badge.svg)](https://codecov.io/gh/tkh44/babel-plugin-styled-components-attr) 11 | 12 | 13 | 14 | ```jsx 15 | const Input = styled.input` 16 | color: attr(color); 17 | width: attr(width %); 18 | margin: attr(margin px, 16); 19 | ` 20 | ``` 21 | 22 | ## Install 23 | 24 | ```bash 25 | npm install -S babel-plugin-styled-components-attr 26 | ``` 27 | 28 | 29 | **.babelrc** 30 | ```json 31 | { 32 | "plugins": [ 33 | "styled-components-attr" 34 | ] 35 | } 36 | ``` 37 | 38 | #### attr 39 | 40 | The [attr](https://developer.mozilla.org/en-US/docs/Web/CSS/attr) CSS function is supported in 41 | a basic capacity. I will be adding more features, but PRs are welcome. 42 | 43 | ```css 44 | /* get value from `width` prop */ 45 | width: attr(width vw); 46 | 47 | /* specify type or unit to apply to value */ 48 | width: attr(width vw); 49 | 50 | /* fallback value if props.width is falsey */ 51 | width: attr(width vw, 50); 52 | ``` 53 | 54 | ```jsx 55 | const H1 = styled.h1` 56 | font-size: attr(fontSize px); 57 | margin: attr(margin rem, 4); 58 | font-family: sans-serif; 59 | color: ${colors.pink[5]}; 60 | @media (min-width: 680px) { 61 | color: attr(desktopColor); 62 | } 63 | ` 64 | 65 | const Title = ({ title, scale }) => { 66 | return ( 67 |

68 | {title} 69 |

70 | ) 71 | } 72 | ``` 73 | 74 | ##### Value types 75 | *checked is supported* 76 | 77 | - [x] em 78 | - [x] ex 79 | - [x] px 80 | - [x] rem 81 | - [x] vw 82 | - [x] vh 83 | - [x] vmin 84 | - [x] vmax 85 | - [x] mm 86 | - [x] cm 87 | - [x] in 88 | - [x] pt 89 | - [x] pc 90 | - [x] % 91 | - [ ] string 92 | - [ ] color 93 | - [ ] url 94 | - [ ] integer 95 | - [ ] number 96 | - [ ] length 97 | - [ ] deg 98 | - [ ] grad 99 | - [ ] rad 100 | - [ ] time 101 | - [ ] s 102 | - [ ] ms 103 | - [ ] frequency 104 | - [ ] Hz 105 | - [ ] kHz 106 | 107 | 108 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "babel-plugin-styled-components-attr", 3 | "version": "0.0.3", 4 | "description": "CSS attr function in your styled components", 5 | "main": "src/index.js", 6 | "files": [ 7 | "src" 8 | ], 9 | "scripts": { 10 | "test": "standard src test && jest --coverage --no-cache", 11 | "test:watch": "jest --watch", 12 | "release": "npm run test && npm version patch && npm publish && git push --tags" 13 | }, 14 | "dependencies": {}, 15 | "devDependencies": { 16 | "babel-cli": "^6.24.1", 17 | "babel-core": "^6.24.1", 18 | "babel-eslint": "^7.2.3", 19 | "babel-jest": "^20.0.3", 20 | "babel-loader": "^7.0.0", 21 | "babel-preset-env": "^1.5.1", 22 | "babel-preset-react": "^6.24.1", 23 | "babel-preset-stage-0": "^6.24.1", 24 | "jest": "^20.0.4", 25 | "jest-cli": "^20.0.4", 26 | "jest-styled-components": "^3.0.0", 27 | "npm-run-all": "^4.0.2", 28 | "react": "^15.5.4", 29 | "react-addons-test-utils": "^15.5.1", 30 | "react-dom": "^15.5.4", 31 | "react-test-renderer": "^15.5.4", 32 | "standard": "^10.0.2", 33 | "styled-components": "^2.0.1" 34 | }, 35 | "author": "Kye Hohenberger", 36 | "homepage": "https://github.com/tkh44/babel-plugin-styled-components-attr#readme", 37 | "license": "MIT", 38 | "repository": { 39 | "type": "git", 40 | "url": "git+https://github.com/tkh44/babel-plugin-styled-components-attr.git" 41 | }, 42 | "directories": { 43 | "test": "tests" 44 | }, 45 | "keywords": [ 46 | "styles", 47 | "babel-plugin-styled-components-attr", 48 | "react", 49 | "css", 50 | "css-in-js", 51 | "styled-components" 52 | ], 53 | "eslintConfig": { 54 | "extends": "standard", 55 | "parser": "babel-eslint" 56 | }, 57 | "standard": { 58 | "parser": "babel-eslint", 59 | "ignore": [ 60 | "/dist/" 61 | ] 62 | }, 63 | "bugs": { 64 | "url": "https://github.com/tkh44/babel-plugin-styled-components-attr/issues" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /preset.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | require('./src/index.js') 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /src/__tests__/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "env", 4 | "stage-0", 5 | "react" 6 | ], 7 | "plugins": [ 8 | ["../index.js"] 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /src/__tests__/__snapshots__/index.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`emotion/babel babel styled component all value types 1`] = ` 4 | "styled('input')\` 5 | margin: \${function getMargin(props) { 6 | return props.margin + 'em'; 7 | }}; 8 | margin: \${function getMargin(props) { 9 | return props.margin + 'ex'; 10 | }}; 11 | margin: \${function getMargin(props) { 12 | return props.margin + 'px'; 13 | }}; 14 | margin: \${function getMargin(props) { 15 | return props.margin + 'rem'; 16 | }}; 17 | margin: \${function getMargin(props) { 18 | return props.margin + 'vw'; 19 | }}; 20 | margin: \${function getMargin(props) { 21 | return props.margin + 'vh'; 22 | }}; 23 | margin: \${function getMargin(props) { 24 | return props.margin + 'vmin'; 25 | }}; 26 | margin: \${function getMargin(props) { 27 | return props.margin + 'vmax'; 28 | }}; 29 | margin: \${function getMargin(props) { 30 | return props.margin + 'mm'; 31 | }}; 32 | margin: \${function getMargin(props) { 33 | return props.margin + 'cm'; 34 | }}; 35 | margin: \${function getMargin(props) { 36 | return props.margin + 'in'; 37 | }}; 38 | margin: \${function getMargin(props) { 39 | return props.margin + 'pt'; 40 | }}; 41 | margin: \${function getMargin(props) { 42 | return props.margin + 'pc'; 43 | }}; 44 | margin: \${function getMargin(props) { 45 | return props.margin + '%'; 46 | }}; 47 | \`;" 48 | `; 49 | 50 | exports[`emotion/babel babel styled component basic 1`] = ` 51 | " 52 | styled('input')\` 53 | height: \${props => props.height}; 54 | margin: \${function getMargin(props) { 55 | return props.margin; 56 | }}; 57 | padding: \${function getPadding(props) { 58 | return undefined === props.padding || null === props.padding ? ('16' + 'em') : props.padding; 59 | }}; 60 | color: blue; 61 | \`;" 62 | `; 63 | 64 | exports[`emotion/babel babel styled component css tag 1`] = ` 65 | "css\` 66 | margin: \${function getMargin(props) { 67 | return undefined === props.margin || null === props.margin ? (\\"16\\" + \\"px\\") : props.margin; 68 | }}; 69 | padding: \${function getPadding(props) { 70 | return undefined === props.padding || null === props.padding ? (\\"16\\" + \\"em\\") : props.padding; 71 | }}; 72 | font-size: attr(fontSize ch, 8); 73 | width: \${function getWidth(props) { 74 | return undefined === props.width || null === props.width ? (\\"95\\" + \\"%\\") : props.width; 75 | }}; 76 | height: \${function getHeight(props) { 77 | return undefined === props.height || null === props.height ? (\\"90\\" + \\"vw\\") : props.height; 78 | }}; 79 | display: \${function getDisplay(props) { 80 | return undefined === props.display || null === props.display ? (\\"flex\\" + \\"\\") : props.display; 81 | }}; 82 | \`;" 83 | `; 84 | 85 | exports[`emotion/babel babel styled component empty 1`] = ` 86 | " 87 | styled('input')\`\`;" 88 | `; 89 | 90 | exports[`emotion/babel babel styled component kitchen sink 1`] = ` 91 | "styled('input')\` 92 | margin: \${function getMargin(props) { 93 | return undefined === props.margin || null === props.margin ? ('16' + 'px') : props.margin; 94 | }}; 95 | padding: \${function getPadding(props) { 96 | return undefined === props.padding || null === props.padding ? ('16' + 'em') : props.padding; 97 | }}; 98 | font-size: attr(fontSize ch, 8); 99 | width: \${function getWidth(props) { 100 | return undefined === props.width || null === props.width ? ('95' + '%') : props.width; 101 | }}; 102 | height: \${function getHeight(props) { 103 | return undefined === props.height || null === props.height ? ('90' + 'vw') : props.height; 104 | }}; 105 | display: \${function getDisplay(props) { 106 | return undefined === props.display || null === props.display ? ('flex' + '') : props.display; 107 | }}; 108 | \`;" 109 | `; 110 | 111 | exports[`emotion/babel babel styled component member expression 1`] = ` 112 | "styled.input\` 113 | margin: \${function getMargin(props) { 114 | return props.margin; 115 | }}; 116 | color: #ffffff; 117 | height: \${props => props.height * props.scale}; 118 | width: \${function getWidth(props) { 119 | return props.width; 120 | }}; 121 | color: blue; 122 | display: \${flex}; 123 | \`;" 124 | `; 125 | 126 | exports[`emotion/babel babel styled component something else 1`] = ` 127 | " 128 | const a = \`color: attr(height);\`;" 129 | `; 130 | 131 | exports[`emotion/babel babel styled component with default value 1`] = ` 132 | "styled('input')\` 133 | margin: \${function getMargin(props) { 134 | return undefined === props.margin || null === props.margin ? ('16' + '') : props.margin; 135 | }}; 136 | \`;" 137 | `; 138 | 139 | exports[`emotion/babel babel styled component with value type 1`] = ` 140 | "styled('input')\` 141 | margin: \${function getMargin(props) { 142 | return props.margin + 'px'; 143 | }}; 144 | \`;" 145 | `; 146 | 147 | exports[`emotion/babel babel styled component with value type 2`] = ` 148 | "styled.input.attrs({ type: 'text' })\` 149 | margin: \${function getMargin(props) { 150 | return props.margin + 'px'; 151 | }}; 152 | \`;" 153 | `; 154 | 155 | exports[`emotion/babel babel styled component with value type and default value 1`] = ` 156 | "styled('input')\` 157 | margin: \${function getMargin(props) { 158 | return undefined === props.margin || null === props.margin ? ('16' + 'px') : props.margin; 159 | }}; 160 | \`;" 161 | `; 162 | -------------------------------------------------------------------------------- /src/__tests__/__snapshots__/react.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`react attr 1`] = ` 4 | .hKjquV { 5 | font-size: 48; 6 | margin: 4rem; 7 | color: #495057; 8 | } 9 | 10 |

15 | `; 16 | 17 | exports[`react call expression 1`] = ` 18 | .etaRTp { 19 | color: #ffffff; 20 | width: 48%; 21 | margin: 16; 22 | } 23 | 24 | .jQCmMp { 25 | font-size: ; 26 | margin: 4rem; 27 | } 28 | 29 |

32 | hello world 33 | 38 |

39 | `; 40 | 41 | exports[`react default value does not show when real value is 0 1`] = ` 42 | .dpjTwL { 43 | height: 0; 44 | } 45 | 46 |

50 | `; 51 | 52 | exports[`react function in expression 1`] = ` 53 | .dFwslv { 54 | height: 48; 55 | font-size: 20px; 56 | } 57 | 58 | .gXhtwV { 59 | font-size: 40; 60 | } 61 | 62 |

67 | hello world 68 |

69 | `; 70 | -------------------------------------------------------------------------------- /src/__tests__/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable jsx-quotes,no-useless-escape,no-template-curly-in-string */ 2 | /* eslint-env jest */ 3 | const plugin = require('../index') 4 | const babel = require('babel-core') 5 | 6 | describe('emotion/babel', () => { 7 | describe('babel styled component', () => { 8 | test('something else', () => { 9 | const basic = ` 10 | const a = \`color: attr(height);\` 11 | ` 12 | const {code} = babel.transform(basic, { 13 | plugins: [plugin] 14 | }) 15 | expect(code).toMatchSnapshot() 16 | }) 17 | 18 | test('empty', () => { 19 | const basic = ` 20 | styled('input')\`\` 21 | ` 22 | const {code} = babel.transform(basic, { 23 | plugins: [plugin] 24 | }) 25 | expect(code).toMatchSnapshot() 26 | }) 27 | 28 | test('css tag', () => { 29 | const basic = `css\` 30 | margin: attr(margin px, 16); 31 | padding: attr(padding em, 16); 32 | font-size: attr(fontSize ch, 8); 33 | width: attr(width %, 95); 34 | height: attr(height vw, 90); 35 | display: attr(display, flex); 36 | \`` 37 | const {code} = babel.transform(basic, { 38 | plugins: [plugin] 39 | }) 40 | expect(code).toMatchSnapshot() 41 | }) 42 | 43 | test('basic', () => { 44 | const basic = ` 45 | styled('input')\` 46 | height: $\{props => props.height}; 47 | margin: attr(margin); 48 | padding: attr(padding em, 16); 49 | color: blue; 50 | \` 51 | ` 52 | const {code} = babel.transform(basic, { 53 | plugins: [plugin] 54 | }) 55 | expect(code).toMatchSnapshot() 56 | }) 57 | 58 | 59 | 60 | test('member expression', () => { 61 | const basic = `styled.input\` 62 | margin: attr(margin); 63 | color: #ffffff; 64 | height: \$\{props => props.height * props.scale\}; 65 | width: attr(width); 66 | color: blue; 67 | display: \$\{flex\}; 68 | \`` 69 | const { code } = babel.transform(basic, { 70 | plugins: [plugin] 71 | }) 72 | expect(code).toMatchSnapshot() 73 | }) 74 | 75 | test('with value type', () => { 76 | const basic = `styled('input')\` 77 | margin: attr(margin px); 78 | \`` 79 | const { code } = babel.transform(basic, { 80 | plugins: [plugin] 81 | }) 82 | expect(code).toMatchSnapshot() 83 | }) 84 | 85 | test('with default value', () => { 86 | const basic = `styled('input')\` 87 | margin: attr(margin, 16); 88 | \`` 89 | const { code } = babel.transform(basic, { 90 | plugins: [plugin] 91 | }) 92 | expect(code).toMatchSnapshot() 93 | }) 94 | 95 | test('with value type and default value', () => { 96 | const basic = `styled('input')\` 97 | margin: attr(margin px, 16); 98 | \`` 99 | const { code } = babel.transform(basic, { 100 | plugins: [plugin] 101 | }) 102 | expect(code).toMatchSnapshot() 103 | }) 104 | 105 | test('with value type', () => { 106 | const basic = `styled.input.attrs({ type: 'text' })\` 107 | margin: attr(margin px); 108 | \`` 109 | const {code} = babel.transform(basic, { 110 | plugins: [plugin] 111 | }) 112 | expect(code).toMatchSnapshot() 113 | }) 114 | 115 | test('kitchen sink', () => { 116 | const basic = `styled('input')\` 117 | margin: attr(margin px, 16); 118 | padding: attr(padding em, 16); 119 | font-size: attr(fontSize ch, 8); 120 | width: attr(width %, 95); 121 | height: attr(height vw, 90); 122 | display: attr(display, flex); 123 | \`` 124 | const { code } = babel.transform(basic, { 125 | plugins: [plugin] 126 | }) 127 | expect(code).toMatchSnapshot() 128 | }) 129 | 130 | test('all value types', () => { 131 | const basic = `styled('input')\` 132 | margin: attr(margin em); 133 | margin: attr(margin ex); 134 | margin: attr(margin px); 135 | margin: attr(margin rem); 136 | margin: attr(margin vw); 137 | margin: attr(margin vh); 138 | margin: attr(margin vmin); 139 | margin: attr(margin vmax); 140 | margin: attr(margin mm); 141 | margin: attr(margin cm); 142 | margin: attr(margin in); 143 | margin: attr(margin pt); 144 | margin: attr(margin pc); 145 | margin: attr(margin %); 146 | \`` 147 | const {code} = babel.transform(basic, { 148 | plugins: [plugin] 149 | }) 150 | expect(code).toMatchSnapshot() 151 | }) 152 | }) 153 | }) 154 | -------------------------------------------------------------------------------- /src/__tests__/react.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable jsx-quotes,no-useless-escape,no-template-curly-in-string */ 2 | /* eslint-env jest */ 3 | import 'jest-styled-components' 4 | import React from 'react' 5 | import renderer from 'react-test-renderer' 6 | import styled, { css } from 'styled-components' 7 | 8 | describe('react', () => { 9 | test('attr', () => { 10 | const baseText = css`color: attr(color, #a9e34b);` 11 | 12 | const H1 = styled.h1` 13 | font-size: attr(fontSize); 14 | margin: attr(margin rem, 4); 15 | ${baseText} 16 | ` 17 | 18 | const Title = ({ title }) => { 19 | return ( 20 |

21 | {title} 22 |

23 | ) 24 | } 25 | 26 | const tree = renderer.create().toJSON() 27 | 28 | expect(tree).toMatchStyledComponentsSnapshot() 29 | }) 30 | 31 | test('call expression', () => { 32 | const Input = styled.input` 33 | color: attr(color); 34 | width: attr(width %); 35 | margin: attr(margin px, 16); 36 | ` 37 | 38 | const H1 = styled('h1')` 39 | font-size: attr(fontSize); 40 | margin: attr(margin rem, 4); 41 | ` 42 | 43 | const tree = renderer 44 | .create( 45 | <H1 className={'legacy__class'}> 46 | hello world 47 | <Input color="#ffffff" width={48} margin={16} /> 48 | </H1> 49 | ) 50 | .toJSON() 51 | 52 | expect(tree).toMatchStyledComponentsSnapshot() 53 | }) 54 | 55 | test('function in expression', () => { 56 | const fontSize = 20 57 | const H1 = styled('h1')` 58 | height: attr(height); 59 | font-size: ${fontSize}px; 60 | ` 61 | 62 | const H2 = styled(H1)`font-size: ${({ scale }) => fontSize * scale}` 63 | 64 | const tree = renderer 65 | .create( 66 | <H2 scale={2} height={48} className={'legacy__class'}> 67 | hello world 68 | </H2> 69 | ) 70 | .toJSON() 71 | 72 | expect(tree).toMatchStyledComponentsSnapshot() 73 | }) 74 | 75 | test('default value does not show when real value is 0', () => { 76 | const H1 = styled('h1')` 77 | height: attr(height px, 99); 78 | ` 79 | 80 | const tree = renderer.create(<H1 height={0} />).toJSON() 81 | 82 | expect(tree).toMatchStyledComponentsSnapshot() 83 | }) 84 | }) 85 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | module.exports = function (babel) { 2 | const { types: t } = babel 3 | 4 | function findAndReplaceAttrs (path) { 5 | let quasis = path.node.quasis 6 | let stubs = path.node.expressions 7 | let didFindAtLeastOneMatch = false 8 | 9 | let [nextQuasis, nextStubs] = quasis.reduce( 10 | (accum, quasi, i) => { 11 | const str = quasi.value.cooked 12 | const regex = /attr\(([\S]+)(?:\s*(em|ex|px|rem|vw|vh|vmin|vmax|mm|cm|in|pt|pc|%)?)(?:,\s*([\S^)]+))?\)/gm 13 | let attrMatch 14 | let matches = [] 15 | while ((attrMatch = regex.exec(str)) !== null) { 16 | didFindAtLeastOneMatch = true 17 | matches.push({ 18 | value: attrMatch[0], 19 | propName: attrMatch[1], 20 | valueType: attrMatch[2], 21 | defaultValue: attrMatch[3], 22 | index: attrMatch.index 23 | }) 24 | } 25 | let cursor = 0 26 | for (let j = 0; j < matches.length; ++j) { 27 | const match = matches[j] 28 | const value = match.value 29 | const propName = match.propName 30 | const valueType = match.valueType 31 | const defaultValue = match.defaultValue 32 | const index = match.index 33 | 34 | const preAttr = `${str.slice(cursor, index)}` 35 | cursor = index + value.length 36 | const postAttr = `${str.slice(cursor)}` 37 | 38 | if (preAttr) { 39 | accum[0].push( 40 | t.templateElement( 41 | { 42 | raw: preAttr, 43 | cooked: preAttr 44 | }, 45 | false 46 | ) 47 | ) 48 | } 49 | 50 | if (postAttr && j === matches.length - 1) { 51 | accum[0].push( 52 | t.templateElement( 53 | { 54 | raw: postAttr, 55 | cooked: postAttr 56 | }, 57 | i === quasis.length - 1 58 | ) 59 | ) 60 | } 61 | 62 | let createMemberExpression = () => 63 | t.memberExpression(t.identifier('props'), t.identifier(propName)) 64 | 65 | let returnValue = createMemberExpression() 66 | 67 | if (valueType) { 68 | returnValue = t.binaryExpression( 69 | '+', 70 | createMemberExpression(), 71 | t.stringLiteral(valueType) 72 | ) 73 | } 74 | 75 | if (defaultValue) { 76 | returnValue = t.conditionalExpression( 77 | t.logicalExpression( 78 | '||', 79 | t.binaryExpression( 80 | '===', 81 | t.identifier('undefined'), 82 | createMemberExpression() 83 | ), 84 | t.binaryExpression( 85 | '===', 86 | t.nullLiteral(), 87 | createMemberExpression() 88 | ) 89 | ), 90 | t.parenthesizedExpression( 91 | t.binaryExpression( 92 | '+', 93 | t.stringLiteral(defaultValue), 94 | t.stringLiteral(valueType || '') 95 | ) 96 | ), 97 | createMemberExpression() 98 | ) 99 | } 100 | 101 | const body = t.blockStatement([t.returnStatement(returnValue)]) 102 | 103 | const expr = t.functionExpression( 104 | t.identifier( 105 | `get${propName.charAt(0).toUpperCase() + propName.slice(1)}` 106 | ), 107 | [t.identifier('props')], 108 | body 109 | ) 110 | accum[1].push(expr) 111 | } 112 | 113 | if (matches.length === 0) { 114 | accum[0].push(quasi) 115 | if (stubs[i]) { 116 | accum[1].push(stubs[i]) 117 | } 118 | } else if (stubs[i]) { 119 | accum[1].push(stubs[i]) 120 | } 121 | 122 | return accum 123 | }, 124 | [[], []] 125 | ) 126 | 127 | if (didFindAtLeastOneMatch) { 128 | return t.templateLiteral(nextQuasis, nextStubs) 129 | } 130 | 131 | return path.node 132 | } 133 | 134 | const Visitor = { 135 | TemplateLiteral (path) { 136 | const parentPath = path.find(path => path.isTaggedTemplateExpression()) 137 | if (!parentPath) { 138 | return 139 | } 140 | let parentTag = parentPath.node.tag 141 | 142 | // grab correct tab is styled.input.attrs type is used 143 | if ( 144 | t.isMemberExpression(parentTag.callee) && 145 | t.isMemberExpression(parentTag.callee.object) && 146 | parentTag.callee.object.object.name === 'styled' 147 | ) { 148 | parentTag = parentTag.callee.object 149 | } 150 | 151 | if (parentTag.name === 'css') { 152 | // css`...` 153 | path.replaceWith(findAndReplaceAttrs(path)) 154 | } else if ( 155 | // styled.h1`...` 156 | t.isMemberExpression(parentTag) && 157 | parentTag.object.name === 'styled' && 158 | t.isTemplateLiteral(path.node) 159 | ) { 160 | path.replaceWith(findAndReplaceAttrs(path)) 161 | } else if ( 162 | // styled('h1')`...` 163 | t.isCallExpression(parentPath.node.tag) && 164 | parentPath.node.tag.callee.name === 'styled' && 165 | t.isTemplateLiteral(path.node) 166 | ) { 167 | path.replaceWith(findAndReplaceAttrs(path)) 168 | } 169 | } 170 | } 171 | 172 | return { 173 | visitor: { 174 | TaggedTemplateExpression (path) { 175 | path.traverse(Visitor) 176 | } 177 | } 178 | } 179 | } 180 | --------------------------------------------------------------------------------