├── test ├── non-class.expected.json ├── non-class.noscope.expected.json ├── basic.expected.json ├── basic.noscope.expected.json ├── media-query.expected.json ├── media-query.noscope.expected.json ├── keyframes-with-decimal.expected.json ├── keyframes-with-decimal.noscope.expected.json ├── non-class.expected.css ├── basic.expected.css ├── basic.noscope.expected.css ├── not-pseudo.noscope.expected.json ├── comments.expected.json ├── keyframes-usage.noscope.expected.json ├── non-class.noscope.expected.css ├── not-pseudo.expected.json ├── comments.noscope.expected.json ├── keyframes-usage.expected.json ├── animation-as-pseudo.noscope.expected.json ├── dot-in-values.noscope.expected.json ├── animation-as-pseudo.expected.json ├── dot-in-values.expected.json ├── multiple-extensions.noscope.expected.json ├── keyframes.noscope.expected.json ├── basic.source.js ├── extensions.noscope.expected.json ├── non-class.source.js ├── dot-in-values.noscope.expected.css ├── keyframes.expected.json ├── multiple-extensions.expected.json ├── not-pseudo.noscope.expected.css ├── basic.noscope.source.js ├── non-class.noscope.source.js ├── not-pseudo.expected.css ├── dot-in-values.expected.css ├── extends-in-media-query.noscope.expected.json ├── keyframes-usage.noscope.expected.css ├── keyframes-with-decimal.expected.css ├── keyframes-with-decimal.noscope.expected.css ├── extensions.expected.json ├── media-query.noscope.expected.css ├── keyframes-usage.expected.css ├── media-query.expected.css ├── multiple-extensions.noscope.expected.css ├── dot-in-values.source.js ├── extends-in-media-query.expected.json ├── not-pseudo.source.js ├── multiple-extensions.expected.css ├── dot-in-values.noscope.source.js ├── not-pseudo.noscope.source.js ├── keyframes-with-decimal.source.js ├── media-query.source.js ├── extensions.noscope.expected.css ├── keyframes-with-decimal.noscope.source.js ├── media-query.noscope.source.js ├── animation-as-pseudo.noscope.expected.css ├── animation-as-pseudo.expected.css ├── extensions.expected.css ├── keyframes-usage.source.js ├── animation-as-pseudo.source.js ├── animation-as-pseudo.noscope.source.js ├── keyframes-usage.noscope.source.js ├── extensions.source.js ├── extensions.noscope.source.js ├── keyframes.noscope.expected.css ├── multiple-extensions.source.js ├── keyframes.expected.css ├── multiple-extensions.noscope.source.js ├── keyframes.source.js ├── keyframes.noscope.source.js ├── extends-in-media-query.noscope.expected.css ├── extends-in-media-query.expected.css ├── extends-in-media-query.source.js ├── extends-in-media-query.noscope.source.js ├── comments.noscope.expected.css ├── comments.expected.css ├── comments.source.js ├── comments.source.noscope.js └── index.js ├── .gitignore ├── csjs.js ├── get-css.js ├── .istanbul.yml ├── lib ├── get-css.js ├── css-key.js ├── scoped-name.js ├── hash-string.js ├── regex.js ├── base62-encode.js ├── extract-exports.js ├── replace-animations.js ├── scopeify.js ├── build-exports.js ├── css-extract-extends.js ├── composition.js └── csjs.js ├── index.js ├── .zuul.yml ├── docs ├── automatic-vendor-prefixing.md ├── comments.md └── keyframe-animations.md ├── LICENSE ├── package.json ├── .travis.yml └── README.md /test/non-class.expected.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /test/non-class.noscope.expected.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage 2 | node_modules 3 | npm-debug.log 4 | -------------------------------------------------------------------------------- /test/basic.expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "foo": "foo_4xZqpC" 3 | } 4 | -------------------------------------------------------------------------------- /test/basic.noscope.expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "foo": "foo" 3 | } 4 | -------------------------------------------------------------------------------- /test/media-query.expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "foo": "foo_nYfJE" 3 | } 4 | -------------------------------------------------------------------------------- /test/media-query.noscope.expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "foo": "foo" 3 | } 4 | -------------------------------------------------------------------------------- /csjs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = require('./lib/csjs'); 4 | -------------------------------------------------------------------------------- /get-css.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = require('./lib/get-css'); 4 | -------------------------------------------------------------------------------- /test/keyframes-with-decimal.expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "woot": "woot_3xZGg2" 3 | } 4 | -------------------------------------------------------------------------------- /test/keyframes-with-decimal.noscope.expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "woot": "woot" 3 | } 4 | -------------------------------------------------------------------------------- /test/non-class.expected.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | #foo { 4 | color: red; 5 | } 6 | 7 | -------------------------------------------------------------------------------- /test/basic.expected.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | .foo_4xZqpC { 4 | color: red; 5 | } 6 | 7 | -------------------------------------------------------------------------------- /test/basic.noscope.expected.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | .foo { 4 | color: red; 5 | } 6 | 7 | -------------------------------------------------------------------------------- /test/not-pseudo.noscope.expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "foo": "foo", 3 | "bar": "bar" 4 | } 5 | -------------------------------------------------------------------------------- /test/comments.expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "foo": "foo_1LMZVZ", 3 | "wow": "wow_1LMZVZ" 4 | } 5 | -------------------------------------------------------------------------------- /test/keyframes-usage.noscope.expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "boo": "boo", 3 | "woo": "woo" 4 | } 5 | -------------------------------------------------------------------------------- /test/non-class.noscope.expected.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | #foo { 4 | color: red; 5 | } 6 | 7 | -------------------------------------------------------------------------------- /test/not-pseudo.expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "foo": "foo_25D7El", 3 | "bar": "bar_25D7El" 4 | } 5 | -------------------------------------------------------------------------------- /test/comments.noscope.expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "foo": "foo_1LMZVZ", 3 | "wow": "wow_1LMZVZ" 4 | } 5 | -------------------------------------------------------------------------------- /test/keyframes-usage.expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "boo": "boo_2MFmAf", 3 | "woo": "woo_2MFmAf" 4 | } 5 | -------------------------------------------------------------------------------- /.istanbul.yml: -------------------------------------------------------------------------------- 1 | instrumentation: 2 | excludes: 3 | - 'lib/base62-encode.js' 4 | - 'lib/hash-string.js' 5 | -------------------------------------------------------------------------------- /test/animation-as-pseudo.noscope.expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "animation": "animation", 3 | "hover": "hover" 4 | } 5 | -------------------------------------------------------------------------------- /test/dot-in-values.noscope.expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "foo": "foo", 3 | "baz": "baz", 4 | "bar": "bar" 5 | } 6 | -------------------------------------------------------------------------------- /test/animation-as-pseudo.expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "animation": "animation_zz7qS", 3 | "hover": "hover_zz7qS" 4 | } 5 | -------------------------------------------------------------------------------- /test/dot-in-values.expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "foo": "foo_1Veq6n", 3 | "baz": "baz_1Veq6n", 4 | "bar": "bar_1Veq6n" 5 | } 6 | -------------------------------------------------------------------------------- /test/multiple-extensions.noscope.expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "lol": "lol", 3 | "baz": "baz lol bar foo", 4 | "fob": "fob foo" 5 | } 6 | -------------------------------------------------------------------------------- /test/keyframes.noscope.expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "yolo": "yolo", 3 | "yoloYolo": "yoloYolo", 4 | "foo": "foo", 5 | "bar": "bar" 6 | } 7 | -------------------------------------------------------------------------------- /test/basic.source.js: -------------------------------------------------------------------------------- 1 | const csjs = require('../'); 2 | 3 | module.exports = csjs` 4 | 5 | .foo { 6 | color: red; 7 | } 8 | 9 | `; 10 | -------------------------------------------------------------------------------- /test/extensions.noscope.expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "foo": "foo", 3 | "bar": "bar foo", 4 | "baz": "baz bar foo", 5 | "bazz": "bazz bar foo" 6 | } 7 | -------------------------------------------------------------------------------- /test/non-class.source.js: -------------------------------------------------------------------------------- 1 | const csjs = require('../'); 2 | 3 | module.exports = csjs` 4 | 5 | #foo { 6 | color: red; 7 | } 8 | 9 | `; 10 | -------------------------------------------------------------------------------- /lib/get-css.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var cssKey = require('./css-key'); 4 | 5 | module.exports = function getCss(csjs) { 6 | return csjs[cssKey]; 7 | }; 8 | -------------------------------------------------------------------------------- /test/dot-in-values.noscope.expected.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | .foo { 4 | font-size: 1.3em; 5 | } 6 | 7 | .bar { font-size: 12.5px; } .baz { width: 33.3% } 8 | 9 | -------------------------------------------------------------------------------- /test/keyframes.expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "yolo": "yolo_4lrRRM", 3 | "yoloYolo": "yoloYolo_4lrRRM", 4 | "foo": "foo_4lrRRM", 5 | "bar": "bar_4lrRRM" 6 | } 7 | -------------------------------------------------------------------------------- /test/multiple-extensions.expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "lol": "lol_IZbgA", 3 | "baz": "baz_IZbgA lol_IZbgA bar_g7Bft foo_g7Bft", 4 | "fob": "fob_IZbgA foo_g7Bft" 5 | } 6 | -------------------------------------------------------------------------------- /test/not-pseudo.noscope.expected.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | @media screen and (min-width: 769px) { 4 | .foo:not(.bar) { 5 | display: flex; 6 | } 7 | } 8 | 9 | -------------------------------------------------------------------------------- /test/basic.noscope.source.js: -------------------------------------------------------------------------------- 1 | const csjs = require('../').noScope; 2 | 3 | module.exports = csjs` 4 | 5 | .foo { 6 | color: red; 7 | } 8 | 9 | `; 10 | -------------------------------------------------------------------------------- /test/non-class.noscope.source.js: -------------------------------------------------------------------------------- 1 | const csjs = require('../').noScope; 2 | 3 | module.exports = csjs` 4 | 5 | #foo { 6 | color: red; 7 | } 8 | 9 | `; 10 | -------------------------------------------------------------------------------- /test/not-pseudo.expected.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | @media screen and (min-width: 769px) { 4 | .foo_25D7El:not(.bar_25D7El) { 5 | display: flex; 6 | } 7 | } 8 | 9 | -------------------------------------------------------------------------------- /test/dot-in-values.expected.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | .foo_1Veq6n { 4 | font-size: 1.3em; 5 | } 6 | 7 | .bar_1Veq6n { font-size: 12.5px; } .baz_1Veq6n { width: 33.3% } 8 | 9 | -------------------------------------------------------------------------------- /test/extends-in-media-query.noscope.expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "hello": "hello", 3 | "underline": "underline", 4 | "woot": "woot foo underline hello yay", 5 | "yay": "yay" 6 | } 7 | -------------------------------------------------------------------------------- /lib/css-key.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * CSS identifiers with whitespace are invalid 5 | * Hence this key will not cause a collision 6 | */ 7 | 8 | module.exports = ' css '; 9 | -------------------------------------------------------------------------------- /test/keyframes-usage.noscope.expected.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | .woo { 4 | animation: yolo 5s infinite; 5 | } 6 | 7 | .boo { 8 | animation-name: yoloYolo; 9 | } 10 | 11 | -------------------------------------------------------------------------------- /test/keyframes-with-decimal.expected.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | @keyframes woot_3xZGg2 { 4 | 0% { opacity: 0; } 5 | 33.3% { opacity: 0.333; } 6 | 100% { opacity: 1; } 7 | } 8 | 9 | -------------------------------------------------------------------------------- /test/keyframes-with-decimal.noscope.expected.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | @keyframes woot { 4 | 0% { opacity: 0; } 5 | 33.3% { opacity: 0.333; } 6 | 100% { opacity: 1; } 7 | } 8 | 9 | -------------------------------------------------------------------------------- /test/extensions.expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "foo": "foo_g7Bft", 3 | "bar": "bar_g7Bft foo_g7Bft", 4 | "baz": "baz_g7Bft bar_g7Bft foo_g7Bft", 5 | "bazz": "bazz_g7Bft bar_g7Bft foo_g7Bft" 6 | } 7 | -------------------------------------------------------------------------------- /test/media-query.noscope.expected.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | .foo { 4 | color: red; 5 | } 6 | 7 | @media (max-width: 480px) { 8 | .foo { 9 | color: blue; 10 | } 11 | } 12 | 13 | -------------------------------------------------------------------------------- /test/keyframes-usage.expected.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | .woo_2MFmAf { 4 | animation: yolo_4lrRRM 5s infinite; 5 | } 6 | 7 | .boo_2MFmAf { 8 | animation-name: yoloYolo_4lrRRM; 9 | } 10 | 11 | -------------------------------------------------------------------------------- /test/media-query.expected.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | .foo_nYfJE { 4 | color: red; 5 | } 6 | 7 | @media (max-width: 480px) { 8 | .foo_nYfJE { 9 | color: blue; 10 | } 11 | } 12 | 13 | -------------------------------------------------------------------------------- /test/multiple-extensions.noscope.expected.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | .lol { 4 | font-family: serif; 5 | } 6 | 7 | .baz { 8 | font-size: 12px; 9 | } 10 | 11 | .fob { 12 | font-weight: 500; 13 | } 14 | 15 | -------------------------------------------------------------------------------- /test/dot-in-values.source.js: -------------------------------------------------------------------------------- 1 | const csjs = require('../'); 2 | 3 | module.exports = csjs` 4 | 5 | .foo { 6 | font-size: 1.3em; 7 | } 8 | 9 | .bar { font-size: 12.5px; } .baz { width: 33.3% } 10 | 11 | `; 12 | -------------------------------------------------------------------------------- /test/extends-in-media-query.expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "hello": "hello_2XWyPW", 3 | "underline": "underline_2XWyPW", 4 | "woot": "woot_2XWyPW foo_4xZqpC underline_2XWyPW hello_2XWyPW yay_2XWyPW", 5 | "yay": "yay_2XWyPW" 6 | } 7 | -------------------------------------------------------------------------------- /test/not-pseudo.source.js: -------------------------------------------------------------------------------- 1 | const csjs = require('../'); 2 | 3 | module.exports = csjs` 4 | 5 | @media screen and (min-width: 769px) { 6 | .foo:not(.bar) { 7 | display: flex; 8 | } 9 | } 10 | 11 | `; 12 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var csjs = require('./csjs'); 4 | 5 | module.exports = csjs(); 6 | module.exports.csjs = csjs; 7 | module.exports.noScope = csjs({ noscope: true }); 8 | module.exports.getCss = require('./get-css'); 9 | -------------------------------------------------------------------------------- /test/multiple-extensions.expected.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | .lol_IZbgA { 4 | font-family: serif; 5 | } 6 | 7 | .baz_IZbgA { 8 | font-size: 12px; 9 | } 10 | 11 | .fob_IZbgA { 12 | font-weight: 500; 13 | } 14 | 15 | -------------------------------------------------------------------------------- /test/dot-in-values.noscope.source.js: -------------------------------------------------------------------------------- 1 | const csjs = require('../').noScope; 2 | 3 | module.exports = csjs` 4 | 5 | .foo { 6 | font-size: 1.3em; 7 | } 8 | 9 | .bar { font-size: 12.5px; } .baz { width: 33.3% } 10 | 11 | `; 12 | -------------------------------------------------------------------------------- /test/not-pseudo.noscope.source.js: -------------------------------------------------------------------------------- 1 | const csjs = require('../').noScope; 2 | 3 | module.exports = csjs` 4 | 5 | @media screen and (min-width: 769px) { 6 | .foo:not(.bar) { 7 | display: flex; 8 | } 9 | } 10 | 11 | `; 12 | -------------------------------------------------------------------------------- /test/keyframes-with-decimal.source.js: -------------------------------------------------------------------------------- 1 | const csjs = require('../'); 2 | 3 | module.exports = csjs` 4 | 5 | @keyframes woot { 6 | 0% { opacity: 0; } 7 | 33.3% { opacity: 0.333; } 8 | 100% { opacity: 1; } 9 | } 10 | 11 | `; 12 | -------------------------------------------------------------------------------- /test/media-query.source.js: -------------------------------------------------------------------------------- 1 | const csjs = require('../'); 2 | 3 | module.exports = csjs` 4 | 5 | .foo { 6 | color: red; 7 | } 8 | 9 | @media (max-width: 480px) { 10 | .foo { 11 | color: blue; 12 | } 13 | } 14 | 15 | `; 16 | -------------------------------------------------------------------------------- /test/extensions.noscope.expected.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | .foo { 4 | color: red; 5 | } 6 | 7 | .bar { 8 | background: blue; 9 | } 10 | 11 | .baz { 12 | text-transform: uppercase; 13 | } 14 | 15 | .bazz { 16 | color: white; 17 | } 18 | 19 | -------------------------------------------------------------------------------- /test/keyframes-with-decimal.noscope.source.js: -------------------------------------------------------------------------------- 1 | const csjs = require('../').noScope; 2 | 3 | module.exports = csjs` 4 | 5 | @keyframes woot { 6 | 0% { opacity: 0; } 7 | 33.3% { opacity: 0.333; } 8 | 100% { opacity: 1; } 9 | } 10 | 11 | `; 12 | -------------------------------------------------------------------------------- /test/media-query.noscope.source.js: -------------------------------------------------------------------------------- 1 | const csjs = require('../').noScope; 2 | 3 | module.exports = csjs` 4 | 5 | .foo { 6 | color: red; 7 | } 8 | 9 | @media (max-width: 480px) { 10 | .foo { 11 | color: blue; 12 | } 13 | } 14 | 15 | `; 16 | -------------------------------------------------------------------------------- /test/animation-as-pseudo.noscope.expected.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | @keyframes hover { 4 | 0% { opacity: 0.0; } 5 | 100% { opacity: 0.5; } 6 | } 7 | 8 | @media (max-width: 480px) { 9 | .animation:hover { 10 | background: green; 11 | } 12 | } 13 | 14 | -------------------------------------------------------------------------------- /test/animation-as-pseudo.expected.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | @keyframes hover_zz7qS { 4 | 0% { opacity: 0.0; } 5 | 100% { opacity: 0.5; } 6 | } 7 | 8 | @media (max-width: 480px) { 9 | .animation_zz7qS:hover { 10 | background: green; 11 | } 12 | } 13 | 14 | -------------------------------------------------------------------------------- /test/extensions.expected.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | .foo_g7Bft { 4 | color: red; 5 | } 6 | 7 | .bar_g7Bft { 8 | background: blue; 9 | } 10 | 11 | .baz_g7Bft { 12 | text-transform: uppercase; 13 | } 14 | 15 | .bazz_g7Bft { 16 | color: white; 17 | } 18 | 19 | -------------------------------------------------------------------------------- /test/keyframes-usage.source.js: -------------------------------------------------------------------------------- 1 | const csjs = require('../'); 2 | 3 | const keyframes = require('./keyframes.source'); 4 | 5 | module.exports = csjs` 6 | 7 | .woo { 8 | animation: ${keyframes.yolo} 5s infinite; 9 | } 10 | 11 | .boo { 12 | animation-name: ${keyframes.yoloYolo}; 13 | } 14 | 15 | `; 16 | -------------------------------------------------------------------------------- /lib/scoped-name.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var encode = require('./base62-encode'); 4 | var hash = require('./hash-string'); 5 | 6 | module.exports = function fileScoper(fileSrc) { 7 | var suffix = encode(hash(fileSrc)); 8 | 9 | return function scopedName(name) { 10 | return name + '_' + suffix; 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /test/animation-as-pseudo.source.js: -------------------------------------------------------------------------------- 1 | const csjs = require('../'); 2 | 3 | module.exports = csjs` 4 | 5 | @keyframes hover { 6 | 0% { opacity: 0.0; } 7 | 100% { opacity: 0.5; } 8 | } 9 | 10 | @media (max-width: 480px) { 11 | .animation:hover { 12 | background: green; 13 | } 14 | } 15 | 16 | `; 17 | -------------------------------------------------------------------------------- /test/animation-as-pseudo.noscope.source.js: -------------------------------------------------------------------------------- 1 | const csjs = require('../').noScope; 2 | 3 | module.exports = csjs` 4 | 5 | @keyframes hover { 6 | 0% { opacity: 0.0; } 7 | 100% { opacity: 0.5; } 8 | } 9 | 10 | @media (max-width: 480px) { 11 | .animation:hover { 12 | background: green; 13 | } 14 | } 15 | 16 | `; 17 | -------------------------------------------------------------------------------- /test/keyframes-usage.noscope.source.js: -------------------------------------------------------------------------------- 1 | const csjs = require('../').noScope; 2 | 3 | const keyframes = require('./keyframes.noscope.source'); 4 | 5 | module.exports = csjs` 6 | 7 | .woo { 8 | animation: ${keyframes.yolo} 5s infinite; 9 | } 10 | 11 | .boo { 12 | animation-name: ${keyframes.yoloYolo}; 13 | } 14 | 15 | `; 16 | -------------------------------------------------------------------------------- /test/extensions.source.js: -------------------------------------------------------------------------------- 1 | const csjs = require('../'); 2 | 3 | module.exports = csjs` 4 | 5 | .foo { 6 | color: red; 7 | } 8 | 9 | .bar extends .foo { 10 | background: blue; 11 | } 12 | 13 | .baz extends .bar { 14 | text-transform: uppercase; 15 | } 16 | 17 | .bazz extends .bar{ 18 | color: white; 19 | } 20 | 21 | `; 22 | -------------------------------------------------------------------------------- /.zuul.yml: -------------------------------------------------------------------------------- 1 | ui: tape 2 | browsers: 3 | - name: chrome 4 | version: latest 5 | - name: firefox 6 | version: latest 7 | - name: safari 8 | version: latest 9 | - name: iphone 10 | version: latest 11 | - name: microsoftedge 12 | version: latest 13 | browserify: 14 | - transform: bulkify 15 | - transform: brfs 16 | - transform: folderify 17 | -------------------------------------------------------------------------------- /docs/automatic-vendor-prefixing.md: -------------------------------------------------------------------------------- 1 | # Automatic Vendor Prefixing 2 | 3 | ## Runtime solutions 4 | 5 | * http://leaverou.github.io/prefixfree/ 6 | 7 | ## Buildtime solutions 8 | 9 | * https://github.com/rtsao/babel-plugin-csjs-postcss w/ https://github.com/postcss/autoprefixer 10 | * https://github.com/rtsao/csjs-extractify w/ https://github.com/postcss/autoprefixer 11 | -------------------------------------------------------------------------------- /test/extensions.noscope.source.js: -------------------------------------------------------------------------------- 1 | const csjs = require('../').noScope; 2 | 3 | module.exports = csjs` 4 | 5 | .foo { 6 | color: red; 7 | } 8 | 9 | .bar extends .foo { 10 | background: blue; 11 | } 12 | 13 | .baz extends .bar { 14 | text-transform: uppercase; 15 | } 16 | 17 | .bazz extends .bar{ 18 | color: white; 19 | } 20 | 21 | `; 22 | -------------------------------------------------------------------------------- /test/keyframes.noscope.expected.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | @keyframes yolo { 4 | 0% { opacity: 0; } 5 | 100% { opacity: 1; } 6 | } 7 | 8 | .foo { 9 | animation: yolo 5s infinite; 10 | } 11 | 12 | @keyframes yoloYolo { 13 | 0% { opacity: 0; } 14 | 100% { opacity: 1; } 15 | } 16 | 17 | .bar { 18 | animation: yoloYolo 5s infinite; 19 | } 20 | 21 | -------------------------------------------------------------------------------- /lib/hash-string.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * djb2 string hash implementation based on string-hash module: 5 | * https://github.com/darkskyapp/string-hash 6 | */ 7 | 8 | module.exports = function hashStr(str) { 9 | var hash = 5381; 10 | var i = str.length; 11 | 12 | while (i) { 13 | hash = (hash * 33) ^ str.charCodeAt(--i) 14 | } 15 | return hash >>> 0; 16 | }; 17 | -------------------------------------------------------------------------------- /test/multiple-extensions.source.js: -------------------------------------------------------------------------------- 1 | const csjs = require('../'); 2 | 3 | const external = require('./extensions.source'); 4 | 5 | module.exports = csjs` 6 | 7 | .lol { 8 | font-family: serif; 9 | } 10 | 11 | .baz extends .lol, ${external.bar} { 12 | font-size: 12px; 13 | } 14 | 15 | .fob extends ${external.foo} { 16 | font-weight: 500; 17 | } 18 | 19 | `; 20 | -------------------------------------------------------------------------------- /test/keyframes.expected.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | @keyframes yolo_4lrRRM { 4 | 0% { opacity: 0; } 5 | 100% { opacity: 1; } 6 | } 7 | 8 | .foo_4lrRRM { 9 | animation: yolo_4lrRRM 5s infinite; 10 | } 11 | 12 | @keyframes yoloYolo_4lrRRM { 13 | 0% { opacity: 0; } 14 | 100% { opacity: 1; } 15 | } 16 | 17 | .bar_4lrRRM { 18 | animation: yoloYolo_4lrRRM 5s infinite; 19 | } 20 | 21 | -------------------------------------------------------------------------------- /test/multiple-extensions.noscope.source.js: -------------------------------------------------------------------------------- 1 | const csjs = require('../').noScope; 2 | 3 | const external = require('./extensions.noscope.source'); 4 | 5 | module.exports = csjs` 6 | 7 | .lol { 8 | font-family: serif; 9 | } 10 | 11 | .baz extends .lol, ${external.bar} { 12 | font-size: 12px; 13 | } 14 | 15 | .fob extends ${external.foo} { 16 | font-weight: 500; 17 | } 18 | 19 | `; 20 | -------------------------------------------------------------------------------- /test/keyframes.source.js: -------------------------------------------------------------------------------- 1 | const csjs = require('../'); 2 | 3 | module.exports = csjs` 4 | 5 | @keyframes yolo { 6 | 0% { opacity: 0; } 7 | 100% { opacity: 1; } 8 | } 9 | 10 | .foo { 11 | animation: yolo 5s infinite; 12 | } 13 | 14 | @keyframes yoloYolo { 15 | 0% { opacity: 0; } 16 | 100% { opacity: 1; } 17 | } 18 | 19 | .bar { 20 | animation: yoloYolo 5s infinite; 21 | } 22 | 23 | `; 24 | -------------------------------------------------------------------------------- /test/keyframes.noscope.source.js: -------------------------------------------------------------------------------- 1 | const csjs = require('../').noScope; 2 | 3 | module.exports = csjs` 4 | 5 | @keyframes yolo { 6 | 0% { opacity: 0; } 7 | 100% { opacity: 1; } 8 | } 9 | 10 | .foo { 11 | animation: yolo 5s infinite; 12 | } 13 | 14 | @keyframes yoloYolo { 15 | 0% { opacity: 0; } 16 | 100% { opacity: 1; } 17 | } 18 | 19 | .bar { 20 | animation: yoloYolo 5s infinite; 21 | } 22 | 23 | `; 24 | -------------------------------------------------------------------------------- /test/extends-in-media-query.noscope.expected.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | .woot { 4 | color: red; 5 | } 6 | 7 | .hello { 8 | font-size: 10px; 9 | } 10 | 11 | .yay { 12 | font-weight: bold; 13 | } 14 | 15 | @media (max-width: 480px) { 16 | .woot { 17 | color: blue; 18 | } 19 | } 20 | 21 | @media (max-width: 580px) { 22 | .woot { 23 | color: green; 24 | } 25 | } 26 | 27 | .underline { 28 | text-decoration: underline; 29 | } 30 | 31 | -------------------------------------------------------------------------------- /test/extends-in-media-query.expected.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | .woot_2XWyPW { 4 | color: red; 5 | } 6 | 7 | .hello_2XWyPW { 8 | font-size: 10px; 9 | } 10 | 11 | .yay_2XWyPW { 12 | font-weight: bold; 13 | } 14 | 15 | @media (max-width: 480px) { 16 | .woot_2XWyPW { 17 | color: blue; 18 | } 19 | } 20 | 21 | @media (max-width: 580px) { 22 | .woot_2XWyPW { 23 | color: green; 24 | } 25 | } 26 | 27 | .underline_2XWyPW { 28 | text-decoration: underline; 29 | } 30 | 31 | -------------------------------------------------------------------------------- /lib/regex.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var findClasses = /(\.)(?!\d)([^\s\.,{\[>+~#:)]*)(?![^{]*})/.source; 4 | var findKeyframes = /(@\S*keyframes\s*)([^{\s]*)/.source; 5 | var ignoreComments = /(?!(?:[^*/]|\*[^/]|\/[^*])*\*+\/)/.source; 6 | 7 | var classRegex = new RegExp(findClasses + ignoreComments, 'g'); 8 | var keyframesRegex = new RegExp(findKeyframes + ignoreComments, 'g'); 9 | 10 | module.exports = { 11 | classRegex: classRegex, 12 | keyframesRegex: keyframesRegex, 13 | ignoreComments: ignoreComments, 14 | }; 15 | -------------------------------------------------------------------------------- /lib/base62-encode.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * base62 encode implementation based on base62 module: 5 | * https://github.com/andrew/base62.js 6 | */ 7 | 8 | var CHARS = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; 9 | 10 | module.exports = function encode(integer) { 11 | if (integer === 0) { 12 | return '0'; 13 | } 14 | var str = ''; 15 | while (integer > 0) { 16 | str = CHARS[integer % 62] + str; 17 | integer = Math.floor(integer / 62); 18 | } 19 | return str; 20 | }; 21 | -------------------------------------------------------------------------------- /lib/extract-exports.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var regex = require('./regex'); 4 | var classRegex = regex.classRegex; 5 | var keyframesRegex = regex.keyframesRegex; 6 | 7 | module.exports = extractExports; 8 | 9 | function extractExports(css) { 10 | return { 11 | css: css, 12 | keyframes: getExport(css, keyframesRegex), 13 | classes: getExport(css, classRegex) 14 | }; 15 | } 16 | 17 | function getExport(css, regex) { 18 | var prop = {}; 19 | var match; 20 | while((match = regex.exec(css)) !== null) { 21 | var name = match[2]; 22 | prop[name] = name; 23 | } 24 | return prop; 25 | } 26 | -------------------------------------------------------------------------------- /test/extends-in-media-query.source.js: -------------------------------------------------------------------------------- 1 | const csjs = require('../'); 2 | 3 | const basic = require('./basic.source'); 4 | 5 | module.exports = csjs` 6 | 7 | .woot { 8 | color: red; 9 | } 10 | 11 | .hello { 12 | font-size: 10px; 13 | } 14 | 15 | .yay { 16 | font-weight: bold; 17 | } 18 | 19 | @media (max-width: 480px) { 20 | .woot extends .hello, .yay { 21 | color: blue; 22 | } 23 | } 24 | 25 | @media (max-width: 580px) { 26 | .woot extends ${basic.foo}, .underline { 27 | color: green; 28 | } 29 | } 30 | 31 | .underline { 32 | text-decoration: underline; 33 | } 34 | 35 | `; 36 | -------------------------------------------------------------------------------- /test/extends-in-media-query.noscope.source.js: -------------------------------------------------------------------------------- 1 | const csjs = require('../').noScope; 2 | 3 | const basic = require('./basic.noscope.source'); 4 | 5 | module.exports = csjs` 6 | 7 | .woot { 8 | color: red; 9 | } 10 | 11 | .hello { 12 | font-size: 10px; 13 | } 14 | 15 | .yay { 16 | font-weight: bold; 17 | } 18 | 19 | @media (max-width: 480px) { 20 | .woot extends .hello, .yay { 21 | color: blue; 22 | } 23 | } 24 | 25 | @media (max-width: 580px) { 26 | .woot extends ${basic.foo}, .underline { 27 | color: green; 28 | } 29 | } 30 | 31 | .underline { 32 | text-decoration: underline; 33 | } 34 | 35 | `; 36 | -------------------------------------------------------------------------------- /test/comments.noscope.expected.css: -------------------------------------------------------------------------------- 1 | 2 | /* Here's a comment */ 3 | /*squishedcomment*/ 4 | /********crazycomment********/ 5 | /* A comment with a period at the end. */ 6 | /* A comment with a period followed by numbers v1.2.3 */ 7 | /* A comment with a .className */ 8 | /* 9 | * Here's a comment 10 | * A comment with a period at the end. 11 | * A comment with a period followed by numbers v1.2.3 12 | * A comment with a .className 13 | */ 14 | /* .inlineCss { color: red; } */ 15 | /* 16 | .commentedOutCss { 17 | color: blue; 18 | } 19 | */ 20 | .foo { 21 | color: red; /* comment on a line */ 22 | animation-name: wow; 23 | } 24 | 25 | @keyframes wow {} 26 | 27 | /* 28 | @keyframes bam {} 29 | */ 30 | 31 | /* .woot { 32 | animation-name: bam; 33 | } */ 34 | 35 | /* .hmm { 36 | animation-name: wow; 37 | } */ 38 | 39 | -------------------------------------------------------------------------------- /test/comments.expected.css: -------------------------------------------------------------------------------- 1 | 2 | /* Here's a comment */ 3 | /*squishedcomment*/ 4 | /********crazycomment********/ 5 | /* A comment with a period at the end. */ 6 | /* A comment with a period followed by numbers v1.2.3 */ 7 | /* A comment with a .className */ 8 | /* 9 | * Here's a comment 10 | * A comment with a period at the end. 11 | * A comment with a period followed by numbers v1.2.3 12 | * A comment with a .className 13 | */ 14 | /* .inlineCss { color: red; } */ 15 | /* 16 | .commentedOutCss { 17 | color: blue; 18 | } 19 | */ 20 | .foo_1LMZVZ { 21 | color: red; /* comment on a line */ 22 | animation-name: wow_1LMZVZ; 23 | } 24 | 25 | @keyframes wow_1LMZVZ {} 26 | 27 | /* 28 | @keyframes bam {} 29 | */ 30 | 31 | /* .woot { 32 | animation-name: bam; 33 | } */ 34 | 35 | /* .hmm { 36 | animation-name: wow; 37 | } */ 38 | 39 | -------------------------------------------------------------------------------- /lib/replace-animations.js: -------------------------------------------------------------------------------- 1 | var ignoreComments = require('./regex').ignoreComments; 2 | 3 | module.exports = replaceAnimations; 4 | 5 | function replaceAnimations(result) { 6 | var animations = Object.keys(result.keyframes).reduce(function(acc, key) { 7 | acc[result.keyframes[key]] = key; 8 | return acc; 9 | }, {}); 10 | var unscoped = Object.keys(animations); 11 | 12 | if (unscoped.length) { 13 | var regexStr = '((?:animation|animation-name)\\s*:[^};]*)(' 14 | + unscoped.join('|') + ')([;\\s])' + ignoreComments; 15 | var regex = new RegExp(regexStr, 'g'); 16 | 17 | var replaced = result.css.replace(regex, function(match, preamble, name, ending) { 18 | return preamble + animations[name] + ending; 19 | }); 20 | 21 | return { 22 | css: replaced, 23 | keyframes: result.keyframes, 24 | classes: result.classes 25 | } 26 | } 27 | 28 | return result; 29 | } 30 | -------------------------------------------------------------------------------- /docs/comments.md: -------------------------------------------------------------------------------- 1 | # CSS Comments 2 | 3 | CSJS will ignore everything inside CSS `/* */` comments, but preserve it in the output CSS. Note that CSJS will break if there are unbalanced comments. 4 | 5 | ([Live editable codepen.io demo](http://codepen.io/rtsao/pen/MKNaPR?editors=0010)) 6 | 7 | ```javascript 8 | const csjs = require('csjs'); 9 | const {h1} = require('react').DOM; 10 | 11 | const styles = csjs` 12 | 13 | /* 14 | @keyframes fadeIn { 15 | from { opacity: 0; } 16 | to { opacity: 1; } 17 | } 18 | */ 19 | 20 | /* .foo { color: red; } */ 21 | 22 | .pulse { 23 | animation: 1s ease-in-out infinite fadeIn alternate; 24 | } 25 | 26 | `; 27 | 28 | const html = require('react-dom/server').renderToStaticMarkup( 29 | h1({className: styles.title}, 'Hello World!') 30 | ); 31 | /* 32 |

