├── .babelrc ├── .gitattributes ├── .gitignore ├── .npmignore ├── .travis.yml ├── HISTORY.md ├── LICENSE ├── README.md ├── expanded.js ├── flat.js ├── index.common.js ├── index.es6.js ├── package.json ├── project.sublime-project ├── scripts ├── mocha-bootload.js └── watch.js ├── source ├── expanded.js ├── flat.js ├── helpers.js ├── index.js └── tabulator.js └── test ├── expanded.js ├── exports.js ├── flat.js ├── helpers.js ├── styling.js └── tabulator.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": 3 | { 4 | "development": 5 | { 6 | "presets": 7 | [ 8 | "es2015", 9 | "stage-2" 10 | ], 11 | 12 | "plugins": 13 | [ 14 | "transform-runtime" 15 | ] 16 | }, 17 | "commonjs": 18 | { 19 | "presets": 20 | [ 21 | "es2015", 22 | "stage-2" 23 | ], 24 | 25 | "plugins": 26 | [ 27 | "transform-runtime" 28 | ] 29 | }, 30 | "es6": 31 | { 32 | "presets": 33 | [ 34 | ["es2015", { modules: false }], 35 | "stage-2" 36 | ], 37 | 38 | "plugins": 39 | [ 40 | "transform-runtime" 41 | ] 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # testing package 2 | /react-styling-*.tgz 3 | 4 | # test coverage folder 5 | /coverage/ 6 | 7 | # npm modules 8 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git 9 | /node_modules/ 10 | 11 | # npm errors 12 | npm-debug.log 13 | 14 | # github pages 15 | /gh-pages/ 16 | 17 | # for OS X users 18 | .DS_Store 19 | 20 | # cache files for sublime text 21 | *.tmlanguage.cache 22 | *.tmPreferences.cache 23 | *.stTheme.cache 24 | 25 | # workspace files are user-specific 26 | *.sublime-workspace 27 | 28 | # webpack build target folder 29 | /build/ 30 | /es6/ 31 | 32 | # NUL 33 | NUL -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # git 2 | .gitignore 3 | 4 | # Babel 5 | .babelrc 6 | 7 | # Sources aren't needed for npm 8 | /source 9 | 10 | # testing package 11 | /react-styling-*.tgz 12 | 13 | # Travis CI 14 | .travis.yml 15 | 16 | # test coverage folder 17 | /coverage/ 18 | 19 | # npm errors 20 | npm-debug.log 21 | 22 | # github pages 23 | /gh-pages/ 24 | 25 | # workspace files are user-specific 26 | *.sublime-workspace 27 | 28 | # NUL 29 | /NUL -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.12" 4 | - "4.0" 5 | - "4.1" 6 | - "4.2" 7 | - "5.0" 8 | sudo: false 9 | before_install: 10 | # Setup Node.js version-specific dependencies 11 | - "test $TRAVIS_NODE_VERSION != '0.8' || npm rm --save-dev istanbul" 12 | # before_script: 13 | # - npm update -g npm 14 | script: 15 | # Run test script, depending on istanbul install 16 | - "test -n $(npm -ps ls istanbul) || npm test" 17 | - "test -z $(npm -ps ls istanbul) || npm run-script test-travis" 18 | after_script: 19 | - "test -e ./coverage/lcov.info && npm install coveralls@2 && cat ./coverage/lcov.info | coveralls" 20 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | 1.6.0 / 03.11.2016 2 | ================== 3 | 4 | * Added `react-styling/expanded` for CSS shorthand properties autoexpansion (contributed by @pwlmaciejewski). E.g. `margin: 1px` -> `margin-left: 1px; ...`. 5 | 6 | 1.5.0 / 30.04.2016 7 | =================== 8 | 9 | * `@bvella` added support for Radium `@keyframes` 10 | 11 | 1.4.0 / 11.11.2015 12 | =================== 13 | 14 | * Added `react-styling/flat` styler 15 | 16 | 1.3.0 / 25.10.2015 17 | =================== 18 | 19 | * Added support for comma separated style classes and further style class extension 20 | 21 | 1.2.0 / 10.07.2015 22 | =================== 23 | 24 | * Pseudo-classes (those ones starting with a colon) and media queries (those ones starting with an at) are now copied to the modifiers (to better support Radium) 25 | 26 | 1.1.0 / 10.07.2015 27 | =================== 28 | 29 | * Changed modifier flag from dot to ampersand 30 | 31 | 1.0.0 / 06.07.2015 32 | =================== 33 | 34 | * Initial release 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2010 Sencha Inc. 4 | Copyright (c) 2011 TJ Holowaychuk 5 | Copyright (c) 2014-2015 Douglas Christopher Wilson 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining 8 | a copy of this software and associated documentation files (the 9 | 'Software'), to deal in the Software without restriction, including 10 | without limitation the rights to use, copy, modify, merge, publish, 11 | distribute, sublicense, and/or sell copies of the Software, and to 12 | permit persons to whom the Software is furnished to do so, subject to 13 | the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be 16 | included in all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 20 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 21 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 23 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 24 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-styling 2 | 3 | [![npm version](https://img.shields.io/npm/v/react-styling.svg?style=flat-square)](https://www.npmjs.com/package/react-styling) 4 | [![npm downloads](https://img.shields.io/npm/dm/react-styling.svg?style=flat-square)](https://www.npmjs.com/package/react-styling) 5 | [![build status](https://img.shields.io/travis/catamphetamine/react-styling/master.svg?style=flat-square)](https://travis-ci.org/catamphetamine/react-styling) 6 | [![coverage](https://img.shields.io/coveralls/catamphetamine/react-styling/master.svg?style=flat-square)](https://coveralls.io/r/catamphetamine/react-styling?branch=master) 7 | 8 | Is a helper function to convert various CSS syntaxes into a React style JSON object 9 | 10 | ## Autoprefixing 11 | 12 | This library doesn't perform CSS autoprefixing. Use [postcss autoprefixer](https://github.com/postcss/postcss-js) for that. 13 | 14 | ## Installation 15 | 16 | ```bash 17 | $ npm install react-styling 18 | ``` 19 | 20 | ## Usage 21 | 22 | This module uses an ES6 feature called [template strings](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/template_strings) which allows you to write multiline strings (finally). You can still use this module with the old ES5 (regular javascript) syntax passing it a regular string, but it's much more convenient for you to just use [Babel](https://babeljs.io/docs/setup/) for ES6 to ES5 conversion (everyone out there does it by the way). 23 | 24 | ```javascript 25 | import React from 'react' 26 | import styler from 'react-styling' 27 | 28 | export default class Page extends React.Component 29 | { 30 | render() 31 | { 32 | return ( 33 |
34 |
35 |
    36 |
  • Login
  • 37 |
  • About
  • 38 |
