├── .gitignore ├── .travis.yml ├── .npmignore ├── .editorconfig ├── CHANGELOG.md ├── package.json ├── LICENSE ├── propsToAlwaysConvert.js ├── README.md ├── test.js └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.log 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6" 4 | - "5" 5 | - "4" 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .gitignore 2 | 3 | node_modules/ 4 | npm-debug.log 5 | 6 | test.js 7 | .travis.yml 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.{json,yml}] 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.9.8 2 | 3 | Bumped the versions of dependencies, added a bunch of tests and fixed a bug with rtl animations. 4 | 5 | # 0.9.2 6 | 7 | Changed the initial test to check if the specific selector already contains dir values (and ignore that selector). 8 | 9 | # 0.9.0 10 | 11 | Now supporting rtlcss directives like /*rtl: … */. Removed props to convert. 12 | 13 | # 0.8.2 14 | 15 | Updated rtlcss version and some props to convert. 16 | 17 | # 0.8.0 18 | 19 | Optimized to not generate too much noise 20 | 21 | # 0.7.0 22 | 23 | The initial commit -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "postcss-inline-rtl", 3 | "version": "0.9.8", 4 | "description": "PostCSS plugin to inline the minimal amount of RTL CSS you need", 5 | "keywords": [ 6 | "postcss", 7 | "css", 8 | "postcss-plugin", 9 | "rtl", 10 | "rtlcss", 11 | "inline" 12 | ], 13 | "author": "Jakob Werner ", 14 | "license": "MIT", 15 | "repository": "jakob101/postcss-inline-rtl", 16 | "bugs": { 17 | "url": "https://github.com/jakob101/postcss-inline-rtl/issues" 18 | }, 19 | "homepage": "https://github.com/jakob101/postcss-inline-rtl", 20 | "dependencies": { 21 | "postcss": "^6.0.1", 22 | "rtlcss": "^2.0.3" 23 | }, 24 | "devDependencies": { 25 | "ava": "^0.19.1", 26 | "eslint": "^3.19.0", 27 | "eslint-config-postcss": "^2.0.0" 28 | }, 29 | "scripts": { 30 | "test": "ava --verbose && eslint *.js" 31 | }, 32 | "eslintConfig": { 33 | "extends": "eslint-config-postcss/es5" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright 2016 Jakob Werner 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /propsToAlwaysConvert.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Consider: 3 | * 4 | * .a { 5 | * margin: 0 5px 0 0; 6 | * } 7 | * 8 | * .a { 9 | * margin: 0; 10 | * } 11 | * 12 | * Would be converted to: 13 | * 14 | * html[dir='ltr'] .a { 15 | * margin: 0 5px 0 0; 16 | * } 17 | * 18 | * html[dir='rtl'] .a { 19 | * margin: 0 0 0 5px; 20 | * } 21 | * 22 | * .a { 23 | * margin: 0; 24 | * } 25 | * 26 | * As the "html[dir] .a" will have more specificity, 27 | * "margin: 0" will be ignored. That's why we must 28 | * convert the following properties *always*, regardless 29 | * of the value. 30 | * 31 | * 32 | */ 33 | module.exports = [ 34 | 'background', 35 | 'background-position', 36 | 'background-position-x', 37 | 'border', 38 | 'border-color', 39 | 'border-radius', 40 | 'border-style', 41 | 'border-width', 42 | 'border-top', 43 | 'border-right', 44 | 'border-bottom', 45 | 'border-left', 46 | 'box-shadow', 47 | 'float', 48 | 'margin', 49 | 'padding', 50 | 'text-shadow', 51 | 'text-align', 52 | 'transform', 53 | 'transform-origin', 54 | 'transition' 55 | ].join('|'); 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PostCSS Inline Rtl [![Build Status][ci-img]][ci] 2 | 3 | [PostCSS] plugin to inline the minimal amount of RTL CSS you need. 4 | 5 | [PostCSS]: https://github.com/postcss/postcss 6 | [ci-img]: https://travis-ci.org/jakob101/postcss-inline-rtl.svg 7 | [ci]: https://travis-ci.org/jakob101/postcss-inline-rtl 8 | 9 | ## Requirement 10 | Always have a `dir="ltr"` or `dir="rtl"` in your HTML tag. 11 | 12 | ## Examples 13 | 14 | ```css 15 | /* Normal code */ 16 | .class { 17 | color: red; 18 | } 19 | 20 | /* => no change */ 21 | ``` 22 | 23 | ```css 24 | .class{ 25 | border-left: 10px; 26 | color: red; 27 | } 28 | 29 | /* Converts to: */ 30 | html[dir='ltr'] .class { 31 | border-left: 10px 32 | } 33 | html[dir='rtl'] .class { 34 | border-right: 10px 35 | } 36 | .class { 37 | color: red; 38 | } 39 | ``` 40 | 41 | ```css 42 | .class { 43 | margin-left: 10px; 44 | } 45 | 46 | /* converts to: */ 47 | html[dir='ltr'] .class { 48 | margin-left: 10px 49 | } 50 | html[dir='rtl'] .class { 51 | margin-right: 10px 52 | } 53 | ``` 54 | 55 | ```css 56 | /* Edge case (cancelling LTR/RTL values) */ 57 | .class { 58 | border-left: 10px; 59 | border: none; /* Notice this doesn't change LTR-RTL */ 60 | } 61 | 62 | /* converts to: */ 63 | html[dir] .class { 64 | border: none; 65 | } 66 | html[dir='ltr'] .class { 67 | border-left: 10px; 68 | } 69 | html[dir='rtl'] .class { 70 | border-right: 10px; 71 | } 72 | ``` 73 | 74 | ```css 75 | /* Edge case (RTL-invariant) + CSS modules */ 76 | .class { 77 | composes: otherClass; 78 | border: none; /* Notice this doesn't change LTR-RTL */ 79 | } 80 | 81 | /* Converts to: */ 82 | .class { 83 | composes: otherClass; 84 | } 85 | html[dir] .class { 86 | border: none; 87 | } 88 | ``` 89 | 90 | ## Usage 91 | 92 | ```js 93 | postcss([ require('postcss-inline-rtl') ]) 94 | ``` 95 | 96 | ## Cred 97 | +1 for [rtlcss](https://github.com/MohammadYounes/rtlcss), as this wouldn't exist without it! 98 | 99 | See [PostCSS] docs for examples for your environment. 100 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | /* eslint max-len:0 */ 2 | import postcss from 'postcss'; 3 | import test from 'ava'; 4 | import plugin from './'; 5 | 6 | /* Write tests here */ 7 | 8 | test('Checks initial configuration', t => { 9 | const input = 'a{ color: red }'; 10 | const output = 'a{ color: red }'; 11 | 12 | return postcss([plugin()]).process(input) 13 | .then( result => { 14 | t.is(result.css, output); 15 | }); 16 | }); 17 | 18 | test('Supports rtlcss control directives', t => { 19 | const input = 'a{ /*rtl:ignore*/left: 0 }'; 20 | const output = 'a{ /*rtl:ignore*/left: 0 }'; 21 | 22 | return postcss([plugin()]).process(input) 23 | .then( result => { 24 | t.is(result.css, output); 25 | }); 26 | }); 27 | 28 | test('Checks basic example', t => { 29 | const input = ` 30 | a { 31 | float: left 32 | }`; 33 | 34 | const output = ` 35 | html[dir="ltr"] a { 36 | float: left 37 | } 38 | html[dir="rtl"] a { 39 | float: right 40 | }`; 41 | return postcss([plugin()]).process(input) 42 | .then( result => { 43 | t.is(result.css, output); 44 | }); 45 | }); 46 | 47 | test('Checks example with two properties which should both be converted', t => { 48 | const input = ` 49 | a { 50 | float: left; 51 | transform: translateX(50%); 52 | }`; 53 | 54 | const output = ` 55 | html[dir="ltr"] a { 56 | float: left; 57 | transform: translateX(50%) 58 | } 59 | html[dir="rtl"] a { 60 | float: right; 61 | transform: translateX(-50%) 62 | }`; 63 | return postcss([plugin()]).process(input) 64 | .then( result => { 65 | t.is(result.css, output); 66 | }); 67 | }); 68 | 69 | test('Checks example with two properties out of which only one should be converted', t => { 70 | const input = ` 71 | a { 72 | color: red; 73 | transform: translateX(50%); 74 | }`; 75 | 76 | const output = ` 77 | a { 78 | color: red; 79 | } 80 | html[dir="ltr"] a { 81 | transform: translateX(50%); 82 | } 83 | html[dir="rtl"] a { 84 | transform: translateX(-50%); 85 | }`; 86 | return postcss([plugin()]).process(input) 87 | .then( result => { 88 | t.is(result.css, output); 89 | }); 90 | }); 91 | 92 | test('Checks example with three properties where the first and third should be converted', t => { 93 | const input = ` 94 | a { 95 | transform: translateX(50%); 96 | color: red; 97 | border-left: 10px; 98 | }`; 99 | 100 | const output = ` 101 | a { 102 | color: red; 103 | } 104 | html[dir="ltr"] a { 105 | transform: translateX(50%); 106 | border-left: 10px; 107 | } 108 | html[dir="rtl"] a { 109 | transform: translateX(-50%); 110 | border-right: 10px; 111 | }`; 112 | return postcss([plugin()]).process(input) 113 | .then( result => { 114 | t.is(result.css, output); 115 | }); 116 | }); 117 | 118 | test('Checks example with "composes" keyword', t => { 119 | const input = ` 120 | a { 121 | composes: foo; 122 | transform: translateX(50%); 123 | color: red; 124 | }`; 125 | 126 | const output = ` 127 | a { 128 | composes: foo; 129 | color: red; 130 | } 131 | html[dir="ltr"] a { 132 | transform: translateX(50%); 133 | } 134 | html[dir="rtl"] a { 135 | transform: translateX(-50%); 136 | }`; 137 | return postcss([plugin()]).process(input) 138 | .then( result => { 139 | t.is(result.css, output); 140 | }); 141 | }); 142 | 143 | test('Checks example with multiple selectors', t => { 144 | const input = ` 145 | .a, .b { 146 | composes: foo; 147 | transform: translateX(50%); 148 | color: red; 149 | }`; 150 | 151 | const output = ` 152 | .a, .b { 153 | composes: foo; 154 | color: red; 155 | } 156 | html[dir="ltr"] .a, html[dir="ltr"] .b { 157 | transform: translateX(50%); 158 | } 159 | html[dir="rtl"] .a, html[dir="rtl"] .b { 160 | transform: translateX(-50%); 161 | }`; 162 | return postcss([plugin()]).process(input) 163 | .then( result => { 164 | t.is(result.css, output); 165 | }); 166 | }); 167 | 168 | test('Checks example where selectors already have ltr styles defined', t => { 169 | const input = ` 170 | html[dir="ltr"] .a { 171 | composes: foo; 172 | transform: translateX(50%); 173 | color: red; 174 | }`; 175 | 176 | const output = ` 177 | html[dir="ltr"] .a { 178 | composes: foo; 179 | transform: translateX(50%); 180 | color: red; 181 | }`; 182 | 183 | return postcss([plugin()]).process(input) 184 | .then( result => { 185 | t.is(result.css, output); 186 | }); 187 | }); 188 | 189 | test('Checks example where selectors already have rtl styles defined', t => { 190 | const input = ` 191 | html[dir="rtl"] .a { 192 | composes: foo; 193 | transform: translateX(50%); 194 | color: red; 195 | }`; 196 | 197 | const output = ` 198 | html[dir="rtl"] .a { 199 | composes: foo; 200 | transform: translateX(50%); 201 | color: red; 202 | }`; 203 | 204 | return postcss([plugin()]).process(input) 205 | .then( result => { 206 | t.is(result.css, output); 207 | }); 208 | }); 209 | 210 | test('Checks example where selectors have [dir="*"] defined', t => { 211 | const input = ` 212 | [dir="rtl"] .a { 213 | composes: foo; 214 | transform: translateX(50%); 215 | color: red; 216 | }`; 217 | 218 | const output = ` 219 | [dir="rtl"] .a { 220 | composes: foo; 221 | transform: translateX(50%); 222 | color: red; 223 | }`; 224 | 225 | return postcss([plugin()]).process(input) 226 | .then( result => { 227 | t.is(result.css, output); 228 | }); 229 | }); 230 | 231 | test('Check @media tag parsing', t => { 232 | const input = ` 233 | @media screen and (max-width:632px) { 234 | .a { 235 | float: left; 236 | } 237 | }`; 238 | 239 | const output = ` 240 | @media screen and (max-width:632px) { 241 | html[dir="ltr"] .a { 242 | float: left 243 | } 244 | html[dir="rtl"] .a { 245 | float: right 246 | } 247 | }`; 248 | 249 | return postcss([plugin()]).process(input) 250 | .then( result => { 251 | t.is(result.css, output); 252 | }); 253 | }); 254 | 255 | test('Checks keyframe parsing', t => { 256 | const input = ` 257 | @keyframes moveLeft { 258 | from { 259 | transform: translateX(10px); 260 | } 261 | to { 262 | transform: translateX(0); 263 | } 264 | }`; 265 | 266 | const output = `@keyframes moveLeft-rtl { 267 | from { 268 | transform: translateX(-10px); 269 | } 270 | to { 271 | transform: translateX(0); 272 | } 273 | } 274 | @keyframes moveLeft { 275 | from { 276 | transform: translateX(10px); 277 | } 278 | to { 279 | transform: translateX(0); 280 | } 281 | }`; 282 | 283 | return postcss([plugin()]).process(input) 284 | .then( result => { 285 | t.is(result.css, output); 286 | }); 287 | }); 288 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var postcss = require('postcss'); 2 | var rtlcss = require('rtlcss'); 3 | var propsToAlwaysConvert = require('./propsToAlwaysConvert.js'); 4 | 5 | module.exports = postcss.plugin('postcss-inline-rtl', function (opts) { 6 | opts = opts || {}; 7 | 8 | // Check if there is an ignore parameter (full selectors to ignore) 9 | var ignoreRegex = opts.ignore || null; 10 | if (!ignoreRegex || ignoreRegex.constructor !== RegExp) { 11 | ignoreRegex = null; 12 | } 13 | 14 | var propsToAlwaysConvertRegex = new RegExp(propsToAlwaysConvert, 'i'); 15 | 16 | return function (css) { 17 | 18 | // Save animation names 19 | var keyFrameNamesToChange = []; 20 | css.walkAtRules(/keyframes/i, function (atRule) { 21 | if (keyFrameNamesToChange.indexOf(atRule.params) < 0) { 22 | keyFrameNamesToChange.push(atRule.params); 23 | } 24 | }); 25 | 26 | // Generate rtl animation declarations 27 | css.walkAtRules(/keyframes/i, function (atRule) { 28 | var newAtRule = atRule.clone(); 29 | newAtRule.params += '-rtl'; 30 | newAtRule = postcss([rtlcss]).process(newAtRule).root; 31 | atRule.parent.insertBefore(atRule, newAtRule); 32 | }); 33 | 34 | /* 35 | * Go through all 'animation' or 'animation-name' css declarations 36 | * If you find an animation name that was converted to rtl above, 37 | * tell rtlcss to append '-rtl' to the end of the animation name. 38 | */ 39 | css.walkDecls(/animation$|animation-name/i, function (decl) { 40 | keyFrameNamesToChange.forEach(function (element) { 41 | var animationNamePosition = decl.value.indexOf(element); 42 | if (animationNamePosition > -1) { 43 | animationNamePosition += element.length; 44 | 45 | // Check if the name is complete 46 | if (!decl.value[animationNamePosition] || 47 | decl.value[animationNamePosition] 48 | .match(/\,|\ |\;|\!/)) { 49 | decl.value = 50 | [decl.value.slice(0, animationNamePosition), 51 | '/*rtl:insert:-rtl*/', 52 | decl.value.slice(animationNamePosition)] 53 | .join(''); 54 | } 55 | } 56 | }); 57 | }); 58 | 59 | css.walkRules(function (rule) { 60 | 61 | // Do we have any selector that starts with 'html' 62 | // or a selector that already specifies ltr/rtl values 63 | if (rule.selectors.some(function (selector) { 64 | return selector.indexOf('html') === 0 || 65 | /^\[dir=['"]?(ltr|rtl)['"]?\]/i.test(selector); 66 | })) { 67 | return; 68 | } 69 | 70 | // Filter rules 71 | if (ignoreRegex && ignoreRegex.test(rule.selector)) { 72 | return; 73 | } 74 | 75 | // If we're inside @rule and it's not 76 | // a media tag, do not parse 77 | if (rule.parent.type === 'atrule' && 78 | !(rule.parent.name.indexOf('media') > -1)) { 79 | return; 80 | } 81 | 82 | var rtl = postcss([rtlcss]).process(rule).root; 83 | 84 | // Go through declarations 85 | var declarationKeeperLTR = []; 86 | var declarationKeeperRTL = []; 87 | var declarationKeeperInvariant = []; 88 | 89 | for (var declIndex = rule.nodes.length - 1; 90 | declIndex >= 0; --declIndex) { 91 | if (rule.nodes[declIndex].type !== 'decl') { 92 | continue; 93 | } 94 | if (rtl.nodes[0].nodes[declIndex] === undefined) { 95 | continue; 96 | } 97 | 98 | var decl = rule.nodes[declIndex]; 99 | var rtlDecl = rtl.nodes[0].nodes[declIndex]; 100 | var rtlValue = rtlDecl.raws.value && rtlDecl.raws.value.raw ? 101 | rtlDecl.raws.value.raw : rtlDecl.value; 102 | 103 | if (rtlDecl.prop !== decl.prop || 104 | rtlValue !== decl.value) { 105 | 106 | declarationKeeperLTR.push(decl); 107 | declarationKeeperRTL.push(rtlDecl); 108 | decl.remove(); 109 | rtlDecl.remove(); 110 | } else if (propsToAlwaysConvertRegex.test(decl.prop)) { 111 | declarationKeeperInvariant.push(decl); 112 | decl.remove(); 113 | rtlDecl.remove(); 114 | } 115 | } 116 | 117 | // Check for transformed properties 118 | if (declarationKeeperLTR.length > 0) { 119 | 120 | var ltrSelectors = rule.selectors.map(function (el) { 121 | if (el.indexOf('html') !== 0) { 122 | return 'html[dir="ltr"] ' + el; 123 | } 124 | 125 | return el; 126 | }); 127 | 128 | var rtlSelectors = rule.selectors.map(function (el) { 129 | if (el.indexOf('html') !== 0) { 130 | return 'html[dir="rtl"] ' + el; 131 | } 132 | 133 | return el; 134 | }); 135 | 136 | // Create RTL rule 137 | var newRTLRule = postcss.rule({ selectors: rtlSelectors }); 138 | newRTLRule.append(declarationKeeperRTL.reverse()); 139 | rule.parent.insertAfter(rule, newRTLRule); 140 | 141 | // create LTR rule 142 | var newLTRRule = postcss.rule({ selectors: ltrSelectors }); 143 | newLTRRule.append(declarationKeeperLTR.reverse()); 144 | rule.parent.insertAfter(rule, newLTRRule); 145 | } 146 | 147 | // Check for invariant properties 148 | if (declarationKeeperInvariant.length > 0) { 149 | var invariantSelectors = rule.selectors.map(function (el) { 150 | if (el.indexOf('html') !== 0) { 151 | return 'html[dir] ' + el; 152 | } 153 | 154 | return el; 155 | }); 156 | 157 | var newInvariantRule = 158 | postcss.rule({ selectors: invariantSelectors }); 159 | newInvariantRule.append(declarationKeeperInvariant.reverse()); 160 | rule.parent.insertAfter(rule, newInvariantRule); 161 | } 162 | 163 | // If we're left with an empty rule (with no declarations) 164 | if (rule.some(function (node) { 165 | return node.type === 'decl'; 166 | }) === false) { 167 | rule.parent.removeChild(rule); 168 | } 169 | }); 170 | 171 | // Clean up /*rtl:insert:-rtl*/ comments 172 | css.walkDecls(/animation$|animation-name/i, function (decl) { 173 | decl.value = decl.value.replace(/\/\*rtl\:insert\:\-rtl\*\//gi, 174 | ''); 175 | }); 176 | }; 177 | }); 178 | --------------------------------------------------------------------------------