Hello World!

33 | */ 34 | 35 | console.log(styles); 36 | // => {pulse: "pulse_1uRFrA"} 37 | ``` 38 | -------------------------------------------------------------------------------- /test/comments.source.js: -------------------------------------------------------------------------------- 1 | const csjs = require('../'); 2 | 3 | module.exports = csjs` 4 | /* Here's a comment */ 5 | /*squishedcomment*/ 6 | /********crazycomment********/ 7 | /* A comment with a period at the end. */ 8 | /* A comment with a period followed by numbers v1.2.3 */ 9 | /* A comment with a .className */ 10 | /* 11 | * Here's a comment 12 | * A comment with a period at the end. 13 | * A comment with a period followed by numbers v1.2.3 14 | * A comment with a .className 15 | */ 16 | /* .inlineCss { color: red; } */ 17 | /* 18 | .commentedOutCss { 19 | color: blue; 20 | } 21 | */ 22 | .foo { 23 | color: red; /* comment on a line */ 24 | animation-name: wow; 25 | } 26 | 27 | @keyframes wow {} 28 | 29 | /* 30 | @keyframes bam {} 31 | */ 32 | 33 | /* .woot { 34 | animation-name: bam; 35 | } */ 36 | 37 | /* .hmm { 38 | animation-name: wow; 39 | } */ 40 | 41 | `; 42 | -------------------------------------------------------------------------------- /test/comments.source.noscope.js: -------------------------------------------------------------------------------- 1 | const csjs = require('../').noScope; 2 | 3 | module.exports = csjs` 4 | /* Here's a comment */ 5 | /*squishedcomment*/ 6 | /********crazycomment********/ 7 | /* A comment with a period at the end. */ 8 | /* A comment with a period followed by numbers v1.2.3 */ 9 | /* A comment with a .className */ 10 | /* 11 | * Here's a comment 12 | * A comment with a period at the end. 13 | * A comment with a period followed by numbers v1.2.3 14 | * A comment with a .className 15 | */ 16 | /* .inlineCss { color: red; } */ 17 | /* 18 | .commentedOutCss { 19 | color: blue; 20 | } 21 | */ 22 | .foo { 23 | color: red; /* comment on a line */ 24 | animation-name: wow; 25 | } 26 | 27 | @keyframes wow {} 28 | 29 | /* 30 | @keyframes bam {} 31 | */ 32 | 33 | /* .woot { 34 | animation-name: bam; 35 | } */ 36 | 37 | /* .hmm { 38 | animation-name: wow; 39 | } */ 40 | 41 | `; 42 | -------------------------------------------------------------------------------- /lib/scopeify.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var fileScoper = require('./scoped-name'); 4 | var replaceAnimations = require('./replace-animations'); 5 | var regex = require('./regex'); 6 | var classRegex = regex.classRegex; 7 | var keyframesRegex = regex.keyframesRegex; 8 | 9 | module.exports = scopify; 10 | 11 | function scopify(css, ignores) { 12 | var makeScopedName = fileScoper(css); 13 | var replacers = { 14 | classes: classRegex, 15 | keyframes: keyframesRegex 16 | }; 17 | 18 | function scopeCss(result, key) { 19 | var replacer = replacers[key]; 20 | function replaceFn(fullMatch, prefix, name) { 21 | var scopedName = ignores[name] ? name : makeScopedName(name); 22 | result[key][scopedName] = name; 23 | return prefix + scopedName; 24 | } 25 | return { 26 | css: result.css.replace(replacer, replaceFn), 27 | keyframes: result.keyframes, 28 | classes: result.classes 29 | }; 30 | } 31 | 32 | var result = Object.keys(replacers).reduce(scopeCss, { 33 | css: css, 34 | keyframes: {}, 35 | classes: {} 36 | }); 37 | 38 | return replaceAnimations(result); 39 | } 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Ryan Tsao 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /docs/keyframe-animations.md: -------------------------------------------------------------------------------- 1 | # CSS3 Keyframe Animations 2 | 3 | ## Scoped Animations 4 | 5 | CSJS scopes your keyframe animations just like class names. Note that they occupy the same namespace, so you can't export a keyframe animation with the same name as a class in the same template string literal. 6 | 7 | ([Live editable codepen.io demo](http://codepen.io/rtsao/pen/KVOdWB?editors=0010)) 8 | 9 | ```javascript 10 | const csjs = require('csjs'); 11 | const {h1} = require('react').DOM; 12 | 13 | const styles = csjs` 14 | 15 | @keyframes fadeIn { 16 | from { opacity: 0; } 17 | to { opacity: 1; } 18 | } 19 | 20 | .pulse { 21 | animation: 1s ease-in-out infinite fadeIn alternate; 22 | } 23 | 24 | `; 25 | 26 | const html = require('react-dom/server').renderToStaticMarkup( 27 | h1({className: styles.title}, 'Hello World!') 28 | ); 29 | /* 30 |

