├── .gitignore ├── .jshintrc ├── .travis.yml ├── HISTORY.md ├── LICENSE.md ├── README.md ├── bin └── css-flip ├── index.js ├── lib ├── background-position.js ├── border-radius.js ├── box-shadow.js ├── direction.js ├── flipProperty.js ├── flipValueOf.js ├── left-right.js ├── quad.js └── transition.js ├── package.json └── test └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "expr": true, 4 | 5 | "predef": [ 6 | "describe", 7 | "it" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | # css-flip history 2 | 3 | ## 0.5.0 - 23 June 2014 4 | 5 | - Support all valid CSS numbers for `background-position`. 6 | - Only flip `background-position` single value if it's a percentage. 7 | - Only flip `background-position` X value of `0%` to `100%`. 8 | - Update to 'css' 2.0.0. 9 | - Add support for `transition` and `transition-property`. 10 | - Drop support for non-standard `border-radius`. 11 | - Fix CLI. 12 | 13 | ## 0.4.0 - 19 May 2014 14 | 15 | - Don't modify names of unflipped properties. 16 | - Add `@replace` directive to perform a precise value substitution. 17 | 18 | ## 0.3.0 - 24 Feb 2014 19 | 20 | - Inital release. 21 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) Twitter, Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # css-flip [![Build Status](https://travis-ci.org/twitter/css-flip.png)](https://travis-ci.org/twitter/css-flip) 2 | 3 | A CSS BiDi flipper. Generate left-to-right (LTR) or right-to-left (RTL) CSS from your source. 4 | 5 | ## Installation 6 | 7 | ```sh 8 | npm install css-flip 9 | ``` 10 | 11 | ## Example use 12 | 13 | ```js 14 | var flip = require('css-flip'); 15 | var css = 'div { float: left; }'; 16 | 17 | flip(css); 18 | // => 'div { float: right; }' 19 | ``` 20 | 21 | As a [Rework](https://github.com/reworkcss/rework) plugin: 22 | 23 | ```js 24 | var flip = require('css-flip'); 25 | var rework = require('rework'); 26 | var css = 'div { float: left; }'; 27 | 28 | rework(css).use(flip.rework()).toString(); 29 | // => 'div { float: right; }' 30 | ``` 31 | 32 | ## Supported CSS Properties (a-z) 33 | 34 | `background-position`, 35 | `background-position-x`, 36 | `border-bottom-left-radius`, 37 | `border-bottom-right-radius`, 38 | `border-color`, 39 | `border-left`, 40 | `border-left-color`, 41 | `border-left-style`, 42 | `border-left-width`, 43 | `border-radius`, 44 | `border-right`, 45 | `border-right-color`, 46 | `border-right-style`, 47 | `border-right-width`, 48 | `border-style`, 49 | `border-top-left-radius`, 50 | `border-top-right-radius`, 51 | `border-width`, 52 | `box-shadow`, 53 | `clear`, 54 | `direction`, 55 | `float`, 56 | `left`, 57 | `margin`, 58 | `margin-left`, 59 | `margin-right`, 60 | `padding`, 61 | `padding-left`, 62 | `padding-right`, 63 | `right`, 64 | `text-align` 65 | `transition` 66 | `transition-property` 67 | 68 | ## Processing directives 69 | 70 | css-flip provides a way to ignore declarations or rules that should not be 71 | flipped, and precisely replace property values. 72 | 73 | ### @noflip 74 | 75 | Prevent a single declaration from being flipped. 76 | 77 | Source: 78 | 79 | ```css 80 | p { 81 | /*@noflip*/ float: left; 82 | clear: left; 83 | } 84 | ``` 85 | 86 | Yields: 87 | 88 | ```css 89 | p { 90 | float: left; 91 | clear: right; 92 | } 93 | ``` 94 | 95 | Prevent all declarations in a rule from being flipped. 96 | 97 | Source: 98 | 99 | ```css 100 | /*@noflip*/ 101 | p { 102 | float: left; 103 | clear: left; 104 | } 105 | ``` 106 | 107 | Yields: 108 | 109 | ```css 110 | p { 111 | float: left; 112 | clear: left; 113 | } 114 | ``` 115 | 116 | ### @replace 117 | 118 | Replace the value of a single declaration. Useful for custom LTR/RTL 119 | adjustments, e.g., changing background sprite positions or using a 120 | different glyph in an icon font. 121 | 122 | Source: 123 | 124 | ```css 125 | p { 126 | /*@replace: -32px -32px*/ background-position: -32px 0; 127 | /*@replace: ">"*/ content: "<"; 128 | } 129 | ``` 130 | 131 | Yields: 132 | 133 | ```css 134 | p { 135 | background-position: -32px -32px; 136 | content: ">"; 137 | } 138 | ``` 139 | 140 | ## CLI 141 | 142 | The CLI can be used globally or locally in a package. 143 | 144 | View available options: 145 | 146 | ``` 147 | css-flip --help 148 | ``` 149 | 150 | Example use: 151 | 152 | ```sh 153 | css-flip path/to/file.css > path/to/file.rtl.css 154 | ``` 155 | 156 | ## Development 157 | 158 | Run the lint and unit tests: 159 | 160 | ``` 161 | npm test 162 | ``` 163 | 164 | Just the JSHint tests: 165 | 166 | ``` 167 | npm run lint 168 | ``` 169 | 170 | Just the Mocha unit tests: 171 | 172 | ``` 173 | npm run unit 174 | ``` 175 | 176 | Run Mocha unit tests in "watch" mode: 177 | 178 | ``` 179 | npm run watch 180 | ``` 181 | 182 | ## License and Acknowledgements 183 | 184 | Copyright 2014 Twitter, Inc. and other contributors. 185 | 186 | Licensed under the MIT License 187 | 188 | css-flip was inspired by [ded/R2](https://github.com/ded/R2) and 189 | [Closure Stylesheets](https://code.google.com/p/closure-stylesheets/). 190 | -------------------------------------------------------------------------------- /bin/css-flip: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /* Copyright 2014 Twitter, Inc. and other contributors; Licensed MIT */ 4 | 5 | var fs = require('fs'); 6 | var program = require('commander'); 7 | var flip = require('..'); 8 | 9 | program 10 | .usage('[options] \n\n css-flip is a CSS BiDi flipper.') 11 | .version(require('../package.json').version) 12 | .option('-c, --compress', 'compress whitespace') 13 | .option('-o, --output ', 'specify an output file') 14 | .option('-, --stdin', 'read from stdin') 15 | .parse(process.argv); 16 | 17 | var options = { compress: program.compress }; 18 | 19 | if (program.stdin) { 20 | flipStdin(options, done); 21 | } 22 | else if (program.args.length) { 23 | flipFiles(program.args, options, done); 24 | } 25 | else { 26 | program.help(); 27 | } 28 | 29 | 30 | // -- Functions -- 31 | 32 | // Writes the given CSS to disk or stdout. 33 | function done(css) { 34 | if (program.output) { 35 | return fs.writeFileSync(program.output, css); 36 | } 37 | 38 | process.stdout.write(css); 39 | } 40 | 41 | // Flips each file and passes the concatenated result to the callback. 42 | function flipFiles(files, options, cb) { 43 | var css = files.reduce(function (acc, file) { 44 | if (!fs.existsSync(file)) { 45 | return acc; 46 | } 47 | 48 | return acc.concat(flip(fs.readFileSync(file, 'utf8'), options)); 49 | }, []); 50 | 51 | cb(css.join('\n')); 52 | } 53 | 54 | // Collects stdin and passes the flipped result to the callback. 55 | function flipStdin(options, cb) { 56 | var css = []; 57 | var stdin = process.stdin; 58 | 59 | stdin.on('data', function (chunk) { 60 | css.push(chunk); 61 | }); 62 | 63 | stdin.on('end', function () { 64 | cb(flip(css.join(''), options)); 65 | }); 66 | 67 | stdin.setEncoding('utf8'); 68 | stdin.resume(); 69 | } 70 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* Copyright 2014 Twitter, Inc. and other contributors; Licensed MIT */ 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | var css = require('css'); 8 | var flipProperty = require('./lib/flipProperty'); 9 | var flipValueOf = require('./lib/flipValueOf'); 10 | 11 | /** 12 | * Constants. 13 | */ 14 | 15 | var RE_NOFLIP = /@noflip/; 16 | var RE_REPLACE = /@replace:\s*(.*)?/; 17 | 18 | /** 19 | * Return whether the given `node` is flippable. 20 | * 21 | * @param {Object} node 22 | * @param {Object} prevNode used for @noflip detection 23 | * @returns {Boolean} 24 | */ 25 | 26 | function isFlippable(node, prevNode) { 27 | if (node.type == 'comment') { 28 | return false; 29 | } 30 | 31 | if (prevNode && prevNode.type == 'comment') { 32 | return !RE_NOFLIP.test(prevNode.comment); 33 | } 34 | 35 | return true; 36 | } 37 | 38 | /** 39 | * Return whether the given `node` is replaceable. 40 | * 41 | * @param {Object} node 42 | * @param {Object} prevNode used for @replace detection 43 | * @returns {Boolean} 44 | */ 45 | 46 | function isReplaceable(node, prevNode) { 47 | if (node.type == 'comment') { 48 | return false; 49 | } 50 | 51 | if (prevNode && prevNode.type == 'comment') { 52 | return RE_REPLACE.test(prevNode.comment); 53 | } 54 | 55 | return false; 56 | } 57 | 58 | /** 59 | * BiDi flip or replace a list of `declarations`. 60 | * 61 | * @param {Array} declarations 62 | */ 63 | 64 | function processDeclarations(declarations) { 65 | return declarations.map(function (declaration, i, all) { 66 | var prevNode = all[i - 1]; 67 | 68 | if (isReplaceable(declaration, prevNode)) { 69 | declaration.value = prevNode.comment.match(RE_REPLACE)[1]; 70 | } 71 | 72 | else if (isFlippable(declaration, prevNode)) { 73 | declaration.property = flipProperty(declaration.property); 74 | declaration.value = flipValueOf(declaration.property, declaration.value); 75 | } 76 | 77 | return declaration; 78 | }); 79 | } 80 | 81 | /** 82 | * BiDi flip a single AST `node`. If the node contains rules, each rule is 83 | * passed recursively to `flipNode()`. If the node contains declarations, 84 | * each flippable declaration is flipped. 85 | * 86 | * @param {Object} node 87 | */ 88 | 89 | function flipNode(node) { 90 | var rules = node.rules || node.keyframes || []; 91 | 92 | rules.forEach(function (rule, i, all) { 93 | if (rule.declarations) { 94 | if (isFlippable(rule, all[i - 1])) { 95 | processDeclarations(rule.declarations); 96 | } 97 | } else { 98 | flipNode(rule); 99 | } 100 | }); 101 | } 102 | 103 | /** 104 | * BiDi flip a CSS string. 105 | * 106 | * @param {String} str 107 | * @param {Object} [options] 108 | * @param {Boolean} [options.compress] Whether to slightly compress output. 109 | * Some newlines and indentation are removed. Comments stay intact. 110 | * @param {String} [options.indent] Default is `' '` (two spaces). 111 | * @returns {String} 112 | */ 113 | 114 | function flip(str, options) { 115 | if (typeof str != 'string') { 116 | throw new Error('input is not a String.'); 117 | } 118 | 119 | var node = css.parse(str, options); 120 | 121 | flipNode(node.stylesheet); 122 | 123 | return css.stringify(node, options); 124 | } 125 | 126 | /** 127 | * A Rework compatible filter. 128 | * 129 | * @returns {Function} 130 | * @param {Object} stylesheet A `stylesheet` AST node. 131 | */ 132 | 133 | function rework() { 134 | return flipNode; 135 | } 136 | 137 | /** 138 | * Module exports. 139 | */ 140 | 141 | exports = module.exports = flip; 142 | exports.rework = rework; 143 | -------------------------------------------------------------------------------- /lib/background-position.js: -------------------------------------------------------------------------------- 1 | var reLeft = /\bleft\b/; 2 | var reRight = /\bright\b/; 3 | var rePct = /^[+-]?\d*(?:\.\d+)?(?:[Ee][+-]?\d+)?%/; 4 | 5 | /** 6 | * Flip a `background-position` value. 7 | * 8 | * @param {String} value Property value 9 | * @param {String} property Property name 10 | * @return {String} 11 | */ 12 | 13 | module.exports = function (value) { 14 | if (value.match(reLeft)) { 15 | value = value.replace(reLeft, 'right'); 16 | } else if (value.match(reRight)) { 17 | value = value.replace(reRight, 'left'); 18 | } 19 | 20 | var elements = value.split(/\s+/); 21 | 22 | if (!elements) { return value; } 23 | 24 | if (elements.length == 1) { 25 | value = flipPercentage(elements[0]); 26 | } 27 | else if (elements.length == 2) { 28 | value = flipPercentage(elements[0]) + ' ' + elements[1]; 29 | } 30 | 31 | return value; 32 | }; 33 | 34 | /** 35 | * Flip a percentage value. 36 | * 30% => 70% (100 - pct) 37 | * 38 | * @param {String} pct Percentage 39 | * @return {String} 40 | */ 41 | 42 | function flipPercentage(value) { 43 | if (rePct.test(value)) { 44 | return (100 - parseFloat(value, 10)) + '%'; 45 | } 46 | 47 | return value; 48 | } 49 | -------------------------------------------------------------------------------- /lib/border-radius.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Flip a `border-radius` value. 3 | * 4 | * @param {String} value Value 5 | * @return {String} 6 | */ 7 | 8 | module.exports = function (value) { 9 | var elements = value.split(/\s*\/\s*/); 10 | 11 | if (!elements) { return value; } 12 | 13 | switch (elements.length) { 14 | // 1px 2px 3px 4px => 2px 1px 4px 3px 15 | case 1: return flipCorners(elements[0]); 16 | 17 | // 1px / 2px 3px => 1px / 3px 2px 18 | // 1px 2px / 3px 4px => 2px 1px / 4px 3px 19 | // etc... 20 | case 2: return flipCorners(elements[0]) + ' / ' + flipCorners(elements[1]); 21 | } 22 | 23 | return value; 24 | }; 25 | 26 | /** 27 | * Flip the corners of a `border-radius` value. 28 | * 29 | * @param {String} value Value 30 | * @return {String} 31 | */ 32 | 33 | function flipCorners(value) { 34 | var elements = value.split(/\s+/); 35 | 36 | if (!elements) { return value; } 37 | 38 | switch (elements.length) { 39 | // 5px 10px 15px 20px => 10px 5px 20px 15px 40 | case 4: return [elements[1], elements[0], elements[3], elements[2]].join(' '); 41 | 42 | // 5px 10px 20px => 10px 5px 10px 20px 43 | case 3: return [elements[1], elements[0], elements[1], elements[2]].join(' '); 44 | 45 | // 5px 10px => 10px 5px 46 | case 2: return [elements[1], elements[0]].join(' '); 47 | } 48 | 49 | return value; 50 | } 51 | -------------------------------------------------------------------------------- /lib/box-shadow.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Flip a `box-shadow` value. 3 | * 4 | * @param {String} value Value 5 | * @return {String} 6 | */ 7 | 8 | module.exports = function (value) { 9 | return value 10 | // Replace commas inside parens (e.g. rbga colors) with a token. 11 | .replace(/\([^)]*\)/g, function (s) { return s.replace(/,/g, '_C_'); }) 12 | // Flip each part of the shadow. 13 | .split(/\s*,\s*/).map(flipShadow).join(', ') 14 | // Restore commas-in-parens. 15 | .replace(/_C_/g, ','); 16 | }; 17 | 18 | /** 19 | * Flip a single `box-shadow` value. 20 | * 21 | * @param {String} value Value 22 | * @return {String} 23 | */ 24 | 25 | function flipShadow(value) { 26 | var elements = value.split(/\s+/); 27 | 28 | if (!elements) { return value; } 29 | 30 | var inset = (elements[0] == 'inset') ? elements.shift() + ' ' : ''; 31 | var property = elements[0].match(/^([-+]?\d+)(\w*)$/); 32 | 33 | if (!property) { return value; } 34 | 35 | return inset + [(-1 * +property[1]) + property[2]] 36 | .concat(elements.splice(1)).join(' '); 37 | } 38 | -------------------------------------------------------------------------------- /lib/direction.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Flip a `direction` value. 3 | * 4 | * @param {String} value Value 5 | * @return {String} 6 | */ 7 | 8 | module.exports = function (value) { 9 | return value.match(/ltr/) ? 'rtl' : value.match(/rtl/) ? 'ltr' : value; 10 | }; 11 | -------------------------------------------------------------------------------- /lib/flipProperty.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Map of property names to their BiDi equivalent. 3 | */ 4 | 5 | var PROPERTIES = { 6 | 'border-left': 'border-right', 7 | 'border-bottom-right-radius': 'border-bottom-left-radius', 8 | 'border-bottom-left-radius': 'border-bottom-right-radius', 9 | 'border-top-right-radius': 'border-top-left-radius', 10 | 'border-top-left-radius': 'border-top-right-radius', 11 | 'border-left-color': 'border-right-color', 12 | 'border-left-style': 'border-right-style', 13 | 'border-left-width': 'border-right-width', 14 | 'border-right': 'border-left', 15 | 'border-right-color': 'border-left-color', 16 | 'border-right-width': 'border-left-width', 17 | 'border-right-style': 'border-left-style', 18 | 'left': 'right', 19 | 'margin-left': 'margin-right', 20 | 'margin-right': 'margin-left', 21 | 'padding-left': 'padding-right', 22 | 'padding-right': 'padding-left', 23 | 'right': 'left' 24 | }; 25 | 26 | /** 27 | * BiDi flip the given property. 28 | * 29 | * @param {String} prop 30 | * @return {String} 31 | */ 32 | 33 | function flipProperty(prop) { 34 | var normalizedProperty = prop.toLowerCase(); 35 | 36 | return PROPERTIES.hasOwnProperty(normalizedProperty) ? PROPERTIES[normalizedProperty] : prop; 37 | } 38 | 39 | /** 40 | * Module exports. 41 | */ 42 | 43 | module.exports = flipProperty; 44 | -------------------------------------------------------------------------------- /lib/flipValueOf.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | 5 | var backgroundPosition = require('./background-position'); 6 | var borderRadius = require('./border-radius'); 7 | var boxShadow = require('./box-shadow'); 8 | var direction = require('./direction'); 9 | var leftRight = require('./left-right'); 10 | var quad = require('./quad'); 11 | var transition = require('./transition'); 12 | 13 | /** 14 | * Map of property values to their BiDi flipping functions. 15 | */ 16 | 17 | var VALUES = { 18 | 'background-position': backgroundPosition, 19 | 'background-position-x': backgroundPosition, 20 | 'border-radius': borderRadius, 21 | 'border-color': quad, 22 | 'border-style': quad, 23 | 'border-width': quad, 24 | 'box-shadow': boxShadow, 25 | 'clear': leftRight, 26 | 'direction': direction, 27 | 'float': leftRight, 28 | 'margin': quad, 29 | 'padding': quad, 30 | 'text-align': leftRight, 31 | 'transition': transition, 32 | 'transition-property': transition 33 | }; 34 | 35 | /** 36 | * BiDi flip the value of a property. 37 | * 38 | * @param {String} prop 39 | * @param {String} value 40 | * @return {String} 41 | */ 42 | 43 | function flipValueOf(prop, value) { 44 | var RE_IMPORTANT = /\s*!important/; 45 | var RE_PREFIX = /^-[a-zA-Z]+-/; 46 | 47 | // find normalized property name (removing any vendor prefixes) 48 | var normalizedProperty = prop.toLowerCase().trim(); 49 | normalizedProperty = (RE_PREFIX.test(normalizedProperty) ? normalizedProperty.split(RE_PREFIX)[1] : normalizedProperty); 50 | 51 | var flipFn = VALUES.hasOwnProperty(normalizedProperty) ? VALUES[normalizedProperty] : false; 52 | 53 | if (!flipFn) { return value; } 54 | 55 | var important = value.match(RE_IMPORTANT); 56 | var newValue = flipFn(value.replace(RE_IMPORTANT, '').trim(), prop); 57 | 58 | if (important && !RE_IMPORTANT.test(newValue)) { 59 | newValue += important[0]; 60 | } 61 | 62 | return newValue; 63 | } 64 | 65 | /** 66 | * Module exports. 67 | */ 68 | 69 | module.exports = flipValueOf; 70 | -------------------------------------------------------------------------------- /lib/left-right.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Flip a `left` or `right` value. 3 | * 4 | * @param {String} value Value 5 | * @return {String} 6 | */ 7 | 8 | module.exports = function (value) { 9 | return value.match(/^\s*left\s*$/) ? 'right' : value.match(/^\s*right\s*$/) ? 'left' : value; 10 | }; 11 | -------------------------------------------------------------------------------- /lib/quad.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Flip a value consisting of 4 parts. 3 | * 4 | * @param {String} value Value 5 | * @return {String} 6 | */ 7 | 8 | module.exports = function (value) { 9 | // Tokenize any rgb[a]/hsl[a] colors before flipping. 10 | var colors = []; 11 | var matches = value.match(/(?:rgb|hsl)a?\([^\)]*\)/g); 12 | 13 | if (matches) { 14 | matches.forEach(function (color, i) { 15 | colors[i] = color; 16 | value = value.replace(color, '_C' + i + '_'); 17 | }); 18 | } 19 | 20 | var elements = value.split(/\s+/); 21 | 22 | if (elements && elements.length == 4) { 23 | // 1px 2px 3px 4px => 1px 4px 3px 2px 24 | value = [elements[0], elements[3], elements[2], elements[1]].join(' '); 25 | } 26 | 27 | if (colors.length) { 28 | // Replace any tokenized colors. 29 | return value.replace(/_C(\d+)_/g, function (match, i) { 30 | return match && colors[i]; 31 | }); 32 | } 33 | 34 | return value; 35 | }; 36 | -------------------------------------------------------------------------------- /lib/transition.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | 5 | var flipProperty = require('./flipProperty'); 6 | 7 | /** 8 | * Flip properties in transitions. 9 | * 10 | * @param {String} value Value 11 | * @return {String} 12 | */ 13 | 14 | function transition(value) { 15 | var RE_PROP = /^\s*([a-zA-z\-]+)/; 16 | var parts = value.split(/\s*,\s*/); 17 | 18 | return parts.map(function (part) { 19 | // extract the property if the value is for the `transition` shorthand 20 | if (RE_PROP.test(part)) { 21 | var prop = part.match(RE_PROP)[1]; 22 | var newProp = flipProperty(prop); 23 | part = part.replace(RE_PROP, newProp); 24 | } 25 | 26 | return part; 27 | }).join(', '); 28 | } 29 | 30 | /** 31 | * Module exports. 32 | */ 33 | 34 | module.exports = transition; 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "css-flip", 3 | "version": "0.5.0", 4 | "description": "A CSS BiDi flipper.", 5 | "license": "MIT", 6 | "main": "index.js", 7 | "bin": { 8 | "css-flip": "bin/css-flip" 9 | }, 10 | "files": [ 11 | "index.js", 12 | "bin", 13 | "lib" 14 | ], 15 | "dependencies": { 16 | "commander": "~2.1.0", 17 | "css": "^2.0.0" 18 | }, 19 | "devDependencies": { 20 | "mocha": "~1.17.1", 21 | "jshint": "~2.4.3" 22 | }, 23 | "scripts": { 24 | "lint": "jshint --show-non-errors bin/css-flip **/*.js", 25 | "test": "npm run lint & npm run unit", 26 | "unit": "mocha --reporter spec", 27 | "watch": "mocha --reporter min --watch" 28 | }, 29 | "keywords": [ 30 | "css", 31 | "ltr", 32 | "rtl", 33 | "bidi", 34 | "flip" 35 | ], 36 | "author": "Brett Stimmerman (http://twitter.com/bretts)", 37 | "repository": { 38 | "type": "git", 39 | "url": "https://github.com/twitter/css-flip.git" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var css = require('css'); 3 | var flip = require('..'); 4 | 5 | function ensure(input, output, options) { 6 | output || (output = input); 7 | options || (options = {compress: true}); 8 | 9 | assert.equal(flip(input, options), output, input + ' => ' + output); 10 | } 11 | 12 | describe('background-position', function () { 13 | it('should flip direction', function () { 14 | ensure('p{background-position:left top;}', 'p{background-position:right top;}'); 15 | ensure('p{background-position:20px;}', 'p{background-position:20px;}'); 16 | ensure('p{background-position:20%;}', 'p{background-position:80%;}'); 17 | ensure('p{background-position:0% 0;}', 'p{background-position:100% 0;}'); 18 | ensure('p{background-position:24.5% 0;}', 'p{background-position:75.5% 0;}'); 19 | ensure('p{background-position:+2.45e1% 0;}', 'p{background-position:75.5% 0;}'); 20 | ensure('p{background-position:-20% 0;}', 'p{background-position:120% 0;}'); 21 | ensure('p{background-position:20% top;}', 'p{background-position:80% top;}'); 22 | ensure('p{background-position:100% 0;}', 'p{background-position:0% 0;}'); 23 | 24 | ensure('p{background-position-x:20px;}', 'p{background-position-x:20px;}'); 25 | ensure('p{background-position-x:20%;}', 'p{background-position-x:80%;}'); 26 | ensure('p{-ms-background-position-x:30%;}', 'p{-ms-background-position-x:70%;}'); 27 | }); 28 | }); 29 | 30 | describe('border', function () { 31 | it('should flip border-{side}', function () { 32 | ensure('p{border-left:1px;}', 'p{border-right:1px;}'); 33 | ensure('p{border-right:1px;}', 'p{border-left:1px;}'); 34 | ensure('p{border-right:1px solid #000;}', 'p{border-left:1px solid #000;}'); 35 | }); 36 | 37 | it('should flip border-style', function () { 38 | ensure('p{border-style:solid;}'); 39 | ensure('p{border-style:none solid;}'); 40 | ensure('p{border-style:none solid dashed;}'); 41 | ensure('p{border-style:none solid dashed double;}', 'p{border-style:none double dashed solid;}'); 42 | }); 43 | 44 | it('should flip border-{side}-color', function () { 45 | ensure('p{border-left-color:#fff;}', 'p{border-right-color:#fff;}'); 46 | ensure('p{border-right-color:#fff;}', 'p{border-left-color:#fff;}'); 47 | }); 48 | 49 | it('should flip border-{side}-style', function () { 50 | ensure('p{border-left-style:solid;}', 'p{border-right-style:solid;}'); 51 | ensure('p{border-left-style:none;}', 'p{border-right-style:none;}'); 52 | ensure('p{border-right-style:dashed;}', 'p{border-left-style:dashed;}'); 53 | ensure('p{border-right-style:double;}', 'p{border-left-style:double;}'); 54 | }); 55 | 56 | it('should flip border-width', function () { 57 | ensure('p{border-width:0;}'); 58 | ensure('p{border-width:0 1px;}'); 59 | ensure('p{border-width:0 1px 2px;}'); 60 | ensure('p{border-width:0 1px 2px 3px;}', 'p{border-width:0 3px 2px 1px;}'); 61 | }); 62 | 63 | it('should flip border-{side}-width', function () { 64 | ensure('p{border-left-width:0;}', 'p{border-right-width:0;}'); 65 | ensure('p{border-right-width:0;}', 'p{border-left-width:0;}'); 66 | }); 67 | }); 68 | 69 | describe('border-color', function () { 70 | it('should flip hex colors', function () { 71 | ensure('p{border-color:#fff;}'); 72 | ensure('p{border-color:#fff #000;}'); 73 | ensure('p{border-color:#000 #111111 #fff;}'); 74 | ensure('p{border-color:#000 #111111 #222222 #333;}', 'p{border-color:#000 #333 #222222 #111111;}'); 75 | }); 76 | 77 | it('should flip keyword colors', function () { 78 | ensure('p{border-color:white;}'); 79 | ensure('p{border-color:white black;}'); 80 | ensure('p{border-color:white red black;}'); 81 | ensure('p{border-color:white blue yellow black;}', 'p{border-color:white black yellow blue;}'); 82 | }); 83 | 84 | it('should flip rbg[a] colors', function () { 85 | ensure('p{border-color:rgb(255,255,255);}'); 86 | ensure('p{border-color:rgb(255, 255, 255) rgb(0, 0, 0);}'); 87 | ensure('p{border-color:rgb(255,255,255) rgb(0,0,0) rgb(128,128,128);}'); 88 | ensure( 89 | 'p{border-color:rgb(0,0,0) rgb(10,10,10) rgb(20,20,20) rgb(30,30,30);}', 90 | 'p{border-color:rgb(0,0,0) rgb(30,30,30) rgb(20,20,20) rgb(10,10,10);}' 91 | ); 92 | 93 | ensure('p{border-color:rgba(255,255,255,1);}'); 94 | ensure('p{border-color:rgba(255, 255, 255, 1) rgba(0, 0, 0, 1);}'); 95 | ensure('p{border-color:rgba(255,255,255,1) rgba(0,0,0,1) rgba(128,128,128,1);}'); 96 | ensure( 97 | 'p{border-color:rgba(0,0,0,1) rgba(10,10,10,1) rgba(20,20,20,1) rgba(30,30,30,1);}', 98 | 'p{border-color:rgba(0,0,0,1) rgba(30,30,30,1) rgba(20,20,20,1) rgba(10,10,10,1);}' 99 | ); 100 | }); 101 | 102 | it('should flip hsl[a] colors', function () { 103 | ensure('p{border-color:hsl(255,255,255);}'); 104 | ensure('p{border-color:hsl(255, 255, 255) hsl(0, 0, 0);}'); 105 | ensure('p{border-color:hsl(255,255,255) hsl(0,0,0) hsl(128,128,128);}'); 106 | ensure( 107 | 'p{border-color:rgb(0,0,0) rgb(10,10,10) rgb(20,20,20) rgb(30,30,30);}', 108 | 'p{border-color:rgb(0,0,0) rgb(30,30,30) rgb(20,20,20) rgb(10,10,10);}' 109 | ); 110 | 111 | ensure('p{border-color:hsla(255,255,255,1);}'); 112 | ensure('p{border-color:hsla(255, 255, 255, 1) hsla(0, 0, 0, 1);}'); 113 | ensure('p{border-color:hsla(255,255,255,1) hsla(0,0,0,1) hsla(128,128,128,1);}'); 114 | ensure( 115 | 'p{border-color:hsla(0,0,0,1) hsla(10,10,10,1) hsla(20,20,20,1) hsla(30,30,30,1);}', 116 | 'p{border-color:hsla(0,0,0,1) hsla(30,30,30,1) hsla(20,20,20,1) hsla(10,10,10,1);}' 117 | ); 118 | }); 119 | }); 120 | 121 | describe('border-radius', function () { 122 | it('should flip border-radius', function () { 123 | ensure('p{border-radius:0;}'); 124 | 125 | // top-left+bottom-right top-right+bottom-left 126 | ensure('p{border-radius:0 1px;}', 'p{border-radius:1px 0;}'); 127 | 128 | // top-left top-right+bottom-left bottom-right 129 | ensure('p{border-radius:0 1px 2px;}', 'p{border-radius:1px 0 1px 2px;}'); 130 | 131 | // top-left top-right bottom-left bottom-right 132 | ensure('p{border-radius:0 1px 2px 3px;}', 'p{border-radius:1px 0 3px 2px;}'); 133 | }); 134 | 135 | it('should flip elliptical values', function () { 136 | // radii-x / radii-y 137 | ensure('p{border-radius:1px/2px 3px;}', 'p{border-radius:1px / 3px 2px;}'); 138 | ensure('p{border-radius:1px 2px 3px 4px/5px;}', 'p{border-radius:2px 1px 4px 3px / 5px;}'); 139 | 140 | ensure('p{border-radius:1px / 2px;}'); 141 | ensure('p{border-radius:1px 2px / 3px;}', 'p{border-radius:2px 1px / 3px;}'); 142 | ensure('p{border-radius:1px 2px 3px / 4px;}', 'p{border-radius:2px 1px 2px 3px / 4px;}'); 143 | ensure('p{border-radius:1px 2px 3px 4px / 5px;}', 'p{border-radius:2px 1px 4px 3px / 5px;}'); 144 | 145 | ensure('p{border-radius:1px / 2px 3px;}', 'p{border-radius:1px / 3px 2px;}'); 146 | ensure('p{border-radius:1px / 2px 3px 4px;}', 'p{border-radius:1px / 3px 2px 3px 4px;}'); 147 | ensure('p{border-radius:1px / 2px 3px 4px 5px;}', 'p{border-radius:1px / 3px 2px 5px 4px;}'); 148 | }); 149 | 150 | it('should flip border-top-left-radius', function () { 151 | ensure('p{border-top-left-radius:0;}', 'p{border-top-right-radius:0;}'); 152 | }); 153 | 154 | it('should flip border-top-right-radius', function () { 155 | ensure('p{border-top-right-radius:0;}', 'p{border-top-left-radius:0;}'); 156 | }); 157 | 158 | it('should flip border-bottom-left-radius', function () { 159 | ensure('p{border-bottom-left-radius:0;}', 'p{border-bottom-right-radius:0;}'); 160 | }); 161 | 162 | it('should flip border-bottom-right-radius', function () { 163 | ensure('p{border-bottom-right-radius:0;}', 'p{border-bottom-left-radius:0;}'); 164 | }); 165 | }); 166 | 167 | describe('box-shadow', function () { 168 | it('should flip shorthand properties', function () { 169 | ensure('p{box-shadow:5px 10px #000;}','p{box-shadow:-5px 10px #000;}'); 170 | ensure('p{box-shadow:5px 10px 20px #000;}','p{box-shadow:-5px 10px 20px #000;}'); 171 | ensure('p{box-shadow:5px 10px 20px 30px #000;}','p{box-shadow:-5px 10px 20px 30px #000;}'); 172 | ensure('p{box-shadow:inset 5px 10px 20px 30px #000;}','p{box-shadow:inset -5px 10px 20px 30px #000;}'); 173 | }); 174 | 175 | it('should flip multiple values', function () { 176 | ensure('p{box-shadow:5px 10px #000, 6px 11px #111}','p{box-shadow:-5px 10px #000, -6px 11px #111;}'); 177 | ensure('p{box-shadow:5px 10px #000, inset 6px 11px #111, 7px 12px #222;}','p{box-shadow:-5px 10px #000, inset -6px 11px #111, -7px 12px #222;}'); 178 | ensure('p{box-shadow:5px 10px rgba(0,0,0,1), 6px 11px rgba(10,10,10,1)}','p{box-shadow:-5px 10px rgba(0,0,0,1), -6px 11px rgba(10,10,10,1);}'); 179 | }); 180 | 181 | it('should flip vendor prefixed properties', function () { 182 | ensure('p{-webkit-box-shadow:5px 10px #000;}', 'p{-webkit-box-shadow:-5px 10px #000;}'); 183 | ensure('p{-moz-box-shadow:5px 10px #000;}', 'p{-moz-box-shadow:-5px 10px #000;}'); 184 | }); 185 | 186 | it('should handle the value "none"', function () { 187 | ensure('p{box-shadow:none;}'); 188 | }); 189 | }); 190 | 191 | describe('clear', function () { 192 | it('should flip direction', function () { 193 | ensure('p{clear:left;}', 'p{clear:right;}'); 194 | ensure('p{clear:right;}', 'p{clear:left;}'); 195 | }); 196 | }); 197 | 198 | describe('direction', function () { 199 | it('should flip direction', function () { 200 | ensure('p{direction:ltr;}', 'p{direction:rtl;}'); 201 | ensure('p{direction:rtl;}', 'p{direction:ltr;}'); 202 | }); 203 | }); 204 | 205 | describe('float', function () { 206 | it('should flip direction', function () { 207 | ensure('p{float:left;}', 'p{float:right;}'); 208 | ensure('p{float:right;}', 'p{float:left;}'); 209 | }); 210 | }); 211 | 212 | describe('margin', function () { 213 | it('should flip shorthand properties', function () { 214 | ensure('p{margin:0;}'); 215 | ensure('p{margin:0 1px;}'); 216 | ensure('p{margin:0 1px 2px;}'); 217 | ensure('p{margin:0 1px 2px 3px;}', 'p{margin:0 3px 2px 1px;}'); 218 | }); 219 | 220 | it('should flip margin-{side}', function () { 221 | ensure('p{margin-left:0;}', 'p{margin-right:0;}'); 222 | ensure('p{margin-right:0;}', 'p{margin-left:0;}'); 223 | }); 224 | }); 225 | 226 | describe('padding', function () { 227 | it('should flip shorthand properties', function () { 228 | ensure('p{padding:0;}'); 229 | ensure('p{padding:0 1px;}'); 230 | ensure('p{padding:0 1px 2px;}'); 231 | ensure('p{padding:0 1px 2px 3px;}', 'p{padding:0 3px 2px 1px;}'); 232 | }); 233 | 234 | it('should flip padding-{side}', function () { 235 | ensure('p{padding-left:0;}', 'p{padding-right:0;}'); 236 | ensure('p{padding-right:0;}', 'p{padding-left:0;}'); 237 | }); 238 | }); 239 | 240 | describe('position', function () { 241 | it('should flip direction', function () { 242 | ensure('p{left:50%;}', 'p{right:50%;}'); 243 | ensure('p{right:50%;}', 'p{left:50%;}'); 244 | }); 245 | }); 246 | 247 | describe('text-align', function () { 248 | it('should flip direction', function () { 249 | ensure('p{text-align:left;}', 'p{text-align:right;}'); 250 | ensure('p{text-align:right;}', 'p{text-align:left;}'); 251 | }); 252 | }); 253 | 254 | describe('transition', function () { 255 | it('should flip properties', function () { 256 | ensure('p{-webkit-transition:left 1s, top 1s;}', 'p{-webkit-transition:right 1s, top 1s;}'); 257 | ensure('p{transition:margin-right 1s ease;}', 'p{transition:margin-left 1s ease;}'); 258 | }); 259 | }); 260 | 261 | describe('transition-property', function () { 262 | it('should flip properties', function () { 263 | ensure('p{-webkit-transition-property:left;}', 'p{-webkit-transition-property:right;}'); 264 | ensure('p{transition-property:margin-right;}', 'p{transition-property:margin-left;}'); 265 | ensure('p{transition-property:left, right, padding-left, color;}', 'p{transition-property:right, left, padding-right, color;}'); 266 | }); 267 | }); 268 | 269 | describe('!important', function () { 270 | it('should be retained', function () { 271 | ensure('p{color:blue !important;}'); 272 | ensure('p{float:left !important;}', 'p{float:right !important;}'); 273 | }); 274 | }); 275 | 276 | describe('@noflip', function () { 277 | it('should advise individual declarations', function () { 278 | ensure('p{/*@noflip*/border-left:1px;padding-right:1px;}', 'p{border-left:1px;padding-left:1px;}'); 279 | ensure('p{border-left:1px;/*@noflip*/padding-right:1px;}', 'p{border-right:1px;padding-right:1px;}'); 280 | }); 281 | 282 | it('should advise entire rules', function () { 283 | ensure('/*@noflip*/p {border-left:1px;padding-right:1px;}', 'p{border-left:1px;padding-right:1px;}'); 284 | }); 285 | }); 286 | 287 | describe('@replace', function () { 288 | it('should replace individual declaration value', function () { 289 | ensure('p{/*@replace:-30px 0*/background-position:0 0;}', 'p{background-position:-30px 0;}'); 290 | ensure('p{/*@replace:linear-gradient(to right, blue, green)*/background:linear-gradient(to left, blue, green);}', 'p{background:linear-gradient(to right, blue, green);}'); 291 | ensure('p{/*@replace:"<°"*/content:">°";}', 'p{content:"<°";}'); 292 | }); 293 | }); 294 | 295 | describe('nested blocks', function () { 296 | it('should be flipped', function () { 297 | ensure('@keyframes foo{from{float:left;}to{float:right;}}', 298 | '@keyframes foo{from{float:right;}to{float:left;}}'); 299 | ensure('@media print{.foo{float:right;}}','@media print{.foo{float:left;}}'); 300 | ensure('@supports (box-shadow){@media print{.foo{float:right;}}}', 301 | '@supports (box-shadow){@media print{.foo{float:left;}}}'); 302 | }); 303 | }); 304 | 305 | describe('lettercase', function () { 306 | it('should not lowercase property names that have not been flipped', function () { 307 | ensure(':root{--Custom-prop:red;}', ':root{--Custom-prop:red;}'); 308 | }); 309 | 310 | it('should lowercase property names that have been flipped', function () { 311 | ensure('p{BORDER-LEFT:1px;}', 'p{border-right:1px;}'); 312 | }); 313 | 314 | it('should not lowercase property values', function () { 315 | ensure('p{border-left:1PX;}', 'p{border-right:1PX;}'); 316 | }); 317 | }); 318 | 319 | describe('invalid CSS', function () { 320 | it('should not crash', function () { 321 | ensure('p{__proto__:42;toString:lolwat;}', 'p{__proto__:42;toString:lolwat;}'); 322 | }); 323 | }); 324 | 325 | describe('rework', function () { 326 | var input = 'p{border-left:1px;}'; 327 | var expect = 'p{border-right:1px;}'; 328 | 329 | it('should support the Rework API', function () { 330 | var ast = css.parse(input); 331 | flip.rework()(ast.stylesheet); 332 | var out = css.stringify(ast, {compress: true}); 333 | 334 | assert.equal(out, expect, out + ' != ' + expect); 335 | }); 336 | }); 337 | --------------------------------------------------------------------------------