├── .babelrc
├── .eslintrc.js
├── .gitignore
├── .travis.yml
├── README.md
├── __tests__
├── __snapshots__
│ └── parser.test.js.snap
├── parser.test.js
├── selector.test.js
├── stylesheet.test.js
└── supports.test.js
├── docs
├── README.md
└── api.md
├── examples
├── animation.js
├── fontFace.js
├── media.js
├── mixins.js
├── selectors.js
├── supports.js
└── variables.js
├── images
├── One.png
├── Two.png
└── stylusreact.png
├── package.json
├── public
├── App.js
├── FiraCode-Retina.ttf
├── Laugh.ttf
└── index.html
├── src
├── index.js
├── main
│ └── createStylusComponent.js
├── parse
│ ├── fontFace.js
│ ├── getRootSelector.js
│ ├── keyframes.js
│ ├── parse.js
│ └── selectors.js
├── rules
│ ├── basicRules.js
│ ├── mediaRules.js
│ └── supportsRules.js
└── utils
│ ├── checkCode.js
│ ├── processStylusCode.js
│ ├── ruleTypes.js
│ └── stylesheet.js
├── todos.md
├── utils
└── testParser.js
├── webpack
├── webpack.config.dev.js
└── webpack.config.js
└── yarn.lock
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "react-app"
4 | ]
5 | }
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: 'airbnb',
3 | parser: 'babel-eslint',
4 | rules: {
5 | semi: 0,
6 | 'arrow-parens': 0
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | build
3 | public
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "6"
4 | notifications:
5 | email: false
6 | script:
7 | - npm run test
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # stylus-in-react
2 | [](https://travis-ci.org/nitin42/stylus-in-react)
3 | 
4 | 
5 | 
6 | 
7 |
8 |
9 |
10 |
11 |
12 | ## Documentation
13 |
14 | You can find the detailed documentation [here](./docs).
15 |
16 | ## License
17 |
18 | MIT
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/__tests__/__snapshots__/parser.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`stylus custom parser basic selector 1`] = `
4 | Object {
5 | "element": "button",
6 | "hash": Object {
7 | "data-css-1tpbuej": "",
8 | },
9 | "stylesheet": Array [
10 | Object {
11 | "borderRadius": "4px",
12 | },
13 | Object {
14 | "border": "2px solid #808080",
15 | },
16 | Object {
17 | "color": "#ffc0cb",
18 | },
19 | Object {
20 | ":hover": Object {
21 | "color": "#800080",
22 | },
23 | },
24 | ],
25 | }
26 | `;
27 |
28 | exports[`stylus custom parser works with @block 1`] = `
29 | Object {
30 | "element": "div",
31 | "hash": Object {
32 | "data-css-16uf9cy": "",
33 | },
34 | "stylesheet": Array [
35 | Object {
36 | "width": "20px",
37 | },
38 | Object {
39 | "height": "20px",
40 | },
41 | ],
42 | }
43 | `;
44 |
45 | exports[`stylus custom parser works with built-in functions 1`] = `
46 | Object {
47 | "element": "button",
48 | "hash": Object {
49 | "data-css-1xlcwgb": "",
50 | },
51 | "stylesheet": Array [
52 | Object {
53 | "color": "#0f0",
54 | },
55 | Object {
56 | "backgroundColor": "#fff",
57 | },
58 | ],
59 | }
60 | `;
61 |
62 | exports[`stylus custom parser works with char escaping 1`] = `
63 | Object {
64 | "element": "div",
65 | "hash": Object {
66 | "data-css-fjjr1s": "",
67 | },
68 | "stylesheet": Array [
69 | Object {
70 | "padding": "3px",
71 | },
72 | ],
73 | }
74 | `;
75 |
76 | exports[`stylus custom parser works with conditionals 1`] = `
77 | Object {
78 | "element": "div",
79 | "hash": Object {
80 | "data-css-1xgqhfw": "",
81 | },
82 | "stylesheet": Array [
83 | Object {
84 | "margin": "5px 10px",
85 | },
86 | ],
87 | }
88 | `;
89 |
90 | exports[`stylus custom parser works with functions 1`] = `
91 | Object {
92 | "element": "button",
93 | "hash": Object {
94 | "data-css-1r0s7ik": "",
95 | },
96 | "stylesheet": Array [
97 | Object {
98 | "padding": "30px",
99 | },
100 | ],
101 | }
102 | `;
103 |
104 | exports[`stylus custom parser works with interpolations 1`] = `
105 | Object {
106 | "element": "button",
107 | "hash": Object {
108 | "data-css-llmc5k": "",
109 | },
110 | "stylesheet": Array [
111 | Object {
112 | "borderRadius": "1px 2px/3px 4px",
113 | },
114 | ],
115 | }
116 | `;
117 |
118 | exports[`stylus custom parser works with mixins 1`] = `
119 | Object {
120 | "element": "button",
121 | "hash": Object {
122 | "data-css-wy342g": "",
123 | },
124 | "stylesheet": Array [
125 | Object {
126 | "color": "#f00",
127 | },
128 | Object {
129 | "borderRadius": "3px",
130 | },
131 | ],
132 | }
133 | `;
134 |
135 | exports[`stylus custom parser works with operators (usage with mixins) 1`] = `
136 | Object {
137 | "element": "body",
138 | "hash": Object {
139 | "data-css-tn55bb": "",
140 | },
141 | "stylesheet": Array [
142 | Object {
143 | "padding": "5px",
144 | },
145 | ],
146 | }
147 | `;
148 |
149 | exports[`stylus custom parser works with pseudo selectors 1`] = `
150 | Object {
151 | "element": "button",
152 | "hash": Object {
153 | "data-css-drp6v7": "",
154 | },
155 | "stylesheet": Array [
156 | Object {
157 | ":focus": Object {
158 | "outline": "none",
159 | },
160 | },
161 | Object {
162 | ":hover": Object {
163 | "color": "#ffc0cb",
164 | },
165 | },
166 | Object {
167 | ":active": Object {
168 | "outline": "none",
169 | },
170 | },
171 | ],
172 | }
173 | `;
174 |
175 | exports[`stylus custom parser works with rest params 1`] = `
176 | Object {
177 | "element": "div",
178 | "hash": Object {
179 | "data-css-10ag0uy": "",
180 | },
181 | "stylesheet": Array [
182 | Object {
183 | "boxShadow": "1px 2px 5px #eee",
184 | },
185 | ],
186 | }
187 | `;
188 |
--------------------------------------------------------------------------------
/__tests__/parser.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import renderer from 'react-test-renderer'
3 |
4 | import parser from '../utils/testParser'
5 |
6 | describe('stylus custom parser', () => {
7 | test('sanity check', () => {
8 | const DIV = `
9 | button
10 | color mistyrose
11 | border-radius 3px
12 | `
13 |
14 | const data = parser(DIV)
15 |
16 | expect(data).toMatchObject({
17 | element: 'button',
18 | hash: { 'data-css-1j9g9kt': '' },
19 | stylesheet: [{ color: '#ffe4e1' }, { borderRadius: '3px' }]
20 | })
21 | })
22 |
23 | test('basic selector', () => {
24 | const BUTTON = `
25 | button
26 | border-radius 4px
27 | border 2px solid grey
28 | color pink
29 | &:hover
30 | color purple
31 | `
32 |
33 | const data = parser(BUTTON)
34 |
35 | expect(data).toMatchSnapshot()
36 | })
37 |
38 | test('pythonic nature i.e gives errors on wrong indentation', () => {
39 | const BUTTON = `
40 | button
41 | border-radius 4px
42 | `
43 |
44 | try {
45 | const data = parser(BUTTON)
46 | } catch (e) {
47 | expect(e.message).toBe('Indentation error')
48 | }
49 | })
50 |
51 | test('works with functions', () => {
52 | const BUTTON = `
53 | plus(a, b)
54 | a + b
55 |
56 | button
57 | padding plus(10px, 20px)
58 | `
59 |
60 | const data = parser(BUTTON)
61 |
62 | expect(data).toMatchSnapshot()
63 | expect(data.stylesheet[0].padding).toBe('30px')
64 | })
65 |
66 | test('works with pseudo selectors', () => {
67 | const BUTTON = `
68 | button
69 | &:focus
70 | outline none
71 | &:hover
72 | color pink
73 | &:active
74 | outline none
75 | `
76 |
77 | const data = parser(BUTTON)
78 |
79 | expect(data).toMatchSnapshot()
80 | })
81 |
82 | test('works with mixins', () => {
83 | const BUTTON = `
84 | border-radius(n)
85 | border-radius n
86 |
87 | button
88 | color red
89 | border-radius(3px)
90 | `
91 |
92 | const data = parser(BUTTON)
93 |
94 | expect(data).toMatchSnapshot()
95 | })
96 |
97 | test('works with built-in functions', () => {
98 | const BUTTON = `
99 | button
100 | color green(#000, 255)
101 | background-color lighten(mistyrose, 30)
102 | `
103 |
104 | const data = parser(BUTTON)
105 |
106 | expect(data).toMatchSnapshot()
107 | })
108 |
109 | test('works with rest params', () => {
110 | const DIV = `
111 | box-shadow(args ...)
112 | box-shadow args
113 |
114 | div
115 | box-shadow 1px 2px 5px #eee
116 | `
117 |
118 | const data = parser(DIV)
119 |
120 | expect(data).toMatchSnapshot()
121 | })
122 |
123 | test('works with @block', () => {
124 | const DIV = `
125 | foo = @block {
126 | width: 20px
127 | height: 20px
128 | }
129 |
130 | div
131 | {foo}
132 | `
133 |
134 | const data = parser(DIV)
135 |
136 | expect(data).toMatchSnapshot()
137 | })
138 |
139 | test('throws error on invalid element', () => {
140 | const DIV = `
141 | d
142 | padding 10px
143 | `
144 |
145 | try {
146 | const data = parser(DIV)
147 | } catch (e) {
148 | expect(e.message).toBe("'d' is not a valid HTML element.")
149 | }
150 | })
151 |
152 | test('works with conditionals', () => {
153 | const DIV = `
154 | overload-padding = true
155 |
156 | if overload-padding
157 | padding(y, x)
158 | margin y x
159 |
160 | div
161 | padding 5px 10px
162 | `
163 |
164 | const data = parser(DIV)
165 |
166 | expect(data).toMatchSnapshot()
167 | })
168 |
169 | test('works with char escaping', () => {
170 | const DIV = `
171 | div
172 | padding (1 \+ 2)px
173 | `
174 |
175 | const data = parser(DIV)
176 |
177 | expect(data).toMatchSnapshot()
178 | })
179 |
180 | test('works with operators (usage with mixins)', () => {
181 | const DIV = `
182 | pad(types = padding, n = 5px)
183 | if padding in types
184 | padding n
185 | if margin in types
186 | margin n
187 |
188 | body
189 | pad()
190 | `
191 |
192 | const data = parser(DIV)
193 |
194 | expect(data).toMatchSnapshot()
195 | })
196 |
197 | test('works with interpolations', () => {
198 | const BUTTON = `
199 | apply(prop, args)
200 | {prop} args
201 |
202 | border-radius()
203 | apply('border-radius', arguments)
204 |
205 | box-shadow()
206 | apply('box-shadow', arguments)
207 |
208 | button
209 | border-radius 1px 2px / 3px 4px
210 | `
211 |
212 | const data = parser(BUTTON)
213 |
214 | expect(data).toMatchSnapshot()
215 | })
216 | })
217 |
--------------------------------------------------------------------------------
/__tests__/selector.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import renderer from 'react-test-renderer'
3 |
4 | import getParentNode from '../src/parse/selectors'
5 |
6 | describe('Selector', () => {
7 | test('returns a root selector', () => {
8 | const selectors = ['button:hover']
9 |
10 | expect(getParentNode(selectors)).toBe('button')
11 | })
12 | })
13 |
--------------------------------------------------------------------------------
/__tests__/stylesheet.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import renderer from 'react-test-renderer'
3 | import cssParser from 'css'
4 | import stylus from 'stylus'
5 |
6 | import createStylesheet from '../src/utils/stylesheet'
7 | import getParentNode from '../src/parse/selectors'
8 |
9 | const Example = `
10 | button
11 | color pink
12 | border 3px solid mistyrose
13 | border-radius 4px
14 | &:hover
15 | color lighten(purple, 20)
16 | background-color mistyrose
17 | `
18 |
19 | describe('Create stylesheet', () => {
20 | test('generates an array of styles', () => {
21 | let rules, selectors, element, AST
22 |
23 | stylus.render(Example, (err, css) => {
24 | AST = cssParser.parse(css, { source: 'css' })
25 | rules = AST.stylesheet.rules
26 | selectors =
27 | AST.stylesheet.rules[0] !== undefined
28 | ? AST.stylesheet.rules[0].selectors
29 | : null
30 | element = getParentNode(selectors)
31 | })
32 |
33 | const styles = createStylesheet(rules, element)
34 |
35 | const output = [
36 | { color: '#ffc0cb' },
37 | { border: '3px solid #ffe4e1' },
38 | { borderRadius: '4px' },
39 | { ':hover': { color: '#e600e6' } },
40 | { ':hover': { backgroundColor: '#ffe4e1' } }
41 | ]
42 |
43 | expect(styles).toEqual(output)
44 | })
45 | })
46 |
--------------------------------------------------------------------------------
/__tests__/supports.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import renderer from 'react-test-renderer'
3 |
4 | import parser from '../utils/testParser'
5 |
6 | describe('@support rules', () => {
7 | test('Example one', () => {
8 | const DIV = `
9 | @supports (display: flex)
10 | div
11 | display flex
12 | `
13 |
14 | const data = parser(DIV)
15 |
16 | expect(data).toEqual({
17 | element: 'div',
18 | hash: { 'data-css-18ahxhe': '' },
19 | stylesheet: [
20 | [
21 | {
22 | display: [
23 | '-webkit-box',
24 | '-moz-box',
25 | '-ms-flexbox',
26 | '-webkit-flex',
27 | 'flex'
28 | ]
29 | }
30 | ]
31 | ]
32 | })
33 | })
34 | })
35 |
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | # Docs
2 |
3 | ## Table of content
4 |
5 | * [Introduction](#introduction)
6 | * [Install](#install)
7 | * [Demo](#demo)
8 | * [Examples](#examples)
9 | * [Supported Features](#supported-features)
10 | * [API Reference](./api.md)
11 |
12 | ### Introduction
13 |
14 | I assume you know what [Stylus]() is and how it can be used in an expressive way to generate CSS. With stylus-in-react, you can directly style your React components with Stylus.
15 |
16 | I created this because Stylus has a lot of great features like transparent mixins, built-in functions, in-language functions and [many other features](http://stylus-lang.com/#features).
17 |
18 | ### Install
19 |
20 | ```
21 | npm install stylus-in-react
22 | ```
23 |
24 | This also depends on React so make sure you've already installed it!
25 |
26 | After you're done with the installation, place this in your index.html
27 |
28 | > IMPORTANT - This is required for client side usage of Stylus
29 |
30 | ```
31 |
32 | ```
33 |
34 | ### Demo
35 |
36 | 
37 |
38 | ### Examples
39 |
40 | You can find all the examples [here](../examples).
41 |
42 | ### Supported features
43 |
44 | * [selectors](http://stylus-lang.com/docs/selectors.html)
45 | * [variables](http://stylus-lang.com/docs/variables.html)
46 | * [interpolation](http://stylus-lang.com/docs/interpolations.html)
47 | * [operators](http://stylus-lang.com/docs/operators.html)
48 | * [mixins](http://stylus-lang.com/docs/mixins.html)
49 | * [functions](http://stylus-lang.com/docs/functions.html)
50 | * [keyword arguments](http://stylus-lang.com/docs/kwargs.html)
51 | * [built-in functions](http://stylus-lang.com/docs/bifs.html)
52 | * [rest params](http://stylus-lang.com/docs/vargs.html)
53 | * [conditionals](http://stylus-lang.com/docs/conditionals.html)
54 | * [@media queries](http://stylus-lang.com/docs/media.html)
55 | * [@font-face](http://stylus-lang.com/docs/font-face.html)
56 | * [@keyframes](http://stylus-lang.com/docs/keyframes.html)
57 | * [@Block](http://stylus-lang.com/docs/block.html)
58 | * [@supports](http://stylus-lang.com/docs/supports.html)
59 | * [char escaping](http://stylus-lang.com/docs/escape.html)
60 | * [iterations](http://stylus-lang.com/docs/iterations.html)
61 | * [hashes](http://stylus-lang.com/docs/hashes.html)
62 |
63 | ### API Reference
64 |
65 | You can the complete api reference [here](./api.md).
66 |
--------------------------------------------------------------------------------
/docs/api.md:
--------------------------------------------------------------------------------
1 | # API Reference
2 |
3 | ## stylus(code, { displayName })
4 |
5 | Creates a stylus component with the stylus `code` and accepts an optional argument `displayName`.
6 |
7 | **Example**
8 |
9 | ```js
10 | const Button = stylus(
11 | `
12 | button-styles(radius, size, color, type)
13 | border-radius radius
14 | border size type color
15 | color mistyrose
16 | background-color white
17 |
18 | button
19 | button-styles(3px, 2px, mistyrose, solid)
20 | `,
21 | { displayName: 'ButtonComponent' }
22 | );
23 | ```
24 |
25 |
26 |
27 |
28 |
29 | ## fontFace(code)
30 |
31 | Loads the given font-face and returns the font family name
32 |
33 | **Example**
34 |
35 | ```js
36 | const Fira = fontFace(`
37 | @font-face
38 | font-family FiraCode
39 | font-style normalize
40 | src url(./FiraCode-Retina.ttf)
41 | `);
42 |
43 | const Name = stylus(`
44 | h2
45 | font-family ${Fira}
46 | `, { displayName: 'Name' });
47 | ```
48 |
49 | ## keyframes(code)
50 |
51 | loads the animation keyframes and returns an animation name
52 |
53 | **Example**
54 |
55 | ```js
56 | const fadeIn = keyframes(`
57 | animation-name = fadeIn
58 |
59 | @keyframes animation-name
60 | for i in 0..10
61 | {10% * i}
62 | opacity (i/10)
63 | `);
64 |
65 | const FadeInButton = stylus(
66 | `
67 | button
68 | animation ${fadeIn} 4s ease-out
69 | `,
70 | { displayName: "FadeInButton" }
71 | );
72 | ```
73 |
74 |
75 |
76 |
--------------------------------------------------------------------------------
/examples/animation.js:
--------------------------------------------------------------------------------
1 | const { keyframes, stylus } = require('../src/')
2 |
3 | const fadeIn = keyframes(`
4 | animation-name = fadeIn
5 |
6 | @keyframes animation-name
7 | for i in 0..10
8 | {10% * i}
9 | opacity (i/10)
10 | `)
11 |
12 | const FadeInButton = stylus(
13 | `
14 | button
15 | animation ${fadeIn} 4s ease-out
16 | `,
17 | { displayName: 'FadeInButton' }
18 | )
19 |
20 | module.exports = FadeInButton
21 |
--------------------------------------------------------------------------------
/examples/fontFace.js:
--------------------------------------------------------------------------------
1 | const { fontFace, stylus } = require('../src/')
2 |
3 | const Fira = fontFace(`
4 | @font-face
5 | font-family FiraCode
6 | font-style normalize
7 | src url(./FiraCode-Retina.ttf)
8 | `)
9 |
10 | const Name = stylus(
11 | `
12 | h2
13 | font-family ${Fira}
14 | `,
15 | { displayName: 'Name' }
16 | )
17 |
18 | module.exports = Name
19 |
--------------------------------------------------------------------------------
/examples/media.js:
--------------------------------------------------------------------------------
1 | const { stylus } = require('../src/')
2 |
3 | const ButtonMedia = stylus(
4 | `
5 | button
6 | color red
7 | padding 10px
8 | @media screen and (min-width: 400px)
9 | padding 15px
10 | margin 10px
11 | `,
12 | { displayName: 'ButtonMedia' }
13 | )
14 |
15 | module.exports = ButtonMedia
16 |
--------------------------------------------------------------------------------
/examples/mixins.js:
--------------------------------------------------------------------------------
1 | const { stylus } = require('../src/')
2 |
3 | const ButtonMixin = stylus(
4 | `
5 | button-styles(radius, size, color, type)
6 | border-radius radius
7 | border size type color
8 | color mistyrose
9 | background-color white
10 |
11 | button
12 | button-styles(3px, 2px, mistyrose, solid)
13 | `,
14 | { displayName: 'MixinButton' }
15 | )
16 |
17 | module.exports = ButtonMixin
18 |
--------------------------------------------------------------------------------
/examples/selectors.js:
--------------------------------------------------------------------------------
1 | const { stylus } = require('../src/')
2 |
3 | const Button = stylus(
4 | `
5 | button
6 | color mistyrose
7 | background-color white
8 | border-radius 3px
9 | border 2px solid mistyrose
10 | &:hover
11 | color black
12 | background-color mistyrose
13 | &:focus
14 | outline none
15 | > *
16 | padding 10px
17 | `,
18 | { displayName: 'Button' }
19 | )
20 |
21 | module.exports = Button
22 |
--------------------------------------------------------------------------------
/examples/supports.js:
--------------------------------------------------------------------------------
1 | const { stylus } = require('../src/')
2 |
3 | const ButtonFloat = stylus(
4 | `
5 | @supports not (display: flex)
6 | button
7 | float: right
8 | padding: 5px
9 | `,
10 | { displayName: 'FloatButton' }
11 | )
12 |
13 | module.exports = ButtonFloat
14 |
--------------------------------------------------------------------------------
/examples/variables.js:
--------------------------------------------------------------------------------
1 | const { stylus } = require('../src/')
2 |
3 | const ButtonVar = stylus(
4 | `
5 | size = 12px
6 |
7 | button
8 | font-size size
9 | `,
10 | { displayName: 'ButtonVar' }
11 | )
12 |
13 | module.exports = ButtonVar
14 |
--------------------------------------------------------------------------------
/images/One.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nitin42/stylus-in-react/56ac8ec4e0cd026f6278588b4ba059abd815688c/images/One.png
--------------------------------------------------------------------------------
/images/Two.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nitin42/stylus-in-react/56ac8ec4e0cd026f6278588b4ba059abd815688c/images/Two.png
--------------------------------------------------------------------------------
/images/stylusreact.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nitin42/stylus-in-react/56ac8ec4e0cd026f6278588b4ba059abd815688c/images/stylusreact.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "stylus-in-react",
3 | "version": "2.0.0",
4 | "description": "Style React components with Stylus",
5 | "main": "build/main.js",
6 | "author": "Nitin Tulswani",
7 | "license": "MIT",
8 | "dependencies": {
9 | "camel-case": "^3.0.0",
10 | "common-tags": "^1.4.0",
11 | "css": "^2.2.1",
12 | "glamor": "^2.20.40",
13 | "html-tags": "^2.0.0",
14 | "inline-style-prefixer": "^3.0.7"
15 | },
16 | "peerDependencies": {
17 | "react": "^16.0.0"
18 | },
19 | "repository": {
20 | "url": "https://github.com/nitin42/stylus-in-react",
21 | "type": "git"
22 | },
23 | "files": [
24 | "build"
25 | ],
26 | "keywords": [
27 | "Stylus",
28 | "React",
29 | "css-in-js",
30 | "css",
31 | "parse css"
32 | ],
33 | "devDependencies": {
34 | "babel-eslint": "^8.0.0",
35 | "babel-loader": "^7.1.2",
36 | "babel-preset-react-app": "^3.0.2",
37 | "compression-webpack-plugin": "^1.0.0",
38 | "eslint": "^4.6.1",
39 | "eslint-config-airbnb": "^15.1.0",
40 | "eslint-plugin-import": "^2.7.0",
41 | "eslint-plugin-jsx-a11y": "^5.1.1",
42 | "eslint-plugin-react": "^7.3.0",
43 | "husky": "^0.14.3",
44 | "jest": "^21.0.2",
45 | "jest-cli": "^21.0.2",
46 | "lint-staged": "^4.2.1",
47 | "prop-types": "^15.5.10",
48 | "react": "^15.6.1",
49 | "react-dom": "^15.6.1",
50 | "react-test-renderer": "^15.6.1",
51 | "stylus": "^0.54.5",
52 | "webpack": "^3.5.6",
53 | "webpack-dev-server": "^2.7.1"
54 | },
55 | "scripts": {
56 | "validate": "yarn test && yarn lint",
57 | "precommit": "lint-staged",
58 | "test": "./node_modules/.bin/jest",
59 | "lint": "./node_modules/.bin/eslint ./src/",
60 | "start": "NODE_ENV=development ./node_modules/.bin/webpack-dev-server --content-base ./public --config ./webpack/webpack.config.dev.js --hot",
61 | "prebuild": "rm -rf ./build",
62 | "build": "NODE_ENV=production ./node_modules/.bin/webpack --config ./webpack/webpack.config.js --progress"
63 | },
64 | "lint-staged": {
65 | "*.js": [
66 | "prettier --single-quote --no-semi --write",
67 | "git add"
68 | ]
69 | },
70 | "jest": {
71 | "testEnvironment": "node"
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/public/App.js:
--------------------------------------------------------------------------------
1 | const React = require('react')
2 | const { render } = require('react-dom')
3 |
4 | // Stylus component examples
5 | // These are few examples but you can do pretty much everything you used to do with Stylus
6 | const Button = require('../examples/selectors')
7 | const ButtonMixin = require('../examples/mixins')
8 | const ButtonVar = require('../examples/variables')
9 | const ButtonMedia = require('../examples/media')
10 | const FadeInButton = require('../examples/animation')
11 | const ButtonFloat = require('../examples/supports')
12 | const Name = require('../examples/fontFace')
13 |
14 | class App extends React.Component {
15 | render() {
16 | return Submit
17 | }
18 | }
19 |
20 | render(, document.getElementById('app'))
21 |
--------------------------------------------------------------------------------
/public/FiraCode-Retina.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nitin42/stylus-in-react/56ac8ec4e0cd026f6278588b4ba059abd815688c/public/FiraCode-Retina.ttf
--------------------------------------------------------------------------------
/public/Laugh.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nitin42/stylus-in-react/56ac8ec4e0cd026f6278588b4ba059abd815688c/public/Laugh.ttf
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | React App
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | const stylus = require('./main/createStylusComponent')
2 | const keyframes = require('./parse/keyframes')
3 | const fontFace = require('./parse/fontFace')
4 |
5 | module.exports = {
6 | stylus,
7 | keyframes,
8 | fontFace
9 | }
10 |
--------------------------------------------------------------------------------
/src/main/createStylusComponent.js:
--------------------------------------------------------------------------------
1 | const React = require('react')
2 | const processStylusCode = require('../utils/processStylusCode')
3 | const parseStylus = require('../parse/parse')
4 | const checkStylusCode = require('../utils/checkCode')
5 |
6 | /**
7 | * This function takes the stylus code, parses it and creates an object of css rules.
8 | * These rules are then passed to glamor's css method and are inserted into the stylesheet
9 | * @param {string} stylusCode
10 | */
11 | function createStylusComponent(stylusCode, { displayName } = {}) {
12 | checkStylusCode(stylusCode, 'stylus()')
13 |
14 | const { hash, element } = parseStylus(processStylusCode(stylusCode))
15 |
16 | /* eslint-disable react/prefer-stateless-function */
17 | class StylusComponent extends React.Component {
18 | render() {
19 | return React.createElement(
20 | element,
21 | {
22 | className: `${hash}`,
23 | ...this.props
24 | },
25 | /* eslint-disable react/prop-types */
26 | this.props.children
27 | )
28 | }
29 | }
30 |
31 | StylusComponent.displayName =
32 | displayName && typeof displayName === 'string' ? displayName : element
33 |
34 | return StylusComponent
35 | }
36 |
37 | module.exports = createStylusComponent
38 |
--------------------------------------------------------------------------------
/src/parse/fontFace.js:
--------------------------------------------------------------------------------
1 | const glamor = require('glamor')
2 | const parser = require('css')
3 | const camelCase = require('camel-case')
4 | const processStylusCode = require('../utils/processStylusCode')
5 | const checkStylusCode = require('../utils/checkCode')
6 |
7 | /**
8 | * This function creates an object of @font-face rule declaration (property: value)
9 | * @param { array } rule
10 | */
11 | function getDeclaration(rule) {
12 | const store = {}
13 |
14 | rule.declarations.forEach(decl => {
15 | Object.assign(store, { [camelCase(decl.property)]: decl.value })
16 | })
17 |
18 | return store
19 | }
20 |
21 | /**
22 | * This function returns an object of @font-face rules
23 | * @param { array } rules @font-face rules
24 | */
25 | function getFontFaceProps(rules) {
26 | const store = {}
27 |
28 | rules.forEach(rule => {
29 | Object.assign(store, getDeclaration(rule))
30 | })
31 |
32 | return store
33 | }
34 |
35 | /**
36 | * This function returns the font-family name
37 | * @param { string } stylusCode Stylus code
38 | */
39 | function fontFace(stylusCode) {
40 | checkStylusCode(stylusCode, 'fontFace()')
41 |
42 | let AST
43 | let rules
44 | let atFontFaceRules
45 |
46 | /* eslint-disable no-undef */
47 | window.stylus.render(
48 | processStylusCode(stylusCode),
49 | { filename: 'source.css' },
50 | (err, css) => {
51 | if (err) {
52 | throw new Error(err)
53 | }
54 |
55 | // CSS ast
56 | AST = parser.parse(css, { source: 'css' })
57 |
58 | if (AST.stylesheet.rules[0].type !== 'font-face') {
59 | throw new Error('Not a @font-face rule.')
60 | }
61 |
62 | // @font-face rules
63 | rules = AST.stylesheet.rules
64 |
65 | // get the font-family name
66 | atFontFaceRules = getFontFaceProps(rules)
67 | }
68 | )
69 |
70 | return glamor.css.fontFace(atFontFaceRules)
71 | }
72 |
73 | module.exports = fontFace
74 |
--------------------------------------------------------------------------------
/src/parse/getRootSelector.js:
--------------------------------------------------------------------------------
1 | /**
2 | * These are required for some checks against the types in AST
3 | */
4 | const typesWhichRequireSelector = ['media', 'supports', 'import']
5 |
6 | /**
7 | * This function returns the root selector required to create a React component
8 | * @param { Object } AST CSS
9 | */
10 | function getRootSelector(AST) {
11 | const type =
12 | AST.stylesheet.rules[0] && AST.stylesheet.rules[0].type
13 | ? AST.stylesheet.rules[0].type
14 | : ''
15 |
16 | if (!typesWhichRequireSelector.includes(type)) {
17 | // Root selector rule
18 | return AST.stylesheet.rules[0].selectors
19 | }
20 |
21 | // alternate rules (@rules, :selector, contextual selectors and nested selectors)
22 | return AST.stylesheet.rules[0].rules[0].selectors
23 | }
24 |
25 | module.exports = getRootSelector
26 |
--------------------------------------------------------------------------------
/src/parse/keyframes.js:
--------------------------------------------------------------------------------
1 | const glamor = require('glamor')
2 | const parser = require('css')
3 | const camelCase = require('camel-case')
4 | const processStylusCode = require('../utils/processStylusCode')
5 | const checkStylusCode = require('../utils/checkCode')
6 |
7 | /**
8 | * This function returns an object of a @keyframes rule
9 | * @param { object } keyframe rule
10 | */
11 | function getProps(keyframe) {
12 | const props = {}
13 |
14 | keyframe.declarations.forEach(declaration => {
15 | Object.assign(props, {
16 | [camelCase(declaration.property)]: declaration.value
17 | })
18 | })
19 |
20 | return props
21 | }
22 |
23 | /**
24 | * This function stores each keyframe rule
25 | * @param { array } rule
26 | */
27 | function assignRule(rule) {
28 | const store = {}
29 |
30 | rule.keyframes.forEach(keyframe => {
31 | Object.assign(store, { [keyframe.values[0]]: getProps(keyframe) })
32 | })
33 |
34 | return store
35 | }
36 |
37 | /**
38 | * This function returns an object of @keyframes rules
39 | * @param { array } rules keyframe rules
40 | */
41 | function getKeyframeProps(rules) {
42 | const store = {}
43 |
44 | rules.forEach(rule => {
45 | Object.assign(store, assignRule(rule))
46 | })
47 |
48 | return store
49 | }
50 |
51 | /**
52 | * This function return an animation name (name_someHash)
53 | * @param { string } stylusCode Stylus code
54 | */
55 | function keyframes(stylusCode) {
56 | checkStylusCode(stylusCode, 'keyframes()')
57 |
58 | let AST
59 | let name
60 | let rules
61 | let AtKeyframesRules
62 |
63 | /* eslint-disable no-undef */
64 | window.stylus.render(
65 | processStylusCode(stylusCode),
66 | { filename: 'source.css' },
67 | (err, css) => {
68 | if (err) {
69 | throw new Error(err)
70 | }
71 |
72 | AST = parser.parse(css, { source: 'css' })
73 |
74 | if (AST.stylesheet.rules[0].type !== 'keyframes') {
75 | throw new Error('Not a keyframe rule.')
76 | }
77 |
78 | // Animation name
79 | name = AST.stylesheet.rules[0].name
80 |
81 | // @keyframes rules
82 | rules = AST.stylesheet.rules
83 |
84 | // @keyframes rules object
85 | AtKeyframesRules = getKeyframeProps(rules)
86 | }
87 | )
88 |
89 | return glamor.css.keyframes(name, AtKeyframesRules)
90 | }
91 |
92 | module.exports = keyframes
93 |
--------------------------------------------------------------------------------
/src/parse/parse.js:
--------------------------------------------------------------------------------
1 | const parser = require('css')
2 | const glamor = require('glamor')
3 |
4 | const getStylesheet = require('../utils/stylesheet')
5 | const getParentNode = require('../parse/selectors')
6 | const getRootSelector = require('../parse/getRootSelector')
7 |
8 | /**
9 | * This function takes the stylus code and parses it. It returns an object containing information
10 | * about the root element, stylesheet and css hash name generated by glamor.
11 | * @param { string } stylusCode
12 | */
13 | function parseStylus(stylusCode) {
14 | let AST
15 | let rules
16 | let stylesheet
17 | let hash
18 | let selector
19 | let element
20 |
21 | /* eslint-disable no-undef */
22 | window.stylus.render(stylusCode, { filename: 'source.css' }, (err, css) => {
23 | // throws parse errors
24 | if (err) {
25 | throw new Error(err)
26 | }
27 |
28 | // Generate css AST
29 | AST = parser.parse(css, { source: 'css' })
30 |
31 | // Get the root selector
32 | selector = getRootSelector(AST)
33 |
34 | // Set the root element
35 | element = getParentNode(selector)
36 |
37 | // Style rules
38 | rules = AST.stylesheet.rules
39 |
40 | // Create array of styles
41 | stylesheet = getStylesheet(rules, element)
42 |
43 | // Pass styles as css rules to glamor's css constructor
44 | hash = glamor.css(stylesheet)
45 | })
46 |
47 | return {
48 | hash,
49 | element,
50 | stylesheet
51 | }
52 | }
53 |
54 | module.exports = parseStylus
55 |
--------------------------------------------------------------------------------
/src/parse/selectors.js:
--------------------------------------------------------------------------------
1 | const domElements = require('html-tags')
2 |
3 | /**
4 | * This function takes a selector array and returns the root selector.
5 | * For example - If there is only one css rule, button:hover{}. So it returns 'button' as
6 | * the root element
7 | * @param { array } selector
8 | */
9 | function getParentNode(selector) {
10 | if (selector !== null) {
11 | const selectorName = selector[0].split(':')[0]
12 |
13 | if (!domElements.includes(selectorName)) {
14 | throw new Error(`'${selectorName}' is not a valid HTML element.`)
15 | }
16 | return selectorName
17 | }
18 |
19 | return null
20 | }
21 |
22 | module.exports = getParentNode
23 |
--------------------------------------------------------------------------------
/src/rules/basicRules.js:
--------------------------------------------------------------------------------
1 | const prefixAll = require('inline-style-prefixer/static')
2 | const camelCase = require('camel-case')
3 |
4 | const types = require('../utils/ruleTypes')
5 |
6 | /**
7 | * This function returns an array of all the style rules
8 | * @param { array } rules
9 | * @param { string } root
10 | */
11 | function getRules(rules, root) {
12 | const arr = []
13 |
14 | rules.forEach(rule => {
15 | if (!types.includes(rule.type)) {
16 | if (rule.selectors[0] === root) {
17 | rule.declarations.forEach(decl => {
18 | arr.push(prefixAll({ [camelCase(decl.property)]: decl.value }))
19 | })
20 | } else {
21 | rule.declarations.forEach(decl => {
22 | arr.push(
23 | prefixAll({
24 | [rule.selectors[0].replace(root, '')]: {
25 | [camelCase(decl.property)]: decl.value
26 | }
27 | })
28 | )
29 | })
30 | }
31 | }
32 | })
33 |
34 | return arr
35 | }
36 |
37 | module.exports = getRules
38 |
--------------------------------------------------------------------------------
/src/rules/mediaRules.js:
--------------------------------------------------------------------------------
1 | const camelCase = require('camel-case')
2 |
3 | /**
4 | * This function creates an object of a @media rule
5 | * @param { array } rule @media rule
6 | * @param { string } root root selector
7 | */
8 | function assignProperty(rule, root) {
9 | const styles = {}
10 |
11 | rule.forEach(i => {
12 | if (i.selectors[0] === root) {
13 | i.declarations.forEach(declaration => {
14 | Object.assign(styles, {
15 | [camelCase(declaration.property)]: declaration.value
16 | })
17 | })
18 | }
19 | })
20 |
21 | return styles
22 | }
23 |
24 | /**
25 | * This function returns an array of all the @media rule
26 | * @param { array } rules @media rules
27 | * @param { string } root root selector
28 | */
29 | function getMediaRules(rules, root) {
30 | const arr = []
31 |
32 | rules.forEach(rule => {
33 | if (rule.type === 'media') {
34 | arr.push({ [`@media ${rule.media}`]: assignProperty(rule.rules, root) })
35 | }
36 | })
37 |
38 | return arr
39 | }
40 |
41 | module.exports = getMediaRules
42 |
--------------------------------------------------------------------------------
/src/rules/supportsRules.js:
--------------------------------------------------------------------------------
1 | const getRules = require('./basicRules')
2 |
3 | /**
4 | * This function returns an array of objects of @supports rules
5 | * @param { array } rules @supports rules
6 | * @param { string } root root selector
7 | */
8 | function supportRules(rules, root) {
9 | const arr = []
10 |
11 | rules.forEach(rule => {
12 | if (rule.type !== 'supports') return
13 | arr.push(getRules(rule.rules, root))
14 | })
15 |
16 | return arr
17 | }
18 |
19 | module.exports = supportRules
20 |
--------------------------------------------------------------------------------
/src/utils/checkCode.js:
--------------------------------------------------------------------------------
1 | /**
2 | * This function validates the string of stylus code
3 | * @param { any } code
4 | * @param { name } constructor name
5 | */
6 | function checkStylusCode(code, name) {
7 | if (typeof code !== 'string') {
8 | throw new Error(
9 | `${name} constructor expected a stylus string. You passed ${code}`
10 | )
11 | }
12 | }
13 |
14 | module.exports = checkStylusCode
15 |
--------------------------------------------------------------------------------
/src/utils/processStylusCode.js:
--------------------------------------------------------------------------------
1 | const { stripIndent } = require('common-tags')
2 |
3 | /**
4 | * This function strips the indentation and preprocess the stylus input
5 | * @param { string } stylusCode
6 | */
7 | function processStylusCode(stylusCode) {
8 | if (!stylusCode) return null
9 | return stripIndent`${stylusCode}`
10 | }
11 |
12 | module.exports = processStylusCode
13 |
--------------------------------------------------------------------------------
/src/utils/ruleTypes.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Keep a track of type of style rule (currently, only some of them are implemented)
3 | */
4 | module.exports = [
5 | 'media',
6 | 'keyframes',
7 | 'font-face',
8 | 'supports',
9 | 'page',
10 | 'namespace',
11 | 'import',
12 | 'host',
13 | 'document',
14 | 'custom-media',
15 | 'charset'
16 | ]
17 |
--------------------------------------------------------------------------------
/src/utils/stylesheet.js:
--------------------------------------------------------------------------------
1 | const domElements = require('html-tags')
2 | const getRules = require('../rules/basicRules')
3 | const mediaRules = require('../rules/mediaRules')
4 | const supportRules = require('../rules/supportsRules')
5 |
6 | /**
7 | * This function creates css rules (array of style objects)
8 | * @param { array } rules css rules array
9 | * @param { string } root root element
10 | */
11 | function getStylesheet(rules, root) {
12 | if (!domElements.includes(root)) {
13 | return {}
14 | }
15 |
16 | const arr = []
17 |
18 | arr.push(
19 | ...getRules(rules, root),
20 | ...supportRules(rules, root),
21 | ...mediaRules(rules, root)
22 | )
23 | return arr
24 | }
25 |
26 | module.exports = getStylesheet
27 |
--------------------------------------------------------------------------------
/todos.md:
--------------------------------------------------------------------------------
1 | # Todos
2 |
3 | * Experiment with returning multiple components
4 | * Experiment with @extend
5 | * Theme component
6 | * Performance
--------------------------------------------------------------------------------
/utils/testParser.js:
--------------------------------------------------------------------------------
1 | const parser = require('css')
2 | const camelCase = require('camel-case')
3 | const glamor = require('glamor')
4 | const stylus = require('stylus')
5 |
6 | const getStylesheet = require('../src/utils/stylesheet')
7 | const getParentNode = require('../src/parse/selectors')
8 | const getRootSelector = require('../src/parse/getRootSelector')
9 |
10 | /**
11 | * This function takes a stylus code and parses it. It returns an object containing information
12 | * about the root element, stylesheet and css hash name generated by glamor.
13 | * @param { string } stylusCode
14 | */
15 | function testParser(stylusCode) {
16 | let AST, rules, stylesheet, hash, selector, element
17 |
18 | stylus.render(stylusCode, { filename: 'source.css' }, (err, css) => {
19 | // throws parse errors
20 | if (err) {
21 | throw new Error('Indentation error')
22 | }
23 |
24 | // Generate css AST
25 | AST = parser.parse(css, { source: 'css' })
26 | // Get the root selector
27 | selector = getRootSelector(AST)
28 | // Set the root element
29 | element = getParentNode(selector)
30 | // Style rules
31 | rules = AST.stylesheet.rules
32 | // Create array of styles
33 | stylesheet = getStylesheet(rules, element)
34 | // Pass styles as css rules to glamor's css constructor
35 | hash = glamor.css(stylesheet)
36 | })
37 |
38 | return {
39 | hash,
40 | element,
41 | stylesheet
42 | }
43 | }
44 |
45 | module.exports = testParser
46 |
--------------------------------------------------------------------------------
/webpack/webpack.config.dev.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const webpack = require('webpack')
3 |
4 | module.exports = {
5 | entry: path.resolve(__dirname, '../public/App.js'),
6 | output: {
7 | filename: '../bundle.js',
8 | path: path.resolve(__dirname),
9 | publicPath: '/'
10 | },
11 | node: {
12 | fs: 'empty'
13 | },
14 | module: {
15 | rules: [
16 | {
17 | test: /\.js$/,
18 | exclude: ['node_modules'],
19 | use: 'babel-loader'
20 | }
21 | ]
22 | },
23 | plugins: [
24 | new webpack.optimize.UglifyJsPlugin({
25 | compress: {
26 | warnings: false,
27 | comparisons: false
28 | },
29 | output: {
30 | comments: false,
31 | ascii_only: true
32 | }
33 | })
34 | ],
35 | devtool: 'cheap-eval-source-maps'
36 | }
37 |
--------------------------------------------------------------------------------
/webpack/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const webpack = require('webpack')
3 | const CompressionPlugin = require('compression-webpack-plugin')
4 |
5 | const output = () => ({
6 | filename: '[name].js',
7 | path: path.resolve(__dirname, '../build'),
8 | publicPath: '/',
9 | library: 'stylus-in-react',
10 | libraryTarget: 'umd'
11 | })
12 |
13 | const externals = () => ({
14 | 'inline-style-prefixer': 'inline-style-prefixer',
15 | 'camel-case': 'camel-case',
16 | css: 'css',
17 | glamor: 'glamor',
18 | 'html-tags': 'html-tags',
19 | react: 'react',
20 | 'common-tags': 'common-tags'
21 | })
22 |
23 | const jsLoader = () => ({
24 | test: /\.js$/,
25 | include: path.resolve(__dirname, '../src'),
26 | exclude: ['node_modules', 'public', 'images', 'utils', 'build', '__tests__'],
27 | use: 'babel-loader'
28 | })
29 |
30 | const plugins = () => [
31 | new webpack.LoaderOptionsPlugin({
32 | minimize: true,
33 | debug: false
34 | }),
35 | new webpack.DefinePlugin({
36 | 'process.env.NODE_ENV': 'production'
37 | }),
38 | new webpack.optimize.ModuleConcatenationPlugin(),
39 | new webpack.optimize.UglifyJsPlugin({
40 | compress: {
41 | warnings: false,
42 | comparisons: false
43 | },
44 | output: {
45 | comments: false,
46 | ascii_only: true
47 | }
48 | }),
49 | new CompressionPlugin({
50 | asset: '[path].gz[query]',
51 | algorithm: 'gzip',
52 | test: /\.(js)$/,
53 | threshold: 10240,
54 | minRatio: 0.8
55 | })
56 | ]
57 |
58 | module.exports = {
59 | entry: path.resolve(__dirname, '../src/index.js'),
60 | output: output(),
61 | target: 'web',
62 | node: {
63 | fs: 'empty'
64 | },
65 | performance: {
66 | hints: 'error'
67 | },
68 | context: path.resolve(__dirname),
69 | devtool: 'source-map',
70 | externals: externals(),
71 | module: {
72 | rules: [jsLoader()]
73 | },
74 | plugins: plugins()
75 | }
76 |
--------------------------------------------------------------------------------