Hello World!

31 | */ 32 | 33 | const css = csjs.getCss(styles); 34 | /* 35 | @keyframes fadeIn_1G6o1t { 36 | from { opacity: 0; } 37 | to { opacity: 1; } 38 | } 39 | 40 | .pulse_1G6o1t { 41 | animation: 1s ease-in-out infinite fadeIn_1G6o1t alternate; 42 | } 43 | */ 44 | ``` 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "csjs", 3 | "version": "1.1.0", 4 | "description": "Cascading Style JavaScripts", 5 | "keywords": [ 6 | "csjs", 7 | "css-modules", 8 | "scoped-css", 9 | "css-in-js", 10 | "modular-css", 11 | "css" 12 | ], 13 | "author": "Ryan Tsao ", 14 | "main": "index.js", 15 | "homepage": "https://github.com/rtsao/csjs", 16 | "repository": "git@github.com:rtsao/csjs.git", 17 | "bugs": "https://github.com/rtsao/csjs/issues", 18 | "scripts": { 19 | "test": "node test/index.js", 20 | "cover": "istanbul cover test/index.js", 21 | "travis-test": "npm run cover && ((cat coverage/lcov.info | coveralls) || exit 0)", 22 | "travis-test-browser": "zuul -- test/index.js" 23 | }, 24 | "dependencies": {}, 25 | "devDependencies": { 26 | "brfs": "^1.4.2", 27 | "bulk-require": "^0.2.1", 28 | "bulkify": "^1.1.1", 29 | "coveralls": "^2.11.4", 30 | "folderify": "https://github.com/rtsao/folderify/tarball/patch-1", 31 | "include-folder": "^1.0.0", 32 | "istanbul": "^0.4.0", 33 | "tape": "^4.2.0", 34 | "zuul": "^3.8.0" 35 | }, 36 | "engines": { 37 | "node": ">=4.0.0" 38 | }, 39 | "license": "MIT" 40 | } 41 | -------------------------------------------------------------------------------- /lib/build-exports.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var makeComposition = require('./composition').makeComposition; 4 | 5 | module.exports = function createExports(classes, keyframes, compositions) { 6 | var keyframesObj = Object.keys(keyframes).reduce(function(acc, key) { 7 | var val = keyframes[key]; 8 | acc[val] = makeComposition([key], [val], true); 9 | return acc; 10 | }, {}); 11 | 12 | var exports = Object.keys(classes).reduce(function(acc, key) { 13 | var val = classes[key]; 14 | var composition = compositions[key]; 15 | var extended = composition ? getClassChain(composition) : []; 16 | var allClasses = [key].concat(extended); 17 | var unscoped = allClasses.map(function(name) { 18 | return classes[name] ? classes[name] : name; 19 | }); 20 | acc[val] = makeComposition(allClasses, unscoped); 21 | return acc; 22 | }, keyframesObj); 23 | 24 | return exports; 25 | } 26 | 27 | function getClassChain(obj) { 28 | var visited = {}, acc = []; 29 | 30 | function traverse(obj) { 31 | return Object.keys(obj).forEach(function(key) { 32 | if (!visited[key]) { 33 | visited[key] = true; 34 | acc.push(key); 35 | traverse(obj[key]); 36 | } 37 | }); 38 | } 39 | 40 | traverse(obj); 41 | return acc; 42 | } 43 | -------------------------------------------------------------------------------- /lib/css-extract-extends.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var makeComposition = require('./composition').makeComposition; 4 | 5 | var regex = /\.([^\s]+)(\s+)(extends\s+)(\.[^{]+)/g; 6 | 7 | module.exports = function extractExtends(css) { 8 | var found, matches = []; 9 | while (found = regex.exec(css)) { 10 | matches.unshift(found); 11 | } 12 | 13 | function extractCompositions(acc, match) { 14 | var extendee = getClassName(match[1]); 15 | var keyword = match[3]; 16 | var extended = match[4]; 17 | 18 | // remove from output css 19 | var index = match.index + match[1].length + match[2].length; 20 | var len = keyword.length + extended.length; 21 | acc.css = acc.css.slice(0, index) + " " + acc.css.slice(index + len + 1); 22 | 23 | var extendedClasses = splitter(extended); 24 | 25 | extendedClasses.forEach(function(className) { 26 | if (!acc.compositions[extendee]) { 27 | acc.compositions[extendee] = {}; 28 | } 29 | if (!acc.compositions[className]) { 30 | acc.compositions[className] = {}; 31 | } 32 | acc.compositions[extendee][className] = acc.compositions[className]; 33 | }); 34 | return acc; 35 | } 36 | 37 | return matches.reduce(extractCompositions, { 38 | css: css, 39 | compositions: {} 40 | }); 41 | 42 | }; 43 | 44 | function splitter(match) { 45 | return match.split(',').map(getClassName); 46 | } 47 | 48 | function getClassName(str) { 49 | var trimmed = str.trim(); 50 | return trimmed[0] === '.' ? trimmed.substr(1) : trimmed; 51 | } 52 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '4' 4 | script: if [ "${TRAVIS_PULL_REQUEST}" = "false" ]; then npm run travis-test && npm run travis-test-browser; else npm run travis-test; fi 5 | env: 6 | global: 7 | - secure: API1krUNaYB5ixJXZoNBKyEmyMqzAvfu/YG6DrVK69ITDCvg2RDbWkv9xm17DTWhX4htczvY0YPs1MX6NNJLwLdTOl3aelGXrbdJt0pGmOYg0KnNuw3hexCwm0std35iT2YOlF7Y5AwVLM5x3tqoPMIuc5WG5MVQoQXJ8yI7YbS2nqA5t9RaO9juaTiQeYTdNhJWMXn0LsFzay42HTi60jf0OQPjZsKYSseLYjsm/J7DU5TkMGed9UDIj8e4qf8CajHEOEUMHX1ZtU6L4TvX4y5H/5Ut5ACg7Fkh1Ojgc2NTb2fAdDrnU3DhpVo/DMhoJ+PYRSBU6+OtJaLuJgdOlg7ekShxMoYz9Y2iDnjiA6gs9/LLgMox3e5SNi7zZkpx0Q+7CtpRD19M4pVsw4EUHLKxuCBFCjwOb9/xGRnf0ow0gLjENPQ2K3XAxpwUtGH9EHO3APOUwBnaLGsz7bjuyrI1igMya/Az/Gzfj+L28dehXWVMK+AwnKwEJw4wvaZpiM0dKfgBFUVzA6/ujmKcwaQ/VgbR2+F0JkBboXCKkHXRItRzDvl44iJETMgkOZNZbQYJXzplqEDgPQ1XJPeg5f34YCuy24ask/CAkOIiEpsN9PJOHyLcWfZ/pX1SZq/NyJcMWXGZUSE/2kwJblLk0FlPZI0TCHxZqz7ALJenZ6E= 8 | - secure: PSwziCwGTcLNqgqWv8iduHD4aJdPg8YTlPSAnMgyj7Q9Xjc1ino3kW9dDhUyAOi7nNSVWHpmSUcG7oFH7zDfHBLkvxW8yZTJJMDgPuUp5G/mDM2RSQINzwSduLhQvazvRS9gp6Dq5DBun9MxSTf4U+n4ZAwS4laPoweoCuFMa9ljo2dOJj8kDsiveXJAcOYIYxknaMoBhcdOWAkF/DS6I0jUO0WLb/TAqdT+SduWbPnC5RUn049xU6i3skaAnmXvaP+sqA1zuRHHF2qaycOo5qAv17HIfFp+51HstnMpjtt68NZI74SWunXbefwzgaFBWOrr/+kkHYw9svXPT34HmmU7BqlykcR8zlqJY6oFqjcQKZBNCaVSTzJaGhNgt6qjzTaPj71/WVmKL8++LNBIftGF78xq5iwq5D+1/a/iiZb4pGEOGuI3oUnT9nfBpE6sluEsufJnymucgC6T0oO9eUUo1iPUJWWhmlKfZrwLotV+8rtJOdLA1ZLQ/HonxSxU/odha0D+9Vm/rj9bXpgtWXkT/meD6wwScRE8m5CrZ7l/HOuO24u2zzsrV+M5Vsf/lTjrWUI7EHD9uAWgvV+Kb/CX/FAQSqTIOnT+sa3bzmqs9vuWNHJD3Giiumf2C77d1sk5SHF16V4y87IpF62ga8dVmxiFjrdWKmaK4EGUDvI= 9 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var fs = require('fs'); 4 | var path = require('path'); 5 | var test = require('tape'); 6 | 7 | var includeFolder = require('include-folder'); 8 | var bulk = require('bulk-require'); 9 | 10 | var csjs = require('../'); 11 | 12 | var extensions = { 13 | source: '.source.js', 14 | json: '.expected.json', 15 | css: '.expected.css' 16 | }; 17 | 18 | var testFiles = fs.readdirSync('test'); 19 | 20 | var required = bulk(__dirname, [ 21 | '*.source.js', 22 | '*.expected.json' 23 | ]); 24 | 25 | var cssFiles = includeFolder('./test', /expected\.css$/, { 26 | preserveFilenames: true 27 | }); 28 | 29 | var fixtureRegex = new RegExp(extensions.source + '$'); 30 | var matchesFixture = fixtureRegex.test.bind(fixtureRegex); 31 | 32 | var tests = testFiles 33 | .filter(matchesFixture) 34 | .map(function toName(file) { 35 | return path.basename(file, extensions.source); 36 | }); 37 | 38 | tests.forEach(testFromName); 39 | 40 | function testFromName(name) { 41 | var fixtures = getFixtures(name); 42 | runTest(name, fixtures.result, fixtures.expected); 43 | } 44 | 45 | function runTest(name, result, expected) { 46 | test('test ' + name, function t(assert) { 47 | assert.equal(csjs.getCss(result), expected.css, 'css matches expected'); 48 | assert.deepEqual(stringifyVals(result), expected.json, 'object matches expected'); 49 | assert.end(); 50 | }); 51 | } 52 | 53 | function getFixtures(name) { 54 | var sourcePath = name + '.source'; 55 | var jsonPath = name + '.expected'; 56 | var cssPath = name + extensions.css; 57 | 58 | return { 59 | result: required[sourcePath], 60 | expected: { 61 | json: required[jsonPath], 62 | css: cssFiles[cssPath] 63 | } 64 | } 65 | } 66 | 67 | function moduleExists(name) { 68 | try { 69 | return Boolean(require.resolve(name)); 70 | } catch(e) { 71 | return false; 72 | } 73 | } 74 | 75 | function fixturePath(name, ext) { 76 | return path.join(__dirname, name + ext); 77 | } 78 | 79 | function stringifyVals(obj) { 80 | return Object.keys(obj).reduce(function(acc, key) { 81 | acc[key] = obj[key].toString(); 82 | return acc; 83 | }, {}); 84 | } 85 | -------------------------------------------------------------------------------- /lib/composition.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | makeComposition: makeComposition, 5 | isComposition: isComposition, 6 | ignoreComposition: ignoreComposition 7 | }; 8 | 9 | /** 10 | * Returns an immutable composition object containing the given class names 11 | * @param {array} classNames - The input array of class names 12 | * @return {Composition} - An immutable object that holds multiple 13 | * representations of the class composition 14 | */ 15 | function makeComposition(classNames, unscoped, isAnimation) { 16 | var classString = classNames.join(' '); 17 | return Object.create(Composition.prototype, { 18 | classNames: { // the original array of class names 19 | value: Object.freeze(classNames), 20 | configurable: false, 21 | writable: false, 22 | enumerable: true 23 | }, 24 | unscoped: { // the original array of class names 25 | value: Object.freeze(unscoped), 26 | configurable: false, 27 | writable: false, 28 | enumerable: true 29 | }, 30 | className: { // space-separated class string for use in HTML 31 | value: classString, 32 | configurable: false, 33 | writable: false, 34 | enumerable: true 35 | }, 36 | selector: { // comma-separated, period-prefixed string for use in CSS 37 | value: classNames.map(function(name) { 38 | return isAnimation ? name : '.' + name; 39 | }).join(', '), 40 | configurable: false, 41 | writable: false, 42 | enumerable: true 43 | }, 44 | toString: { // toString() method, returns class string for use in HTML 45 | value: function() { 46 | return classString; 47 | }, 48 | configurable: false, 49 | writeable: false, 50 | enumerable: false 51 | } 52 | }); 53 | } 54 | 55 | /** 56 | * Returns whether the input value is a Composition 57 | * @param value - value to check 58 | * @return {boolean} - whether value is a Composition or not 59 | */ 60 | function isComposition(value) { 61 | return value instanceof Composition; 62 | } 63 | 64 | function ignoreComposition(values) { 65 | return values.reduce(function(acc, val) { 66 | if (isComposition(val)) { 67 | val.classNames.forEach(function(name, i) { 68 | acc[name] = val.unscoped[i]; 69 | }); 70 | } 71 | return acc; 72 | }, {}); 73 | } 74 | 75 | /** 76 | * Private constructor for use in `instanceof` checks 77 | */ 78 | function Composition() {} 79 | -------------------------------------------------------------------------------- /lib/csjs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var extractExtends = require('./css-extract-extends'); 4 | var composition = require('./composition'); 5 | var isComposition = composition.isComposition; 6 | var ignoreComposition = composition.ignoreComposition; 7 | var buildExports = require('./build-exports'); 8 | var scopify = require('./scopeify'); 9 | var cssKey = require('./css-key'); 10 | var extractExports = require('./extract-exports'); 11 | 12 | module.exports = function csjsTemplate(opts) { 13 | opts = (typeof opts === 'undefined') ? {} : opts; 14 | var noscope = (typeof opts.noscope === 'undefined') ? false : opts.noscope; 15 | 16 | return function csjsHandler(strings, values) { 17 | // Fast path to prevent arguments deopt 18 | var values = Array(arguments.length - 1); 19 | for (var i = 1; i < arguments.length; i++) { 20 | values[i - 1] = arguments[i]; 21 | } 22 | var css = joiner(strings, values.map(selectorize)); 23 | var ignores = ignoreComposition(values); 24 | 25 | var scope = noscope ? extractExports(css) : scopify(css, ignores); 26 | var extracted = extractExtends(scope.css); 27 | var localClasses = without(scope.classes, ignores); 28 | var localKeyframes = without(scope.keyframes, ignores); 29 | var compositions = extracted.compositions; 30 | 31 | var exports = buildExports(localClasses, localKeyframes, compositions); 32 | 33 | return Object.defineProperty(exports, cssKey, { 34 | enumerable: false, 35 | configurable: false, 36 | writeable: false, 37 | value: extracted.css 38 | }); 39 | } 40 | } 41 | 42 | /** 43 | * Replaces class compositions with comma seperated class selectors 44 | * @param value - the potential class composition 45 | * @return - the original value or the selectorized class composition 46 | */ 47 | function selectorize(value) { 48 | return isComposition(value) ? value.selector : value; 49 | } 50 | 51 | /** 52 | * Joins template string literals and values 53 | * @param {array} strings - array of strings 54 | * @param {array} values - array of values 55 | * @return {string} - strings and values joined 56 | */ 57 | function joiner(strings, values) { 58 | return strings.map(function(str, i) { 59 | return (i !== values.length) ? str + values[i] : str; 60 | }).join(''); 61 | } 62 | 63 | /** 64 | * Returns first object without keys of second 65 | * @param {object} obj - source object 66 | * @param {object} unwanted - object with unwanted keys 67 | * @return {object} - first object without unwanted keys 68 | */ 69 | function without(obj, unwanted) { 70 | return Object.keys(obj).reduce(function(acc, key) { 71 | if (!unwanted[key]) { 72 | acc[key] = obj[key]; 73 | } 74 | return acc; 75 | }, {}); 76 | } 77 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [![CSJS logo](https://cdn.rawgit.com/rtsao/csjs/logo/logo.svg "CSJS (Cascading Style JavaScripts)")](https://github.com/rtsao/csjs) 2 | 3 | [![build status][build-badge]][build-href] 4 | [![coverage status][coverage-badge]][coverage-href] 5 | [![dependencies status][deps-badge]][deps-href] 6 | [![npm version][npm-badge]][npm-href] 7 | 8 | > CSJS allows you to write modular, scoped CSS with valid JavaScript. 9 | 10 | ## Features 11 | * Extremely simple and lightweight 12 | * Zero dependencies, [~2KB minified and gzipped][csjs-bundle] 13 | * Leverages native ES6 and CSS features [(1)] rather than reinventing the wheel 14 | * Seamless modular, scoped styles with explicit dependencies powered by CommonJS/ES6 modules 15 | * Dead-simple variables/mixins using tagged ES6 template strings 16 | * Style composition with optimal reuse via natural class composition mechanics already in CSS/HTML[(2)] 17 | * Works tooling-free; no required transpilation/compilation/build steps [(3)] 18 | * Framework-agnostic (No React dependency; works with Web Components, etc.) 19 | * Fully supported native CSS media queries, pseudo-classes, keyframe animations, etc. 20 | * Server-rendered/universal JavaScript support 21 | 22 | ## Quick Example 23 | ([Live editable codepen.io demo](http://codepen.io/rtsao/pen/jWRJZj?editors=0010)) 24 | ```javascript 25 | const csjs = require('csjs'); 26 | const {div, h1} = require('react').DOM; 27 | 28 | const green = '#33aa22'; 29 | 30 | const styles = csjs` 31 | 32 | .panel { 33 | border: 1px solid black; 34 | background-color: ${green}; 35 | } 36 | 37 | .title { 38 | padding: 4px; 39 | font-size: 15px; 40 | } 41 | 42 | `; 43 | 44 | const html = require('react-dom/server').renderToStaticMarkup( 45 | div({className: styles.panel}, [ 46 | h1({className: styles.title}, 'Hello World!') 47 | ]) 48 | ); 49 | /* 50 |
51 |

Hello World!

52 |
53 | */ 54 | 55 | const css = csjs.getCss(styles); 56 | /* 57 | .panel_4Eda43 { 58 | border: 1px solid black; 59 | background-color: #33aa22; 60 | } 61 | 62 | .title_4Eda43 { 63 | padding: 4px; 64 | font-size: 15px; 65 | } 66 | */ 67 | ``` 68 | 69 | ### Simple, tooling-free 70 | 71 | CSJS runs in ES6 environments without transpilation, compilation, or build steps (including Node 4+ and latest stable Chrome/Firefox/Safari/Edge). 72 | 73 | [![sauce labs test status][sauce-badge]][sauce-href] 74 | 75 | Of course, you can always transpile ES6 template strings using Babel, allowing you to use CSJS in any ES5 environment. 76 | 77 | ### Framework-agnostic 78 | 79 | CSJS works with any framework, be it React, native Web Components, or something else. 80 | 81 | ### Full power of JavaScript in your CSS 82 | 83 | * Real, full-fledged JavaScript 84 | * Obviates the need for Sass/Less or other preprocessors 85 | * Proper imports/require 86 | * Real variables, functions, loops, etc. 87 | * As extensible as JavaScript itself 88 | 89 | ### Class Composition Syntax 90 | 91 | CSJS also features class composition that works like [CSS Modules]: 92 | 93 | ([Live editable codepen.io demo](http://codepen.io/rtsao/pen/RrmpdX?editors=0010)) 94 | 95 | **common-styles.js** 96 | ```javascript 97 | const csjs = require('csjs'); 98 | 99 | module.exports = csjs` 100 | 101 | .border { 102 | border: 1px solid black; 103 | } 104 | 105 | .italic { 106 | font-family: serif; 107 | font-style: italic; 108 | } 109 | 110 | `; 111 | 112 | ``` 113 | 114 | **quote-styles.js** 115 | ```javascript 116 | const csjs = require('csjs'); 117 | 118 | const common = require('./common-styles'); 119 | 120 | module.exports = csjs` 121 | 122 | .blockQuote extends ${common.italic} { 123 | background: #ccc; 124 | padding: 8px; 125 | border-radius: 4px; 126 | } 127 | 128 | .pullQuote extends .blockQuote, ${common.border} { 129 | background: #eee; 130 | font-weight: bold; 131 | } 132 | 133 | `; 134 | 135 | ``` 136 | 137 | **app.js** 138 | ```javascript 139 | const getCss = require('csjs/get-css'); 140 | const commonStyles = require('./common-styles'); 141 | const quoteStyles = require('./quote-styles'); 142 | 143 | quoteStyles.blockQuote; 144 | // => "blockQuote_2bVd7K italic_3YGtO7" 145 | 146 | quoteStyles.pullQuote; 147 | // => "pullQuote_2bVd7K blockQuote_2bVd7K italic_3YGtO7 border_3YGtO7" 148 | 149 | getCss(quoteStyles); 150 | /* 151 | .blockQuote_2bVd7K { 152 | background: #ccc; 153 | padding: 8px; 154 | border-radius: 4px; 155 | } 156 | 157 | .pullQuote_2bVd7K { 158 | background: #eee; 159 | font-weight: bold; 160 | } 161 | */ 162 | 163 | getCss(commonStyles); 164 | /* 165 | .border_3YGtO7 { 166 | border: 1px solid black; 167 | } 168 | 169 | .italic_3YGtO7 { 170 | font-family: serif; 171 | font-style: italic; 172 | } 173 | */ 174 | ``` 175 | 176 | ### Optional tooling 177 | 178 | #### Extracted static CSS bundles 179 | 180 | [csjs-extractify](https://github.com/rtsao/csjs-extractify) is a browserify plugin that allows you to extract your application's CSJS into a static CSS file at build time. 181 | 182 | #### Automatic CSS injection 183 | 184 | [csjs-injectify](https://github.com/rtsao/csjs-injectify) is a browserify transform that automatically replaces `csjs` with [`csjs-inject`](https://github.com/rtsao/csjs-inject), which automatically injects your scoped CSS into the `` at runtime. It is recommended to use this rather than the [csjs-inject](https://github.com/rtsao/csjs-inject) module directly. 185 | 186 | #### PostCSS 187 | 188 | [babel-plugin-csjs-postcss](https://github.com/rtsao/babel-plugin-csjs-postcss) is a Babel plugin that allows you to run PostCSS on the CSS contained within CSJS template string literals at build time. Works with plugins such as [Autoprefixer]. 189 | 190 | #### Syntax highlighting 191 | [neurosnap](https://github.com/neurosnap) has created an [Atom plugin for syntax highlighting](https://github.com/neurosnap/language-csjs) CSS within CSJS tagged template strings. 192 | 193 | 194 | ## FAQ 195 | 196 | ##### Why the name CSJS? 197 | 198 | CSJS is 100% valid JavaScript, hence the name Cascading Style JavaScripts. 199 | 200 | ##### Why not Sass? 201 | 202 | Sass doesn't provide any way to scope CSS, thus encapsulation of styles in components isn't possible with Sass alone. Additionally, because Sass was designed for use in a global CSS namespace, many of its features just don't make sense when styles are scoped and encapsulated in components. `@extend` in Sass is extremely problematic, whereas CSJS has a proper mechanism for class composition that actually works like it should. Furthermore, with CSJS, you have the ability to use real JavaScript in CSS, which is significantly more powerful and extensible than the language features included in Sass, so there's not really any reason to use Sass at all. 203 | 204 | ##### Why not CSS Modules? 205 | 206 | CSJS was inspired by [CSS Modules] and they are virtually identical in concept. However, unlike CSS Modules which attempts to reproduce an ES6-style module system into CSS itself, CSJS simply uses native JS modules. CSJS also uses normal JS variables whereas CSS Modules invents its own CSS variable syntax. 207 | 208 | Consquently, CSJS is merely plain JavaScript and works without any extra tooling (CSS Modules is not valid CSS). Furthermore, because CSJS is essentially an amalgamation of plain JavaScript and plain CSS, there's no any new syntax or semantics to learn (besides the optional composition syntactic sugar, which closely mimicks ES6 classes). 209 | 210 | ##### Why not Radium? 211 | 212 | Inline styles are cool, but there are limitations to using pure inline styles. For example, CSS pseudo-classes and media queries aren't possible with inline styles. This is the premise behind Radium, which works around this by re-implementing these CSS features using JavaScript. 213 | 214 | Whereas Radium is wholly dependent on React and involves performance trade-offs in its JavaScript implementations of CSS features, CSJS works regardless of framework (or lack thereof) and allows for the use of all CSS features natively (including media queries and pseudo-classes). 215 | 216 | ## See Also 217 | * https://github.com/rtsao/csjs-example-app 218 | * https://github.com/rtsao/csjs-extractify 219 | * https://github.com/rtsao/csjs-injectify 220 | * https://github.com/rtsao/csjs-inject 221 | * https://github.com/rtsao/babel-plugin-csjs-postcss 222 | * https://github.com/rtsao/scope-styles 223 | 224 | ## License 225 | MIT 226 | 227 | [(1)]: #full-power-of-javascript-in-your-css 228 | [(2)]: #class-composition-syntax 229 | [(3)]: #simple-tooling-free 230 | 231 | [CSS Modules]: https://github.com/css-modules/css-modules 232 | [Autoprefixer]: https://github.com/postcss/autoprefixer 233 | 234 | [csjs-bundle]: https://wzrd.in/bundle/csjs@latest 235 | 236 | [npm-badge]: https://badge.fury.io/js/csjs.svg 237 | [npm-href]: https://www.npmjs.com/package/csjs 238 | [build-badge]: https://travis-ci.org/rtsao/csjs.svg?branch=master 239 | [build-href]: https://travis-ci.org/rtsao/csjs 240 | [coverage-badge]: https://coveralls.io/repos/rtsao/csjs/badge.svg?branch=master&service=github 241 | [coverage-href]: https://coveralls.io/github/rtsao/csjs?branch=master 242 | [deps-badge]: https://img.shields.io/badge/dependencies-none-brightgreen.svg 243 | [deps-href]: https://david-dm.org/rtsao/csjs 244 | [sauce-badge]: https://saucelabs.com/browser-matrix/csjs.svg 245 | [sauce-href]: https://saucelabs.com/u/csjs 246 | --------------------------------------------------------------------------------