39 |
40 | 41 | 42 |
43 | ) 44 | } 45 | } 46 | 47 | const style = styler 48 | ` 49 | menu 50 | list-style-type: none 51 | 52 | item 53 | display: inline-block 54 | 55 | link 56 | display : inline-block 57 | text-decoration : none 58 | color : #000000 59 | padding : 0.4em 60 | 61 | // notice the ampersand character here: 62 | // this feature is called a "modifier" class 63 | // (see the "Modifiers" section of this document) 64 | ¤t 65 | color : #ffffff 66 | background-color : #000000 67 | 68 | // supports comma separated style classes 69 | // and further style class extension 70 | 71 | can_style, multiple_classes, at_once 72 | font-family : Sans 73 | 74 | can_style 75 | font-size : 12pt 76 | 77 | multiple_classes, at_once 78 | font-size : 8pt 79 | 80 | /* 81 | multi 82 | line 83 | comment 84 | */ 85 | 86 | .old-school-regular-css-syntax { 87 | box-sizing: border-box; 88 | color: black; 89 | } 90 | 91 | .scss_less { 92 | color: white; 93 | 94 | &:hover { 95 | text-decoration: underline; 96 | } 97 | } 98 | 99 | curly_braces_fan { 100 | background: none 101 | 102 | curly_braces_fan_number_two { 103 | background: transparent 104 | } 105 | } 106 | 107 | YAML_fan: 108 | display: inline-block 109 | 110 | python: 111 | length: 99999px 112 | 113 | // for Radium users 114 | @media (min-width: 320px) 115 | width: 100% 116 | 117 | :hover 118 | background: white 119 | ` 120 | ``` 121 | 122 | The example is self-explanatory. The CSS text in the example above will be transformed to this JSON object 123 | 124 | ```javascript 125 | { 126 | menu: 127 | { 128 | listStyleType: 'none', 129 | 130 | item: 131 | { 132 | display: 'inline-block', 133 | 134 | link: 135 | { 136 | display : 'inline-block', 137 | textDecoration : 'none', 138 | color : '#000000', 139 | padding : '0.4em', 140 | 141 | current: 142 | { 143 | display : 'inline-block', 144 | textDecoration : 'none', 145 | color : '#ffffff', 146 | backgroundColor : '#000000', 147 | padding : '0.4em' 148 | } 149 | } 150 | } 151 | }, 152 | 153 | can_style: 154 | { 155 | fontFamily : 'Sans', 156 | fontSize : '12pt' 157 | }, 158 | 159 | multiple_classes: 160 | { 161 | fontFamily : 'Sans', 162 | fontSize : '8pt' 163 | }, 164 | 165 | at_once: 166 | { 167 | fontFamily : 'Sans', 168 | fontSize : '8pt' 169 | }, 170 | 171 | 'old-school-regular-css-syntax': 172 | { 173 | boxSizing: 'border-box', 174 | color: 'black' 175 | }, 176 | 177 | scss_less: 178 | { 179 | color: 'white', 180 | 181 | ':hover' 182 | { 183 | color: 'white', 184 | textDecoration: 'underline' 185 | } 186 | }, 187 | 188 | curly_braces_fan: 189 | { 190 | background: 'none', 191 | 192 | curly_braces_fan_number_two: 193 | { 194 | background: 'transparent' 195 | } 196 | }, 197 | 198 | YAML_fan: 199 | { 200 | display: 'inline-block', 201 | 202 | python: 203 | { 204 | length: '99999px' 205 | } 206 | }, 207 | 208 | '@media (min-width: 320px)': 209 | { 210 | width: '100%', 211 | 212 | ':hover': 213 | { 214 | background: 'white' 215 | } 216 | } 217 | } 218 | ``` 219 | 220 | And that's it. No fancy stuff, it's just what this module does. You can then take this JSON object and use it as you wish. 221 | 222 | Pay attention to the tabulation as it's required for the whole thing to work properly. If you're one of those people who (for some strange reason) prefer spaces over tabs then you can still use it with spaces. Again, make sure that you keep all your spacing in order. And you can't mix tabs and spaces. 223 | 224 | You can use your good old pure CSS syntax with curly braces, semicolons and dotted style class names (in this case the leading dots in CSS style class names will be omitted for later JSON object keying convenience). 225 | 226 | Curly braces are a survival from the dark ages of 80s and the good old C language. Still you are free to use your curly braces for decoration - they'll simply be filtered out. 227 | 228 | You can also use YAML-alike syntax if you're one of those Python people. 229 | 230 | You can use both one-line comments and multiline comments. 231 | 232 | ### Nesting 233 | 234 | In the example above the result is a JSON object with a nested tree of CSS style classes. You can flatten it if you like by using `import { flat as styler } from 'react-styling'` instead of the default `import styler from 'react-styling'`. 235 | 236 | The difference is that the flat styler will flatten the CSS style class tree by prefixing all the style class names accordingly. 237 | 238 | The reason this feature was introduced is that, for example, [Radium](#radium) would give warnings if a style object contained child style objects. 239 | 240 | Also, I noticed that React, given a style object containing child style objects, creates irrelevant inline styles, e.g. ``: it doesn't break anything, but if some day React starts emitting warnings for that then just start using the `flat` styler. 241 | 242 | ### Modifiers 243 | 244 | In the example above, notice the ampersand before the "current" style class - this feature is optional (you don't need to use it at all), and it means that this style class is a "modifier" and all the style from its parent style class will be included in this style class. In this example, the padding, color, display and text-decoration from the "link" style class will be included in the "current" style class, so it works just like LESS/SASS ampersand. If you opt in to using the "modifiers" feature then you won't need to do manual merging like `style="extend({}, style.menu.item.link, style.menu.item.link.current)"`. 245 | 246 | Modifiers, when populated with the parent's styles, will also be populated with all the parent's pseudo-classes (those ones starting with a colon) and media queries (those ones starting with an at). This is done for better and seamless integration with [Radium](https://github.com/catamphetamine/react-styling#radium). 247 | 248 | Modifiers are applied all the way down to the bottom of the style subtree and, therefore, all the child styles are "modified" too. For example, this stylesheet 249 | 250 | ```javascript 251 | original 252 | display : inline-block 253 | 254 | item 255 | border : none 256 | color : black 257 | 258 | &active 259 | item 260 | color : white 261 | background : black 262 | ``` 263 | 264 | will be transformed to this style object 265 | 266 | ```javascript 267 | original: 268 | { 269 | display: 'inline-block', 270 | 271 | item: 272 | { 273 | border : 'none', 274 | color : 'black' 275 | }, 276 | 277 | active: 278 | { 279 | display: 'inline-block', 280 | 281 | item: 282 | { 283 | border : 'none', 284 | color : 'white', 285 | background : 'black' 286 | } 287 | } 288 | } 289 | ``` 290 | 291 | ### Shorthand style property expansion 292 | 293 | [A request was made](https://github.com/catamphetamine/react-styling/issues/3) to [add](https://github.com/catamphetamine/react-styling/pull/4) shorthand style property expansion feature to this library. The motivation is that when writing a CSS rule like `border: 1px solid red` in a base class and then overriding it with `border-color: blue` in some modifier class (like `:hover`) it's all merged correctly both when `:hover` is added and when `:hover` is removed. In React though, style rule update algorythm is not nearly that straightforward and bulletproof, and is in fact [a very basic one](https://github.com/facebook/react/issues/5397) which results in React not handling shorhand CSS property updates correctly. In these cases a special flavour of `react-styling` can be used: 294 | 295 | ```js 296 | import { expanded as styler } from 'react-styling' 297 | 298 | styler ` 299 | margin: 10px 300 | border: 1px solid red 301 | ` 302 | ``` 303 | 304 | Which results in the following style object 305 | 306 | ```js 307 | { 308 | marginTop : '10px', 309 | marginBottom : '10px', 310 | marginLeft : '10px', 311 | marginRight : '10px', 312 | 313 | borderTopWidth: '1px', 314 | borderTopStyle: 'solid', 315 | borderTopColor: 'red', 316 | // etc 317 | } 318 | ``` 319 | 320 | ### Radium 321 | 322 | There's a (popular) thing called [Radium](https://github.com/FormidableLabs/radium), which allows you to (citation): 323 | 324 | * Browser state styles to support :hover, :focus, and :active 325 | * Media queries 326 | * Automatic vendor prefixing 327 | * Keyframes animation helper 328 | 329 | You can use react-styling with this Radium library too: write you styles in text, then transform the text using react-styling into a JSON object, and then use that JSON object with Radium. If you opt in to use the "modifiers" feature of this module then you won't have to write `style={[style.a, style.a.b]}`, you can just write `style={style.a.b}`. 330 | 331 | Here is the [DroidList example](https://github.com/FormidableLabs/radium/tree/master/docs/faq#how-do-i-use-pseudo-selectors-like-checked-or-last) from Radium FAQ rewritten using react-styling. Because `first` and `last` are "modifiers" here the `:hover` pseudo-class will be present inside each of them as well. 332 | 333 | ```javascript 334 | // Notice the use of the "flat" styler as opposed to the default one: 335 | // it flattens the nested style object into a shallow style object. 336 | import { flat as styler } from 'react-styling' 337 | 338 | var droids = [ 339 | 'R2-D2', 340 | 'C-3PO', 341 | 'Huyang', 342 | 'Droideka', 343 | 'Probe Droid' 344 | ] 345 | 346 | @Radium 347 | class DroidList extends React.Component { 348 | render() { 349 | return ( 350 |
    351 | {droids.map((droid, index, droids) => 352 |
  • 353 | {droid} 354 |
  • 355 | )} 356 |
357 | ) 358 | } 359 | } 360 | 361 | const style = styler` 362 | droids 363 | padding : 0 364 | 365 | droid 366 | border-color : black 367 | border-style : solid 368 | border-width : 1px 1px 0 1px 369 | cursor : pointer 370 | list-style : none 371 | padding : 12px 372 | 373 | :hover 374 | background : #eee 375 | 376 | &first 377 | border-radius : 12px 12px 0 0 378 | 379 | &last 380 | border-radius : 0 0 12px 12px 381 | border-width : 1px 382 | ` 383 | ``` 384 | 385 | ### Performance 386 | 387 | In the examples above, `react-styling` transforms style text into a JSON object every time a React component is instantiated and then it will reuse that JSON style object for all `.render()` calls. React component instantiation happens, for example, in a `for ... of` loop or when a user navigates a page. I guess the penalty on the performance is negligible in this scenario. Yet, if someone wants to play with Babel they can write a Babel plugin (similar to [the one](https://github.com/facebook/relay/blob/master/scripts/babel-relay-plugin/src/getBabelRelayPlugin.js#L105) they use in [Relay](https://facebook.github.io/relay/docs/guides-babel-plugin.html#content)) and submit a Pull Request. 388 | 389 | ### Contributing 390 | 391 | After cloning this repo, ensure dependencies are installed by running: 392 | 393 | ```sh 394 | npm install 395 | ``` 396 | 397 | This module is written in ES6 and uses [Babel](http://babeljs.io/) for ES5 398 | transpilation. Widely consumable JavaScript can be produced by running: 399 | 400 | ```sh 401 | npm run build 402 | ``` 403 | 404 | Once `npm run build` has run, you may `import` or `require()` directly from 405 | node. 406 | 407 | After developing, the full test suite can be evaluated by running: 408 | 409 | ```sh 410 | npm test 411 | ``` 412 | 413 | While actively developing, one can use (personally I don't use it) 414 | 415 | ```sh 416 | npm run watch 417 | ``` 418 | 419 | in a terminal. This will watch the file system and run tests automatically 420 | whenever you save a js file. 421 | 422 | When you're ready to test your new functionality on a real project, you can run 423 | 424 | ```sh 425 | npm pack 426 | ``` 427 | 428 | It will `build`, `test` and then create a `.tgz` archive which you can then install in your project folder 429 | 430 | ```sh 431 | npm install [module name with version].tar.gz 432 | ``` 433 | 434 | ## License 435 | 436 | [MIT](LICENSE) -------------------------------------------------------------------------------- /expanded.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./build/expanded').default -------------------------------------------------------------------------------- /flat.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./build/flat').default -------------------------------------------------------------------------------- /index.common.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var styler = require('./build/index').default 4 | 5 | exports = module.exports = styler 6 | 7 | exports.flat = require('./build/flat').default 8 | exports.expanded = require('./build/expanded').default 9 | 10 | exports['default'] = styler -------------------------------------------------------------------------------- /index.es6.js: -------------------------------------------------------------------------------- 1 | export 2 | { 3 | default as default 4 | } 5 | from './es6/index' 6 | 7 | export 8 | { 9 | default as flat 10 | } 11 | from './es6/flat' 12 | 13 | export 14 | { 15 | default as expanded 16 | } 17 | from './es6/expanded' -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-styling", 3 | "version": "1.6.4", 4 | "description": "Transforms CSS-alike text into a React style JSON object", 5 | "main": "index.common.js", 6 | "module": "index.es6.js", 7 | "dependencies": { 8 | "babel-runtime": "^6.6.1", 9 | "style-builder": "^1.0.13" 10 | }, 11 | "devDependencies": { 12 | "babel-cli": "^6.6.5", 13 | "babel-core": "^6.7.2", 14 | "babel-loader": "^6.2.2", 15 | "babel-plugin-transform-runtime": "^6.6.0", 16 | "babel-preset-es2015": "^6.6.0", 17 | "babel-preset-stage-2": "^6.18.0", 18 | "better-npm-run": "0.0.14", 19 | "chai": "^3.5.0", 20 | "istanbul": "^1.0.0-alpha.2", 21 | "minimist": "^1.2.0", 22 | "mocha": "^2.4.5", 23 | "npm-run-all": "^1.4.0", 24 | "rimraf": "^2.5.0", 25 | "webpack": "^1.13.1", 26 | "webpack-merge": "^0.14.0" 27 | }, 28 | "scripts": { 29 | "test": "mocha --compilers js:babel-core/register --colors --bail --reporter spec test/ --recursive", 30 | "test-coverage": "istanbul cover node_modules/mocha/bin/_mocha -- --compilers js:babel-core/register --colors --reporter dot test/ --recursive", 31 | "test-travis": "istanbul cover node_modules/mocha/bin/_mocha --report lcovonly -- --compilers js:babel-core/register --colors --reporter spec test/ --recursive", 32 | "build": "npm-run-all clean-for-build build-commonjs-modules build-es6-modules", 33 | "build-regular": "webpack --action=build --color", 34 | "build-minified": "webpack --action=build-minified --color", 35 | "clean-for-build": "rimraf ./build/**/* ./es6/**/*", 36 | "build-commonjs-modules": "better-npm-run build-commonjs-modules", 37 | "build-es6-modules": "better-npm-run build-es6-modules", 38 | "prepublish": "npm-run-all build test" 39 | }, 40 | "betterScripts": { 41 | "build-commonjs-modules": { 42 | "command": "babel ./source --out-dir ./build --source-maps", 43 | "env": { 44 | "BABEL_ENV": "commonjs" 45 | } 46 | }, 47 | "build-es6-modules": { 48 | "command": "babel ./source --out-dir ./es6 --source-maps", 49 | "env": { 50 | "BABEL_ENV": "es6" 51 | } 52 | } 53 | }, 54 | "repository": { 55 | "type": "git", 56 | "url": "git+https://github.com/catamphetamine/react-styling.git" 57 | }, 58 | "keywords": [ 59 | "react", 60 | "style", 61 | "inline", 62 | "css" 63 | ], 64 | "author": "catamphetamine ", 65 | "license": "MIT", 66 | "bugs": { 67 | "url": "https://github.com/catamphetamine/react-styling/issues" 68 | }, 69 | "homepage": "https://github.com/catamphetamine/react-styling#readme" 70 | } 71 | -------------------------------------------------------------------------------- /project.sublime-project: -------------------------------------------------------------------------------- 1 | { 2 | "folders": 3 | [ 4 | { 5 | "follow_symlinks": true, 6 | "path": ".", 7 | "folder_exclude_patterns": ["react-styling/node_modules", "react-styling/coverage", "react-styling/build", "react-styling/build", "react-styling/es6", "react-styling/project.sublime-workspace"] 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /scripts/mocha-bootload.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | require('babel/register')({ 11 | optional: ['runtime', 'es7.asyncFunctions'] 12 | }); 13 | 14 | var chai = require('chai'); 15 | 16 | // var chaiSubset = require('chai-subset'); 17 | // chai.use(chaiSubset); 18 | 19 | process.on('unhandledRejection', function (error) { 20 | console.error('Unhandled Promise Rejection:'); 21 | console.error(error && error.stack || error); 22 | }); -------------------------------------------------------------------------------- /scripts/watch.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | import sane from 'sane'; 11 | import { resolve as resolvePath } from 'path'; 12 | import { spawn } from 'child_process'; 13 | 14 | process.env.PATH += ':./node_modules/.bin'; 15 | 16 | const is_windows = process.platform === 'win32' 17 | 18 | // still creates the NUL file in the current working directory 19 | const dev_null = is_windows ? 'NUL' : '/dev/null' 20 | 21 | var cmd = resolvePath(__dirname); 22 | var srcDir = resolvePath(cmd, './source'); 23 | 24 | function exec(command, options) { 25 | return new Promise(function (resolve, reject) { 26 | var child = spawn(command, options, { 27 | cmd: cmd, 28 | env: process.env, 29 | stdio: 'inherit' 30 | }); 31 | child.on('exit', function (code) { 32 | if (code === 0) { 33 | resolve(true); 34 | } else { 35 | reject(new Error('Error code: ' + code)); 36 | } 37 | }); 38 | }); 39 | } 40 | 41 | var watcher = sane(srcDir, { glob: ['**/*.*'] }) 42 | .on('ready', startWatch) 43 | .on('add', changeFile) 44 | .on('delete', deleteFile) 45 | .on('change', changeFile); 46 | 47 | process.on('SIGINT', function () { 48 | watcher.close(); 49 | console.log(CLEARLINE + yellow(invert('stopped watching'))); 50 | process.exit(); 51 | }); 52 | 53 | var isChecking; 54 | var needsCheck; 55 | var toCheck = {}; 56 | var timeout; 57 | 58 | function startWatch() { 59 | process.stdout.write(CLEARSCREEN + green(invert('watching...'))); 60 | } 61 | 62 | function changeFile(filepath, root, stat) { 63 | if (!stat.isDirectory()) { 64 | toCheck[filepath] = true; 65 | debouncedCheck(); 66 | } 67 | } 68 | 69 | function deleteFile(filepath) { 70 | delete toCheck[filepath]; 71 | debouncedCheck(); 72 | } 73 | 74 | function debouncedCheck() { 75 | needsCheck = true; 76 | clearTimeout(timeout); 77 | timeout = setTimeout(guardedCheck, 250); 78 | } 79 | 80 | function guardedCheck() { 81 | if (isChecking || !needsCheck) { 82 | return; 83 | } 84 | isChecking = true; 85 | var filepaths = Object.keys(toCheck); 86 | toCheck = {}; 87 | needsCheck = false; 88 | checkFiles(filepaths).then(() => { 89 | isChecking = false; 90 | process.nextTick(guardedCheck); 91 | }); 92 | } 93 | 94 | function checkFiles(filepaths) { 95 | console.log('\u001b[2J'); 96 | 97 | // return parseFiles(filepaths) 98 | // .then(() => runTests(filepaths)) 99 | return runTests(filepaths) 100 | .catch(() => false) 101 | .then(success => { 102 | process.stdout.write( 103 | '\n' + (success ? '' : '\x07') + green(invert('watching...')) 104 | ); 105 | }); 106 | } 107 | 108 | // Checking steps 109 | 110 | function executable(command) { 111 | if (is_windows) { 112 | return command + '.cmd' 113 | } 114 | return command 115 | } 116 | 117 | function parseFiles(filepaths) { 118 | console.log('Checking Syntax'); 119 | 120 | return Promise.all(filepaths.map(filepath => { 121 | if (isJS(filepath) && !isTest(filepath)) { 122 | return exec(executable('babel'), [ 123 | '--optional', 'runtime', 124 | '--out-file', dev_null, 125 | srcPath(filepath) 126 | ]); 127 | } 128 | })); 129 | } 130 | 131 | function runTests(filepaths) { 132 | console.log('\nRunning Tests'); 133 | 134 | return exec(executable('mocha'), [ 135 | '--reporter', 'spec', 136 | '--bail', 137 | '--require', 'scripts/mocha-bootload' 138 | ].concat( 139 | allTests(filepaths) ? filepaths.map(srcPath) : ['test/**/*.js'] // 'src/**/__tests__/**/*.js' 140 | )).catch(() => false); 141 | } 142 | 143 | // Filepath 144 | 145 | function srcPath(filepath) { 146 | return resolvePath(srcDir, filepath); 147 | } 148 | 149 | // Predicates 150 | 151 | function isJS(filepath) { 152 | return filepath.indexOf('.js') === filepath.length - 3; 153 | } 154 | 155 | function allTests(filepaths) { 156 | return filepaths.length > 0 && filepaths.every(isTest); 157 | } 158 | 159 | function isTest(filepath) { 160 | return isJS(filepath) && ~filepath.indexOf('__tests__/'); 161 | } 162 | 163 | // Print helpers 164 | 165 | var CLEARSCREEN = '\u001b[2J'; 166 | var CLEARLINE = '\r\x1B[K'; 167 | var CHECK = green('\u2713'); 168 | var X = red('\u2718'); 169 | 170 | function invert(str) { 171 | return `\u001b[7m ${str} \u001b[27m`; 172 | } 173 | 174 | function red(str) { 175 | return `\x1B[K\u001b[1m\u001b[31m${str}\u001b[39m\u001b[22m`; 176 | } 177 | 178 | function green(str) { 179 | return `\x1B[K\u001b[1m\u001b[32m${str}\u001b[39m\u001b[22m`; 180 | } 181 | 182 | function yellow(str) { 183 | return `\x1B[K\u001b[1m\u001b[33m${str}\u001b[39m\u001b[22m`; 184 | } -------------------------------------------------------------------------------- /source/expanded.js: -------------------------------------------------------------------------------- 1 | import { styler } from './index' 2 | 3 | // expands CSS shorthand properties 4 | // (e.g. `margin: 1px` -> `margin-left: 1px; ...`) 5 | export default function(strings, ...values) 6 | { 7 | return styler(strings, values, { expand: true }) 8 | } -------------------------------------------------------------------------------- /source/flat.js: -------------------------------------------------------------------------------- 1 | import styler, { is_pseudo_class, is_media_query } from './index' 2 | import { is_object } from './helpers' 3 | 4 | // for Radium it flattens the style class hierarchy: 5 | // moves nested style classes to the top of the naming tree 6 | // while prefixing them accordingly 7 | // (except modifiers and media queries) 8 | export default function(strings, ...values) 9 | { 10 | const style = styler.apply(this, [strings].concat(values)) 11 | 12 | move_up(style) 13 | 14 | return style 15 | } 16 | 17 | // moves child style classes to the surface of the style class tree 18 | // prefixing them accordingly 19 | function move_up(object, upside, new_name) 20 | { 21 | let prefix 22 | 23 | if (upside) 24 | { 25 | upside[new_name] = object 26 | prefix = `${new_name}_` 27 | } 28 | else 29 | { 30 | upside = object 31 | prefix = '' 32 | } 33 | 34 | for (let key of Object.keys(object)) 35 | { 36 | const child_object = object[key] 37 | 38 | if (is_object(child_object) && !is_pseudo_class(key) && !is_media_query(key)) 39 | { 40 | delete object[key] 41 | move_up(child_object, upside, `${prefix}${key}`) 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /source/helpers.js: -------------------------------------------------------------------------------- 1 | // // if the variable is defined 2 | export const exists = what => typeof what !== 'undefined' 3 | 4 | // used for JSON object type checking 5 | const object_constructor = {}.constructor 6 | 7 | // detects a JSON object 8 | export function is_object(object) 9 | { 10 | return exists(object) && (object !== null) && object.constructor === object_constructor 11 | } 12 | 13 | // extends the first object with 14 | /* istanbul ignore next: some weird transpiled code, not testable */ 15 | export function extend(...objects) 16 | { 17 | const to = objects[0] 18 | const from = objects[1] 19 | 20 | if (objects.length > 2) 21 | { 22 | const last = objects.pop() 23 | const intermediary_result = extend.apply(this, objects) 24 | return extend(intermediary_result, last) 25 | } 26 | 27 | for (let key of Object.keys(from)) 28 | { 29 | if (is_object(from[key])) 30 | { 31 | if (!is_object(to[key])) 32 | { 33 | to[key] = {} 34 | } 35 | 36 | extend(to[key], from[key]) 37 | } 38 | else 39 | { 40 | to[key] = from[key] 41 | } 42 | } 43 | 44 | return to 45 | } 46 | 47 | export function merge() 48 | { 49 | const parameters = Array.prototype.slice.call(arguments, 0) 50 | parameters.unshift({}) 51 | return extend.apply(this, parameters) 52 | } 53 | 54 | export function clone(object) 55 | { 56 | return merge({}, object) 57 | } 58 | 59 | // creates camelCased aliases for all the keys of an object 60 | export function convert_from_camel_case(object) 61 | { 62 | for (let key of Object.keys(object)) 63 | { 64 | if (/[A-Z]/.test(key)) 65 | // if (key.indexOf('_') >= 0) 66 | { 67 | // const camel_cased_key = key.replace(/_(.)/g, function(match, group_1) 68 | // { 69 | // return group_1.toUpperCase() 70 | // }) 71 | 72 | // if (!exists(object[camel_cased_key])) 73 | // { 74 | // object[camel_cased_key] = object[key] 75 | // delete object[key] 76 | // } 77 | 78 | const lo_dashed_key = key.replace(/([A-Z])/g, function(match, group_1) 79 | { 80 | return '_' + group_1.toLowerCase() 81 | }) 82 | 83 | if (!exists(object[lo_dashed_key])) 84 | { 85 | object[lo_dashed_key] = object[key] 86 | delete object[key] 87 | } 88 | } 89 | } 90 | 91 | return object 92 | } 93 | 94 | function escape_regexp(string) 95 | { 96 | const specials = new RegExp("[.*+?|()\\[\\]{}\\\\]", 'g') 97 | return string.replace(specials, "\\$&") 98 | } 99 | 100 | export function replace_all(where, what, with_what) 101 | { 102 | const regexp = new RegExp(escape_regexp(what), 'g') 103 | return where.replace(regexp, with_what) 104 | } 105 | 106 | export function starts_with(string, substring) 107 | { 108 | return string.indexOf(substring) === 0 109 | } 110 | 111 | export function ends_with(string, substring) 112 | { 113 | const index = string.lastIndexOf(substring) 114 | return index >= 0 && index === string.length - substring.length 115 | } 116 | 117 | export function is_empty(array) 118 | { 119 | return array.length === 0 120 | } 121 | 122 | export function not_empty(array) 123 | { 124 | return array.length > 0 125 | } 126 | 127 | // repeat string N times 128 | export function repeat(what, times) 129 | { 130 | let result = '' 131 | while (times > 0) 132 | { 133 | result += what 134 | times-- 135 | } 136 | return result 137 | } 138 | 139 | // if the text is blank 140 | export function is_blank(text) 141 | { 142 | return !exists(text) || !text.replace(/\s/g, '') 143 | } 144 | 145 | // zips two arrays 146 | export function zip(a, b) 147 | { 148 | return a.map(function(_, index) 149 | { 150 | return [a[index], b[index]] 151 | }) 152 | } -------------------------------------------------------------------------------- /source/index.js: -------------------------------------------------------------------------------- 1 | import { exists, starts_with, ends_with, is_blank, zip, extend, not_empty } from './helpers' 2 | import Tabulator from './tabulator' 3 | import style_builder from 'style-builder' 4 | 5 | // using ES6 template strings 6 | // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/template_strings 7 | export default function(strings, ...values) 8 | { 9 | return styler(strings, values) 10 | } 11 | 12 | // (the main function) 13 | // Parses CSS into a JSON object 14 | export function styler(strings, values, options = {}) 15 | { 16 | let style = '' 17 | 18 | // restore the whole string from "strings" and "values" parts 19 | let i = 0 20 | while (i < strings.length) 21 | { 22 | style += strings[i] 23 | if (exists(values[i])) 24 | { 25 | style += values[i] 26 | } 27 | i++ 28 | } 29 | 30 | return parse_style_json_object(style, options) 31 | } 32 | 33 | // converts text to JSON object 34 | function parse_style_json_object(text, options) 35 | { 36 | // remove multiline comments 37 | text = text.replace(/\/\*([\s\S]*?)\*\//g, '') 38 | 39 | // ignore curly braces for now. 40 | // maybe support curly braces along with tabulation in future 41 | text = text.replace(/[\{\}]/g, '') 42 | 43 | const lines = text.split('\n') 44 | 45 | // helper class for dealing with tabulation 46 | const tabulator = new Tabulator(Tabulator.determine_tabulation(lines)) 47 | 48 | // parse text into JSON object 49 | let style_json = parse_style_class(tabulator.extract_tabulation(lines), []) 50 | 51 | // expand CSS shorthand properties 52 | // (e.g. `margin: 1px` -> `margin-left: 1px; ...`) 53 | if (options.expand) 54 | { 55 | style_json = style_builder.build(style_json) 56 | } 57 | 58 | // expand "modifier" style classes 59 | // (which can override some of the expanded shorthand CSS properties) 60 | return expand_modifier_style_classes(style_json) 61 | } 62 | 63 | // parse child nodes' lines (and this node's styles) into this node's style JSON object 64 | function parse_node_json(styles, children_lines, node_names) 65 | { 66 | // transform this node's style lines from text to JSON properties and their values 67 | const style_object = styles.map(function(style) 68 | { 69 | let key = style.substring(0, style.indexOf(':')).trim() 70 | let value = style.substring(style.indexOf(':') + ':'.length).trim() 71 | 72 | // transform dashed key to camelCase key (it's required by React) 73 | key = key.replace(/([-]{1}[a-z]{1})/g, character => character.substring(1).toUpperCase()) 74 | 75 | // support old CSS syntax 76 | value = value.replace(/;$/, '').trim() 77 | 78 | // check if the value can be parsed into an integer 79 | if (String(parseInt(value)) === value) 80 | { 81 | value = parseInt(value) 82 | } 83 | 84 | // check if the value can be parsed into a float 85 | if (String(parseFloat(value)) === value) 86 | { 87 | value = parseFloat(value) 88 | } 89 | 90 | return { key, value } 91 | }) 92 | // combine the styles into a JSON object 93 | .reduce(function(styles, style) 94 | { 95 | styles[style.key] = style.value 96 | return styles 97 | }, 98 | {}) 99 | 100 | // parse child nodes and add them to this node's JSON object 101 | return extend(style_object, parse_children(children_lines, node_names)) 102 | } 103 | 104 | // separates style lines from children lines 105 | function split_into_style_lines_and_children_lines(lines) 106 | { 107 | // get this node style lines 108 | const style_lines = lines.filter(function(line) 109 | { 110 | // styles always have indentation of 1 111 | if (line.tabs !== 1) 112 | { 113 | return false 114 | } 115 | 116 | // detect generic css style line (skip modifier classes and media queries) 117 | const colon_index = line.line.indexOf(':') 118 | 119 | // is not a modifier class 120 | return !starts_with(line.line, '&') 121 | // is not a media query style class name declaration 122 | && !starts_with(line.line, '@media') 123 | // is not a keyframes style class name declaration 124 | && !starts_with(line.line, '@keyframes') 125 | // has a colon 126 | && colon_index >= 0 127 | // is not a state class (e.g. :hover) name declaration 128 | && colon_index !== 0 129 | // is not a yaml-style class name declaration 130 | && colon_index < line.line.length - 1 131 | }) 132 | 133 | // get children nodes' lines 134 | const children_lines = lines.filter(line => style_lines.indexOf(line) < 0) 135 | 136 | // reduce tabulation for this child node's (or these child nodes') child nodes' lines 137 | children_lines.forEach(line => line.tabs--) 138 | 139 | return { style_lines, children_lines} 140 | } 141 | 142 | // parses a style class node name 143 | function parse_node_name(name) 144 | { 145 | // is it a "modifier" style class 146 | let is_a_modifier = false 147 | 148 | // detect modifier style classes 149 | if (starts_with(name, '&')) 150 | { 151 | name = name.substring('&'.length) 152 | is_a_modifier = true 153 | } 154 | 155 | // support old-school CSS syntax 156 | if (starts_with(name, '.')) 157 | { 158 | name = name.substring('.'.length) 159 | } 160 | 161 | // if there is a trailing colon in the style class name - trim it 162 | // (Python people with yaml-alike syntax) 163 | if (ends_with(name, ':')) 164 | { 165 | name = name.substring(0, name.length - ':'.length) 166 | // throw new Error(`Remove the trailing colon at line: ${original_line}`) 167 | } 168 | 169 | return { name, is_a_modifier } 170 | } 171 | 172 | // parses child nodes' lines of text into the corresponding child node JSON objects 173 | function parse_children(lines, parent_node_names) 174 | { 175 | // preprocess the lines (filter out comments, blank lines, etc) 176 | lines = filter_lines_for_parsing(lines) 177 | 178 | // return empty object if there are no lines to parse 179 | if (lines.length === 0) 180 | { 181 | return {} 182 | } 183 | 184 | // parse each child node's lines 185 | return split_lines_by_child_nodes(lines).map(function(lines) 186 | { 187 | // the first line is this child node's name (or names) 188 | const declaration_line = lines.shift() 189 | 190 | // check for excessive indentation of the first child style class 191 | if (declaration_line.tabs !== 0) 192 | { 193 | throw new Error(`Excessive indentation (${declaration_line.tabs} more "tabs" than needed) at line ${declaration_line.index}: "${declaration_line.original_line}"`) 194 | } 195 | 196 | // style class name declaration 197 | const declaration = declaration_line.line 198 | 199 | // child nodes' names 200 | const names = declaration.split(',').map(name => name.trim()) 201 | 202 | // style class nesting validation 203 | validate_child_style_class_types(parent_node_names, names) 204 | 205 | // parse own CSS styles and recursively parse all child nodes 206 | const style_json = parse_style_class(lines, names) 207 | 208 | // generate style json for this child node (or child nodes) 209 | return names.map(function(node_declaration) 210 | { 211 | // parse this child node name 212 | const { name, is_a_modifier } = parse_node_name(node_declaration) 213 | 214 | // clone the style JSON object for this child node 215 | const json = extend({}, style_json) 216 | 217 | // set the modifier flag if it's the case 218 | if (is_a_modifier) 219 | { 220 | json._is_a_modifier = true 221 | } 222 | 223 | // this child node's style JSON object 224 | return { name, json } 225 | }) 226 | }) 227 | // convert an array of arrays to a flat array 228 | .reduce(function(array, child_array) 229 | { 230 | return array.concat(child_array); 231 | }, 232 | []) 233 | // combine all the child nodes into a single JSON object 234 | .reduce(function(nodes, node) 235 | { 236 | // if style already exists for this child node, extend it 237 | if (nodes[node.name]) 238 | { 239 | extend(nodes[node.name], node.json) 240 | } 241 | else 242 | { 243 | nodes[node.name] = node.json 244 | } 245 | 246 | return nodes 247 | }, 248 | {}) 249 | } 250 | 251 | // filters out comments, blank lines, etc 252 | function filter_lines_for_parsing(lines) 253 | { 254 | // filter out blank lines 255 | lines = lines.filter(line => !is_blank(line.line)) 256 | 257 | lines.forEach(function(line) 258 | { 259 | // remove single line comments 260 | line.line = line.line.replace(/^\s*\/\/.*/, '') 261 | // remove any trailing whitespace 262 | line.line = line.line.trim() 263 | }) 264 | 265 | return lines 266 | } 267 | 268 | // takes the whole lines array and splits it by its top-tier child nodes 269 | function split_lines_by_child_nodes(lines) 270 | { 271 | // determine lines with indentation = 0 (child node entry lines) 272 | const node_entry_lines = lines.map((line, index) => 273 | { 274 | return { tabs: line.tabs, index } 275 | }) 276 | .filter(line => line.tabs === 0) 277 | .map(line => line.index) 278 | 279 | // deduce corresponding child node ending lines 280 | const node_ending_lines = node_entry_lines.map(line_index => line_index - 1) 281 | node_ending_lines.shift() 282 | node_ending_lines.push(lines.length - 1) 283 | 284 | // each child node boundaries in terms of starting line index and ending line index 285 | const from_to = zip(node_entry_lines, node_ending_lines) 286 | 287 | // now lines are split by child nodes 288 | return from_to.map(from_to => lines.slice(from_to[0], from_to[1] + 1)) 289 | } 290 | 291 | // expand modifier style classes 292 | function expand_modifier_style_classes(node) 293 | { 294 | const style = get_node_style(node) 295 | const pseudo_classes_and_media_queries_and_keyframes = get_node_pseudo_classes_and_media_queries_and_keyframes(node) 296 | 297 | const modifiers = Object.keys(node) 298 | // get all modifier style class nodes 299 | .filter(name => typeof(node[name]) === 'object' && node[name]._is_a_modifier) 300 | 301 | // for each modifier style class node 302 | modifiers.forEach(function(name) 303 | { 304 | // // delete the modifier flags 305 | // delete node[name]._is_a_modifier 306 | 307 | // include parent node's styles and pseudo-classes into the modifier style class node 308 | node[name] = extend({}, style, pseudo_classes_and_media_queries_and_keyframes, node[name]) 309 | 310 | // expand descendant style class nodes of this modifier 311 | expand_modified_subtree(node, node[name]) 312 | }) 313 | 314 | // for each modifier style class node 315 | modifiers.forEach(function(name) 316 | { 317 | // delete the modifier flags 318 | delete node[name]._is_a_modifier 319 | }) 320 | 321 | // recurse 322 | Object.keys(node) 323 | // get all style class nodes 324 | .filter(name => typeof(node[name]) === 'object') 325 | // for each style class node 326 | .forEach(function(name) 327 | { 328 | // recurse 329 | expand_modifier_style_classes(node[name]) 330 | }) 331 | 332 | return node 333 | } 334 | 335 | // extracts root css styles of this style class node 336 | function get_node_style(node) 337 | { 338 | return Object.keys(node) 339 | // get all CSS styles of this style class node 340 | .filter(property => typeof(node[property]) !== 'object') 341 | // for each CSS style of this style class node 342 | .reduce(function(style, style_property) 343 | { 344 | style[style_property] = node[style_property] 345 | return style 346 | }, 347 | {}) 348 | } 349 | 350 | // extracts root pseudo-classes and media queries of this style class node 351 | function get_node_pseudo_classes_and_media_queries_and_keyframes(node) 352 | { 353 | return Object.keys(node) 354 | // get all child style classes this style class node, 355 | // which aren't modifiers and are a pseudoclass or a media query or keyframes 356 | .filter(property => typeof(node[property]) === 'object' 357 | && (is_pseudo_class(property) || is_media_query(property) || is_keyframes(property)) 358 | && !node[property]._is_a_modifier) 359 | // for each child style class of this style class node 360 | .reduce(function(pseudo_classes_and_media_queries_and_keyframes, name) 361 | { 362 | pseudo_classes_and_media_queries_and_keyframes[name] = node[name] 363 | return pseudo_classes_and_media_queries_and_keyframes 364 | }, 365 | {}) 366 | } 367 | 368 | // for each (non-modifier) child style class of the modifier style class, 369 | // check if "this child style class" is also present 370 | // as a (non-modifier) "child of the current style class". 371 | // if it is, then extend "this child style class" with the style 372 | // from the "child of the current style class". 373 | // (and repeat recursively) 374 | function expand_modified_subtree(node, modified_node) 375 | { 376 | // from the modified style class node 377 | Object.keys(modified_node) 378 | // for all non-pseudo-classes and non-media-queries 379 | .filter(name => !is_pseudo_class(name) && !is_media_query(name) && !is_keyframes(name)) 380 | // get all non-modifier style class nodes 381 | .filter(name => typeof(modified_node[name]) === 'object' && !modified_node[name]._is_a_modifier) 382 | // which are also present as non-modifier style classes 383 | // in the base style class node 384 | .filter(name => typeof(node[name]) === 'object' && !node[name]._is_a_modifier) 385 | 386 | // for each such style class node 387 | .forEach(function(name) 388 | { 389 | // style of the original style class node 390 | const style = get_node_style(node[name]) 391 | 392 | // pseudo-classes of the original style class node 393 | const pseudo_classes_and_media_queries_and_keyframes = get_node_pseudo_classes_and_media_queries_and_keyframes(node[name]) 394 | 395 | // mix in the styles 396 | modified_node[name] = extend({}, style, pseudo_classes_and_media_queries_and_keyframes, modified_node[name]) 397 | 398 | // recurse 399 | return expand_modified_subtree(node[name], modified_node[name]) 400 | }) 401 | } 402 | 403 | // checks if this style class name designates a pseudo-class 404 | export function is_pseudo_class(name) 405 | { 406 | return starts_with(name, ':') 407 | } 408 | 409 | // checks if this style class name is a media query (i.e. @media (...)) 410 | export function is_media_query(name) 411 | { 412 | return starts_with(name, '@media') 413 | } 414 | 415 | export function is_keyframe_selector(name) 416 | { 417 | return ends_with(name, '%') || (name === 'from') || (name === 'to'); 418 | } 419 | 420 | 421 | // checks if this style class name is a media query (i.e. @media (...)) 422 | export function is_keyframes(name) 423 | { 424 | return starts_with(name, '@keyframes') 425 | } 426 | 427 | // style class nesting validation 428 | function validate_child_style_class_types(parent_node_names, names) 429 | { 430 | for (let parent of parent_node_names) 431 | { 432 | // if it's a pseudoclass, it can't contain any style classes 433 | if (is_pseudo_class(parent) && not_empty(names)) 434 | { 435 | throw new Error(`A style class declaration "${names[0]}" found inside a pseudoclass "${parent}". Pseudoclasses (:hover, etc) can't contain child style classes.`) 436 | } 437 | 438 | // if it's a media query style class, it must contain only pseudoclasses 439 | if (is_media_query(parent)) 440 | { 441 | const non_pseudoclass = names.filter(x => !is_pseudo_class(x))[0] 442 | 443 | if (non_pseudoclass) 444 | { 445 | throw new Error(`A non-pseudoclass "${non_pseudoclass}" found inside a media query style class "${parent}". Media query style classes can only contain pseudoclasses (:hover, etc).`) 446 | } 447 | } 448 | 449 | // if it's a keyframes style class, it must contain only keyframe selectors 450 | if (is_keyframes(parent)) { 451 | const non_keyframe_selector = names.filter(x => !is_keyframe_selector(x))[0] 452 | 453 | if (non_keyframe_selector) 454 | { 455 | throw new Error(`A non-keyframe-selector "${non_keyframe_selector}" found inside a keyframes style class "${parent}". Keyframes style classes can only contain keyframe selectors (from, 100%, etc).`); 456 | } 457 | } 458 | } 459 | } 460 | 461 | // parse CSS style class 462 | function parse_style_class(lines, node_names) 463 | { 464 | // separate style lines from children lines 465 | const { style_lines, children_lines } = split_into_style_lines_and_children_lines(lines) 466 | 467 | // convert style lines info to just text lines 468 | const styles = style_lines.map(line => line.line) 469 | 470 | // using this child node's (or these child nodes') style lines 471 | // and this child node's (or these child nodes') child nodes' lines, 472 | // generate this child node's (or these child nodes') style JSON object 473 | // (this is gonna be a recursion) 474 | return parse_node_json(styles, children_lines, node_names) 475 | } -------------------------------------------------------------------------------- /source/tabulator.js: -------------------------------------------------------------------------------- 1 | import { starts_with, is_blank, repeat } from './helpers' 2 | 3 | // tabulation utilities 4 | export default class Tabulator 5 | { 6 | constructor(tab) 7 | { 8 | this.tab = tab 9 | } 10 | 11 | // remove some tabs in the beginning 12 | reduce_indentation(line, how_much) 13 | { 14 | return line.substring(this.tab.symbol.length * how_much) 15 | } 16 | 17 | // how many "tabs" are there before content of this line 18 | calculate_indentation(line) 19 | { 20 | const matches = line.match(this.tab.regexp) 21 | 22 | if (!matches || matches[0] === '') 23 | { 24 | return 0 25 | } 26 | 27 | return matches[0].length / this.tab.symbol.length 28 | } 29 | 30 | extract_tabulation(lines) 31 | { 32 | lines = lines 33 | // preserve line indexes 34 | .map((line, index) => 35 | { 36 | index++ 37 | return { line, index } 38 | }) 39 | // filter out blank lines 40 | .filter(line => !is_blank(line.line)) 41 | 42 | // calculate each line's indentation 43 | lines.forEach(line => 44 | { 45 | const tabs = this.calculate_indentation(line.line) 46 | const pure_line = this.reduce_indentation(line.line, tabs) 47 | 48 | // check for messed up space indentation 49 | if (starts_with(pure_line, ' ')) 50 | { 51 | let reason 52 | if (this.tab.symbol === '\t') 53 | { 54 | reason = 'mixed tabs and spaces' 55 | } 56 | else 57 | { 58 | reason = 'extra leading spaces' 59 | } 60 | 61 | throw new Error(`Invalid indentation (${reason}) at line ${line.index}: "${this.reveal_whitespace(line.line)}"`) 62 | } 63 | 64 | // check for tabs in spaced intentation 65 | if (starts_with(pure_line, '\t')) 66 | { 67 | throw new Error(`Invalid indentation (mixed tabs and spaces) at line ${line.index}: "${this.reveal_whitespace(line.line)}"`) 68 | } 69 | 70 | line.tabs = tabs 71 | line.original_line = line.line 72 | line.line = pure_line 73 | }) 74 | 75 | // get the minimum indentation level 76 | const minimum_indentation = lines 77 | .reduce((minimum, line) => Math.min(minimum, line.tabs), Infinity) 78 | 79 | // if there is initial tabulation missing - add it 80 | if (minimum_indentation === 0) 81 | { 82 | lines.forEach(function(line) 83 | { 84 | line.tabs++ 85 | }) 86 | } 87 | // if there is excessive tabulation - trim it 88 | else if (minimum_indentation > 1) 89 | { 90 | lines.forEach(function(line) 91 | { 92 | line.tabs -= minimum_indentation - 1 93 | }) 94 | } 95 | 96 | // check for messed up tabulation 97 | if (lines.length > 0 && lines[0].tabs !== 1) 98 | { 99 | throw new Error(`Invalid indentation at line ${lines[0].index}: "${lines[0].original_line}"`) 100 | } 101 | 102 | return lines 103 | } 104 | 105 | reveal_whitespace(text) 106 | { 107 | const whitespace_count = text.length - text.replace(/^\s*/, '').length 108 | 109 | const whitespace = text.substring(0, whitespace_count + 1) 110 | .replace(this.tab.regexp_anywhere, '[indent]') 111 | .replace(/ /g, '[space]') 112 | .replace(/\t/g, '[tab]') 113 | 114 | const rest = text.substring(whitespace_count + 1) 115 | 116 | return whitespace + rest 117 | } 118 | } 119 | 120 | // decide whether it's tabs or spaces 121 | Tabulator.determine_tabulation = function(lines) 122 | { 123 | const substract = pair => pair[0] - pair[1] 124 | 125 | function is_tabulated(line) 126 | { 127 | // if we're using tabs for tabulation 128 | if (starts_with(line, '\t')) 129 | { 130 | const tab = 131 | { 132 | symbol: '\t', 133 | regexp: new RegExp('^(\t)+', 'g'), 134 | regexp_anywhere: new RegExp('(\t)+', 'g') 135 | } 136 | 137 | return tab 138 | } 139 | } 140 | 141 | function spaced_tab(tab_width) 142 | { 143 | const symbol = repeat(' ', tab_width) 144 | 145 | const spaced_tab = 146 | { 147 | symbol: symbol, 148 | regexp: new RegExp(`^(${symbol})+`, 'g'), 149 | regexp_anywhere: new RegExp(`(${symbol})+`, 'g') 150 | } 151 | 152 | return spaced_tab 153 | } 154 | 155 | function calculate_leading_spaces(line) 156 | { 157 | let counter = 0 158 | line.replace(/^( )+/g, function(match) { counter = match.length }) 159 | return counter 160 | } 161 | 162 | // take all meaningful lines 163 | lines = lines.filter(line => !is_blank(line)) 164 | 165 | // has to be at least two of them 166 | if (lines.length === 0) 167 | { 168 | return tab 169 | // throw new Error(`Couldn't decide on tabulation type. Not enough lines.`) 170 | } 171 | 172 | if (lines.length === 1) 173 | { 174 | const tab = is_tabulated(lines[0]) 175 | if (tab) 176 | { 177 | return tab 178 | } 179 | 180 | return spaced_tab(calculate_leading_spaces(lines[0])) 181 | } 182 | 183 | // if we're using tabs for tabulation 184 | const tab = is_tabulated(lines[1]) 185 | if (tab) 186 | { 187 | return tab 188 | } 189 | 190 | // take the first two lines, 191 | // calculate their indentation, 192 | // substract it and you've got the tab width 193 | const tab_width = Math.abs(substract 194 | ( 195 | lines 196 | .slice(0, 2) 197 | .map(calculate_leading_spaces) 198 | )) 199 | || 1 200 | 201 | // if (tab_width === 0) 202 | // { 203 | // throw new Error(`Couldn't decide on tabulation type. Same indentation.`) 204 | // } 205 | 206 | return spaced_tab(tab_width) 207 | } -------------------------------------------------------------------------------- /test/expanded.js: -------------------------------------------------------------------------------- 1 | // Contributed by @pwlmaciejewski 2 | 3 | import chai from 'chai' 4 | import styler from './../source/expanded' 5 | // `react-styling/expanded` export test 6 | // import styler from './../expanded' 7 | 8 | chai.should() 9 | 10 | describe('shorthand property expansion', function() 11 | { 12 | it('should expand margin property', function() 13 | { 14 | const style = styler 15 | ` 16 | can_break_apart_margin: 17 | margin : 1px 2px 18 | ` 19 | 20 | const object = 21 | { 22 | can_break_apart_margin: 23 | { 24 | marginTop : '1px', 25 | marginRight : '2px', 26 | marginBottom : '1px', 27 | marginLeft : '2px' 28 | } 29 | } 30 | 31 | style.should.deep.equal(object) 32 | }) 33 | 34 | it('should expand padding property', function() 35 | { 36 | const style = styler 37 | ` 38 | can_break_apart_padding: 39 | padding : 1px 2px 3px 40 | ` 41 | 42 | const object = 43 | { 44 | can_break_apart_padding: 45 | { 46 | paddingTop : '1px', 47 | paddingRight : '2px', 48 | paddingBottom : '3px', 49 | paddingLeft : '2px' 50 | } 51 | } 52 | 53 | style.should.deep.equal(object) 54 | }) 55 | 56 | it('should expand border property', function() 57 | { 58 | const style = styler 59 | ` 60 | can_break_apart_border: 61 | border : 1px solid black 62 | ` 63 | 64 | const object = 65 | { 66 | can_break_apart_border: 67 | { 68 | borderBottomColor : 'black', 69 | borderBottomStyle : 'solid', 70 | borderBottomWidth : '1px', 71 | borderLeftColor : 'black', 72 | borderLeftStyle : 'solid', 73 | borderLeftWidth : '1px', 74 | borderRightColor : 'black', 75 | borderRightStyle : 'solid', 76 | borderRightWidth : '1px', 77 | borderTopColor : 'black', 78 | borderTopStyle : 'solid', 79 | borderTopWidth : '1px' 80 | } 81 | } 82 | 83 | style.should.deep.equal(object) 84 | }) 85 | }) -------------------------------------------------------------------------------- /test/exports.js: -------------------------------------------------------------------------------- 1 | import 2 | styler, 3 | { 4 | flat, 5 | expanded 6 | } 7 | from '../index.es6' 8 | 9 | describe(`exports`, function() 10 | { 11 | it(`should export ES6`, function() 12 | { 13 | styler('') 14 | flat('') 15 | expanded('') 16 | }) 17 | 18 | it(`should export CommonJS`, function() 19 | { 20 | const Library = require('../index.common') 21 | 22 | Library('') 23 | Library.flat('') 24 | Library.expanded('') 25 | 26 | // Legacy CommonJS exports 27 | require('../flat')('') 28 | require('../expanded')('') 29 | }) 30 | }) -------------------------------------------------------------------------------- /test/flat.js: -------------------------------------------------------------------------------- 1 | import flat_styler from '../source/flat' 2 | 3 | import chai from 'chai' 4 | 5 | chai.should() 6 | 7 | describe('flat styler', function() 8 | { 9 | it('should convert text to JSON object', function() 10 | { 11 | const style = flat_styler 12 | ` 13 | background: red 14 | 15 | menu 16 | list-style-type: none 17 | 18 | .item 19 | display: inline-block 20 | 21 | link 22 | display : inline-block 23 | text-decoration : none 24 | color : #000000 25 | 26 | padding-left : 0.4em 27 | padding-right : 0.4em 28 | padding-top : 0.2em 29 | padding-bottom : 0.2em 30 | 31 | ¤t 32 | color : #ffffff 33 | background-color : #000000 34 | 35 | @media (min-width: 320px) 36 | width: 100% 37 | 38 | :hover 39 | background: white 40 | whatever: 1.3 41 | ` 42 | 43 | const object = 44 | { 45 | background: 'red', 46 | 47 | menu: 48 | { 49 | listStyleType: 'none' 50 | }, 51 | 52 | menu_item: 53 | { 54 | display: 'inline-block' 55 | }, 56 | 57 | menu_item_link: 58 | { 59 | display : 'inline-block', 60 | textDecoration : 'none', 61 | color : '#000000', 62 | paddingLeft : '0.4em', 63 | paddingRight : '0.4em', 64 | paddingTop : '0.2em', 65 | paddingBottom : '0.2em' 66 | }, 67 | 68 | menu_item_link_current: 69 | { 70 | display : 'inline-block', 71 | textDecoration : 'none', 72 | color : '#ffffff', 73 | paddingLeft : '0.4em', 74 | paddingRight : '0.4em', 75 | paddingTop : '0.2em', 76 | paddingBottom : '0.2em', 77 | backgroundColor : '#000000', 78 | 79 | '@media (min-width: 320px)': 80 | { 81 | width: '100%', 82 | 83 | ':hover': 84 | { 85 | background: 'white', 86 | whatever: 1.3 87 | } 88 | } 89 | } 90 | } 91 | 92 | style.should.deep.equal(object) 93 | }) 94 | }) -------------------------------------------------------------------------------- /test/helpers.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai' 2 | import { exists, is_object, extend, merge, clone, convert_from_camel_case, replace_all, starts_with, ends_with, is_empty, not_empty, repeat, is_blank, zip } from '../source/helpers' 3 | 4 | chai.should() 5 | 6 | describe('helpers', function() 7 | { 8 | it('should extend JSON objects', function() 9 | { 10 | const a = 11 | { 12 | a: 13 | { 14 | b: 1 15 | } 16 | } 17 | 18 | const b = 19 | { 20 | a: 21 | { 22 | c: 1 23 | }, 24 | d: 25 | { 26 | e: 2 27 | } 28 | } 29 | 30 | const c = 31 | { 32 | d: 33 | { 34 | e: 3, 35 | f: 4 36 | } 37 | } 38 | 39 | extend(a, b, c) 40 | 41 | b.d.e.should.equal(2) 42 | 43 | const ab = 44 | { 45 | a: 46 | { 47 | b: 1, 48 | c: 1 49 | }, 50 | d: 51 | { 52 | e: 3, 53 | f: 4 54 | } 55 | } 56 | 57 | a.should.deep.equal(ab) 58 | }) 59 | 60 | it('should detect if variable exists', function() 61 | { 62 | exists(0).should.equal(true) 63 | exists('').should.equal(true) 64 | exists(null).should.equal(true) 65 | exists([]).should.equal(true) 66 | exists(undefined).should.equal(false) 67 | }) 68 | 69 | it('should detect JSON objects', function() 70 | { 71 | is_object({}).should.equal(true) 72 | is_object(0).should.equal(false) 73 | is_object('').should.equal(false) 74 | is_object(null).should.equal(false) 75 | is_object([]).should.equal(false) 76 | is_object(undefined).should.equal(false) 77 | }) 78 | 79 | it('should merge objects', function() 80 | { 81 | const a = { b: { c: 1 }} 82 | const b = merge(a, { b: { c: 2 }}) 83 | 84 | a.b.c.should.equal(1) 85 | b.b.c.should.equal(2) 86 | }) 87 | 88 | it('should clone objects', function() 89 | { 90 | const a = { b: { c: 1 }} 91 | const b = clone(a) 92 | 93 | a.b.c = 2 94 | b.b.c.should.equal(1) 95 | }) 96 | 97 | it('should convert from camel case', function() 98 | { 99 | const camel_cased_a = 100 | { 101 | a: 1, 102 | 103 | bCdEf: 104 | { 105 | g_h: true 106 | } 107 | } 108 | 109 | const a = 110 | { 111 | a: 1, 112 | 113 | b_cd_ef: 114 | { 115 | g_h: true 116 | } 117 | } 118 | 119 | convert_from_camel_case(camel_cased_a).should.deep.equal(a) 120 | }) 121 | 122 | it('should replace strings', function() 123 | { 124 | replace_all('Testing \\ string', '\\', '-').should.equal('Testing - string') 125 | }) 126 | 127 | it('should determine if a string starts with a substring', function() 128 | { 129 | starts_with('#$% test', '#').should.equal(true) 130 | starts_with('#$% test', '$').should.equal(false) 131 | }) 132 | 133 | it('should determine if a string ends with a substring', function() 134 | { 135 | ends_with('#$% test !', '!').should.equal(true) 136 | ends_with('#$% test !', '#').should.equal(false) 137 | }) 138 | 139 | it('should determine if an array is (not) empty', function() 140 | { 141 | is_empty([]).should.equal(true) 142 | is_empty([0]).should.equal(false) 143 | 144 | not_empty([]).should.equal(false) 145 | not_empty([0]).should.equal(true) 146 | }) 147 | 148 | it('should repeat strings', function() 149 | { 150 | repeat('abc', 3).should.equal('abcabcabc') 151 | }) 152 | 153 | it('should test if a string is blank', function() 154 | { 155 | is_blank('abc').should.equal(false) 156 | is_blank('').should.equal(true) 157 | is_blank(' ').should.equal(true) 158 | is_blank(' \t\n').should.equal(true) 159 | is_blank(' \t\n a').should.equal(false) 160 | }) 161 | 162 | it('should zip arrays', function() 163 | { 164 | zip([], []).should.deep.equal([]) 165 | zip([1], []).should.deep.equal([[1, undefined]]) 166 | zip([1, 2, 3], [4, 5, 6]).should.deep.equal([[1, 4], [2, 5], [3, 6]]) 167 | }) 168 | }) -------------------------------------------------------------------------------- /test/styling.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai' 2 | import styler from './../source/index' 3 | 4 | chai.should() 5 | 6 | describe('styler', function() 7 | { 8 | it('shouldn\'t allow invalid style class type hierarchy', function() 9 | { 10 | const style_function = (style) => (() => styler(style)) 11 | 12 | style_function 13 | (` 14 | menu 15 | :hover 16 | :active 17 | `) 18 | .should.throw('Pseudoclasses (:hover, etc) can\'t contain child style classes') 19 | 20 | style_function 21 | (` 22 | menu 23 | @media 24 | :active 25 | `) 26 | .should.not.throw('good function') 27 | 28 | style_function 29 | (` 30 | menu 31 | @media 32 | passive 33 | `) 34 | .should.throw('Media query style classes can only contain pseudoclasses (:hover, etc)') 35 | 36 | style_function 37 | (` 38 | menu 39 | @keyframes 40 | 10% 41 | `) 42 | .should.not.throw('good function') 43 | 44 | style_function 45 | (` 46 | menu 47 | @keyframes 48 | whatever 49 | `) 50 | .should.throw('Keyframes style classes can only contain keyframe selectors (from, 100%, etc)') 51 | 52 | }) 53 | 54 | it('should convert text to JSON object', function() 55 | { 56 | const dummy_variable_for_template_string_testing = 'none' 57 | 58 | const style = styler 59 | ` 60 | background: red 61 | 62 | menu 63 | list-style-type: ${dummy_variable_for_template_string_testing} 64 | 65 | item 66 | display: inline-block 67 | 68 | link 69 | display : inline-block 70 | text-decoration : none 71 | color : #000000 72 | 73 | padding-left : 0.4em 74 | padding-right : 0.4em 75 | padding-top : 0.2em 76 | padding-bottom : 0.2em 77 | 78 | ¤t 79 | color : #ffffff 80 | background-color : #000000 81 | 82 | @media (min-width: 320px) 83 | width: 100% 84 | 85 | :hover 86 | background: white 87 | whatever: 1.3 88 | 89 | @keyframes test 90 | 0% 91 | padding: 0px 92 | 100% 93 | padding: 10px 94 | ` 95 | 96 | const object = 97 | { 98 | background: 'red', 99 | 100 | menu: 101 | { 102 | listStyleType: 'none', 103 | 104 | item: 105 | { 106 | display: 'inline-block', 107 | 108 | link: 109 | { 110 | display : 'inline-block', 111 | textDecoration : 'none', 112 | color : '#000000', 113 | paddingLeft : '0.4em', 114 | paddingRight : '0.4em', 115 | paddingTop : '0.2em', 116 | paddingBottom : '0.2em', 117 | 118 | current: 119 | { 120 | display : 'inline-block', 121 | textDecoration : 'none', 122 | color : '#ffffff', 123 | paddingLeft : '0.4em', 124 | paddingRight : '0.4em', 125 | paddingTop : '0.2em', 126 | paddingBottom : '0.2em', 127 | backgroundColor : '#000000', 128 | 129 | '@media (min-width: 320px)': 130 | { 131 | width: '100%', 132 | 133 | ':hover': 134 | { 135 | background: 'white', 136 | whatever: 1.3 137 | } 138 | }, 139 | 140 | '@keyframes test': 141 | { 142 | '0%': 143 | { 144 | padding: '0px' 145 | }, 146 | '100%': 147 | { 148 | padding: '10px' 149 | } 150 | } 151 | } 152 | } 153 | } 154 | } 155 | } 156 | 157 | style.should.deep.equal(object) 158 | }) 159 | 160 | it('should work with curly braces', function() 161 | { 162 | const style = styler 163 | ` 164 | menu { 165 | list-style-type: none 166 | 167 | item 168 | { 169 | display: inline-block 170 | 171 | link { 172 | display : inline-block 173 | text-decoration : none 174 | color : #000000 175 | 176 | padding-left : 0.4em 177 | padding-right : 0.4em 178 | padding-top : 0.2em 179 | padding-bottom : 0.2em 180 | 181 | ¤t 182 | { 183 | color : #ffffff 184 | background-color : #000000 185 | } 186 | } 187 | } 188 | } 189 | ` 190 | 191 | const object = 192 | { 193 | menu: 194 | { 195 | listStyleType: 'none', 196 | 197 | item: 198 | { 199 | display: 'inline-block', 200 | 201 | link: 202 | { 203 | display : 'inline-block', 204 | textDecoration : 'none', 205 | color : '#000000', 206 | paddingLeft : '0.4em', 207 | paddingRight : '0.4em', 208 | paddingTop : '0.2em', 209 | paddingBottom : '0.2em', 210 | 211 | current: 212 | { 213 | display : 'inline-block', 214 | textDecoration : 'none', 215 | color : '#ffffff', 216 | backgroundColor : '#000000', 217 | paddingLeft : '0.4em', 218 | paddingRight : '0.4em', 219 | paddingTop : '0.2em', 220 | paddingBottom : '0.2em' 221 | } 222 | } 223 | } 224 | } 225 | } 226 | 227 | style.should.deep.equal(object) 228 | }) 229 | 230 | it('should support YAML-alike syntax', function() 231 | { 232 | const style = styler 233 | ` 234 | menu: 235 | list-style-type: none 236 | 237 | item: 238 | display: inline-block 239 | 240 | link: 241 | display : inline-block 242 | text-decoration : none 243 | color : #000000 244 | 245 | padding-left : 0.4em 246 | padding-right : 0.4em 247 | padding-top : 0.2em 248 | padding-bottom : 0.2em 249 | 250 | ¤t: 251 | color : #ffffff 252 | background-color : #000000 253 | ` 254 | 255 | const object = 256 | { 257 | menu: 258 | { 259 | listStyleType: 'none', 260 | 261 | item: 262 | { 263 | display: 'inline-block', 264 | 265 | link: 266 | { 267 | display : 'inline-block', 268 | textDecoration : 'none', 269 | color : '#000000', 270 | paddingLeft : '0.4em', 271 | paddingRight : '0.4em', 272 | paddingTop : '0.2em', 273 | paddingBottom : '0.2em', 274 | 275 | current: 276 | { 277 | display : 'inline-block', 278 | textDecoration : 'none', 279 | color : '#ffffff', 280 | backgroundColor : '#000000', 281 | paddingLeft : '0.4em', 282 | paddingRight : '0.4em', 283 | paddingTop : '0.2em', 284 | paddingBottom : '0.2em' 285 | } 286 | } 287 | } 288 | } 289 | } 290 | 291 | style.should.deep.equal(object) 292 | }) 293 | 294 | it('should work with spaces instead of tabs', function() 295 | { 296 | const style = styler 297 | ` 298 | menu 299 | list-style-type: none 300 | 301 | item 302 | display: inline-block 303 | 304 | link 305 | display : inline-block 306 | text-decoration : none 307 | color : #000000 308 | 309 | padding-left : 0.4em 310 | padding-right : 0.4em 311 | padding-top : 0.2em 312 | padding-bottom : 0.2em 313 | 314 | ¤t 315 | color : #ffffff 316 | background-color : #000000 317 | ` 318 | 319 | const object = 320 | { 321 | menu: 322 | { 323 | listStyleType: 'none', 324 | 325 | item: 326 | { 327 | display: 'inline-block', 328 | 329 | link: 330 | { 331 | display : 'inline-block', 332 | textDecoration : 'none', 333 | color : '#000000', 334 | paddingLeft : '0.4em', 335 | paddingRight : '0.4em', 336 | paddingTop : '0.2em', 337 | paddingBottom : '0.2em', 338 | 339 | current: 340 | { 341 | display : 'inline-block', 342 | textDecoration : 'none', 343 | color : '#ffffff', 344 | backgroundColor : '#000000', 345 | paddingLeft : '0.4em', 346 | paddingRight : '0.4em', 347 | paddingTop : '0.2em', 348 | paddingBottom : '0.2em' 349 | } 350 | } 351 | } 352 | } 353 | } 354 | 355 | style.should.deep.equal(object) 356 | }) 357 | 358 | it('should add initial tabulation', function() 359 | { 360 | const style = styler 361 | ` 362 | menu 363 | list-style-type: none 364 | 365 | item 366 | display: inline-block 367 | 368 | link 369 | display : inline-block 370 | text-decoration : none 371 | color : #000000 372 | 373 | padding-left : 0.4em 374 | padding-right : 0.4em 375 | padding-top : 0.2em 376 | padding-bottom : 0.2em 377 | 378 | ¤t 379 | color : #ffffff 380 | background-color : #000000 381 | ` 382 | 383 | const object = 384 | { 385 | menu: 386 | { 387 | listStyleType: 'none', 388 | 389 | item: 390 | { 391 | display: 'inline-block', 392 | 393 | link: 394 | { 395 | display : 'inline-block', 396 | textDecoration : 'none', 397 | color : '#000000', 398 | paddingLeft : '0.4em', 399 | paddingRight : '0.4em', 400 | paddingTop : '0.2em', 401 | paddingBottom : '0.2em', 402 | 403 | current: 404 | { 405 | display : 'inline-block', 406 | textDecoration : 'none', 407 | color : '#ffffff', 408 | backgroundColor : '#000000', 409 | paddingLeft : '0.4em', 410 | paddingRight : '0.4em', 411 | paddingTop : '0.2em', 412 | paddingBottom : '0.2em' 413 | } 414 | } 415 | } 416 | } 417 | } 418 | 419 | style.should.deep.equal(object) 420 | }) 421 | 422 | it('should support comments', function() 423 | { 424 | const style = styler 425 | ` 426 | menu 427 | list-style-type: none 428 | 429 | // notice the dot character here. 430 | // this is called a "modifier" class 431 | // (see the explanation of this term below) 432 | &one 433 | // inline-block here 434 | display: inline-block 435 | 436 | /* 437 | multi 438 | line 439 | comment 440 | */ 441 | two 442 | display : block 443 | text-decoration : none 444 | color : #000000 445 | // single line comments may only start from the beginning of the line 446 | // so that urls in background-images and such won't get broken 447 | background-image : url(http://xhamster.com/) 448 | ` 449 | 450 | const object = 451 | { 452 | menu: 453 | { 454 | listStyleType: 'none', 455 | 456 | one: 457 | { 458 | listStyleType : 'none', 459 | display : 'inline-block', 460 | 461 | two: 462 | { 463 | display : 'block', 464 | textDecoration : 'none', 465 | color : '#000000', 466 | backgroundImage : 'url(http://xhamster.com/)' 467 | } 468 | } 469 | } 470 | } 471 | 472 | style.should.deep.equal(object) 473 | }) 474 | 475 | it('should support nested modifiers', function() 476 | { 477 | const style = styler 478 | ` 479 | menu 480 | list-style-type: none 481 | 482 | // notice the dot character here. 483 | // this is called a "modifier" class 484 | // (see the explanation of this term below) 485 | &one 486 | display: inline-block 487 | background: none 488 | 489 | // this is a "modifier" class 490 | &two 491 | display : block 492 | text-decoration : none 493 | color : #000000 494 | 495 | // this is a "modifier" class 496 | &three 497 | color : #ffffff 498 | ` 499 | 500 | const object = 501 | { 502 | menu: 503 | { 504 | listStyleType: 'none', 505 | 506 | one: 507 | { 508 | listStyleType : 'none', 509 | display : 'inline-block', 510 | background : 'none', 511 | 512 | two: 513 | { 514 | listStyleType : 'none', 515 | display : 'block', 516 | textDecoration : 'none', 517 | color : '#000000', 518 | background : 'none' 519 | }, 520 | 521 | three: 522 | { 523 | listStyleType : 'none', 524 | display : 'inline-block', 525 | color : '#ffffff', 526 | background : 'none' 527 | } 528 | } 529 | } 530 | } 531 | 532 | style.should.deep.equal(object) 533 | }) 534 | 535 | it('should support old school CSS syntax', function() 536 | { 537 | const style = styler` 538 | .old-school-regular-css-syntax { 539 | box-sizing: border-box; 540 | 541 | &.modifier { 542 | border-width: 1px; 543 | } 544 | } 545 | 546 | .blah { 547 | border: none; 548 | color: white; 549 | 550 | .nested { 551 | color: black; 552 | } 553 | } 554 | ` 555 | 556 | const object = 557 | { 558 | 'old-school-regular-css-syntax': 559 | { 560 | boxSizing: 'border-box', 561 | 562 | modifier: 563 | { 564 | boxSizing: 'border-box', 565 | borderWidth: '1px' 566 | } 567 | }, 568 | 569 | blah: 570 | { 571 | border: 'none', 572 | color: 'white', 573 | 574 | nested: 575 | { 576 | color: 'black' 577 | } 578 | } 579 | } 580 | 581 | style.should.deep.equal(object) 582 | }) 583 | 584 | it('should preserve pseudo-classes and media queries in modifiers', function() 585 | { 586 | const style = styler 587 | ` 588 | droids 589 | padding : 0 590 | 591 | droid 592 | border-color : black 593 | border-style : solid 594 | border-width : 1px 1px 0 1px 595 | cursor : pointer 596 | list-style : none 597 | padding : 12px 598 | 599 | :hover 600 | background : #eee 601 | 602 | @media-query-test 603 | box-sizing : border-box 604 | 605 | &@non-media-query-test 606 | background : black 607 | 608 | &:this_class_wont_be_copied_into_modifiers_because_it_is_itself_a_modifier 609 | background : transparent 610 | 611 | &:first 612 | border-radius : 12px 12px 0 0 613 | 614 | &:last 615 | border-radius : 0 0 12px 12px 616 | border-width : 1px 617 | ` 618 | 619 | const object = 620 | { 621 | droids: 622 | { 623 | padding: 0, 624 | 625 | droid: 626 | { 627 | borderColor : 'black', 628 | borderStyle : 'solid', 629 | borderWidth : '1px 1px 0 1px', 630 | cursor : 'pointer', 631 | listStyle : 'none', 632 | padding : '12px', 633 | 634 | ':hover': 635 | { 636 | background : '#eee' 637 | }, 638 | 639 | '@media-query-test': 640 | { 641 | boxSizing : 'border-box' 642 | }, 643 | 644 | '@non-media-query-test': 645 | { 646 | borderColor : 'black', 647 | borderStyle : 'solid', 648 | borderWidth : '1px 1px 0 1px', 649 | cursor : 'pointer', 650 | listStyle : 'none', 651 | padding : '12px', 652 | 653 | background : 'black', 654 | 655 | ':hover': 656 | { 657 | background : '#eee' 658 | }, 659 | 660 | '@media-query-test': 661 | { 662 | boxSizing : 'border-box' 663 | } 664 | }, 665 | 666 | ':this_class_wont_be_copied_into_modifiers_because_it_is_itself_a_modifier': 667 | { 668 | borderColor : 'black', 669 | borderStyle : 'solid', 670 | borderWidth : '1px 1px 0 1px', 671 | cursor : 'pointer', 672 | listStyle : 'none', 673 | padding : '12px', 674 | 675 | background : 'transparent', 676 | 677 | ':hover': 678 | { 679 | background : '#eee' 680 | }, 681 | 682 | '@media-query-test': 683 | { 684 | boxSizing : 'border-box' 685 | } 686 | }, 687 | 688 | ':first': 689 | { 690 | borderColor : 'black', 691 | borderStyle : 'solid', 692 | borderWidth : '1px 1px 0 1px', 693 | cursor : 'pointer', 694 | listStyle : 'none', 695 | padding : '12px', 696 | 697 | borderRadius : '12px 12px 0 0', 698 | 699 | ':hover': 700 | { 701 | background : '#eee' 702 | }, 703 | 704 | '@media-query-test': 705 | { 706 | boxSizing : 'border-box' 707 | } 708 | }, 709 | 710 | ':last': 711 | { 712 | borderColor : 'black', 713 | borderStyle : 'solid', 714 | cursor : 'pointer', 715 | listStyle : 'none', 716 | padding : '12px', 717 | 718 | borderRadius : '0 0 12px 12px', 719 | borderWidth : '1px', 720 | 721 | ':hover': 722 | { 723 | background : '#eee' 724 | }, 725 | 726 | '@media-query-test': 727 | { 728 | boxSizing : 'border-box' 729 | } 730 | } 731 | } 732 | } 733 | } 734 | 735 | style.should.deep.equal(object) 736 | }) 737 | 738 | it('should extend modifiers\' subclasses with the corresponding styles from the original style tree node', function() 739 | { 740 | const style = styler 741 | ` 742 | original 743 | display : inline-block 744 | 745 | item 746 | border : none 747 | color : black 748 | 749 | item_link 750 | text-decoration : none 751 | 752 | :hover 753 | font-weight: bold 754 | 755 | &active 756 | item 757 | color : white 758 | background : black 759 | 760 | item_link 761 | border : 1px 762 | ` 763 | 764 | const object = 765 | { 766 | original: 767 | { 768 | display: 'inline-block', 769 | 770 | item: 771 | { 772 | border : 'none', 773 | color : 'black', 774 | 775 | item_link: 776 | { 777 | textDecoration : 'none', 778 | 779 | ':hover': 780 | { 781 | fontWeight: 'bold' 782 | } 783 | } 784 | }, 785 | 786 | active: 787 | { 788 | display: 'inline-block', 789 | 790 | item: 791 | { 792 | border : 'none', 793 | color : 'white', 794 | background : 'black', 795 | 796 | item_link: 797 | { 798 | textDecoration : 'none', 799 | border : '1px', 800 | 801 | ':hover': 802 | { 803 | fontWeight: 'bold' 804 | } 805 | } 806 | } 807 | } 808 | } 809 | } 810 | 811 | style.should.deep.equal(object) 812 | }) 813 | 814 | it('should support comma separated styles', function() 815 | { 816 | 817 | const style = styler 818 | ` 819 | can_style, multiple_classes, at_once 820 | font-family : Sans 821 | 822 | child 823 | display : none 824 | 825 | can_style 826 | font-size : 12pt 827 | 828 | multiple_classes, at_once 829 | font-size : 8pt 830 | 831 | &modifier 832 | color : red 833 | ` 834 | 835 | const object = 836 | { 837 | can_style: 838 | { 839 | fontFamily : 'Sans', 840 | fontSize : '12pt', 841 | 842 | child: 843 | { 844 | display : 'none' 845 | } 846 | }, 847 | 848 | multiple_classes: 849 | { 850 | fontFamily : 'Sans', 851 | fontSize : '8pt', 852 | 853 | child: 854 | { 855 | display : 'none' 856 | }, 857 | 858 | modifier: 859 | { 860 | fontFamily : 'Sans', 861 | fontSize : '8pt', 862 | color : 'red' 863 | } 864 | }, 865 | 866 | at_once: 867 | { 868 | fontFamily : 'Sans', 869 | fontSize : '8pt', 870 | 871 | child: 872 | { 873 | display : 'none' 874 | }, 875 | 876 | modifier: 877 | { 878 | fontFamily : 'Sans', 879 | fontSize : '8pt', 880 | color : 'red' 881 | } 882 | } 883 | } 884 | 885 | style.should.deep.equal(object) 886 | }) 887 | }) -------------------------------------------------------------------------------- /test/tabulator.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai' 2 | import Tabulator from './../source/tabulator' 3 | 4 | chai.should() 5 | 6 | function process_style(text) 7 | { 8 | const lines = text.split('\n') 9 | const tabulator = new Tabulator(Tabulator.determine_tabulation(lines)) 10 | tabulator.extract_tabulation(lines) 11 | } 12 | 13 | describe('tabulator', function() 14 | { 15 | it('should fail on extra spaces', function() 16 | { 17 | const style = 18 | ` 19 | menu 20 | list-style-type: none 21 | 22 | item 23 | display: inline-block 24 | 25 | link 26 | display : inline-block 27 | text-decoration : none 28 | color : #000000 29 | 30 | padding-left : 0.4em 31 | padding-right : 0.4em 32 | padding-top : 0.2em 33 | padding-bottom : 0.2em 34 | 35 | ¤t 36 | color : #ffffff 37 | background-color : #000000 38 | ` 39 | 40 | const test = () => process_style(style) 41 | test.should.throw('Invalid indentation (extra leading spaces)') 42 | }) 43 | 44 | it('should fail on tabs used when indenting with spaces', function() 45 | { 46 | const style = 47 | ` 48 | menu 49 | list-style-type: none 50 | 51 | item 52 | display: inline-block 53 | 54 | link 55 | display : inline-block 56 | text-decoration : none 57 | color : #000000 58 | 59 | padding-left : 0.4em 60 | padding-right : 0.4em 61 | padding-top : 0.2em 62 | padding-bottom : 0.2em 63 | 64 | ¤t 65 | color : #ffffff 66 | background-color : #000000 67 | ` 68 | 69 | const test = () => process_style(style) 70 | test.should.throw('mixed tabs and spaces') 71 | }) 72 | 73 | it('should fail on spaces used when indenting with tabs', function() 74 | { 75 | const style = 76 | ` 77 | menu 78 | list-style-type: none 79 | ` 80 | 81 | const test = () => process_style(style) 82 | test.should.throw('mixed tabs and spaces') 83 | }) 84 | 85 | it('should fail on messed up indentation levels', function() 86 | { 87 | const style = 88 | ` 89 | menu 90 | list-style-type: none 91 | 92 | item 93 | display: inline-block 94 | 95 | link 96 | display : inline-block 97 | text-decoration : none 98 | color : #000000 99 | 100 | padding-left : 0.4em 101 | padding-right : 0.4em 102 | padding-top : 0.2em 103 | padding-bottom : 0.2em 104 | 105 | ¤t 106 | color : #ffffff 107 | background-color : #000000 108 | ` 109 | 110 | const test = () => process_style(style) 111 | test.should.throw('Invalid indentation at line') 112 | }) 113 | }) --------------------------------------------------------------------------------