├── run-tests.js ├── fixtures ├── output_clean.css ├── input_clean.css ├── output_all.css └── input_all.css ├── package.json ├── LICENCE.md ├── CHANGES.md ├── bin └── flipcss.bin.js ├── README.md ├── test ├── bin-test.js └── test.js └── lib └── flipcss.js /run-tests.js: -------------------------------------------------------------------------------- 1 | require("./test/bin-test"); 2 | require("./test/test"); 3 | -------------------------------------------------------------------------------- /fixtures/output_clean.css: -------------------------------------------------------------------------------- 1 | body{direction:ltr;}.foo { 2 | 3 | display: inline; 4 | float: right; 5 | padding: 1em; 6 | margin-left: 2em; 7 | margin-right: 3em; 8 | } 9 | #bar { 10 | background: url("@{image-url}/foo.bar") 60% 0 no-repeat; 11 | margin: 1px 2px 3px 4px; 12 | font-style: italic; /* !ltr-only */ 13 | 14 | clear: left; 15 | 16 | .baz { 17 | text-align: right; 18 | display: inline-block; 19 | 20 | margin-right: 2em; 21 | padding: 1em 2em 3em 4em; /*!direction-ignore*/ 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /fixtures/input_clean.css: -------------------------------------------------------------------------------- 1 | .foo { 2 | font-style: normal !important; /* !rtl-only */ 3 | display: inline; 4 | float: right; 5 | padding: 1em; 6 | margin-left: 2em; 7 | margin-right: 3em; 8 | } 9 | #bar { 10 | background: url("@{image-url}/foo.bar") 60% 0 no-repeat; 11 | margin: 1px 2px 3px 4px; 12 | font-style: italic; /* !ltr-only */ 13 | float: right; /* !rtl-only some comment */ 14 | clear: left; 15 | 16 | .baz { 17 | text-align: right; 18 | display: inline-block; 19 | font-style: italic; 20 | /*!rtl-only */ 21 | margin-right: 2em;font-style: italic; /* !rtl-only */ 22 | padding: 1em 2em 3em 4em; /*!direction-ignore*/ 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Øyvind Håkestad (https://github.com/oyvindeh)", 3 | "contributors": [ 4 | "Neil Jenkins (https://github.com/neilj)", 5 | "Torgeir Lorange Østby (https://github.com/torgeilo)" 6 | ], 7 | "name": "flipcss", 8 | "description": "Flip CSS from ltr to rtl (and vice versa).", 9 | "version": "0.2.8", 10 | "homepage": "https://github.com/oyvindeh/flipcss", 11 | "repository": { 12 | "type": "git", 13 | "url": "git@github.com:oyvindeh/flipcss.git" 14 | }, 15 | "main": "./lib/flipcss.js", 16 | "bin": { 17 | "flipcss": "bin/flipcss.bin.js" 18 | }, 19 | "keywords": [ 20 | "css", 21 | "rtl", 22 | "ltr", 23 | "right-to-left", 24 | "left-to-right", 25 | "i18n", 26 | "internationalization" 27 | ], 28 | "devDependencies": { 29 | "buster": "0.7.x", 30 | "sinon": "1.17.x" 31 | }, 32 | "license": "BSD", 33 | "scripts": { 34 | "test": "node run-tests.js" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /fixtures/output_all.css: -------------------------------------------------------------------------------- 1 | * { 2 | line-height: 1.3em; 3 | margin: 0; 4 | 5 | padding: 0; 6 | } 7 | body {direction:rtl; 8 | background: black; 9 | } 10 | .foo { 11 | display: inline; 12 | float: left; 13 | padding: 1em; 14 | margin-right: 2em; 15 | margin-left: 3em; 16 | } 17 | #bar { 18 | background: url("@{image-url}/foo.bar") 40% 0 no-repeat; 19 | margin: 1px 4px 3px 2px; 20 | float: left; 21 | clear: right; 22 | } 23 | .baz { 24 | position: relative; 25 | background-position: 20% 10%; 26 | left: 1em; 27 | clear: both; 28 | color: red; 29 | font-style: italic; 30 | padding: 1em 4em 3em 2em; 31 | float: right; /*!direction-ignore*/ 32 | 33 | .qux { 34 | text-align: left; 35 | display: inline-block; 36 | margin-left: 2em; 37 | padding: 1em 2em 3em 4em; /*!direction-ignore*/ 38 | } 39 | } 40 | .quux .corge { 41 | padding-left: 2em; 42 | position: absolute; 43 | top: 0; 44 | right: 10em; 45 | } 46 | .pull-left { 47 | float: right !important; /* !direction-ignore */ 48 | } 49 | .grault { 50 | display: inline; 51 | } 52 | .comment { 53 | float: right !important; /* !direction-ignore comment */ 54 | background: red; 55 | } -------------------------------------------------------------------------------- /fixtures/input_all.css: -------------------------------------------------------------------------------- 1 | * { 2 | line-height: 1.3em; 3 | margin: 0; 4 | font-style: normal !important; /* !ltr-only */ 5 | padding: 0; 6 | } 7 | body { 8 | background: black; 9 | } 10 | .foo { 11 | display: inline; 12 | float: right; 13 | padding: 1em; 14 | margin-left: 2em; 15 | margin-right: 3em; 16 | } 17 | #bar { 18 | background: url("@{image-url}/foo.bar") 60% 0 no-repeat; 19 | margin: 1px 2px 3px 4px; 20 | float: right; 21 | clear: left; 22 | } 23 | .baz { 24 | position: relative; 25 | background-position: 80% 10%; 26 | right: 1em; 27 | clear: both; 28 | color: red; 29 | font-style: italic; 30 | padding: 1em 2em 3em 4em; 31 | float: right; /*!direction-ignore*/ 32 | 33 | .qux { 34 | text-align: right; 35 | display: inline-block; 36 | margin-right: 2em; 37 | padding: 1em 2em 3em 4em; /*!direction-ignore*/ 38 | } 39 | } 40 | .quux .corge { 41 | padding-right: 2em; 42 | position: absolute; 43 | top: 0; 44 | left: 10em; 45 | } 46 | .pull-right { 47 | float: right !important; /* !direction-ignore */ 48 | } 49 | .grault { 50 | display: inline; 51 | } 52 | .comment { 53 | float: right !important; /* !direction-ignore comment */ 54 | background: red; 55 | } -------------------------------------------------------------------------------- /LICENCE.md: -------------------------------------------------------------------------------- 1 | This code is offered under the Open Source [BSD license](http://www.opensource.org/licenses/bsd-license.php). 2 | 3 | # BSD License 4 | 5 | Copyright © 2016, Øyvind Håkestad 6 | Copyright © 2012, Opera Software 7 | All rights reserved. 8 | 9 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 10 | 11 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 12 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 13 | * Neither the name of Opera Software nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | ## 2014.08.01, Version 0.2.8 2 | 3 | * Fix #9, add option for not swapping the words "left" and "right" in URLs. 4 | * Add option for not swapping the words "left" and "right" in selectors. 5 | 6 | ## 2014.07.25, Version 0.2.7 7 | 8 | * Fix #8, add support for swapping :before and :after. 9 | 10 | ## 2014.03.18, Version 0.2.5 11 | 12 | * Fix #7, add command line-option for clean-only. 13 | 14 | ## 2013.01.14, Version 0.2.4 15 | 16 | * Fix bug in 0.2.3, where meta comments with extra text were not removed by clean(). 17 | 18 | ## 2013.01.14, Version 0.2.3 19 | 20 | * Add support for extra text in a meta comment, e.g. "/* !rtl-only some extra text */" 21 | 22 | ## 2012.06.28, Version 0.2.2 23 | 24 | * Add command line support. 25 | 26 | ## 2012.06.20, Version 0.2.1 27 | 28 | * Fix bug where value swapping fails for rules with extra keywords (like "!important"). 29 | * Refactor tests: Get rid of most fixtures. 30 | 31 | ## 2012.06.20, Version 0.2.0 32 | 33 | * Add support for rtl to ltr. Adding of CSS direction rule is moved from 34 | flip() to clean(). If both functions are used together, this should be 35 | backwards compatible. If you only use flip(), be aware that 36 | "direction:ltr;" will no longer be added to the output of this function. 37 | 38 | ## 2012.06.18, Version 0.1.4 39 | 40 | * Fix bug: Direction specific rules where swapped, but they are now left 41 | unchanged. 42 | * Refactor tests. 43 | 44 | ## 2012.06.15, Version 0.1.3 45 | 46 | * Change swapping of the words "left" and "right" to be more conservative: 47 | Instead of swapping all instances, the ones that are part of other words 48 | (e.g. "copyright") are now left alone. When separated by other characters 49 | than letters and digits, they will be swapped. 50 | * Make lib remove newlines before meta comments (e.g. "/*!direction-ignore*/"), 51 | which may have been added by e.g. CSS compilers. 52 | 53 | ## 2012.06.14, Version 0.1.2 54 | 55 | * Fix bug where negative horizonal values in background positions where not 56 | recognized, causing vertical values to be flipped instead. Also, skip 57 | negative values. 58 | 59 | ## 2012.06.07, Version 0.1.1 60 | 61 | * Fix bug where e.g. values in linear gradients were mistaken for background 62 | position values. 63 | -------------------------------------------------------------------------------- /bin/flipcss.bin.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var flipcss = require('../lib/flipcss'); 4 | var fs = require('fs'); 5 | 6 | /** 7 | * Handle command line arguments 8 | * @param {Array} argv Command line arguments (with commands stripped off) 9 | * @throws {InvalidArgumentsError} If invalid argument(s) 10 | * @returns {Object} with options, or null. 11 | */ 12 | function handleArgv(argv) { 13 | // Usage info 14 | var usage = ["Usage: node flipcss [OPTION] ... INFILE OUTFILE", 15 | " -r, --rtl Flip CSS LTR>RTL", 16 | " -l, --ltr Flip CSS RTL>LTR", 17 | " -w, --warnings Output warnings", 18 | " -h, --help Usage information", 19 | " -c, --clean-only Clean only (requires a direction, -r or -l)", 20 | " -p, --swap-pseudo Swap :before and :after", 21 | " -u, --ignore-urls Do not swap the words left and right inside url()", 22 | " -s, --ignore-selectors Do not swap the words left and right in selectors", 23 | "If no direction is given, the CSS is just flipped (with no cleaning of direction specific rules)." 24 | ].join("\n"); 25 | 26 | // Asked for help 27 | if (argv[0] === "-h" || argv[0] === "--help") { 28 | console.log(usage.toString()); 29 | return null; 30 | } 31 | 32 | // Vars 33 | var direction = "none"; 34 | var warnings = false; 35 | var cleanOnly = false; 36 | var swapPseudo = false; 37 | var flipUrls = true; 38 | var flipSelectors = true; 39 | var validArgs = { 40 | "-r": "rtl", 41 | "--rtl": "rtl", 42 | "-l": "ltr", 43 | "--ltr": "ltr", 44 | "-w": "warnings", 45 | "--warnings": "warnings", 46 | "-c": "cleanonly", 47 | "--clean-only": "cleanonly", 48 | "-p": "swappseudo", 49 | "--swap-pseudo": "swappseudo", 50 | "-u": "ignoreurls", 51 | "--ignore-urls": "ignoreurls", 52 | "-s": "ignoreselectors", 53 | "--ignore-selectors": "ignoreselectors" 54 | }; 55 | var optCount = 0; 56 | 57 | // Process args 58 | for (var arg in validArgs) { 59 | if(validArgs.hasOwnProperty(arg)) { 60 | var i = argv.indexOf(arg); 61 | if (-1 < i) { 62 | optCount++; 63 | 64 | argv.splice(i,1); 65 | 66 | switch (validArgs[arg]) { 67 | case 'rtl': 68 | direction = "rtl"; 69 | break; 70 | case 'ltr': 71 | direction = "ltr"; 72 | break; 73 | case 'warnings': 74 | warnings = true; 75 | break; 76 | case 'cleanonly': 77 | cleanOnly = true; 78 | break; 79 | case 'swappseudo': 80 | swapPseudo = true; 81 | break; 82 | case 'ignoreurls': 83 | flipUrls = false; 84 | break; 85 | case 'ignoreselectors': 86 | flipSelectors = false; 87 | break; 88 | } 89 | } 90 | } 91 | } 92 | 93 | // Invalid arguments 94 | if (2 < optCount || 95 | argv.length !== 2 || 96 | (cleanOnly && direction === "none")) 97 | { 98 | throw { name: "InvalidArgumentsError", 99 | message: "Invalid option(s).\n" + usage.toString() }; 100 | } 101 | 102 | return { 103 | direction: direction, 104 | warnings: warnings, 105 | cleanOnly: cleanOnly, 106 | swapPseudo: swapPseudo, 107 | flipUrls: flipUrls, 108 | flipSelectors: flipSelectors, 109 | input: argv[0], 110 | output: argv[1] 111 | }; 112 | } 113 | 114 | 115 | /** 116 | * Transform CSS from LTR>RTL or vice versa. 117 | * @param {String} css CSS to transform 118 | * @param {String} direction Direction ("ltr", "rtl", or empty/"none") 119 | * @param {Boolean} warnings Output warnings 120 | * @param {Boolean} swapPseudo Swap :before and :after 121 | * @param {Boolean} flipUrl flip words "left" and "right" inside url() 122 | * @param {Boolean} flipSelectors flip words "left" and "right" in selectors 123 | * @return {String} Processed CSS 124 | */ 125 | function transform(css, direction, warnings, cleanOnly, 126 | swapPseudo, flipUrls, flipSelectors) { 127 | if (direction === "ltr" || direction === "rtl") { 128 | css = flipcss.clean(css, direction); 129 | } 130 | 131 | if (!cleanOnly) { 132 | return flipcss.flip(css, warnings, swapPseudo, 133 | flipUrls, flipSelectors); 134 | } else { 135 | return css; 136 | } 137 | } 138 | 139 | /** 140 | * Main. 141 | */ 142 | function main() { 143 | var res; 144 | try { 145 | res = handleArgv(process.argv.slice(2)); 146 | if (!res) { 147 | process.exit(0); 148 | } 149 | } catch (err) { 150 | console.log(err.message); 151 | process.exit(2); 152 | } 153 | 154 | var infileName = res.input; 155 | var outfileName = res.output; 156 | 157 | fs.readFile(infileName, "utf-8", function (err, data) { 158 | if (err) { 159 | console.log(err.message); 160 | process.exit(1); 161 | } 162 | 163 | var outfile = fs.openSync(outfileName, "w"); 164 | 165 | var outdata = transform(data, res.direction, res.warnings, 166 | res.cleanOnly, res.swapPseudo, res.flipUrls, 167 | res.flipSelectors); 168 | 169 | fs.write(outfile, outdata); 170 | fs.close(outfile); 171 | }); 172 | } 173 | 174 | 175 | if (require.main === module) { 176 | main(); 177 | } else { 178 | module.exports = {handleArgv: handleArgv, 179 | transform: transform}; 180 | } 181 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## FlipCSS 2 | Create right-to-left (RTL) CSS from left-to-right (LTR) CSS, and vice versa. 3 | This is useful for making websites work visually for both LTR languages (like English) and RTL languages (like Arabic). 4 | 5 | FlipCSS takes a stylesheet as input, and outputs one that flows in the opposite direction. When somebody browse your web page, you can use your backend to serve the stylesheet that fits the requested language. 6 | 7 | Wonder what it looks like? Check http://addons.opera.com/en vs. http://addons.opera.com/ar 8 | 9 | The library is written for [Node](http://www.nodejs.org/). However, it should be easy to use it in other contexts as well. FlipCSS can be used from the command line. 10 | 11 | PLEASE NOTE: This library will be obsoleted by [CSS3 Writing Modes](http://dev.w3.org/csswg/css3-writing-modes/) and [CSS Images Level 4](http://dev.w3.org/csswg/css4-images/#bidi-images). 12 | 13 | ### Installation 14 | 15 | `npm install flipcss` 16 | 17 | Using [Grunt](http://gruntjs.com/)? Check out [grunt-flipcss](https://github.com/behrang/grunt-flipcss). 18 | 19 | ### Usage (command line) 20 | 21 | ``` 22 | $ flipcss -h 23 | Usage: node flipcss [OPTION] ... INFILE OUTFILE 24 | -r, --rtl Flip CSS LTR>RTL 25 | -l, --ltr Flip CSS RTL>LTR 26 | -w, --warnings Output warnings 27 | -h, --help Usage information 28 | -c, --clean-only Clean only (requires a direction, -r or -l) 29 | -p, --swap-pseudo Swap :before and :after 30 | -u, --ignore-urls Do not swap the words left and right inside url() 31 | -s, --ignore-selectors Do not swap the words left and right in selectors 32 | 33 | If no direction is given, the CSS is just flipped (with no cleaning of direction specific rules). 34 | ``` 35 | 36 | ### Usage (as library) 37 | FlipCSS has two public functions: 38 | 39 | * `flip(String css, [Boolean warnings=false], [Boolean flipPseudo=false], [Boolean flipUrls=true], [Boolean flipSelectors=true])` 40 | * `clean(String css, String direction)` 41 | 42 | flip() does the RTL flipping. It takes five arguments. The first is mandatory, and is the CSS to flip. The rest are optional. If "warnings" is true, warnings will be printed to console (deaults to false). If "flipPseudo" is true, :before and :after will be swapped (defaults to false). If "flipUrls" is true, the words "left" and "right" will be swapped inside URLs (i.e., inside "url()" - defaults to true). If flipSelectors is true, the words "left" and "right" will be swapped inside selectors (i.e. ".pull-left {}" will become ".pull-right" - defaults to true). 43 | 44 | clean() removes direction specific CSS rules. It takes two arguments: The CSS to clean, and the direction ("rtl" or "ltr"). If you have direction-specific rules in your CSS, you would want to run this both for your RTL CSS and your LTR CSS. This function will also add a CSS direction rule (e.g. "direction:ltr;") to the CSS, based on the direction given as the second parameter. 45 | 46 | If your web page supports both LTR and RTL, you will need to have two stylesheets, one for each direction. 47 | 48 | Please see the example below. 49 | 50 | ### What is done when flipping? 51 | A number of operations are done when you call flip(): 52 | 53 | * All instances of the words "left" and "right" are swapped. See more details below. 54 | * Horizontal values in margin and padding rules are swapped. 55 | * Horizontal background position are swapped, if given as percentages, or as the keywords "left" and "right". 56 | 57 | The pseudo elements :before and :after can also be swapped, but this is not done by default. 58 | 59 | CSS rules marked as direction specific are not touched by flip(). See below for more info on direction specific rules. 60 | 61 | #### The words "left" and "right" 62 | The code for swapping the words "left" and "right" is naive, and just swaps every instance it can find, disregarding context. The basic exception is when these words are part of other words (e.g. "copyright"). When separated by other characters than letters and digits (e.g. hypens), they will be swapped. This means that you can have things like direction specific image files, and get those handled automatically: Just add "left" or "right" in the file names. For example, "arrow-right.png" will be changed to "arrow-left.png". 63 | 64 | If you want a slightly less eager behaviour, you can specify URLs and selectors to be left untouched by using the "-u" and "-s" commandline flags respectively (or use the appropriate arguments for API use). But beware that the implementation will still be pretty naive, and will change these words in other contexts (e.g. in comments). Please file bugs if it changes something that should probably not be changed. 65 | 66 | ### What is done when cleaning 67 | * All direction specific rules not relevant for the direction given are removed. 68 | * "direction: rtl" is added to the body group in the CSS. If there is no body group, it is added. 69 | 70 | ### Direction-specific CSS rules 71 | If you want some rules to only be applied for LTR, you can add a comment after the rule saying `/* !ltr-only */`. For RTL, you can use `/* !rtl-only */`. This is useful for e.g. italic text, which is seldom used in Arabic (some fonts even doesn't support it, making things look very bad). So, you could do something like: 72 | 73 | ```body { font-style: normal !important; /* !rtl-only */ } 74 | .foo { font-style: italic; /* !ltr-only */}``` 75 | 76 | If you want larger groups of CSS rules to be direction specific, you should keep them in separate CSS files. 77 | 78 | ### :before and :after 79 | The :before and :after pseudo elements can also be swapped, although they are kept as-is by default. To swap all of them, you can use the command-line flag "-p" or "--swap-pseudo". If you want to swap just a few instances, add /* !swap */ after the brace, like this: 80 | 81 | ``` 82 | .foo:before { /* !swap */ 83 | content: "Foo"; 84 | } 85 | ``` 86 | Please note that the comment must be after the brace. 87 | 88 | If you use the command line flag to swap all instances of :before and :after, you may use `/* !direction-ignore */` to ignore certain instances. However, `/* !rtl-only */` and `/* !ltr-only */` does not work for pseudo elements. 89 | 90 | ### Keep rules as is 91 | If you want a certain CSS rule not to be flipped by the FlipCSS processing (e.g. a div that always should be floated right), add a comment saying `/* !direction-ignore */` after the rule. 92 | 93 | If you want larger groups of CSS rules to be ignored, you should keep them in separate CSS files. 94 | 95 | ### Tips, tricks and limitations 96 | Below are some things to keep in mind when automatically generating RTL CSS: 97 | 98 | #### Semicolons 99 | Although [the CSS spec allows you to omit the semicolon in cases where you have only one declaration](http://www.w3.org/TR/CSS2/syndata.html#declaration), FlipCSS has trouble with this. Thus, it is recommended that you always include the semicolon after a declaration. 100 | 101 | #### Your HTML 102 | Remember to set "dir=rtl" on the html element (and to actually load the RTL stylesheet) when a RTL language is used. 103 | 104 | If you have blobs of content on your RTL page that is LTR, you can set "dir=ltr" on the containers of that content. 105 | 106 | #### Inline elements 107 | Be careful when explicitly setting elements to be inline; the flow of elements may then be a bit different than expected in RTL mode. Converting these to inline-block should solve most problems. FlipCSS can warn about inline elements. 108 | 109 | #### Pre-processors 110 | You may be using a pre-processor, like LESS or SASS. Since these will concatenate files for you, you may want to run them before running FlipCSS. But beware that things may happen to comments, and thus meta information for FlipCSS. One example is that minification will remove all comments. 111 | 112 | Another example is that LESS removes duplicate comments. So, if you have several rules that are RTL only in the same code block, only one of these comments will get through the LESS compilation. Because of this, a lot of your RTL only rules will be applied to your LTR page. 113 | 114 | To get around this, the meta information comments inside a block must be unique, so that LESS does not strip them away. FlipCSS allows you to add text after a meta information, so you could do something like this: 115 | 116 | ``` 117 | img.overlay { 118 | -webkit-transform: scaleX(-1); /* !rtl-only 1 */ 119 | -moz-transform: scaleX(-1); /* !rtl-only 2 */ 120 | -ie-transform: scaleX(-1); /* !rtl-only 3 */ 121 | -o-transform: scaleX(-1); /* !rtl-only 4 */ 122 | transform: scaleX(-1); /* !rtl-only 5 */ 123 | } 124 | ``` 125 | 126 | #### What languages are RTL? 127 | The following languages are written right-to-left: Arabic (ar), Farsi/Persian (fa), Urdu (ur), Hebrew (he), and Yiddish (yi). 128 | 129 | ### Example 130 | If you have a ltr stylesheet (with direction specific rules both for ltr and rtl), and you want to create a rtl stylesheet: 131 | 132 | ``` 133 | body { 134 | font-style: normal !important; /* !rtl-only */ 135 | } 136 | .foo { 137 | float: left; 138 | font-style: italic; /* !ltr-only */ 139 | } 140 | ``` 141 | 142 | Running the following code... 143 | 144 | ``` 145 | > css = "..." 146 | > 147 | > cssLtr = flipcss.clean(css, "ltr"); 148 | > 149 | > cssRtl = flipcss.clean(css, "rtl"); 150 | > cssRtl = flipcss.flip(cssRtl); 151 | ``` 152 | 153 | ...will result in this LTR CSS... 154 | 155 | ``` 156 | body { 157 | direction:ltr; 158 | } 159 | .foo { 160 | float: left; 161 | font-style: italic; /* !ltr-only */ 162 | } 163 | ``` 164 | 165 | ...and this RTL CSS: 166 | 167 | ``` 168 | body { 169 | direction:rtl; 170 | font-style: normal !important; /* !rtl-only */ 171 | } 172 | .foo { 173 | float: right; 174 | } 175 | ``` 176 | -------------------------------------------------------------------------------- /test/bin-test.js: -------------------------------------------------------------------------------- 1 | /*global assert:true */ 2 | 3 | var sinon = require("sinon"); 4 | 5 | if (typeof require !== "undefined") { 6 | var buster = require("buster"); 7 | var lib = require("../bin/flipcss.bin.js"); 8 | } 9 | 10 | var assert = buster.referee.assert; 11 | var refute = buster.referee.refute; 12 | 13 | 14 | buster.testCase("Command line arguments parser", { 15 | "works with no arguments": function() { 16 | var expected = { 17 | direction: "none", 18 | warnings: false, 19 | cleanOnly: false, 20 | swapPseudo: false, 21 | flipUrls: true, 22 | flipSelectors: true, 23 | input: "style.css", 24 | output: "style-rtl.css" 25 | }; 26 | 27 | var argv = ["style.css", "style-rtl.css"]; 28 | var result = lib.handleArgv(argv); 29 | assert.equals(expected, result); 30 | }, 31 | "understands request to show warnings": function() { 32 | var expected = { 33 | direction: "none", 34 | warnings: true, 35 | cleanOnly: false, 36 | swapPseudo: false, 37 | flipUrls: true, 38 | flipSelectors: true, 39 | input: "style.css", 40 | output: "style-rtl.css" 41 | }; 42 | 43 | var argv = ["-w", "style.css", "style-rtl.css"]; 44 | var result = lib.handleArgv(argv); 45 | assert.equals(expected, result); 46 | 47 | argv = ["--warnings", "style.css", "style-rtl.css"]; 48 | result = lib.handleArgv(argv); 49 | assert.equals(expected, result); 50 | }, 51 | "understands request to do RTL>LTR": function() { 52 | var expected = { 53 | direction: "ltr", 54 | warnings: false, 55 | cleanOnly: false, 56 | swapPseudo: false, 57 | flipUrls: true, 58 | flipSelectors: true, 59 | input: "style.css", 60 | output: "style-rtl.css" 61 | }; 62 | 63 | var argv = ["-l", "style.css", "style-rtl.css"]; 64 | var result = lib.handleArgv(argv); 65 | assert.equals(expected, result); 66 | 67 | argv = ["--ltr", "style.css", "style-rtl.css"]; 68 | result = lib.handleArgv(argv); 69 | assert.equals(expected, result); 70 | }, 71 | "understands request to do LTR>RTL": function() { 72 | var expected = { 73 | direction: "rtl", 74 | warnings: false, 75 | cleanOnly: false, 76 | swapPseudo: false, 77 | flipUrls: true, 78 | flipSelectors: true, 79 | input: "style.css", 80 | output: "style-rtl.css" 81 | }; 82 | 83 | var argv = ["-r", "style.css", "style-rtl.css"]; 84 | var result = lib.handleArgv(argv); 85 | assert.equals(expected, result); 86 | 87 | argv = ["--rtl", "style.css", "style-rtl.css"]; 88 | result = lib.handleArgv(argv); 89 | assert.equals(expected, result); 90 | }, 91 | "understands request to do clean only": function() { 92 | var expected = { 93 | direction: "ltr", 94 | warnings: false, 95 | cleanOnly: true, 96 | swapPseudo: false, 97 | flipUrls: true, 98 | flipSelectors: true, 99 | input: "style.css", 100 | output: "style-rtl.css" 101 | }; 102 | 103 | // Missing direction 104 | var argv = ["style.css", "style-rtl.css", "--clean-only"]; 105 | assert.exception(function() { lib.handleArgv(argv); }, 106 | "InvalidArgumentsError"); 107 | 108 | // With direction, long form 109 | argv = ["style.css", "style-rtl.css", "--clean-only", "--ltr"]; 110 | var result = lib.handleArgv(argv); 111 | assert.equals(expected, result); 112 | 113 | // With direction, short form 114 | argv = ["style.css", "style-rtl.css", "-c", "--ltr"]; 115 | result = lib.handleArgv(argv); 116 | assert.equals(expected, result); 117 | 118 | }, 119 | "understands request to swap pseudo elements": function() { 120 | var expected = { 121 | direction: "none", 122 | warnings: false, 123 | cleanOnly: false, 124 | swapPseudo: true, 125 | flipUrls: true, 126 | flipSelectors: true, 127 | input: "style.css", 128 | output: "style-rtl.css" 129 | }; 130 | 131 | // long form 132 | var argv = ["style.css", "style-rtl.css", "-p"]; 133 | var result = lib.handleArgv(argv); 134 | assert.equals(expected, result); 135 | 136 | // short form 137 | argv = ["style.css", "style-rtl.css", "--swap-pseudo"]; 138 | result = lib.handleArgv(argv); 139 | assert.equals(expected, result); 140 | }, 141 | "understands request to ignore URLs": function() { 142 | var expected = { 143 | direction: "none", 144 | warnings: false, 145 | cleanOnly: false, 146 | swapPseudo: false, 147 | flipUrls: false, 148 | flipSelectors: true, 149 | input: "style.css", 150 | output: "style-rtl.css" 151 | }; 152 | 153 | // long form 154 | var argv = ["style.css", "style-rtl.css", "--ignore-urls"]; 155 | var result = lib.handleArgv(argv); 156 | assert.equals(expected, result); 157 | 158 | // short form 159 | argv = ["style.css", "style-rtl.css", "-u"]; 160 | result = lib.handleArgv(argv); 161 | assert.equals(expected, result); 162 | }, 163 | "understands request to ignore selectors": function() { 164 | var expected = { 165 | direction: "none", 166 | warnings: false, 167 | cleanOnly: false, 168 | swapPseudo: false, 169 | flipUrls: true, 170 | flipSelectors: false, 171 | input: "style.css", 172 | output: "style-rtl.css" 173 | }; 174 | 175 | var argv = ["style.css", "style-rtl.css", "--ignore-selectors"]; 176 | var result = lib.handleArgv(argv); 177 | assert.equals(expected, result); 178 | 179 | argv = ["style.css", "style-rtl.css", "-s"]; 180 | result = lib.handleArgv(argv); 181 | assert.equals(expected, result); 182 | }, 183 | "gives error when too few arguments": function() { 184 | var argv; 185 | 186 | // Missing input/output file 187 | argv = ["-r", "-w", "style.css"]; 188 | assert.exception(function() { lib.handleArgv(argv); }, 189 | "InvalidArgumentsError"); 190 | 191 | // Missing input and output file 192 | argv = ["-r", "-w"]; 193 | assert.exception(function() { lib.handleArgv(argv); }, 194 | "InvalidArgumentsError"); 195 | }, 196 | "gives error when too many arguments": function() { 197 | var argv; 198 | 199 | // Extra option 200 | argv = ["-w", "-r", "-a", "style.css", "style-rtl.css"]; 201 | assert.exception(function() { lib.handleArgv(argv); }, 202 | "InvalidArgumentsError"); 203 | 204 | // Extra trailing options 205 | argv = ["-w", "-r", "style.css", "style-rtl.css", "foo", "bar"]; 206 | assert.exception(function() { lib.handleArgv(argv); }, 207 | "InvalidArgumentsError"); 208 | }, 209 | "gives typeof on invalid arguments": function() { 210 | var argv; 211 | 212 | // Invalid argument 213 | argv = ["-r", "-a", "style.css", "style-rtl.css"]; 214 | assert.exception(function() { lib.handleArgv(argv); }, 215 | "InvalidArgumentsError"); 216 | 217 | // Invalid argument 218 | argv = ["-a", "-r", "style.css", "style-rtl.css"]; 219 | assert.exception(function() { lib.handleArgv(argv); }, 220 | "InvalidArgumentsError"); 221 | 222 | // Several invalid arguments 223 | argv = ["-a", "-r", "-b", "style.css", "style-rtl.css"]; 224 | assert.exception(function() { lib.handleArgv(argv); }, 225 | "InvalidArgumentsError"); 226 | }, 227 | "understands request for usage info": function() { 228 | var argv, result; 229 | 230 | argv = ["-h"]; 231 | result = lib.handleArgv(argv); 232 | assert.equals(null, result); 233 | 234 | argv = ["--help"]; 235 | result = lib.handleArgv(argv); 236 | assert.equals(null, result); 237 | } 238 | }); 239 | 240 | 241 | buster.testCase("Css transformer", { 242 | setUp: function () { 243 | sinon.spy(console, "log"); 244 | }, 245 | 246 | tearDown: function () { 247 | console.log.restore(); 248 | }, 249 | "can flip css without direction specified": function() { 250 | var data = ".foo{float:left;}"; 251 | var expected = ".foo{float:right;}"; 252 | var result = lib.transform(data, "none", true); 253 | assert.equals(result, expected); 254 | }, 255 | "can output warnings": function() { 256 | var expected, result; 257 | 258 | var data = ".foo{float:left;}"; 259 | 260 | expected = "body{direction:ltr;}.foo{float:right;}"; 261 | result = lib.transform(data, "ltr", true); 262 | assert.equals(result, expected); 263 | 264 | expected = "body{direction:rtl;}.foo{float:right;}"; 265 | result = lib.transform(data, "rtl", true); 266 | assert.equals(result, expected); 267 | }, 268 | "can flip css with direction specified": function() { 269 | var data = ".foo{float:right;display:inline;}"; 270 | 271 | lib.transform(data, "ltr", true); 272 | 273 | // Check that warnings are given 274 | assert(console.log.calledOnce); 275 | var spyCall = console.log.getCall(0); 276 | assert(-1 < spyCall.args[0].indexOf("Warning: Inline")); 277 | } 278 | }); 279 | -------------------------------------------------------------------------------- /lib/flipcss.js: -------------------------------------------------------------------------------- 1 | var flipcss = { 2 | /** 3 | * Pattern matching rules to ignore (marked with "!direction-ignore" in a 4 | * comment) 5 | * 6 | * This pattern is to be added to other patterns that matches a full CSS 7 | * rule. 8 | * 9 | * Note: This regexp does not match the end of the comment, allowing extra 10 | * text to follow the meta information. This regexp is not used to delete 11 | * rules. 12 | * 13 | * (?!y) -> not followed by y. 14 | * @private 15 | */ 16 | _ruleMatchIgnorePattern: "(?![^\n]*/\\*\\s*!" + 17 | "(direction-ignore|rtl-only|ltr-only)" + 18 | ")", 19 | 20 | 21 | /** 22 | * Swap two words in a string (mostly "left" and "right"), and vice versa 23 | * This function is pretty naive, and just swaps every instance it comes 24 | * across, with some exceptions: 25 | * - Words where word1 or word2 are parts of other words (like "copyright") 26 | * are left alone. (Words like "margin-left" are not.) 27 | * - You can set some flags to leave the words to be swapped unchanged 28 | * in some contexts. 29 | * 30 | * @private 31 | * @param {String} string String to process 32 | * @param {String} word1 Word to swap with word2 (alphanumeric chars only) 33 | * @param {String} word2 Word to swap with word1 (alphanumeric chars only) 34 | * @param {Boolean} flipUrls 35 | * @returns {String} Processed string. 36 | */ 37 | _swapWords: function(string, word1, word2, flipUrls, flipSelectors) { 38 | // Regexp parts 39 | var rp = []; 40 | 41 | // Should only match whole words, or words separated by e.g. a hyphen 42 | // ("margin-left" would be matched, but not "copyright"). Thus, we 43 | // need to check for alphanumeric chars right before and right 44 | // after the word: 45 | var noAlphaNum = "([^a-z0-9])"; 46 | rp.push(noAlphaNum); // Charactes not allowed right before word 47 | 48 | // The two words to swap; look for either 49 | rp.push("(" + word1 + "|" + word2 + ")"); 50 | 51 | // If filenames are not to be flipped: 52 | if (!flipUrls) { 53 | // Filenames are inside parantheses (e.g. "url()"), so look for 54 | // closing bracet: 55 | rp.push("(?!.*\\))"); 56 | } 57 | 58 | // If selectors are not to be flipped: 59 | if (!flipSelectors) { 60 | rp.push("(?!.*\\s*{)"); 61 | } 62 | 63 | // TODO: Add option to not swap words in comments 64 | 65 | rp.push(noAlphaNum); // Alphanumerics not allowed right after word 66 | 67 | // Ignore pattern, in case this specific rule is to be ignored. 68 | rp.push(this._ruleMatchIgnorePattern); 69 | 70 | // Do replace 71 | var pattern = new RegExp(rp.join(""), "g"); 72 | 73 | return string.replace(pattern, function(_, pre, word, post) { 74 | return pre + (word === word1 ? word2 : word1) + post; 75 | }); 76 | }, 77 | 78 | 79 | /** 80 | * Swap left/right values in four-value rules. 81 | * Example: 82 | * margin: 1px 2px 3px 4px ---> margin: 1px 4px 3px 2px 83 | * 84 | * @private 85 | * @param {String} string String to process 86 | * @returns {String} Processed string 87 | */ 88 | _swapValues: function(string) { 89 | // Matches pattern and margin rules, including semicolon and ignore 90 | // pattern. 91 | // Captures all parts, except whitespace and semicolon. 92 | var pattern = new RegExp( 93 | "(padding|margin):\\s*" 94 | // Optional number and dot, then number and units (0-3 letters), 95 | // followed by whitespace. 96 | + "((?:\\d*\\.)?\\d+[a-z%]{0,3})\\s+" 97 | + "((?:\\d*\\.)?\\d+[a-z%]{0,3})\\s+" 98 | + "((?:\\d*\\.)?\\d+[a-z%]{0,3})\\s+" 99 | + "((?:\\d*\\.)?\\d+[a-z%]{0,3})\\s*" 100 | + "(.*);" 101 | + this._ruleMatchIgnorePattern, "g"); 102 | 103 | return string.replace(pattern, function(_, prop, d1, d2, d3, d4, d5) { 104 | return prop + ": " + d1 + " " + d4 + " " + d3 + " " + d2 105 | + (d5 ? " " + d5 : "") 106 | + ";"; 107 | }); 108 | }, 109 | 110 | 111 | /** 112 | * Swap left/right values for background position. 113 | * Example: 114 | * background:url("@{image-url}/foo.bar") 0% 0 no-repeat; ---> 115 | * background:url("@{image-url}/foo.bar") 100% 0 no-repeat; ---> 116 | * 117 | * Note: positions given as strings ("left"/"right") are handled 118 | * by _swapWords(). 119 | * 120 | * @private 121 | * @param {String} string String to process 122 | * @returns {String} Processed string 123 | */ 124 | _swapBackgroundPosition: function(string) { 125 | // Matches background and background-position rules. 126 | // Captures everything, except semi-colon 127 | var pattern = new RegExp( 128 | "(background(?:-position)?):(.*);" 129 | + this._ruleMatchIgnorePattern, "g"); 130 | 131 | return string.replace(pattern, function(_, prop, d1) { 132 | // Split string into parts, and operate on each part. 133 | // Only first of x,y value pair should be inverted, and only if: 134 | // * given as a positive percentage value, or "0" 135 | // * not inside parenteses 136 | var parts = d1.trim().split(/\s+/); 137 | var insideParentheses = false; 138 | 139 | for (var i=0; i Rule will be removed from output 345 | * 346 | * clean("font-style: italic; \/\* !ltr-only \*\/", "ltr"); 347 | * --> Rule will be kept in output 348 | * 349 | * (Note: Comment should be regular CSS comment.) 350 | * 351 | * Given that you have both ltr and rtl specific rules, and your original 352 | * CSS is ltr, you will have to run clean on both the original and on the 353 | * generated rtl output. 354 | * 355 | * @param {String} string CSS string to clean 356 | * @param {String} dir Direction of the output ("rtl" or "ltr") 357 | * @returns {String} Cleaned CSS 358 | */ 359 | clean: function(string, dir) { 360 | // Do preprocessing 361 | string = flipcss._cleanLineFeeds(string); 362 | 363 | // Do processing 364 | if ("rtl" === dir || "ltr" === dir) { 365 | // If preprocess rtl, remove ltr-only rules and vice versa. 366 | var str = (dir === "rtl") ? "ltr" : "rtl"; 367 | string = flipcss._deleteRule(string, "!" + str + "-only"); 368 | string = flipcss._addRule(string, "body", "direction:" + dir ); 369 | } 370 | return string; 371 | } 372 | }; 373 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | /* global assert:true */ 2 | 3 | var fs = require("fs"); 4 | var sinon = require("sinon"); 5 | 6 | if (typeof require !== "undefined") { 7 | var buster = require("buster"); 8 | var lib = require("../lib/flipcss"); 9 | } 10 | 11 | var assert = buster.referee.assert; 12 | var refute = buster.referee.refute; 13 | 14 | 15 | buster.assertions.add("pathFlipsTo", { 16 | assert: function (inputPath, expectedOutputPath) { 17 | var input = fs.readFileSync(inputPath).toString(); 18 | var expectedOutput = fs.readFileSync(expectedOutputPath).toString(); 19 | 20 | this.output = lib.flip(input); 21 | return this.output === expectedOutput; 22 | }, 23 | assertMessage: "Expected ${0} to flip to ${1}, got \"${output}\".", 24 | refuteMessage: "Expected ${0} to not flip to ${1}, got \"${output}\"." 25 | }); 26 | 27 | 28 | buster.assertions.add("flipsTo", { 29 | assert: function (input, expectedOutput) { 30 | this.output = lib.flip(input); 31 | return this.output === expectedOutput; 32 | }, 33 | assertMessage: "Expected \"${0}\" to flip to \"${1}\", got \"${output}\".", 34 | refuteMessage: "Expected \"${0}\" to not flip to \"${1}\"," 35 | + " got \"${output}\"." 36 | }); 37 | 38 | 39 | buster.assertions.add("notFlipUrls", { 40 | assert: function (input, expectedOutput) { 41 | this.output = lib.flip(input, false, false, false); 42 | return this.output === expectedOutput; 43 | }, 44 | assertMessage: "Expected \"${0}\" to flip to \"${1}\", got \"${output}\".", 45 | refuteMessage: "Expected \"${0}\" to not flip to \"${1}\"," 46 | + " got \"${output}\"." 47 | }); 48 | 49 | 50 | buster.assertions.add("notFlipSelectors", { 51 | assert: function (input, expectedOutput) { 52 | this.output = lib.flip(input, false, false, false, false); 53 | return this.output === expectedOutput; 54 | }, 55 | assertMessage: "Expected \"${0}\" to flip to \"${1}\", got \"${output}\".", 56 | refuteMessage: "Expected \"${0}\" to not flip to \"${1}\"," 57 | + " got \"${output}\"." 58 | }); 59 | 60 | 61 | buster.assertions.add("flipsPseudo", { 62 | assert: function (input, expectedOutput) { 63 | this.output = lib.flip(input, false, true); 64 | return this.output === expectedOutput; 65 | }, 66 | assertMessage: "Expected \"${0}\" to flip to \"${1}\", got \"${output}\".", 67 | refuteMessage: "Expected \"${0}\" to not flip to \"${1}\"," 68 | + " got \"${output}\"." 69 | }); 70 | 71 | 72 | buster.testCase("Functional tests: Flip stylesheet w/ pre-processing", { 73 | setUp: function () { 74 | sinon.spy(console, "log"); 75 | }, 76 | 77 | tearDown: function () { 78 | console.log.restore(); 79 | }, 80 | 81 | "flip without warnings": function() { 82 | var input = fs.readFileSync("fixtures/input_all.css").toString(); 83 | var output = fs.readFileSync("fixtures/output_all.css").toString(); 84 | 85 | input = lib.clean(input, "rtl"); 86 | assert.flipsTo(input, output); 87 | }, 88 | 89 | "flip with warnings": function() { 90 | var input = fs.readFileSync("fixtures/input_all.css").toString(); 91 | var output = fs.readFileSync("fixtures/output_all.css").toString(); 92 | 93 | input = lib.clean(input, "rtl"); 94 | assert.equals(lib.flip(input, true), output); 95 | 96 | // Check that warnings are given 97 | assert(console.log.calledTwice); 98 | var spyCall = console.log.getCall(0); 99 | assert(-1 < spyCall.args[0].indexOf("Warning: Inline")); 100 | } 101 | }); 102 | 103 | 104 | buster.testCase("CSS word swapper", { 105 | "swaps floats": function() { 106 | // Basic case: Swap right with left 107 | assert.flipsTo(".foo { float: right; }", 108 | ".foo { float: left; }"); 109 | // Basic case: Swap left with right 110 | assert.flipsTo(".foo { clear: left; }", 111 | ".foo { clear: right; }"); 112 | // Extra keyword: Swap left with right 113 | assert.flipsTo(".foo { float: right !important; }", 114 | ".foo { float: left !important; }"); 115 | // No whitespace: Swap left with right 116 | assert.flipsTo(".foo{float:right !important;}", 117 | ".foo{float:left !important;}"); 118 | // Extra whitespace: Swap left with right 119 | assert.flipsTo(" .foo { float:right !important ; } ", 120 | " .foo { float:left !important ; } "); 121 | }, 122 | "swaps text-align": function() { 123 | // Basic case: Swap left with right 124 | assert.flipsTo(".foo { text-align: left; }", 125 | ".foo { text-align: right; }"); 126 | }, 127 | "swaps margins and paddings": function() { 128 | // Basic case: Swap margin-left with margin-right 129 | assert.flipsTo(".foo { margin-left: 2em; }", 130 | ".foo { margin-right: 2em; }"); 131 | // Basic case: Swap padding-left with padding-right 132 | assert.flipsTo(".foo { padding-left: 2em; }", 133 | ".foo { padding-right: 2em; }"); 134 | // Extra keyword: Swap padding-left with padding-right 135 | assert.flipsTo(".foo { padding-left: 2em !important; }", 136 | ".foo { padding-right: 2em !important; }"); 137 | }, 138 | "swaps left and right positioning": function() { 139 | // Basic case: Swap left with right 140 | assert.flipsTo(".foo { left: 10px; }", 141 | ".foo { right: 10px; }"); 142 | }, 143 | "understands the difference between words and subwords": function() { 144 | // "Copyright" should be unchanged (full word), but float should be changed. 145 | assert.flipsTo(".copyright {}", 146 | ".copyright {}"); 147 | // "rights.png" Should not be changed (subword) 148 | assert.flipsTo(".rights {}", 149 | ".rights {}"); 150 | }, 151 | "leaves urls alone when asked to": function() { 152 | assert.notFlipUrls("background: url('arrow-left.png')", 153 | "background: url('arrow-left.png')"); 154 | }, 155 | "swaps urls by default": function() { 156 | assert.flipsTo("background: url('arrow-left.png')", 157 | "background: url('arrow-right.png')"); 158 | assert.flipsTo("background: url('left-imgs/arrow.png')", 159 | "background: url('right-imgs/arrow.png')"); 160 | }, 161 | "swaps selectors by default": function() { 162 | assert.flipsTo(".pull-right { float: right; }", 163 | ".pull-left { float: left; }"); 164 | }, 165 | "leaves selectors alone when asked to": function() { 166 | // "pull-right" should be changed (subword) 167 | assert.notFlipSelectors(".pull-right { float: right; }", 168 | ".pull-right { float: left; }"); 169 | }, 170 | "leaves ignored rules alone": function() { 171 | // Basic case: Nothing should change. 172 | assert.flipsTo(".foo { clear: left; /* !direction-ignore */ }", 173 | ".foo { clear: left; /* !direction-ignore */ }"); 174 | // Extra keywords: Nothing should change. 175 | assert.flipsTo(".foo { clear: left !important; /* !direction-ignore */ }", 176 | ".foo { clear: left !important; /* !direction-ignore */ }"); 177 | // Without whitespace: Nothing should change. 178 | assert.flipsTo(".foo{clear:left;/*!direction-ignore*/}", 179 | ".foo{clear:left;/*!direction-ignore*/}"); 180 | // Extra whitespace: Nothing should change. 181 | assert.flipsTo(" .foo { clear: left !important; /* !direction-ignore */ } ", 182 | " .foo { clear: left !important; /* !direction-ignore */ } "); 183 | // Newline before comment: Nothing should change, except newline should be removed. 184 | assert.flipsTo(".foo { clear: left !important;\n /* !direction-ignore */ }", 185 | ".foo { clear: left !important; /* !direction-ignore */ }"); 186 | } 187 | }); 188 | 189 | 190 | buster.testCase("CSS value swapper", { 191 | "swaps four value rules": function() { 192 | // Second and fourth should swap (ints) 193 | assert.flipsTo(".foo { padding: 1em 2em 3em 4em; }", 194 | ".foo { padding: 1em 4em 3em 2em; }"); 195 | // Check that the basic test also works for margin 196 | assert.flipsTo(".foo { margin: 1em 2em 3em 4em; }", 197 | ".foo { margin: 1em 4em 3em 2em; }"); 198 | // Second and fourth should swap (as percents) 199 | assert.flipsTo(".foo { padding: 1% 2% 3% 4%; }", 200 | ".foo { padding: 1% 4% 3% 2%; }"); 201 | // Second and fourth should swap (floats) 202 | assert.flipsTo(".foo { padding: 1.1px 2.2px 3.3px 4.4px; }", 203 | ".foo { padding: 1.1px 4.4px 3.3px 2.2px; }"); 204 | // Second and fourth should swap (with zeros) 205 | assert.flipsTo(".foo { padding: 0 0 0 4.4em; }", 206 | ".foo { padding: 0 4.4em 0 0; }"); 207 | // Extra keywords, second and fourth should swap (with zeros) 208 | assert.flipsTo(".foo { padding: 0 0 0 4.4em !important; }", 209 | ".foo { padding: 0 4.4em 0 0 !important; }"); 210 | // No whitespace, second and fourth should swap 211 | assert.flipsTo(".foo{padding: 1.1em 2.2em 3.3em 4.4em !important;}", 212 | ".foo{padding: 1.1em 4.4em 3.3em 2.2em !important;}"); 213 | // Whitespace, second and fourth should swap 214 | assert.flipsTo(" .foo { padding: 1.1em 2.2em 3.3em 4.4em !important; } ", 215 | " .foo { padding: 1.1em 4.4em 3.3em 2.2em !important; } "); 216 | }, 217 | "ignores two value rules": function() { 218 | // Two values, nothing should change 219 | assert.flipsTo(".foo { padding: 1.2em 3em; }", 220 | ".foo { padding: 1.2em 3em; }"); 221 | // Two values, nothing should change 222 | assert.flipsTo(".foo { padding: 0 3em; }", 223 | ".foo { padding: 0 3em; }"); 224 | }, 225 | 226 | "leaves ignored rules alone": function() { 227 | // Basic case: Nothing should change. 228 | assert.flipsTo(".foo { padding: 1em 2em 3em 4em; /* !direction-ignore */ }", 229 | ".foo { padding: 1em 2em 3em 4em; /* !direction-ignore */ }"); 230 | // Extra keywords: Nothing should change. 231 | assert.flipsTo(".foo { padding: 1em 2em 3em 4em !important; /* !direction-ignore */ }", 232 | ".foo { padding: 1em 2em 3em 4em !important; /* !direction-ignore */ }"); 233 | // Without whitespace: Nothing should change. 234 | assert.flipsTo(".foo{padding:1em 2em 3em 4em;/*!direction-ignore*/}", 235 | ".foo{padding:1em 2em 3em 4em;/*!direction-ignore*/}"); 236 | // Extra whitespace: Nothing should change. 237 | assert.flipsTo(" .foo { padding: 1em 2em 3em 4em !important; /* !direction-ignore */ } ", 238 | " .foo { padding: 1em 2em 3em 4em !important; /* !direction-ignore */ } "); 239 | // Newline before comment: Nothing should change, except newline should be removed. 240 | assert.flipsTo(".foo { padding:1em 2em 3em 4em !important;\n /* !direction-ignore */ }", 241 | ".foo { padding:1em 2em 3em 4em !important; /* !direction-ignore */ }"); 242 | } 243 | }); 244 | 245 | 246 | buster.testCase("CSS background position inverter", { 247 | "understands background rules": function() { 248 | // Basic case, horizontal position should be inverted 249 | assert.flipsTo(".foo { background: 0% 100%; }", 250 | ".foo { background: 100% 100%; }"); 251 | // Only horizontal position given, and it should be inverted 252 | assert.flipsTo("background: url('@{image-url}/foo.bar') 30%;", 253 | "background: url('@{image-url}/foo.bar') 70%;"); 254 | // Color given, horizontal position should be inverted 255 | assert.flipsTo(".foo { background: #fff 0% 100%; }", 256 | ".foo { background: #fff 100% 100%; }"); 257 | // Image url given, horizontal position should be inverted 258 | assert.flipsTo(".foo { background: url('/star-12px.png') 0% 100%; }", 259 | ".foo { background: url('/star-12px.png') 100% 100%; }"); 260 | // Extra keyword no-repeat given, horizontal position should be inverted 261 | assert.flipsTo(".foo { background: url('@{image-url}/foo.bar') no-repeat 10% 80%; }", 262 | ".foo { background: url('@{image-url}/foo.bar') no-repeat 90% 80%; }"); 263 | // Vertical position is keyword, horizontal position should be inverted 264 | assert.flipsTo("background: url('@{image-url}/up_arrow.png') no-repeat 95% center;", 265 | "background: url('@{image-url}/up_arrow.png') no-repeat 5% center;"); 266 | // Horizontal position is center, should be kept unchanged 267 | assert.flipsTo("background: url('@{image-url}/up_arrow.png') no-repeat center 95%;", 268 | "background: url('@{image-url}/up_arrow.png') no-repeat center 95%;"); 269 | // Horizontal position is left, should be inverted to right 270 | assert.flipsTo("background: url(/static/desktop/images/btn_edit_comment.png) no-repeat left 0%;", 271 | "background: url(/static/desktop/images/btn_edit_comment.png) no-repeat right 0%;"); 272 | // Rule is important, horizontal position should be inverted 273 | assert.flipsTo("background: url(/static/desktop/images/btn_edit_comment.png) no-repeat 10% 0% !important;", 274 | "background: url(/static/desktop/images/btn_edit_comment.png) no-repeat 90% 0% !important;"); 275 | // Linear gradient given, should be kept unchanged 276 | assert.flipsTo("background: linear-gradient(top, #29b8e0 0%, #0669bb 100%);", 277 | "background: linear-gradient(top, #29b8e0 0%, #0669bb 100%);"); 278 | // Horizontal position is 50%, should be kept unchanged 279 | assert.flipsTo("background: url('@{image-url}/foo.bar') no-repeat 50% 0;", 280 | "background: url('@{image-url}/foo.bar') no-repeat 50% 0;"); 281 | // Horizontal position is given as a px value (int), should be kept unchanged 282 | assert.flipsTo("background: #333 url('@{image-url}/foo.bar') no-repeat 50px 0;", 283 | "background: #333 url('@{image-url}/foo.bar') no-repeat 50px 0;"); 284 | // Horizontal position is given as a px value (float), should be kept unchanged 285 | assert.flipsTo("background: url('@{image-url}/foo.bar') no-repeat 1.3px 0;", 286 | "background: url('@{image-url}/foo.bar') no-repeat 1.3px 0;"); 287 | // Horizontal position is given only as 0, should be inverted to 100% 288 | assert.flipsTo("background: url('@{image-url}/foo.bar') no-repeat 0 50px;", 289 | "background: url('@{image-url}/foo.bar') no-repeat 100% 50px;"); 290 | // Horizontal position is given only as 0, should be inverted to 100% (keyword no-repeat moved last) 291 | assert.flipsTo("background: url('@{image-url}/foo.bar') 0 50% no-repeat;", 292 | "background: url('@{image-url}/foo.bar') 100% 50% no-repeat;"); 293 | // No horizontal position is given, should be kept unchanged 294 | assert.flipsTo("background: url('@{image-url}/foo.bar') no-repeat;", 295 | "background: url('@{image-url}/foo.bar') no-repeat;"); 296 | // No horizontal position is given, should be kept unchanged 297 | assert.flipsTo("background: url('@{image-url}/foo.bar');", 298 | "background: url('@{image-url}/foo.bar');"); 299 | // No horizontal position is given, should be kept unchanged 300 | assert.flipsTo("background: url('@{image-url}/foo.bar') 1.5% 50px no-repeat;", 301 | "background: url('@{image-url}/foo.bar') 98.5% 50px no-repeat;"); 302 | // Horizontal position given as center keyword, should be kept unchanged 303 | assert.flipsTo("background: url('@{image-url}/foo.bar') center center no-repeat;", 304 | "background: url('@{image-url}/foo.bar') center center no-repeat;"); 305 | // No whitespace, should be inverted 306 | assert.flipsTo(".foo{background:url('@{image-url}/foo.bar') no-repeat 10% 80%;}", 307 | ".foo{background: url('@{image-url}/foo.bar') no-repeat 90% 80%;}"); 308 | // Extra whitespace, should be inverted 309 | assert.flipsTo(" .foo { background: url('@{image-url}/foo.bar') no-repeat 10% 80%; } ", 310 | " .foo { background: url('@{image-url}/foo.bar') no-repeat 90% 80%; } "); 311 | }, 312 | "understands background-position rules": function() { 313 | // Basic case, horizontal position should be inverted 314 | assert.flipsTo("background-position: 40% 10%;", 315 | "background-position: 60% 10%;"); 316 | // Only horizontal position given, and it should be inverted 317 | assert.flipsTo("background-position: 70%;", 318 | "background-position: 30%;"); 319 | // Horizontal position given as 0, and it should be inverted 320 | assert.flipsTo("background-position: 0;", 321 | "background-position: 100%;"); 322 | // Extra keyword no-repeat given, horizontal position should be inverted 323 | assert.flipsTo("background-position: no-repeat 20% 10%;", 324 | "background-position: no-repeat 80% 10%;"); 325 | // Extra keyword no-repeat moved last, horizontal position should be inverted 326 | assert.flipsTo("background-position: 30% 10% no-repeat;", 327 | "background-position: 70% 10% no-repeat;"); 328 | // Horizontal position is 50%, should be kept unchanged 329 | assert.flipsTo("background-position: 50% 10%;", 330 | "background-position: 50% 10%;"); 331 | // Horizontal position is negative (in px), should be kept unchanged 332 | assert.flipsTo("background-position: -50px 0%;", 333 | "background-position: -50px 0%;"); 334 | // Horizontal position is negative (in %), should be kept unchanged 335 | assert.flipsTo("background-position: -30% 0%;", 336 | "background-position: -30% 0%;"); 337 | // No whitespace, horizontal position should be inverted 338 | assert.flipsTo("background-position: 10%;", 339 | "background-position: 90%;"); 340 | // Extra whitespace, horizontal position should be inverted 341 | assert.flipsTo(" background-position: 0 ; ", 342 | " background-position: 100%; "); 343 | }, 344 | "leaves ignored rules alone": function() { 345 | // Basic case: Nothing should change. 346 | assert.flipsTo(".foo { background: #333 100% 0%; /*!direction-ignore */}", 347 | ".foo { background: #333 100% 0%; /*!direction-ignore */}"); 348 | assert.flipsTo(".foo { background-position: 100% 0%; /*!direction-ignore */}", 349 | ".foo { background-position: 100% 0%; /*!direction-ignore */}"); 350 | // Complex rule: Nothing should change. 351 | assert.flipsTo(".foo { background: url('@{image-url}/foo.bar') no-repeat 100% 0%; /*!direction-ignore */}", 352 | ".foo { background: url('@{image-url}/foo.bar') no-repeat 100% 0%; /*!direction-ignore */}"); 353 | // No whitespace: Nothing should change 354 | assert.flipsTo(".foo{background:url('@{image-url}/foo.bar') no-repeat 100% 0%;/*!direction-ignore*/}", 355 | ".foo{background:url('@{image-url}/foo.bar') no-repeat 100% 0%;/*!direction-ignore*/}"); 356 | // Extra whitespace: Nothing should change 357 | assert.flipsTo(" .foo { background: url('@{image-url}/foo.bar') no-repeat 100% 0% ; /* !direction-ignore */ } ", 358 | " .foo { background: url('@{image-url}/foo.bar') no-repeat 100% 0% ; /* !direction-ignore */ } "); 359 | // Newline before comment: Nothing should change, except newline should be removed. 360 | assert.flipsTo(".foo { background: url('@{image-url}/foo.bar') no-repeat 100% 0%;\n /*!direction-ignore */}", 361 | ".foo { background: url('@{image-url}/foo.bar') no-repeat 100% 0%; /*!direction-ignore */}"); 362 | assert.flipsTo("background-position: 50% 10%;\n /* !direction-ignore*/", 363 | "background-position: 50% 10%; /* !direction-ignore*/"); 364 | 365 | // Extra comment in meta comment. 366 | assert.flipsTo(".foo { background: #333 100% 0%; /*!direction-ignore comment */}", 367 | ".foo { background: #333 100% 0%; /*!direction-ignore comment */}"); 368 | } 369 | }); 370 | 371 | 372 | buster.testCase("CSS cleaner", { 373 | "can add direction rule to body": function() { 374 | var input, output; 375 | 376 | input = "body { display: inline-block; }"; 377 | output = "body {direction:rtl; display: inline-block; }"; 378 | assert.equals(lib.clean(input, "rtl"), output); 379 | 380 | input = "foo {} body { display: inline-block; } bar {}"; 381 | output = "foo {} body {direction:rtl; display: inline-block; } bar {}"; 382 | assert.equals(lib.clean(input, "rtl"), output); 383 | }, 384 | "can add body group with direction rule": function() { 385 | var input = "div { display: inline-block; }"; 386 | var output = "body{direction:rtl;}div { display: inline-block; }"; 387 | assert.equals(lib.clean(input, "rtl"), output); 388 | }, 389 | "leaves direction-specific rules unchanged on flip": function() { 390 | var input, output; 391 | 392 | // Left/right swapping: 393 | input = "margin: left; /* !rtl-only */"; 394 | output = "margin: left; /* !rtl-only */"; 395 | assert.flipsTo(input, output); 396 | 397 | input = "margin: left; /* !ltr-only */"; // would normally be cleaned 398 | output = "margin: left; /* !ltr-only */"; 399 | assert.flipsTo(input, output); 400 | 401 | // Background position swapping 402 | input = "background: url('@{image-url}/foo.bar') 60% 0 no-repeat; /* !rtl-only */"; 403 | output = "background: url('@{image-url}/foo.bar') 60% 0 no-repeat; /* !rtl-only */"; 404 | 405 | // Margin/padding value swapping 406 | input = "padding: 0.5em 1em 0.5em 3.2em; /* !rtl-only */"; 407 | output = "padding: 0.5em 1em 0.5em 3.2em; " 408 | + "/* !rtl-only */"; 409 | assert.flipsTo(input, output); 410 | 411 | // Allowing extra text in meta comment: 412 | input = "margin: left; /* !rtl-only comment */"; 413 | output = "margin: left; /* !rtl-only comment */"; 414 | assert.flipsTo(input, output); 415 | }, 416 | "deletes rtl-only CSS rules": function() { 417 | var func = lib.clean; 418 | 419 | var input = fs.readFileSync("fixtures/input_clean.css").toString(); 420 | var output = fs.readFileSync("fixtures/output_clean.css").toString(); 421 | assert.equals(func(input, "ltr"), output); 422 | } 423 | }); 424 | 425 | 426 | buster.testCase("CSS :before/:after pseudo elements", { 427 | "left alone by default": function() { 428 | assert.flipsTo(".foo:before { content: 'foo'; }", 429 | ".foo:before { content: 'foo'; }"); 430 | assert.flipsTo(".foo:after { content: 'foo'; }", 431 | ".foo:after { content: 'foo'; }"); 432 | }, 433 | "swaps on keyword": function() { 434 | assert.flipsTo(".foo:before { /* !swap */ content: 'foo'; }", 435 | ".foo:after { content: 'foo'; }"); 436 | assert.flipsTo(".foo:after { /* !swap */ content: 'foo'; }", 437 | ".foo:before { content: 'foo'; }"); 438 | }, 439 | "swaps on flag": function() { 440 | assert.flipsPseudo(".foo:before { content: 'foo'; }", 441 | ".foo:after { content: 'foo'; }"); 442 | assert.flipsPseudo(".foo:after { content: 'foo'; }", 443 | ".foo:before { content: 'foo'; }"); 444 | }, 445 | "swaps on flag, but leaves !direction-ignore alone": function() { 446 | assert.flipsPseudo(".foo:before { /* !direction-ignore */ content: 'foo'; }", 447 | ".foo:before { /* !direction-ignore */ content: 'foo'; }"); 448 | assert.flipsPseudo(".foo:after { /* !direction-ignore */ content: 'foo'; }", 449 | ".foo:after { /* !direction-ignore */ content: 'foo'; }"); 450 | }, 451 | }); 452 | --------------------------------------------------------------------------------